You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by wa...@apache.org on 2023/03/14 13:43:32 UTC

[incubator-devlake] branch main updated: refactor(bitbucket): refactor scope (#4661)

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

warren 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 615a88f48 refactor(bitbucket): refactor scope (#4661)
615a88f48 is described below

commit 615a88f4865fb149666facde25fcac9ea8a6d78e
Author: Warren Chen <yi...@merico.dev>
AuthorDate: Tue Mar 14 21:43:26 2023 +0800

    refactor(bitbucket): refactor scope (#4661)
---
 backend/helpers/pluginhelper/api/scope_helper.go   |  46 +-----
 .../helpers/pluginhelper/api/scope_helper_test.go  |  41 -----
 backend/plugins/bamboo/api/init.go                 |  13 +-
 backend/plugins/bamboo/api/remote.go               |   5 +
 backend/plugins/bamboo/api/scope.go                | 171 ++-------------------
 backend/plugins/bamboo/impl/impl.go                |   2 +-
 backend/plugins/bamboo/models/project.go           |   4 +-
 backend/plugins/bitbucket/api/init.go              |   7 +
 backend/plugins/bitbucket/api/remote.go            |   5 +
 backend/plugins/bitbucket/api/scope.go             | 147 ++----------------
 backend/plugins/bitbucket/impl/impl.go             |   2 +-
 backend/plugins/bitbucket/models/repo.go           |   4 +-
 backend/plugins/github/api/scope.go                |   5 +-
 backend/plugins/github/models/repo.go              |   2 +-
 backend/plugins/gitlab/api/scope.go                |  13 +-
 backend/plugins/gitlab/impl/impl.go                |   2 +-
 backend/plugins/gitlab/models/project.go           |   2 +-
 17 files changed, 78 insertions(+), 393 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/scope_helper.go b/backend/helpers/pluginhelper/api/scope_helper.go
index 04244290e..b153dfe27 100644
--- a/backend/helpers/pluginhelper/api/scope_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_helper.go
@@ -66,7 +66,7 @@ func NewScopeHelper[Conn any, Scope any, Tr any](
 
 type ScopeRes[T any] struct {
 	Scope                  T      `mapstructure:",squash"`
-	TransformationRuleName string `json:"transformationRuleName,omitempty"`
+	TransformationRuleName string `mapstructure:"transformationRuleName,omitempty"`
 }
 
 type ScopeReq[T any] struct {
@@ -331,9 +331,9 @@ func VerifyScope(scope interface{}, vld *validator.Validate) errors.Error {
 }
 
 // Implement MarshalJSON method to flatten all fields
-func (sr ScopeRes[T]) MarshalJSON() ([]byte, error) {
-	// Create an empty map to store flattened fields and values
-	flatMap, err := flattenStruct(sr)
+func (sr *ScopeRes[T]) MarshalJSON() ([]byte, error) {
+	var flatMap map[string]interface{}
+	err := mapstructure.Decode(sr, &flatMap)
 	if err != nil {
 		return nil, err
 	}
@@ -345,41 +345,3 @@ func (sr ScopeRes[T]) MarshalJSON() ([]byte, error) {
 
 	return result, nil
 }
-
-// A helper function to flatten nested structs
-func flattenStruct(s interface{}) (map[string]interface{}, error) {
-	flatMap := make(map[string]interface{})
-
-	// Use reflection to get all fields of the nested struct type
-	fields := reflect.TypeOf(s).NumField()
-
-	// Traverse all fields of the nested struct and add them to flatMap
-	for i := 0; i < fields; i++ {
-		field := reflect.TypeOf(s).Field(i)
-		fieldValue := reflect.ValueOf(s).Field(i)
-		if strings.Contains(field.Tag.Get("swaggerignore"), "true") {
-			continue
-		}
-		if fieldValue.IsZero() && strings.Contains(field.Tag.Get("json"), "omitempty") {
-			continue
-		}
-		// If the field is a nested struct, recursively flatten its fields
-		if field.Type.Kind() == reflect.Struct && strings.Contains(field.Tag.Get("mapstructure"), "squash") {
-			nestedFields, err := flattenStruct(fieldValue.Interface())
-			if err != nil {
-				return nil, err
-			}
-			for k, v := range nestedFields {
-				flatMap[lowerCaseFirst(k)] = v
-			}
-		} else {
-			// If the field is not a nested struct, add its name and value to flatMap
-			flatMap[lowerCaseFirst(field.Name)] = fieldValue.Interface()
-		}
-	}
-	return flatMap, nil
-}
-
-func lowerCaseFirst(name string) string {
-	return strings.ToLower(string(name[0])) + name[1:]
-}
diff --git a/backend/helpers/pluginhelper/api/scope_helper_test.go b/backend/helpers/pluginhelper/api/scope_helper_test.go
index cc0657eea..c90afffa4 100644
--- a/backend/helpers/pluginhelper/api/scope_helper_test.go
+++ b/backend/helpers/pluginhelper/api/scope_helper_test.go
@@ -291,44 +291,3 @@ func TestScopeApiHelper_Put(t *testing.T) {
 	_, err := apiHelper.Put(input)
 	assert.NoError(t, err)
 }
-
-func TestFlattenStruct(t *testing.T) {
-	type InnerStruct struct {
-		Foo int
-		Bar string
-	}
-
-	type OuterStruct struct {
-		Baz       bool
-		Qux       float64
-		Inner     InnerStruct `mapstructure:",squash"`
-		OtherProp string
-	}
-
-	input := OuterStruct{
-		Baz: true,
-		Qux: 3.14,
-		Inner: InnerStruct{
-			Foo: 42,
-			Bar: "hello",
-		},
-		OtherProp: "world",
-	}
-
-	expectedOutput := map[string]interface{}{
-		"baz":       true,
-		"qux":       3.14,
-		"foo":       42,
-		"bar":       "hello",
-		"otherProp": "world",
-	}
-
-	output, err := flattenStruct(input)
-	if err != nil {
-		t.Errorf("flattenStruct returned an error: %v", err)
-	}
-
-	if !reflect.DeepEqual(output, expectedOutput) {
-		t.Errorf("flattenStruct returned incorrect output.\nExpected: %v\nActual:   %v", expectedOutput, output)
-	}
-}
diff --git a/backend/plugins/bamboo/api/init.go b/backend/plugins/bamboo/api/init.go
index cd019a36f..3044a56ba 100644
--- a/backend/plugins/bamboo/api/init.go
+++ b/backend/plugins/bamboo/api/init.go
@@ -19,19 +19,26 @@ package api
 
 import (
 	"github.com/apache/incubator-devlake/core/context"
-	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/github/models"
 	"github.com/go-playground/validator/v10"
 )
 
 var vld *validator.Validate
-var connectionHelper *helper.ConnectionApiHelper
+var connectionHelper *api.ConnectionApiHelper
+var scopeHelper *api.ScopeApiHelper[models.GithubConnection, models.GithubRepo, models.GithubTransformationRule]
 var basicRes context.BasicRes
 
 func Init(br context.BasicRes) {
 	basicRes = br
 	vld = validator.New()
-	connectionHelper = helper.NewConnectionHelper(
+	connectionHelper = api.NewConnectionHelper(
 		basicRes,
 		vld,
 	)
+	scopeHelper = api.NewScopeHelper[models.GithubConnection, models.GithubRepo, models.GithubTransformationRule](
+		basicRes,
+		vld,
+		connectionHelper,
+	)
 }
diff --git a/backend/plugins/bamboo/api/remote.go b/backend/plugins/bamboo/api/remote.go
index 250571390..91551001c 100644
--- a/backend/plugins/bamboo/api/remote.go
+++ b/backend/plugins/bamboo/api/remote.go
@@ -300,3 +300,8 @@ func GetQueryForSearchProject(search string, page int, perPage int) url.Values {
 
 	return query
 }
+func extractParam(params map[string]string) (uint64, string) {
+	connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
+	projectKey := params["projectKey"]
+	return connectionId, projectKey
+}
diff --git a/backend/plugins/bamboo/api/scope.go b/backend/plugins/bamboo/api/scope.go
index 703f20a39..3b318da8f 100644
--- a/backend/plugins/bamboo/api/scope.go
+++ b/backend/plugins/bamboo/api/scope.go
@@ -18,26 +18,18 @@ limitations under the License.
 package api
 
 import (
-	"context"
-	"net/http"
-	"strconv"
-
-	"github.com/apache/incubator-devlake/core/dal"
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 	"github.com/apache/incubator-devlake/plugins/bamboo/models"
-	"github.com/mitchellh/mapstructure"
 )
 
-type apiProject struct {
+type ScopeRes struct {
 	models.BambooProject
 	TransformationRuleName string `json:"transformationRuleName,omitempty"`
 }
 
-type req struct {
-	Data []*models.BambooProject `json:"data"`
-}
+type ScopeReq api.ScopeReq[models.BambooProject]
 
 // PutScope create or update bamboo project
 // @Summary create or update bamboo project
@@ -45,40 +37,13 @@ type req struct {
 // @Tags plugins/bamboo
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param scope body req true "json"
+// @Param scope body ScopeReq true "json"
 // @Success 200  {object} []models.BambooProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bamboo/connections/{connectionId}/scopes [PUT]
 func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connectionId, _ := extractParam(input.Params)
-
-	if connectionId == 0 {
-		return nil, errors.BadInput.New("invalid connectionId")
-	}
-	var projects req
-	err := errors.Convert(mapstructure.Decode(input.Body, &projects))
-	if err != nil {
-		return nil, errors.BadInput.Wrap(err, "decoding Bamboo project error")
-	}
-	keeper := make(map[string]struct{})
-	for _, project := range projects.Data {
-		if _, ok := keeper[project.ProjectKey]; ok {
-			return nil, errors.BadInput.New("duplicated item")
-		} else {
-			keeper[project.ProjectKey] = struct{}{}
-		}
-		project.ConnectionId = connectionId
-		err = verifyProject(project)
-		if err != nil {
-			return nil, err
-		}
-	}
-	err = basicRes.GetDal().CreateOrUpdate(projects.Data)
-	if err != nil {
-		return nil, errors.Default.Wrap(err, "error on saving BambooProject")
-	}
-	return &plugin.ApiResourceOutput{Body: projects.Data, Status: http.StatusOK}, nil
+	return scopeHelper.Put(input)
 }
 
 // UpdateScope patch to bamboo project
@@ -87,35 +52,14 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Tags plugins/bamboo
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param projectKey path int false "project ID"
+// @Param scopeId path int false "project ID"
 // @Param scope body models.BambooProject true "json"
 // @Success 200  {object} models.BambooProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bamboo/connections/{connectionId}/scopes/{projectKey} [PATCH]
+// @Router /plugins/bamboo/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connectionId, projectKey := extractParam(input.Params)
-	if connectionId == 0 || projectKey == "" {
-		return nil, errors.BadInput.New("invalid path params")
-	}
-	var project models.BambooProject
-	err := basicRes.GetDal().First(&project, dal.Where("connection_id = ? AND project_key = ?", connectionId, projectKey))
-	if err != nil {
-		return nil, errors.Default.Wrap(err, "getting BambooProject error")
-	}
-	err = api.DecodeMapStruct(input.Body, &project)
-	if err != nil {
-		return nil, errors.Default.Wrap(err, "patch bamboo project error")
-	}
-	err = verifyProject(&project)
-	if err != nil {
-		return nil, err
-	}
-	err = basicRes.GetDal().Update(project)
-	if err != nil {
-		return nil, errors.Default.Wrap(err, "error on saving BambooProject")
-	}
-	return &plugin.ApiResourceOutput{Body: project, Status: http.StatusOK}, nil
+	return scopeHelper.Update(input, "project_key")
 }
 
 // GetScopeList get Bamboo projects
@@ -123,43 +67,12 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @Description get Bamboo projects
 // @Tags plugins/bamboo
 // @Param connectionId path int false "connection ID"
-// @Success 200  {object} []apiProject
+// @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bamboo/connections/{connectionId}/scopes/ [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var projects []models.BambooProject
-	connectionId, _ := extractParam(input.Params)
-	if connectionId == 0 {
-		return nil, errors.BadInput.New("invalid path params")
-	}
-	limit, offset := api.GetLimitOffset(input.Query, "pageSize", "page")
-	err := basicRes.GetDal().All(&projects, dal.Where("connection_id = ?", connectionId), dal.Limit(limit), dal.Offset(offset))
-	if err != nil {
-		return nil, err
-	}
-	var ruleIds []uint64
-	for _, proj := range projects {
-		if proj.TransformationRuleId > 0 {
-			ruleIds = append(ruleIds, proj.TransformationRuleId)
-		}
-	}
-	var rules []models.BambooTransformationRule
-	if len(ruleIds) > 0 {
-		err = basicRes.GetDal().All(&rules, dal.Where("id IN (?)", ruleIds))
-		if err != nil {
-			return nil, err
-		}
-	}
-	names := make(map[uint64]string)
-	for _, rule := range rules {
-		names[rule.ID] = rule.Name
-	}
-	var apiProjects []apiProject
-	for _, proj := range projects {
-		apiProjects = append(apiProjects, apiProject{proj, names[proj.TransformationRuleId]})
-	}
-	return &plugin.ApiResourceOutput{Body: apiProjects, Status: http.StatusOK}, nil
+	return scopeHelper.GetScopeList(input)
 }
 
 // GetScope get one Bamboo project
@@ -167,71 +80,13 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Description get one Bamboo project
 // @Tags plugins/bamboo
 // @Param connectionId path int false "connection ID"
-// @Param projectKey path int false "project ID"
+// @Param scopeId path int false "project ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
-// @Success 200  {object} apiProject
+// @Success 200  {object} ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bamboo/connections/{connectionId}/scopes/{projectKey} [GET]
+// @Router /plugins/bamboo/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var project models.BambooProject
-	connectionId, projectKey := extractParam(input.Params)
-	if connectionId == 0 || projectKey == "" {
-		return nil, errors.BadInput.New("invalid path params")
-	}
-	db := basicRes.GetDal()
-	err := db.First(&project, dal.Where("connection_id = ? AND project_key = ?", connectionId, projectKey))
-	if err != nil && db.IsErrorNotFound(err) {
-		var scope models.BambooProject
-		connection := &models.BambooConnection{}
-		err = connectionHelper.First(connection, input.Params)
-		if err != nil {
-			return nil, err
-		}
-		apiClient, err := api.NewApiClientFromConnection(context.TODO(), basicRes, connection)
-		if err != nil {
-			return nil, err
-		}
-
-		apiProject, err := GetApiProject(projectKey, apiClient)
-		if err != nil {
-			return nil, err
-		}
-
-		scope.Convert(apiProject)
-		scope.ConnectionId = connectionId
-		err = db.CreateIfNotExist(&scope)
-		if err != nil {
-			return nil, err
-		}
-		return nil, errors.NotFound.New("record not found")
-	} else if err != nil {
-		return nil, err
-	}
-
-	var rule models.BambooTransformationRule
-	if project.TransformationRuleId > 0 {
-		err = basicRes.GetDal().First(&rule, dal.Where("id = ?", project.TransformationRuleId))
-		if err != nil {
-			return nil, err
-		}
-	}
-	return &plugin.ApiResourceOutput{Body: apiProject{project, rule.Name}, Status: http.StatusOK}, nil
-}
-
-func extractParam(params map[string]string) (uint64, string) {
-	connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
-	projectKey := params["projectKey"]
-	return connectionId, projectKey
-}
-
-func verifyProject(project *models.BambooProject) errors.Error {
-	if project.ConnectionId == 0 {
-		return errors.BadInput.New("invalid connectionId")
-	}
-	if project.ProjectKey == "" {
-		return errors.BadInput.New("invalid projectKey")
-	}
-	return nil
+	return scopeHelper.GetScope(input, "project_key")
 }
diff --git a/backend/plugins/bamboo/impl/impl.go b/backend/plugins/bamboo/impl/impl.go
index 50de2945b..f5612f6cd 100644
--- a/backend/plugins/bamboo/impl/impl.go
+++ b/backend/plugins/bamboo/impl/impl.go
@@ -213,7 +213,7 @@ func (p Bamboo) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 		"connections/:connectionId/search-remote-scopes": {
 			"GET": api.SearchRemoteScopes,
 		},
-		"connections/:connectionId/scopes/:projectKey": {
+		"connections/:connectionId/scopes/:scopeId": {
 			"GET":   api.GetScope,
 			"PATCH": api.UpdateScope,
 		},
diff --git a/backend/plugins/bamboo/models/project.go b/backend/plugins/bamboo/models/project.go
index 89e7caa4b..279b996e3 100644
--- a/backend/plugins/bamboo/models/project.go
+++ b/backend/plugins/bamboo/models/project.go
@@ -24,8 +24,8 @@ import (
 )
 
 type BambooProject struct {
-	ConnectionId         uint64 `json:"connectionId" mapstructure:"connectionId" gorm:"primaryKey"`
-	ProjectKey           string `json:"projectKey" gorm:"primaryKey;type:varchar(256)"`
+	ConnectionId         uint64 `json:"connectionId" mapstructure:"connectionId" validate:"required" gorm:"primaryKey"`
+	ProjectKey           string `json:"projectKey" gorm:"primaryKey;type:varchar(256)" validate:"required"`
 	TransformationRuleId uint64 `json:"transformationRuleId,omitempty" mapstructure:"transformationRuleId"`
 	Name                 string `json:"name" gorm:"index;type:varchar(256)"`
 	Description          string `json:"description"`
diff --git a/backend/plugins/bitbucket/api/init.go b/backend/plugins/bitbucket/api/init.go
index d92c2b334..a91525bd4 100644
--- a/backend/plugins/bitbucket/api/init.go
+++ b/backend/plugins/bitbucket/api/init.go
@@ -20,11 +20,13 @@ package api
 import (
 	"github.com/apache/incubator-devlake/core/context"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/bitbucket/models"
 	"github.com/go-playground/validator/v10"
 )
 
 var vld *validator.Validate
 var connectionHelper *api.ConnectionApiHelper
+var scopeHelper *api.ScopeApiHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketTransformationRule]
 var basicRes context.BasicRes
 
 func Init(br context.BasicRes) {
@@ -34,4 +36,9 @@ func Init(br context.BasicRes) {
 		basicRes,
 		vld,
 	)
+	scopeHelper = api.NewScopeHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketTransformationRule](
+		basicRes,
+		vld,
+		connectionHelper,
+	)
 }
diff --git a/backend/plugins/bitbucket/api/remote.go b/backend/plugins/bitbucket/api/remote.go
index 9fc116842..49f85b8a7 100644
--- a/backend/plugins/bitbucket/api/remote.go
+++ b/backend/plugins/bitbucket/api/remote.go
@@ -361,3 +361,8 @@ func GetQueryFromPageData(pageData *PageData) (url.Values, errors.Error) {
 	query.Set("pagelen", fmt.Sprintf("%v", pageData.PerPage))
 	return query, nil
 }
+func extractParam(params map[string]string) (uint64, string) {
+	connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
+	fullName := strings.TrimLeft(params["repoId"], "/")
+	return connectionId, fullName
+}
diff --git a/backend/plugins/bitbucket/api/scope.go b/backend/plugins/bitbucket/api/scope.go
index e9053e56c..496def2e9 100644
--- a/backend/plugins/bitbucket/api/scope.go
+++ b/backend/plugins/bitbucket/api/scope.go
@@ -18,25 +18,18 @@ limitations under the License.
 package api
 
 import (
-	"github.com/apache/incubator-devlake/core/dal"
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 	"github.com/apache/incubator-devlake/plugins/bitbucket/models"
-	"github.com/mitchellh/mapstructure"
-	"net/http"
-	"strconv"
-	"strings"
 )
 
-type apiRepo struct {
+type ScopeRes struct {
 	models.BitbucketRepo
 	TransformationRuleName string `json:"transformationRuleName,omitempty"`
 }
 
-type req struct {
-	Data []*models.BitbucketRepo `json:"data"`
-}
+type ScopeReq api.ScopeReq[models.BitbucketRepo]
 
 // PutScope create or update repo
 // @Summary create or update repo
@@ -44,39 +37,13 @@ type req struct {
 // @Tags plugins/bitbucket
 // @Accept application/json
 // @Param connectionId path int true "connection ID"
-// @Param scope body req true "json"
+// @Param scope body ScopeReq true "json"
 // @Success 200  {object} []models.BitbucketRepo
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId}/scopes [PUT]
 func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connectionId, _ := extractParam(input.Params)
-	if connectionId == 0 {
-		return nil, errors.BadInput.New("invalid connectionId")
-	}
-	var repos req
-	err := errors.Convert(mapstructure.Decode(input.Body, &repos))
-	if err != nil {
-		return nil, errors.BadInput.Wrap(err, "decoding repo error")
-	}
-	keeper := make(map[string]struct{})
-	for _, repo := range repos.Data {
-		if _, ok := keeper[repo.BitbucketId]; ok {
-			return nil, errors.BadInput.New("duplicated item")
-		} else {
-			keeper[repo.BitbucketId] = struct{}{}
-		}
-		repo.ConnectionId = connectionId
-		err = verifyRepo(repo)
-		if err != nil {
-			return nil, err
-		}
-	}
-	err = basicRes.GetDal().CreateOrUpdate(repos.Data)
-	if err != nil {
-		return nil, errors.Default.Wrap(err, "error on saving BitbucketRepo")
-	}
-	return &plugin.ApiResourceOutput{Body: repos.Data, Status: http.StatusOK}, nil
+	return scopeHelper.Put(input)
 }
 
 // UpdateScope patch to repo
@@ -85,35 +52,14 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Tags plugins/bitbucket
 // @Accept application/json
 // @Param connectionId path int true "connection ID"
-// @Param repoId path string true "repo ID"
+// @Param scopeId path string true "repo ID"
 // @Param scope body models.BitbucketRepo true "json"
 // @Success 200  {object} models.BitbucketRepo
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bitbucket/connections/{connectionId}/scopes/{repoId} [PATCH]
+// @Router /plugins/bitbucket/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connectionId, repoId := extractParam(input.Params)
-	if connectionId == 0 || repoId == "" {
-		return nil, errors.BadInput.New("invalid connectionId or repoId")
-	}
-	var repo models.BitbucketRepo
-	err := basicRes.GetDal().First(&repo, dal.Where("connection_id = ? AND bitbucket_id = ?", connectionId, repoId))
-	if err != nil {
-		return nil, errors.Default.Wrap(err, "getting Repo error")
-	}
-	err = api.DecodeMapStruct(input.Body, &repo)
-	if err != nil {
-		return nil, errors.Default.Wrap(err, "patch repo error")
-	}
-	err = verifyRepo(&repo)
-	if err != nil {
-		return nil, err
-	}
-	err = basicRes.GetDal().Update(repo)
-	if err != nil {
-		return nil, errors.Default.Wrap(err, "error on saving BitbucketRepo")
-	}
-	return &plugin.ApiResourceOutput{Body: repo, Status: http.StatusOK}, nil
+	return scopeHelper.Update(input, "bitbucket_id")
 }
 
 // GetScopeList get repos
@@ -123,43 +69,12 @@ 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"
-// @Success 200  {object} []apiRepo
+// @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId}/scopes/ [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var repos []models.BitbucketRepo
-	connectionId, _ := extractParam(input.Params)
-	if connectionId == 0 {
-		return nil, errors.BadInput.New("invalid path params")
-	}
-	limit, offset := api.GetLimitOffset(input.Query, "pageSize", "page")
-	err := basicRes.GetDal().All(&repos, dal.Where("connection_id = ?", connectionId), dal.Limit(limit), dal.Offset(offset))
-	if err != nil {
-		return nil, err
-	}
-	var ruleIds []uint64
-	for _, repo := range repos {
-		if repo.TransformationRuleId > 0 {
-			ruleIds = append(ruleIds, repo.TransformationRuleId)
-		}
-	}
-	var rules []models.BitbucketTransformationRule
-	if len(ruleIds) > 0 {
-		err = basicRes.GetDal().All(&rules, dal.Where("id IN (?)", ruleIds))
-		if err != nil {
-			return nil, err
-		}
-	}
-	names := make(map[uint64]string)
-	for _, rule := range rules {
-		names[rule.ID] = rule.Name
-	}
-	var apiRepos []apiRepo
-	for _, repo := range repos {
-		apiRepos = append(apiRepos, apiRepo{repo, names[repo.TransformationRuleId]})
-	}
-	return &plugin.ApiResourceOutput{Body: apiRepos, Status: http.StatusOK}, nil
+	return scopeHelper.GetScopeList(input)
 }
 
 // GetScope get one repo
@@ -167,47 +82,11 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Description get one repo
 // @Tags plugins/bitbucket
 // @Param connectionId path int true "connection ID"
-// @Param repoId path string true "repo ID"
-// @Success 200  {object} apiRepo
+// @Param scopeId path string true "repo ID"
+// @Success 200  {object} ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bitbucket/connections/{connectionId}/scopes/{repoId} [GET]
+// @Router /plugins/bitbucket/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var repo models.BitbucketRepo
-	connectionId, repoId := extractParam(input.Params)
-	if connectionId == 0 || repoId == "" {
-		return nil, errors.BadInput.New("invalid connectionId or repoId")
-	}
-	db := basicRes.GetDal()
-	err := db.First(&repo, dal.Where("connection_id = ? AND bitbucket_id = ?", connectionId, repoId))
-	if db.IsErrorNotFound(err) {
-		return nil, errors.NotFound.New("record not found")
-	}
-	if err != nil {
-		return nil, err
-	}
-	var rule models.BitbucketTransformationRule
-	if repo.TransformationRuleId > 0 {
-		err = basicRes.GetDal().First(&rule, dal.Where("id = ?", repo.TransformationRuleId))
-		if err != nil {
-			return nil, err
-		}
-	}
-	return &plugin.ApiResourceOutput{Body: apiRepo{repo, rule.Name}, Status: http.StatusOK}, nil
-}
-
-func extractParam(params map[string]string) (uint64, string) {
-	connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
-	fullName := strings.TrimLeft(params["repoId"], "/")
-	return connectionId, fullName
-}
-
-func verifyRepo(repo *models.BitbucketRepo) errors.Error {
-	if repo.ConnectionId == 0 {
-		return errors.BadInput.New("invalid connectionId")
-	}
-	if repo.BitbucketId == `` {
-		return errors.BadInput.New("invalid bitbucket ID or full name")
-	}
-	return nil
+	return scopeHelper.GetScope(input, "bitbucket_id")
 }
diff --git a/backend/plugins/bitbucket/impl/impl.go b/backend/plugins/bitbucket/impl/impl.go
index 927667108..50b2d19ca 100644
--- a/backend/plugins/bitbucket/impl/impl.go
+++ b/backend/plugins/bitbucket/impl/impl.go
@@ -193,7 +193,7 @@ func (p Bitbucket) ApiResources() map[string]map[string]plugin.ApiResourceHandle
 			"DELETE": api.DeleteConnection,
 			"GET":    api.GetConnection,
 		},
-		"connections/:connectionId/scopes/*repoId": {
+		"connections/:connectionId/scopes/*scopeId": {
 			"GET":   api.GetScope,
 			"PATCH": api.UpdateScope,
 		},
diff --git a/backend/plugins/bitbucket/models/repo.go b/backend/plugins/bitbucket/models/repo.go
index 249651a7b..417ebe064 100644
--- a/backend/plugins/bitbucket/models/repo.go
+++ b/backend/plugins/bitbucket/models/repo.go
@@ -23,8 +23,8 @@ import (
 )
 
 type BitbucketRepo struct {
-	ConnectionId         uint64     `json:"connectionId" gorm:"primaryKey" mapstructure:"connectionId,omitempty"`
-	BitbucketId          string     `json:"bitbucketId" gorm:"primaryKey;type:varchar(255)" mapstructure:"bitbucketId"`
+	ConnectionId         uint64     `json:"connectionId" gorm:"primaryKey" validate:"required" mapstructure:"connectionId,omitempty"`
+	BitbucketId          string     `json:"bitbucketId" gorm:"primaryKey;type:varchar(255)" validate:"required" mapstructure:"bitbucketId"`
 	Name                 string     `json:"name" gorm:"type:varchar(255)" mapstructure:"name,omitempty"`
 	HTMLUrl              string     `json:"HTMLUrl" gorm:"type:varchar(255)" mapstructure:"HTMLUrl,omitempty"`
 	Description          string     `json:"description" mapstructure:"description,omitempty"`
diff --git a/backend/plugins/github/api/scope.go b/backend/plugins/github/api/scope.go
index d9ba2499a..1225e9edd 100644
--- a/backend/plugins/github/api/scope.go
+++ b/backend/plugins/github/api/scope.go
@@ -20,6 +20,7 @@ package api
 import (
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 	"github.com/apache/incubator-devlake/plugins/github/models"
 )
 
@@ -28,13 +29,15 @@ type ScopeRes struct {
 	TransformationRuleName string `json:"transformationRuleName,omitempty"`
 }
 
+type ScopeReq api.ScopeReq[models.GithubRepo]
+
 // PutScope create or update github repo
 // @Summary create or update github repo
 // @Description Create or update github repo
 // @Tags plugins/github
 // @Accept application/json
 // @Param connectionId path int true "connection ID"
-// @Param scope body req true "json"
+// @Param scope body ScopeReq true "json"
 // @Success 200  {object} []models.GithubRepo
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
diff --git a/backend/plugins/github/models/repo.go b/backend/plugins/github/models/repo.go
index 045437180..9eda5e118 100644
--- a/backend/plugins/github/models/repo.go
+++ b/backend/plugins/github/models/repo.go
@@ -36,7 +36,7 @@ type GithubRepo struct {
 	CloneUrl             string     `json:"cloneUrl" gorm:"type:varchar(255)" mapstructure:"cloneUrl,omitempty"`
 	CreatedDate          *time.Time `json:"createdDate" mapstructure:"-"`
 	UpdatedDate          *time.Time `json:"updatedDate" mapstructure:"-"`
-	common.NoPKModel     `json:"-" mapstructure:",squash"`
+	common.NoPKModel     `json:"-" mapstructure:"-"`
 }
 
 func (GithubRepo) TableName() string {
diff --git a/backend/plugins/gitlab/api/scope.go b/backend/plugins/gitlab/api/scope.go
index 9f3da5b67..a40ccf6f0 100644
--- a/backend/plugins/gitlab/api/scope.go
+++ b/backend/plugins/gitlab/api/scope.go
@@ -20,6 +20,7 @@ package api
 import (
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 	"github.com/apache/incubator-devlake/plugins/gitlab/models"
 )
 
@@ -28,13 +29,15 @@ type ScopeRes struct {
 	TransformationRuleName string `json:"transformationRuleName,omitempty"`
 }
 
+type ScopeReq api.ScopeReq[models.GitlabProject]
+
 // PutScope create or update gitlab project
 // @Summary create or update gitlab project
 // @Description Create or update gitlab project
 // @Tags plugins/gitlab
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param scope body req true "json"
+// @Param scope body ScopeReq true "json"
 // @Success 200  {object} []models.GitlabProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -49,12 +52,12 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Tags plugins/gitlab
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param projectId path int false "project ID"
+// @Param scopeId path int false "project ID"
 // @Param scope body models.GitlabProject true "json"
 // @Success 200  {object} models.GitlabProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/gitlab/connections/{connectionId}/scopes/{projectId} [PATCH]
+// @Router /plugins/gitlab/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return scopeHelper.Update(input, "gitlab_id")
 }
@@ -77,13 +80,13 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Description get one Gitlab project
 // @Tags plugins/gitlab
 // @Param connectionId path int false "connection ID"
-// @Param projectId path int false "project ID"
+// @Param scopeId path int false "project ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
 // @Success 200  {object} ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/gitlab/connections/{connectionId}/scopes/{projectId} [GET]
+// @Router /plugins/gitlab/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return scopeHelper.GetScope(input, "gitlab_id")
 }
diff --git a/backend/plugins/gitlab/impl/impl.go b/backend/plugins/gitlab/impl/impl.go
index a5949aa49..99c2dd968 100644
--- a/backend/plugins/gitlab/impl/impl.go
+++ b/backend/plugins/gitlab/impl/impl.go
@@ -246,7 +246,7 @@ func (p Gitlab) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"DELETE": api.DeleteConnection,
 			"GET":    api.GetConnection,
 		},
-		"connections/:connectionId/scopes/:projectId": {
+		"connections/:connectionId/scopes/:scopeId": {
 			"GET":   api.GetScope,
 			"PATCH": api.UpdateScope,
 		},
diff --git a/backend/plugins/gitlab/models/project.go b/backend/plugins/gitlab/models/project.go
index 91db090ff..41c6855f5 100644
--- a/backend/plugins/gitlab/models/project.go
+++ b/backend/plugins/gitlab/models/project.go
@@ -42,7 +42,7 @@ type GitlabProject struct {
 
 	CreatedDate      time.Time  `json:"createdDate" mapstructure:"-"`
 	UpdatedDate      *time.Time `json:"updatedDate" mapstructure:"-"`
-	common.NoPKModel `json:"-" mapstructure:",squash"`
+	common.NoPKModel `json:"-" mapstructure:"-"`
 }
 
 func (GitlabProject) TableName() string {