You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2023/05/26 07:21:50 UTC

[incubator-devlake] branch main updated: [feat-5277]: Delete Scopes support for all plugins (#5290)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new b6c9e6cf6 [feat-5277]: Delete Scopes support for all plugins (#5290)
b6c9e6cf6 is described below

commit b6c9e6cf6aa4a0e07443c6f98eea2e7adf528e41
Author: Keon Amini <ke...@merico.dev>
AuthorDate: Fri May 26 02:21:45 2023 -0500

    [feat-5277]: Delete Scopes support for all plugins (#5290)
    
    * refactor: delete scopes won't return blueprints anymore because that's not needed
    
    * refactor: Plugins adapted to use the new ScopeHelper + DeleteScopes implemented + Swagger docs updated
---
 .../pluginhelper/api/scope_generic_helper.go       | 37 ++++++++++-----------
 backend/helpers/pluginhelper/api/scope_helper.go   | 35 +++-----------------
 .../helpers/pluginhelper/api/scope_helper_test.go  |  2 +-
 backend/plugins/bamboo/api/init.go                 |  8 +++++
 backend/plugins/bamboo/api/scope.go                | 20 ++++++++++--
 backend/plugins/bamboo/impl/impl.go                |  5 +--
 backend/plugins/bitbucket/api/init.go              |  8 +++++
 backend/plugins/bitbucket/api/scope.go             | 20 ++++++++++--
 backend/plugins/bitbucket/impl/impl.go             |  5 +--
 backend/plugins/github/api/init.go                 | 27 +++++++++++++++
 backend/plugins/github/api/scope.go                | 20 ++++++++++--
 backend/plugins/github/impl/impl.go                |  5 +--
 backend/plugins/gitlab/api/init.go                 |  9 ++++-
 backend/plugins/gitlab/api/scope.go                | 20 ++++++++++--
 backend/plugins/gitlab/impl/impl.go                |  5 +--
 backend/plugins/jenkins/api/init.go                |  8 +++++
 backend/plugins/jenkins/api/scope.go               | 20 ++++++++++--
 backend/plugins/jenkins/impl/impl.go               |  5 +--
 backend/plugins/jira/api/init.go                   |  9 ++++-
 backend/plugins/jira/api/scope.go                  | 20 ++++++++++--
 backend/plugins/jira/impl/impl.go                  |  5 +--
 backend/plugins/pagerduty/api/init.go              |  2 +-
 backend/plugins/pagerduty/api/scope.go             |  6 ++--
 backend/plugins/sonarqube/api/init.go              | 10 +++++-
 backend/plugins/sonarqube/api/scope.go             | 20 ++++++++++--
 backend/plugins/sonarqube/impl/impl.go             |  5 +--
 backend/plugins/tapd/api/init.go                   |  8 +++++
 backend/plugins/tapd/api/scope.go                  | 20 ++++++++++--
 backend/plugins/tapd/impl/impl.go                  |  5 +--
 backend/plugins/trello/api/scope.go                | 16 +++++++++
 backend/plugins/trello/impl/impl.go                |  5 +--
 backend/plugins/zentao/api/init.go                 | 16 +++++++++
 backend/plugins/zentao/api/scope.go                | 38 +++++++++++++++++++---
 backend/plugins/zentao/impl/impl.go                | 10 +++---
 backend/server/services/remote/plugin/scope_api.go |  4 +--
 35 files changed, 356 insertions(+), 102 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/scope_generic_helper.go b/backend/helpers/pluginhelper/api/scope_generic_helper.go
index 20b8be0eb..ede430882 100644
--- a/backend/helpers/pluginhelper/api/scope_generic_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_generic_helper.go
@@ -270,17 +270,17 @@ func (c *GenericScopeApiHelper[Conn, Scope, Tr]) GetScope(input *plugin.ApiResou
 	return scopeRes, nil
 }
 
-func (c *GenericScopeApiHelper[Conn, Scope, Tr]) DeleteScope(input *plugin.ApiResourceInput) ([]*models.Blueprint, errors.Error) {
+func (c *GenericScopeApiHelper[Conn, Scope, Tr]) DeleteScope(input *plugin.ApiResourceInput) errors.Error {
 	params := c.extractFromDeleteReqParam(input)
 	if params == nil || params.connectionId == 0 {
-		return nil, errors.BadInput.New("invalid path params: \"connectionId\" not set")
+		return errors.BadInput.New("invalid path params: \"connectionId\" not set")
 	}
 	if len(params.scopeId) == 0 || params.scopeId == "0" {
-		return nil, errors.BadInput.New("invalid path params: \"scopeId\" not set/invalid")
+		return errors.BadInput.New("invalid path params: \"scopeId\" not set/invalid")
 	}
 	err := c.dbHelper.VerifyConnection(params.connectionId)
 	if err != nil {
-		return nil, errors.Default.Wrap(err, fmt.Sprintf("error verifying connection for connection ID %d", params.connectionId))
+		return errors.Default.Wrap(err, fmt.Sprintf("error verifying connection for connection ID %d", params.connectionId))
 	}
 	// delete all the plugin records referencing this scope
 	if c.reflectionParams.RawScopeParamName != "" {
@@ -288,32 +288,31 @@ func (c *GenericScopeApiHelper[Conn, Scope, Tr]) DeleteScope(input *plugin.ApiRe
 		if c.opts.GetScopeParamValue != nil {
 			scopeParamValue, err = c.opts.GetScopeParamValue(c.db, params.scopeId)
 			if err != nil {
-				return nil, errors.Default.Wrap(err, fmt.Sprintf("error extracting scope parameter name for scope %s", params.scopeId))
+				return errors.Default.Wrap(err, fmt.Sprintf("error extracting scope parameter name for scope %s", params.scopeId))
 			}
 		}
 		// find all tables for this plugin
 		tables, err := getAffectedTables(params.plugin)
 		if err != nil {
-			return nil, errors.Default.Wrap(err, fmt.Sprintf("error getting database tables managed by plugin %s", params.plugin))
+			return errors.Default.Wrap(err, fmt.Sprintf("error getting database tables managed by plugin %s", params.plugin))
 		}
 		err = c.transactionalDelete(tables, scopeParamValue)
 		if err != nil {
-			return nil, errors.Default.Wrap(err, fmt.Sprintf("error deleting data bound to scope %s for plugin %s", params.scopeId, params.plugin))
+			return errors.Default.Wrap(err, fmt.Sprintf("error deleting data bound to scope %s for plugin %s", params.scopeId, params.plugin))
 		}
 	}
-	var impactedBlueprints []*models.Blueprint
 	if !params.deleteDataOnly {
 		// Delete the scope itself
 		err = c.dbHelper.DeleteScope(params.connectionId, params.scopeId)
 		if err != nil {
-			return nil, errors.Default.Wrap(err, fmt.Sprintf("error deleting scope %s", params.scopeId))
+			return errors.Default.Wrap(err, fmt.Sprintf("error deleting scope %s", params.scopeId))
 		}
-		impactedBlueprints, err = c.updateBlueprints(params.connectionId, params.scopeId)
+		err = c.updateBlueprints(params.connectionId, params.scopeId)
 		if err != nil {
-			return nil, err
+			return err
 		}
 	}
-	return impactedBlueprints, nil
+	return nil
 }
 
 func (c *GenericScopeApiHelper[Conn, Scope, Tr]) addTransformationName(scopes ...*Scope) ([]*ScopeRes[Scope], errors.Error) {
@@ -507,13 +506,12 @@ func (c *GenericScopeApiHelper[Conn, Scope, Tr]) validatePrimaryKeys(scopes []*S
 	return nil
 }
 
-func (c *GenericScopeApiHelper[Conn, Scope, Tr]) updateBlueprints(connectionId uint64, scopeId string) ([]*models.Blueprint, errors.Error) {
+func (c *GenericScopeApiHelper[Conn, Scope, Tr]) updateBlueprints(connectionId uint64, scopeId string) errors.Error {
 	blueprintsMap, err := c.bpManager.GetBlueprintsByScopes(connectionId, scopeId)
 	if err != nil {
-		return nil, errors.Default.Wrap(err, fmt.Sprintf("error retrieving scope with scope ID %s", scopeId))
+		return errors.Default.Wrap(err, fmt.Sprintf("error retrieving scope with scope ID %s", scopeId))
 	}
 	blueprints := blueprintsMap[scopeId]
-	var impactedBlueprints []*models.Blueprint
 	// update the blueprints (remove scope reference from them)
 	for _, blueprint := range blueprints {
 		settings, _ := blueprint.UnmarshalSettings()
@@ -531,21 +529,20 @@ func (c *GenericScopeApiHelper[Conn, Scope, Tr]) updateBlueprints(connectionId u
 			return nil
 		})
 		if err != nil {
-			return nil, errors.Default.Wrap(err, fmt.Sprintf("error removing scope %s from blueprint %d", scopeId, blueprint.ID))
+			return errors.Default.Wrap(err, fmt.Sprintf("error removing scope %s from blueprint %d", scopeId, blueprint.ID))
 		}
 		if changed {
 			err = blueprint.UpdateSettings(&settings)
 			if err != nil {
-				return nil, errors.Default.Wrap(err, fmt.Sprintf("error writing new settings into blueprint %s", blueprint.Name))
+				return errors.Default.Wrap(err, fmt.Sprintf("error writing new settings into blueprint %s", blueprint.Name))
 			}
 			err = c.bpManager.SaveDbBlueprint(blueprint)
 			if err != nil {
-				return nil, errors.Default.Wrap(err, fmt.Sprintf("error saving the updated blueprint %s", blueprint.Name))
+				return errors.Default.Wrap(err, fmt.Sprintf("error saving the updated blueprint %s", blueprint.Name))
 			}
-			impactedBlueprints = append(impactedBlueprints, blueprint)
 		}
 	}
-	return impactedBlueprints, nil
+	return nil
 }
 
 func (c *GenericScopeApiHelper[Conn, Scope, Tr]) transactionalDelete(tables []string, scopeId string) errors.Error {
diff --git a/backend/helpers/pluginhelper/api/scope_helper.go b/backend/helpers/pluginhelper/api/scope_helper.go
index 51439f2d3..c95284dda 100644
--- a/backend/helpers/pluginhelper/api/scope_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_helper.go
@@ -41,25 +41,8 @@ type (
 	}
 )
 
-// Kept for backward compatibility. Use NewScopeHelper2 instead until we do a mass refactor
-func NewScopeHelper[Conn any, Scope any, Tr any](
-	basicRes context.BasicRes,
-	vld *validator.Validate,
-	connHelper *ConnectionApiHelper,
-) *ScopeApiHelper[Conn, Scope, Tr] {
-	reflectionParams := ReflectionParameters{}
-	return NewScopeHelper2[Conn, Scope, Tr](
-		basicRes,
-		vld,
-		connHelper,
-		NewScopeDatabaseHelperImpl[Conn, Scope, Tr](basicRes, connHelper, &reflectionParams),
-		&reflectionParams,
-		nil,
-	)
-}
-
 // NewScopeHelper creates a ScopeHelper for scopes management
-func NewScopeHelper2[Conn any, Scope any, Tr any](
+func NewScopeHelper[Conn any, Scope any, Tr any](
 	basicRes context.BasicRes,
 	vld *validator.Validate,
 	connHelper *ConnectionApiHelper,
@@ -92,11 +75,7 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) Put(input *plugin.ApiResourceInput) (*
 	return &plugin.ApiResourceOutput{Body: apiScopes, Status: http.StatusOK}, nil
 }
 
-// TODO remove fieldName param in the future and adjust plugins to use reflection params on init
-func (c *ScopeApiHelper[Conn, Scope, Tr]) Update(input *plugin.ApiResourceInput, fieldName string) (*plugin.ApiResourceOutput, errors.Error) {
-	if fieldName != "" {
-		c.reflectionParams.ScopeIdColumnName = fieldName //for backward compatibility
-	}
+func (c *ScopeApiHelper[Conn, Scope, Tr]) Update(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	apiScope, err := c.GenericScopeApiHelper.UpdateScope(input)
 	if err != nil {
 		return nil, err
@@ -112,11 +91,7 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScopeList(input *plugin.ApiResource
 	return &plugin.ApiResourceOutput{Body: scopes, Status: http.StatusOK}, nil
 }
 
-// TODO remove fieldName param in the future and adjust plugins to use reflection params on init
-func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScope(input *plugin.ApiResourceInput, fieldName string) (*plugin.ApiResourceOutput, errors.Error) {
-	if fieldName != "" {
-		c.reflectionParams.ScopeIdColumnName = fieldName //for backward compatibility
-	}
+func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	scope, err := c.GenericScopeApiHelper.GetScope(input)
 	if err != nil {
 		return nil, err
@@ -125,9 +100,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) {
-	bps, err := c.DeleteScope(input)
+	err := c.DeleteScope(input)
 	if err != nil {
 		return nil, err
 	}
-	return &plugin.ApiResourceOutput{Body: bps, Status: http.StatusOK}, nil
+	return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
 }
diff --git a/backend/helpers/pluginhelper/api/scope_helper_test.go b/backend/helpers/pluginhelper/api/scope_helper_test.go
index e812d1ab2..86f934801 100644
--- a/backend/helpers/pluginhelper/api/scope_helper_test.go
+++ b/backend/helpers/pluginhelper/api/scope_helper_test.go
@@ -298,7 +298,7 @@ func TestScopeApiHelper_Put(t *testing.T) {
 	params := &ReflectionParameters{}
 	dbHelper := NewScopeDatabaseHelperImpl[TestConnection, TestRepo, TestTransformationRule](mockRes, connHelper, params)
 	// create a mock ScopeApiHelper with a mock database connection
-	apiHelper := NewScopeHelper2[TestConnection, TestRepo, TestTransformationRule](mockRes, nil, connHelper, dbHelper, params, nil)
+	apiHelper := NewScopeHelper[TestConnection, TestRepo, TestTransformationRule](mockRes, nil, connHelper, dbHelper, params, nil)
 	// test a successful call to Put
 	_, err := apiHelper.Put(input)
 	assert.NoError(t, err)
diff --git a/backend/plugins/bamboo/api/init.go b/backend/plugins/bamboo/api/init.go
index e6833f341..a99863e72 100644
--- a/backend/plugins/bamboo/api/init.go
+++ b/backend/plugins/bamboo/api/init.go
@@ -39,10 +39,18 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:  "ProjectKey",
+		ScopeIdColumnName: "project_key",
+	}
 	scopeHelper = api.NewScopeHelper[models.BambooConnection, models.BambooProject, models.BambooScopeConfig](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.BambooConnection, models.BambooProject, models.BambooScopeConfig](
+			basicRes, connectionHelper, params),
+		params,
+		nil,
 	)
 	remoteHelper = api.NewRemoteHelper[models.BambooConnection, models.BambooProject, models.ApiBambooProject, api.NoRemoteGroupResponse](
 		basicRes,
diff --git a/backend/plugins/bamboo/api/scope.go b/backend/plugins/bamboo/api/scope.go
index 0870eabb2..61c783cc0 100644
--- a/backend/plugins/bamboo/api/scope.go
+++ b/backend/plugins/bamboo/api/scope.go
@@ -59,7 +59,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bamboo/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input, "project_key")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get Bamboo projects
@@ -67,6 +67,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Description get Bamboo projects
 // @Tags plugins/bamboo
 // @Param connectionId path int false "connection ID"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -88,5 +89,20 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bamboo/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.GetScope(input, "project_key")
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/bamboo
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/bamboo/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
 }
diff --git a/backend/plugins/bamboo/impl/impl.go b/backend/plugins/bamboo/impl/impl.go
index dc19f9e54..d8a88d488 100644
--- a/backend/plugins/bamboo/impl/impl.go
+++ b/backend/plugins/bamboo/impl/impl.go
@@ -225,8 +225,9 @@ func (p Bamboo) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"GET": api.SearchRemoteScopes,
 		},
 		"connections/:connectionId/scopes/:scopeId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 	}
 }
diff --git a/backend/plugins/bitbucket/api/init.go b/backend/plugins/bitbucket/api/init.go
index 0337e0f8e..59a90c6a0 100644
--- a/backend/plugins/bitbucket/api/init.go
+++ b/backend/plugins/bitbucket/api/init.go
@@ -38,10 +38,18 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:  "BitbucketId",
+		ScopeIdColumnName: "bitbucket_id",
+	}
 	scopeHelper = api.NewScopeHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketTransformationRule](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketTransformationRule](
+			basicRes, connectionHelper, params),
+		params,
+		nil,
 	)
 	remoteHelper = api.NewRemoteHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketApiRepo, models.GroupResponse](
 		basicRes,
diff --git a/backend/plugins/bitbucket/api/scope.go b/backend/plugins/bitbucket/api/scope.go
index 80f4a49c1..e1709bf7e 100644
--- a/backend/plugins/bitbucket/api/scope.go
+++ b/backend/plugins/bitbucket/api/scope.go
@@ -61,7 +61,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Router /plugins/bitbucket/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
-	return scopeHelper.Update(input, "bitbucket_id")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get repos
@@ -71,6 +71,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Param connectionId path int true "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -91,5 +92,20 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Router /plugins/bitbucket/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
-	return scopeHelper.GetScope(input, "bitbucket_id")
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/bitbucket
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/bitbucket/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
 }
diff --git a/backend/plugins/bitbucket/impl/impl.go b/backend/plugins/bitbucket/impl/impl.go
index 07037e298..569ae6064 100644
--- a/backend/plugins/bitbucket/impl/impl.go
+++ b/backend/plugins/bitbucket/impl/impl.go
@@ -204,8 +204,9 @@ func (p Bitbucket) ApiResources() map[string]map[string]plugin.ApiResourceHandle
 			"GET":    api.GetConnection,
 		},
 		"connections/:connectionId/scopes/*scopeId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/remote-scopes": {
 			"GET": api.RemoteScopes,
diff --git a/backend/plugins/github/api/init.go b/backend/plugins/github/api/init.go
index 179ea7ff4..f7e1ab7b2 100644
--- a/backend/plugins/github/api/init.go
+++ b/backend/plugins/github/api/init.go
@@ -19,9 +19,12 @@ package api
 
 import (
 	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 	"github.com/apache/incubator-devlake/plugins/github/models"
 	"github.com/go-playground/validator/v10"
+	"strconv"
 )
 
 var vld *validator.Validate
@@ -37,10 +40,34 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:  "GithubId",
+		ScopeIdColumnName: "github_id",
+		RawScopeParamName: "Name",
+	}
 	scopeHelper = api.NewScopeHelper[models.GithubConnection, models.GithubRepo, models.GithubTransformationRule](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.GithubConnection, models.GithubRepo, models.GithubTransformationRule](
+			basicRes, connectionHelper, params),
+		params,
+		&api.ScopeHelperOptions{
+			GetScopeParamValue: func(db dal.Dal, scopeId string) (string, errors.Error) {
+				id, err := errors.Convert01(strconv.ParseInt(scopeId, 10, 64))
+				if err != nil {
+					return "", err
+				}
+				repo := models.GithubRepo{
+					GithubId: int(id),
+				}
+				err = db.First(&repo)
+				if err != nil {
+					return "", err
+				}
+				return repo.Name, nil
+			},
+		},
 	)
 	trHelper = api.NewTransformationRuleHelper[models.GithubTransformationRule](
 		basicRes,
diff --git a/backend/plugins/github/api/scope.go b/backend/plugins/github/api/scope.go
index 1225e9edd..3346b113c 100644
--- a/backend/plugins/github/api/scope.go
+++ b/backend/plugins/github/api/scope.go
@@ -59,7 +59,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/github/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input, "github_id")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get Github repos
@@ -69,6 +69,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Param connectionId path int true "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -88,5 +89,20 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/github/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.GetScope(input, "github_id")
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/github
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/github/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
 }
diff --git a/backend/plugins/github/impl/impl.go b/backend/plugins/github/impl/impl.go
index 3fa51360c..c5ebaae5d 100644
--- a/backend/plugins/github/impl/impl.go
+++ b/backend/plugins/github/impl/impl.go
@@ -215,8 +215,9 @@ func (p Github) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"DELETE": api.DeleteConnection,
 		},
 		"connections/:connectionId/scopes/:scopeId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/scopes": {
 			"GET": api.GetScopeList,
diff --git a/backend/plugins/gitlab/api/init.go b/backend/plugins/gitlab/api/init.go
index c29d34ac9..cbc336966 100644
--- a/backend/plugins/gitlab/api/init.go
+++ b/backend/plugins/gitlab/api/init.go
@@ -38,12 +38,19 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:  "GitlabId",
+		ScopeIdColumnName: "gitlab_id",
+	}
 	scopeHelper = api.NewScopeHelper[models.GitlabConnection, models.GitlabProject, models.GitlabTransformationRule](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.GitlabConnection, models.GitlabProject, models.GitlabTransformationRule](
+			basicRes, connectionHelper, params),
+		params,
+		nil,
 	)
-
 	remoteHelper = api.NewRemoteHelper[models.GitlabConnection, models.GitlabProject, models.GitlabApiProject, models.GroupResponse](
 		basicRes,
 		vld,
diff --git a/backend/plugins/gitlab/api/scope.go b/backend/plugins/gitlab/api/scope.go
index a40ccf6f0..32927e559 100644
--- a/backend/plugins/gitlab/api/scope.go
+++ b/backend/plugins/gitlab/api/scope.go
@@ -59,7 +59,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/gitlab/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input, "gitlab_id")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get Gitlab projects
@@ -67,6 +67,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Description get Gitlab projects
 // @Tags plugins/gitlab
 // @Param connectionId path int false "connection ID"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -88,5 +89,20 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/gitlab/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.GetScope(input, "gitlab_id")
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/gitlab
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/gitlab/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
 }
diff --git a/backend/plugins/gitlab/impl/impl.go b/backend/plugins/gitlab/impl/impl.go
index fb04f6207..2966c5b28 100644
--- a/backend/plugins/gitlab/impl/impl.go
+++ b/backend/plugins/gitlab/impl/impl.go
@@ -258,8 +258,9 @@ func (p Gitlab) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"GET":    api.GetConnection,
 		},
 		"connections/:connectionId/scopes/:scopeId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/remote-scopes": {
 			"GET": api.RemoteScopes,
diff --git a/backend/plugins/jenkins/api/init.go b/backend/plugins/jenkins/api/init.go
index d0f125dd8..8fe30ce1e 100644
--- a/backend/plugins/jenkins/api/init.go
+++ b/backend/plugins/jenkins/api/init.go
@@ -37,10 +37,18 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:  "FullName",
+		ScopeIdColumnName: "full_name",
+	}
 	scopeHelper = api.NewScopeHelper[models.JenkinsConnection, models.JenkinsJob, models.JenkinsTransformationRule](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.JenkinsConnection, models.JenkinsJob, models.JenkinsTransformationRule](
+			basicRes, connectionHelper, params),
+		params,
+		nil,
 	)
 	trHelper = api.NewTransformationRuleHelper[models.JenkinsTransformationRule](
 		basicRes,
diff --git a/backend/plugins/jenkins/api/scope.go b/backend/plugins/jenkins/api/scope.go
index 0c932a22c..525aa52cd 100644
--- a/backend/plugins/jenkins/api/scope.go
+++ b/backend/plugins/jenkins/api/scope.go
@@ -61,7 +61,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Router /plugins/jenkins/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
-	return scopeHelper.Update(input, "full_name")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get Jenkins jobs
@@ -71,6 +71,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Param connectionId path int false "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -91,5 +92,20 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Router /plugins/jenkins/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
-	return scopeHelper.GetScope(input, "full_name")
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/jenkins
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/jenkins/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
 }
diff --git a/backend/plugins/jenkins/impl/impl.go b/backend/plugins/jenkins/impl/impl.go
index 104ba38fe..f60447451 100644
--- a/backend/plugins/jenkins/impl/impl.go
+++ b/backend/plugins/jenkins/impl/impl.go
@@ -183,8 +183,9 @@ func (p Jenkins) ApiResources() map[string]map[string]plugin.ApiResourceHandler
 			"GET":    api.GetConnection,
 		},
 		"connections/:connectionId/scopes/*scopeId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/scopes": {
 			"GET": api.GetScopeList,
diff --git a/backend/plugins/jira/api/init.go b/backend/plugins/jira/api/init.go
index eec4ee406..9944236e0 100644
--- a/backend/plugins/jira/api/init.go
+++ b/backend/plugins/jira/api/init.go
@@ -37,12 +37,19 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:  "BoardID",
+		ScopeIdColumnName: "board_id",
+	}
 	scopeHelper = api.NewScopeHelper[models.JiraConnection, models.JiraBoard, models.JiraTransformationRule](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.JiraConnection, models.JiraBoard, models.JiraTransformationRule](
+			basicRes, connectionHelper, params),
+		params,
+		nil,
 	)
-
 	trHelper = api.NewTransformationRuleHelper[models.JiraTransformationRule](
 		basicRes,
 		vld,
diff --git a/backend/plugins/jira/api/scope.go b/backend/plugins/jira/api/scope.go
index 7a4fabd30..2743639eb 100644
--- a/backend/plugins/jira/api/scope.go
+++ b/backend/plugins/jira/api/scope.go
@@ -66,7 +66,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jira/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input, "board_id")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get Jira boards
@@ -76,6 +76,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Param connectionId path int false "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -95,7 +96,22 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jira/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.GetScope(input, "board_id")
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/jira
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/jira/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
 }
 
 func GetApiJira(op *tasks.JiraOptions, apiClient aha.ApiClientAbstract) (*apiv2models.Board, errors.Error) {
diff --git a/backend/plugins/jira/impl/impl.go b/backend/plugins/jira/impl/impl.go
index 7955a4dc7..c1263bef9 100644
--- a/backend/plugins/jira/impl/impl.go
+++ b/backend/plugins/jira/impl/impl.go
@@ -288,8 +288,9 @@ func (p Jira) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"GET": api.Proxy,
 		},
 		"connections/:connectionId/scopes/:scopeId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/scopes": {
 			"GET": api.GetScopeList,
diff --git a/backend/plugins/pagerduty/api/init.go b/backend/plugins/pagerduty/api/init.go
index 80c2fbcdd..84729850c 100644
--- a/backend/plugins/pagerduty/api/init.go
+++ b/backend/plugins/pagerduty/api/init.go
@@ -44,7 +44,7 @@ func Init(br context.BasicRes) {
 		ScopeIdColumnName: "id",
 		RawScopeParamName: "ScopeId",
 	}
-	scopeHelper = api.NewScopeHelper2[models.PagerDutyConnection, models.Service, models.PagerdutyTransformationRule](
+	scopeHelper = api.NewScopeHelper[models.PagerDutyConnection, models.Service, models.PagerdutyTransformationRule](
 		basicRes,
 		vld,
 		connectionHelper,
diff --git a/backend/plugins/pagerduty/api/scope.go b/backend/plugins/pagerduty/api/scope.go
index 7560fa570..02d7cc2cb 100644
--- a/backend/plugins/pagerduty/api/scope.go
+++ b/backend/plugins/pagerduty/api/scope.go
@@ -58,7 +58,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/scopes/{serviceId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input, "")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get PagerDuty repos
@@ -89,7 +89,7 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/scopes/{serviceId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.GetScope(input, "")
+	return scopeHelper.GetScope(input)
 }
 
 // DeleteScope delete plugin data associated with the scope and optionally the scope itself
@@ -99,7 +99,7 @@ func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Param connectionId path int true "connection ID"
 // @Param serviceId path int true "service ID"
 // @Param delete_data_only query bool false "Only delete the scope data, not the scope itself"
-// @Success 200  {object} []models.Blueprint "list of blueprints impacted by the deletion"
+// @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/scopes/{serviceId} [DELETE]
diff --git a/backend/plugins/sonarqube/api/init.go b/backend/plugins/sonarqube/api/init.go
index 37bf1fcfb..2a5273b04 100644
--- a/backend/plugins/sonarqube/api/init.go
+++ b/backend/plugins/sonarqube/api/init.go
@@ -37,10 +37,18 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
-	scopeHelper = api.NewScopeHelper[models.SonarqubeConnection, models.SonarqubeProject, interface{}](
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:  "ProjectKey",
+		ScopeIdColumnName: "project_key",
+	}
+	scopeHelper = api.NewScopeHelper[models.SonarqubeConnection, models.SonarqubeProject, any](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.SonarqubeConnection, models.SonarqubeProject, any](
+			basicRes, connectionHelper, params),
+		params,
+		nil,
 	)
 	remoteHelper = api.NewRemoteHelper[models.SonarqubeConnection, models.SonarqubeProject, models.SonarqubeApiProject, api.NoRemoteGroupResponse](
 		basicRes,
diff --git a/backend/plugins/sonarqube/api/scope.go b/backend/plugins/sonarqube/api/scope.go
index e2946eec3..96184a9c2 100644
--- a/backend/plugins/sonarqube/api/scope.go
+++ b/backend/plugins/sonarqube/api/scope.go
@@ -59,7 +59,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input, "project_key")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get Sonarqube projects
@@ -69,6 +69,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Param connectionId path int false "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -88,5 +89,20 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.GetScope(input, "project_key")
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/sonarqube
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
 }
diff --git a/backend/plugins/sonarqube/impl/impl.go b/backend/plugins/sonarqube/impl/impl.go
index 0457685e3..a63d0d273 100644
--- a/backend/plugins/sonarqube/impl/impl.go
+++ b/backend/plugins/sonarqube/impl/impl.go
@@ -173,8 +173,9 @@ func (p Sonarqube) ApiResources() map[string]map[string]plugin.ApiResourceHandle
 			"GET": api.SearchRemoteScopes,
 		},
 		"connections/:connectionId/scopes/:scopeId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/scopes": {
 			"GET": api.GetScopeList,
diff --git a/backend/plugins/tapd/api/init.go b/backend/plugins/tapd/api/init.go
index 267042567..42cf79f91 100644
--- a/backend/plugins/tapd/api/init.go
+++ b/backend/plugins/tapd/api/init.go
@@ -38,10 +38,18 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:  "Id",
+		ScopeIdColumnName: "id",
+	}
 	scopeHelper = api.NewScopeHelper[models.TapdConnection, models.TapdWorkspace, models.TapdTransformationRule](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.TapdConnection, models.TapdWorkspace, models.TapdTransformationRule](
+			basicRes, connectionHelper, params),
+		params,
+		nil,
 	)
 	remoteHelper = api.NewRemoteHelper[models.TapdConnection, models.TapdWorkspace, models.TapdWorkspace, api.BaseRemoteGroupResponse](
 		basicRes,
diff --git a/backend/plugins/tapd/api/scope.go b/backend/plugins/tapd/api/scope.go
index d5d41d828..5ed0bd109 100644
--- a/backend/plugins/tapd/api/scope.go
+++ b/backend/plugins/tapd/api/scope.go
@@ -60,7 +60,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/tapd/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input, "id")
+	return scopeHelper.Update(input)
 }
 
 // GetScopeList get tapd jobs
@@ -70,6 +70,7 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Param connectionId path int false "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
 // @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -90,5 +91,20 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Router /plugins/tapd/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
-	return scopeHelper.GetScope(input, "id")
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/tapd
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/tapd/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
 }
diff --git a/backend/plugins/tapd/impl/impl.go b/backend/plugins/tapd/impl/impl.go
index 97ed805af..5bf0e7f1f 100644
--- a/backend/plugins/tapd/impl/impl.go
+++ b/backend/plugins/tapd/impl/impl.go
@@ -278,8 +278,9 @@ func (p Tapd) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"GET": api.Proxy,
 		},
 		"connections/:connectionId/scopes/:scopeId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/remote-scopes-prepare-token": {
 			"GET": api.PrepareFirstPageToken,
diff --git a/backend/plugins/trello/api/scope.go b/backend/plugins/trello/api/scope.go
index 7f9675780..3c2769612 100644
--- a/backend/plugins/trello/api/scope.go
+++ b/backend/plugins/trello/api/scope.go
@@ -197,6 +197,22 @@ func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 	return &plugin.ApiResourceOutput{Body: apiBoard{board, rule.Name}, Status: http.StatusOK}, nil
 }
 
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/trello
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/trello/connections/{connectionId}/scopes/{scopeId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	// This plugin needs to be redone in terms of helpers later
+	return &plugin.ApiResourceOutput{Status: http.StatusNotFound}, nil
+}
+
 func extractParam(params map[string]string) (uint64, string) {
 	connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
 	return connectionId, params["boardId"]
diff --git a/backend/plugins/trello/impl/impl.go b/backend/plugins/trello/impl/impl.go
index 4ac8730fb..2d508d340 100644
--- a/backend/plugins/trello/impl/impl.go
+++ b/backend/plugins/trello/impl/impl.go
@@ -167,8 +167,9 @@ func (p Trello) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"GET":   api.GetTransformationRule,
 		},
 		"connections/:connectionId/scopes/:boardId": {
-			"GET":   api.GetScope,
-			"PATCH": api.UpdateScope,
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/scopes": {
 			"GET": api.GetScopeList,
diff --git a/backend/plugins/zentao/api/init.go b/backend/plugins/zentao/api/init.go
index 1f3f51bb2..440ea3e56 100644
--- a/backend/plugins/zentao/api/init.go
+++ b/backend/plugins/zentao/api/init.go
@@ -45,15 +45,31 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	productParams := &api.ReflectionParameters{
+		ScopeIdFieldName:  "Id",
+		ScopeIdColumnName: "id",
+	}
 	productScopeHelper = api.NewScopeHelper[models.ZentaoConnection, models.ZentaoProduct, api.NoTransformation](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.ZentaoConnection, models.ZentaoProduct, api.NoTransformation](
+			basicRes, connectionHelper, productParams),
+		productParams,
+		nil,
 	)
+	projectParams := &api.ReflectionParameters{
+		ScopeIdFieldName:  "Project",
+		ScopeIdColumnName: "project",
+	}
 	projectScopeHelper = api.NewScopeHelper[models.ZentaoConnection, models.ZentaoProject, api.NoTransformation](
 		basicRes,
 		vld,
 		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.ZentaoConnection, models.ZentaoProject, api.NoTransformation](
+			basicRes, connectionHelper, projectParams),
+		projectParams,
+		nil,
 	)
 	productRemoteHelper = api.NewRemoteHelper[models.ZentaoConnection, models.ZentaoProduct, models.ZentaoProductRes, api.BaseRemoteGroupResponse](
 		basicRes,
diff --git a/backend/plugins/zentao/api/scope.go b/backend/plugins/zentao/api/scope.go
index 974ec8ec0..f2a03019b 100644
--- a/backend/plugins/zentao/api/scope.go
+++ b/backend/plugins/zentao/api/scope.go
@@ -81,7 +81,7 @@ func PutProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId}/scopes/product/{scopeId} [PATCH]
 func UpdateProductScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return productScopeHelper.Update(input, "id")
+	return productScopeHelper.Update(input)
 }
 
 // UpdateProjectScope patch to zentao project
@@ -97,7 +97,7 @@ func UpdateProductScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutp
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId}/scopes/project/{scopeId} [PATCH]
 func UpdateProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return projectScopeHelper.Update(input, "id")
+	return projectScopeHelper.Update(input)
 }
 
 // TODO GetScopeList get zentao projects and products
@@ -113,7 +113,7 @@ func UpdateProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutp
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId}/scopes/product/{scopeId} [GET]
 func GetProductScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return productScopeHelper.GetScope(input, "id")
+	return productScopeHelper.GetScope(input)
 }
 
 // GetProjectScope get one project
@@ -127,5 +127,35 @@ func GetProductScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId}/scopes/project/{scopeId} [GET]
 func GetProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return projectScopeHelper.GetScope(input, "id")
+	return projectScopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/zentao
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/zentao/connections/{connectionId}/scopes/product/{scopeId} [DELETE]
+func DeleteProductScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return productScopeHelper.Delete(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/zentao
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @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 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/zentao/connections/{connectionId}/scopes/project/{scopeId} [DELETE]
+func DeleteProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return projectScopeHelper.Delete(input)
 }
diff --git a/backend/plugins/zentao/impl/impl.go b/backend/plugins/zentao/impl/impl.go
index 56b8257e0..9433d0919 100644
--- a/backend/plugins/zentao/impl/impl.go
+++ b/backend/plugins/zentao/impl/impl.go
@@ -164,12 +164,14 @@ func (p Zentao) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"PUT": api.PutProjectScope,
 		},
 		"connections/:connectionId/scopes/product/:scopeId": {
-			"GET":   api.GetProductScope,
-			"PATCH": api.UpdateProductScope,
+			"GET":    api.GetProductScope,
+			"PATCH":  api.UpdateProductScope,
+			"DELETE": api.DeleteProductScope,
 		},
 		"connections/:connectionId/scopes/project/:scopeId": {
-			"GET":   api.GetProjectScope,
-			"PATCH": api.UpdateProjectScope,
+			"GET":    api.GetProjectScope,
+			"PATCH":  api.UpdateProjectScope,
+			"DELETE": api.DeleteProjectScope,
 		},
 		"connections/:connectionId/remote-scopes": {
 			"GET": api.RemoteScopes,
diff --git a/backend/server/services/remote/plugin/scope_api.go b/backend/server/services/remote/plugin/scope_api.go
index 77fd71a11..0d1839fca 100644
--- a/backend/server/services/remote/plugin/scope_api.go
+++ b/backend/server/services/remote/plugin/scope_api.go
@@ -94,11 +94,11 @@ func (pa *pluginAPI) GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResour
 }
 
 func (pa *pluginAPI) DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	bps, err := scopeHelper.DeleteScope(input)
+	err := scopeHelper.DeleteScope(input)
 	if err != nil {
 		return nil, err
 	}
-	return &plugin.ApiResourceOutput{Body: bps, Status: http.StatusOK}, nil
+	return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
 }
 
 // convertScopeResponse adapt the "remote" scopes to a serializable api.ScopeRes