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

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

commit 05b798e8d3cfc4063fcd6acdcffc18b132612d8b
Author: Klesh Wong <[email protected]>
AuthorDate: Mon Apr 15 20:04:27 2024 +0800

    refactor: dshelper refactor
---
 .../pluginhelper/api/ds_scope_api_helper.go        |   3 +
 .../pluginhelper/api/scope_generic_helper.go       |   1 -
 backend/helpers/srvhelper/model_service_helper.go  |   1 -
 .../api/{scope.go => scope_api.go}                 |  14 +-
 .../api/{blueprint200.go => blueprint_v200.go}     |   0
 backend/plugins/circleci/tasks/task_data.go        |   6 +-
 backend/plugins/opsgenie/api/blueprint_v200.go     |  22 +--
 .../api/{connection.go => connection_api.go}       |  41 +----
 backend/plugins/opsgenie/api/init.go               |  16 +-
 .../opsgenie/api/{remote.go => remote_api.go}      | 166 ++++++---------------
 .../opsgenie/api/{scope.go => scope_api.go}        |  16 +-
 backend/plugins/tapd/api/blueprint_v200.go         |  90 +++++------
 backend/plugins/tapd/api/blueprint_v200_test.go    | 129 ----------------
 .../tapd/api/{connection.go => connection_api.go}  |  52 +------
 backend/plugins/tapd/api/init.go                   |  48 ++----
 .../plugins/tapd/api/{remote.go => remote_api.go}  |  67 +++++++++
 .../plugins/tapd/api/{scope.go => scope_api.go}    |  26 ++--
 .../api/{scope_config.go => scope_config_api.go}   |  14 +-
 backend/plugins/tapd/impl/impl.go                  |   6 +-
 backend/plugins/tapd/models/connection.go          |   9 ++
 ...go => 20240415_add_company_id_to_connection.go} |  38 +++--
 .../tapd/models/migrationscripts/register.go       |   1 +
 backend/plugins/tapd/tasks/task_data.go            |  12 +-
 23 files changed, 266 insertions(+), 512 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/ds_scope_api_helper.go 
b/backend/helpers/pluginhelper/api/ds_scope_api_helper.go
index 3775f19e3..eb0b8af51 100644
--- a/backend/helpers/pluginhelper/api/ds_scope_api_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_scope_api_helper.go
@@ -24,10 +24,13 @@ import (
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models/common"
        "github.com/apache/incubator-devlake/core/plugin"
+       serviceHelper 
"github.com/apache/incubator-devlake/helpers/pluginhelper/services"
        "github.com/apache/incubator-devlake/helpers/srvhelper"
        "github.com/apache/incubator-devlake/server/api/shared"
 )
 
+type ScopeRefDoc = serviceHelper.BlueprintProjectPairs
+
 type PutScopesReqBody[T any] struct {
        Data []*T `json:"data"`
 }
diff --git a/backend/helpers/pluginhelper/api/scope_generic_helper.go 
b/backend/helpers/pluginhelper/api/scope_generic_helper.go
index 1fe90fae9..a782b4e09 100644
--- a/backend/helpers/pluginhelper/api/scope_generic_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_generic_helper.go
@@ -60,7 +60,6 @@ type (
                Blueprints  []*models.Blueprint 
`mapstructure:"blueprints,omitempty" json:"blueprints"`
        }
        // Alias, for swagger purposes
-       ScopeRefDoc                                            = 
serviceHelper.BlueprintProjectPairs
        ScopeRes[Scope plugin.ToolLayerScope, ScopeConfig any] struct {
                Scope       Scope               `mapstructure:"scope,omitempty" 
json:"scope,omitempty"`
                ScopeConfig *ScopeConfig        
`mapstructure:"scopeConfig,omitempty" json:"scopeConfig,omitempty"`
diff --git a/backend/helpers/srvhelper/model_service_helper.go 
b/backend/helpers/srvhelper/model_service_helper.go
index 9f040d1a7..3ec9abe39 100644
--- a/backend/helpers/srvhelper/model_service_helper.go
+++ b/backend/helpers/srvhelper/model_service_helper.go
@@ -96,7 +96,6 @@ func (srv *ModelSrvHelper[M]) ValidateModel(model *M) 
errors.Error {
 
 // Create validates given model and insert it into database if validation 
passed
 func (srv *ModelSrvHelper[M]) Create(model *M) errors.Error {
-       println("create model")
        err := srv.ValidateModel(model)
        if err != nil {
                return err
diff --git a/backend/plugins/bitbucket_server/api/scope.go 
b/backend/plugins/bitbucket_server/api/scope_api.go
similarity index 94%
rename from backend/plugins/bitbucket_server/api/scope.go
rename to backend/plugins/bitbucket_server/api/scope_api.go
index 3b98ceeec..442566d76 100644
--- a/backend/plugins/bitbucket_server/api/scope.go
+++ b/backend/plugins/bitbucket_server/api/scope_api.go
@@ -26,12 +26,8 @@ import (
        "github.com/apache/incubator-devlake/plugins/bitbucket_server/models"
 )
 
-type ScopeRes struct {
-       models.BitbucketServerRepo
-       api.ScopeResDoc[models.BitbucketServerScopeConfig]
-}
-
-type ScopeReq api.ScopeReq[models.BitbucketServerRepo]
+type PutScopesReqBody api.PutScopesReqBody[models.BitbucketServerRepo]
+type ScopeDetail api.ScopeDetail[models.BitbucketServerRepo, 
models.BitbucketServerScopeConfig]
 
 // PutScope create or update repo
 // @Summary create or update repo
@@ -39,7 +35,7 @@ type ScopeReq api.ScopeReq[models.BitbucketServerRepo]
 // @Tags plugins/bitbucket_server
 // @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.BitbucketServerRepo
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
@@ -74,7 +70,7 @@ 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_server/connections/{connectionId}/scopes/ [GET]
@@ -97,7 +93,7 @@ func GetScopeDispatcher(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutp
 // @Tags plugins/bitbucket_server
 // @Param connectionId path int true "connection ID"
 // @Param scopeId path string true "repo ID"
-// @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_server/connections/{connectionId}/scopes/{scopeId} [GET]
diff --git a/backend/plugins/circleci/api/blueprint200.go 
b/backend/plugins/circleci/api/blueprint_v200.go
similarity index 100%
rename from backend/plugins/circleci/api/blueprint200.go
rename to backend/plugins/circleci/api/blueprint_v200.go
diff --git a/backend/plugins/circleci/tasks/task_data.go 
b/backend/plugins/circleci/tasks/task_data.go
index fafccd952..960325696 100644
--- a/backend/plugins/circleci/tasks/task_data.go
+++ b/backend/plugins/circleci/tasks/task_data.go
@@ -26,9 +26,9 @@ import (
 type CircleciOptions struct {
        ConnectionId  uint64                      `json:"connectionId" 
mapstructure:"connectionId"`
        ProjectSlug   string                      `json:"projectSlug" 
mapstructure:"projectSlug"`
-       PageSize      uint64                      `mapstruct:"pageSize" 
mapstructure:"pageSize,omitempty"`
-       ScopeConfigId uint64                      `json:"scopeConfigId" 
mapstructure:"scopeConfigId,omitempty"`
-       ScopeConfig   *models.CircleciScopeConfig `json:"scopeConfig" 
mapstructure:"scopeConfig,omitempty"`
+       PageSize      uint64                      `json:"pageSize,omitempty" 
mapstructure:"pageSize,omitempty"`
+       ScopeConfigId uint64                      
`json:"scopeConfigId,omitempty" mapstructure:"scopeConfigId,omitempty"`
+       ScopeConfig   *models.CircleciScopeConfig `json:"scopeConfig,omitempty" 
mapstructure:"scopeConfig,omitempty"`
 }
 
 type CircleciTaskData struct {
diff --git a/backend/plugins/opsgenie/api/blueprint_v200.go 
b/backend/plugins/opsgenie/api/blueprint_v200.go
index 3f691ddba..3b34e7fef 100644
--- a/backend/plugins/opsgenie/api/blueprint_v200.go
+++ b/backend/plugins/opsgenie/api/blueprint_v200.go
@@ -35,9 +35,7 @@ func MakeDataSourcePipelinePlanV200(
        connectionId uint64,
        bpScopes []*coreModels.BlueprintScope,
 ) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
-       // get the connection info for url
-       connection := &models.OpsgenieConnection{}
-       err := connectionHelper.FirstById(connection, connectionId)
+       connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
        if err != nil {
                return nil, nil, err
        }
@@ -45,20 +43,15 @@ func MakeDataSourcePipelinePlanV200(
        if err != nil {
                return nil, nil, err
        }
-
-       plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails, 
connection)
+       plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, 
connection)
        if err != nil {
                return nil, nil, err
        }
        scopes, err := makeScopesV200(scopeDetails, connection)
-       if err != nil {
-               return nil, nil, err
-       }
-
-       return plan, scopes, nil
+       return plan, scopes, err
 }
 
-func makeDataSourcePipelinePlanV200(
+func makePipelinePlanV200(
        subtaskMetas []plugin.SubTaskMeta,
        scopeDetails []*srvhelper.ScopeDetail[models.Service, 
models.OpsenieScopeConfig],
        connection *models.OpsgenieConnection,
@@ -77,7 +70,7 @@ func makeDataSourcePipelinePlanV200(
                        subtaskMetas,
                        scopeConfig.Entities,
                        OpsgenieTaskOptions{
-                               ConnectionId: scope.ConnectionId,
+                               ConnectionId: connection.ID,
                                ServiceId:    scope.Id,
                                ServiceName:  scope.Name,
                        },
@@ -95,7 +88,8 @@ func makeDataSourcePipelinePlanV200(
 
 func makeScopesV200(
        scopeDetails []*srvhelper.ScopeDetail[models.Service, 
models.OpsenieScopeConfig],
-       connection *models.OpsgenieConnection) ([]plugin.Scope, errors.Error) {
+       connection *models.OpsgenieConnection,
+) ([]plugin.Scope, errors.Error) {
        scopes := make([]plugin.Scope, 0)
        for _, scopeDetail := range scopeDetails {
                opService, scopeConfig := scopeDetail.Scope, 
scopeDetail.ScopeConfig
@@ -103,7 +97,7 @@ func makeScopesV200(
                if utils.StringsContains(scopeConfig.Entities, 
plugin.DOMAIN_TYPE_TICKET) {
                        domainBoard := &ticket.Board{
                                DomainEntity: domainlayer.DomainEntity{
-                                       Id: 
didgen.NewDomainIdGenerator(&models.Service{}).Generate(opService.ConnectionId, 
opService.Id),
+                                       Id: 
didgen.NewDomainIdGenerator(&models.Service{}).Generate(connection.ID, 
opService.Id),
                                },
                                Name: opService.Name,
                        }
diff --git a/backend/plugins/opsgenie/api/connection.go 
b/backend/plugins/opsgenie/api/connection_api.go
similarity index 83%
rename from backend/plugins/opsgenie/api/connection.go
rename to backend/plugins/opsgenie/api/connection_api.go
index 77f4989e3..c2844efaa 100644
--- a/backend/plugins/opsgenie/api/connection.go
+++ b/backend/plugins/opsgenie/api/connection_api.go
@@ -68,8 +68,7 @@ func testOpsgenieConn(ctx context.Context, connection 
models.OpsgenieConn) (*plu
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/opsgenie/connections/{connectionId}/test [POST]
 func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       connection := &models.OpsgenieConnection{}
-       err := connectionHelper.First(connection, input.Params)
+       connection, err := dsHelper.ConnApi.GetMergedConnection(input)
        if err != nil {
                return nil, err
        }
@@ -114,12 +113,7 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/opsgenie/connections [POST]
 func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       connection := &models.OpsgenieConnection{}
-       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 opsgenie connection
@@ -131,17 +125,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/opsgenie/connections/{connectionId} [PATCH]
 func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       connection := &models.OpsgenieConnection{}
-       if err := connectionHelper.First(&connection, input.Params); err != nil 
{
-               return nil, err
-       }
-       if err := (&models.OpsgenieConnection{}).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(), Status: 
http.StatusOK}, nil
+       return dsHelper.ConnApi.Patch(input)
 }
 
 // @Summary delete opsgenie connection
@@ -153,7 +137,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/opsgenie/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       return connectionHelper.Delete(&models.OpsgenieConnection{}, input)
+       return dsHelper.ConnApi.Delete(input)
 }
 
 // @Summary list opsgenie connections
@@ -164,15 +148,7 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/opsgenie/connections [GET]
 func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       var connections []models.OpsgenieConnection
-       err := connectionHelper.List(&connections)
-       if err != nil {
-               return nil, err
-       }
-       for idx, c := range connections {
-               connections[idx] = c.Sanitize()
-       }
-       return &plugin.ApiResourceOutput{Body: connections}, nil
+       return dsHelper.ConnApi.GetAll(input)
 }
 
 // @Summary get opsgenie connection
@@ -183,10 +159,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/opsgenie/connections/{connectionId} [GET]
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connection := &models.OpsgenieConnection{}
-       err := connectionHelper.First(connection, input.Params)
-       if err != nil {
-               return nil, err
-       }
-       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
+       return dsHelper.ConnApi.GetDetail(input)
 }
diff --git a/backend/plugins/opsgenie/api/init.go 
b/backend/plugins/opsgenie/api/init.go
index 3641eab40..c72ee736f 100644
--- a/backend/plugins/opsgenie/api/init.go
+++ b/backend/plugins/opsgenie/api/init.go
@@ -26,20 +26,15 @@ import (
 )
 
 var vld *validator.Validate
-var connectionHelper *api.ConnectionApiHelper
-var dsHelper *api.DsHelper[models.OpsgenieConnection, models.Service, 
models.OpsenieScopeConfig]
+
 var basicRes context.BasicRes
+var dsHelper *api.DsHelper[models.OpsgenieConnection, models.Service, 
models.OpsenieScopeConfig]
+var raProxy *api.DsRemoteApiProxyHelper[models.OpsgenieConnection]
+var raScopeList *api.DsRemoteApiScopeListHelper[models.OpsgenieConnection, 
models.Service, OpsgenieRemotePagination]
 
 func Init(br context.BasicRes, p plugin.PluginMeta) {
-
        basicRes = br
        vld = validator.New()
-       connectionHelper = api.NewConnectionHelper(
-               basicRes,
-               vld,
-               p.Name(),
-       )
-
        dsHelper = api.NewDataSourceHelper[
                models.OpsgenieConnection, models.Service, 
models.OpsenieScopeConfig,
        ](
@@ -52,5 +47,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
                nil,
                nil,
        )
-
+       raProxy = 
api.NewDsRemoteApiProxyHelper[models.OpsgenieConnection](dsHelper.ConnApi.ModelApiHelper)
+       raScopeList = 
api.NewDsRemoteApiScopeListHelper[models.OpsgenieConnection, models.Service, 
OpsgenieRemotePagination](raProxy, listOpsgenieRemoteScopes)
 }
diff --git a/backend/plugins/opsgenie/api/remote.go 
b/backend/plugins/opsgenie/api/remote_api.go
similarity index 51%
rename from backend/plugins/opsgenie/api/remote.go
rename to backend/plugins/opsgenie/api/remote_api.go
index b6b1780b4..bc3e1999f 100644
--- a/backend/plugins/opsgenie/api/remote.go
+++ b/backend/plugins/opsgenie/api/remote_api.go
@@ -18,43 +18,20 @@ limitations under the License.
 package api
 
 import (
-       "context"
-       "encoding/base64"
-       "encoding/json"
        "fmt"
        "net/http"
        "net/url"
-       "strconv"
 
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models/common"
        "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/opsgenie/models"
        "github.com/apache/incubator-devlake/plugins/opsgenie/models/raw"
 )
 
-type RemoteScopesChild struct {
-       Type     string      `json:"type"`
-       ParentId *string     `json:"parentId"`
-       Id       string      `json:"id"`
-       Name     string      `json:"name"`
-       FullName string      `json:"fullName"`
-       Data     interface{} `json:"data"`
-}
-
-type RemoteScopesOutput struct {
-       Children      []RemoteScopesChild `json:"children"`
-       NextPageToken string              `json:"nextPageToken"`
-}
-
-type SearchRemoteScopesOutput struct {
-       Children []RemoteScopesChild `json:"children"`
-       Page     int                 `json:"page"`
-       PageSize int                 `json:"pageSize"`
-}
-
-type PageData struct {
+type OpsgenieRemotePagination struct {
        Page    int `json:"page"`
        PerPage int `json:"per_page"`
 }
@@ -64,74 +41,46 @@ type ServiceResponse struct {
        Data       []raw.Service `json:"data"`
 }
 
-const RemoteScopesPerPage int = 100
-const TypeScope string = "scope"
-
-// RemoteScopes list all available scopes (services) for this connection
-// @Summary list all available scopes (services) for this connection
-// @Description list all available scopes (services) for this connection
-// @Tags plugins/opsgenie
-// @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} RemoteScopesOutput
-// @Failure 400  {object} shared.ApiBody "Bad Request"
-// @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/opsgenie/connections/{connectionId}/remote-scopes [GET]
-func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, _ := extractParam(input.Params)
-       if connectionId == 0 {
-               return nil, errors.BadInput.New("invalid connectionId")
+func listOpsgenieRemoteScopes(
+       connection *models.OpsgenieConnection,
+       apiClient plugin.ApiClient,
+       groupId string,
+       page OpsgenieRemotePagination,
+) (
+       children []dsmodels.DsRemoteApiScopeListEntry[models.Service],
+       nextPage *OpsgenieRemotePagination,
+       err errors.Error,
+) {
+       if page.Page == 0 {
+               page.Page = 1
        }
-
-       connection := &models.OpsgenieConnection{}
-       err := connectionHelper.First(connection, input.Params)
-       if err != nil {
-               return nil, err
-       }
-
-       pageToken, ok := input.Query["pageToken"]
-       if !ok || len(pageToken) == 0 {
-               pageToken = []string{""}
+       if page.PerPage == 0 {
+               page.PerPage = 100
        }
 
-       pageData, err := DecodeFromPageToken(pageToken[0])
-       if err != nil {
-               return nil, errors.BadInput.New("failed to get page token")
-       }
-
-       // create api client
-       apiClient, err := api.NewApiClientFromConnection(context.TODO(), 
basicRes, connection)
-       if err != nil {
-               return nil, err
-       }
-
-       query, err := GetQueryFromPageData(pageData)
-       if err != nil {
-               return nil, err
+       query := url.Values{
+               "page":     []string{fmt.Sprintf("%v", page.Page)},
+               "per_page": []string{fmt.Sprintf("%v", page.PerPage)},
        }
 
-       var res *http.Response
-       outputBody := &RemoteScopesOutput{}
-       res, err = apiClient.Get("v1/services", query, nil)
+       res, err := apiClient.Get("v1/services", query, nil)
        if err != nil {
-               return nil, err
+               return nil, nil, err
        }
        response := &ServiceResponse{}
        err = api.UnmarshalResponse(res, response)
        if err != nil {
-               return nil, err
+               return nil, nil, err
        }
 
        // append service to output
        for _, service := range response.Data {
-               child := RemoteScopesChild{
-                       Type:     TypeScope,
+               children = append(children, 
dsmodels.DsRemoteApiScopeListEntry[models.Service]{
+                       Type:     api.RAS_ENTRY_TYPE_SCOPE,
                        Id:       service.Id,
                        Name:     service.Name,
                        FullName: service.Name,
-                       Data: models.Service{
+                       Data: &models.Service{
                                Url:    service.Links.Web,
                                Id:     service.Id,
                                Name:   service.Name,
@@ -141,11 +90,26 @@ func RemoteScopes(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
                                        ConnectionId: connection.ID,
                                },
                        },
-               }
-               outputBody.Children = append(outputBody.Children, child)
+               })
        }
 
-       return &plugin.ApiResourceOutput{Body: outputBody, Status: 
http.StatusOK}, nil
+       return
+}
+
+// RemoteScopes list all available scopes (services) for this connection
+// @Summary list all available scopes (services) for this connection
+// @Description list all available scopes (services) for this connection
+// @Tags plugins/opsgenie
+// @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} RemoteScopesOutput
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId}/remote-scopes [GET]
+func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return raScopeList.Get(input)
 }
 
 // SearchRemoteScopes use the Search API and only return project
@@ -165,47 +129,3 @@ func SearchRemoteScopes(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutp
        // Not supported
        return &plugin.ApiResourceOutput{Body: nil, Status: 
http.StatusMethodNotAllowed}, nil
 }
-
-func EncodeToPageToken(pageData *PageData) (string, errors.Error) {
-       // Marshal json
-       pageTokenDecode, err := json.Marshal(pageData)
-       if err != nil {
-               return "", errors.Default.Wrap(err, fmt.Sprintf("Marshal 
pageToken failed %+v", pageData))
-       }
-       // Encode pageToken Base64
-       return base64.StdEncoding.EncodeToString(pageTokenDecode), nil
-}
-
-func DecodeFromPageToken(pageToken string) (*PageData, errors.Error) {
-       if pageToken == "" {
-               return &PageData{
-                       Page:    0,
-                       PerPage: RemoteScopesPerPage,
-               }, nil
-       }
-       // Decode pageToken Base64
-       pageTokenDecode, err := base64.StdEncoding.DecodeString(pageToken)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, fmt.Sprintf("decode 
pageToken failed %s", pageToken))
-       }
-       // Unmarshal json
-       pt := &PageData{}
-       err = json.Unmarshal(pageTokenDecode, pt)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, fmt.Sprintf("json 
Unmarshal pageTokenDecode failed %s", pageTokenDecode))
-       }
-       return pt, nil
-}
-
-func GetQueryFromPageData(pageData *PageData) (url.Values, errors.Error) {
-       query := url.Values{}
-       query.Set("offset", fmt.Sprintf("%v", pageData.Page*pageData.PerPage))
-       query.Set("limit", fmt.Sprintf("%v", pageData.PerPage))
-       return query, nil
-}
-
-func extractParam(params map[string]string) (uint64, uint64) {
-       connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
-       serviceId, _ := strconv.ParseUint(params["serviceId"], 10, 64)
-       return connectionId, serviceId
-}
diff --git a/backend/plugins/opsgenie/api/scope.go 
b/backend/plugins/opsgenie/api/scope_api.go
similarity index 93%
rename from backend/plugins/opsgenie/api/scope.go
rename to backend/plugins/opsgenie/api/scope_api.go
index 4e0cb2f76..572e75d29 100644
--- a/backend/plugins/opsgenie/api/scope.go
+++ b/backend/plugins/opsgenie/api/scope_api.go
@@ -24,12 +24,8 @@ import (
        "github.com/apache/incubator-devlake/plugins/opsgenie/models"
 )
 
-type ScopeRes struct {
-       models.Service
-       api.ScopeResDoc[models.Service]
-}
-
-type ScopeReq api.ScopeReq[models.Service]
+type PutScopesReqBody api.PutScopesReqBody[models.Service]
+type ScopeDetail api.ScopeDetail[models.Service, models.OpsenieScopeConfig]
 
 // PutScope create or update opsgenie service
 // @Summary create or update opsgenie service
@@ -37,8 +33,8 @@ type ScopeReq api.ScopeReq[models.Service]
 // @Tags plugins/opsgenie
 // @Accept application/json
 // @Param connectionId path int true "connection ID"
-// @Param scope body ScopeReq true "json"
-// @Success 200  {object} []ScopeRes
+// @Param scope body PutScopesReqBody true "json"
+// @Success 200  {object} []models.Service
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/opsgenie/connections/{connectionId}/scopes [PUT]
@@ -71,7 +67,7 @@ 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/opsgenie/connections/{connectionId}/scopes/ [GET]
@@ -86,7 +82,7 @@ func GetScopeList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
 // @Param connectionId path int true "connection ID"
 // @Param serviceId path int true "service ID"
 // @Param blueprints query bool false "also return blueprints using this scope 
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/opsgenie/connections/{connectionId}/scopes/{serviceId} 
[GET]
diff --git a/backend/plugins/tapd/api/blueprint_v200.go 
b/backend/plugins/tapd/api/blueprint_v200.go
index 8ab9cb0cb..159c1ec3a 100644
--- a/backend/plugins/tapd/api/blueprint_v200.go
+++ b/backend/plugins/tapd/api/blueprint_v200.go
@@ -18,17 +18,16 @@ limitations under the License.
 package api
 
 import (
-       "strconv"
-
        "github.com/apache/incubator-devlake/core/errors"
        coreModels "github.com/apache/incubator-devlake/core/models"
-       "github.com/apache/incubator-devlake/core/models/domainlayer"
        "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
        "github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
        "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/tapd/models"
+       "github.com/apache/incubator-devlake/plugins/tapd/tasks"
 )
 
 func MakeDataSourcePipelinePlanV200(
@@ -36,53 +35,50 @@ func MakeDataSourcePipelinePlanV200(
        connectionId uint64,
        bpScopes []*coreModels.BlueprintScope,
 ) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
-       plan := make(coreModels.PipelinePlan, len(bpScopes))
-       plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, plan, 
bpScopes, connectionId)
+       connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
        if err != nil {
                return nil, nil, err
        }
-       scopes, err := makeScopesV200(bpScopes, connectionId)
+       scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, 
bpScopes)
        if err != nil {
                return nil, nil, err
        }
-
-       return plan, scopes, nil
+       plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, 
connection)
+       if err != nil {
+               return nil, nil, err
+       }
+       scopes, err := makeScopesV200(scopeDetails, connection)
+       return plan, scopes, err
 }
 
-func makeDataSourcePipelinePlanV200(
+func makePipelinePlanV200(
        subtaskMetas []plugin.SubTaskMeta,
-       plan coreModels.PipelinePlan,
-       bpScopes []*coreModels.BlueprintScope,
-       connectionId uint64,
+       scopeDetails []*srvhelper.ScopeDetail[models.TapdWorkspace, 
models.TapdScopeConfig],
+       connection *models.TapdConnection,
 ) (coreModels.PipelinePlan, errors.Error) {
-       for i, bpScope := range bpScopes {
+
+       plan := make(coreModels.PipelinePlan, len(scopeDetails))
+       for i, scopeDetail := range scopeDetails {
                stage := plan[i]
                if stage == nil {
                        stage = coreModels.PipelineStage{}
                }
-               // construct task options for tapd
-               options := make(map[string]interface{})
-               intNum, err := errors.Convert01(strconv.Atoi(bpScope.ScopeId))
-               if err != nil {
-                       return nil, err
-               }
-               options["workspaceId"] = intNum
-               options["connectionId"] = connectionId
 
-               _, scopeConfig, err := 
scopeHelper.DbHelper().GetScopeAndConfig(connectionId, bpScope.ScopeId)
+               scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
+               // construct task options for circleci
+               task, err := helper.MakePipelinePlanTask(
+                       "tapd",
+                       subtaskMetas,
+                       scopeConfig.Entities,
+                       tasks.TapdOptions{
+                               ConnectionId: connection.ID,
+                               WorkspaceId:  scope.Id,
+                       },
+               )
                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:   "tapd",
-                       Subtasks: subtasks,
-                       Options:  options,
-               })
+               stage = append(stage, task)
                plan[i] = stage
        }
 
@@ -90,28 +86,22 @@ func makeDataSourcePipelinePlanV200(
 }
 
 func makeScopesV200(
-       bpScopes []*coreModels.BlueprintScope,
-       connectionId uint64) ([]plugin.Scope, errors.Error,
-) {
-       scopes := make([]plugin.Scope, 0)
-       for _, bpScope := range bpScopes {
-               // get workspace and scope config from db
+       scopeDetails []*srvhelper.ScopeDetail[models.TapdWorkspace, 
models.TapdScopeConfig],
+       connection *models.TapdConnection,
+) ([]plugin.Scope, errors.Error) {
+       scopes := make([]plugin.Scope, len(scopeDetails))
 
-               tapdWorkspace, scopeConfig, err := 
scopeHelper.DbHelper().GetScopeAndConfig(connectionId, bpScope.ScopeId)
-               if err != nil {
-                       return nil, err
-               }
+       idgen := didgen.NewDomainIdGenerator(&models.TapdWorkspace{})
+       for _, scopeDetail := range scopeDetails {
+               // get workspace and scope config from db
+               tapdWorkspace, scopeConfig := scopeDetail.Scope, 
scopeDetail.ScopeConfig
 
                // add wrokspace to scopes
                if utils.StringsContains(scopeConfig.Entities, 
plugin.DOMAIN_TYPE_TICKET) {
-                       domainBoard := &ticket.Board{
-                               DomainEntity: domainlayer.DomainEntity{
-                                       Id: 
didgen.NewDomainIdGenerator(&models.TapdWorkspace{}).Generate(tapdWorkspace.ConnectionId,
 tapdWorkspace.Id),
-                               },
-                               Name: tapdWorkspace.Name,
-                               Type: "scrum",
-                       }
-                       scopes = append(scopes, domainBoard)
+                       id := idgen.Generate(connection.ID, tapdWorkspace)
+                       board := ticket.NewBoard(id, tapdWorkspace.Name)
+                       board.Type = "scrum"
+                       scopes = append(scopes, board)
                }
        }
        return scopes, nil
diff --git a/backend/plugins/tapd/api/blueprint_v200_test.go 
b/backend/plugins/tapd/api/blueprint_v200_test.go
deleted file mode 100644
index 9b050470c..000000000
--- a/backend/plugins/tapd/api/blueprint_v200_test.go
+++ /dev/null
@@ -1,129 +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"
-       "github.com/apache/incubator-devlake/core/dal"
-       "gorm.io/gorm/migrator"
-       "testing"
-
-       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/ticket"
-       "github.com/apache/incubator-devlake/core/plugin"
-       "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/tapd/models"
-       "github.com/stretchr/testify/assert"
-       "github.com/stretchr/testify/mock"
-)
-
-func TestMakeDataSourcePipelinePlanV200(t *testing.T) {
-       mockMeta := mockplugin.NewPluginMeta(t)
-       
mockMeta.On("RootPkgPath").Return("github.com/apache/incubator-devlake/plugins/tapd")
-       mockMeta.On("Name").Return("dummy").Maybe()
-       err := plugin.RegisterPlugin("tapd", mockMeta)
-       assert.Nil(t, err)
-       bs := &coreModels.BlueprintScope{
-               ScopeId: "10",
-       }
-       bpScopes := make([]*coreModels.BlueprintScope, 0)
-       bpScopes = append(bpScopes, bs)
-       plan := make(coreModels.PipelinePlan, len(bpScopes))
-       mockBasicRes(t)
-
-       plan, err = makeDataSourcePipelinePlanV200(nil, plan, bpScopes, 
uint64(1))
-       assert.Nil(t, err)
-       scopes, err := makeScopesV200(bpScopes, uint64(1))
-       assert.Nil(t, err)
-
-       expectPlan := coreModels.PipelinePlan{
-               coreModels.PipelineStage{
-                       {
-                               Plugin:   "tapd",
-                               Subtasks: []string{},
-                               Options: map[string]interface{}{
-                                       "connectionId": uint64(1),
-                                       "workspaceId":  10,
-                               },
-                       },
-               },
-       }
-       assert.Equal(t, expectPlan, plan)
-
-       expectScopes := make([]plugin.Scope, 0)
-       tapdBoard := &ticket.Board{
-               DomainEntity: domainlayer.DomainEntity{
-                       Id: "tapd:TapdWorkspace:1:10",
-               },
-               Name: "a",
-               Type: "scrum",
-       }
-
-       expectScopes = append(expectScopes, tapdBoard)
-       assert.Equal(t, expectScopes, scopes)
-}
-
-func mockBasicRes(t *testing.T) {
-       tapdWorkspace := &models.TapdWorkspace{
-               Scope: common.Scope{
-                       ConnectionId: 1,
-               },
-               Id:   10,
-               Name: "a",
-       }
-       scopeConfig := &models.TapdScopeConfig{
-               ScopeConfig: common.ScopeConfig{
-                       Entities: []string{plugin.DOMAIN_TYPE_TICKET},
-               },
-       }
-       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.TapdScopeConfig"), mock.Anything).Run(func(args 
mock.Arguments) {
-                       dst := args.Get(0).(*models.TapdScopeConfig)
-                       *dst = *scopeConfig
-               }).Return(nil)
-               mockDal.On("First", 
mock.AnythingOfType("*models.TapdWorkspace"), mock.Anything).Run(func(args 
mock.Arguments) {
-                       dst := args.Get(0).(*models.TapdWorkspace)
-                       *dst = *tapdWorkspace
-               }).Return(nil)
-               mockDal.On("GetPrimarykeyColumns", 
mock.AnythingOfType("*models.TapdConnection"), mock.Anything).Run(nil).Return(
-                       testColumTypes, nil)
-               mockDal.On("GetColumns", 
mock.AnythingOfType("models.TapdConnection"), mock.Anything).Run(nil).Return(
-                       testColumTypes, nil)
-               mockDal.On("GetColumns", 
mock.AnythingOfType("models.TapdWorkspace"), mock.Anything).Run(nil).Return(
-                       testColumTypes, nil)
-               mockDal.On("GetColumns", 
mock.AnythingOfType("models.TapdScopeConfig"), 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/tapd/api/connection.go 
b/backend/plugins/tapd/api/connection_api.go
similarity index 80%
rename from backend/plugins/tapd/api/connection.go
rename to backend/plugins/tapd/api/connection_api.go
index e7e9733ab..97224d75d 100644
--- a/backend/plugins/tapd/api/connection.go
+++ b/backend/plugins/tapd/api/connection_api.go
@@ -100,8 +100,7 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/tapd/connections/{connectionId}/test [POST]
 func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       connection := &models.TapdConnection{}
-       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,17 +124,7 @@ func TestExistingConnection(input 
*plugin.ApiResourceInput) (*plugin.ApiResource
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/tapd/connections [POST]
 func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       // create a new connections
-       connection := &models.TapdConnection{}
-
-       // update from request and save to database
-       //err := refreshAndSaveTapdConnection(tapdConnection, input.Body)
-       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 tapd connection
@@ -147,17 +136,7 @@ func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/tapd/connections/{connectionId} [PATCH]
 func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       connection := &models.TapdConnection{}
-       if err := connectionHelper.First(&connection, input.Params); err != nil 
{
-               return nil, err
-       }
-       if err := (&models.TapdConnection{}).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 tapd connection
@@ -169,13 +148,7 @@ func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/tapd/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       conn := &models.TapdConnection{}
-       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 tapd connections
@@ -186,15 +159,7 @@ func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/tapd/connections [GET]
 func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       var connections []models.TapdConnection
-       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 tapd connection detail
@@ -205,10 +170,5 @@ func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/tapd/connections/{connectionId} [GET]
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connection := &models.TapdConnection{}
-       err := connectionHelper.First(connection, input.Params)
-       if err != nil {
-               return nil, err
-       }
-       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
+       return dsHelper.ConnApi.GetDetail(input)
 }
diff --git a/backend/plugins/tapd/api/init.go b/backend/plugins/tapd/api/init.go
index 9a387f616..db1534c0b 100644
--- a/backend/plugins/tapd/api/init.go
+++ b/backend/plugins/tapd/api/init.go
@@ -26,48 +26,20 @@ import (
 )
 
 var vld *validator.Validate
-var connectionHelper *api.ConnectionApiHelper
 var basicRes context.BasicRes
-var scopeHelper *api.ScopeApiHelper[models.TapdConnection, 
models.TapdWorkspace, models.TapdScopeConfig]
-var remoteHelper *api.RemoteApiHelper[models.TapdConnection, 
models.TapdWorkspace, models.TapdWorkspace, api.BaseRemoteGroupResponse]
-var scHelper *api.ScopeConfigHelper[models.TapdScopeConfig, 
*models.TapdScopeConfig]
 var dsHelper *api.DsHelper[models.TapdConnection, models.TapdWorkspace, 
models.TapdScopeConfig]
+var raProxy *api.DsRemoteApiProxyHelper[models.TapdConnection]
+var raScopeList *api.DsRemoteApiScopeListHelper[models.TapdConnection, 
models.TapdWorkspace, api.BaseRemoteGroupResponse]
+var raScopeSearch *api.DsRemoteApiScopeSearchHelper[models.TapdConnection, 
models.TapdWorkspace]
 
 func Init(br context.BasicRes, p plugin.PluginMeta) {
-
        basicRes = br
        vld = validator.New()
-       connectionHelper = api.NewConnectionHelper(
-               basicRes,
-               vld,
-               p.Name(),
-       )
-       params := &api.ReflectionParameters{
-               ScopeIdFieldName:     "Id",
-               ScopeIdColumnName:    "id",
-               RawScopeParamName:    "WorkSpaceId",
-               SearchScopeParamName: "name",
-       }
-       scopeHelper = api.NewScopeHelper[models.TapdConnection, 
models.TapdWorkspace, models.TapdScopeConfig](
-               basicRes,
-               vld,
-               connectionHelper,
-               api.NewScopeDatabaseHelperImpl[models.TapdConnection, 
models.TapdWorkspace, models.TapdScopeConfig](
-                       basicRes, connectionHelper, params),
-               params,
-               nil,
-       )
-       remoteHelper = api.NewRemoteHelper[models.TapdConnection, 
models.TapdWorkspace, models.TapdWorkspace, api.BaseRemoteGroupResponse](
-               basicRes,
-               vld,
-               connectionHelper,
-       )
-       scHelper = api.NewScopeConfigHelper[models.TapdScopeConfig, 
*models.TapdScopeConfig](
-               basicRes,
-               vld,
-               p.Name(),
-       )
-
+       // remoteHelper = api.NewRemoteHelper[models.TapdConnection, 
models.TapdWorkspace, models.TapdWorkspace, api.BaseRemoteGroupResponse](
+       //      basicRes,
+       //      vld,
+       //      connectionHelper,
+       // )
        dsHelper = api.NewDataSourceHelper[
                models.TapdConnection, models.TapdWorkspace, 
models.TapdScopeConfig,
        ](
@@ -80,5 +52,7 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
                nil,
                nil,
        )
-
+       raProxy = 
api.NewDsRemoteApiProxyHelper[models.TapdConnection](dsHelper.ConnApi.ModelApiHelper)
+       raScopeList = api.NewDsRemoteApiScopeListHelper[models.TapdConnection, 
models.TapdWorkspace, BaseRemoteGroupResponse](raProxy, listTapdRemoteScopes)
+       raScopeSearch = 
api.NewDsRemoteApiScopeSearchHelper[models.TapdConnection, 
models.TapdWorkspace](raProxy, searchTapdRepos)
 }
diff --git a/backend/plugins/tapd/api/remote.go 
b/backend/plugins/tapd/api/remote_api.go
similarity index 77%
rename from backend/plugins/tapd/api/remote.go
rename to backend/plugins/tapd/api/remote_api.go
index edc403538..067ec42a3 100644
--- a/backend/plugins/tapd/api/remote.go
+++ b/backend/plugins/tapd/api/remote_api.go
@@ -29,10 +29,77 @@ import (
        "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/helpers/srvhelper"
        "github.com/apache/incubator-devlake/plugins/tapd/models"
        "github.com/apache/incubator-devlake/plugins/tapd/tasks"
 )
 
+func listTapdRemoteScopes(
+       apiClient plugin.ApiClient,
+       groupId string,
+       page srvhelper.NoPagintation,
+) (
+       children []dsmodels.DsRemoteApiScopeListEntry[models.TapdWorkspace],
+       nextPage *srvhelper.NoPagintation,
+       err errors.Error,
+) {
+       // if page.Page == 0 { page.Page = 1
+       // }
+       // if page.PerPage == 0 {
+       //      page.PerPage = 100
+       // }
+
+       if groupId == "" {
+               return listTapdParents(apiClient, page)
+       }
+       return listTapdLeafs(apiClient, groupId, page)
+}
+
+func listTapdParents(
+       apiClient plugin.ApiClient,
+       page srvhelper.NoPagintation,
+) (
+       children []dsmodels.DsRemoteApiScopeListEntry[models.TapdWorkspace],
+       nextPage *srvhelper.NoPagintation,
+       err errors.Error,
+) {
+       query := url.Values{}
+       query.Set("company_id", apiClient.GetData("company_id").(string))
+       res, err := apiClient.Get("/workspaces/projects", query, nil)
+       if err != nil {
+               return
+       }
+
+       var resBody models.WorkspacesResponse
+       err = api.UnmarshalResponse(res, &resBody)
+       if err != nil {
+               return nil, nil, err
+       }
+       if resBody.Status != 1 {
+               return nil, nil, errors.BadInput.Wrap(err, "failed to get 
workspaces")
+       }
+
+       // check if workspace is a group
+       isGroupMap := map[uint64]bool{}
+       for _, workspace := range resBody.Data {
+               isGroupMap[workspace.TapdWorkspace.ParentId] = true
+       }
+
+       groups := []api.BaseRemoteGroupResponse{}
+       for _, workspace := range resBody.Data {
+               if fmt.Sprintf(`%d`, workspace.TapdWorkspace.ParentId) == gid &&
+                       isGroupMap[workspace.TapdWorkspace.Id] {
+                       groups = append(groups, api.BaseRemoteGroupResponse{
+                               Id:   fmt.Sprintf(`%d`, 
workspace.TapdWorkspace.Id),
+                               Name: workspace.TapdWorkspace.Name,
+                       })
+               }
+       }
+
+       return groups, err
+}
+
 // PrepareFirstPageToken prepare first page token
 // @Summary prepare first page token
 // @Description prepare first page token
diff --git a/backend/plugins/tapd/api/scope.go 
b/backend/plugins/tapd/api/scope_api.go
similarity index 88%
rename from backend/plugins/tapd/api/scope.go
rename to backend/plugins/tapd/api/scope_api.go
index 673194113..8f020ccaa 100644
--- a/backend/plugins/tapd/api/scope.go
+++ b/backend/plugins/tapd/api/scope_api.go
@@ -26,12 +26,8 @@ import (
        "github.com/apache/incubator-devlake/plugins/tapd/models"
 )
 
-type ScopeRes struct {
-       models.TapdWorkspace
-       api.ScopeResDoc[models.TapdScopeConfig]
-}
-
-type ScopeReq api.ScopeReq[models.TapdWorkspace]
+type PutScopesReqBody api.PutScopesReqBody[models.TapdWorkspace]
+type ScopeDetail api.ScopeDetail[models.TapdWorkspace, models.TapdScopeConfig]
 
 // PutScope create or update tapd job
 // @Summary create or update tapd job
@@ -39,13 +35,13 @@ type ScopeReq api.ScopeReq[models.TapdWorkspace]
 // @Tags plugins/tapd
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param scope body ScopeReq true "json"
+// @Param scope body PutScopesReqBody true "json"
 // @Success 200  {object} []models.TapdWorkspace
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/tapd/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 tapd job
@@ -61,7 +57,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)
+       return dsHelper.ScopeApi.Patch(input)
 }
 
 // GetScopeList get tapd jobs
@@ -73,12 +69,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/tapd/connections/{connectionId}/scopes [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       return scopeHelper.GetScopeList(input)
+       return dsHelper.ScopeApi.GetPage(input)
 }
 
 // GetScope get one tapd job
@@ -87,13 +83,13 @@ func GetScopeList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
 // @Tags plugins/tapd
 // @Param connectionId path int false "connection ID"
 // @Param scopeId path string false "workspace ID"
-// @Success 200  {object} ScopeRes
+// @Success 200  {object} ScopeDetail
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @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)
+       return dsHelper.ScopeApi.GetScopeDetail(input)
 }
 
 // DeleteScope delete plugin data associated with the scope and optionally the 
scope itself
@@ -109,5 +105,5 @@ func GetScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @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)
+       return dsHelper.ScopeApi.Delete(input)
 }
diff --git a/backend/plugins/tapd/api/scope_config.go 
b/backend/plugins/tapd/api/scope_config_api.go
similarity index 89%
rename from backend/plugins/tapd/api/scope_config.go
rename to backend/plugins/tapd/api/scope_config_api.go
index 7f64714d0..abca28949 100644
--- a/backend/plugins/tapd/api/scope_config.go
+++ b/backend/plugins/tapd/api/scope_config_api.go
@@ -33,8 +33,8 @@ import (
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/tapd/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 Tapd
@@ -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/tapd/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/tapd/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/tapd/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/tapd/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/tapd/impl/impl.go 
b/backend/plugins/tapd/impl/impl.go
index 40835318f..cbaa0aaa8 100644
--- a/backend/plugins/tapd/impl/impl.go
+++ b/backend/plugins/tapd/impl/impl.go
@@ -309,14 +309,14 @@ func (p Tapd) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                },
                "connections/:connectionId/scopes": {
                        "GET": api.GetScopeList,
-                       "PUT": api.PutScope,
+                       "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,
                },
diff --git a/backend/plugins/tapd/models/connection.go 
b/backend/plugins/tapd/models/connection.go
index 2238d712e..84aaeffff 100644
--- a/backend/plugins/tapd/models/connection.go
+++ b/backend/plugins/tapd/models/connection.go
@@ -18,6 +18,8 @@ limitations under the License.
 package models
 
 import (
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
        helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 )
 
@@ -25,6 +27,7 @@ import (
 type TapdConn struct {
        helper.RestConnection `mapstructure:",squash"`
        helper.BasicAuth      `mapstructure:",squash"`
+       CompanyId             uint64 `gorm:"type:BIGINT" 
mapstructure:"company_id,string" json:"company_id,string" validate:"required"`
 }
 
 func (connection TapdConn) Sanitize() TapdConn {
@@ -58,3 +61,9 @@ func (connection *TapdConnection) MergeFromRequest(target 
*TapdConnection, body
        }
        return nil
 }
+
+// PrepareApiClient splits Token to tokens for SetupAuthentication to utilize
+func (conn *TapdConn) PrepareApiClient(apiClient plugin.ApiClient) 
errors.Error {
+       apiClient.SetData("company_id", conn.CompanyId)
+       return nil
+}
diff --git a/backend/plugins/tapd/models/migrationscripts/register.go 
b/backend/plugins/tapd/models/migrationscripts/20240415_add_company_id_to_connection.go
similarity index 53%
copy from backend/plugins/tapd/models/migrationscripts/register.go
copy to 
backend/plugins/tapd/models/migrationscripts/20240415_add_company_id_to_connection.go
index cd4f5d3a5..347e2de76 100644
--- a/backend/plugins/tapd/models/migrationscripts/register.go
+++ 
b/backend/plugins/tapd/models/migrationscripts/20240415_add_company_id_to_connection.go
@@ -18,20 +18,32 @@ limitations under the License.
 package migrationscripts
 
 import (
+       "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/migrationhelper"
 )
 
-// All return all the migration scripts
-func All() []plugin.MigrationScript {
-       return []plugin.MigrationScript{
-               new(addInitTables),
-               new(encodeConnToken),
-               new(addTransformation),
-               new(deleteIssue),
-               new(modifyCustomFieldName),
-               new(addCustomFieldValue),
-               new(renameTr2ScopeConfig),
-               new(addRawParamTableForScope),
-               new(addConnIdToLabels),
-       }
+var _ plugin.MigrationScript = (*addCompanyIdToConnection)(nil)
+
+type connection20240415 struct {
+       CompanyId uint64
+}
+
+func (connection20240415) TableName() string {
+       return "_tool_tapd_connections"
+}
+
+type addCompanyIdToConnection struct{}
+
+func (script *addCompanyIdToConnection) Up(basicRes context.BasicRes) 
errors.Error {
+       return migrationhelper.AutoMigrateTables(basicRes, 
&connection20240415{})
+}
+
+func (*addCompanyIdToConnection) Version() uint64 {
+       return 20240415000000
+}
+
+func (script *addCompanyIdToConnection) Name() string {
+       return "add CompanyId to Connection"
 }
diff --git a/backend/plugins/tapd/models/migrationscripts/register.go 
b/backend/plugins/tapd/models/migrationscripts/register.go
index cd4f5d3a5..276ab8997 100644
--- a/backend/plugins/tapd/models/migrationscripts/register.go
+++ b/backend/plugins/tapd/models/migrationscripts/register.go
@@ -33,5 +33,6 @@ func All() []plugin.MigrationScript {
                new(renameTr2ScopeConfig),
                new(addRawParamTableForScope),
                new(addConnIdToLabels),
+               new(addCompanyIdToConnection),
        }
 }
diff --git a/backend/plugins/tapd/tasks/task_data.go 
b/backend/plugins/tapd/tasks/task_data.go
index 16b077bbb..467d055a8 100644
--- a/backend/plugins/tapd/tasks/task_data.go
+++ b/backend/plugins/tapd/tasks/task_data.go
@@ -26,12 +26,12 @@ import (
 )
 
 type TapdOptions struct {
-       ConnectionId  uint64 `mapstruct:"connectionId"`
-       WorkspaceId   uint64 `mapstruct:"workspaceId"`
-       PageSize      uint64 `mapstruct:"pageSize"`
-       CstZone       *time.Location
-       ScopeConfigId uint64
-       ScopeConfig   *models.TapdScopeConfig `json:"scopeConfig"`
+       ConnectionId  uint64                  `json:"connectionId" 
mapstructure:"connectionId,omitempty"`
+       WorkspaceId   uint64                  `json:"workspaceId" 
mapstructure:"workspaceId,omitempty"`
+       PageSize      uint64                  `json:"pageSize,omitempty" 
mapstructure:"pageSize,omitempty"`
+       CstZone       *time.Location          `json:"cstZone,omitempty" 
mapstructure:"cstZone,omitempty"`
+       ScopeConfigId uint64                  `json:"scopeConfigId,omitempty" 
mapstructure:"scopeConfigId,omitempty"`
+       ScopeConfig   *models.TapdScopeConfig `json:"scopeConfig,omitempty" 
mapstructure:"pageSize,omitempty"`
 }
 
 type TapdTaskData struct {

Reply via email to