backend/helpers/pluginhelper/api/remote_api_helper.go (275 lines of code) (raw):

/* 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 ( "encoding/base64" "encoding/json" "fmt" "net/http" "strconv" coreContext "github.com/apache/incubator-devlake/core/context" "github.com/apache/incubator-devlake/core/errors" "github.com/apache/incubator-devlake/core/plugin" "github.com/go-playground/validator/v10" ) type RemoteScopesChild struct { Type string `json:"type"` ParentId *string `json:"parentId"` Id string `json:"id"` Name string `json:"name"` FullName string `json:"fullName"` Data interface{} `json:"data"` } type RemoteQueryData struct { Page int `json:"page"` PerPage int `json:"per_page"` CustomInfo string `json:"custom"` Tag string `json:"tag"` Search []string } type FirstPageTokenOutput struct { PageToken string `json:"pageToken"` } type RemoteScopesOutput struct { Children []RemoteScopesChild `json:"children"` NextPageToken string `json:"nextPageToken"` } type SearchRemoteScopesOutput struct { Children []RemoteScopesChild `json:"children"` Page int `json:"page"` PageSize int `json:"pageSize"` } // RemoteApiHelper is used to write the CURD of connection type RemoteApiHelper[Conn plugin.ApiConnection, Scope plugin.ToolLayerScope, ApiScope plugin.ApiScope, Group plugin.ApiGroup] struct { basicRes coreContext.BasicRes validator *validator.Validate connHelper *ConnectionApiHelper } // NewRemoteHelper creates a ScopeHelper for connection management func NewRemoteHelper[Conn plugin.ApiConnection, Scope plugin.ToolLayerScope, ApiScope plugin.ApiScope, Group plugin.ApiGroup]( basicRes coreContext.BasicRes, vld *validator.Validate, connHelper *ConnectionApiHelper, ) *RemoteApiHelper[Conn, Scope, ApiScope, Group] { if vld == nil { vld = validator.New() } if connHelper == nil { return nil } return &RemoteApiHelper[Conn, Scope, ApiScope, Group]{ basicRes: basicRes, validator: vld, connHelper: connHelper, } } type NoRemoteGroupResponse struct { } func (NoRemoteGroupResponse) GroupId() string { return "" } func (NoRemoteGroupResponse) GroupName() string { return "" } type BaseRemoteGroupResponse struct { Id string Name string } func (g BaseRemoteGroupResponse) GroupId() string { return g.Id } func (g BaseRemoteGroupResponse) GroupName() string { return g.Name } const remoteScopesPerPage int = 100 const TypeProject string = "scope" const TypeGroup string = "group" // PrepareFirstPageToken prepares the first page token func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) PrepareFirstPageToken(customInfo string) (*plugin.ApiResourceOutput, errors.Error) { outputBody := &FirstPageTokenOutput{} pageToken, err := getPageTokenFromPageData(&RemoteQueryData{ Page: 1, PerPage: remoteScopesPerPage, CustomInfo: customInfo, Tag: "group", }) if err != nil { return nil, err } outputBody.PageToken = pageToken return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil } // GetScopesFromRemote gets the scopes from api func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) GetScopesFromRemote(input *plugin.ApiResourceInput, getGroup func(basicRes coreContext.BasicRes, gid string, queryData *RemoteQueryData, connection Conn) ([]Group, errors.Error), getScope func(basicRes coreContext.BasicRes, gid string, queryData *RemoteQueryData, connection Conn) ([]ApiScope, errors.Error), ) (*plugin.ApiResourceOutput, errors.Error) { connectionId, err := errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64)) if err != nil || connectionId == 0 { return nil, errors.BadInput.New("invalid connectionId") } var connection Conn err = r.connHelper.First(&connection, input.Params) if err != nil { return nil, err } groupId, ok := input.Query["groupId"] if !ok || len(groupId) == 0 { groupId = []string{""} } pageToken, ok := input.Query["pageToken"] if !ok || len(pageToken) == 0 { pageToken = []string{""} } // get gid and pageData gid := groupId[0] queryData, err := getPageDataFromPageToken(pageToken[0]) if err != nil { return nil, errors.BadInput.New("failed to get page token") } outputBody := &RemoteScopesOutput{} // list groups part if queryData.Tag == TypeGroup { var resBody []Group if getGroup != nil { resBody, err = getGroup(r.basicRes, gid, queryData, connection) } if err != nil { return nil, err } // if len(resBody) == 0, will skip the following steps, this will happen in some plugins which don't have group // append group to output for _, group := range resBody { child := RemoteScopesChild{ Type: TypeGroup, Id: group.GroupId(), Name: group.GroupName(), // don't need to save group into data Data: nil, } child.ParentId = &gid if *child.ParentId == "" { child.ParentId = nil } outputBody.Children = append(outputBody.Children, child) } // check groups count if len(resBody) < queryData.PerPage { queryData.Tag = TypeProject queryData.Page = 1 queryData.PerPage = queryData.PerPage - len(resBody) } } // list projects part if queryData.Tag == TypeProject && getScope != nil { var resBody []ApiScope resBody, err = getScope(r.basicRes, gid, queryData, connection) if err != nil { return nil, err } // append project to output for _, project := range resBody { scope := project.ConvertApiScope() child := RemoteScopesChild{ Type: TypeProject, Id: scope.ScopeId(), Name: scope.ScopeName(), FullName: scope.ScopeFullName(), Data: &scope, } child.ParentId = &gid if *child.ParentId == "" { child.ParentId = nil } outputBody.Children = append(outputBody.Children, child) } // check project count if len(resBody) < queryData.PerPage { queryData = nil } } // get the next page token outputBody.NextPageToken = "" if queryData != nil { queryData.Page += 1 outputBody.NextPageToken, err = getPageTokenFromPageData(queryData) if err != nil { return nil, err } } return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil } func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) SearchRemoteScopes(input *plugin.ApiResourceInput, searchScope func(basicRes coreContext.BasicRes, queryData *RemoteQueryData, connection Conn) ([]ApiScope, errors.Error), ) (*plugin.ApiResourceOutput, errors.Error) { connectionId, err := errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64)) if err != nil || connectionId == 0 { return nil, errors.BadInput.New("invalid connectionId") } var connection Conn err = r.connHelper.First(&connection, input.Params) if err != nil { return nil, err } search, ok := input.Query["search"] if !ok || len(search) == 0 { search = []string{""} } var p int var err1 error page, ok := input.Query["page"] if !ok || len(page) == 0 { p = 1 } else { p, err1 = strconv.Atoi(page[0]) if err != nil { return nil, errors.BadInput.Wrap(err1, fmt.Sprintf("failed to Atoi page:%s", page[0])) } } var ps int pageSize, ok := input.Query["pageSize"] if !ok || len(pageSize) == 0 { ps = remoteScopesPerPage } else { ps, err1 = strconv.Atoi(pageSize[0]) if err1 != nil { return nil, errors.BadInput.Wrap(err1, fmt.Sprintf("failed to Atoi pageSize:%s", pageSize[0])) } } queryData := &RemoteQueryData{ Page: p, PerPage: ps, Search: search, } var resBody []ApiScope resBody, err = searchScope(r.basicRes, queryData, connection) if err != nil { return nil, err } outputBody := &SearchRemoteScopesOutput{} // append project to output for _, project := range resBody { scope := project.ConvertApiScope() child := RemoteScopesChild{ Type: TypeProject, Id: scope.ScopeId(), ParentId: nil, Name: scope.ScopeName(), FullName: scope.ScopeFullName(), Data: scope, } outputBody.Children = append(outputBody.Children, child) } outputBody.Page = p outputBody.PageSize = ps return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil } func getPageTokenFromPageData(pageData *RemoteQueryData) (string, errors.Error) { // Marshal json pageTokenDecode, err := json.Marshal(pageData) if err != nil { return "", errors.Default.Wrap(err, fmt.Sprintf("Marshal pageToken failed %+v", pageData)) } // Encode pageToken Base64 return base64.StdEncoding.EncodeToString(pageTokenDecode), nil } func getPageDataFromPageToken(pageToken string) (*RemoteQueryData, errors.Error) { if pageToken == "" { return &RemoteQueryData{ Page: 1, PerPage: remoteScopesPerPage, Tag: "group", }, nil } // Decode pageToken Base64 pageTokenDecode, err := base64.StdEncoding.DecodeString(pageToken) if err != nil { return nil, errors.Default.Wrap(err, fmt.Sprintf("decode pageToken failed %s", pageToken)) } // Unmarshal json pt := &RemoteQueryData{} err = json.Unmarshal(pageTokenDecode, pt) if err != nil { return nil, errors.Default.Wrap(err, fmt.Sprintf("json Unmarshal pageTokenDecode failed %s", pageTokenDecode)) } return pt, nil }