From 435e1b2eb4a6b73b493ae1b637087c754a25774b Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 8 Jul 2024 16:05:20 +0100 Subject: [PATCH 01/28] feat: add Jira component --- application/jira/v0/assets/jira.svg | 1 + application/jira/v0/board.go | 70 +++++++++ application/jira/v0/client.go | 41 ++++++ application/jira/v0/config/definition.json | 19 +++ application/jira/v0/config/setup.json | 25 ++++ application/jira/v0/config/tasks.json | 105 ++++++++++++++ application/jira/v0/main.go | 158 +++++++++++++++++++++ go.mod | 4 + go.sum | 14 ++ store/store.go | 3 +- 10 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 application/jira/v0/assets/jira.svg create mode 100644 application/jira/v0/board.go create mode 100644 application/jira/v0/client.go create mode 100644 application/jira/v0/config/definition.json create mode 100644 application/jira/v0/config/setup.json create mode 100644 application/jira/v0/config/tasks.json create mode 100644 application/jira/v0/main.go diff --git a/application/jira/v0/assets/jira.svg b/application/jira/v0/assets/jira.svg new file mode 100644 index 00000000..d1eeed52 --- /dev/null +++ b/application/jira/v0/assets/jira.svg @@ -0,0 +1 @@ + diff --git a/application/jira/v0/board.go b/application/jira/v0/board.go new file mode 100644 index 00000000..c84fb6ce --- /dev/null +++ b/application/jira/v0/board.go @@ -0,0 +1,70 @@ +package jira + +import ( + "context" + _ "embed" + + jira "github.com/andygrunwald/go-jira" + "github.com/instill-ai/component/base" + "google.golang.org/protobuf/types/known/structpb" +) + +type Board struct { + ID int `json:"id"` + Name string `json:"name"` + Self string `json:"self"` + BoardType string `json:"type"` +} + +type GetBoardsInput struct { + ProjectKeyOrID string `json:"project_key_or_id,omitempty"` + BoardType string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + StartAt int `json:"start_at,omitempty"` + MaxResults int `json:"max_results,omitempty"` +} + +type GetBoardsOutput struct { + Boards []Board `json:"boards"` + StartAt int `json:"start_at"` + MaxResults int `json:"max_results"` + IsLast bool `json:"is_last"` +} + +func (jiraClient *Client) extractBoard(board *jira.Board) *Board { + return &Board{ + ID: board.ID, + Name: board.Name, + Self: board.Self, + BoardType: board.Type, + } +} +func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { + var input GetBoardsInput + if err := base.ConvertFromStructpb(props, &input); err != nil { + return nil, err + } + + opt := jira.BoardListOptions{ + SearchOptions: jira.SearchOptions{ + StartAt: input.StartAt, + MaxResults: input.MaxResults, + }, + BoardType: input.BoardType, + Name: input.Name, + ProjectKeyOrID: input.ProjectKeyOrID, + } + + boards, _, err := jiraClient.Client.Board.GetAllBoardsWithContext(ctx, &opt) + if err != nil { + return nil, err + } + var output GetBoardsOutput + for _, board := range boards.Values { + output.Boards = append(output.Boards, *jiraClient.extractBoard(&board)) + } + output.StartAt = boards.StartAt + output.MaxResults = boards.MaxResults + output.IsLast = boards.IsLast + return base.ConvertToStructpb(output) +} diff --git a/application/jira/v0/client.go b/application/jira/v0/client.go new file mode 100644 index 00000000..36ac17f7 --- /dev/null +++ b/application/jira/v0/client.go @@ -0,0 +1,41 @@ +package jira + +import ( + "context" + "fmt" + + "github.com/andygrunwald/go-jira" + "github.com/instill-ai/x/errmsg" + "google.golang.org/protobuf/types/known/structpb" +) + +type Client struct { + *jira.Client +} + +func newClient(_ context.Context, setup *structpb.Struct) (*Client, error) { + token := getToken(setup) + + if token == "" { + return nil, errmsg.AddMessage( + fmt.Errorf("token not provided"), + "token not provided", + ) + } + + authTransport := jira.BearerAuthTransport{ + Token: token, + } + jiraClient, err := jira.NewClient(authTransport.Client(), baseURL) + if err != nil { + return nil, err + } + client := &Client{ + jiraClient, + } + return client, nil +} + +func getToken(setup *structpb.Struct) string { + return setup.GetFields()["token"].GetStringValue() +} diff --git a/application/jira/v0/config/definition.json b/application/jira/v0/config/definition.json new file mode 100644 index 00000000..e48af13f --- /dev/null +++ b/application/jira/v0/config/definition.json @@ -0,0 +1,19 @@ +{ + "availableTasks": [ + "TASK_LIST_BOARDS" + ], + "custom": false, + "documentationUrl": "https://www.instill.tech/docs/component/application/jira", + "icon": "assets/Jira.svg", + "iconUrl": "", + "id": "jira", + "public": true, + "title": "Jira", + "description": "Do anything available on Jira", + "tombstone": false, + "type": "COMPONENT_TYPE_APPLICATION", + "uid": "3b27f50d-a754-4b9d-8141-95aaec647cc5", + "version": "0.1.0", + "sourceUrl": "https://github.com/instill-ai/component/blob/main/application/jira/v0", + "releaseStage": "RELEASE_STAGE_ALPHA" +} diff --git a/application/jira/v0/config/setup.json b/application/jira/v0/config/setup.json new file mode 100644 index 00000000..3062e509 --- /dev/null +++ b/application/jira/v0/config/setup.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "token": { + "description": "Fill in your Jira API token. You can generate one from your Jira account \"settings > security > API tokens\".", + "instillUpstreamTypes": [ + "reference" + ], + "instillAcceptFormats": [ + "string" + ], + "instillSecret": true, + "instillUIOrder": 0, + "title": "Token", + "type": "string" + } + }, + "required": [ + "token" + ], + "instillEditOnNodeFields": [], + "title": "Jira Connection", + "type": "object" +} diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json new file mode 100644 index 00000000..43293e28 --- /dev/null +++ b/application/jira/v0/config/tasks.json @@ -0,0 +1,105 @@ +{ + "$defs": {}, + "TASK_LIST_BOARDS": { + "instillShortDescription": "List all boards in Jira", + "input": { + "description": "List all boards in Jira", + "instillUIOrder": 0, + "instillEditOnNodeFields": [ + "owner", + "repository" + ], + "properties": { + "$ref": "#/$defs/repository_info", + "state": { + "default": "open", + "title": "State", + "description": "State of the PRs, including open, closed, all. Default is open", + "enum": [ + "open", + "closed", + "all" + ], + "instillFormat": "string", + "instillAcceptFormats": [ + "string" + ], + "instillUpstreamTypes": [ + "value", + "reference" + ], + "instillUIOrder": 10, + "type": "string" + }, + "sort": { + "default": "created", + "title": "Sort", + "description": "Sort the PRs by created, updated, popularity, or long-running. Default is created", + "enum": [ + "created", + "updated", + "popularity", + "long-running" + ], + "instillFormat": "string", + "instillAcceptFormats": [ + "string" + ], + "instillUpstreamTypes": [ + "value", + "reference" + ], + "instillUIOrder": 11, + "type": "string" + }, + "direction": { + "default": "desc", + "title": "Direction", + "description": "Direction of the sort, including asc or desc. Default is asc", + "enum": [ + "asc", + "desc" + ], + "instillFormat": "string", + "instillAcceptFormats": [ + "string" + ], + "instillUpstreamTypes": [ + "value", + "reference" + ], + "instillUIOrder": 12, + "type": "string" + } + }, + "required": [ + "owner", + "repository" + ], + "title": "Input", + "type": "object" + }, + "output": { + "description": "All PRs in GitHub repository", + "instillUIOrder": 0, + "properties": { + "pull_requests": { + "description": "An array of PRs", + "instillUIOrder": 1, + "title": "Pull Requests", + "type": "array", + "items": { + "$ref": "#/$defs/pull_request", + "required": [], + "description": "A pull request in GitHub" + } + } + }, + "required": [ + "pull_requests" + ], + "title": "Output", + "type": "object" + } + } +} diff --git a/application/jira/v0/main.go b/application/jira/v0/main.go new file mode 100644 index 00000000..e05b6eda --- /dev/null +++ b/application/jira/v0/main.go @@ -0,0 +1,158 @@ +//go:generate compogen readme ./config ./README.mdx +package jira + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "sync" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/instill-ai/component/base" + "github.com/instill-ai/x/errmsg" +) + +const ( + baseURL = "https://jira.atlassian.com" + taskListBoards = "TASK_LIST_BOARDS" +) + +var ( + //go:embed config/definition.json + definitionJSON []byte + //go:embed config/setup.json + setupJSON []byte + //go:embed config/tasks.json + tasksJSON []byte + + once sync.Once + comp *component +) + +type component struct { + base.Component +} + +type execution struct { + base.ComponentExecution + execute func(context.Context, *structpb.Struct) (*structpb.Struct, error) + client Client +} + +// Init returns an implementation of IConnector that interacts with Slack. +func Init(bc base.Component) *component { + once.Do(func() { + comp = &component{Component: bc} + err := comp.LoadDefinition(definitionJSON, setupJSON, tasksJSON, nil) + if err != nil { + panic(err) + } + }) + + return comp +} + +func (c *component) CreateExecution(sysVars map[string]any, setup *structpb.Struct, task string) (*base.ExecutionWrapper, error) { + ctx := context.Background() + jiraClient, err := newClient(ctx, setup) + if err != nil { + return nil, err + } + e := &execution{ + ComponentExecution: base.ComponentExecution{Component: c, SystemVariables: sysVars, Setup: setup, Task: task}, + client: *jiraClient, + } + + switch task { + case taskListBoards: + e.execute = e.client.listBoardsTask + default: + return nil, errmsg.AddMessage( + fmt.Errorf("not supported task: %s", task), + fmt.Sprintf("%s task is not supported.", task), + ) + } + + return &base.ExecutionWrapper{Execution: e}, nil +} + +func (e *execution) fillInDefaultValues(input *structpb.Struct) (*structpb.Struct, error) { + task := e.Task + taskSpec, ok := e.Component.GetTaskInputSchemas()[task] + if !ok { + return nil, errmsg.AddMessage( + fmt.Errorf("task %s not found", task), + fmt.Sprintf("Task %s not found", task), + ) + } + // Unmarshal the taskSpec into a map + var taskSpecMap map[string]interface{} + err := json.Unmarshal([]byte(taskSpec), &taskSpecMap) + if err != nil { + return nil, errmsg.AddMessage( + err, + "Failed to unmarshal input", + ) + } + inputMap := taskSpecMap["properties"].(map[string]interface{}) + // iterate over the inputMap and fill in the default values + for key, value := range inputMap { + valueMap, ok := value.(map[string]interface{}) + if !ok { + // fmt.Println("value is not a map", key) + continue + } + if _, ok := valueMap["default"]; !ok { + // fmt.Println("default value not found", key) + continue + } + if _, ok := input.GetFields()[key]; ok { + // fmt.Println("key already exists", key) + continue + } + defaultValue := valueMap["default"] + typeValue := valueMap["type"] + switch typeValue { + case "string": + input.GetFields()[key] = &structpb.Value{ + Kind: &structpb.Value_StringValue{ + StringValue: fmt.Sprintf("%v", defaultValue), + }, + } + case "integer", "number": + input.GetFields()[key] = &structpb.Value{ + Kind: &structpb.Value_NumberValue{ + NumberValue: defaultValue.(float64), + }, + } + case "boolean": + input.GetFields()[key] = &structpb.Value{ + Kind: &structpb.Value_BoolValue{ + BoolValue: defaultValue.(bool), + }, + } + } + } + return input, nil +} + +func (e *execution) Execute(ctx context.Context, inputs []*structpb.Struct) ([]*structpb.Struct, error) { + outputs := make([]*structpb.Struct, len(inputs)) + + for i, input := range inputs { + input, err := e.fillInDefaultValues(input) + if err != nil { + return nil, err + } + output, err := e.execute(ctx, input) + if err != nil { + return nil, err + } + + outputs[i] = output + } + + return outputs, nil +} diff --git a/go.mod b/go.mod index 92c90f86..1eb5e3fe 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( code.sajari.com/docconv v1.3.8 github.com/JohannesKaufmann/html-to-markdown v1.5.0 github.com/PuerkitoBio/goquery v1.9.1 + github.com/andygrunwald/go-jira v1.16.0 github.com/cohere-ai/cohere-go/v2 v2.8.5 github.com/emersion/go-imap/v2 v2.0.0-beta.3 github.com/emersion/go-message v0.18.1 @@ -68,12 +69,14 @@ require ( github.com/dlclark/regexp2 v1.10.0 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/fatih/set v0.2.1 // indirect + github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -108,6 +111,7 @@ require ( github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/temoto/robotstxt v1.1.2 // indirect + github.com/trivago/tgo v1.0.7 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 // indirect gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 // indirect diff --git a/go.sum b/go.sum index 6b225ac4..148036d0 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxB github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/andygrunwald/go-jira v1.16.0 h1:PU7C7Fkk5L96JvPc6vDVIrd99vdPnYudHu4ju2c2ikQ= +github.com/andygrunwald/go-jira v1.16.0/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU= github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0= github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= @@ -88,6 +90,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= @@ -121,6 +125,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gojuno/minimock/v3 v3.3.6 h1:tZQQaDgKSxsKiVia9vt6zZ/qsKNGBw2D0ubHQPr+mHc= github.com/gojuno/minimock/v3 v3.3.6/go.mod h1:kjvubEBVT8aUQ9e+g8x/hPfAhiOoqW7WinzzJgzr4ws= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -151,9 +157,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -161,6 +169,8 @@ github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwM github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= @@ -296,6 +306,8 @@ github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fx github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= github.com/tmc/langchaingo v0.1.10 h1:+cssnyaY1avZwzdDFvJYlVUsch9oFRgoqw3Avk5Zig4= github.com/tmc/langchaingo v0.1.10/go.mod h1:lPKUIu8ZGI7RAksRFtKbgtS2v3LL0j7LcccHPCvgNfY= +github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= +github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -403,6 +415,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -415,6 +428,7 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/store/store.go b/store/store.go index 49885e32..fd68ebff 100644 --- a/store/store.go +++ b/store/store.go @@ -19,6 +19,7 @@ import ( "github.com/instill-ai/component/application/email/v0" "github.com/instill-ai/component/application/github/v0" "github.com/instill-ai/component/application/googlesearch/v0" + "github.com/instill-ai/component/application/jira/v0" "github.com/instill-ai/component/application/numbers/v0" "github.com/instill-ai/component/application/restapi/v0" @@ -132,7 +133,7 @@ func Init( compStore.Import(website.Init(baseComp)) compStore.Import(slack.Init(baseComp)) compStore.Import(email.Init(baseComp)) - + compStore.Import(jira.Init(baseComp)) }) return compStore } From a739ba5fc083114442777d528d20cc2844e50e20 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Tue, 9 Jul 2024 13:16:42 +0100 Subject: [PATCH 02/28] feat: add list boards --- application/jira/v0/board.go | 70 ---------- application/jira/v0/boards.go | 87 ++++++++++++ application/jira/v0/client.go | 99 ++++++++++++-- application/jira/v0/config/setup.json | 36 ++++- application/jira/v0/config/tasks.json | 185 +++++++++++++++++++------- application/jira/v0/main.go | 9 +- go.mod | 4 - go.sum | 14 -- 8 files changed, 348 insertions(+), 156 deletions(-) delete mode 100644 application/jira/v0/board.go create mode 100644 application/jira/v0/boards.go diff --git a/application/jira/v0/board.go b/application/jira/v0/board.go deleted file mode 100644 index c84fb6ce..00000000 --- a/application/jira/v0/board.go +++ /dev/null @@ -1,70 +0,0 @@ -package jira - -import ( - "context" - _ "embed" - - jira "github.com/andygrunwald/go-jira" - "github.com/instill-ai/component/base" - "google.golang.org/protobuf/types/known/structpb" -) - -type Board struct { - ID int `json:"id"` - Name string `json:"name"` - Self string `json:"self"` - BoardType string `json:"type"` -} - -type GetBoardsInput struct { - ProjectKeyOrID string `json:"project_key_or_id,omitempty"` - BoardType string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - StartAt int `json:"start_at,omitempty"` - MaxResults int `json:"max_results,omitempty"` -} - -type GetBoardsOutput struct { - Boards []Board `json:"boards"` - StartAt int `json:"start_at"` - MaxResults int `json:"max_results"` - IsLast bool `json:"is_last"` -} - -func (jiraClient *Client) extractBoard(board *jira.Board) *Board { - return &Board{ - ID: board.ID, - Name: board.Name, - Self: board.Self, - BoardType: board.Type, - } -} -func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { - var input GetBoardsInput - if err := base.ConvertFromStructpb(props, &input); err != nil { - return nil, err - } - - opt := jira.BoardListOptions{ - SearchOptions: jira.SearchOptions{ - StartAt: input.StartAt, - MaxResults: input.MaxResults, - }, - BoardType: input.BoardType, - Name: input.Name, - ProjectKeyOrID: input.ProjectKeyOrID, - } - - boards, _, err := jiraClient.Client.Board.GetAllBoardsWithContext(ctx, &opt) - if err != nil { - return nil, err - } - var output GetBoardsOutput - for _, board := range boards.Values { - output.Boards = append(output.Boards, *jiraClient.extractBoard(&board)) - } - output.StartAt = boards.StartAt - output.MaxResults = boards.MaxResults - output.IsLast = boards.IsLast - return base.ConvertToStructpb(output) -} diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go new file mode 100644 index 00000000..e767e200 --- /dev/null +++ b/application/jira/v0/boards.go @@ -0,0 +1,87 @@ +package jira + +import ( + "context" + _ "embed" + "fmt" + + "github.com/instill-ai/component/base" + "github.com/instill-ai/x/errmsg" + "google.golang.org/protobuf/types/known/structpb" +) + +type Board struct { + ID int `json:"id"` + Name string `json:"name"` + Self string `json:"self"` + BoardType string `json:"type"` +} + +type GetBoardsInput struct { + ProjectKeyOrID string `json:"project_key_or_id,omitempty" struct:"projectKeyOrID"` + BoardType string `json:"board_type,omitempty" struct:"boardType"` + Name string `json:"name,omitempty" struct:"name"` + StartAt int `json:"start_at,omitempty" struct:"startAt"` + MaxResults int `json:"max_results,omitempty" struct:"maxResults"` +} +type GetBoardsResp struct { + Values []Board `json:"values"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` + Total int `json:"total"` + IsLast bool `json:"isLast"` +} + +type GetBoardsOutput struct { + Boards []Board `json:"boards"` + StartAt int `json:"start_at"` + MaxResults int `json:"max_results"` + Total int `json:"total"` + IsLast bool `json:"is_last"` +} + +func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { + var opt GetBoardsInput + if err := base.ConvertFromStructpb(props, &opt); err != nil { + return nil, err + } + + boards, err := jiraClient.listBoards(ctx, &opt) + if err != nil { + return nil, err + } + var output GetBoardsOutput + output.Boards = append(output.Boards, boards.Values...) + if output.Boards == nil { + output.Boards = []Board{} + } + output.StartAt = boards.StartAt + output.MaxResults = boards.MaxResults + output.IsLast = boards.IsLast + output.Total = boards.Total + return base.ConvertToStructpb(output) +} + +func (jiraClient *Client) listBoards(_ context.Context, opt *GetBoardsInput) (*GetBoardsResp, error) { + apiEndpoint := "rest/agile/1.0/board" + + req := jiraClient.Client.R().SetResult(&GetBoardsResp{}) + err := addQueryOptions(req, *opt) + if err != nil { + return nil, err + } + resp, err := req.Get(apiEndpoint) + if resp.StatusCode() == 404 { + return nil, fmt.Errorf( + err.Error(), + errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", + ) + } + if err != nil { + return nil, fmt.Errorf( + err.Error(), errmsg.Message(err), + ) + } + boards := resp.Result().(*GetBoardsResp) + return boards, err +} diff --git a/application/jira/v0/client.go b/application/jira/v0/client.go index 36ac17f7..cc32f970 100644 --- a/application/jira/v0/client.go +++ b/application/jira/v0/client.go @@ -3,39 +3,118 @@ package jira import ( "context" "fmt" + "reflect" - "github.com/andygrunwald/go-jira" + "github.com/go-resty/resty/v2" + "github.com/instill-ai/component/base" + "github.com/instill-ai/component/internal/util/httpclient" "github.com/instill-ai/x/errmsg" "google.golang.org/protobuf/types/known/structpb" ) type Client struct { - *jira.Client + *httpclient.Client + APIBaseURL string `json:"api_base_url"` + Domain string `json:"domain"` + CloudID string `json:"cloud_id"` +} + +type CloudID struct { + ID string `json:"cloudId"` +} + +type AuthConfig struct { + Email string `json:"email"` + Token string `json:"token"` + BaseURL string `json:"base_url"` } func newClient(_ context.Context, setup *structpb.Struct) (*Client, error) { - token := getToken(setup) + var authConfig AuthConfig + if err := base.ConvertFromStructpb(setup, &authConfig); err != nil { + return nil, err + } + email := authConfig.Email + token := authConfig.Token + baseURL := authConfig.BaseURL if token == "" { return nil, errmsg.AddMessage( fmt.Errorf("token not provided"), "token not provided", ) } - - authTransport := jira.BearerAuthTransport{ - Token: token, + if email == "" { + return nil, errmsg.AddMessage( + fmt.Errorf("email not provided"), + "email not provided", + ) } - jiraClient, err := jira.NewClient(authTransport.Client(), baseURL) + cloudID, err := getCloudID(baseURL) if err != nil { return nil, err } + + jiraClient := httpclient.New( + "Jira-Client", + baseURL, + httpclient.WithEndUserError(new(errBody)), + ) + jiraClient.SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json") + jiraClient.SetBasicAuth(email, token) client := &Client{ - jiraClient, + Client: jiraClient, + APIBaseURL: apiBaseURL, + Domain: baseURL, + CloudID: cloudID, } return client, nil } -func getToken(setup *structpb.Struct) string { - return setup.GetFields()["token"].GetStringValue() +func getCloudID(baseURL string) (string, error) { + client := httpclient.New("Get-Domain-ID", baseURL, httpclient.WithEndUserError(new(errBody))) + resp := CloudID{} + req := client.R().SetResult(&resp) + // See https://developer.atlassian.com/cloud/jira/software/rest/intro/#base-url-differences + if _, err := req.Get("_edge/tenant_info"); err != nil { + return "", err + } + return resp.ID, nil +} + +type errBody struct { + Msg string `json:"message"` + Status int `json:"status"` +} + +func (e errBody) Message() string { + return fmt.Sprintf("%d %s", e.Status, e.Msg) +} + +func addQueryOptions(req *resty.Request, opt interface{}) error { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return nil + } + typeOfS := v.Type() + for i := 0; i < v.NumField(); i++ { + stringVal, ok := v.Field(i).Interface().(string) + if !ok { + intVal, ok := v.Field(i).Interface().(int) + if !ok { + continue + } + stringVal = fmt.Sprintf("%d", intVal) + } + if stringVal == "" { + continue + } + paramName := typeOfS.Field(i).Tag.Get("struct") + if paramName == "" { + paramName = typeOfS.Field(i).Name + } + req.SetQueryParam(paramName, stringVal) + } + return nil } diff --git a/application/jira/v0/config/setup.json b/application/jira/v0/config/setup.json index 3062e509..1abd7ea5 100644 --- a/application/jira/v0/config/setup.json +++ b/application/jira/v0/config/setup.json @@ -14,12 +14,44 @@ "instillUIOrder": 0, "title": "Token", "type": "string" + }, + "email": { + "description": "Fill in your Jira email address.", + "instillUpstreamTypes": [ + "value", + "reference" + ], + "instillAcceptFormats": [ + "string" + ], + "instillUIOrder": 1, + "title": "Email", + "type": "string" + }, + "base_url": { + "description": "Fill in your Jira base URL. For example, if your Jira URL is https://mycompany.atlassian.net, then your base URL is https://mycompany.atlassian.net.", + "instillUpstreamTypes": [ + "value", + "reference" + ], + "instillAcceptFormats": [ + "string" + ], + "instillUIOrder": 1, + "title": "Base URL", + "type": "string" } }, "required": [ - "token" + "token", + "email", + "base_url" + ], + "instillEditOnNodeFields": [ + "token", + "email", + "base_url" ], - "instillEditOnNodeFields": [], "title": "Jira Connection", "type": "object" } diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 43293e28..60431268 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -1,102 +1,189 @@ { - "$defs": {}, + "$defs": { + "common_query_params": { + "start_at": { + "default": 0, + "description": "The starting index of the returned boards. Base index: 0. Default is 0", + "instillUIOrder": 3, + "title": "Start At", + "instillFormat": "integer", + "instillAcceptFormats": [ + "integer" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "integer" + }, + "max_results": { + "default": 50, + "description": "The maximum number of boards to return. Default is 50", + "instillUIOrder": 4, + "title": "Max Results", + "instillFormat": "integer", + "instillAcceptFormats": [ + "integer" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "integer" + } + } + }, "TASK_LIST_BOARDS": { "instillShortDescription": "List all boards in Jira", "input": { "description": "List all boards in Jira", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "owner", - "repository" - ], + "instillEditOnNodeFields": [], "properties": { - "$ref": "#/$defs/repository_info", - "state": { - "default": "open", - "title": "State", - "description": "State of the PRs, including open, closed, all. Default is open", - "enum": [ - "open", - "closed", - "all" - ], + "project_key_or_id": { + "default": "", + "title": "Project Key or ID", + "description": "This filters results to boards that are relevant to a project. Relevance meaning that the JQL filter defined in board contains a reference to a project.", + "instillShortDescription": "The project key or ID. Default is empty", + "instillUIOrder": 0, "instillFormat": "string", "instillAcceptFormats": [ "string" ], "instillUpstreamTypes": [ "value", - "reference" + "reference", + "template" ], - "instillUIOrder": 10, "type": "string" }, - "sort": { - "default": "created", - "title": "Sort", - "description": "Sort the PRs by created, updated, popularity, or long-running. Default is created", + "board_type": { + "default": "scrum", + "description": "The type of board, can be: scrum, kanban. Default is scrum", + "instillUIOrder": 1, "enum": [ - "created", - "updated", - "popularity", - "long-running" + "scrum", + "kanban", + "simple" ], + "title": "Board Type", "instillFormat": "string", "instillAcceptFormats": [ "string" ], "instillUpstreamTypes": [ "value", - "reference" + "reference", + "template" ], - "instillUIOrder": 11, "type": "string" }, - "direction": { - "default": "desc", - "title": "Direction", - "description": "Direction of the sort, including asc or desc. Default is asc", - "enum": [ - "asc", - "desc" - ], + "name": { + "default": "", + "description": "Name filters results to boards that match or partially match the specified name. Default is empty", + "instillUIOrder": 2, + "title": "Name", "instillFormat": "string", "instillAcceptFormats": [ "string" ], "instillUpstreamTypes": [ "value", - "reference" + "reference", + "template" ], - "instillUIOrder": 12, "type": "string" + }, + "start_at": { + "$ref": "#/$defs/common_query_params/start_at", + "instillUIOrder": 3 + }, + "max_results": { + "$ref": "#/$defs/common_query_params/max_results", + "instillUIOrder": 4 } }, - "required": [ - "owner", - "repository" - ], + "required": [], "title": "Input", "type": "object" }, "output": { - "description": "All PRs in GitHub repository", + "description": "List all boards in Jira", "instillUIOrder": 0, "properties": { - "pull_requests": { - "description": "An array of PRs", + "boards": { + "description": "A array of boards in Jira", "instillUIOrder": 1, - "title": "Pull Requests", + "title": "Boards", "type": "array", "items": { - "$ref": "#/$defs/pull_request", - "required": [], - "description": "A pull request in GitHub" + "properties": { + "id": { + "description": "The ID of the board", + "instillUIOrder": 0, + "title": "ID", + "type": "integer" + }, + "name": { + "description": "The name of the board", + "instillUIOrder": 1, + "title": "Name", + "type": "string" + }, + "type": { + "description": "The type of the board", + "instillUIOrder": 2, + "title": "Type", + "type": "string" + }, + "self": { + "description": "The URL of the board", + "instillUIOrder": 3, + "title": "Self", + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "name", + "type", + "self" + ] } + }, + "start_at": { + "description": "The starting index of the returned boards. Base index: 0", + "instillUIOrder": 2, + "title": "Start At", + "type": "integer" + }, + "max_results": { + "description": "The maximum number of boards", + "instillUIOrder": 3, + "title": "Max Results", + "type": "integer" + }, + "total": { + "description": "The total number of boards", + "instillUIOrder": 4, + "title": "Total", + "type": "integer" + }, + "is_last": { + "description": "Whether the last board is reached", + "instillUIOrder": 5, + "title": "Is Last", + "type": "boolean" } }, "required": [ - "pull_requests" + "start_at", + "max_results", + "total", + "is_last" ], "title": "Output", "type": "object" diff --git a/application/jira/v0/main.go b/application/jira/v0/main.go index e05b6eda..5188adcf 100644 --- a/application/jira/v0/main.go +++ b/application/jira/v0/main.go @@ -15,7 +15,7 @@ import ( ) const ( - baseURL = "https://jira.atlassian.com" + apiBaseURL = "https://api.atlassian.com" taskListBoards = "TASK_LIST_BOARDS" ) @@ -64,7 +64,7 @@ func (c *component) CreateExecution(sysVars map[string]any, setup *structpb.Stru ComponentExecution: base.ComponentExecution{Component: c, SystemVariables: sysVars, Setup: setup, Task: task}, client: *jiraClient, } - + // docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about switch task { case taskListBoards: e.execute = e.client.listBoardsTask @@ -87,7 +87,6 @@ func (e *execution) fillInDefaultValues(input *structpb.Struct) (*structpb.Struc fmt.Sprintf("Task %s not found", task), ) } - // Unmarshal the taskSpec into a map var taskSpecMap map[string]interface{} err := json.Unmarshal([]byte(taskSpec), &taskSpecMap) if err != nil { @@ -97,19 +96,15 @@ func (e *execution) fillInDefaultValues(input *structpb.Struct) (*structpb.Struc ) } inputMap := taskSpecMap["properties"].(map[string]interface{}) - // iterate over the inputMap and fill in the default values for key, value := range inputMap { valueMap, ok := value.(map[string]interface{}) if !ok { - // fmt.Println("value is not a map", key) continue } if _, ok := valueMap["default"]; !ok { - // fmt.Println("default value not found", key) continue } if _, ok := input.GetFields()[key]; ok { - // fmt.Println("key already exists", key) continue } defaultValue := valueMap["default"] diff --git a/go.mod b/go.mod index 1eb5e3fe..92c90f86 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( code.sajari.com/docconv v1.3.8 github.com/JohannesKaufmann/html-to-markdown v1.5.0 github.com/PuerkitoBio/goquery v1.9.1 - github.com/andygrunwald/go-jira v1.16.0 github.com/cohere-ai/cohere-go/v2 v2.8.5 github.com/emersion/go-imap/v2 v2.0.0-beta.3 github.com/emersion/go-message v0.18.1 @@ -69,14 +68,12 @@ require ( github.com/dlclark/regexp2 v1.10.0 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/fatih/set v0.2.1 // indirect - github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -111,7 +108,6 @@ require ( github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/temoto/robotstxt v1.1.2 // indirect - github.com/trivago/tgo v1.0.7 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 // indirect gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 // indirect diff --git a/go.sum b/go.sum index 148036d0..6b225ac4 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,6 @@ github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxB github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= -github.com/andygrunwald/go-jira v1.16.0 h1:PU7C7Fkk5L96JvPc6vDVIrd99vdPnYudHu4ju2c2ikQ= -github.com/andygrunwald/go-jira v1.16.0/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU= github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0= github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= @@ -90,8 +88,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= @@ -125,8 +121,6 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gojuno/minimock/v3 v3.3.6 h1:tZQQaDgKSxsKiVia9vt6zZ/qsKNGBw2D0ubHQPr+mHc= github.com/gojuno/minimock/v3 v3.3.6/go.mod h1:kjvubEBVT8aUQ9e+g8x/hPfAhiOoqW7WinzzJgzr4ws= -github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= -github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -157,11 +151,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -169,8 +161,6 @@ github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwM github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= @@ -306,8 +296,6 @@ github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fx github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= github.com/tmc/langchaingo v0.1.10 h1:+cssnyaY1avZwzdDFvJYlVUsch9oFRgoqw3Avk5Zig4= github.com/tmc/langchaingo v0.1.10/go.mod h1:lPKUIu8ZGI7RAksRFtKbgtS2v3LL0j7LcccHPCvgNfY= -github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= -github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -415,7 +403,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -428,7 +415,6 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From 3705e9305ae042382c0eb131eafa2244214e9725 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Tue, 9 Jul 2024 16:37:54 +0100 Subject: [PATCH 03/28] feat: add get issue and debug logger --- application/jira/v0/boards.go | 27 +++- application/jira/v0/client.go | 39 +++-- application/jira/v0/config/definition.json | 3 +- application/jira/v0/config/tasks.json | 90 +++++++++++ application/jira/v0/debug.go | 170 +++++++++++++++++++++ application/jira/v0/issues.go | 84 ++++++++++ application/jira/v0/main.go | 3 + 7 files changed, 397 insertions(+), 19 deletions(-) create mode 100644 application/jira/v0/debug.go create mode 100644 application/jira/v0/issues.go diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index e767e200..b044600d 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -17,14 +17,14 @@ type Board struct { BoardType string `json:"type"` } -type GetBoardsInput struct { +type ListBoardsInput struct { ProjectKeyOrID string `json:"project_key_or_id,omitempty" struct:"projectKeyOrID"` BoardType string `json:"board_type,omitempty" struct:"boardType"` Name string `json:"name,omitempty" struct:"name"` StartAt int `json:"start_at,omitempty" struct:"startAt"` MaxResults int `json:"max_results,omitempty" struct:"maxResults"` } -type GetBoardsResp struct { +type ListBoardsResp struct { Values []Board `json:"values"` StartAt int `json:"startAt"` MaxResults int `json:"maxResults"` @@ -32,7 +32,7 @@ type GetBoardsResp struct { IsLast bool `json:"isLast"` } -type GetBoardsOutput struct { +type ListBoardsOutput struct { Boards []Board `json:"boards"` StartAt int `json:"start_at"` MaxResults int `json:"max_results"` @@ -41,7 +41,7 @@ type GetBoardsOutput struct { } func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { - var opt GetBoardsInput + var opt ListBoardsInput if err := base.ConvertFromStructpb(props, &opt); err != nil { return nil, err } @@ -50,7 +50,7 @@ func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.St if err != nil { return nil, err } - var output GetBoardsOutput + var output ListBoardsOutput output.Boards = append(output.Boards, boards.Values...) if output.Boards == nil { output.Boards = []Board{} @@ -62,15 +62,26 @@ func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.St return base.ConvertToStructpb(output) } -func (jiraClient *Client) listBoards(_ context.Context, opt *GetBoardsInput) (*GetBoardsResp, error) { +func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (*ListBoardsResp, error) { + var debug DebugSession + debug.SessionStart("listBoards", StaticVerboseLevel) + defer debug.SessionEnd() + apiEndpoint := "rest/agile/1.0/board" - req := jiraClient.Client.R().SetResult(&GetBoardsResp{}) + req := jiraClient.Client.R().SetResult(&ListBoardsResp{}) + debug.AddMapMessage("opt", *opt) err := addQueryOptions(req, *opt) if err != nil { return nil, err } resp, err := req.Get(apiEndpoint) + + debug.AddMessage("GET", apiEndpoint) + debug.AddMapMessage("QueryParam", resp.Request.QueryParam) + debug.AddMessage("Status", resp.Status()) + debug.AddMapMessage("Error", resp.Error()) + if resp.StatusCode() == 404 { return nil, fmt.Errorf( err.Error(), @@ -82,6 +93,6 @@ func (jiraClient *Client) listBoards(_ context.Context, opt *GetBoardsInput) (*G err.Error(), errmsg.Message(err), ) } - boards := resp.Result().(*GetBoardsResp) + boards := resp.Result().(*ListBoardsResp) return boards, err } diff --git a/application/jira/v0/client.go b/application/jira/v0/client.go index cc32f970..428be788 100644 --- a/application/jira/v0/client.go +++ b/application/jira/v0/client.go @@ -60,9 +60,10 @@ func newClient(_ context.Context, setup *structpb.Struct) (*Client, error) { baseURL, httpclient.WithEndUserError(new(errBody)), ) - jiraClient.SetHeader("Accept", "application/json"). - SetHeader("Content-Type", "application/json") - jiraClient.SetBasicAuth(email, token) + jiraClient. + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json"). + SetBasicAuth(email, token) client := &Client{ Client: jiraClient, APIBaseURL: apiBaseURL, @@ -93,21 +94,39 @@ func (e errBody) Message() string { } func addQueryOptions(req *resty.Request, opt interface{}) error { + var debug DebugSession + debug.SessionStart("addQueryOptions", StaticVerboseLevel) + defer debug.SessionEnd() + v := reflect.ValueOf(opt) if v.Kind() == reflect.Ptr && v.IsNil() { return nil } typeOfS := v.Type() for i := 0; i < v.NumField(); i++ { - stringVal, ok := v.Field(i).Interface().(string) - if !ok { - intVal, ok := v.Field(i).Interface().(int) - if !ok { + if !v.Field(i).IsValid() || !v.Field(i).CanInterface(){ + debug.AddMessage(typeOfS.Field(i).Name,"Not a valid field") + continue + } + val := v.Field(i).Interface() + var stringVal string + switch val := val.(type) { + case string: + if val == "" { continue } - stringVal = fmt.Sprintf("%d", intVal) - } - if stringVal == "" { + stringVal = val + case int: + if val == 0 { + continue + } + stringVal = fmt.Sprintf("%d", val) + case bool: + if !val { + continue + } + stringVal = fmt.Sprintf("%t", val) + default: continue } paramName := typeOfS.Field(i).Tag.Get("struct") diff --git a/application/jira/v0/config/definition.json b/application/jira/v0/config/definition.json index e48af13f..9e16cca7 100644 --- a/application/jira/v0/config/definition.json +++ b/application/jira/v0/config/definition.json @@ -1,6 +1,7 @@ { "availableTasks": [ - "TASK_LIST_BOARDS" + "TASK_LIST_BOARDS", + "TASK_GET_ISSUE" ], "custom": false, "documentationUrl": "https://www.instill.tech/docs/component/application/jira", diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 60431268..0bd458d6 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -32,6 +32,19 @@ "template" ], "type": "integer" + }, + "update_history": { + "description": "Whether the project in which the issue is created is added to the user's Recently viewed project list, as shown under Projects in Jira.", + "title": "Update History", + "instillUIOrder": 5, + "instillFormat": "boolean", + "instillAcceptFormats": [ + "boolean" + ], + "instillUpstreamTypes": [ + "value" + ], + "type": "boolean" } } }, @@ -188,5 +201,82 @@ "title": "Output", "type": "object" } + }, + "TASK_GET_ISSUE": { + "description": "Get an issue in Jira", + "instillShortDescription": "Get an issue in Jira", + "input": { + "description": "Get an issue in Jira", + "instillUIOrder": 0, + "instillEditOnNodeFields": [ + "issue_id_or_key" + ], + "properties": { + "issue_id_or_key": { + "title": "Issue ID or Key", + "description": "The ID or key of the issue", + "instillShortDescription": "The ID or key of the issue", + "instillUIOrder": 0, + "instillFormat": "string", + "instillAcceptFormats": [ + "string" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "string" + }, + "update_history": { + "$ref": "#/$defs/common_query_params/update_history", + "instillUIOrder": 1 + } + }, + "required": [ + "issue_id_or_key" + ], + "title": "Input", + "type": "object" + }, + "output": { + "description": "Get an issue in Jira", + "instillUIOrder": 0, + "properties": { + "id": { + "description": "The ID of the issue", + "instillUIOrder": 0, + "title": "ID", + "type": "string" + }, + "key": { + "description": "The key of the issue", + "instillUIOrder": 1, + "title": "Key", + "type": "string" + }, + "self": { + "description": "The URL of the issue", + "instillUIOrder": 2, + "title": "Self", + "type": "string" + }, + "fields": { + "description": "The fields of the issue. By default, all navigable and Agile fields are returned", + "instillUIOrder": 3, + "title": "Fields", + "type": "object", + "required": [] + } + }, + "required": [ + "id", + "key", + "self", + "fields" + ], + "title": "Output", + "type": "object" + } } } diff --git a/application/jira/v0/debug.go b/application/jira/v0/debug.go new file mode 100644 index 00000000..9ba67386 --- /dev/null +++ b/application/jira/v0/debug.go @@ -0,0 +1,170 @@ +package jira + +import ( + "fmt" + "reflect" + "strings" +) + +const ( + Verbose = 1 + DefaultVerboseLevel = 1 + DevelopVerboseLevel = 1 + StaticVerboseLevel = 2 + +) + +type DebugSession struct { + SessionID string `json:"session_id"` + Title string `json:"title"` + Messages []string `json:"messages"` + halfBannerLen int + indentLevel int + maxDepth int + verboseLevel int +} + +// Session Logger is only verbose when package verbose is greater than the verbose level specified here +func (d *DebugSession) SessionStart(name string, verboseLevel int) { + d.verboseLevel = verboseLevel + if Verbose < d.verboseLevel { + return + } + defer d.flush() + d.SessionID = name + d.halfBannerLen = 20 + halfBanner := strings.Repeat("=", d.halfBannerLen) + if d.Messages == nil { + d.Messages = []string{} + } + d.Title = fmt.Sprintf("%s %s %s", halfBanner, name, halfBanner) + d.Messages = append(d.Messages, d.Title) + d.indentLevel = 0 + d.maxDepth = 5 * Verbose +} + +func (d *DebugSession) AddMessage(msg ...string) { + if Verbose < d.verboseLevel { + return + } + defer d.flush() + parseMsg := strings.Join(msg, " ") + d.Messages = append(d.Messages, + fmt.Sprintf("[%s] %s%s", d.SessionID, strings.Repeat("\t", d.indentLevel), parseMsg)) +} + +// addMapMessage adds a map message to the debug session +// if the map is empty, it will simply add "Map: {}" +func (d *DebugSession) AddMapMessage(name string, m interface{}) { + if Verbose < d.verboseLevel { + return + } + defer d.flush() + if name == "" { + d.AddMessage("Map: {") + } else { + d.AddMessage(name + ": {") + } + defer d.AddMessage("}") + + v := reflect.ValueOf(m) + if v.Kind() == reflect.Ptr && v.IsNil() { + d.AddMessage("Not a map") + return + } + mapVal := make(map[string]interface{}) + if v.Kind() == reflect.Map { + for _, key := range v.MapKeys() { + if v.MapIndex(key).IsValid() && v.MapIndex(key).CanInterface(){ + mapVal[fmt.Sprintf("%v", key)] = v.MapIndex(key).Interface() + } + } + } else if v.Kind() == reflect.Struct { + typeOfS := v.Type() + for i := 0; i < v.NumField(); i++ { + if !v.Field(i).IsValid() || !v.Field(i).CanInterface(){ + continue + } + + val := v.Field(i).Interface() + paramName := typeOfS.Field(i).Name + mapVal[paramName] = val + } + } + d.addControledMapMessage(mapVal, 0) +} + +func (d *DebugSession) AddRawMessage(m interface{}) { + defer d.flush() + d.Messages = append(d.Messages, + fmt.Sprintf("[%s] %s%v", d.SessionID, strings.Repeat("\t", d.indentLevel), m)) +} + +func (d *DebugSession) addControledMapMessage(m map[string]interface{}, depth int) { + d.indentLevel++ + defer func() { + d.indentLevel-- + }() + if depth > d.maxDepth { + d.AddMessage("...") + return + } + for k, v := range m { + switch v := v.(type) { + case map[string]interface{}: + d.AddMessage(k + ":") + d.addControledMapMessage(v, depth+1) + case []interface{}: + d.AddMessage(k + ":") + d.addControledSliceMessage(v, depth+1) + default: + d.AddMessage(fmt.Sprintf("%s: %v", k, v)) + } + } +} + +func (d *DebugSession) addControledSliceMessage(s []interface{}, depth int) { + d.indentLevel++ + defer func() { + d.indentLevel-- + }() + if depth > d.maxDepth { + d.AddMessage("...") + return + } + for _, v := range s { + switch v := v.(type) { + case map[string]interface{}: + d.AddMessage("-") + d.addControledMapMessage(v, depth+1) + case []interface{}: + d.AddMessage("-") + d.addControledSliceMessage(v, depth+1) + default: + d.AddMessage(fmt.Sprintf("- %v", v)) + } + } +} + +func (d *DebugSession) SessionEnd() { + if Verbose < d.verboseLevel { + return + } + defer d.flush() + defer func() { + d.indentLevel = 0 + }() + endHalfBanner := strings.Repeat("=", d.halfBannerLen - 2) + endBanner := fmt.Sprintf("%s %s end %s", endHalfBanner, d.SessionID, endHalfBanner) + d.Messages = append(d.Messages, endBanner) +} + +func (d *DebugSession) flush() { + if Verbose < d.verboseLevel { + return + } + for _, msg := range d.Messages { + fmt.Println(msg) + } + d.Messages = []string{} +} diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go new file mode 100644 index 00000000..3a5dfc8f --- /dev/null +++ b/application/jira/v0/issues.go @@ -0,0 +1,84 @@ +package jira + +import ( + "context" + _ "embed" + "fmt" + + "github.com/instill-ai/component/base" + "github.com/instill-ai/x/errmsg" + "google.golang.org/protobuf/types/known/structpb" +) + +type Issue struct { + ID string `json:"id"` + Key string `json:"key"` + Description string `json:"description"` + Fields map[string]interface{} `json:"fields"` + Self string `json:"self"` +} + +type GetIssueInput struct { + IssueKeyOrID string `json:"issue_id_or_key,omitempty" struct:"issueIdOrKey"` + UpdateHistory bool `json:"update_history,omitempty" struct:"updateHistory"` +} +type GetIssueOutput struct { + Issue +} + +func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { + var debug DebugSession + debug.SessionStart("getIssueTask", StaticVerboseLevel) + defer debug.SessionEnd() + + var opt GetIssueInput + if err := base.ConvertFromStructpb(props, &opt); err != nil { + return nil, err + } + debug.AddMessage(fmt.Sprintf("GetIssueInput: %+v", opt)) + debug.AddMapMessage("opt", opt) + issue, err := jiraClient.getIssue(ctx, &opt) + if err != nil { + return nil, err + } + return base.ConvertToStructpb(issue) +} + +func (jiraClient *Client) getIssue(_ context.Context, opt *GetIssueInput) (*GetIssueOutput, error) { + var debug DebugSession + debug.SessionStart("getIssue", StaticVerboseLevel) + defer debug.SessionEnd() + + apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", opt.IssueKeyOrID) + + req := jiraClient.Client.R().SetResult(&GetIssueOutput{}) + + debug.AddMapMessage("opt", *opt) + + opt.IssueKeyOrID = "" // Remove from query params + err := addQueryOptions(req, *opt) + if err != nil { + return nil, err + } + resp, err := req.Get(apiEndpoint) + + debug.AddMessage("GET", apiEndpoint) + debug.AddMapMessage("QueryParam", resp.Request.QueryParam) + debug.AddMessage("Status", resp.Status()) + debug.AddMapMessage("Error", resp.Error()) + + if resp.StatusCode() == 404 { + return nil, fmt.Errorf( + err.Error(), + errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", + ) + } + if err != nil { + return nil, fmt.Errorf( + err.Error(), errmsg.Message(err), + ) + } + + boards := resp.Result().(*GetIssueOutput) + return boards, err +} diff --git a/application/jira/v0/main.go b/application/jira/v0/main.go index 5188adcf..f1740cfb 100644 --- a/application/jira/v0/main.go +++ b/application/jira/v0/main.go @@ -17,6 +17,7 @@ import ( const ( apiBaseURL = "https://api.atlassian.com" taskListBoards = "TASK_LIST_BOARDS" + taskListIssues = "TASK_GET_ISSUE" ) var ( @@ -68,6 +69,8 @@ func (c *component) CreateExecution(sysVars map[string]any, setup *structpb.Stru switch task { case taskListBoards: e.execute = e.client.listBoardsTask + case taskListIssues: + e.execute = e.client.getIssueTask default: return nil, errmsg.AddMessage( fmt.Errorf("not supported task: %s", task), From b9fb7ea13e8d52e383495976426818ceb3e40b50 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Tue, 9 Jul 2024 17:00:25 +0100 Subject: [PATCH 04/28] fix: fix error check --- application/jira/v0/boards.go | 10 ++++------ application/jira/v0/client.go | 4 ++-- application/jira/v0/debug.go | 23 +++++++++++------------ application/jira/v0/issues.go | 10 ++++------ 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index b044600d..9581544d 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -77,12 +77,7 @@ func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (* } resp, err := req.Get(apiEndpoint) - debug.AddMessage("GET", apiEndpoint) - debug.AddMapMessage("QueryParam", resp.Request.QueryParam) - debug.AddMessage("Status", resp.Status()) - debug.AddMapMessage("Error", resp.Error()) - - if resp.StatusCode() == 404 { + if resp != nil && resp.StatusCode() == 404 { return nil, fmt.Errorf( err.Error(), errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", @@ -93,6 +88,9 @@ func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (* err.Error(), errmsg.Message(err), ) } + debug.AddMessage("GET", apiEndpoint) + debug.AddMapMessage("QueryParam", resp.Request.QueryParam) + debug.AddMessage("Status", resp.Status()) boards := resp.Result().(*ListBoardsResp) return boards, err } diff --git a/application/jira/v0/client.go b/application/jira/v0/client.go index 428be788..8b898176 100644 --- a/application/jira/v0/client.go +++ b/application/jira/v0/client.go @@ -104,8 +104,8 @@ func addQueryOptions(req *resty.Request, opt interface{}) error { } typeOfS := v.Type() for i := 0; i < v.NumField(); i++ { - if !v.Field(i).IsValid() || !v.Field(i).CanInterface(){ - debug.AddMessage(typeOfS.Field(i).Name,"Not a valid field") + if !v.Field(i).IsValid() || !v.Field(i).CanInterface() { + debug.AddMessage(typeOfS.Field(i).Name, "Not a valid field") continue } val := v.Field(i).Interface() diff --git a/application/jira/v0/debug.go b/application/jira/v0/debug.go index 9ba67386..a4f2af48 100644 --- a/application/jira/v0/debug.go +++ b/application/jira/v0/debug.go @@ -7,21 +7,20 @@ import ( ) const ( - Verbose = 1 + Verbose = 1 DefaultVerboseLevel = 1 DevelopVerboseLevel = 1 - StaticVerboseLevel = 2 - + StaticVerboseLevel = 2 ) type DebugSession struct { - SessionID string `json:"session_id"` - Title string `json:"title"` - Messages []string `json:"messages"` + SessionID string `json:"session_id"` + Title string `json:"title"` + Messages []string `json:"messages"` halfBannerLen int - indentLevel int - maxDepth int - verboseLevel int + indentLevel int + maxDepth int + verboseLevel int } // Session Logger is only verbose when package verbose is greater than the verbose level specified here @@ -75,14 +74,14 @@ func (d *DebugSession) AddMapMessage(name string, m interface{}) { mapVal := make(map[string]interface{}) if v.Kind() == reflect.Map { for _, key := range v.MapKeys() { - if v.MapIndex(key).IsValid() && v.MapIndex(key).CanInterface(){ + if v.MapIndex(key).IsValid() && v.MapIndex(key).CanInterface() { mapVal[fmt.Sprintf("%v", key)] = v.MapIndex(key).Interface() } } } else if v.Kind() == reflect.Struct { typeOfS := v.Type() for i := 0; i < v.NumField(); i++ { - if !v.Field(i).IsValid() || !v.Field(i).CanInterface(){ + if !v.Field(i).IsValid() || !v.Field(i).CanInterface() { continue } @@ -154,7 +153,7 @@ func (d *DebugSession) SessionEnd() { defer func() { d.indentLevel = 0 }() - endHalfBanner := strings.Repeat("=", d.halfBannerLen - 2) + endHalfBanner := strings.Repeat("=", d.halfBannerLen-2) endBanner := fmt.Sprintf("%s %s end %s", endHalfBanner, d.SessionID, endHalfBanner) d.Messages = append(d.Messages, endBanner) } diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 3a5dfc8f..589c7e28 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -62,12 +62,7 @@ func (jiraClient *Client) getIssue(_ context.Context, opt *GetIssueInput) (*GetI } resp, err := req.Get(apiEndpoint) - debug.AddMessage("GET", apiEndpoint) - debug.AddMapMessage("QueryParam", resp.Request.QueryParam) - debug.AddMessage("Status", resp.Status()) - debug.AddMapMessage("Error", resp.Error()) - - if resp.StatusCode() == 404 { + if resp != nil && resp.StatusCode() == 404 { return nil, fmt.Errorf( err.Error(), errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", @@ -78,6 +73,9 @@ func (jiraClient *Client) getIssue(_ context.Context, opt *GetIssueInput) (*GetI err.Error(), errmsg.Message(err), ) } + debug.AddMessage("GET", apiEndpoint) + debug.AddMapMessage("QueryParam", resp.Request.QueryParam) + debug.AddMessage("Status", resp.Status()) boards := resp.Result().(*GetIssueOutput) return boards, err From eb2414fcf213ab5bb6128bcb60630ca67abc08db Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Thu, 11 Jul 2024 16:57:33 +0100 Subject: [PATCH 05/28] feat: add get sprint --- application/jira/v0/boards.go | 14 +- application/jira/v0/client.go | 6 +- application/jira/v0/config/definition.json | 3 +- application/jira/v0/config/setup.json | 6 +- application/jira/v0/config/tasks.json | 173 ++++++++++++++++++--- application/jira/v0/debug.go | 21 ++- application/jira/v0/issues.go | 59 ++++--- application/jira/v0/main.go | 7 +- application/jira/v0/sprint.go | 97 ++++++++++++ 9 files changed, 317 insertions(+), 69 deletions(-) create mode 100644 application/jira/v0/sprint.go diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index 9581544d..d9f62476 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -18,11 +18,11 @@ type Board struct { } type ListBoardsInput struct { - ProjectKeyOrID string `json:"project_key_or_id,omitempty" struct:"projectKeyOrID"` - BoardType string `json:"board_type,omitempty" struct:"boardType"` + ProjectKeyOrID string `json:"project-key-or-id,omitempty" struct:"projectKeyOrID"` + BoardType string `json:"board-type,omitempty" struct:"boardType"` Name string `json:"name,omitempty" struct:"name"` - StartAt int `json:"start_at,omitempty" struct:"startAt"` - MaxResults int `json:"max_results,omitempty" struct:"maxResults"` + StartAt int `json:"start-at,omitempty" struct:"startAt"` + MaxResults int `json:"max-results,omitempty" struct:"maxResults"` } type ListBoardsResp struct { Values []Board `json:"values"` @@ -34,10 +34,10 @@ type ListBoardsResp struct { type ListBoardsOutput struct { Boards []Board `json:"boards"` - StartAt int `json:"start_at"` - MaxResults int `json:"max_results"` + StartAt int `json:"start-at"` + MaxResults int `json:"max-results"` Total int `json:"total"` - IsLast bool `json:"is_last"` + IsLast bool `json:"is-last"` } func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { diff --git a/application/jira/v0/client.go b/application/jira/v0/client.go index 8b898176..3edccdb6 100644 --- a/application/jira/v0/client.go +++ b/application/jira/v0/client.go @@ -14,9 +14,9 @@ import ( type Client struct { *httpclient.Client - APIBaseURL string `json:"api_base_url"` + APIBaseURL string `json:"api-base-url"` Domain string `json:"domain"` - CloudID string `json:"cloud_id"` + CloudID string `json:"cloud-id"` } type CloudID struct { @@ -26,7 +26,7 @@ type CloudID struct { type AuthConfig struct { Email string `json:"email"` Token string `json:"token"` - BaseURL string `json:"base_url"` + BaseURL string `json:"base-url"` } func newClient(_ context.Context, setup *structpb.Struct) (*Client, error) { diff --git a/application/jira/v0/config/definition.json b/application/jira/v0/config/definition.json index 9e16cca7..d89fb9d9 100644 --- a/application/jira/v0/config/definition.json +++ b/application/jira/v0/config/definition.json @@ -1,7 +1,8 @@ { "availableTasks": [ "TASK_LIST_BOARDS", - "TASK_GET_ISSUE" + "TASK_GET_ISSUE", + "TASK_GET_SPRINT" ], "custom": false, "documentationUrl": "https://www.instill.tech/docs/component/application/jira", diff --git a/application/jira/v0/config/setup.json b/application/jira/v0/config/setup.json index 1abd7ea5..d06ca2cb 100644 --- a/application/jira/v0/config/setup.json +++ b/application/jira/v0/config/setup.json @@ -28,7 +28,7 @@ "title": "Email", "type": "string" }, - "base_url": { + "base-url": { "description": "Fill in your Jira base URL. For example, if your Jira URL is https://mycompany.atlassian.net, then your base URL is https://mycompany.atlassian.net.", "instillUpstreamTypes": [ "value", @@ -45,12 +45,12 @@ "required": [ "token", "email", - "base_url" + "base-url" ], "instillEditOnNodeFields": [ "token", "email", - "base_url" + "base-url" ], "title": "Jira Connection", "type": "object" diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 0bd458d6..badc5730 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -1,7 +1,7 @@ { "$defs": { - "common_query_params": { - "start_at": { + "common-query-params": { + "start-at": { "default": 0, "description": "The starting index of the returned boards. Base index: 0. Default is 0", "instillUIOrder": 3, @@ -17,7 +17,7 @@ ], "type": "integer" }, - "max_results": { + "max-results": { "default": 50, "description": "The maximum number of boards to return. Default is 50", "instillUIOrder": 4, @@ -33,7 +33,7 @@ ], "type": "integer" }, - "update_history": { + "update-history": { "description": "Whether the project in which the issue is created is added to the user's Recently viewed project list, as shown under Projects in Jira.", "title": "Update History", "instillUIOrder": 5, @@ -55,7 +55,7 @@ "instillUIOrder": 0, "instillEditOnNodeFields": [], "properties": { - "project_key_or_id": { + "project-key-or-id": { "default": "", "title": "Project Key or ID", "description": "This filters results to boards that are relevant to a project. Relevance meaning that the JQL filter defined in board contains a reference to a project.", @@ -72,7 +72,7 @@ ], "type": "string" }, - "board_type": { + "board-type": { "default": "scrum", "description": "The type of board, can be: scrum, kanban. Default is scrum", "instillUIOrder": 1, @@ -109,12 +109,12 @@ ], "type": "string" }, - "start_at": { - "$ref": "#/$defs/common_query_params/start_at", + "start-at": { + "$ref": "#/$defs/common-query-params/start-at", "instillUIOrder": 3 }, - "max_results": { - "$ref": "#/$defs/common_query_params/max_results", + "max-results": { + "$ref": "#/$defs/common-query-params/max-results", "instillUIOrder": 4 } }, @@ -167,13 +167,13 @@ ] } }, - "start_at": { + "start-at": { "description": "The starting index of the returned boards. Base index: 0", "instillUIOrder": 2, "title": "Start At", "type": "integer" }, - "max_results": { + "max-results": { "description": "The maximum number of boards", "instillUIOrder": 3, "title": "Max Results", @@ -185,7 +185,7 @@ "title": "Total", "type": "integer" }, - "is_last": { + "is-last": { "description": "Whether the last board is reached", "instillUIOrder": 5, "title": "Is Last", @@ -193,10 +193,10 @@ } }, "required": [ - "start_at", - "max_results", + "start-at", + "max-results", "total", - "is_last" + "is-last" ], "title": "Output", "type": "object" @@ -209,10 +209,10 @@ "description": "Get an issue in Jira", "instillUIOrder": 0, "instillEditOnNodeFields": [ - "issue_id_or_key" + "issue-id-or-key" ], "properties": { - "issue_id_or_key": { + "issue-id-or-key": { "title": "Issue ID or Key", "description": "The ID or key of the issue", "instillShortDescription": "The ID or key of the issue", @@ -228,13 +228,13 @@ ], "type": "string" }, - "update_history": { - "$ref": "#/$defs/common_query_params/update_history", + "update-history": { + "$ref": "#/$defs/common-query-params/update-history", "instillUIOrder": 1 } }, "required": [ - "issue_id_or_key" + "issue-id-or-key" ], "title": "Input", "type": "object" @@ -247,26 +247,58 @@ "description": "The ID of the issue", "instillUIOrder": 0, "title": "ID", + "instillFormat": "string", "type": "string" }, "key": { "description": "The key of the issue", "instillUIOrder": 1, + "instillFormat": "string", "title": "Key", "type": "string" }, "self": { "description": "The URL of the issue", "instillUIOrder": 2, + "instillFormat": "string", "title": "Self", "type": "string" }, "fields": { - "description": "The fields of the issue. By default, all navigable and Agile fields are returned", + "description": "The fields of the issue. All navigable and Agile fields are returned", "instillUIOrder": 3, + "instillFormat": "object", "title": "Fields", "type": "object", "required": [] + }, + "issue-type": { + "description": "The type of the issue, can be: `Task`, `Epic`", + "instillUIOrder": 4, + "instillFormat": "string", + "title": "Issue Type", + "type": "string" + }, + "summary": { + "description": "The summary of the issue", + "instillUIOrder": 5, + "instillFormat": "string", + "title": "Summary", + "type": "string" + }, + "description": { + "description": "The description of the issue", + "instillUIOrder": 6, + "instillFormat": "string", + "title": "Description", + "type": "string" + }, + "status": { + "description": "The status of the issue, can be: `To Do`, `In Progress`, `Done`", + "instillUIOrder": 7, + "instillFormat": "string", + "title": "Status", + "type": "string" } }, "required": [ @@ -278,5 +310,102 @@ "title": "Output", "type": "object" } + }, + "TASK_GET_SPRINT": { + "description": "Get a sprint in Jira", + "instillShortDescription": "Get a sprint in Jira", + "input": { + "description": "Get an sprint in Jira", + "instillUIOrder": 0, + "instillEditOnNodeFields": [ + "sprint-id" + ], + "properties": { + "sprint-id": { + "title": "Sprint ID", + "description": "The ID of the sprint. The sprint will only be returned if you can view the board that the sprint was created on, or view at least one of the issues in the sprint.", + "instillShortDescription": "The ID of the sprint", + "instillUIOrder": 0, + "instillFormat": "integer", + "instillAcceptFormats": [ + "integer" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "integer" + } + }, + "required": [ + "sprint-id" + ], + "title": "Input", + "type": "object" + }, + "output": { + "description": "Get an issue in Jira", + "instillUIOrder": 0, + "properties": { + "id": { + "title": "ID", + "description": "The ID of the sprint", + "type": "integer", + "instillFormat": "integer" + }, + "self": { + "title": "Self", + "description": "The URL of the sprint", + "type": "string", + "instillFormat": "string" + }, + "state": { + "title": "State", + "description": "The state of the sprint, can be: `active`, `closed`, `future`", + "type": "string", + "instillFormat": "string" + }, + "name": { + "title": "Name", + "description": "The name of the sprint", + "type": "string", + "instillFormat": "string" + }, + "start-date": { + "title": "Start Date", + "description": "The start date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", + "type": "string", + "instillFormat": "string" + }, + "end-date": { + "title": "End Date", + "description": "The end date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", + "type": "string", + "instillFormat": "string" + }, + "complete-date": { + "title": "Complete Date", + "description": "The complete date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", + "type": "string", + "instillFormat": "string" + }, + "origin-board-id": { + "title": "Origin Board ID", + "description": "The ID of the origin board", + "type": "integer", + "instillFormat": "integer" + }, + "goal": { + "title": "Goal", + "description": "The Goal of the sprint", + "type": "string", + "instillFormat": "string" + } + }, + "required": [], + "title": "Output", + "type": "object" + } } } diff --git a/application/jira/v0/debug.go b/application/jira/v0/debug.go index a4f2af48..ec1bde27 100644 --- a/application/jira/v0/debug.go +++ b/application/jira/v0/debug.go @@ -14,7 +14,7 @@ const ( ) type DebugSession struct { - SessionID string `json:"session_id"` + SessionID string `json:"session-id"` Title string `json:"title"` Messages []string `json:"messages"` halfBannerLen int @@ -60,10 +60,9 @@ func (d *DebugSession) AddMapMessage(name string, m interface{}) { } defer d.flush() if name == "" { - d.AddMessage("Map: {") - } else { - d.AddMessage(name + ": {") + name = "Map" } + d.AddMessage(name + ": {") defer d.AddMessage("}") v := reflect.ValueOf(m) @@ -90,7 +89,7 @@ func (d *DebugSession) AddMapMessage(name string, m interface{}) { mapVal[paramName] = val } } - d.addControledMapMessage(mapVal, 0) + d.addControlledMapMessage(mapVal, 0) } func (d *DebugSession) AddRawMessage(m interface{}) { @@ -99,7 +98,7 @@ func (d *DebugSession) AddRawMessage(m interface{}) { fmt.Sprintf("[%s] %s%v", d.SessionID, strings.Repeat("\t", d.indentLevel), m)) } -func (d *DebugSession) addControledMapMessage(m map[string]interface{}, depth int) { +func (d *DebugSession) addControlledMapMessage(m map[string]interface{}, depth int) { d.indentLevel++ defer func() { d.indentLevel-- @@ -112,17 +111,17 @@ func (d *DebugSession) addControledMapMessage(m map[string]interface{}, depth in switch v := v.(type) { case map[string]interface{}: d.AddMessage(k + ":") - d.addControledMapMessage(v, depth+1) + d.addControlledMapMessage(v, depth+1) case []interface{}: d.AddMessage(k + ":") - d.addControledSliceMessage(v, depth+1) + d.addControlledSliceMessage(v, depth+1) default: d.AddMessage(fmt.Sprintf("%s: %v", k, v)) } } } -func (d *DebugSession) addControledSliceMessage(s []interface{}, depth int) { +func (d *DebugSession) addControlledSliceMessage(s []interface{}, depth int) { d.indentLevel++ defer func() { d.indentLevel-- @@ -135,10 +134,10 @@ func (d *DebugSession) addControledSliceMessage(s []interface{}, depth int) { switch v := v.(type) { case map[string]interface{}: d.AddMessage("-") - d.addControledMapMessage(v, depth+1) + d.addControlledMapMessage(v, depth+1) case []interface{}: d.AddMessage("-") - d.addControledSliceMessage(v, depth+1) + d.addControlledSliceMessage(v, depth+1) default: d.AddMessage(fmt.Sprintf("- %v", v)) } diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 589c7e28..7f3f5ae1 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -14,13 +14,16 @@ type Issue struct { ID string `json:"id"` Key string `json:"key"` Description string `json:"description"` + Summary string `json:"summary"` Fields map[string]interface{} `json:"fields"` Self string `json:"self"` + IssueType string `json:"issue-type"` + Status string `json:"status"` } type GetIssueInput struct { - IssueKeyOrID string `json:"issue_id_or_key,omitempty" struct:"issueIdOrKey"` - UpdateHistory bool `json:"update_history,omitempty" struct:"updateHistory"` + IssueKeyOrID string `json:"issue-id-or-key,omitempty" struct:"issueIdOrKey"` + UpdateHistory bool `json:"update-history,omitempty" struct:"updateHistory"` } type GetIssueOutput struct { Issue @@ -36,27 +39,12 @@ func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Stru return nil, err } debug.AddMessage(fmt.Sprintf("GetIssueInput: %+v", opt)) - debug.AddMapMessage("opt", opt) - issue, err := jiraClient.getIssue(ctx, &opt) - if err != nil { - return nil, err - } - return base.ConvertToStructpb(issue) -} - -func (jiraClient *Client) getIssue(_ context.Context, opt *GetIssueInput) (*GetIssueOutput, error) { - var debug DebugSession - debug.SessionStart("getIssue", StaticVerboseLevel) - defer debug.SessionEnd() apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", opt.IssueKeyOrID) - req := jiraClient.Client.R().SetResult(&GetIssueOutput{}) - debug.AddMapMessage("opt", *opt) - opt.IssueKeyOrID = "" // Remove from query params - err := addQueryOptions(req, *opt) + err := addQueryOptions(req, opt) if err != nil { return nil, err } @@ -77,6 +65,37 @@ func (jiraClient *Client) getIssue(_ context.Context, opt *GetIssueInput) (*GetI debug.AddMapMessage("QueryParam", resp.Request.QueryParam) debug.AddMessage("Status", resp.Status()) - boards := resp.Result().(*GetIssueOutput) - return boards, err + issue, ok := resp.Result().(*GetIssueOutput) + if !ok { + return nil, errmsg.AddMessage( + fmt.Errorf("failed to convert response to `Get Issue` Output"), + fmt.Sprintf("failed to convert %v to `Get Issue` Output", resp.Result()), + ) + } + + if issue.Description == "" && issue.Fields["description"] != nil { + if issue.Description, ok = issue.Fields["description"].(string); !ok { + issue.Description = "" + } + } + if issue.Summary == "" && issue.Fields["summary"] != nil { + if issue.Summary, ok = issue.Fields["summary"].(string); !ok { + issue.Summary = "" + } + } + if issue.IssueType == "" && issue.Fields["issuetype"] != nil { + if issueType, ok := issue.Fields["issuetype"]; ok { + if issue.IssueType, ok = issueType.(map[string]interface{})["name"].(string); !ok { + issue.IssueType = "" + } + } + } + if issue.Status == "" && issue.Fields["status"] != nil { + if status, ok := issue.Fields["status"]; ok { + if issue.Status, ok = status.(map[string]interface{})["name"].(string); !ok { + issue.Status = "" + } + } + } + return base.ConvertToStructpb(issue) } diff --git a/application/jira/v0/main.go b/application/jira/v0/main.go index f1740cfb..0ec9cec3 100644 --- a/application/jira/v0/main.go +++ b/application/jira/v0/main.go @@ -17,7 +17,8 @@ import ( const ( apiBaseURL = "https://api.atlassian.com" taskListBoards = "TASK_LIST_BOARDS" - taskListIssues = "TASK_GET_ISSUE" + taskGetIssue = "TASK_GET_ISSUE" + taskGetSprint = "TASK_GET_SPRINT" ) var ( @@ -69,8 +70,10 @@ func (c *component) CreateExecution(sysVars map[string]any, setup *structpb.Stru switch task { case taskListBoards: e.execute = e.client.listBoardsTask - case taskListIssues: + case taskGetIssue: e.execute = e.client.getIssueTask + case taskGetSprint: + e.execute = e.client.getSprintTask default: return nil, errmsg.AddMessage( fmt.Errorf("not supported task: %s", task), diff --git a/application/jira/v0/sprint.go b/application/jira/v0/sprint.go new file mode 100644 index 00000000..05d0ae90 --- /dev/null +++ b/application/jira/v0/sprint.go @@ -0,0 +1,97 @@ +package jira + +import ( + "context" + _ "embed" + "fmt" + + "github.com/instill-ai/component/base" + "github.com/instill-ai/x/errmsg" + "google.golang.org/protobuf/types/known/structpb" +) + +type Sprint struct { + ID int `json:"id"` + Self string `json:"self"` + State string `json:"state"` + Name string `json:"name"` + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + CompleteDate string `json:"completeDate"` + OriginBoardID int `json:"originBoardId"` + Goal string `json:"goal"` +} + +type GetSprintInput struct { + SprintID int `json:"sprint-id,omitempty" struct:"sprintId"` +} +type GetSprintOutput struct { + ID int `json:"id"` + Self string `json:"self"` + State string `json:"state"` + Name string `json:"name"` + StartDate string `json:"start-date"` + EndDate string `json:"end-date"` + CompleteDate string `json:"complete-date"` + OriginBoardID int `json:"origin-board-id"` + Goal string `json:"goal"` +} + +func (jiraClient *Client) extractSprintOutput(sprint *Sprint) *GetSprintOutput { + return &GetSprintOutput{ + ID: sprint.ID, + Self: sprint.Self, + State: sprint.State, + Name: sprint.Name, + StartDate: sprint.StartDate, + EndDate: sprint.EndDate, + CompleteDate: sprint.CompleteDate, + OriginBoardID: sprint.OriginBoardID, + Goal: sprint.Goal, + } +} +func (jiraClient *Client) getSprintTask(_ context.Context, props *structpb.Struct) (*structpb.Struct, error) { + var debug DebugSession + debug.SessionStart("getSprintTask", StaticVerboseLevel) + defer debug.SessionEnd() + + var opt GetSprintInput + if err := base.ConvertFromStructpb(props, &opt); err != nil { + return nil, err + } + debug.AddMessage(fmt.Sprintf("GetSprintInput: %+v", opt)) + + apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%v", opt.SprintID) + req := jiraClient.Client.R().SetResult(&Sprint{}) + resp, err := req.Get(apiEndpoint) + + if resp != nil && resp.StatusCode() == 404 { + return nil, fmt.Errorf( + err.Error(), + errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", + ) + } else if resp != nil && resp.StatusCode() == 401 { + return nil, fmt.Errorf( + err.Error(), + errmsg.Message(err)+"You are not logged in. Please provide a valid token and an email account.", + ) + } + if err != nil { + return nil, fmt.Errorf( + err.Error(), errmsg.Message(err), + ) + } + debug.AddMessage("GET", apiEndpoint) + debug.AddMapMessage("QueryParam", resp.Request.QueryParam) + debug.AddMessage("Status", resp.Status()) + + issue, ok := resp.Result().(*Sprint) + if !ok { + return nil, errmsg.AddMessage( + fmt.Errorf("failed to convert response to `Get Sprint` Output"), + fmt.Sprintf("failed to convert %v to `Get Sprint` Output", resp.Result()), + ) + } + out := jiraClient.extractSprintOutput(issue) + return base.ConvertToStructpb(out) +} From e713a77ce930797e1be6aedc724bc79f3ea94103 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Fri, 12 Jul 2024 09:48:02 +0100 Subject: [PATCH 06/28] fix: fix list boards bug --- application/jira/v0/boards.go | 2 +- application/jira/v0/client.go | 14 +++++--------- application/jira/v0/sprint.go | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index d9f62476..32ce04fd 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -19,7 +19,7 @@ type Board struct { type ListBoardsInput struct { ProjectKeyOrID string `json:"project-key-or-id,omitempty" struct:"projectKeyOrID"` - BoardType string `json:"board-type,omitempty" struct:"boardType"` + BoardType string `json:"board-type,omitempty" struct:"type"` Name string `json:"name,omitempty" struct:"name"` StartAt int `json:"start-at,omitempty" struct:"startAt"` MaxResults int `json:"max-results,omitempty" struct:"maxResults"` diff --git a/application/jira/v0/client.go b/application/jira/v0/client.go index 3edccdb6..b4c03dd1 100644 --- a/application/jira/v0/client.go +++ b/application/jira/v0/client.go @@ -112,23 +112,19 @@ func addQueryOptions(req *resty.Request, opt interface{}) error { var stringVal string switch val := val.(type) { case string: - if val == "" { - continue - } stringVal = val case int: - if val == 0 { - continue - } stringVal = fmt.Sprintf("%d", val) case bool: - if !val { - continue - } stringVal = fmt.Sprintf("%t", val) default: continue } + + if stringVal == fmt.Sprintf("%v", reflect.Zero(reflect.TypeOf(val))) { + debug.AddMessage(typeOfS.Field(i).Name, "Default value is not set. Skipping.") + continue + } paramName := typeOfS.Field(i).Tag.Get("struct") if paramName == "" { paramName = typeOfS.Field(i).Name diff --git a/application/jira/v0/sprint.go b/application/jira/v0/sprint.go index 05d0ae90..f9914fd9 100644 --- a/application/jira/v0/sprint.go +++ b/application/jira/v0/sprint.go @@ -23,7 +23,7 @@ type Sprint struct { } type GetSprintInput struct { - SprintID int `json:"sprint-id,omitempty" struct:"sprintId"` + SprintID int `json:"sprint-id"` } type GetSprintOutput struct { ID int `json:"id"` From 9c4f1352b0749f22bffd1035dad2583c5f542765 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Fri, 12 Jul 2024 19:37:36 +0100 Subject: [PATCH 07/28] feat: add list issue task --- application/jira/v0/boards.go | 21 ++ application/jira/v0/client.go | 84 ++++-- application/jira/v0/config/definition.json | 1 + application/jira/v0/config/tasks.json | 324 +++++++++++++++++++++ application/jira/v0/issues.go | 277 ++++++++++++++++-- application/jira/v0/main.go | 3 + 6 files changed, 662 insertions(+), 48 deletions(-) diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index 32ce04fd..589e819a 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -94,3 +94,24 @@ func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (* boards := resp.Result().(*ListBoardsResp) return boards, err } + +func (jiraClient *Client) getBoard(_ context.Context, boardID int) (*Board, error) { + var debug DebugSession + debug.SessionStart("getBoard", DevelopVerboseLevel) + defer debug.SessionEnd() + + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) + req := jiraClient.Client.R().SetResult(&Board{}) + resp, err := req.Get(apiEndpoint) + + if err != nil { + return nil, fmt.Errorf( + err.Error(), errmsg.Message(err), + ) + } + debug.AddMessage("GET", apiEndpoint) + debug.AddMapMessage("QueryParam", resp.Request.QueryParam) + debug.AddMessage("Status", resp.Status()) + board := resp.Result().(*Board) + return board, err +} diff --git a/application/jira/v0/client.go b/application/jira/v0/client.go index b4c03dd1..b9401e44 100644 --- a/application/jira/v0/client.go +++ b/application/jira/v0/client.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "strings" "github.com/go-resty/resty/v2" "github.com/instill-ai/component/base" @@ -85,12 +86,13 @@ func getCloudID(baseURL string) (string, error) { } type errBody struct { - Msg string `json:"message"` - Status int `json:"status"` + Body struct { + Msg []string `json:"errorMessages"` + } `json:"body"` } func (e errBody) Message() string { - return fmt.Sprintf("%d %s", e.Status, e.Msg) + return strings.Join(e.Body.Msg, " ") } func addQueryOptions(req *resty.Request, opt interface{}) error { @@ -102,34 +104,58 @@ func addQueryOptions(req *resty.Request, opt interface{}) error { if v.Kind() == reflect.Ptr && v.IsNil() { return nil } - typeOfS := v.Type() - for i := 0; i < v.NumField(); i++ { - if !v.Field(i).IsValid() || !v.Field(i).CanInterface() { - debug.AddMessage(typeOfS.Field(i).Name, "Not a valid field") - continue + if v.Kind() == reflect.Map { + for _, key := range v.MapKeys() { + if v.MapIndex(key).IsValid() && v.MapIndex(key).CanInterface() { + val := v.MapIndex(key).Interface() + var stringVal string + switch val := val.(type) { + case string: + stringVal = val + case int: + stringVal = fmt.Sprintf("%d", val) + case bool: + stringVal = fmt.Sprintf("%t", val) + default: + continue + } + if stringVal == fmt.Sprintf("%v", reflect.Zero(reflect.TypeOf(val))) { + debug.AddMessage(key.String(), "Default value is not set. Skipping.") + continue + } + paramName := key.String() + req.SetQueryParam(paramName, stringVal) + } } - val := v.Field(i).Interface() - var stringVal string - switch val := val.(type) { - case string: - stringVal = val - case int: - stringVal = fmt.Sprintf("%d", val) - case bool: - stringVal = fmt.Sprintf("%t", val) - default: - continue + } else if v.Kind() == reflect.Struct { + typeOfS := v.Type() + for i := 0; i < v.NumField(); i++ { + if !v.Field(i).IsValid() || !v.Field(i).CanInterface() { + debug.AddMessage(typeOfS.Field(i).Name, "Not a valid field") + continue + } + val := v.Field(i).Interface() + var stringVal string + switch val := val.(type) { + case string: + stringVal = val + case int: + stringVal = fmt.Sprintf("%d", val) + case bool: + stringVal = fmt.Sprintf("%t", val) + default: + continue + } + if stringVal == fmt.Sprintf("%v", reflect.Zero(reflect.TypeOf(val))) { + debug.AddMessage(typeOfS.Field(i).Name, "Default value is not set. Skipping.") + continue + } + paramName := typeOfS.Field(i).Tag.Get("struct") + if paramName == "" { + paramName = typeOfS.Field(i).Name + } + req.SetQueryParam(paramName, stringVal) } - - if stringVal == fmt.Sprintf("%v", reflect.Zero(reflect.TypeOf(val))) { - debug.AddMessage(typeOfS.Field(i).Name, "Default value is not set. Skipping.") - continue - } - paramName := typeOfS.Field(i).Tag.Get("struct") - if paramName == "" { - paramName = typeOfS.Field(i).Name - } - req.SetQueryParam(paramName, stringVal) } return nil } diff --git a/application/jira/v0/config/definition.json b/application/jira/v0/config/definition.json index d89fb9d9..bea93bc4 100644 --- a/application/jira/v0/config/definition.json +++ b/application/jira/v0/config/definition.json @@ -1,6 +1,7 @@ { "availableTasks": [ "TASK_LIST_BOARDS", + "TASK_LIST_ISSUES", "TASK_GET_ISSUE", "TASK_GET_SPRINT" ], diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index badc5730..fe6067cf 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -46,6 +46,75 @@ ], "type": "boolean" } + }, + "issue": { + "properties": { + "id": { + "description": "The ID of the issue", + "instillUIOrder": 0, + "title": "ID", + "instillFormat": "string", + "type": "string" + }, + "key": { + "description": "The key of the issue", + "instillUIOrder": 1, + "instillFormat": "string", + "title": "Key", + "type": "string" + }, + "self": { + "description": "The URL of the issue", + "instillUIOrder": 2, + "instillFormat": "string", + "title": "Self", + "type": "string" + }, + "fields": { + "description": "The fields of the issue. All navigable and Agile fields are returned", + "instillUIOrder": 3, + "instillFormat": "object", + "title": "Fields", + "type": "object", + "required": [] + }, + "issue-type": { + "description": "The type of the issue, can be: `Task`, `Epic`", + "instillUIOrder": 4, + "instillFormat": "string", + "title": "Issue Type", + "type": "string" + }, + "summary": { + "description": "The summary of the issue", + "instillUIOrder": 5, + "instillFormat": "string", + "title": "Summary", + "type": "string" + }, + "description": { + "description": "The description of the issue", + "instillUIOrder": 6, + "instillFormat": "string", + "title": "Description", + "type": "string" + }, + "status": { + "description": "The status of the issue, can be: `To Do`, `In Progress`, `Done`", + "instillUIOrder": 7, + "instillFormat": "string", + "title": "Status", + "type": "string" + } + }, + "required": [ + "id", + "key", + "self", + "fields" + ], + "title": "Issue", + "type": "object" } }, "TASK_LIST_BOARDS": { @@ -137,24 +206,28 @@ "description": "The ID of the board", "instillUIOrder": 0, "title": "ID", + "instillFormat": "integer", "type": "integer" }, "name": { "description": "The name of the board", "instillUIOrder": 1, "title": "Name", + "instillFormat": "string", "type": "string" }, "type": { "description": "The type of the board", "instillUIOrder": 2, "title": "Type", + "instillFormat": "string", "type": "string" }, "self": { "description": "The URL of the board", "instillUIOrder": 3, "title": "Self", + "instillFormat": "string", "type": "string" } }, @@ -171,24 +244,28 @@ "description": "The starting index of the returned boards. Base index: 0", "instillUIOrder": 2, "title": "Start At", + "instillFormat": "integer", "type": "integer" }, "max-results": { "description": "The maximum number of boards", "instillUIOrder": 3, "title": "Max Results", + "instillFormat": "integer", "type": "integer" }, "total": { "description": "The total number of boards", "instillUIOrder": 4, "title": "Total", + "instillFormat": "integer", "type": "integer" }, "is-last": { "description": "Whether the last board is reached", "instillUIOrder": 5, "title": "Is Last", + "instillFormat": "boolean", "type": "boolean" } }, @@ -202,6 +279,253 @@ "type": "object" } }, + "TASK_LIST_ISSUES": { + "description": "List issues in Jira", + "instillShortDescription": "List issues in Jira", + "input": { + "description": "List issues in Jira", + "instillUIOrder": 0, + "instillEditOnNodeFields": [ + "board-id", + "range" + ], + "properties": { + "board-id": { + "title": "Board ID", + "description": "The ID of the board", + "instillShortDescription": "The ID of the board", + "instillUIOrder": 0, + "instillFormat": "integer", + "instillAcceptFormats": [ + "integer" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "integer" + }, + "range": { + "title": "Range", + "description": "Choose the range of issues to return. Default is `all`", + "instillUIOrder": 1, + "additionalProperties": true, + "instillFormat": "object", + "type": "object", + "required": [ + "range" + ], + "oneOf": [ + { + "properties": { + "range": { + "const": "All", + "type": "string" + } + }, + "required": [ + "range" + ], + "instillEditOnNodeFields": [ + "range" + ], + "instillFormat": "object", + "type": "object" + }, + { + "properties": { + "range": { + "const": "Epics only", + "type": "string" + } + }, + "required": [ + "range" + ], + "instillEditOnNodeFields": [ + "range" + ], + "instillFormat": "object", + "type": "object" + }, + { + "properties": { + "range": { + "const": "Sprints only", + "type": "string" + } + }, + "required": [ + "range" + ], + "instillEditOnNodeFields": [ + "range" + ], + "instillFormat": "object", + "type": "object" + }, + { + "properties": { + "range": { + "const": "Issues of an epic", + "type": "string" + }, + "epic-key": { + "title": "Epic Key", + "description": "The Key of the epic", + "instillShortDescription": "The Key of the epic", + "instillUIOrder": 0, + "instillFormat": "string", + "instillAcceptFormats": [ + "string" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "string" + } + }, + "required": [ + "range", + "epic-key" + ], + "instillEditOnNodeFields": [ + "range", + "epic-key" + ], + "instillFormat": "object", + "type": "object" + }, + { + "properties": { + "range": { + "const": "Issues of an sprint", + "type": "string" + }, + "sprint-key": { + "title": "Sprint Key", + "description": "The Key of the sprint", + "instillShortDescription": "The Key of the sprint", + "instillUIOrder": 0, + "instillFormat": "string", + "instillAcceptFormats": [ + "string" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "string" + } + }, + "required": [ + "range", + "sprint-key" + ], + "instillEditOnNodeFields": [ + "range", + "sprint-key" + ], + "instillFormat": "object", + "type": "object" + }, + { + "properties": { + "range": { + "const": "In backlog only", + "type": "string" + } + }, + "required": [ + "range" + ], + "instillEditOnNodeFields": [ + "range" + ], + "instillFormat": "object", + "type": "object" + }, + { + "properties": { + "range": { + "const": "Issues without epic assigned", + "type": "string" + } + }, + "required": [ + "range" + ], + "instillEditOnNodeFields": [ + "range" + ], + "instillFormat": "object", + "type": "object" + } + ] + }, + "start-at": { + "$ref": "#/$defs/common-query-params/start-at", + "instillUIOrder": 3 + }, + "max-results": { + "$ref": "#/$defs/common-query-params/max-results", + "instillUIOrder": 4 + } + }, + "required": [ + "board-id" + ], + "title": "Input", + "type": "object" + }, + "output": { + "description": "Get an issue in Jira", + "instillUIOrder": 0, + "properties": { + "issues": { + "description": "A array of issues in Jira", + "instillUIOrder": 1, + "title": "Issues", + "type": "array", + "items": { + "$ref": "#/$defs/issue" + } + }, + "start-at": { + "description": "The starting index of the returned boards. Base index: 0", + "instillUIOrder": 2, + "title": "Start At", + "instillFormat": "integer", + "type": "integer" + }, + "max-results": { + "description": "The maximum number of boards", + "instillUIOrder": 3, + "title": "Max Results", + "instillFormat": "integer", + "type": "integer" + }, + "total": { + "description": "The total number of boards", + "instillUIOrder": 4, + "title": "Total", + "instillFormat": "integer", + "type": "integer" + } + }, + "required": [ + "start-at", + "max-results", + "total" + ], + "title": "Output", + "type": "object" + } + }, "TASK_GET_ISSUE": { "description": "Get an issue in Jira", "instillShortDescription": "Get an issue in Jira", diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 7f3f5ae1..c319e583 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -4,7 +4,9 @@ import ( "context" _ "embed" "fmt" + "strings" + "github.com/go-resty/resty/v2" "github.com/instill-ai/component/base" "github.com/instill-ai/x/errmsg" "google.golang.org/protobuf/types/known/structpb" @@ -20,18 +22,76 @@ type Issue struct { IssueType string `json:"issue-type"` Status string `json:"status"` } +type SprintOrEpic struct { + ID int `json:"id"` + Key string `json:"key"` + Name string `json:"Name"` + Summary string `json:"summary"` + Self string `json:"self"` + Done bool `json:"done"` + Fields map[string]interface{} `json:"fields"` +} type GetIssueInput struct { IssueKeyOrID string `json:"issue-id-or-key,omitempty" struct:"issueIdOrKey"` UpdateHistory bool `json:"update-history,omitempty" struct:"updateHistory"` + FromBacklog bool `json:"from-backlog,omitempty" struct:"fromBacklog"` } type GetIssueOutput struct { Issue } +func extractIssue(issue *Issue) *Issue { + if issue.Description == "" && issue.Fields["description"] != nil { + description, ok := issue.Fields["description"].(string) + if ok { + issue.Description = description + } + } + if issue.Summary == "" && issue.Fields["summary"] != nil { + summary, ok := issue.Fields["summary"].(string) + if ok { + issue.Summary = summary + } + } + if issue.IssueType == "" && issue.Fields["issuetype"] != nil { + if issueType, ok := issue.Fields["issuetype"]; ok { + if issue.IssueType, ok = issueType.(map[string]interface{})["name"].(string); !ok { + issue.IssueType = "" + } + } + } + if issue.Status == "" && issue.Fields["status"] != nil { + if status, ok := issue.Fields["status"]; ok { + if issue.Status, ok = status.(map[string]interface{})["name"].(string); !ok { + issue.Status = "" + } + } + } + return issue +} + +func transformToIssue(val *SprintOrEpic) *Issue { + fields := make(map[string]interface{}) + if val.Fields != nil { + for key, value := range val.Fields { + fields[key] = value + } + } + + return &Issue{ + ID: fmt.Sprintf("%d", val.ID), + Key: val.Key, + Description: val.Name, + Summary: val.Summary, + Self: val.Self, + Fields: fields, + } +} + func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { var debug DebugSession - debug.SessionStart("getIssueTask", StaticVerboseLevel) + debug.SessionStart("getIssueTask", DevelopVerboseLevel) defer debug.SessionEnd() var opt GetIssueInput @@ -65,37 +125,216 @@ func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Stru debug.AddMapMessage("QueryParam", resp.Request.QueryParam) debug.AddMessage("Status", resp.Status()) - issue, ok := resp.Result().(*GetIssueOutput) + issue, ok := resp.Result().(*Issue) if !ok { return nil, errmsg.AddMessage( fmt.Errorf("failed to convert response to `Get Issue` Output"), fmt.Sprintf("failed to convert %v to `Get Issue` Output", resp.Result()), ) } + issue = extractIssue(issue) + issueOutput := GetIssueOutput{Issue: *issue} + return base.ConvertToStructpb(issueOutput) +} - if issue.Description == "" && issue.Fields["description"] != nil { - if issue.Description, ok = issue.Fields["description"].(string); !ok { - issue.Description = "" +type ListIssuesInput struct { + BoardID int `json:"board-id,omitempty" struct:"boardId"` + MaxResults int `json:"max-results,omitempty" struct:"maxResults"` + StartAt int `json:"start-at,omitempty" struct:"startAt"` + Range struct { + Range string `json:"range,omitempty"` + EpicKey string `json:"epic-key,omitempty"` + SprintKey string `json:"sprint-key,omitempty"` + } `json:"range,omitempty"` +} + +type ListIssuesResp struct { + Issues []Issue `json:"issues"` + Values []SprintOrEpic `json:"values"` + StartAt int `json:"start-at"` + MaxResults int `json:"max-results"` + Total int `json:"total"` +} +type ListIssuesOutput struct { + Issues []Issue `json:"issues"` + StartAt int `json:"start-at"` + MaxResults int `json:"max-results"` + Total int `json:"total"` +} + +func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { + var debug DebugSession + debug.SessionStart("listIssuesTask", DevelopVerboseLevel) + defer debug.SessionEnd() + + debug.AddRawMessage(props) + var opt ListIssuesInput + if err := base.ConvertFromStructpb(props, &opt); err != nil { + return nil, err + } + debug.AddMessage(fmt.Sprintf("GetIssueInput: %+v", opt)) + board, err := jiraClient.getBoard(ctx, opt.BoardID) + if err != nil { + return nil, err + } + debug.AddMapMessage("board", *board) + boardKey := strings.Split(board.Name, " ")[0] + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d", opt.BoardID) + switch opt.Range.Range { + case "All": + apiEndpoint = apiEndpoint + "/issue" + case "Epics only": + apiEndpoint = apiEndpoint + "/epic" + case "Sprints only": + apiEndpoint = apiEndpoint + "/sprint" + case "Issues of an epic": + return jiraClient.nextGenIssuesSearch(ctx, nextGenSearchRequest{ + JQL: fmt.Sprintf("project=\"%s\" AND parent=\"%s\"", boardKey, opt.Range.EpicKey), + MaxResults: opt.MaxResults, + StartAt: opt.StartAt, + }, + ) + case "Issues of a sprint": + return jiraClient.nextGenIssuesSearch(ctx, nextGenSearchRequest{ + JQL: fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", boardKey, opt.Range.EpicKey), + MaxResults: opt.MaxResults, + StartAt: opt.StartAt, + }, + ) + case "In backlog only": + apiEndpoint = apiEndpoint + "/backlog" + case "Issues without epic assigned": + apiEndpoint = apiEndpoint + "/epic/none/issue" + default: + return nil, errmsg.AddMessage( + fmt.Errorf("invalid range"), + fmt.Sprintf("%s is an invalid range", opt.Range.Range), + ) + } + + debug.AddMapMessage("opt", opt) + req := jiraClient.Client.R().SetResult(&ListIssuesResp{}) + + err = addQueryOptions(req, map[string]interface{}{ + "maxResults": opt.MaxResults, + "startAt": opt.StartAt, + }) + if err != nil { + return nil, err + } + debug.AddMessage("GET", apiEndpoint) + resp, err := req.Get(apiEndpoint) + debug.AddMessage("GET", apiEndpoint) + debug.AddMapMessage("QueryParam", resp.Request.QueryParam) + + if err != nil { + debug.AddMessage("Err", err.Error()) + debug.AddMessage("Err Message", errmsg.Message(err)) + if resp != nil && resp.StatusCode() == 404 { + return nil, fmt.Errorf( + err.Error(), + errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", + ) + } else { + return nil, err } } - if issue.Summary == "" && issue.Fields["summary"] != nil { - if issue.Summary, ok = issue.Fields["summary"].(string); !ok { - issue.Summary = "" + debug.AddMessage("Status", resp.Status()) + // debug.AddMessage("Result", fmt.Sprintf("%v", resp.Result())) + + issues, ok := resp.Result().(*ListIssuesResp) + if !ok { + return nil, errmsg.AddMessage( + fmt.Errorf("failed to convert response to `List Issue` Output"), + fmt.Sprintf("failed to convert %v to `List Issue` Output", resp.Result()), + ) + } + + if issues.Issues == nil && issues.Values == nil { + issues.Issues = []Issue{} + } else if issues.Issues == nil { + issues.Issues = make([]Issue, len(issues.Values)) + for i, val := range issues.Values { + issues.Issues[i] = *transformToIssue(&val) } } - if issue.IssueType == "" && issue.Fields["issuetype"] != nil { - if issueType, ok := issue.Fields["issuetype"]; ok { - if issue.IssueType, ok = issueType.(map[string]interface{})["name"].(string); !ok { - issue.IssueType = "" - } + + output := ListIssuesOutput{ + Issues: issues.Issues, + StartAt: issues.StartAt, + MaxResults: issues.MaxResults, + Total: issues.Total, + } + for _, issue := range output.Issues { + extractIssue(&issue) + } + return base.ConvertToStructpb(output) +} + +// https://support.atlassian.com/jira-software-cloud/docs/jql-fields/ +type nextGenSearchRequest struct { + JQL string `json:"jql,omitempty"` + MaxResults int `json:"maxResults,omitempty"` + StartAt int `json:"startAt,omitempty"` +} + +// https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-get +// https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-post +func (jiraClient *Client) nextGenIssuesSearch(_ context.Context, opt nextGenSearchRequest) (*structpb.Struct, error) { + var debug DebugSession + debug.SessionStart("nextGenIssuesSearch", DevelopVerboseLevel) + defer debug.SessionEnd() + + debug.AddRawMessage(opt) + var err error + apiEndpoint := "/rest/api/2/search" + + req := jiraClient.Client.R().SetResult(&ListIssuesResp{}) + + var resp *resty.Response + if len(opt.JQL) < 50 { + // filter seems not working + if err := addQueryOptions(req, opt); err != nil { + return nil, err } + resp, err = req.Get(apiEndpoint) + } else { + req.SetBody(opt) + resp, err = req.Post(apiEndpoint) } - if issue.Status == "" && issue.Fields["status"] != nil { - if status, ok := issue.Fields["status"]; ok { - if issue.Status, ok = status.(map[string]interface{})["name"].(string); !ok { - issue.Status = "" - } + + if err != nil { + return nil, err + } + debug.AddMessage("Status", resp.Status()) + + issues, ok := resp.Result().(*ListIssuesResp) + if !ok { + return nil, errmsg.AddMessage( + fmt.Errorf("failed to convert response to `List Issue` Output"), + fmt.Sprintf("failed to convert %v to `List Issue` Output", resp.Result()), + ) + } + + if issues.Issues == nil && issues.Values == nil { + issues.Issues = []Issue{} + } else if issues.Issues == nil { + issues.Issues = make([]Issue, len(issues.Values)) + for i, val := range issues.Values { + issues.Issues[i] = *transformToIssue(&val) } } - return base.ConvertToStructpb(issue) + + output := ListIssuesOutput{ + Issues: issues.Issues, + StartAt: issues.StartAt, + MaxResults: issues.MaxResults, + Total: issues.Total, + } + for _, issue := range output.Issues { + extractIssue(&issue) + } + return base.ConvertToStructpb(output) } + +var JQLReserveWords = []string{"a", "an", "abort", "access", "add", "after", "alias", "all", "alter", "and", "any", "are", "as", "asc", "at", "audit", "avg", "be", "before", "begin", "between", "boolean", "break", "but", "by", "byte", "catch", "cf", "char", "character", "check", "checkpoint", "collate", "collation", "column", "commit", "connect", "continue", "count", "create", "current", "date", "decimal", "declare", "decrement", "default", "defaults", "define", "delete", "delimiter", "desc", "difference", "distinct", "divide", "do", "double", "drop", "else", "empty", "encoding", "end", "equals", "escape", "exclusive", "exec", "execute", "exists", "explain", "false", "fetch", "file", "field", "first", "float", "for", "from", "function", "go", "goto", "grant", "greater", "group", "having", "identified", "if", "immediate", "in", "increment", "index", "initial", "inner", "inout", "input", "insert", "int", "integer", "intersect", "intersection", "into", "is", "isempty", "isnull", "it", "join", "last", "left", "less", "like", "limit", "lock", "long", "max", "min", "minus", "mode", "modify", "modulo", "more", "multiply", "next", "no", "noaudit", "not", "notin", "nowait", "null", "number", "object", "of", "on", "option", "or", "order", "outer", "output", "power", "previous", "prior", "privileges", "public", "raise", "raw", "remainder", "rename", "resource", "return", "returns", "revoke", "right", "row", "rowid", "rownum", "rows", "select", "session", "set", "share", "size", "sqrt", "start", "strict", "string", "subtract", "such", "sum", "synonym", "table", "that", "the", "their", "then", "there", "these", "they", "this", "to", "trans", "transaction", "trigger", "true", "uid", "union", "unique", "update", "user", "validate", "values", "view", "was", "when", "whenever", "where", "while", "will", "with"} diff --git a/application/jira/v0/main.go b/application/jira/v0/main.go index 0ec9cec3..3b694a4d 100644 --- a/application/jira/v0/main.go +++ b/application/jira/v0/main.go @@ -17,6 +17,7 @@ import ( const ( apiBaseURL = "https://api.atlassian.com" taskListBoards = "TASK_LIST_BOARDS" + taskListIssues = "TASK_LIST_ISSUES" taskGetIssue = "TASK_GET_ISSUE" taskGetSprint = "TASK_GET_SPRINT" ) @@ -74,6 +75,8 @@ func (c *component) CreateExecution(sysVars map[string]any, setup *structpb.Stru e.execute = e.client.getIssueTask case taskGetSprint: e.execute = e.client.getSprintTask + case taskListIssues: + e.execute = e.client.listIssuesTask default: return nil, errmsg.AddMessage( fmt.Errorf("not supported task: %s", task), From d16dcbbfcf36d8bbf5a977da8dab35c919defec3 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 15 Jul 2024 10:50:02 +0100 Subject: [PATCH 08/28] fix: fix advance search bug --- application/jira/v0/boards.go | 10 +- application/jira/v0/client.go | 6 +- application/jira/v0/config/tasks.json | 4 +- application/jira/v0/debug.go | 25 +++-- application/jira/v0/issues.go | 140 +++++++++++--------------- 5 files changed, 85 insertions(+), 100 deletions(-) diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index 589e819a..5a47e512 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -18,11 +18,11 @@ type Board struct { } type ListBoardsInput struct { - ProjectKeyOrID string `json:"project-key-or-id,omitempty" struct:"projectKeyOrID"` - BoardType string `json:"board-type,omitempty" struct:"type"` - Name string `json:"name,omitempty" struct:"name"` - StartAt int `json:"start-at,omitempty" struct:"startAt"` - MaxResults int `json:"max-results,omitempty" struct:"maxResults"` + ProjectKeyOrID string `json:"project-key-or-id,omitempty" api:"projectKeyOrID"` + BoardType string `json:"board-type,omitempty" api:"type"` + Name string `json:"name,omitempty" api:"name"` + StartAt int `json:"start-at,omitempty" api:"startAt"` + MaxResults int `json:"max-results,omitempty" api:"maxResults"` } type ListBoardsResp struct { Values []Board `json:"values"` diff --git a/application/jira/v0/client.go b/application/jira/v0/client.go index b9401e44..6da3bde5 100644 --- a/application/jira/v0/client.go +++ b/application/jira/v0/client.go @@ -120,7 +120,7 @@ func addQueryOptions(req *resty.Request, opt interface{}) error { continue } if stringVal == fmt.Sprintf("%v", reflect.Zero(reflect.TypeOf(val))) { - debug.AddMessage(key.String(), "Default value is not set. Skipping.") + debug.AddMessage(key.String(), "Query value is not set. Skipping.") continue } paramName := key.String() @@ -147,10 +147,10 @@ func addQueryOptions(req *resty.Request, opt interface{}) error { continue } if stringVal == fmt.Sprintf("%v", reflect.Zero(reflect.TypeOf(val))) { - debug.AddMessage(typeOfS.Field(i).Name, "Default value is not set. Skipping.") + debug.AddMessage(typeOfS.Field(i).Name, "Query value is not set. Skipping.") continue } - paramName := typeOfS.Field(i).Tag.Get("struct") + paramName := typeOfS.Field(i).Tag.Get("api") if paramName == "" { paramName = typeOfS.Field(i).Name } diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index fe6067cf..9ee705f6 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -527,7 +527,7 @@ } }, "TASK_GET_ISSUE": { - "description": "Get an issue in Jira", + "description": "Get an issue in Jira. The issue will only be returned if the user has permission to view it. Issues returned from this resource include Agile fields, like sprint, closedSprints, flagged, and epic.", "instillShortDescription": "Get an issue in Jira", "input": { "description": "Get an issue in Jira", @@ -636,7 +636,7 @@ } }, "TASK_GET_SPRINT": { - "description": "Get a sprint in Jira", + "description": "Get a sprint in Jira. The sprint will only be returned if the user can view the board that the sprint was created on, or view at least one of the issues in the sprint.", "instillShortDescription": "Get a sprint in Jira", "input": { "description": "Get an sprint in Jira", diff --git a/application/jira/v0/debug.go b/application/jira/v0/debug.go index ec1bde27..ed1b1bff 100644 --- a/application/jira/v0/debug.go +++ b/application/jira/v0/debug.go @@ -69,6 +69,8 @@ func (d *DebugSession) AddMapMessage(name string, m interface{}) { if v.Kind() == reflect.Ptr && v.IsNil() { d.AddMessage("Not a map") return + } else if v.Kind() == reflect.Ptr { + v = v.Elem() } mapVal := make(map[string]interface{}) if v.Kind() == reflect.Map { @@ -88,8 +90,11 @@ func (d *DebugSession) AddMapMessage(name string, m interface{}) { paramName := typeOfS.Field(i).Name mapVal[paramName] = val } + } else { + d.AddMessage("Not parseable as a map. Type: ", v.Kind().String()) + return } - d.addControlledMapMessage(mapVal, 0) + d.addInternalMapMessage(mapVal, 0) } func (d *DebugSession) AddRawMessage(m interface{}) { @@ -98,7 +103,7 @@ func (d *DebugSession) AddRawMessage(m interface{}) { fmt.Sprintf("[%s] %s%v", d.SessionID, strings.Repeat("\t", d.indentLevel), m)) } -func (d *DebugSession) addControlledMapMessage(m map[string]interface{}, depth int) { +func (d *DebugSession) addInternalMapMessage(m map[string]interface{}, depth int) { d.indentLevel++ defer func() { d.indentLevel-- @@ -110,18 +115,20 @@ func (d *DebugSession) addControlledMapMessage(m map[string]interface{}, depth i for k, v := range m { switch v := v.(type) { case map[string]interface{}: - d.AddMessage(k + ":") - d.addControlledMapMessage(v, depth+1) + d.AddMessage(k + ": {") + d.addInternalMapMessage(v, depth+1) + d.AddMessage("}") case []interface{}: - d.AddMessage(k + ":") - d.addControlledSliceMessage(v, depth+1) + d.AddMessage(k + ": {") + d.addInternalSliceMessage(v, depth+1) + d.AddMessage("}") default: d.AddMessage(fmt.Sprintf("%s: %v", k, v)) } } } -func (d *DebugSession) addControlledSliceMessage(s []interface{}, depth int) { +func (d *DebugSession) addInternalSliceMessage(s []interface{}, depth int) { d.indentLevel++ defer func() { d.indentLevel-- @@ -134,10 +141,10 @@ func (d *DebugSession) addControlledSliceMessage(s []interface{}, depth int) { switch v := v.(type) { case map[string]interface{}: d.AddMessage("-") - d.addControlledMapMessage(v, depth+1) + d.addInternalMapMessage(v, depth+1) case []interface{}: d.AddMessage("-") - d.addControlledSliceMessage(v, depth+1) + d.addInternalSliceMessage(v, depth+1) default: d.AddMessage(fmt.Sprintf("- %v", v)) } diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index c319e583..b2c00a78 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "fmt" + "slices" "strings" "github.com/go-resty/resty/v2" @@ -25,7 +26,7 @@ type Issue struct { type SprintOrEpic struct { ID int `json:"id"` Key string `json:"key"` - Name string `json:"Name"` + Name string `json:"name"` Summary string `json:"summary"` Self string `json:"self"` Done bool `json:"done"` @@ -101,7 +102,7 @@ func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Stru debug.AddMessage(fmt.Sprintf("GetIssueInput: %+v", opt)) apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", opt.IssueKeyOrID) - req := jiraClient.Client.R().SetResult(&GetIssueOutput{}) + req := jiraClient.Client.R().SetResult(&Issue{}) opt.IssueKeyOrID = "" // Remove from query params err := addQueryOptions(req, opt) @@ -124,7 +125,7 @@ func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Stru debug.AddMessage("GET", apiEndpoint) debug.AddMapMessage("QueryParam", resp.Request.QueryParam) debug.AddMessage("Status", resp.Status()) - + debug.AddMapMessage("resp.Result()", resp.Result()) issue, ok := resp.Result().(*Issue) if !ok { return nil, errmsg.AddMessage( @@ -138,9 +139,9 @@ func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Stru } type ListIssuesInput struct { - BoardID int `json:"board-id,omitempty" struct:"boardId"` - MaxResults int `json:"max-results,omitempty" struct:"maxResults"` - StartAt int `json:"start-at,omitempty" struct:"startAt"` + BoardID int `json:"board-id,omitempty" api:"boardId"` + MaxResults int `json:"max-results,omitempty" api:"maxResults"` + StartAt int `json:"start-at,omitempty" api:"startAt"` Range struct { Range string `json:"range,omitempty"` EpicKey string `json:"epic-key,omitempty"` @@ -168,11 +169,15 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St defer debug.SessionEnd() debug.AddRawMessage(props) - var opt ListIssuesInput + var ( + opt ListIssuesInput + jql string + ) + if err := base.ConvertFromStructpb(props, &opt); err != nil { return nil, err } - debug.AddMessage(fmt.Sprintf("GetIssueInput: %+v", opt)) + debug.AddMessage(fmt.Sprintf("ListIssuesInput: %+v", opt)) board, err := jiraClient.getBoard(ctx, opt.BoardID) if err != nil { return nil, err @@ -182,28 +187,27 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d", opt.BoardID) switch opt.Range.Range { case "All": + // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-issue-get apiEndpoint = apiEndpoint + "/issue" case "Epics only": + // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-epic-get apiEndpoint = apiEndpoint + "/epic" case "Sprints only": + // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-sprint-get apiEndpoint = apiEndpoint + "/sprint" case "Issues of an epic": - return jiraClient.nextGenIssuesSearch(ctx, nextGenSearchRequest{ - JQL: fmt.Sprintf("project=\"%s\" AND parent=\"%s\"", boardKey, opt.Range.EpicKey), - MaxResults: opt.MaxResults, - StartAt: opt.StartAt, - }, - ) + // API not working: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-epic-epicid-issue-get + // use JQL instead + jql = fmt.Sprintf("project=\"%s\" AND parent=\"%s\"", boardKey, opt.Range.EpicKey) case "Issues of a sprint": - return jiraClient.nextGenIssuesSearch(ctx, nextGenSearchRequest{ - JQL: fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", boardKey, opt.Range.EpicKey), - MaxResults: opt.MaxResults, - StartAt: opt.StartAt, - }, - ) + // API not working: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-sprint-sprintid-issue-get + // use JQL instead + jql = fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", boardKey, opt.Range.SprintKey) case "In backlog only": + // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-backlog-get apiEndpoint = apiEndpoint + "/backlog" case "Issues without epic assigned": + // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-epic-none-issue-get apiEndpoint = apiEndpoint + "/epic/none/issue" default: return nil, errmsg.AddMessage( @@ -212,35 +216,30 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St ) } - debug.AddMapMessage("opt", opt) - req := jiraClient.Client.R().SetResult(&ListIssuesResp{}) - - err = addQueryOptions(req, map[string]interface{}{ - "maxResults": opt.MaxResults, - "startAt": opt.StartAt, - }) - if err != nil { - return nil, err + var resp *resty.Response + if slices.Contains([]string{"Issues of an epic", "Issues of a sprint"}, opt.Range.Range) { + resp, err = jiraClient.nextGenIssuesSearch(ctx, nextGenSearchRequest{ + JQL: jql, + MaxResults: opt.MaxResults, + StartAt: opt.StartAt, + }, + ) + } else { + req := jiraClient.Client.R().SetResult(&ListIssuesResp{}) + err = addQueryOptions(req, map[string]interface{}{ + "maxResults": opt.MaxResults, + "startAt": opt.StartAt, + }) + if err != nil { + return nil, err + } + resp, err = req.Get(apiEndpoint) } - debug.AddMessage("GET", apiEndpoint) - resp, err := req.Get(apiEndpoint) - debug.AddMessage("GET", apiEndpoint) - debug.AddMapMessage("QueryParam", resp.Request.QueryParam) if err != nil { - debug.AddMessage("Err", err.Error()) - debug.AddMessage("Err Message", errmsg.Message(err)) - if resp != nil && resp.StatusCode() == 404 { - return nil, fmt.Errorf( - err.Error(), - errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", - ) - } else { - return nil, err - } + return nil, err } debug.AddMessage("Status", resp.Status()) - // debug.AddMessage("Result", fmt.Sprintf("%v", resp.Result())) issues, ok := resp.Result().(*ListIssuesResp) if !ok { @@ -265,35 +264,40 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St MaxResults: issues.MaxResults, Total: issues.Total, } - for _, issue := range output.Issues { - extractIssue(&issue) + for idx, issue := range output.Issues { + output.Issues[idx] = *extractIssue(&issue) + if opt.Range.Range == "Epics only" { + output.Issues[idx].IssueType = "Epic" + } else if opt.Range.Range == "Sprints only" { + output.Issues[idx].IssueType = "Sprint" + } } return base.ConvertToStructpb(output) } // https://support.atlassian.com/jira-software-cloud/docs/jql-fields/ type nextGenSearchRequest struct { - JQL string `json:"jql,omitempty"` - MaxResults int `json:"maxResults,omitempty"` - StartAt int `json:"startAt,omitempty"` + JQL string `json:"jql,omitempty" api:"jql"` + MaxResults int `json:"maxResults,omitempty" api:"maxResults"` + StartAt int `json:"startAt,omitempty" api:"startAt"` } // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-get // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-post -func (jiraClient *Client) nextGenIssuesSearch(_ context.Context, opt nextGenSearchRequest) (*structpb.Struct, error) { +func (jiraClient *Client) nextGenIssuesSearch(_ context.Context, opt nextGenSearchRequest) (*resty.Response, error) { var debug DebugSession debug.SessionStart("nextGenIssuesSearch", DevelopVerboseLevel) defer debug.SessionEnd() + debug.AddMessage("opt:") debug.AddRawMessage(opt) var err error apiEndpoint := "/rest/api/2/search" req := jiraClient.Client.R().SetResult(&ListIssuesResp{}) - var resp *resty.Response if len(opt.JQL) < 50 { - // filter seems not working + // 50 is an arbitrary number to determine if the JQL is too long to be a query param if err := addQueryOptions(req, opt); err != nil { return nil, err } @@ -306,35 +310,9 @@ func (jiraClient *Client) nextGenIssuesSearch(_ context.Context, opt nextGenSear if err != nil { return nil, err } + debug.AddMapMessage("Query", req.QueryParam) debug.AddMessage("Status", resp.Status()) - - issues, ok := resp.Result().(*ListIssuesResp) - if !ok { - return nil, errmsg.AddMessage( - fmt.Errorf("failed to convert response to `List Issue` Output"), - fmt.Sprintf("failed to convert %v to `List Issue` Output", resp.Result()), - ) - } - - if issues.Issues == nil && issues.Values == nil { - issues.Issues = []Issue{} - } else if issues.Issues == nil { - issues.Issues = make([]Issue, len(issues.Values)) - for i, val := range issues.Values { - issues.Issues[i] = *transformToIssue(&val) - } - } - - output := ListIssuesOutput{ - Issues: issues.Issues, - StartAt: issues.StartAt, - MaxResults: issues.MaxResults, - Total: issues.Total, - } - for _, issue := range output.Issues { - extractIssue(&issue) - } - return base.ConvertToStructpb(output) + return resp, nil } -var JQLReserveWords = []string{"a", "an", "abort", "access", "add", "after", "alias", "all", "alter", "and", "any", "are", "as", "asc", "at", "audit", "avg", "be", "before", "begin", "between", "boolean", "break", "but", "by", "byte", "catch", "cf", "char", "character", "check", "checkpoint", "collate", "collation", "column", "commit", "connect", "continue", "count", "create", "current", "date", "decimal", "declare", "decrement", "default", "defaults", "define", "delete", "delimiter", "desc", "difference", "distinct", "divide", "do", "double", "drop", "else", "empty", "encoding", "end", "equals", "escape", "exclusive", "exec", "execute", "exists", "explain", "false", "fetch", "file", "field", "first", "float", "for", "from", "function", "go", "goto", "grant", "greater", "group", "having", "identified", "if", "immediate", "in", "increment", "index", "initial", "inner", "inout", "input", "insert", "int", "integer", "intersect", "intersection", "into", "is", "isempty", "isnull", "it", "join", "last", "left", "less", "like", "limit", "lock", "long", "max", "min", "minus", "mode", "modify", "modulo", "more", "multiply", "next", "no", "noaudit", "not", "notin", "nowait", "null", "number", "object", "of", "on", "option", "or", "order", "outer", "output", "power", "previous", "prior", "privileges", "public", "raise", "raw", "remainder", "rename", "resource", "return", "returns", "revoke", "right", "row", "rowid", "rownum", "rows", "select", "session", "set", "share", "size", "sqrt", "start", "strict", "string", "subtract", "such", "sum", "synonym", "table", "that", "the", "their", "then", "there", "these", "they", "this", "to", "trans", "transaction", "trigger", "true", "uid", "union", "unique", "update", "user", "validate", "values", "view", "was", "when", "whenever", "where", "while", "will", "with"} +var JQLReservedWords = []string{"a", "an", "abort", "access", "add", "after", "alias", "all", "alter", "and", "any", "are", "as", "asc", "at", "audit", "avg", "be", "before", "begin", "between", "boolean", "break", "but", "by", "byte", "catch", "cf", "char", "character", "check", "checkpoint", "collate", "collation", "column", "commit", "connect", "continue", "count", "create", "current", "date", "decimal", "declare", "decrement", "default", "defaults", "define", "delete", "delimiter", "desc", "difference", "distinct", "divide", "do", "double", "drop", "else", "empty", "encoding", "end", "equals", "escape", "exclusive", "exec", "execute", "exists", "explain", "false", "fetch", "file", "field", "first", "float", "for", "from", "function", "go", "goto", "grant", "greater", "group", "having", "identified", "if", "immediate", "in", "increment", "index", "initial", "inner", "inout", "input", "insert", "int", "integer", "intersect", "intersection", "into", "is", "isempty", "isnull", "it", "join", "last", "left", "less", "like", "limit", "lock", "long", "max", "min", "minus", "mode", "modify", "modulo", "more", "multiply", "next", "no", "noaudit", "not", "notin", "nowait", "null", "number", "object", "of", "on", "option", "or", "order", "outer", "output", "power", "previous", "prior", "privileges", "public", "raise", "raw", "remainder", "rename", "resource", "return", "returns", "revoke", "right", "row", "rowid", "rownum", "rows", "select", "session", "set", "share", "size", "sqrt", "start", "strict", "string", "subtract", "such", "sum", "synonym", "table", "that", "the", "their", "then", "there", "these", "they", "this", "to", "trans", "transaction", "trigger", "true", "uid", "union", "unique", "update", "user", "validate", "values", "view", "was", "when", "whenever", "where", "while", "will", "with"} From 8bb0730c32ba8fccb6aeba2d4f1d79d4773ce93d Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 15 Jul 2024 11:00:41 +0100 Subject: [PATCH 09/28] fix: fix typo --- application/jira/v0/boards.go | 4 ++-- application/jira/v0/config/tasks.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index 5a47e512..b88ad6e3 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -18,7 +18,7 @@ type Board struct { } type ListBoardsInput struct { - ProjectKeyOrID string `json:"project-key-or-id,omitempty" api:"projectKeyOrID"` + ProjectKeyOrID string `json:"project-key-or-id,omitempty" api:"projectKeyOrId"` BoardType string `json:"board-type,omitempty" api:"type"` Name string `json:"name,omitempty" api:"name"` StartAt int `json:"start-at,omitempty" api:"startAt"` @@ -64,7 +64,7 @@ func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.St func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (*ListBoardsResp, error) { var debug DebugSession - debug.SessionStart("listBoards", StaticVerboseLevel) + debug.SessionStart("listBoards", DevelopVerboseLevel) defer debug.SessionEnd() apiEndpoint := "rest/agile/1.0/board" diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 9ee705f6..5b75b32c 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -142,8 +142,8 @@ "type": "string" }, "board-type": { - "default": "scrum", - "description": "The type of board, can be: scrum, kanban. Default is scrum", + "default": "simple", + "description": "The type of board, can be: scrum, kanban, simple. Default is simple", "instillUIOrder": 1, "enum": [ "scrum", From 56d4b3d00bd62bb188832f57c0a721f3f36591a5 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Tue, 16 Jul 2024 20:19:07 +0100 Subject: [PATCH 10/28] feat: add list board test --- application/jira/v0/boards.go | 10 +- application/jira/v0/component_test.go | 129 ++++++++++++++++++++++ application/jira/v0/config/tasks.json | 17 +++ application/jira/v0/debug.go | 30 ++++-- application/jira/v0/issues.go | 21 ++-- application/jira/v0/main.go | 148 +++++++++++++++++++++++++- application/jira/v0/mock_database.go | 38 +++++++ application/jira/v0/mock_server.go | 111 +++++++++++++++++++ 8 files changed, 471 insertions(+), 33 deletions(-) create mode 100644 application/jira/v0/component_test.go create mode 100644 application/jira/v0/mock_database.go create mode 100644 application/jira/v0/mock_server.go diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index b88ad6e3..25605c78 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -77,16 +77,8 @@ func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (* } resp, err := req.Get(apiEndpoint) - if resp != nil && resp.StatusCode() == 404 { - return nil, fmt.Errorf( - err.Error(), - errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", - ) - } if err != nil { - return nil, fmt.Errorf( - err.Error(), errmsg.Message(err), - ) + return nil, err } debug.AddMessage("GET", apiEndpoint) debug.AddMapMessage("QueryParam", resp.Request.QueryParam) diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go new file mode 100644 index 00000000..30b772b7 --- /dev/null +++ b/application/jira/v0/component_test.go @@ -0,0 +1,129 @@ +package jira + +import ( + "context" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + qt "github.com/frankban/quicktest" + "github.com/instill-ai/component/base" + "github.com/instill-ai/component/internal/util/httpclient" + "go.uber.org/zap" + "google.golang.org/protobuf/types/known/structpb" +) + +const ( + email = "testemail@gmail.com" + token = "testToken" + errResp = `{"message": "Bad request"}` + okResp = `{"title": "Be the wheel"}` +) + +type TaskCase[inType any, outType any] struct { + _type string + name string + input inType + wantResp outType + wantErr string +} + +func TestComponent_ListBoardsTask(t *testing.T) { + testcases := []TaskCase[ListBoardsInput, ListBoardsOutput]{ + { + _type: "ok", + name: "get all boards", + input: ListBoardsInput{ + MaxResults: 10, + StartAt: 0, + }, + wantResp: ListBoardsOutput{ + Total: 1, + StartAt: 0, + MaxResults: 10, + IsLast: true, + Boards: []Board{ + { + ID: 3, + Name: "TST", + BoardType: "simple", + Self: "https://test.atlassian.net/rest/agile/1.0/board/3", + }, + }, + }, + }, + { + _type: "ok", + name: "get filtered boards", + input: ListBoardsInput{ + MaxResults: 10, + StartAt: 1, + BoardType: "kanban", + }, + wantResp: ListBoardsOutput{ + Total: 1, + StartAt: 1, + MaxResults: 10, + IsLast: true, + Boards: []Board{}, + }, + }, + { + _type: "nok", + name: "400 - Not Found", + input: ListBoardsInput{ + MaxResults: 10, + StartAt: 1, + ProjectKeyOrID: "test", + }, + wantErr: "unsuccessful HTTP response", + }, + } + taskTesting(testcases, taskListBoards, t) +} + +func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType], task string, t *testing.T) { + c := qt.New(t) + ctx := context.Background() + bc := base.Component{Logger: zap.NewNop()} + connector := Init(bc) + + for _, tc := range testcases { + c.Run(tc._type+`-`+tc.name, func(c *qt.C) { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/_edge/tenant_info" { + auth := base64.StdEncoding.EncodeToString([]byte(email + ":" + token)) + c.Check(r.Header.Get("Authorization"), qt.Equals, "Basic "+auth) + } + w.Header().Set("Content-Type", httpclient.MIMETypeJSON) + router(w, r) + }) + + srv := httptest.NewServer(h) + c.Cleanup(srv.Close) + + setup, err := structpb.NewStruct(map[string]any{ + "token": token, + "email": email, + "base-url": srv.URL, + }) + c.Assert(err, qt.IsNil) + + exec, err := connector.CreateExecution(nil, setup, task) + c.Assert(err, qt.IsNil) + pbIn, err := base.ConvertToStructpb(tc.input) + c.Assert(err, qt.IsNil) + + got, err := exec.Execution.Execute(ctx, []*structpb.Struct{pbIn}) + if tc.wantErr != "" { + c.Assert(err, qt.ErrorMatches, tc.wantErr) + return + } + wantJSON, err := json.Marshal(tc.wantResp) + c.Assert(err, qt.IsNil) + c.Check(wantJSON, qt.JSONEquals, got[0].AsMap()) + }) + } +} diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 5b75b32c..16df760e 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -322,6 +322,23 @@ "range": { "const": "All", "type": "string" + }, + "epic-key": { + "default": "optional", + "title": "Epic Key (Optional field)", + "description": "Optional field", + "instillShortDescription": "Optional field", + "instillUIOrder": 0, + "instillFormat": "string", + "instillAcceptFormats": [ + "string" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "string" } }, "required": [ diff --git a/application/jira/v0/debug.go b/application/jira/v0/debug.go index ed1b1bff..2c710021 100644 --- a/application/jira/v0/debug.go +++ b/application/jira/v0/debug.go @@ -85,7 +85,6 @@ func (d *DebugSession) AddMapMessage(name string, m interface{}) { if !v.Field(i).IsValid() || !v.Field(i).CanInterface() { continue } - val := v.Field(i).Interface() paramName := typeOfS.Field(i).Name mapVal[paramName] = val @@ -104,10 +103,7 @@ func (d *DebugSession) AddRawMessage(m interface{}) { } func (d *DebugSession) addInternalMapMessage(m map[string]interface{}, depth int) { - d.indentLevel++ - defer func() { - d.indentLevel-- - }() + defer d.Indent()() if depth > d.maxDepth { d.AddMessage("...") return @@ -129,10 +125,8 @@ func (d *DebugSession) addInternalMapMessage(m map[string]interface{}, depth int } func (d *DebugSession) addInternalSliceMessage(s []interface{}, depth int) { - d.indentLevel++ - defer func() { - d.indentLevel-- - }() + defer d.Indent()() + if depth > d.maxDepth { d.AddMessage("...") return @@ -164,6 +158,24 @@ func (d *DebugSession) SessionEnd() { d.Messages = append(d.Messages, endBanner) } +func (d *DebugSession) IncrementIndent() { + d.indentLevel++ +} +func (d *DebugSession) DecrementIndent() { + d.indentLevel-- +} +func (d *DebugSession) Indent() func() { + d.IncrementIndent() + return d.DecrementIndent +} + +func (d *DebugSession) Separator() { + if Verbose < d.verboseLevel { + return + } + d.Messages = append(d.Messages, strings.Repeat("=", d.halfBannerLen*2+len(d.SessionID)+2)) +} + func (d *DebugSession) flush() { if Verbose < d.verboseLevel { return diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index b2c00a78..386cba5a 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -138,15 +138,16 @@ func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Stru return base.ConvertToStructpb(issueOutput) } +type Range struct { + Range string `json:"range,omitempty"` + EpicKey string `json:"epic-key,omitempty"` + SprintKey string `json:"sprint-key,omitempty"` +} type ListIssuesInput struct { - BoardID int `json:"board-id,omitempty" api:"boardId"` - MaxResults int `json:"max-results,omitempty" api:"maxResults"` - StartAt int `json:"start-at,omitempty" api:"startAt"` - Range struct { - Range string `json:"range,omitempty"` - EpicKey string `json:"epic-key,omitempty"` - SprintKey string `json:"sprint-key,omitempty"` - } `json:"range,omitempty"` + BoardID int `json:"board-id,omitempty" api:"boardId"` + MaxResults int `json:"max-results,omitempty" api:"maxResults"` + StartAt int `json:"start-at,omitempty" api:"startAt"` + Range Range `json:"range,omitempty"` } type ListIssuesResp struct { @@ -168,7 +169,7 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St debug.SessionStart("listIssuesTask", DevelopVerboseLevel) defer debug.SessionEnd() - debug.AddRawMessage(props) + debug.AddMapMessage("props", props) var ( opt ListIssuesInput jql string @@ -177,7 +178,7 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St if err := base.ConvertFromStructpb(props, &opt); err != nil { return nil, err } - debug.AddMessage(fmt.Sprintf("ListIssuesInput: %+v", opt)) + debug.AddMapMessage("ListIssuesInput", opt) board, err := jiraClient.getBoard(ctx, opt.BoardID) if err != nil { return nil, err diff --git a/application/jira/v0/main.go b/application/jira/v0/main.go index 3b694a4d..230a78e3 100644 --- a/application/jira/v0/main.go +++ b/application/jira/v0/main.go @@ -71,12 +71,12 @@ func (c *component) CreateExecution(sysVars map[string]any, setup *structpb.Stru switch task { case taskListBoards: e.execute = e.client.listBoardsTask + case taskListIssues: + e.execute = e.client.listIssuesTask case taskGetIssue: e.execute = e.client.getIssueTask case taskGetSprint: e.execute = e.client.getSprintTask - case taskListIssues: - e.execute = e.client.listIssuesTask default: return nil, errmsg.AddMessage( fmt.Errorf("not supported task: %s", task), @@ -87,8 +87,7 @@ func (c *component) CreateExecution(sysVars map[string]any, setup *structpb.Stru return &base.ExecutionWrapper{Execution: e}, nil } -func (e *execution) fillInDefaultValues(input *structpb.Struct) (*structpb.Struct, error) { - task := e.Task +func (e *execution) getInputSchemaJSON(task string) (map[string]interface{}, error) { taskSpec, ok := e.Component.GetTaskInputSchemas()[task] if !ok { return nil, errmsg.AddMessage( @@ -105,12 +104,116 @@ func (e *execution) fillInDefaultValues(input *structpb.Struct) (*structpb.Struc ) } inputMap := taskSpecMap["properties"].(map[string]interface{}) - for key, value := range inputMap { + return inputMap, nil +} +func (e *execution) fillInDefaultValues(input *structpb.Struct) (*structpb.Struct, error) { + inputMap, err := e.getInputSchemaJSON(e.Task) + if err != nil { + return nil, err + } + return e.fillInDefaultValuesWithReference(input, inputMap) +} +func hasNextLevel(valueMap map[string]interface{}) bool { + if valType, ok := valueMap["type"]; ok { + if valType != "object" { + return false + } + } + if _, ok := valueMap["properties"]; ok { + return true + } + for _, target := range []string{"allOf", "anyOf", "oneOf"} { + if _, ok := valueMap[target]; ok { + items := valueMap[target].([]interface{}) + for _, v := range items { + if _, ok := v.(map[string]interface{})["properties"].(map[string]interface{}); ok { + return true + } + } + } + } + return false +} +func optionMatch(valueMap *structpb.Struct, reference map[string]interface{}, checkFields []string) bool { + for _, checkField := range checkFields { + if _, ok := valueMap.GetFields()[checkField]; !ok { + return false + } + if val, ok := reference[checkField].(map[string]interface{})["const"]; ok { + if valueMap.GetFields()[checkField].GetStringValue() != val { + return false + } + } + } + return true +} +func (e *execution) fillInDefaultValuesWithReference(input *structpb.Struct, reference map[string]interface{}) (*structpb.Struct, error) { + for key, value := range reference { valueMap, ok := value.(map[string]interface{}) if !ok { continue } if _, ok := valueMap["default"]; !ok { + if !hasNextLevel(valueMap) { + continue + } + if _, ok := input.GetFields()[key]; !ok { + input.GetFields()[key] = &structpb.Value{ + Kind: &structpb.Value_StructValue{ + StructValue: &structpb.Struct{ + Fields: make(map[string]*structpb.Value), + }, + }, + } + } + var properties map[string]interface{} + if _, ok := valueMap["properties"]; !ok { + var requiredFieldsRaw []interface{} + if requiredFieldsRaw, ok = valueMap["required"].([]interface{}); !ok { + continue + } + requiredFields := make([]string, len(requiredFieldsRaw)) + for idx, v := range requiredFieldsRaw { + requiredFields[idx] = fmt.Sprintf("%v", v) + } + for _, target := range []string{"allOf", "anyOf", "oneOf"} { + var items []interface{} + if items, ok = valueMap[target].([]interface{}); !ok { + continue + } + for _, v := range items { + if properties, ok = v.(map[string]interface{})["properties"].(map[string]interface{}); !ok { + continue + } + inputSubField := input.GetFields()[key].GetStructValue() + if target == "oneOf" && !optionMatch(inputSubField, properties, requiredFields) { + continue + } + subField, err := e.fillInDefaultValuesWithReference(inputSubField, properties) + if err != nil { + return nil, err + } + input.GetFields()[key] = &structpb.Value{ + Kind: &structpb.Value_StructValue{ + StructValue: subField, + }, + } + } + } + } else { + if properties, ok = valueMap["properties"].(map[string]interface{}); !ok { + continue + } + subField, err := e.fillInDefaultValuesWithReference(input.GetFields()[key].GetStructValue(), properties) + if err != nil { + return nil, err + } + input.GetFields()[key] = &structpb.Value{ + Kind: &structpb.Value_StructValue{ + StructValue: subField, + }, + } + } continue } if _, ok := input.GetFields()[key]; ok { @@ -137,6 +240,41 @@ func (e *execution) fillInDefaultValues(input *structpb.Struct) (*structpb.Struc BoolValue: defaultValue.(bool), }, } + case "array": + input.GetFields()[key] = &structpb.Value{ + Kind: &structpb.Value_ListValue{ + ListValue: &structpb.ListValue{ + Values: []*structpb.Value{}, + }, + }, + } + itemType := valueMap["items"].(map[string]interface{})["type"] + switch itemType { + case "string": + for _, v := range defaultValue.([]interface{}) { + input.GetFields()[key].GetListValue().Values = append(input.GetFields()[key].GetListValue().Values, &structpb.Value{ + Kind: &structpb.Value_StringValue{ + StringValue: fmt.Sprintf("%v", v), + }, + }) + } + case "integer", "number": + for _, v := range defaultValue.([]interface{}) { + input.GetFields()[key].GetListValue().Values = append(input.GetFields()[key].GetListValue().Values, &structpb.Value{ + Kind: &structpb.Value_NumberValue{ + NumberValue: v.(float64), + }, + }) + } + case "boolean": + for _, v := range defaultValue.([]interface{}) { + input.GetFields()[key].GetListValue().Values = append(input.GetFields()[key].GetListValue().Values, &structpb.Value{ + Kind: &structpb.Value_BoolValue{ + BoolValue: v.(bool), + }, + }) + } + } } } return input, nil diff --git a/application/jira/v0/mock_database.go b/application/jira/v0/mock_database.go new file mode 100644 index 00000000..8c50c467 --- /dev/null +++ b/application/jira/v0/mock_database.go @@ -0,0 +1,38 @@ +package jira + +import "fmt" + +var fakeBoards = []FakeBoard{ + { + Board: Board{ + ID: 1, + Name: "KAN", + BoardType: "kanban", + }, + }, + { + Board: Board{ + ID: 2, + Name: "SCR", + BoardType: "scrum", + }, + }, + { + Board: Board{ + ID: 3, + Name: "TST", + BoardType: "simple", + }, + }, +} + +type FakeBoard struct { + Board +} + +func (f *FakeBoard) getSelf() string { + if f.Self == "" { + f.Self = fmt.Sprintf("https://test.atlassian.net/rest/agile/1.0/board/%d", f.ID) + } + return f.Self +} diff --git a/application/jira/v0/mock_server.go b/application/jira/v0/mock_server.go new file mode 100644 index 00000000..890b7748 --- /dev/null +++ b/application/jira/v0/mock_server.go @@ -0,0 +1,111 @@ +package jira + +import ( + "fmt" + "net/http" + "strconv" + "strings" +) + +func router(res http.ResponseWriter, req *http.Request) { + path := req.URL.Path + var logger DebugSession + logger.SessionStart("router", StaticVerboseLevel) + defer logger.SessionEnd() + switch path { + case "/_edge/tenant_info": + res.WriteHeader(http.StatusOK) + _, err := res.Write([]byte(`{"id":"12345678-1234-1234-1234-123456789012"}`)) + if err != nil { + fmt.Println("/_edge/tenant_info", err) + } + case "/rest/agile/1.0/board": + mockListBoards(res, req) + case "/rest/agile/1.0/board/1": + case "/rest/api/2/issue": + default: + http.NotFound(res, req) + } +} + +func mockListBoards(res http.ResponseWriter, req *http.Request) { + var err error + opt := req.URL.Query() + boardType := opt.Get("type") + startAt := opt.Get("startAt") + maxResults := opt.Get("maxResults") + name := opt.Get("name") + projectKeyOrID := opt.Get("projectKeyOrId") + // filter boards + var boards []FakeBoard + pjNotFound := projectKeyOrID != "" + for _, board := range fakeBoards { + if boardType != "" && board.BoardType != boardType { + continue + } + if name != "" && !strings.Contains(board.Name, name) { + continue + } + if projectKeyOrID != "" { + if !strings.EqualFold(board.Name, projectKeyOrID) { + continue + } + pjNotFound = false + } + boards = append(boards, board) + } + if pjNotFound { + res.WriteHeader(http.StatusBadRequest) + _, err := res.Write([]byte(fmt.Sprintf(`{"errorMessages":["No project could be found with key or id '%s'"]}`, projectKeyOrID))) + if err != nil { + fmt.Println("/rest/agile/1.0/board", err) + } + return + } + // pagination + start, end := 0, len(boards) + if startAt != "" { + start, err = strconv.Atoi(startAt) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + _, err := res.Write([]byte(`{"errorMessages":["The 'startAt' parameter must be a number"]}`)) + if err != nil { + fmt.Println("/rest/agile/1.0/board", err) + } + return + } + } + maxResultsNum := len(boards) + if maxResults != "" { + maxResultsNum, err = strconv.Atoi(maxResults) + if err != nil { + res.WriteHeader(http.StatusBadRequest) + _, err := res.Write([]byte(`{"errorMessages":["The 'maxResults' parameter must be a number"]}`)) + if err != nil { + fmt.Println("/rest/agile/1.0/board", err) + } + return + } + end = start + maxResultsNum + if end > len(boards) { + end = len(boards) + } + } + // response + res.WriteHeader(http.StatusOK) + respText := `{"values":[` + if len(boards) != 0 { + for i, board := range boards[start:end] { + if i > 0 { + respText += "," + } + respText += fmt.Sprintf(`{"id":%d,"name":"%s","type":"%s","self":"%s"}`, board.ID, board.Name, board.BoardType, board.getSelf()) + } + } + respText += `],` + respText += `"total":` + strconv.Itoa(len(boards)) + `,"startAt":` + strconv.Itoa(start) + `,"maxResults":` + strconv.Itoa(maxResultsNum) + `,"isLast":` + strconv.FormatBool(end == len(boards)) + `}` + _, err = res.Write([]byte(respText)) + if err != nil { + fmt.Println("/rest/agile/1.0/board", err) + } +} From de035599095a28713c7784e9f6677fbca0977f7c Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 11:49:14 +0100 Subject: [PATCH 11/28] feat: add test for get issue and sprint --- application/jira/v0/component_test.go | 142 ++++++++++++++++++++++++-- application/jira/v0/issues.go | 1 - application/jira/v0/mock_database.go | 116 ++++++++++++++++++++- application/jira/v0/mock_server.go | 116 ++++++++++++++++++--- application/jira/v0/sprint.go | 4 +- go.mod | 1 + go.sum | 2 + 7 files changed, 347 insertions(+), 35 deletions(-) diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index 30b772b7..a808452f 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -10,7 +10,6 @@ import ( qt "github.com/frankban/quicktest" "github.com/instill-ai/component/base" - "github.com/instill-ai/component/internal/util/httpclient" "go.uber.org/zap" "google.golang.org/protobuf/types/known/structpb" ) @@ -78,12 +77,123 @@ func TestComponent_ListBoardsTask(t *testing.T) { StartAt: 1, ProjectKeyOrID: "test", }, - wantErr: "unsuccessful HTTP response", + wantErr: "unsuccessful HTTP response.*", }, } taskTesting(testcases, taskListBoards, t) } +func TestComponent_GetIssueTask(t *testing.T) { + testcases := []TaskCase[GetIssueInput, GetIssueOutput]{ + { + _type: "ok", + name: "get issue-Task", + input: GetIssueInput{ + IssueKeyOrID: "1", + UpdateHistory: true, + }, + wantResp: GetIssueOutput{ + Issue: Issue{ + ID: "1", + Key: "TST-1", + Fields: map[string]interface{}{ + "summary": "Test issue 1", + "description": "Test description 1", + "status": map[string]interface{}{ + "name": "To Do", + }, + "issuetype": map[string]interface{}{ + "name": "Task", + }, + }, + Self: "https://test.atlassian.net/rest/agile/1.0/issue/1", + Summary: "Test issue 1", + Status: "To Do", + Description: "Test description 1", + IssueType: "Task", + }, + }, + }, + { + _type: "ok", + name: "get issue-Epic", + input: GetIssueInput{ + IssueKeyOrID: "4", + UpdateHistory: false, + }, + wantResp: GetIssueOutput{ + Issue: Issue{ + ID: "4", + Key: "KAN-4", + Fields: map[string]interface{}{ + "summary": "Test issue 4", + "status": map[string]interface{}{ + "name": "Done", + }, + "issuetype": map[string]interface{}{ + "name": "Epic", + }, + }, + Self: "https://test.atlassian.net/rest/agile/1.0/issue/4", + Summary: "Test issue 4", + Status: "Done", + IssueType: "Epic", + }, + }, + }, + { + _type: "nok", + name: "404 - Not Found", + input: GetIssueInput{ + IssueKeyOrID: "5", + UpdateHistory: true, + }, + wantErr: "unsuccessful HTTP response.*", + }, + } + taskTesting(testcases, taskGetIssue, t) +} + +func TestComponent_GetSprintTask(t *testing.T) { + testcases := []TaskCase[GetSprintInput, GetSprintOutput]{ + { + _type: "ok", + name: "get sprint", + input: GetSprintInput{ + SprintID: 1, + }, + wantResp: GetSprintOutput{ + ID: 1, + Self: "https://test.atlassian.net/rest/agile/1.0/sprint/1", + State: "active", + Name: "Sprint 1", + StartDate: "2021-01-01T00:00:00.000Z", + EndDate: "2021-01-15T00:00:00.000Z", + CompleteDate: "2021-01-15T00:00:00.000Z", + OriginBoardID: 1, + Goal: "Sprint goal", + }, + }, + { + _type: "nok", + name: "400 - Bad Request", + input: GetSprintInput{ + SprintID: -1, + }, + wantErr: "unsuccessful HTTP response.*", + }, + { + _type: "nok", + name: "404 - Not Found", + input: GetSprintInput{ + SprintID: 2, + }, + wantErr: "unsuccessful HTTP response.*", + }, + } + taskTesting(testcases, taskGetSprint, t) +} + func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType], task string, t *testing.T) { c := qt.New(t) ctx := context.Background() @@ -92,16 +202,24 @@ func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType], for _, tc := range testcases { c.Run(tc._type+`-`+tc.name, func(c *qt.C) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/_edge/tenant_info" { - auth := base64.StdEncoding.EncodeToString([]byte(email + ":" + token)) - c.Check(r.Header.Get("Authorization"), qt.Equals, "Basic "+auth) + authenticationMiddleware := func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/_edge/tenant_info" { + auth := base64.StdEncoding.EncodeToString([]byte(email + ":" + token)) + c.Check(r.Header.Get("Authorization"), qt.Equals, "Basic "+auth) + } + next.ServeHTTP(w, r) } - w.Header().Set("Content-Type", httpclient.MIMETypeJSON) - router(w, r) - }) - - srv := httptest.NewServer(h) + return http.HandlerFunc(fn) + } + setContentTypeMiddleware := func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } + srv := httptest.NewServer(router(authenticationMiddleware, setContentTypeMiddleware)) c.Cleanup(srv.Close) setup, err := structpb.NewStruct(map[string]any{ @@ -121,8 +239,10 @@ func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType], c.Assert(err, qt.ErrorMatches, tc.wantErr) return } + c.Assert(err, qt.IsNil) wantJSON, err := json.Marshal(tc.wantResp) c.Assert(err, qt.IsNil) + c.Assert(got, qt.HasLen, 1) c.Check(wantJSON, qt.JSONEquals, got[0].AsMap()) }) } diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 386cba5a..a52781de 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -36,7 +36,6 @@ type SprintOrEpic struct { type GetIssueInput struct { IssueKeyOrID string `json:"issue-id-or-key,omitempty" struct:"issueIdOrKey"` UpdateHistory bool `json:"update-history,omitempty" struct:"updateHistory"` - FromBacklog bool `json:"from-backlog,omitempty" struct:"fromBacklog"` } type GetIssueOutput struct { Issue diff --git a/application/jira/v0/mock_database.go b/application/jira/v0/mock_database.go index 8c50c467..34b2339c 100644 --- a/application/jira/v0/mock_database.go +++ b/application/jira/v0/mock_database.go @@ -1,6 +1,19 @@ package jira -import "fmt" +import ( + "fmt" +) + +type FakeBoard struct { + Board +} + +func (f *FakeBoard) getSelf() string { + if f.Self == "" { + f.Self = fmt.Sprintf("https://test.atlassian.net/rest/agile/1.0/board/%d", f.ID) + } + return f.Self +} var fakeBoards = []FakeBoard{ { @@ -26,13 +39,106 @@ var fakeBoards = []FakeBoard{ }, } -type FakeBoard struct { - Board +type FakeIssue struct { + ID string `json:"id"` + Key string `json:"key"` + Self string `json:"self"` + Fields map[string]interface{} `json:"fields"` } -func (f *FakeBoard) getSelf() string { +func (f *FakeIssue) getSelf() string { if f.Self == "" { - f.Self = fmt.Sprintf("https://test.atlassian.net/rest/agile/1.0/board/%d", f.ID) + f.Self = fmt.Sprintf("https://test.atlassian.net/rest/agile/1.0/issue/%s", f.ID) + } + return f.Self +} + +var fakeIssues = []FakeIssue{ + { + ID: "1", + Key: "TST-1", + Fields: map[string]interface{}{ + "summary": "Test issue 1", + "description": "Test description 1", + "status": map[string]interface{}{ + "name": "To Do", + }, + "issuetype": map[string]interface{}{ + "name": "Task", + }, + }, + }, + { + ID: "2", + Key: "TST-2", + Fields: map[string]interface{}{ + "summary": "Test issue 2", + "description": "Test description 2", + "status": map[string]interface{}{ + "name": "In Progress", + }, + "issuetype": map[string]interface{}{ + "name": "Task", + }, + }, + }, + { + ID: "3", + Key: "TST-3", + Fields: map[string]interface{}{ + "summary": "Test issue 3", + "description": "Test description 3", + "status": map[string]interface{}{ + "name": "Done", + }, + "issuetype": map[string]interface{}{ + "name": "Task", + }, + }, + }, + { + ID: "4", + Key: "KAN-4", + Fields: map[string]interface{}{ + "summary": "Test issue 4", + "status": map[string]interface{}{ + "name": "Done", + }, + "issuetype": map[string]interface{}{ + "name": "Epic", + }, + }, + }, +} + +type FakeSprint struct { + ID int `json:"id"` + Self string `json:"self"` + State string `json:"state"` + Name string `json:"name"` + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + CompleteDate string `json:"completeDate"` + OriginBoardID int `json:"originBoardId"` + Goal string `json:"goal"` +} + +func (f *FakeSprint) getSelf() string { + if f.Self == "" { + f.Self = fmt.Sprintf("https://test.atlassian.net/rest/agile/1.0/sprint/%d", f.ID) } return f.Self } + +var fakeSprints = []FakeSprint{ + { + ID: 1, + State: "active", + Name: "Sprint 1", + StartDate: "2021-01-01T00:00:00.000Z", + EndDate: "2021-01-15T00:00:00.000Z", + CompleteDate: "2021-01-15T00:00:00.000Z", + OriginBoardID: 1, + Goal: "Sprint goal", + }, +} diff --git a/application/jira/v0/mock_server.go b/application/jira/v0/mock_server.go index 890b7748..a4b8c1d3 100644 --- a/application/jira/v0/mock_server.go +++ b/application/jira/v0/mock_server.go @@ -1,31 +1,33 @@ package jira import ( + "encoding/json" "fmt" "net/http" "strconv" "strings" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" ) -func router(res http.ResponseWriter, req *http.Request) { - path := req.URL.Path - var logger DebugSession - logger.SessionStart("router", StaticVerboseLevel) - defer logger.SessionEnd() - switch path { - case "/_edge/tenant_info": - res.WriteHeader(http.StatusOK) - _, err := res.Write([]byte(`{"id":"12345678-1234-1234-1234-123456789012"}`)) +func router(middlewares ...func(http.Handler) http.Handler) http.Handler { + r := chi.NewRouter() + r.Use(middleware.Logger) + for _, m := range middlewares { + r.Use(m) + } + r.Get("/_edge/tenant_info", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"cloudId":"12345678-1234-1234-1234-123456789012"}`)) if err != nil { fmt.Println("/_edge/tenant_info", err) } - case "/rest/agile/1.0/board": - mockListBoards(res, req) - case "/rest/agile/1.0/board/1": - case "/rest/api/2/issue": - default: - http.NotFound(res, req) - } + }) + r.Get("/rest/agile/1.0/board", mockListBoards) + r.Get("/rest/agile/1.0/issue/{issueIdOrKey:[a-zA-z0-9-]+}", mockGetIssue) + r.Get("/rest/agile/1.0/sprint/{sprintId}", mockGetSprint) + return r } func mockListBoards(res http.ResponseWriter, req *http.Request) { @@ -109,3 +111,85 @@ func mockListBoards(res http.ResponseWriter, req *http.Request) { fmt.Println("/rest/agile/1.0/board", err) } } + +func mockGetIssue(res http.ResponseWriter, req *http.Request) { + var err error + + issueID := chi.URLParam(req, "issueIdOrKey") + if issueID == "" { + res.WriteHeader(http.StatusBadRequest) + _, err := res.Write([]byte(`{"errorMessages":["Issue id or key is required"]}`)) + if err != nil { + fmt.Println("/rest/agile/1.0/issue", err) + } + return + } + // find issue + var issue *FakeIssue + for _, i := range fakeIssues { + if i.ID == issueID || i.Key == issueID { + issue = &i + issue.getSelf() + break + } + } + if issue == nil { + res.WriteHeader(http.StatusNotFound) + _, err := res.Write([]byte(`{"errorMessages":["Issue does not exist or you do not have permission to see it"]}`)) + if err != nil { + fmt.Println("/rest/agile/1.0/issue", err) + } + return + } + fmt.Println(issue) + // response + res.WriteHeader(http.StatusOK) + respText, err := json.Marshal(issue) + if err != nil { + fmt.Println("/rest/agile/1.0/issue", err) + } + _, err = res.Write(respText) + if err != nil { + fmt.Println("/rest/agile/1.0/issue", err) + } +} + +func mockGetSprint(res http.ResponseWriter, req *http.Request) { + var err error + sprintID := chi.URLParam(req, "sprintId") + if sprintID == "" { + res.WriteHeader(http.StatusBadRequest) + _, err := res.Write([]byte(`{"errorMessages":["Sprint id is required"]}`)) + if err != nil { + fmt.Println("/rest/agile/1.0/sprint", err) + } + return + } + // find sprint + var sprint *FakeSprint + for _, s := range fakeSprints { + if strconv.Itoa(s.ID) == sprintID { + sprint = &s + sprint.getSelf() + break + } + } + if sprint == nil { + res.WriteHeader(http.StatusNotFound) + _, err := res.Write([]byte(`{"errorMessages":["Sprint does not exist or you do not have permission to see it"]}`)) + if err != nil { + fmt.Println("/rest/agile/1.0/sprint", err) + } + return + } + // response + res.WriteHeader(http.StatusOK) + respText, err := json.Marshal(sprint) + if err != nil { + fmt.Println("/rest/agile/1.0/sprint", err) + } + _, err = res.Write(respText) + if err != nil { + fmt.Println("/rest/agile/1.0/sprint", err) + } +} diff --git a/application/jira/v0/sprint.go b/application/jira/v0/sprint.go index f9914fd9..aa9ee84b 100644 --- a/application/jira/v0/sprint.go +++ b/application/jira/v0/sprint.go @@ -37,7 +37,7 @@ type GetSprintOutput struct { Goal string `json:"goal"` } -func (jiraClient *Client) extractSprintOutput(sprint *Sprint) *GetSprintOutput { +func extractSprintOutput(sprint *Sprint) *GetSprintOutput { return &GetSprintOutput{ ID: sprint.ID, Self: sprint.Self, @@ -92,6 +92,6 @@ func (jiraClient *Client) getSprintTask(_ context.Context, props *structpb.Struc fmt.Sprintf("failed to convert %v to `Get Sprint` Output", resp.Result()), ) } - out := jiraClient.extractSprintOutput(issue) + out := extractSprintOutput(issue) return base.ConvertToStructpb(out) } diff --git a/go.mod b/go.mod index 92c90f86..d6e7ca57 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/frankban/quicktest v1.14.6 github.com/gabriel-vasile/mimetype v1.4.3 github.com/gage-technologies/mistral-go v1.1.0 + github.com/go-chi/chi/v5 v5.1.0 github.com/go-resty/resty/v2 v2.12.0 github.com/gocolly/colly/v2 v2.1.0 github.com/gofrs/uuid v4.4.0+incompatible diff --git a/go.sum b/go.sum index 6b225ac4..b21758bf 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/gage-technologies/mistral-go v1.1.0 h1:POv1wM9jA/9OBXGV2YdPi9Y/h09+Mj github.com/gage-technologies/mistral-go v1.1.0/go.mod h1:tF++Xt7U975GcLlzhrjSQb8l/x+PrriO9QEdsgm9l28= github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 h1:u8AQ9bPa9oC+8/A/jlWouakhIvkFfuxgIIRjiy8av7I= github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From b49b6363b22d3009abfb6dc06312479d8695473d Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 12:24:28 +0100 Subject: [PATCH 12/28] feat: add test for list issue --- application/jira/v0/component_test.go | 42 ++++++++++ application/jira/v0/mock_server.go | 114 +++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index a808452f..77e1db24 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -194,6 +194,48 @@ func TestComponent_GetSprintTask(t *testing.T) { taskTesting(testcases, taskGetSprint, t) } +func TestComponent_ListIssuesTask(t *testing.T) { + testcases := []TaskCase[ListIssuesInput, ListIssuesOutput]{ + { + _type: "ok", + name: "get all issues", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "All", + }, + }, + wantResp: ListIssuesOutput{ + Total: 1, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{ + { + ID: "4", + Key: "KAN-4", + Fields: map[string]interface{}{ + "summary": "Test issue 4", + "status": map[string]interface{}{ + "name": "Done", + }, + "issuetype": map[string]interface{}{ + "name": "Epic", + }, + }, + IssueType: "Epic", + Self: "https://test.atlassian.net/rest/agile/1.0/issue/4", + Status: "Done", + Summary: "Test issue 4", + }, + }, + }, + }, + } + taskTesting(testcases, taskListIssues, t) +} + func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType], task string, t *testing.T) { c := qt.New(t) ctx := context.Background() diff --git a/application/jira/v0/mock_server.go b/application/jira/v0/mock_server.go index a4b8c1d3..66be5c1b 100644 --- a/application/jira/v0/mock_server.go +++ b/application/jira/v0/mock_server.go @@ -24,9 +24,15 @@ func router(middlewares ...func(http.Handler) http.Handler) http.Handler { fmt.Println("/_edge/tenant_info", err) } }) - r.Get("/rest/agile/1.0/board", mockListBoards) r.Get("/rest/agile/1.0/issue/{issueIdOrKey:[a-zA-z0-9-]+}", mockGetIssue) r.Get("/rest/agile/1.0/sprint/{sprintId}", mockGetSprint) + r.Get("/rest/agile/1.0/board/{boardId}/issue", mockListIssues) // list all issues + r.Get("/rest/agile/1.0/board/{boardId}/epic", mockListIssues) // list all epic + r.Get("/rest/agile/1.0/board/{boardId}/sprint", mockListIssues) // list all sprint + r.Get("/rest/agile/1.0/board/{boardId}/backlog", mockListIssues) // list all issues in backlog + r.Get("/rest/agile/1.0/board/{boardId}/epic/none/issue", mockListIssues) // list all issues without epic assigned + r.Get("/rest/agile/1.0/board/{boardId}", mockGetBoard) + r.Get("/rest/agile/1.0/board", mockListBoards) return r } @@ -111,6 +117,36 @@ func mockListBoards(res http.ResponseWriter, req *http.Request) { fmt.Println("/rest/agile/1.0/board", err) } } +func mockGetBoard(res http.ResponseWriter, req *http.Request) { + var err error + boardID := chi.URLParam(req, "boardId") + // filter boards + var board *FakeBoard + for _, b := range fakeBoards { + if boardID != "" && strconv.Itoa(b.ID) != boardID { + continue + } + board = &b + } + if board == nil { + res.WriteHeader(http.StatusNotFound) + _, err := res.Write([]byte(`{"errorMessages":["Board does not exist or you do not have permission to see it"]}`)) + if err != nil { + fmt.Println("mockGetBoard", err) + } + return + } + // response + res.WriteHeader(http.StatusOK) + respText, err := json.Marshal(board) + if err != nil { + fmt.Println("mockGetBoard", err) + } + _, err = res.Write([]byte(respText)) + if err != nil { + fmt.Println("/rest/agile/1.0/board", err) + } +} func mockGetIssue(res http.ResponseWriter, req *http.Request) { var err error @@ -193,3 +229,79 @@ func mockGetSprint(res http.ResponseWriter, req *http.Request) { fmt.Println("/rest/agile/1.0/sprint", err) } } + +type MockListIssuesResponse struct { + Issues []FakeIssue `json:"issues"` + Total int `json:"total"` + StartAt int `json:"start-at"` + MaxResults int `json:"max-results"` +} + +func mockListIssues(res http.ResponseWriter, req *http.Request) { + var err error + opt := req.URL.Query() + boardID := chi.URLParam(req, "boardId") + jql := opt.Get("jql") + startAt := opt.Get("startAt") + maxResults := opt.Get("maxResults") + // find board + var board *FakeBoard + for _, b := range fakeBoards { + if strconv.Itoa(b.ID) == boardID { + board = &b + break + } + } + if board == nil { + res.WriteHeader(http.StatusNotFound) + _, err := res.Write([]byte(`{"errorMessages":["Board does not exist or you do not have permission to see it"]}`)) + if err != nil { + fmt.Println("mockListIssues", err) + } + return + } + // filter issues + var issues []FakeIssue + for _, issue := range fakeIssues { + prefix := strings.Split(issue.Key, "-")[0] + if board.Name != "" && prefix != board.Name { + fmt.Println("prefix", prefix, "board.Name", board.Name) + continue + } + if jql != "" { + // Skip JQL filter as there is no need to implement it + continue + } + issue.getSelf() + issues = append(issues, issue) + } + // response + res.WriteHeader(http.StatusOK) + startAtNum := 0 + if startAt != "" { + startAtNum, err = strconv.Atoi(startAt) + if err != nil { + fmt.Println("mockListIssues", err) + return + } + } + maxResultsNum, err := strconv.Atoi(maxResults) + if err != nil { + fmt.Println("mockListIssues", err) + return + } + resp := MockListIssuesResponse{ + Issues: issues, + Total: len(issues), + StartAt: startAtNum, + MaxResults: maxResultsNum, + } + respText, err := json.Marshal(resp) + if err != nil { + fmt.Println("mockListIssues", err) + } + _, err = res.Write([]byte(respText)) + if err != nil { + fmt.Println("mockListIssues", err) + } +} From 61a3364607a14b811b1d15e805dbb750dc6e5790 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 14:09:20 +0100 Subject: [PATCH 13/28] feat: add more test cases --- application/jira/v0/component_test.go | 204 +++++++++++++++++++++++++- application/jira/v0/mock_database.go | 16 ++ application/jira/v0/mock_server.go | 187 ++++++++++++++--------- 3 files changed, 336 insertions(+), 71 deletions(-) diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index 77e1db24..1604726a 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" qt "github.com/frankban/quicktest" @@ -198,7 +199,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { testcases := []TaskCase[ListIssuesInput, ListIssuesOutput]{ { _type: "ok", - name: "get all issues", + name: "All", input: ListIssuesInput{ BoardID: 1, MaxResults: 10, @@ -232,6 +233,207 @@ func TestComponent_ListIssuesTask(t *testing.T) { }, }, }, + { + _type: "ok", + name: "Epics only", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "Epics only", + }, + }, + wantResp: ListIssuesOutput{ + Total: 1, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{ + { + ID: "4", + Key: "KAN-4", + Fields: map[string]interface{}{ + "summary": "Test issue 4", + "status": map[string]interface{}{ + "name": "Done", + }, + "issuetype": map[string]interface{}{ + "name": "Epic", + }, + }, + IssueType: "Epic", + Self: "https://test.atlassian.net/rest/agile/1.0/issue/4", + Status: "Done", + Summary: "Test issue 4", + }, + }, + }, + }, + { + _type: "ok", + name: "Sprints only", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "Sprints only", + }, + }, + wantResp: ListIssuesOutput{ + Total: 1, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{ + { + ID: "4", + Key: "KAN-4", + Fields: map[string]interface{}{ + "summary": "Test issue 4", + "status": map[string]interface{}{ + "name": "Done", + }, + "issuetype": map[string]interface{}{ + "name": "Sprint", + }, + }, + IssueType: "Sprint", + Self: "https://test.atlassian.net/rest/agile/1.0/issue/4", + Status: "Done", + Summary: "Test issue 4", + }, + }, + }, + }, + { + _type: "ok", + name: "In backlog only", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "In backlog only", + }, + }, + wantResp: ListIssuesOutput{ + Total: 1, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{ + { + ID: "4", + Key: "KAN-4", + Fields: map[string]interface{}{ + "summary": "Test issue 4", + "status": map[string]interface{}{ + "name": "Done", + }, + "issuetype": map[string]interface{}{ + "name": "Epic", + }, + }, + IssueType: "Epic", + Self: "https://test.atlassian.net/rest/agile/1.0/issue/4", + Status: "Done", + Summary: "Test issue 4", + }, + }, + }, + }, + { + _type: "ok", + name: "Issues without epic assigned", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "Issues without epic assigned", + }, + }, + wantResp: ListIssuesOutput{ + Total: 1, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{ + { + ID: "4", + Key: "KAN-4", + Fields: map[string]interface{}{ + "summary": "Test issue 4", + "status": map[string]interface{}{ + "name": "Done", + }, + "issuetype": map[string]interface{}{ + "name": "Epic", + }, + }, + IssueType: "Epic", + Self: "https://test.atlassian.net/rest/agile/1.0/issue/4", + Status: "Done", + Summary: "Test issue 4", + }, + }, + }, + }, + { + _type: "ok", + name: "Issues of an epic", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "Issues of an epic", + EpicKey: "KAN-4", + }, + }, + wantResp: ListIssuesOutput{ + Total: 0, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{}, + }, + }, + { + _type: "ok", + name: "Issues of an epic(long query)", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "Issues of an epic", + EpicKey: "KAN-4" + strings.Repeat("-0", 100), + }, + }, + wantResp: ListIssuesOutput{ + Total: 0, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{}, + }, + }, + { + _type: "ok", + name: "Issues of a sprint", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "Issues of a sprint", + SprintKey: "1", + }, + }, + wantResp: ListIssuesOutput{ + Total: 0, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{}, + }, + }, } taskTesting(testcases, taskListIssues, t) } diff --git a/application/jira/v0/mock_database.go b/application/jira/v0/mock_database.go index 34b2339c..1a4856b7 100644 --- a/application/jira/v0/mock_database.go +++ b/application/jira/v0/mock_database.go @@ -45,6 +45,12 @@ type FakeIssue struct { Self string `json:"self"` Fields map[string]interface{} `json:"fields"` } +type FakeSprintAsIssue struct { + ID int `json:"id"` + Key string `json:"key"` + Self string `json:"self"` + Fields map[string]interface{} `json:"fields"` +} func (f *FakeIssue) getSelf() string { if f.Self == "" { @@ -111,6 +117,16 @@ var fakeIssues = []FakeIssue{ }, } +func (f *FakeIssue) toSprint() func() { + recovery := f.Fields["issuetype"].(map[string]interface{})["name"].(string) + f.Fields["issuetype"] = map[string]interface{}{ + "name": "Sprint", + } + return func() { + f.Fields["issuetype"].(map[string]interface{})["name"] = recovery + } +} + type FakeSprint struct { ID int `json:"id"` Self string `json:"self"` diff --git a/application/jira/v0/mock_server.go b/application/jira/v0/mock_server.go index 66be5c1b..45565853 100644 --- a/application/jira/v0/mock_server.go +++ b/application/jira/v0/mock_server.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "strconv" "strings" @@ -19,10 +20,7 @@ func router(middlewares ...func(http.Handler) http.Handler) http.Handler { } r.Get("/_edge/tenant_info", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(`{"cloudId":"12345678-1234-1234-1234-123456789012"}`)) - if err != nil { - fmt.Println("/_edge/tenant_info", err) - } + _, _ = w.Write([]byte(`{"cloudId":"12345678-1234-1234-1234-123456789012"}`)) }) r.Get("/rest/agile/1.0/issue/{issueIdOrKey:[a-zA-z0-9-]+}", mockGetIssue) r.Get("/rest/agile/1.0/sprint/{sprintId}", mockGetSprint) @@ -33,6 +31,9 @@ func router(middlewares ...func(http.Handler) http.Handler) http.Handler { r.Get("/rest/agile/1.0/board/{boardId}/epic/none/issue", mockListIssues) // list all issues without epic assigned r.Get("/rest/agile/1.0/board/{boardId}", mockGetBoard) r.Get("/rest/agile/1.0/board", mockListBoards) + + r.Get("/rest/api/2/search", mockIssuesSearch) + r.Post("/rest/api/2/search", mockIssuesSearch) return r } @@ -64,10 +65,7 @@ func mockListBoards(res http.ResponseWriter, req *http.Request) { } if pjNotFound { res.WriteHeader(http.StatusBadRequest) - _, err := res.Write([]byte(fmt.Sprintf(`{"errorMessages":["No project could be found with key or id '%s'"]}`, projectKeyOrID))) - if err != nil { - fmt.Println("/rest/agile/1.0/board", err) - } + _, _ = res.Write([]byte(`{"errorMessages":["No project could be found with key or id"]}`)) return } // pagination @@ -75,11 +73,6 @@ func mockListBoards(res http.ResponseWriter, req *http.Request) { if startAt != "" { start, err = strconv.Atoi(startAt) if err != nil { - res.WriteHeader(http.StatusBadRequest) - _, err := res.Write([]byte(`{"errorMessages":["The 'startAt' parameter must be a number"]}`)) - if err != nil { - fmt.Println("/rest/agile/1.0/board", err) - } return } } @@ -87,11 +80,6 @@ func mockListBoards(res http.ResponseWriter, req *http.Request) { if maxResults != "" { maxResultsNum, err = strconv.Atoi(maxResults) if err != nil { - res.WriteHeader(http.StatusBadRequest) - _, err := res.Write([]byte(`{"errorMessages":["The 'maxResults' parameter must be a number"]}`)) - if err != nil { - fmt.Println("/rest/agile/1.0/board", err) - } return } end = start + maxResultsNum @@ -112,11 +100,9 @@ func mockListBoards(res http.ResponseWriter, req *http.Request) { } respText += `],` respText += `"total":` + strconv.Itoa(len(boards)) + `,"startAt":` + strconv.Itoa(start) + `,"maxResults":` + strconv.Itoa(maxResultsNum) + `,"isLast":` + strconv.FormatBool(end == len(boards)) + `}` - _, err = res.Write([]byte(respText)) - if err != nil { - fmt.Println("/rest/agile/1.0/board", err) - } + _, _ = res.Write([]byte(respText)) } + func mockGetBoard(res http.ResponseWriter, req *http.Request) { var err error boardID := chi.URLParam(req, "boardId") @@ -130,22 +116,16 @@ func mockGetBoard(res http.ResponseWriter, req *http.Request) { } if board == nil { res.WriteHeader(http.StatusNotFound) - _, err := res.Write([]byte(`{"errorMessages":["Board does not exist or you do not have permission to see it"]}`)) - if err != nil { - fmt.Println("mockGetBoard", err) - } + _, _ = res.Write([]byte(`{"errorMessages":["Board does not exist or you do not have permission to see it"]}`)) return } // response res.WriteHeader(http.StatusOK) respText, err := json.Marshal(board) if err != nil { - fmt.Println("mockGetBoard", err) - } - _, err = res.Write([]byte(respText)) - if err != nil { - fmt.Println("/rest/agile/1.0/board", err) + return } + _, _ = res.Write([]byte(respText)) } func mockGetIssue(res http.ResponseWriter, req *http.Request) { @@ -154,10 +134,7 @@ func mockGetIssue(res http.ResponseWriter, req *http.Request) { issueID := chi.URLParam(req, "issueIdOrKey") if issueID == "" { res.WriteHeader(http.StatusBadRequest) - _, err := res.Write([]byte(`{"errorMessages":["Issue id or key is required"]}`)) - if err != nil { - fmt.Println("/rest/agile/1.0/issue", err) - } + _, _ = res.Write([]byte(`{"errorMessages":["Issue id or key is required"]}`)) return } // find issue @@ -171,10 +148,7 @@ func mockGetIssue(res http.ResponseWriter, req *http.Request) { } if issue == nil { res.WriteHeader(http.StatusNotFound) - _, err := res.Write([]byte(`{"errorMessages":["Issue does not exist or you do not have permission to see it"]}`)) - if err != nil { - fmt.Println("/rest/agile/1.0/issue", err) - } + _, _ = res.Write([]byte(`{"errorMessages":["Issue does not exist or you do not have permission to see it"]}`)) return } fmt.Println(issue) @@ -182,12 +156,9 @@ func mockGetIssue(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusOK) respText, err := json.Marshal(issue) if err != nil { - fmt.Println("/rest/agile/1.0/issue", err) - } - _, err = res.Write(respText) - if err != nil { - fmt.Println("/rest/agile/1.0/issue", err) + return } + _, _ = res.Write(respText) } func mockGetSprint(res http.ResponseWriter, req *http.Request) { @@ -195,10 +166,7 @@ func mockGetSprint(res http.ResponseWriter, req *http.Request) { sprintID := chi.URLParam(req, "sprintId") if sprintID == "" { res.WriteHeader(http.StatusBadRequest) - _, err := res.Write([]byte(`{"errorMessages":["Sprint id is required"]}`)) - if err != nil { - fmt.Println("/rest/agile/1.0/sprint", err) - } + _, _ = res.Write([]byte(`{"errorMessages":["Sprint id is required"]}`)) return } // find sprint @@ -212,29 +180,24 @@ func mockGetSprint(res http.ResponseWriter, req *http.Request) { } if sprint == nil { res.WriteHeader(http.StatusNotFound) - _, err := res.Write([]byte(`{"errorMessages":["Sprint does not exist or you do not have permission to see it"]}`)) - if err != nil { - fmt.Println("/rest/agile/1.0/sprint", err) - } + _, _ = res.Write([]byte(`{"errorMessages":["Sprint does not exist or you do not have permission to see it"]}`)) return } // response res.WriteHeader(http.StatusOK) respText, err := json.Marshal(sprint) if err != nil { - fmt.Println("/rest/agile/1.0/sprint", err) - } - _, err = res.Write(respText) - if err != nil { - fmt.Println("/rest/agile/1.0/sprint", err) + return } + _, _ = res.Write(respText) } type MockListIssuesResponse struct { - Issues []FakeIssue `json:"issues"` - Total int `json:"total"` - StartAt int `json:"start-at"` - MaxResults int `json:"max-results"` + Values []FakeSprintAsIssue `json:"values"` + Issues []FakeIssue `json:"issues"` + Total int `json:"total"` + StartAt int `json:"start-at"` + MaxResults int `json:"max-results"` } func mockListIssues(res http.ResponseWriter, req *http.Request) { @@ -254,10 +217,7 @@ func mockListIssues(res http.ResponseWriter, req *http.Request) { } if board == nil { res.WriteHeader(http.StatusNotFound) - _, err := res.Write([]byte(`{"errorMessages":["Board does not exist or you do not have permission to see it"]}`)) - if err != nil { - fmt.Println("mockListIssues", err) - } + _, _ = res.Write([]byte(`{"errorMessages":["Board does not exist or you do not have permission to see it"]}`)) return } // filter issues @@ -273,21 +233,37 @@ func mockListIssues(res http.ResponseWriter, req *http.Request) { continue } issue.getSelf() + if strings.Contains(req.URL.Path, "sprint") { + defer issue.toSprint()() + } issues = append(issues, issue) } + var fakeSprintAsIssue []FakeSprintAsIssue + if strings.Contains(req.URL.Path, "sprint") { + for _, sprint := range issues { + idNum, err := strconv.Atoi(sprint.ID) + if err != nil { + continue + } + fakeSprintAsIssue = append(fakeSprintAsIssue, FakeSprintAsIssue{ + ID: idNum, + Key: sprint.Key, + Self: sprint.getSelf(), + Fields: sprint.Fields, + }) + } + } // response res.WriteHeader(http.StatusOK) startAtNum := 0 if startAt != "" { startAtNum, err = strconv.Atoi(startAt) if err != nil { - fmt.Println("mockListIssues", err) return } } maxResultsNum, err := strconv.Atoi(maxResults) if err != nil { - fmt.Println("mockListIssues", err) return } resp := MockListIssuesResponse{ @@ -296,12 +272,83 @@ func mockListIssues(res http.ResponseWriter, req *http.Request) { StartAt: startAtNum, MaxResults: maxResultsNum, } + if strings.Contains(req.URL.Path, "sprint") { + resp.Values = fakeSprintAsIssue + resp.Issues = nil + } respText, err := json.Marshal(resp) if err != nil { - fmt.Println("mockListIssues", err) + return + } + _, _ = res.Write([]byte(respText)) +} + + +type MockIssuesSearchRequest struct { + JQL string `json:"jql"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` +} +func mockIssuesSearch(res http.ResponseWriter, req *http.Request) { + var err error + var ( + opt url.Values + jql string + startAt string + maxResults string + ) + if req.Method == http.MethodGet { + opt = req.URL.Query() + jql = opt.Get("jql") + startAt = opt.Get("startAt") + maxResults = opt.Get("maxResults") + }else if req.Method == http.MethodPost { + body := MockIssuesSearchRequest{} + err = json.NewDecoder(req.Body).Decode(&body) + if err != nil { + fmt.Println(err) + return + } + jql = body.JQL + startAt = strconv.Itoa(body.StartAt) + maxResults = strconv.Itoa(body.MaxResults) + }else{ + res.WriteHeader(http.StatusMethodNotAllowed) + _, _ = res.Write([]byte(`{"errorMessages":["Method not allowed"]}`)) + return + } + // filter issues + var issues []FakeIssue + for _, issue := range fakeIssues { + if jql != "" { + // Skip JQL filter as there is no need to implement it + continue + } + issue.getSelf() + issues = append(issues, issue) + } + // response + res.WriteHeader(http.StatusOK) + startAtNum := 0 + if startAt != "" { + startAtNum, err = strconv.Atoi(startAt) + if err != nil { + return + } + } + maxResultsNum, err := strconv.Atoi(maxResults) + if err != nil { + return + } + resp := MockListIssuesResponse{ + Issues: issues, + Total: len(issues), + StartAt: startAtNum, + MaxResults: maxResultsNum, } - _, err = res.Write([]byte(respText)) + respText, err := json.Marshal(resp) if err != nil { - fmt.Println("mockListIssues", err) + return } + _, _ = res.Write([]byte(respText)) } From 5640d4523b459002c8696689d666972b7e94e738 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 14:45:32 +0100 Subject: [PATCH 14/28] chore: silence the debug logger --- application/jira/v0/boards.go | 4 ++-- application/jira/v0/issues.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index 25605c78..f1b363c9 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -64,7 +64,7 @@ func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.St func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (*ListBoardsResp, error) { var debug DebugSession - debug.SessionStart("listBoards", DevelopVerboseLevel) + debug.SessionStart("listBoards", StaticVerboseLevel) defer debug.SessionEnd() apiEndpoint := "rest/agile/1.0/board" @@ -89,7 +89,7 @@ func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (* func (jiraClient *Client) getBoard(_ context.Context, boardID int) (*Board, error) { var debug DebugSession - debug.SessionStart("getBoard", DevelopVerboseLevel) + debug.SessionStart("getBoard", StaticVerboseLevel) defer debug.SessionEnd() apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index a52781de..07363e5a 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -91,7 +91,7 @@ func transformToIssue(val *SprintOrEpic) *Issue { func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { var debug DebugSession - debug.SessionStart("getIssueTask", DevelopVerboseLevel) + debug.SessionStart("getIssueTask", StaticVerboseLevel) defer debug.SessionEnd() var opt GetIssueInput @@ -165,7 +165,7 @@ type ListIssuesOutput struct { func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { var debug DebugSession - debug.SessionStart("listIssuesTask", DevelopVerboseLevel) + debug.SessionStart("listIssuesTask", StaticVerboseLevel) defer debug.SessionEnd() debug.AddMapMessage("props", props) @@ -286,7 +286,7 @@ type nextGenSearchRequest struct { // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-post func (jiraClient *Client) nextGenIssuesSearch(_ context.Context, opt nextGenSearchRequest) (*resty.Response, error) { var debug DebugSession - debug.SessionStart("nextGenIssuesSearch", DevelopVerboseLevel) + debug.SessionStart("nextGenIssuesSearch", StaticVerboseLevel) defer debug.SessionEnd() debug.AddMessage("opt:") From 43ed49892a0e0182daa8512f326d1b4d73974a74 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 15:07:43 +0100 Subject: [PATCH 15/28] chore: fix typo and gen docs --- application/jira/v0/README.mdx | 159 ++++++++++++++++++++++++++ application/jira/v0/config/tasks.json | 32 ++---- application/jira/v0/issues.go | 8 +- 3 files changed, 175 insertions(+), 24 deletions(-) create mode 100644 application/jira/v0/README.mdx diff --git a/application/jira/v0/README.mdx b/application/jira/v0/README.mdx new file mode 100644 index 00000000..16569f57 --- /dev/null +++ b/application/jira/v0/README.mdx @@ -0,0 +1,159 @@ +--- +title: "Jira" +lang: "en-US" +draft: false +description: "Learn about how to set up a VDP Jira component https://github.com/instill-ai/instill-core" +--- + +The Jira component is an application component that allows users to do anything available on Jira. +It can carry out the following tasks: + +- [List Boards](#list-boards) +- [List Issues](#list-issues) +- [Get Issue](#get-issue) +- [Get Sprint](#get-sprint) + + + +## Release Stage + +`Alpha` + + + +## Configuration + +The component configuration is defined and maintained [here](https://github.com/instill-ai/component/blob/main/application/jira/v0/config/definition.json). + + + + +## Setup + + +| Field | Field ID | Type | Note | +| :--- | :--- | :--- | :--- | +| Token (required) | `token` | string | Fill in your Jira API token. You can generate one from your Jira account "settings > security > API tokens". | +| Base URL (required) | `base-url` | string | Fill in your Jira base URL. For example, if your Jira URL is https://mycompany.atlassian.net, then your base URL is https://mycompany.atlassian.net. | +| Email (required) | `email` | string | Fill in your Jira email address. | + + + + +## Supported Tasks + +### List Boards + +List all boards in Jira + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_LIST_BOARDS` | +| Project Key or ID | `project-key-or-id` | string | This filters results to boards that are relevant to a project. Relevance meaning that the JQL filter defined in board contains a reference to a project. | +| Board Type | `board-type` | string | The type of board, can be: scrum, kanban, simple. Default is simple | +| Name | `name` | string | Name filters results to boards that match or partially match the specified name. Default is empty | +| Start At | `start-at` | integer | The starting index of the returned boards. Base index: 0. Default is 0 | +| Max Results | `max-results` | integer | The maximum number of boards to return. Default is 50 | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Boards (optional) | `boards` | array[object] | A array of boards in Jira | +| Start At | `start-at` | integer | The starting index of the returned boards. Base index: 0 | +| Max Results | `max-results` | integer | The maximum number of boards | +| Total | `total` | integer | The total number of boards | +| Is Last | `is-last` | boolean | Whether the last board is reached | + + + + + + +### List Issues + +List issues in Jira + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_LIST_ISSUES` | +| Board ID (required) | `board-id` | integer | The ID of the board | +| Range | `range` | object | Choose the range of issues to return. Default is `all` | +| Start At | `start-at` | integer | The starting index of the returned boards. Base index: 0. Default is 0 | +| Max Results | `max-results` | integer | The maximum number of boards to return. Default is 50 | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Issues (optional) | `issues` | array[object] | A array of issues in Jira | +| Start At | `start-at` | integer | The starting index of the returned boards. Base index: 0 | +| Max Results | `max-results` | integer | The maximum number of boards | +| Total | `total` | integer | The total number of boards | + + + + + + +### Get Issue + +Get an issue in Jira + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_GET_ISSUE` | +| Issue ID or Key (required) | `issue-id-or-key` | string | The ID or key of the issue | +| Update History | `update-history` | boolean | Whether the project in which the issue is created is added to the user's Recently viewed project list, as shown under Projects in Jira. | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| ID | `id` | string | The ID of the issue | +| Key | `key` | string | The key of the issue | +| Self | `self` | string | The URL of the issue | +| Fields | `fields` | object | The fields of the issue. All navigable and Agile fields are returned | +| Issue Type (optional) | `issue-type` | string | The type of the issue, can be: `Task`, `Epic` | +| Summary (optional) | `summary` | string | The summary of the issue | +| Description (optional) | `description` | string | The description of the issue | +| Status (optional) | `status` | string | The status of the issue, can be: `To Do`, `In Progress`, `Done` | + + + + + + +### Get Sprint + +Get a sprint in Jira + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_GET_SPRINT` | +| Sprint ID (required) | `sprint-id` | integer | The ID of the sprint. The sprint will only be returned if you can view the board that the sprint was created on, or view at least one of the issues in the sprint. | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| ID (optional) | `id` | integer | The ID of the sprint | +| Self (optional) | `self` | string | The URL of the sprint | +| State (optional) | `state` | string | The state of the sprint, can be: `active`, `closed`, `future` | +| Name (optional) | `name` | string | The name of the sprint | +| Start Date (optional) | `start-date` | string | The start date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z | +| End Date (optional) | `end-date` | string | The end date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z | +| Complete Date (optional) | `complete-date` | string | The complete date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z | +| Origin Board ID (optional) | `origin-board-id` | integer | The ID of the origin board | +| Goal (optional) | `goal` | string | The Goal of the sprint | + + + + + + + diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 16df760e..deca6c9b 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -322,23 +322,6 @@ "range": { "const": "All", "type": "string" - }, - "epic-key": { - "default": "optional", - "title": "Epic Key (Optional field)", - "description": "Optional field", - "instillShortDescription": "Optional field", - "instillUIOrder": 0, - "instillFormat": "string", - "instillAcceptFormats": [ - "string" - ], - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], - "type": "string" } }, "required": [ @@ -392,7 +375,7 @@ "title": "Epic Key", "description": "The Key of the epic", "instillShortDescription": "The Key of the epic", - "instillUIOrder": 0, + "instillUIOrder": 10, "instillFormat": "string", "instillAcceptFormats": [ "string" @@ -419,14 +402,14 @@ { "properties": { "range": { - "const": "Issues of an sprint", + "const": "Issues of a sprint", "type": "string" }, "sprint-key": { "title": "Sprint Key", "description": "The Key of the sprint", "instillShortDescription": "The Key of the sprint", - "instillUIOrder": 0, + "instillUIOrder": 10, "instillFormat": "string", "instillAcceptFormats": [ "string" @@ -693,54 +676,63 @@ "title": "ID", "description": "The ID of the sprint", "type": "integer", + "instillUIOrder": 0, "instillFormat": "integer" }, "self": { "title": "Self", "description": "The URL of the sprint", "type": "string", + "instillUIOrder": 1, "instillFormat": "string" }, "state": { "title": "State", "description": "The state of the sprint, can be: `active`, `closed`, `future`", "type": "string", + "instillUIOrder": 2, "instillFormat": "string" }, "name": { "title": "Name", "description": "The name of the sprint", "type": "string", + "instillUIOrder": 3, "instillFormat": "string" }, "start-date": { "title": "Start Date", "description": "The start date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", "type": "string", + "instillUIOrder": 4, "instillFormat": "string" }, "end-date": { "title": "End Date", "description": "The end date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", "type": "string", + "instillUIOrder": 5, "instillFormat": "string" }, "complete-date": { "title": "Complete Date", "description": "The complete date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", "type": "string", + "instillUIOrder": 6, "instillFormat": "string" }, "origin-board-id": { "title": "Origin Board ID", "description": "The ID of the origin board", "type": "integer", + "instillUIOrder": 7, "instillFormat": "integer" }, "goal": { "title": "Goal", "description": "The Goal of the sprint", "type": "string", + "instillUIOrder": 8, "instillFormat": "string" } }, diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 07363e5a..ca796f6c 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -34,8 +34,8 @@ type SprintOrEpic struct { } type GetIssueInput struct { - IssueKeyOrID string `json:"issue-id-or-key,omitempty" struct:"issueIdOrKey"` - UpdateHistory bool `json:"update-history,omitempty" struct:"updateHistory"` + IssueKeyOrID string `json:"issue-id-or-key,omitempty" api:"issueIdOrKey"` + UpdateHistory bool `json:"update-history,omitempty" api:"updateHistory"` } type GetIssueOutput struct { Issue @@ -152,8 +152,8 @@ type ListIssuesInput struct { type ListIssuesResp struct { Issues []Issue `json:"issues"` Values []SprintOrEpic `json:"values"` - StartAt int `json:"start-at"` - MaxResults int `json:"max-results"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` Total int `json:"total"` } type ListIssuesOutput struct { From 945e48b9c5447f014d842bbc0f8459b055d07d9e Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 15:10:52 +0100 Subject: [PATCH 16/28] chore: fix typo --- application/jira/v0/mock_server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/jira/v0/mock_server.go b/application/jira/v0/mock_server.go index 45565853..4ad85c80 100644 --- a/application/jira/v0/mock_server.go +++ b/application/jira/v0/mock_server.go @@ -196,8 +196,8 @@ type MockListIssuesResponse struct { Values []FakeSprintAsIssue `json:"values"` Issues []FakeIssue `json:"issues"` Total int `json:"total"` - StartAt int `json:"start-at"` - MaxResults int `json:"max-results"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` } func mockListIssues(res http.ResponseWriter, req *http.Request) { From c2a325446a107137f1b28af422bddbcae5478e29 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 15:11:18 +0100 Subject: [PATCH 17/28] chore: remove irrelavent params --- application/jira/v0/component_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index 1604726a..931462cd 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -18,8 +18,6 @@ import ( const ( email = "testemail@gmail.com" token = "testToken" - errResp = `{"message": "Bad request"}` - okResp = `{"title": "Be the wheel"}` ) type TaskCase[inType any, outType any] struct { From 6eafef52f2cfa677d6ae2ca8c25e3d60505d2dd8 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 15:16:35 +0100 Subject: [PATCH 18/28] chore: format code --- application/jira/v0/component_test.go | 10 +++++----- application/jira/v0/mock_server.go | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index 931462cd..37e8a719 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -16,8 +16,8 @@ import ( ) const ( - email = "testemail@gmail.com" - token = "testToken" + email = "testemail@gmail.com" + token = "testToken" ) type TaskCase[inType any, outType any] struct { @@ -383,7 +383,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { MaxResults: 10, StartAt: 0, Range: Range{ - Range: "Issues of an epic", + Range: "Issues of an epic", EpicKey: "KAN-4", }, }, @@ -402,7 +402,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { MaxResults: 10, StartAt: 0, Range: Range{ - Range: "Issues of an epic", + Range: "Issues of an epic", EpicKey: "KAN-4" + strings.Repeat("-0", 100), }, }, @@ -421,7 +421,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { MaxResults: 10, StartAt: 0, Range: Range{ - Range: "Issues of a sprint", + Range: "Issues of a sprint", SprintKey: "1", }, }, diff --git a/application/jira/v0/mock_server.go b/application/jira/v0/mock_server.go index 4ad85c80..4f30f5d5 100644 --- a/application/jira/v0/mock_server.go +++ b/application/jira/v0/mock_server.go @@ -283,18 +283,18 @@ func mockListIssues(res http.ResponseWriter, req *http.Request) { _, _ = res.Write([]byte(respText)) } - type MockIssuesSearchRequest struct { JQL string `json:"jql"` - StartAt int `json:"startAt"` - MaxResults int `json:"maxResults"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` } + func mockIssuesSearch(res http.ResponseWriter, req *http.Request) { var err error var ( - opt url.Values - jql string - startAt string + opt url.Values + jql string + startAt string maxResults string ) if req.Method == http.MethodGet { @@ -302,7 +302,7 @@ func mockIssuesSearch(res http.ResponseWriter, req *http.Request) { jql = opt.Get("jql") startAt = opt.Get("startAt") maxResults = opt.Get("maxResults") - }else if req.Method == http.MethodPost { + } else if req.Method == http.MethodPost { body := MockIssuesSearchRequest{} err = json.NewDecoder(req.Body).Decode(&body) if err != nil { @@ -312,7 +312,7 @@ func mockIssuesSearch(res http.ResponseWriter, req *http.Request) { jql = body.JQL startAt = strconv.Itoa(body.StartAt) maxResults = strconv.Itoa(body.MaxResults) - }else{ + } else { res.WriteHeader(http.StatusMethodNotAllowed) _, _ = res.Write([]byte(`{"errorMessages":["Method not allowed"]}`)) return From 19b2efe4408fbc51914a3e0396b73f56cd319c2b Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 17 Jul 2024 15:44:12 +0100 Subject: [PATCH 19/28] chore: update icon --- application/jira/v0/assets/jira.svg | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/application/jira/v0/assets/jira.svg b/application/jira/v0/assets/jira.svg index d1eeed52..aca92e1b 100644 --- a/application/jira/v0/assets/jira.svg +++ b/application/jira/v0/assets/jira.svg @@ -1 +1,15 @@ - + + + + + + + + + + + + + + + From e0ccbefd2c75e55fef857b6754c8403298c6db0d Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 22 Jul 2024 12:33:30 +0100 Subject: [PATCH 20/28] feat: add list standard issues --- application/jira/v0/config/definition.json | 2 - application/jira/v0/config/tasks.json | 225 +++++++++------------ application/jira/v0/issues.go | 9 +- 3 files changed, 100 insertions(+), 136 deletions(-) diff --git a/application/jira/v0/config/definition.json b/application/jira/v0/config/definition.json index bea93bc4..e177f313 100644 --- a/application/jira/v0/config/definition.json +++ b/application/jira/v0/config/definition.json @@ -5,10 +5,8 @@ "TASK_GET_ISSUE", "TASK_GET_SPRINT" ], - "custom": false, "documentationUrl": "https://www.instill.tech/docs/component/application/jira", "icon": "assets/Jira.svg", - "iconUrl": "", "id": "jira", "public": true, "title": "Jira", diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index deca6c9b..d78fd00a 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -115,6 +115,79 @@ ], "title": "Issue", "type": "object" + }, + "sprint": { + "properties": { + "id": { + "title": "ID", + "description": "The ID of the sprint", + "type": "integer", + "instillUIOrder": 0, + "instillFormat": "integer" + }, + "self": { + "title": "Self", + "description": "The URL of the sprint", + "type": "string", + "instillUIOrder": 1, + "instillFormat": "string" + }, + "state": { + "title": "State", + "description": "The state of the sprint, can be: `active`, `closed`, `future`", + "type": "string", + "instillUIOrder": 2, + "instillFormat": "string" + }, + "name": { + "title": "Name", + "description": "The name of the sprint", + "type": "string", + "instillUIOrder": 3, + "instillFormat": "string" + }, + "start-date": { + "title": "Start Date", + "description": "The start date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", + "type": "string", + "instillUIOrder": 4, + "instillFormat": "string" + }, + "end-date": { + "title": "End Date", + "description": "The end date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", + "type": "string", + "instillUIOrder": 5, + "instillFormat": "string" + }, + "complete-date": { + "title": "Complete Date", + "description": "The complete date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", + "type": "string", + "instillUIOrder": 6, + "instillFormat": "string" + }, + "origin-board-id": { + "title": "Origin Board ID", + "description": "The ID of the origin board", + "type": "integer", + "instillUIOrder": 7, + "instillFormat": "integer" + }, + "goal": { + "title": "Goal", + "description": "The Goal of the sprint", + "type": "string", + "instillUIOrder": 8, + "instillFormat": "string" + } + }, + "required": [ + "id", + "self" + ], + "title": "Sprint", + "type": "object" } }, "TASK_LIST_BOARDS": { @@ -333,6 +406,22 @@ "instillFormat": "object", "type": "object" }, + { + "properties": { + "range": { + "const": "Standard Issues", + "type": "string" + } + }, + "required": [ + "range" + ], + "instillEditOnNodeFields": [ + "range" + ], + "instillFormat": "object", + "type": "object" + }, { "properties": { "range": { @@ -483,7 +572,7 @@ "type": "object" }, "output": { - "description": "Get an issue in Jira", + "description": "Get issues in Jira", "instillUIOrder": 0, "properties": { "issues": { @@ -566,71 +655,7 @@ "output": { "description": "Get an issue in Jira", "instillUIOrder": 0, - "properties": { - "id": { - "description": "The ID of the issue", - "instillUIOrder": 0, - "title": "ID", - "instillFormat": "string", - "type": "string" - }, - "key": { - "description": "The key of the issue", - "instillUIOrder": 1, - "instillFormat": "string", - "title": "Key", - "type": "string" - }, - "self": { - "description": "The URL of the issue", - "instillUIOrder": 2, - "instillFormat": "string", - "title": "Self", - "type": "string" - }, - "fields": { - "description": "The fields of the issue. All navigable and Agile fields are returned", - "instillUIOrder": 3, - "instillFormat": "object", - "title": "Fields", - "type": "object", - "required": [] - }, - "issue-type": { - "description": "The type of the issue, can be: `Task`, `Epic`", - "instillUIOrder": 4, - "instillFormat": "string", - "title": "Issue Type", - "type": "string" - }, - "summary": { - "description": "The summary of the issue", - "instillUIOrder": 5, - "instillFormat": "string", - "title": "Summary", - "type": "string" - }, - "description": { - "description": "The description of the issue", - "instillUIOrder": 6, - "instillFormat": "string", - "title": "Description", - "type": "string" - }, - "status": { - "description": "The status of the issue, can be: `To Do`, `In Progress`, `Done`", - "instillUIOrder": 7, - "instillFormat": "string", - "title": "Status", - "type": "string" - } - }, - "required": [ - "id", - "key", - "self", - "fields" - ], + "$ref": "#/$defs/issue", "title": "Output", "type": "object" } @@ -669,73 +694,9 @@ "type": "object" }, "output": { - "description": "Get an issue in Jira", + "description": "Get an sprint in Jira", "instillUIOrder": 0, - "properties": { - "id": { - "title": "ID", - "description": "The ID of the sprint", - "type": "integer", - "instillUIOrder": 0, - "instillFormat": "integer" - }, - "self": { - "title": "Self", - "description": "The URL of the sprint", - "type": "string", - "instillUIOrder": 1, - "instillFormat": "string" - }, - "state": { - "title": "State", - "description": "The state of the sprint, can be: `active`, `closed`, `future`", - "type": "string", - "instillUIOrder": 2, - "instillFormat": "string" - }, - "name": { - "title": "Name", - "description": "The name of the sprint", - "type": "string", - "instillUIOrder": 3, - "instillFormat": "string" - }, - "start-date": { - "title": "Start Date", - "description": "The start date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", - "type": "string", - "instillUIOrder": 4, - "instillFormat": "string" - }, - "end-date": { - "title": "End Date", - "description": "The end date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", - "type": "string", - "instillUIOrder": 5, - "instillFormat": "string" - }, - "complete-date": { - "title": "Complete Date", - "description": "The complete date of the sprint. In the RFC3339 format, e.g. 2018-03-05T00:00:00Z", - "type": "string", - "instillUIOrder": 6, - "instillFormat": "string" - }, - "origin-board-id": { - "title": "Origin Board ID", - "description": "The ID of the origin board", - "type": "integer", - "instillUIOrder": 7, - "instillFormat": "integer" - }, - "goal": { - "title": "Goal", - "description": "The Goal of the sprint", - "type": "string", - "instillUIOrder": 8, - "instillFormat": "string" - } - }, + "$ref": "#/$defs/sprint", "required": [], "title": "Output", "type": "object" diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index ca796f6c..8bb3779d 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -4,7 +4,6 @@ import ( "context" _ "embed" "fmt" - "slices" "strings" "github.com/go-resty/resty/v2" @@ -141,6 +140,7 @@ type Range struct { Range string `json:"range,omitempty"` EpicKey string `json:"epic-key,omitempty"` SprintKey string `json:"sprint-key,omitempty"` + JQL string `json:"jql,omitempty"` } type ListIssuesInput struct { BoardID int `json:"board-id,omitempty" api:"boardId"` @@ -209,6 +209,11 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St case "Issues without epic assigned": // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-epic-none-issue-get apiEndpoint = apiEndpoint + "/epic/none/issue" + case "Standard Issues": + // https://support.atlassian.com/jira-cloud-administration/docs/what-are-issue-types/ + jql = fmt.Sprintf("project=\"%s\" AND issuetype not in (Epic, subtask)", boardKey) + case "JQL query": + jql = opt.Range.JQL default: return nil, errmsg.AddMessage( fmt.Errorf("invalid range"), @@ -217,7 +222,7 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St } var resp *resty.Response - if slices.Contains([]string{"Issues of an epic", "Issues of a sprint"}, opt.Range.Range) { + if jql != "" { resp, err = jiraClient.nextGenIssuesSearch(ctx, nextGenSearchRequest{ JQL: jql, MaxResults: opt.MaxResults, From 1d306363b8c4800f1091db87874fa948af509e88 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 22 Jul 2024 12:34:42 +0100 Subject: [PATCH 21/28] feat: add list sprint --- application/jira/v0/config/definition.json | 1 + application/jira/v0/config/tasks.json | 85 ++++++++++++++++++++++ application/jira/v0/main.go | 13 ++-- application/jira/v0/sprint.go | 57 +++++++++++++++ 4 files changed, 151 insertions(+), 5 deletions(-) diff --git a/application/jira/v0/config/definition.json b/application/jira/v0/config/definition.json index e177f313..caf07d46 100644 --- a/application/jira/v0/config/definition.json +++ b/application/jira/v0/config/definition.json @@ -2,6 +2,7 @@ "availableTasks": [ "TASK_LIST_BOARDS", "TASK_LIST_ISSUES", + "TASK_LIST_SPRINTS", "TASK_GET_ISSUE", "TASK_GET_SPRINT" ], diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index d78fd00a..197421f3 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -615,6 +615,91 @@ "type": "object" } }, + "TASK_LIST_SPRINTS": { + "description": "List sprints in Jira", + "instillShortDescription": "List sprints in Jira", + "input": { + "description": "List sprints in Jira", + "instillUIOrder": 0, + "instillEditOnNodeFields": [ + "board-id" + ], + "properties": { + "board-id": { + "title": "Board ID", + "description": "The ID of the board", + "instillShortDescription": "The ID of the board", + "instillUIOrder": 0, + "instillFormat": "integer", + "instillAcceptFormats": [ + "integer" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], + "type": "integer" + }, + "start-at": { + "$ref": "#/$defs/common-query-params/start-at", + "instillUIOrder": 1 + }, + "max-results": { + "$ref": "#/$defs/common-query-params/max-results", + "instillUIOrder": 2 + } + }, + "required": [ + "board-id" + ], + "title": "Input", + "type": "object" + }, + "output": { + "description": "Get sprints in Jira", + "instillUIOrder": 0, + "properties": { + "sprints": { + "description": "A array of sprints in Jira", + "instillUIOrder": 1, + "title": "Sprints", + "type": "array", + "items": { + "$ref": "#/$defs/sprint" + } + }, + "start-at": { + "description": "The starting index of the returned boards. Base index: 0", + "instillUIOrder": 2, + "title": "Start At", + "instillFormat": "integer", + "type": "integer" + }, + "max-results": { + "description": "The maximum number of boards", + "instillUIOrder": 3, + "title": "Max Results", + "instillFormat": "integer", + "type": "integer" + }, + "total": { + "description": "The total number of boards", + "instillUIOrder": 4, + "title": "Total", + "instillFormat": "integer", + "type": "integer" + } + }, + "required": [ + "start-at", + "max-results", + "total" + ], + "title": "Output", + "type": "object" + } + }, "TASK_GET_ISSUE": { "description": "Get an issue in Jira. The issue will only be returned if the user has permission to view it. Issues returned from this resource include Agile fields, like sprint, closedSprints, flagged, and epic.", "instillShortDescription": "Get an issue in Jira", diff --git a/application/jira/v0/main.go b/application/jira/v0/main.go index 230a78e3..6c6d6b48 100644 --- a/application/jira/v0/main.go +++ b/application/jira/v0/main.go @@ -15,11 +15,12 @@ import ( ) const ( - apiBaseURL = "https://api.atlassian.com" - taskListBoards = "TASK_LIST_BOARDS" - taskListIssues = "TASK_LIST_ISSUES" - taskGetIssue = "TASK_GET_ISSUE" - taskGetSprint = "TASK_GET_SPRINT" + apiBaseURL = "https://api.atlassian.com" + taskListBoards = "TASK_LIST_BOARDS" + taskListIssues = "TASK_LIST_ISSUES" + taskListSprints = "TASK_LIST_SPRINTS" + taskGetIssue = "TASK_GET_ISSUE" + taskGetSprint = "TASK_GET_SPRINT" ) var ( @@ -73,6 +74,8 @@ func (c *component) CreateExecution(sysVars map[string]any, setup *structpb.Stru e.execute = e.client.listBoardsTask case taskListIssues: e.execute = e.client.listIssuesTask + case taskListSprints: + e.execute = e.client.listSprintsTask case taskGetIssue: e.execute = e.client.getIssueTask case taskGetSprint: diff --git a/application/jira/v0/sprint.go b/application/jira/v0/sprint.go index aa9ee84b..409c1d82 100644 --- a/application/jira/v0/sprint.go +++ b/application/jira/v0/sprint.go @@ -95,3 +95,60 @@ func (jiraClient *Client) getSprintTask(_ context.Context, props *structpb.Struc out := extractSprintOutput(issue) return base.ConvertToStructpb(out) } + +type ListSprintInput struct { + BoardID int `json:"board-id"` +} + +type ListSprintsResp struct { + Values []Sprint `json:"values"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` + Total int `json:"total"` +} +type ListSprintsOutput struct { + Sprints []*GetSprintOutput `json:"sprints"` + StartAt int `json:"start-at"` + MaxResults int `json:"max-results"` + Total int `json:"total"` +} + +func (jiraClient *Client) listSprintsTask(_ context.Context, props *structpb.Struct) (*structpb.Struct, error) { + var debug DebugSession + debug.SessionStart("listSprintsTask", StaticVerboseLevel) + defer debug.SessionEnd() + + var opt ListSprintInput + if err := base.ConvertFromStructpb(props, &opt); err != nil { + return nil, err + } + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", opt.BoardID) + + req := jiraClient.Client.R().SetResult(&ListSprintsResp{}) + resp, err := req.Get(apiEndpoint) + + if err != nil { + return nil, fmt.Errorf( + err.Error(), errmsg.Message(err), + ) + } + debug.AddMessage("GET", apiEndpoint) + debug.AddMapMessage("QueryParam", resp.Request.QueryParam) + debug.AddMessage("Status", resp.Status()) + + issues, ok := resp.Result().(*ListSprintsResp) + if !ok { + return nil, errmsg.AddMessage( + fmt.Errorf("failed to convert response to `List Sprint` Output"), + fmt.Sprintf("failed to convert %v to `List Sprint` Output", resp.Result()), + ) + } + var out ListSprintsOutput + for _, issue := range issues.Values { + out.Sprints = append(out.Sprints, extractSprintOutput(&issue)) + } + out.StartAt = issues.StartAt + out.MaxResults = issues.MaxResults + out.Total = issues.Total + return base.ConvertToStructpb(out) +} From d19e2a90e3220fdad02637455db11355ad7f681d Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 22 Jul 2024 12:38:51 +0100 Subject: [PATCH 22/28] feat: reoder list issue, add jql --- application/jira/v0/config/tasks.json | 28 ++++++++++++++++++++++----- application/jira/v0/issues.go | 3 --- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 197421f3..ec771c4c 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -441,7 +441,7 @@ { "properties": { "range": { - "const": "Sprints only", + "const": "In backlog only", "type": "string" } }, @@ -525,7 +525,7 @@ { "properties": { "range": { - "const": "In backlog only", + "const": "Issues without epic assigned", "type": "string" } }, @@ -541,15 +541,33 @@ { "properties": { "range": { - "const": "Issues without epic assigned", + "const": "JQL query", + "type": "string" + }, + "jql": { + "title": "JQL", + "description": "The JQL query. For example, `project = JRA AND status = Done`. For more information, see [Advanced searching reference](https://support.atlassian.com/jira-software-cloud/docs/what-is-advanced-search-in-jira-cloud/)", + "instillShortDescription": "The JQL query", + "instillUIOrder": 10, + "instillFormat": "string", + "instillAcceptFormats": [ + "string" + ], + "instillUpstreamTypes": [ + "value", + "reference", + "template" + ], "type": "string" } }, "required": [ - "range" + "range", + "jql" ], "instillEditOnNodeFields": [ - "range" + "range", + "jql" ], "instillFormat": "object", "type": "object" diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 8bb3779d..2f1df081 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -192,9 +192,6 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St case "Epics only": // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-epic-get apiEndpoint = apiEndpoint + "/epic" - case "Sprints only": - // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-sprint-get - apiEndpoint = apiEndpoint + "/sprint" case "Issues of an epic": // API not working: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-epic-epicid-issue-get // use JQL instead From c6bb84a2354236c0a5cb8685a4e8fefbc81c3776 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 22 Jul 2024 14:56:35 +0100 Subject: [PATCH 23/28] fix: testing problem --- application/jira/v0/component_test.go | 129 +++++++++++++++++++------- application/jira/v0/config/tasks.json | 2 +- application/jira/v0/issues.go | 47 ++-------- application/jira/v0/mock_database.go | 16 ---- application/jira/v0/mock_server.go | 108 +++++++++++++++------ application/jira/v0/sprint.go | 25 +++-- 6 files changed, 193 insertions(+), 134 deletions(-) diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index 37e8a719..4c7b02f4 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -267,42 +267,6 @@ func TestComponent_ListIssuesTask(t *testing.T) { }, }, }, - { - _type: "ok", - name: "Sprints only", - input: ListIssuesInput{ - BoardID: 1, - MaxResults: 10, - StartAt: 0, - Range: Range{ - Range: "Sprints only", - }, - }, - wantResp: ListIssuesOutput{ - Total: 1, - StartAt: 0, - MaxResults: 10, - Issues: []Issue{ - { - ID: "4", - Key: "KAN-4", - Fields: map[string]interface{}{ - "summary": "Test issue 4", - "status": map[string]interface{}{ - "name": "Done", - }, - "issuetype": map[string]interface{}{ - "name": "Sprint", - }, - }, - IssueType: "Sprint", - Self: "https://test.atlassian.net/rest/agile/1.0/issue/4", - Status: "Done", - Summary: "Test issue 4", - }, - }, - }, - }, { _type: "ok", name: "In backlog only", @@ -432,10 +396,103 @@ func TestComponent_ListIssuesTask(t *testing.T) { Issues: []Issue{}, }, }, + { + _type: "ok", + name: "Standard Issues", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "Standard Issues", + }, + }, + wantResp: ListIssuesOutput{ + Total: 0, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{}, + }, + }, + { + _type: "ok", + name: "JQL", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "JQL query", + JQL: "project = TST", + }, + }, + wantResp: ListIssuesOutput{ + Total: 0, + StartAt: 0, + MaxResults: 10, + Issues: []Issue{}, + }, + }, + { + _type: "nok", + name: "invalid range", + input: ListIssuesInput{ + BoardID: 1, + MaxResults: 10, + StartAt: 0, + Range: Range{ + Range: "invalid", + }, + }, + wantErr: "invalid range", + }, } taskTesting(testcases, taskListIssues, t) } +func TestComponent_ListSprintsTask(t *testing.T) { + testcases := []TaskCase[ListSprintInput, ListSprintsOutput]{ + { + _type: "ok", + name: "get all sprints", + input: ListSprintInput{ + BoardID: 1, + StartAt: 0, + MaxResults: 10, + }, + wantResp: ListSprintsOutput{ + Total: 1, + StartAt: 0, + MaxResults: 10, + Sprints: []*GetSprintOutput{ + { + ID: 1, + Self: "https://test.atlassian.net/rest/agile/1.0/sprint/1", + State: "active", + Name: "Sprint 1", + StartDate: "2021-01-01T00:00:00.000Z", + EndDate: "2021-01-15T00:00:00.000Z", + CompleteDate: "2021-01-15T00:00:00.000Z", + OriginBoardID: 1, + Goal: "Sprint goal", + }, + }, + }, + }, + { + _type: "nok", + name: "400 - Bad Request", + input: ListSprintInput{ + BoardID: -1, + StartAt: 0, + MaxResults: 10, + }, + wantErr: "unsuccessful HTTP response.*", + }, + } + taskTesting(testcases, taskListSprints, t) +} + func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType], task string, t *testing.T) { c := qt.New(t) ctx := context.Background() diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index ec771c4c..871d27e1 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -73,7 +73,7 @@ "fields": { "description": "The fields of the issue. All navigable and Agile fields are returned", "instillUIOrder": 3, - "instillFormat": "object", + "instillFormat": "semi-structured/json", "title": "Fields", "type": "object", "required": [] diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 2f1df081..eb50e67d 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -22,15 +22,6 @@ type Issue struct { IssueType string `json:"issue-type"` Status string `json:"status"` } -type SprintOrEpic struct { - ID int `json:"id"` - Key string `json:"key"` - Name string `json:"name"` - Summary string `json:"summary"` - Self string `json:"self"` - Done bool `json:"done"` - Fields map[string]interface{} `json:"fields"` -} type GetIssueInput struct { IssueKeyOrID string `json:"issue-id-or-key,omitempty" api:"issueIdOrKey"` @@ -70,24 +61,6 @@ func extractIssue(issue *Issue) *Issue { return issue } -func transformToIssue(val *SprintOrEpic) *Issue { - fields := make(map[string]interface{}) - if val.Fields != nil { - for key, value := range val.Fields { - fields[key] = value - } - } - - return &Issue{ - ID: fmt.Sprintf("%d", val.ID), - Key: val.Key, - Description: val.Name, - Summary: val.Summary, - Self: val.Self, - Fields: fields, - } -} - func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { var debug DebugSession debug.SessionStart("getIssueTask", StaticVerboseLevel) @@ -150,11 +123,10 @@ type ListIssuesInput struct { } type ListIssuesResp struct { - Issues []Issue `json:"issues"` - Values []SprintOrEpic `json:"values"` - StartAt int `json:"startAt"` - MaxResults int `json:"maxResults"` - Total int `json:"total"` + Issues []Issue `json:"issues"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` + Total int `json:"total"` } type ListIssuesOutput struct { Issues []Issue `json:"issues"` @@ -251,13 +223,8 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St ) } - if issues.Issues == nil && issues.Values == nil { + if issues.Issues == nil { issues.Issues = []Issue{} - } else if issues.Issues == nil { - issues.Issues = make([]Issue, len(issues.Values)) - for i, val := range issues.Values { - issues.Issues[i] = *transformToIssue(&val) - } } output := ListIssuesOutput{ @@ -270,8 +237,6 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St output.Issues[idx] = *extractIssue(&issue) if opt.Range.Range == "Epics only" { output.Issues[idx].IssueType = "Epic" - } else if opt.Range.Range == "Sprints only" { - output.Issues[idx].IssueType = "Sprint" } } return base.ConvertToStructpb(output) @@ -288,7 +253,7 @@ type nextGenSearchRequest struct { // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-post func (jiraClient *Client) nextGenIssuesSearch(_ context.Context, opt nextGenSearchRequest) (*resty.Response, error) { var debug DebugSession - debug.SessionStart("nextGenIssuesSearch", StaticVerboseLevel) + debug.SessionStart("nextGenIssuesSearch", DevelopVerboseLevel) defer debug.SessionEnd() debug.AddMessage("opt:") diff --git a/application/jira/v0/mock_database.go b/application/jira/v0/mock_database.go index 1a4856b7..34b2339c 100644 --- a/application/jira/v0/mock_database.go +++ b/application/jira/v0/mock_database.go @@ -45,12 +45,6 @@ type FakeIssue struct { Self string `json:"self"` Fields map[string]interface{} `json:"fields"` } -type FakeSprintAsIssue struct { - ID int `json:"id"` - Key string `json:"key"` - Self string `json:"self"` - Fields map[string]interface{} `json:"fields"` -} func (f *FakeIssue) getSelf() string { if f.Self == "" { @@ -117,16 +111,6 @@ var fakeIssues = []FakeIssue{ }, } -func (f *FakeIssue) toSprint() func() { - recovery := f.Fields["issuetype"].(map[string]interface{})["name"].(string) - f.Fields["issuetype"] = map[string]interface{}{ - "name": "Sprint", - } - return func() { - f.Fields["issuetype"].(map[string]interface{})["name"] = recovery - } -} - type FakeSprint struct { ID int `json:"id"` Self string `json:"self"` diff --git a/application/jira/v0/mock_server.go b/application/jira/v0/mock_server.go index 4f30f5d5..c3eb09ce 100644 --- a/application/jira/v0/mock_server.go +++ b/application/jira/v0/mock_server.go @@ -26,7 +26,7 @@ func router(middlewares ...func(http.Handler) http.Handler) http.Handler { r.Get("/rest/agile/1.0/sprint/{sprintId}", mockGetSprint) r.Get("/rest/agile/1.0/board/{boardId}/issue", mockListIssues) // list all issues r.Get("/rest/agile/1.0/board/{boardId}/epic", mockListIssues) // list all epic - r.Get("/rest/agile/1.0/board/{boardId}/sprint", mockListIssues) // list all sprint + r.Get("/rest/agile/1.0/board/{boardId}/sprint", mockListSprints) // list all sprint r.Get("/rest/agile/1.0/board/{boardId}/backlog", mockListIssues) // list all issues in backlog r.Get("/rest/agile/1.0/board/{boardId}/epic/none/issue", mockListIssues) // list all issues without epic assigned r.Get("/rest/agile/1.0/board/{boardId}", mockGetBoard) @@ -193,11 +193,10 @@ func mockGetSprint(res http.ResponseWriter, req *http.Request) { } type MockListIssuesResponse struct { - Values []FakeSprintAsIssue `json:"values"` - Issues []FakeIssue `json:"issues"` - Total int `json:"total"` - StartAt int `json:"startAt"` - MaxResults int `json:"maxResults"` + Issues []FakeIssue `json:"issues"` + Total int `json:"total"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` } func mockListIssues(res http.ResponseWriter, req *http.Request) { @@ -233,26 +232,8 @@ func mockListIssues(res http.ResponseWriter, req *http.Request) { continue } issue.getSelf() - if strings.Contains(req.URL.Path, "sprint") { - defer issue.toSprint()() - } issues = append(issues, issue) } - var fakeSprintAsIssue []FakeSprintAsIssue - if strings.Contains(req.URL.Path, "sprint") { - for _, sprint := range issues { - idNum, err := strconv.Atoi(sprint.ID) - if err != nil { - continue - } - fakeSprintAsIssue = append(fakeSprintAsIssue, FakeSprintAsIssue{ - ID: idNum, - Key: sprint.Key, - Self: sprint.getSelf(), - Fields: sprint.Fields, - }) - } - } // response res.WriteHeader(http.StatusOK) startAtNum := 0 @@ -272,9 +253,82 @@ func mockListIssues(res http.ResponseWriter, req *http.Request) { StartAt: startAtNum, MaxResults: maxResultsNum, } - if strings.Contains(req.URL.Path, "sprint") { - resp.Values = fakeSprintAsIssue - resp.Issues = nil + respText, err := json.Marshal(resp) + if err != nil { + return + } + _, _ = res.Write([]byte(respText)) +} + +type MockListSprintsResponse struct { + Values []FakeSprint `json:"values"` + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` + Total int `json:"total"` +} + +func mockListSprints(res http.ResponseWriter, req *http.Request) { + var err error + opt := req.URL.Query() + boardID := chi.URLParam(req, "boardId") + state := opt.Get("state") + startAt := opt.Get("startAt") + maxResults := opt.Get("maxResults") + // find board + var board *FakeBoard + for _, b := range fakeBoards { + if strconv.Itoa(b.ID) == boardID { + board = &b + break + } + } + if board == nil { + res.WriteHeader(http.StatusNotFound) + _, _ = res.Write([]byte(`{"errorMessages":["Board does not exist or you do not have permission to see it"]}`)) + return + } + // filter sprints + var sprints []FakeSprint + for _, sprint := range fakeSprints { + if sprint.ID != board.ID { + continue + } + if state != "" && sprint.State != state { + continue + } + sprints = append(sprints, sprint) + } + // pagination + start, end := 0, len(sprints) + if startAt != "" { + start, err = strconv.Atoi(startAt) + if err != nil { + return + } + } + maxResultsNum := len(sprints) + if maxResults != "" { + maxResultsNum, err = strconv.Atoi(maxResults) + if err != nil { + return + } + end = start + maxResultsNum + if end > len(sprints) { + end = len(sprints) + } + } + // response + res.WriteHeader(http.StatusOK) + + resp := MockListSprintsResponse{ + Values: sprints[start:end], + StartAt: start, + MaxResults: maxResultsNum, + Total: len(sprints), + } + for i := range resp.Values { + resp.Values[i].getSelf() + } respText, err := json.Marshal(resp) if err != nil { diff --git a/application/jira/v0/sprint.go b/application/jira/v0/sprint.go index 409c1d82..14aab923 100644 --- a/application/jira/v0/sprint.go +++ b/application/jira/v0/sprint.go @@ -65,17 +65,6 @@ func (jiraClient *Client) getSprintTask(_ context.Context, props *structpb.Struc req := jiraClient.Client.R().SetResult(&Sprint{}) resp, err := req.Get(apiEndpoint) - if resp != nil && resp.StatusCode() == 404 { - return nil, fmt.Errorf( - err.Error(), - errmsg.Message(err)+"Please check you have the correct permissions to access this resource.", - ) - } else if resp != nil && resp.StatusCode() == 401 { - return nil, fmt.Errorf( - err.Error(), - errmsg.Message(err)+"You are not logged in. Please provide a valid token and an email account.", - ) - } if err != nil { return nil, fmt.Errorf( err.Error(), errmsg.Message(err), @@ -97,7 +86,9 @@ func (jiraClient *Client) getSprintTask(_ context.Context, props *structpb.Struc } type ListSprintInput struct { - BoardID int `json:"board-id"` + BoardID int `json:"board-id"` + StartAt int `json:"start-at" api:"startAt"` + MaxResults int `json:"max-results" api:"maxResults"` } type ListSprintsResp struct { @@ -115,16 +106,24 @@ type ListSprintsOutput struct { func (jiraClient *Client) listSprintsTask(_ context.Context, props *structpb.Struct) (*structpb.Struct, error) { var debug DebugSession - debug.SessionStart("listSprintsTask", StaticVerboseLevel) + debug.SessionStart("listSprintsTask", DevelopVerboseLevel) defer debug.SessionEnd() var opt ListSprintInput if err := base.ConvertFromStructpb(props, &opt); err != nil { return nil, err } + debug.AddMapMessage("props", props) + debug.AddMapMessage("opt", opt) apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", opt.BoardID) req := jiraClient.Client.R().SetResult(&ListSprintsResp{}) + opt.BoardID = 0 + err := addQueryOptions(req, opt) + if err != nil { + return nil, err + } + resp, err := req.Get(apiEndpoint) if err != nil { From 416bac41e7c8cca84ab0fb22084ec68b8daa0924 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 22 Jul 2024 14:57:11 +0100 Subject: [PATCH 24/28] chore: update readme --- application/jira/v0/README.mdx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/application/jira/v0/README.mdx b/application/jira/v0/README.mdx index 16569f57..cd9e8d56 100644 --- a/application/jira/v0/README.mdx +++ b/application/jira/v0/README.mdx @@ -10,6 +10,7 @@ It can carry out the following tasks: - [List Boards](#list-boards) - [List Issues](#list-issues) +- [List Sprints](#list-sprints) - [Get Issue](#get-issue) - [Get Sprint](#get-sprint) @@ -98,6 +99,32 @@ List issues in Jira +### List Sprints + +List sprints in Jira + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_LIST_SPRINTS` | +| Board ID (required) | `board-id` | integer | The ID of the board | +| Start At | `start-at` | integer | The starting index of the returned boards. Base index: 0. Default is 0 | +| Max Results | `max-results` | integer | The maximum number of boards to return. Default is 50 | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Sprints (optional) | `sprints` | array[object] | A array of sprints in Jira | +| Start At | `start-at` | integer | The starting index of the returned boards. Base index: 0 | +| Max Results | `max-results` | integer | The maximum number of boards | +| Total | `total` | integer | The total number of boards | + + + + + + ### Get Issue Get an issue in Jira From eed8d7e28026bc6d95d02ba91094f012fa814069 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Mon, 22 Jul 2024 15:03:50 +0100 Subject: [PATCH 25/28] chore: silence logger --- application/jira/v0/issues.go | 2 +- application/jira/v0/sprint.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index eb50e67d..a60dca9d 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -253,7 +253,7 @@ type nextGenSearchRequest struct { // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-post func (jiraClient *Client) nextGenIssuesSearch(_ context.Context, opt nextGenSearchRequest) (*resty.Response, error) { var debug DebugSession - debug.SessionStart("nextGenIssuesSearch", DevelopVerboseLevel) + debug.SessionStart("nextGenIssuesSearch", StaticVerboseLevel) defer debug.SessionEnd() debug.AddMessage("opt:") diff --git a/application/jira/v0/sprint.go b/application/jira/v0/sprint.go index 14aab923..b4cf1cd0 100644 --- a/application/jira/v0/sprint.go +++ b/application/jira/v0/sprint.go @@ -106,7 +106,7 @@ type ListSprintsOutput struct { func (jiraClient *Client) listSprintsTask(_ context.Context, props *structpb.Struct) (*structpb.Struct, error) { var debug DebugSession - debug.SessionStart("listSprintsTask", DevelopVerboseLevel) + debug.SessionStart("listSprintsTask", StaticVerboseLevel) defer debug.SessionEnd() var opt ListSprintInput From 2817bb140cd9dfe4af1ba34b7d4caa95bf95283e Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Tue, 23 Jul 2024 18:35:54 +0100 Subject: [PATCH 26/28] chore: change params name for better clarity --- application/jira/v0/component_test.go | 10 ++++----- application/jira/v0/config/tasks.json | 32 +++++++++++++-------------- application/jira/v0/issues.go | 18 +++++++-------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index 4c7b02f4..c18d7664 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -88,7 +88,7 @@ func TestComponent_GetIssueTask(t *testing.T) { _type: "ok", name: "get issue-Task", input: GetIssueInput{ - IssueKeyOrID: "1", + IssueKey: "TST-1", UpdateHistory: true, }, wantResp: GetIssueOutput{ @@ -117,7 +117,7 @@ func TestComponent_GetIssueTask(t *testing.T) { _type: "ok", name: "get issue-Epic", input: GetIssueInput{ - IssueKeyOrID: "4", + IssueKey: "KAN-4", UpdateHistory: false, }, wantResp: GetIssueOutput{ @@ -144,7 +144,7 @@ func TestComponent_GetIssueTask(t *testing.T) { _type: "nok", name: "404 - Not Found", input: GetIssueInput{ - IssueKeyOrID: "5", + IssueKey: "5", UpdateHistory: true, }, wantErr: "unsuccessful HTTP response.*", @@ -385,8 +385,8 @@ func TestComponent_ListIssuesTask(t *testing.T) { MaxResults: 10, StartAt: 0, Range: Range{ - Range: "Issues of a sprint", - SprintKey: "1", + Range: "Issues of a sprint", + SprintName: "KAN Sprint 1", }, }, wantResp: ListIssuesOutput{ diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 871d27e1..eb6a183e 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -34,7 +34,7 @@ "type": "integer" }, "update-history": { - "description": "Whether the project in which the issue is created is added to the user's Recently viewed project list, as shown under Projects in Jira.", + "description": "Whether the action taken is added to the user's Recent history, as shown under `Your Work` in Jira.", "title": "Update History", "instillUIOrder": 5, "instillFormat": "boolean", @@ -57,7 +57,7 @@ "type": "string" }, "key": { - "description": "The key of the issue", + "description": "The key of the issue, e.g. `JRA-1330`", "instillUIOrder": 1, "instillFormat": "string", "title": "Key", @@ -201,7 +201,7 @@ "default": "", "title": "Project Key or ID", "description": "This filters results to boards that are relevant to a project. Relevance meaning that the JQL filter defined in board contains a reference to a project.", - "instillShortDescription": "The project key or ID. Default is empty", + "instillShortDescription": "The project key or ID, e.g. `INS`. Default is empty", "instillUIOrder": 0, "instillFormat": "string", "instillAcceptFormats": [ @@ -462,7 +462,7 @@ }, "epic-key": { "title": "Epic Key", - "description": "The Key of the epic", + "description": "The Key of the epic, e.g. `JRA-1330`", "instillShortDescription": "The Key of the epic", "instillUIOrder": 10, "instillFormat": "string", @@ -494,10 +494,10 @@ "const": "Issues of a sprint", "type": "string" }, - "sprint-key": { - "title": "Sprint Key", - "description": "The Key of the sprint", - "instillShortDescription": "The Key of the sprint", + "sprint-name": { + "title": "Sprint Name", + "description": "The name of the sprint", + "instillShortDescription": "The Name of the sprint", "instillUIOrder": 10, "instillFormat": "string", "instillAcceptFormats": [ @@ -513,11 +513,11 @@ }, "required": [ "range", - "sprint-key" + "sprint-name" ], "instillEditOnNodeFields": [ "range", - "sprint-key" + "sprint-name" ], "instillFormat": "object", "type": "object" @@ -725,13 +725,13 @@ "description": "Get an issue in Jira", "instillUIOrder": 0, "instillEditOnNodeFields": [ - "issue-id-or-key" + "issue-key" ], "properties": { - "issue-id-or-key": { - "title": "Issue ID or Key", - "description": "The ID or key of the issue", - "instillShortDescription": "The ID or key of the issue", + "issue-key": { + "title": "Issue Key", + "description": "The key of the issue, e.g. `JRA-1330`", + "instillShortDescription": "The key of the issue", "instillUIOrder": 0, "instillFormat": "string", "instillAcceptFormats": [ @@ -750,7 +750,7 @@ } }, "required": [ - "issue-id-or-key" + "issue-key" ], "title": "Input", "type": "object" diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index a60dca9d..3341373b 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -24,7 +24,7 @@ type Issue struct { } type GetIssueInput struct { - IssueKeyOrID string `json:"issue-id-or-key,omitempty" api:"issueIdOrKey"` + IssueKey string `json:"issue-key,omitempty" api:"issueIdOrKey"` UpdateHistory bool `json:"update-history,omitempty" api:"updateHistory"` } type GetIssueOutput struct { @@ -72,10 +72,10 @@ func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Stru } debug.AddMessage(fmt.Sprintf("GetIssueInput: %+v", opt)) - apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", opt.IssueKeyOrID) + apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", opt.IssueKey) req := jiraClient.Client.R().SetResult(&Issue{}) - opt.IssueKeyOrID = "" // Remove from query params + opt.IssueKey = "" // Remove from query params err := addQueryOptions(req, opt) if err != nil { return nil, err @@ -110,10 +110,10 @@ func (jiraClient *Client) getIssueTask(ctx context.Context, props *structpb.Stru } type Range struct { - Range string `json:"range,omitempty"` - EpicKey string `json:"epic-key,omitempty"` - SprintKey string `json:"sprint-key,omitempty"` - JQL string `json:"jql,omitempty"` + Range string `json:"range,omitempty"` + EpicKey string `json:"epic-key,omitempty"` + SprintName string `json:"sprint-name,omitempty"` + JQL string `json:"jql,omitempty"` } type ListIssuesInput struct { BoardID int `json:"board-id,omitempty" api:"boardId"` @@ -171,7 +171,7 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St case "Issues of a sprint": // API not working: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-sprint-sprintid-issue-get // use JQL instead - jql = fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", boardKey, opt.Range.SprintKey) + jql = fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", boardKey, opt.Range.SprintName) case "In backlog only": // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-backlog-get apiEndpoint = apiEndpoint + "/backlog" @@ -281,5 +281,3 @@ func (jiraClient *Client) nextGenIssuesSearch(_ context.Context, opt nextGenSear debug.AddMessage("Status", resp.Status()) return resp, nil } - -var JQLReservedWords = []string{"a", "an", "abort", "access", "add", "after", "alias", "all", "alter", "and", "any", "are", "as", "asc", "at", "audit", "avg", "be", "before", "begin", "between", "boolean", "break", "but", "by", "byte", "catch", "cf", "char", "character", "check", "checkpoint", "collate", "collation", "column", "commit", "connect", "continue", "count", "create", "current", "date", "decimal", "declare", "decrement", "default", "defaults", "define", "delete", "delimiter", "desc", "difference", "distinct", "divide", "do", "double", "drop", "else", "empty", "encoding", "end", "equals", "escape", "exclusive", "exec", "execute", "exists", "explain", "false", "fetch", "file", "field", "first", "float", "for", "from", "function", "go", "goto", "grant", "greater", "group", "having", "identified", "if", "immediate", "in", "increment", "index", "initial", "inner", "inout", "input", "insert", "int", "integer", "intersect", "intersection", "into", "is", "isempty", "isnull", "it", "join", "last", "left", "less", "like", "limit", "lock", "long", "max", "min", "minus", "mode", "modify", "modulo", "more", "multiply", "next", "no", "noaudit", "not", "notin", "nowait", "null", "number", "object", "of", "on", "option", "or", "order", "outer", "output", "power", "previous", "prior", "privileges", "public", "raise", "raw", "remainder", "rename", "resource", "return", "returns", "revoke", "right", "row", "rowid", "rownum", "rows", "select", "session", "set", "share", "size", "sqrt", "start", "strict", "string", "subtract", "such", "sum", "synonym", "table", "that", "the", "their", "then", "there", "these", "they", "this", "to", "trans", "transaction", "trigger", "true", "uid", "union", "unique", "update", "user", "validate", "values", "view", "was", "when", "whenever", "where", "while", "will", "with"} From bbcdf0213be1456dc44eb712bf426e9a540177ec Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 24 Jul 2024 11:55:54 +0100 Subject: [PATCH 27/28] chore: improve list issue UX --- application/jira/v0/boards.go | 38 +++++++++++----------- application/jira/v0/component_test.go | 46 ++++++++++++++++++++------ application/jira/v0/config/tasks.json | 20 ++++++------ application/jira/v0/issues.go | 47 +++++++++++++++++++-------- 4 files changed, 99 insertions(+), 52 deletions(-) diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index f1b363c9..812e8895 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -63,14 +63,9 @@ func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.St } func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (*ListBoardsResp, error) { - var debug DebugSession - debug.SessionStart("listBoards", StaticVerboseLevel) - defer debug.SessionEnd() - apiEndpoint := "rest/agile/1.0/board" req := jiraClient.Client.R().SetResult(&ListBoardsResp{}) - debug.AddMapMessage("opt", *opt) err := addQueryOptions(req, *opt) if err != nil { return nil, err @@ -80,30 +75,35 @@ func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (* if err != nil { return nil, err } - debug.AddMessage("GET", apiEndpoint) - debug.AddMapMessage("QueryParam", resp.Request.QueryParam) - debug.AddMessage("Status", resp.Status()) boards := resp.Result().(*ListBoardsResp) return boards, err } -func (jiraClient *Client) getBoard(_ context.Context, boardID int) (*Board, error) { - var debug DebugSession - debug.SessionStart("getBoard", StaticVerboseLevel) - defer debug.SessionEnd() +type GetBoardResp struct { + Location struct { + DisplayName string `json:"displayName"` + Name string `json:"name"` + ProjectKey string `json:"projectKey"` + ProjectID int `json:"projectId"` + ProjectName string `json:"projectName"` + ProjectTypeKey string `json:"projectTypeKey"` + UserAccountID string `json:"userAccountId"` + UserID string `json:"userId"` + } `json:"location"` + Board +} +func (jiraClient *Client) getBoard(_ context.Context, boardID int) (*GetBoardResp, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) - req := jiraClient.Client.R().SetResult(&Board{}) - resp, err := req.Get(apiEndpoint) + req := jiraClient.Client.R().SetResult(&GetBoardResp{}) + resp, err := req.Get(apiEndpoint) if err != nil { return nil, fmt.Errorf( err.Error(), errmsg.Message(err), ) } - debug.AddMessage("GET", apiEndpoint) - debug.AddMapMessage("QueryParam", resp.Request.QueryParam) - debug.AddMessage("Status", resp.Status()) - board := resp.Result().(*Board) - return board, err + result := resp.Result().(*GetBoardResp) + + return result, err } diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index c18d7664..4b1a97c1 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -199,7 +199,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "All", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -235,7 +235,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Epics only", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -271,7 +271,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "In backlog only", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -307,7 +307,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Issues without epic assigned", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -343,7 +343,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Issues of an epic", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -362,7 +362,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Issues of an epic(long query)", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -381,7 +381,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Issues of a sprint", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -400,7 +400,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Standard Issues", input: ListIssuesInput{ - BoardID: 1, + BoardName: "TST", MaxResults: 10, StartAt: 0, Range: Range{ @@ -418,7 +418,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "JQL", input: ListIssuesInput{ - BoardID: 1, + BoardName: "TST", MaxResults: 10, StartAt: 0, Range: Range{ @@ -437,7 +437,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "nok", name: "invalid range", input: ListIssuesInput{ - BoardID: 1, + BoardName: "TST", MaxResults: 10, StartAt: 0, Range: Range{ @@ -493,6 +493,32 @@ func TestComponent_ListSprintsTask(t *testing.T) { taskTesting(testcases, taskListSprints, t) } +func TestAuth_nok(t *testing.T) { + c := qt.New(t) + bc := base.Component{Logger: zap.NewNop()} + connector := Init(bc) + c.Run("nok-empty token", func(c *qt.C) { + setup, err := structpb.NewStruct(map[string]any{ + "token": "", + "email": email, + "base-url": "url", + }) + c.Assert(err, qt.IsNil) + _, err = connector.CreateExecution(nil, setup, "invalid") + c.Assert(err, qt.ErrorMatches, "token not provided") + }) + c.Run("nok-empty email", func(c *qt.C) { + setup, err := structpb.NewStruct(map[string]any{ + "token": token, + "email": "", + "base-url": "url", + }) + c.Assert(err, qt.IsNil) + _, err = connector.CreateExecution(nil, setup, "invalid") + c.Assert(err, qt.ErrorMatches, "email not provided") + }) +} + func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType], task string, t *testing.T) { c := qt.New(t) ctx := context.Background() diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index eb6a183e..9683c58c 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -359,25 +359,25 @@ "description": "List issues in Jira", "instillUIOrder": 0, "instillEditOnNodeFields": [ - "board-id", + "board-name", "range" ], "properties": { - "board-id": { - "title": "Board ID", - "description": "The ID of the board", - "instillShortDescription": "The ID of the board", + "board-name": { + "title": "Board Name", + "description": "The name of the board", + "instillShortDescription": "The name of the board", "instillUIOrder": 0, - "instillFormat": "integer", + "instillFormat": "string", "instillAcceptFormats": [ - "integer" + "string" ], "instillUpstreamTypes": [ "value", "reference", "template" ], - "type": "integer" + "type": "string" }, "range": { "title": "Range", @@ -546,7 +546,7 @@ }, "jql": { "title": "JQL", - "description": "The JQL query. For example, `project = JRA AND status = Done`. For more information, see [Advanced searching reference](https://support.atlassian.com/jira-software-cloud/docs/what-is-advanced-search-in-jira-cloud/)", + "description": "The JQL query. For example, `type = \"Task\" AND status = \"Done\"`. For more information, see [Advanced searching](https://support.atlassian.com/jira-software-cloud/docs/what-is-advanced-search-in-jira-cloud/)", "instillShortDescription": "The JQL query", "instillUIOrder": 10, "instillFormat": "string", @@ -584,7 +584,7 @@ } }, "required": [ - "board-id" + "board-name" ], "title": "Input", "type": "object" diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 3341373b..05cf1010 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -115,11 +115,12 @@ type Range struct { SprintName string `json:"sprint-name,omitempty"` JQL string `json:"jql,omitempty"` } + type ListIssuesInput struct { - BoardID int `json:"board-id,omitempty" api:"boardId"` - MaxResults int `json:"max-results,omitempty" api:"maxResults"` - StartAt int `json:"start-at,omitempty" api:"startAt"` - Range Range `json:"range,omitempty"` + BoardName string `json:"board-name,omitempty" api:"boardName"` + MaxResults int `json:"max-results,omitempty" api:"maxResults"` + StartAt int `json:"start-at,omitempty" api:"startAt"` + Range Range `json:"range,omitempty"` } type ListIssuesResp struct { @@ -137,7 +138,7 @@ type ListIssuesOutput struct { func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { var debug DebugSession - debug.SessionStart("listIssuesTask", StaticVerboseLevel) + debug.SessionStart("listIssuesTask", DevelopVerboseLevel) defer debug.SessionEnd() debug.AddMapMessage("props", props) @@ -149,14 +150,34 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St if err := base.ConvertFromStructpb(props, &opt); err != nil { return nil, err } - debug.AddMapMessage("ListIssuesInput", opt) - board, err := jiraClient.getBoard(ctx, opt.BoardID) + + boards, err := jiraClient.listBoards(ctx, &ListBoardsInput{Name: opt.BoardName}) + if err != nil { + return nil, err + } + if len(boards.Values) == 0 { + return nil, errmsg.AddMessage( + fmt.Errorf("board not found"), + fmt.Sprintf("board with name %s not found", opt.BoardName), + ) + } else if len(boards.Values) > 1 { + return nil, errmsg.AddMessage( + fmt.Errorf("multiple boards found"), + fmt.Sprintf("multiple boards are found with the partial name \"%s\". Please provide a more specific name", opt.BoardName), + ) + } + debug.AddMapMessage("boards", boards) + board := boards.Values[0] + + boardDetails, err := jiraClient.getBoard(ctx, board.ID) if err != nil { return nil, err } - debug.AddMapMessage("board", *board) - boardKey := strings.Split(board.Name, " ")[0] - apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d", opt.BoardID) + projectKey := boardDetails.Location.ProjectKey + if projectKey == "" { + projectKey = strings.Split(board.Name, "-")[0] + } + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d", board.ID) switch opt.Range.Range { case "All": // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-issue-get @@ -167,11 +188,11 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St case "Issues of an epic": // API not working: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-epic-epicid-issue-get // use JQL instead - jql = fmt.Sprintf("project=\"%s\" AND parent=\"%s\"", boardKey, opt.Range.EpicKey) + jql = fmt.Sprintf("project=\"%s\" AND parent=\"%s\"", projectKey, opt.Range.EpicKey) case "Issues of a sprint": // API not working: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-sprint-sprintid-issue-get // use JQL instead - jql = fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", boardKey, opt.Range.SprintName) + jql = fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", projectKey, opt.Range.SprintName) case "In backlog only": // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-backlog-get apiEndpoint = apiEndpoint + "/backlog" @@ -180,7 +201,7 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St apiEndpoint = apiEndpoint + "/epic/none/issue" case "Standard Issues": // https://support.atlassian.com/jira-cloud-administration/docs/what-are-issue-types/ - jql = fmt.Sprintf("project=\"%s\" AND issuetype not in (Epic, subtask)", boardKey) + jql = fmt.Sprintf("project=\"%s\" AND issuetype not in (Epic, subtask)", projectKey) case "JQL query": jql = opt.Range.JQL default: From 31c22d1bd77499eeddd8ece4ddb61b7d30f15f65 Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Thu, 25 Jul 2024 12:45:49 +0100 Subject: [PATCH 28/28] fix: change link format --- application/jira/v0/config/setup.json | 2 +- application/jira/v0/config/tasks.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/jira/v0/config/setup.json b/application/jira/v0/config/setup.json index d06ca2cb..e9b18f77 100644 --- a/application/jira/v0/config/setup.json +++ b/application/jira/v0/config/setup.json @@ -29,7 +29,7 @@ "type": "string" }, "base-url": { - "description": "Fill in your Jira base URL. For example, if your Jira URL is https://mycompany.atlassian.net, then your base URL is https://mycompany.atlassian.net.", + "description": "Fill in your Jira base URL. For example, if your Jira URL is \"https://mycompany.atlassian.net/...\", then your base URL is https://mycompany.atlassian.net.", "instillUpstreamTypes": [ "value", "reference" diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index 9683c58c..e2239ea1 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -546,7 +546,7 @@ }, "jql": { "title": "JQL", - "description": "The JQL query. For example, `type = \"Task\" AND status = \"Done\"`. For more information, see [Advanced searching](https://support.atlassian.com/jira-software-cloud/docs/what-is-advanced-search-in-jira-cloud/)", + "description": "The JQL query. For example, `type = \"Task\" AND status = \"Done\"`. For more information, see Advanced searching", "instillShortDescription": "The JQL query", "instillUIOrder": 10, "instillFormat": "string",