@atlaskit/link-client-extension 8.0.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/compass.yml +7 -4
  3. package/dist/cjs/services/use-integration-actions-client/build-request-headers.js +40 -0
  4. package/dist/cjs/services/use-integration-actions-client/constants.js +18 -0
  5. package/dist/cjs/services/use-integration-actions-client/get-agg-url.js +34 -0
  6. package/dist/cjs/services/use-integration-actions-client/index.js +143 -0
  7. package/dist/cjs/services/use-integration-actions-client/integration-action-error.js +42 -0
  8. package/dist/cjs/services/use-integration-actions-client/mocks.js +91 -0
  9. package/dist/cjs/services/use-integration-actions-client/parse-execute-integration-action-response.js +168 -0
  10. package/dist/cjs/services/use-integration-actions-client/types.js +1 -0
  11. package/dist/es2019/services/use-integration-actions-client/build-request-headers.js +36 -0
  12. package/dist/es2019/services/use-integration-actions-client/constants.js +29 -0
  13. package/dist/es2019/services/use-integration-actions-client/get-agg-url.js +28 -0
  14. package/dist/es2019/services/use-integration-actions-client/index.js +84 -0
  15. package/dist/es2019/services/use-integration-actions-client/integration-action-error.js +21 -0
  16. package/dist/es2019/services/use-integration-actions-client/mocks.js +85 -0
  17. package/dist/es2019/services/use-integration-actions-client/parse-execute-integration-action-response.js +148 -0
  18. package/dist/es2019/services/use-integration-actions-client/types.js +0 -0
  19. package/dist/esm/services/use-integration-actions-client/build-request-headers.js +35 -0
  20. package/dist/esm/services/use-integration-actions-client/constants.js +12 -0
  21. package/dist/esm/services/use-integration-actions-client/get-agg-url.js +28 -0
  22. package/dist/esm/services/use-integration-actions-client/index.js +136 -0
  23. package/dist/esm/services/use-integration-actions-client/integration-action-error.js +35 -0
  24. package/dist/esm/services/use-integration-actions-client/mocks.js +85 -0
  25. package/dist/esm/services/use-integration-actions-client/parse-execute-integration-action-response.js +161 -0
  26. package/dist/esm/services/use-integration-actions-client/types.js +0 -0
  27. package/dist/types/services/use-integration-actions-client/build-request-headers.d.ts +15 -0
  28. package/dist/types/services/use-integration-actions-client/constants.d.ts +9 -0
  29. package/dist/types/services/use-integration-actions-client/get-agg-url.d.ts +13 -0
  30. package/dist/types/services/use-integration-actions-client/index.d.ts +32 -0
  31. package/dist/types/services/use-integration-actions-client/integration-action-error.d.ts +26 -0
  32. package/dist/types/services/use-integration-actions-client/mocks.d.ts +11 -0
  33. package/dist/types/services/use-integration-actions-client/parse-execute-integration-action-response.d.ts +12 -0
  34. package/dist/types/services/use-integration-actions-client/types.d.ts +112 -0
  35. package/package.json +6 -6
@@ -0,0 +1,28 @@
1
+ import { getBaseUrl } from '@atlaskit/linking-common/get-base-url';
2
+ import { EXECUTE_INTEGRATION_ACTION_OPERATION_NAME } from './constants';
3
+ const EDGE_PROXY_AGG_PATH = '/gateway/api/graphql';
4
+
5
+ /**
6
+ * AGG endpoint for the CardClient's environment.
7
+ * - No envKey (running inside a product): same-origin `/gateway/api/graphql`. On the server there
8
+ * is no `window`, so this is relative and requests fail with an `http` error instead of crashing.
9
+ * - `custom`: `baseUrlOverride` is a full object resolver URL (e.g. a direct service URL for SSR),
10
+ * so we can't build an AGG URL from it. Use the relative edge proxy path instead, the same
11
+ * fallback `getResolverUrl` uses.
12
+ * - Other envKeys (e.g. `stg` in examples): the Stargate base URL for that environment + `/graphql`,
13
+ * e.g. `https://pug.jira-dev.com/gateway/api/graphql`.
14
+ * The operation name is added as a query param so requests are easy to identify.
15
+ */
16
+ export const getAggUrl = envKey => {
17
+ let graphqlUrl;
18
+ if (envKey === 'custom') {
19
+ graphqlUrl = EDGE_PROXY_AGG_PATH;
20
+ } else if (envKey) {
21
+ const stargateBaseUrl = getBaseUrl(envKey);
22
+ graphqlUrl = `${stargateBaseUrl.replace(/\/$/, '')}/graphql`;
23
+ } else {
24
+ const origin = getBaseUrl();
25
+ graphqlUrl = `${origin}${EDGE_PROXY_AGG_PATH}`;
26
+ }
27
+ return `${graphqlUrl}?operationName=${EXECUTE_INTEGRATION_ACTION_OPERATION_NAME}`;
28
+ };
@@ -0,0 +1,84 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { request } from '@atlaskit/linking-common/api';
3
+ import { buildRequestHeaders } from './build-request-headers';
4
+ import { EXECUTE_INTEGRATION_ACTION_MUTATION, EXECUTE_INTEGRATION_ACTION_OPERATION_NAME } from './constants';
5
+ import { getAggUrl } from './get-agg-url';
6
+ import { IntegrationActionError } from './integration-action-error';
7
+ import { parseExecuteIntegrationActionResponse } from './parse-execute-integration-action-response';
8
+ // Check the shape rather than `instanceof Response`; `instanceof` checks on errors have caused HOTs before.
9
+ const isResponseLike = value => typeof value === 'object' && value !== null && typeof value.json === 'function';
10
+ const readErrorBody = async response => {
11
+ try {
12
+ return await response.json();
13
+ } catch {
14
+ return undefined;
15
+ }
16
+ };
17
+ const toHttpError = (error, statusCode) => new IntegrationActionError('http', error instanceof Error && error.message ? error.message : `Request failed${statusCode ? ` with status ${statusCode}` : ''}`, {
18
+ statusCode,
19
+ originalError: error
20
+ });
21
+ /**
22
+ * useIntegrationActionsClient
23
+ *
24
+ * Runs Integrations Service (IS) actions on 3P resources (e.g. listing or adding Google Docs
25
+ * comments) through the AGG `actions { execute }` mutation. Uses a plain fetch rather than Relay,
26
+ * so it works in any host that provides a CardClient.
27
+ *
28
+ * `executeAction` resolves with the action's `outputs`. It throws an `IntegrationActionError` when:
29
+ * - the user needs to connect their 3P account (`kind: 'consent-required'`, with `authUrl`)
30
+ * - IS reports the action failed (`kind: 'operation'`)
31
+ * - AGG returns GraphQL errors (`kind: 'graphql'`)
32
+ * - the request fails (`kind: 'http'`)
33
+ *
34
+ * Each request sends the AGG metrics attribution header (`atl-attribution`), using
35
+ * `actionInput.cloudId` as the tenant.
36
+ *
37
+ * Call `executeAction` from effects or event handlers, not during render (it doesn't run in SSR).
38
+ *
39
+ * @param cardClient used to pick the AGG endpoint for the environment
40
+ * @param options.product product hosting the Smart Link, for metrics attribution
41
+ */
42
+ export const useIntegrationActionsClient = (cardClient, {
43
+ product
44
+ } = {}) => {
45
+ const aggUrl = useMemo(() => getAggUrl(cardClient === null || cardClient === void 0 ? void 0 : cardClient.envKey), [cardClient === null || cardClient === void 0 ? void 0 : cardClient.envKey]);
46
+ const executeAction = useCallback(async ({
47
+ actionTypeKey,
48
+ filter,
49
+ actionInput,
50
+ workspace
51
+ }) => {
52
+ let body;
53
+ try {
54
+ body = await request('post', aggUrl, {
55
+ operationName: EXECUTE_INTEGRATION_ACTION_OPERATION_NAME,
56
+ query: EXECUTE_INTEGRATION_ACTION_MUTATION,
57
+ variables: {
58
+ actionTypeKey,
59
+ filter,
60
+ actionInput,
61
+ workspace
62
+ }
63
+ }, buildRequestHeaders({
64
+ cloudId: actionInput === null || actionInput === void 0 ? void 0 : actionInput.cloudId,
65
+ product
66
+ }), [200]);
67
+ } catch (error) {
68
+ // `request` throws the Response for non-2xx statuses. AGG may still include GraphQL errors.
69
+ if (isResponseLike(error)) {
70
+ var _errorBody$errors, _errorBody$data, _errorBody$data$actio;
71
+ const errorBody = await readErrorBody(error);
72
+ if (errorBody !== null && errorBody !== void 0 && (_errorBody$errors = errorBody.errors) !== null && _errorBody$errors !== void 0 && _errorBody$errors.length || errorBody !== null && errorBody !== void 0 && (_errorBody$data = errorBody.data) !== null && _errorBody$data !== void 0 && (_errorBody$data$actio = _errorBody$data.actions) !== null && _errorBody$data$actio !== void 0 && _errorBody$data$actio.execute) {
73
+ return parseExecuteIntegrationActionResponse(errorBody, error.status);
74
+ }
75
+ throw toHttpError(undefined, error.status);
76
+ }
77
+ throw toHttpError(error);
78
+ }
79
+ return parseExecuteIntegrationActionResponse(body, 200);
80
+ }, [aggUrl, product]);
81
+ return useMemo(() => ({
82
+ executeAction
83
+ }), [executeAction]);
84
+ };
@@ -0,0 +1,21 @@
1
+ /** Thrown by `executeAction` when an action can't be run or fails. */
2
+ export class IntegrationActionError extends Error {
3
+ /** Set when `kind` is `consent-required`: where the user can connect their 3P account. */
4
+
5
+ /** HTTP status, or the status IS reported for the action. */
6
+
7
+ /** The underlying error, when the request itself failed. */
8
+
9
+ constructor(kind, message, options = {}) {
10
+ super(message);
11
+ this.name = 'IntegrationActionError';
12
+ this.kind = kind;
13
+ this.authUrl = options.authUrl;
14
+ this.errorType = options.errorType;
15
+ this.retryable = options.retryable;
16
+ this.statusCode = options.statusCode;
17
+ this.graphQLErrors = options.graphQLErrors;
18
+ this.operationErrors = options.operationErrors;
19
+ this.originalError = options.originalError;
20
+ }
21
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Mock AGG `actions { execute }` responses, based on what the ASIMO-5527 spike saw
3
+ * on staging.
4
+ */
5
+
6
+ export const mockExecuteIntegrationActionSuccessResponse = {
7
+ data: {
8
+ actions: {
9
+ execute: {
10
+ status: 200,
11
+ outputs: {
12
+ entities: {
13
+ comments: [{
14
+ id: 'comment-1',
15
+ content: 'Looks good to me',
16
+ author: {
17
+ displayName: 'Jane Smith'
18
+ },
19
+ createdTime: '2026-10-27T01:00:00.000Z'
20
+ }]
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ };
27
+ export const mockAuthUrl = 'https://id.atlassian.com/outboundAuth/start?key=google-drive';
28
+
29
+ /** IS returns consent errors in `outputs.errors` with HTTP 200 and no GraphQL `errors`. */
30
+ export const mockExecuteIntegrationActionConsentRequiredResponse = {
31
+ data: {
32
+ actions: {
33
+ execute: {
34
+ status: 401,
35
+ outputs: {
36
+ errors: [{
37
+ message: 'User consent is required to access this integration',
38
+ statusCode: 401,
39
+ extensions: {
40
+ errorType: 'USER_CONSENT_REQUIRED',
41
+ errorCategory: 'AUTH',
42
+ retryable: false,
43
+ authUrl: mockAuthUrl
44
+ }
45
+ }]
46
+ }
47
+ }
48
+ }
49
+ }
50
+ };
51
+ export const mockExecuteIntegrationActionOperationFailureResponse = {
52
+ data: {
53
+ actions: {
54
+ execute: {
55
+ status: 404,
56
+ outputs: {
57
+ errors: [{
58
+ message: 'File not found',
59
+ statusCode: 404,
60
+ extensions: {
61
+ errorType: 'NOT_FOUND',
62
+ errorCategory: 'CLIENT',
63
+ retryable: false
64
+ }
65
+ }]
66
+ }
67
+ }
68
+ }
69
+ }
70
+ };
71
+ export const mockExecuteIntegrationActionGraphQLErrorResponse = {
72
+ data: {
73
+ actions: {
74
+ execute: null
75
+ }
76
+ },
77
+ errors: [{
78
+ message: 'Action not found for filter',
79
+ path: ['actions', 'execute'],
80
+ extensions: {
81
+ classification: 'DataFetchingException',
82
+ statusCode: 400
83
+ }
84
+ }]
85
+ };
@@ -0,0 +1,148 @@
1
+ import { GENERIC_GRAPHQL_ERROR_MESSAGE, GENERIC_OPERATION_ERROR_MESSAGE, USER_CONSENT_REQUIRED } from './constants';
2
+ import { IntegrationActionError } from './integration-action-error';
3
+ const isRecord = value => typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ const isSuccessStatus = status => status >= 200 && status < 300;
5
+ const getStringExtension = (extensions, key) => {
6
+ const value = extensions === null || extensions === void 0 ? void 0 : extensions[key];
7
+ return typeof value === 'string' && value !== '' ? value : undefined;
8
+ };
9
+
10
+ /**
11
+ * Returns `authUrl` from the extensions only if it's an absolute `https:` URL, so a malformed or
12
+ * unsafe value (e.g. `javascript:`) is never handed to the UI to open.
13
+ */
14
+ const getAuthUrl = extensions => {
15
+ const value = getStringExtension(extensions, 'authUrl');
16
+ if (!value) {
17
+ return undefined;
18
+ }
19
+ try {
20
+ return new URL(value).protocol === 'https:' ? value : undefined;
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ };
25
+
26
+ /** Normalises an entry of `outputs.errors`, which IS doesn't guarantee is well-formed. */
27
+ const toOperationErrorEntry = error => {
28
+ if (typeof error === 'string') {
29
+ return {
30
+ message: error || GENERIC_OPERATION_ERROR_MESSAGE
31
+ };
32
+ }
33
+ if (!isRecord(error)) {
34
+ return {
35
+ message: GENERIC_OPERATION_ERROR_MESSAGE
36
+ };
37
+ }
38
+ return {
39
+ message: typeof error.message === 'string' && error.message !== '' ? error.message : GENERIC_OPERATION_ERROR_MESSAGE,
40
+ // Values are read with runtime checks below, so the cast doesn't trust their types.
41
+ ...(isRecord(error.extensions) ? {
42
+ extensions: error.extensions
43
+ } : {}),
44
+ ...(typeof error.statusCode === 'number' ? {
45
+ statusCode: error.statusCode
46
+ } : {})
47
+ };
48
+ };
49
+
50
+ /** Normalises an entry of the GraphQL `errors` array, which may be malformed. */
51
+ const toGraphQLErrorEntry = error => {
52
+ if (!isRecord(error)) {
53
+ return {
54
+ message: typeof error === 'string' && error ? error : GENERIC_GRAPHQL_ERROR_MESSAGE
55
+ };
56
+ }
57
+ return {
58
+ message: typeof error.message === 'string' && error.message !== '' ? error.message : GENERIC_GRAPHQL_ERROR_MESSAGE,
59
+ ...(isRecord(error.extensions) ? {
60
+ extensions: error.extensions
61
+ } : {}),
62
+ ...(Array.isArray(error.path) ? {
63
+ path: error.path
64
+ } : {})
65
+ };
66
+ };
67
+
68
+ /** A non-empty `outputs.errors` always means the action failed, even if the entries are malformed. */
69
+ const getOperationErrors = outputs => {
70
+ const errors = isRecord(outputs) ? outputs.errors : undefined;
71
+ if (!Array.isArray(errors) || errors.length === 0) {
72
+ return undefined;
73
+ }
74
+ return errors.map(toOperationErrorEntry);
75
+ };
76
+ const toOperationError = (operationErrors, status) => {
77
+ var _error$extensions, _error$statusCode;
78
+ const consentError = operationErrors.find(error => getStringExtension(error.extensions, 'errorType') === USER_CONSENT_REQUIRED || !!getAuthUrl(error.extensions));
79
+ const error = consentError !== null && consentError !== void 0 ? consentError : operationErrors[0];
80
+ const authUrl = getAuthUrl(consentError === null || consentError === void 0 ? void 0 : consentError.extensions);
81
+ const retryable = (_error$extensions = error.extensions) === null || _error$extensions === void 0 ? void 0 : _error$extensions.retryable;
82
+ const options = {
83
+ authUrl,
84
+ errorType: getStringExtension(error.extensions, 'errorType'),
85
+ retryable: typeof retryable === 'boolean' ? retryable : undefined,
86
+ statusCode: (_error$statusCode = error.statusCode) !== null && _error$statusCode !== void 0 ? _error$statusCode : status,
87
+ operationErrors
88
+ };
89
+ if (consentError && authUrl) {
90
+ return new IntegrationActionError('consent-required', error.message, options);
91
+ }
92
+ return new IntegrationActionError('operation', error.message, options);
93
+ };
94
+ const toGraphQLError = (graphQLErrors, statusCode) => {
95
+ const authUrl = graphQLErrors.map(error => getAuthUrl(error.extensions)).find(url => !!url);
96
+ const errorType = graphQLErrors.map(error => {
97
+ var _getStringExtension;
98
+ return (_getStringExtension = getStringExtension(error.extensions, 'errorType')) !== null && _getStringExtension !== void 0 ? _getStringExtension : getStringExtension(error.extensions, 'classification');
99
+ }).find(type => !!type);
100
+ const message = graphQLErrors.map(error => error.message).join('; ');
101
+ const options = {
102
+ authUrl,
103
+ errorType,
104
+ statusCode,
105
+ graphQLErrors
106
+ };
107
+ if (authUrl) {
108
+ return new IntegrationActionError('consent-required', message, options);
109
+ }
110
+ return new IntegrationActionError('graphql', message, options);
111
+ };
112
+
113
+ /**
114
+ * Turns an AGG response body into the action response, or throws an `IntegrationActionError`.
115
+ *
116
+ * Errors are checked in this order:
117
+ * 1. IS operation errors in `outputs.errors` (consent required, or the action failed).
118
+ * 2. GraphQL `errors`, even with a partial `execute` result. The mutation only selects
119
+ * `actions.execute`, so any GraphQL error relates to the action.
120
+ * 3. A missing `execute` result.
121
+ * 4. A non-2xx `status` from IS.
122
+ */
123
+ export const parseExecuteIntegrationActionResponse = (body, httpStatus) => {
124
+ var _body$data, _body$data$actions;
125
+ const execute = body === null || body === void 0 ? void 0 : (_body$data = body.data) === null || _body$data === void 0 ? void 0 : (_body$data$actions = _body$data.actions) === null || _body$data$actions === void 0 ? void 0 : _body$data$actions.execute;
126
+ const graphQLErrors = Array.isArray(body === null || body === void 0 ? void 0 : body.errors) && body.errors.length ? body.errors.map(toGraphQLErrorEntry) : undefined;
127
+ const operationErrors = getOperationErrors(execute === null || execute === void 0 ? void 0 : execute.outputs);
128
+ if (operationErrors !== null && operationErrors !== void 0 && operationErrors.length) {
129
+ throw toOperationError(operationErrors, execute === null || execute === void 0 ? void 0 : execute.status);
130
+ }
131
+ if (graphQLErrors) {
132
+ throw toGraphQLError(graphQLErrors, httpStatus);
133
+ }
134
+ if (!execute) {
135
+ throw new IntegrationActionError('graphql', 'No action result returned', {
136
+ statusCode: httpStatus
137
+ });
138
+ }
139
+ if (typeof execute.status === 'number' && !isSuccessStatus(execute.status)) {
140
+ throw new IntegrationActionError('operation', `Action failed with status ${execute.status}`, {
141
+ statusCode: execute.status
142
+ });
143
+ }
144
+ return {
145
+ status: execute.status,
146
+ outputs: execute.outputs
147
+ };
148
+ };
@@ -0,0 +1,35 @@
1
+ import { ATTRIBUTION_SERVICE } from './constants';
2
+
3
+ /**
4
+ * Smart Links product → GTM product name, from the AGG Metrics Attribution list.
5
+ * Products without a clear match are left out, so `product` is omitted from the header.
6
+ */
7
+ var GTM_PRODUCT = {
8
+ ATLAS: 'Atlas',
9
+ BITBUCKET: 'Bitbucket Cloud',
10
+ CONFLUENCE: 'Confluence Cloud',
11
+ CSM: 'Customer Service Management',
12
+ JPD: 'Jira Product Discovery',
13
+ JSM: 'Jira Service Management',
14
+ JSW: 'Jira',
15
+ JWM: 'Jira',
16
+ TRELLO: 'Trello'
17
+ };
18
+ /**
19
+ * The `atl-attribution` header AGG asks frontend clients to send for metrics attribution:
20
+ * JSON with `tenantId`, `product` (when known) and `service`.
21
+ *
22
+ * See https://hello.atlassian.net/wiki/spaces/aggp/pages/5954141702
23
+ */
24
+ export var buildRequestHeaders = function buildRequestHeaders(_ref) {
25
+ var cloudId = _ref.cloudId,
26
+ product = _ref.product;
27
+ var attribution = {
28
+ tenantId: cloudId || null,
29
+ product: product ? GTM_PRODUCT[product] : undefined,
30
+ service: ATTRIBUTION_SERVICE
31
+ };
32
+ return {
33
+ 'atl-attribution': JSON.stringify(attribution)
34
+ };
35
+ };
@@ -0,0 +1,12 @@
1
+ export var EXECUTE_INTEGRATION_ACTION_OPERATION_NAME = 'LinkClientExtension_ExecuteIntegrationAction';
2
+ export var EXECUTE_INTEGRATION_ACTION_MUTATION = "mutation ".concat(EXECUTE_INTEGRATION_ACTION_OPERATION_NAME, "(\n\t$actionTypeKey: String\n\t$filter: ActionsExecuteActionFilter!\n\t$actionInput: ActionsExecuteActionInput!\n\t$workspace: String\n) {\n\tactions {\n\t\texecute(\n\t\t\tactionTypeKey: $actionTypeKey\n\t\t\tfilter: $filter\n\t\t\tactionInput: $actionInput\n\t\t\tworkspace: $workspace\n\t\t) {\n\t\t\tstatus\n\t\t\toutputs\n\t\t}\n\t}\n}");
3
+ export var USER_CONSENT_REQUIRED = 'USER_CONSENT_REQUIRED';
4
+
5
+ /** `service` value for the AGG `atl-attribution` header. */
6
+ export var ATTRIBUTION_SERVICE = 'link-client-extension';
7
+
8
+ /** Fallback message when IS reports an error without one. */
9
+ export var GENERIC_OPERATION_ERROR_MESSAGE = 'Action failed';
10
+
11
+ /** Fallback message when AGG returns a GraphQL error without one. */
12
+ export var GENERIC_GRAPHQL_ERROR_MESSAGE = 'GraphQL error';
@@ -0,0 +1,28 @@
1
+ import { getBaseUrl } from '@atlaskit/linking-common/get-base-url';
2
+ import { EXECUTE_INTEGRATION_ACTION_OPERATION_NAME } from './constants';
3
+ var EDGE_PROXY_AGG_PATH = '/gateway/api/graphql';
4
+
5
+ /**
6
+ * AGG endpoint for the CardClient's environment.
7
+ * - No envKey (running inside a product): same-origin `/gateway/api/graphql`. On the server there
8
+ * is no `window`, so this is relative and requests fail with an `http` error instead of crashing.
9
+ * - `custom`: `baseUrlOverride` is a full object resolver URL (e.g. a direct service URL for SSR),
10
+ * so we can't build an AGG URL from it. Use the relative edge proxy path instead, the same
11
+ * fallback `getResolverUrl` uses.
12
+ * - Other envKeys (e.g. `stg` in examples): the Stargate base URL for that environment + `/graphql`,
13
+ * e.g. `https://pug.jira-dev.com/gateway/api/graphql`.
14
+ * The operation name is added as a query param so requests are easy to identify.
15
+ */
16
+ export var getAggUrl = function getAggUrl(envKey) {
17
+ var graphqlUrl;
18
+ if (envKey === 'custom') {
19
+ graphqlUrl = EDGE_PROXY_AGG_PATH;
20
+ } else if (envKey) {
21
+ var stargateBaseUrl = getBaseUrl(envKey);
22
+ graphqlUrl = "".concat(stargateBaseUrl.replace(/\/$/, ''), "/graphql");
23
+ } else {
24
+ var origin = getBaseUrl();
25
+ graphqlUrl = "".concat(origin).concat(EDGE_PROXY_AGG_PATH);
26
+ }
27
+ return "".concat(graphqlUrl, "?operationName=").concat(EXECUTE_INTEGRATION_ACTION_OPERATION_NAME);
28
+ };
@@ -0,0 +1,136 @@
1
+ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
+ import _typeof from "@babel/runtime/helpers/typeof";
3
+ import _regeneratorRuntime from "@babel/runtime/regenerator";
4
+ import { useCallback, useMemo } from 'react';
5
+ import { request } from '@atlaskit/linking-common/api';
6
+ import { buildRequestHeaders } from './build-request-headers';
7
+ import { EXECUTE_INTEGRATION_ACTION_MUTATION, EXECUTE_INTEGRATION_ACTION_OPERATION_NAME } from './constants';
8
+ import { getAggUrl } from './get-agg-url';
9
+ import { IntegrationActionError } from './integration-action-error';
10
+ import { parseExecuteIntegrationActionResponse } from './parse-execute-integration-action-response';
11
+ // Check the shape rather than `instanceof Response`; `instanceof` checks on errors have caused HOTs before.
12
+ var isResponseLike = function isResponseLike(value) {
13
+ return _typeof(value) === 'object' && value !== null && typeof value.json === 'function';
14
+ };
15
+ var readErrorBody = /*#__PURE__*/function () {
16
+ var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(response) {
17
+ var _t;
18
+ return _regeneratorRuntime.wrap(function (_context) {
19
+ while (1) switch (_context.prev = _context.next) {
20
+ case 0:
21
+ _context.prev = 0;
22
+ _context.next = 1;
23
+ return response.json();
24
+ case 1:
25
+ return _context.abrupt("return", _context.sent);
26
+ case 2:
27
+ _context.prev = 2;
28
+ _t = _context["catch"](0);
29
+ return _context.abrupt("return", undefined);
30
+ case 3:
31
+ case "end":
32
+ return _context.stop();
33
+ }
34
+ }, _callee, null, [[0, 2]]);
35
+ }));
36
+ return function readErrorBody(_x) {
37
+ return _ref.apply(this, arguments);
38
+ };
39
+ }();
40
+ var toHttpError = function toHttpError(error, statusCode) {
41
+ return new IntegrationActionError('http', error instanceof Error && error.message ? error.message : "Request failed".concat(statusCode ? " with status ".concat(statusCode) : ''), {
42
+ statusCode: statusCode,
43
+ originalError: error
44
+ });
45
+ };
46
+ /**
47
+ * useIntegrationActionsClient
48
+ *
49
+ * Runs Integrations Service (IS) actions on 3P resources (e.g. listing or adding Google Docs
50
+ * comments) through the AGG `actions { execute }` mutation. Uses a plain fetch rather than Relay,
51
+ * so it works in any host that provides a CardClient.
52
+ *
53
+ * `executeAction` resolves with the action's `outputs`. It throws an `IntegrationActionError` when:
54
+ * - the user needs to connect their 3P account (`kind: 'consent-required'`, with `authUrl`)
55
+ * - IS reports the action failed (`kind: 'operation'`)
56
+ * - AGG returns GraphQL errors (`kind: 'graphql'`)
57
+ * - the request fails (`kind: 'http'`)
58
+ *
59
+ * Each request sends the AGG metrics attribution header (`atl-attribution`), using
60
+ * `actionInput.cloudId` as the tenant.
61
+ *
62
+ * Call `executeAction` from effects or event handlers, not during render (it doesn't run in SSR).
63
+ *
64
+ * @param cardClient used to pick the AGG endpoint for the environment
65
+ * @param options.product product hosting the Smart Link, for metrics attribution
66
+ */
67
+ export var useIntegrationActionsClient = function useIntegrationActionsClient(cardClient) {
68
+ var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
69
+ product = _ref2.product;
70
+ var aggUrl = useMemo(function () {
71
+ return getAggUrl(cardClient === null || cardClient === void 0 ? void 0 : cardClient.envKey);
72
+ }, [cardClient === null || cardClient === void 0 ? void 0 : cardClient.envKey]);
73
+ var executeAction = useCallback( /*#__PURE__*/function () {
74
+ var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(_ref3) {
75
+ var actionTypeKey, filter, actionInput, workspace, body, _errorBody$errors, _errorBody$data, errorBody, _t2;
76
+ return _regeneratorRuntime.wrap(function (_context2) {
77
+ while (1) switch (_context2.prev = _context2.next) {
78
+ case 0:
79
+ actionTypeKey = _ref3.actionTypeKey, filter = _ref3.filter, actionInput = _ref3.actionInput, workspace = _ref3.workspace;
80
+ _context2.prev = 1;
81
+ _context2.next = 2;
82
+ return request('post', aggUrl, {
83
+ operationName: EXECUTE_INTEGRATION_ACTION_OPERATION_NAME,
84
+ query: EXECUTE_INTEGRATION_ACTION_MUTATION,
85
+ variables: {
86
+ actionTypeKey: actionTypeKey,
87
+ filter: filter,
88
+ actionInput: actionInput,
89
+ workspace: workspace
90
+ }
91
+ }, buildRequestHeaders({
92
+ cloudId: actionInput === null || actionInput === void 0 ? void 0 : actionInput.cloudId,
93
+ product: product
94
+ }), [200]);
95
+ case 2:
96
+ body = _context2.sent;
97
+ _context2.next = 7;
98
+ break;
99
+ case 3:
100
+ _context2.prev = 3;
101
+ _t2 = _context2["catch"](1);
102
+ if (!isResponseLike(_t2)) {
103
+ _context2.next = 6;
104
+ break;
105
+ }
106
+ _context2.next = 4;
107
+ return readErrorBody(_t2);
108
+ case 4:
109
+ errorBody = _context2.sent;
110
+ if (!(errorBody !== null && errorBody !== void 0 && (_errorBody$errors = errorBody.errors) !== null && _errorBody$errors !== void 0 && _errorBody$errors.length || errorBody !== null && errorBody !== void 0 && (_errorBody$data = errorBody.data) !== null && _errorBody$data !== void 0 && (_errorBody$data = _errorBody$data.actions) !== null && _errorBody$data !== void 0 && _errorBody$data.execute)) {
111
+ _context2.next = 5;
112
+ break;
113
+ }
114
+ return _context2.abrupt("return", parseExecuteIntegrationActionResponse(errorBody, _t2.status));
115
+ case 5:
116
+ throw toHttpError(undefined, _t2.status);
117
+ case 6:
118
+ throw toHttpError(_t2);
119
+ case 7:
120
+ return _context2.abrupt("return", parseExecuteIntegrationActionResponse(body, 200));
121
+ case 8:
122
+ case "end":
123
+ return _context2.stop();
124
+ }
125
+ }, _callee2, null, [[1, 3]]);
126
+ }));
127
+ return function (_x2) {
128
+ return _ref4.apply(this, arguments);
129
+ };
130
+ }(), [aggUrl, product]);
131
+ return useMemo(function () {
132
+ return {
133
+ executeAction: executeAction
134
+ };
135
+ }, [executeAction]);
136
+ };
@@ -0,0 +1,35 @@
1
+ import _createClass from "@babel/runtime/helpers/createClass";
2
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
3
+ import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
4
+ import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
5
+ import _inherits from "@babel/runtime/helpers/inherits";
6
+ import _wrapNativeSuper from "@babel/runtime/helpers/wrapNativeSuper";
7
+ function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
8
+ function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
9
+ /** Thrown by `executeAction` when an action can't be run or fails. */
10
+ export var IntegrationActionError = /*#__PURE__*/function (_Error) {
11
+ /** Set when `kind` is `consent-required`: where the user can connect their 3P account. */
12
+
13
+ /** HTTP status, or the status IS reported for the action. */
14
+
15
+ /** The underlying error, when the request itself failed. */
16
+
17
+ function IntegrationActionError(kind, message) {
18
+ var _this;
19
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
20
+ _classCallCheck(this, IntegrationActionError);
21
+ _this = _callSuper(this, IntegrationActionError, [message]);
22
+ _this.name = 'IntegrationActionError';
23
+ _this.kind = kind;
24
+ _this.authUrl = options.authUrl;
25
+ _this.errorType = options.errorType;
26
+ _this.retryable = options.retryable;
27
+ _this.statusCode = options.statusCode;
28
+ _this.graphQLErrors = options.graphQLErrors;
29
+ _this.operationErrors = options.operationErrors;
30
+ _this.originalError = options.originalError;
31
+ return _this;
32
+ }
33
+ _inherits(IntegrationActionError, _Error);
34
+ return _createClass(IntegrationActionError);
35
+ }( /*#__PURE__*/_wrapNativeSuper(Error));