UNPKG

19.5 kBJavaScriptView Raw
1/*! shopify/graphql-client@0.10.4 -- Copyright (c) 2023-present, Shopify Inc. -- license (MIT): https://github.com/Shopify/shopify-app-js/blob/main/LICENSE.md */
2(function (global, factory) {
3 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4 typeof define === 'function' && define.amd ? define(['exports'], factory) :
5 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ShopifyGraphQLClient = {}));
6})(this, (function (exports) { 'use strict';
7
8 const CLIENT = 'GraphQL Client';
9 const MIN_RETRIES = 0;
10 const MAX_RETRIES = 3;
11 const GQL_API_ERROR = "An error occurred while fetching from the API. Review 'graphQLErrors' for details.";
12 const UNEXPECTED_CONTENT_TYPE_ERROR = 'Response returned unexpected Content-Type:';
13 const NO_DATA_OR_ERRORS_ERROR = 'An unknown error has occurred. The API did not return a data object or any errors in its response.';
14 const CONTENT_TYPES = {
15 json: 'application/json',
16 multipart: 'multipart/mixed',
17 };
18 const SDK_VARIANT_HEADER = 'X-SDK-Variant';
19 const SDK_VERSION_HEADER = 'X-SDK-Version';
20 const DEFAULT_SDK_VARIANT = 'shopify-graphql-client';
21 // This is value is replaced with package.json version during rollup build process
22 const DEFAULT_CLIENT_VERSION = '0.10.4';
23 const RETRY_WAIT_TIME = 1000;
24 const RETRIABLE_STATUS_CODES = [429, 503];
25 const DEFER_OPERATION_REGEX = /@(defer)\b/i;
26 const NEWLINE_SEPARATOR = '\r\n';
27 const BOUNDARY_HEADER_REGEX = /boundary="?([^=";]+)"?/i;
28 const HEADER_SEPARATOR = NEWLINE_SEPARATOR + NEWLINE_SEPARATOR;
29
30 function formatErrorMessage(message, client = CLIENT) {
31 return message.startsWith(`${client}`) ? message : `${client}: ${message}`;
32 }
33 function getErrorMessage(error) {
34 return error instanceof Error ? error.message : JSON.stringify(error);
35 }
36 function getErrorCause(error) {
37 return error instanceof Error && error.cause ? error.cause : undefined;
38 }
39 function combineErrors(dataArray) {
40 return dataArray.flatMap(({ errors }) => {
41 return errors ?? [];
42 });
43 }
44 function validateRetries({ client, retries, }) {
45 if (retries !== undefined &&
46 (typeof retries !== 'number' ||
47 retries < MIN_RETRIES ||
48 retries > MAX_RETRIES)) {
49 throw new Error(`${client}: The provided "retries" value (${retries}) is invalid - it cannot be less than ${MIN_RETRIES} or greater than ${MAX_RETRIES}`);
50 }
51 }
52 function getKeyValueIfValid(key, value) {
53 return value &&
54 (typeof value !== 'object' ||
55 Array.isArray(value) ||
56 (typeof value === 'object' && Object.keys(value).length > 0))
57 ? { [key]: value }
58 : {};
59 }
60 function buildDataObjectByPath(path, data) {
61 if (path.length === 0) {
62 return data;
63 }
64 const key = path.pop();
65 const newData = {
66 [key]: data,
67 };
68 if (path.length === 0) {
69 return newData;
70 }
71 return buildDataObjectByPath(path, newData);
72 }
73 function combineObjects(baseObject, newObject) {
74 return Object.keys(newObject || {}).reduce((acc, key) => {
75 if ((typeof newObject[key] === 'object' || Array.isArray(newObject[key])) &&
76 baseObject[key]) {
77 acc[key] = combineObjects(baseObject[key], newObject[key]);
78 return acc;
79 }
80 acc[key] = newObject[key];
81 return acc;
82 }, Array.isArray(baseObject) ? [...baseObject] : { ...baseObject });
83 }
84 function buildCombinedDataObject([initialDatum, ...remainingData]) {
85 return remainingData.reduce(combineObjects, { ...initialDatum });
86 }
87
88 function generateHttpFetch({ clientLogger, customFetchApi = fetch, client = CLIENT, defaultRetryWaitTime = RETRY_WAIT_TIME, retriableCodes = RETRIABLE_STATUS_CODES, }) {
89 const httpFetch = async (requestParams, count, maxRetries) => {
90 const nextCount = count + 1;
91 const maxTries = maxRetries + 1;
92 let response;
93 try {
94 response = await customFetchApi(...requestParams);
95 clientLogger({
96 type: 'HTTP-Response',
97 content: {
98 requestParams,
99 response,
100 },
101 });
102 if (!response.ok &&
103 retriableCodes.includes(response.status) &&
104 nextCount <= maxTries) {
105 throw new Error();
106 }
107 return response;
108 }
109 catch (error) {
110 if (nextCount <= maxTries) {
111 const retryAfter = response?.headers.get('Retry-After');
112 await sleep(retryAfter ? parseInt(retryAfter, 10) : defaultRetryWaitTime);
113 clientLogger({
114 type: 'HTTP-Retry',
115 content: {
116 requestParams,
117 lastResponse: response,
118 retryAttempt: count,
119 maxRetries,
120 },
121 });
122 return httpFetch(requestParams, nextCount, maxRetries);
123 }
124 throw new Error(formatErrorMessage(`${maxRetries > 0
125 ? `Attempted maximum number of ${maxRetries} network retries. Last message - `
126 : ''}${getErrorMessage(error)}`, client));
127 }
128 };
129 return httpFetch;
130 }
131 async function sleep(waitTime) {
132 return new Promise((resolve) => setTimeout(resolve, waitTime));
133 }
134
135 function createGraphQLClient({ headers, url, customFetchApi = fetch, retries = 0, logger, }) {
136 validateRetries({ client: CLIENT, retries });
137 const config = {
138 headers,
139 url,
140 retries,
141 };
142 const clientLogger = generateClientLogger(logger);
143 const httpFetch = generateHttpFetch({
144 customFetchApi,
145 clientLogger,
146 defaultRetryWaitTime: RETRY_WAIT_TIME,
147 });
148 const fetch = generateFetch(httpFetch, config);
149 const request = generateRequest(fetch);
150 const requestStream = generateRequestStream(fetch);
151 return {
152 config,
153 fetch,
154 request,
155 requestStream,
156 };
157 }
158 function generateClientLogger(logger) {
159 return (logContent) => {
160 if (logger) {
161 logger(logContent);
162 }
163 };
164 }
165 async function processJSONResponse(response) {
166 const { errors, data, extensions } = await response.json();
167 return {
168 ...getKeyValueIfValid('data', data),
169 ...getKeyValueIfValid('extensions', extensions),
170 ...(errors || !data
171 ? {
172 errors: {
173 networkStatusCode: response.status,
174 message: formatErrorMessage(errors ? GQL_API_ERROR : NO_DATA_OR_ERRORS_ERROR),
175 ...getKeyValueIfValid('graphQLErrors', errors),
176 response,
177 },
178 }
179 : {}),
180 };
181 }
182 function generateFetch(httpFetch, { url, headers, retries }) {
183 return async (operation, options = {}) => {
184 const { variables, headers: overrideHeaders, url: overrideUrl, retries: overrideRetries, } = options;
185 const body = JSON.stringify({
186 query: operation,
187 variables,
188 });
189 validateRetries({ client: CLIENT, retries: overrideRetries });
190 const flatHeaders = Object.entries({
191 ...headers,
192 ...overrideHeaders,
193 }).reduce((headers, [key, value]) => {
194 headers[key] = Array.isArray(value) ? value.join(', ') : value.toString();
195 return headers;
196 }, {});
197 if (!flatHeaders[SDK_VARIANT_HEADER] && !flatHeaders[SDK_VERSION_HEADER]) {
198 flatHeaders[SDK_VARIANT_HEADER] = DEFAULT_SDK_VARIANT;
199 flatHeaders[SDK_VERSION_HEADER] = DEFAULT_CLIENT_VERSION;
200 }
201 const fetchParams = [
202 overrideUrl ?? url,
203 {
204 method: 'POST',
205 headers: flatHeaders,
206 body,
207 },
208 ];
209 return httpFetch(fetchParams, 1, overrideRetries ?? retries);
210 };
211 }
212 function generateRequest(fetch) {
213 return async (...props) => {
214 if (DEFER_OPERATION_REGEX.test(props[0])) {
215 throw new Error(formatErrorMessage('This operation will result in a streamable response - use requestStream() instead.'));
216 }
217 try {
218 const response = await fetch(...props);
219 const { status, statusText } = response;
220 const contentType = response.headers.get('content-type') || '';
221 if (!response.ok) {
222 return {
223 errors: {
224 networkStatusCode: status,
225 message: formatErrorMessage(statusText),
226 response,
227 },
228 };
229 }
230 if (!contentType.includes(CONTENT_TYPES.json)) {
231 return {
232 errors: {
233 networkStatusCode: status,
234 message: formatErrorMessage(`${UNEXPECTED_CONTENT_TYPE_ERROR} ${contentType}`),
235 response,
236 },
237 };
238 }
239 return processJSONResponse(response);
240 }
241 catch (error) {
242 return {
243 errors: {
244 message: getErrorMessage(error),
245 },
246 };
247 }
248 };
249 }
250 async function* getStreamBodyIterator(response) {
251 const decoder = new TextDecoder();
252 // Response body is an async iterator
253 if (response.body[Symbol.asyncIterator]) {
254 for await (const chunk of response.body) {
255 yield decoder.decode(chunk);
256 }
257 }
258 else {
259 const reader = response.body.getReader();
260 let readResult;
261 try {
262 while (!(readResult = await reader.read()).done) {
263 yield decoder.decode(readResult.value);
264 }
265 }
266 finally {
267 reader.cancel();
268 }
269 }
270 }
271 function readStreamChunk(streamBodyIterator, boundary) {
272 return {
273 async *[Symbol.asyncIterator]() {
274 try {
275 let buffer = '';
276 for await (const textChunk of streamBodyIterator) {
277 buffer += textChunk;
278 if (buffer.indexOf(boundary) > -1) {
279 const lastBoundaryIndex = buffer.lastIndexOf(boundary);
280 const fullResponses = buffer.slice(0, lastBoundaryIndex);
281 const chunkBodies = fullResponses
282 .split(boundary)
283 .filter((chunk) => chunk.trim().length > 0)
284 .map((chunk) => {
285 const body = chunk
286 .slice(chunk.indexOf(HEADER_SEPARATOR) + HEADER_SEPARATOR.length)
287 .trim();
288 return body;
289 });
290 if (chunkBodies.length > 0) {
291 yield chunkBodies;
292 }
293 buffer = buffer.slice(lastBoundaryIndex + boundary.length);
294 if (buffer.trim() === `--`) {
295 buffer = '';
296 }
297 }
298 }
299 }
300 catch (error) {
301 throw new Error(`Error occured while processing stream payload - ${getErrorMessage(error)}`);
302 }
303 },
304 };
305 }
306 function createJsonResponseAsyncIterator(response) {
307 return {
308 async *[Symbol.asyncIterator]() {
309 const processedResponse = await processJSONResponse(response);
310 yield {
311 ...processedResponse,
312 hasNext: false,
313 };
314 },
315 };
316 }
317 function getResponseDataFromChunkBodies(chunkBodies) {
318 return chunkBodies
319 .map((value) => {
320 try {
321 return JSON.parse(value);
322 }
323 catch (error) {
324 throw new Error(`Error in parsing multipart response - ${getErrorMessage(error)}`);
325 }
326 })
327 .map((payload) => {
328 const { data, incremental, hasNext, extensions, errors } = payload;
329 // initial data chunk
330 if (!incremental) {
331 return {
332 data: data || {},
333 ...getKeyValueIfValid('errors', errors),
334 ...getKeyValueIfValid('extensions', extensions),
335 hasNext,
336 };
337 }
338 // subsequent data chunks
339 const incrementalArray = incremental.map(({ data, path, errors }) => {
340 return {
341 data: data && path ? buildDataObjectByPath(path, data) : {},
342 ...getKeyValueIfValid('errors', errors),
343 };
344 });
345 return {
346 data: incrementalArray.length === 1
347 ? incrementalArray[0].data
348 : buildCombinedDataObject([
349 ...incrementalArray.map(({ data }) => data),
350 ]),
351 ...getKeyValueIfValid('errors', combineErrors(incrementalArray)),
352 hasNext,
353 };
354 });
355 }
356 function validateResponseData(responseErrors, combinedData) {
357 if (responseErrors.length > 0) {
358 throw new Error(GQL_API_ERROR, {
359 cause: {
360 graphQLErrors: responseErrors,
361 },
362 });
363 }
364 if (Object.keys(combinedData).length === 0) {
365 throw new Error(NO_DATA_OR_ERRORS_ERROR);
366 }
367 }
368 function createMultipartResponseAsyncInterator(response, responseContentType) {
369 const boundaryHeader = (responseContentType ?? '').match(BOUNDARY_HEADER_REGEX);
370 const boundary = `--${boundaryHeader ? boundaryHeader[1] : '-'}`;
371 if (!response.body?.getReader &&
372 !response.body[Symbol.asyncIterator]) {
373 throw new Error('API multipart response did not return an iterable body', {
374 cause: response,
375 });
376 }
377 const streamBodyIterator = getStreamBodyIterator(response);
378 let combinedData = {};
379 let responseExtensions;
380 return {
381 async *[Symbol.asyncIterator]() {
382 try {
383 let streamHasNext = true;
384 for await (const chunkBodies of readStreamChunk(streamBodyIterator, boundary)) {
385 const responseData = getResponseDataFromChunkBodies(chunkBodies);
386 responseExtensions =
387 responseData.find((datum) => datum.extensions)?.extensions ??
388 responseExtensions;
389 const responseErrors = combineErrors(responseData);
390 combinedData = buildCombinedDataObject([
391 combinedData,
392 ...responseData.map(({ data }) => data),
393 ]);
394 streamHasNext = responseData.slice(-1)[0].hasNext;
395 validateResponseData(responseErrors, combinedData);
396 yield {
397 ...getKeyValueIfValid('data', combinedData),
398 ...getKeyValueIfValid('extensions', responseExtensions),
399 hasNext: streamHasNext,
400 };
401 }
402 if (streamHasNext) {
403 throw new Error(`Response stream terminated unexpectedly`);
404 }
405 }
406 catch (error) {
407 const cause = getErrorCause(error);
408 yield {
409 ...getKeyValueIfValid('data', combinedData),
410 ...getKeyValueIfValid('extensions', responseExtensions),
411 errors: {
412 message: formatErrorMessage(getErrorMessage(error)),
413 networkStatusCode: response.status,
414 ...getKeyValueIfValid('graphQLErrors', cause?.graphQLErrors),
415 response,
416 },
417 hasNext: false,
418 };
419 }
420 },
421 };
422 }
423 function generateRequestStream(fetch) {
424 return async (...props) => {
425 if (!DEFER_OPERATION_REGEX.test(props[0])) {
426 throw new Error(formatErrorMessage('This operation does not result in a streamable response - use request() instead.'));
427 }
428 try {
429 const response = await fetch(...props);
430 const { statusText } = response;
431 if (!response.ok) {
432 throw new Error(statusText, { cause: response });
433 }
434 const responseContentType = response.headers.get('content-type') || '';
435 switch (true) {
436 case responseContentType.includes(CONTENT_TYPES.json):
437 return createJsonResponseAsyncIterator(response);
438 case responseContentType.includes(CONTENT_TYPES.multipart):
439 return createMultipartResponseAsyncInterator(response, responseContentType);
440 default:
441 throw new Error(`${UNEXPECTED_CONTENT_TYPE_ERROR} ${responseContentType}`, { cause: response });
442 }
443 }
444 catch (error) {
445 return {
446 async *[Symbol.asyncIterator]() {
447 const response = getErrorCause(error);
448 yield {
449 errors: {
450 message: formatErrorMessage(getErrorMessage(error)),
451 ...getKeyValueIfValid('networkStatusCode', response?.status),
452 ...getKeyValueIfValid('response', response),
453 },
454 hasNext: false,
455 };
456 },
457 };
458 }
459 };
460 }
461
462 exports.createGraphQLClient = createGraphQLClient;
463
464}));
465//# sourceMappingURL=graphql-client.js.map