You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by ab...@apache.org on 2024/02/04 02:11:02 UTC

(incubator-devlake) branch main updated: refactor: 5519 bitbucket adopts new dshelpers (#6867)

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

abeizn 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 a80db0024 refactor: 5519 bitbucket adopts new dshelpers (#6867)
a80db0024 is described below

commit a80db00249ac22ef416af9b9a7dbbc00edbba996
Author: Klesh Wong <zh...@merico.dev>
AuthorDate: Sun Feb 4 10:10:55 2024 +0800

    refactor: 5519 bitbucket adopts new dshelpers (#6867)
    
    * refactor: 5519 bitbucket wip
    
    * refactor: bitbucket adopts new dshelper
---
 .../pluginhelper/api/ds_connection_api_helper.go   |   5 +-
 .../plugins/bitbucket/api/blueprint_V200_test.go   | 196 ------------------
 backend/plugins/bitbucket/api/blueprint_v200.go    | 119 ++++++-----
 .../api/{connection.go => connection_api.go}       |  46 +----
 backend/plugins/bitbucket/api/init.go              |  43 +---
 backend/plugins/bitbucket/api/remote.go            | 159 ---------------
 backend/plugins/bitbucket/api/remote_api.go        | 219 +++++++++++++++++++++
 .../bitbucket/api/{scope.go => scope_api.go}       |  34 ++--
 .../api/{scope_config.go => scope_config_api.go}   |  20 +-
 backend/plugins/bitbucket/e2e/repo_test.go         |   2 +-
 backend/plugins/bitbucket/impl/impl.go             |  12 +-
 backend/plugins/bitbucket/models/repo.go           |   3 +-
 12 files changed, 327 insertions(+), 531 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/ds_connection_api_helper.go b/backend/helpers/pluginhelper/api/ds_connection_api_helper.go
index b404fce6b..27eeb4383 100644
--- a/backend/helpers/pluginhelper/api/ds_connection_api_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_connection_api_helper.go
@@ -18,9 +18,10 @@ limitations under the License.
 package api
 
 import (
-	"github.com/apache/incubator-devlake/server/api/shared"
 	"strconv"
 
+	"github.com/apache/incubator-devlake/server/api/shared"
+
 	"github.com/apache/incubator-devlake/core/context"
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
@@ -47,7 +48,7 @@ func NewDsConnectionApiHelper[
 	}
 }
 
-func (connApi *DsConnectionApiHelper[C, S, SC]) GetMergedConnection(input *plugin.ApiResourceInput) (*C, error) {
+func (connApi *DsConnectionApiHelper[C, S, SC]) GetMergedConnection(input *plugin.ApiResourceInput) (*C, errors.Error) {
 	connection, err := connApi.FindByPk(input)
 	if err != nil {
 		return nil, errors.BadInput.Wrap(err, "find connection from db")
diff --git a/backend/plugins/bitbucket/api/blueprint_V200_test.go b/backend/plugins/bitbucket/api/blueprint_V200_test.go
deleted file mode 100644
index 25fa0d379..000000000
--- a/backend/plugins/bitbucket/api/blueprint_V200_test.go
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements.  See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License.  You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
-	"database/sql"
-	"testing"
-
-	"github.com/apache/incubator-devlake/core/dal"
-	"gorm.io/gorm/migrator"
-
-	coreModels "github.com/apache/incubator-devlake/core/models"
-	"github.com/apache/incubator-devlake/core/models/common"
-	"github.com/apache/incubator-devlake/core/models/domainlayer"
-	"github.com/apache/incubator-devlake/core/models/domainlayer/code"
-	"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
-	"github.com/apache/incubator-devlake/core/plugin"
-	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-	"github.com/apache/incubator-devlake/helpers/unithelper"
-	mockdal "github.com/apache/incubator-devlake/mocks/core/dal"
-	mockplugin "github.com/apache/incubator-devlake/mocks/core/plugin"
-	"github.com/apache/incubator-devlake/plugins/bitbucket/models"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-)
-
-func TestMakeDataSourcePipelinePlanV200(t *testing.T) {
-	connection := &models.BitbucketConnection{
-		BaseConnection: helper.BaseConnection{
-			Name: "bitbucket-test",
-			Model: common.Model{
-				ID: 1,
-			},
-		},
-		BitbucketConn: models.BitbucketConn{
-			RestConnection: helper.RestConnection{
-				Endpoint:         "https://api.bitbucket.org/2.0/",
-				Proxy:            "",
-				RateLimitPerHour: 0,
-			},
-			BasicAuth: helper.BasicAuth{
-				Username: "Username",
-				Password: "Password",
-			},
-		},
-	}
-	mockMeta := mockplugin.NewPluginMeta(t)
-	mockMeta.On("RootPkgPath").Return("github.com/apache/incubator-devlake/plugins/bitbucket")
-	mockMeta.On("Name").Return("bitbucket").Maybe()
-	err := plugin.RegisterPlugin("bitbucket", mockMeta)
-	assert.Nil(t, err)
-	// Refresh Global Variables and set the sql mock
-	mockBasicRes(t)
-
-	bs := &coreModels.BlueprintScope{
-		ScopeId: "1",
-	}
-	bpScopes := make([]*coreModels.BlueprintScope, 0)
-	bpScopes = append(bpScopes, bs)
-
-	plan := make(coreModels.PipelinePlan, len(bpScopes))
-	plan, err = makeDataSourcePipelinePlanV200(nil, plan, bpScopes, connection)
-	assert.Nil(t, err)
-	scopes, err := makeScopesV200(bpScopes, connection)
-	assert.Nil(t, err)
-
-	expectPlan := coreModels.PipelinePlan{
-		coreModels.PipelineStage{
-			{
-				Plugin:   "bitbucket",
-				Subtasks: []string{},
-				Options: map[string]interface{}{
-					"fullName":     "likyh/likyhphp",
-					"connectionId": uint64(1),
-				},
-			},
-			{
-				Plugin: "gitextractor",
-				Options: map[string]interface{}{
-					"proxy":  "",
-					"repoId": "bitbucket:BitbucketRepo:1:likyh/likyhphp",
-					"name":   "likyh/likyhphp",
-					"url":    "https://Username:Password@this_is_cloneUrl",
-				},
-			},
-		},
-		coreModels.PipelineStage{
-			{
-				Plugin: "refdiff",
-				Options: map[string]interface{}{
-					"repoId":      "bitbucket:BitbucketRepo:1:likyh/likyhphp",
-					"tagsLimit":   10,
-					"tagsOrder":   "reverse semver",
-					"tagsPattern": "pattern",
-				},
-			},
-		},
-	}
-	assert.Equal(t, expectPlan, plan)
-	expectScopes := make([]plugin.Scope, 0)
-	scopeRepo := &code.Repo{
-		DomainEntity: domainlayer.DomainEntity{
-			Id: "bitbucket:BitbucketRepo:1:likyh/likyhphp",
-		},
-		Name: "likyh/likyhphp",
-	}
-
-	scopeTicket := &ticket.Board{
-		DomainEntity: domainlayer.DomainEntity{
-			Id: "bitbucket:BitbucketRepo:1:likyh/likyhphp",
-		},
-		Name:        "likyh/likyhphp",
-		Description: "",
-		Url:         "",
-		CreatedDate: nil,
-		Type:        "",
-	}
-
-	expectScopes = append(expectScopes, scopeRepo, scopeTicket)
-	assert.Equal(t, expectScopes, scopes)
-}
-
-func mockBasicRes(t *testing.T) {
-	testBitbucketRepo := &models.BitbucketRepo{
-		Scope: common.Scope{
-			ConnectionId:  1,
-			ScopeConfigId: 1,
-		},
-		BitbucketId: "likyh/likyhphp",
-		Name:        "test/testRepo",
-		CloneUrl:    "https://this_is_cloneUrl",
-	}
-
-	testScopeConfig := &models.BitbucketScopeConfig{
-		ScopeConfig: common.ScopeConfig{
-			Entities: []string{"CODE", "TICKET"},
-			Model: common.Model{
-				ID: 1,
-			},
-			Name: "Bitbucket scope config",
-		},
-		IssueStatusTodo: "new,open,wantfix",
-		Refdiff: map[string]interface{}{
-			"tagsPattern": "pattern",
-			"tagsLimit":   10,
-			"tagsOrder":   "reverse semver",
-		},
-	}
-	var testColumTypes = []dal.ColumnMeta{
-		migrator.ColumnType{
-			NameValue: sql.NullString{
-				String: "abc",
-				Valid:  true,
-			},
-		},
-	}
-
-	// Refresh Global Variables and set the sql mock
-	mockRes := unithelper.DummyBasicRes(func(mockDal *mockdal.Dal) {
-		mockDal.On("First", mock.AnythingOfType("*models.BitbucketRepo"), mock.Anything).Run(func(args mock.Arguments) {
-			dst := args.Get(0).(*models.BitbucketRepo)
-			*dst = *testBitbucketRepo
-		}).Return(nil)
-
-		mockDal.On("First", mock.AnythingOfType("*models.BitbucketScopeConfig"), mock.Anything).Run(func(args mock.Arguments) {
-			dst := args.Get(0).(*models.BitbucketScopeConfig)
-			*dst = *testScopeConfig
-		}).Return(nil)
-		mockDal.On("GetPrimarykeyColumns", mock.AnythingOfType("*models.BitbucketConnection"), mock.Anything).Run(nil).Return(
-			testColumTypes, nil)
-		mockDal.On("GetColumns", mock.AnythingOfType("models.BitbucketConnection"), mock.Anything).Run(nil).Return(
-			testColumTypes, nil)
-		mockDal.On("GetColumns", mock.AnythingOfType("models.BitbucketRepo"), mock.Anything).Run(nil).Return(
-			testColumTypes, nil)
-		mockDal.On("GetColumns", mock.AnythingOfType("models.BitbucketScopeConfig"), mock.Anything).Run(nil).Return(
-			testColumTypes, nil)
-	})
-	p := mockplugin.NewPluginMeta(t)
-	p.On("Name").Return("dummy").Maybe()
-	Init(mockRes, p)
-}
diff --git a/backend/plugins/bitbucket/api/blueprint_v200.go b/backend/plugins/bitbucket/api/blueprint_v200.go
index d1b562103..f1547df5c 100644
--- a/backend/plugins/bitbucket/api/blueprint_v200.go
+++ b/backend/plugins/bitbucket/api/blueprint_v200.go
@@ -18,6 +18,7 @@ limitations under the License.
 package api
 
 import (
+	"context"
 	"net/url"
 
 	"github.com/apache/incubator-devlake/core/errors"
@@ -30,6 +31,7 @@ import (
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/core/utils"
 	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/helpers/srvhelper"
 	"github.com/apache/incubator-devlake/plugins/bitbucket/models"
 	"github.com/apache/incubator-devlake/plugins/bitbucket/tasks"
 )
@@ -39,19 +41,28 @@ func MakeDataSourcePipelinePlanV200(
 	connectionId uint64,
 	bpScopes []*coreModels.BlueprintScope,
 ) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
-	// get the connection info for url
-	connection := &models.BitbucketConnection{}
-	err := connectionHelper.FirstById(connection, connectionId)
+	// load connection, scope and scopeConfig from the db
+	connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
+	if err != nil {
+		return nil, nil, err
+	}
+	scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	plan := make(coreModels.PipelinePlan, len(bpScopes))
-	plan, err = makeDataSourcePipelinePlanV200(subtaskMetas, plan, bpScopes, connection)
+	// needed for the connection to populate its access tokens
+	// if AppKey authentication method is selected
+	_, err = helper.NewApiClientFromConnection(context.TODO(), basicRes, connection)
 	if err != nil {
 		return nil, nil, err
 	}
-	scopes, err := makeScopesV200(bpScopes, connection)
+
+	plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails, connection)
+	if err != nil {
+		return nil, nil, err
+	}
+	scopes, err := makeScopesV200(scopeDetails, connection)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -61,20 +72,32 @@ func MakeDataSourcePipelinePlanV200(
 
 func makeDataSourcePipelinePlanV200(
 	subtaskMetas []plugin.SubTaskMeta,
-	plan coreModels.PipelinePlan,
-	bpScopes []*coreModels.BlueprintScope,
+	scopeDetails []*srvhelper.ScopeDetail[models.BitbucketRepo, models.BitbucketScopeConfig],
 	connection *models.BitbucketConnection,
 ) (coreModels.PipelinePlan, errors.Error) {
-	for i, bpScope := range bpScopes {
+	plan := make(coreModels.PipelinePlan, len(scopeDetails))
+	for i, scopeDetail := range scopeDetails {
+		bitbucketRepo, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
 		stage := plan[i]
 		if stage == nil {
 			stage = coreModels.PipelineStage{}
 		}
-		// get repo and scope config from db
-		repo, scopeConfig, err := scopeHelper.DbHelper().GetScopeAndConfig(connection.ID, bpScope.ScopeId)
+		// construct task options for Jira
+		task, err := helper.MakePipelinePlanTask(
+			"bamboo",
+			subtaskMetas,
+			scopeConfig.Entities,
+			tasks.BitbucketOptions{
+				ConnectionId: bitbucketRepo.ConnectionId,
+				FullName:     bitbucketRepo.BitbucketId,
+			},
+		)
 		if err != nil {
 			return nil, err
 		}
+
+		stage = append(stage, task)
+
 		// refdiff
 		if scopeConfig != nil && scopeConfig.Refdiff != nil {
 			// add a new task to next stage
@@ -83,7 +106,7 @@ func makeDataSourcePipelinePlanV200(
 				plan = append(plan, nil)
 			}
 			refdiffOp := scopeConfig.Refdiff
-			refdiffOp["repoId"] = didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).Generate(connection.ID, repo.BitbucketId)
+			refdiffOp["repoId"] = didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).Generate(connection.ID, bitbucketRepo.BitbucketId)
 			plan[j] = coreModels.PipelineStage{
 				{
 					Plugin:  "refdiff",
@@ -92,33 +115,9 @@ func makeDataSourcePipelinePlanV200(
 			}
 			scopeConfig.Refdiff = nil
 		}
-
-		// construct task options for bitbucket
-		op := &tasks.BitbucketOptions{
-			ConnectionId: repo.ConnectionId,
-			FullName:     repo.BitbucketId,
-		}
-		options, err := tasks.EncodeTaskOptions(op)
-		if err != nil {
-			return nil, err
-		}
-
-		subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, scopeConfig.Entities)
-		if err != nil {
-			return nil, err
-		}
-		stage = append(stage, &coreModels.PipelineTask{
-			Plugin:   "bitbucket",
-			Subtasks: subtasks,
-			Options:  options,
-		})
-		if err != nil {
-			return nil, err
-		}
-
 		// add gitex stage
 		if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_CODE) {
-			cloneUrl, err := errors.Convert01(url.Parse(repo.CloneUrl))
+			cloneUrl, err := errors.Convert01(url.Parse(bitbucketRepo.CloneUrl))
 			if err != nil {
 				return nil, err
 			}
@@ -127,8 +126,8 @@ func makeDataSourcePipelinePlanV200(
 				Plugin: "gitextractor",
 				Options: map[string]interface{}{
 					"url":    cloneUrl.String(),
-					"name":   repo.BitbucketId,
-					"repoId": didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).Generate(connection.ID, repo.BitbucketId),
+					"name":   bitbucketRepo.BitbucketId,
+					"repoId": didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).Generate(connection.ID, bitbucketRepo.BitbucketId),
 					"proxy":  connection.Proxy,
 				},
 			})
@@ -139,44 +138,44 @@ func makeDataSourcePipelinePlanV200(
 	return plan, nil
 }
 
-func makeScopesV200(bpScopes []*coreModels.BlueprintScope, connection *models.BitbucketConnection) ([]plugin.Scope, errors.Error) {
+func makeScopesV200(
+	scopeDetails []*srvhelper.ScopeDetail[models.BitbucketRepo, models.BitbucketScopeConfig],
+	connection *models.BitbucketConnection,
+) ([]plugin.Scope, errors.Error) {
 	scopes := make([]plugin.Scope, 0)
-	for _, bpScope := range bpScopes {
-		repo, scopeConfig, err := scopeHelper.DbHelper().GetScopeAndConfig(connection.ID, bpScope.ScopeId)
-		if err != nil {
-			return nil, err
-		}
+	idgen := didgen.NewDomainIdGenerator(&models.BitbucketRepo{})
+	for _, scopeDetail := range scopeDetails {
+		scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
+		id := idgen.Generate(connection.ID, scope.BitbucketId)
+
 		if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_CODE_REVIEW) ||
 			utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_CODE) ||
 			utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_CROSS) {
 			// if we don't need to collect gitex, we need to add repo to scopes here
-			scopeRepo := &code.Repo{
+			scopes = append(scopes, &code.Repo{
 				DomainEntity: domainlayer.DomainEntity{
-					Id: didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).Generate(connection.ID, repo.BitbucketId),
+					Id: id,
 				},
-				Name: repo.BitbucketId,
-			}
-			scopes = append(scopes, scopeRepo)
+				Name: scope.BitbucketId,
+			})
 		}
 		// add cicd_scope to scopes
 		if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_CICD) {
-			scopeCICD := &devops.CicdScope{
+			scopes = append(scopes, &devops.CicdScope{
 				DomainEntity: domainlayer.DomainEntity{
-					Id: didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).Generate(connection.ID, repo.BitbucketId),
+					Id: id,
 				},
-				Name: repo.BitbucketId,
-			}
-			scopes = append(scopes, scopeCICD)
+				Name: scope.BitbucketId,
+			})
 		}
 		// add board to scopes
 		if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_TICKET) {
-			scopeTicket := &ticket.Board{
+			scopes = append(scopes, &ticket.Board{
 				DomainEntity: domainlayer.DomainEntity{
-					Id: didgen.NewDomainIdGenerator(&models.BitbucketRepo{}).Generate(connection.ID, repo.BitbucketId),
+					Id: id,
 				},
-				Name: repo.BitbucketId,
-			}
-			scopes = append(scopes, scopeTicket)
+				Name: scope.BitbucketId,
+			})
 		}
 	}
 	return scopes, nil
diff --git a/backend/plugins/bitbucket/api/connection.go b/backend/plugins/bitbucket/api/connection_api.go
similarity index 81%
rename from backend/plugins/bitbucket/api/connection.go
rename to backend/plugins/bitbucket/api/connection_api.go
index ad1b9a9a7..f9f185389 100644
--- a/backend/plugins/bitbucket/api/connection.go
+++ b/backend/plugins/bitbucket/api/connection_api.go
@@ -100,8 +100,7 @@ func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/bitbucket/{connectionId}/test [POST]
 func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.BitbucketConnection{}
-	err := connectionHelper.First(connection, input.Params)
+	connection, err := dsHelper.ConnApi.GetMergedConnection(input)
 	if err != nil {
 		return nil, errors.BadInput.Wrap(err, "find connection from db")
 	}
@@ -125,13 +124,7 @@ func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResource
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/bitbucket/connections [POST]
 func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	// update from request and save to database
-	connection := &models.BitbucketConnection{}
-	err := connectionHelper.Create(connection, input)
-	if err != nil {
-		return nil, err
-	}
-	return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: http.StatusOK}, nil
+	return dsHelper.ConnApi.Post(input)
 }
 
 // @Summary patch bitbucket connection
@@ -143,17 +136,7 @@ func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId} [PATCH]
 func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.BitbucketConnection{}
-	if err := connectionHelper.First(&connection, input.Params); err != nil {
-		return nil, err
-	}
-	if err := (&models.BitbucketConnection{}).MergeFromRequest(connection, input.Body); err != nil {
-		return nil, errors.Convert(err)
-	}
-	if err := connectionHelper.SaveWithCreateOrUpdate(connection); err != nil {
-		return nil, err
-	}
-	return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
+	return dsHelper.ConnApi.Patch(input)
 }
 
 // @Summary delete a bitbucket connection
@@ -165,14 +148,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	conn := &models.BitbucketConnection{}
-	output, err := connectionHelper.Delete(conn, input)
-	if err != nil {
-		return output, err
-	}
-	output.Body = conn.Sanitize()
-	return output, nil
-
+	return dsHelper.ConnApi.Delete(input)
 }
 
 // @Summary get all bitbucket connections
@@ -183,15 +159,7 @@ func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/bitbucket/connections [GET]
 func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var connections []models.BitbucketConnection
-	err := connectionHelper.List(&connections)
-	if err != nil {
-		return nil, err
-	}
-	for idx, c := range connections {
-		connections[idx] = c.Sanitize()
-	}
-	return &plugin.ApiResourceOutput{Body: connections, Status: http.StatusOK}, nil
+	return dsHelper.ConnApi.GetAll(input)
 }
 
 // @Summary get bitbucket connection detail
@@ -202,7 +170,5 @@ func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId} [GET]
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.BitbucketConnection{}
-	err := connectionHelper.First(connection, input.Params)
-	return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
+	return dsHelper.ConnApi.GetDetail(input)
 }
diff --git a/backend/plugins/bitbucket/api/init.go b/backend/plugins/bitbucket/api/init.go
index 4676dacfd..5e0dcb98c 100644
--- a/backend/plugins/bitbucket/api/init.go
+++ b/backend/plugins/bitbucket/api/init.go
@@ -25,48 +25,16 @@ import (
 	"github.com/go-playground/validator/v10"
 )
 
+var basicRes context.BasicRes
 var vld *validator.Validate
-var connectionHelper *api.ConnectionApiHelper
-var scopeHelper *api.ScopeApiHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketScopeConfig]
-var remoteHelper *api.RemoteApiHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketApiRepo, models.GroupResponse]
-var scHelper *api.ScopeConfigHelper[models.BitbucketScopeConfig, *models.BitbucketScopeConfig]
 var dsHelper *api.DsHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketScopeConfig]
-var basicRes context.BasicRes
+var raProxy *api.DsRemoteApiProxyHelper[models.BitbucketConnection]
+var raScopeList *api.DsRemoteApiScopeListHelper[models.BitbucketConnection, models.BitbucketRepo, BitbucketRemotePagination]
+var raScopeSearch *api.DsRemoteApiScopeSearchHelper[models.BitbucketConnection, models.BitbucketRepo]
 
 func Init(br context.BasicRes, p plugin.PluginMeta) {
 
 	basicRes = br
-	vld = validator.New()
-	connectionHelper = api.NewConnectionHelper(
-		basicRes,
-		vld,
-		p.Name(),
-	)
-	params := &api.ReflectionParameters{
-		ScopeIdFieldName:     "BitbucketId",
-		ScopeIdColumnName:    "bitbucket_id",
-		RawScopeParamName:    "FullName",
-		SearchScopeParamName: "name",
-	}
-	scopeHelper = api.NewScopeHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketScopeConfig](
-		basicRes,
-		vld,
-		connectionHelper,
-		api.NewScopeDatabaseHelperImpl[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketScopeConfig](
-			basicRes, connectionHelper, params),
-		params,
-		nil,
-	)
-	remoteHelper = api.NewRemoteHelper[models.BitbucketConnection, models.BitbucketRepo, models.BitbucketApiRepo, models.GroupResponse](
-		basicRes,
-		vld,
-		connectionHelper,
-	)
-	scHelper = api.NewScopeConfigHelper[models.BitbucketScopeConfig, *models.BitbucketScopeConfig](
-		basicRes,
-		vld,
-		p.Name(),
-	)
 
 	dsHelper = api.NewDataSourceHelper[
 		models.BitbucketConnection, models.BitbucketRepo, models.BitbucketScopeConfig,
@@ -80,5 +48,8 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
 		nil,
 		nil,
 	)
+	raProxy = api.NewDsRemoteApiProxyHelper[models.BitbucketConnection](dsHelper.ConnApi.ModelApiHelper)
+	raScopeList = api.NewDsRemoteApiScopeListHelper[models.BitbucketConnection, models.BitbucketRepo, BitbucketRemotePagination](raProxy, listBitbucketRemoteScopes)
+	raScopeSearch = api.NewDsRemoteApiScopeSearchHelper[models.BitbucketConnection, models.BitbucketRepo](raProxy, searchBitbucketRepos)
 
 }
diff --git a/backend/plugins/bitbucket/api/remote.go b/backend/plugins/bitbucket/api/remote.go
deleted file mode 100644
index 820c1ece7..000000000
--- a/backend/plugins/bitbucket/api/remote.go
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements.  See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License.  You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
-	gocontext "context"
-	"fmt"
-	"net/http"
-	"net/url"
-	"strings"
-
-	"github.com/apache/incubator-devlake/core/context"
-	"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"
-)
-
-// RemoteScopes list all available scope for users
-// @Summary list all available scope for users
-// @Description list all available scope for users
-// @Tags plugins/bitbucket
-// @Accept application/json
-// @Param connectionId path int false "connection ID"
-// @Param groupId query string false "group ID"
-// @Param pageToken query string false "page Token"
-// @Success 200  {object} api.RemoteScopesOutput
-// @Failure 400  {object} shared.ApiBody "Bad Request"
-// @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bitbucket/connections/{connectionId}/remote-scopes [GET]
-func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return remoteHelper.GetScopesFromRemote(input,
-		func(basicRes context.BasicRes, gid string, queryData *api.RemoteQueryData, connection models.BitbucketConnection) ([]models.GroupResponse, errors.Error) {
-			if gid != "" {
-				return nil, nil
-			}
-			query := initialQuery(queryData)
-
-			apiClient, err := api.NewApiClientFromConnection(gocontext.TODO(), basicRes, &connection)
-			if err != nil {
-				return nil, errors.BadInput.Wrap(err, "failed to get create apiClient")
-			}
-			var res *http.Response
-			query.Set("sort", "workspace.slug")
-			query.Set("fields", "values.workspace.slug,values.workspace.name,pagelen,page,size")
-			res, err = apiClient.Get("/user/permissions/workspaces", query, nil)
-			if err != nil {
-				return nil, err
-			}
-
-			resBody := &models.WorkspaceResponse{}
-			err = api.UnmarshalResponse(res, resBody)
-			if err != nil {
-				return nil, err
-			}
-
-			return resBody.Values, err
-		},
-		func(basicRes context.BasicRes, gid string, queryData *api.RemoteQueryData, connection models.BitbucketConnection) ([]models.BitbucketApiRepo, errors.Error) {
-			if gid == "" {
-				return nil, nil
-			}
-			query := initialQuery(queryData)
-
-			apiClient, err := api.NewApiClientFromConnection(gocontext.TODO(), basicRes, &connection)
-			if err != nil {
-				return nil, errors.BadInput.Wrap(err, "failed to get create apiClient")
-			}
-			var res *http.Response
-			query.Set("fields", "values.name,values.full_name,values.language,values.description,values.owner.display_name,values.created_on,values.updated_on,values.links.clone,values.links.html,pagelen,page,size")
-			// list projects part
-			res, err = apiClient.Get(fmt.Sprintf("/repositories/%s", gid), query, nil)
-			if err != nil {
-				return nil, err
-			}
-			var resBody models.ReposResponse
-			err = api.UnmarshalResponse(res, &resBody)
-			if err != nil {
-				return nil, err
-			}
-			return resBody.Values, err
-		},
-	)
-}
-
-// SearchRemoteScopes use the Search API and only return project
-// @Summary use the Search API and only return project
-// @Description use the Search API and only return project
-// @Tags plugins/bitbucket
-// @Accept application/json
-// @Param connectionId path int false "connection ID"
-// @Param search query string false "search"
-// @Param page query int false "page number"
-// @Param pageSize query int false "page size per page"
-// @Success 200  {object} api.SearchRemoteScopesOutput
-// @Failure 400  {object} shared.ApiBody "Bad Request"
-// @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bitbucket/connections/{connectionId}/search-remote-scopes [GET]
-func SearchRemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return remoteHelper.SearchRemoteScopes(input,
-		func(basicRes context.BasicRes, queryData *api.RemoteQueryData, connection models.BitbucketConnection) ([]models.BitbucketApiRepo, errors.Error) {
-			// create api client
-			apiClient, err := api.NewApiClientFromConnection(gocontext.TODO(), basicRes, &connection)
-			if err != nil {
-				return nil, errors.BadInput.Wrap(err, "failed to get create apiClient")
-			}
-
-			// request search
-			query := initialQuery(queryData)
-			if len(queryData.Search) == 0 {
-				return nil, errors.BadInput.New("empty search query")
-			}
-			s := queryData.Search[0]
-			if strings.Contains(s, `"`) {
-				return nil, errors.BadInput.New("search query contains invalid character")
-			}
-			query.Set("sort", "name")
-			query.Set("fields", "values.name,values.full_name,values.language,values.description,values.owner.display_name,values.created_on,values.updated_on,values.links.clone,values.links.html,pagelen,page,size")
-			query.Set("role", "member")
-			query.Set("q", fmt.Sprintf(`full_name~"%s"`, s))
-
-			// list repos part
-			res, err := apiClient.Get("/repositories", query, nil)
-			if err != nil {
-				return nil, err
-			}
-
-			var resBody models.ReposResponse
-			err = api.UnmarshalResponse(res, &resBody)
-			if err != nil {
-				return nil, err
-			}
-
-			return resBody.Values, err
-		},
-	)
-}
-
-func initialQuery(queryData *api.RemoteQueryData) url.Values {
-	query := url.Values{}
-	query.Set("page", fmt.Sprintf("%v", queryData.Page))
-	query.Set("pagelen", fmt.Sprintf("%v", queryData.PerPage))
-	return query
-}
diff --git a/backend/plugins/bitbucket/api/remote_api.go b/backend/plugins/bitbucket/api/remote_api.go
new file mode 100644
index 000000000..d81c0d027
--- /dev/null
+++ b/backend/plugins/bitbucket/api/remote_api.go
@@ -0,0 +1,219 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	dsmodels "github.com/apache/incubator-devlake/helpers/pluginhelper/api/models"
+	"github.com/apache/incubator-devlake/plugins/bitbucket/models"
+)
+
+type BitbucketRemotePagination struct {
+	Page    int `json:"page" validate:"required"`
+	PageLen int `json:"pagelen" validate:"required"`
+}
+
+func listBitbucketRemoteScopes(
+	connection *models.BitbucketConnection,
+	apiClient plugin.ApiClient,
+	groupId string,
+	page BitbucketRemotePagination,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo],
+	nextPage *BitbucketRemotePagination,
+	err errors.Error,
+) {
+	if page.Page == 0 {
+		page.Page = 1
+	}
+	if page.PageLen == 0 {
+		page.PageLen = 100
+	}
+
+	if groupId == "" {
+		return listBitbucketWorkspaces(apiClient, page)
+	}
+	return listBitbucketRepos(apiClient, groupId, page)
+}
+
+func listBitbucketWorkspaces(
+	apiClient plugin.ApiClient,
+	page BitbucketRemotePagination,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo],
+	nextPage *BitbucketRemotePagination,
+	err errors.Error,
+) {
+	var res *http.Response
+	res, err = apiClient.Get(
+		"/user/permissions/workspaces",
+		url.Values{
+			"sort":    {"workspace.slug"},
+			"fields":  {"values.workspace.slug,values.workspace.name,pagelen,page,size"},
+			"page":    {fmt.Sprintf("%v", page.Page)},
+			"pagelen": {fmt.Sprintf("%v", page.PageLen)},
+		},
+		nil,
+	)
+	if err != nil {
+		return
+	}
+
+	resBody := &models.WorkspaceResponse{}
+	err = api.UnmarshalResponse(res, resBody)
+	if err != nil {
+		return
+	}
+	for _, r := range resBody.Values {
+		children = append(children, dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo]{
+			Type:     api.RAS_ENTRY_TYPE_GROUP,
+			Id:       r.Workspace.Slug,
+			Name:     r.Workspace.Name,
+			FullName: r.Workspace.Name,
+		})
+	}
+	return
+}
+
+func listBitbucketRepos(
+	apiClient plugin.ApiClient,
+	workspace string,
+	page BitbucketRemotePagination,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo],
+	nextPage *BitbucketRemotePagination,
+	err errors.Error,
+) {
+
+	var res *http.Response
+	// list projects part
+	res, err = apiClient.Get(fmt.Sprintf("/repositories/%s", workspace), url.Values{
+		"fields":  {"values.name,values.full_name,values.language,values.description,values.owner.display_name,values.created_on,values.updated_on,values.links.clone,values.links.html,pagelen,page,size"},
+		"page":    {fmt.Sprintf("%v", page.Page)},
+		"pagelen": {fmt.Sprintf("%v", page.PageLen)},
+	}, nil)
+	if err != nil {
+		return
+	}
+	var resBody models.ReposResponse
+	err = api.UnmarshalResponse(res, &resBody)
+	if err != nil {
+		return
+	}
+
+	for _, r := range resBody.Values {
+		children = append(children, dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo]{
+			Type:     api.RAS_ENTRY_TYPE_SCOPE,
+			Id:       r.FullName,
+			ParentId: &workspace,
+			Name:     r.Name,
+			FullName: r.FullName,
+			Data:     r.ConvertApiScope(),
+		})
+	}
+	return
+}
+
+func searchBitbucketRepos(
+	apiClient plugin.ApiClient,
+	params *dsmodels.DsRemoteApiScopeSearchParams,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo],
+	err errors.Error,
+) {
+	var res *http.Response
+	res, err = apiClient.Get(
+		"/repositories",
+		url.Values{
+			"sort":    {"name"},
+			"fields":  {"values.name,values.full_name,values.language,values.description,values.owner.display_name,values.created_on,values.updated_on,values.links.clone,values.links.html,pagelen,page,size"},
+			"role":    {"member"},
+			"q":       {fmt.Sprintf(`full_name~"%s"`, params.Search)},
+			"page":    {fmt.Sprintf("%v", params.Page)},
+			"pagelen": {fmt.Sprintf("%v", params.PageSize)},
+		},
+		nil,
+	)
+	if err != nil {
+		return nil, err
+	}
+	var resBody models.ReposResponse
+	err = api.UnmarshalResponse(res, &resBody)
+	if err != nil {
+		return
+	}
+	for _, r := range resBody.Values {
+		children = append(children, dsmodels.DsRemoteApiScopeListEntry[models.BitbucketRepo]{
+			Type:     api.RAS_ENTRY_TYPE_SCOPE,
+			Id:       r.FullName,
+			Name:     r.Name,
+			FullName: r.FullName,
+			Data:     r.ConvertApiScope(),
+		})
+	}
+	return
+}
+
+// RemoteScopes list all available scopes on the remote server
+// @Summary list all available scopes on the remote server
+// @Description list all available scopes on the remote server
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param groupId query string false "group ID"
+// @Param pageToken query string false "page Token"
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Success 200  {object} dsmodels.DsRemoteApiScopeList[models.BitbucketRepo]
+// @Tags plugins/bitbucket
+// @Router /plugins/bitbucket/connections/{connectionId}/remote-scopes [GET]
+func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return raScopeList.Get(input)
+}
+
+// SearchRemoteScopes searches scopes on the remote server
+// @Summary searches scopes on the remote server
+// @Description searches scopes on the remote server
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param search query string false "search"
+// @Param page query int false "page number"
+// @Param pageSize query int false "page size per page"
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Success 200  {object} dsmodels.DsRemoteApiScopeList[models.BitbucketRepo] "the parentIds are always null"
+// @Tags plugins/bitbucket
+// @Router /plugins/bitbucket/connections/{connectionId}/search-remote-scopes [GET]
+func SearchRemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return raScopeSearch.Get(input)
+}
+
+// @Summary Remote server API proxy
+// @Description Forward API requests to the specified remote server
+// @Param connectionId path int true "connection ID"
+// @Param path path string true "path to a API endpoint"
+// @Tags plugins/bitbucket
+// @Router /plugins/bitbucket/connections/{connectionId}/proxy/{path} [GET]
+func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return raProxy.Proxy(input)
+}
diff --git a/backend/plugins/bitbucket/api/scope.go b/backend/plugins/bitbucket/api/scope_api.go
similarity index 84%
rename from backend/plugins/bitbucket/api/scope.go
rename to backend/plugins/bitbucket/api/scope_api.go
index c64e400f2..13fddfe2e 100644
--- a/backend/plugins/bitbucket/api/scope.go
+++ b/backend/plugins/bitbucket/api/scope_api.go
@@ -26,29 +26,25 @@ import (
 	"github.com/apache/incubator-devlake/plugins/bitbucket/models"
 )
 
-type ScopeRes struct {
-	models.BitbucketRepo
-	api.ScopeResDoc[models.BitbucketScopeConfig]
-}
-
-type ScopeReq api.ScopeReq[models.BitbucketRepo]
+type PutScopesReqBody api.PutScopesReqBody[models.BitbucketRepo]
+type ScopeDetail api.ScopeDetail[models.BitbucketRepo, models.BitbucketScopeConfig]
 
-// PutScope create or update repo
+// PutScopes create or update repo
 // @Summary create or update repo
 // @Description Create or update repo
 // @Tags plugins/bitbucket
 // @Accept application/json
 // @Param connectionId path int true "connection ID"
-// @Param scope body ScopeReq true "json"
+// @Param scope body PutScopesReqBody 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) {
-	return scopeHelper.Put(input)
+func PutScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return dsHelper.ScopeApi.PutMultiple(input)
 }
 
-// UpdateScope patch to repo
+// PatchScope patch to repo
 // @Summary patch to repo
 // @Description patch to repo
 // @Tags plugins/bitbucket
@@ -60,12 +56,12 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId}/scopes/{scopeId} [PATCH]
-func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
-	return scopeHelper.Update(input)
+	return dsHelper.ScopeApi.Patch(input)
 }
 
-// GetScopeList get repos
+// GetScopes get repos
 // @Summary get repos
 // @Description get repos
 // @Tags plugins/bitbucket
@@ -74,12 +70,12 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err
 // @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
+// @Success 200  {object} []ScopeDetail
 // @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) {
-	return scopeHelper.GetScopeList(input)
+func GetScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return dsHelper.ScopeApi.GetPage(input)
 }
 
 func GetScopeDispatcher(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
@@ -103,7 +99,7 @@ func GetScopeDispatcher(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutp
 // @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)
+	return dsHelper.ScopeApi.GetScopeDetail(input)
 }
 
 // DeleteScope delete plugin data associated with the scope and optionally the scope itself
@@ -120,5 +116,5 @@ func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Router /plugins/bitbucket/connections/{connectionId}/scopes/{scopeId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
-	return scopeHelper.Delete(input)
+	return dsHelper.ScopeApi.Delete(input)
 }
diff --git a/backend/plugins/bitbucket/api/scope_config.go b/backend/plugins/bitbucket/api/scope_config_api.go
similarity index 87%
rename from backend/plugins/bitbucket/api/scope_config.go
rename to backend/plugins/bitbucket/api/scope_config_api.go
index fc0defde0..1e894165c 100644
--- a/backend/plugins/bitbucket/api/scope_config.go
+++ b/backend/plugins/bitbucket/api/scope_config_api.go
@@ -22,22 +22,22 @@ import (
 	"github.com/apache/incubator-devlake/core/plugin"
 )
 
-// CreateScopeConfig create scope config for Bitbucket
+// PostScopeConfig create scope config for Bitbucket
 // @Summary create scope config for Bitbucket
 // @Description create scope config for Bitbucket
-// @Tags plugins/bitbucket
 // @Accept application/json
 // @Param connectionId path int true "connectionId"
 // @Param scopeConfig body models.BitbucketScopeConfig true "scope config"
 // @Success 200  {object} models.BitbucketScopeConfig
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Tags plugins/bitbucket
 // @Router /plugins/bitbucket/connections/{connectionId}/scope-configs [POST]
-func CreateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scHelper.Create(input)
+func PostScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return dsHelper.ScopeConfigApi.Post(input)
 }
 
-// UpdateScopeConfig update scope config for Bitbucket
+// PatchScopeConfig update scope config for Bitbucket
 // @Summary update scope config for Bitbucket
 // @Description update scope config for Bitbucket
 // @Tags plugins/bitbucket
@@ -49,8 +49,8 @@ func CreateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutpu
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId}/scope-configs/{id} [PATCH]
-func UpdateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scHelper.Update(input)
+func PatchScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return dsHelper.ScopeConfigApi.Patch(input)
 }
 
 // GetScopeConfig return one scope config
@@ -64,7 +64,7 @@ func UpdateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutpu
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId}/scope-configs/{id} [GET]
 func GetScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scHelper.Get(input)
+	return dsHelper.ScopeConfigApi.GetDetail(input)
 }
 
 // GetScopeConfigList return all scope configs
@@ -79,7 +79,7 @@ func GetScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/bitbucket/connections/{connectionId}/scope-configs [GET]
 func GetScopeConfigList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scHelper.List(input)
+	return dsHelper.ScopeConfigApi.GetAll(input)
 }
 
 // DeleteScopeConfig delete a scope config
@@ -93,5 +93,5 @@ func GetScopeConfigList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutp
 // @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)
+	return dsHelper.ScopeConfigApi.Delete(input)
 }
diff --git a/backend/plugins/bitbucket/e2e/repo_test.go b/backend/plugins/bitbucket/e2e/repo_test.go
index 239bf6c88..abc764650 100644
--- a/backend/plugins/bitbucket/e2e/repo_test.go
+++ b/backend/plugins/bitbucket/e2e/repo_test.go
@@ -59,7 +59,7 @@ func TestRepoDataFlow(t *testing.T) {
 
 	// verify extraction
 	dataflowTester.FlushTabler(&models.BitbucketRepo{})
-	scope := apiRepo.ConvertApiScope().(*models.BitbucketRepo)
+	scope := apiRepo.ConvertApiScope()
 	scope.ConnectionId = 1
 	err := dataflowTester.Dal.CreateIfNotExist(scope)
 	assert.Nil(t, err)
diff --git a/backend/plugins/bitbucket/impl/impl.go b/backend/plugins/bitbucket/impl/impl.go
index 868288618..9e29984ba 100644
--- a/backend/plugins/bitbucket/impl/impl.go
+++ b/backend/plugins/bitbucket/impl/impl.go
@@ -216,7 +216,7 @@ func (p Bitbucket) ApiResources() map[string]map[string]plugin.ApiResourceHandle
 			// GetScope "connections/:connectionId/scopes/:scopeId"
 			// Because there may be slash in scopeId, so we handle it manually.
 			"GET":    api.GetScopeDispatcher,
-			"PATCH":  api.UpdateScope,
+			"PATCH":  api.PatchScope,
 			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/remote-scopes": {
@@ -226,15 +226,15 @@ func (p Bitbucket) ApiResources() map[string]map[string]plugin.ApiResourceHandle
 			"GET": api.SearchRemoteScopes,
 		},
 		"connections/:connectionId/scopes": {
-			"GET": api.GetScopeList,
-			"PUT": api.PutScope,
+			"GET": api.GetScopes,
+			"PUT": api.PutScopes,
 		},
 		"connections/:connectionId/scope-configs": {
-			"POST": api.CreateScopeConfig,
+			"POST": api.PostScopeConfig,
 			"GET":  api.GetScopeConfigList,
 		},
 		"connections/:connectionId/scope-configs/:id": {
-			"PATCH":  api.UpdateScopeConfig,
+			"PATCH":  api.PatchScopeConfig,
 			"GET":    api.GetScopeConfig,
 			"DELETE": api.DeleteScopeConfig,
 		},
@@ -275,7 +275,7 @@ func EnrichOptions(taskCtx plugin.TaskContext,
 				return err
 			}
 			logger.Debug(fmt.Sprintf("Current repo: %s", repo.FullName))
-			scope := repo.ConvertApiScope().(*models.BitbucketRepo)
+			scope := repo.ConvertApiScope()
 			scope.ConnectionId = op.ConnectionId
 			err = taskCtx.GetDal().CreateIfNotExist(scope)
 			if err != nil {
diff --git a/backend/plugins/bitbucket/models/repo.go b/backend/plugins/bitbucket/models/repo.go
index be68dee91..799b549b9 100644
--- a/backend/plugins/bitbucket/models/repo.go
+++ b/backend/plugins/bitbucket/models/repo.go
@@ -26,7 +26,6 @@ import (
 
 var _ plugin.ToolLayerScope = (*BitbucketRepo)(nil)
 var _ plugin.ApiGroup = (*GroupResponse)(nil)
-var _ plugin.ApiScope = (*BitbucketApiRepo)(nil)
 
 type BitbucketRepo struct {
 	common.Scope `mapstructure:",squash"`
@@ -91,7 +90,7 @@ type BitbucketApiRepo struct {
 	} `json:"links"`
 }
 
-func (b BitbucketApiRepo) ConvertApiScope() plugin.ToolLayerScope {
+func (b BitbucketApiRepo) ConvertApiScope() *BitbucketRepo {
 	scope := &BitbucketRepo{}
 	scope.BitbucketId = b.FullName
 	scope.CreatedDate = b.CreatedAt