@decocms/apps-shopify 7.2.0

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.
Files changed (37) hide show
  1. package/package.json +48 -0
  2. package/src/actions/cart/addItems.ts +36 -0
  3. package/src/actions/cart/updateCoupons.ts +31 -0
  4. package/src/actions/cart/updateItems.ts +31 -0
  5. package/src/actions/user/signIn.ts +40 -0
  6. package/src/actions/user/signUp.ts +31 -0
  7. package/src/client.ts +58 -0
  8. package/src/index.ts +43 -0
  9. package/src/init.ts +39 -0
  10. package/src/loaders/ProductDetailsPage.ts +31 -0
  11. package/src/loaders/ProductList.ts +102 -0
  12. package/src/loaders/ProductListingPage.ts +172 -0
  13. package/src/loaders/RelatedProducts.ts +42 -0
  14. package/src/loaders/cart.ts +74 -0
  15. package/src/loaders/shop.ts +35 -0
  16. package/src/loaders/user.ts +39 -0
  17. package/src/mod.ts +67 -0
  18. package/src/registry.ts +9 -0
  19. package/src/utils/__tests__/graphqlOperationName.test.ts +80 -0
  20. package/src/utils/__tests__/instrumentedFetch.test.ts +84 -0
  21. package/src/utils/__tests__/operationRouter.test.ts +56 -0
  22. package/src/utils/__tests__/transform.test.ts +216 -0
  23. package/src/utils/admin/admin.ts +57 -0
  24. package/src/utils/admin/queries.ts +29 -0
  25. package/src/utils/cart.ts +32 -0
  26. package/src/utils/cookies.ts +82 -0
  27. package/src/utils/enums.ts +438 -0
  28. package/src/utils/graphql.ts +69 -0
  29. package/src/utils/graphqlOperationName.ts +67 -0
  30. package/src/utils/instrumentedFetch.ts +55 -0
  31. package/src/utils/operationRouter.ts +70 -0
  32. package/src/utils/storefront/queries.ts +525 -0
  33. package/src/utils/transform.ts +430 -0
  34. package/src/utils/types.ts +191 -0
  35. package/src/utils/user.ts +23 -0
  36. package/src/utils/utils.ts +159 -0
  37. package/tsconfig.json +7 -0
@@ -0,0 +1,438 @@
1
+ export enum CurrencyCode {
2
+ AED = "AED",
3
+ AFN = "AFN",
4
+ ALL = "ALL",
5
+ AMD = "AMD",
6
+ ANG = "ANG",
7
+ AOA = "AOA",
8
+ ARS = "ARS",
9
+ AUD = "AUD",
10
+ AWG = "AWG",
11
+ AZN = "AZN",
12
+ BAM = "BAM",
13
+ BBD = "BBD",
14
+ BDT = "BDT",
15
+ BGN = "BGN",
16
+ BHD = "BHD",
17
+ BIF = "BIF",
18
+ BMD = "BMD",
19
+ BND = "BND",
20
+ BOB = "BOB",
21
+ BRL = "BRL",
22
+ BSD = "BSD",
23
+ BTN = "BTN",
24
+ BWP = "BWP",
25
+ BYN = "BYN",
26
+ BZD = "BZD",
27
+ CAD = "CAD",
28
+ CDF = "CDF",
29
+ CHF = "CHF",
30
+ CLP = "CLP",
31
+ CNY = "CNY",
32
+ COP = "COP",
33
+ CRC = "CRC",
34
+ CVE = "CVE",
35
+ CZK = "CZK",
36
+ DJF = "DJF",
37
+ DKK = "DKK",
38
+ DOP = "DOP",
39
+ DZD = "DZD",
40
+ EGP = "EGP",
41
+ ERN = "ERN",
42
+ ETB = "ETB",
43
+ EUR = "EUR",
44
+ FJD = "FJD",
45
+ FKP = "FKP",
46
+ GBP = "GBP",
47
+ GEL = "GEL",
48
+ GHS = "GHS",
49
+ GIP = "GIP",
50
+ GMD = "GMD",
51
+ GNF = "GNF",
52
+ GTQ = "GTQ",
53
+ GYD = "GYD",
54
+ HKD = "HKD",
55
+ HNL = "HNL",
56
+ HRK = "HRK",
57
+ HTG = "HTG",
58
+ HUF = "HUF",
59
+ IDR = "IDR",
60
+ ILS = "ILS",
61
+ INR = "INR",
62
+ IQD = "IQD",
63
+ IRR = "IRR",
64
+ ISK = "ISK",
65
+ JEP = "JEP",
66
+ JMD = "JMD",
67
+ JOD = "JOD",
68
+ JPY = "JPY",
69
+ KES = "KES",
70
+ KGS = "KGS",
71
+ KHR = "KHR",
72
+ KID = "KID",
73
+ KMF = "KMF",
74
+ KRW = "KRW",
75
+ KWD = "KWD",
76
+ KYD = "KYD",
77
+ KZT = "KZT",
78
+ LAK = "LAK",
79
+ LBP = "LBP",
80
+ LKR = "LKR",
81
+ LRD = "LRD",
82
+ LSL = "LSL",
83
+ LTL = "LTL",
84
+ LVL = "LVL",
85
+ LYD = "LYD",
86
+ MAD = "MAD",
87
+ MDL = "MDL",
88
+ MGA = "MGA",
89
+ MKD = "MKD",
90
+ MMK = "MMK",
91
+ MNT = "MNT",
92
+ MOP = "MOP",
93
+ MRU = "MRU",
94
+ MUR = "MUR",
95
+ MVR = "MVR",
96
+ MWK = "MWK",
97
+ MXN = "MXN",
98
+ MYR = "MYR",
99
+ MZN = "MZN",
100
+ NAD = "NAD",
101
+ NGN = "NGN",
102
+ NIO = "NIO",
103
+ NOK = "NOK",
104
+ NPR = "NPR",
105
+ NZD = "NZD",
106
+ OMR = "OMR",
107
+ PAB = "PAB",
108
+ PEN = "PEN",
109
+ PGK = "PGK",
110
+ PHP = "PHP",
111
+ PKR = "PKR",
112
+ PLN = "PLN",
113
+ PYG = "PYG",
114
+ QAR = "QAR",
115
+ RON = "RON",
116
+ RSD = "RSD",
117
+ RUB = "RUB",
118
+ RWF = "RWF",
119
+ SAR = "SAR",
120
+ SBD = "SBD",
121
+ SCR = "SCR",
122
+ SDG = "SDG",
123
+ SEK = "SEK",
124
+ SGD = "SGD",
125
+ SHP = "SHP",
126
+ SLL = "SLL",
127
+ SOS = "SOS",
128
+ SRD = "SRD",
129
+ SSP = "SSP",
130
+ STN = "STN",
131
+ SYP = "SYP",
132
+ SZL = "SZL",
133
+ THB = "THB",
134
+ TJS = "TJS",
135
+ TMT = "TMT",
136
+ TND = "TND",
137
+ TOP = "TOP",
138
+ TRY = "TRY",
139
+ TTD = "TTD",
140
+ TWD = "TWD",
141
+ TZS = "TZS",
142
+ UAH = "UAH",
143
+ UGX = "UGX",
144
+ USD = "USD",
145
+ UYU = "UYU",
146
+ UZS = "UZS",
147
+ VED = "VED",
148
+ VES = "VES",
149
+ VND = "VND",
150
+ VUV = "VUV",
151
+ WST = "WST",
152
+ XAF = "XAF",
153
+ XCD = "XCD",
154
+ XOF = "XOF",
155
+ XPF = "XPF",
156
+ XXX = "XXX",
157
+ YER = "YER",
158
+ ZAR = "ZAR",
159
+ ZMW = "ZMW",
160
+ }
161
+
162
+ export enum CountryCode {
163
+ AC = "AC",
164
+ AD = "AD",
165
+ AE = "AE",
166
+ AF = "AF",
167
+ AG = "AG",
168
+ AI = "AI",
169
+ AL = "AL",
170
+ AM = "AM",
171
+ AN = "AN",
172
+ AO = "AO",
173
+ AR = "AR",
174
+ AT = "AT",
175
+ AU = "AU",
176
+ AW = "AW",
177
+ AX = "AX",
178
+ AZ = "AZ",
179
+ BA = "BA",
180
+ BB = "BB",
181
+ BD = "BD",
182
+ BE = "BE",
183
+ BF = "BF",
184
+ BG = "BG",
185
+ BH = "BH",
186
+ BI = "BI",
187
+ BJ = "BJ",
188
+ BL = "BL",
189
+ BM = "BM",
190
+ BN = "BN",
191
+ BO = "BO",
192
+ BQ = "BQ",
193
+ BR = "BR",
194
+ BS = "BS",
195
+ BT = "BT",
196
+ BV = "BV",
197
+ BW = "BW",
198
+ BY = "BY",
199
+ BZ = "BZ",
200
+ CA = "CA",
201
+ CC = "CC",
202
+ CD = "CD",
203
+ CF = "CF",
204
+ CG = "CG",
205
+ CH = "CH",
206
+ CI = "CI",
207
+ CK = "CK",
208
+ CL = "CL",
209
+ CM = "CM",
210
+ CN = "CN",
211
+ CO = "CO",
212
+ CR = "CR",
213
+ CU = "CU",
214
+ CV = "CV",
215
+ CW = "CW",
216
+ CX = "CX",
217
+ CY = "CY",
218
+ CZ = "CZ",
219
+ DE = "DE",
220
+ DJ = "DJ",
221
+ DK = "DK",
222
+ DM = "DM",
223
+ DO = "DO",
224
+ DZ = "DZ",
225
+ EC = "EC",
226
+ EE = "EE",
227
+ EG = "EG",
228
+ EH = "EH",
229
+ ER = "ER",
230
+ ES = "ES",
231
+ ET = "ET",
232
+ FI = "FI",
233
+ FJ = "FJ",
234
+ FK = "FK",
235
+ FO = "FO",
236
+ FR = "FR",
237
+ GA = "GA",
238
+ GB = "GB",
239
+ GD = "GD",
240
+ GE = "GE",
241
+ GF = "GF",
242
+ GG = "GG",
243
+ GH = "GH",
244
+ GI = "GI",
245
+ GL = "GL",
246
+ GM = "GM",
247
+ GN = "GN",
248
+ GP = "GP",
249
+ GQ = "GQ",
250
+ GR = "GR",
251
+ GS = "GS",
252
+ GT = "GT",
253
+ GW = "GW",
254
+ GY = "GY",
255
+ HK = "HK",
256
+ HM = "HM",
257
+ HN = "HN",
258
+ HR = "HR",
259
+ HT = "HT",
260
+ HU = "HU",
261
+ ID = "ID",
262
+ IE = "IE",
263
+ IL = "IL",
264
+ IM = "IM",
265
+ IN = "IN",
266
+ IO = "IO",
267
+ IQ = "IQ",
268
+ IR = "IR",
269
+ IS = "IS",
270
+ IT = "IT",
271
+ JE = "JE",
272
+ JM = "JM",
273
+ JO = "JO",
274
+ JP = "JP",
275
+ KE = "KE",
276
+ KG = "KG",
277
+ KH = "KH",
278
+ KI = "KI",
279
+ KM = "KM",
280
+ KN = "KN",
281
+ KP = "KP",
282
+ KR = "KR",
283
+ KW = "KW",
284
+ KY = "KY",
285
+ KZ = "KZ",
286
+ LA = "LA",
287
+ LB = "LB",
288
+ LC = "LC",
289
+ LI = "LI",
290
+ LK = "LK",
291
+ LR = "LR",
292
+ LS = "LS",
293
+ LT = "LT",
294
+ LU = "LU",
295
+ LV = "LV",
296
+ LY = "LY",
297
+ MA = "MA",
298
+ MC = "MC",
299
+ MD = "MD",
300
+ ME = "ME",
301
+ MF = "MF",
302
+ MG = "MG",
303
+ MK = "MK",
304
+ ML = "ML",
305
+ MM = "MM",
306
+ MN = "MN",
307
+ MO = "MO",
308
+ MQ = "MQ",
309
+ MR = "MR",
310
+ MS = "MS",
311
+ MT = "MT",
312
+ MU = "MU",
313
+ MV = "MV",
314
+ MW = "MW",
315
+ MX = "MX",
316
+ MY = "MY",
317
+ MZ = "MZ",
318
+ NA = "NA",
319
+ NC = "NC",
320
+ NE = "NE",
321
+ NF = "NF",
322
+ NG = "NG",
323
+ NI = "NI",
324
+ NL = "NL",
325
+ NO = "NO",
326
+ NP = "NP",
327
+ NR = "NR",
328
+ NU = "NU",
329
+ NZ = "NZ",
330
+ OM = "OM",
331
+ PA = "PA",
332
+ PE = "PE",
333
+ PF = "PF",
334
+ PG = "PG",
335
+ PH = "PH",
336
+ PK = "PK",
337
+ PL = "PL",
338
+ PM = "PM",
339
+ PN = "PN",
340
+ PS = "PS",
341
+ PT = "PT",
342
+ PY = "PY",
343
+ QA = "QA",
344
+ RE = "RE",
345
+ RO = "RO",
346
+ RS = "RS",
347
+ RU = "RU",
348
+ RW = "RW",
349
+ SA = "SA",
350
+ SB = "SB",
351
+ SC = "SC",
352
+ SD = "SD",
353
+ SE = "SE",
354
+ SG = "SG",
355
+ SH = "SH",
356
+ SI = "SI",
357
+ SJ = "SJ",
358
+ SK = "SK",
359
+ SL = "SL",
360
+ SM = "SM",
361
+ SN = "SN",
362
+ SO = "SO",
363
+ SR = "SR",
364
+ SS = "SS",
365
+ ST = "ST",
366
+ SV = "SV",
367
+ SX = "SX",
368
+ SY = "SY",
369
+ SZ = "SZ",
370
+ TA = "TA",
371
+ TC = "TC",
372
+ TD = "TD",
373
+ TF = "TF",
374
+ TG = "TG",
375
+ TH = "TH",
376
+ TJ = "TJ",
377
+ TK = "TK",
378
+ TL = "TL",
379
+ TM = "TM",
380
+ TN = "TN",
381
+ TO = "TO",
382
+ TR = "TR",
383
+ TT = "TT",
384
+ TV = "TV",
385
+ TW = "TW",
386
+ TZ = "TZ",
387
+ UA = "UA",
388
+ UG = "UG",
389
+ UM = "UM",
390
+ US = "US",
391
+ UY = "UY",
392
+ UZ = "UZ",
393
+ VA = "VA",
394
+ VC = "VC",
395
+ VE = "VE",
396
+ VG = "VG",
397
+ VN = "VN",
398
+ VU = "VU",
399
+ WF = "WF",
400
+ WS = "WS",
401
+ XK = "XK",
402
+ YE = "YE",
403
+ YT = "YT",
404
+ ZA = "ZA",
405
+ ZM = "ZM",
406
+ ZW = "ZW",
407
+ ZZ = "ZZ",
408
+ }
409
+
410
+ export enum OrderCancelReason {
411
+ CUSTOMER = "CUSTOMER",
412
+ DECLINED = "DECLINED",
413
+ FRAUD = "FRAUD",
414
+ INVENTORY = "INVENTORY",
415
+ OTHER = "OTHER",
416
+ }
417
+
418
+ export enum OrderFinancialStatus {
419
+ AUTHORIZED = "AUTHORIZED",
420
+ PAID = "PAID",
421
+ PARTIALLY_PAID = "PARTIALLY_PAID",
422
+ PARTIALLY_REFUNDED = "PARTIALLY_REFUNDED",
423
+ PENDING = "PENDING",
424
+ REFUNDED = "REFUNDED",
425
+ VOIDED = "VOIDED",
426
+ }
427
+
428
+ export enum OrderFulfillmentStatus {
429
+ FULFILLED = "FULFILLED",
430
+ IN_PROGRESS = "IN_PROGRESS",
431
+ ON_HOLD = "ON_HOLD",
432
+ OPEN = "OPEN",
433
+ PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED",
434
+ PENDING_FULFILLMENT = "PENDING_FULFILLMENT",
435
+ RESTOCKED = "RESTOCKED",
436
+ SCHEDULED = "SCHEDULED",
437
+ UNFULFILLED = "UNFULFILLED",
438
+ }
@@ -0,0 +1,69 @@
1
+ import type { InstrumentedFetchInit } from "@decocms/blocks/sdk/instrumentedFetch";
2
+ import { extractGraphqlOperationName } from "./graphqlOperationName";
3
+
4
+ export function gql(strings: TemplateStringsArray, ...values: unknown[]): string {
5
+ return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "");
6
+ }
7
+
8
+ export interface QueryDefinition {
9
+ fragments?: string[];
10
+ query: string;
11
+ }
12
+
13
+ export function buildQuery(def: QueryDefinition): string {
14
+ const fragments = def.fragments?.join("\n") ?? "";
15
+ return fragments ? `${fragments}\n${def.query}` : def.query;
16
+ }
17
+
18
+ export interface GraphQLClient {
19
+ query<T>(query: string | QueryDefinition, variables?: Record<string, unknown>): Promise<T>;
20
+ }
21
+
22
+ export function createGraphqlClient(
23
+ endpoint: string,
24
+ headers: Record<string, string>,
25
+ fetchFn?: typeof fetch,
26
+ ): GraphQLClient {
27
+ const _fetch = fetchFn ?? globalThis.fetch;
28
+ return {
29
+ async query<T>(
30
+ queryOrDef: string | QueryDefinition,
31
+ variables?: Record<string, unknown>,
32
+ ): Promise<T> {
33
+ const query = typeof queryOrDef === "string" ? queryOrDef : buildQuery(queryOrDef);
34
+
35
+ // Stamp the GraphQL operation as init.operation so the framework's
36
+ // span name becomes `shopify.<OperationName>` instead of the
37
+ // generic `shopify.storefront.graphql` from the URL router. The
38
+ // extra field is silently dropped by plain `fetch` and read by
39
+ // any `InstrumentedFetch` configured via `setShopifyFetch`.
40
+ const operation = extractGraphqlOperationName(query);
41
+ const init: InstrumentedFetchInit = {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ ...headers,
46
+ },
47
+ body: JSON.stringify({ query, variables }),
48
+ ...(operation ? { operation } : {}),
49
+ };
50
+ const response = await _fetch(endpoint, init);
51
+
52
+ if (!response.ok) {
53
+ throw new Error(`Shopify GraphQL error: ${response.status} ${response.statusText}`);
54
+ }
55
+
56
+ const json = (await response.json()) as { data?: T; errors?: Array<{ message: string }> };
57
+
58
+ if (json.errors?.length) {
59
+ throw new Error(`Shopify GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
60
+ }
61
+
62
+ if (json.data === undefined) {
63
+ throw new Error("Shopify GraphQL response missing data");
64
+ }
65
+
66
+ return json.data;
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Extract a semantic operation name from a GraphQL document.
3
+ *
4
+ * Used at the Shopify GraphQL client layer to stamp `init.operation`
5
+ * on the outbound fetch. The framework then suffixes the integration
6
+ * name (`shopify.<operation>`) onto the span and uses the same string
7
+ * as the `fetch.operation` attribute + histogram label.
8
+ *
9
+ * Resolution order:
10
+ *
11
+ * 1. An explicit `operationName` argument (e.g. when the client
12
+ * received one alongside a multi-operation document) wins.
13
+ * 2. If the document has exactly one named operation, that name
14
+ * is used.
15
+ * 3. If the document has zero or many anonymous operations, we
16
+ * return `undefined` so the caller can fall back (typically to
17
+ * the URL-derived `storefront.graphql` / `admin.graphql`).
18
+ *
19
+ * The parser is deliberately a small regex pass, not a full GraphQL
20
+ * tokenizer:
21
+ *
22
+ * - GraphQL operation definitions live at the top level of the
23
+ * document, never nested inside other operations, fragments, or
24
+ * selection sets, so positional context isn't required to find
25
+ * them — only to not match the literal words `query` /
26
+ * `mutation` / `subscription` inside string values.
27
+ * - We strip block strings (`""" … """`), string literals
28
+ * (`"…"`), and `# …` comments before matching, which is enough
29
+ * to make false-positive matches inside comments / docs vanish.
30
+ *
31
+ * If a Shopify operation is ever sufficiently mis-named to break
32
+ * this (unlikely, since the storefront SDK names them deliberately),
33
+ * the caller can always set `init.operation` explicitly.
34
+ */
35
+
36
+ const OPERATION_RE = /\b(?:query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
37
+
38
+ const stripCommentsAndStrings = (doc: string): string =>
39
+ doc
40
+ .replace(/"""[\s\S]*?"""/g, '""')
41
+ .replace(/"(?:\\.|[^"\\])*"/g, '""')
42
+ .replace(/#[^\n]*/g, "");
43
+
44
+ export function extractGraphqlOperationName(
45
+ document: string,
46
+ explicit?: string,
47
+ ): string | undefined {
48
+ if (explicit) return explicit;
49
+ if (!document) return undefined;
50
+
51
+ const stripped = stripCommentsAndStrings(document);
52
+ const names: string[] = [];
53
+
54
+ OPERATION_RE.lastIndex = 0;
55
+ for (
56
+ let match = OPERATION_RE.exec(stripped);
57
+ match !== null;
58
+ match = OPERATION_RE.exec(stripped)
59
+ ) {
60
+ const [, name] = match;
61
+ if (name) names.push(name);
62
+ if (names.length > 1) break;
63
+ }
64
+
65
+ if (names.length === 1) return names[0];
66
+ return undefined;
67
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Pre-wired instrumented fetch factory for Shopify.
3
+ *
4
+ * Mirrors `vtex/utils/instrumentedFetch.ts`. Bundles:
5
+ *
6
+ * 1. `createInstrumentedFetch` from `@decocms/start` (spans,
7
+ * traceparent, URL redaction).
8
+ * 2. `shopifyOperationRouter` as the URL fallback for non-GraphQL
9
+ * and unnamed-GraphQL calls.
10
+ * 3. An `onComplete` that records the canonical
11
+ * `http.client.request.duration` histogram (via the framework's
12
+ * `recordCommerceMetric(...)` helper) with `provider: "shopify"`.
13
+ *
14
+ * Sites do:
15
+ *
16
+ * ```ts
17
+ * import { setShopifyFetch, createShopifyFetch } from "@decocms/apps/shopify";
18
+ * setShopifyFetch(createShopifyFetch());
19
+ * ```
20
+ *
21
+ * Per-call operation names come from `extractGraphqlOperationName`
22
+ * (wired in `./graphql.ts`); the URL router fires only when the
23
+ * extractor returns `undefined`.
24
+ */
25
+
26
+ import {
27
+ createInstrumentedFetch,
28
+ type InstrumentedFetch,
29
+ } from "@decocms/blocks/sdk/instrumentedFetch";
30
+ import { recordCommerceMetric } from "@decocms/blocks/sdk/observability";
31
+ import { shopifyOperationRouter } from "./operationRouter";
32
+
33
+ export interface CreateShopifyFetchOptions {
34
+ baseFetch?: typeof fetch;
35
+ disableHistogram?: boolean;
36
+ }
37
+
38
+ export function createShopifyFetch(options: CreateShopifyFetchOptions = {}): InstrumentedFetch {
39
+ const { baseFetch, disableHistogram = false } = options;
40
+ return createInstrumentedFetch({
41
+ name: "shopify",
42
+ baseFetch,
43
+ resolveOperation: shopifyOperationRouter,
44
+ onComplete: disableHistogram
45
+ ? undefined
46
+ : ({ operation, status, durationMs, cached }) => {
47
+ recordCommerceMetric(durationMs, {
48
+ provider: "shopify",
49
+ operation,
50
+ status_class: `${Math.floor(status / 100)}xx`,
51
+ cached,
52
+ });
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * URL-derived operation name router for Shopify API calls.
3
+ *
4
+ * Plugged into `@decocms/start`'s `createInstrumentedFetch` via the
5
+ * `resolveOperation(url, method)` option. Mirrors the shape of the
6
+ * VTEX router in `../../vtex/utils/operationRouter.ts`.
7
+ *
8
+ * Shopify's API surface from this repo is overwhelmingly GraphQL —
9
+ * a single endpoint per environment (storefront vs admin). That means
10
+ * the URL alone can only tell us *which GraphQL surface* a call is
11
+ * hitting, not what the call actually does. The semantic operation
12
+ * name lives in the GraphQL document itself (`query Foo { ... }`),
13
+ * and is extracted by `extractGraphqlOperationName` (see
14
+ * `./graphqlOperationName.ts`) at the client layer and stamped as
15
+ * `init.operation`, which always wins over this router.
16
+ *
17
+ * So this router exists for:
18
+ *
19
+ * - non-GraphQL Shopify REST endpoints we may add later
20
+ * (cart API, customer accounts, billing, etc.);
21
+ * - giving the GraphQL endpoints a *fallback* operation when the
22
+ * extractor can't parse a name (anonymous queries, missing body).
23
+ */
24
+
25
+ type OperationResolver = string | ((match: RegExpMatchArray, method: string) => string);
26
+
27
+ interface Matcher {
28
+ pattern: RegExp;
29
+ operation: OperationResolver;
30
+ }
31
+
32
+ const m = (pattern: RegExp, operation: OperationResolver): Matcher => ({ pattern, operation });
33
+
34
+ const MATCHERS: ReadonlyArray<Matcher> = [
35
+ m(/^\/admin\/api\/[0-9]{4}-[0-9]{2}\/graphql\.json/, "admin.graphql"),
36
+ m(/^\/api\/[0-9]{4}-[0-9]{2}\/graphql\.json/, "storefront.graphql"),
37
+
38
+ m(/^\/admin\/api\/[0-9]{4}-[0-9]{2}\/products/, "admin.products"),
39
+ m(/^\/admin\/api\/[0-9]{4}-[0-9]{2}\/orders/, "admin.orders"),
40
+ m(/^\/admin\/api\/[0-9]{4}-[0-9]{2}\/customers/, "admin.customers"),
41
+ m(/^\/admin\/api\/[0-9]{4}-[0-9]{2}\/inventory/, "admin.inventory"),
42
+
43
+ m(/^\/api\/[0-9]{4}-[0-9]{2}\/checkouts/, "storefront.checkout"),
44
+ m(/^\/cart(?:\/|\.js|$)/, "storefront.cart"),
45
+ ];
46
+
47
+ /**
48
+ * Resolve an operation name for a Shopify URL. Returns `undefined`
49
+ * if no matcher fires, which causes the framework to fall back to
50
+ * `shopify.fetch`.
51
+ */
52
+ export function shopifyOperationRouter(url: string, method: string): string | undefined {
53
+ let pathname: string;
54
+ try {
55
+ pathname = new URL(url).pathname;
56
+ } catch {
57
+ const qs = url.indexOf("?");
58
+ const hash = url.indexOf("#");
59
+ const end = [qs, hash].filter((i) => i >= 0).sort((a, b) => a - b)[0];
60
+ pathname = end === undefined ? url : url.slice(0, end);
61
+ }
62
+
63
+ const upperMethod = method.toUpperCase();
64
+ for (const { pattern, operation } of MATCHERS) {
65
+ const match = pathname.match(pattern);
66
+ if (!match) continue;
67
+ return typeof operation === "function" ? operation(match, upperMethod) : operation;
68
+ }
69
+ return undefined;
70
+ }