@porulle/adapter-shopify 0.10.8 → 0.11.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.
- package/dist/index.js +110 -17
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/index.ts +141 -17
package/dist/index.js
CHANGED
|
@@ -7,11 +7,60 @@ export const REQUIRED_SCOPES = [
|
|
|
7
7
|
"write_orders",
|
|
8
8
|
"read_fulfillments",
|
|
9
9
|
];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const zeroDecimalCurrencies = new Set([
|
|
11
|
+
"BIF",
|
|
12
|
+
"CLP",
|
|
13
|
+
"DJF",
|
|
14
|
+
"GNF",
|
|
15
|
+
"ISK",
|
|
16
|
+
"JPY",
|
|
17
|
+
"KMF",
|
|
18
|
+
"KRW",
|
|
19
|
+
"PYG",
|
|
20
|
+
"RWF",
|
|
21
|
+
"UGX",
|
|
22
|
+
"VND",
|
|
23
|
+
"VUV",
|
|
24
|
+
"XAF",
|
|
25
|
+
"XOF",
|
|
26
|
+
"XPF",
|
|
27
|
+
]);
|
|
28
|
+
function normalizeCurrency(value) {
|
|
29
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
30
|
+
return undefined;
|
|
31
|
+
return value.trim().toUpperCase();
|
|
32
|
+
}
|
|
33
|
+
function parseMoney(value, currency) {
|
|
34
|
+
if (value == null || value.trim() === "")
|
|
35
|
+
return undefined;
|
|
36
|
+
const parsed = Number(value);
|
|
37
|
+
if (!Number.isFinite(parsed))
|
|
38
|
+
return undefined;
|
|
39
|
+
const exponent = zeroDecimalCurrencies.has(currency) ? 0 : 2;
|
|
40
|
+
return Math.round(parsed * (10 ** exponent));
|
|
41
|
+
}
|
|
42
|
+
function pricesForVariant(variant, currency) {
|
|
43
|
+
if (!currency)
|
|
44
|
+
return undefined;
|
|
45
|
+
const amount = parseMoney(variant.price, currency);
|
|
46
|
+
if (amount === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const compareAtAmount = parseMoney(variant.compare_at_price, currency);
|
|
49
|
+
return [{
|
|
50
|
+
currency,
|
|
51
|
+
amount,
|
|
52
|
+
...(compareAtAmount !== undefined && compareAtAmount !== amount ? { compareAtAmount } : {}),
|
|
53
|
+
}];
|
|
54
|
+
}
|
|
55
|
+
function catalogStatus(value) {
|
|
56
|
+
return value === "draft" || value === "active" || value === "archived" ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
function slugify(value) {
|
|
59
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
60
|
+
}
|
|
61
|
+
async function fetchShopCurrency(fetchImpl, url, accessToken) {
|
|
62
|
+
const result = await request(fetchImpl, url, accessToken);
|
|
63
|
+
return result.ok ? normalizeCurrency(result.value.data.shop?.currency) : undefined;
|
|
15
64
|
}
|
|
16
65
|
function apiBase(store, version) {
|
|
17
66
|
return `https://${store.storeDomain.replace(/^https?:\/\//, "").replace(/\/$/, "")}/admin/api/${version}`;
|
|
@@ -76,6 +125,7 @@ function oauthError(code, message) {
|
|
|
76
125
|
export function shopifyConnector(options = {}) {
|
|
77
126
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
78
127
|
const version = options.apiVersion ?? "2024-10";
|
|
128
|
+
const currencyCache = new Map();
|
|
79
129
|
return defineChannelConnector({
|
|
80
130
|
providerId: "shopify",
|
|
81
131
|
capabilities: { importCatalog: true, importInventory: true, pushOrder: true, receiveWebhooks: true },
|
|
@@ -135,6 +185,14 @@ export function shopifyConnector(options = {}) {
|
|
|
135
185
|
const token = credentials(store);
|
|
136
186
|
if (!token)
|
|
137
187
|
return Err({ code: "SHOPIFY_CREDENTIALS_REQUIRED", message: "Shopify accessToken is required." });
|
|
188
|
+
const currencyUrl = `${apiBase(store, version)}/shop.json`;
|
|
189
|
+
const currencyKey = apiBase(store, version);
|
|
190
|
+
let currencyPromise = currencyCache.get(currencyKey);
|
|
191
|
+
if (!currencyPromise) {
|
|
192
|
+
currencyPromise = fetchShopCurrency(fetchImpl, currencyUrl, token);
|
|
193
|
+
currencyCache.set(currencyKey, currencyPromise);
|
|
194
|
+
}
|
|
195
|
+
const currency = await currencyPromise;
|
|
138
196
|
const url = cursor ?? `${apiBase(store, version)}/products.json?limit=250`;
|
|
139
197
|
const result = await request(fetchImpl, url, token);
|
|
140
198
|
if (!result.ok)
|
|
@@ -142,18 +200,53 @@ export function shopifyConnector(options = {}) {
|
|
|
142
200
|
const link = result.value.response.headers.get("link") ?? "";
|
|
143
201
|
const next = link.match(/<([^>]+)>;\s*rel="next"/)?.[1] ?? null;
|
|
144
202
|
return Ok({
|
|
145
|
-
items: result.value.data.products.map((product) =>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
203
|
+
items: result.value.data.products.map((product) => {
|
|
204
|
+
const options = product.options?.map((option, index) => ({
|
|
205
|
+
name: option.name,
|
|
206
|
+
displayName: option.name,
|
|
207
|
+
...(option.position != null ? { sortOrder: option.position } : { sortOrder: index }),
|
|
208
|
+
values: (option.values ?? []).map((value, valueIndex) => ({ value, displayValue: value, sortOrder: valueIndex })),
|
|
209
|
+
}));
|
|
210
|
+
const variants = (product.variants ?? []).map((variant) => {
|
|
211
|
+
const selectors = [variant.option1, variant.option2, variant.option3];
|
|
212
|
+
const optionValues = Object.fromEntries((product.options ?? []).slice(0, 3).flatMap((option, index) => {
|
|
213
|
+
const value = selectors[index];
|
|
214
|
+
return value != null && value !== "" ? [[option.name, value]] : [];
|
|
215
|
+
}));
|
|
216
|
+
const prices = pricesForVariant(variant, currency);
|
|
217
|
+
return {
|
|
218
|
+
externalId: String(variant.id),
|
|
219
|
+
...(variant.sku ? { sku: variant.sku } : {}),
|
|
220
|
+
...(variant.barcode ? { barcode: variant.barcode } : {}),
|
|
221
|
+
...(Object.keys(optionValues).length > 0 ? { optionValues } : {}),
|
|
222
|
+
...(prices ? { prices } : {}),
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
const category = product.product_type ? slugify(product.product_type) : "";
|
|
226
|
+
const status = catalogStatus(product.status);
|
|
227
|
+
return {
|
|
228
|
+
externalId: String(product.id),
|
|
229
|
+
slug: product.handle ?? String(product.id),
|
|
230
|
+
title: product.title,
|
|
231
|
+
attributes: [{ locale: "en", title: product.title, ...(product.body_html != null ? { description: product.body_html } : {}) }],
|
|
232
|
+
variants,
|
|
233
|
+
...(product.images ? {
|
|
234
|
+
images: product.images.map((image, index) => ({
|
|
235
|
+
externalId: String(image.id),
|
|
236
|
+
url: image.src,
|
|
237
|
+
...(image.alt != null ? { alt: image.alt } : {}),
|
|
238
|
+
role: index === 0 ? "primary" : "gallery",
|
|
239
|
+
...(image.position != null ? { sortOrder: image.position } : {}),
|
|
240
|
+
...(image.variant_ids != null ? { variantExternalIds: image.variant_ids.map(String) } : {}),
|
|
241
|
+
})),
|
|
242
|
+
} : {}),
|
|
243
|
+
...(options ? { options } : {}),
|
|
244
|
+
...(product.tags != null ? { tags: product.tags.split(",").map((tag) => tag.trim()).filter(Boolean) } : {}),
|
|
245
|
+
...(product.vendor ? { brand: product.vendor } : {}),
|
|
246
|
+
...(category ? { categories: [category] } : {}),
|
|
247
|
+
...(status ? { status } : {}),
|
|
248
|
+
};
|
|
249
|
+
}),
|
|
157
250
|
nextCursor: next,
|
|
158
251
|
});
|
|
159
252
|
},
|