This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 70ea96de2 [feat-4762] part1: Minimal support for deleting scopes 
(#5153)
70ea96de2 is described below

commit 70ea96de2fb7673caafcd1608dcee4c67780f65f
Author: Keon Amini <[email protected]>
AuthorDate: Fri May 12 01:33:11 2023 -0700

    [feat-4762] part1: Minimal support for deleting scopes (#5153)
    
    * feat: Minimal support for deleting scopes
    
    * feat: pagerduty adapted to use Delete Scopes
    
    * fix: fixes added per PR feedback
---
 backend/core/models/blueprint.go                   |  89 ++++++
 backend/go.mod                                     |   4 +-
 backend/go.sum                                     |  15 -
 backend/helpers/pluginhelper/api/api_collector.go  |   6 +-
 .../helpers/pluginhelper/api/api_collector_test.go |  17 +-
 .../pluginhelper/api/api_collector_with_state.go   |   7 +-
 backend/helpers/pluginhelper/api/api_extractor.go  |   1 -
 backend/helpers/pluginhelper/api/api_rawdata.go    |  26 +-
 .../helpers/pluginhelper/api/remote_api_helper.go  |  12 +-
 backend/helpers/pluginhelper/api/scope_helper.go   | 356 ++++++++++++++++++---
 .../helpers/pluginhelper/api/scope_helper_test.go  |   4 +-
 .../pluginhelper}/services/blueprint_helper.go     | 115 +++++--
 backend/plugins/pagerduty/api/scope.go             |  21 +-
 .../e2e/raw_tables/_raw_pagerduty_incidents.csv    |   6 +-
 .../_tool_pagerduty_assignments.csv                |   6 +-
 .../snapshot_tables/_tool_pagerduty_incidents.csv  |   6 +-
 .../e2e/snapshot_tables/_tool_pagerduty_users.csv  |   4 +-
 backend/plugins/pagerduty/impl/impl.go             |  17 +-
 .../plugins/pagerduty/tasks/incidents_collector.go |   8 +-
 .../plugins/pagerduty/tasks/incidents_converter.go |   8 +-
 .../plugins/pagerduty/tasks/incidents_extractor.go |   8 +-
 backend/plugins/pagerduty/tasks/task_data.go       |   8 +
 backend/server/api/router.go                       |   7 +-
 backend/server/services/blueprint.go               |  30 +-
 backend/server/services/init.go                    |   5 +-
 backend/test/e2e/remote/helper.go                  |  22 +-
 backend/test/e2e/remote/python_plugin_test.go      |  41 ++-
 backend/test/helper/api.go                         |  46 ++-
 backend/test/helper/json_helper.go                 |  12 +
 backend/test/helper/models.go                      |   7 +
 30 files changed, 727 insertions(+), 187 deletions(-)

diff --git a/backend/core/models/blueprint.go b/backend/core/models/blueprint.go
index 2f2643c98..f09b39d29 100644
--- a/backend/core/models/blueprint.go
+++ b/backend/core/models/blueprint.go
@@ -55,6 +55,36 @@ type BlueprintSettings struct {
        AfterPlan   json.RawMessage `json:"after_plan"`
 }
 
+// UpdateConnections unmarshals the connections on this BlueprintSettings
+func (bps *BlueprintSettings) UnmarshalConnections() 
([]*plugin.BlueprintConnectionV200, errors.Error) {
+       var connections []*plugin.BlueprintConnectionV200
+       err := json.Unmarshal(bps.Connections, &connections)
+       if err != nil {
+               return nil, errors.Default.Wrap(err, `unmarshal connections 
fail`)
+       }
+       return connections, nil
+}
+
+// UpdateConnections updates the connections on this BlueprintSettings 
reference according to the updater function
+func (bps *BlueprintSettings) UpdateConnections(updater func(c 
*plugin.BlueprintConnectionV200) errors.Error) errors.Error {
+       conns, err := bps.UnmarshalConnections()
+       if err != nil {
+               return err
+       }
+       for i, conn := range conns {
+               err = updater(conn)
+               if err != nil {
+                       return err
+               }
+               conns[i] = conn
+       }
+       bps.Connections, err = errors.Convert01(json.Marshal(&conns))
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
 // UnmarshalPlan unmarshals Plan in JSON to strong-typed plugin.PipelinePlan
 func (bp *Blueprint) UnmarshalPlan() (plugin.PipelinePlan, errors.Error) {
        var plan plugin.PipelinePlan
@@ -65,6 +95,65 @@ func (bp *Blueprint) UnmarshalPlan() (plugin.PipelinePlan, 
errors.Error) {
        return plan, nil
 }
 
+// UnmarshalSettings unmarshals the BlueprintSettings on the Blueprint
+func (bp *Blueprint) UnmarshalSettings() (BlueprintSettings, errors.Error) {
+       var settings BlueprintSettings
+       err := errors.Convert(json.Unmarshal(bp.Settings, &settings))
+       if err != nil {
+               return settings, errors.Default.Wrap(err, `unmarshal settings 
fail`)
+       }
+       return settings, nil
+}
+
+// GetConnections Gets all the blueprint connections for this blueprint
+func (bp *Blueprint) GetConnections() ([]*plugin.BlueprintConnectionV200, 
errors.Error) {
+       settings, err := bp.UnmarshalSettings()
+       if err != nil {
+               return nil, err
+       }
+       conns, err := settings.UnmarshalConnections()
+       if err != nil {
+               return nil, err
+       }
+       return conns, nil
+}
+
+// UpdateSettings updates the blueprint instance with this settings reference
+func (bp *Blueprint) UpdateSettings(settings *BlueprintSettings) errors.Error {
+       if settings.Connections == nil {
+               bp.Settings = nil
+       } else {
+               settingsRaw, err := errors.Convert01(json.Marshal(settings))
+               if err != nil {
+                       return err
+               }
+               bp.Settings = settingsRaw
+       }
+       return nil
+}
+
+// GetScopes Gets all the scopes across all the connections for this blueprint
+func (bp *Blueprint) GetScopes(connectionId uint64) 
([]*plugin.BlueprintScopeV200, errors.Error) {
+       conns, err := bp.GetConnections()
+       if err != nil {
+               return nil, err
+       }
+       visited := map[string]any{}
+       var result []*plugin.BlueprintScopeV200
+       for _, conn := range conns {
+               if conn.ConnectionId != connectionId {
+                       continue
+               }
+               for _, scope := range conn.Scopes {
+                       if _, ok := visited[scope.Id]; !ok {
+                               result = append(result, scope)
+                               visited[scope.Id] = true
+                       }
+               }
+       }
+       return result, nil
+}
+
 func (Blueprint) TableName() string {
        return "_devlake_blueprints"
 }
diff --git a/backend/go.mod b/backend/go.mod
index 34a700b92..416f8fe91 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -47,9 +47,7 @@ require (
        gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755
 )
 
-require (
-       github.com/jmespath/go-jmespath v0.4.0 // indirect
-)
+require github.com/jmespath/go-jmespath v0.4.0 // indirect
 
 require (
        github.com/KyleBanks/depth v1.2.1 // indirect
diff --git a/backend/go.sum b/backend/go.sum
index 352a9013c..0b792721f 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -128,8 +128,6 @@ github.com/creack/pty v1.1.9/go.mod 
h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
 github.com/davecgh/go-spew v1.1.0/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod 
h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 
v4.0.0-20210816181553-5444fa50b93d/go.mod 
h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
 github.com/denisenkom/go-mssqldb v0.9.0 
h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk=
 github.com/denisenkom/go-mssqldb v0.9.0/go.mod 
h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/dgraph-io/badger v1.6.0/go.mod 
h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
@@ -234,9 +232,6 @@ github.com/gobwas/pool v0.2.0/go.mod 
h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
 github.com/gobwas/ws v1.0.2/go.mod 
h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/gocarina/gocsv v0.0.0-20220707092902-b9da1f06c77e 
h1:GMIV+S6grz+vlIaUsP+fedQ6L+FovyMPMY26WO8dwQE=
 github.com/gocarina/gocsv v0.0.0-20220707092902-b9da1f06c77e/go.mod 
h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
-github.com/goccy/go-json v0.9.7/go.mod 
h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/goccy/go-json v0.10.2 
h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
-github.com/goccy/go-json v0.10.2/go.mod 
h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.0.4/go.mod 
h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod 
h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gofrs/uuid v4.0.0+incompatible 
h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
@@ -505,15 +500,6 @@ github.com/leodido/go-urn v1.1.0/go.mod 
h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdA
 github.com/leodido/go-urn v1.2.0/go.mod 
h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/leodido/go-urn v1.2.1 
h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
 github.com/leodido/go-urn v1.2.1/go.mod 
h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
-github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod 
h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
-github.com/lestrrat-go/blackmagic v1.0.0/go.mod 
h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
-github.com/lestrrat-go/httpcc v1.0.1/go.mod 
h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
-github.com/lestrrat-go/iter v1.0.1/go.mod 
h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
-github.com/lestrrat-go/jwx v1.2.25 
h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
-github.com/lestrrat-go/jwx v1.2.25/go.mod 
h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
-github.com/lestrrat-go/option v1.0.0/go.mod 
h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
-github.com/lestrrat-go/option v1.0.1 
h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
-github.com/lestrrat-go/option v1.0.1/go.mod 
h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -801,7 +787,6 @@ golang.org/x/crypto 
v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
 golang.org/x/crypto v0.8.0/go.mod 
h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
diff --git a/backend/helpers/pluginhelper/api/api_collector.go 
b/backend/helpers/pluginhelper/api/api_collector.go
index 517025dea..051008e3a 100644
--- a/backend/helpers/pluginhelper/api/api_collector.go
+++ b/backend/helpers/pluginhelper/api/api_collector.go
@@ -354,10 +354,14 @@ func (collector *ApiCollector) 
fetchPagesUndetermined(reqData *RequestData) {
 }
 
 func (collector *ApiCollector) generateUrl(pager *Pager, input interface{}) 
(string, errors.Error) {
+       params := collector.args.Params
+       if collector.args.Options != nil {
+               params = collector.args.Options.GetParams()
+       }
        var buf bytes.Buffer
        err := collector.urlTemplate.Execute(&buf, &RequestData{
                Pager:  pager,
-               Params: collector.args.Params,
+               Params: params,
                Input:  input,
        })
        if err != nil {
diff --git a/backend/helpers/pluginhelper/api/api_collector_test.go 
b/backend/helpers/pluginhelper/api/api_collector_test.go
index 8b4c6d187..e90e4b11b 100644
--- a/backend/helpers/pluginhelper/api/api_collector_test.go
+++ b/backend/helpers/pluginhelper/api/api_collector_test.go
@@ -33,6 +33,14 @@ import (
        "github.com/stretchr/testify/mock"
 )
 
+type TestOpts struct{}
+
+func (t TestOpts) GetParams() any {
+       return struct {
+               Name string
+       }{Name: "testparams"}
+}
+
 func TestFetchPageUndetermined(t *testing.T) {
        mockDal := new(mockdal.Dal)
        mockDal.On("AutoMigrate", mock.Anything, 
mock.Anything).Return(nil).Once()
@@ -76,16 +84,13 @@ func TestFetchPageUndetermined(t *testing.T) {
        mockApi.On("WaitAsync").Return(nil)
        mockApi.On("GetAfterFunction", mock.Anything).Return(nil)
        mockApi.On("SetAfterFunction", mock.Anything).Return()
-       params := struct {
-               Name string
-       }{Name: "testparams"}
        mockApi.On("Release").Return()
 
        collector, err := NewApiCollector(ApiCollectorArgs{
                RawDataSubTaskArgs: RawDataSubTaskArgs{
-                       Ctx:    mockCtx,
-                       Table:  "whatever rawtable",
-                       Params: params,
+                       Ctx:     mockCtx,
+                       Table:   "whatever rawtable",
+                       Options: &TestOpts{},
                },
                ApiClient:      mockApi,
                Input:          mockInput,
diff --git a/backend/helpers/pluginhelper/api/api_collector_with_state.go 
b/backend/helpers/pluginhelper/api/api_collector_with_state.go
index f62d6b365..65d4e2176 100644
--- a/backend/helpers/pluginhelper/api/api_collector_with_state.go
+++ b/backend/helpers/pluginhelper/api/api_collector_with_state.go
@@ -145,9 +145,10 @@ func (m *ApiCollectorStateManager) Execute() errors.Error {
 func NewStatefulApiCollectorForFinalizableEntity(args 
FinalizableApiCollectorArgs) (plugin.SubTask, errors.Error) {
        // create a manager which could execute multiple collector but acts as 
a single subtask to callers
        manager, err := NewStatefulApiCollector(RawDataSubTaskArgs{
-               Ctx:    args.Ctx,
-               Params: args.Params,
-               Table:  args.Table,
+               Ctx:     args.Ctx,
+               Options: args.Options,
+               Params:  args.Params,
+               Table:   args.Table,
        }, args.TimeAfter)
        if err != nil {
                return nil, err
diff --git a/backend/helpers/pluginhelper/api/api_extractor.go 
b/backend/helpers/pluginhelper/api/api_extractor.go
index 2b4f86e5d..2a4c89ba6 100644
--- a/backend/helpers/pluginhelper/api/api_extractor.go
+++ b/backend/helpers/pluginhelper/api/api_extractor.go
@@ -106,7 +106,6 @@ func (extractor *ApiExtractor) Execute() errors.Error {
                if err != nil {
                        return errors.Default.Wrap(err, "error calling plugin 
Extract implementation")
                }
-
                for _, result := range results {
                        // get the batch operator for the specific type
                        batch, err := divider.ForType(reflect.TypeOf(result))
diff --git a/backend/helpers/pluginhelper/api/api_rawdata.go 
b/backend/helpers/pluginhelper/api/api_rawdata.go
index 24199c35c..d743cd66d 100644
--- a/backend/helpers/pluginhelper/api/api_rawdata.go
+++ b/backend/helpers/pluginhelper/api/api_rawdata.go
@@ -22,6 +22,7 @@ import (
        "fmt"
        "github.com/apache/incubator-devlake/core/errors"
        plugin "github.com/apache/incubator-devlake/core/plugin"
+       "reflect"
        "time"
 
        "gorm.io/datatypes"
@@ -37,6 +38,10 @@ type RawData struct {
        CreatedAt time.Time
 }
 
+type TaskOptions interface {
+       GetParams() any
+}
+
 // RawDataSubTaskArgs FIXME ...
 type RawDataSubTaskArgs struct {
        Ctx plugin.SubTaskContext
@@ -44,9 +49,12 @@ type RawDataSubTaskArgs struct {
        //      Table store raw data
        Table string `comment:"Raw data table name"`
 
-       //      This struct will be JSONEncoded and stored into database along 
with raw data itself, to identity minimal
-       //      set of data to be process, for example, we process JiraIssues 
by Board
-       Params interface{} `comment:"To identify a set of records with same 
UrlTemplate, i.e. {ConnectionId, BoardId} for jira entities"`
+       // Deprecated: Use Options instead
+       // This struct will be JSONEncoded and stored into database along with 
raw data itself, to identity minimal set of
+       // data to be processed, for example, we process JiraIssues by Board
+       Params any `comment:"To identify a set of records with same 
UrlTemplate, i.e. {ConnectionId, BoardId} for jira entities"`
+
+       Options TaskOptions `comment:"To identify a set of records with same 
UrlTemplate, i.e. {ConnectionId, BoardId} for jira entities"`
 }
 
 // RawDataSubTask is Common features for raw data sub-tasks
@@ -64,12 +72,18 @@ func NewRawDataSubTask(args RawDataSubTaskArgs) 
(*RawDataSubTask, errors.Error)
        if args.Table == "" {
                return nil, errors.Default.New("Table is required for 
RawDataSubTask")
        }
+       var params any
+       if args.Options != nil {
+               params = args.Options.GetParams()
+       } else { // fallback to old way
+               params = args.Params
+       }
        paramsString := ""
-       if args.Params == nil {
-               args.Ctx.GetLogger().Warn(nil, "Missing `Params` for raw data 
subtask %s", args.Ctx.GetName())
+       if params == nil || reflect.ValueOf(params).IsZero() {
+               args.Ctx.GetLogger().Warn(nil, fmt.Sprintf("Missing `Params` 
for raw data subtask %s", args.Ctx.GetName()))
        } else {
                // TODO: maybe sort it to make it consistent
-               paramsBytes, err := json.Marshal(args.Params)
+               paramsBytes, err := json.Marshal(params)
                if err != nil {
                        return nil, errors.Default.Wrap(err, "unable to 
serialize subtask parameters")
                }
diff --git a/backend/helpers/pluginhelper/api/remote_api_helper.go 
b/backend/helpers/pluginhelper/api/remote_api_helper.go
index a34457e38..798801a20 100644
--- a/backend/helpers/pluginhelper/api/remote_api_helper.go
+++ b/backend/helpers/pluginhelper/api/remote_api_helper.go
@@ -135,13 +135,13 @@ func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) 
GetScopesFromRemote(inpu
        getGroup func(basicRes coreContext.BasicRes, gid string, queryData 
*RemoteQueryData, connection Conn) ([]Group, errors.Error),
        getScope func(basicRes coreContext.BasicRes, gid string, queryData 
*RemoteQueryData, connection Conn) ([]ApiScope, errors.Error),
 ) (*plugin.ApiResourceOutput, errors.Error) {
-       connectionId, _ := extractFromReqParam(input.Params)
-       if connectionId == 0 {
+       connectionId, err := 
errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64))
+       if err != nil || connectionId == 0 {
                return nil, errors.BadInput.New("invalid connectionId")
        }
 
        var connection Conn
-       err := r.connHelper.First(&connection, input.Params)
+       err = r.connHelper.First(&connection, input.Params)
        if err != nil {
                return nil, err
        }
@@ -245,13 +245,13 @@ func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) 
GetScopesFromRemote(inpu
 }
 
 func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) 
SearchRemoteScopes(input *plugin.ApiResourceInput, searchScope func(basicRes 
coreContext.BasicRes, queryData *RemoteQueryData, connection Conn) ([]ApiScope, 
errors.Error)) (*plugin.ApiResourceOutput, errors.Error) {
-       connectionId, _ := extractFromReqParam(input.Params)
-       if connectionId == 0 {
+       connectionId, err := 
errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64))
+       if err != nil || connectionId == 0 {
                return nil, errors.BadInput.New("invalid connectionId")
        }
 
        var connection Conn
-       err := r.connHelper.First(&connection, input.Params)
+       err = r.connHelper.First(&connection, input.Params)
        if err != nil {
                return nil, err
        }
diff --git a/backend/helpers/pluginhelper/api/scope_helper.go 
b/backend/helpers/pluginhelper/api/scope_helper.go
index 10cdc37c5..a2edbcf7c 100644
--- a/backend/helpers/pluginhelper/api/scope_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_helper.go
@@ -20,9 +20,13 @@ package api
 import (
        "encoding/json"
        "fmt"
+       "github.com/apache/incubator-devlake/core/models"
+       "github.com/apache/incubator-devlake/core/models/domainlayer/domaininfo"
+       serviceHelper 
"github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
        "strconv"
        "strings"
+       "sync"
        "time"
 
        "github.com/apache/incubator-devlake/core/context"
@@ -37,6 +41,11 @@ import (
        "reflect"
 )
 
+var (
+       tablesCache       []string // these cached vars can probably be moved 
somewhere more centralized later
+       tablesCacheLoader = new(sync.Once)
+)
+
 type NoTransformation struct{}
 
 // ScopeApiHelper is used to write the CURD of scopes
@@ -44,9 +53,27 @@ type ScopeApiHelper[Conn any, Scope any, Tr any] struct {
        log        log.Logger
        db         dal.Dal
        validator  *validator.Validate
+       bpManager  *serviceHelper.BlueprintManager
        connHelper *ConnectionApiHelper
 }
 
+type (
+       requestParams struct {
+               connectionId uint64
+               scopeId      string
+               plugin       string
+       }
+       deleteRequestParams struct {
+               requestParams
+               deleteDataOnly bool
+       }
+
+       getRequestParams struct {
+               requestParams
+               loadBlueprints bool
+       }
+)
+
 // NewScopeHelper creates a ScopeHelper for scopes management
 func NewScopeHelper[Conn any, Scope any, Tr any](
        basicRes context.BasicRes,
@@ -59,10 +86,18 @@ func NewScopeHelper[Conn any, Scope any, Tr any](
        if connHelper == nil {
                return nil
        }
+       tablesCacheLoader.Do(func() {
+               var err errors.Error
+               tablesCache, err = basicRes.GetDal().AllTables()
+               if err != nil {
+                       panic(err)
+               }
+       })
        return &ScopeApiHelper[Conn, Scope, Tr]{
                log:        basicRes.GetLogger(),
                db:         basicRes.GetDal(),
                validator:  vld,
+               bpManager:  
serviceHelper.NewBlueprintManager(basicRes.GetDal()),
                connHelper: connHelper,
        }
 }
@@ -70,6 +105,7 @@ func NewScopeHelper[Conn any, Scope any, Tr any](
 type ScopeRes[T any] struct {
        Scope                  T      `mapstructure:",squash"`
        TransformationRuleName string 
`mapstructure:"transformationRuleName,omitempty"`
+       Blueprints             []*models.Blueprint
 }
 
 type ScopeReq[T any] struct {
@@ -87,12 +123,11 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) Put(input 
*plugin.ApiResourceInput) (*
        if err != nil {
                return nil, errors.BadInput.Wrap(err, "decoding scope error")
        }
-       // Extract the connection ID from the input.Params map
-       connectionId, _ := extractFromReqParam(input.Params)
-       if connectionId == 0 {
+       params := c.extractFromReqParam(input)
+       if params.connectionId == 0 {
                return nil, errors.BadInput.New("invalid connectionId")
        }
-       err = c.VerifyConnection(connectionId)
+       err = c.VerifyConnection(params.connectionId)
        if err != nil {
                return nil, err
        }
@@ -111,7 +146,7 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) Put(input 
*plugin.ApiResourceInput) (*
                }
 
                // Set the connection ID, CreatedDate, and UpdatedDate fields
-               setScopeFields(v, connectionId, &now, &now)
+               setScopeFields(v, params.connectionId, &now, &now)
 
                // Verify that the primary key value is valid
                err = VerifyScope(v, c.validator)
@@ -136,20 +171,19 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) Put(input 
*plugin.ApiResourceInput) (*
 }
 
 func (c *ScopeApiHelper[Conn, Scope, Tr]) Update(input 
*plugin.ApiResourceInput, fieldName string) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, scopeId := extractFromReqParam(input.Params)
-
-       if connectionId == 0 {
-               return &plugin.ApiResourceOutput{Body: nil, Status: 
http.StatusInternalServerError}, errors.BadInput.New("invalid connectionId")
+       params := c.extractFromReqParam(input)
+       if params.connectionId == 0 {
+               return nil, errors.BadInput.New("invalid connectionId")
        }
-       if len(scopeId) == 0 || scopeId == "0" {
-               return &plugin.ApiResourceOutput{Body: nil, Status: 
http.StatusInternalServerError}, errors.BadInput.New("invalid scopeId")
+       if len(params.scopeId) == 0 {
+               return nil, errors.BadInput.New("invalid scopeId")
        }
-       err := c.VerifyConnection(connectionId)
+       err := c.VerifyConnection(params.connectionId)
        if err != nil {
                return &plugin.ApiResourceOutput{Body: nil, Status: 
http.StatusInternalServerError}, err
        }
        var scope Scope
-       err = c.db.First(&scope, dal.Where(fmt.Sprintf("connection_id = ? AND 
%s = ?", fieldName), connectionId, scopeId))
+       err = c.db.First(&scope, dal.Where(fmt.Sprintf("connection_id = ? AND 
%s = ?", fieldName), params.connectionId, params.scopeId))
        if err != nil {
                return &plugin.ApiResourceOutput{Body: nil, Status: 
http.StatusInternalServerError}, errors.Default.New("getting Scope error")
        }
@@ -178,7 +212,9 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) Update(input 
*plugin.ApiResourceInput,
                        return nil, errors.NotFound.New("transformationRule not 
found")
                }
        }
-       scopeRes := &ScopeRes[Scope]{scope, 
reflect.ValueOf(rule).FieldByName("Name").String()}
+       scopeRes := &ScopeRes[Scope]{
+               Scope:                  scope,
+               TransformationRuleName: 
reflect.ValueOf(rule).FieldByName("Name").String()}
 
        return &plugin.ApiResourceOutput{Body: scopeRes, Status: 
http.StatusOK}, nil
 }
@@ -186,19 +222,19 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) Update(input 
*plugin.ApiResourceInput,
 // GetScopeList returns a list of scopes. It expects a fieldName argument, 
which is used
 // to extract the connection ID from the input.Params map.
 
-func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScopeList(input 
*plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScopeList(input 
*plugin.ApiResourceInput, scopeIdFieldName ...string) 
(*plugin.ApiResourceOutput, errors.Error) {
        // Extract the connection ID from the input.Params map
-       connectionId, _ := extractFromReqParam(input.Params)
-       if connectionId == 0 {
+       params := c.extractFromGetReqParam(input)
+       if params.connectionId == 0 {
                return nil, errors.BadInput.New("invalid path params: 
\"connectionId\" not set")
        }
-       err := c.VerifyConnection(connectionId)
+       err := c.VerifyConnection(params.connectionId)
        if err != nil {
                return nil, err
        }
        limit, offset := GetLimitOffset(input.Query, "pageSize", "page")
        var scopes []*Scope
-       err = c.db.All(&scopes, dal.Where("connection_id = ?", connectionId), 
dal.Limit(limit), dal.Offset(offset))
+       err = c.db.All(&scopes, dal.Where("connection_id = ?", 
params.connectionId), dal.Limit(limit), dal.Offset(offset))
        if err != nil {
                return nil, err
        }
@@ -207,24 +243,53 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) 
GetScopeList(input *plugin.ApiResource
        if err != nil {
                return nil, err
        }
+       if params.loadBlueprints {
+               if len(scopeIdFieldName) == 0 {
+                       return nil, errors.Default.New("scope Id field name is 
not known") //temporary, limited solution until I properly refactor all of this 
in another PR
+               }
+               scopesById := c.mapByScopeId(apiScopes, scopeIdFieldName[0])
+               var scopeIds []string
+               for id := range scopesById {
+                       scopeIds = append(scopeIds, id)
+               }
+               blueprintMap, err := 
c.bpManager.GetBlueprintsByScopes(params.connectionId, scopeIds...)
+               if err != nil {
+                       return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
getting blueprints for scopes from connection %d", params.connectionId))
+               }
+               apiScopes = nil
+               for scopeId, scope := range scopesById {
+                       if bps, ok := blueprintMap[scopeId]; ok {
+                               scope.Blueprints = bps
+                               delete(blueprintMap, scopeId)
+                       }
+                       apiScopes = append(apiScopes, scope)
+               }
+               if len(blueprintMap) > 0 {
+                       var danglingIds []string
+                       for bpId := range blueprintMap {
+                               danglingIds = append(danglingIds, bpId)
+                       }
+                       c.log.Warn(nil, "The following dangling scopes were 
found: %v", danglingIds)
+               }
+       }
        return &plugin.ApiResourceOutput{Body: apiScopes, Status: 
http.StatusOK}, nil
 }
 
-func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScope(input 
*plugin.ApiResourceInput, fieldName string) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, scopeId := extractFromReqParam(input.Params)
-       if connectionId == 0 {
+func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScope(input 
*plugin.ApiResourceInput, scopeIdColumnName string) (*plugin.ApiResourceOutput, 
errors.Error) {
+       params := c.extractFromGetReqParam(input)
+       if params == nil || params.connectionId == 0 {
                return nil, errors.BadInput.New("invalid path params: 
\"connectionId\" not set")
        }
-       if len(scopeId) == 0 || scopeId == "0" {
+       if len(params.scopeId) == 0 || params.scopeId == "0" {
                return nil, errors.BadInput.New("invalid path params: 
\"scopeId\" not set/invalid")
        }
-       err := c.VerifyConnection(connectionId)
+       err := c.VerifyConnection(params.connectionId)
        if err != nil {
                return nil, err
        }
        db := c.db
 
-       query := dal.Where(fmt.Sprintf("connection_id = ? AND %s = ?", 
fieldName), connectionId, scopeId)
+       query := dal.Where(fmt.Sprintf("connection_id = ? AND %s = ?", 
scopeIdColumnName), params.connectionId, params.scopeId)
        var scope Scope
        err = db.First(&scope, query)
        if db.IsErrorNotFound(err) {
@@ -245,9 +310,95 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScope(input 
*plugin.ApiResourceInpu
                        return nil, errors.NotFound.New("transformationRule not 
found")
                }
        }
-       scopeRes := &ScopeRes[Scope]{scope, 
reflect.ValueOf(rule).FieldByName("Name").String()}
+       scopeRes := &ScopeRes[Scope]{
+               Scope:                  scope,
+               TransformationRuleName: 
reflect.ValueOf(rule).FieldByName("Name").String(),
+       }
        return &plugin.ApiResourceOutput{Body: scopeRes, Status: 
http.StatusOK}, nil
 }
+func (c *ScopeApiHelper[Conn, Scope, Tr]) DeleteScope(input 
*plugin.ApiResourceInput, scopeIdFieldName string, rawScopeParamName string,
+       getScopeParamValue func(db dal.Dal, scopeId string) (string, 
errors.Error)) (*plugin.ApiResourceOutput, errors.Error) {
+       params := c.extractFromDeleteReqParam(input)
+       if params == nil || params.connectionId == 0 {
+               return nil, errors.BadInput.New("invalid path params: 
\"connectionId\" not set")
+       }
+       if len(params.scopeId) == 0 || params.scopeId == "0" {
+               return nil, errors.BadInput.New("invalid path params: 
\"scopeId\" not set/invalid")
+       }
+       err := c.VerifyConnection(params.connectionId)
+       if err != nil {
+               return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
verifying connection for connection ID %d", params.connectionId))
+       }
+       db := c.db
+       blueprintsMap, err := 
c.bpManager.GetBlueprintsByScopes(params.connectionId, params.scopeId)
+       if err != nil {
+               return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
retrieving scope with scope ID %s", params.scopeId))
+       }
+       blueprints := blueprintsMap[params.scopeId]
+       // find all tables for this plugin
+       tables, err := getAffectedTables(params.plugin)
+       if err != nil {
+               return nil, errors.Default.Wrap(err, fmt.Sprintf("error getting 
database tables managed by plugin %s", params.plugin))
+       }
+       // delete all the plugin records referencing this scope
+       if rawScopeParamName != "" {
+               scopeParamValue := params.scopeId
+               if getScopeParamValue != nil {
+                       scopeParamValue, err = getScopeParamValue(c.db, 
params.scopeId) // this function is optional - use it if API data params stores 
a value different to the scope id (e.g. github plugin)
+                       if err != nil {
+                               return nil, errors.Default.Wrap(err, 
fmt.Sprintf("error extracting scope parameter name for scope %s", 
params.scopeId))
+                       }
+               }
+               for _, table := range tables {
+                       err = db.Exec(createDeleteQuery(table, 
rawScopeParamName, scopeParamValue))
+                       if err != nil {
+                               return nil, errors.Default.Wrap(err, 
fmt.Sprintf("error deleting data bound to scope %s for plugin %s", 
params.scopeId, params.plugin))
+                       }
+               }
+       }
+       var impactedBlueprints []*models.Blueprint
+       if !params.deleteDataOnly {
+               // Delete the scope itself
+               scope := new(Scope)
+               err = c.db.Delete(&scope, dal.Where(fmt.Sprintf("connection_id 
= ? AND %s = ?", scopeIdFieldName),
+                       params.connectionId, params.scopeId))
+               if err != nil {
+                       return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
deleting scope %s", params.scopeId))
+               }
+               // update the blueprints (remove scope reference from them)
+               for _, blueprint := range blueprints {
+                       settings, _ := blueprint.UnmarshalSettings()
+                       var changed bool
+                       err = settings.UpdateConnections(func(c 
*plugin.BlueprintConnectionV200) errors.Error {
+                               var retainedScopes []*plugin.BlueprintScopeV200
+                               for _, bpScope := range c.Scopes {
+                                       if bpScope.Id == params.scopeId { // 
we'll be removing this one
+                                               changed = true
+                                       } else {
+                                               retainedScopes = 
append(retainedScopes, bpScope)
+                                       }
+                               }
+                               c.Scopes = retainedScopes
+                               return nil
+                       })
+                       if err != nil {
+                               return nil, errors.Default.Wrap(err, 
fmt.Sprintf("error removing scope %s from blueprint %d", params.scopeId, 
blueprint.ID))
+                       }
+                       if changed {
+                               err = blueprint.UpdateSettings(&settings)
+                               if err != nil {
+                                       return nil, errors.Default.Wrap(err, 
fmt.Sprintf("error writing new settings into blueprint %s", blueprint.Name))
+                               }
+                               err = c.bpManager.SaveDbBlueprint(blueprint)
+                               if err != nil {
+                                       return nil, errors.Default.Wrap(err, 
fmt.Sprintf("error saving the updated blueprint %s", blueprint.Name))
+                               }
+                               impactedBlueprints = append(impactedBlueprints, 
blueprint)
+                       }
+               }
+       }
+       return &plugin.ApiResourceOutput{Body: impactedBlueprints, Status: 
http.StatusOK}, nil
+}
 
 func (c *ScopeApiHelper[Conn, Scope, Tr]) VerifyConnection(connId uint64) 
errors.Error {
        var conn Conn
@@ -261,10 +412,10 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) 
VerifyConnection(connId uint64) errors
        return nil
 }
 
-func (c *ScopeApiHelper[Conn, Scope, Tr]) addTransformationName(scopes 
[]*Scope) ([]ScopeRes[Scope], errors.Error) {
+func (c *ScopeApiHelper[Conn, Scope, Tr]) addTransformationName(scopes 
[]*Scope) ([]*ScopeRes[Scope], errors.Error) {
        var ruleIds []uint64
 
-       apiScopes := make([]ScopeRes[Scope], 0)
+       apiScopes := make([]*ScopeRes[Scope], 0)
        for _, scope := range scopes {
                valueRepoRuleId := 
reflect.ValueOf(scope).Elem().FieldByName("TransformationRuleId")
                if !valueRepoRuleId.IsValid() {
@@ -291,9 +442,12 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) 
addTransformationName(scopes []*Scope)
        for _, scope := range scopes {
                field := 
reflect.ValueOf(scope).Elem().FieldByName("TransformationRuleId")
                if field.IsValid() {
-                       apiScopes = append(apiScopes, ScopeRes[Scope]{*scope, 
names[field.Uint()]})
+                       apiScopes = append(apiScopes, &ScopeRes[Scope]{
+                               Scope:                  *scope,
+                               TransformationRuleName: names[field.Uint()],
+                       })
                } else {
-                       apiScopes = append(apiScopes, ScopeRes[Scope]{Scope: 
*scope, TransformationRuleName: ""})
+                       apiScopes = append(apiScopes, &ScopeRes[Scope]{Scope: 
*scope, TransformationRuleName: ""})
                }
 
        }
@@ -312,13 +466,65 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) save(scope 
interface{}) errors.Error {
        return nil
 }
 
-func extractFromReqParam(params map[string]string) (uint64, string) {
-       connectionId, err := strconv.ParseUint(params["connectionId"], 10, 64)
-       if err != nil {
-               return 0, ""
+func (c *ScopeApiHelper[Conn, Scope, Tr]) mapByScopeId(scopes 
[]*ScopeRes[Scope], scopeIdFieldName string) map[string]*ScopeRes[Scope] {
+       scopeMap := map[string]*ScopeRes[Scope]{}
+       for _, scope := range scopes {
+               scopeId := fmt.Sprintf("%v", reflectField(scope.Scope, 
scopeIdFieldName).Interface())
+               scopeMap[scopeId] = scope
+       }
+       return scopeMap
+}
+
+func (c *ScopeApiHelper[Conn, Scope, Tr]) extractFromReqParam(input 
*plugin.ApiResourceInput) *requestParams {
+       connectionId, err := strconv.ParseUint(input.Params["connectionId"], 
10, 64)
+       if err != nil || connectionId == 0 {
+               connectionId = 0
+       }
+       scopeId := input.Params["scopeId"]
+       pluginName := input.Params["plugin"]
+       return &requestParams{
+               connectionId: connectionId,
+               scopeId:      scopeId,
+               plugin:       pluginName,
+       }
+}
+
+func (c *ScopeApiHelper[Conn, Scope, Tr]) extractFromDeleteReqParam(input 
*plugin.ApiResourceInput) *deleteRequestParams {
+       params := c.extractFromReqParam(input)
+       var err errors.Error
+       var deleteDataOnly bool
+       {
+               ddo, ok := input.Query["delete_data_only"]
+               if ok {
+                       deleteDataOnly, err = 
errors.Convert01(strconv.ParseBool(ddo[0]))
+               }
+               if err != nil {
+                       deleteDataOnly = false
+               }
+       }
+       return &deleteRequestParams{
+               requestParams:  *params,
+               deleteDataOnly: deleteDataOnly,
+       }
+}
+
+func (c *ScopeApiHelper[Conn, Scope, Tr]) extractFromGetReqParam(input 
*plugin.ApiResourceInput) *getRequestParams {
+       params := c.extractFromReqParam(input)
+       var err errors.Error
+       var loadBlueprints bool
+       {
+               lbps, ok := input.Query["blueprints"]
+               if ok {
+                       loadBlueprints, err = 
errors.Convert01(strconv.ParseBool(lbps[0]))
+               }
+               if err != nil {
+                       loadBlueprints = false
+               }
+       }
+       return &getRequestParams{
+               requestParams:  *params,
+               loadBlueprints: loadBlueprints,
        }
-       scopeId := params["scopeId"]
-       return connectionId, scopeId
 }
 
 func setScopeFields(p interface{}, connectionId uint64, createdDate 
*time.Time, updatedDate *time.Time) {
@@ -410,3 +616,81 @@ func (sr *ScopeRes[T]) MarshalJSON() ([]byte, error) {
 
        return result, nil
 }
+
+func createDeleteQuery(tableName string, scopeIdKey string, scopeId string) 
string {
+       column := "_raw_data_params"
+       if tableName == (models.CollectorLatestState{}.TableName()) {
+               column = "raw_data_params"
+       } else if strings.HasPrefix(tableName, "_raw_") {
+               column = "params"
+       }
+       query := `DELETE FROM ` + tableName + ` WHERE ` + column + ` LIKE '%"` 
+ scopeIdKey + `":"` + scopeId + `"%'`
+       return query
+}
+
+func getAffectedTables(pluginName string) ([]string, errors.Error) {
+       var tables []string
+       meta, err := plugin.GetPlugin(pluginName)
+       if err != nil {
+               return nil, err
+       }
+       if pluginModel, ok := meta.(plugin.PluginModel); !ok {
+               return nil, errors.Default.New(fmt.Sprintf("plugin \"%s\" does 
not implement listing its tables", pluginName))
+       } else {
+               // collect raw tables
+               for _, table := range tablesCache {
+                       if strings.HasPrefix(table, "_raw_"+pluginName) {
+                               tables = append(tables, table)
+                       }
+               }
+               // collect tool tables
+               tablesInfo := pluginModel.GetTablesInfo()
+               for _, table := range tablesInfo {
+                       // we only care about tables with RawOrigin
+                       ok = hasField(table, "RawDataParams")
+                       if ok {
+                               tables = append(tables, table.TableName())
+                       }
+               }
+               // collect domain tables
+               for _, domainTable := range domaininfo.GetDomainTablesInfo() {
+                       // we only care about tables with RawOrigin
+                       ok = hasField(domainTable, "RawDataParams")
+                       if ok {
+                               tables = append(tables, domainTable.TableName())
+                       }
+               }
+               // additional tables
+               tables = append(tables, 
models.CollectorLatestState{}.TableName())
+       }
+       return tables, nil
+}
+
+func reflectField(obj any, fieldName string) reflect.Value {
+       return reflectValue(obj).FieldByName(fieldName)
+}
+
+func hasField(obj any, fieldName string) bool {
+       _, ok := reflectType(obj).FieldByName(fieldName)
+       return ok
+}
+
+func reflectValue(obj any) reflect.Value {
+       val := reflect.ValueOf(obj)
+       kind := val.Kind()
+       for kind == reflect.Ptr || kind == reflect.Interface {
+               val = val.Elem()
+               kind = val.Kind()
+       }
+       return val
+}
+
+func reflectType(obj any) reflect.Type {
+       typ := reflect.TypeOf(obj)
+       kind := typ.Kind()
+       for kind == reflect.Ptr {
+               typ = typ.Elem()
+               kind = typ.Kind()
+       }
+       return typ
+}
diff --git a/backend/helpers/pluginhelper/api/scope_helper_test.go 
b/backend/helpers/pluginhelper/api/scope_helper_test.go
index f77acb079..a7f405267 100644
--- a/backend/helpers/pluginhelper/api/scope_helper_test.go
+++ b/backend/helpers/pluginhelper/api/scope_helper_test.go
@@ -257,6 +257,7 @@ func TestScopeApiHelper_Put(t *testing.T) {
        mockDal.On("CreateOrUpdate", mock.Anything, mock.Anything).Return(nil)
        mockDal.On("First", mock.Anything, mock.Anything).Return(nil)
        mockDal.On("All", mock.Anything, mock.Anything).Return(nil)
+       mockDal.On("AllTables").Return(nil, nil)
 
        connHelper := NewConnectionHelper(mockRes, nil)
 
@@ -293,9 +294,8 @@ func TestScopeApiHelper_Put(t *testing.T) {
                                "updatedAt":            "string",
                                "updatedDate":          "string",
                        }}}}
-
        // create a mock ScopeApiHelper with a mock database connection
-       apiHelper := &ScopeApiHelper[TestConnection, TestRepo, 
TestTransformationRule]{db: mockDal, connHelper: connHelper}
+       apiHelper := NewScopeHelper[TestConnection, TestRepo, 
TestTransformationRule](mockRes, nil, connHelper)
        // test a successful call to Put
        _, err := apiHelper.Put(input)
        assert.NoError(t, err)
diff --git a/backend/server/services/blueprint_helper.go 
b/backend/helpers/pluginhelper/services/blueprint_helper.go
similarity index 53%
rename from backend/server/services/blueprint_helper.go
rename to backend/helpers/pluginhelper/services/blueprint_helper.go
index adf5f6f39..d784be53c 100644
--- a/backend/server/services/blueprint_helper.go
+++ b/backend/helpers/pluginhelper/services/blueprint_helper.go
@@ -22,20 +22,39 @@ import (
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models"
+       "github.com/apache/incubator-devlake/core/plugin"
 )
 
+type BlueprintManager struct {
+       db dal.Dal
+}
+
+type GetBlueprintQuery struct {
+       Enable      *bool
+       IsManual    *bool
+       Label       string
+       SkipRecords int
+       PageSize    int
+}
+
+func NewBlueprintManager(db dal.Dal) *BlueprintManager {
+       return &BlueprintManager{
+               db: db,
+       }
+}
+
 // SaveDbBlueprint accepts a Blueprint instance and upsert it to database
-func SaveDbBlueprint(blueprint *models.Blueprint) errors.Error {
+func (b *BlueprintManager) SaveDbBlueprint(blueprint *models.Blueprint) 
errors.Error {
        var err error
        if blueprint.ID != 0 {
-               err = db.Update(&blueprint)
+               err = b.db.Update(&blueprint)
        } else {
-               err = db.Create(&blueprint)
+               err = b.db.Create(&blueprint)
        }
        if err != nil {
                return errors.Default.Wrap(err, "error creating DB blueprint")
        }
-       err = db.Delete(&models.DbBlueprintLabel{}, dal.Where(`blueprint_id = 
?`, blueprint.ID))
+       err = b.db.Delete(&models.DbBlueprintLabel{}, dal.Where(`blueprint_id = 
?`, blueprint.ID))
        if err != nil {
                return errors.Default.Wrap(err, "error delete DB blueprint's 
old labelModels")
        }
@@ -47,7 +66,7 @@ func SaveDbBlueprint(blueprint *models.Blueprint) 
errors.Error {
                                Name:        blueprint.Labels[i],
                        })
                }
-               err = db.Create(&blueprintLabels)
+               err = b.db.Create(&blueprintLabels)
                if err != nil {
                        return errors.Default.Wrap(err, "error creating DB 
blueprint's labelModels")
                }
@@ -56,7 +75,7 @@ func SaveDbBlueprint(blueprint *models.Blueprint) 
errors.Error {
 }
 
 // GetDbBlueprints returns a paginated list of Blueprints based on `query`
-func GetDbBlueprints(query *BlueprintQuery) ([]*models.Blueprint, int64, 
errors.Error) {
+func (b *BlueprintManager) GetDbBlueprints(query *GetBlueprintQuery) 
([]*models.Blueprint, int64, errors.Error) {
        // process query parameters
        clauses := []dal.Clause{dal.From(&models.Blueprint{})}
        if query.Enable != nil {
@@ -73,26 +92,27 @@ func GetDbBlueprints(query *BlueprintQuery) 
([]*models.Blueprint, int64, errors.
        }
 
        // count total records
-       count, err := db.Count(clauses...)
+       count, err := b.db.Count(clauses...)
        if err != nil {
                return nil, 0, err
        }
-
+       clauses = append(clauses, dal.Orderby("id DESC"))
        // load paginated blueprints from database
-       clauses = append(clauses,
-               dal.Orderby("id DESC"),
-               dal.Offset(query.GetSkip()),
-               dal.Limit(query.GetPageSize()),
-       )
+       if query.SkipRecords != 0 {
+               clauses = append(clauses, dal.Offset(query.SkipRecords))
+       }
+       if query.PageSize != 0 {
+               clauses = append(clauses, dal.Limit(query.PageSize))
+       }
        dbBlueprints := make([]*models.Blueprint, 0)
-       err = db.All(&dbBlueprints, clauses...)
+       err = b.db.All(&dbBlueprints, clauses...)
        if err != nil {
                return nil, 0, errors.Default.Wrap(err, "error getting DB count 
of blueprints")
        }
 
        // load labels for blueprints
        for _, dbBlueprint := range dbBlueprints {
-               err = fillBlueprintDetail(dbBlueprint)
+               err = b.fillBlueprintDetail(dbBlueprint)
                if err != nil {
                        return nil, 0, err
                }
@@ -102,43 +122,88 @@ func GetDbBlueprints(query *BlueprintQuery) 
([]*models.Blueprint, int64, errors.
 }
 
 // GetDbBlueprint returns the detail of a given Blueprint ID
-func GetDbBlueprint(blueprintId uint64) (*models.Blueprint, errors.Error) {
+func (b *BlueprintManager) GetDbBlueprint(blueprintId uint64) 
(*models.Blueprint, errors.Error) {
        blueprint := &models.Blueprint{}
-       err := db.First(blueprint, dal.Where("id = ?", blueprintId))
+       err := b.db.First(blueprint, dal.Where("id = ?", blueprintId))
        if err != nil {
-               if db.IsErrorNotFound(err) {
+               if b.db.IsErrorNotFound(err) {
                        return nil, errors.NotFound.Wrap(err, "could not find 
blueprint in DB")
                }
                return nil, errors.Default.Wrap(err, "error getting blueprint 
from DB")
        }
-       err = fillBlueprintDetail(blueprint)
+       err = b.fillBlueprintDetail(blueprint)
        if err != nil {
                return nil, err
        }
        return blueprint, nil
 }
 
+// GetBlueprintsByScopes returns all blueprints that have these scopeIds
+func (b *BlueprintManager) GetBlueprintsByScopes(connectionId uint64, scopeIds 
...string) (map[string][]*models.Blueprint, errors.Error) {
+       bps, _, err := b.GetDbBlueprints(&GetBlueprintQuery{})
+       if err != nil {
+               return nil, err
+       }
+       scopeMap := map[string][]*models.Blueprint{}
+       for _, bp := range bps {
+               scopes, err := bp.GetScopes(connectionId)
+               if err != nil {
+                       return nil, err
+               }
+               for _, scope := range scopes {
+                       if contains(scopeIds, scope.Id) {
+                               if inserted, ok := scopeMap[scope.Id]; !ok {
+                                       scopeMap[scope.Id] = 
[]*models.Blueprint{bp}
+                               } else {
+                                       inserted = append(inserted, bp)
+                                       scopeMap[scope.Id] = inserted
+                               }
+                               break
+                       }
+               }
+       }
+       return scopeMap, nil
+}
+
+// GetBlueprintConnections returns the connections associated with this 
blueprint Id
+func (b *BlueprintManager) GetBlueprintConnections(blueprintId uint64) 
([]*plugin.BlueprintConnectionV200, errors.Error) {
+       bp, err := b.GetDbBlueprint(blueprintId)
+       if err != nil {
+               return nil, err
+       }
+       return bp.GetConnections()
+}
+
 // GetDbBlueprintByProjectName returns the detail of a given projectName
-func GetDbBlueprintByProjectName(projectName string) (*models.Blueprint, 
errors.Error) {
+func (b *BlueprintManager) GetDbBlueprintByProjectName(projectName string) 
(*models.Blueprint, errors.Error) {
        dbBlueprint := &models.Blueprint{}
-       err := db.First(dbBlueprint, dal.Where("project_name = ?", projectName))
+       err := b.db.First(dbBlueprint, dal.Where("project_name = ?", 
projectName))
        if err != nil {
-               if db.IsErrorNotFound(err) {
+               if b.db.IsErrorNotFound(err) {
                        return nil, errors.NotFound.Wrap(err, 
fmt.Sprintf("could not find blueprint in DB by projectName %s", projectName))
                }
                return nil, errors.Default.Wrap(err, fmt.Sprintf("error getting 
blueprint from DB by projectName %s", projectName))
        }
-       err = fillBlueprintDetail(dbBlueprint)
+       err = b.fillBlueprintDetail(dbBlueprint)
        if err != nil {
                return nil, err
        }
        return dbBlueprint, nil
 }
 
-func fillBlueprintDetail(blueprint *models.Blueprint) errors.Error {
-       err := db.Pluck("name", &blueprint.Labels, 
dal.From(&models.DbBlueprintLabel{}), dal.Where("blueprint_id = ?", 
blueprint.ID))
+func (b *BlueprintManager) fillBlueprintDetail(blueprint *models.Blueprint) 
errors.Error {
+       err := b.db.Pluck("name", &blueprint.Labels, 
dal.From(&models.DbBlueprintLabel{}), dal.Where("blueprint_id = ?", 
blueprint.ID))
        if err != nil {
                return errors.Internal.Wrap(err, "error getting the blueprint 
labels from database")
        }
        return nil
 }
+
+func contains(list []string, target string) bool {
+       for _, t := range list {
+               if t == target {
+                       return true
+               }
+       }
+       return false
+}
diff --git a/backend/plugins/pagerduty/api/scope.go 
b/backend/plugins/pagerduty/api/scope.go
index 59fae3b9a..ec5996c77 100644
--- a/backend/plugins/pagerduty/api/scope.go
+++ b/backend/plugins/pagerduty/api/scope.go
@@ -58,7 +58,7 @@ func PutScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/scopes/{serviceId} 
[PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       return scopeHelper.Update(input, "id")
+       return scopeHelper.Update(input, "")
 }
 
 // GetScopeList get PagerDuty repos
@@ -68,12 +68,13 @@ func UpdateScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, err
 // @Param connectionId path int true "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these 
scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/scopes/ [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       return scopeHelper.GetScopeList(input)
+       return scopeHelper.GetScopeList(input, "Id")
 }
 
 // GetScope get one PagerDuty service
@@ -82,6 +83,7 @@ func GetScopeList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
 // @Tags plugins/pagerduty
 // @Param connectionId path int true "connection ID"
 // @Param serviceId path int true "service ID"
+// @Param blueprints query bool false "also return blueprints using this scope 
as part of the payload"
 // @Success 200  {object} ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -89,3 +91,18 @@ func GetScopeList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        return scopeHelper.GetScope(input, "id")
 }
+
+// DeleteScope delete plugin data associated with the scope and optionally the 
scope itself
+// @Summary delete plugin data associated with the scope and optionally the 
scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/pagerduty
+// @Param connectionId path int true "connection ID"
+// @Param serviceId path int true "service ID"
+// @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
+// @Success 200  {object} []models.Blueprint "list of blueprints impacted by 
the deletion"
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/pagerduty/connections/{connectionId}/scopes/{serviceId} 
[DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return scopeHelper.DeleteScope(input, "Id", "ScopeId", nil)
+}
diff --git 
a/backend/plugins/pagerduty/e2e/raw_tables/_raw_pagerduty_incidents.csv 
b/backend/plugins/pagerduty/e2e/raw_tables/_raw_pagerduty_incidents.csv
index 8cda8d9c8..65e43ff14 100644
--- a/backend/plugins/pagerduty/e2e/raw_tables/_raw_pagerduty_incidents.csv
+++ b/backend/plugins/pagerduty/e2e/raw_tables/_raw_pagerduty_incidents.csv
@@ -1,4 +1,4 @@
 id,params,data,url,input,created_at
-1,"{""ConnectionId"":1}","{""incident_number"": 4, ""title"": ""Crash 
reported"", ""created_at"": ""2022-11-03T06:23:06.000000Z"", ""status"": 
""triggered"", ""incident_key"": ""bb60942875634ee6a7fe94ddb51c3a09"", 
""service"": {""id"": ""PIKL83L"", ""type"": ""service_reference"", 
""summary"": ""DevService"", ""self"": 
""https://api.pagerduty.com/services/PIKL83L"";, ""html_url"": 
""https://keon-test.pagerduty.com/service-directory/PIKL83L""}, 
""assignments"": [{""at"": ""2022-11-03T06:23 [...]
-2,"{""ConnectionId"":1}","{""incident_number"": 5, ""title"": ""Slow 
startup"", ""created_at"": ""2022-11-03T06:44:28.000000Z"", ""status"": 
""acknowledged"", ""incident_key"": ""d7bc6d39c37e4af8b206a12ff6b05793"", 
""service"": {""id"": ""PIKL83L"", ""type"": ""service_reference"", 
""summary"": ""DevService"", ""self"": 
""https://api.pagerduty.com/services/PIKL83L"";, ""html_url"": 
""https://keon-test.pagerduty.com/service-directory/PIKL83L""}, 
""assignments"": [{""at"": ""2022-11-03T06:4 [...]
-3,"{""ConnectionId"":1}","{""incident_number"": 6, ""title"": ""Spamming 
logs"", ""created_at"": ""2022-11-03T06:45:36.000000Z"", ""status"": 
""resolved"", ""incident_key"": ""9f5acd07975e4c57bc717d8d9e066785"", 
""service"": {""id"": ""PIKL83L"", ""type"": ""service_reference"", 
""summary"": ""DevService"", ""self"": 
""https://api.pagerduty.com/services/PIKL83L"";, ""html_url"": 
""https://keon-test.pagerduty.com/service-directory/PIKL83L""}, 
""assignments"": [], ""last_status_change_at"": [...]
+1,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}","{""incident_number"": 4, 
""title"": ""Crash reported"", ""created_at"": ""2022-11-03T06:23:06.000000Z"", 
""status"": ""triggered"", ""incident_key"": 
""bb60942875634ee6a7fe94ddb51c3a09"", ""service"": {""id"": ""PIKL83L"", 
""type"": ""service_reference"", ""summary"": ""DevService"", ""self"": 
""https://api.pagerduty.com/services/PIKL83L"";, ""html_url"": 
""https://keon-test.pagerduty.com/service-directory/PIKL83L""}, 
""assignments"": [{"" [...]
+2,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}","{""incident_number"": 5, 
""title"": ""Slow startup"", ""created_at"": ""2022-11-03T06:44:28.000000Z"", 
""status"": ""acknowledged"", ""incident_key"": 
""d7bc6d39c37e4af8b206a12ff6b05793"", ""service"": {""id"": ""PIKL83L"", 
""type"": ""service_reference"", ""summary"": ""DevService"", ""self"": 
""https://api.pagerduty.com/services/PIKL83L"";, ""html_url"": 
""https://keon-test.pagerduty.com/service-directory/PIKL83L""}, 
""assignments"": [{" [...]
+3,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}","{""incident_number"": 6, 
""title"": ""Spamming logs"", ""created_at"": ""2022-11-03T06:45:36.000000Z"", 
""status"": ""resolved"", ""incident_key"": 
""9f5acd07975e4c57bc717d8d9e066785"", ""service"": {""id"": ""PIKL83L"", 
""type"": ""service_reference"", ""summary"": ""DevService"", ""self"": 
""https://api.pagerduty.com/services/PIKL83L"";, ""html_url"": 
""https://keon-test.pagerduty.com/service-directory/PIKL83L""}, 
""assignments"": [], "" [...]
diff --git 
a/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_assignments.csv 
b/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_assignments.csv
index a1b1bc317..3201f177f 100644
--- 
a/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_assignments.csv
+++ 
b/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_assignments.csv
@@ -1,4 +1,4 @@
 
incident_number,user_id,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,connection_id,assigned_at
-4,P25K520,2022-11-03T07:11:37.415+00:00,2022-11-03T07:11:37.415+00:00,"{""ConnectionId"":1}",_raw_pagerduty_incidents,1,,1,2022-11-03T07:02:36.000+00:00
-4,PQYACO3,2022-11-03T07:11:37.415+00:00,2022-11-03T07:11:37.415+00:00,"{""ConnectionId"":1}",_raw_pagerduty_incidents,1,,1,2022-11-03T06:23:06.000+00:00
-5,PQYACO3,2022-11-03T07:11:37.415+00:00,2022-11-03T07:11:37.415+00:00,"{""ConnectionId"":1}",_raw_pagerduty_incidents,2,,1,2022-11-03T06:44:37.000+00:00
+4,P25K520,2022-11-03T07:11:37.415+00:00,2022-11-03T07:11:37.415+00:00,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}",_raw_pagerduty_incidents,1,,1,2022-11-03T07:02:36.000+00:00
+4,PQYACO3,2022-11-03T07:11:37.415+00:00,2022-11-03T07:11:37.415+00:00,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}",_raw_pagerduty_incidents,1,,1,2022-11-03T06:23:06.000+00:00
+5,PQYACO3,2022-11-03T07:11:37.415+00:00,2022-11-03T07:11:37.415+00:00,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}",_raw_pagerduty_incidents,2,,1,2022-11-03T06:44:37.000+00:00
diff --git 
a/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_incidents.csv 
b/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_incidents.csv
index de1a59010..539209e20 100644
--- 
a/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_incidents.csv
+++ 
b/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_incidents.csv
@@ -1,4 +1,4 @@
 
connection_id,number,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,url,service_id,summary,status,urgency,created_date,updated_date
-1,4,2022-11-03T07:11:37.422+00:00,2022-11-03T07:11:37.422+00:00,"{""ConnectionId"":1}",_raw_pagerduty_incidents,1,,https://keon-test.pagerduty.com/incidents/Q3YON8WNWTZMRQ,PIKL83L,[#4]
 Crash 
reported,triggered,high,2022-11-03T06:23:06.000+00:00,2022-11-03T07:02:36.000+00:00
-1,5,2022-11-03T07:11:37.422+00:00,2022-11-03T07:11:37.422+00:00,"{""ConnectionId"":1}",_raw_pagerduty_incidents,2,,https://keon-test.pagerduty.com/incidents/Q3CZAU7Q4008QD,PIKL83L,[#5]
 Slow 
startup,acknowledged,high,2022-11-03T06:44:28.000+00:00,2022-11-03T06:44:37.000+00:00
-1,6,2022-11-03T07:11:37.422+00:00,2022-11-03T07:11:37.422+00:00,"{""ConnectionId"":1}",_raw_pagerduty_incidents,3,,https://keon-test.pagerduty.com/incidents/Q1OHFWFP3GPXOG,PIKL83L,[#6]
 Spamming 
logs,resolved,low,2022-11-03T06:45:36.000+00:00,2022-11-03T06:51:44.000+00:00
+1,4,2022-11-03T07:11:37.422+00:00,2022-11-03T07:11:37.422+00:00,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}",_raw_pagerduty_incidents,1,,https://keon-test.pagerduty.com/incidents/Q3YON8WNWTZMRQ,PIKL83L,[#4]
 Crash 
reported,triggered,high,2022-11-03T06:23:06.000+00:00,2022-11-03T07:02:36.000+00:00
+1,5,2022-11-03T07:11:37.422+00:00,2022-11-03T07:11:37.422+00:00,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}",_raw_pagerduty_incidents,2,,https://keon-test.pagerduty.com/incidents/Q3CZAU7Q4008QD,PIKL83L,[#5]
 Slow 
startup,acknowledged,high,2022-11-03T06:44:28.000+00:00,2022-11-03T06:44:37.000+00:00
+1,6,2022-11-03T07:11:37.422+00:00,2022-11-03T07:11:37.422+00:00,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}",_raw_pagerduty_incidents,3,,https://keon-test.pagerduty.com/incidents/Q1OHFWFP3GPXOG,PIKL83L,[#6]
 Spamming 
logs,resolved,low,2022-11-03T06:45:36.000+00:00,2022-11-03T06:51:44.000+00:00
diff --git 
a/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_users.csv 
b/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_users.csv
index a47f8b668..a679b64e0 100644
--- a/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_users.csv
+++ b/backend/plugins/pagerduty/e2e/snapshot_tables/_tool_pagerduty_users.csv
@@ -1,3 +1,3 @@
 
connection_id,id,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,url,name
-1,P25K520,2022-11-03T07:11:37.418+00:00,2022-11-03T07:11:37.418+00:00,"{""ConnectionId"":1}",_raw_pagerduty_incidents,1,,https://keon-test.pagerduty.com/users/P25K520,Kian
 Amini
-1,PQYACO3,2022-11-03T07:11:37.418+00:00,2022-11-03T07:11:37.418+00:00,"{""ConnectionId"":1}",_raw_pagerduty_incidents,2,,https://keon-test.pagerduty.com/users/PQYACO3,Keon
 Amini
+1,P25K520,2022-11-03T07:11:37.418+00:00,2022-11-03T07:11:37.418+00:00,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}",_raw_pagerduty_incidents,1,,https://keon-test.pagerduty.com/users/P25K520,Kian
 Amini
+1,PQYACO3,2022-11-03T07:11:37.418+00:00,2022-11-03T07:11:37.418+00:00,"{""ConnectionId"":1,""ScopeId"":""PIKL83L""}",_raw_pagerduty_incidents,2,,https://keon-test.pagerduty.com/users/PQYACO3,Keon
 Amini
diff --git a/backend/plugins/pagerduty/impl/impl.go 
b/backend/plugins/pagerduty/impl/impl.go
index d669e7e82..01b11285b 100644
--- a/backend/plugins/pagerduty/impl/impl.go
+++ b/backend/plugins/pagerduty/impl/impl.go
@@ -20,6 +20,7 @@ package impl
 import (
        "fmt"
        "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -35,6 +36,8 @@ var _ plugin.PluginMeta = (*PagerDuty)(nil)
 var _ plugin.PluginInit = (*PagerDuty)(nil)
 var _ plugin.PluginTask = (*PagerDuty)(nil)
 var _ plugin.PluginApi = (*PagerDuty)(nil)
+
+var _ plugin.PluginModel = (*PagerDuty)(nil)
 var _ plugin.PluginBlueprintV100 = (*PagerDuty)(nil)
 var _ plugin.CloseablePluginTask = (*PagerDuty)(nil)
 
@@ -57,6 +60,15 @@ func (p PagerDuty) SubTaskMetas() []plugin.SubTaskMeta {
        }
 }
 
+func (p PagerDuty) GetTablesInfo() []dal.Tabler {
+       return []dal.Tabler{
+               models.Service{},
+               models.Incident{},
+               models.User{},
+               models.Assignment{},
+       }
+}
+
 func (p PagerDuty) PrepareTaskData(taskCtx plugin.TaskContext, options 
map[string]interface{}) (interface{}, errors.Error) {
        op, err := tasks.DecodeAndValidateTaskOptions(options)
        if err != nil {
@@ -130,8 +142,9 @@ func (p PagerDuty) ApiResources() 
map[string]map[string]plugin.ApiResourceHandle
                        "PUT": api.PutScope,
                },
                "connections/:connectionId/scopes/:scopeId": {
-                       "GET":   api.GetScope,
-                       "PATCH": api.UpdateScope,
+                       "GET":    api.GetScope,
+                       "PATCH":  api.UpdateScope,
+                       "DELETE": api.DeleteScope,
                },
                "connections/:connectionId/transformation_rules": {
                        "POST": api.CreateTransformationRule,
diff --git a/backend/plugins/pagerduty/tasks/incidents_collector.go 
b/backend/plugins/pagerduty/tasks/incidents_collector.go
index 883f071bf..65b6b1cc8 100644
--- a/backend/plugins/pagerduty/tasks/incidents_collector.go
+++ b/backend/plugins/pagerduty/tasks/incidents_collector.go
@@ -61,11 +61,9 @@ func CollectIncidents(taskCtx plugin.SubTaskContext) 
errors.Error {
        data := taskCtx.GetData().(*PagerDutyTaskData)
        db := taskCtx.GetDal()
        args := api.RawDataSubTaskArgs{
-               Ctx: taskCtx,
-               Params: PagerDutyParams{
-                       ConnectionId: data.Options.ConnectionId,
-               },
-               Table: RAW_INCIDENTS_TABLE,
+               Ctx:     taskCtx,
+               Options: data.Options,
+               Table:   RAW_INCIDENTS_TABLE,
        }
        collector, err := 
api.NewStatefulApiCollectorForFinalizableEntity(api.FinalizableApiCollectorArgs{
                RawDataSubTaskArgs: args,
diff --git a/backend/plugins/pagerduty/tasks/incidents_converter.go 
b/backend/plugins/pagerduty/tasks/incidents_converter.go
index 4f1d4bc9a..dda345389 100644
--- a/backend/plugins/pagerduty/tasks/incidents_converter.go
+++ b/backend/plugins/pagerduty/tasks/incidents_converter.go
@@ -68,11 +68,9 @@ func ConvertIncidents(taskCtx plugin.SubTaskContext) 
errors.Error {
        idGen := didgen.NewDomainIdGenerator(&models.Incident{})
        converter, err := api.NewDataConverter(api.DataConverterArgs{
                RawDataSubTaskArgs: api.RawDataSubTaskArgs{
-                       Ctx: taskCtx,
-                       Params: PagerDutyParams{
-                               ConnectionId: data.Options.ConnectionId,
-                       },
-                       Table: RAW_INCIDENTS_TABLE,
+                       Ctx:     taskCtx,
+                       Options: data.Options,
+                       Table:   RAW_INCIDENTS_TABLE,
                },
                InputRowType: reflect.TypeOf(IncidentWithUser{}),
                Input:        cursor,
diff --git a/backend/plugins/pagerduty/tasks/incidents_extractor.go 
b/backend/plugins/pagerduty/tasks/incidents_extractor.go
index d5ba7eb26..6c83830b9 100644
--- a/backend/plugins/pagerduty/tasks/incidents_extractor.go
+++ b/backend/plugins/pagerduty/tasks/incidents_extractor.go
@@ -32,11 +32,9 @@ func ExtractIncidents(taskCtx plugin.SubTaskContext) 
errors.Error {
        data := taskCtx.GetData().(*PagerDutyTaskData)
        extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
                RawDataSubTaskArgs: api.RawDataSubTaskArgs{
-                       Ctx: taskCtx,
-                       Params: PagerDutyParams{
-                               ConnectionId: data.Options.ConnectionId,
-                       },
-                       Table: RAW_INCIDENTS_TABLE,
+                       Ctx:     taskCtx,
+                       Options: data.Options,
+                       Table:   RAW_INCIDENTS_TABLE,
                },
                Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
                        incidentRaw := &raw.Incidents{}
diff --git a/backend/plugins/pagerduty/tasks/task_data.go 
b/backend/plugins/pagerduty/tasks/task_data.go
index 91ba00329..b223a167e 100644
--- a/backend/plugins/pagerduty/tasks/task_data.go
+++ b/backend/plugins/pagerduty/tasks/task_data.go
@@ -41,6 +41,14 @@ type PagerDutyTaskData struct {
 
 type PagerDutyParams struct {
        ConnectionId uint64
+       ScopeId      string
+}
+
+func (p *PagerDutyOptions) GetParams() any {
+       return PagerDutyParams{
+               ConnectionId: p.ConnectionId,
+               ScopeId:      p.ServiceId,
+       }
 }
 
 func DecodeAndValidateTaskOptions(options map[string]interface{}) 
(*PagerDutyOptions, errors.Error) {
diff --git a/backend/server/api/router.go b/backend/server/api/router.go
index 26114da9b..73e21856d 100644
--- a/backend/server/api/router.go
+++ b/backend/server/api/router.go
@@ -88,22 +88,23 @@ func registerPluginEndpoints(r *gin.Engine, pluginName 
string, apiResources map[
                        r.Handle(
                                method,
                                fmt.Sprintf("/plugins/%s/%s", pluginName, 
resourcePath),
-                               handlePluginCall(h),
+                               handlePluginCall(pluginName, h),
                        )
                }
        }
 }
 
-func handlePluginCall(handler plugin.ApiResourceHandler) func(c *gin.Context) {
+func handlePluginCall(pluginName string, handler plugin.ApiResourceHandler) 
func(c *gin.Context) {
        return func(c *gin.Context) {
                var err error
                input := &plugin.ApiResourceInput{}
+               input.Params = make(map[string]string)
                if len(c.Params) > 0 {
-                       input.Params = make(map[string]string)
                        for _, param := range c.Params {
                                input.Params[param.Key] = param.Value
                        }
                }
+               input.Params["plugin"] = pluginName
                input.Query = c.Request.URL.Query()
                if c.Request.Body != nil {
                        if 
strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data;") 
{
diff --git a/backend/server/services/blueprint.go 
b/backend/server/services/blueprint.go
index aec8a22cc..0e8d5c176 100644
--- a/backend/server/services/blueprint.go
+++ b/backend/server/services/blueprint.go
@@ -20,6 +20,7 @@ package services
 import (
        "encoding/json"
        "fmt"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "strings"
 
        "github.com/apache/incubator-devlake/core/dal"
@@ -31,6 +32,10 @@ import (
        "github.com/robfig/cron/v3"
 )
 
+var (
+       blueprintLog = logruslog.Global.Nested("blueprint")
+)
+
 // BlueprintQuery is a query for GetBlueprints
 type BlueprintQuery struct {
        Pagination
@@ -39,10 +44,6 @@ type BlueprintQuery struct {
        Label    string `form:"label"`
 }
 
-var (
-       blueprintLog = logruslog.Global.Nested("blueprint")
-)
-
 type BlueprintJob struct {
        Blueprint *models.Blueprint
 }
@@ -63,7 +64,7 @@ func CreateBlueprint(blueprint *models.Blueprint) 
errors.Error {
        if err != nil {
                return err
        }
-       err = SaveDbBlueprint(blueprint)
+       err = bpManager.SaveDbBlueprint(blueprint)
        if err != nil {
                return err
        }
@@ -76,7 +77,13 @@ func CreateBlueprint(blueprint *models.Blueprint) 
errors.Error {
 
 // GetBlueprints returns a paginated list of Blueprints based on `query`
 func GetBlueprints(query *BlueprintQuery) ([]*models.Blueprint, int64, 
errors.Error) {
-       blueprints, count, err := GetDbBlueprints(query)
+       blueprints, count, err := 
bpManager.GetDbBlueprints(&services.GetBlueprintQuery{
+               Enable:      query.Enable,
+               IsManual:    query.IsManual,
+               Label:       query.Label,
+               SkipRecords: query.GetSkip(),
+               PageSize:    query.GetPageSize(),
+       })
        if err != nil {
                return nil, 0, errors.Convert(err)
        }
@@ -85,7 +92,7 @@ func GetBlueprints(query *BlueprintQuery) 
([]*models.Blueprint, int64, errors.Er
 
 // GetBlueprint returns the detail of a given Blueprint ID
 func GetBlueprint(blueprintId uint64) (*models.Blueprint, errors.Error) {
-       blueprint, err := GetDbBlueprint(blueprintId)
+       blueprint, err := bpManager.GetDbBlueprint(blueprintId)
        if err != nil {
                if db.IsErrorNotFound(err) {
                        return nil, errors.NotFound.New("blueprint not found")
@@ -100,7 +107,7 @@ func GetBlueprintByProjectName(projectName string) 
(*models.Blueprint, errors.Er
        if projectName == "" {
                return nil, errors.Internal.New("can not use the empty 
projectName to search the unique blueprint")
        }
-       blueprint, err := GetDbBlueprintByProjectName(projectName)
+       blueprint, err := bpManager.GetDbBlueprintByProjectName(projectName)
        if err != nil {
                // Allow specific projectName to fail to find the corresponding 
blueprint
                if db.IsErrorNotFound(err) {
@@ -177,7 +184,7 @@ func saveBlueprint(blueprint *models.Blueprint) 
(*models.Blueprint, errors.Error
        if err != nil {
                return nil, errors.BadInput.WrapRaw(err)
        }
-       err = SaveDbBlueprint(blueprint)
+       err = bpManager.SaveDbBlueprint(blueprint)
        if err != nil {
                return nil, err
        }
@@ -221,7 +228,10 @@ func PatchBlueprint(id uint64, body 
map[string]interface{}) (*models.Blueprint,
 func ReloadBlueprints(c *cron.Cron) errors.Error {
        enable := true
        isManual := false
-       blueprints, _, err := GetDbBlueprints(&BlueprintQuery{Enable: &enable, 
IsManual: &isManual})
+       blueprints, _, err := 
bpManager.GetDbBlueprints(&services.GetBlueprintQuery{
+               Enable:   &enable,
+               IsManual: &isManual,
+       })
        if err != nil {
                return err
        }
diff --git a/backend/server/services/init.go b/backend/server/services/init.go
index d1eead9fa..e4f9c065b 100644
--- a/backend/server/services/init.go
+++ b/backend/server/services/init.go
@@ -29,6 +29,7 @@ import (
        "github.com/apache/incubator-devlake/core/models/migrationscripts"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/core/runner"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "github.com/apache/incubator-devlake/impls/dalgorm"
        "github.com/apache/incubator-devlake/impls/logruslog"
        "github.com/apache/incubator-devlake/server/services/auth"
@@ -39,6 +40,8 @@ import (
 var cfg config.ConfigReader
 var logger log.Logger
 var db dal.Dal
+
+var bpManager *services.BlueprintManager
 var basicRes context.BasicRes
 var migrator plugin.Migrator
 var cronManager *cron.Cron
@@ -57,7 +60,7 @@ func InitResources() {
        cfg = basicRes.GetConfigReader()
        logger = basicRes.GetLogger()
        db = basicRes.GetDal()
-
+       bpManager = services.NewBlueprintManager(db)
        // initialize db migrator
        migrator, err = runner.InitMigrator(basicRes)
        if err != nil {
diff --git a/backend/test/e2e/remote/helper.go 
b/backend/test/e2e/remote/helper.go
index e62cbef1b..e47be0613 100644
--- a/backend/test/e2e/remote/helper.go
+++ b/backend/test/e2e/remote/helper.go
@@ -78,23 +78,21 @@ func CreateTestConnection(client *helper.DevlakeClient) 
*helper.Connection {
        return connection
 }
 
-func CreateTestScope(client *helper.DevlakeClient, connectionId uint64) any {
-       res := client.CreateTransformationRule(PLUGIN_NAME, connectionId, 
FakeTxRule{Name: "Tx rule", Env: "test env"})
-       rule, ok := res.(map[string]interface{})
-       if !ok {
-               panic("Cannot cast transform rule")
-       }
-       ruleId := uint64(rule["id"].(float64))
-
-       scope := client.CreateScope(PLUGIN_NAME,
+func CreateTestScope(client *helper.DevlakeClient, rule *FakeTxRule, 
connectionId uint64) *FakeProject {
+       scopes := helper.Cast[[]FakeProject](client.CreateScope(PLUGIN_NAME,
                connectionId,
                FakeProject{
                        Id:                   "p1",
                        Name:                 "Project 1",
                        ConnectionId:         connectionId,
                        Url:                  "http://fake.org/api/project/p1";,
-                       TransformationRuleId: ruleId,
+                       TransformationRuleId: rule.Id,
                },
-       )
-       return scope
+       ))
+       return &scopes[0]
+}
+
+func CreateTestTransformationRule(client *helper.DevlakeClient, connectionId 
uint64) *FakeTxRule {
+       rule := 
helper.Cast[FakeTxRule](client.CreateTransformationRule(PLUGIN_NAME, 
connectionId, FakeTxRule{Name: "Tx rule", Env: "test env"}))
+       return &rule
 }
diff --git a/backend/test/e2e/remote/python_plugin_test.go 
b/backend/test/e2e/remote/python_plugin_test.go
index 8c7d385e4..e20ca493b 100644
--- a/backend/test/e2e/remote/python_plugin_test.go
+++ b/backend/test/e2e/remote/python_plugin_test.go
@@ -58,13 +58,11 @@ func TestRemoteScopeGroups(t *testing.T) {
 func TestRemoteScopes(t *testing.T) {
        client := CreateClient(t)
        connection := CreateTestConnection(client)
-
        output := client.RemoteScopes(helper.RemoteScopesQuery{
                PluginName:   PLUGIN_NAME,
                ConnectionId: connection.ID,
                GroupId:      "group1",
        })
-
        scopes := output.Children
        require.Equal(t, 1, len(scopes))
        scope := scopes[0]
@@ -82,25 +80,28 @@ func TestRemoteScopes(t *testing.T) {
 
 func TestCreateScope(t *testing.T) {
        client := CreateClient(t)
-       var connectionId uint64 = 1
-
-       CreateTestScope(client, connectionId)
+       conn := CreateTestConnection(client)
+       rule := CreateTestTransformationRule(client, conn.ID)
+       scope := CreateTestScope(client, rule, conn.ID)
 
-       scopes := client.ListScopes(PLUGIN_NAME, connectionId)
+       scopes := client.ListScopes(PLUGIN_NAME, conn.ID, false)
        require.Equal(t, 1, len(scopes))
-       cicd_scope := helper.Cast[FakeProject](scopes[0])
-       require.Equal(t, connectionId, cicd_scope.ConnectionId)
-       require.Equal(t, "p1", cicd_scope.Id)
-       require.Equal(t, "Project 1", cicd_scope.Name)
-       require.Equal(t, "http://fake.org/api/project/p1";, cicd_scope.Url)
+
+       cicdScope := helper.Cast[FakeProject](scopes[0].Scope)
+       require.Equal(t, conn.ID, cicdScope.ConnectionId)
+       require.Equal(t, "p1", cicdScope.Id)
+       require.Equal(t, "Project 1", cicdScope.Name)
+       require.Equal(t, "http://fake.org/api/project/p1";, cicdScope.Url)
+
+       cicdScope.Name = "scope-name-2"
+       client.UpdateScope(PLUGIN_NAME, conn.ID, cicdScope.Id, scope)
 }
 
 func TestRunPipeline(t *testing.T) {
        client := CreateClient(t)
        conn := CreateTestConnection(client)
-
-       CreateTestScope(client, conn.ID)
-
+       rule := CreateTestTransformationRule(client, conn.ID)
+       scope := CreateTestScope(client, rule, conn.ID)
        pipeline := client.RunPipeline(models.NewPipeline{
                Name: "remote_test",
                Plan: []plugin.PipelineStage{
@@ -110,13 +111,12 @@ func TestRunPipeline(t *testing.T) {
                                        Subtasks: nil,
                                        Options: map[string]interface{}{
                                                "connectionId": conn.ID,
-                                               "scopeId":      "p1",
+                                               "scopeId":      scope.Id,
                                        },
                                },
                        },
                },
        })
-
        require.Equal(t, models.TASK_COMPLETED, pipeline.Status)
        require.Equal(t, 1, pipeline.FinishedTasks)
        require.Equal(t, "", pipeline.ErrorName)
@@ -129,8 +129,8 @@ func TestBlueprintV200(t *testing.T) {
        client.CreateProject(&helper.ProjectConfig{
                ProjectName: projectName,
        })
-       CreateTestScope(client, connection.ID)
-
+       rule := CreateTestTransformationRule(client, connection.ID)
+       scope := CreateTestScope(client, rule, connection.ID)
        blueprint := client.CreateBasicBlueprintV2(
                "Test blueprint",
                &helper.BlueprintV2Config{
@@ -139,7 +139,7 @@ func TestBlueprintV200(t *testing.T) {
                                ConnectionId: connection.ID,
                                Scopes: []*plugin.BlueprintScopeV200{
                                        {
-                                               Id:   "p1",
+                                               Id:   scope.Id,
                                                Name: "Test scope",
                                                Entities: []string{
                                                        plugin.DOMAIN_TYPE_CICD,
@@ -158,8 +158,7 @@ func TestBlueprintV200(t *testing.T) {
 
        project := client.GetProject(projectName)
        require.Equal(t, blueprint.Name, project.Blueprint.Name)
-       pipeline := client.TriggerBlueprint(blueprint.ID)
-       require.Equal(t, pipeline.Status, models.TASK_COMPLETED)
+       client.TriggerBlueprint(blueprint.ID)
 }
 
 func TestCreateTxRule(t *testing.T) {
diff --git a/backend/test/helper/api.go b/backend/test/helper/api.go
index 9c8fb18c0..ed6d5c0e4 100644
--- a/backend/test/helper/api.go
+++ b/backend/test/helper/api.go
@@ -26,6 +26,8 @@ import (
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models"
        "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/server/api/blueprints"
        apiProject "github.com/apache/incubator-devlake/server/api/project"
        "github.com/stretchr/testify/require"
 )
@@ -88,6 +90,20 @@ func (d *DevlakeClient) CreateBasicBlueprintV2(name string, 
config *BlueprintV2C
        return blueprint
 }
 
+func (d *DevlakeClient) ListBlueprints() blueprints.PaginatedBlueprint {
+       return sendHttpRequest[blueprints.PaginatedBlueprint](d.testCtx, 
d.timeout, debugInfo{
+               print:      true,
+               inlineJson: false,
+       }, http.MethodGet, fmt.Sprintf("%s/blueprints", d.Endpoint), nil, nil)
+}
+
+func (d *DevlakeClient) GetBlueprint(blueprintId uint64) models.Blueprint {
+       return sendHttpRequest[models.Blueprint](d.testCtx, d.timeout, 
debugInfo{
+               print:      true,
+               inlineJson: false,
+       }, http.MethodGet, fmt.Sprintf("%s/blueprint/%d", d.Endpoint, 
blueprintId), nil, nil)
+}
+
 func (d *DevlakeClient) CreateProject(project *ProjectConfig) 
models.ApiOutputProject {
        var metrics []models.BaseMetric
        doraSeen := false
@@ -152,18 +168,30 @@ func (d *DevlakeClient) UpdateScope(pluginName string, 
connectionId uint64, scop
        }, http.MethodPatch, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scopes/%s", d.Endpoint, pluginName, 
connectionId, scopeId), nil, scope)
 }
 
-func (d *DevlakeClient) ListScopes(pluginName string, connectionId uint64) 
[]any {
-       return sendHttpRequest[[]any](d.testCtx, d.timeout, debugInfo{
+func (d *DevlakeClient) ListScopes(pluginName string, connectionId uint64, 
listBlueprints bool) []ScopeResponse {
+       scopesRaw := sendHttpRequest[[]map[string]any](d.testCtx, d.timeout, 
debugInfo{
                print:      true,
                inlineJson: false,
-       }, http.MethodGet, fmt.Sprintf("%s/plugins/%s/connections/%d/scopes", 
d.Endpoint, pluginName, connectionId), nil, nil)
+       }, http.MethodGet, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scopes?blueprints=%v", d.Endpoint, 
pluginName, connectionId, listBlueprints), nil, nil)
+       var responses []ScopeResponse
+       for _, scopeRaw := range scopesRaw {
+               responses = append(responses, getScopeResponse(scopeRaw))
+       }
+       return responses
 }
 
-func (d *DevlakeClient) GetScope(pluginName string, connectionId uint64, 
scopeId string) any {
-       return sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
+func (d *DevlakeClient) GetScope(pluginName string, connectionId uint64, 
scopeId string, listBlueprints bool) any {
+       return sendHttpRequest[api.ScopeRes[any]](d.testCtx, d.timeout, 
debugInfo{
+               print:      true,
+               inlineJson: false,
+       }, http.MethodGet, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scopes/%s?blueprints=%v", d.Endpoint, 
pluginName, connectionId, scopeId, listBlueprints), nil, nil)
+}
+
+func (d *DevlakeClient) DeleteScope(pluginName string, connectionId uint64, 
scopeId string, deleteDataOnly bool) []models.Blueprint {
+       return sendHttpRequest[[]models.Blueprint](d.testCtx, d.timeout, 
debugInfo{
                print:      true,
                inlineJson: false,
-       }, http.MethodGet, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scopes/%s", d.Endpoint, pluginName, 
connectionId, scopeId), nil, nil)
+       }, http.MethodDelete, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scopes/%s?delete_data_only=%v", 
d.Endpoint, pluginName, connectionId, scopeId, deleteDataOnly), nil, nil)
 }
 
 func (d *DevlakeClient) CreateTransformationRule(pluginName string, 
connectionId uint64, rules any) any {
@@ -316,3 +344,9 @@ func (d *DevlakeClient) monitorPipeline(id uint64) 
models.Pipeline {
        }))
        return pipelineResult
 }
+
+func getScopeResponse(scopeRaw map[string]any) ScopeResponse {
+       response := Cast[ScopeResponse](scopeRaw)
+       response.Scope = scopeRaw
+       return response
+}
diff --git a/backend/test/helper/json_helper.go 
b/backend/test/helper/json_helper.go
index 780cdf0aa..3a771230a 100644
--- a/backend/test/helper/json_helper.go
+++ b/backend/test/helper/json_helper.go
@@ -44,6 +44,18 @@ func ToMap(x any) map[string]any {
        return m
 }
 
+func FromMap[T any](x map[string]any) *T {
+       b, err := json.Marshal(x)
+       if err != nil {
+               panic(err)
+       }
+       t := new(T)
+       if err = json.Unmarshal(b, &t); err != nil {
+               panic(err)
+       }
+       return t
+}
+
 // ToCleanJson FIXME
 func ToCleanJson(inline bool, x any) json.RawMessage {
        j, err := json.Marshal(x)
diff --git a/backend/test/helper/models.go b/backend/test/helper/models.go
index 5c3541fa8..a362f4ce7 100644
--- a/backend/test/helper/models.go
+++ b/backend/test/helper/models.go
@@ -18,6 +18,7 @@ limitations under the License.
 package helper
 
 import (
+       "github.com/apache/incubator-devlake/core/models"
        "time"
 
        "github.com/apache/incubator-devlake/core/plugin"
@@ -35,6 +36,12 @@ type (
                EnableDora         bool
                MetricPlugins      []ProjectPlugin
        }
+
+       ScopeResponse struct {
+               Scope                  any
+               TransformationRuleName string
+               Blueprints             []*models.Blueprint
+       }
 )
 
 type Connection struct {

Reply via email to