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 990c19ded [feat-5517]: Add Delete API for Scope configs (#5558)
990c19ded is described below

commit 990c19ded0f6e23f60aa05cd15bb6bc3e9e66c95
Author: Keon Amini <[email protected]>
AuthorDate: Mon Jun 26 12:43:58 2023 -0500

    [feat-5517]: Add Delete API for Scope configs (#5558)
    
    * feat: Add Delete API for scope configs
    
    * feat: deleting a scope-config now nulls out refs from scope tables
    
    * feat: deleting a connection now cascade deletes it scopes and 
scope-configs
    
    * refactor: code simplification in IT client
    
    * chore: fix linter nag
    
    * fix: removed cascade deletion of scopes per requirement clarification
    
    * refactor: use dal.UpdateColumn to set scope_config_id = null
---
 .../helpers/pluginhelper/api/connection_helper.go  | 29 +++++---
 .../pluginhelper/api/scope_config_helper.go        | 84 +++++++++++++++-------
 .../pluginhelper/api/scope_generic_helper.go       |  7 +-
 backend/plugins/bamboo/api/init.go                 |  1 +
 backend/plugins/bamboo/api/scope_config.go         | 14 ++++
 backend/plugins/bamboo/impl/impl.go                |  5 +-
 backend/plugins/bitbucket/api/init.go              |  1 +
 backend/plugins/bitbucket/api/scope_config.go      | 14 ++++
 backend/plugins/bitbucket/impl/impl.go             |  5 +-
 backend/plugins/github/api/init.go                 |  1 +
 backend/plugins/github/api/scope_config.go         | 14 ++++
 backend/plugins/github/impl/impl.go                |  5 +-
 backend/plugins/gitlab/api/init.go                 |  1 +
 backend/plugins/gitlab/api/scope_config.go         | 14 ++++
 backend/plugins/gitlab/impl/impl.go                |  5 +-
 .../gitlab/tasks/pipeline_commit_convertor.go      | 16 ++---
 backend/plugins/gitlab/tasks/pipeline_convertor.go | 14 ++--
 backend/plugins/jenkins/api/init.go                |  1 +
 backend/plugins/jenkins/api/scope_config.go        | 14 ++++
 backend/plugins/jenkins/impl/impl.go               |  5 +-
 backend/plugins/jira/api/init.go                   |  1 +
 backend/plugins/jira/api/scope_config.go           | 14 ++++
 backend/plugins/jira/impl/impl.go                  |  5 +-
 backend/plugins/tapd/api/init.go                   |  1 +
 backend/plugins/tapd/api/scope_config.go           | 14 ++++
 backend/plugins/tapd/impl/impl.go                  |  5 +-
 backend/plugins/trello/api/init.go                 |  1 +
 backend/plugins/trello/api/scope_config.go         | 14 ++++
 backend/plugins/trello/impl/impl.go                |  5 +-
 backend/plugins/zentao/api/init.go                 |  1 +
 backend/plugins/zentao/api/scope_config.go         | 14 ++++
 backend/plugins/zentao/impl/impl.go                |  5 +-
 .../server/services/remote/plugin/default_api.go   |  5 +-
 .../services/remote/plugin/scope_config_api.go     | 41 ++++++++---
 backend/test/e2e/manual/azuredevops/azure_test.go  | 18 ++++-
 backend/test/e2e/manual/gitlab/gitlab_test.go      | 23 ++++--
 .../test/e2e/manual/pagerduty/pagerduty_test.go    |  8 ++-
 backend/test/e2e/remote/python_plugin_test.go      | 53 ++++++++++++--
 backend/test/helper/api.go                         |  9 +++
 backend/test/helper/client.go                      | 32 ++++-----
 backend/test/helper/client_factory.go              |  2 +-
 41 files changed, 405 insertions(+), 116 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/connection_helper.go 
b/backend/helpers/pluginhelper/api/connection_helper.go
index 70d9dfada..41fb3abf2 100644
--- a/backend/helpers/pluginhelper/api/connection_helper.go
+++ b/backend/helpers/pluginhelper/api/connection_helper.go
@@ -117,13 +117,26 @@ func (c *ConnectionApiHelper) Delete(connection 
interface{}) (*services.Blueprin
        if len(referencingBps) > 0 {
                return services.NewBlueprintProjectPairs(referencingBps), 
errors.Conflict.New("Found one or more blueprint/project references to this 
connection")
        }
-       scopeModel := c.getScopeModel()
-       count, err := c.db.Count(dal.From(scopeModel.TableName()), 
dal.Where("connection_id = ?", connectionId))
+       src, err := c.getPluginSource()
        if err != nil {
-               return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
deleting scopes for plugin %s using connection %d", c.pluginName, connectionId))
+               return nil, err
        }
-       if count > 0 {
-               return nil, errors.Conflict.New("Found one or more scope 
references to this connection")
+       if scopeModel := src.Scope(); scopeModel != nil {
+               // ensure the connection has no scopes using it
+               count, err := c.db.Count(dal.From(scopeModel.TableName()), 
dal.Where("connection_id = ?", connectionId))
+               if err != nil {
+                       return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
counting scopes for plugin %s using connection %d", c.pluginName, connectionId))
+               }
+               if count > 0 {
+                       return nil, errors.Conflict.New(fmt.Sprintf("Found %d 
scopes using connection %d", count, connectionId))
+               }
+       }
+       if scopeConfigModel := src.ScopeConfig(); scopeConfigModel != nil {
+               // remove scope-configs that use this connection
+               err = CallDB(c.db.Delete, scopeConfigModel, 
dal.Where("connection_id = ?", connectionId))
+               if err != nil {
+                       return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
deleting scope-configs for plugin %s using connection %d", c.pluginName, 
connectionId))
+               }
        }
        return nil, CallDB(c.db.Delete, connection)
 }
@@ -151,11 +164,11 @@ func (c *ConnectionApiHelper) save(connection 
interface{}, method func(entity in
        return nil
 }
 
-func (c *ConnectionApiHelper) getScopeModel() plugin.ToolLayerScope {
+func (c *ConnectionApiHelper) getPluginSource() (plugin.PluginSource, 
errors.Error) {
        pluginMeta, _ := plugin.GetPlugin(c.pluginName)
        pluginSrc, ok := pluginMeta.(plugin.PluginSource)
        if !ok {
-               return nil
+               return nil, errors.Default.New("plugin doesn't implement 
PluginSource")
        }
-       return pluginSrc.Scope()
+       return pluginSrc, nil
 }
diff --git a/backend/helpers/pluginhelper/api/scope_config_helper.go 
b/backend/helpers/pluginhelper/api/scope_config_helper.go
index c6b5f48d1..7fd0f9070 100644
--- a/backend/helpers/pluginhelper/api/scope_config_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_config_helper.go
@@ -31,61 +31,64 @@ import (
 )
 
 // ScopeConfigHelper is used to write the CURD of scope config
-type ScopeConfigHelper[Tr dal.Tabler] struct {
-       log       log.Logger
-       db        dal.Dal
-       validator *validator.Validate
+type ScopeConfigHelper[ScopeConfig dal.Tabler] struct {
+       log        log.Logger
+       db         dal.Dal
+       validator  *validator.Validate
+       pluginName string
 }
 
 // NewScopeConfigHelper creates a ScopeConfigHelper for scope config management
 func NewScopeConfigHelper[Tr dal.Tabler](
        basicRes context.BasicRes,
        vld *validator.Validate,
+       pluginName string,
 ) *ScopeConfigHelper[Tr] {
        if vld == nil {
                vld = validator.New()
        }
        return &ScopeConfigHelper[Tr]{
-               log:       basicRes.GetLogger(),
-               db:        basicRes.GetDal(),
-               validator: vld,
+               log:        basicRes.GetLogger(),
+               db:         basicRes.GetDal(),
+               validator:  vld,
+               pluginName: pluginName,
        }
 }
 
-func (t ScopeConfigHelper[Tr]) Create(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+func (t ScopeConfigHelper[ScopeConfig]) Create(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        connectionId, e := strconv.ParseUint(input.Params["connectionId"], 10, 
64)
        if e != nil || connectionId == 0 {
                return nil, errors.Default.Wrap(e, "the connection ID should be 
an non-zero integer")
        }
-       var rule Tr
-       if err := DecodeMapStruct(input.Body, &rule, false); err != nil {
+       var config ScopeConfig
+       if err := DecodeMapStruct(input.Body, &config, false); err != nil {
                return nil, errors.Default.Wrap(err, "error in decoding scope 
config")
        }
        if t.validator != nil {
-               if err := t.validator.Struct(rule); err != nil {
+               if err := t.validator.Struct(config); err != nil {
                        return nil, errors.Default.Wrap(err, "error validating 
scope config")
                }
        }
-       valueConnectionId := 
reflect.ValueOf(&rule).Elem().FieldByName("ConnectionId")
+       valueConnectionId := 
reflect.ValueOf(&config).Elem().FieldByName("ConnectionId")
        if valueConnectionId.IsValid() {
                valueConnectionId.SetUint(connectionId)
        }
 
-       if err := t.db.Create(&rule); err != nil {
+       if err := t.db.Create(&config); err != nil {
                if t.db.IsDuplicationError(err) {
                        return nil, errors.BadInput.New("there was a scope 
config with the same name, please choose another name")
                }
                return nil, errors.BadInput.Wrap(err, "error on saving 
ScopeConfig")
        }
-       return &plugin.ApiResourceOutput{Body: rule, Status: http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: config, Status: http.StatusOK}, 
nil
 }
 
-func (t ScopeConfigHelper[Tr]) Update(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+func (t ScopeConfigHelper[ScopeConfig]) Update(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        scopeConfigId, e := strconv.ParseUint(input.Params["id"], 10, 64)
        if e != nil {
                return nil, errors.Default.Wrap(e, "the scope config ID should 
be an integer")
        }
-       var old Tr
+       var old ScopeConfig
        err := t.db.First(&old, dal.Where("id = ?", scopeConfigId))
        if err != nil {
                return nil, errors.Default.Wrap(err, "error on saving 
ScopeConfig")
@@ -104,29 +107,62 @@ func (t ScopeConfigHelper[Tr]) Update(input 
*plugin.ApiResourceInput) (*plugin.A
        return &plugin.ApiResourceOutput{Body: old, Status: http.StatusOK}, nil
 }
 
-func (t ScopeConfigHelper[Tr]) Get(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+func (t ScopeConfigHelper[ScopeConfig]) Get(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        scopeConfigId, err := strconv.ParseUint(input.Params["id"], 10, 64)
        if err != nil {
                return nil, errors.Default.Wrap(err, "the scope config ID 
should be an integer")
        }
-       var rule Tr
-       err = t.db.First(&rule, dal.Where("id = ?", scopeConfigId))
+       var config ScopeConfig
+       err = t.db.First(&config, dal.Where("id = ?", scopeConfigId))
        if err != nil {
                return nil, errors.Default.Wrap(err, "error on get ScopeConfig")
        }
-       return &plugin.ApiResourceOutput{Body: rule, Status: http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: config, Status: http.StatusOK}, 
nil
 }
 
-func (t ScopeConfigHelper[Tr]) List(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+func (t ScopeConfigHelper[ScopeConfig]) List(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        connectionId, e := strconv.ParseUint(input.Params["connectionId"], 10, 
64)
        if e != nil || connectionId == 0 {
                return nil, errors.Default.Wrap(e, "the connection ID should be 
an non-zero integer")
        }
-       var rules []Tr
+       var configs []ScopeConfig
        limit, offset := GetLimitOffset(input.Query, "pageSize", "page")
-       err := t.db.All(&rules, dal.Where("connection_id = ?", connectionId), 
dal.Limit(limit), dal.Offset(offset))
+       err := t.db.All(&configs, dal.Where("connection_id = ?", connectionId), 
dal.Limit(limit), dal.Offset(offset))
        if err != nil {
                return nil, errors.Default.Wrap(err, "error on get ScopeConfig 
list")
        }
-       return &plugin.ApiResourceOutput{Body: rules, Status: http.StatusOK}, 
nil
+       return &plugin.ApiResourceOutput{Body: configs, Status: http.StatusOK}, 
nil
+}
+
+func (t ScopeConfigHelper[ScopeConfig]) Delete(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       scopeConfigId, err := 
errors.Convert01(strconv.ParseUint(input.Params["id"], 10, 64))
+       if err != nil {
+               return nil, errors.Default.Wrap(err, "the scope config ID 
should be an integer")
+       }
+       connectionId, err := 
errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64))
+       if err != nil {
+               return nil, errors.Default.Wrap(err, "the scope connection ID 
should be an integer")
+       }
+       var config ScopeConfig
+       err = t.db.Delete(&config, dal.Where("id = ? AND connection_id = ?", 
scopeConfigId, connectionId))
+       if err != nil {
+               return nil, errors.Default.Wrap(err, "error deleting 
ScopeConfig")
+       }
+       err = t.nullOutScopeReferences(scopeConfigId)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
+}
+func (t ScopeConfigHelper[ScopeConfig]) nullOutScopeReferences(scopeConfigId 
uint64) errors.Error {
+       p, _ := plugin.GetPlugin(t.pluginName)
+       pluginSrc, ok := p.(plugin.PluginSource)
+       if !ok {
+               return errors.Default.New("plugin doesn't implement 
PluginSource")
+       }
+       scopeModel := pluginSrc.Scope()
+       if scopeModel == nil {
+               return nil
+       }
+       return t.db.UpdateColumn(scopeModel.TableName(), "scope_config_id", 
nil, dal.Where("scope_config_id = ?", scopeConfigId))
 }
diff --git a/backend/helpers/pluginhelper/api/scope_generic_helper.go 
b/backend/helpers/pluginhelper/api/scope_generic_helper.go
index 290977b8e..d6ab4642b 100644
--- a/backend/helpers/pluginhelper/api/scope_generic_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_generic_helper.go
@@ -306,15 +306,10 @@ func (gs *GenericScopeApiHelper[Conn, Scope, 
ScopeConfig]) DeleteScope(input *pl
        if err != nil {
                return nil, errors.Default.Wrap(err, fmt.Sprintf("error 
verifying connection for connection ID %d", params.connectionId))
        }
-       // TODO: the Scope type in this helper must implement the 
ToolLayerScope interface and we should enforce that.
        scope, err := gs.dbHelper.GetScope(params.connectionId, params.scopeId)
        if err != nil {
                return nil, err
        }
-       toolScope, ok := interface{}(scope).(plugin.ToolLayerScope)
-       if !ok {
-               panic(fmt.Errorf("%v does not implement the 
plugin.ToolLayerScope interface", scope))
-       }
        // now we can as scope to state its `Params` for data bloodline 
identification
        if refs, err := gs.getScopeReferences(params.connectionId, 
params.scopeId); err != nil || refs != nil {
                if err != nil {
@@ -322,7 +317,7 @@ func (gs *GenericScopeApiHelper[Conn, Scope, ScopeConfig]) 
DeleteScope(input *pl
                }
                return refs, errors.Conflict.New("Found one or more references 
to this scope")
        }
-       if err = gs.deleteScopeData(toolScope); err != nil {
+       if err = gs.deleteScopeData(*scope); err != nil {
                return nil, err
        }
        if !params.deleteDataOnly {
diff --git a/backend/plugins/bamboo/api/init.go 
b/backend/plugins/bamboo/api/init.go
index 59cf12a83..3122788a3 100644
--- a/backend/plugins/bamboo/api/init.go
+++ b/backend/plugins/bamboo/api/init.go
@@ -64,5 +64,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scopeConfigHelper = api.NewScopeConfigHelper[models.BambooScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
 }
diff --git a/backend/plugins/bamboo/api/scope_config.go 
b/backend/plugins/bamboo/api/scope_config.go
index 076840b37..01921146d 100644
--- a/backend/plugins/bamboo/api/scope_config.go
+++ b/backend/plugins/bamboo/api/scope_config.go
@@ -81,3 +81,17 @@ func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return scopeConfigHelper.List(input)
 }
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/bamboo
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/bamboo/connections/{connectionId}/scope-configs/{id} 
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scopeConfigHelper.Delete(input)
+}
diff --git a/backend/plugins/bamboo/impl/impl.go 
b/backend/plugins/bamboo/impl/impl.go
index 540048269..1b9484263 100644
--- a/backend/plugins/bamboo/impl/impl.go
+++ b/backend/plugins/bamboo/impl/impl.go
@@ -221,8 +221,9 @@ func (p Bamboo) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
                "connections/:connectionId/scopes": {
                        "GET": api.GetScopeList,
diff --git a/backend/plugins/bitbucket/api/init.go 
b/backend/plugins/bitbucket/api/init.go
index 205ab59b5..dbdf77b0b 100644
--- a/backend/plugins/bitbucket/api/init.go
+++ b/backend/plugins/bitbucket/api/init.go
@@ -63,5 +63,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scHelper = api.NewScopeConfigHelper[models.BitbucketScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
 }
diff --git a/backend/plugins/bitbucket/api/scope_config.go 
b/backend/plugins/bitbucket/api/scope_config.go
index 19e52f43d..fc0defde0 100644
--- a/backend/plugins/bitbucket/api/scope_config.go
+++ b/backend/plugins/bitbucket/api/scope_config.go
@@ -81,3 +81,17 @@ func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return scHelper.List(input)
 }
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/bitbucket
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/bitbucket/connections/{connectionId}/scope-configs/{id} 
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scHelper.Delete(input)
+}
diff --git a/backend/plugins/bitbucket/impl/impl.go 
b/backend/plugins/bitbucket/impl/impl.go
index cab8224bf..812e37b8b 100644
--- a/backend/plugins/bitbucket/impl/impl.go
+++ b/backend/plugins/bitbucket/impl/impl.go
@@ -236,8 +236,9 @@ func (p Bitbucket) ApiResources() 
map[string]map[string]plugin.ApiResourceHandle
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
        }
 }
diff --git a/backend/plugins/github/api/init.go 
b/backend/plugins/github/api/init.go
index 45580331e..53877f920 100644
--- a/backend/plugins/github/api/init.go
+++ b/backend/plugins/github/api/init.go
@@ -77,6 +77,7 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scHelper = api.NewScopeConfigHelper[models.GithubScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
        remoteHelper = api.NewRemoteHelper[models.GithubConnection, 
models.GithubRepo, repo, plugin.ApiGroup](
                basicRes,
diff --git a/backend/plugins/github/api/scope_config.go 
b/backend/plugins/github/api/scope_config.go
index edd3ac727..ee1ee8f8e 100644
--- a/backend/plugins/github/api/scope_config.go
+++ b/backend/plugins/github/api/scope_config.go
@@ -81,3 +81,17 @@ func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return scHelper.List(input)
 }
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/github
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/github/connections/{connectionId}/scope-configs/{id} 
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scHelper.Delete(input)
+}
diff --git a/backend/plugins/github/impl/impl.go 
b/backend/plugins/github/impl/impl.go
index f07e7f096..08f2ef986 100644
--- a/backend/plugins/github/impl/impl.go
+++ b/backend/plugins/github/impl/impl.go
@@ -240,8 +240,9 @@ func (p Github) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
                "connections/:connectionId/remote-scopes": {
                        "GET": api.RemoteScopes,
diff --git a/backend/plugins/gitlab/api/init.go 
b/backend/plugins/gitlab/api/init.go
index c929569f2..4962c0220 100644
--- a/backend/plugins/gitlab/api/init.go
+++ b/backend/plugins/gitlab/api/init.go
@@ -63,5 +63,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scHelper = api.NewScopeConfigHelper[models.GitlabScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
 }
diff --git a/backend/plugins/gitlab/api/scope_config.go 
b/backend/plugins/gitlab/api/scope_config.go
index f2681a890..4ef4ed700 100644
--- a/backend/plugins/gitlab/api/scope_config.go
+++ b/backend/plugins/gitlab/api/scope_config.go
@@ -81,3 +81,17 @@ func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return scHelper.List(input)
 }
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/gitlab
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/gitlab/connections/{connectionId}/scope-configs/{id} 
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scHelper.Delete(input)
+}
diff --git a/backend/plugins/gitlab/impl/impl.go 
b/backend/plugins/gitlab/impl/impl.go
index e04079886..835020c24 100644
--- a/backend/plugins/gitlab/impl/impl.go
+++ b/backend/plugins/gitlab/impl/impl.go
@@ -255,8 +255,9 @@ func (p Gitlab) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
                "connections/:connectionId/proxy/rest/*path": {
                        "GET": api.Proxy,
diff --git a/backend/plugins/gitlab/tasks/pipeline_commit_convertor.go 
b/backend/plugins/gitlab/tasks/pipeline_commit_convertor.go
index 03cab5c88..5e76cad07 100644
--- a/backend/plugins/gitlab/tasks/pipeline_commit_convertor.go
+++ b/backend/plugins/gitlab/tasks/pipeline_commit_convertor.go
@@ -26,7 +26,7 @@ import (
        "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
        "github.com/apache/incubator-devlake/core/plugin"
        helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-       gitlabModels "github.com/apache/incubator-devlake/plugins/gitlab/models"
+       "github.com/apache/incubator-devlake/plugins/gitlab/models"
 )
 
 func init() {
@@ -46,40 +46,40 @@ func ConvertPipelineCommits(taskCtx plugin.SubTaskContext) 
errors.Error {
        db := taskCtx.GetDal()
        data := taskCtx.GetData().(*GitlabTaskData)
 
-       repo := &gitlabModels.GitlabProject{}
+       repo := &models.GitlabProject{}
        err := db.First(repo, dal.Where("gitlab_id = ? and connection_id = ?", 
data.Options.ProjectId, data.Options.ConnectionId))
        if err != nil {
                return err
        }
 
-       cursor, err := db.Cursor(dal.From(gitlabModels.GitlabPipelineProject{}),
+       cursor, err := db.Cursor(dal.From(models.GitlabPipelineProject{}),
                dal.Where("project_id = ? and connection_id = ?", 
data.Options.ProjectId, data.Options.ConnectionId))
        if err != nil {
                return err
        }
        defer cursor.Close()
 
-       pipelineIdGen := 
didgen.NewDomainIdGenerator(&gitlabModels.GitlabPipeline{})
+       pipelineIdGen := didgen.NewDomainIdGenerator(&models.GitlabPipeline{})
 
        converter, err := helper.NewDataConverter(helper.DataConverterArgs{
-               InputRowType: 
reflect.TypeOf(gitlabModels.GitlabPipelineProject{}),
+               InputRowType: reflect.TypeOf(models.GitlabPipelineProject{}),
                Input:        cursor,
                RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
                        Ctx: taskCtx,
-                       Params: gitlabModels.GitlabApiParams{
+                       Params: models.GitlabApiParams{
                                ConnectionId: data.Options.ConnectionId,
                                ProjectId:    data.Options.ProjectId,
                        },
                        Table: RAW_PIPELINE_TABLE,
                },
                Convert: func(inputRow interface{}) ([]interface{}, 
errors.Error) {
-                       gitlabPipelineCommit := 
inputRow.(*gitlabModels.GitlabPipelineProject)
+                       gitlabPipelineCommit := 
inputRow.(*models.GitlabPipelineProject)
 
                        domainPipelineCommit := &devops.CiCDPipelineCommit{
                                PipelineId: 
pipelineIdGen.Generate(data.Options.ConnectionId, 
gitlabPipelineCommit.PipelineId),
                                CommitSha:  gitlabPipelineCommit.Sha,
                                Branch:     gitlabPipelineCommit.Ref,
-                               RepoId: 
didgen.NewDomainIdGenerator(&gitlabModels.GitlabProject{}).
+                               RepoId: 
didgen.NewDomainIdGenerator(&models.GitlabProject{}).
                                        
Generate(gitlabPipelineCommit.ConnectionId, gitlabPipelineCommit.ProjectId),
                                RepoUrl: repo.WebUrl,
                        }
diff --git a/backend/plugins/gitlab/tasks/pipeline_convertor.go 
b/backend/plugins/gitlab/tasks/pipeline_convertor.go
index a8ff94c5d..320a508ed 100644
--- a/backend/plugins/gitlab/tasks/pipeline_convertor.go
+++ b/backend/plugins/gitlab/tasks/pipeline_convertor.go
@@ -29,7 +29,7 @@ import (
        "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
        "github.com/apache/incubator-devlake/core/plugin"
        helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-       gitlabModels "github.com/apache/incubator-devlake/plugins/gitlab/models"
+       "github.com/apache/incubator-devlake/plugins/gitlab/models"
 )
 
 func init() {
@@ -49,29 +49,29 @@ func ConvertPipelines(taskCtx plugin.SubTaskContext) 
errors.Error {
        db := taskCtx.GetDal()
        data := taskCtx.GetData().(*GitlabTaskData)
 
-       cursor, err := db.Cursor(dal.From(gitlabModels.GitlabPipeline{}),
+       cursor, err := db.Cursor(dal.From(models.GitlabPipeline{}),
                dal.Where("project_id = ? and connection_id = ?", 
data.Options.ProjectId, data.Options.ConnectionId))
        if err != nil {
                return err
        }
        defer cursor.Close()
 
-       pipelineIdGen := 
didgen.NewDomainIdGenerator(&gitlabModels.GitlabPipeline{})
-       projectIdGen := 
didgen.NewDomainIdGenerator(&gitlabModels.GitlabProject{})
+       pipelineIdGen := didgen.NewDomainIdGenerator(&models.GitlabPipeline{})
+       projectIdGen := didgen.NewDomainIdGenerator(&models.GitlabProject{})
 
        converter, err := helper.NewDataConverter(helper.DataConverterArgs{
-               InputRowType: reflect.TypeOf(gitlabModels.GitlabPipeline{}),
+               InputRowType: reflect.TypeOf(models.GitlabPipeline{}),
                Input:        cursor,
                RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
                        Ctx: taskCtx,
-                       Params: gitlabModels.GitlabApiParams{
+                       Params: models.GitlabApiParams{
                                ConnectionId: data.Options.ConnectionId,
                                ProjectId:    data.Options.ProjectId,
                        },
                        Table: RAW_PIPELINE_TABLE,
                },
                Convert: func(inputRow interface{}) ([]interface{}, 
errors.Error) {
-                       gitlabPipeline := 
inputRow.(*gitlabModels.GitlabPipeline)
+                       gitlabPipeline := inputRow.(*models.GitlabPipeline)
 
                        createdAt := time.Now()
                        if gitlabPipeline.GitlabCreatedAt != nil {
diff --git a/backend/plugins/jenkins/api/init.go 
b/backend/plugins/jenkins/api/init.go
index 457a22049..d36b7f80e 100644
--- a/backend/plugins/jenkins/api/init.go
+++ b/backend/plugins/jenkins/api/init.go
@@ -64,5 +64,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scHelper = api.NewScopeConfigHelper[models.JenkinsScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
 }
diff --git a/backend/plugins/jenkins/api/scope_config.go 
b/backend/plugins/jenkins/api/scope_config.go
index e9e75174b..fee76e6bb 100644
--- a/backend/plugins/jenkins/api/scope_config.go
+++ b/backend/plugins/jenkins/api/scope_config.go
@@ -81,3 +81,17 @@ func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return scHelper.List(input)
 }
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/jenkins
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/jenkins/connections/{connectionId}/scope-configs/{id} 
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scHelper.Delete(input)
+}
diff --git a/backend/plugins/jenkins/impl/impl.go 
b/backend/plugins/jenkins/impl/impl.go
index c7e186e39..6bb0ed362 100644
--- a/backend/plugins/jenkins/impl/impl.go
+++ b/backend/plugins/jenkins/impl/impl.go
@@ -206,8 +206,9 @@ func (p Jenkins) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
                "connections/:connectionId/proxy/rest/*path": {
                        "GET": api.Proxy,
diff --git a/backend/plugins/jira/api/init.go b/backend/plugins/jira/api/init.go
index b82191c28..22143ef45 100644
--- a/backend/plugins/jira/api/init.go
+++ b/backend/plugins/jira/api/init.go
@@ -65,5 +65,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scHelper = api.NewScopeConfigHelper[models.JiraScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
 }
diff --git a/backend/plugins/jira/api/scope_config.go 
b/backend/plugins/jira/api/scope_config.go
index 178c94a32..78ce0937d 100644
--- a/backend/plugins/jira/api/scope_config.go
+++ b/backend/plugins/jira/api/scope_config.go
@@ -177,6 +177,20 @@ func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutp
        return scHelper.List(input)
 }
 
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/jira
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/jira/connections/{connectionId}/scope-configs/{id} [DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scHelper.Delete(input)
+}
+
 // GetApplicationTypes return issue application types
 // @Summary return issue application types
 // @Description return issue application types
diff --git a/backend/plugins/jira/impl/impl.go 
b/backend/plugins/jira/impl/impl.go
index e9b513dea..f2df6626e 100644
--- a/backend/plugins/jira/impl/impl.go
+++ b/backend/plugins/jira/impl/impl.go
@@ -310,8 +310,9 @@ func (p Jira) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
                "connections/:connectionId/application-types": {
                        "GET": api.GetApplicationTypes,
diff --git a/backend/plugins/tapd/api/init.go b/backend/plugins/tapd/api/init.go
index 8765f1c17..559acd70b 100644
--- a/backend/plugins/tapd/api/init.go
+++ b/backend/plugins/tapd/api/init.go
@@ -63,5 +63,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scHelper = api.NewScopeConfigHelper[models.TapdScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
 }
diff --git a/backend/plugins/tapd/api/scope_config.go 
b/backend/plugins/tapd/api/scope_config.go
index 428b416d5..7f64714d0 100644
--- a/backend/plugins/tapd/api/scope_config.go
+++ b/backend/plugins/tapd/api/scope_config.go
@@ -81,3 +81,17 @@ func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return scHelper.List(input)
 }
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/tapd
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/tapd/connections/{connectionId}/scope-configs/{id} [DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scHelper.Delete(input)
+}
diff --git a/backend/plugins/tapd/impl/impl.go 
b/backend/plugins/tapd/impl/impl.go
index c6ed4d51b..b77ca2d21 100644
--- a/backend/plugins/tapd/impl/impl.go
+++ b/backend/plugins/tapd/impl/impl.go
@@ -320,8 +320,9 @@ func (p Tapd) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
        }
 }
diff --git a/backend/plugins/trello/api/init.go 
b/backend/plugins/trello/api/init.go
index a27808fea..223c5272a 100644
--- a/backend/plugins/trello/api/init.go
+++ b/backend/plugins/trello/api/init.go
@@ -56,5 +56,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scHelper = api.NewScopeConfigHelper[models.TrelloScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
 }
diff --git a/backend/plugins/trello/api/scope_config.go 
b/backend/plugins/trello/api/scope_config.go
index c8ee64b02..4562ff109 100644
--- a/backend/plugins/trello/api/scope_config.go
+++ b/backend/plugins/trello/api/scope_config.go
@@ -77,3 +77,17 @@ func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return scHelper.List(input)
 }
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/trello
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/trello/connections/{connectionId}/scope-configs/{id} 
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scHelper.Delete(input)
+}
diff --git a/backend/plugins/trello/impl/impl.go 
b/backend/plugins/trello/impl/impl.go
index b49180055..e20e5ad86 100644
--- a/backend/plugins/trello/impl/impl.go
+++ b/backend/plugins/trello/impl/impl.go
@@ -175,8 +175,9 @@ func (p Trello) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
                "connections/:connectionId/scopes/:boardId": {
                        "GET":    api.GetScope,
diff --git a/backend/plugins/zentao/api/init.go 
b/backend/plugins/zentao/api/init.go
index 33d665dfc..944e7f017 100644
--- a/backend/plugins/zentao/api/init.go
+++ b/backend/plugins/zentao/api/init.go
@@ -90,5 +90,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        scHelper = api.NewScopeConfigHelper[models.ZentaoScopeConfig](
                basicRes,
                vld,
+               p.Name(),
        )
 }
diff --git a/backend/plugins/zentao/api/scope_config.go 
b/backend/plugins/zentao/api/scope_config.go
index b598f52f9..eb11ac6d7 100644
--- a/backend/plugins/zentao/api/scope_config.go
+++ b/backend/plugins/zentao/api/scope_config.go
@@ -81,3 +81,17 @@ func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        return scHelper.List(input)
 }
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/zentao
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/zentao/connections/{connectionId}/scope-configs/{id} 
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return scHelper.Delete(input)
+}
diff --git a/backend/plugins/zentao/impl/impl.go 
b/backend/plugins/zentao/impl/impl.go
index 7069908c0..cd36ca648 100644
--- a/backend/plugins/zentao/impl/impl.go
+++ b/backend/plugins/zentao/impl/impl.go
@@ -282,8 +282,9 @@ func (p Zentao) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "GET":  api.GetScopeConfigList,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "PATCH": api.UpdateScopeConfig,
-                       "GET":   api.GetScopeConfig,
+                       "PATCH":  api.UpdateScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
                },
                "connections/:connectionId/remote-scopes": {
                        "GET": api.RemoteScopes,
diff --git a/backend/server/services/remote/plugin/default_api.go 
b/backend/server/services/remote/plugin/default_api.go
index 759618a6d..b0c2c6751 100644
--- a/backend/server/services/remote/plugin/default_api.go
+++ b/backend/server/services/remote/plugin/default_api.go
@@ -76,8 +76,9 @@ func GetDefaultAPI(
                        "GET":  papi.ListScopeConfigs,
                },
                "connections/:connectionId/scope-configs/:id": {
-                       "GET":   papi.GetScopeConfig,
-                       "PATCH": papi.PatchScopeConfig,
+                       "GET":    papi.GetScopeConfig,
+                       "PATCH":  papi.PatchScopeConfig,
+                       "DELETE": papi.DeleteScopeConfig,
                },
                "connections/:connectionId/remote-scopes": {
                        "GET": papi.GetRemoteScopes,
diff --git a/backend/server/services/remote/plugin/scope_config_api.go 
b/backend/server/services/remote/plugin/scope_config_api.go
index 42f1f7cf6..c772e9fa0 100644
--- a/backend/server/services/remote/plugin/scope_config_api.go
+++ b/backend/server/services/remote/plugin/scope_config_api.go
@@ -18,6 +18,7 @@ limitations under the License.
 package plugin
 
 import (
+       "github.com/apache/incubator-devlake/server/services/remote/models"
        "net/http"
        "strconv"
 
@@ -47,7 +48,7 @@ func (pa *pluginAPI) PostScopeConfigs(input 
*plugin.ApiResourceInput) (*plugin.A
 }
 
 func (pa *pluginAPI) PatchScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       connectionId, trId, err := extractTrParam(input.Params)
+       connectionId, trId, err := extractConfigParam(input.Params)
        if err != nil {
                return nil, err
        }
@@ -76,11 +77,11 @@ func (pa *pluginAPI) PatchScopeConfig(input 
*plugin.ApiResourceInput) (*plugin.A
 func (pa *pluginAPI) GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        scopeConfig := pa.scopeConfigType.New()
        db := basicRes.GetDal()
-       connectionId, trId, err := extractTrParam(input.Params)
+       connectionId, configId, err := extractConfigParam(input.Params)
        if err != nil {
                return nil, err
        }
-       err = api.CallDB(db.First, scopeConfig, dal.Where("connection_id = ? 
AND id = ?", connectionId, trId))
+       err = api.CallDB(db.First, scopeConfig, dal.Where("connection_id = ? 
AND id = ?", connectionId, configId))
        if err != nil {
                return nil, errors.Default.Wrap(err, "no scope config with 
given id")
        }
@@ -103,15 +104,39 @@ func (pa *pluginAPI) ListScopeConfigs(input 
*plugin.ApiResourceInput) (*plugin.A
        return &plugin.ApiResourceOutput{Body: scopeConfigs.Unwrap()}, nil
 }
 
-func extractTrParam(params map[string]string) (connectionId uint64, 
transformationId uint64, err errors.Error) {
+func (pa *pluginAPI) DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connectionId, configId, err := extractConfigParam(input.Params)
+       if err != nil {
+               return nil, err
+       }
+       scopeConfig := pa.scopeConfigType.New()
+       db := basicRes.GetDal()
+       err = api.CallDB(db.Delete, scopeConfig, dal.Where("connection_id = ? 
AND id = ?", connectionId, configId))
+       if err != nil {
+               return nil, errors.Default.Wrap(err, "no scope config with 
given id")
+       }
+       err = pa.nullOutScopeReferences(configId)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: scopeConfig.Unwrap()}, nil
+}
+
+func (pa *pluginAPI) nullOutScopeReferences(scopeConfigId uint64) errors.Error 
{
+       scopeModel := models.NewDynamicScopeModel(pa.scopeType)
+       db := basicRes.GetDal()
+       return db.UpdateColumn(scopeModel.TableName(), "scope_config_id", nil, 
dal.Where("scope_config_id = ?", scopeConfigId))
+}
+
+func extractConfigParam(params map[string]string) (connectionId uint64, 
configId uint64, err errors.Error) {
        connectionId, _ = strconv.ParseUint(params["connectionId"], 10, 64)
-       transformationId, _ = strconv.ParseUint(params["id"], 10, 64)
+       configId, _ = strconv.ParseUint(params["id"], 10, 64)
        if connectionId == 0 {
                return 0, 0, errors.BadInput.New("invalid connectionId")
        }
-       if transformationId == 0 {
-               return 0, 0, errors.BadInput.New("invalid transformationId")
+       if configId == 0 {
+               return 0, 0, errors.BadInput.New("invalid configId")
        }
 
-       return connectionId, transformationId, nil
+       return connectionId, configId, nil
 }
diff --git a/backend/test/e2e/manual/azuredevops/azure_test.go 
b/backend/test/e2e/manual/azuredevops/azure_test.go
index 012068700..fe2cdd35b 100644
--- a/backend/test/e2e/manual/azuredevops/azure_test.go
+++ b/backend/test/e2e/manual/azuredevops/azure_test.go
@@ -27,6 +27,8 @@ import (
        pluginmodels 
"github.com/apache/incubator-devlake/plugins/pagerduty/models"
        "github.com/apache/incubator-devlake/test/helper"
        "github.com/stretchr/testify/require"
+       "net/http"
+       "strings"
        "testing"
        "time"
 )
@@ -43,8 +45,8 @@ func TestAzure(t *testing.T) {
                CreateServer: true,
                DropDb:       false,
                TruncateDb:   true,
-               Plugins: map[string]plugin.PluginMeta{
-                       "gitextractor": gitextractor.GitExtractor{},
+               Plugins: []plugin.PluginMeta{
+                       gitextractor.GitExtractor{},
                },
        })
        client.SetTimeout(60 * time.Second)
@@ -133,12 +135,16 @@ func TestAzure(t *testing.T) {
                require.Equal(t, models.TASK_COMPLETED, pipeline.Status)
                createdScopesList := client.ListScopes(azurePlugin, 
connection.ID, true)
                require.True(t, len(createdScopesList) > 0)
+               
client.SetExpectedStatusCode(http.StatusConflict).DeleteConnection(azurePlugin, 
connection.ID)
+               client.DeleteScopeConfig(azurePlugin, connection.ID, 
repoConfig.ID)
+               client.DeleteBlueprint(bp.ID)
                for _, scope := range createdScopesList {
                        scopeCast := 
helper.Cast[pluginmodels.Service](scope.Scope)
                        fmt.Printf("Deleting scope %s\n", scopeCast.Id)
                        client.DeleteScope(azurePlugin, connection.ID, 
scopeCast.Id, false)
                        fmt.Printf("Deleted scope %s\n", scopeCast.Id)
                }
+               client.DeleteConnection(azurePlugin, connection.ID)
        })
        fmt.Println("========DONE=======")
 }
@@ -147,8 +153,14 @@ func remoteScopesToScopes(remoteScopes 
helper.RemoteScopesOutput, filters []stri
        var a []any
        for _, c := range remoteScopes.Children {
                repo := helper.Cast[AzureGitRepo](c.Data)
-               if len(filters) == 0 || helper.Contains(filters, repo.Name) {
+               if len(filters) == 0 {
                        a = append(a, repo)
+               } else {
+                       for _, f := range filters {
+                               if len(filters) == 0 || 
strings.Contains(repo.Name, f) {
+                                       a = append(a, repo)
+                               }
+                       }
                }
        }
        return a
diff --git a/backend/test/e2e/manual/gitlab/gitlab_test.go 
b/backend/test/e2e/manual/gitlab/gitlab_test.go
index c4b0d8ad7..9aea5c7c5 100644
--- a/backend/test/e2e/manual/gitlab/gitlab_test.go
+++ b/backend/test/e2e/manual/gitlab/gitlab_test.go
@@ -29,6 +29,7 @@ import (
        pluginmodels "github.com/apache/incubator-devlake/plugins/gitlab/models"
        "github.com/apache/incubator-devlake/test/helper"
        "github.com/stretchr/testify/require"
+       "net/http"
        "testing"
 )
 
@@ -58,15 +59,15 @@ func TestGitlabPlugin(t *testing.T) {
                CreateServer: true,
                DropDb:       false,
                TruncateDb:   true,
-               Plugins: map[string]plugin.PluginMeta{
-                       "gitlab":       gitlab.Gitlab(""),
-                       "gitextractor": gitextractor.GitExtractor{},
+               Plugins: []plugin.PluginMeta{
+                       gitlab.Gitlab(""),
+                       gitextractor.GitExtractor{},
                },
        })
        cfg := helper.GetTestConfig[TestConfig]()
        connection := createConnection(cfg, client)
        t.Run("blueprint v200", func(t *testing.T) {
-               rule := 
helper.Cast[pluginmodels.GitlabScopeConfig](client.CreateScopeConfig("gitlab", 
connection.ID,
+               scopeConfig := 
helper.Cast[pluginmodels.GitlabScopeConfig](client.CreateScopeConfig("gitlab", 
connection.ID,
                        pluginmodels.GitlabScopeConfig{
                                ScopeConfig: common.ScopeConfig{
                                        Entities: []string{
@@ -75,7 +76,7 @@ func TestGitlabPlugin(t *testing.T) {
                                                plugin.DOMAIN_TYPE_CODE_REVIEW,
                                        },
                                },
-                               Name:                 "rule-1",
+                               Name:                 "config-1",
                                PrType:               "",
                                PrComponent:          "",
                                PrBodyClosePattern:   "",
@@ -89,7 +90,7 @@ func TestGitlabPlugin(t *testing.T) {
                                ProductionPattern:    ".*",             // this 
triggers dora
                                Refdiff:              map[string]any{}, // this 
is technically a true/false (nil or not)
                        }))
-               _ = rule
+               _ = scopeConfig
                remoteScopes := client.RemoteScopes(helper.RemoteScopesQuery{
                        PluginName:   pluginName,
                        ConnectionId: connection.ID,
@@ -113,7 +114,7 @@ func TestGitlabPlugin(t *testing.T) {
                        if remoteScope.Type == "scope" {
                                data := 
helper.Cast[pluginmodels.GitlabProject](remoteScope.Data)
                                if len(cfg.Projects) == 0 || 
helper.Contains(cfg.Projects, data.Name) {
-                                       data.ScopeConfigId = rule.ID
+                                       data.ScopeConfigId = scopeConfig.ID
                                        scopeData = append(scopeData, data)
                                }
                        }
@@ -150,6 +151,14 @@ func TestGitlabPlugin(t *testing.T) {
                fmt.Printf("=========================Triggering blueprint for 
project %s =========================\n", outputProject.Name)
                pipeline := client.TriggerBlueprint(bp.ID)
                require.Equal(t, models.TASK_COMPLETED, pipeline.Status)
+               
client.SetExpectedStatusCode(http.StatusConflict).DeleteConnection(pluginName, 
connection.ID)
+               client.DeleteScopeConfig(pluginName, connection.ID, 
scopeConfig.ID)
+               client.DeleteBlueprint(bp.ID)
+               for _, scope := range listedScopes {
+                       project := 
helper.Cast[pluginmodels.GitlabProject](scope.Scope)
+                       client.DeleteScope(pluginName, connection.ID, 
project.ScopeId(), false)
+               }
+               client.DeleteConnection(pluginName, connection.ID)
        })
        fmt.Println("======DONE======")
 }
diff --git a/backend/test/e2e/manual/pagerduty/pagerduty_test.go 
b/backend/test/e2e/manual/pagerduty/pagerduty_test.go
index d8257e2c5..12bd7b2f8 100644
--- a/backend/test/e2e/manual/pagerduty/pagerduty_test.go
+++ b/backend/test/e2e/manual/pagerduty/pagerduty_test.go
@@ -27,6 +27,7 @@ import (
        pluginmodels 
"github.com/apache/incubator-devlake/plugins/pagerduty/models"
        "github.com/apache/incubator-devlake/test/helper"
        "github.com/stretchr/testify/require"
+       "net/http"
        "testing"
        "time"
 )
@@ -41,8 +42,8 @@ func TestPagerDutyPlugin(t *testing.T) {
                CreateServer: true,
                DropDb:       false,
                TruncateDb:   true,
-               Plugins: map[string]plugin.PluginMeta{
-                       pluginName: &impl.PagerDuty{},
+               Plugins: []plugin.PluginMeta{
+                       &impl.PagerDuty{},
                },
        })
        client.SetTimeout(0)
@@ -101,12 +102,15 @@ func TestPagerDutyPlugin(t *testing.T) {
                require.Equal(t, models.TASK_COMPLETED, pipeline.Status)
                createdScopesList := client.ListScopes(pluginName, 
connection.ID, true)
                require.True(t, len(createdScopesList) > 0)
+               
client.SetExpectedStatusCode(http.StatusConflict).DeleteConnection(pluginName, 
connection.ID)
+               client.DeleteBlueprint(bp.ID)
                for _, scope := range createdScopesList {
                        scopeCast := 
helper.Cast[pluginmodels.Service](scope.Scope)
                        fmt.Printf("Deleting scope %s\n", scopeCast.Id)
                        client.DeleteScope(pluginName, connection.ID, 
scopeCast.Id, false)
                        fmt.Printf("Deleted scope %s\n", scopeCast.Id)
                }
+               client.DeleteConnection(pluginName, connection.ID)
        })
        fmt.Println("======DONE======")
 }
diff --git a/backend/test/e2e/remote/python_plugin_test.go 
b/backend/test/e2e/remote/python_plugin_test.go
index 3b0bbfc80..10b778ff2 100644
--- a/backend/test/e2e/remote/python_plugin_test.go
+++ b/backend/test/e2e/remote/python_plugin_test.go
@@ -56,19 +56,31 @@ func TestDeleteConnection_Conflict(t *testing.T) {
        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)
+       refs := 
client.SetExpectedStatusCode(http.StatusConflict).DeleteConnection(PLUGIN_NAME, 
params.connection.ID)
        require.Equal(t, 1, len(refs.Projects))
        require.Equal(t, 1, len(refs.Blueprints))
        client.DeleteBlueprint(params.blueprints[0].ID)
-       refs = 
client.SetExpectedStatusCode(http.StatusConflict).DeleteConnection(PLUGIN_NAME, 
conns[0].ID)
-       // should still conflict because we have scopes tied to this connection
+       
client.SetExpectedStatusCode(http.StatusConflict).DeleteConnection(PLUGIN_NAME, 
params.connection.ID)
+       client.DeleteScope(PLUGIN_NAME, params.connection.ID, params.scope.Id, 
false)
+       client.DeleteConnection(PLUGIN_NAME, conns[0].ID)
+}
+
+func TestDeleteConnection_WithDependentScopesAndConfig(t *testing.T) {
+       client := CreateClient(t)
+       connection := CreateTestConnection(client)
+       config := CreateTestScopeConfig(client, connection.ID)
+       scope := CreateTestScope(client, config, connection.ID)
+       refs := 
client.SetExpectedStatusCode(http.StatusConflict).DeleteConnection(PLUGIN_NAME, 
connection.ID)
        require.Equal(t, 0, len(refs.Projects))
        require.Equal(t, 0, len(refs.Blueprints))
-       client.DeleteScope(PLUGIN_NAME, params.connection.ID, params.scope.Id, 
false)
-       refs = client.DeleteConnection(PLUGIN_NAME, conns[0].ID)
+       client.DeleteScope(PLUGIN_NAME, connection.ID, scope.Id, false)
+       refs = client.DeleteConnection(PLUGIN_NAME, connection.ID)
        require.Equal(t, 0, len(refs.Projects))
        require.Equal(t, 0, len(refs.Blueprints))
-
+       scopeRes := 
client.SetExpectedStatusCode(http.StatusBadRequest).ListScopes(PLUGIN_NAME, 
connection.ID, false)
+       require.Equal(t, 0, len(scopeRes))
+       configs := client.ListScopeConfigs(PLUGIN_NAME, connection.ID)
+       require.Equal(t, 0, len(configs))
 }
 
 func TestRemoteScopeGroups(t *testing.T) {
@@ -262,3 +274,32 @@ func TestUpdateScopeConfig(t *testing.T) {
        require.Equal(t, "new name", scopeConfig.Name)
        require.Equal(t, "new env", scopeConfig.Env)
 }
+
+func TestDeleteScopeConfig(t *testing.T) {
+       client := CreateClient(t)
+       connection := CreateTestConnection(client)
+       scopeConfig := FakeScopeConfig{Name: "Scope config", Env: "test env", 
Entities: []string{plugin.DOMAIN_TYPE_CICD}}
+       scopeConfig = 
helper.Cast[FakeScopeConfig](client.CreateScopeConfig(PLUGIN_NAME, 
connection.ID, scopeConfig))
+
+       configs := 
helper.Cast[[]FakeScopeConfig](client.ListScopeConfigs(PLUGIN_NAME, 
connection.ID))
+       require.Equal(t, 1, len(configs))
+
+       client.DeleteScopeConfig(PLUGIN_NAME, connection.ID, scopeConfig.Id)
+       configs = 
helper.Cast[[]FakeScopeConfig](client.ListScopeConfigs(PLUGIN_NAME, 
connection.ID))
+       require.Equal(t, 0, len(configs))
+}
+
+func TestDeleteScopeConfig_WithReferencingScope(t *testing.T) {
+       client := CreateClient(t)
+       connection := CreateTestConnection(client)
+       scopeConfig := FakeScopeConfig{Name: "Scope config", Env: "test env", 
Entities: []string{plugin.DOMAIN_TYPE_CICD}}
+       scopeConfig = 
helper.Cast[FakeScopeConfig](client.CreateScopeConfig(PLUGIN_NAME, 
connection.ID, scopeConfig))
+
+       scope := CreateTestScope(client, &scopeConfig, connection.ID)
+       require.Equal(t, scopeConfig.Id, scope.ScopeConfigId)
+
+       client.DeleteScopeConfig(PLUGIN_NAME, connection.ID, scopeConfig.Id)
+       scope = helper.Cast[*FakeProject](client.GetScope(PLUGIN_NAME, 
connection.ID, scope.Id, false))
+       require.Equal(t, uint64(0), scope.ScopeConfigId)
+
+}
diff --git a/backend/test/helper/api.go b/backend/test/helper/api.go
index be6dfd069..f1c15483b 100644
--- a/backend/test/helper/api.go
+++ b/backend/test/helper/api.go
@@ -272,6 +272,15 @@ func (d *DevlakeClient) GetScopeConfig(pluginName string, 
connectionId uint64, s
                d.Endpoint, pluginName, connectionId, scopeConfigId), nil, nil)
 }
 
+func (d *DevlakeClient) DeleteScopeConfig(pluginName string, connectionId 
uint64, scopeConfigId uint64) {
+       sendHttpRequest[any](d.testCtx, d.timeout, &testContext{
+               client:       d,
+               printPayload: true,
+               inlineJson:   false,
+       }, http.MethodDelete, 
fmt.Sprintf("%s/plugins/%s/connections/%d/scope-configs/%d",
+               d.Endpoint, pluginName, connectionId, scopeConfigId), nil, nil)
+}
+
 func (d *DevlakeClient) RemoteScopes(query RemoteScopesQuery) 
RemoteScopesOutput {
        url := fmt.Sprintf("%s/plugins/%s/connections/%d/remote-scopes",
                d.Endpoint,
diff --git a/backend/test/helper/client.go b/backend/test/helper/client.go
index ccf9902e4..d29f70572 100644
--- a/backend/test/helper/client.go
+++ b/backend/test/helper/client.go
@@ -94,7 +94,7 @@ type (
                CreateServer    bool
                DropDb          bool
                TruncateDb      bool
-               Plugins         map[string]plugin.PluginMeta
+               Plugins         []plugin.PluginMeta
                Timeout         time.Duration
                PipelineTimeout time.Duration
        }
@@ -286,16 +286,15 @@ func (d *DevlakeClient) forceSendHttpRequest(retries 
uint, req *http.Request, on
 
 func (d *DevlakeClient) initPlugins(cfg *LocalClientConfig) {
        d.testCtx.Helper()
-       if cfg.Plugins == nil {
-               cfg.Plugins = map[string]plugin.PluginMeta{}
-       }
        // default plugins
-       cfg.Plugins["org"] = org.Org{}
-       cfg.Plugins["dora"] = dora.Dora{}
-       cfg.Plugins["refdiff"] = refdiff.RefDiff{}
+       cfg.Plugins = append(cfg.Plugins, []plugin.PluginMeta{
+               org.Org{},
+               dora.Dora{},
+               refdiff.RefDiff{},
+       }...)
        // register and init plugins
-       for name, p := range cfg.Plugins {
-               require.NoError(d.testCtx, plugin.RegisterPlugin(name, p))
+       for _, p := range cfg.Plugins {
+               require.NoError(d.testCtx, plugin.RegisterPlugin(p.Name(), p))
        }
        for _, p := range plugin.AllPlugins() {
                if pi, ok := p.(plugin.PluginInit); ok {
@@ -404,18 +403,19 @@ func sendHttpRequest[Res any](t *testing.T, timeout 
time.Duration, ctx *testCont
                }()
                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))
+                               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))
                        }
                }
                b, _ = io.ReadAll(response.Body)
                if err = json.Unmarshal(b, &result); err != nil {
-                       return false, errors.Convert(err)
+                       if response.StatusCode < 300 {
+                               return false, errors.Convert(err)
+                       }
+                       // it's probably ok since the request failed anyway
                }
                if ctx.printPayload {
                        coloredPrintf("result: %s\n", 
ToCleanJson(ctx.inlineJson, b))
diff --git a/backend/test/helper/client_factory.go 
b/backend/test/helper/client_factory.go
index a4aa1c4c0..cbd58dc14 100644
--- a/backend/test/helper/client_factory.go
+++ b/backend/test/helper/client_factory.go
@@ -24,7 +24,7 @@ import (
 )
 
 // Creates a new in-memory DevLake server with default settings and returns a 
client to it
-func StartDevLakeServer(t *testing.T, loadedGoPlugins 
map[string]plugin.PluginMeta) *DevlakeClient {
+func StartDevLakeServer(t *testing.T, loadedGoPlugins []plugin.PluginMeta) 
*DevlakeClient {
        client := ConnectLocalServer(t, &LocalClientConfig{
                ServerPort:   8089,
                DbURL:        config.GetConfig().GetString("E2E_DB_URL"),

Reply via email to