@cobrastyle/adapter-magento2 1.0.1

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.
@@ -0,0 +1,815 @@
1
+ import { makeCartUserError } from "./cart-errors";
2
+ import { graphqlFetch } from "./client";
3
+ import { getConfig, withCustomerAuth } from "./config";
4
+ import * as queries from "./queries";
5
+ import * as mappers from "./mappers";
6
+ import { stripSuffix } from "./urls";
7
+ import { getStores } from "./stores";
8
+ import { getStoreConfig } from "./store-config";
9
+ import { getAttributeLabels } from "./attribute-labels";
10
+ /**
11
+ * Next.js cache tags attached to cacheable content reads so they can be purged
12
+ * on demand via revalidateTag() (e.g. from a product-import webhook) instead of
13
+ * only expiring on the time-based revalidate window. These strings are a shared
14
+ * contract with the host app's revalidation endpoint — keep them in sync with
15
+ * the app's cacheTags map.
16
+ */
17
+ const CONTENT_TAGS = {
18
+ products: "products",
19
+ product: (sku) => `product:${sku}`,
20
+ categories: "categories",
21
+ category: (id) => `category:${id}`,
22
+ navigation: "navigation",
23
+ cms: "cms",
24
+ search: "search",
25
+ };
26
+ // Map a Magento cart to our Cart, applying the store's product URL suffix to
27
+ // line-item links. Centralizes the storeConfig lookup (memoized) so every cart
28
+ // return path stays consistent without threading the suffix through each one.
29
+ async function mapCartWithUrls(cart) {
30
+ const { productUrlSuffix } = await getStoreConfig();
31
+ return mappers.mapCart(cart, productUrlSuffix);
32
+ }
33
+ export const magento2Adapter = {
34
+ product: {
35
+ get: async (id) => {
36
+ const [{ productUrlSuffix, categoryUrlSuffix }, attributeLabels] = await Promise.all([getStoreConfig(), getAttributeLabels()]);
37
+ // Note: id is typically the SKU in our system
38
+ const data = await graphqlFetch(queries.GET_PRODUCT_BY_ID, { sku: id }, { tags: [CONTENT_TAGS.products, CONTENT_TAGS.product(String(id))] });
39
+ const product = data.products.items[0];
40
+ return product
41
+ ? mappers.mapProduct(product, productUrlSuffix, categoryUrlSuffix, attributeLabels)
42
+ : null;
43
+ },
44
+ getByUrlKey: async (urlKey) => {
45
+ const [{ productUrlSuffix, categoryUrlSuffix }, attributeLabels] = await Promise.all([getStoreConfig(), getAttributeLabels()]);
46
+ // Strip any store URL suffix (e.g. ".html") and, for nested paths like
47
+ // "category/product-url-key", extract the last segment.
48
+ const stripped = stripSuffix(urlKey, productUrlSuffix);
49
+ const productUrlKey = stripped.includes("/")
50
+ ? stripped.split("/").pop()
51
+ : stripped;
52
+ const data = await graphqlFetch(queries.GET_PRODUCT_BY_URL_KEY, { urlKey: productUrlKey }, { tags: [CONTENT_TAGS.products] });
53
+ const product = data.products.items[0];
54
+ return product
55
+ ? mappers.mapProduct(product, productUrlSuffix, categoryUrlSuffix, attributeLabels)
56
+ : null;
57
+ },
58
+ getRelated: async (productId) => {
59
+ const { productUrlSuffix } = await getStoreConfig();
60
+ // productId is typically the SKU - fetch directly with related_products
61
+ const data = await graphqlFetch(queries.GET_RELATED_PRODUCTS_BY_SKU, { sku: productId });
62
+ const relatedProducts = data.products.items[0]?.related_products ?? [];
63
+ return relatedProducts.map((p) => mappers.mapProductListItem(p, productUrlSuffix));
64
+ },
65
+ getUpsells: async (productId) => {
66
+ const { productUrlSuffix } = await getStoreConfig();
67
+ // productId is typically the SKU - fetch directly with upsell_products
68
+ const data = await graphqlFetch(queries.GET_RELATED_PRODUCTS_BY_SKU, { sku: productId });
69
+ const upsellProducts = data.products.items[0]?.upsell_products ?? [];
70
+ return upsellProducts.map((p) => mappers.mapProductListItem(p, productUrlSuffix));
71
+ },
72
+ getBySkus: async (skus) => {
73
+ const unique = [...new Set(skus)].filter(Boolean);
74
+ if (unique.length === 0)
75
+ return [];
76
+ const { productUrlSuffix } = await getStoreConfig();
77
+ const data = await graphqlFetch(queries.GET_PRODUCTS_BY_SKUS, { skus: unique, pageSize: unique.length }, { tags: [CONTENT_TAGS.products] });
78
+ return data.products.items.map((p) => mappers.mapProductListItem(p, productUrlSuffix));
79
+ },
80
+ },
81
+ category: {
82
+ get: async (id) => {
83
+ const { categoryUrlSuffix } = await getStoreConfig();
84
+ const data = await graphqlFetch(queries.GET_CATEGORY_BY_URL_KEY, { urlKey: id }, { tags: [CONTENT_TAGS.categories, CONTENT_TAGS.category(String(id))] });
85
+ const category = data.categoryList[0];
86
+ return category ? mappers.mapCategory(category, categoryUrlSuffix) : null;
87
+ },
88
+ // Resolve a category by its Magento uid. This is the robust lookup used by
89
+ // URL resolution: uid is unambiguous, immune to stale `url_path` and to
90
+ // duplicate `url_key`s across the tree (e.g. two "magasin" categories).
91
+ getByUid: async (uid) => {
92
+ const { categoryUrlSuffix } = await getStoreConfig();
93
+ const data = await graphqlFetch(queries.GET_CATEGORY_BY_UID, { uid }, { tags: [CONTENT_TAGS.categories] });
94
+ const category = data.categoryList[0];
95
+ return category ? mappers.mapCategory(category, categoryUrlSuffix) : null;
96
+ },
97
+ getByUrlKey: async (urlKey) => {
98
+ const { categoryUrlSuffix } = await getStoreConfig();
99
+ // Accept paths that still carry the store URL suffix (e.g. ".html").
100
+ const path = stripSuffix(urlKey, categoryUrlSuffix);
101
+ // First try the full path (e.g. "produkter/backljus").
102
+ let data = await graphqlFetch(queries.GET_CATEGORY_BY_URL_KEY, { urlKey: path }, { tags: [CONTENT_TAGS.categories] });
103
+ // If not found and the path contains slashes, try the last segment only.
104
+ // (Magento's `url_path` can be stale, so the full-path filter may miss.)
105
+ if (!data.categoryList[0] && path.includes("/")) {
106
+ const lastSegment = path.split("/").pop();
107
+ data = await graphqlFetch(queries.GET_CATEGORY_BY_URL_KEY_SINGLE, { urlKey: lastSegment }, { tags: [CONTENT_TAGS.categories] });
108
+ }
109
+ // If still not found, try as a single url_key.
110
+ if (!data.categoryList[0]) {
111
+ data = await graphqlFetch(queries.GET_CATEGORY_BY_URL_KEY_SINGLE, { urlKey: path }, { tags: [CONTENT_TAGS.categories] });
112
+ }
113
+ const category = data.categoryList[0];
114
+ return category ? mappers.mapCategory(category, categoryUrlSuffix) : null;
115
+ },
116
+ getProducts: async (input) => {
117
+ const { productUrlSuffix } = await getStoreConfig();
118
+ // Build the filter object for Magento GraphQL
119
+ // Use category_uid for Magento 2.4+ compatibility
120
+ // If categoryId looks like a numeric ID, convert to base64 UID
121
+ const categoryUid = /^\d+$/.test(input.categoryId)
122
+ ? Buffer.from(input.categoryId).toString("base64")
123
+ : input.categoryId;
124
+ const filter = {
125
+ category_uid: { eq: categoryUid },
126
+ };
127
+ // Helper to detect and parse range filter values (format: "from_to" like "10_100" or "*_100" or "100_*")
128
+ const parseRangeValue = (value) => {
129
+ // Range values typically have underscore separator: "min_max"
130
+ const rangeMatch = value.match(/^(\*|\d+(?:\.\d+)?)_(\*|\d+(?:\.\d+)?)$/);
131
+ if (rangeMatch) {
132
+ const [, fromVal, toVal] = rangeMatch;
133
+ const result = {};
134
+ if (fromVal !== "*")
135
+ result.from = fromVal;
136
+ if (toVal !== "*")
137
+ result.to = toVal;
138
+ return result;
139
+ }
140
+ return null;
141
+ };
142
+ // Known range filter attributes in Magento (price is always range, others can be custom)
143
+ const knownRangeFilters = new Set(["price"]);
144
+ // Add attribute filters from input
145
+ if (input.filters) {
146
+ for (const [code, values] of Object.entries(input.filters)) {
147
+ const valueArray = Array.isArray(values) ? values : [values];
148
+ if (valueArray.length === 0)
149
+ continue;
150
+ // Check if this looks like a range filter
151
+ const firstValue = valueArray[0];
152
+ const rangeValue = parseRangeValue(firstValue);
153
+ if (rangeValue || knownRangeFilters.has(code)) {
154
+ // This is a range filter - use from/to syntax
155
+ // For range filters, we use the first value only (ranges can't be combined with "in")
156
+ const parsed = rangeValue || parseRangeValue(firstValue);
157
+ if (parsed) {
158
+ filter[code] = parsed;
159
+ }
160
+ else {
161
+ // Fallback: treat as a single equality value if it doesn't match range pattern
162
+ filter[code] = { eq: firstValue };
163
+ }
164
+ }
165
+ else {
166
+ // Standard filter - use eq/in syntax
167
+ filter[code] =
168
+ valueArray.length === 1
169
+ ? { eq: valueArray[0] }
170
+ : { in: valueArray };
171
+ }
172
+ }
173
+ }
174
+ const data = await graphqlFetch(queries.GET_CATEGORY_PRODUCTS, {
175
+ filter,
176
+ pageSize: input.pageSize ?? 12,
177
+ currentPage: input.page ?? 1,
178
+ sort: input.sort
179
+ ? { [input.sort.field]: input.sort.direction }
180
+ : undefined,
181
+ }, { tags: [CONTENT_TAGS.categories, CONTENT_TAGS.products] });
182
+ return {
183
+ items: data.products.items.map((p) => mappers.mapProductListItem(p, productUrlSuffix)),
184
+ totalCount: data.products.total_count,
185
+ pageInfo: {
186
+ currentPage: data.products.page_info.current_page,
187
+ pageSize: data.products.page_info.page_size,
188
+ totalPages: data.products.page_info.total_pages,
189
+ },
190
+ filters: (data.products.aggregations ?? []).map((agg) => {
191
+ // Detect range filters - price is always range, others check for underscore pattern in values
192
+ const isRangeFilter = agg.attribute_code === "price" ||
193
+ agg.options?.some((opt) => /^\*?\d*_\d*\*?$/.test(opt.value));
194
+ return {
195
+ code: agg.attribute_code,
196
+ label: agg.label,
197
+ type: isRangeFilter ? "range" : "multiselect",
198
+ values: (agg.options ?? []).map((opt) => ({
199
+ value: opt.value,
200
+ label: opt.label,
201
+ count: opt.count,
202
+ })),
203
+ };
204
+ }),
205
+ sortOptions: (data.products.sort_fields?.options ?? []).map((opt) => ({
206
+ value: opt.value,
207
+ label: opt.label,
208
+ })),
209
+ };
210
+ },
211
+ searchByName: async (query, limit = 5) => {
212
+ const q = query.trim();
213
+ if (q.length < 2)
214
+ return [];
215
+ const data = await graphqlFetch(queries.GET_CATEGORY_SUGGESTIONS, { name: q });
216
+ return (data.categoryList ?? [])
217
+ .slice(0, limit)
218
+ .map((c) => ({ name: c.name, urlKey: c.url_key, urlPath: c.url_path }));
219
+ },
220
+ },
221
+ navigation: {
222
+ get: async () => {
223
+ try {
224
+ // Need the store config for the URL suffix; it also provides the root
225
+ // category id when it isn't set via env.
226
+ const storeConfig = await getStoreConfig();
227
+ const rootCategoryId = getConfig().rootCategoryId ?? storeConfig.rootCategoryId;
228
+ const suffix = storeConfig.categoryUrlSuffix;
229
+ const data = await graphqlFetch(queries.GET_NAVIGATION, { id: rootCategoryId }, { tags: [CONTENT_TAGS.navigation] });
230
+ if (!data.category)
231
+ return [];
232
+ return (data.category.children ?? [])
233
+ .filter(mappers.isIncludedInMenu)
234
+ .map((child) => mappers.mapNavigationItem(child, 1, [], suffix));
235
+ }
236
+ catch (error) {
237
+ console.warn("Failed to fetch navigation:", error);
238
+ return [];
239
+ }
240
+ },
241
+ },
242
+ cart: {
243
+ create: async () => {
244
+ const authOpts = await withCustomerAuth();
245
+ let cartId;
246
+ if (authOpts.customerToken) {
247
+ // Authenticated: get or create the customer's cart
248
+ const data = await graphqlFetch(queries.GET_CUSTOMER_CART, undefined, authOpts);
249
+ cartId = data.customerCart.id;
250
+ }
251
+ else {
252
+ // Guest: create a new empty cart
253
+ try {
254
+ const data = await graphqlFetch(queries.CREATE_EMPTY_CART);
255
+ cartId = data.createEmptyCart;
256
+ }
257
+ catch (error) {
258
+ // Magento blocks guest cart creation when login is required
259
+ const message = error instanceof Error ? error.message : String(error);
260
+ if (message.includes("allowed for logged in customer") ||
261
+ message.includes("requires authentication")) {
262
+ throw new Error("Guest checkout is not available. Please log in or create an account to continue shopping.");
263
+ }
264
+ throw error;
265
+ }
266
+ }
267
+ return {
268
+ id: cartId,
269
+ items: [],
270
+ totals: {
271
+ subtotal: 0,
272
+ subtotalIncludingTax: 0,
273
+ grandTotal: 0,
274
+ discounts: [],
275
+ taxes: [],
276
+ currency: "USD",
277
+ },
278
+ appliedCoupons: [],
279
+ shippingAddresses: [],
280
+ availableShippingMethods: [],
281
+ availablePaymentMethods: [],
282
+ };
283
+ },
284
+ get: async (cartId) => {
285
+ try {
286
+ const data = await graphqlFetch(queries.GET_CART, {
287
+ cartId,
288
+ }, await withCustomerAuth({ cache: "no-store" })); // Cart data is user-specific, never cache
289
+ return mapCartWithUrls(data.cart);
290
+ }
291
+ catch (error) {
292
+ return null;
293
+ }
294
+ },
295
+ addItem: async (cartId, input) => {
296
+ // Handle grouped products - add each child item separately
297
+ if (input.groupedItems && input.groupedItems.length > 0) {
298
+ const cartItems = input.groupedItems
299
+ .filter((item) => item.quantity > 0)
300
+ .map((item) => ({
301
+ sku: item.sku,
302
+ quantity: item.quantity,
303
+ }));
304
+ if (cartItems.length === 0) {
305
+ throw new Error("Please specify the quantity for at least one product");
306
+ }
307
+ const data = await graphqlFetch(queries.ADD_TO_CART, {
308
+ cartId,
309
+ cartItems,
310
+ }, await withCustomerAuth());
311
+ if (data.addProductsToCart.user_errors?.length) {
312
+ throw new Error(data.addProductsToCart.user_errors[0].message);
313
+ }
314
+ return mapCartWithUrls(data.addProductsToCart.cart);
315
+ }
316
+ // Build cart item input for simple/configurable products
317
+ const cartItem = {
318
+ sku: input.sku,
319
+ quantity: input.quantity,
320
+ };
321
+ // Add selected options for configurable products
322
+ if (input.selectedOptions && input.selectedOptions.length > 0) {
323
+ cartItem.selected_options = input.selectedOptions;
324
+ }
325
+ const data = await graphqlFetch(queries.ADD_TO_CART, {
326
+ cartId,
327
+ cartItems: [cartItem],
328
+ }, await withCustomerAuth());
329
+ if (data.addProductsToCart.user_errors?.length) {
330
+ throw makeCartUserError(data.addProductsToCart.user_errors[0]);
331
+ }
332
+ return mapCartWithUrls(data.addProductsToCart.cart);
333
+ },
334
+ updateItem: async (cartId, input) => {
335
+ const data = await graphqlFetch(queries.UPDATE_CART_ITEM, {
336
+ cartId,
337
+ itemUid: input.itemUid,
338
+ quantity: input.quantity,
339
+ }, await withCustomerAuth());
340
+ return mapCartWithUrls(data.updateCartItems.cart);
341
+ },
342
+ removeItem: async (cartId, itemUid) => {
343
+ const data = await graphqlFetch(queries.REMOVE_CART_ITEM, { cartId, itemUid }, await withCustomerAuth());
344
+ return mapCartWithUrls(data.removeItemFromCart.cart);
345
+ },
346
+ applyCoupon: async (cartId, input) => {
347
+ const authOpts = await withCustomerAuth();
348
+ const data = await graphqlFetch(`mutation ApplyCoupon($cartId: String!, $couponCode: String!) {
349
+ applyCouponToCart(input: { cart_id: $cartId, coupon_code: $couponCode }) {
350
+ cart { id applied_coupons { code } }
351
+ }
352
+ }`, { cartId, couponCode: input.couponCode }, authOpts);
353
+ // Re-fetch full cart to get updated totals
354
+ const fullCart = await graphqlFetch(queries.GET_CART, {
355
+ cartId,
356
+ }, await withCustomerAuth({ cache: "no-store" }));
357
+ return mapCartWithUrls(fullCart.cart);
358
+ },
359
+ removeCoupon: async (cartId) => {
360
+ const authOpts = await withCustomerAuth();
361
+ await graphqlFetch(`mutation RemoveCoupon($cartId: String!) {
362
+ removeCouponFromCart(input: { cart_id: $cartId }) {
363
+ cart { id }
364
+ }
365
+ }`, { cartId }, authOpts);
366
+ const fullCart = await graphqlFetch(queries.GET_CART, {
367
+ cartId,
368
+ }, await withCustomerAuth({ cache: "no-store" }));
369
+ return mapCartWithUrls(fullCart.cart);
370
+ },
371
+ setEmail: async (cartId, email) => {
372
+ const authOpts = await withCustomerAuth();
373
+ // setGuestEmailOnCart is only for guests; for logged-in customers
374
+ // the email is already associated with their account.
375
+ if (!authOpts.customerToken) {
376
+ await graphqlFetch(`mutation SetGuestEmail($cartId: String!, $email: String!) {
377
+ setGuestEmailOnCart(input: { cart_id: $cartId, email: $email }) {
378
+ cart { id email }
379
+ }
380
+ }`, { cartId, email }, authOpts);
381
+ }
382
+ const fullCart = await graphqlFetch(queries.GET_CART, {
383
+ cartId,
384
+ }, await withCustomerAuth({ cache: "no-store" }));
385
+ return mapCartWithUrls(fullCart.cart);
386
+ },
387
+ merge: async (guestCartId, customerCartId) => {
388
+ const authOpts = await withCustomerAuth();
389
+ await graphqlFetch(queries.MERGE_CARTS, { guestCartId, customerCartId }, authOpts);
390
+ const fullCart = await graphqlFetch(queries.GET_CART, {
391
+ cartId: customerCartId,
392
+ }, await withCustomerAuth({ cache: "no-store" }));
393
+ return mapCartWithUrls(fullCart.cart);
394
+ },
395
+ },
396
+ checkout: {
397
+ setShippingAddress: async (cartId, input) => {
398
+ const authOpts = await withCustomerAuth();
399
+ await graphqlFetch(queries.SET_SHIPPING_ADDRESS, {
400
+ cartId,
401
+ address: {
402
+ address: {
403
+ firstname: input.address.firstname,
404
+ lastname: input.address.lastname,
405
+ street: input.address.street,
406
+ city: input.address.city,
407
+ // Magento requires region_id for countries that have predefined
408
+ // regions; `region` (free text) is used when there is no id.
409
+ region: input.address.regionCode || input.address.region,
410
+ region_id: input.address.regionId,
411
+ postcode: input.address.postcode,
412
+ country_code: input.address.countryCode,
413
+ telephone: input.address.telephone,
414
+ company: input.address.company,
415
+ // Don't add a new entry to the customer's address book on every
416
+ // checkout (Magento otherwise saves it, creating duplicates).
417
+ save_in_address_book: false,
418
+ },
419
+ },
420
+ }, authOpts);
421
+ const fullCart = await graphqlFetch(queries.GET_CART, {
422
+ cartId,
423
+ }, await withCustomerAuth({ cache: "no-store" }));
424
+ return mapCartWithUrls(fullCart.cart);
425
+ },
426
+ setBillingAddress: async (cartId, input) => {
427
+ const authOpts = await withCustomerAuth();
428
+ await graphqlFetch(`mutation SetBillingAddress($cartId: String!, $address: BillingAddressInput!) {
429
+ setBillingAddressOnCart(input: { cart_id: $cartId, billing_address: $address }) {
430
+ cart { id }
431
+ }
432
+ }`, {
433
+ cartId,
434
+ address: input.sameAsShipping
435
+ ? { same_as_shipping: true }
436
+ : {
437
+ address: {
438
+ firstname: input.address.firstname,
439
+ lastname: input.address.lastname,
440
+ street: input.address.street,
441
+ city: input.address.city,
442
+ region: input.address.regionCode || input.address.region,
443
+ region_id: input.address.regionId,
444
+ postcode: input.address.postcode,
445
+ country_code: input.address.countryCode,
446
+ telephone: input.address.telephone,
447
+ save_in_address_book: false,
448
+ },
449
+ },
450
+ }, authOpts);
451
+ const fullCart = await graphqlFetch(queries.GET_CART, {
452
+ cartId,
453
+ }, await withCustomerAuth({ cache: "no-store" }));
454
+ return mapCartWithUrls(fullCart.cart);
455
+ },
456
+ setShippingMethod: async (cartId, input) => {
457
+ const authOpts = await withCustomerAuth();
458
+ await graphqlFetch(queries.SET_SHIPPING_METHOD, {
459
+ cartId,
460
+ carrierCode: input.carrierCode,
461
+ methodCode: input.methodCode,
462
+ }, authOpts);
463
+ const fullCart = await graphqlFetch(queries.GET_CART, {
464
+ cartId,
465
+ }, await withCustomerAuth({ cache: "no-store" }));
466
+ return mapCartWithUrls(fullCart.cart);
467
+ },
468
+ setPaymentMethod: async (cartId, input) => {
469
+ const authOpts = await withCustomerAuth();
470
+ await graphqlFetch(queries.SET_PAYMENT_METHOD, {
471
+ cartId,
472
+ code: input.code,
473
+ }, authOpts);
474
+ const fullCart = await graphqlFetch(queries.GET_CART, {
475
+ cartId,
476
+ }, await withCustomerAuth({ cache: "no-store" }));
477
+ return mapCartWithUrls(fullCart.cart);
478
+ },
479
+ placeOrder: async (cartId) => {
480
+ try {
481
+ const data = await graphqlFetch(queries.PLACE_ORDER, { cartId }, await withCustomerAuth());
482
+ return {
483
+ success: true,
484
+ orderNumber: data.placeOrder.order.order_number,
485
+ orderId: data.placeOrder.order.order_number,
486
+ };
487
+ }
488
+ catch (error) {
489
+ return {
490
+ success: false,
491
+ error: error instanceof Error ? error.message : "Order placement failed",
492
+ };
493
+ }
494
+ },
495
+ getCountries: async () => {
496
+ const data = await graphqlFetch(queries.GET_COUNTRIES);
497
+ return mappers.mapCountries(data.countries ?? []);
498
+ },
499
+ },
500
+ cms: {
501
+ getPage: async (identifier) => {
502
+ try {
503
+ const data = await graphqlFetch(queries.GET_CMS_PAGE, {
504
+ identifier,
505
+ }, { tags: [CONTENT_TAGS.cms] });
506
+ return data.cmsPage ? mappers.mapCmsPage(data.cmsPage) : null;
507
+ }
508
+ catch {
509
+ return null;
510
+ }
511
+ },
512
+ getPageByUrlKey: async (urlKey) => {
513
+ try {
514
+ const data = await graphqlFetch(queries.GET_CMS_PAGE, {
515
+ identifier: urlKey,
516
+ }, { tags: [CONTENT_TAGS.cms] });
517
+ return data.cmsPage ? mappers.mapCmsPage(data.cmsPage) : null;
518
+ }
519
+ catch {
520
+ return null;
521
+ }
522
+ },
523
+ getBlock: async (identifier) => {
524
+ try {
525
+ const data = await graphqlFetch(queries.GET_CMS_BLOCKS, { identifiers: [identifier] }, { tags: [CONTENT_TAGS.cms] });
526
+ const item = data.cmsBlocks?.items?.[0];
527
+ return item ? mappers.mapCmsBlock(item) : null;
528
+ }
529
+ catch {
530
+ return null;
531
+ }
532
+ },
533
+ resolveUrl: async (urlKey) => {
534
+ try {
535
+ const data = await graphqlFetch(queries.URL_RESOLVER, {
536
+ url: urlKey,
537
+ });
538
+ return mappers.mapUrlResolver(data.route);
539
+ }
540
+ catch {
541
+ return { type: "NOT_FOUND", id: "", urlKey };
542
+ }
543
+ },
544
+ getStoreConfig: async () => {
545
+ const config = await getStoreConfig();
546
+ return {
547
+ storeCode: config.storeCode,
548
+ storeName: config.storeName,
549
+ baseCurrencyCode: config.baseCurrencyCode,
550
+ defaultDisplayCurrencyCode: config.defaultDisplayCurrencyCode,
551
+ locale: config.locale,
552
+ timezone: config.timezone,
553
+ useStoreInUrl: config.useStoreInUrl,
554
+ homePageIdentifier: config.homePageIdentifier,
555
+ notFoundPageIdentifier: config.notFoundPageIdentifier,
556
+ defaultTitle: config.defaultTitle,
557
+ defaultDescription: config.defaultDescription,
558
+ defaultKeywords: config.defaultKeywords,
559
+ titlePrefix: config.titlePrefix,
560
+ titleSuffix: config.titleSuffix,
561
+ titleSeparator: config.titleSeparator,
562
+ };
563
+ },
564
+ getStores: async () => getStores(),
565
+ },
566
+ search: {
567
+ search: async (query, options) => {
568
+ const { productUrlSuffix } = await getStoreConfig();
569
+ const data = await graphqlFetch(queries.SEARCH_PRODUCTS, {
570
+ search: query,
571
+ pageSize: options?.pageSize ?? 12,
572
+ currentPage: options?.page ?? 1,
573
+ }, { tags: [CONTENT_TAGS.search] });
574
+ return {
575
+ items: data.products.items.map((p) => mappers.mapProductListItem(p, productUrlSuffix)),
576
+ totalCount: data.products.total_count,
577
+ pageInfo: {
578
+ currentPage: data.products.page_info.current_page,
579
+ pageSize: data.products.page_info.page_size,
580
+ totalPages: data.products.page_info.total_pages,
581
+ },
582
+ };
583
+ },
584
+ },
585
+ customer: {
586
+ login: async (credentials) => {
587
+ const tokenData = await graphqlFetch(queries.GENERATE_CUSTOMER_TOKEN, {
588
+ email: credentials.email,
589
+ password: credentials.password,
590
+ });
591
+ const token = tokenData.generateCustomerToken.token;
592
+ const customerData = await graphqlFetch(queries.GET_CUSTOMER, {}, { customerToken: token });
593
+ return {
594
+ customer: mappers.mapCustomer(customerData.customer),
595
+ token,
596
+ expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour
597
+ };
598
+ },
599
+ register: async (input) => {
600
+ const data = await graphqlFetch(queries.CREATE_CUSTOMER, {
601
+ input: {
602
+ email: input.email,
603
+ password: input.password,
604
+ firstname: input.firstname,
605
+ lastname: input.lastname,
606
+ date_of_birth: input.dateOfBirth,
607
+ is_subscribed: input.isSubscribed,
608
+ },
609
+ });
610
+ // Auto-login after registration
611
+ const tokenData = await graphqlFetch(queries.GENERATE_CUSTOMER_TOKEN, {
612
+ email: input.email,
613
+ password: input.password,
614
+ });
615
+ return {
616
+ customer: mappers.mapCustomer(data.createCustomerV2.customer),
617
+ token: tokenData.generateCustomerToken.token,
618
+ expiresAt: new Date(Date.now() + 3600000).toISOString(),
619
+ };
620
+ },
621
+ logout: async (token) => {
622
+ await graphqlFetch(`mutation RevokeCustomerToken { revokeCustomerToken { result } }`, {}, { customerToken: token });
623
+ },
624
+ get: async (token) => {
625
+ try {
626
+ const data = await graphqlFetch(queries.GET_CUSTOMER, {}, { customerToken: token });
627
+ return mappers.mapCustomer(data.customer);
628
+ }
629
+ catch {
630
+ return null;
631
+ }
632
+ },
633
+ update: async (token, input) => {
634
+ const data = await graphqlFetch(`mutation UpdateCustomer($input: CustomerUpdateInput!) {
635
+ updateCustomerV2(input: $input) {
636
+ customer {
637
+ id email firstname lastname date_of_birth gender created_at
638
+ }
639
+ }
640
+ }`, {
641
+ input: {
642
+ firstname: input.firstname,
643
+ lastname: input.lastname,
644
+ email: input.email,
645
+ date_of_birth: input.dateOfBirth,
646
+ },
647
+ }, { customerToken: token });
648
+ return mappers.mapCustomer(data.updateCustomerV2.customer);
649
+ },
650
+ changePassword: async (token, input) => {
651
+ await graphqlFetch(`mutation ChangePassword($currentPassword: String!, $newPassword: String!) {
652
+ changeCustomerPassword(currentPassword: $currentPassword, newPassword: $newPassword) {
653
+ email
654
+ }
655
+ }`, {
656
+ currentPassword: input.currentPassword,
657
+ newPassword: input.newPassword,
658
+ }, { customerToken: token });
659
+ return true;
660
+ },
661
+ requestPasswordReset: async (email) => {
662
+ await graphqlFetch(`mutation RequestPasswordReset($email: String!) {
663
+ requestPasswordResetEmail(email: $email)
664
+ }`, { email });
665
+ return true;
666
+ },
667
+ getOrders: async (token, page = 1, pageSize = 10) => {
668
+ const data = await graphqlFetch(queries.GET_CUSTOMER_ORDERS, { pageSize, currentPage: page }, { customerToken: token });
669
+ return {
670
+ items: data.customer.orders.items.map(mappers.mapCustomerOrder),
671
+ totalCount: data.customer.orders.total_count,
672
+ pageInfo: {
673
+ currentPage: data.customer.orders.page_info.current_page,
674
+ pageSize: data.customer.orders.page_info.page_size,
675
+ totalPages: data.customer.orders.page_info.total_pages,
676
+ },
677
+ };
678
+ },
679
+ createAddress: async (token, input) => {
680
+ const data = await graphqlFetch(queries.CREATE_CUSTOMER_ADDRESS, {
681
+ input: {
682
+ firstname: input.firstname,
683
+ lastname: input.lastname,
684
+ street: input.street,
685
+ city: input.city,
686
+ region: input.regionId != null || input.regionCode || input.region
687
+ ? {
688
+ region_id: input.regionId,
689
+ region_code: input.regionCode || undefined,
690
+ region: input.region || undefined,
691
+ }
692
+ : undefined,
693
+ postcode: input.postcode,
694
+ country_code: input.countryCode,
695
+ telephone: input.telephone,
696
+ company: input.company,
697
+ default_shipping: input.isDefaultShipping ?? false,
698
+ default_billing: input.isDefaultBilling ?? false,
699
+ },
700
+ }, { customerToken: token });
701
+ return mappers.mapCustomerAddress(data.createCustomerAddress);
702
+ },
703
+ updateAddress: async (token, input) => {
704
+ const { id, ...fields } = input;
705
+ const addressInput = {};
706
+ if (fields.firstname !== undefined)
707
+ addressInput.firstname = fields.firstname;
708
+ if (fields.lastname !== undefined)
709
+ addressInput.lastname = fields.lastname;
710
+ if (fields.street !== undefined)
711
+ addressInput.street = fields.street;
712
+ if (fields.city !== undefined)
713
+ addressInput.city = fields.city;
714
+ if (fields.regionId != null ||
715
+ fields.regionCode !== undefined ||
716
+ fields.region !== undefined)
717
+ addressInput.region = {
718
+ region_id: fields.regionId,
719
+ region_code: fields.regionCode || undefined,
720
+ region: fields.region || undefined,
721
+ };
722
+ if (fields.postcode !== undefined)
723
+ addressInput.postcode = fields.postcode;
724
+ if (fields.countryCode !== undefined)
725
+ addressInput.country_code = fields.countryCode;
726
+ if (fields.telephone !== undefined)
727
+ addressInput.telephone = fields.telephone;
728
+ if (fields.company !== undefined)
729
+ addressInput.company = fields.company;
730
+ if (fields.isDefaultShipping !== undefined)
731
+ addressInput.default_shipping = fields.isDefaultShipping;
732
+ if (fields.isDefaultBilling !== undefined)
733
+ addressInput.default_billing = fields.isDefaultBilling;
734
+ const data = await graphqlFetch(queries.UPDATE_CUSTOMER_ADDRESS, { id: parseInt(id, 10), input: addressInput }, { customerToken: token });
735
+ return mappers.mapCustomerAddress(data.updateCustomerAddress);
736
+ },
737
+ deleteAddress: async (token, addressId) => {
738
+ await graphqlFetch(queries.DELETE_CUSTOMER_ADDRESS, { id: parseInt(addressId, 10) }, { customerToken: token });
739
+ return true;
740
+ },
741
+ },
742
+ wishlist: {
743
+ get: async (token) => {
744
+ const data = await graphqlFetch(queries.GET_CUSTOMER_WISHLIST, {}, { customerToken: token });
745
+ // wishlists returns an array - take the first (default) wishlist
746
+ const wishlist = data.customer.wishlists[0];
747
+ if (!wishlist) {
748
+ // Return empty wishlist if none exists
749
+ return { id: "", items: [], itemsCount: 0 };
750
+ }
751
+ return mappers.mapWishlist(wishlist);
752
+ },
753
+ addItem: async (token, productSku) => {
754
+ // First get the wishlist ID
755
+ const wishlistData = await graphqlFetch(queries.GET_CUSTOMER_WISHLIST, {}, { customerToken: token });
756
+ const wishlist = wishlistData.customer.wishlists[0];
757
+ if (!wishlist) {
758
+ throw new Error("No wishlist found for customer");
759
+ }
760
+ const wishlistId = wishlist.id;
761
+ // Add product to wishlist
762
+ const data = await graphqlFetch(queries.ADD_TO_WISHLIST, {
763
+ wishlistId,
764
+ items: [{ sku: productSku, quantity: 1 }],
765
+ }, { customerToken: token });
766
+ if (data.addProductsToWishlist.user_errors?.length > 0) {
767
+ throw new Error(data.addProductsToWishlist.user_errors[0].message);
768
+ }
769
+ return mappers.mapWishlist(data.addProductsToWishlist.wishlist);
770
+ },
771
+ removeItem: async (token, itemId) => {
772
+ // First get the wishlist ID
773
+ const wishlistData = await graphqlFetch(queries.GET_CUSTOMER_WISHLIST, {}, { customerToken: token });
774
+ const wishlist = wishlistData.customer.wishlists[0];
775
+ if (!wishlist) {
776
+ throw new Error("No wishlist found for customer");
777
+ }
778
+ const wishlistId = wishlist.id;
779
+ // Remove product from wishlist
780
+ const data = await graphqlFetch(queries.REMOVE_FROM_WISHLIST, {
781
+ wishlistId,
782
+ itemIds: [itemId],
783
+ }, { customerToken: token });
784
+ if (data.removeProductsFromWishlist.user_errors?.length > 0) {
785
+ throw new Error(data.removeProductsFromWishlist.user_errors[0].message);
786
+ }
787
+ return mappers.mapWishlist(data.removeProductsFromWishlist.wishlist);
788
+ },
789
+ clear: async (token) => {
790
+ // Get wishlist with all items
791
+ const wishlistData = await graphqlFetch(queries.GET_CUSTOMER_WISHLIST, {}, { customerToken: token });
792
+ const wishlist = wishlistData.customer.wishlists[0];
793
+ if (!wishlist) {
794
+ return { id: "", items: [], itemsCount: 0 };
795
+ }
796
+ const wishlistId = wishlist.id;
797
+ const itemIds = (wishlist.items_v2?.items ?? [])
798
+ .filter((item) => item != null)
799
+ .map((item) => item.id);
800
+ if (itemIds.length === 0) {
801
+ return mappers.mapWishlist(wishlist);
802
+ }
803
+ // Remove all items
804
+ const data = await graphqlFetch(queries.REMOVE_FROM_WISHLIST, {
805
+ wishlistId,
806
+ itemIds,
807
+ }, { customerToken: token });
808
+ if (data.removeProductsFromWishlist.user_errors?.length > 0) {
809
+ throw new Error(data.removeProductsFromWishlist.user_errors[0].message);
810
+ }
811
+ return mappers.mapWishlist(data.removeProductsFromWishlist.wishlist);
812
+ },
813
+ },
814
+ };
815
+ export default magento2Adapter;