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 6a6e86685 refactor(framework): add scopeHelper (#4632)
6a6e86685 is described below

commit 6a6e86685c5732dafa9e05e91209e32009092209
Author: Warren Chen <[email protected]>
AuthorDate: Tue Mar 14 14:56:01 2023 +0800

    refactor(framework): add scopeHelper (#4632)
    
    * refactor(framework): add scopeHelper
    
    * feat(framework): finish scope helper
    
    * test(framework): add unit test for put
---
 backend/helpers/pluginhelper/api/scope_helper.go   | 346 +++++++++++++++++++++
 .../helpers/pluginhelper/api/scope_helper_test.go  | 269 ++++++++++++++++
 backend/plugins/github/api/init.go                 |   6 +
 backend/plugins/github/api/scope.go                | 123 +-------
 backend/plugins/github/impl/impl.go                |   2 +-
 backend/plugins/gitlab/api/init.go                 |  12 +-
 backend/plugins/gitlab/api/scope.go                |  39 +--
 7 files changed, 651 insertions(+), 146 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/scope_helper.go 
b/backend/helpers/pluginhelper/api/scope_helper.go
new file mode 100644
index 000000000..0901f69d8
--- /dev/null
+++ b/backend/helpers/pluginhelper/api/scope_helper.go
@@ -0,0 +1,346 @@
+/*
+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"
+       "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/log"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/go-playground/validator/v10"
+       "github.com/mitchellh/mapstructure"
+       "gorm.io/gorm"
+       "strconv"
+       "strings"
+       "time"
+
+       "reflect"
+)
+
+// ScopeApiHelper is used to write the CURD of connection
+type ScopeApiHelper struct {
+       log        log.Logger
+       db         dal.Dal
+       validator  *validator.Validate
+       connHelper *ConnectionApiHelper
+}
+
+// NewScopeHelper creates a ScopeHelper for connection management
+func NewScopeHelper(
+       basicRes context.BasicRes,
+       vld *validator.Validate,
+       connHelper *ConnectionApiHelper,
+) *ScopeApiHelper {
+       if vld == nil {
+               vld = validator.New()
+       }
+       if connHelper == nil {
+               return nil
+       }
+       return &ScopeApiHelper{
+               log:        basicRes.GetLogger(),
+               db:         basicRes.GetDal(),
+               validator:  vld,
+               connHelper: connHelper,
+       }
+}
+
+// Put saves the given scopes to the database. It expects a slice of struct 
pointers
+// as the scopes argument. It also expects a fieldName argument, which is used 
to extract
+// the connection ID from the input.Params map.
+func (c *ScopeApiHelper) Put(input *plugin.ApiResourceInput, apiScope 
interface{}, connection interface{}) errors.Error {
+       err := errors.Convert(mapstructure.Decode(input.Body, apiScope))
+       if err != nil {
+               return errors.BadInput.Wrap(err, "decoding Github repo error")
+       }
+       // Ensure that the scopes argument is a slice
+       v := reflect.ValueOf(apiScope)
+       scopesValue := v.Elem().FieldByName("Data")
+       if scopesValue.Kind() != reflect.Slice {
+               panic("expected a slice")
+       }
+       // Extract the connection ID from the input.Params map
+       connectionId, _ := ExtractParam(input.Params)
+       if connectionId == 0 {
+               return errors.BadInput.New("invalid connectionId or scopeId")
+       }
+       err = c.VerifyConnection(connection, connectionId)
+       if err != nil {
+               return err
+       }
+       // Create a map to keep track of primary key values
+       keeper := make(map[string]struct{})
+
+       // Set the CreatedDate and UpdatedDate fields to the current time for 
each scope
+       now := time.Now()
+       for i := 0; i < scopesValue.Len(); i++ {
+               // Get the reflect.Value of the i-th struct pointer in the slice
+               structValue := scopesValue.Index(i)
+
+               // Ensure that the structValue is a pointer to a struct
+               if structValue.Kind() != reflect.Ptr || 
structValue.Elem().Kind() != reflect.Struct {
+                       panic("expected a pointer to a struct")
+               }
+
+               // Ensure that the primary key value is unique
+               primaryValueStr := 
ReturnPrimaryKeyValue(structValue.Elem().Interface())
+               if _, ok := keeper[primaryValueStr]; ok {
+                       return errors.BadInput.New("duplicated item")
+               } else {
+                       keeper[primaryValueStr] = struct{}{}
+               }
+
+               // Set the connection ID, CreatedDate, and UpdatedDate fields
+               SetScopeFields(structValue.Interface(), connectionId, &now, 
&now)
+
+               // Verify that the primary key value is valid
+               err = VerifyPrimaryKeyValue(structValue.Elem().Interface())
+               if err != nil {
+                       return err
+               }
+       }
+
+       // Save the scopes to the database
+       return c.save(scopesValue.Interface(), c.db.Create)
+}
+
+func (c *ScopeApiHelper) Update(input *plugin.ApiResourceInput, fieldName 
string, connection interface{}, scope interface{}) errors.Error {
+       connectionId, scopeId := ExtractParam(input.Params)
+
+       if connectionId == 0 || len(scopeId) == 0 || scopeId == "0" {
+               return errors.BadInput.New("invalid connectionId")
+       }
+       err := c.VerifyConnection(connection, connectionId)
+       if err != nil {
+               return err
+       }
+
+       err = c.db.First(scope, dal.Where(fmt.Sprintf("connection_id = ? AND %s 
= ?", fieldName), connectionId, scopeId))
+       if err != nil {
+               return errors.Default.New("getting Scope error")
+       }
+       err = DecodeMapStruct(input.Body, scope)
+       if err != nil {
+               return errors.Default.Wrap(err, "patch scope error")
+       }
+       err = VerifyPrimaryKeyValue(scope)
+       if err != nil {
+               return err
+       }
+       err = c.db.Update(scope)
+       if err != nil {
+               return errors.Default.Wrap(err, "error on saving Scope")
+       }
+       return nil
+}
+
+func (c *ScopeApiHelper) GetScopeList(input *plugin.ApiResourceInput, 
connection interface{}, scopes interface{}, rules interface{}) 
(map[uint64]string, errors.Error) {
+       connectionId, _ := ExtractParam(input.Params)
+       if connectionId == 0 {
+               return nil, errors.BadInput.New("invalid path params")
+       }
+       err := c.VerifyConnection(connection, connectionId)
+       if err != nil {
+               return nil, err
+       }
+       limit, offset := GetLimitOffset(input.Query, "pageSize", "page")
+       err = c.db.All(scopes, dal.Where("connection_id = ?", connectionId), 
dal.Limit(limit), dal.Offset(offset))
+       if err != nil {
+               return nil, err
+       }
+
+       scopesValue := 
reflect.ValueOf(reflect.ValueOf(scopes).Elem().Interface())
+       if scopesValue.Kind() != reflect.Slice {
+               panic("expected a slice")
+       }
+       var ruleIds []uint64
+       for i := 0; i < scopesValue.Len(); i++ {
+               // Get the reflect.Value of the i-th struct pointer in the slice
+               structValue := scopesValue.Index(i)
+
+               // Ensure that the structValue is a pointer to a struct
+               if structValue.Kind() != reflect.Ptr || 
structValue.Elem().Kind() != reflect.Struct {
+                       panic("expected a pointer to a struct")
+               }
+               ruleId := 
structValue.Elem().FieldByName("TransformationRuleId").Uint()
+               if ruleId > 0 {
+                       ruleIds = append(ruleIds, ruleId)
+               }
+       }
+
+       if len(ruleIds) > 0 {
+               err = c.db.All(rules, dal.Where("id IN (?)", ruleIds))
+               if err != nil {
+                       return nil, err
+               }
+       }
+       rulesValue := reflect.ValueOf(reflect.ValueOf(rules).Elem().Interface())
+       if scopesValue.Kind() != reflect.Slice {
+               panic("expected a slice")
+       }
+       names := make(map[uint64]string)
+       for i := 0; i < rulesValue.Len(); i++ {
+               // Get the reflect.Value of the i-th struct pointer in the slice
+               structValue := rulesValue.Index(i)
+               names[structValue.FieldByName("ID").Uint()] = 
structValue.FieldByName("Name").String()
+       }
+       return names, nil
+}
+
+func (c *ScopeApiHelper) GetScope(input *plugin.ApiResourceInput, fieldName 
string, connection interface{}, scope interface{}, rule interface{}) 
errors.Error {
+       connectionId, scopeId := ExtractParam(input.Params)
+       if connectionId == 0 || len(scopeId) == 0 || scopeId == "0" {
+               return errors.BadInput.New("invalid path params")
+       }
+       err := c.VerifyConnection(connection, connectionId)
+       if err != nil {
+               return err
+       }
+       db := c.db
+       query := dal.Where(fmt.Sprintf("connection_id = ? AND %s = ?", 
fieldName), connectionId, scopeId)
+       err = db.First(scope, query)
+       if db.IsErrorNotFound(err) {
+               return errors.NotFound.New("Scope not found")
+       }
+       if err != nil {
+               return err
+       }
+       repoRuleId := 
reflect.ValueOf(scope).Elem().FieldByName("TransformationRuleId").Uint()
+       if repoRuleId > 0 {
+               err = db.First(rule, dal.Where("id = ?", repoRuleId))
+               if err != nil {
+                       return errors.NotFound.New("transformationRule not 
found")
+               }
+       }
+       return nil
+}
+
+func (c *ScopeApiHelper) VerifyConnection(connection interface{}, connId 
uint64) errors.Error {
+       err := c.connHelper.FirstById(&connection, connId)
+       if err != nil {
+               if errors.Is(err, gorm.ErrRecordNotFound) {
+                       return errors.BadInput.New("Invalid Connection Id")
+               }
+               return err
+       }
+       return nil
+}
+
+func (c *ScopeApiHelper) save(scope interface{}, method func(entity 
interface{}, clauses ...dal.Clause) errors.Error) errors.Error {
+       err := c.db.CreateOrUpdate(scope)
+       if err != nil {
+               if c.db.IsDuplicationError(err) {
+                       return errors.BadInput.New("the scope already exists")
+               }
+               return err
+       }
+       return nil
+}
+
+func ExtractParam(params map[string]string) (uint64, string) {
+       connectionId, err := strconv.ParseUint(params["connectionId"], 10, 64)
+       if err != nil {
+               return 0, ""
+       }
+       scopeId := params["scopeId"]
+       return connectionId, scopeId
+}
+
+// VerifyPrimaryKeyValue function verifies that the primary key value of a 
given struct instance is not zero or empty.
+func VerifyPrimaryKeyValue(i interface{}) errors.Error {
+       var value reflect.Value
+       pType := reflect.TypeOf(i)
+       if pType.Kind() == reflect.Ptr {
+               value = reflect.ValueOf(reflect.ValueOf(i).Elem().Interface())
+       } else {
+               value = reflect.ValueOf(i)
+       }
+       // Loop through the fields of the input struct using reflection
+       for j := 0; j < value.NumField(); j++ {
+               field := value.Field(j)
+               tag := value.Type().Field(j).Tag.Get("gorm")
+
+               // Check if the field is tagged as a primary key using the GORM 
tag "primaryKey"
+               if strings.Contains(tag, "primaryKey") {
+                       // If the field value is zero or nil, return an error 
indicating that the primary key value is invalid
+                       if field.Interface() == 
reflect.Zero(field.Type()).Interface() || field.Interface() == nil {
+                               return errors.Default.New("primary key value is 
zero or empty")
+                       }
+               }
+       }
+       // If all primary key values are valid, return nil (no error)
+       return nil
+}
+
+func SetScopeFields(p interface{}, connectionId uint64, createdDate 
*time.Time, updatedDate *time.Time) {
+       pType := reflect.TypeOf(p)
+       if pType.Kind() != reflect.Ptr {
+               panic("expected a pointer to a struct")
+       }
+       pValue := reflect.ValueOf(p).Elem()
+
+       // set connectionId
+       connIdField := pValue.FieldByName("ConnectionId")
+       connIdField.SetUint(connectionId)
+
+       // set CreatedDate
+       createdDateField := pValue.FieldByName("CreatedDate")
+       createdDateField.Set(reflect.ValueOf(createdDate))
+
+       // set UpdatedDate
+       updatedDateField := pValue.FieldByName("UpdatedDate")
+       if updatedDate == nil {
+               // if updatedDate is nil, set UpdatedDate to be nil
+               updatedDateField.Set(reflect.Zero(updatedDateField.Type()))
+       } else {
+               // if updatedDate is not nil, set UpdatedDate to be the value
+               updatedDateFieldValue := reflect.ValueOf(updatedDate)
+               updatedDateField.Set(updatedDateFieldValue)
+       }
+}
+
+// ReturnPrimaryKeyValue returns a string containing the primary key value(s) 
of a struct, concatenated with "-" between them.
+// This function receives an interface{} type argument p, which can be a 
pointer to any struct.
+// The function uses reflection to iterate through the fields of the struct, 
and checks if each field is tagged as "primaryKey".
+func ReturnPrimaryKeyValue(p interface{}) string {
+       result := ""
+       // get the type and value of the input interface using reflection
+       t := reflect.TypeOf(p)
+       v := reflect.ValueOf(p)
+       // iterate over each field in the struct type
+       for i := 0; i < t.NumField(); i++ {
+               // get the i-th field
+               field := t.Field(i)
+
+               // check if the field is marked as "primaryKey" in the struct 
tag
+               if strings.Contains(string(field.Tag), "primaryKey") {
+                       // if this is the first primaryKey field encountered, 
set the result to be its value
+                       if result == "" {
+                               result = fmt.Sprintf("%v", 
v.Field(i).Interface())
+                       } else {
+                               // if this is not the first primaryKey field, 
append its value to the result with a "-" separator
+                               result = fmt.Sprintf("%s-%v", result, 
v.Field(i).Interface())
+                       }
+               }
+       }
+
+       // return the final primary key value as a string
+       return result
+}
diff --git a/backend/helpers/pluginhelper/api/scope_helper_test.go 
b/backend/helpers/pluginhelper/api/scope_helper_test.go
new file mode 100644
index 000000000..ad29684d9
--- /dev/null
+++ b/backend/helpers/pluginhelper/api/scope_helper_test.go
@@ -0,0 +1,269 @@
+/*
+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 (
+       "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/unithelper"
+       mockcontext "github.com/apache/incubator-devlake/mocks/core/context"
+       mockdal "github.com/apache/incubator-devlake/mocks/core/dal"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+       "gorm.io/gorm"
+       "reflect"
+       "testing"
+       "time"
+)
+
+type TestModel struct {
+       ID   uint   `gorm:"primaryKey"`
+       Name string `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+}
+
+type GithubRepo struct {
+       ConnectionId         uint64     `json:"connectionId" gorm:"primaryKey" 
mapstructure:"connectionId,omitempty"`
+       GithubId             int        `json:"githubId" gorm:"primaryKey" 
mapstructure:"githubId"`
+       Name                 string     `json:"name" gorm:"type:varchar(255)" 
mapstructure:"name,omitempty"`
+       HTMLUrl              string     `json:"HTMLUrl" 
gorm:"type:varchar(255)" mapstructure:"HTMLUrl,omitempty"`
+       Description          string     `json:"description" 
mapstructure:"description,omitempty"`
+       TransformationRuleId uint64     `json:"transformationRuleId,omitempty" 
mapstructure:"transformationRuleId,omitempty"`
+       OwnerId              int        `json:"ownerId" 
mapstructure:"ownerId,omitempty"`
+       Language             string     `json:"language" 
gorm:"type:varchar(255)" mapstructure:"language,omitempty"`
+       ParentGithubId       int        `json:"parentId" 
mapstructure:"parentGithubId,omitempty"`
+       ParentHTMLUrl        string     `json:"parentHtmlUrl" 
mapstructure:"parentHtmlUrl,omitempty"`
+       CloneUrl             string     `json:"cloneUrl" 
gorm:"type:varchar(255)" mapstructure:"cloneUrl,omitempty"`
+       CreatedDate          *time.Time `json:"createdDate" mapstructure:"-"`
+       UpdatedDate          *time.Time `json:"updatedDate" mapstructure:"-"`
+       common.NoPKModel     `json:"-" mapstructure:"-"`
+}
+
+func (GithubRepo) TableName() string {
+       return "_tool_github_repos"
+}
+
+type GithubConnection struct {
+       common.Model
+       Name             string `gorm:"type:varchar(100);uniqueIndex" 
json:"name" validate:"required"`
+       Endpoint         string `mapstructure:"endpoint" env:"GITHUB_ENDPOINT" 
validate:"required"`
+       Proxy            string `mapstructure:"proxy" env:"GITHUB_PROXY"`
+       RateLimitPerHour int    `comment:"api request rate limit per hour"`
+       Token            string `mapstructure:"token" env:"GITHUB_AUTH" 
validate:"required" encrypt:"yes"`
+}
+
+func (GithubConnection) TableName() string {
+       return "_tool_github_connections"
+}
+
+type req struct {
+       Data []*GithubRepo `json:"data"`
+}
+
+func TestCheckPrimaryKeyValue(t *testing.T) {
+       testCases := []struct {
+               name    string
+               model   TestModel
+               wantErr bool
+       }{
+               {
+                       name: "valid case",
+                       model: TestModel{
+                               ID:   1,
+                               Name: "test name",
+                       },
+                       wantErr: false,
+               },
+               {
+                       name: "zero value",
+                       model: TestModel{
+                               ID:   0,
+                               Name: "test name",
+                       },
+                       wantErr: true,
+               },
+               {
+                       name: "nil value",
+                       model: TestModel{
+                               ID:   1,
+                               Name: "",
+                       },
+                       wantErr: true,
+               },
+       }
+
+       for _, tc := range testCases {
+               err := VerifyPrimaryKeyValue(tc.model)
+               if (err != nil) != tc.wantErr {
+                       t.Errorf("unexpected error value - got: %v, want: %v", 
err, tc.wantErr)
+               }
+
+       }
+}
+
+func TestSetGitlabProjectFields(t *testing.T) {
+       // create a struct
+       var p struct {
+               ConnectionId uint64 `json:"connectionId" 
mapstructure:"connectionId" gorm:"primaryKey"`
+               GitlabId     int    `json:"gitlabId" mapstructure:"gitlabId" 
gorm:"primaryKey"`
+
+               CreatedDate      *time.Time `json:"createdDate" 
mapstructure:"-"`
+               UpdatedDate      *time.Time `json:"updatedDate" 
mapstructure:"-"`
+               common.NoPKModel `json:"-" mapstructure:"-"`
+       }
+
+       // call SetScopeFields to assign value
+       connectionId := uint64(123)
+       createdDate := time.Now()
+       updatedDate := &createdDate
+       SetScopeFields(&p, connectionId, &createdDate, updatedDate)
+
+       // verify fields
+       if p.ConnectionId != connectionId {
+               t.Errorf("ConnectionId not set correctly, expected: %v, got: 
%v", connectionId, p.ConnectionId)
+       }
+
+       if !p.CreatedDate.Equal(createdDate) {
+               t.Errorf("CreatedDate not set correctly, expected: %v, got: 
%v", createdDate, p.CreatedDate)
+       }
+
+       if p.UpdatedDate == nil {
+               t.Errorf("UpdatedDate not set correctly, expected: %v, got: 
%v", updatedDate, p.UpdatedDate)
+       } else if !p.UpdatedDate.Equal(*updatedDate) {
+               t.Errorf("UpdatedDate not set correctly, expected: %v, got: 
%v", updatedDate, p.UpdatedDate)
+       }
+
+       SetScopeFields(&p, connectionId, &createdDate, nil)
+
+       // verify fields
+       if p.ConnectionId != connectionId {
+               t.Errorf("ConnectionId not set correctly, expected: %v, got: 
%v", connectionId, p.ConnectionId)
+       }
+
+       if !p.CreatedDate.Equal(createdDate) {
+               t.Errorf("CreatedDate not set correctly, expected: %v, got: 
%v", createdDate, p.CreatedDate)
+       }
+
+       if p.UpdatedDate != nil {
+               t.Errorf("UpdatedDate not set correctly, expected: %v, got: 
%v", nil, p.UpdatedDate)
+       }
+}
+
+func TestReturnPrimaryKeyValue(t *testing.T) {
+       // Define a test struct with the primaryKey tag on one of its fields.
+       type TestStruct struct {
+               ConnectionId int    `json:"connectionId" gorm:"primaryKey"`
+               Id           int    `json:"id" gorm:"primaryKey"`
+               Name         string `json:"name"`
+               CreatedAt    time.Time
+               UpdatedAt    time.Time
+               DeletedAt    gorm.DeletedAt `gorm:"index"`
+       }
+
+       // Create an instance of the test struct.
+       test := TestStruct{
+               ConnectionId: 1,
+               Id:           123,
+               Name:         "Test",
+               CreatedAt:    time.Now(),
+       }
+
+       // Call the function and check if it returns the correct primary key 
value.
+       result := ReturnPrimaryKeyValue(test)
+       expected := "1-123"
+       if result != expected {
+               t.Errorf("ReturnPrimaryKeyValue returned %s, expected %s", 
result, expected)
+       }
+
+       // Test with a different struct that has no field with primaryKey tag.
+       type TestStruct2 struct {
+               Id        int    `json:"id"`
+               Name      string `json:"name"`
+               CreatedAt time.Time
+               UpdatedAt time.Time
+       }
+
+       test2 := TestStruct2{
+               Id:        456,
+               Name:      "Test 2",
+               CreatedAt: time.Now(),
+       }
+
+       result2 := ReturnPrimaryKeyValue(test2)
+       expected2 := ""
+       if result2 != expected2 {
+               t.Errorf("ReturnPrimaryKeyValue returned %s, expected %s", 
result2, expected2)
+       }
+}
+
+func TestScopeApiHelper_Put(t *testing.T) {
+       mockDal := new(mockdal.Dal)
+       mockLogger := unithelper.DummyLogger()
+       mockRes := new(mockcontext.BasicRes)
+
+       mockRes.On("GetDal").Return(mockDal)
+       mockRes.On("GetConfig", mock.Anything).Return("")
+       mockRes.On("GetLogger").Return(mockLogger)
+
+       // we expect total 2 deletion calls after all code got carried out
+       mockDal.On("Delete", mock.Anything, mock.Anything).Return(nil).Twice()
+       mockDal.On("GetPrimaryKeyFields", mock.Anything).Return(
+               []reflect.StructField{
+                       {Name: "ID", Type: reflect.TypeOf("")},
+               },
+       )
+       mockDal.On("CreateOrUpdate", mock.Anything, mock.Anything).Return(nil)
+       mockDal.On("First", mock.Anything, mock.Anything).Return(nil)
+       mockDal.On("All", mock.Anything, mock.Anything).Return(nil)
+
+       connHelper := NewConnectionHelper(mockRes, nil)
+
+       // create a mock input, scopes, and connection
+       input := &plugin.ApiResourceInput{Params: 
map[string]string{"connectionId": "123"}}
+       scopes := []*GithubRepo{
+               {GithubId: 1, Name: "scope1"},
+               {GithubId: 2, Name: "scope2"},
+       }
+       connection := &GithubConnection{}
+       connection.ID = 3
+       connection.Name = "test"
+
+       // create a mock ScopeApiHelper with a mock database connection
+       apiHelper := &ScopeApiHelper{db: mockDal, connHelper: connHelper}
+       apiReq := req{Data: scopes}
+       // test a successful call to Put
+       err := apiHelper.Put(input, &apiReq, connection)
+       assert.NoError(t, err)
+
+       // test a call to Put with a duplicate primary key value
+       duplicateScopes := []*GithubRepo{
+               {GithubId: 1, Name: "scope1"},
+               {GithubId: 1, Name: "scope2"},
+       }
+       apiReq.Data = duplicateScopes
+       err = apiHelper.Put(input, &apiReq, connection)
+       assert.Error(t, err)
+
+       // test a call to Put with an invalid primary key value
+       invalidScopes := []*GithubRepo{
+               {GithubId: 0, Name: "scope1"},
+       }
+       apiReq.Data = invalidScopes
+
+       err = apiHelper.Put(input, &apiReq, connection)
+       assert.Error(t, err)
+}
diff --git a/backend/plugins/github/api/init.go 
b/backend/plugins/github/api/init.go
index d92c2b334..8f0dc3bad 100644
--- a/backend/plugins/github/api/init.go
+++ b/backend/plugins/github/api/init.go
@@ -25,6 +25,7 @@ import (
 
 var vld *validator.Validate
 var connectionHelper *api.ConnectionApiHelper
+var scopeHelper *api.ScopeApiHelper
 var basicRes context.BasicRes
 
 func Init(br context.BasicRes) {
@@ -34,4 +35,9 @@ func Init(br context.BasicRes) {
                basicRes,
                vld,
        )
+       scopeHelper = api.NewScopeHelper(
+               basicRes,
+               vld,
+               connectionHelper,
+       )
 }
diff --git a/backend/plugins/github/api/scope.go 
b/backend/plugins/github/api/scope.go
index 75d819e5c..245d6eb39 100644
--- a/backend/plugins/github/api/scope.go
+++ b/backend/plugins/github/api/scope.go
@@ -18,16 +18,10 @@ 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/github/models"
        "net/http"
-       "strconv"
-       "time"
-
-       "github.com/mitchellh/mapstructure"
 )
 
 type apiRepo struct {
@@ -51,33 +45,8 @@ type req struct {
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/github/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 repos req
-       err := errors.Convert(mapstructure.Decode(input.Body, &repos))
-       if err != nil {
-               return nil, errors.BadInput.Wrap(err, "decoding Github repo 
error")
-       }
-       keeper := make(map[int]struct{})
-       now := time.Now()
-       for _, repo := range repos.Data {
-               if _, ok := keeper[repo.GithubId]; ok {
-                       return nil, errors.BadInput.New("duplicated item")
-               } else {
-                       keeper[repo.GithubId] = struct{}{}
-               }
-               repo.ConnectionId = connectionId
-               // Fixme: why do we set this to now?
-               repo.CreatedDate = &now
-               repo.UpdatedDate = &now
-               err = verifyRepo(repo)
-               if err != nil {
-                       return nil, err
-               }
-       }
-       err = basicRes.GetDal().CreateOrUpdate(repos.Data)
+       err := scopeHelper.Put(input, &repos, &models.GithubConnection{})
        if err != nil {
                return nil, errors.Default.Wrap(err, "error on saving 
GithubRepo")
        }
@@ -95,28 +64,13 @@ func PutScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors
 // @Success 200  {object} models.GithubRepo
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/github/connections/{connectionId}/scopes/{repoId} [PATCH]
+// @Router /plugins/github/connections/{connectionId}/scopes/{scopeId} [PATCH]
 func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       connectionId, repoId := extractParam(input.Params)
-       if connectionId*repoId == 0 {
-               return nil, errors.BadInput.New("invalid connectionId or 
repoId")
-       }
        var repo models.GithubRepo
-       err := basicRes.GetDal().First(&repo, dal.Where("connection_id = ? AND 
github_id = ?", connectionId, repoId))
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "getting GithubRepo error")
-       }
-       err = api.DecodeMapStruct(input.Body, &repo)
+       var conn models.GithubConnection
+       err := scopeHelper.Update(input, "github_id", &conn, &repo)
        if err != nil {
-               return nil, errors.Default.Wrap(err, "patch github repo error")
-       }
-       err = verifyRepo(&repo)
-       if err != nil {
-               return nil, err
-       }
-       err = basicRes.GetDal().Update(repo)
-       if err != nil {
-               return nil, errors.Default.Wrap(err, "error on saving 
GithubRepo")
+               return &plugin.ApiResourceOutput{Body: nil, Status: 
http.StatusInternalServerError}, err
        }
        return &plugin.ApiResourceOutput{Body: repo, Status: http.StatusOK}, nil
 }
@@ -133,36 +87,16 @@ func UpdateScope(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, err
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/github/connections/{connectionId}/scopes/ [GET]
 func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
-       var repos []models.GithubRepo
-       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(&repos, dal.Where("connection_id = ?", 
connectionId), dal.Limit(limit), dal.Offset(offset))
+       var repos []*models.GithubRepo
+       var rules []*models.GithubTransformationRule
+       var conn models.GithubConnection
+       names, err := scopeHelper.GetScopeList(input, conn, &repos, &rules)
        if err != nil {
                return nil, err
        }
-       var ruleIds []uint64
-       for _, repo := range repos {
-               if repo.TransformationRuleId > 0 {
-                       ruleIds = append(ruleIds, repo.TransformationRuleId)
-               }
-       }
-       var rules []models.GithubTransformationRule
-       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 apiRepos []apiRepo
        for _, repo := range repos {
-               apiRepos = append(apiRepos, apiRepo{repo, 
names[repo.TransformationRuleId]})
+               apiRepos = append(apiRepos, apiRepo{*repo, 
names[repo.TransformationRuleId]})
        }
        return &plugin.ApiResourceOutput{Body: apiRepos, Status: 
http.StatusOK}, nil
 }
@@ -176,43 +110,14 @@ func GetScopeList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, er
 // @Success 200  {object} apiRepo
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/github/connections/{connectionId}/scopes/{repoId} [GET]
+// @Router /plugins/github/connections/{connectionId}/scopes/{id} [GET]
 func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
        var repo models.GithubRepo
-       connectionId, repoId := extractParam(input.Params)
-       if connectionId*repoId == 0 {
-               return nil, errors.BadInput.New("invalid path params")
-       }
-       db := basicRes.GetDal()
-       err := db.First(&repo, dal.Where("connection_id = ? AND github_id = ?", 
connectionId, repoId))
-       if db.IsErrorNotFound(err) {
-               return nil, errors.NotFound.New("record not found")
-       }
+       var rule models.GithubTransformationRule
+       var conn models.GithubConnection
+       err := scopeHelper.GetScope(input, "github_id", conn, &repo, &rule)
        if err != nil {
                return nil, err
        }
-       var rule models.GithubTransformationRule
-       if repo.TransformationRuleId > 0 {
-               err = basicRes.GetDal().First(&rule, dal.Where("id = ?", 
repo.TransformationRuleId))
-               if err != nil {
-                       return nil, err
-               }
-       }
        return &plugin.ApiResourceOutput{Body: apiRepo{repo, rule.Name}, 
Status: http.StatusOK}, nil
 }
-
-func extractParam(params map[string]string) (uint64, uint64) {
-       connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
-       repoId, _ := strconv.ParseUint(params["repoId"], 10, 64)
-       return connectionId, repoId
-}
-
-func verifyRepo(repo *models.GithubRepo) errors.Error {
-       if repo.ConnectionId == 0 {
-               return errors.BadInput.New("invalid connectionId")
-       }
-       if repo.GithubId <= 0 {
-               return errors.BadInput.New("invalid github ID")
-       }
-       return nil
-}
diff --git a/backend/plugins/github/impl/impl.go 
b/backend/plugins/github/impl/impl.go
index 756e73097..71dd2dcea 100644
--- a/backend/plugins/github/impl/impl.go
+++ b/backend/plugins/github/impl/impl.go
@@ -204,7 +204,7 @@ func (p Github) ApiResources() 
map[string]map[string]plugin.ApiResourceHandler {
                        "PATCH":  api.PatchConnection,
                        "DELETE": api.DeleteConnection,
                },
-               "connections/:connectionId/scopes/:repoId": {
+               "connections/:connectionId/scopes/:scopeId": {
                        "GET":   api.GetScope,
                        "PATCH": api.UpdateScope,
                },
diff --git a/backend/plugins/gitlab/api/init.go 
b/backend/plugins/gitlab/api/init.go
index cd019a36f..8f0dc3bad 100644
--- a/backend/plugins/gitlab/api/init.go
+++ b/backend/plugins/gitlab/api/init.go
@@ -19,19 +19,25 @@ 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/go-playground/validator/v10"
 )
 
 var vld *validator.Validate
-var connectionHelper *helper.ConnectionApiHelper
+var connectionHelper *api.ConnectionApiHelper
+var scopeHelper *api.ScopeApiHelper
 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(
+               basicRes,
+               vld,
+               connectionHelper,
+       )
 }
diff --git a/backend/plugins/gitlab/api/scope.go 
b/backend/plugins/gitlab/api/scope.go
index 6af3c4b1e..4e660b9a2 100644
--- a/backend/plugins/gitlab/api/scope.go
+++ b/backend/plugins/gitlab/api/scope.go
@@ -18,16 +18,13 @@ limitations under the License.
 package api
 
 import (
-       "net/http"
-       "strconv"
-       "time"
-
        "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/gitlab/models"
-       "github.com/mitchellh/mapstructure"
+       "net/http"
+       "strconv"
 )
 
 type apiProject struct {
@@ -51,36 +48,12 @@ type req struct {
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /plugins/gitlab/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
-       err := errors.Convert(mapstructure.Decode(input.Body, &projects))
+       var scopes req
+       err := scopeHelper.Put(input, &scopes, &models.GitlabConnection{})
        if err != nil {
-               return nil, errors.BadInput.Wrap(err, "decoding Gitlab project 
error")
-       }
-       keeper := make(map[int]struct{})
-       now := time.Now()
-       for _, project := range projects.Data {
-               if _, ok := keeper[project.GitlabId]; ok {
-                       return nil, errors.BadInput.New("duplicated item")
-               } else {
-                       keeper[project.GitlabId] = struct{}{}
-               }
-               project.ConnectionId = connectionId
-               project.CreatedDate = now
-               project.UpdatedDate = &now
-               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 
GitlabProject")
+               return nil, errors.Default.Wrap(err, "error on saving 
GithubRepo")
        }
-       return &plugin.ApiResourceOutput{Body: projects.Data, Status: 
http.StatusOK}, nil
+       return &plugin.ApiResourceOutput{Body: scopes.Data, Status: 
http.StatusOK}, nil
 }
 
 // UpdateScope patch to gitlab project

Reply via email to