@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.
- package/LICENSE +21 -0
- package/dist/adapter.d.ts +3 -0
- package/dist/adapter.js +815 -0
- package/dist/attribute-labels.d.ts +4 -0
- package/dist/attribute-labels.js +56 -0
- package/dist/cart-errors.d.ts +10 -0
- package/dist/cart-errors.js +14 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +337 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.js +28 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/logger.d.ts +59 -0
- package/dist/logger.js +258 -0
- package/dist/magento-types.d.ts +357 -0
- package/dist/magento-types.js +11 -0
- package/dist/mappers.d.ts +50 -0
- package/dist/mappers.js +495 -0
- package/dist/queries.d.ts +40 -0
- package/dist/queries.js +1193 -0
- package/dist/store-config.d.ts +42 -0
- package/dist/store-config.js +59 -0
- package/dist/stores.d.ts +10 -0
- package/dist/stores.js +46 -0
- package/dist/urls.d.ts +32 -0
- package/dist/urls.js +44 -0
- package/package.json +36 -0
package/dist/adapter.js
ADDED
|
@@ -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;
|