You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2023/12/15 09:45:29 UTC

(incubator-devlake) branch kw-5519-dshelper-sonarqube created (now 8001719ae)

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

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


      at 8001719ae refactor: sonarqube adopts the new dshelper

This branch includes the following new commits:

     new d9254d2b0 docs: fix connection api doc
     new 8001719ae refactor: sonarqube adopts the new dshelper

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(incubator-devlake) 01/02: docs: fix connection api doc

Posted by kl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit d9254d2b0702d0346dbccf3f77412178cd43a442
Author: Klesh Wong <zh...@merico.dev>
AuthorDate: Fri Dec 15 17:44:39 2023 +0800

    docs: fix connection api doc
---
 backend/plugins/github/api/connection_api.go  | 2 +-
 backend/plugins/gitlab/api/connection_api.go  | 2 +-
 backend/plugins/jenkins/api/connection_api.go | 2 +-
 backend/plugins/jira/api/connection_api.go    | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/backend/plugins/github/api/connection_api.go b/backend/plugins/github/api/connection_api.go
index bbee00bcf..77aa985e5 100644
--- a/backend/plugins/github/api/connection_api.go
+++ b/backend/plugins/github/api/connection_api.go
@@ -112,7 +112,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Tags plugins/github
 // @Success 200  {object} models.GithubConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 409  {object} services.BlueprintProjectPairs "References exist to this connection"
+// @Failure 409  {object} srvhelper.DsRefs "References exist to this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/github/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
diff --git a/backend/plugins/gitlab/api/connection_api.go b/backend/plugins/gitlab/api/connection_api.go
index c03cf1727..e70151bf5 100644
--- a/backend/plugins/gitlab/api/connection_api.go
+++ b/backend/plugins/gitlab/api/connection_api.go
@@ -145,7 +145,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Tags plugins/gitlab
 // @Success 200  {object} models.GitlabConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 409  {object} services.BlueprintProjectPairs "References exist to this connection"
+// @Failure 409  {object} srvhelper.DsRefs "References exist to this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/gitlab/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
diff --git a/backend/plugins/jenkins/api/connection_api.go b/backend/plugins/jenkins/api/connection_api.go
index ea6ee046a..d3dc5947f 100644
--- a/backend/plugins/jenkins/api/connection_api.go
+++ b/backend/plugins/jenkins/api/connection_api.go
@@ -147,7 +147,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Tags plugins/jenkins
 // @Success 200  {object} models.JenkinsConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 409  {object} services.BlueprintProjectPairs "References exist to this connection"
+// @Failure 409  {object} srvhelper.DsRefs "References exist to this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/jenkins/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
diff --git a/backend/plugins/jira/api/connection_api.go b/backend/plugins/jira/api/connection_api.go
index 8297ac5d8..b47ad0aee 100644
--- a/backend/plugins/jira/api/connection_api.go
+++ b/backend/plugins/jira/api/connection_api.go
@@ -192,7 +192,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Tags plugins/jira
 // @Success 200  {object} models.JiraConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 409  {object} services.BlueprintProjectPairs "References exist to this connection"
+// @Failure 409  {object} srvhelper.DsRefs "References exist to this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/jira/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {


(incubator-devlake) 02/02: refactor: sonarqube adopts the new dshelper

Posted by kl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 8001719ae7d18f6464d14a8c936a92cec0e16b4f
Author: Klesh Wong <zh...@merico.dev>
AuthorDate: Fri Dec 15 17:45:16 2023 +0800

    refactor: sonarqube adopts the new dshelper
---
 backend/helpers/pluginhelper/api/ds_helper.go      |  14 +-
 .../helpers/srvhelper/connection_service_helper.go |   8 +-
 .../srvhelper/scope_config_service_helper.go       |   7 +
 backend/plugins/sonarqube/api/blueprint_v200.go    |  80 ++++++-----
 .../plugins/sonarqube/api/blueprint_v200_test.go   | 104 --------------
 .../api/{connection.go => connection_api.go}       |  45 ++----
 backend/plugins/sonarqube/api/init.go              |  50 ++++---
 backend/plugins/sonarqube/api/proxy.go             |  61 --------
 backend/plugins/sonarqube/api/remote.go            | 116 ---------------
 backend/plugins/sonarqube/api/remote_api.go        | 160 +++++++++++++++++++++
 .../sonarqube/api/{scope.go => scope_api.go}       |  31 ++--
 backend/plugins/sonarqube/impl/impl.go             |   3 +-
 .../plugins/sonarqube/models/sonarqube_project.go  |   6 +-
 backend/plugins/sonarqube/tasks/task_data.go       |   5 +-
 14 files changed, 286 insertions(+), 404 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/ds_helper.go b/backend/helpers/pluginhelper/api/ds_helper.go
index e4e823a86..0b14356e3 100644
--- a/backend/helpers/pluginhelper/api/ds_helper.go
+++ b/backend/helpers/pluginhelper/api/ds_helper.go
@@ -18,11 +18,15 @@ limitations under the License.
 package api
 
 import (
+	"reflect"
+
 	"github.com/apache/incubator-devlake/core/context"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/srvhelper"
 )
 
+var noScopeConfig = reflect.TypeOf(new(srvhelper.NoScopeConfig))
+
 type DsHelper[
 	C plugin.ToolLayerConnection,
 	S plugin.ToolLayerScope,
@@ -52,8 +56,14 @@ func NewDataSourceHelper[
 	connApi := NewDsConnectionApiHelper[C, S, SC](basicRes, connSrv, connectionSterilizer)
 	scopeSrv := srvhelper.NewScopeSrvHelper[C, S, SC](basicRes, pluginName, scopeSearchColumns)
 	scopeApi := NewDsScopeApiHelper[C, S, SC](basicRes, scopeSrv, scopeSterilizer)
-	scSrv := srvhelper.NewScopeConfigSrvHelper[C, S, SC](basicRes, scopeSearchColumns)
-	scApi := NewDsScopeConfigApiHelper[C, S, SC](basicRes, scSrv, scopeConfigSterilizer)
+
+	var scSrv *srvhelper.ScopeConfigSrvHelper[C, S, SC]
+	var scApi *DsScopeConfigApiHelper[C, S, SC]
+	scType := reflect.TypeOf(new(SC))
+	if scType != noScopeConfig {
+		scSrv = srvhelper.NewScopeConfigSrvHelper[C, S, SC](basicRes, scopeSearchColumns)
+		scApi = NewDsScopeConfigApiHelper[C, S, SC](basicRes, scSrv, scopeConfigSterilizer)
+	}
 	return &DsHelper[C, S, SC]{
 		ConnSrv:        connSrv,
 		ConnApi:        connApi,
diff --git a/backend/helpers/srvhelper/connection_service_helper.go b/backend/helpers/srvhelper/connection_service_helper.go
index 9cc1dea28..7244e2724 100644
--- a/backend/helpers/srvhelper/connection_service_helper.go
+++ b/backend/helpers/srvhelper/connection_service_helper.go
@@ -18,6 +18,8 @@ limitations under the License.
 package srvhelper
 
 import (
+	"reflect"
+
 	"github.com/apache/incubator-devlake/core/context"
 	"github.com/apache/incubator-devlake/core/dal"
 	"github.com/apache/incubator-devlake/core/errors"
@@ -52,14 +54,16 @@ func (connSrv *ConnectionSrvHelper[C, S, SC]) DeleteConnection(connection *C) (r
 		connectionId := (*connection).ConnectionId()
 		refs = toDsRefs(connSrv.getAllBlueprinsByConnection(connectionId))
 		if refs != nil {
-			return errors.Conflict.New("Cannot delete the scope because it is referenced by blueprints")
+			return errors.Conflict.New("Cannot delete the connection because it is referenced by blueprints")
 		}
 		scopeCount := errors.Must1(connSrv.db.Count(dal.From(new(S)), dal.Where("connection_id = ?", connectionId)))
 		if scopeCount > 0 {
 			return errors.Conflict.New("Please delete all data scope(s) before you delete this Data Connection.")
 		}
 		errors.Must(tx.Delete(connection))
-		errors.Must(connSrv.db.Delete(new(SC), dal.Where("connection_id = ?", connectionId)))
+		if reflect.TypeOf(new(SC)) != reflect.TypeOf(new(NoScopeConfig)) {
+			errors.Must(connSrv.db.Delete(new(SC), dal.Where("connection_id = ?", connectionId)))
+		}
 		return nil
 	})
 	return
diff --git a/backend/helpers/srvhelper/scope_config_service_helper.go b/backend/helpers/srvhelper/scope_config_service_helper.go
index 506c64e39..796ba29a5 100644
--- a/backend/helpers/srvhelper/scope_config_service_helper.go
+++ b/backend/helpers/srvhelper/scope_config_service_helper.go
@@ -24,6 +24,13 @@ import (
 	"github.com/apache/incubator-devlake/core/plugin"
 )
 
+// NoScopeConfig is a placeholder for plugins that don't have any scope configuration yet
+type NoScopeConfig struct{}
+
+func (NoScopeConfig) TableName() string               { return "" }
+func (NoScopeConfig) ScopeConfigId() uint64           { return 0 }
+func (NoScopeConfig) ScopeConfigConnectionId() uint64 { return 0 }
+
 // ScopeConfigSrvHelper
 type ScopeConfigSrvHelper[C plugin.ToolLayerConnection, S plugin.ToolLayerScope, SC plugin.ToolLayerScopeConfig] struct {
 	*ModelSrvHelper[SC]
diff --git a/backend/plugins/sonarqube/api/blueprint_v200.go b/backend/plugins/sonarqube/api/blueprint_v200.go
index f1feff666..24a488a31 100644
--- a/backend/plugins/sonarqube/api/blueprint_v200.go
+++ b/backend/plugins/sonarqube/api/blueprint_v200.go
@@ -18,11 +18,11 @@ limitations under the License.
 package api
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"net/url"
 
-	"github.com/apache/incubator-devlake/core/dal"
 	"github.com/apache/incubator-devlake/core/errors"
 	coreModels "github.com/apache/incubator-devlake/core/models"
 	"github.com/apache/incubator-devlake/core/models/domainlayer"
@@ -30,7 +30,9 @@ import (
 	"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
 	"github.com/apache/incubator-devlake/core/plugin"
 	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/helpers/srvhelper"
 	"github.com/apache/incubator-devlake/plugins/sonarqube/models"
+	"github.com/apache/incubator-devlake/plugins/sonarqube/tasks"
 )
 
 func MakeDataSourcePipelinePlanV200(
@@ -38,12 +40,28 @@ 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)
+	// load connection, scope and scopeConfig from the db
+	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
+	}
+
+	// needed for the connection to populate its access tokens
+	// if AppKey authentication method is selected
+	_, err = helper.NewApiClientFromConnection(context.TODO(), basicRes, connection)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails, connection)
+	if err != nil {
+		return nil, nil, err
+	}
+	scopes, err := makeScopesV200(scopeDetails, connection)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -53,57 +71,55 @@ func MakeDataSourcePipelinePlanV200(
 
 func makeDataSourcePipelinePlanV200(
 	subtaskMetas []plugin.SubTaskMeta,
-	plan coreModels.PipelinePlan,
-	bpScopes []*coreModels.BlueprintScope,
-	connectionId uint64,
+	scopeDetails []*srvhelper.ScopeDetail[models.SonarqubeProject, srvhelper.NoScopeConfig],
+	connection *models.SonarqubeConnection,
 ) (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 Sonarqube
-		options := make(map[string]interface{})
-		options["connectionId"] = connectionId
-		options["projectKey"] = bpScope.ScopeId
 
-		subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, plugin.DOMAIN_TYPES)
+		scope := scopeDetail.Scope
+		// construct task options for Jira
+		task, err := helper.MakePipelinePlanTask(
+			"sonarqube",
+			subtaskMetas,
+			nil,
+			tasks.SonarqubeOptions{
+				ConnectionId: scope.ConnectionId,
+				ProjectKey:   scope.ProjectKey,
+			},
+		)
 		if err != nil {
 			return nil, err
 		}
-		stage = append(stage, &coreModels.PipelineTask{
-			Plugin:   "sonarqube",
-			Subtasks: subtasks,
-			Options:  options,
-		})
+
+		stage = append(stage, task)
 		plan[i] = stage
 	}
 
 	return plan, nil
 }
 
-func makeScopesV200(bpScopes []*coreModels.BlueprintScope, connectionId uint64) ([]plugin.Scope, errors.Error) {
+func makeScopesV200(
+	scopeDetails []*srvhelper.ScopeDetail[models.SonarqubeProject, srvhelper.NoScopeConfig],
+	connection *models.SonarqubeConnection,
+) ([]plugin.Scope, errors.Error) {
 	scopes := make([]plugin.Scope, 0)
-	for _, bpScope := range bpScopes {
-		sonarqubeProject := &models.SonarqubeProject{}
-		// get repo from db
-		err := basicRes.GetDal().First(sonarqubeProject,
-			dal.Where("connection_id = ? and project_key = ?",
-				connectionId, bpScope.ScopeId))
-		if err != nil {
-			return nil, errors.Default.Wrap(err, fmt.Sprintf("fail to find sonarqube project %s", bpScope.ScopeId))
-		}
+	for _, scopeDetail := range scopeDetails {
+		sonarqubeProject := scopeDetail.Scope
 		// add board to scopes
-		// if utils.StringsContains(bpScope.Entities, plugin.DOMAIN_TYPE_CODE_QUALITY) {
-		stProject := &codequality.CqProject{
+		domainBoard := &codequality.CqProject{
 			DomainEntity: domainlayer.DomainEntity{
 				Id: didgen.NewDomainIdGenerator(&models.SonarqubeProject{}).Generate(sonarqubeProject.ConnectionId, sonarqubeProject.ProjectKey),
 			},
 			Name: sonarqubeProject.Name,
 		}
-		scopes = append(scopes, stProject)
-		// }
+		scopes = append(scopes, domainBoard)
 	}
+
 	return scopes, nil
 }
 
diff --git a/backend/plugins/sonarqube/api/blueprint_v200_test.go b/backend/plugins/sonarqube/api/blueprint_v200_test.go
deleted file mode 100644
index 365548f77..000000000
--- a/backend/plugins/sonarqube/api/blueprint_v200_test.go
+++ /dev/null
@@ -1,104 +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 (
-	"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/codequality"
-	"github.com/apache/incubator-devlake/core/plugin"
-	mockcontext "github.com/apache/incubator-devlake/mocks/core/context"
-	mockdal "github.com/apache/incubator-devlake/mocks/core/dal"
-	mockplugin "github.com/apache/incubator-devlake/mocks/core/plugin"
-	"github.com/apache/incubator-devlake/plugins/sonarqube/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/sonarqube")
-	mockMeta.On("Name").Return("sonarqube").Maybe()
-	err := plugin.RegisterPlugin("sonarqube", mockMeta)
-	assert.Nil(t, err)
-	bs := &coreModels.BlueprintScope{
-		ScopeId: "f5a50c63-2e8f-4107-9014-853f6f467757",
-	}
-
-	bpScopes := make([]*coreModels.BlueprintScope, 0)
-	bpScopes = append(bpScopes, bs)
-	plan := make(coreModels.PipelinePlan, len(bpScopes))
-	plan, err = makeDataSourcePipelinePlanV200(nil, plan, bpScopes, uint64(1))
-	assert.Nil(t, err)
-	basicRes = NewMockBasicRes()
-
-	scopes, err := makeScopesV200(bpScopes, uint64(1))
-	assert.Nil(t, err)
-
-	expectPlan := coreModels.PipelinePlan{
-		coreModels.PipelineStage{
-			{
-				Plugin:   "sonarqube",
-				Subtasks: []string{},
-				Options: map[string]interface{}{
-					"connectionId": uint64(1),
-					"projectKey":   "f5a50c63-2e8f-4107-9014-853f6f467757",
-				},
-			},
-		},
-	}
-	assert.Equal(t, expectPlan, plan)
-
-	expectScopes := make([]plugin.Scope, 0)
-	sonarqubeProject := &codequality.CqProject{
-		DomainEntity: domainlayer.DomainEntity{
-			Id: "sonarqube:SonarqubeProject:1:f5a50c63-2e8f-4107-9014-853f6f467757",
-		},
-		Name: "aerosolve",
-	}
-
-	expectScopes = append(expectScopes, sonarqubeProject)
-	assert.Equal(t, expectScopes, scopes)
-}
-
-// NewMockBasicRes FIXME ...
-func NewMockBasicRes() *mockcontext.BasicRes {
-	sonarqubeProject := &models.SonarqubeProject{
-		Scope: common.Scope{
-			ConnectionId: 1,
-		},
-		ProjectKey: "f5a50c63-2e8f-4107-9014-853f6f467757",
-		Name:       "aerosolve",
-	}
-
-	mockRes := new(mockcontext.BasicRes)
-	mockDal := new(mockdal.Dal)
-
-	mockDal.On("First", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
-		dst := args.Get(0).(*models.SonarqubeProject)
-		*dst = *sonarqubeProject
-	}).Return(nil).Once()
-
-	mockRes.On("GetDal").Return(mockDal)
-	mockRes.On("GetConfig", mock.Anything).Return("")
-
-	return mockRes
-}
diff --git a/backend/plugins/sonarqube/api/connection.go b/backend/plugins/sonarqube/api/connection_api.go
similarity index 82%
rename from backend/plugins/sonarqube/api/connection.go
rename to backend/plugins/sonarqube/api/connection_api.go
index cb70e715f..6da210221 100644
--- a/backend/plugins/sonarqube/api/connection.go
+++ b/backend/plugins/sonarqube/api/connection_api.go
@@ -103,10 +103,9 @@ func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/sonarqube/{connectionId}/test [POST]
 func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.SonarqubeConnection{}
-	err := connectionHelper.First(connection, input.Params)
+	connection, err := dsHelper.ConnApi.FindByPk(input)
 	if err != nil {
-		return nil, errors.BadInput.Wrap(err, "find connection from db")
+		return nil, err
 	}
 	// test connection
 	return testConnection(context.TODO(), connection.SonarqubeConn)
@@ -122,13 +121,7 @@ func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResource
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/sonarqube/connections [POST]
 func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	// update from request and save to database
-	connection := &models.SonarqubeConnection{}
-	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)
 }
 
 // PatchConnection patch sonarqube connection
@@ -142,12 +135,7 @@ func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId} [PATCH]
 func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.SonarqubeConnection{}
-	err := connectionHelper.Patch(connection, input)
-	if err != nil {
-		return nil, err
-	}
-	return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
+	return dsHelper.ConnApi.Patch(input)
 }
 
 // DeleteConnection delete a sonarqube connection
@@ -157,18 +145,11 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Param connectionId path int false "connection ID"
 // @Success 200  {object} models.SonarqubeConnection
 // @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 409  {object} services.BlueprintProjectPairs "References exist to this connection"
+// @Failure 409  {object} srvhelper.DsRefs "References exist to this connection"
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId} [DELETE]
 func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	conn := &models.SonarqubeConnection{}
-	output, err := connectionHelper.Delete(conn, input)
-	if err != nil {
-		return output, err
-	}
-	output.Body = conn.Sanitize()
-	return output, nil
-
+	return dsHelper.ConnApi.Delete(input)
 }
 
 // ListConnections get all sonarqube connections
@@ -180,15 +161,7 @@ func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/sonarqube/connections [GET]
 func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	var connections []models.SonarqubeConnection
-	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)
 }
 
 // GetConnection get sonarqube connection detail
@@ -201,7 +174,5 @@ func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
 // @Failure 500  {string} errcode.Error "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId} [GET]
 func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.SonarqubeConnection{}
-	err := connectionHelper.First(connection, input.Params)
-	return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
+	return dsHelper.ConnApi.GetDetail(input)
 }
diff --git a/backend/plugins/sonarqube/api/init.go b/backend/plugins/sonarqube/api/init.go
index 3ad87a245..e8042e343 100644
--- a/backend/plugins/sonarqube/api/init.go
+++ b/backend/plugins/sonarqube/api/init.go
@@ -21,43 +21,41 @@ import (
 	"github.com/apache/incubator-devlake/core/context"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/helpers/srvhelper"
 	"github.com/apache/incubator-devlake/plugins/sonarqube/models"
 	"github.com/go-playground/validator/v10"
 )
 
 var vld *validator.Validate
-var connectionHelper *api.ConnectionApiHelper
-var scopeHelper *api.ScopeApiHelper[models.SonarqubeConnection, models.SonarqubeProject, interface{}]
-var remoteHelper *api.RemoteApiHelper[models.SonarqubeConnection, models.SonarqubeProject, models.SonarqubeApiProject, api.NoRemoteGroupResponse]
 var basicRes context.BasicRes
 
-func Init(br context.BasicRes, p plugin.PluginMeta) {
+var dsHelper *api.DsHelper[models.SonarqubeConnection, models.SonarqubeProject, srvhelper.NoScopeConfig]
+var raProxy *api.DsRemoteApiProxyHelper[models.SonarqubeConnection]
+var raScopeList *api.DsRemoteApiScopeListHelper[models.SonarqubeConnection, models.SonarqubeProject, SonarqubeRemotePagination]
+var raScopeSearch *api.DsRemoteApiScopeSearchHelper[models.SonarqubeConnection, models.SonarqubeProject]
 
+func Init(br context.BasicRes, p plugin.PluginMeta) {
 	basicRes = br
 	vld = validator.New()
-	connectionHelper = api.NewConnectionHelper(
-		basicRes,
-		vld,
+	dsHelper = api.NewDataSourceHelper[
+		models.SonarqubeConnection,
+		models.SonarqubeProject,
+		srvhelper.NoScopeConfig,
+	](
+		br,
 		p.Name(),
-	)
-	params := &api.ReflectionParameters{
-		ScopeIdFieldName:     "ProjectKey",
-		ScopeIdColumnName:    "project_key",
-		RawScopeParamName:    "ProjectKey",
-		SearchScopeParamName: "name",
-	}
-	scopeHelper = api.NewScopeHelper[models.SonarqubeConnection, models.SonarqubeProject, any](
-		basicRes,
-		vld,
-		connectionHelper,
-		api.NewScopeDatabaseHelperImpl[models.SonarqubeConnection, models.SonarqubeProject, any](
-			basicRes, connectionHelper, params),
-		params,
+		[]string{"full_name"},
+		func(c models.SonarqubeConnection) models.SonarqubeConnection {
+			return c.Sanitize()
+		},
+		nil,
 		nil,
 	)
-	remoteHelper = api.NewRemoteHelper[models.SonarqubeConnection, models.SonarqubeProject, models.SonarqubeApiProject, api.NoRemoteGroupResponse](
-		basicRes,
-		vld,
-		connectionHelper,
-	)
+	raProxy = api.NewDsRemoteApiProxyHelper[models.SonarqubeConnection](dsHelper.ConnApi.ModelApiHelper)
+	raScopeList = api.NewDsRemoteApiScopeListHelper[
+		models.SonarqubeConnection,
+		models.SonarqubeProject,
+		SonarqubeRemotePagination,
+	](raProxy, listSonarqubeRemoteScopes)
+	raScopeSearch = api.NewDsRemoteApiScopeSearchHelper[models.SonarqubeConnection, models.SonarqubeProject](raProxy, searchSonarqubeRemoteProjects)
 }
diff --git a/backend/plugins/sonarqube/api/proxy.go b/backend/plugins/sonarqube/api/proxy.go
deleted file mode 100644
index da50350a7..000000000
--- a/backend/plugins/sonarqube/api/proxy.go
+++ /dev/null
@@ -1,61 +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"
-	"io"
-
-	"github.com/apache/incubator-devlake/core/errors"
-	"github.com/apache/incubator-devlake/core/plugin"
-	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-	"github.com/apache/incubator-devlake/plugins/sonarqube/models"
-)
-
-// Proxy proxy api request to upstream sonarqube
-// @Summary proxy api request to upstream sonarqube
-// @Description Proxy HTTP GET request to the sonarqube behind this connection.
-// @Tags plugins/sonarqube
-// @Param connectionId path int false "connection ID"
-// @Param path path string false "API Path"
-// @Success 200  {object} interface{} "Success"
-// @Failure 400  {string} errcode.Error "Bad Request"
-// @Failure 500  {string} errcode.Error "Internal Error"
-// @Router /plugins/sonarqube/connections/{connectionId}/proxy/rest/{path} [GET]
-func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	connection := &models.SonarqubeConnection{}
-	err := connectionHelper.First(connection, input.Params)
-	if err != nil {
-		return nil, err
-	}
-	apiClient, err := helper.NewApiClientFromConnection(context.TODO(), basicRes, connection)
-	if err != nil {
-		return nil, err
-	}
-	resp, err := apiClient.Get(input.Params["path"], input.Query, nil)
-	if err != nil {
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	body, err := errors.Convert01(io.ReadAll(resp.Body))
-	if err != nil {
-		return nil, err
-	}
-	return &plugin.ApiResourceOutput{Status: resp.StatusCode, ContentType: resp.Header.Get("Content-Type"), Body: body}, nil
-}
diff --git a/backend/plugins/sonarqube/api/remote.go b/backend/plugins/sonarqube/api/remote.go
deleted file mode 100644
index f836ce093..000000000
--- a/backend/plugins/sonarqube/api/remote.go
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements.  See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License.  You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
-	gocontext "context"
-	"fmt"
-	"github.com/apache/incubator-devlake/core/context"
-	"github.com/apache/incubator-devlake/core/errors"
-	"github.com/apache/incubator-devlake/core/plugin"
-	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-	"github.com/apache/incubator-devlake/plugins/sonarqube/models"
-	"net/url"
-)
-
-// RemoteScopes list all available scope for users
-// @Summary list all available scope for users
-// @Description list all available scope for users
-// @Tags plugins/sonarqube
-// @Accept application/json
-// @Param connectionId path int false "connection ID"
-// @Param pageToken query string false "page Token"
-// @Success 200  {object} api.RemoteScopesOutput
-// @Failure 400  {object} shared.ApiBody "Bad Request"
-// @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/sonarqube/connections/{connectionId}/remote-scopes [GET]
-func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return remoteHelper.GetScopesFromRemote(input,
-		nil,
-		func(basicRes context.BasicRes, gid string, queryData *api.RemoteQueryData, connection models.SonarqubeConnection) ([]models.SonarqubeApiProject, errors.Error) {
-			query := initialQuery(queryData)
-			// create api client
-			apiClient, err := api.NewApiClientFromConnection(gocontext.TODO(), basicRes, &connection)
-			if err != nil {
-				return nil, err
-			}
-
-			res, err := apiClient.Get("projects/search", query, nil)
-			if err != nil {
-				return nil, err
-			}
-
-			var resBody struct {
-				Data []models.SonarqubeApiProject `json:"components"`
-			}
-			err = api.UnmarshalResponse(res, &resBody)
-			if err != nil {
-				return nil, err
-			}
-
-			return resBody.Data, 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/sonarqube
-// @Accept application/json
-// @Param connectionId path int false "connection ID"
-// @Param search query string false "search keyword"
-// @Param page query int false "page number"
-// @Param pageSize query int false "page size per page"
-// @Success 200  {object} api.SearchRemoteScopesOutput
-// @Failure 400  {object} shared.ApiBody "Bad Request"
-// @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/sonarqube/connections/{connectionId}/search-remote-scopes [GET]
-func SearchRemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return remoteHelper.SearchRemoteScopes(input,
-		func(basicRes context.BasicRes, queryData *api.RemoteQueryData, connection models.SonarqubeConnection) ([]models.SonarqubeApiProject, errors.Error) {
-			query := initialQuery(queryData)
-			query.Set("q", queryData.Search[0])
-			// create api client
-			apiClient, err := api.NewApiClientFromConnection(gocontext.TODO(), basicRes, &connection)
-			if err != nil {
-				return nil, err
-			}
-
-			// request search
-			res, err := apiClient.Get("projects/search", query, nil)
-			if err != nil {
-				return nil, err
-			}
-			var resBody struct {
-				Data []models.SonarqubeApiProject `json:"components"`
-			}
-			err = api.UnmarshalResponse(res, &resBody)
-			if err != nil {
-				return nil, err
-			}
-
-			return resBody.Data, nil
-		})
-}
-
-func initialQuery(queryData *api.RemoteQueryData) url.Values {
-	query := url.Values{}
-	query.Set("p", fmt.Sprintf("%v", queryData.Page))
-	query.Set("ps", fmt.Sprintf("%v", queryData.PerPage))
-	return query
-}
diff --git a/backend/plugins/sonarqube/api/remote_api.go b/backend/plugins/sonarqube/api/remote_api.go
new file mode 100644
index 000000000..167925a7f
--- /dev/null
+++ b/backend/plugins/sonarqube/api/remote_api.go
@@ -0,0 +1,160 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	dsmodels "github.com/apache/incubator-devlake/helpers/pluginhelper/api/models"
+	"github.com/apache/incubator-devlake/plugins/sonarqube/models"
+)
+
+type SonarqubeRemotePagination struct {
+	Page     int `json:"p"`
+	PageSize int `json:"ps"`
+}
+
+func querySonarqubeProjects(
+	apiClient plugin.ApiClient,
+	keyword string,
+	page SonarqubeRemotePagination,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.SonarqubeProject],
+	nextPage *SonarqubeRemotePagination,
+	err errors.Error,
+) {
+	if page.PageSize == 0 {
+		page.PageSize = 100
+	}
+	if page.Page == 0 {
+		page.Page = 1
+	}
+	res, err := apiClient.Get("projects/search", url.Values{
+		"p":  {fmt.Sprintf("%v", page.Page)},
+		"ps": {fmt.Sprintf("%v", page.PageSize)},
+		"q":  {keyword},
+	}, nil)
+	if err != nil {
+		return
+	}
+
+	resBody := struct {
+		Paging struct {
+			PageIndex int `json:"pageIndex"`
+			PageSize  int `json:"pageSize"`
+			Total     int `json:"total"`
+		} `json:"paging"`
+		Components []*models.SonarqubeApiProject
+	}{}
+
+	err = api.UnmarshalResponse(res, &resBody)
+	if err != nil {
+		return
+	}
+
+	for _, project := range resBody.Components {
+		children = append(children, dsmodels.DsRemoteApiScopeListEntry[models.SonarqubeProject]{
+			Type:     api.RAS_ENTRY_TYPE_SCOPE,
+			Id:       fmt.Sprintf("%v", project.ProjectKey),
+			ParentId: nil,
+			Name:     project.Name,
+			FullName: project.Name,
+			Data:     project.ConvertApiScope(),
+		})
+	}
+
+	return
+}
+
+func listSonarqubeRemoteScopes(
+	connection *models.SonarqubeConnection,
+	apiClient plugin.ApiClient,
+	groupId string,
+	page SonarqubeRemotePagination,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.SonarqubeProject],
+	nextPage *SonarqubeRemotePagination,
+	err errors.Error,
+) {
+	return querySonarqubeProjects(apiClient, "", page)
+}
+
+// RemoteScopes list all available scopes on the remote server
+// @Summary list all available scopes on the remote server
+// @Description list all available scopes on the remote server
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param groupId query string false "group ID"
+// @Param pageToken query string false "page Token"
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Success 200  {object} dsmodels.DsRemoteApiScopeList[models.SonarqubeProject]
+// @Tags plugins/sonarqube
+// @Router /plugins/sonarqube/connections/{connectionId}/remote-scopes [GET]
+func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return raScopeList.Get(input)
+}
+
+func searchSonarqubeRemoteProjects(
+	apiClient plugin.ApiClient,
+	params *dsmodels.DsRemoteApiScopeSearchParams,
+) (
+	children []dsmodels.DsRemoteApiScopeListEntry[models.SonarqubeProject],
+	err errors.Error,
+) {
+	if params.Page == 0 {
+		params.Page = 1
+	}
+	page := SonarqubeRemotePagination{
+		Page:     params.Page,
+		PageSize: params.PageSize,
+	}
+	children, _, err = querySonarqubeProjects(apiClient, params.Search, page)
+	return
+}
+
+// SearchRemoteScopes searches scopes on the remote server
+// @Summary searches scopes on the remote server
+// @Description searches scopes on the remote server
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param search query string false "search"
+// @Param page query int false "page number"
+// @Param pageSize query int false "page size per page"
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Success 200  {object} dsmodels.DsRemoteApiScopeList[models.SonarqubeProject] "the parentIds are always null"
+// @Tags plugins/sonarqube
+// @Router /plugins/sonarqube/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/sonarqube
+// @Router /plugins/sonarqube/connections/{connectionId}/proxy/{path} [GET]
+func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return raProxy.Proxy(input)
+}
diff --git a/backend/plugins/sonarqube/api/scope.go b/backend/plugins/sonarqube/api/scope_api.go
similarity index 86%
rename from backend/plugins/sonarqube/api/scope.go
rename to backend/plugins/sonarqube/api/scope_api.go
index 7ef286fda..2e7b46fbf 100644
--- a/backend/plugins/sonarqube/api/scope.go
+++ b/backend/plugins/sonarqube/api/scope_api.go
@@ -22,14 +22,12 @@ import (
 	"github.com/apache/incubator-devlake/core/models/common"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/helpers/srvhelper"
 	"github.com/apache/incubator-devlake/plugins/sonarqube/models"
 )
 
-type ScopeRes struct {
-	models.SonarqubeProject
-}
-
-type ScopeReq api.ScopeReq[models.SonarqubeProject]
+type PutScopesReqBody api.PutScopesReqBody[models.SonarqubeProject]
+type ScopeDetail api.ScopeDetail[models.SonarqubeProject, srvhelper.NoScopeConfig]
 
 // PutScope create or update sonarqube project
 // @Summary create or update sonarqube project
@@ -37,14 +35,17 @@ type ScopeReq api.ScopeReq[models.SonarqubeProject]
 // @Tags plugins/sonarqube
 // @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.SonarqubeProject
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes [PUT]
 func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	// decode request body to scope, deal with lastAnalysisDate format
-	data := input.Body["data"].([]interface{})
+	data, ok := input.Body["data"].([]interface{})
+	if !ok {
+		return nil, errors.BadInput.New("invalid `data`")
+	}
 	for _, item := range data {
 		dateStr, ok := item.(map[string]interface{})["lastAnalysisDate"].(string)
 		if !ok {
@@ -59,7 +60,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 
 	}
 
-	return scopeHelper.Put(input)
+	return dsHelper.ScopeApi.PutMultiple(input)
 }
 
 // UpdateScope patch to sonarqube project
@@ -75,7 +76,7 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Update(input)
+	return dsHelper.ScopeApi.Patch(input)
 }
 
 // GetScopeList get Sonarqube projects
@@ -87,12 +88,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/sonarqube/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 Sonarqube project
@@ -101,12 +102,12 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
 // @Tags plugins/sonarqube
 // @Param connectionId path int false "connection ID"
 // @Param scopeId path string false "project key"
-// @Success 200  {object} ScopeRes
+// @Success 200  {object} ScopeDetail
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.GetScope(input)
+	return dsHelper.ScopeApi.GetScopeDetail(input)
 }
 
 // DeleteScope delete plugin data associated with the scope and optionally the scope itself
@@ -118,9 +119,9 @@ func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors
 // @Param delete_data_only query bool false "Only delete the scope data, not the scope itself"
 // @Success 200
 // @Failure 400  {object} shared.ApiBody "Bad Request"
-// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
+// @Failure 409  {object} srvhelper.DsRefs "References exist to this scope"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} [DELETE]
 func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	return scopeHelper.Delete(input)
+	return dsHelper.ScopeApi.Delete(input)
 }
diff --git a/backend/plugins/sonarqube/impl/impl.go b/backend/plugins/sonarqube/impl/impl.go
index f0a976f5b..7400edc90 100644
--- a/backend/plugins/sonarqube/impl/impl.go
+++ b/backend/plugins/sonarqube/impl/impl.go
@@ -135,14 +135,13 @@ func (p Sonarqube) PrepareTaskData(taskCtx plugin.TaskContext, options map[strin
 		TaskStartTime: time.Now(),
 	}
 	// even we have project in _tool_sonaqube_projects, we still need to collect project to update LastAnalysisDate
-	var scope models.SonarqubeProject
 	var apiProject *models.SonarqubeApiProject
 	apiProject, err = api.GetApiProject(op.ProjectKey, apiClient)
 	if err != nil {
 		return nil, err
 	}
 	logger.Debug(fmt.Sprintf("Current project: %s", apiProject.ProjectKey))
-	scope = apiProject.ConvertApiScope().(models.SonarqubeProject)
+	scope := apiProject.ConvertApiScope()
 	scope.ConnectionId = op.ConnectionId
 	err = taskCtx.GetDal().CreateOrUpdate(&scope)
 	if err != nil {
diff --git a/backend/plugins/sonarqube/models/sonarqube_project.go b/backend/plugins/sonarqube/models/sonarqube_project.go
index b40f6c197..3e78680de 100644
--- a/backend/plugins/sonarqube/models/sonarqube_project.go
+++ b/backend/plugins/sonarqube/models/sonarqube_project.go
@@ -23,7 +23,6 @@ import (
 )
 
 var _ plugin.ToolLayerScope = (*SonarqubeProject)(nil)
-var _ plugin.ApiScope = (*SonarqubeApiProject)(nil)
 
 type SonarqubeProject struct {
 	common.Scope     `mapstructure:",squash"`
@@ -68,8 +67,8 @@ type SonarqubeApiProject struct {
 }
 
 // Convert the API response to our DB model instance
-func (sonarqubeApiProject SonarqubeApiProject) ConvertApiScope() plugin.ToolLayerScope {
-	sonarqubeProject := SonarqubeProject{
+func (sonarqubeApiProject *SonarqubeApiProject) ConvertApiScope() *SonarqubeProject {
+	return &SonarqubeProject{
 		ProjectKey:       sonarqubeApiProject.ProjectKey,
 		Name:             sonarqubeApiProject.Name,
 		Qualifier:        sonarqubeApiProject.Qualifier,
@@ -77,7 +76,6 @@ func (sonarqubeApiProject SonarqubeApiProject) ConvertApiScope() plugin.ToolLaye
 		LastAnalysisDate: sonarqubeApiProject.LastAnalysisDate,
 		Revision:         sonarqubeApiProject.Revision,
 	}
-	return sonarqubeProject
 }
 
 type SonarqubeApiParams struct {
diff --git a/backend/plugins/sonarqube/tasks/task_data.go b/backend/plugins/sonarqube/tasks/task_data.go
index cec529707..1d6ab3f9c 100644
--- a/backend/plugins/sonarqube/tasks/task_data.go
+++ b/backend/plugins/sonarqube/tasks/task_data.go
@@ -28,9 +28,8 @@ type SonarqubeOptions struct {
 	// options means some custom params required by plugin running.
 	// Such As How many rows do your want
 	// You can use it in subtasks, and you need to pass it to main.go and pipelines.
-	ConnectionId         uint64   `json:"connectionId"`
-	ProjectKey           string   `json:"projectKey"`
-	Tasks                []string `json:"tasks,omitempty"`
+	ConnectionId         uint64 `json:"connectionId"`
+	ProjectKey           string `json:"projectKey"`
 	api.CollectorOptions `mapstructure:",squash"`
 }