diff --git a/backend/plugins/circleci/api/remote_api.go b/backend/plugins/circleci/api/remote_api.go index 5aa812fcb43..d4358445e3f 100644 --- a/backend/plugins/circleci/api/remote_api.go +++ b/backend/plugins/circleci/api/remote_api.go @@ -103,7 +103,7 @@ func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er // @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/circle +// @Tags plugins/circleci // @Router /plugins/bitbucket/connections/{connectionId}/proxy/{path} [GET] func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { return raProxy.Proxy(input) diff --git a/backend/plugins/teambition/api/blueprint200.go b/backend/plugins/teambition/api/blueprint200.go deleted file mode 100644 index 4c5611b7c89..00000000000 --- a/backend/plugins/teambition/api/blueprint200.go +++ /dev/null @@ -1,107 +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 ( - "fmt" - - "github.com/apache/incubator-devlake/plugins/teambition/models" - - "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/didgen" - "github.com/apache/incubator-devlake/core/models/domainlayer/ticket" - "github.com/apache/incubator-devlake/core/plugin" - helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" -) - -func MakeDataSourcePipelinePlanV200( - subtaskMetas []plugin.SubTaskMeta, - connectionId uint64, - bpScopes []*coreModels.BlueprintScope, -) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) { - plan := make(coreModels.PipelinePlan, len(bpScopes)) - plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, plan, bpScopes, connectionId) - if err != nil { - return nil, nil, err - } - scopes, err := makeScopesV200(bpScopes, connectionId) - if err != nil { - return nil, nil, err - } - - return plan, scopes, nil -} - -func makeDataSourcePipelinePlanV200( - subtaskMetas []plugin.SubTaskMeta, - plan coreModels.PipelinePlan, - bpScopes []*coreModels.BlueprintScope, - connectionId uint64, -) (coreModels.PipelinePlan, errors.Error) { - for i, bpScope := range bpScopes { - stage := plan[i] - if stage == nil { - stage = coreModels.PipelineStage{} - } - // construct task options for teambition - options := make(map[string]interface{}) - options["projectId"] = bpScope.ScopeId - options["connectionId"] = connectionId - - subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, plugin.DOMAIN_TYPES) - if err != nil { - return nil, err - } - stage = append(stage, &coreModels.PipelineTask{ - Plugin: "teambition", - Subtasks: subtasks, - Options: options, - }) - plan[i] = stage - } - - return plan, nil -} - -func makeScopesV200(bpScopes []*coreModels.BlueprintScope, connectionId uint64) ([]plugin.Scope, errors.Error) { - scopes := make([]plugin.Scope, 0) - for _, bpScope := range bpScopes { - project := &models.TeambitionProject{} - // get project from db - err := basicRes.GetDal().First(project, - dal.Where(`connection_id = ? and id = ?`, - connectionId, bpScope.ScopeId)) - if err != nil { - return nil, errors.Default.Wrap(err, fmt.Sprintf("fail to find project %s", bpScope.ScopeId)) - } - // add board to scopes - // if utils.StringsContains(bpScope.Entities, plugin.DOMAIN_TYPE_TICKET) { - domainBoard := &ticket.Board{ - DomainEntity: domainlayer.DomainEntity{ - Id: didgen.NewDomainIdGenerator(&models.TeambitionProject{}).Generate(project.ConnectionId, project.Id), - }, - Name: project.Name, - } - scopes = append(scopes, domainBoard) - // } - } - return scopes, nil -} diff --git a/backend/plugins/teambition/api/blueprint_v200.go b/backend/plugins/teambition/api/blueprint_v200.go new file mode 100644 index 00000000000..c71d5b644fc --- /dev/null +++ b/backend/plugins/teambition/api/blueprint_v200.go @@ -0,0 +1,105 @@ +/* +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/plugins/teambition/models" + "github.com/apache/incubator-devlake/plugins/teambition/tasks" + + "github.com/apache/incubator-devlake/core/errors" + coreModels "github.com/apache/incubator-devlake/core/models" + "github.com/apache/incubator-devlake/core/models/domainlayer/devops" + "github.com/apache/incubator-devlake/core/models/domainlayer/didgen" + "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/helpers/srvhelper" +) + +func MakeDataSourcePipelinePlanV200( + subtaskMetas []plugin.SubTaskMeta, + connectionId uint64, + bpScopes []*coreModels.BlueprintScope, +) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) { + connection, err := dsHelper.ConnSrv.FindByPk(connectionId) + if err != nil { + return nil, nil, err + } + scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes) + if err != nil { + return nil, nil, err + } + plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, connection) + if err != nil { + return nil, nil, err + } + scopes, err := makeScopesV200(scopeDetails, connection) + return plan, scopes, err +} + +func makePipelinePlanV200( + subtaskMetas []plugin.SubTaskMeta, + scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject, srvhelper.NoScopeConfig], + connection *models.TeambitionConnection, +) (coreModels.PipelinePlan, errors.Error) { + plan := make(coreModels.PipelinePlan, len(scopeDetails)) + for i, scopeDetail := range scopeDetails { + stage := plan[i] + if stage == nil { + stage = coreModels.PipelineStage{} + } + + scope := scopeDetail.Scope + // construct task options for circleci + task, err := helper.MakePipelinePlanTask( + "teambition", + subtaskMetas, + nil, + tasks.TeambitionOptions{ + ConnectionId: connection.ID, + ProjectId: scope.Id, + }, + ) + if err != nil { + return nil, err + } + stage = append(stage, task) + plan[i] = stage + } + + return plan, nil +} + +func makeScopesV200( + scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject, srvhelper.NoScopeConfig], + connection *models.TeambitionConnection, +) ([]plugin.Scope, errors.Error) { + scopes := make([]plugin.Scope, 0, len(scopeDetails)) + + idgen := didgen.NewDomainIdGenerator(&models.TeambitionProject{}) + for _, scopeDetail := range scopeDetails { + scope, _ := scopeDetail.Scope, scopeDetail.ScopeConfig + id := idgen.Generate(connection.ID, scope.Id) + + // add cicd_scope to scopes + // if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_CICD) { + scopes = append(scopes, devops.NewCicdScope(id, scope.Name)) + // } + } + + return scopes, nil +} diff --git a/backend/plugins/teambition/api/connection.go b/backend/plugins/teambition/api/connection_api.go similarity index 81% rename from backend/plugins/teambition/api/connection.go rename to backend/plugins/teambition/api/connection_api.go index ee40bc8ed18..d8d1053efa6 100644 --- a/backend/plugins/teambition/api/connection.go +++ b/backend/plugins/teambition/api/connection_api.go @@ -114,12 +114,8 @@ func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/teambition/connections/{connectionId}/test [POST] func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.TeambitionConnection{} - err := connectionHelper.First(connection, input.Params) + connection, err := dsHelper.ConnApi.GetMergedConnection(input) if err != nil { - return nil, errors.BadInput.Wrap(err, "find connection from db") - } - if err := api.DecodeMapStruct(input.Body, connection, false); err != nil { return nil, err } testConnectionResult, testConnectionErr := testConnection(context.TODO(), connection.TeambitionConn) @@ -138,13 +134,7 @@ func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResource // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/teambition/connections [POST] func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - // update from request and save to database - connection := &models.TeambitionConnection{} - 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 @Summary patch teambition connection @@ -156,17 +146,7 @@ func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/teambition/connections/{connectionId} [PATCH] func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.TeambitionConnection{} - if err := connectionHelper.First(&connection, input.Params); err != nil { - return nil, err - } - if err := (&models.TeambitionConnection{}).MergeFromRequest(connection, input.Body); err != nil { - return nil, errors.Convert(err) - } - if err := connectionHelper.SaveWithCreateOrUpdate(connection); err != nil { - return nil, err - } - return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil + return dsHelper.ConnApi.Patch(input) } // DeleteConnection @Summary delete a teambition connection @@ -178,14 +158,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/teambition/connections/{connectionId} [DELETE] func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - conn := &models.TeambitionConnection{} - 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 @Summary get all teambition connections @@ -196,15 +169,7 @@ func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/teambition/connections [GET] func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - var connections []models.TeambitionConnection - 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 @Summary get teambition connection detail @@ -215,7 +180,5 @@ func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/teambition/connections/{connectionId} [GET] func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.TeambitionConnection{} - err := connectionHelper.First(connection, input.Params) - return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err + return dsHelper.ConnApi.GetDetail(input) } diff --git a/backend/plugins/teambition/api/init.go b/backend/plugins/teambition/api/init.go index aef9f75f70c..f1e3e0c8b20 100644 --- a/backend/plugins/teambition/api/init.go +++ b/backend/plugins/teambition/api/init.go @@ -20,21 +20,31 @@ package api import ( "github.com/apache/incubator-devlake/core/context" "github.com/apache/incubator-devlake/core/plugin" - helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/helpers/srvhelper" + "github.com/apache/incubator-devlake/plugins/teambition/models" "github.com/go-playground/validator/v10" ) var vld *validator.Validate -var connectionHelper *helper.ConnectionApiHelper +var dsHelper *api.DsHelper[models.TeambitionConnection, models.TeambitionProject, srvhelper.NoScopeConfig] var basicRes context.BasicRes func Init(br context.BasicRes, p plugin.PluginMeta) { - basicRes = br vld = validator.New() - connectionHelper = helper.NewConnectionHelper( - basicRes, - vld, + dsHelper = api.NewDataSourceHelper[ + models.TeambitionConnection, + models.TeambitionProject, + srvhelper.NoScopeConfig, + ]( + br, p.Name(), + []string{"name"}, + func(c models.TeambitionConnection) models.TeambitionConnection { + return c.Sanitize() + }, + nil, + nil, ) } diff --git a/backend/plugins/teambition/e2e/snapshot_tables/_tool_teambition_projects.csv b/backend/plugins/teambition/e2e/snapshot_tables/_tool_teambition_projects.csv index 69c565cf45a..60d217789ec 100644 --- a/backend/plugins/teambition/e2e/snapshot_tables/_tool_teambition_projects.csv +++ b/backend/plugins/teambition/e2e/snapshot_tables/_tool_teambition_projects.csv @@ -1,2 +1,2 @@ -connection_id,id,name,logo,description,organization_id,visibility,is_template,creator_id,is_archived,is_suspended,unique_id_prefix,created,updated,start_date,end_date,customfields,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -1,64132c94f0d59df1c9825ab8,缺陷管理,https://tcs-ga.teambition.net/thumbnail/312qf26a0a3a77f700a4fded1f2a4c19b257/w/600/h/300,"",640b1c30c933fd85bb11ca31,project,0,5f27709685e4266322e2690a,0,0,"",2023-03-16 14:49:56.415,2023-03-16 14:49:56.415,,,[],2023-03-23 14:24:57.135,2023-03-23 14:24:57.135,"{""ConnectionId"":1,""OrganizationId"":"""",""ProjectId"":""64132c94f0d59df1c9825ab8""}",_raw_teambition_api_projects,1,"" +connection_id,id,name,logo,description,organization_id,visibility,is_template,creator_id,is_archived,is_suspended,unique_id_prefix,created,updated,start_date,end_date,customfields,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,scope_config_id +1,64132c94f0d59df1c9825ab8,缺陷管理,https://tcs-ga.teambition.net/thumbnail/312qf26a0a3a77f700a4fded1f2a4c19b257/w/600/h/300,"",640b1c30c933fd85bb11ca31,project,0,5f27709685e4266322e2690a,0,0,"",2023-03-16 14:49:56.415,2023-03-16 14:49:56.415,,,[],2023-03-23 14:24:57.135,2023-03-23 14:24:57.135,"{""ConnectionId"":1,""OrganizationId"":"""",""ProjectId"":""64132c94f0d59df1c9825ab8""}",_raw_teambition_api_projects,1,"",0 diff --git a/backend/plugins/teambition/models/migrationscripts/20240417_add_scope_config_id.go b/backend/plugins/teambition/models/migrationscripts/20240417_add_scope_config_id.go new file mode 100644 index 00000000000..f445676a0cf --- /dev/null +++ b/backend/plugins/teambition/models/migrationscripts/20240417_add_scope_config_id.go @@ -0,0 +1,49 @@ +/* +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 migrationscripts + +import ( + "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/migrationhelper" +) + +var _ plugin.MigrationScript = (*addScopeConfigId)(nil) + +type project20240417 struct { + ScopeConfigId uint64 +} + +func (project20240417) TableName() string { + return "_tool_teambition_projects" +} + +type addScopeConfigId struct{} + +func (*addScopeConfigId) Up(basicRes context.BasicRes) errors.Error { + return migrationhelper.AutoMigrateTables(basicRes, &project20240417{}) +} + +func (*addScopeConfigId) Version() uint64 { + return 20240417165745 +} + +func (*addScopeConfigId) Name() string { + return "add scope_config_id to scope table" +} diff --git a/backend/plugins/teambition/models/migrationscripts/register.go b/backend/plugins/teambition/models/migrationscripts/register.go index dc265c84913..0f5567d5b00 100644 --- a/backend/plugins/teambition/models/migrationscripts/register.go +++ b/backend/plugins/teambition/models/migrationscripts/register.go @@ -24,5 +24,6 @@ func All() []plugin.MigrationScript { return []plugin.MigrationScript{ new(addInitTables), new(reCreateTeambitionConnections), + new(addScopeConfigId), } } diff --git a/backend/plugins/teambition/models/project.go b/backend/plugins/teambition/models/project.go index 734fb5e4e86..0d50e8f0e7a 100644 --- a/backend/plugins/teambition/models/project.go +++ b/backend/plugins/teambition/models/project.go @@ -19,10 +19,13 @@ package models import ( "github.com/apache/incubator-devlake/core/models/common" + "github.com/apache/incubator-devlake/core/plugin" ) +var _ plugin.ToolLayerScope = (*TeambitionProject)(nil) + type TeambitionProject struct { - ConnectionId uint64 `gorm:"primaryKey;type:BIGINT"` + common.Scope Id string `gorm:"primaryKey;type:varchar(100)" json:"id"` Name string `gorm:"type:varchar(255)" json:"name"` Logo string `gorm:"type:varchar(255)" json:"logo"` @@ -39,8 +42,29 @@ type TeambitionProject struct { StartDate *common.Iso8601Time `json:"startDate"` EndDate *common.Iso8601Time `json:"endDate"` Customfields []TeambitionProjectCustomField `gorm:"serializer:json;type:text" json:"customfields"` +} + +// ScopeFullName implements plugin.ToolLayerScope. +func (t TeambitionProject) ScopeFullName() string { + return t.Name +} + +// ScopeId implements plugin.ToolLayerScope. +func (t TeambitionProject) ScopeId() string { + return t.Id +} + +// ScopeName implements plugin.ToolLayerScope. +func (t TeambitionProject) ScopeName() string { + return t.Name +} - common.NoPKModel +// ScopeParams implements plugin.ToolLayerScope. +func (t TeambitionProject) ScopeParams() interface{} { + return &TeambitionApiParams{ + ConnectionId: t.ConnectionId, + ProjectId: t.Id, + } } type TeambitionProjectCustomField struct { @@ -58,3 +82,9 @@ type TeambitionProjectCustomFieldValue struct { func (TeambitionProject) TableName() string { return "_tool_teambition_projects" } + +type TeambitionApiParams struct { + ConnectionId uint64 + OrganizationId string + ProjectId string +} diff --git a/backend/plugins/teambition/tasks/shared.go b/backend/plugins/teambition/tasks/shared.go index ce2b4012962..9831c69b01a 100644 --- a/backend/plugins/teambition/tasks/shared.go +++ b/backend/plugins/teambition/tasks/shared.go @@ -18,21 +18,16 @@ limitations under the License. package tasks import ( + "strings" + "github.com/apache/incubator-devlake/core/dal" "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/models/domainlayer/didgen" "github.com/apache/incubator-devlake/core/plugin" "github.com/apache/incubator-devlake/helpers/pluginhelper/api" "github.com/apache/incubator-devlake/plugins/teambition/models" - "strings" ) -type TeambitionApiParams struct { - ConnectionId uint64 - OrganizationId string - ProjectId string -} - type TeambitionComRes[T any] struct { NextPageToken string `json:"nextPageToken"` TotalSize int `json:"totalSize"` @@ -96,7 +91,7 @@ func CreateRawDataSubTaskArgs(taskCtx plugin.SubTaskContext, rawTable string) (* filteredData := *data filteredData.Options = &TeambitionOptions{} *filteredData.Options = *data.Options - params := TeambitionApiParams{ + params := models.TeambitionApiParams{ ConnectionId: data.Options.ConnectionId, ProjectId: data.Options.ProjectId, } diff --git a/backend/plugins/trello/api/blueprint_v200.go b/backend/plugins/trello/api/blueprint_v200.go index 836e5cdd3e8..4e6cd792972 100644 --- a/backend/plugins/trello/api/blueprint_v200.go +++ b/backend/plugins/trello/api/blueprint_v200.go @@ -18,10 +18,10 @@ limitations under the License. package api import ( - "github.com/apache/incubator-devlake/core/models/domainlayer" - "github.com/apache/incubator-devlake/core/models/domainlayer/ticket" + "github.com/apache/incubator-devlake/core/models/domainlayer/devops" "github.com/apache/incubator-devlake/plugins/trello/models" + "github.com/apache/incubator-devlake/plugins/trello/tasks" "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/utils" @@ -30,84 +30,79 @@ import ( "github.com/apache/incubator-devlake/core/models/domainlayer/didgen" "github.com/apache/incubator-devlake/core/plugin" helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/helpers/srvhelper" ) func MakePipelinePlanV200( subtaskMetas []plugin.SubTaskMeta, connectionId uint64, - scope []*coreModels.BlueprintScope, + bpScopes []*coreModels.BlueprintScope, ) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) { - scopes, err := makeScopeV200(connectionId, scope) + connection, err := dsHelper.ConnSrv.FindByPk(connectionId) if err != nil { return nil, nil, err } - - plan := make(coreModels.PipelinePlan, len(scope)) - plan, err = makePipelinePlanV200(subtaskMetas, plan, scope, connectionId) + scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes) if err != nil { return nil, nil, err } - - return plan, scopes, nil -} - -func makeScopeV200(connectionId uint64, scopes []*coreModels.BlueprintScope) ([]plugin.Scope, errors.Error) { - sc := make([]plugin.Scope, 0, len(scopes)) - - for _, scope := range scopes { - trelloBoard, scopeConfig, err := scopeHelper.DbHelper().GetScopeAndConfig(connectionId, scope.ScopeId) - if err != nil { - return nil, err - } - // add board to scopes - if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_TICKET) { - domainBoard := &ticket.Board{ - DomainEntity: domainlayer.DomainEntity{ - Id: didgen.NewDomainIdGenerator(&models.TrelloConnection{}).Generate(trelloBoard.ConnectionId, trelloBoard.BoardId), - }, - Name: trelloBoard.Name, - } - sc = append(sc, domainBoard) - } + plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, connection) + if err != nil { + return nil, nil, err } - - return sc, nil + scopes, err := makeScopesV200(scopeDetails, connection) + return plan, scopes, err } func makePipelinePlanV200( subtaskMetas []plugin.SubTaskMeta, - plan coreModels.PipelinePlan, - scopes []*coreModels.BlueprintScope, - connectionId uint64, + scopeDetails []*srvhelper.ScopeDetail[models.TrelloBoard, models.TrelloScopeConfig], + connection *models.TrelloConnection, ) (coreModels.PipelinePlan, errors.Error) { - for i, scope := range scopes { + plan := make(coreModels.PipelinePlan, len(scopeDetails)) + for i, scopeDetail := range scopeDetails { stage := plan[i] if stage == nil { stage = coreModels.PipelineStage{} } - // construct task options for trello - options := make(map[string]interface{}) - options["connectionId"] = connectionId - options["scopeId"] = scope.ScopeId - - _, scopeConfig, err := scopeHelper.DbHelper().GetScopeAndConfig(connectionId, scope.ScopeId) + scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig + // construct task options for circleci + task, err := helper.MakePipelinePlanTask( + "trello", + subtaskMetas, + scopeConfig.Entities, + tasks.TrelloOptions{ + ConnectionId: connection.ID, + BoardId: scope.BoardId, + }, + ) if err != nil { return nil, err } - // construct subtasks - subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, scopeConfig.Entities) - if err != nil { - return nil, err - } - - stage = append(stage, &coreModels.PipelineTask{ - Plugin: "trello", - Subtasks: subtasks, - Options: options, - }) - + stage = append(stage, task) plan[i] = stage } + return plan, nil } + +func makeScopesV200( + scopeDetails []*srvhelper.ScopeDetail[models.TrelloBoard, models.TrelloScopeConfig], + connection *models.TrelloConnection, +) ([]plugin.Scope, errors.Error) { + scopes := make([]plugin.Scope, 0, len(scopeDetails)) + + idgen := didgen.NewDomainIdGenerator(&models.TrelloBoard{}) + for _, scopeDetail := range scopeDetails { + scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig + id := idgen.Generate(connection.ID, scope.BoardId) + + // add cicd_scope to scopes + if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_TICKET) { + scopes = append(scopes, devops.NewCicdScope(id, scope.Name)) + } + } + + return scopes, nil +} diff --git a/backend/plugins/trello/api/connection.go b/backend/plugins/trello/api/connection_api.go similarity index 81% rename from backend/plugins/trello/api/connection.go rename to backend/plugins/trello/api/connection_api.go index 39aeaa3ba15..994065e2b06 100644 --- a/backend/plugins/trello/api/connection.go +++ b/backend/plugins/trello/api/connection_api.go @@ -100,8 +100,7 @@ func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/trello/connections/{connectionId}/test [POST] func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.TrelloConnection{} - err := connectionHelper.First(connection, input.Params) + connection, err := dsHelper.ConnApi.GetMergedConnection(input) if err != nil { return nil, errors.BadInput.Wrap(err, "find connection from db") } @@ -124,13 +123,7 @@ func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResource // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/trello/connections [POST] func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - // update from request and save to database - connection := &models.TrelloConnection{} - err := connectionHelper.Create(connection, input) - if err != nil { - return nil, err - } - return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: http.StatusOK}, nil + return dsHelper.ConnApi.Post(input) } // @Summary patch trello connection @@ -142,17 +135,7 @@ func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/trello/connections/{connectionId} [PATCH] func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.TrelloConnection{} - if err := connectionHelper.First(&connection, input.Params); err != nil { - return nil, err - } - if err := (&models.TrelloConnection{}).MergeFromRequest(connection, input.Body); err != nil { - return nil, errors.Convert(err) - } - if err := connectionHelper.SaveWithCreateOrUpdate(connection); err != nil { - return nil, err - } - return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil + return dsHelper.ConnApi.Patch(input) } // @Summary delete a trello connection @@ -164,14 +147,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/trello/connections/{connectionId} [DELETE] func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - conn := &models.TrelloConnection{} - output, err := connectionHelper.Delete(conn, input) - if err != nil { - return output, err - } - output.Body = conn.Sanitize() - return output, nil - + return dsHelper.ConnApi.Delete(input) } // @Summary get all trello connections @@ -182,15 +158,7 @@ func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/trello/connections [GET] func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - var connections []models.TrelloConnection - 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) } // @Summary get trello connection detail @@ -201,7 +169,5 @@ func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/trello/connections/{connectionId} [GET] func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.TrelloConnection{} - err := connectionHelper.First(connection, input.Params) - return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err + return dsHelper.ConnApi.GetDetail(input) } diff --git a/backend/plugins/trello/api/init.go b/backend/plugins/trello/api/init.go index 08f2950356c..3b9b152c95e 100644 --- a/backend/plugins/trello/api/init.go +++ b/backend/plugins/trello/api/init.go @@ -26,36 +26,31 @@ import ( ) var vld *validator.Validate -var connectionHelper *api.ConnectionApiHelper -var scopeHelper *api.ScopeApiHelper[models.TrelloConnection, models.TrelloBoard, models.TrelloScopeConfig] var basicRes context.BasicRes -var scHelper *api.ScopeConfigHelper[models.TrelloScopeConfig, *models.TrelloScopeConfig] + +var dsHelper *api.DsHelper[models.TrelloConnection, models.TrelloBoard, models.TrelloScopeConfig] +var raProxy *api.DsRemoteApiProxyHelper[models.TrelloConnection] + +// var raScopeList *api.DsRemoteApiScopeListHelper[models.TrelloConnection, models.TrelloBoard, srvhelper.NoPagintation] + +// var raScopeSearch *api.DsRemoteApiScopeSearchHelper[models.TrelloConnection, models.TrelloBoard] func Init(br context.BasicRes, p plugin.PluginMeta) { basicRes = br vld = validator.New() - connectionHelper = api.NewConnectionHelper( - basicRes, - vld, + dsHelper = api.NewDataSourceHelper[ + models.TrelloConnection, models.TrelloBoard, models.TrelloScopeConfig, + ]( + br, p.Name(), - ) - params := &api.ReflectionParameters{ - ScopeIdFieldName: "BoardId", - ScopeIdColumnName: "board_id", - RawScopeParamName: "BoardId", - } - scopeHelper = api.NewScopeHelper[models.TrelloConnection, models.TrelloBoard, models.TrelloScopeConfig]( - basicRes, - vld, - connectionHelper, - api.NewScopeDatabaseHelperImpl[models.TrelloConnection, models.TrelloBoard, models.TrelloScopeConfig]( - basicRes, connectionHelper, params), - params, + []string{"name"}, + func(c models.TrelloConnection) models.TrelloConnection { + return c.Sanitize() + }, + nil, nil, ) - scHelper = api.NewScopeConfigHelper[models.TrelloScopeConfig, *models.TrelloScopeConfig]( - basicRes, - vld, - p.Name(), - ) + raProxy = api.NewDsRemoteApiProxyHelper[models.TrelloConnection](dsHelper.ConnApi.ModelApiHelper) + // raScopeList = api.NewDsRemoteApiScopeListHelper[models.TrelloConnection, models.TrelloBoard, srvhelper.NoPagintation](raProxy, listCircleciRemoteScopes) + // raScopeSearch = api.NewDsRemoteApiScopeSearchHelper[models.TrelloConnection, models.TrelloBoard](raProxy, searchCircleciProjects) } diff --git a/backend/plugins/trello/api/proxy.go b/backend/plugins/trello/api/remote_api.go similarity index 56% rename from backend/plugins/trello/api/proxy.go rename to backend/plugins/trello/api/remote_api.go index 6930a806ad5..75ffb33e1cc 100644 --- a/backend/plugins/trello/api/proxy.go +++ b/backend/plugins/trello/api/remote_api.go @@ -18,34 +18,16 @@ limitations under the License. package api import ( - "context" - "github.com/apache/incubator-devlake/plugins/trello/models" - "io" - "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/plugin" - helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" ) +// @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/trello +// @Router /plugins/bitbucket/connections/{connectionId}/proxy/{path} [GET] func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.TrelloConnection{} - 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 + return raProxy.Proxy(input) } diff --git a/backend/plugins/trello/api/scope.go b/backend/plugins/trello/api/scope_api.go similarity index 85% rename from backend/plugins/trello/api/scope.go rename to backend/plugins/trello/api/scope_api.go index 83876f51dd3..dd5a67d71a5 100644 --- a/backend/plugins/trello/api/scope.go +++ b/backend/plugins/trello/api/scope_api.go @@ -30,7 +30,10 @@ type ScopeRes struct { type ScopeReq api.ScopeReq[models.TrelloBoard] -// PutScope create or update trello board +type PutScopesReqBody api.PutScopesReqBody[models.TrelloBoard] +type ScopeDetail api.ScopeDetail[models.TrelloBoard, models.TrelloScopeConfig] + +// PutScopes create or update trello board // @Summary create or update trello board // @Description Create or update trello board // @Tags plugins/trello @@ -41,11 +44,11 @@ type ScopeReq api.ScopeReq[models.TrelloBoard] // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/connections/{connectionId}/scopes [PUT] -func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scopeHelper.Put(input) +func PutScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeApi.PutMultiple(input) } -// UpdateScope patch to trello board +// PatchScope patch to trello board // @Summary patch to trello board // @Description patch to trello board // @Tags plugins/trello @@ -57,8 +60,8 @@ func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/connections/{connectionId}/scopes/{boardId} [PATCH] -func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scopeHelper.Update(input) +func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeApi.Patch(input) } // GetScopeList get Trello boards @@ -69,12 +72,12 @@ func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, err // @Param searchTerm query string false "search term for scope name" // @Param pageSize query int false "page size, default 50" // @Param page query int false "page size, default 1" -// @Success 200 {object} []models.TrelloBoard +// @Success 200 {object} []ScopeDetail // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/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 Trello board @@ -83,12 +86,12 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er // @Tags plugins/trello // @Param connectionId path int false "connection ID" // @Param boardId path string false "board ID" -// @Success 200 {object} models.TrelloBoard +// @Success 200 {object} ScopeDetail // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/connections/{connectionId}/scopes/{boardId} [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 @@ -104,5 +107,5 @@ func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/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/trello/api/scope_config.go b/backend/plugins/trello/api/scope_config_api.go similarity index 86% rename from backend/plugins/trello/api/scope_config.go rename to backend/plugins/trello/api/scope_config_api.go index 4562ff109ed..5a372cd344a 100644 --- a/backend/plugins/trello/api/scope_config.go +++ b/backend/plugins/trello/api/scope_config_api.go @@ -22,7 +22,7 @@ import ( "github.com/apache/incubator-devlake/core/plugin" ) -// CreateScopeConfig create scope config for Trello +// PostScopeConfig create scope config for Trello // @Summary create scope config for Trello // @Description create scope config for Trello // @Tags plugins/trello @@ -32,11 +32,11 @@ import ( // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/connections/{connectionId}/scope-configs [POST] -func CreateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.Create(input) +func PostScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeConfigApi.Post(input) } -// UpdateScopeConfig update scope config for Trello +// PatchScopeConfig update scope config for Trello // @Summary update scope config for Trello // @Description update scope config for Trello // @Tags plugins/trello @@ -47,8 +47,8 @@ func CreateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutpu // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/connections/{connectionId}/scope-configs/{id} [PATCH] -func UpdateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.Update(input) +func PatchScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeConfigApi.Patch(input) } // GetScopeConfig return one scope config @@ -61,7 +61,7 @@ func UpdateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutpu // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/connections/{connectionId}/scope-configs/{id} [GET] func GetScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.Get(input) + return dsHelper.ScopeConfigApi.GetDetail(input) } // GetScopeConfigList return all scope configs @@ -75,7 +75,7 @@ func GetScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/connections/{connectionId}/scope-configs [GET] func GetScopeConfigList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.List(input) + return dsHelper.ScopeConfigApi.GetAll(input) } // DeleteScopeConfig delete a scope config @@ -89,5 +89,5 @@ func GetScopeConfigList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutp // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/trello/connections/{connectionId}/scope-configs/{id} [DELETE] func DeleteScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.Delete(input) + return dsHelper.ScopeConfigApi.Delete(input) } diff --git a/backend/plugins/trello/impl/impl.go b/backend/plugins/trello/impl/impl.go index 8454f9b3386..49dbb7c9ad2 100644 --- a/backend/plugins/trello/impl/impl.go +++ b/backend/plugins/trello/impl/impl.go @@ -99,10 +99,7 @@ func (p Trello) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]i return nil, errors.Default.Wrap(err, "Trello plugin could not decode options") } if op.BoardId == "" { - if op.ScopeId == "" { - return nil, errors.BadInput.New("one of boardId and scopeId is required") - } - op.BoardId = op.ScopeId + return nil, errors.BadInput.New("trello boardId is required") } if op.ConnectionId == 0 { return nil, errors.BadInput.New("trello connectionId is invalid") @@ -175,22 +172,22 @@ func (p Trello) ApiResources() map[string]map[string]plugin.ApiResourceHandler { "GET": api.Proxy, }, "connections/:connectionId/scope-configs": { - "POST": api.CreateScopeConfig, + "POST": api.PostScopeConfig, "GET": api.GetScopeConfigList, }, "connections/:connectionId/scope-configs/:id": { - "PATCH": api.UpdateScopeConfig, + "PATCH": api.PatchScopeConfig, "GET": api.GetScopeConfig, "DELETE": api.DeleteScopeConfig, }, "connections/:connectionId/scopes/:boardId": { "GET": api.GetScope, - "PATCH": api.UpdateScope, + "PATCH": api.PatchScope, "DELETE": api.DeleteScope, }, "connections/:connectionId/scopes": { "GET": api.GetScopeList, - "PUT": api.PutScope, + "PUT": api.PutScopes, }, } } diff --git a/backend/plugins/trello/tasks/task_data.go b/backend/plugins/trello/tasks/task_data.go index 273fc3f715e..a3e859d093b 100644 --- a/backend/plugins/trello/tasks/task_data.go +++ b/backend/plugins/trello/tasks/task_data.go @@ -23,10 +23,9 @@ import ( ) type TrelloOptions struct { - ConnectionId uint64 `json:"connectionId"` - BoardId string `json:"boardId"` - ScopeId string - ScopeConfigId uint64 + ConnectionId uint64 `json:"connectionId" mapstructure:"connectionId,omitempty"` + BoardId string `json:"boardId" mapstructure:"boardId,omitempty"` + ScopeConfigId uint64 `json:"scopeConfigId" mapstructure:"scopeConfigId,omitempty"` } type TrelloTaskData struct { diff --git a/backend/plugins/zentao/api/blueprint_V200_test.go b/backend/plugins/zentao/api/blueprint_V200_test.go deleted file mode 100644 index 983c78816e2..00000000000 --- a/backend/plugins/zentao/api/blueprint_V200_test.go +++ /dev/null @@ -1,184 +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 ( - "database/sql" - "testing" - - "github.com/apache/incubator-devlake/core/dal" - "gorm.io/gorm/migrator" - - 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/ticket" - "github.com/apache/incubator-devlake/core/plugin" - helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" - "github.com/apache/incubator-devlake/helpers/unithelper" - mockdal "github.com/apache/incubator-devlake/mocks/core/dal" - mockplugin "github.com/apache/incubator-devlake/mocks/core/plugin" - "github.com/apache/incubator-devlake/plugins/zentao/models" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestMakeDataSourcePipelinePlanV200(t *testing.T) { - connection := &models.ZentaoConnection{ - BaseConnection: helper.BaseConnection{ - Name: "zentao-test", - Model: common.Model{ - ID: 1, - }, - }, - ZentaoConn: models.ZentaoConn{ - RestConnection: helper.RestConnection{ - Endpoint: "https://zentao.example.org/api.php/v1/", - Proxy: "", - RateLimitPerHour: 0, - }, - BasicAuth: helper.BasicAuth{ - Username: "Username", - Password: "Password", - }, - }, - } - mockMeta := mockplugin.NewPluginMeta(t) - mockMeta.On("RootPkgPath").Return("github.com/apache/incubator-devlake/plugins/zentao") - mockMeta.On("Name").Return("zentao").Maybe() - err := plugin.RegisterPlugin("zentao", mockMeta) - assert.Nil(t, err) - // Refresh Global Variables and set the sql mock - mockBasicRes(t) - - bs := &coreModels.BlueprintScope{ - ScopeId: "1", - } - /*bs2 := &coreModels.BlueprintScope{ - Id: "product/1", - }*/ - bpScopes := make([]*coreModels.BlueprintScope, 0) - bpScopes = append(bpScopes, bs) - - plan := make(coreModels.PipelinePlan, len(bpScopes)) - plan, scopes, err := makePipelinePlanV200(nil, plan, bpScopes, connection) - assert.Nil(t, err) - - expectPlan := coreModels.PipelinePlan{ - coreModels.PipelineStage{ - { - Plugin: "zentao", - Subtasks: []string{}, - Options: map[string]interface{}{ - "ConnectionId": uint64(1), - "projectId": int64(1), - }, - }, - }, - /*coreModels.PipelineStage{ - { - Plugin: "zentao", - Subtasks: []string{}, - Options: map[string]interface{}{ - "ConnectionId": uint64(1), - "productId": int64(1), - "projectId": int64(0), - }, - }, - },*/ - } - assert.Equal(t, expectPlan, plan) - expectScopes := make([]plugin.Scope, 0) - scopeTicket1 := &ticket.Board{ - DomainEntity: domainlayer.DomainEntity{ - Id: "zentao:ZentaoProject:1:1", - }, - Name: "test/testRepo", - Description: "", - Url: "", - CreatedDate: nil, - Type: `project`, - } - /*scopeTicket2 := &ticket.Board{ - DomainEntity: domainlayer.DomainEntity{ - Id: "zentao:ZentaoProduct:1:1", - }, - Name: "test/testRepo", - Description: "", - Url: "", - CreatedDate: nil, - Type: `product/normal`, - }*/ - - expectScopes = append(expectScopes, scopeTicket1) - assert.Equal(t, expectScopes, scopes) -} - -// mockBasicRes FIXME ... -func mockBasicRes(t *testing.T) { - /*testZentaoProduct := &models.ZentaoProduct{ - ConnectionId: 1, - Id: 1, - Name: "test/testRepo", - Type: `product/normal`, - ScopeConfigId: 0, - }*/ - testZentaoProject := &models.ZentaoProject{ - Scope: common.Scope{ - ConnectionId: 1, - ScopeConfigId: 0, - }, - Id: 1, - Name: "test/testRepo", - Type: `project`, - } - var testColumTypes = []dal.ColumnMeta{ - migrator.ColumnType{ - NameValue: sql.NullString{ - String: "abc", - Valid: true, - }, - }, - } - - mockRes := unithelper.DummyBasicRes(func(mockDal *mockdal.Dal) { - mockDal.On("First", mock.AnythingOfType("*models.ZentaoProject"), mock.Anything).Run(func(args mock.Arguments) { - dst := args.Get(0).(*models.ZentaoProject) - *dst = *testZentaoProject - }).Return(nil) - - /*mockDal.On("First", mock.AnythingOfType("*models.ZentaoProduct"), mock.Anything).Run(func(args mock.Arguments) { - dst := args.Get(0).(*models.ZentaoProduct) - *dst = *testZentaoProduct - }).Return(nil)*/ - - mockDal.On("First", mock.AnythingOfType("*models.ZentaoScopeConfig"), mock.Anything).Run(func(args mock.Arguments) { - panic("The empty scope should not call First() for ZentaoScopeConfig") - }).Return(nil) - mockDal.On("GetColumns", mock.AnythingOfType("models.ZentaoConnection"), mock.Anything).Run(nil).Return( - testColumTypes, nil) - mockDal.On("GetColumns", mock.AnythingOfType("models.ZentaoProject"), mock.Anything).Run(nil).Return( - testColumTypes, nil) - mockDal.On("GetColumns", mock.AnythingOfType("models.ZentaoScopeConfig"), mock.Anything).Run(nil).Return( - testColumTypes, nil) - }) - p := mockplugin.NewPluginMeta(t) - p.On("Name").Return("dummy").Maybe() - Init(mockRes, p) -} diff --git a/backend/plugins/zentao/api/blueprint_v200.go b/backend/plugins/zentao/api/blueprint_v200.go index d861050217e..1da9355d4c8 100644 --- a/backend/plugins/zentao/api/blueprint_v200.go +++ b/backend/plugins/zentao/api/blueprint_v200.go @@ -20,12 +20,12 @@ package api import ( "github.com/apache/incubator-devlake/core/errors" coreModels "github.com/apache/incubator-devlake/core/models" - "github.com/apache/incubator-devlake/core/models/domainlayer" "github.com/apache/incubator-devlake/core/models/domainlayer/didgen" "github.com/apache/incubator-devlake/core/models/domainlayer/ticket" "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/zentao/models" "github.com/apache/incubator-devlake/plugins/zentao/tasks" ) @@ -35,97 +35,70 @@ func MakeDataSourcePipelinePlanV200( connectionId uint64, bpScopes []*coreModels.BlueprintScope, ) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) { - // get the connection info for url - connection := &models.ZentaoConnection{} - err := connectionHelper.FirstById(connection, connectionId) + connection, err := dsHelper.ConnSrv.FindByPk(connectionId) if err != nil { return nil, nil, err } - - plan := make(coreModels.PipelinePlan, len(bpScopes)) - plan, scopes, err := makePipelinePlanV200(subtaskMetas, plan, bpScopes, connection) + scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes) if err != nil { return nil, nil, err } - - return plan, scopes, nil + plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, connection) + if err != nil { + return nil, nil, err + } + scopes, err := makeScopesV200(scopeDetails, connection) + return plan, scopes, err } func makePipelinePlanV200( subtaskMetas []plugin.SubTaskMeta, - plan coreModels.PipelinePlan, - bpScopes []*coreModels.BlueprintScope, + scopeDetails []*srvhelper.ScopeDetail[models.ZentaoProject, models.ZentaoScopeConfig], connection *models.ZentaoConnection, -) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) { - domainScopes := make([]plugin.Scope, 0) - for i, bpScope := range bpScopes { +) (coreModels.PipelinePlan, errors.Error) { + plan := make(coreModels.PipelinePlan, len(scopeDetails)) + for i, scopeDetail := range scopeDetails { stage := plan[i] if stage == nil { stage = coreModels.PipelineStage{} } - // construct task options - op := &tasks.ZentaoOptions{ - ConnectionId: connection.ID, - } - - var entities []string - project, scopeConfig, err := projectScopeHelper.DbHelper().GetScopeAndConfig(connection.ID, bpScope.ScopeId) + scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig + // construct task options for circleci + task, err := helper.MakePipelinePlanTask( + "zentao", + subtaskMetas, + scopeConfig.Entities, + tasks.ZentaoOptions{ + ConnectionId: connection.ID, + ProjectId: scope.Id, + }, + ) if err != nil { - return nil, nil, err + return nil, err } - op.ProjectId = project.Id - entities = scopeConfig.Entities + stage = append(stage, task) + plan[i] = stage + } - if utils.StringsContains(entities, plugin.DOMAIN_TYPE_TICKET) { - scopeTicket := &ticket.Board{ - DomainEntity: domainlayer.DomainEntity{ - Id: didgen.NewDomainIdGenerator(&models.ZentaoProject{}).Generate(connection.ID, project.Id), - }, - Name: project.Name, - Type: project.Type, - } - domainScopes = append(domainScopes, scopeTicket) - } - /*} else { - product, scopeConfig, err := productScopeHelper.DbHelper().GetScopeAndConfig(connection.ID, scopeId) - if err != nil { - return nil, nil, err - } - op.ProductId = product.Id - entities = scopeConfig.Entities + return plan, nil +} - if utils.StringsContains(entities, plugin.DOMAIN_TYPE_TICKET) { - scopeTicket := &ticket.Board{ - DomainEntity: domainlayer.DomainEntity{ - Id: didgen.NewDomainIdGenerator(&models.ZentaoProduct{}).Generate(connection.ID, product.Id), - }, - Name: product.Name, - Type: product.Type, - } - domainScopes = append(domainScopes, scopeTicket) - } - }*/ +func makeScopesV200( + scopeDetails []*srvhelper.ScopeDetail[models.ZentaoProject, models.ZentaoScopeConfig], + connection *models.ZentaoConnection, +) ([]plugin.Scope, errors.Error) { + scopes := make([]plugin.Scope, 0, len(scopeDetails)) - options, err := tasks.EncodeTaskOptions(op) - if err != nil { - return nil, nil, err - } + idgen := didgen.NewDomainIdGenerator(&models.ZentaoProject{}) + for _, scopeDetail := range scopeDetails { + scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig + id := idgen.Generate(connection.ID, scope.Id) - subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, entities) - if err != nil { - return nil, nil, err + if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_TICKET) { + scopes = append(scopes, ticket.NewBoard(id, scope.Name)) } - stage = append(stage, &coreModels.PipelineTask{ - Plugin: "zentao", - Subtasks: subtasks, - Options: options, - }) - if err != nil { - return nil, nil, err - } - - plan[i] = stage } - return plan, domainScopes, nil + + return scopes, nil } diff --git a/backend/plugins/zentao/api/connection.go b/backend/plugins/zentao/api/connection_api.go similarity index 81% rename from backend/plugins/zentao/api/connection.go rename to backend/plugins/zentao/api/connection_api.go index 48993d4dc8c..f3e17db97df 100644 --- a/backend/plugins/zentao/api/connection.go +++ b/backend/plugins/zentao/api/connection_api.go @@ -19,10 +19,11 @@ package api import ( "context" - "github.com/apache/incubator-devlake/core/runner" "net/http" "time" + "github.com/apache/incubator-devlake/core/runner" + "github.com/apache/incubator-devlake/server/api/shared" "github.com/apache/incubator-devlake/core/errors" @@ -107,8 +108,7 @@ func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/test [POST] func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.ZentaoConnection{} - err := connectionHelper.First(connection, input.Params) + connection, err := dsHelper.ConnApi.GetMergedConnection(input) if err != nil { return nil, errors.BadInput.Wrap(err, "find connection from db") } @@ -131,13 +131,7 @@ func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResource // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/zentao/connections [POST] func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - // update from request and save to database - connection := &models.ZentaoConnection{} - err := connectionHelper.Create(connection, input) - if err != nil { - return nil, err - } - return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: http.StatusOK}, nil + return dsHelper.ConnApi.Post(input) } // @Summary patch zentao connection @@ -149,17 +143,7 @@ func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/zentao/connections/{connectionId} [PATCH] func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - existedConnection := models.ZentaoConnection{} - if err := connectionHelper.First(&existedConnection, input.Params); err != nil { - return nil, err - } - if err := (&models.ZentaoConnection{}).MergeFromRequest(&existedConnection, input.Body); err != nil { - return nil, errors.Convert(err) - } - if err := connectionHelper.SaveWithCreateOrUpdate(&existedConnection); err != nil { - return nil, err - } - return &plugin.ApiResourceOutput{Body: existedConnection.Sanitize()}, nil + return dsHelper.ConnApi.Patch(input) } // @Summary delete a zentao connection @@ -171,13 +155,7 @@ func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/zentao/connections/{connectionId} [DELETE] func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - conn := &models.ZentaoConnection{} - output, err := connectionHelper.Delete(conn, input) - if err != nil { - return output, err - } - output.Body = conn.Sanitize() - return output, nil + return dsHelper.ConnApi.Delete(input) } // @Summary get all zentao connections @@ -188,15 +166,7 @@ func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/zentao/connections [GET] func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - var connections []models.ZentaoConnection - 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) } // @Summary get zentao connection detail @@ -207,7 +177,5 @@ func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {string} errcode.Error "Internal Error" // @Router /plugins/zentao/connections/{connectionId} [GET] func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.ZentaoConnection{} - err := connectionHelper.First(connection, input.Params) - return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err + return dsHelper.ConnApi.GetDetail(input) } diff --git a/backend/plugins/zentao/api/init.go b/backend/plugins/zentao/api/init.go index 1d14130ce83..e73abe3c144 100644 --- a/backend/plugins/zentao/api/init.go +++ b/backend/plugins/zentao/api/init.go @@ -19,52 +19,23 @@ package api import ( "github.com/apache/incubator-devlake/core/context" - "github.com/apache/incubator-devlake/core/log" "github.com/apache/incubator-devlake/core/plugin" "github.com/apache/incubator-devlake/helpers/pluginhelper/api" "github.com/apache/incubator-devlake/plugins/zentao/models" "github.com/go-playground/validator/v10" ) -type MixScopes struct { - ZentaoProduct *models.ZentaoProduct `json:"product"` - ZentaoProject *models.ZentaoProject `json:"project"` -} - -var logger log.Logger var vld *validator.Validate -var connectionHelper *api.ConnectionApiHelper -var projectScopeHelper *api.ScopeApiHelper[models.ZentaoConnection, models.ZentaoProject, models.ZentaoScopeConfig] -var dsHelper *api.DsHelper[models.ZentaoConnection, models.ZentaoProject, models.ZentaoScopeConfig] -var projectRemoteHelper *api.RemoteApiHelper[models.ZentaoConnection, models.ZentaoProject, models.ZentaoProject, api.BaseRemoteGroupResponse] var basicRes context.BasicRes -var scHelper *api.ScopeConfigHelper[models.ZentaoScopeConfig, *models.ZentaoScopeConfig] -func Init(br context.BasicRes, p plugin.PluginMeta) { +var dsHelper *api.DsHelper[models.ZentaoConnection, models.ZentaoProject, models.ZentaoScopeConfig] +var raProxy *api.DsRemoteApiProxyHelper[models.ZentaoConnection] +var raScopeList *api.DsRemoteApiScopeListHelper[models.ZentaoConnection, models.ZentaoProject, ZentaoRemotePagination] - basicRes = br - logger = basicRes.GetLogger() +func Init(br context.BasicRes, p plugin.PluginMeta) { vld = validator.New() - connectionHelper = api.NewConnectionHelper(basicRes, vld, p.Name()) - - projectParams := &api.ReflectionParameters{ - ScopeIdFieldName: "Id", - ScopeIdColumnName: "id", - RawScopeParamName: "ProjectId", - SearchScopeParamName: "name", - } - projectScopeHelper = api.NewScopeHelper[models.ZentaoConnection, models.ZentaoProject, models.ZentaoScopeConfig]( - basicRes, - vld, - connectionHelper, - api.NewScopeDatabaseHelperImpl[models.ZentaoConnection, models.ZentaoProject, models.ZentaoScopeConfig]( - basicRes, connectionHelper, projectParams), - projectParams, - nil, - ) + basicRes = br - projectRemoteHelper = api.NewRemoteHelper[models.ZentaoConnection, models.ZentaoProject, models.ZentaoProject, api.BaseRemoteGroupResponse](basicRes, vld, connectionHelper) - scHelper = api.NewScopeConfigHelper[models.ZentaoScopeConfig, *models.ZentaoScopeConfig](basicRes, vld, p.Name()) dsHelper = api.NewDataSourceHelper[ models.ZentaoConnection, models.ZentaoProject, models.ZentaoScopeConfig, ]( @@ -77,4 +48,6 @@ func Init(br context.BasicRes, p plugin.PluginMeta) { nil, nil, ) + raProxy = api.NewDsRemoteApiProxyHelper(dsHelper.ConnApi.ModelApiHelper) + raScopeList = api.NewDsRemoteApiScopeListHelper(raProxy, listZentaoRemoteScopes) } diff --git a/backend/plugins/zentao/api/proxy.go b/backend/plugins/zentao/api/proxy.go deleted file mode 100644 index 5ce50e94b6e..00000000000 --- a/backend/plugins/zentao/api/proxy.go +++ /dev/null @@ -1,58 +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/zentao/models" -) - -// Proxy is a proxy to Zentao API -// @Summary Proxy to Zentao API -// @Description Proxy to Zentao API -// @Tags plugins/zentao -// @Param connectionId path int true "connection ID" -// @Param path path string true "path to Zentao API" -// @Router /plugins/zentao/connections/{connectionId}/proxy/{path} [GET] -func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - connection := &models.ZentaoConnection{} - 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/zentao/api/remote.go b/backend/plugins/zentao/api/remote.go deleted file mode 100644 index 85b6b953401..00000000000 --- a/backend/plugins/zentao/api/remote.go +++ /dev/null @@ -1,114 +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" - "net/url" - - "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/zentao/models" -) - -type ProjectResponse struct { - Limit int `json:"limit"` - Page int `json:"page"` - Total int `json:"total"` - Values []models.ZentaoProject `json:"projects"` -} - -func getGroup(basicRes context.BasicRes, gid string, queryData *api.RemoteQueryData, connection models.ZentaoConnection) ([]api.BaseRemoteGroupResponse, errors.Error) { - return []api.BaseRemoteGroupResponse{ - /*{ - Id: `products`, - Name: `Products`, - },*/ - { - Id: `projects`, - Name: `Projects`, - }, - }, nil -} - -// RemoteScopes list all available scope for users -// @Summary list all available scope for users -// @Description list all available scope for users -// @Tags plugins/zentao -// @Accept application/json -// @Param connectionId path int false "connection ID" -// @Param groupId query string false "group ID" -// @Param pageToken query string false "page Token" -// @Success 200 {object} api.RemoteScopesOutput -// @Failure 400 {object} shared.ApiBody "Bad Request" -// @Failure 500 {object} shared.ApiBody "Internal Error" -// @Router /plugins/zentao/connections/{connectionId}/remote-scopes [GET] -func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - gid := input.Query.Get("groupId") - if gid == "" { - apiResourceOutput, err := projectRemoteHelper.GetScopesFromRemote(input, getGroup, nil) - if err != nil { - return apiResourceOutput, err - } - if _, ok := apiResourceOutput.Body.(*api.RemoteScopesOutput); ok { - apiResourceOutput.Body.(*api.RemoteScopesOutput).NextPageToken = "" - } - return apiResourceOutput, err - } else if gid == `projects` { - return projectRemoteHelper.GetScopesFromRemote(input, - nil, - func(basicRes context.BasicRes, gid string, queryData *api.RemoteQueryData, connection models.ZentaoConnection) ([]models.ZentaoProject, errors.Error) { - query := initialQuery(queryData) - // create api client - apiClient, err := api.NewApiClientFromConnection(gocontext.TODO(), basicRes, &connection) - if err != nil { - return nil, err - } - - query.Set("sort", "name") - // list projects part - res, err := apiClient.Get("/projects", query, nil) - if err != nil { - logger.Error(err, "call projects api") - return nil, err - } - - resBody := &ProjectResponse{} - err = api.UnmarshalResponse(res, resBody) - if err != nil { - logger.Error(err, "unmarshal projects response") - return nil, err - } - if resBody.Page < queryData.Page { - return nil, nil - } - return resBody.Values, nil - }) - } - return nil, nil -} - -func initialQuery(queryData *api.RemoteQueryData) url.Values { - query := url.Values{} - query.Set("page", fmt.Sprintf("%v", queryData.Page)) - query.Set("limit", fmt.Sprintf("%v", queryData.PerPage)) - return query -} diff --git a/backend/plugins/zentao/api/remote_api.go b/backend/plugins/zentao/api/remote_api.go new file mode 100644 index 00000000000..608c0e90285 --- /dev/null +++ b/backend/plugins/zentao/api/remote_api.go @@ -0,0 +1,116 @@ +/* +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/zentao/models" +) + +type ZentaoRemotePagination struct { + Limit int `json:"limit"` + Page int `json:"page"` +} + +type ZentaoRemoteProjects struct { + ZentaoRemotePagination + Total int `json:"total"` + Values []models.ZentaoProject `json:"projects"` +} + +func listZentaoRemoteScopes( + connection *models.ZentaoConnection, + apiClient plugin.ApiClient, + groupId string, + page ZentaoRemotePagination, +) ( + children []dsmodels.DsRemoteApiScopeListEntry[models.ZentaoProject], + nextPage *ZentaoRemotePagination, + err errors.Error, +) { + if page.Page == 0 { + page.Page = 1 + } + if page.Limit == 0 { + page.Limit = 20 + } + // list projects part + res, err := apiClient.Get("/projects", url.Values{ + "page": {fmt.Sprintf("%d", page.Page)}, + "limit": {fmt.Sprintf("%d", page.Limit)}, + }, nil) + if err != nil { + return + } + // parse response body + resBody := &ZentaoRemoteProjects{} + err = api.UnmarshalResponse(res, resBody) + if err != nil { + return + } + // cnvert to dsmodels.DsRemoteApiScopeListEntry + for _, p := range resBody.Values { + children = append(children, dsmodels.DsRemoteApiScopeListEntry[models.ZentaoProject]{ + Type: api.RAS_ENTRY_TYPE_SCOPE, + Id: fmt.Sprintf("%v", p.Id), + Name: p.Name, + FullName: p.Name, + Data: &p, + }) + } + // next page + if (resBody.Page-1)*resBody.Limit+len(resBody.Values) < resBody.Total { + nextPage = &ZentaoRemotePagination{ + Page: page.Page + 1, + Limit: page.Limit, + } + } + return +} + +// RemoteScopes list all available scope for users +// @Summary list all available scope for users +// @Description list all available scope for users +// @Tags plugins/zentao +// @Accept application/json +// @Param connectionId path int false "connection ID" +// @Param groupId query string false "group ID" +// @Param pageToken query string false "page Token" +// @Success 200 {object} api.RemoteScopesOutput +// @Failure 400 {object} shared.ApiBody "Bad Request" +// @Failure 500 {object} shared.ApiBody "Internal Error" +// @Router /plugins/zentao/connections/{connectionId}/remote-scopes [GET] +func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return raScopeList.GetAll(input) +} + +// @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/zentao +// @Router /plugins/zentao/connections/{connectionId}/proxy/{path} [GET] +func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return raProxy.Proxy(input) +} diff --git a/backend/plugins/zentao/api/project_scope.go b/backend/plugins/zentao/api/scope_api.go similarity index 78% rename from backend/plugins/zentao/api/project_scope.go rename to backend/plugins/zentao/api/scope_api.go index a82e95652d9..a2a18f4f071 100644 --- a/backend/plugins/zentao/api/project_scope.go +++ b/backend/plugins/zentao/api/scope_api.go @@ -24,29 +24,25 @@ import ( "github.com/apache/incubator-devlake/plugins/zentao/models" ) -type ProjectScopeRes struct { - models.ZentaoProject - api.ScopeResDoc[models.ZentaoScopeConfig] -} - -type ProjectScopeReq api.ScopeReq[models.ZentaoProject] +type PutScopesReqBody api.PutScopesReqBody[models.ZentaoProject] +type ScopeDetail api.ScopeDetail[models.ZentaoProject, models.ZentaoScopeConfig] -// PutProjectScope create or update zentao projects +// PutScopes create or update zentao projects // @Summary create or update zentao projects // @Description Create or update zentao projects // @Tags plugins/zentao // @Accept application/json // @Param connectionId path int true "connection ID" // @Param scope body ProjectScopeReq true "json" -// @Success 200 {object} []models.ZentaoProject +// @Success 200 {object} []ScopeDetails // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scopes [PUT] -func PutProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return projectScopeHelper.Put(input) +func PutScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeApi.PutMultiple(input) } -// UpdateProjectScope patch to zentao project +// PatchScope patch to zentao project // @Summary patch to zentao project // @Description patch to zentao project // @Tags plugins/zentao @@ -58,26 +54,26 @@ func PutProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scopes/{scopeId} [PATCH] -func UpdateProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return projectScopeHelper.Update(input) +func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeApi.Patch(input) } -// GetProjectScopeList get Gitlab projects +// GetScopes get Gitlab projects // @Summary get Gitlab projects // @Description get Gitlab projects // @Tags plugins/gitlab // @Param connectionId path int false "connection ID" // @Param searchTerm query string false "search term for scope name" // @Param blueprints query bool false "also return blueprints using these scopes as part of the payload" -// @Success 200 {object} []ProjectScopeRes +// @Success 200 {object} []ScopeDetails // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/gitlab/connections/{connectionId}/scopes [GET] -func GetProjectScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return projectScopeHelper.GetScopeList(input) +func GetScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeApi.GetPage(input) } -// GetProjectScope get one project +// GetScope get one project // @Summary get one project // @Description get one project // @Tags plugins/zentao @@ -87,8 +83,8 @@ func GetProjectScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOut // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scopes/{scopeId} [GET] -func GetProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return projectScopeHelper.GetScope(input) +func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeApi.GetScopeDetail(input) } // DeleteProjectScope delete plugin data associated with the scope and optionally the scope itself @@ -104,5 +100,5 @@ func GetProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scopes/{scopeId} [DELETE] func DeleteProjectScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return projectScopeHelper.Delete(input) + return dsHelper.ScopeApi.Delete(input) } diff --git a/backend/plugins/zentao/api/scope_config.go b/backend/plugins/zentao/api/scope_config_api.go similarity index 87% rename from backend/plugins/zentao/api/scope_config.go rename to backend/plugins/zentao/api/scope_config_api.go index eb11ac6d710..53b40e65738 100644 --- a/backend/plugins/zentao/api/scope_config.go +++ b/backend/plugins/zentao/api/scope_config_api.go @@ -22,7 +22,7 @@ import ( "github.com/apache/incubator-devlake/core/plugin" ) -// CreateScopeConfig create scope config for Zentao +// PostScopeConfig create scope config for Zentao // @Summary create scope config for Zentao // @Description create scope config for Zentao // @Tags plugins/zentao @@ -33,11 +33,11 @@ import ( // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scope-configs [POST] -func CreateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.Create(input) +func PostScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeConfigApi.Post(input) } -// UpdateScopeConfig update scope config for Zentao +// PatchScopeConfig update scope config for Zentao // @Summary update scope config for Zentao // @Description update scope config for Zentao // @Tags plugins/zentao @@ -49,8 +49,8 @@ func CreateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutpu // @Failure 400 {object} shared.ApiBody "Bad Request" // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scope-configs/{id} [PATCH] -func UpdateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.Update(input) +func PatchScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { + return dsHelper.ScopeConfigApi.Patch(input) } // GetScopeConfig return one scope config @@ -64,7 +64,7 @@ func UpdateScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutpu // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scope-configs/{id} [GET] func GetScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.Get(input) + return dsHelper.ScopeConfigApi.GetDetail(input) } // GetScopeConfigList return all scope configs @@ -79,7 +79,7 @@ func GetScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scope-configs [GET] func GetScopeConfigList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.List(input) + return dsHelper.ScopeConfigApi.GetAll(input) } // DeleteScopeConfig delete a scope config @@ -93,5 +93,5 @@ func GetScopeConfigList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutp // @Failure 500 {object} shared.ApiBody "Internal Error" // @Router /plugins/zentao/connections/{connectionId}/scope-configs/{id} [DELETE] func DeleteScopeConfig(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { - return scHelper.Delete(input) + return dsHelper.ScopeConfigApi.Delete(input) } diff --git a/backend/plugins/zentao/impl/impl.go b/backend/plugins/zentao/impl/impl.go index 891b3e645b1..fa367534653 100644 --- a/backend/plugins/zentao/impl/impl.go +++ b/backend/plugins/zentao/impl/impl.go @@ -256,20 +256,20 @@ func (p Zentao) ApiResources() map[string]map[string]plugin.ApiResourceHandler { "POST": api.TestExistingConnection, }, "connections/:connectionId/scopes": { - "PUT": api.PutProjectScope, - "GET": api.GetProjectScopeList, + "PUT": api.PutScopes, + "GET": api.GetScopes, }, "connections/:connectionId/scopes/:scopeId": { - "GET": api.GetProjectScope, - "PATCH": api.UpdateProjectScope, + "GET": api.GetScope, + "PATCH": api.PatchScope, "DELETE": api.DeleteProjectScope, }, "connections/:connectionId/scope-configs": { - "POST": api.CreateScopeConfig, + "POST": api.PostScopeConfig, "GET": api.GetScopeConfigList, }, "connections/:connectionId/scope-configs/:id": { - "PATCH": api.UpdateScopeConfig, + "PATCH": api.PatchScopeConfig, "GET": api.GetScopeConfig, "DELETE": api.DeleteScopeConfig, },