UNPKG

@shopify/cli-kit

Version:

A set of utilities, interfaces, and models that are common across all the platform features

174 lines • 7.55 kB
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