@shopify/graphql-client
Version:
Shopify GraphQL Client - A lightweight generic GraphQL JS client to interact with Shopify GraphQL APIs
340 lines (336 loc) • 13.3 kB
JavaScript
var httpFetch = require('./http-fetch.js');
var constants = require('./constants.js');
var utilities = require('./utilities.js');
function createGraphQLClient({ headers, url, customFetchApi = fetch, retries = 0, logger, }) {
utilities.validateRetries({ client: constants.CLIENT, retries });
const config = {
headers,
url,
retries,
};
const clientLogger = generateClientLogger(logger);
const httpFetch$1 = httpFetch.generateHttpFetch({
customFetchApi,
clientLogger,
defaultRetryWaitTime: constants.RETRY_WAIT_TIME,
});
const fetchFn = generateFetch(httpFetch$1, config);
const request = generateRequest(fetchFn);
const requestStream = generateRequestStream(fetchFn);
return {
config,
fetch: fetchFn,
request,
requestStream,
};
}
function generateClientLogger(logger) {
return (logContent) => {
if (logger) {
logger(logContent);
}
};
}
async function processJSONResponse(response) {
const { errors, data, extensions } = await response.json();
return {
...utilities.getKeyValueIfValid('data', data),
...utilities.getKeyValueIfValid('extensions', extensions),
headers: response.headers,
...(errors || !data
? {
errors: {
networkStatusCode: response.status,
message: utilities.formatErrorMessage(errors ? constants.GQL_API_ERROR : constants.NO_DATA_OR_ERRORS_ERROR),
...utilities.getKeyValueIfValid('graphQLErrors', errors),
response,
},
}
: {}),
};
}
function generateFetch(httpFetch, { url, headers, retries }) {
return async (operation, options = {}) => {
const { variables, headers: overrideHeaders, url: overrideUrl, retries: overrideRetries, keepalive, signal, } = options;
const body = JSON.stringify({
query: operation,
variables,
});
utilities.validateRetries({ client: constants.CLIENT, retries: overrideRetries });
const flatHeaders = Object.entries({
...headers,
...overrideHeaders,
}).reduce((headers, [key, value]) => {
headers[key] = Array.isArray(value) ? value.join(', ') : value.toString();
return headers;
}, {});
if (!flatHeaders[constants.SDK_VARIANT_HEADER] && !flatHeaders[constants.SDK_VERSION_HEADER]) {
flatHeaders[constants.SDK_VARIANT_HEADER] = constants.DEFAULT_SDK_VARIANT;
flatHeaders[constants.SDK_VERSION_HEADER] = constants.DEFAULT_CLIENT_VERSION;
}
const fetchParams = [
overrideUrl ?? url,
{
method: 'POST',
headers: flatHeaders,
body,
signal,
keepalive,
},
];
return httpFetch(fetchParams, 1, overrideRetries ?? retries);
};
}
function generateRequest(fetchFn) {
return async (...props) => {
if (constants.DEFER_OPERATION_REGEX.test(props[0])) {
throw new Error(utilities.formatErrorMessage('This operation will result in a streamable response - use requestStream() instead.'));
}
try {
const response = await fetchFn(...props);
const { status, statusText } = response;
const contentType = response.headers.get('content-type') || '';
if (!response.ok) {
return {
errors: {
networkStatusCode: status,
message: utilities.formatErrorMessage(statusText),
response,
},
};
}
if (!contentType.includes(constants.CONTENT_TYPES.json)) {
return {
errors: {
networkStatusCode: status,
message: utilities.formatErrorMessage(`${constants.UNEXPECTED_CONTENT_TYPE_ERROR} ${contentType}`),
response,
},
};
}
return processJSONResponse(response);
}
catch (error) {
return {
errors: {
message: utilities.getErrorMessage(error),
},
};
}
};
}
async function* getStreamBodyIterator(response) {
const decoder = new TextDecoder();
// Response body is an async iterator
if (response.body[Symbol.asyncIterator]) {
for await (const chunk of response.body) {
yield decoder.decode(chunk);
}
}
else {
const reader = response.body.getReader();
let readResult;
try {
while (!(readResult = await reader.read()).done) {
yield decoder.decode(readResult.value);
}
}
finally {
reader.cancel();
}
}
}
function readStreamChunk(streamBodyIterator, boundary) {
return {
async *[Symbol.asyncIterator]() {
try {
let buffer = '';
for await (const textChunk of streamBodyIterator) {
buffer += textChunk;
if (buffer.indexOf(boundary) > -1) {
const lastBoundaryIndex = buffer.lastIndexOf(boundary);
const fullResponses = buffer.slice(0, lastBoundaryIndex);
const chunkBodies = fullResponses
.split(boundary)
.filter((chunk) => chunk.trim().length > 0)
.map((chunk) => {
const body = chunk
.slice(chunk.indexOf(constants.HEADER_SEPARATOR) + constants.HEADER_SEPARATOR.length)
.trim();
return body;
});
if (chunkBodies.length > 0) {
yield chunkBodies;
}
buffer = buffer.slice(lastBoundaryIndex + boundary.length);
if (buffer.trim() === `--`) {
buffer = '';
}
}
}
}
catch (error) {
throw new Error(`Error occured while processing stream payload - ${utilities.getErrorMessage(error)}`);
}
},
};
}
function createJsonResponseAsyncIterator(response) {
return {
async *[Symbol.asyncIterator]() {
const processedResponse = await processJSONResponse(response);
yield {
...processedResponse,
hasNext: false,
};
},
};
}
function getResponseDataFromChunkBodies(chunkBodies) {
return chunkBodies
.map((value) => {
try {
return JSON.parse(value);
}
catch (error) {
throw new Error(`Error in parsing multipart response - ${utilities.getErrorMessage(error)}`);
}
})
.map((payload) => {
const { data, incremental, hasNext, extensions, errors } = payload;
// initial data chunk
if (!incremental) {
return {
data: data || {},
...utilities.getKeyValueIfValid('errors', errors),
...utilities.getKeyValueIfValid('extensions', extensions),
hasNext,
};
}
// subsequent data chunks
const incrementalArray = incremental.map(({ data, path, errors }) => {
return {
data: data && path ? utilities.buildDataObjectByPath(path, data) : {},
...utilities.getKeyValueIfValid('errors', errors),
};
});
return {
data: incrementalArray.length === 1
? incrementalArray[0].data
: utilities.buildCombinedDataObject([
...incrementalArray.map(({ data }) => data),
]),
...utilities.getKeyValueIfValid('errors', utilities.combineErrors(incrementalArray)),
hasNext,
};
});
}
function validateResponseData(responseErrors, combinedData) {
if (responseErrors.length > 0) {
throw new Error(constants.GQL_API_ERROR, {
cause: {
graphQLErrors: responseErrors,
},
});
}
if (Object.keys(combinedData).length === 0) {
throw new Error(constants.NO_DATA_OR_ERRORS_ERROR);
}
}
function createMultipartResponseAsyncInterator(response, responseContentType) {
const boundaryHeader = (responseContentType ?? '').match(constants.BOUNDARY_HEADER_REGEX);
const boundary = `--${boundaryHeader ? boundaryHeader[1] : '-'}`;
if (!response.body?.getReader &&
!response.body?.[Symbol.asyncIterator]) {
throw new Error('API multipart response did not return an iterable body', {
cause: response,
});
}
const streamBodyIterator = getStreamBodyIterator(response);
let combinedData = {};
let responseExtensions;
return {
async *[Symbol.asyncIterator]() {
try {
let streamHasNext = true;
for await (const chunkBodies of readStreamChunk(streamBodyIterator, boundary)) {
const responseData = getResponseDataFromChunkBodies(chunkBodies);
responseExtensions =
responseData.find((datum) => datum.extensions)?.extensions ??
responseExtensions;
const responseErrors = utilities.combineErrors(responseData);
combinedData = utilities.buildCombinedDataObject([
combinedData,
...responseData.map(({ data }) => data),
]);
streamHasNext = responseData.slice(-1)[0].hasNext;
validateResponseData(responseErrors, combinedData);
yield {
...utilities.getKeyValueIfValid('data', combinedData),
...utilities.getKeyValueIfValid('extensions', responseExtensions),
hasNext: streamHasNext,
};
}
if (streamHasNext) {
throw new Error(`Response stream terminated unexpectedly`);
}
}
catch (error) {
const cause = utilities.getErrorCause(error);
yield {
...utilities.getKeyValueIfValid('data', combinedData),
...utilities.getKeyValueIfValid('extensions', responseExtensions),
errors: {
message: utilities.formatErrorMessage(utilities.getErrorMessage(error)),
networkStatusCode: response.status,
...utilities.getKeyValueIfValid('graphQLErrors', cause?.graphQLErrors),
response,
},
hasNext: false,
};
}
},
};
}
function generateRequestStream(fetchFn) {
return async (...props) => {
if (!constants.DEFER_OPERATION_REGEX.test(props[0])) {
throw new Error(utilities.formatErrorMessage('This operation does not result in a streamable response - use request() instead.'));
}
try {
const response = await fetchFn(...props);
const { statusText } = response;
if (!response.ok) {
throw new Error(statusText, { cause: response });
}
const responseContentType = response.headers.get('content-type') || '';
switch (true) {
case responseContentType.includes(constants.CONTENT_TYPES.json):
return createJsonResponseAsyncIterator(response);
case responseContentType.includes(constants.CONTENT_TYPES.multipart):
return createMultipartResponseAsyncInterator(response, responseContentType);
default:
throw new Error(`${constants.UNEXPECTED_CONTENT_TYPE_ERROR} ${responseContentType}`, { cause: response });
}
}
catch (error) {
return {
async *[Symbol.asyncIterator]() {
const response = utilities.getErrorCause(error);
yield {
errors: {
message: utilities.formatErrorMessage(utilities.getErrorMessage(error)),
...utilities.getKeyValueIfValid('networkStatusCode', response?.status),
...utilities.getKeyValueIfValid('response', response),
},
hasNext: false,
};
},
};
}
};
}
exports.createGraphQLClient = createGraphQLClient;
exports.generateClientLogger = generateClientLogger;
//# sourceMappingURL=graphql-client.js.map
;