@atlaskit/link-client-extension 7.3.1 → 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.
- package/CHANGELOG.md +20 -0
- package/compass.yml +7 -4
- package/dist/cjs/services/use-integration-actions-client/build-request-headers.js +40 -0
- package/dist/cjs/services/use-integration-actions-client/constants.js +18 -0
- package/dist/cjs/services/use-integration-actions-client/get-agg-url.js +34 -0
- package/dist/cjs/services/use-integration-actions-client/index.js +143 -0
- package/dist/cjs/services/use-integration-actions-client/integration-action-error.js +42 -0
- package/dist/cjs/services/use-integration-actions-client/mocks.js +91 -0
- package/dist/cjs/services/use-integration-actions-client/parse-execute-integration-action-response.js +168 -0
- package/dist/cjs/services/use-integration-actions-client/types.js +1 -0
- package/dist/es2019/services/use-integration-actions-client/build-request-headers.js +36 -0
- package/dist/es2019/services/use-integration-actions-client/constants.js +29 -0
- package/dist/es2019/services/use-integration-actions-client/get-agg-url.js +28 -0
- package/dist/es2019/services/use-integration-actions-client/index.js +84 -0
- package/dist/es2019/services/use-integration-actions-client/integration-action-error.js +21 -0
- package/dist/es2019/services/use-integration-actions-client/mocks.js +85 -0
- package/dist/es2019/services/use-integration-actions-client/parse-execute-integration-action-response.js +148 -0
- package/dist/es2019/services/use-integration-actions-client/types.js +0 -0
- package/dist/esm/services/use-integration-actions-client/build-request-headers.js +35 -0
- package/dist/esm/services/use-integration-actions-client/constants.js +12 -0
- package/dist/esm/services/use-integration-actions-client/get-agg-url.js +28 -0
- package/dist/esm/services/use-integration-actions-client/index.js +136 -0
- package/dist/esm/services/use-integration-actions-client/integration-action-error.js +35 -0
- package/dist/esm/services/use-integration-actions-client/mocks.js +85 -0
- package/dist/esm/services/use-integration-actions-client/parse-execute-integration-action-response.js +161 -0
- package/dist/esm/services/use-integration-actions-client/types.js +0 -0
- package/dist/types/services/use-data-source-client-extension/index.d.ts +1 -1
- package/dist/types/services/use-integration-actions-client/build-request-headers.d.ts +15 -0
- package/dist/types/services/use-integration-actions-client/constants.d.ts +9 -0
- package/dist/types/services/use-integration-actions-client/get-agg-url.d.ts +13 -0
- package/dist/types/services/use-integration-actions-client/index.d.ts +32 -0
- package/dist/types/services/use-integration-actions-client/integration-action-error.d.ts +26 -0
- package/dist/types/services/use-integration-actions-client/mocks.d.ts +11 -0
- package/dist/types/services/use-integration-actions-client/parse-execute-integration-action-response.d.ts +12 -0
- package/dist/types/services/use-integration-actions-client/types.d.ts +112 -0
- package/package.json +11 -8
- package/dist/cjs/entry-points/data-source-client-extension-mocks.js +0 -53
- package/dist/es2019/entry-points/data-source-client-extension-mocks.js +0 -25
- package/dist/esm/entry-points/data-source-client-extension-mocks.js +0 -25
- package/dist/types/entry-points/data-source-client-extension-mocks.d.ts +0 -24
- package/use-data-source-client-extension/mocks/package.json +0 -8
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const EXECUTE_INTEGRATION_ACTION_OPERATION_NAME = 'LinkClientExtension_ExecuteIntegrationAction';
|
|
2
|
+
export const EXECUTE_INTEGRATION_ACTION_MUTATION = `mutation ${EXECUTE_INTEGRATION_ACTION_OPERATION_NAME}(
|
|
3
|
+
$actionTypeKey: String
|
|
4
|
+
$filter: ActionsExecuteActionFilter!
|
|
5
|
+
$actionInput: ActionsExecuteActionInput!
|
|
6
|
+
$workspace: String
|
|
7
|
+
) {
|
|
8
|
+
actions {
|
|
9
|
+
execute(
|
|
10
|
+
actionTypeKey: $actionTypeKey
|
|
11
|
+
filter: $filter
|
|
12
|
+
actionInput: $actionInput
|
|
13
|
+
workspace: $workspace
|
|
14
|
+
) {
|
|
15
|
+
status
|
|
16
|
+
outputs
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}`;
|
|
20
|
+
export const USER_CONSENT_REQUIRED = 'USER_CONSENT_REQUIRED';
|
|
21
|
+
|
|
22
|
+
/** `service` value for the AGG `atl-attribution` header. */
|
|
23
|
+
export const ATTRIBUTION_SERVICE = 'link-client-extension';
|
|
24
|
+
|
|
25
|
+
/** Fallback message when IS reports an error without one. */
|
|
26
|
+
export const GENERIC_OPERATION_ERROR_MESSAGE = 'Action failed';
|
|
27
|
+
|
|
28
|
+
/** Fallback message when AGG returns a GraphQL error without one. */
|
|
29
|
+
export const 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
|
+
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
|
+
};
|
|
File without changes
|
|
@@ -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));
|