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/04/22 04:12:39 UTC

(incubator-devlake) branch main updated: refactor: pagerduty adopts the new dshelpers (#7353)

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 5e3a4d733 refactor: pagerduty adopts the new dshelpers (#7353)
5e3a4d733 is described below

commit 5e3a4d7337ded3b27db1931e074a5a46cb1aaca8
Author: Klesh Wong <zh...@merico.dev>
AuthorDate: Mon Apr 22 12:12:35 2024 +0800

    refactor: pagerduty adopts the new dshelpers (#7353)
    
    * refactor: pagerduty adopts the new dshelpers
    
    * fix: linting
---
 backend/plugins/circleci/api/connection_api.go     |   3 +-
 backend/plugins/jenkins/api/scope_api.go           |  18 +-
 backend/plugins/jenkins/impl/impl.go               |   2 +-
 backend/plugins/pagerduty/api/blueprint_v200.go    |  95 ++++-----
 .../api/{connection.go => connection_api.go}       |  47 +----
 backend/plugins/pagerduty/api/init.go              |  35 +---
 backend/plugins/pagerduty/api/remote.go            | 233 ---------------------
 backend/plugins/pagerduty/api/remote_api.go        | 173 +++++++++++++++
 .../pagerduty/api/{scope.go => scope_api.go}       |  32 ++-
 backend/plugins/pagerduty/e2e/incident_test.go     |   9 +-
 backend/plugins/pagerduty/impl/impl.go             |   4 +-
 backend/plugins/pagerduty/tasks/task_data.go       |  10 +-
 backend/plugins/trello/api/scope_api.go            |   6 -
 backend/plugins/zentao/api/remote_api.go           |   2 +-
 14 files changed, 263 insertions(+), 406 deletions(-)

diff --git a/backend/plugins/circleci/api/connection_api.go b/backend/plugins/circleci/api/connection_api.go
index 0b644d14d..56c8f7ca9 100644
--- a/backend/plugins/circleci/api/connection_api.go
+++ b/backend/plugins/circleci/api/connection_api.go
@@ -73,7 +73,8 @@ func testConnection(ctx context.Context, connection *models.CircleciConnection)
 // @Router /plugins/circleci/test [POST]
 func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	// process input
-	connection, err := dsHelper.ConnApi.GetMergedConnection(input)
+	connection := &models.CircleciConnection{}
+	err := api.Decode(input.Body, connection, vld)
 	if err != nil {
 		return nil, err
 	}
diff --git a/backend/plugins/jenkins/api/scope_api.go b/backend/plugins/jenkins/api/scope_api.go
index a7c4ea428..2746bdd43 100644
--- a/backend/plugins/jenkins/api/scope_api.go
+++ b/backend/plugins/jenkins/api/scope_api.go
@@ -26,25 +26,21 @@ import (
 	"github.com/apache/incubator-devlake/plugins/jenkins/models"
 )
 
-type ScopeRes struct {
-	models.JenkinsJob
-	api.ScopeResDoc[models.JenkinsScopeConfig]
-}
-
-type ScopeReq api.ScopeReq[models.JenkinsJob]
+type PutScopesReqBody api.PutScopesReqBody[models.JenkinsJob]
+type ScopeDetail api.ScopeDetail[models.JenkinsJob, models.JenkinsScopeConfig]
 
-// PutScope create or update jenkins job
+// PutScopes create or update jenkins job
 // @Summary create or update jenkins job
 // @Description Create or update jenkins job
 // @Tags plugins/jenkins
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
 // @Param scope body ScopeReq true "json"
-// @Success 200  {object} []models.JenkinsJob
+// @Success 200  {object} PutScopesReqBody
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jenkins/connections/{connectionId}/scopes [PUT]
-func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+func PutScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return dsHelper.ScopeApi.PutMultiple(input)
 }
 
@@ -74,7 +70,7 @@ func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, erro
 // @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/jenkins/connections/{connectionId}/scopes [GET]
@@ -97,7 +93,7 @@ func GetScopeDispatcher(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutp
 // @Tags plugins/jenkins
 // @Param connectionId path int false "connection ID"
 // @Param scopeId path string false "job's full name"
-// @Success 200  {object} ScopeRes
+// @Success 200  {object} ScopeDetail
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jenkins/connections/{connectionId}/scopes/{scopeId} [GET]
diff --git a/backend/plugins/jenkins/impl/impl.go b/backend/plugins/jenkins/impl/impl.go
index c575623fb..7042a5b4b 100644
--- a/backend/plugins/jenkins/impl/impl.go
+++ b/backend/plugins/jenkins/impl/impl.go
@@ -204,7 +204,7 @@ func (p Jenkins) 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,
diff --git a/backend/plugins/pagerduty/api/blueprint_v200.go b/backend/plugins/pagerduty/api/blueprint_v200.go
index a1c8042fe..34dc32302 100644
--- a/backend/plugins/pagerduty/api/blueprint_v200.go
+++ b/backend/plugins/pagerduty/api/blueprint_v200.go
@@ -20,12 +20,12 @@ package api
 import (
 	"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/devops"
 	"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"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/helpers/srvhelper"
 	"github.com/apache/incubator-devlake/plugins/pagerduty/models"
 	"github.com/apache/incubator-devlake/plugins/pagerduty/tasks"
 )
@@ -35,85 +35,70 @@ func MakeDataSourcePipelinePlanV200(
 	connectionId uint64,
 	bpScopes []*coreModels.BlueprintScope,
 ) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
-	// get the connection info for url
-	connection := &models.PagerDutyConnection{}
-	err := connectionHelper.FirstById(connection, connectionId)
+	connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
 	if err != nil {
 		return nil, nil, err
 	}
-
-	plan := make(coreModels.PipelinePlan, len(bpScopes))
-	plan, err = makeDataSourcePipelinePlanV200(subtaskMetas, plan, bpScopes, connection)
+	scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
 	if err != nil {
 		return nil, nil, err
 	}
-	scopes, err := makeScopesV200(bpScopes, connection)
+	plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, connection)
 	if err != nil {
 		return nil, nil, err
 	}
-
-	return plan, scopes, nil
+	scopes, err := makeScopesV200(scopeDetails, connection)
+	return plan, scopes, err
 }
 
-func makeDataSourcePipelinePlanV200(
+func makePipelinePlanV200(
 	subtaskMetas []plugin.SubTaskMeta,
-	plan coreModels.PipelinePlan,
-	bpScopes []*coreModels.BlueprintScope,
+	scopeDetails []*srvhelper.ScopeDetail[models.Service, models.PagerdutyScopeConfig],
 	connection *models.PagerDutyConnection,
 ) (coreModels.PipelinePlan, errors.Error) {
-	for i, bpScope := range bpScopes {
-		// get board and scope config from db
-		service, scopeConfig, err := scopeHelper.DbHelper().GetScopeAndConfig(connection.ID, bpScope.ScopeId)
-		if err != nil {
-			return nil, err
-		}
-		// construct task options for pagerduty
-		op := &tasks.PagerDutyOptions{
-			ConnectionId: service.ConnectionId,
-			ServiceId:    service.Id,
-			ServiceName:  service.Name,
+	plan := make(coreModels.PipelinePlan, len(scopeDetails))
+	for i, scopeDetail := range scopeDetails {
+		stage := plan[i]
+		if stage == nil {
+			stage = coreModels.PipelineStage{}
 		}
 
-		var options map[string]any
-		options, err = tasks.EncodeTaskOptions(op)
-		if err != nil {
-			return nil, err
-		}
-		var subtasks []string
-		subtasks, err = api.MakePipelinePlanSubtasks(subtaskMetas, scopeConfig.Entities)
+		scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
+		// construct task options for circleci
+		task, err := api.MakePipelinePlanTask(
+			"pagerduty",
+			subtaskMetas,
+			scopeConfig.Entities,
+			tasks.PagerDutyOptions{
+				ConnectionId: connection.ID,
+				ServiceId:    scope.Id,
+			},
+		)
 		if err != nil {
 			return nil, err
 		}
-		stage := []*coreModels.PipelineTask{
-			{
-				Plugin:   "pagerduty",
-				Subtasks: subtasks,
-				Options:  options,
-			},
-		}
+		stage = append(stage, task)
 		plan[i] = stage
 	}
+
 	return plan, nil
 }
 
-func makeScopesV200(bpScopes []*coreModels.BlueprintScope, connection *models.PagerDutyConnection) ([]plugin.Scope, errors.Error) {
-	scopes := make([]plugin.Scope, 0)
-	for _, bpScope := range bpScopes {
-		// get board and scope config from db
-		service, scopeConfig, err := scopeHelper.DbHelper().GetScopeAndConfig(connection.ID, bpScope.ScopeId)
-		if err != nil {
-			return nil, err
-		}
-		// add board to scopes
+func makeScopesV200(
+	scopeDetails []*srvhelper.ScopeDetail[models.Service, models.PagerdutyScopeConfig],
+	connection *models.PagerDutyConnection,
+) ([]plugin.Scope, errors.Error) {
+	scopes := make([]plugin.Scope, 0, len(scopeDetails))
+
+	idgen := didgen.NewDomainIdGenerator(&models.Service{})
+	for _, scopeDetail := range scopeDetails {
+		scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
+		id := idgen.Generate(connection.ID, scope.Id)
+
 		if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_TICKET) {
-			scopeTicket := &ticket.Board{
-				DomainEntity: domainlayer.DomainEntity{
-					Id: didgen.NewDomainIdGenerator(&models.Service{}).Generate(connection.ID, service.Id),
-				},
-				Name: service.Name,
-			}
-			scopes = append(scopes, scopeTicket)
+			scopes = append(scopes, devops.NewCicdScope(id, scope.Name))
 		}
 	}
+
 	return scopes, nil
 }
diff --git a/backend/plugins/pagerduty/api/connection.go b/backend/plugins/pagerduty/api/connection_api.go
similarity index 81%
rename from backend/plugins/pagerduty/api/connection.go
rename to backend/plugins/pagerduty/api/connection_api.go
index e3295719a..dec5d96c8 100644
--- a/backend/plugins/pagerduty/api/connection.go
+++ b/backend/plugins/pagerduty/api/connection_api.go
@@ -82,8 +82,7 @@ func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/test [POST]
 func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.PagerDutyConnection{}
-	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")
 	}
@@ -106,12 +105,7 @@ func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResource
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/pagerduty/connections [POST]
 func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.PagerDutyConnection{}
-	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 pagerduty connection
@@ -123,17 +117,7 @@ func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId} [PATCH]
 func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.PagerDutyConnection{}
-	if err := connectionHelper.First(&connection, input.Params); err != nil {
-		return nil, err
-	}
-	if err := (&models.PagerDutyConnection{}).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 pagerduty connection
@@ -145,13 +129,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	conn := &models.PagerDutyConnection{}
-	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 list pagerduty connections
@@ -162,15 +140,7 @@ func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/pagerduty/connections [GET]
 func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var connections []models.PagerDutyConnection
-	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 pagerduty connection
@@ -181,10 +151,5 @@ func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId} [GET]
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.PagerDutyConnection{}
-	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/pagerduty/api/init.go b/backend/plugins/pagerduty/api/init.go
index dd76a0a4f..93eaf14e0 100644
--- a/backend/plugins/pagerduty/api/init.go
+++ b/backend/plugins/pagerduty/api/init.go
@@ -26,38 +26,17 @@ import (
 )
 
 var vld *validator.Validate
-var connectionHelper *api.ConnectionApiHelper
+var basicRes context.BasicRes
 
-var scopeHelper *api.ScopeApiHelper[models.PagerDutyConnection, models.Service, models.PagerdutyScopeConfig]
 var dsHelper *api.DsHelper[models.PagerDutyConnection, models.Service, models.PagerdutyScopeConfig]
+var raProxy *api.DsRemoteApiProxyHelper[models.PagerDutyConnection]
+var raScopeList *api.DsRemoteApiScopeListHelper[models.PagerDutyConnection, models.Service, PagerdutyRemotePagination]
 
-var basicRes context.BasicRes
+var raScopeSearch *api.DsRemoteApiScopeSearchHelper[models.PagerDutyConnection, models.Service]
 
 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:    "ScopeId",
-		SearchScopeParamName: "name",
-	}
-	scopeHelper = api.NewScopeHelper[models.PagerDutyConnection, models.Service, models.PagerdutyScopeConfig](
-		basicRes,
-		vld,
-		connectionHelper,
-		api.NewScopeDatabaseHelperImpl[models.PagerDutyConnection, models.Service, models.PagerdutyScopeConfig](
-			basicRes, connectionHelper, params),
-		params,
-		&api.ScopeHelperOptions{},
-	)
-
+	basicRes = br
 	dsHelper = api.NewDataSourceHelper[
 		models.PagerDutyConnection, models.Service, models.PagerdutyScopeConfig,
 	](
@@ -70,5 +49,7 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
 		nil,
 		nil,
 	)
-
+	raProxy = api.NewDsRemoteApiProxyHelper[models.PagerDutyConnection](dsHelper.ConnApi.ModelApiHelper)
+	raScopeList = api.NewDsRemoteApiScopeListHelper[models.PagerDutyConnection, models.Service, PagerdutyRemotePagination](raProxy, listPagerdutyRemoteScopes)
+	raScopeSearch = api.NewDsRemoteApiScopeSearchHelper[models.PagerDutyConnection, models.Service](raProxy, searchPagerdutyRemoteScopes)
 }
diff --git a/backend/plugins/pagerduty/api/remote.go b/backend/plugins/pagerduty/api/remote.go
deleted file mode 100644
index 86577a085..000000000
--- a/backend/plugins/pagerduty/api/remote.go
+++ /dev/null
@@ -1,233 +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 (
-	"context"
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
-	"github.com/apache/incubator-devlake/core/models/common"
-	"net/http"
-	"net/url"
-	"strconv"
-
-	"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/pagerduty/models"
-	"github.com/apache/incubator-devlake/plugins/pagerduty/models/raw"
-)
-
-type RemoteScopesChild struct {
-	Type     string      `json:"type"`
-	ParentId *string     `json:"parentId"`
-	Id       string      `json:"id"`
-	Name     string      `json:"name"`
-	Data     interface{} `json:"data"`
-	FullName string      `json:"fullName"` // temporary fix, to adopt remote scope helper
-}
-
-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 {
-	Page    int `json:"page"`
-	PerPage int `json:"per_page"`
-}
-
-type ServiceResponse struct {
-	Offset   int           `json:"offset"`
-	Limit    int           `json:"limit"`
-	More     bool          `json:"more"`
-	Total    int           `json:"total"`
-	Services []raw.Service `json:"services"`
-}
-
-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/pagerduty
-// @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/pagerduty/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")
-	}
-
-	connection := &models.PagerDutyConnection{}
-	err := connectionHelper.First(connection, input.Params)
-	if err != nil {
-		return nil, err
-	}
-
-	pageToken, ok := input.Query["pageToken"]
-	if !ok || len(pageToken) == 0 {
-		pageToken = []string{""}
-	}
-
-	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
-	}
-
-	search := input.Query.Get("search")
-	if search != "" {
-		query.Set("query", search)
-	}
-
-	var res *http.Response
-	outputBody := &RemoteScopesOutput{}
-	res, err = apiClient.Get("/services", query, nil)
-	if err != nil {
-		return nil, err
-	}
-	response := &ServiceResponse{}
-	err = api.UnmarshalResponse(res, response)
-	if err != nil {
-		return nil, err
-	}
-	// append service to output
-	for _, service := range response.Services {
-		child := RemoteScopesChild{
-			Type:     TypeScope,
-			Id:       service.Id,
-			Name:     service.Name,
-			FullName: service.Name,
-			Data: models.Service{
-				Url:  service.HtmlUrl,
-				Id:   service.Id,
-				Name: service.Name,
-				Scope: common.Scope{
-					NoPKModel: common.NoPKModel{
-						CreatedAt: service.CreatedAt,
-					},
-					ConnectionId: connection.ID,
-				},
-			},
-		}
-		outputBody.Children = append(outputBody.Children, child)
-	}
-
-	// check service count
-	if !response.More {
-		pageData = nil
-	}
-
-	// get the next page token
-	outputBody.NextPageToken = ""
-	if pageData != nil {
-		pageData.Page += 1
-		outputBody.NextPageToken, err = EncodeToPageToken(pageData)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil
-}
-
-// 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/pagerduty
-// @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} SearchRemoteScopesOutput
-// @Failure 400  {object} shared.ApiBody "Bad Request"
-// @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/pagerduty/connections/{connectionId}/search-remote-scopes [GET]
-func SearchRemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return RemoteScopes(input)
-}
-
-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/pagerduty/api/remote_api.go b/backend/plugins/pagerduty/api/remote_api.go
new file mode 100644
index 000000000..a3953432d
--- /dev/null
+++ b/backend/plugins/pagerduty/api/remote_api.go
@@ -0,0 +1,173 @@
+/*
+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 (
+	"net/http"
+	"net/url"
+	"strconv"
+
+	"github.com/apache/incubator-devlake/core/models/common"
+
+	"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/pagerduty/models"
+	"github.com/apache/incubator-devlake/plugins/pagerduty/models/raw"
+)
+
+type PagerdutyRemotePagination struct {
+	Offset int `json:"offset"`
+	Limit  int `json:"limit"`
+}
+
+type ServiceResponse struct {
+	Offset   int           `json:"offset"`
+	Limit    int           `json:"limit"`
+	More     bool          `json:"more"`
+	Total    int           `json:"total"`
+	Services []raw.Service `json:"services"`
+}
+
+func queryPagerdutyRemoteScopes(
+	connection *models.PagerDutyConnection,
+	apiClient plugin.ApiClient,
+	groupId string,
+	page PagerdutyRemotePagination,
+	search string,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.Service],
+	nextPage *PagerdutyRemotePagination,
+	err errors.Error,
+) {
+	var res *http.Response
+	res, err = apiClient.Get("/services", url.Values{
+		"offset": {strconv.Itoa(page.Offset)},
+		"limit":  {strconv.Itoa(page.Limit)},
+		"search": {search},
+	}, nil)
+	if err != nil {
+		return
+	}
+	response := &ServiceResponse{}
+	err = api.UnmarshalResponse(res, response)
+	if err != nil {
+		return
+	}
+	// append service to output
+	for _, service := range response.Services {
+		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{
+				Url:  service.HtmlUrl,
+				Id:   service.Id,
+				Name: service.Name,
+				Scope: common.Scope{
+					NoPKModel: common.NoPKModel{
+						CreatedAt: service.CreatedAt,
+					},
+					ConnectionId: connection.ID,
+				},
+			},
+		})
+	}
+
+	// check service count
+	if response.More {
+		nextPage = &PagerdutyRemotePagination{
+			Offset: page.Offset + page.Limit,
+			Limit:  page.Limit,
+		}
+	}
+
+	return
+}
+
+func listPagerdutyRemoteScopes(
+	connection *models.PagerDutyConnection,
+	apiClient plugin.ApiClient,
+	groupId string,
+	page PagerdutyRemotePagination,
+) (
+	[]dsmodels.DsRemoteApiScopeListEntry[models.Service],
+	*PagerdutyRemotePagination,
+	errors.Error,
+) {
+	return queryPagerdutyRemoteScopes(connection, apiClient, groupId, page, "")
+}
+
+func searchPagerdutyRemoteScopes(
+	apiClient plugin.ApiClient,
+	params *dsmodels.DsRemoteApiScopeSearchParams,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.Service],
+	err errors.Error,
+) {
+	children, _, err = queryPagerdutyRemoteScopes(nil, apiClient, "", PagerdutyRemotePagination{
+		Offset: (params.Page - 1) * params.PageSize,
+		Limit:  params.PageSize,
+	}, params.Search)
+	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/pagerduty
+// @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/pagerduty/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
+// @Summary use the Search API and only return project
+// @Description use the Search API and only return project
+// @Tags plugins/pagerduty
+// @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} SearchRemoteScopesOutput
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/pagerduty/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/github
+// @Router /plugins/github/connections/{connectionId}/proxy/{path} [GET]
+func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return raProxy.Proxy(input)
+}
diff --git a/backend/plugins/pagerduty/api/scope.go b/backend/plugins/pagerduty/api/scope_api.go
similarity index 85%
rename from backend/plugins/pagerduty/api/scope.go
rename to backend/plugins/pagerduty/api/scope_api.go
index b9c8dfce9..81d094e4a 100644
--- a/backend/plugins/pagerduty/api/scope.go
+++ b/backend/plugins/pagerduty/api/scope_api.go
@@ -24,29 +24,25 @@ import (
 	"github.com/apache/incubator-devlake/plugins/pagerduty/models"
 )
 
-type ScopeRes struct {
-	models.Service
-	api.ScopeResDoc[any]
-}
-
-type ScopeReq api.ScopeReq[models.Service]
+type PutScopesReqBody api.PutScopesReqBody[models.Service]
+type ScopeDetail api.ScopeDetail[models.Service, models.PagerdutyScopeConfig]
 
-// PutScope create or update pagerduty service
+// PutScopes create or update pagerduty service
 // @Summary create or update pagerduty service
 // @Description Create or update pagerduty service
 // @Tags plugins/pagerduty
 // @Accept application/json
 // @Param connectionId path int true "connection ID"
 // @Param scope body ScopeReq true "json"
-// @Success 200  {object} []ScopeRes
+// @Success 200  {object} []ScopeDetail
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/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 pagerduty service
+// PatchScope patch to pagerduty service
 // @Summary patch to pagerduty service
 // @Description patch to pagerduty service
 // @Tags plugins/pagerduty
@@ -58,8 +54,8 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/scopes/{serviceId} [PATCH]
-func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input)
+func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return dsHelper.ScopeApi.Patch(input)
 }
 
 // GetScopeList get PagerDuty repos
@@ -71,12 +67,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/pagerduty/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 PagerDuty service
@@ -86,12 +82,12 @@ 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/pagerduty/connections/{connectionId}/scopes/{serviceId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.GetScope(input)
+	return dsHelper.ScopeApi.GetScopeDetail(input)
 }
 
 // DeleteScope delete plugin data associated with the scope and optionally the scope itself
@@ -107,5 +103,5 @@ func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/pagerduty/connections/{connectionId}/scopes/{serviceId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Delete(input)
+	return dsHelper.ScopeApi.Delete(input)
 }
diff --git a/backend/plugins/pagerduty/e2e/incident_test.go b/backend/plugins/pagerduty/e2e/incident_test.go
index fe8762437..bba3c601b 100644
--- a/backend/plugins/pagerduty/e2e/incident_test.go
+++ b/backend/plugins/pagerduty/e2e/incident_test.go
@@ -34,11 +34,10 @@ func TestIncidentDataFlow(t *testing.T) {
 	var plugin impl.PagerDuty
 	dataflowTester := e2ehelper.NewDataFlowTester(t, "pagerduty", plugin)
 	options := tasks.PagerDutyOptions{
-		ConnectionId:         1,
-		ServiceId:            "PIKL83L",
-		ServiceName:          "DevService",
-		Tasks:                nil,
-		PagerdutyScopeConfig: nil,
+		ConnectionId: 1,
+		ServiceId:    "PIKL83L",
+		ServiceName:  "DevService",
+		ScopeConfig:  nil,
 	}
 	taskData := &tasks.PagerDutyTaskData{
 		Options: &options,
diff --git a/backend/plugins/pagerduty/impl/impl.go b/backend/plugins/pagerduty/impl/impl.go
index 9fdbf5256..417156b72 100644
--- a/backend/plugins/pagerduty/impl/impl.go
+++ b/backend/plugins/pagerduty/impl/impl.go
@@ -158,11 +158,11 @@ func (p PagerDuty) ApiResources() map[string]map[string]plugin.ApiResourceHandle
 		},
 		"connections/:connectionId/scopes": {
 			"GET": api.GetScopeList,
-			"PUT": api.PutScope,
+			"PUT": api.PutScopes,
 		},
 		"connections/:connectionId/scopes/:scopeId": {
 			"GET":    api.GetScope,
-			"PATCH":  api.UpdateScope,
+			"PATCH":  api.PatchScope,
 			"DELETE": api.DeleteScope,
 		},
 		"connections/:connectionId/scopes/:scopeId/latest-sync-state": {
diff --git a/backend/plugins/pagerduty/tasks/task_data.go b/backend/plugins/pagerduty/tasks/task_data.go
index 5f456f725..41d42e426 100644
--- a/backend/plugins/pagerduty/tasks/task_data.go
+++ b/backend/plugins/pagerduty/tasks/task_data.go
@@ -24,11 +24,11 @@ import (
 )
 
 type PagerDutyOptions struct {
-	ConnectionId uint64   `json:"connectionId"`
-	ServiceId    string   `json:"service_id,omitempty"`
-	ServiceName  string   `json:"service_name,omitempty"`
-	Tasks        []string `json:"tasks,omitempty"`
-	*models.PagerdutyScopeConfig
+	ConnectionId  uint64                       `json:"connectionId" mapstructure:"connectionId,omitempty"`
+	ServiceId     string                       `json:"serviceId,omitempty" mapstructure:"serviceId,omitempty"`
+	ServiceName   string                       `json:"serviceName,omitempty" mapstructure:"serviceName,omitempty"`
+	ScopeConfigId uint64                       `json:"scopeConfigId,omitempty" mapstructure:"scopeConfigId,omitempty"`
+	ScopeConfig   *models.PagerdutyScopeConfig `json:"service_name,omitempty" mapstructure:"serviceName,omitempty"`
 }
 
 type PagerDutyTaskData struct {
diff --git a/backend/plugins/trello/api/scope_api.go b/backend/plugins/trello/api/scope_api.go
index dd5a67d71..89eedc400 100644
--- a/backend/plugins/trello/api/scope_api.go
+++ b/backend/plugins/trello/api/scope_api.go
@@ -24,12 +24,6 @@ import (
 	"github.com/apache/incubator-devlake/plugins/trello/models"
 )
 
-type ScopeRes struct {
-	models.TrelloBoard
-}
-
-type ScopeReq api.ScopeReq[models.TrelloBoard]
-
 type PutScopesReqBody api.PutScopesReqBody[models.TrelloBoard]
 type ScopeDetail api.ScopeDetail[models.TrelloBoard, models.TrelloScopeConfig]
 
diff --git a/backend/plugins/zentao/api/remote_api.go b/backend/plugins/zentao/api/remote_api.go
index 608c0e902..32170c899 100644
--- a/backend/plugins/zentao/api/remote_api.go
+++ b/backend/plugins/zentao/api/remote_api.go
@@ -102,7 +102,7 @@ func listZentaoRemoteScopes(
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/zentao/connections/{connectionId}/remote-scopes [GET]
 func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return raScopeList.GetAll(input)
+	return raScopeList.Get(input)
 }
 
 // @Summary Remote server API proxy