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 a4ceca52a refactor: sonarqube adopts the new dshelper (#6644)
a4ceca52a is described below

commit a4ceca52a327bf1c62f672f95546ae05b1e13cf4
Author: Klesh Wong <[email protected]>
AuthorDate: Fri Dec 22 15:39:36 2023 +0800

    refactor: sonarqube adopts the new dshelper (#6644)
    
    * docs: fix connection api doc
    
    * refactor: sonarqube adopts the new dshelper
    
    * fix: scope config service helper should fill the enitites when omitted
    
    * fix: typo
---
 backend/helpers/pluginhelper/api/ds_helper.go      |  14 +-
 .../helpers/srvhelper/connection_service_helper.go |   8 +-
 .../srvhelper/scope_config_service_helper.go       |   7 +
 backend/helpers/srvhelper/scope_service_helper.go  |  24 +++-
 .../helpers/srvhelper/scope_service_helper_test.go |  55 +++++++
 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 +-
 backend/plugins/sonarqube/api/blueprint_v200.go    | 101 +++++++------
 .../plugins/sonarqube/api/blueprint_v200_test.go   | 104 --------------
 .../api/{connection.go => connection_api.go}       |  45 ++----
 backend/plugins/sonarqube/api/init.go              |  44 ++----
 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 +-
 backend/test/e2e/manual/gitlab/gitlab_test.go      |   2 +-
 21 files changed, 370 insertions(+), 424 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/helpers/srvhelper/scope_service_helper.go 
b/backend/helpers/srvhelper/scope_service_helper.go
index 630457e25..4235bafa3 100644
--- a/backend/helpers/srvhelper/scope_service_helper.go
+++ b/backend/helpers/srvhelper/scope_service_helper.go
@@ -19,15 +19,16 @@ package srvhelper
 
 import (
        "fmt"
+       "reflect"
+       "sort"
+       "strings"
+
        "github.com/apache/incubator-devlake/core/context"
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models"
        "github.com/apache/incubator-devlake/core/models/domainlayer/domaininfo"
        "github.com/apache/incubator-devlake/core/plugin"
-       "reflect"
-       "sort"
-       "strings"
 )
 
 type ScopePagination struct {
@@ -132,6 +133,7 @@ func (scopeSrv *ScopeSrvHelper[C, S, SC]) 
MapScopeDetails(connectionId uint64, b
                if scopeDetails[i].ScopeConfig == nil {
                        scopeDetails[i].ScopeConfig = new(SC)
                }
+               setDefaultEntities(scopeDetails[i].ScopeConfig)
        }
        return scopeDetails, nil
 }
@@ -318,3 +320,19 @@ func reflectType(obj any) reflect.Type {
        }
        return typ
 }
+
+func setDefaultEntities(sc interface{}) {
+       v := reflect.ValueOf(sc)
+       if v.Kind() != reflect.Pointer {
+               panic(fmt.Errorf("sc must be a pointer"))
+       }
+       entities := v.Elem().FieldByName("Entities")
+       if !entities.IsValid() ||
+               !(entities.Kind() == reflect.Array || entities.Kind() == 
reflect.Slice) ||
+               entities.Type().Elem().Kind() != reflect.String {
+               return
+       }
+       if entities.IsNil() || entities.Len() == 0 {
+               entities.Set(reflect.ValueOf(plugin.DOMAIN_TYPES))
+       }
+}
diff --git a/backend/helpers/srvhelper/scope_service_helper_test.go 
b/backend/helpers/srvhelper/scope_service_helper_test.go
new file mode 100644
index 000000000..1052b8164
--- /dev/null
+++ b/backend/helpers/srvhelper/scope_service_helper_test.go
@@ -0,0 +1,55 @@
+/*
+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 srvhelper
+
+import (
+       "testing"
+
+       "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/magiconair/properties/assert"
+)
+
+func Test_setDefaultEntities(t *testing.T) {
+       // plugin doesn't embed the common ScopeConfig
+       sc1 := &struct {
+               Entities []string
+       }{
+               Entities: nil,
+       }
+       setDefaultEntities(sc1)
+       assert.Equal(t, sc1.Entities, plugin.DOMAIN_TYPES)
+
+       // plugin embeded the common ScopeConfig
+       sc2 := &struct {
+               common.ScopeConfig
+       }{
+               ScopeConfig: common.ScopeConfig{
+                       Entities: nil,
+               },
+       }
+       setDefaultEntities(sc2)
+       assert.Equal(t, sc2.Entities, plugin.DOMAIN_TYPES)
+
+       // should not override a non empty slice
+       sc3 := &common.ScopeConfig{
+               Entities: []string{plugin.DOMAIN_TYPE_CICD},
+       }
+       setDefaultEntities(sc3)
+       assert.Equal(t, sc3.Entities, []string{plugin.DOMAIN_TYPE_CICD})
+}
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) {
diff --git a/backend/plugins/sonarqube/api/blueprint_v200.go 
b/backend/plugins/sonarqube/api/blueprint_v200.go
index f1feff666..cd396541b 100644
--- a/backend/plugins/sonarqube/api/blueprint_v200.go
+++ b/backend/plugins/sonarqube/api/blueprint_v200.go
@@ -18,19 +18,22 @@ 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"
        
"github.com/apache/incubator-devlake/core/models/domainlayer/codequality"
        "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
        "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/sonarqube/models"
+       "github.com/apache/incubator-devlake/plugins/sonarqube/tasks"
 )
 
 func MakeDataSourcePipelinePlanV200(
@@ -38,12 +41,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 +72,59 @@ func MakeDataSourcePipelinePlanV200(
 
 func makeDataSourcePipelinePlanV200(
        subtaskMetas []plugin.SubTaskMeta,
-       plan coreModels.PipelinePlan,
-       bpScopes []*coreModels.BlueprintScope,
-       connectionId uint64,
+       scopeDetails []*srvhelper.ScopeDetail[models.SonarqubeProject, 
models.SonarqubeScopeConfig],
+       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)
-               if err != nil {
-                       return nil, err
+
+               scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
+               // construct task options
+               if utils.StringsContains(scopeConfig.Entities, 
plugin.DOMAIN_TYPE_CODE_QUALITY) {
+                       task, err := helper.MakePipelinePlanTask(
+                               "sonarqube",
+                               subtaskMetas,
+                               nil,
+                               tasks.SonarqubeOptions{
+                                       ConnectionId: scope.ConnectionId,
+                                       ProjectKey:   scope.ProjectKey,
+                               },
+                       )
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       stage = append(stage, task)
+                       plan[i] = stage
                }
-               stage = append(stage, &coreModels.PipelineTask{
-                       Plugin:   "sonarqube",
-                       Subtasks: subtasks,
-                       Options:  options,
-               })
-               plan[i] = stage
        }
 
        return plan, nil
 }
 
-func makeScopesV200(bpScopes []*coreModels.BlueprintScope, connectionId 
uint64) ([]plugin.Scope, errors.Error) {
+func makeScopesV200(
+       scopeDetails []*srvhelper.ScopeDetail[models.SonarqubeProject, 
models.SonarqubeScopeConfig],
+       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, scopeConfig := scopeDetail.Scope, 
scopeDetail.ScopeConfig
                // add board to scopes
-               // if utils.StringsContains(bpScope.Entities, 
plugin.DOMAIN_TYPE_CODE_QUALITY) {
-               stProject := &codequality.CqProject{
-                       DomainEntity: domainlayer.DomainEntity{
-                               Id: 
didgen.NewDomainIdGenerator(&models.SonarqubeProject{}).Generate(sonarqubeProject.ConnectionId,
 sonarqubeProject.ProjectKey),
-                       },
-                       Name: sonarqubeProject.Name,
+               if utils.StringsContains(scopeConfig.Entities, 
plugin.DOMAIN_TYPE_CODE_QUALITY) {
+                       domainBoard := &codequality.CqProject{
+                               DomainEntity: domainlayer.DomainEntity{
+                                       Id: 
didgen.NewDomainIdGenerator(&models.SonarqubeProject{}).Generate(sonarqubeProject.ConnectionId,
 sonarqubeProject.ProjectKey),
+                               },
+                               Name: sonarqubeProject.Name,
+                       }
+                       scopes = append(scopes, domainBoard)
                }
-               scopes = append(scopes, stProject)
-               // }
        }
+
        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 32d1e65b2..25ec647f6 100644
--- a/backend/plugins/sonarqube/api/init.go
+++ b/backend/plugins/sonarqube/api/init.go
@@ -26,43 +26,20 @@ import (
 )
 
 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
+
 var dsHelper *api.DsHelper[models.SonarqubeConnection, 
models.SonarqubeProject, models.SonarqubeScopeConfig]
+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,
-               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,
-               nil,
-       )
-       remoteHelper = api.NewRemoteHelper[models.SonarqubeConnection, 
models.SonarqubeProject, models.SonarqubeApiProject, api.NoRemoteGroupResponse](
-               basicRes,
-               vld,
-               connectionHelper,
-       )
        dsHelper = api.NewDataSourceHelper[
-               models.SonarqubeConnection, models.SonarqubeProject, 
models.SonarqubeScopeConfig,
+               models.SonarqubeConnection,
+               models.SonarqubeProject,
+               models.SonarqubeScopeConfig,
        ](
                br,
                p.Name(),
@@ -73,4 +50,11 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
                nil,
                nil,
        )
+       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 f3fa16bd7..9fbef9213 100644
--- a/backend/plugins/sonarqube/impl/impl.go
+++ b/backend/plugins/sonarqube/impl/impl.go
@@ -136,14 +136,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 fd2d49d32..d99c437cc 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"`
 }
 
diff --git a/backend/test/e2e/manual/gitlab/gitlab_test.go 
b/backend/test/e2e/manual/gitlab/gitlab_test.go
index af6247aa5..2b8b5abb6 100644
--- a/backend/test/e2e/manual/gitlab/gitlab_test.go
+++ b/backend/test/e2e/manual/gitlab/gitlab_test.go
@@ -76,8 +76,8 @@ func TestGitlabPlugin(t *testing.T) {
                                                plugin.DOMAIN_TYPE_CODE,
                                                plugin.DOMAIN_TYPE_CODE_REVIEW,
                                        },
+                                       Name: "config-1",
                                },
-                               Name:                 "config-1",
                                PrType:               "",
                                PrComponent:          "",
                                PrBodyClosePattern:   "",

Reply via email to