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

warren 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 c6714ef2f fix(plugins): refactor jira jenkins and sonarqube (#4667)
c6714ef2f is described below

commit c6714ef2f72fdf0d56fbc32636c4d5c54f5fc389
Author: Warren Chen <[email protected]>
AuthorDate: Wed Mar 15 13:24:03 2023 +0800

    fix(plugins): refactor jira jenkins and sonarqube (#4667)
---
 backend/helpers/pluginhelper/api/scope_helper.go   |  15 ++-
 backend/plugins/jenkins/api/init.go                |   7 +
 backend/plugins/jenkins/api/scope.go               | 141 +++----------------
 backend/plugins/jenkins/impl/impl.go               |   2 +-
 backend/plugins/jenkins/models/job.go              |   4 +-
 backend/plugins/jira/api/init.go                   |  13 +-
 backend/plugins/jira/api/scope.go                  | 150 +++------------------
 backend/plugins/jira/impl/impl.go                  |   2 +-
 backend/plugins/jira/models/board.go               |   4 +-
 backend/plugins/sonarqube/api/init.go              |  13 +-
 backend/plugins/sonarqube/api/remote.go            |   6 +
 backend/plugins/sonarqube/api/scope.go             | 115 ++--------------
 backend/plugins/sonarqube/impl/impl.go             |   2 +-
 .../plugins/sonarqube/models/sonarqube_project.go  |   4 +-
 14 files changed, 94 insertions(+), 384 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/scope_helper.go 
b/backend/helpers/pluginhelper/api/scope_helper.go
index 57e508726..749372831 100644
--- a/backend/helpers/pluginhelper/api/scope_helper.go
+++ b/backend/helpers/pluginhelper/api/scope_helper.go
@@ -81,7 +81,7 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) Put(input 
*plugin.ApiResourceInput) (*
        var req struct {
                Data []*Scope `json:"data"`
        }
-       err := errors.Convert(mapstructure.Decode(input.Body, &req))
+       err := errors.Convert(DecodeMapStruct(input.Body, &req))
        if err != nil {
                return nil, errors.BadInput.Wrap(err, "decoding Github repo 
error")
        }
@@ -174,8 +174,12 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) 
GetScopeList(input *plugin.ApiResource
        }
 
        var ruleIds []uint64
-       for _, structValue := range scopes {
-               ruleId := 
reflect.ValueOf(structValue).Elem().FieldByName("TransformationRuleId").Uint()
+       for _, scope := range scopes {
+               valueRepoRuleId := 
reflect.ValueOf(scope).Elem().FieldByName("TransformationRuleId")
+               if !valueRepoRuleId.IsValid() {
+                       return &plugin.ApiResourceOutput{Body: scopes, Status: 
http.StatusOK}, nil
+               }
+               ruleId := 
reflect.ValueOf(scope).Elem().FieldByName("TransformationRuleId").Uint()
                if ruleId > 0 {
                        ruleIds = append(ruleIds, ruleId)
                }
@@ -209,6 +213,7 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScope(input 
*plugin.ApiResourceInpu
                return nil, err
        }
        db := c.db
+
        query := dal.Where(fmt.Sprintf("connection_id = ? AND %s = ?", 
fieldName), connectionId, scopeId)
        var scope Scope
        err = db.First(&scope, query)
@@ -218,6 +223,10 @@ func (c *ScopeApiHelper[Conn, Scope, Tr]) GetScope(input 
*plugin.ApiResourceInpu
        if err != nil {
                return nil, err
        }
+       valueRepoRuleId := 
reflect.ValueOf(scope).FieldByName("TransformationRuleId")
+       if !valueRepoRuleId.IsValid() {
+               return &plugin.ApiResourceOutput{Body: scope, Status: 
http.StatusOK}, nil
+       }
        repoRuleId := 
reflect.ValueOf(scope).FieldByName("TransformationRuleId").Uint()
        var rule Tr
        if repoRuleId > 0 {
diff --git a/backend/plugins/jenkins/api/init.go 
b/backend/plugins/jenkins/api/init.go
index d92c2b334..40c3b7104 100644
--- a/backend/plugins/jenkins/api/init.go
+++ b/backend/plugins/jenkins/api/init.go
@@ -20,11 +20,13 @@ package api
 import (
        "github.com/apache/incubator-devlake/core/context"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/jenkins/models"
        "github.com/go-playground/validator/v10"
 )
 
 var vld *validator.Validate
 var connectionHelper *api.ConnectionApiHelper
+var scopeHelper *api.ScopeApiHelper[models.JenkinsConnection, 
models.JenkinsJob, models.JenkinsTransformationRule]
 var basicRes context.BasicRes
 
 func Init(br context.BasicRes) {
@@ -34,4 +36,9 @@ func Init(br context.BasicRes) {
                basicRes,
                vld,
        )
+       scopeHelper = api.NewScopeHelper[models.JenkinsConnection, 
models.JenkinsJob, models.JenkinsTransformationRule](
+               basicRes,
+               vld,
+               connectionHelper,
+       )
 }
diff --git a/backend/plugins/jenkins/api/scope.go 
b/backend/plugins/jenkins/api/scope.go
index ccade6104..0c932a22c 100644
--- a/backend/plugins/jenkins/api/scope.go
+++ b/backend/plugins/jenkins/api/scope.go
@@ -18,26 +18,19 @@ limitations under the License.
 package api
 
 import (
-       "github.com/apache/incubator-devlake/core/dal"
        "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/jenkins/models"
-       "net/http"
-       "strconv"
        "strings"
-
-       "github.com/mitchellh/mapstructure"
 )
 
-type apiJob struct {
+type ScopeRes struct {
        models.JenkinsJob
        TransformationRuleName string `json:"transformationRuleName,omitempty"`
 }
 
-type req struct {
-       Data []*models.JenkinsJob `json:"data"`
-}
+type ScopeReq api.ScopeReq[models.JenkinsJob]
 
 // PutScope create or update jenkins job
 // @Summary create or update jenkins job
@@ -45,36 +38,13 @@ type req struct {
 // @Tags plugins/jenkins
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param scope body req true "json"
+// @Param scope body ScopeReq true "json"
 // @Success 200  {object} []models.JenkinsJob
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jenkins/connections/{connectionId}/scopes [PUT]
 func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, _ := strconv.ParseUint(input.Params["connectionId"], 10, 
64)
-       if connectionId == 0 {
-               return nil, errors.BadInput.New("invalid connectionId")
-       }
-       var jobs req
-       err := errors.Convert(mapstructure.Decode(input.Body, &jobs))
-       if err != nil {
-               return nil, errors.BadInput.Wrap(err, "decoding Jenkins job 
error")
-       }
-       keeper := make(map[string]struct{})
-       for _, job := range jobs.Data {
-               if _, ok := keeper[job.FullName]; ok {
-                       return nil, errors.BadInput.New("duplicated item")
-               } else {
-                       keeper[job.FullName] = struct{}{}
-               }
-               job.ConnectionId = connectionId
-
-       }
-       err = basicRes.GetDal().CreateOrUpdate(jobs.Data)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "error on saving 
JenkinsJob")
-       }
-       return &plugin.ApiResourceOutput{Body: jobs.Data, Status: 
http.StatusOK}, nil
+       return scopeHelper.Put(input)
 }
 
 // UpdateScope patch to jenkins job
@@ -83,33 +53,15 @@ func PutScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Tags plugins/jenkins
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param fullName path string false "job's full name"
+// @Param scopeId path string false "job's full name"
 // @Param scope body models.JenkinsJob true "json"
 // @Success 200  {object} models.JenkinsJob
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jenkins/connections/{connectionId}/scopes/{fullName} 
[PATCH]
+// @Router /plugins/jenkins/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, fullName, err := extractParam(input.Params)
-       if err != nil {
-               return nil, err
-       }
-       var job models.JenkinsJob
-       job.ConnectionId = connectionId
-       job.FullName = fullName
-       err = basicRes.GetDal().First(&job, dal.Where("connection_id = ? AND 
full_name = ?", connectionId, fullName))
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "getting JenkinsJob error")
-       }
-       err = api.DecodeMapStruct(input.Body, &job)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "patch jenkins job error")
-       }
-       err = basicRes.GetDal().Update(&job)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "error on saving 
JenkinsJob")
-       }
-       return &plugin.ApiResourceOutput{Body: job, Status: http.StatusOK}, nil
+       input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
+       return scopeHelper.Update(input, "full_name")
 }
 
 // GetScopeList get Jenkins jobs
@@ -119,43 +71,12 @@ func UpdateScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, err
 // @Param connectionId path int false "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
-// @Success 200  {object} []apiJob
+// @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jenkins/connections/{connectionId}/scopes [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       var jobs []models.JenkinsJob
-       connectionId, _ := strconv.ParseUint(input.Params["connectionId"], 10, 
64)
-       if connectionId == 0 {
-               return nil, errors.BadInput.New("invalid path params")
-       }
-       limit, offset := api.GetLimitOffset(input.Query, "pageSize", "page")
-       err := basicRes.GetDal().All(&jobs, dal.Where("connection_id = ?", 
connectionId), dal.Limit(limit), dal.Offset(offset))
-       if err != nil {
-               return nil, err
-       }
-       var ruleIds []uint64
-       for _, job := range jobs {
-               if job.TransformationRuleId > 0 {
-                       ruleIds = append(ruleIds, job.TransformationRuleId)
-               }
-       }
-       var rules []models.JenkinsTransformationRule
-       if len(ruleIds) > 0 {
-               err = basicRes.GetDal().All(&rules, dal.Where("id IN (?)", 
ruleIds))
-               if err != nil {
-                       return nil, err
-               }
-       }
-       names := make(map[uint64]string)
-       for _, rule := range rules {
-               names[rule.ID] = rule.Name
-       }
-       var apiJobs []apiJob
-       for _, job := range jobs {
-               apiJobs = append(apiJobs, apiJob{job, 
names[job.TransformationRuleId]})
-       }
-       return &plugin.ApiResourceOutput{Body: apiJobs, Status: http.StatusOK}, 
nil
+       return scopeHelper.GetScopeList(input)
 }
 
 // GetScope get one Jenkins job
@@ -163,44 +84,12 @@ func GetScopeList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
 // @Description get one Jenkins job
 // @Tags plugins/jenkins
 // @Param connectionId path int false "connection ID"
-// @Param fullName path string false "job's full name"
-// @Success 200  {object} apiJob
+// @Param scopeId path string false "job's full name"
+// @Success 200  {object} ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jenkins/connections/{connectionId}/scopes/{fullName} [GET]
+// @Router /plugins/jenkins/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       var job models.JenkinsJob
-       connectionId, fullName, err := extractParam(input.Params)
-       if err != nil {
-               return nil, err
-       }
-       db := basicRes.GetDal()
-       err = db.First(&job, dal.Where("connection_id = ? AND full_name = ?", 
connectionId, fullName))
-       if db.IsErrorNotFound(err) {
-               return nil, errors.NotFound.New("record not found")
-       }
-       if err != nil {
-               return nil, err
-       }
-
-       var rule models.JenkinsTransformationRule
-       if job.TransformationRuleId > 0 {
-               err = basicRes.GetDal().First(&rule, dal.Where("id = ?", 
job.TransformationRuleId))
-               if err != nil {
-                       return nil, err
-               }
-       }
-       return &plugin.ApiResourceOutput{Body: apiJob{job, rule.Name}, Status: 
http.StatusOK}, nil
-}
-
-func extractParam(params map[string]string) (uint64, string, errors.Error) {
-       connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
-       if connectionId == 0 {
-               return 0, "", errors.BadInput.New("invalid connectionId")
-       }
-       fullName := strings.TrimLeft(params["fullName"], "/")
-       if fullName == "" {
-               return 0, "", errors.BadInput.New("invalid fullName")
-       }
-       return connectionId, fullName, nil
+       input.Params["scopeId"] = strings.TrimLeft(input.Params["scopeId"], "/")
+       return scopeHelper.GetScope(input, "full_name")
 }
diff --git a/backend/plugins/jenkins/impl/impl.go 
b/backend/plugins/jenkins/impl/impl.go
index eddc252e0..04c2a3742 100644
--- a/backend/plugins/jenkins/impl/impl.go
+++ b/backend/plugins/jenkins/impl/impl.go
@@ -173,7 +173,7 @@ func (p Jenkins) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler
                        "DELETE": api.DeleteConnection,
                        "GET":    api.GetConnection,
                },
-               "connections/:connectionId/scopes/*fullName": {
+               "connections/:connectionId/scopes/*scopeId": {
                        "GET":   api.GetScope,
                        "PATCH": api.UpdateScope,
                },
diff --git a/backend/plugins/jenkins/models/job.go 
b/backend/plugins/jenkins/models/job.go
index eeaaddfa9..3f328cab2 100644
--- a/backend/plugins/jenkins/models/job.go
+++ b/backend/plugins/jenkins/models/job.go
@@ -23,8 +23,8 @@ import (
 
 // JenkinsJob db entity for jenkins job
 type JenkinsJob struct {
-       ConnectionId         uint64 `gorm:"primaryKey" 
mapstructure:"connectionId,omitempty" json:"connectionId"`
-       FullName             string `gorm:"primaryKey;type:varchar(255)" 
mapstructure:"jobFullName" json:"jobFullName"` // "path1/path2/job name"
+       ConnectionId         uint64 `gorm:"primaryKey" 
mapstructure:"connectionId,omitempty" validate:"required" json:"connectionId"`
+       FullName             string `gorm:"primaryKey;type:varchar(255)" 
mapstructure:"jobFullName" validate:"required" json:"jobFullName"` // 
"path1/path2/job name"
        TransformationRuleId uint64 
`mapstructure:"transformationRuleId,omitempty" 
json:"transformationRuleId,omitempty"`
        Name                 string `gorm:"index;type:varchar(255)" 
mapstructure:"name" json:"name"`     // scope name now is same to `jobFullName`
        Path                 string `gorm:"index;type:varchar(511)" 
mapstructure:"-,omitempty" json:"-"` // "job/path1/job/path2"
diff --git a/backend/plugins/jira/api/init.go b/backend/plugins/jira/api/init.go
index cd019a36f..a9050797e 100644
--- a/backend/plugins/jira/api/init.go
+++ b/backend/plugins/jira/api/init.go
@@ -19,19 +19,26 @@ package api
 
 import (
        "github.com/apache/incubator-devlake/core/context"
-       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/jira/models"
        "github.com/go-playground/validator/v10"
 )
 
 var vld *validator.Validate
-var connectionHelper *helper.ConnectionApiHelper
+var connectionHelper *api.ConnectionApiHelper
+var scopeHelper *api.ScopeApiHelper[models.JiraConnection, models.JiraBoard, 
models.JiraTransformationRule]
 var basicRes context.BasicRes
 
 func Init(br context.BasicRes) {
        basicRes = br
        vld = validator.New()
-       connectionHelper = helper.NewConnectionHelper(
+       connectionHelper = api.NewConnectionHelper(
                basicRes,
                vld,
        )
+       scopeHelper = api.NewScopeHelper[models.JiraConnection, 
models.JiraBoard, models.JiraTransformationRule](
+               basicRes,
+               vld,
+               connectionHelper,
+       )
 }
diff --git a/backend/plugins/jira/api/scope.go 
b/backend/plugins/jira/api/scope.go
index 533d0bab3..7a4fabd30 100644
--- a/backend/plugins/jira/api/scope.go
+++ b/backend/plugins/jira/api/scope.go
@@ -20,11 +20,6 @@ package api
 import (
        "encoding/json"
        "fmt"
-       "io"
-       "net/http"
-       "strconv"
-
-       "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -32,17 +27,16 @@ import (
        "github.com/apache/incubator-devlake/plugins/jira/models"
        "github.com/apache/incubator-devlake/plugins/jira/tasks"
        "github.com/apache/incubator-devlake/plugins/jira/tasks/apiv2models"
-       "github.com/mitchellh/mapstructure"
+       "io"
+       "net/http"
 )
 
-type apiBoard struct {
+type ScopeRes struct {
        models.JiraBoard
        TransformationRuleName string `json:"transformationRuleName,omitempty"`
 }
 
-type req struct {
-       Data []*models.JiraBoard `json:"data"`
-}
+type ScopeReq api.ScopeReq[models.JiraBoard]
 
 // PutScope create or update jira board
 // @Summary create or update jira board
@@ -50,39 +44,13 @@ type req struct {
 // @Tags plugins/jira
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param scope body req true "json"
+// @Param scope body ScopeReq true "json"
 // @Success 200  {object} []models.JiraBoard
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jira/connections/{connectionId}/scopes [PUT]
 func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, _ := extractParam(input.Params)
-       if connectionId == 0 {
-               return nil, errors.BadInput.New("invalid connectionId")
-       }
-       var boards req
-       err := errors.Convert(mapstructure.Decode(input.Body, &boards))
-       if err != nil {
-               return nil, errors.BadInput.Wrap(err, "decoding Jira board 
error")
-       }
-       keeper := make(map[uint64]struct{})
-       for _, board := range boards.Data {
-               if _, ok := keeper[board.BoardId]; ok {
-                       return nil, errors.BadInput.New("duplicated item")
-               } else {
-                       keeper[board.BoardId] = struct{}{}
-               }
-               board.ConnectionId = connectionId
-               err = verifyBoard(board)
-               if err != nil {
-                       return nil, err
-               }
-       }
-       err = basicRes.GetDal().CreateOrUpdate(boards.Data)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "error on saving 
JiraBoard")
-       }
-       return &plugin.ApiResourceOutput{Body: boards.Data, Status: 
http.StatusOK}, nil
+       return scopeHelper.Put(input)
 }
 
 // UpdateScope patch to jira board
@@ -91,35 +59,14 @@ func PutScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Tags plugins/jira
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param boardId path int false "board ID"
+// @Param scopeId path int false "board ID"
 // @Param scope body models.JiraBoard true "json"
 // @Success 200  {object} models.JiraBoard
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jira/connections/{connectionId}/scopes/{boardId} [PATCH]
+// @Router /plugins/jira/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, boardId := extractParam(input.Params)
-       if connectionId*boardId == 0 {
-               return nil, errors.BadInput.New("invalid connectionId or 
boardId")
-       }
-       var board models.JiraBoard
-       err := basicRes.GetDal().First(&board, dal.Where("connection_id = ? AND 
board_id = ?", connectionId, boardId))
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "getting JiraBoard error")
-       }
-       err = api.DecodeMapStruct(input.Body, &board)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "patch jira board error")
-       }
-       err = verifyBoard(&board)
-       if err != nil {
-               return nil, err
-       }
-       err = basicRes.GetDal().Update(board)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "error on saving 
JiraBoard")
-       }
-       return &plugin.ApiResourceOutput{Body: board, Status: http.StatusOK}, 
nil
+       return scopeHelper.Update(input, "board_id")
 }
 
 // GetScopeList get Jira boards
@@ -129,43 +76,12 @@ func UpdateScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, err
 // @Param connectionId path int false "connection ID"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
-// @Success 200  {object} []apiBoard
+// @Success 200  {object} []ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/jira/connections/{connectionId}/scopes/ [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       var boards []models.JiraBoard
-       connectionId, _ := extractParam(input.Params)
-       if connectionId == 0 {
-               return nil, errors.BadInput.New("invalid path params")
-       }
-       limit, offset := api.GetLimitOffset(input.Query, "pageSize", "page")
-       err := basicRes.GetDal().All(&boards, dal.Where("connection_id = ?", 
connectionId), dal.Limit(limit), dal.Offset(offset))
-       if err != nil {
-               return nil, err
-       }
-       var ruleIds []uint64
-       for _, board := range boards {
-               if board.TransformationRuleId > 0 {
-                       ruleIds = append(ruleIds, board.TransformationRuleId)
-               }
-       }
-       var rules []models.JiraTransformationRule
-       if len(ruleIds) > 0 {
-               err = basicRes.GetDal().All(&rules, dal.Where("id IN (?)", 
ruleIds))
-               if err != nil {
-                       return nil, err
-               }
-       }
-       names := make(map[uint64]string)
-       for _, rule := range rules {
-               names[rule.ID] = rule.Name
-       }
-       var apiBoards []apiBoard
-       for _, board := range boards {
-               apiBoards = append(apiBoards, apiBoard{board, 
names[board.TransformationRuleId]})
-       }
-       return &plugin.ApiResourceOutput{Body: apiBoards, Status: 
http.StatusOK}, nil
+       return scopeHelper.GetScopeList(input)
 }
 
 // GetScope get one Jira board
@@ -173,49 +89,13 @@ func GetScopeList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
 // @Description get one Jira board
 // @Tags plugins/jira
 // @Param connectionId path int false "connection ID"
-// @Param boardId path int false "board ID"
-// @Success 200  {object} models.JiraBoard
+// @Param scopeId path int false "board ID"
+// @Success 200  {object} ScopeRes
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jira/connections/{connectionId}/scopes/{boardId} [GET]
+// @Router /plugins/jira/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       var board models.JiraBoard
-       connectionId, boardId := extractParam(input.Params)
-       if connectionId*boardId == 0 {
-               return nil, errors.BadInput.New("invalid path params")
-       }
-       db := basicRes.GetDal()
-       err := db.First(&board, dal.Where("connection_id = ? AND board_id = ?", 
connectionId, boardId))
-       if db.IsErrorNotFound(err) {
-               return nil, errors.NotFound.New("record not found")
-       }
-       if err != nil {
-               return nil, err
-       }
-       var rule models.JiraTransformationRule
-       if board.TransformationRuleId > 0 {
-               err = basicRes.GetDal().First(&rule, dal.Where("id = ?", 
board.TransformationRuleId))
-               if err != nil {
-                       return nil, err
-               }
-       }
-       return &plugin.ApiResourceOutput{Body: apiBoard{board, rule.Name}, 
Status: http.StatusOK}, nil
-}
-
-func extractParam(params map[string]string) (uint64, uint64) {
-       connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
-       boardId, _ := strconv.ParseUint(params["boardId"], 10, 64)
-       return connectionId, boardId
-}
-
-func verifyBoard(board *models.JiraBoard) errors.Error {
-       if board.ConnectionId == 0 {
-               return errors.BadInput.New("invalid connectionId")
-       }
-       if board.BoardId == 0 {
-               return errors.BadInput.New("invalid boardId")
-       }
-       return nil
+       return scopeHelper.GetScope(input, "board_id")
 }
 
 func GetApiJira(op *tasks.JiraOptions, apiClient aha.ApiClientAbstract) 
(*apiv2models.Board, errors.Error) {
diff --git a/backend/plugins/jira/impl/impl.go 
b/backend/plugins/jira/impl/impl.go
index fb9a6ea85..3663a672d 100644
--- a/backend/plugins/jira/impl/impl.go
+++ b/backend/plugins/jira/impl/impl.go
@@ -283,7 +283,7 @@ func (p Jira) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                "connections/:connectionId/proxy/rest/*path": {
                        "GET": api.Proxy,
                },
-               "connections/:connectionId/scopes/:boardId": {
+               "connections/:connectionId/scopes/:scopeId": {
                        "GET":   api.GetScope,
                        "PATCH": api.UpdateScope,
                },
diff --git a/backend/plugins/jira/models/board.go 
b/backend/plugins/jira/models/board.go
index 5992df979..82a737a64 100644
--- a/backend/plugins/jira/models/board.go
+++ b/backend/plugins/jira/models/board.go
@@ -23,8 +23,8 @@ import (
 
 type JiraBoard struct {
        common.NoPKModel     `json:"-" mapstructure:"-"`
-       ConnectionId         uint64 `json:"connectionId" 
mapstructure:"connectionId" gorm:"primaryKey"`
-       BoardId              uint64 `json:"boardId" mapstructure:"boardId" 
gorm:"primaryKey"`
+       ConnectionId         uint64 `json:"connectionId" 
mapstructure:"connectionId" validate:"required" gorm:"primaryKey"`
+       BoardId              uint64 `json:"boardId" mapstructure:"boardId" 
validate:"required" gorm:"primaryKey"`
        TransformationRuleId uint64 `json:"transformationRuleId,omitempty" 
mapstructure:"transformationRuleId"`
        ProjectId            uint   `json:"projectId" mapstructure:"projectId"`
        Name                 string `json:"name" mapstructure:"name" 
gorm:"type:varchar(255)"`
diff --git a/backend/plugins/sonarqube/api/init.go 
b/backend/plugins/sonarqube/api/init.go
index cd019a36f..54c9c2bff 100644
--- a/backend/plugins/sonarqube/api/init.go
+++ b/backend/plugins/sonarqube/api/init.go
@@ -19,19 +19,26 @@ package api
 
 import (
        "github.com/apache/incubator-devlake/core/context"
-       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/sonarqube/models"
        "github.com/go-playground/validator/v10"
 )
 
 var vld *validator.Validate
-var connectionHelper *helper.ConnectionApiHelper
+var connectionHelper *api.ConnectionApiHelper
+var scopeHelper *api.ScopeApiHelper[models.SonarqubeConnection, 
models.SonarqubeProject, interface{}]
 var basicRes context.BasicRes
 
 func Init(br context.BasicRes) {
        basicRes = br
        vld = validator.New()
-       connectionHelper = helper.NewConnectionHelper(
+       connectionHelper = api.NewConnectionHelper(
                basicRes,
                vld,
        )
+       scopeHelper = api.NewScopeHelper[models.SonarqubeConnection, 
models.SonarqubeProject, interface{}](
+               basicRes,
+               vld,
+               connectionHelper,
+       )
 }
diff --git a/backend/plugins/sonarqube/api/remote.go 
b/backend/plugins/sonarqube/api/remote.go
index 02e8f05a9..ba5e18c08 100644
--- a/backend/plugins/sonarqube/api/remote.go
+++ b/backend/plugins/sonarqube/api/remote.go
@@ -296,3 +296,9 @@ func GetQueryForSearchProject(search string, page int, 
perPage int) (url.Values,
        query.Set("q", search)
        return query, nil
 }
+
+func extractParam(params map[string]string) (uint64, string) {
+       connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
+       projectKey := params["projectKey"]
+       return connectionId, projectKey
+}
diff --git a/backend/plugins/sonarqube/api/scope.go 
b/backend/plugins/sonarqube/api/scope.go
index 3ec71af35..046a0d77f 100644
--- a/backend/plugins/sonarqube/api/scope.go
+++ b/backend/plugins/sonarqube/api/scope.go
@@ -18,19 +18,13 @@ limitations under the License.
 package api
 
 import (
-       "net/http"
-       "strconv"
-
-       "github.com/apache/incubator-devlake/core/dal"
        "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"
 )
 
-type req struct {
-       Data []*models.SonarqubeProject `json:"data"`
-}
+type ScopeReq api.ScopeReq[models.SonarqubeProject]
 
 // PutScope create or update sonarqube project
 // @Summary create or update sonarqube project
@@ -38,40 +32,13 @@ type req struct {
 // @Tags plugins/sonarqube
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param scope body req true "json"
+// @Param scope body ScopeReq 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) {
-       connectionId, _ := extractParam(input.Params)
-       if connectionId == 0 {
-               return nil, errors.BadInput.New("invalid connectionId")
-       }
-       var projects req
-       // As we need to process *api.Iso8601Time, we need to use 
DecodeMapStruct instead of mapstructure.Decode
-       err := errors.Convert(api.DecodeMapStruct(input.Body, &projects))
-       if err != nil {
-               return nil, errors.BadInput.Wrap(err, "decoding Sonarqube 
project error")
-       }
-       keeper := make(map[string]struct{})
-       for _, project := range projects.Data {
-               if _, ok := keeper[project.ProjectKey]; ok {
-                       return nil, errors.BadInput.New("duplicated item")
-               } else {
-                       keeper[project.ProjectKey] = struct{}{}
-               }
-               project.ConnectionId = connectionId
-               err = verifyProject(project)
-               if err != nil {
-                       return nil, err
-               }
-       }
-       err = basicRes.GetDal().CreateOrUpdate(projects.Data)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "error on saving 
SonarqubeProject")
-       }
-       return &plugin.ApiResourceOutput{Body: projects.Data, Status: 
http.StatusOK}, nil
+       return scopeHelper.Put(input)
 }
 
 // UpdateScope patch to sonarqube project
@@ -80,35 +47,14 @@ func PutScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Tags plugins/sonarqube
 // @Accept application/json
 // @Param connectionId path int false "connection ID"
-// @Param projectKey path string false "project Key"
+// @Param scopeId path string false "project Key"
 // @Param scope body models.SonarqubeProject 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/{projectKey} 
[PATCH]
+// @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} 
[PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, projectKey := extractParam(input.Params)
-       if connectionId*uint64(len(projectKey)) == 0 {
-               return nil, errors.BadInput.New("invalid connectionId or 
projectKey")
-       }
-       var project models.SonarqubeProject
-       err := basicRes.GetDal().First(&project, dal.Where("connection_id = ? 
AND project_key = ?", connectionId, projectKey))
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "getting SonarqubeProject 
error")
-       }
-       err = api.DecodeMapStruct(input.Body, &project)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "patch sonarqube project 
error")
-       }
-       err = verifyProject(&project)
-       if err != nil {
-               return nil, err
-       }
-       err = basicRes.GetDal().Update(project)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "error on saving 
SonarqubeProject")
-       }
-       return &plugin.ApiResourceOutput{Body: project, Status: http.StatusOK}, 
nil
+       return scopeHelper.Update(input, "project_key")
 }
 
 // GetScopeList get Sonarqube projects
@@ -121,18 +67,7 @@ func UpdateScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, err
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/sonarqube/connections/{connectionId}/scopes/ [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       var projects []models.SonarqubeProject
-       connectionId, _ := extractParam(input.Params)
-       if connectionId == 0 {
-               return nil, errors.BadInput.New("invalid path params")
-       }
-       limit, offset := api.GetLimitOffset(input.Query, "pageSize", "page")
-       err := basicRes.GetDal().All(&projects, dal.Where("connection_id = ?", 
connectionId), dal.Limit(limit), dal.Offset(offset))
-       if err != nil {
-               return nil, err
-       }
-
-       return &plugin.ApiResourceOutput{Body: projects, Status: 
http.StatusOK}, nil
+       return scopeHelper.GetScopeList(input)
 }
 
 // GetScope get one Sonarqube project
@@ -140,43 +75,13 @@ func GetScopeList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
 // @Description get one Sonarqube project
 // @Tags plugins/sonarqube
 // @Param connectionId path int false "connection ID"
-// @Param projectKey path string false "project key"
+// @Param scopeId path string false "project key"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
 // @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/{projectKey} 
[GET]
+// @Router /plugins/sonarqube/connections/{connectionId}/scopes/{scopeId} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       var project models.SonarqubeProject
-       connectionId, projectKey := extractParam(input.Params)
-       if connectionId*uint64(len(projectKey)) == 0 {
-               return nil, errors.BadInput.New("invalid path params")
-       }
-       db := basicRes.GetDal()
-       err := db.First(&project, dal.Where("connection_id = ? AND project_key 
= ?", connectionId, projectKey))
-       if db.IsErrorNotFound(err) {
-               return nil, errors.NotFound.New("record not found")
-       }
-       if err != nil {
-               return nil, err
-       }
-
-       return &plugin.ApiResourceOutput{Body: project, Status: http.StatusOK}, 
nil
-}
-
-func extractParam(params map[string]string) (uint64, string) {
-       connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
-       projectKey := params["projectKey"]
-       return connectionId, projectKey
-}
-
-func verifyProject(project *models.SonarqubeProject) errors.Error {
-       if project.ConnectionId == 0 {
-               return errors.BadInput.New("invalid connectionId")
-       }
-       if len(project.ProjectKey) == 0 {
-               return errors.BadInput.New("invalid project key")
-       }
-       return nil
+       return scopeHelper.GetScope(input, "project_key")
 }
diff --git a/backend/plugins/sonarqube/impl/impl.go 
b/backend/plugins/sonarqube/impl/impl.go
index 74e4cf8a6..779cffc68 100644
--- a/backend/plugins/sonarqube/impl/impl.go
+++ b/backend/plugins/sonarqube/impl/impl.go
@@ -172,7 +172,7 @@ func (p Sonarqube) ApiResources() 
map[string]map[string]plugin.ApiResourceHandle
                "connections/:connectionId/search-remote-scopes": {
                        "GET": api.SearchRemoteScopes,
                },
-               "connections/:connectionId/scopes/:projectKey": {
+               "connections/:connectionId/scopes/:scopeId": {
                        "GET":   api.GetScope,
                        "PATCH": api.UpdateScope,
                },
diff --git a/backend/plugins/sonarqube/models/sonarqube_project.go 
b/backend/plugins/sonarqube/models/sonarqube_project.go
index 90616b821..47fc6037d 100644
--- a/backend/plugins/sonarqube/models/sonarqube_project.go
+++ b/backend/plugins/sonarqube/models/sonarqube_project.go
@@ -24,8 +24,8 @@ import (
 
 type SonarqubeProject struct {
        common.NoPKModel `json:"-" mapstructure:"-"`
-       ConnectionId     uint64           `json:"connectionId" 
gorm:"primaryKey"`
-       ProjectKey       string           `json:"projectKey" 
gorm:"type:varchar(64);primaryKey"`
+       ConnectionId     uint64           `json:"connectionId" 
validate:"required" gorm:"primaryKey"`
+       ProjectKey       string           `json:"projectKey" 
validate:"required" gorm:"type:varchar(64);primaryKey"`
        Name             string           `json:"name" gorm:"type:varchar(255)"`
        Qualifier        string           `json:"qualifier" 
gorm:"type:varchar(255)"`
        Visibility       string           `json:"visibility" 
gorm:"type:varchar(64)"`


Reply via email to