@shopify/cli-kit
Version:
A set of utilities, interfaces, and models that are common across all the platform features
174 lines • 7.55 kB
JavaScript
import { buildHeaders, httpsAgent } from '../../../private/node/api/headers.js';
import { debugLogRequestInfo, errorHandler } from '../../../private/node/api/graphql.js';
import { addPublicMetadata, runWithTimer } from '../metadata.js';
import { retryAwareRequest } from '../../../private/node/api.js';
import { requestIdsCollection } from '../../../private/node/request-ids.js';
import { nonRandomUUID } from '../crypto.js';
import { cacheRetrieveOrRepopulate, timeIntervalToMilliseconds, } from '../../../private/node/conf-store.js';
import { abortSignalFromRequestBehaviour, requestMode } from '../http.js';
import { CLI_KIT_VERSION } from '../../common/version.js';
import { sleep } from '../system.js';
import { outputContent, outputDebug } from '../output.js';
import { GraphQLClient, resolveRequestDocument, ClientError, } from 'graphql-request';
const MAX_RATE_LIMIT_RESTORE_DELAY_SECONDS = 0.3;
async function createGraphQLClient({ url, addedHeaders, token, }) {
const headers = {
...addedHeaders,
...buildHeaders(token),
};
const clientOptions = { agent: await httpsAgent(), headers };
return {
client: new GraphQLClient(url, clientOptions),
headers,
};
}
async function waitForRateLimitRestore(fullResponse) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cost = fullResponse.extensions?.cost;
const actualQueryCost = cost?.actualQueryCost;
const restoreRate = cost?.throttleStatus?.restoreRate;
if (actualQueryCost && typeof actualQueryCost === 'number' && restoreRate && typeof restoreRate === 'number') {
const secondsToRestoreRate = actualQueryCost / restoreRate;
outputDebug(outputContent `Sleeping for ${secondsToRestoreRate.toString()} seconds to restore the rate limit.`);
await sleep(Math.min(secondsToRestoreRate, MAX_RATE_LIMIT_RESTORE_DELAY_SECONDS));
}
}
async function runSingleRawGraphQLRequest(options) {
const { client, behaviour, queryAsString, variables, autoRateLimitRestore } = options;
let fullResponse;
// there is a errorPolicy option which returns rather than throwing on errors, but we _do_ ultimately want to
// throw.
try {
client.setAbortSignal(abortSignalFromRequestBehaviour(behaviour));
fullResponse = await client.rawRequest(queryAsString, variables);
await logLastRequestIdFromResponse(fullResponse);
if (autoRateLimitRestore) {
await waitForRateLimitRestore(fullResponse);
}
return fullResponse;
}
catch (error) {
if (error instanceof ClientError) {
// error.response does have a headers property like a normal response, but it's not typed as such.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await logLastRequestIdFromResponse(error.response);
}
throw error;
}
}
async function performGraphQLRequest(options) {
const { token, addedHeaders, queryAsString, variables, api, url, responseOptions, unauthorizedHandler, cacheOptions, autoRateLimitRestore, } = options;
const behaviour = requestMode(options.preferredBehaviour ?? 'default');
let { headers, client } = await createGraphQLClient({ url, addedHeaders, token });
debugLogRequestInfo(api, queryAsString, url, variables, headers);
const rawGraphQLRequest = async () => {
return runSingleRawGraphQLRequest({
client: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setAbortSignal: (signal) => {
client.requestConfig.signal = signal;
},
rawRequest: (query, variables) => client.rawRequest(query, variables),
},
behaviour,
queryAsString,
variables,
autoRateLimitRestore: autoRateLimitRestore ?? false,
});
};
const tokenRefreshHandler = unauthorizedHandler?.handler;
const tokenRefreshUnauthorizedHandlerFunction = tokenRefreshHandler
? async () => {
const refreshTokenResult = await tokenRefreshHandler();
if (refreshTokenResult.token) {
const { client: newClient, headers: newHeaders } = await createGraphQLClient({
url,
addedHeaders,
token: refreshTokenResult.token,
});
client = newClient;
headers = newHeaders;
return true;
}
else {
return false;
}
}
: undefined;
const request = () => retryAwareRequest({ request: rawGraphQLRequest, url, ...behaviour }, responseOptions?.handleErrors === false ? undefined : errorHandler(api));
const executeWithTimer = () => runWithTimer('cmd_all_timing_network_ms')(async () => {
let response;
try {
response = await request();
}
catch (error) {
if (error instanceof ClientError && error.response.status === 401 && tokenRefreshUnauthorizedHandlerFunction) {
if (await tokenRefreshUnauthorizedHandlerFunction()) {
response = await request();
}
else {
throw error;
}
}
else {
throw error;
}
}
if (responseOptions?.onResponse) {
responseOptions.onResponse(response);
}
return response.data;
});
// If there is no cache config for this query, just execute it and return the result.
if (cacheOptions === undefined) {
return executeWithTimer();
}
const { cacheTTL, cacheExtraKey, cacheStore } = cacheOptions;
// The cache key is a combination of the hashed query and variables, with an optional extra key provided by the user.
const queryHash = nonRandomUUID(queryAsString);
const variablesHash = nonRandomUUID(JSON.stringify(variables ?? {}));
const cacheKey = `q-${queryHash}-${variablesHash}-${CLI_KIT_VERSION}-${cacheExtraKey ?? ''}`;
const result = await cacheRetrieveOrRepopulate(cacheKey, async () => {
const result = await executeWithTimer();
return JSON.stringify(result);
}, timeIntervalToMilliseconds(cacheTTL), cacheStore);
return JSON.parse(result);
}
async function logLastRequestIdFromResponse(response) {
try {
const requestId = response.headers.get('x-request-id');
requestIdsCollection.addRequestId(requestId);
await addPublicMetadata(() => ({
cmd_all_last_graphql_request_id: requestId ?? undefined,
}));
// eslint-disable-next-line no-catch-all/no-catch-all
}
catch {
// no problem if unable to get request ID.
}
}
/**
* Executes a GraphQL query to an endpoint.
*
* @param options - GraphQL request options.
* @returns The response of the query of generic type <T>.
*/
export async function graphqlRequest(options) {
return performGraphQLRequest({
...options,
queryAsString: options.query,
});
}
/**
* Executes a GraphQL query to an endpoint. Uses typed documents.
*
* @param options - GraphQL request options.
* @returns The response of the query of generic type <TResult>.
*/
export async function graphqlRequestDoc(options) {
return performGraphQLRequest({
...options,
queryAsString: resolveRequestDocument(options.query).query,
});
}
//# sourceMappingURL=graphql.js.map