diff --git a/src/appmixer/jira/artifacts/test-flows/test-flow-make-api-call.json b/src/appmixer/jira/artifacts/test-flows/test-flow-make-api-call.json new file mode 100644 index 0000000000..7a2c0532af --- /dev/null +++ b/src/appmixer/jira/artifacts/test-flows/test-flow-make-api-call.json @@ -0,0 +1,422 @@ +{ + "name": "E2E Jira - make-api-call", + "description": "End-to-end test for Jira connector - tests MakeApiCall component using GET, POST, and DELETE HTTP operations including create, verify, and cleanup of a Jira issue", + "flow": { + "on-start": { + "type": "appmixer.utils.controls.OnStart", + "x": 64, + "y": 16, + "source": {}, + "version": "1.0.0", + "config": {} + }, + "set-variables": { + "type": "appmixer.utils.controls.SetVariable", + "x": 256, + "y": 16, + "version": "1.0.0", + "source": { + "in": { + "on-start": [ + "out" + ] + } + }, + "config": { + "transform": { + "in": { + "on-start": { + "out": { + "type": "json2new", + "modifiers": { + "variables": {} + }, + "lambda": { + "variables": { + "ADD": [ + { + "type": "text", + "name": "projectKey", + "text": "TEST" + }, + { + "type": "text", + "name": "issueSummary", + "text": "E2E Test Issue" + } + ] + } + } + } + } + } + } + } + }, + "make-api-call-get-myself": { + "type": "appmixer.jira.core.MakeApiCall", + "x": 448, + "y": 16, + "version": "1.0.0", + "source": { + "in": { + "set-variables": [ + "out" + ] + } + }, + "config": { + "transform": { + "in": { + "set-variables": { + "out": { + "type": "json2new", + "modifiers": { + "url": {}, + "method": {} + }, + "lambda": { + "url": "/myself", + "method": "GET" + } + } + } + } + } + } + }, + "make-api-call-create-issue": { + "type": "appmixer.jira.core.MakeApiCall", + "x": 448, + "y": 144, + "version": "1.0.0", + "source": { + "in": { + "set-variables": [ + "out" + ] + } + }, + "config": { + "transform": { + "in": { + "set-variables": { + "out": { + "type": "json2new", + "modifiers": { + "url": {}, + "method": {}, + "body": { + "pk-var": { + "variable": "$.set-variables.out.projectKey", + "functions": [] + }, + "summary-var": { + "variable": "$.set-variables.out.issueSummary", + "functions": [] + } + } + }, + "lambda": { + "url": "/issue", + "method": "POST", + "body": "{\"fields\":{\"project\":{\"key\":\"{{{pk-var}}}\"},\"summary\":\"{{{summary-var}}}\",\"issuetype\":{\"name\":\"Task\"}}}" + } + } + } + } + } + } + }, + "make-api-call-get-issue": { + "type": "appmixer.jira.core.MakeApiCall", + "x": 640, + "y": 272, + "version": "1.0.0", + "source": { + "in": { + "make-api-call-create-issue": [ + "out" + ] + } + }, + "config": { + "transform": { + "in": { + "make-api-call-create-issue": { + "out": { + "type": "json2new", + "modifiers": { + "url": { + "key-var": { + "variable": "$.make-api-call-create-issue.out.body.key", + "functions": [ + { + "name": "g_javascript", + "params": [ + { + "value": "'/issue/' + input" + } + ] + } + ] + } + }, + "method": {} + }, + "lambda": { + "url": "{{{key-var}}}", + "method": "GET" + } + } + } + } + } + } + }, + "assert-get-myself": { + "type": "appmixer.utils.test.Assert", + "x": 1040, + "y": 16, + "version": "1.0.0", + "source": { + "in": { + "make-api-call-get-myself": [ + "out" + ] + } + }, + "config": { + "transform": { + "in": { + "make-api-call-get-myself": { + "out": { + "type": "json2new", + "modifiers": { + "expression": { + "account-id-check": { + "variable": "$.make-api-call-get-myself.out.body.accountId", + "functions": [] + } + } + }, + "lambda": { + "expression": { + "AND": [ + { + "field": "{{{account-id-check}}}", + "assertion": "notEmpty" + } + ] + } + } + } + } + } + } + } + }, + "assert-create-issue": { + "type": "appmixer.utils.test.Assert", + "x": 1040, + "y": 144, + "version": "1.0.0", + "source": { + "in": { + "make-api-call-create-issue": [ + "out" + ] + } + }, + "config": { + "transform": { + "in": { + "make-api-call-create-issue": { + "out": { + "type": "json2new", + "modifiers": { + "expression": { + "issue-key-check": { + "variable": "$.make-api-call-create-issue.out.body.key", + "functions": [] + } + } + }, + "lambda": { + "expression": { + "AND": [ + { + "field": "{{{issue-key-check}}}", + "assertion": "notEmpty" + } + ] + } + } + } + } + } + } + } + }, + "assert-get-issue": { + "type": "appmixer.utils.test.Assert", + "x": 1040, + "y": 272, + "version": "1.0.0", + "source": { + "in": { + "make-api-call-get-issue": [ + "out" + ] + } + }, + "config": { + "transform": { + "in": { + "make-api-call-get-issue": { + "out": { + "type": "json2new", + "modifiers": { + "expression": { + "summary-check": { + "variable": "$.make-api-call-get-issue.out.body.fields.summary", + "functions": [] + }, + "expected-summary": { + "variable": "$.set-variables.out.issueSummary", + "functions": [] + } + } + }, + "lambda": { + "expression": { + "AND": [ + { + "field": "{{{summary-check}}}", + "assertion": "equal", + "expected": "{{{expected-summary}}}" + } + ] + } + } + } + } + } + } + } + }, + "after-all": { + "type": "appmixer.utils.test.AfterAll", + "x": 1232, + "y": 144, + "version": "1.0.0", + "source": { + "in": { + "assert-get-myself": [ + "out" + ], + "assert-create-issue": [ + "out" + ], + "assert-get-issue": [ + "out" + ] + } + }, + "config": { + "properties": { + "timeout": 30 + } + } + }, + "make-api-call-delete-issue": { + "type": "appmixer.jira.core.MakeApiCall", + "x": 1424, + "y": 144, + "version": "1.0.0", + "source": { + "in": { + "after-all": [ + "out" + ] + } + }, + "config": { + "transform": { + "in": { + "after-all": { + "out": { + "type": "json2new", + "modifiers": { + "url": { + "del-key-var": { + "variable": "$.make-api-call-create-issue.out.body.key", + "functions": [ + { + "name": "g_javascript", + "params": [ + { + "value": "'/issue/' + input" + } + ] + } + ] + } + }, + "method": {} + }, + "lambda": { + "url": "{{{del-key-var}}}", + "method": "DELETE" + } + } + } + } + } + } + }, + "process-results": { + "type": "appmixer.utils.test.ProcessE2EResults", + "x": 1616, + "y": 144, + "version": "1.0.0", + "source": { + "in": { + "make-api-call-delete-issue": [ + "out" + ] + } + }, + "config": { + "properties": { + "successStoreId": "64f6f1f9193228000754082f", + "failedStoreId": "64f6f1f0193228000754082e" + }, + "transform": { + "in": { + "make-api-call-delete-issue": { + "out": { + "type": "json2new", + "modifiers": { + "recipients": {}, + "testCase": {}, + "result": { + "result-var": { + "variable": "$.after-all.out", + "functions": [] + } + } + }, + "lambda": { + "recipients": "jirka@client.io", + "testCase": "E2E Jira - make-api-call", + "result": "{{{result-var}}}" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/appmixer/jira/bundle.json b/src/appmixer/jira/bundle.json index 67235281fd..a306fde016 100644 --- a/src/appmixer/jira/bundle.json +++ b/src/appmixer/jira/bundle.json @@ -1,6 +1,6 @@ { "name": "appmixer.jira", - "version": "3.0.0", + "version": "3.0.1", "changelog": { "1.0.0": [ "Initial version" @@ -53,7 +53,7 @@ "Create Issue, Update Issue: fix support for custom fields." ], "2.0.3": [ - "CreateIssue, IssueMetadata: add pagination support for createmeta endpoints — required fields (e.g. Summary) were missing in inspector when projects had more fields than the default page size (50)." + "CreateIssue, IssueMetadata: add pagination support for createmeta endpoints \u2014 required fields (e.g. Summary) were missing in inspector when projects had more fields than the default page size (50)." ], "3.0.0": [ "Breaking changes: existing Jira connections must be re-authenticated after this update due to changed OAuth scopes.", @@ -61,6 +61,9 @@ "DeleteProject: fix scope from write:jira-work to manage:jira-configuration.", "GetInstanceInfo: add missing scope read:jira-work.", "ListTemplates: remove unnecessary scope (private component, no API call)." + ], + "3.0.1": [ + "Added MakeApiCall component." ] } -} +} \ No newline at end of file diff --git a/src/appmixer/jira/core/MakeApiCall/MakeApiCall.js b/src/appmixer/jira/core/MakeApiCall/MakeApiCall.js new file mode 100644 index 0000000000..c63db0add7 --- /dev/null +++ b/src/appmixer/jira/core/MakeApiCall/MakeApiCall.js @@ -0,0 +1,50 @@ +'use strict'; + +module.exports = { + + async receive(context) { + + const { url, method, headers, parameters, body } = context.messages.in.content; + const { profileInfo, auth } = context; + + // profileInfo.apiUrl = https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/ + // Strip trailing slash so path like /issue/ID works cleanly + const baseUrl = profileInfo.apiUrl.replace(/\/$/, ''); + const fullUrl = baseUrl + url; + + const requestOptions = { + method, + url: fullUrl, + headers: { + 'Authorization': `Bearer ${auth.accessToken}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }; + + if (headers && headers.length > 0) { + headers.forEach(({ key, value }) => { + if (key) requestOptions.headers[key] = value; + }); + } + + if (parameters && parameters.length > 0) { + requestOptions.params = parameters.reduce((acc, { key, value }) => { + if (key) acc[key] = value; + return acc; + }, {}); + } + + if (body) { + requestOptions.data = JSON.parse(body); + } + + const response = await context.httpRequest(requestOptions); + + return context.sendJson({ + status: response.status, + headers: response.headers, + body: response.data + }, 'out'); + } +}; diff --git a/src/appmixer/jira/core/MakeApiCall/component.json b/src/appmixer/jira/core/MakeApiCall/component.json new file mode 100644 index 0000000000..814195cac9 --- /dev/null +++ b/src/appmixer/jira/core/MakeApiCall/component.json @@ -0,0 +1,188 @@ +{ + "name": "appmixer.jira.core.MakeApiCall", + "author": "Appmixer ", + "description": "Performs an arbitrary authorized API call to the Jira API.", + "private": false, + "auth": { + "service": "appmixer:jira", + "scope": [ + "read:jira-work", + "write:jira-work", + "manage:jira-project", + "manage:jira-configuration", + "read:jira-user", + "report:personal-data", + "offline_access" + ] + }, + "inPorts": [ + { + "name": "in", + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET" + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "parameters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "body": { + "type": "string" + } + }, + "required": [ + "url", + "method" + ] + }, + "inspector": { + "inputs": { + "url": { + "type": "text", + "index": 1, + "label": "API Endpoint Path", + "tooltip": "Enter the API endpoint path. This will be appended to your Jira instance base URL (e.g. https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3). For example: /issue/{issueId}." + }, + "method": { + "type": "select", + "index": 2, + "label": "HTTP Method", + "defaultValue": "GET", + "tooltip": "Select the HTTP method for the API call.", + "options": [ + { + "label": "GET", + "value": "GET" + }, + { + "label": "POST", + "value": "POST" + }, + { + "label": "PUT", + "value": "PUT" + }, + { + "label": "PATCH", + "value": "PATCH" + }, + { + "label": "DELETE", + "value": "DELETE" + } + ] + }, + "headers": { + "type": "expression", + "index": 3, + "label": "Headers", + "tooltip": "Enter key-value pairs to be sent as additional request headers.", + "levels": [ + "ADD" + ], + "fields": { + "key": { + "type": "text", + "label": "Key", + "index": 1 + }, + "value": { + "type": "text", + "label": "Value", + "index": 2 + } + } + }, + "parameters": { + "type": "expression", + "index": 4, + "label": "Query Parameters", + "tooltip": "Enter key-value pairs to be sent as query parameters.", + "levels": [ + "ADD" + ], + "fields": { + "key": { + "type": "text", + "label": "Key", + "index": 1 + }, + "value": { + "type": "text", + "label": "Value", + "index": 2 + } + } + }, + "body": { + "type": "textarea", + "index": 5, + "label": "Request Body", + "tooltip": "Enter the request body in JSON format (for POST, PUT, PATCH requests)." + } + } + } + } + ], + "outPorts": [ + { + "name": "out", + "options": [ + { + "label": "Status Code", + "value": "status" + }, + { + "label": "Response Headers", + "value": "headers" + }, + { + "label": "Response Body", + "value": "body", + "schema": { + "type": "object", + "properties": {} + } + } + ] + } + ], + "icon": "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI1MDAiIHZpZXdCb3g9IjIuNTkgMCAyMTQuMDkxMDEwMDggMjI0IiB3aWR0aD0iMjM2MSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGxpbmVhckdyYWRpZW50IGlkPSJhIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDAgMjY0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIxMDIuNCIgeDI9IjU2LjE1IiB5MT0iMjE4LjYzIiB5Mj0iMTcyLjM5Ij48c3RvcCBvZmZzZXQ9Ii4xOCIgc3RvcC1jb2xvcj0iIzAwNTJjYyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzI2ODRmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJiIiB4MT0iMTE0LjY1IiB4Mj0iMTYwLjgxIiB4bGluazpocmVmPSIjYSIgeTE9Ijg1Ljc3IiB5Mj0iMTMxLjkyIi8+PHBhdGggZD0ibTIxNC4wNiAxMDUuNzMtOTYuMzktOTYuMzktOS4zNC05LjM0LTcyLjU2IDcyLjU2LTMzLjE4IDMzLjE3YTguODkgOC44OSAwIDAgMCAwIDEyLjU0bDY2LjI5IDY2LjI5IDM5LjQ1IDM5LjQ0IDcyLjU1LTcyLjU2IDEuMTMtMS4xMiAzMi4wNS0zMmE4Ljg3IDguODcgMCAwIDAgMC0xMi41OXptLTEwNS43MyAzOS4zOS0zMy4xMi0zMy4xMiAzMy4xMi0zMy4xMiAzMy4xMSAzMy4xMnoiIGZpbGw9IiMyNjg0ZmYiLz48cGF0aCBkPSJtMTA4LjMzIDc4Ljg4YTU1Ljc1IDU1Ljc1IDAgMCAxIC0uMjQtNzguNjFsLTcyLjQ3IDcyLjQ0IDM5LjQ0IDM5LjQ0eiIgZmlsbD0idXJsKCNhKSIvPjxwYXRoIGQ9Im0xNDEuNTMgMTExLjkxLTMzLjIgMzMuMjFhNTUuNzcgNTUuNzcgMCAwIDEgMCA3OC44Nmw3Mi42Ny03Mi42M3oiIGZpbGw9InVybCgjYikiLz48L3N2Zz4=" +} \ No newline at end of file