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

hez 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 42b730a6d [feat-5503]: Deletable scopes and connections (#5506)
42b730a6d is described below

commit 42b730a6da302e18ae854282bfeef537a6fe186d
Author: Keon Amini <[email protected]>
AuthorDate: Fri Jun 16 18:20:46 2023 -0500

    [feat-5503]: Deletable scopes and connections (#5506)
    
    * feat: Scopes and Connection are only deleted if they are deletable
    
    * feat: auto-delete a project's blueprint when that project is deleted
    
    * test: Added integration test cases
    
    * fix: Allow clean scope data in spite of existing references to that scope
    
    * test: Added scope data integrity validations to tests
    fix: fixed python setting the wrong scope_id on CICDScope
    fix: scope_helper now excludes the scope model from data deletion
    
    * fix: remove double-delete of blueprint in delete-project
    
    * fix: minor fixes to get the tests passing
---
 backend/Makefile                                   |   8 +-
 backend/core/errors/types.go                       |  12 +-
 backend/core/plugin/plugin_api.go                  |   5 +
 .../helpers/pluginhelper/api/connection_helper.go  |  15 +-
 .../pluginhelper/api/scope_generic_helper.go       | 162 +++++++++++-------
 backend/helpers/pluginhelper/api/scope_helper.go   |   4 +-
 .../pluginhelper/services/blueprint_helper.go      |  36 ++++
 backend/plugins/ae/api/connection.go               |   8 +-
 backend/plugins/bamboo/api/connection.go           |   8 +-
 backend/plugins/bamboo/api/scope.go                |   1 +
 backend/plugins/bitbucket/api/connection.go        |   8 +-
 backend/plugins/bitbucket/api/scope.go             |   1 +
 backend/plugins/feishu/api/connection.go           |   8 +-
 backend/plugins/gitee/api/connection.go            |   8 +-
 backend/plugins/github/api/connection.go           |   8 +-
 backend/plugins/github/api/scope.go                |   1 +
 backend/plugins/gitlab/api/connection.go           |   8 +-
 backend/plugins/gitlab/api/scope.go                |   1 +
 backend/plugins/jenkins/api/connection.go          |   8 +-
 backend/plugins/jenkins/api/scope.go               |   1 +
 backend/plugins/jira/api/connection.go             |   8 +-
 backend/plugins/jira/api/scope.go                  |   1 +
 backend/plugins/pagerduty/api/connection.go        |   8 +-
 backend/plugins/pagerduty/api/scope.go             |   1 +
 backend/plugins/slack/api/connection.go            |   8 +-
 backend/plugins/sonarqube/api/connection.go        |   8 +-
 backend/plugins/sonarqube/api/scope.go             |   1 +
 backend/plugins/tapd/api/connection.go             |   8 +-
 backend/plugins/tapd/api/scope.go                  |   1 +
 backend/plugins/teambition/api/connection.go       |   8 +-
 backend/plugins/trello/api/connection.go           |   8 +-
 backend/plugins/trello/api/scope.go                |   1 +
 backend/plugins/webhook/api/connection.go          |   8 +-
 backend/plugins/zentao/api/connection.go           |   8 +-
 backend/plugins/zentao/api/scope.go                |   2 +
 backend/python/pydevlake/pydevlake/plugin.py       |   2 +-
 backend/server/api/router.go                       |  15 +-
 backend/server/services/project.go                 |  38 +++--
 .../services/remote/plugin/connection_api.go       |   5 +-
 .../server/services/remote/plugin/plugin_impl.go   |  10 +-
 backend/server/services/remote/plugin/scope_api.go |   4 +-
 backend/test/e2e/remote/helper.go                  |  63 ++++++-
 backend/test/e2e/remote/python_plugin_test.go      |  89 +++++++++-
 backend/test/helper/api.go                         | 187 ++++++++++++---------
 backend/test/helper/client.go                      | 101 ++++++++---
 backend/test/helper/client_factory.go              |  10 ++
 46 files changed, 685 insertions(+), 229 deletions(-)

diff --git a/backend/Makefile b/backend/Makefile
index 01d1f343d..b1bee77a7 100644
--- a/backend/Makefile
+++ b/backend/Makefile
@@ -110,16 +110,20 @@ e2e-plugins-test:
        done; \
        exit $$exit_code
 
-e2e-test:
+e2e-test-init:
        export ENV_PATH=$(shell readlink -f .env);\
        set -e;\
        go run ./test/init.go || exit $$?;\
+
+e2e-test-run:
        exit_code=0;\
-       for m in $$(go list ./test/e2e/... | grep -v manual/); do \
+       for m in $$(go list ./test/e2e/... | grep -v manual); do \
                echo $$m; go test -p 1 -timeout 300s -v $$m || exit_code=$$?; \
        done; \
        exit $$exit_code
 
+e2e-test: e2e-test-init e2e-test-run
+
 integration-test:
        export ENV_PATH=$(shell readlink -f .env);\
        set -e;\
diff --git a/backend/core/errors/types.go b/backend/core/errors/types.go
index 3b12e0f68..a4af60ef2 100644
--- a/backend/core/errors/types.go
+++ b/backend/core/errors/types.go
@@ -27,13 +27,17 @@ var (
        // Default special error type. If it's wrapping another error, then it 
will take the type of that error if it's an Error. Otherwise, it equates to 
Internal.
        Default = register(nil)
 
-       SubtaskErr   = register(&Type{meta: "subtask"})
-       NotFound     = register(&Type{httpCode: http.StatusNotFound, meta: 
"not-found"})
+       SubtaskErr = register(&Type{meta: "subtask"})
+       //400+
        BadInput     = register(&Type{httpCode: http.StatusBadRequest, meta: 
"bad-input"})
        Unauthorized = register(&Type{httpCode: http.StatusUnauthorized, meta: 
"unauthorized"})
        Forbidden    = register(&Type{httpCode: http.StatusForbidden, meta: 
"forbidden"})
-       Internal     = register(&Type{httpCode: http.StatusInternalServerError, 
meta: "internal"})
-       Timeout      = register(&Type{httpCode: http.StatusGatewayTimeout, 
meta: "timeout"})
+       NotFound     = register(&Type{httpCode: http.StatusNotFound, meta: 
"not-found"})
+       Conflict     = register(&Type{httpCode: http.StatusConflict, meta: 
"internal"})
+
+       //500+
+       Internal = register(&Type{httpCode: http.StatusInternalServerError, 
meta: "internal"})
+       Timeout  = register(&Type{httpCode: http.StatusGatewayTimeout, meta: 
"timeout"})
 
        //cached values
        typesByHttpCode = newSyncMap[int, *Type]()
diff --git a/backend/core/plugin/plugin_api.go 
b/backend/core/plugin/plugin_api.go
index 50b19072b..9ee36c2ff 100644
--- a/backend/core/plugin/plugin_api.go
+++ b/backend/core/plugin/plugin_api.go
@@ -31,6 +31,11 @@ type ApiResourceInput struct {
        Request *http.Request
 }
 
+// GetPlugin get the plugin in context
+func (input *ApiResourceInput) GetPlugin() string {
+       return input.Params["plugin"]
+}
+
 // OutputFile is the file returned
 type OutputFile struct {
        ContentType string
diff --git a/backend/helpers/pluginhelper/api/connection_helper.go 
b/backend/helpers/pluginhelper/api/connection_helper.go
index b49f59719..fd2d4c205 100644
--- a/backend/helpers/pluginhelper/api/connection_helper.go
+++ b/backend/helpers/pluginhelper/api/connection_helper.go
@@ -18,6 +18,7 @@ limitations under the License.
 package api
 
 import (
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "strconv"
 
        "github.com/apache/incubator-devlake/core/context"
@@ -35,6 +36,7 @@ type ConnectionApiHelper struct {
        log              log.Logger
        db               dal.Dal
        validator        *validator.Validate
+       bpManager        *services.BlueprintManager
 }
 
 // NewConnectionHelper creates a ConnectionHelper for connection management
@@ -50,6 +52,7 @@ func NewConnectionHelper(
                log:              basicRes.GetLogger(),
                db:               basicRes.GetDal(),
                validator:        vld,
+               bpManager:        
services.NewBlueprintManager(basicRes.GetDal()),
        }
 }
 
@@ -101,8 +104,16 @@ func (c *ConnectionApiHelper) List(connections 
interface{}) errors.Error {
 }
 
 // Delete connection
-func (c *ConnectionApiHelper) Delete(connection interface{}) errors.Error {
-       return CallDB(c.db.Delete, connection)
+func (c *ConnectionApiHelper) Delete(plugin string, connection interface{}) 
(*services.BlueprintProjectPairs, errors.Error) {
+       connectionId := reflectField(connection, "ID").Uint()
+       referencingBps, err := c.bpManager.GetBlueprintsByConnection(plugin, 
connectionId)
+       if err != nil {
+               return nil, err
+       }
+       if len(referencingBps) > 0 {
+               return services.NewBlueprintProjectPairs(referencingBps), 
errors.Conflict.New("Found one or more references to this connection")
+       }
+       return nil, CallDB(c.db.Delete, connection)
 }
 
 func (c *ConnectionApiHelper) merge(connection interface{}, body 
map[string]interface{}) errors.Error {
diff --git a/backend/helpers/pluginhelper/api/scope_generic_helper.go 
b/backend/helpers/pluginhelper/api/scope_generic_helper.go
index 904a72d55..fb0d2dc18 100644
--- a/backend/helpers/pluginhelper/api/scope_generic_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_generic_helper.go
@@ -46,12 +46,12 @@ var (
 type NoScopeConfig struct{}
 
 type (
-       GenericScopeApiHelper[Conn any, Scope any, Tr any] struct {
+       GenericScopeApiHelper[Conn any, Scope any, ScopeConfig any] struct {
                log              log.Logger
                db               dal.Dal
                validator        *validator.Validate
                reflectionParams *ReflectionParameters
-               dbHelper         ScopeDatabaseHelper[Conn, Scope, Tr]
+               dbHelper         ScopeDatabaseHelper[Conn, Scope, ScopeConfig]
                bpManager        *serviceHelper.BlueprintManager
                connHelper       *ConnectionApiHelper
                opts             *ScopeHelperOptions
@@ -63,6 +63,8 @@ type (
                ScopeConfig *ScopeConfig        
`mapstructure:"scopeConfig,omitempty" json:"scopeConfig"`
                Blueprints  []*models.Blueprint 
`mapstructure:"blueprints,omitempty" json:"blueprints"`
        }
+       // Alias, for swagger purposes
+       ScopeRefDoc                          = 
serviceHelper.BlueprintProjectPairs
        ScopeRes[Scope any, ScopeConfig any] struct {
                Scope                    *Scope                   
`mapstructure:",squash"` // ideally we need this field to be embedded in the 
struct
                ScopeResDoc[ScopeConfig] `mapstructure:",squash"` // however, 
only this type of embeding is supported as of golang 1.20
@@ -99,14 +101,14 @@ type (
        }
 )
 
-func NewGenericScopeHelper[Conn any, Scope any, Tr any](
+func NewGenericScopeHelper[Conn any, Scope any, ScopeConfig any](
        basicRes context.BasicRes,
        vld *validator.Validate,
        connHelper *ConnectionApiHelper,
-       dbHelper ScopeDatabaseHelper[Conn, Scope, Tr],
+       dbHelper ScopeDatabaseHelper[Conn, Scope, ScopeConfig],
        params *ReflectionParameters,
        opts *ScopeHelperOptions,
-) *GenericScopeApiHelper[Conn, Scope, Tr] {
+) *GenericScopeApiHelper[Conn, Scope, ScopeConfig] {
        if connHelper == nil {
                panic("nil connHelper")
        }
@@ -120,14 +122,7 @@ func NewGenericScopeHelper[Conn any, Scope any, Tr any](
        if opts == nil {
                opts = &ScopeHelperOptions{}
        }
-       tablesCacheLoader.Do(func() {
-               var err errors.Error
-               tablesCache, err = basicRes.GetDal().AllTables()
-               if err != nil {
-                       panic(err)
-               }
-       })
-       return &GenericScopeApiHelper[Conn, Scope, Tr]{
+       return &GenericScopeApiHelper[Conn, Scope, ScopeConfig]{
                log:              basicRes.GetLogger(),
                db:               basicRes.GetDal(),
                validator:        vld,
@@ -139,11 +134,11 @@ func NewGenericScopeHelper[Conn any, Scope any, Tr any](
        }
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) DbHelper() 
ScopeDatabaseHelper[Conn, Scope, Tr] {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) DbHelper() 
ScopeDatabaseHelper[Conn, Scope, ScopeConfig] {
        return gs.dbHelper
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) PutScopes(input 
*plugin.ApiResourceInput, scopes []*Scope) ([]*ScopeRes[Scope, Tr], 
errors.Error) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) PutScopes(input 
*plugin.ApiResourceInput, scopes []*Scope) ([]*ScopeRes[Scope, ScopeConfig], 
errors.Error) {
        params, err := gs.extractFromReqParam(input, false)
        if err != nil {
                return nil, err
@@ -179,7 +174,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
PutScopes(input *plugin.ApiRes
        return apiScopes, nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) UpdateScope(input 
*plugin.ApiResourceInput) (*ScopeRes[Scope, Tr], errors.Error) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) UpdateScope(input 
*plugin.ApiResourceInput) (*ScopeRes[Scope, ScopeConfig], errors.Error) {
        params, err := gs.extractFromReqParam(input, true)
        if err != nil {
                return nil, err
@@ -211,7 +206,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
UpdateScope(input *plugin.ApiR
        return scopeRes[0], nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) GetScopes(input 
*plugin.ApiResourceInput) ([]*ScopeRes[Scope, Tr], errors.Error) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) GetScopes(input 
*plugin.ApiResourceInput) ([]*ScopeRes[Scope, ScopeConfig], errors.Error) {
        params, err := gs.extractFromGetReqParam(input, false)
        if err != nil {
                return nil, errors.BadInput.New("invalid path params: 
\"connectionId\" not set")
@@ -258,7 +253,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
GetScopes(input *plugin.ApiRes
        return apiScopes, nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) GetScope(input 
*plugin.ApiResourceInput) (*ScopeRes[Scope, Tr], errors.Error) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) GetScope(input 
*plugin.ApiResourceInput) (*ScopeRes[Scope, ScopeConfig], errors.Error) {
        params, err := gs.extractFromGetReqParam(input, true)
        if err != nil {
                return nil, err
@@ -288,49 +283,45 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
GetScope(input *plugin.ApiReso
        return scopeRes, nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) DeleteScope(input 
*plugin.ApiResourceInput) errors.Error {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) DeleteScope(input 
*plugin.ApiResourceInput) (*serviceHelper.BlueprintProjectPairs, errors.Error) {
        params, err := gs.extractFromDeleteReqParam(input)
        if err != nil {
-               return err
+               return nil, err
        }
        err = gs.dbHelper.VerifyConnection(params.connectionId)
        if err != nil {
-               return errors.Default.Wrap(err, fmt.Sprintf("error verifying 
connection for connection ID %d", params.connectionId))
+               return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
verifying connection for connection ID %d", params.connectionId))
        }
-       scopeParamValue := params.scopeId
-       if gs.opts.GetScopeParamValue != nil {
-               scopeParamValue, err = gs.opts.GetScopeParamValue(gs.db, 
params.scopeId)
+       if refs, err := gs.getScopeReferences(input.GetPlugin(), 
params.connectionId, params.scopeId); err != nil || refs != nil {
                if err != nil {
-                       return errors.Default.Wrap(err, fmt.Sprintf("error 
extracting scope parameter name for scope %s", params.scopeId))
+                       return nil, err
                }
+               if err = gs.deleteScopeData(params.plugin, params.scopeId); err 
!= nil {
+                       return nil, err
+               }
+               return refs, errors.Conflict.New("Found one or more references 
to this scope")
        }
-       // find all tables for this plugin
-       tables, err := gs.getAffectedTables(params.plugin)
-       if err != nil {
-               return errors.Default.Wrap(err, fmt.Sprintf("error getting 
database tables managed by plugin %s", params.plugin))
-       }
-       err = gs.transactionalDelete(tables, scopeParamValue)
-       if err != nil {
-               return errors.Default.Wrap(err, fmt.Sprintf("error deleting 
data bound to scope %s for plugin %s", params.scopeId, params.plugin))
+       if err = gs.deleteScopeData(params.plugin, params.scopeId); err != nil {
+               return nil, err
        }
        if !params.deleteDataOnly {
                // Delete the scope itself
                err = gs.dbHelper.DeleteScope(params.connectionId, 
params.scopeId)
                if err != nil {
-                       return errors.Default.Wrap(err, fmt.Sprintf("error 
deleting scope %s", params.scopeId))
+                       return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
deleting scope %s", params.scopeId))
                }
                err = gs.updateBlueprints(params.connectionId, params.plugin, 
params.scopeId)
                if err != nil {
-                       return err
+                       return nil, err
                }
        }
-       return nil
+       return nil, nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) addScopeConfig(scopes 
...*Scope) ([]*ScopeRes[Scope, Tr], errors.Error) {
-       apiScopes := make([]*ScopeRes[Scope, Tr], len(scopes))
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
addScopeConfig(scopes ...*Scope) ([]*ScopeRes[Scope, ScopeConfig], 
errors.Error) {
+       apiScopes := make([]*ScopeRes[Scope, ScopeConfig], len(scopes))
        for i, scope := range scopes {
-               apiScopes[i] = &ScopeRes[Scope, Tr]{
+               apiScopes[i] = &ScopeRes[Scope, ScopeConfig]{
                        Scope: scope,
                }
                scIdField := reflectField(scope, "ScopeConfigId")
@@ -345,8 +336,20 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
addScopeConfig(scopes ...*Scop
        return apiScopes, nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) mapByScopeId(scopes 
[]*ScopeRes[Scope, Tr]) map[string]*ScopeRes[Scope, Tr] {
-       scopeMap := map[string]*ScopeRes[Scope, Tr]{}
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
getScopeReferences(pluginName string, connectionId uint64, scopeId string) 
(*serviceHelper.BlueprintProjectPairs, errors.Error) {
+       blueprintMap, err := gs.bpManager.GetBlueprintsByScopes(connectionId, 
pluginName, scopeId)
+       if err != nil {
+               return nil, err
+       }
+       blueprints := blueprintMap[scopeId]
+       if len(blueprints) == 0 {
+               return nil, nil
+       }
+       return serviceHelper.NewBlueprintProjectPairs(blueprints), nil
+}
+
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) mapByScopeId(scopes 
[]*ScopeRes[Scope, ScopeConfig]) map[string]*ScopeRes[Scope, ScopeConfig] {
+       scopeMap := map[string]*ScopeRes[Scope, ScopeConfig]{}
        for _, scope := range scopes {
                scopeId := fmt.Sprintf("%v", reflectField(scope.Scope, 
gs.reflectionParams.ScopeIdFieldName).Interface())
                scopeMap[scopeId] = scope
@@ -354,7 +357,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
mapByScopeId(scopes []*ScopeRe
        return scopeMap
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) extractFromReqParam(input 
*plugin.ApiResourceInput, withScopeId bool) (*requestParams, errors.Error) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
extractFromReqParam(input *plugin.ApiResourceInput, withScopeId bool) 
(*requestParams, errors.Error) {
        connectionId, err := strconv.ParseUint(input.Params["connectionId"], 
10, 64)
        if err != nil {
                return nil, errors.BadInput.Wrap(err, "Invalid 
\"connectionId\"")
@@ -370,7 +373,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
extractFromReqParam(input *plu
                        scopeId = scopeId[1:]
                }
        }
-       pluginName := input.Params["plugin"]
+       pluginName := input.GetPlugin()
        return &requestParams{
                connectionId: connectionId,
                plugin:       pluginName,
@@ -378,7 +381,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
extractFromReqParam(input *plu
        }, nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
extractFromDeleteReqParam(input *plugin.ApiResourceInput) 
(*deleteRequestParams, errors.Error) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
extractFromDeleteReqParam(input *plugin.ApiResourceInput) 
(*deleteRequestParams, errors.Error) {
        params, err := gs.extractFromReqParam(input, true)
        if err != nil {
                return nil, err
@@ -399,7 +402,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
extractFromDeleteReqParam(inpu
        }, nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) extractFromGetReqParam(input 
*plugin.ApiResourceInput, withScopeId bool) (*getRequestParams, errors.Error) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
extractFromGetReqParam(input *plugin.ApiResourceInput, withScopeId bool) 
(*getRequestParams, errors.Error) {
        params, err := gs.extractFromReqParam(input, withScopeId)
        if err != nil {
                return nil, err
@@ -420,7 +423,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
extractFromGetReqParam(input *
        }, nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) getRawParams(connectionId 
uint64, scopeId any) string {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
getRawParams(connectionId uint64, scopeId any) string {
        paramsMap := map[string]any{
                "ConnectionId":                        connectionId,
                gs.reflectionParams.RawScopeParamName: scopeId,
@@ -432,7 +435,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
getRawParams(connectionId uint
        return string(b)
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) setScopeFields(p 
interface{}, connectionId uint64, createdDate *time.Time, updatedDate 
*time.Time) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) setScopeFields(p 
interface{}, connectionId uint64, createdDate *time.Time, updatedDate 
*time.Time) {
        pType := reflect.TypeOf(p)
        if pType.Kind() != reflect.Ptr {
                panic("expected a pointer to a struct")
@@ -500,7 +503,7 @@ func returnPrimaryKeyValue(p interface{}) string {
        return result
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) verifyScope(scope 
interface{}, vld *validator.Validate) errors.Error {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) verifyScope(scope 
interface{}, vld *validator.Validate) errors.Error {
        if gs.opts.IsRemote {
                return nil
        }
@@ -514,7 +517,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
verifyScope(scope interface{},
        return nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) validatePrimaryKeys(scopes 
[]*Scope) errors.Error {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
validatePrimaryKeys(scopes []*Scope) errors.Error {
        if gs.opts.IsRemote {
                return nil
        }
@@ -531,7 +534,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
validatePrimaryKeys(scopes []*
        return nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
updateBlueprints(connectionId uint64, pluginName string, scopeId string) 
errors.Error {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
updateBlueprints(connectionId uint64, pluginName string, scopeId string) 
errors.Error {
        blueprintsMap, err := gs.bpManager.GetBlueprintsByScopes(connectionId, 
pluginName, scopeId)
        if err != nil {
                return errors.Default.Wrap(err, fmt.Sprintf("error retrieving 
scope with scope ID %s", scopeId))
@@ -570,7 +573,28 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
updateBlueprints(connectionId
        return nil
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) transactionalDelete(tables 
[]string, scopeId string) errors.Error {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
deleteScopeData(plugin string, scopeId string) errors.Error {
+       var err errors.Error
+       scopeParamValue := scopeId
+       if gs.opts.GetScopeParamValue != nil {
+               scopeParamValue, err = gs.opts.GetScopeParamValue(gs.db, 
scopeId)
+               if err != nil {
+                       return errors.Default.Wrap(err, fmt.Sprintf("error 
extracting scope parameter name for scope %s", scopeId))
+               }
+       }
+       // find all tables for this plugin
+       tables, err := gs.getAffectedTables(plugin)
+       if err != nil {
+               return errors.Default.Wrap(err, fmt.Sprintf("error getting 
database tables managed by plugin %s", plugin))
+       }
+       err = gs.transactionalDelete(tables, scopeParamValue)
+       if err != nil {
+               return errors.Default.Wrap(err, fmt.Sprintf("error deleting 
data bound to scope %s for plugin %s", scopeId, plugin))
+       }
+       return nil
+}
+
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
transactionalDelete(tables []string, scopeId string) errors.Error {
        tx := gs.db.Begin()
        for _, table := range tables {
                query := createDeleteQuery(table, 
gs.reflectionParams.RawScopeParamName, scopeId)
@@ -617,7 +641,15 @@ func createDeleteQuery(tableName string, scopeIdKey 
string, scopeId string) stri
        return query
 }
 
-func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) getAffectedTables(pluginName 
string) ([]string, errors.Error) {
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) lazyCacheTables() 
errors.Error {
+       var err errors.Error
+       tablesCacheLoader.Do(func() {
+               tablesCache, err = gs.db.AllTables()
+       })
+       return err
+}
+
+func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
getAffectedTables(pluginName string) ([]string, errors.Error) {
        var tables []string
        meta, err := plugin.GetPlugin(pluginName)
        if err != nil {
@@ -626,6 +658,9 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
getAffectedTables(pluginName s
        if pluginModel, ok := meta.(plugin.PluginModel); !ok {
                return nil, errors.Default.New(fmt.Sprintf("plugin \"%s\" does 
not implement listing its tables", pluginName))
        } else {
+               if err = gs.lazyCacheTables(); err != nil {
+                       return nil, err
+               }
                // collect raw tables
                for _, table := range tablesCache {
                        if strings.HasPrefix(table, "_raw_"+pluginName) {
@@ -633,20 +668,18 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
getAffectedTables(pluginName s
                        }
                }
                // 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())
+               toolModels := pluginModel.GetTablesInfo()
+               for _, toolModel := range toolModels {
+                       if !isScopeModel(toolModel) && hasField(toolModel, 
"RawDataParams") {
+                               tables = append(tables, toolModel.TableName())
                        }
                }
                // collect domain tables
-               for _, domainTable := range domaininfo.GetDomainTablesInfo() {
+               for _, domainModel := range domaininfo.GetDomainTablesInfo() {
                        // we only care about tables with RawOrigin
-                       ok = hasField(domainTable, "RawDataParams")
+                       ok = hasField(domainModel, "RawDataParams")
                        if ok {
-                               tables = append(tables, domainTable.TableName())
+                               tables = append(tables, domainModel.TableName())
                        }
                }
                // additional tables
@@ -655,3 +688,10 @@ func (gs *GenericScopeApiHelper[Conn, Scope, Tr]) 
getAffectedTables(pluginName s
        gs.log.Debug("Discovered %d tables used by plugin \"%s\": %v", 
len(tables), pluginName, tables)
        return tables, nil
 }
+
+func isScopeModel(obj dal.Tabler) bool {
+       if _, ok := obj.(plugin.ToolLayerScope); ok {
+               return true
+       }
+       return reflectField(obj, "ScopeConfigId").IsValid()
+}
diff --git a/backend/helpers/pluginhelper/api/scope_helper.go 
b/backend/helpers/pluginhelper/api/scope_helper.go
index c26c661df..91d8fdd0d 100644
--- a/backend/helpers/pluginhelper/api/scope_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_helper.go
@@ -95,9 +95,9 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScope(input 
*plugin.ApiResourceInpu
 }
 
 func (c *ScopeApiHelper[Conn, Scope, Tr]) Delete(input 
*plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-       err := c.DeleteScope(input)
+       refs, err := c.DeleteScope(input)
        if err != nil {
-               return nil, err
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, nil
        }
        return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
 }
diff --git a/backend/helpers/pluginhelper/services/blueprint_helper.go 
b/backend/helpers/pluginhelper/services/blueprint_helper.go
index d88a98ce8..6282c21fe 100644
--- a/backend/helpers/pluginhelper/services/blueprint_helper.go
+++ b/backend/helpers/pluginhelper/services/blueprint_helper.go
@@ -38,12 +38,26 @@ type GetBlueprintQuery struct {
        PageSize    int
 }
 
+type BlueprintProjectPairs struct {
+       Projects   []string `json:"projects"`
+       Blueprints []string `json:"blueprints"`
+}
+
 func NewBlueprintManager(db dal.Dal) *BlueprintManager {
        return &BlueprintManager{
                db: db,
        }
 }
 
+func NewBlueprintProjectPairs(bps []*models.Blueprint) *BlueprintProjectPairs {
+       pairs := &BlueprintProjectPairs{}
+       for _, bp := range bps {
+               pairs.Blueprints = append(pairs.Blueprints, bp.Name)
+               pairs.Projects = append(pairs.Projects, bp.ProjectName)
+       }
+       return pairs
+}
+
 // SaveDbBlueprint accepts a Blueprint instance and upsert it to database
 func (b *BlueprintManager) SaveDbBlueprint(blueprint *models.Blueprint) 
errors.Error {
        var err error
@@ -160,6 +174,28 @@ func (b *BlueprintManager) 
GetBlueprintsByScopes(connectionId uint64, pluginName
        return scopeMap, nil
 }
 
+// GetBlueprintsByScopes returns all blueprints that have these scopeIds and 
this connection Id
+func (b *BlueprintManager) GetBlueprintsByConnection(plugin string, 
connectionId uint64) ([]*models.Blueprint, errors.Error) {
+       bps, _, err := b.GetDbBlueprints(&GetBlueprintQuery{})
+       if err != nil {
+               return nil, err
+       }
+       var filteredBps []*models.Blueprint
+       for _, bp := range bps {
+               connections, err := bp.GetConnections()
+               if err != nil {
+                       return nil, err
+               }
+               for _, connection := range connections {
+                       if connection.ConnectionId == connectionId && 
connection.Plugin == plugin {
+                               filteredBps = append(filteredBps, bp)
+                               break
+                       }
+               }
+       }
+       return filteredBps, nil
+}
+
 // GetDbBlueprintByProjectName returns the detail of a given projectName
 func (b *BlueprintManager) GetDbBlueprintByProjectName(projectName string) 
(*models.Blueprint, errors.Error) {
        dbBlueprint := &models.Blueprint{}
diff --git a/backend/plugins/ae/api/connection.go 
b/backend/plugins/ae/api/connection.go
index 7cc80df0f..00a9d3c99 100644
--- a/backend/plugins/ae/api/connection.go
+++ b/backend/plugins/ae/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/core/errors"
@@ -134,6 +135,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/ae
 // @Success 200 {object} models.AeConnection "Success"
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/ae/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -142,6 +144,10 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
diff --git a/backend/plugins/bamboo/api/connection.go 
b/backend/plugins/bamboo/api/connection.go
index ca627af97..7c1995ac8 100644
--- a/backend/plugins/bamboo/api/connection.go
+++ b/backend/plugins/bamboo/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/core/errors"
@@ -107,6 +108,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/bamboo
 // @Success 200  {object} models.BambooConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internel Error"
 // @Router /plugins/bamboo/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -115,7 +117,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/bamboo/api/scope.go 
b/backend/plugins/bamboo/api/scope.go
index 0989000b7..62172865c 100644
--- a/backend/plugins/bamboo/api/scope.go
+++ b/backend/plugins/bamboo/api/scope.go
@@ -101,6 +101,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bamboo/connections/{connectionId}/scopes/{scopeId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/bitbucket/api/connection.go 
b/backend/plugins/bitbucket/api/connection.go
index 64e5d7c31..0f19f90f5 100644
--- a/backend/plugins/bitbucket/api/connection.go
+++ b/backend/plugins/bitbucket/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/server/api/shared"
@@ -114,6 +115,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/bitbucket
 // @Success 200  {object} models.BitbucketConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -122,7 +124,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/bitbucket/api/scope.go 
b/backend/plugins/bitbucket/api/scope.go
index a22adbfe0..1639fa96a 100644
--- a/backend/plugins/bitbucket/api/scope.go
+++ b/backend/plugins/bitbucket/api/scope.go
@@ -105,6 +105,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId}/scopes/{scopeId} 
[DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/feishu/api/connection.go 
b/backend/plugins/feishu/api/connection.go
index bcf48f3c3..a570f6208 100644
--- a/backend/plugins/feishu/api/connection.go
+++ b/backend/plugins/feishu/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "github.com/apache/incubator-devlake/server/api/shared"
        "net/http"
 
@@ -100,6 +101,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/feishu
 // @Success 200  {object} models.FeishuConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/feishu/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -108,7 +110,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/gitee/api/connection.go 
b/backend/plugins/gitee/api/connection.go
index 8fac8a660..0bb87fa59 100644
--- a/backend/plugins/gitee/api/connection.go
+++ b/backend/plugins/gitee/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/server/api/shared"
@@ -117,6 +118,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/gitee
 // @Success 200  {object} models.GiteeConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/gitee/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -125,7 +127,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/github/api/connection.go 
b/backend/plugins/github/api/connection.go
index f2e240267..72894c500 100644
--- a/backend/plugins/github/api/connection.go
+++ b/backend/plugins/github/api/connection.go
@@ -20,6 +20,7 @@ package api
 import (
        "context"
        "fmt"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
        "strings"
 
@@ -242,6 +243,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/github
 // @Success 200  {object} models.GithubConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/github/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -250,7 +252,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/github/api/scope.go 
b/backend/plugins/github/api/scope.go
index 1f0102c37..8ca575a11 100644
--- a/backend/plugins/github/api/scope.go
+++ b/backend/plugins/github/api/scope.go
@@ -101,6 +101,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/github/connections/{connectionId}/scopes/{scopeId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/gitlab/api/connection.go 
b/backend/plugins/gitlab/api/connection.go
index d23d9dc3e..df6bece61 100644
--- a/backend/plugins/gitlab/api/connection.go
+++ b/backend/plugins/gitlab/api/connection.go
@@ -20,6 +20,7 @@ package api
 import (
        "context"
        "fmt"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
        "net/url"
 
@@ -121,6 +122,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/gitlab
 // @Success 200  {object} models.GitlabConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/gitlab/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -129,7 +131,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/gitlab/api/scope.go 
b/backend/plugins/gitlab/api/scope.go
index 73dc5da45..705ca6c2e 100644
--- a/backend/plugins/gitlab/api/scope.go
+++ b/backend/plugins/gitlab/api/scope.go
@@ -101,6 +101,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/gitlab/connections/{connectionId}/scopes/{scopeId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/jenkins/api/connection.go 
b/backend/plugins/jenkins/api/connection.go
index 9ade3fa6b..634063497 100644
--- a/backend/plugins/jenkins/api/connection.go
+++ b/backend/plugins/jenkins/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
        "strings"
 
@@ -123,6 +124,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/jenkins
 // @Success 200  {object} models.JenkinsConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/jenkins/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -131,7 +133,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/jenkins/api/scope.go 
b/backend/plugins/jenkins/api/scope.go
index 3bef31f9a..f1165bb1e 100644
--- a/backend/plugins/jenkins/api/scope.go
+++ b/backend/plugins/jenkins/api/scope.go
@@ -105,6 +105,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jenkins/connections/{connectionId}/scopes/{scopeId} 
[DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/jira/api/connection.go 
b/backend/plugins/jira/api/connection.go
index c0513685f..419af61c5 100644
--- a/backend/plugins/jira/api/connection.go
+++ b/backend/plugins/jira/api/connection.go
@@ -20,6 +20,7 @@ package api
 import (
        "context"
        "fmt"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
        "net/url"
        "strings"
@@ -168,6 +169,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/jira
 // @Success 200  {object} models.JiraConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/jira/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -176,7 +178,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/jira/api/scope.go 
b/backend/plugins/jira/api/scope.go
index 93feb8034..b76893bfa 100644
--- a/backend/plugins/jira/api/scope.go
+++ b/backend/plugins/jira/api/scope.go
@@ -109,6 +109,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jira/connections/{connectionId}/scopes/{scopeId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/pagerduty/api/connection.go 
b/backend/plugins/pagerduty/api/connection.go
index 932e585e9..040a0ea1f 100644
--- a/backend/plugins/pagerduty/api/connection.go
+++ b/backend/plugins/pagerduty/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/core/errors"
@@ -99,6 +100,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/pagerduty
 // @Success 200  {object} models.PagerDutyConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -107,7 +109,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/pagerduty/api/scope.go 
b/backend/plugins/pagerduty/api/scope.go
index b86e55b72..871de3d65 100644
--- a/backend/plugins/pagerduty/api/scope.go
+++ b/backend/plugins/pagerduty/api/scope.go
@@ -102,6 +102,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/scopes/{serviceId} 
[DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/slack/api/connection.go 
b/backend/plugins/slack/api/connection.go
index b1bf7ccd3..303c74fb4 100644
--- a/backend/plugins/slack/api/connection.go
+++ b/backend/plugins/slack/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "github.com/apache/incubator-devlake/server/api/shared"
        "net/http"
 
@@ -100,6 +101,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/slack
 // @Success 200  {object} models.SlackConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/slack/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -108,7 +110,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/sonarqube/api/connection.go 
b/backend/plugins/sonarqube/api/connection.go
index 9dc000e61..3a1bb781e 100644
--- a/backend/plugins/sonarqube/api/connection.go
+++ b/backend/plugins/sonarqube/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/core/errors"
@@ -128,6 +129,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Param connectionId path int false "connection ID"
 // @Success 200  {object} models.SonarqubeConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -136,7 +138,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/sonarqube/api/scope.go 
b/backend/plugins/sonarqube/api/scope.go
index 880f3bf46..e31f36ddd 100644
--- a/backend/plugins/sonarqube/api/scope.go
+++ b/backend/plugins/sonarqube/api/scope.go
@@ -100,6 +100,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} 
[DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/tapd/api/connection.go 
b/backend/plugins/tapd/api/connection.go
index 3bca819bc..7545709b3 100644
--- a/backend/plugins/tapd/api/connection.go
+++ b/backend/plugins/tapd/api/connection.go
@@ -20,6 +20,7 @@ package api
 import (
        "context"
        "fmt"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/server/api/shared"
@@ -119,6 +120,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/tapd
 // @Success 200  {object} models.TapdConnection "Success"
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/tapd/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -127,7 +129,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/tapd/api/scope.go 
b/backend/plugins/tapd/api/scope.go
index 04d8b5f60..54da0d3f8 100644
--- a/backend/plugins/tapd/api/scope.go
+++ b/backend/plugins/tapd/api/scope.go
@@ -104,6 +104,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/tapd/connections/{connectionId}/scopes/{scopeId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/teambition/api/connection.go 
b/backend/plugins/teambition/api/connection.go
index a163848d2..009124591 100644
--- a/backend/plugins/teambition/api/connection.go
+++ b/backend/plugins/teambition/api/connection.go
@@ -20,6 +20,7 @@ package api
 import (
        "context"
        "fmt"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/core/errors"
@@ -128,6 +129,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/teambition
 // @Success 200  {object} models.TeambitionConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/teambition/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -136,7 +138,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/trello/api/connection.go 
b/backend/plugins/trello/api/connection.go
index be7378f7d..d10a900b5 100644
--- a/backend/plugins/trello/api/connection.go
+++ b/backend/plugins/trello/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/server/api/shared"
@@ -114,6 +115,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/trello
 // @Success 200  {object} models.TrelloConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/trello/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -122,7 +124,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/trello/api/scope.go 
b/backend/plugins/trello/api/scope.go
index 5b2100724..94aa605de 100644
--- a/backend/plugins/trello/api/scope.go
+++ b/backend/plugins/trello/api/scope.go
@@ -99,6 +99,7 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/trello/connections/{connectionId}/scopes/{scopeId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
diff --git a/backend/plugins/webhook/api/connection.go 
b/backend/plugins/webhook/api/connection.go
index 6698c184b..fea459af8 100644
--- a/backend/plugins/webhook/api/connection.go
+++ b/backend/plugins/webhook/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "fmt"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/core/errors"
@@ -69,6 +70,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/webhook
 // @Success 200  {object} models.WebhookConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/webhook/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -77,7 +79,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/zentao/api/connection.go 
b/backend/plugins/zentao/api/connection.go
index ccf5cf2de..429cd4587 100644
--- a/backend/plugins/zentao/api/connection.go
+++ b/backend/plugins/zentao/api/connection.go
@@ -19,6 +19,7 @@ package api
 
 import (
        "context"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
 
        "github.com/apache/incubator-devlake/server/api/shared"
@@ -103,6 +104,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Tags plugins/zentao
 // @Success 200  {object} models.ZentaoConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to 
this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -111,7 +113,11 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
        if err != nil {
                return nil, err
        }
-       err = connectionHelper.Delete(connection)
+       var refs *services.BlueprintProjectPairs
+       refs, err = connectionHelper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        return &plugin.ApiResourceOutput{Body: connection}, err
 }
 
diff --git a/backend/plugins/zentao/api/scope.go 
b/backend/plugins/zentao/api/scope.go
index 8772c52c9..2a9731f89 100644
--- a/backend/plugins/zentao/api/scope.go
+++ b/backend/plugins/zentao/api/scope.go
@@ -139,6 +139,7 @@ func GetProjectScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId}/scopes/product/{scopeId} 
[DELETE]
 func DeleteProductScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
@@ -154,6 +155,7 @@ func DeleteProductScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutp
 // @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId}/scopes/project/{scopeId} 
[DELETE]
 func DeleteProjectScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
diff --git a/backend/python/pydevlake/pydevlake/plugin.py 
b/backend/python/pydevlake/pydevlake/plugin.py
index e6cbe8e96..2a9461cf0 100644
--- a/backend/python/pydevlake/pydevlake/plugin.py
+++ b/backend/python/pydevlake/pydevlake/plugin.py
@@ -139,7 +139,7 @@ class Plugin(ABC):
         for tool_scope, _ in scope_config_pairs:
             for scope in self.domain_scopes(tool_scope):
                 scope.id = tool_scope.domain_id()
-                scope.raw_data_params = raw_data_params(connection.id, 
scope.id)
+                scope.raw_data_params = raw_data_params(connection.id, 
tool_scope.id)
                 domain_scopes.append(
                     msg.DynamicDomainScope(
                         type_name=type(scope).__name__,
diff --git a/backend/server/api/router.go b/backend/server/api/router.go
index 55ccba612..5acdd8d57 100644
--- a/backend/server/api/router.go
+++ b/backend/server/api/router.go
@@ -19,6 +19,8 @@ package api
 
 import (
        "fmt"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/impls/logruslog"
        "net/http"
        "strings"
 
@@ -96,7 +98,7 @@ func registerPluginEndpoints(r *gin.Engine, pluginName 
string, apiResources map[
 
 func handlePluginCall(pluginName string, handler plugin.ApiResourceHandler) 
func(c *gin.Context) {
        return func(c *gin.Context) {
-               var err error
+               var err errors.Error
                input := &plugin.ApiResourceInput{}
                input.Params = make(map[string]string)
                if len(c.Params) > 0 {
@@ -110,8 +112,8 @@ func handlePluginCall(pluginName string, handler 
plugin.ApiResourceHandler) func
                        if 
strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data;") 
{
                                input.Request = c.Request
                        } else {
-                               err = c.ShouldBindJSON(&input.Body)
-                               if err != nil && err.Error() != "EOF" {
+                               err2 := c.ShouldBindJSON(&input.Body)
+                               if err2 != nil && err2.Error() != "EOF" {
                                        shared.ApiOutputError(c, err)
                                        return
                                }
@@ -119,7 +121,12 @@ func handlePluginCall(pluginName string, handler 
plugin.ApiResourceHandler) func
                }
                output, err := handler(input)
                if err != nil {
-                       shared.ApiOutputError(c, err)
+                       if output != nil && output.Body != nil {
+                               logruslog.Global.Error(err, "")
+                               shared.ApiOutputSuccess(c, output.Body, 
err.GetType().GetHttpCode())
+                       } else {
+                               shared.ApiOutputError(c, err)
+                       }
                } else if output != nil {
                        status := output.Status
                        if status < http.StatusContinue {
diff --git a/backend/server/services/project.go 
b/backend/server/services/project.go
index 4413a8225..48594bbeb 100644
--- a/backend/server/services/project.go
+++ b/backend/server/services/project.go
@@ -253,7 +253,15 @@ func DeleteProject(name string) errors.Error {
        if name == "" {
                return errors.BadInput.New("project name is missing")
        }
-       var err errors.Error
+       // verify exists
+       _, err := getProjectByName(db, name)
+       if err != nil {
+               return err
+       }
+       err = deleteProjectBlueprint(name)
+       if err != nil {
+               return err
+       }
        tx := db.Begin()
        defer func() {
                if r := recover(); r != nil || err != nil {
@@ -263,10 +271,6 @@ func DeleteProject(name string) errors.Error {
                        }
                }
        }()
-       _, err = getProjectByName(tx, name)
-       if err != nil {
-               return err
-       }
        err = tx.Delete(&models.Project{}, dal.Where("name = ?", name))
        if err != nil {
                return errors.Default.Wrap(err, "error deleting project")
@@ -287,20 +291,20 @@ func DeleteProject(name string) errors.Error {
        if err != nil {
                return errors.Default.Wrap(err, "error deleting project Issue 
metric")
        }
-       err = tx.Commit()
-       if err != nil {
-               return err
-       }
-       bp, err := bpManager.GetDbBlueprintByProjectName(name)
+       return tx.Commit()
+}
+
+func deleteProjectBlueprint(projectName string) errors.Error {
+       bp, err := bpManager.GetDbBlueprintByProjectName(projectName)
        if err != nil {
-               if tx.IsErrorNotFound(err) {
-                       return nil
+               if !db.IsErrorNotFound(err) {
+                       return errors.Default.Wrap(err, fmt.Sprintf("error 
finding blueprint associated with project %s", projectName))
+               }
+       } else {
+               err = bpManager.DeleteBlueprint(bp.ID)
+               if err != nil {
+                       return errors.Default.Wrap(err, fmt.Sprintf("error 
deleting blueprint associated with project %s", projectName))
                }
-               return err
-       }
-       err = bpManager.DeleteBlueprint(bp.ID)
-       if err != nil {
-               return err
        }
        return nil
 }
diff --git a/backend/server/services/remote/plugin/connection_api.go 
b/backend/server/services/remote/plugin/connection_api.go
index 93cdc8751..4a4929aae 100644
--- a/backend/server/services/remote/plugin/connection_api.go
+++ b/backend/server/services/remote/plugin/connection_api.go
@@ -97,7 +97,10 @@ func (pa *pluginAPI) DeleteConnection(input 
*plugin.ApiResourceInput) (*plugin.A
        if err != nil {
                return nil, err
        }
-       err = pa.helper.Delete(connection)
+       refs, err := pa.helper.Delete(input.GetPlugin(), connection)
+       if err != nil {
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, err
+       }
        conn := connection.Unwrap()
        return &plugin.ApiResourceOutput{Body: conn}, err
 }
diff --git a/backend/server/services/remote/plugin/plugin_impl.go 
b/backend/server/services/remote/plugin/plugin_impl.go
index 0ac2daa06..f07f86388 100644
--- a/backend/server/services/remote/plugin/plugin_impl.go
+++ b/backend/server/services/remote/plugin/plugin_impl.go
@@ -68,13 +68,17 @@ func newPlugin(info *models.PluginInfo, invoker 
bridge.Invoker) (*remotePluginIm
        if err != nil {
                return nil, errors.Default.Wrap(err, fmt.Sprintf("Couldn't load 
ScopeConfig type for plugin %s", info.Name))
        }
-       toolModelTablers := make([]*coreModels.DynamicTabler, 
len(info.ToolModelInfos))
-       for i, toolModelInfo := range info.ToolModelInfos {
+       // put the scope and connection models in the tool list to be 
consistent with Go plugins
+       toolModelTablers := []*coreModels.DynamicTabler{
+               connectionTabler.New(),
+               scopeTabler.New(),
+       }
+       for _, toolModelInfo := range info.ToolModelInfos {
                toolModelTabler, err := 
toolModelInfo.LoadDynamicTabler(common.NoPKModel{})
                if err != nil {
                        return nil, errors.Default.Wrap(err, 
fmt.Sprintf("Couldn't load ToolModel type for plugin %s", info.Name))
                }
-               toolModelTablers[i] = toolModelTabler.New()
+               toolModelTablers = append(toolModelTablers, 
toolModelTabler.New())
        }
        openApiSpec, err := doc.GenerateOpenApiSpec(info)
        if err != nil {
diff --git a/backend/server/services/remote/plugin/scope_api.go 
b/backend/server/services/remote/plugin/scope_api.go
index 9ff150a5a..1f65bd84c 100644
--- a/backend/server/services/remote/plugin/scope_api.go
+++ b/backend/server/services/remote/plugin/scope_api.go
@@ -95,9 +95,9 @@ func (pa *pluginAPI) GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResour
 }
 
 func (pa *pluginAPI) DeleteScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       err := scopeHelper.DeleteScope(input)
+       refs, err := scopeHelper.DeleteScope(input)
        if err != nil {
-               return nil, err
+               return &plugin.ApiResourceOutput{Body: refs, Status: 
err.GetType().GetHttpCode()}, nil
        }
        return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
 }
diff --git a/backend/test/e2e/remote/helper.go 
b/backend/test/e2e/remote/helper.go
index 11999e8ad..6038bc6cd 100644
--- a/backend/test/e2e/remote/helper.go
+++ b/backend/test/e2e/remote/helper.go
@@ -19,6 +19,9 @@ package remote
 
 import (
        "fmt"
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
+       "net/http"
        "os"
        "path/filepath"
        "testing"
@@ -37,6 +40,20 @@ const (
        FAKE_PLUGIN_DIR = "python/test/fakeplugin"
 )
 
+var (
+       PluginDataTables = []string{
+               "_raw_fake_fakepipelinestream",
+               "_tool_fakeplugin_fakepipelines",
+               "cicd_pipelines",
+       }
+       PluginConfigTables = []string{
+               "_tool_fakeplugin_fakescopeconfigs",
+               "_tool_fakeplugin_fakeconnections",
+               "cicd_scopes",
+       }
+       PluginScopeTable = "_tool_fakeplugin_fakeprojects"
+)
+
 type (
        FakePluginConnection struct {
                Id    uint64 `json:"id"`
@@ -68,7 +85,8 @@ type (
 func ConnectLocalServer(t *testing.T) *helper.DevlakeClient {
        fmt.Println("Connect to server")
        client := helper.StartDevLakeServer(t, nil)
-       client.SetTimeout(30 * time.Second)
+       client.SetTimeout(60 * time.Second)
+       client.SetPipelineTimeout(60 * time.Second)
        return client
 }
 
@@ -105,7 +123,11 @@ func CreateTestScope(client *helper.DevlakeClient, config 
*FakeScopeConfig, conn
 }
 
 func CreateTestScopeConfig(client *helper.DevlakeClient, connectionId uint64) 
*FakeScopeConfig {
-       config := 
helper.Cast[FakeScopeConfig](client.CreateScopeConfig(PLUGIN_NAME, 
connectionId, FakeScopeConfig{Name: "Scope config", Env: "test env", Entities: 
[]string{"CICD"}}))
+       config := 
helper.Cast[FakeScopeConfig](client.CreateScopeConfig(PLUGIN_NAME, 
connectionId, FakeScopeConfig{
+               Name:     "Scope config",
+               Env:      "test env",
+               Entities: []string{plugin.DOMAIN_TYPE_CICD},
+       }))
        return &config
 }
 
@@ -154,3 +176,40 @@ func CreateTestBlueprints(t *testing.T, client 
*helper.DevlakeClient, count int)
                scope:      scope,
        }
 }
+
+func DeleteScopeWithDataIntegrityValidation(t *testing.T, client 
*helper.DevlakeClient, connectionId uint64, scopeId string, deleteDataOnly 
bool) services.BlueprintProjectPairs {
+       db := client.GetDal()
+       for _, table := range PluginDataTables {
+               count, err := db.Count(dal.From(table))
+               require.NoError(t, err)
+               require.Greaterf(t, int(count), 0, fmt.Sprintf("no data was 
found in table: %s", table))
+       }
+       configData := map[string]int{}
+       for _, table := range PluginConfigTables {
+               count, err := db.Count(dal.From(table))
+               require.NoError(t, err)
+               require.Greaterf(t, int(count), 0, fmt.Sprintf("no data was 
found in table: %s", table))
+               configData[table] = int(count)
+       }
+       refs := client.DeleteScope(PLUGIN_NAME, connectionId, scopeId, 
deleteDataOnly)
+       for _, table := range PluginDataTables {
+               count, err := db.Count(dal.From(table))
+               require.NoError(t, err)
+               require.Equalf(t, 0, int(count), fmt.Sprintf("data was found in 
table: %s", table))
+       }
+       if !deleteDataOnly && client.LastReturnedStatusCode() == http.StatusOK {
+               count, err := db.Count(dal.From(PluginScopeTable))
+               require.NoError(t, err)
+               require.Equalf(t, 0, int(count), fmt.Sprintf("data was found in 
table: %s", PluginScopeTable))
+       } else {
+               count, err := db.Count(dal.From(PluginScopeTable))
+               require.NoError(t, err)
+               require.Greaterf(t, int(count), 0, fmt.Sprintf("no data was 
found in table: %s", PluginScopeTable))
+       }
+       for _, table := range PluginConfigTables {
+               count, err := db.Count(dal.From(table))
+               require.NoError(t, err)
+               require.Equalf(t, configData[table], int(count), 
fmt.Sprintf("data was unexpectedly changed in table: %s", table))
+       }
+       return refs
+}
diff --git a/backend/test/e2e/remote/python_plugin_test.go 
b/backend/test/e2e/remote/python_plugin_test.go
index 2e6e8e83d..6e64923ac 100644
--- a/backend/test/e2e/remote/python_plugin_test.go
+++ b/backend/test/e2e/remote/python_plugin_test.go
@@ -18,6 +18,7 @@ limitations under the License.
 package remote
 
 import (
+       "net/http"
        "testing"
 
        "github.com/apache/incubator-devlake/core/models"
@@ -36,6 +37,30 @@ func TestCreateConnection(t *testing.T) {
        require.Equal(t, TOKEN, conns[0].Token)
 }
 
+func TestDeleteConnection(t *testing.T) {
+       client := CreateClient(t)
+
+       CreateTestConnection(client)
+
+       conns := client.ListConnections(PLUGIN_NAME)
+       require.Equal(t, 1, len(conns))
+       require.Equal(t, TOKEN, conns[0].Token)
+       refs := client.DeleteConnection(PLUGIN_NAME, conns[0].ID)
+       require.Equal(t, 0, len(refs.Projects))
+       require.Equal(t, 0, len(refs.Blueprints))
+}
+
+func TestDeleteConnection_Conflict(t *testing.T) {
+       client := CreateClient(t)
+       _ = CreateTestBlueprints(t, client, 1)
+       conns := client.ListConnections(PLUGIN_NAME)
+       require.Equal(t, 1, len(conns))
+       require.Equal(t, TOKEN, conns[0].Token)
+       refs := 
client.SetExpectedStatusCode(http.StatusConflict).DeleteConnection(PLUGIN_NAME, 
conns[0].ID)
+       require.Equal(t, 1, len(refs.Projects))
+       require.Equal(t, 1, len(refs.Blueprints))
+}
+
 func TestRemoteScopeGroups(t *testing.T) {
        client := CreateClient(t)
        connection := CreateTestConnection(client)
@@ -122,16 +147,18 @@ func TestRunPipeline(t *testing.T) {
        require.Equal(t, "", pipeline.ErrorName)
 }
 
-func TestBlueprintV200_withScopeDeletion(t *testing.T) {
+func TestBlueprintV200_withScopeDeletion_Conflict(t *testing.T) {
        client := CreateClient(t)
        params := CreateTestBlueprints(t, client, 1)
        client.TriggerBlueprint(params.blueprints[0].ID)
        scopesResponse := client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
        require.Equal(t, 1, len(scopesResponse))
        require.Equal(t, 1, len(scopesResponse[0].Blueprints))
-       client.DeleteScope(PLUGIN_NAME, params.connection.ID, params.scope.Id, 
false)
+       refs := DeleteScopeWithDataIntegrityValidation(t, 
client.SetExpectedStatusCode(http.StatusConflict), params.connection.ID, 
params.scope.Id, false)
+       require.Equal(t, 1, len(refs.Blueprints))
+       require.Equal(t, 1, len(refs.Projects))
        scopesResponse = client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
-       require.Equal(t, 0, len(scopesResponse))
+       require.Equal(t, 1, len(scopesResponse))
        bpsResult := client.ListBlueprints()
        require.Equal(t, 1, len(bpsResult.Blueprints))
 }
@@ -145,16 +172,60 @@ func TestBlueprintV200_withBlueprintDeletion(t 
*testing.T) {
        require.Equal(t, 2, len(scopesResponse[0].Blueprints))
        client.DeleteBlueprint(params.blueprints[0].ID)
        scopesResponse = client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
-       require.Equal(t, 1, len(scopesResponse)) //scopes are NOT 
cascade-deleted when bp is deleted
+       require.Equal(t, 1, len(scopesResponse))
        bpsList := client.ListBlueprints()
        require.Equal(t, 1, len(bpsList.Blueprints))
        require.Equal(t, params.blueprints[1].ID, bpsList.Blueprints[0].ID)
+       projectsResponse := client.ListProjects()
+       require.Equal(t, 2, len(projectsResponse.Projects))
+}
+
+func TestBlueprintV200_withBlueprintDeletion_thenScopeDeletion(t *testing.T) {
+       client := CreateClient(t)
+       params := CreateTestBlueprints(t, client, 1)
+       client.TriggerBlueprint(params.blueprints[0].ID)
+       scopesResponse := client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
+       require.Equal(t, 1, len(scopesResponse))
+       require.Equal(t, 1, len(scopesResponse[0].Blueprints))
+       client.DeleteBlueprint(params.blueprints[0].ID)
+       scopesResponse = client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
+       require.Equal(t, 1, len(scopesResponse))
+       bpsList := client.ListBlueprints()
+       require.Equal(t, 0, len(bpsList.Blueprints))
+       refs := DeleteScopeWithDataIntegrityValidation(t, client, 
params.connection.ID, params.scope.Id, false)
+       require.Equal(t, 0, len(refs.Blueprints))
+       require.Equal(t, 0, len(refs.Projects))
+       scopesResponse = client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
+       require.Equal(t, 0, len(scopesResponse))
+       projectsResponse := client.ListProjects()
+       require.Equal(t, 1, len(projectsResponse.Projects))
+}
+
+func TestBlueprintV200_withProjectDeletion_thenScopeDeletion(t *testing.T) {
+       client := CreateClient(t)
+       params := CreateTestBlueprints(t, client, 1)
+       client.TriggerBlueprint(params.blueprints[0].ID)
+       scopesResponse := client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
+       require.Equal(t, 1, len(scopesResponse))
+       require.Equal(t, 1, len(scopesResponse[0].Blueprints))
+       client.DeleteProject(params.projects[0].Name)
+       scopesResponse = client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
+       require.Equal(t, 1, len(scopesResponse))
+       bpsList := client.ListBlueprints()
+       require.Equal(t, 0, len(bpsList.Blueprints))
+       refs := DeleteScopeWithDataIntegrityValidation(t, client, 
params.connection.ID, params.scope.Id, false)
+       require.Equal(t, 0, len(refs.Blueprints))
+       require.Equal(t, 0, len(refs.Projects))
+       scopesResponse = client.ListScopes(PLUGIN_NAME, params.connection.ID, 
true)
+       require.Equal(t, 0, len(scopesResponse))
+       projectsResponse := client.ListProjects()
+       require.Equal(t, 0, len(projectsResponse.Projects))
 }
 
 func TestCreateScopeConfig(t *testing.T) {
        client := CreateClient(t)
        connection := CreateTestConnection(client)
-       scopeConfig := FakeScopeConfig{Name: "Scope config", Env: "test env", 
Entities: []string{"CICD"}}
+       scopeConfig := FakeScopeConfig{Name: "Scope config", Env: "test env", 
Entities: []string{plugin.DOMAIN_TYPE_CICD}}
 
        res := client.CreateScopeConfig(PLUGIN_NAME, connection.ID, scopeConfig)
        scopeConfig = helper.Cast[FakeScopeConfig](res)
@@ -163,18 +234,18 @@ func TestCreateScopeConfig(t *testing.T) {
        scopeConfig = helper.Cast[FakeScopeConfig](res)
        require.Equal(t, "Scope config", scopeConfig.Name)
        require.Equal(t, "test env", scopeConfig.Env)
-       require.Equal(t, []string{"CICD"}, scopeConfig.Entities)
+       require.Equal(t, []string{plugin.DOMAIN_TYPE_CICD}, 
scopeConfig.Entities)
 }
 
 func TestUpdateScopeConfig(t *testing.T) {
        client := CreateClient(t)
        connection := CreateTestConnection(client)
        res := client.CreateScopeConfig(PLUGIN_NAME, connection.ID, 
FakeScopeConfig{Name: "old name", Env: "old env", Entities: []string{}})
-       oldscopeConfig := helper.Cast[FakeScopeConfig](res)
+       oldScopeConfig := helper.Cast[FakeScopeConfig](res)
 
-       client.PatchScopeConfig(PLUGIN_NAME, connection.ID, oldscopeConfig.Id, 
FakeScopeConfig{Name: "new name", Env: "new env", Entities: []string{"CICD"}})
+       client.PatchScopeConfig(PLUGIN_NAME, connection.ID, oldScopeConfig.Id, 
FakeScopeConfig{Name: "new name", Env: "new env", Entities: 
[]string{plugin.DOMAIN_TYPE_CICD}})
 
-       res = client.GetScopeConfig(PLUGIN_NAME, connection.ID, 
oldscopeConfig.Id)
+       res = client.GetScopeConfig(PLUGIN_NAME, connection.ID, 
oldScopeConfig.Id)
        scopeConfig := helper.Cast[FakeScopeConfig](res)
        require.Equal(t, "new name", scopeConfig.Name)
        require.Equal(t, "new env", scopeConfig.Env)
diff --git a/backend/test/helper/api.go b/backend/test/helper/api.go
index e02111092..022458e2f 100644
--- a/backend/test/helper/api.go
+++ b/backend/test/helper/api.go
@@ -19,6 +19,7 @@ package helper
 
 import (
        "fmt"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "net/http"
        "reflect"
        "strings"
@@ -35,18 +36,20 @@ import (
 // CreateConnection FIXME
 func (d *DevlakeClient) TestConnection(pluginName string, connection any) {
        d.testCtx.Helper()
-       _ = sendHttpRequest[Connection](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       _ = sendHttpRequest[Connection](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPost, fmt.Sprintf("%s/plugins/%s/test", d.Endpoint, 
pluginName), nil, connection)
 }
 
 // CreateConnection FIXME
 func (d *DevlakeClient) CreateConnection(pluginName string, connection any) 
*Connection {
        d.testCtx.Helper()
-       created := sendHttpRequest[Connection](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       created := sendHttpRequest[Connection](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPost, fmt.Sprintf("%s/plugins/%s/connections", 
d.Endpoint, pluginName), nil, connection)
        return &created
 }
@@ -54,13 +57,25 @@ func (d *DevlakeClient) CreateConnection(pluginName string, 
connection any) *Con
 // ListConnections FIXME
 func (d *DevlakeClient) ListConnections(pluginName string) []*Connection {
        d.testCtx.Helper()
-       all := sendHttpRequest[[]*Connection](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       all := sendHttpRequest[[]*Connection](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodGet, fmt.Sprintf("%s/plugins/%s/connections", d.Endpoint, 
pluginName), nil, nil)
        return all
 }
 
+// DeleteConnection FIXME
+func (d *DevlakeClient) DeleteConnection(pluginName string, connectionId 
uint64) services.BlueprintProjectPairs {
+       d.testCtx.Helper()
+       refs := sendHttpRequest[services.BlueprintProjectPairs](d.testCtx, 
d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
+       }, http.MethodDelete, fmt.Sprintf("%s/plugins/%s/connections/%d", 
d.Endpoint, pluginName, connectionId), nil, nil)
+       return refs
+}
+
 // CreateBasicBlueprintV2 FIXME
 func (d *DevlakeClient) CreateBasicBlueprintV2(name string, config 
*BlueprintV2Config) models.Blueprint {
        settings := &models.BlueprintSettings{
@@ -83,31 +98,35 @@ func (d *DevlakeClient) CreateBasicBlueprintV2(name string, 
config *BlueprintV2C
                Settings:    ToJson(settings),
        }
        d.testCtx.Helper()
-       blueprint = sendHttpRequest[models.Blueprint](d.testCtx, d.timeout, 
debugInfo{
-               print:      true,
-               inlineJson: false,
+       blueprint = sendHttpRequest[models.Blueprint](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPost, fmt.Sprintf("%s/blueprints", d.Endpoint), nil, 
&blueprint)
        return blueprint
 }
 
 func (d *DevlakeClient) ListBlueprints() blueprints.PaginatedBlueprint {
-       return sendHttpRequest[blueprints.PaginatedBlueprint](d.testCtx, 
d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[blueprints.PaginatedBlueprint](d.testCtx, 
d.timeout, &testContext{
+               client:       d,
+               printPayload: 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,
+       return sendHttpRequest[models.Blueprint](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodGet, fmt.Sprintf("%s/blueprints/%d", d.Endpoint, 
blueprintId), nil, nil)
 }
 
 func (d *DevlakeClient) DeleteBlueprint(blueprintId uint64) {
-       sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       sendHttpRequest[any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodDelete, fmt.Sprintf("%s/blueprints/%d", d.Endpoint, 
blueprintId), nil, nil)
 }
 
@@ -131,9 +150,10 @@ func (d *DevlakeClient) CreateProject(project 
*ProjectConfig) models.ApiOutputPr
                        Enable:       true,
                })
        }
-       return sendHttpRequest[models.ApiOutputProject](d.testCtx, d.timeout, 
debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[models.ApiOutputProject](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPost, fmt.Sprintf("%s/projects", d.Endpoint), nil, 
&models.ApiInputProject{
                BaseProject: models.BaseProject{
                        Name:        project.ProjectName,
@@ -145,23 +165,26 @@ func (d *DevlakeClient) CreateProject(project 
*ProjectConfig) models.ApiOutputPr
 }
 
 func (d *DevlakeClient) GetProject(projectName string) models.ApiOutputProject 
{
-       return sendHttpRequest[models.ApiOutputProject](d.testCtx, d.timeout, 
debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[models.ApiOutputProject](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodGet, fmt.Sprintf("%s/projects/%s", d.Endpoint, 
projectName), nil, nil)
 }
 
 func (d *DevlakeClient) ListProjects() apiProject.PaginatedProjects {
-       return sendHttpRequest[apiProject.PaginatedProjects](d.testCtx, 
d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[apiProject.PaginatedProjects](d.testCtx, 
d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodGet, fmt.Sprintf("%s/projects", d.Endpoint), nil, nil)
 }
 
 func (d *DevlakeClient) DeleteProject(projectName string) {
-       sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       sendHttpRequest[any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodDelete, fmt.Sprintf("%s/projects/%s", d.Endpoint, 
projectName), nil, nil)
 }
 
@@ -169,23 +192,26 @@ func (d *DevlakeClient) CreateScopes(pluginName string, 
connectionId uint64, sco
        request := map[string]any{
                "data": scopes,
        }
-       return sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPut, fmt.Sprintf("%s/plugins/%s/connections/%d/scopes", 
d.Endpoint, pluginName, connectionId), nil, request)
 }
 
 func (d *DevlakeClient) UpdateScope(pluginName string, connectionId uint64, 
scopeId string, scope any) any {
-       return sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, 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, 
listBlueprints bool) []ScopeResponse {
-       scopesRaw := sendHttpRequest[[]map[string]any](d.testCtx, d.timeout, 
debugInfo{
-               print:      true,
-               inlineJson: false,
+       scopesRaw := sendHttpRequest[[]map[string]any](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, 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 {
@@ -195,47 +221,53 @@ func (d *DevlakeClient) ListScopes(pluginName string, 
connectionId uint64, listB
 }
 
 func (d *DevlakeClient) GetScope(pluginName string, connectionId uint64, 
scopeId string, listBlueprints bool) any {
-       return sendHttpRequest[api.ScopeRes[any, any]](d.testCtx, d.timeout, 
debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[api.ScopeRes[any, any]](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: 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) {
-       sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+func (d *DevlakeClient) DeleteScope(pluginName string, connectionId uint64, 
scopeId string, deleteDataOnly bool) services.BlueprintProjectPairs {
+       return sendHttpRequest[services.BlueprintProjectPairs](d.testCtx, 
d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, 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) CreateScopeConfig(pluginName string, connectionId 
uint64, scopeConfig any) any {
-       return sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPost, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scope-configs",
                d.Endpoint, pluginName, connectionId), nil, scopeConfig)
 }
 
 func (d *DevlakeClient) PatchScopeConfig(pluginName string, connectionId 
uint64, scopeConfigId uint64, scopeConfig any) any {
-       return sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPatch, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scope-configs/%d",
                d.Endpoint, pluginName, connectionId, scopeConfigId), nil, 
scopeConfig)
 }
 
 func (d *DevlakeClient) ListScopeConfigs(pluginName string, connectionId 
uint64) []any {
-       return sendHttpRequest[[]any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[[]any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodGet, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scope-configs?pageSize=20&page=1",
                d.Endpoint, pluginName, connectionId), nil, nil)
 }
 
 func (d *DevlakeClient) GetScopeConfig(pluginName string, connectionId uint64, 
scopeConfigId uint64) any {
-       return sendHttpRequest[any](d.testCtx, d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodGet, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scope-configs/%d",
                d.Endpoint, pluginName, connectionId, scopeConfigId), nil, nil)
 }
@@ -258,17 +290,19 @@ func (d *DevlakeClient) RemoteScopes(query 
RemoteScopesQuery) RemoteScopesOutput
        if len(query.Params) > 0 {
                url = url + "?" + mapToQueryString(query.Params)
        }
-       return sendHttpRequest[RemoteScopesOutput](d.testCtx, d.timeout, 
debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[RemoteScopesOutput](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodGet, url, nil, nil)
 }
 
 // SearchRemoteScopes makes calls to the "scope API" indirectly. "Search" is 
the remote endpoint to hit.
 func (d *DevlakeClient) SearchRemoteScopes(query SearchRemoteScopesQuery) 
SearchRemoteScopesOutput {
-       return sendHttpRequest[SearchRemoteScopesOutput](d.testCtx, d.timeout, 
debugInfo{
-               print:      true,
-               inlineJson: false,
+       return sendHttpRequest[SearchRemoteScopesOutput](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodGet, 
fmt.Sprintf("%s/plugins/%s/connections/%d/search-remote-scopes?search=%s&page=%d&pageSize=%d&%s",
                d.Endpoint,
                query.PluginName,
@@ -283,9 +317,10 @@ func (d *DevlakeClient) SearchRemoteScopes(query 
SearchRemoteScopesQuery) Search
 // TriggerBlueprint FIXME
 func (d *DevlakeClient) TriggerBlueprint(blueprintId uint64) models.Pipeline {
        d.testCtx.Helper()
-       pipeline := sendHttpRequest[models.Pipeline](d.testCtx, d.timeout, 
debugInfo{
-               print:      true,
-               inlineJson: false,
+       pipeline := sendHttpRequest[models.Pipeline](d.testCtx, d.timeout, 
&testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPost, fmt.Sprintf("%s/blueprints/%d/trigger", d.Endpoint, 
blueprintId), nil, nil)
        return d.monitorPipeline(pipeline.ID)
 }
@@ -293,9 +328,10 @@ func (d *DevlakeClient) TriggerBlueprint(blueprintId 
uint64) models.Pipeline {
 // RunPipeline FIXME
 func (d *DevlakeClient) RunPipeline(pipeline models.NewPipeline) 
models.Pipeline {
        d.testCtx.Helper()
-       pipelineResult := sendHttpRequest[models.Pipeline](d.testCtx, 
d.timeout, debugInfo{
-               print:      true,
-               inlineJson: false,
+       pipelineResult := sendHttpRequest[models.Pipeline](d.testCtx, 
d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
        }, http.MethodPost, fmt.Sprintf("%s/pipelines", d.Endpoint), nil, 
&pipeline)
        return d.monitorPipeline(pipelineResult.ID)
 }
@@ -316,8 +352,9 @@ func (d *DevlakeClient) monitorPipeline(id uint64) 
models.Pipeline {
        coloredPrintf("calling:\n\t%s %s\nwith:\n%s\n", http.MethodGet, 
endpoint, string(ToCleanJson(false, nil)))
        var pipelineResult models.Pipeline
        require.NoError(d.testCtx, runWithTimeout(d.pipelineTimeout, func() 
(bool, errors.Error) {
-               pipelineResult = sendHttpRequest[models.Pipeline](d.testCtx, 
d.pipelineTimeout, debugInfo{
-                       print: false,
+               pipelineResult = sendHttpRequest[models.Pipeline](d.testCtx, 
d.pipelineTimeout, &testContext{
+                       client:       d,
+                       printPayload: false,
                }, http.MethodGet, fmt.Sprintf("%s/pipelines/%d", d.Endpoint, 
id), nil, nil)
                if pipelineResult.Status == models.TASK_COMPLETED {
                        coloredPrintf("result: %s\n", ToCleanJson(true, 
&pipelineResult))
diff --git a/backend/test/helper/client.go b/backend/test/helper/client.go
index 320dee298..ccf9902e4 100644
--- a/backend/test/helper/client.go
+++ b/backend/test/helper/client.go
@@ -76,14 +76,17 @@ func init() {
 // DevlakeClient FIXME
 type (
        DevlakeClient struct {
-               Endpoint        string
-               db              *gorm.DB
-               log             log.Logger
-               cfg             *viper.Viper
-               testCtx         *testing.T
-               basicRes        corectx.BasicRes
-               timeout         time.Duration
-               pipelineTimeout time.Duration
+               Endpoint               string
+               db                     *gorm.DB
+               log                    log.Logger
+               cfg                    *viper.Viper
+               testCtx                *testing.T
+               basicRes               corectx.BasicRes
+               timeout                time.Duration
+               pipelineTimeout        time.Duration
+               expectedStatusCode     int
+               lastReturnedStatusCode int
+               isRemote               bool
        }
        LocalClientConfig struct {
                ServerPort      uint
@@ -96,18 +99,37 @@ type (
                PipelineTimeout time.Duration
        }
        RemoteClientConfig struct {
-               Endpoint string
+               Endpoint   string
+               DbURL      string
+               TruncateDb bool
        }
 )
 
 // ConnectRemoteServer returns a client to an existing server based on the 
config
-func ConnectRemoteServer(t *testing.T, cfg *RemoteClientConfig) *DevlakeClient 
{
-       return &DevlakeClient{
-               Endpoint: cfg.Endpoint,
-               db:       nil,
-               log:      nil,
+func ConnectRemoteServer(t *testing.T, clientConfig *RemoteClientConfig) 
*DevlakeClient {
+       var db *gorm.DB
+       var err errors.Error
+       logger := logruslog.Global.Nested("test")
+       cfg := config.GetConfig()
+       if clientConfig.DbURL != "" {
+               cfg.Set("DB_URL", clientConfig.DbURL)
+               db, err = runner.NewGormDb(cfg, logger)
+               require.NoError(t, err)
+       }
+       logger.Info("Connecting to remote server: %s", clientConfig.Endpoint)
+       client := &DevlakeClient{
+               isRemote: true,
+               Endpoint: clientConfig.Endpoint,
+               db:       db,
+               cfg:      cfg,
+               log:      logger,
                testCtx:  t,
+               basicRes: contextimpl.NewDefaultBasicRes(cfg, logger, 
dalgorm.NewDalgorm(db)),
        }
+       client.prepareDB(&LocalClientConfig{
+               TruncateDb: clientConfig.TruncateDb,
+       })
+       return client
 }
 
 // ConnectLocalServer spins up a local server from the config and returns a 
client connected to it
@@ -174,6 +196,17 @@ func (d *DevlakeClient) SetPipelineTimeout(timeout 
time.Duration) {
        d.pipelineTimeout = timeout
 }
 
+// SetExpectedStatusCode override the expected status code of the next API 
call. If it's anything but this, the test will fail.
+func (d *DevlakeClient) SetExpectedStatusCode(code int) *DevlakeClient {
+       d.expectedStatusCode = code
+       return d
+}
+
+// SetExpectedStatusCode return the last http status code
+func (d *DevlakeClient) LastReturnedStatusCode() int {
+       return d.lastReturnedStatusCode
+}
+
 // GetDal get a reference to the dal.Dal used by the server
 func (d *DevlakeClient) GetDal() dal.Dal {
        return dalgorm.NewDalgorm(d.db)
@@ -181,6 +214,9 @@ func (d *DevlakeClient) GetDal() dal.Dal {
 
 // AwaitPluginAvailability wait for this plugin to become available on the 
server given a timeout. Returns false if this condition does not get met.
 func (d *DevlakeClient) AwaitPluginAvailability(pluginName string, timeout 
time.Duration) {
+       if d.isRemote {
+               return
+       }
        err := runWithTimeout(timeout, func() (bool, errors.Error) {
                _, err := plugin.GetPlugin(pluginName)
                return err == nil, nil
@@ -339,11 +375,14 @@ func runWithTimeout(timeout time.Duration, f func() 
(bool, errors.Error)) errors
        }
 }
 
-func sendHttpRequest[Res any](t *testing.T, timeout time.Duration, debug 
debugInfo, httpMethod string, endpoint string, headers map[string]string, body 
any) Res {
+func sendHttpRequest[Res any](t *testing.T, timeout time.Duration, ctx 
*testContext, httpMethod string, endpoint string, headers map[string]string, 
body any) Res {
        t.Helper()
+       defer func() {
+               ctx.client.expectedStatusCode = 0
+       }()
        b := ToJson(body)
-       if debug.print {
-               coloredPrintf("calling:\n\t%s %s\nwith:\n%s\n", httpMethod, 
endpoint, string(ToCleanJson(debug.inlineJson, body)))
+       if ctx.printPayload {
+               coloredPrintf("calling:\n\t%s %s\nwith:\n%s\n", httpMethod, 
endpoint, string(ToCleanJson(ctx.inlineJson, body)))
        }
        var result Res
        err := runWithTimeout(timeout, func() (bool, errors.Error) {
@@ -360,19 +399,26 @@ func sendHttpRequest[Res any](t *testing.T, timeout 
time.Duration, debug debugIn
                if err != nil {
                        return false, errors.Convert(err)
                }
-               if response.StatusCode >= 300 {
-                       if err = response.Body.Close(); err != nil {
-                               return false, errors.Convert(err)
+               defer func() {
+                       ctx.client.lastReturnedStatusCode = response.StatusCode
+               }()
+               if ctx.client.expectedStatusCode > 0 || response.StatusCode >= 
300 {
+                       if ctx.client.expectedStatusCode == 0 || 
ctx.client.expectedStatusCode != response.StatusCode {
+                               if response.StatusCode >= 300 {
+                                       if err = response.Body.Close(); err != 
nil {
+                                               return false, 
errors.Convert(err)
+                                       }
+                                       response.Close = true
+                                       return false, 
errors.HttpStatus(response.StatusCode).New(fmt.Sprintf("unexpected http status 
code calling [%s] %s: %d", httpMethod, endpoint, response.StatusCode))
+                               }
                        }
-                       response.Close = true
-                       return false, 
errors.HttpStatus(response.StatusCode).New(fmt.Sprintf("unexpected http status 
code calling [%s] %s: %d", httpMethod, endpoint, response.StatusCode))
                }
                b, _ = io.ReadAll(response.Body)
                if err = json.Unmarshal(b, &result); err != nil {
                        return false, errors.Convert(err)
                }
-               if debug.print {
-                       coloredPrintf("result: %s\n", 
ToCleanJson(debug.inlineJson, b))
+               if ctx.printPayload {
+                       coloredPrintf("result: %s\n", 
ToCleanJson(ctx.inlineJson, b))
                }
                if err = response.Body.Close(); err != nil {
                        return false, errors.Convert(err)
@@ -390,7 +436,8 @@ func coloredPrintf(msg string, args ...any) {
        fmt.Printf(colorifier, msg)
 }
 
-type debugInfo struct {
-       print      bool
-       inlineJson bool
+type testContext struct {
+       printPayload bool
+       inlineJson   bool
+       client       *DevlakeClient
 }
diff --git a/backend/test/helper/client_factory.go 
b/backend/test/helper/client_factory.go
index 68559f0cd..a4aa1c4c0 100644
--- a/backend/test/helper/client_factory.go
+++ b/backend/test/helper/client_factory.go
@@ -35,3 +35,13 @@ func StartDevLakeServer(t *testing.T, loadedGoPlugins 
map[string]plugin.PluginMe
        })
        return client
 }
+
+// Connect to an existing DevLake server with default config. Tables are 
truncated. Useful for troubleshooting outside the IDE.
+func ConnectDevLakeServer(t *testing.T) *DevlakeClient {
+       client := ConnectRemoteServer(t, &RemoteClientConfig{
+               Endpoint:   "http://localhost:8089";,
+               DbURL:      config.GetConfig().GetString("E2E_DB_URL"),
+               TruncateDb: true,
+       })
+       return client
+}

Reply via email to