@tallyui/connector-vendure 1.0.0 → 3.0.0-next.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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +100 -28
- package/dist/index.js +661 -145
- package/package.json +13 -4
- package/src/auth.ts +79 -0
- package/src/capabilities.ts +44 -0
- package/src/global-settings.ts +18 -0
- package/src/index.ts +86 -47
- package/src/reconcile/ids.ts +38 -0
- package/src/reconcile/prices.ts +58 -0
- package/src/reconcile/stock.ts +48 -0
- package/src/replication/products.ts +162 -62
- package/src/replication/variant-feed.ts +108 -0
- package/src/schemas/products.ts +19 -1
- package/src/session-probe.ts +39 -0
- package/src/store-settings.ts +81 -0
- package/src/sync/products.ts +16 -9
- package/src/traits/product.ts +123 -23
- package/src/__tests__/product-traits.test.ts +0 -248
- package/src/replication/products.test.ts +0 -204
package/dist/index.js
CHANGED
|
@@ -1,6 +1,141 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { combinePullAdapters, createReconcileFeed } from "@tallyui/core";
|
|
3
|
+
|
|
4
|
+
// src/auth.ts
|
|
5
|
+
import { SignInError } from "@tallyui/core";
|
|
6
|
+
|
|
7
|
+
// src/capabilities.ts
|
|
8
|
+
import { ConnectorUnauthorizedError as ConnectorUnauthorizedError2, parseInfoCapabilities } from "@tallyui/core";
|
|
9
|
+
|
|
10
|
+
// src/session-probe.ts
|
|
11
|
+
import { ConnectorUnauthorizedError } from "@tallyui/core";
|
|
12
|
+
async function signedOut(context) {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`${context.baseUrl}/admin-api`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json", ...context.headers },
|
|
17
|
+
body: JSON.stringify({ query: "query { activeAdministrator { id } }" }),
|
|
18
|
+
signal: context.signal
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok) return void 0;
|
|
21
|
+
const body = await res.json();
|
|
22
|
+
if (body.errors?.length) return void 0;
|
|
23
|
+
if (body.data?.activeAdministrator === null) return true;
|
|
24
|
+
return body.data?.activeAdministrator?.id != null ? false : void 0;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (context.signal?.aborted) throw error;
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function forbiddenError(context, forbidden) {
|
|
31
|
+
const out = await signedOut(context);
|
|
32
|
+
if (out === true) return new ConnectorUnauthorizedError(`Vendure GraphQL error: ${forbidden[0].message}`);
|
|
33
|
+
const what = forbidden.map(({ message, path }) => path?.length ? `${path.join(".")}: ${message}` : message).join("; ");
|
|
34
|
+
if (out === false) return new Error(`Vendure GraphQL error: FORBIDDEN: the store refused this request although the session is signed in (a permission is missing): ${what}`);
|
|
35
|
+
return new Error(`Vendure GraphQL error: FORBIDDEN: ${what} (the session check failed, so this may be transient)`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/capabilities.ts
|
|
39
|
+
var CAPABILITIES_PATH = "/tally/v1/info";
|
|
40
|
+
async function readVendureCapabilities(baseUrl, headers, init = {}) {
|
|
41
|
+
const doFetch = init.fetch ?? fetch;
|
|
42
|
+
let res;
|
|
43
|
+
try {
|
|
44
|
+
res = await doFetch(`${baseUrl}${CAPABILITIES_PATH}`, { method: "GET", headers, signal: init.signal });
|
|
45
|
+
} catch {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
if (res.status === 401 || res.status === 403) {
|
|
49
|
+
const out = await signedOut({ connectorId: "vendure", baseUrl, headers, signal: init.signal });
|
|
50
|
+
if (out === true) throw new ConnectorUnauthorizedError2(`Vendure refused ${CAPABILITIES_PATH}: the session is signed out`, res.status);
|
|
51
|
+
if (out === false) throw new Error(`Vendure refused ${CAPABILITIES_PATH} although the session is signed in: the CreateOrder permission is missing (HTTP ${res.status})`);
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
if (res.status === 404) return { orderCreate: 1 };
|
|
55
|
+
if (!res.ok) return void 0;
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = await res.json();
|
|
59
|
+
} catch {
|
|
60
|
+
return { orderCreate: 1 };
|
|
61
|
+
}
|
|
62
|
+
return parseInfoCapabilities(body, (reason) => console.warn(`@tallyui/connector-vendure: ${reason}`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/auth.ts
|
|
66
|
+
var LOGIN = `mutation Login($email: String!, $password: String!) {
|
|
67
|
+
login(username: $email, password: $password) {
|
|
68
|
+
__typename
|
|
69
|
+
... on CurrentUser { id }
|
|
70
|
+
... on ErrorResult { errorCode message }
|
|
71
|
+
}
|
|
72
|
+
}`;
|
|
73
|
+
var vendureSignIn = async (baseUrl, { email, password }, init = {}) => {
|
|
74
|
+
const doFetch = init.fetch ?? fetch;
|
|
75
|
+
let res;
|
|
76
|
+
try {
|
|
77
|
+
res = await doFetch(`${baseUrl}/admin-api`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
signal: init.signal,
|
|
81
|
+
body: JSON.stringify({ query: LOGIN, variables: { email, password } })
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error?.name === "AbortError" || init.signal?.aborted) throw error;
|
|
85
|
+
throw new SignInError("failed", `Could not reach Vendure at ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
body = await res.json();
|
|
90
|
+
} catch {
|
|
91
|
+
throw new SignInError("server_error", `Vendure sent an unreadable response (HTTP ${res.status})`, res.status);
|
|
92
|
+
}
|
|
93
|
+
if (!res.ok) throw new SignInError("server_error", body?.errors?.[0]?.message ?? `Vendure sign-in failed (HTTP ${res.status})`, res.status);
|
|
94
|
+
const login = body?.data?.login;
|
|
95
|
+
if (body?.errors?.length || !login) {
|
|
96
|
+
throw new SignInError("server_error", body?.errors?.[0]?.message ?? `Vendure sign-in failed (HTTP ${res.status})`, res.status);
|
|
97
|
+
}
|
|
98
|
+
if (login.errorCode === "INVALID_CREDENTIALS_ERROR") throw new SignInError("invalid_credentials", login.message ?? "Invalid email or password");
|
|
99
|
+
if (login.errorCode === "NATIVE_AUTH_STRATEGY_ERROR") throw new SignInError("unsupported", login.message ?? "Native email and password sign-in is disabled on this Vendure server");
|
|
100
|
+
if (login.__typename !== "CurrentUser") throw new SignInError("server_error", login.message ?? `Vendure sign-in failed (${login.__typename})`, res.status);
|
|
101
|
+
const token = res.headers.get("vendure-auth-token");
|
|
102
|
+
if (!token) {
|
|
103
|
+
throw new SignInError("unsupported", "Vendure signed in but sent no 'vendure-auth-token' header. Add 'bearer' to the server's authOptions.tokenMethod. This connector reads the default header name, so a server that renames authOptions.authTokenHeaderKey also gives this error.");
|
|
104
|
+
}
|
|
105
|
+
const capabilities = await readVendureCapabilities(baseUrl, { Authorization: `Bearer ${token}` }, { fetch: doFetch, signal: init.signal }).catch(() => void 0);
|
|
106
|
+
return { token, expiresAt: void 0, capabilities };
|
|
107
|
+
};
|
|
108
|
+
var warnedAuthToken = false;
|
|
109
|
+
var vendureAuth = {
|
|
110
|
+
type: "Vendure Admin API",
|
|
111
|
+
fields: [
|
|
112
|
+
{ key: "url", label: "Backend URL", type: "url", placeholder: "https://my-vendure-server.com", required: true },
|
|
113
|
+
{ key: "email", label: "Email", type: "text", required: true },
|
|
114
|
+
{ key: "password", label: "Password", type: "password", required: true },
|
|
115
|
+
{ key: "channel_token", label: "Channel token (optional)", type: "text" }
|
|
116
|
+
],
|
|
117
|
+
// An API key (Vendure 3.6+) wins over a signed-in session token; channel_token selects a non-default channel.
|
|
118
|
+
// auth_token is the deprecated name for token, kept so existing apps keep working.
|
|
119
|
+
getHeaders: (credentials) => {
|
|
120
|
+
if (credentials.token === void 0 && credentials.auth_token !== void 0 && !warnedAuthToken) {
|
|
121
|
+
warnedAuthToken = true;
|
|
122
|
+
console.warn("@tallyui/connector-vendure: the 'auth_token' credential is deprecated; store the credential as 'token' instead.");
|
|
123
|
+
}
|
|
124
|
+
const token = credentials.token ?? credentials.auth_token;
|
|
125
|
+
return {
|
|
126
|
+
...credentials.api_key ? { "vendure-api-key": credentials.api_key } : token ? { Authorization: `Bearer ${token}` } : {},
|
|
127
|
+
...credentials.channel_token ? { "vendure-token": credentials.channel_token } : {}
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
signIn: vendureSignIn
|
|
131
|
+
};
|
|
132
|
+
|
|
1
133
|
// src/schemas/products.ts
|
|
2
134
|
var vendureProductSchema = {
|
|
3
|
-
|
|
135
|
+
// 1: declares variant trackInventory/outOfStockThreshold/useGlobalOutOfStockThreshold/enabled
|
|
136
|
+
// (backlog 28). 2: declares variant taxCategory { id } (#288). A bump drops and resyncs
|
|
137
|
+
// (ConnectorSchemas, ADR-060 amendment 9).
|
|
138
|
+
version: 2,
|
|
4
139
|
primaryKey: "id",
|
|
5
140
|
type: "object",
|
|
6
141
|
properties: {
|
|
@@ -68,7 +203,21 @@ var vendureProductSchema = {
|
|
|
68
203
|
currencyCode: { type: "string" },
|
|
69
204
|
stockLevel: { type: "string" },
|
|
70
205
|
stockOnHand: { type: "number" },
|
|
206
|
+
stockLevels: {
|
|
207
|
+
type: "array",
|
|
208
|
+
items: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
stockLocationId: { type: "string" },
|
|
212
|
+
stockOnHand: { type: "number" },
|
|
213
|
+
stockAllocated: { type: "number" }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
71
217
|
trackInventory: { type: "string" },
|
|
218
|
+
outOfStockThreshold: { type: "number" },
|
|
219
|
+
useGlobalOutOfStockThreshold: { type: "boolean" },
|
|
220
|
+
enabled: { type: "boolean" },
|
|
72
221
|
featuredAsset: {
|
|
73
222
|
type: ["object", "null"],
|
|
74
223
|
properties: {
|
|
@@ -87,7 +236,8 @@ var vendureProductSchema = {
|
|
|
87
236
|
}
|
|
88
237
|
}
|
|
89
238
|
},
|
|
90
|
-
customFields: { type: "object" }
|
|
239
|
+
customFields: { type: "object" },
|
|
240
|
+
taxCategory: { type: ["object", "null"], properties: { id: { type: "string" } } }
|
|
91
241
|
}
|
|
92
242
|
}
|
|
93
243
|
},
|
|
@@ -98,55 +248,109 @@ var vendureProductSchema = {
|
|
|
98
248
|
};
|
|
99
249
|
|
|
100
250
|
// src/traits/product.ts
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
251
|
+
function productStock(variants2) {
|
|
252
|
+
if (!variants2.length) return { status: "unknown" };
|
|
253
|
+
const status = variants2.some((stock) => stock.status === "in_stock") ? "in_stock" : variants2.some((stock) => stock.status === "backorder") ? "backorder" : variants2.every((stock) => stock.status === "out_of_stock") ? "out_of_stock" : "unknown";
|
|
254
|
+
const quantity = variants2.every((stock) => stock.quantity != null) ? variants2.reduce((sum, stock) => sum + stock.quantity, 0) : void 0;
|
|
255
|
+
return quantity === void 0 ? { status } : { status, quantity };
|
|
256
|
+
}
|
|
257
|
+
function variantStock(variant, stockLocationId, globalTrackInventory, globalOutOfStockThreshold) {
|
|
258
|
+
if (!variant) return { status: "unknown" };
|
|
259
|
+
const tracked = variant.trackInventory === "TRUE" || variant.trackInventory !== "FALSE" && globalTrackInventory;
|
|
260
|
+
if (!tracked) return { status: "in_stock" };
|
|
261
|
+
const threshold = variant.useGlobalOutOfStockThreshold !== false ? globalOutOfStockThreshold : variant.outOfStockThreshold ?? 0;
|
|
262
|
+
let onHandLessAllocated;
|
|
263
|
+
if (variant.stockLevels != null) {
|
|
264
|
+
onHandLessAllocated = variant.stockLevels.filter((level) => stockLocationId == null || String(level.stockLocationId) === stockLocationId).reduce((sum, level) => sum + level.stockOnHand - level.stockAllocated, 0);
|
|
265
|
+
} else if (variant.stockOnHand != null) {
|
|
266
|
+
onHandLessAllocated = variant.stockOnHand;
|
|
267
|
+
}
|
|
268
|
+
if (onHandLessAllocated != null) {
|
|
269
|
+
const saleable = onHandLessAllocated - threshold;
|
|
270
|
+
const status = saleable <= 0 ? "out_of_stock" : onHandLessAllocated > 0 ? "in_stock" : "backorder";
|
|
271
|
+
return { status, quantity: Math.max(saleable, 0) };
|
|
272
|
+
}
|
|
273
|
+
if (variant.stockLevel === "IN_STOCK" || variant.stockLevel === "LOW_STOCK") return { status: "in_stock" };
|
|
274
|
+
if (variant.stockLevel === "OUT_OF_STOCK") return { status: "out_of_stock" };
|
|
275
|
+
return { status: "unknown" };
|
|
276
|
+
}
|
|
277
|
+
function liveVariants(doc) {
|
|
278
|
+
return (doc.variants ?? []).filter((variant) => variant.enabled !== false);
|
|
279
|
+
}
|
|
280
|
+
function createVendureProductTraits(barcodeField, stockLocationId, pricesIncludeTax = false, globalTrackInventory = true, globalOutOfStockThreshold = 0) {
|
|
281
|
+
const priceField = pricesIncludeTax ? "priceWithTax" : "price";
|
|
282
|
+
const traits = {
|
|
283
|
+
getId: (doc) => String(doc.id),
|
|
284
|
+
getName: (doc) => doc.name ?? "",
|
|
285
|
+
getSku: (doc) => doc.variants?.[0]?.sku || void 0,
|
|
286
|
+
getPrices: (doc, context) => {
|
|
287
|
+
const variant = liveVariants(doc)[0];
|
|
288
|
+
const amount = variant?.[priceField];
|
|
289
|
+
if (amount == null) return [];
|
|
290
|
+
const currency = String(variant.currencyCode ?? context?.currency ?? "XXX").toUpperCase();
|
|
291
|
+
return [{ amount, currency, kind: "base" }];
|
|
292
|
+
},
|
|
293
|
+
getVariants: (doc, context) => liveVariants(doc).map((variant) => ({
|
|
294
|
+
id: String(variant.id),
|
|
295
|
+
title: variant.name || void 0,
|
|
296
|
+
sku: variant.sku || void 0,
|
|
297
|
+
barcode: barcodeField ? variant.customFields?.[barcodeField] || void 0 : void 0,
|
|
298
|
+
prices: traits.getPrices({ variants: [variant] }, context),
|
|
299
|
+
stock: traits.getStock({ variants: [variant] })
|
|
300
|
+
})),
|
|
301
|
+
// The variant's tax category id: store-settings.ts keys taxRatesPpm by the same id. No variant id: the default
|
|
302
|
+
// (first live) variant, as getPrices.
|
|
303
|
+
getTaxClass: (doc, variantId) => {
|
|
304
|
+
const variant = variantId === void 0 ? liveVariants(doc)[0] : (doc.variants ?? []).find((v) => String(v.id) === variantId);
|
|
305
|
+
return variant?.taxCategory?.id == null ? void 0 : String(variant.taxCategory.id);
|
|
306
|
+
},
|
|
307
|
+
getStock: (doc) => productStock(liveVariants(doc).map((variant) => variantStock(variant, stockLocationId, globalTrackInventory, globalOutOfStockThreshold))),
|
|
308
|
+
getPrice: (doc) => {
|
|
309
|
+
const amount = liveVariants(doc)[0]?.[priceField];
|
|
310
|
+
if (amount == null) return void 0;
|
|
311
|
+
return (amount / 100).toFixed(2);
|
|
312
|
+
},
|
|
313
|
+
getRegularPrice: (doc) => {
|
|
314
|
+
const amount = liveVariants(doc)[0]?.[priceField];
|
|
315
|
+
if (amount == null) return void 0;
|
|
316
|
+
return (amount / 100).toFixed(2);
|
|
317
|
+
},
|
|
318
|
+
getSalePrice: () => {
|
|
319
|
+
return void 0;
|
|
320
|
+
},
|
|
321
|
+
isOnSale: () => {
|
|
322
|
+
return false;
|
|
323
|
+
},
|
|
324
|
+
getImageUrl: (doc) => doc.featuredAsset?.preview || doc.assets?.[0]?.preview || void 0,
|
|
325
|
+
getImageUrls: (doc) => (doc.assets ?? []).map((a) => a.preview).filter(Boolean),
|
|
326
|
+
getDescription: (doc) => doc.description || void 0,
|
|
327
|
+
// Aggregated across every variant, like getStock (backlog 28), not variant 0.
|
|
328
|
+
getStockStatus: (doc) => {
|
|
329
|
+
const status = traits.getStock(doc).status;
|
|
330
|
+
if (status === "in_stock" || status === "backorder") return "instock";
|
|
331
|
+
if (status === "out_of_stock") return "outofstock";
|
|
332
|
+
return "unknown";
|
|
333
|
+
},
|
|
334
|
+
getStockQuantity: (doc) => traits.getStock(doc).quantity ?? null,
|
|
335
|
+
hasVariants: (doc) => (doc.variants?.length ?? 0) > 1,
|
|
336
|
+
// A product with no variants at all keeps the product-level-only answer.
|
|
337
|
+
isSellable: (doc) => doc.enabled !== false && ((doc.variants?.length ?? 0) === 0 || liveVariants(doc).length > 0),
|
|
338
|
+
getVariantCount: (doc) => doc.variants ? liveVariants(doc).length : 1,
|
|
339
|
+
getType: (doc) => {
|
|
340
|
+
if ((doc.variants?.length ?? 0) > 1) return "variable";
|
|
341
|
+
return "simple";
|
|
342
|
+
},
|
|
343
|
+
getBarcode: (doc) => {
|
|
344
|
+
return barcodeField ? liveVariants(doc)[0]?.customFields?.[barcodeField] || void 0 : void 0;
|
|
345
|
+
},
|
|
346
|
+
getCategoryNames: (doc) => (doc.collections ?? []).map((c) => c.name).filter(Boolean)
|
|
347
|
+
};
|
|
348
|
+
return traits;
|
|
349
|
+
}
|
|
350
|
+
var vendureProductTraits = createVendureProductTraits();
|
|
148
351
|
|
|
149
352
|
// src/sync/products.ts
|
|
353
|
+
import { ConnectorUnauthorizedError as ConnectorUnauthorizedError3 } from "@tallyui/core";
|
|
150
354
|
var PRODUCT_LIST_QUERY = `
|
|
151
355
|
query GetProducts($options: ProductListOptions) {
|
|
152
356
|
products(options: $options) {
|
|
@@ -158,7 +362,7 @@ var PRODUCT_LIST_QUERY = `
|
|
|
158
362
|
}
|
|
159
363
|
}
|
|
160
364
|
`;
|
|
161
|
-
var PRODUCT_DETAIL_QUERY = `
|
|
365
|
+
var PRODUCT_DETAIL_QUERY = (barcodeField) => `
|
|
162
366
|
query GetProduct($id: ID!) {
|
|
163
367
|
product(id: $id) {
|
|
164
368
|
id
|
|
@@ -179,17 +383,16 @@ var PRODUCT_DETAIL_QUERY = `
|
|
|
179
383
|
price
|
|
180
384
|
priceWithTax
|
|
181
385
|
currencyCode
|
|
182
|
-
|
|
183
|
-
stockOnHand
|
|
386
|
+
stockLevels { stockLocationId stockOnHand stockAllocated }
|
|
184
387
|
trackInventory
|
|
185
388
|
featuredAsset { id preview }
|
|
186
389
|
options { id name code }
|
|
187
|
-
customFields
|
|
390
|
+
${barcodeField ? `customFields { ${barcodeField} }` : ""}
|
|
188
391
|
}
|
|
189
392
|
}
|
|
190
393
|
}
|
|
191
394
|
`;
|
|
192
|
-
var
|
|
395
|
+
var createVendureProductSync = (barcodeField) => ({
|
|
193
396
|
fetchAllIds: async (context) => {
|
|
194
397
|
const entries = [];
|
|
195
398
|
const take = 100;
|
|
@@ -215,7 +418,7 @@ var vendureProductSync = {
|
|
|
215
418
|
fetchByIds: async (ids, context) => {
|
|
216
419
|
const products = [];
|
|
217
420
|
for (const id of ids) {
|
|
218
|
-
const res = await gql(context, PRODUCT_DETAIL_QUERY, { id });
|
|
421
|
+
const res = await gql(context, PRODUCT_DETAIL_QUERY(barcodeField), { id });
|
|
219
422
|
if (res.data?.product) {
|
|
220
423
|
products.push(res.data.product);
|
|
221
424
|
}
|
|
@@ -241,7 +444,7 @@ var vendureProductSync = {
|
|
|
241
444
|
if (!data) break;
|
|
242
445
|
totalItems = data.totalItems;
|
|
243
446
|
for (const item of data.items ?? []) {
|
|
244
|
-
const detail = await gql(context, PRODUCT_DETAIL_QUERY, { id: item.id });
|
|
447
|
+
const detail = await gql(context, PRODUCT_DETAIL_QUERY(barcodeField), { id: item.id });
|
|
245
448
|
if (detail.data?.product) {
|
|
246
449
|
products.push(detail.data.product);
|
|
247
450
|
}
|
|
@@ -250,23 +453,29 @@ var vendureProductSync = {
|
|
|
250
453
|
}
|
|
251
454
|
return products;
|
|
252
455
|
}
|
|
253
|
-
};
|
|
254
|
-
|
|
456
|
+
});
|
|
457
|
+
var vendureProductSync = createVendureProductSync();
|
|
458
|
+
async function gql(context, query2, variables) {
|
|
255
459
|
const res = await fetch(`${context.baseUrl}/admin-api`, {
|
|
256
460
|
method: "POST",
|
|
257
461
|
headers: {
|
|
258
462
|
"Content-Type": "application/json",
|
|
259
463
|
...context.headers
|
|
260
464
|
},
|
|
261
|
-
body: JSON.stringify({ query, variables }),
|
|
465
|
+
body: JSON.stringify({ query: query2, variables }),
|
|
262
466
|
signal: context.signal
|
|
263
467
|
});
|
|
468
|
+
if (res.status === 401 || res.status === 403) throw new ConnectorUnauthorizedError3(`Vendure API error: ${res.status}`, res.status);
|
|
264
469
|
if (!res.ok) throw new Error(`Vendure API error: ${res.status}`);
|
|
265
|
-
|
|
470
|
+
const body = await res.json();
|
|
471
|
+
const forbidden = body.errors?.filter((error) => error.extensions?.code === "FORBIDDEN") ?? [];
|
|
472
|
+
if (forbidden.length) throw await forbiddenError(context, forbidden);
|
|
473
|
+
return body;
|
|
266
474
|
}
|
|
267
475
|
|
|
268
476
|
// src/replication/products.ts
|
|
269
|
-
|
|
477
|
+
import { compareIds, ConnectorUnauthorizedError as ConnectorUnauthorizedError4 } from "@tallyui/core";
|
|
478
|
+
var PRODUCT_LIST_QUERY2 = (barcodeField) => `
|
|
270
479
|
query GetProducts($options: ProductListOptions) {
|
|
271
480
|
products(options: $options) {
|
|
272
481
|
items {
|
|
@@ -288,136 +497,443 @@ var PRODUCT_LIST_QUERY2 = `
|
|
|
288
497
|
price
|
|
289
498
|
priceWithTax
|
|
290
499
|
currencyCode
|
|
291
|
-
|
|
292
|
-
stockOnHand
|
|
500
|
+
stockLevels { stockLocationId stockOnHand stockAllocated }
|
|
293
501
|
trackInventory
|
|
502
|
+
outOfStockThreshold
|
|
503
|
+
useGlobalOutOfStockThreshold
|
|
504
|
+
enabled
|
|
294
505
|
featuredAsset { id preview }
|
|
295
506
|
options { id name code }
|
|
296
|
-
|
|
507
|
+
taxCategory { id }
|
|
508
|
+
${barcodeField ? `customFields { ${barcodeField} }` : ""}
|
|
297
509
|
}
|
|
298
510
|
}
|
|
299
511
|
totalItems
|
|
300
512
|
}
|
|
301
513
|
}
|
|
302
514
|
`;
|
|
303
|
-
var
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
id
|
|
307
|
-
|
|
515
|
+
var PRODUCT_MARK_QUERY = `
|
|
516
|
+
query GetProductsMark($options: ProductListOptions) {
|
|
517
|
+
products(options: $options) {
|
|
518
|
+
items { id updatedAt }
|
|
519
|
+
totalItems
|
|
308
520
|
}
|
|
309
521
|
}
|
|
310
522
|
`;
|
|
311
|
-
async function gql2(context,
|
|
523
|
+
async function gql2(context, query2, variables) {
|
|
312
524
|
const res = await fetch(`${context.baseUrl}/admin-api`, {
|
|
313
525
|
method: "POST",
|
|
314
526
|
headers: {
|
|
315
527
|
"Content-Type": "application/json",
|
|
316
528
|
...context.headers
|
|
317
529
|
},
|
|
318
|
-
body: JSON.stringify({ query, variables }),
|
|
530
|
+
body: JSON.stringify({ query: query2, variables }),
|
|
319
531
|
signal: context.signal
|
|
320
532
|
});
|
|
321
|
-
if (!res.ok)
|
|
322
|
-
|
|
533
|
+
if (!res.ok) {
|
|
534
|
+
const body2 = await res.json().catch(() => void 0);
|
|
535
|
+
const message = body2?.errors?.[0]?.message;
|
|
536
|
+
if (res.status === 401 || res.status === 403) throw new ConnectorUnauthorizedError4(`Vendure API error: ${res.status}${message ? `: ${message}` : ""}`, res.status);
|
|
537
|
+
throw new Error(`Vendure API error: ${res.status}${message ? `: ${message}` : ""}`);
|
|
538
|
+
}
|
|
539
|
+
const body = await res.json();
|
|
540
|
+
const forbidden = body.errors?.filter((error) => error.extensions?.code === "FORBIDDEN") ?? [];
|
|
541
|
+
if (forbidden.length) throw await forbiddenError(context, forbidden);
|
|
542
|
+
if (body.errors?.length) throw new Error(`Vendure GraphQL error: ${body.errors[0].message}`);
|
|
543
|
+
return body;
|
|
323
544
|
}
|
|
324
|
-
var
|
|
545
|
+
var TIMEZONE_MESSAGE = "Vendure updatedAt filters miss changes: run Vendure with TZ=UTC or set updatedAtSkewMs to at least the magnitude of the server UTC offset in milliseconds.";
|
|
546
|
+
var VendureTimezoneConfigError = class extends Error {
|
|
547
|
+
name = "VendureTimezoneConfigError";
|
|
548
|
+
code = "store_misconfigured";
|
|
549
|
+
/** Only the store owner can fix it on the server, so the pull waits the store delay (`errorKind`). */
|
|
550
|
+
fixedBy = "store";
|
|
551
|
+
/**
|
|
552
|
+
* The store-side remedy, for `SyncStatus`'s detail. The message's other remedy,
|
|
553
|
+
* `updatedAtSkewMs`, is a connector option on the till, so it stays in the message only.
|
|
554
|
+
*/
|
|
555
|
+
fix = "run the Vendure server with its time zone set to UTC";
|
|
556
|
+
constructor() {
|
|
557
|
+
super(TIMEZONE_MESSAGE);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
async function probeUpdatedAtSkew(passHighWater, updatedAtSkewMs, fetchTotal, remark) {
|
|
561
|
+
for (const overlap of [1, 0]) {
|
|
562
|
+
if (overlap === 0 && updatedAtSkewMs > 0) continue;
|
|
563
|
+
const after = (mark) => new Date(Date.parse(mark) + (overlap === 0 ? 1 : -1) - updatedAtSkewMs).toISOString();
|
|
564
|
+
let total = await fetchTotal(after(passHighWater));
|
|
565
|
+
if (overlap === 1 && total === 0 && remark) {
|
|
566
|
+
const remarked = await remark();
|
|
567
|
+
if (remarked) {
|
|
568
|
+
passHighWater = remarked;
|
|
569
|
+
total = await fetchTotal(after(passHighWater));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (overlap === 1 && total === 0) {
|
|
573
|
+
if (remark) throw new VendureTimezoneConfigError();
|
|
574
|
+
throw new Error(TIMEZONE_MESSAGE);
|
|
575
|
+
}
|
|
576
|
+
if (overlap === 0 && total > 0) {
|
|
577
|
+
console.warn("Vendure updatedAt filters over-fetch because the server is not in UTC.");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return passHighWater;
|
|
581
|
+
}
|
|
582
|
+
function toProductDocument(p) {
|
|
583
|
+
const doc = { _deleted: false };
|
|
584
|
+
for (const field of Object.keys(vendureProductSchema.properties)) {
|
|
585
|
+
if (p[field] !== void 0) doc[field] = p[field];
|
|
586
|
+
}
|
|
587
|
+
if (Array.isArray(doc.variants)) {
|
|
588
|
+
doc.variants = [...doc.variants].sort((a, b) => compareIds(a.id, b.id));
|
|
589
|
+
}
|
|
590
|
+
return doc;
|
|
591
|
+
}
|
|
592
|
+
var createVendureProductReplication = (barcodeField, updatedAtSkewMs = 0) => ({
|
|
325
593
|
pull: {
|
|
326
594
|
async handler(lastCheckpoint, batchSize, context) {
|
|
595
|
+
if (batchSize > 1e3) throw new Error("Vendure Admin API take must not exceed 1000");
|
|
596
|
+
if (lastCheckpoint?.skip && lastCheckpoint.passHighWater === void 0 && lastCheckpoint.passTotal === void 0) {
|
|
597
|
+
lastCheckpoint = { skip: 0, updatedAt: lastCheckpoint.updatedAt ?? "" };
|
|
598
|
+
}
|
|
599
|
+
let passHighWater = lastCheckpoint?.passHighWater ?? lastCheckpoint?.updatedAt ?? "";
|
|
600
|
+
if (!lastCheckpoint?.skip) {
|
|
601
|
+
const fetchMark = async () => {
|
|
602
|
+
const head = await gql2(context, PRODUCT_MARK_QUERY, { options: { take: 1, sort: { updatedAt: "DESC" } } });
|
|
603
|
+
return head.data?.products?.items?.[0]?.updatedAt;
|
|
604
|
+
};
|
|
605
|
+
const mark = await fetchMark();
|
|
606
|
+
passHighWater = mark ?? lastCheckpoint?.updatedAt ?? "";
|
|
607
|
+
if (mark === void 0 || lastCheckpoint?.updatedAt && passHighWater === lastCheckpoint.updatedAt) {
|
|
608
|
+
return { documents: [], checkpoint: lastCheckpoint ?? { skip: 0, updatedAt: "" } };
|
|
609
|
+
}
|
|
610
|
+
passHighWater = await probeUpdatedAtSkew(passHighWater, updatedAtSkewMs, async (after) => {
|
|
611
|
+
const probe = await gql2(context, PRODUCT_MARK_QUERY, {
|
|
612
|
+
options: { take: 1, sort: { updatedAt: "DESC" }, filter: { updatedAt: { after } } }
|
|
613
|
+
});
|
|
614
|
+
return probe.data.products.totalItems;
|
|
615
|
+
}, fetchMark);
|
|
616
|
+
}
|
|
327
617
|
const options = {
|
|
328
618
|
take: batchSize,
|
|
329
|
-
skip: lastCheckpoint?.skip ?? 0
|
|
619
|
+
skip: lastCheckpoint?.skip ?? 0,
|
|
620
|
+
sort: { id: "ASC" }
|
|
330
621
|
};
|
|
331
622
|
if (lastCheckpoint?.updatedAt) {
|
|
332
623
|
options.filter = {
|
|
333
|
-
|
|
624
|
+
// Vendure after is strict; overlap by 1 ms to include timestamp ties.
|
|
625
|
+
updatedAt: { after: new Date(Date.parse(lastCheckpoint.updatedAt) - 1 - updatedAtSkewMs).toISOString() }
|
|
334
626
|
};
|
|
335
627
|
}
|
|
336
|
-
const res = await gql2(context, PRODUCT_LIST_QUERY2, { options });
|
|
337
|
-
|
|
338
|
-
|
|
628
|
+
const res = await gql2(context, PRODUCT_LIST_QUERY2(barcodeField), { options });
|
|
629
|
+
let data = res.data?.products;
|
|
630
|
+
let passTotal = options.skip === 0 ? data.totalItems : lastCheckpoint?.passTotal ?? data.totalItems;
|
|
631
|
+
if (options.skip > 0 && data.totalItems < passTotal) {
|
|
632
|
+
options.skip = 0;
|
|
633
|
+
const restarted = await gql2(context, PRODUCT_LIST_QUERY2(barcodeField), { options });
|
|
634
|
+
data = restarted.data?.products;
|
|
635
|
+
passTotal = data.totalItems;
|
|
339
636
|
}
|
|
340
|
-
const data = res.data?.products;
|
|
341
637
|
const products = data?.items ?? [];
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
638
|
+
if (!products.length && options.skip > 0) {
|
|
639
|
+
return createVendureProductReplication(barcodeField, updatedAtSkewMs).pull.handler(
|
|
640
|
+
{ skip: 0, updatedAt: lastCheckpoint?.updatedAt ?? "" },
|
|
641
|
+
batchSize,
|
|
642
|
+
context
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
const documents = products.map(toProductDocument);
|
|
646
|
+
const checkpoint = options.skip + products.length >= data.totalItems ? { skip: 0, updatedAt: passHighWater, passHighWater: void 0, passTotal: void 0 } : {
|
|
647
|
+
skip: options.skip + products.length,
|
|
648
|
+
updatedAt: lastCheckpoint?.updatedAt ?? "",
|
|
649
|
+
passHighWater,
|
|
650
|
+
passTotal
|
|
651
|
+
};
|
|
348
652
|
return { documents, checkpoint };
|
|
349
653
|
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
var vendureProductReplication = createVendureProductReplication();
|
|
657
|
+
|
|
658
|
+
// src/replication/variant-feed.ts
|
|
659
|
+
var MAX_TAKE = 1e3;
|
|
660
|
+
var variants = (context, fields, options) => gql2(context, `
|
|
661
|
+
query GetVariants($options: ProductVariantListOptions) {
|
|
662
|
+
productVariants(options: $options) { items { ${fields} } totalItems }
|
|
663
|
+
}
|
|
664
|
+
`, { options }).then((res) => res.data.productVariants);
|
|
665
|
+
var newest = (context) => variants(context, "id updatedAt", { take: 1, sort: { updatedAt: "DESC" } });
|
|
666
|
+
var createVendureVariantFeedReplication = (barcodeField, updatedAtSkewMs = 0, variantPageSize = MAX_TAKE) => ({
|
|
667
|
+
pull: {
|
|
668
|
+
// The next call's unchanged-high-water check then ends at one read, unless
|
|
669
|
+
// a variant changed since. The skew probe runs when a pass starts from it.
|
|
670
|
+
async seedCheckpoint(context) {
|
|
671
|
+
const head = await newest(context);
|
|
672
|
+
return { skip: 0, updatedAt: head.items[0]?.updatedAt ?? "", passHighWater: void 0, passTotal: void 0 };
|
|
673
|
+
},
|
|
674
|
+
async handler(lastCheckpoint, batchSize, context) {
|
|
675
|
+
if (batchSize > MAX_TAKE || variantPageSize > MAX_TAKE) throw new Error("Vendure Admin API take must not exceed 1000");
|
|
676
|
+
let passHighWater = lastCheckpoint?.passHighWater ?? lastCheckpoint?.updatedAt ?? "";
|
|
677
|
+
let skip = lastCheckpoint?.skip ?? 0;
|
|
678
|
+
let passTotal = lastCheckpoint?.passTotal;
|
|
679
|
+
if (!skip) {
|
|
680
|
+
const head = await newest(context);
|
|
681
|
+
passHighWater = head.items[0]?.updatedAt ?? lastCheckpoint?.updatedAt ?? "";
|
|
682
|
+
if (!head.items.length || lastCheckpoint?.updatedAt && passHighWater === lastCheckpoint.updatedAt) {
|
|
683
|
+
return { documents: [], checkpoint: lastCheckpoint ?? { skip: 0, updatedAt: "" } };
|
|
684
|
+
}
|
|
685
|
+
await probeUpdatedAtSkew(passHighWater, updatedAtSkewMs, async (after) => {
|
|
686
|
+
const probe = await variants(context, "id updatedAt", { take: 1, sort: { updatedAt: "DESC" }, filter: { updatedAt: { after } } });
|
|
687
|
+
return probe.totalItems;
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
const filter = lastCheckpoint?.updatedAt ? { updatedAt: { after: new Date(Date.parse(lastCheckpoint.updatedAt) - 1 - updatedAtSkewMs).toISOString() } } : void 0;
|
|
691
|
+
const page = (from) => variants(context, "id productId", { take: variantPageSize, skip: from, sort: { id: "ASC" }, filter });
|
|
692
|
+
const seen = /* @__PURE__ */ new Set();
|
|
693
|
+
const documents = [];
|
|
694
|
+
let complete = false;
|
|
695
|
+
while (!complete && documents.length < batchSize) {
|
|
696
|
+
let data = await page(skip);
|
|
697
|
+
if (skip > 0 && (!data.items.length || data.totalItems < (passTotal ?? data.totalItems))) {
|
|
698
|
+
skip = 0;
|
|
699
|
+
data = await page(0);
|
|
700
|
+
}
|
|
701
|
+
if (skip === 0) passTotal = data.totalItems;
|
|
702
|
+
const parentIds = [...new Set(data.items.map((v) => String(v.productId)))].filter((id) => !seen.has(id));
|
|
703
|
+
parentIds.forEach((id) => seen.add(id));
|
|
704
|
+
if (parentIds.length) {
|
|
705
|
+
const res = await gql2(context, PRODUCT_LIST_QUERY2(barcodeField), {
|
|
706
|
+
options: { take: parentIds.length, filter: { id: { in: parentIds } } }
|
|
359
707
|
});
|
|
360
|
-
|
|
361
|
-
if (row.assumedMasterState) {
|
|
362
|
-
conflicts.push({ ...row.assumedMasterState, _deleted: false });
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
} catch {
|
|
366
|
-
if (row.assumedMasterState) {
|
|
367
|
-
conflicts.push({ ...row.assumedMasterState, _deleted: false });
|
|
368
|
-
}
|
|
708
|
+
documents.push(...(res.data?.products?.items ?? []).map(toProductDocument));
|
|
369
709
|
}
|
|
710
|
+
skip += data.items.length;
|
|
711
|
+
complete = skip >= data.totalItems || !data.items.length;
|
|
370
712
|
}
|
|
371
|
-
|
|
713
|
+
const checkpoint = complete ? { skip: 0, updatedAt: passHighWater, passHighWater: void 0, passTotal: void 0 } : { skip, updatedAt: lastCheckpoint?.updatedAt ?? "", passHighWater, passTotal };
|
|
714
|
+
const moved = ["skip", "updatedAt", "passHighWater", "passTotal"].some((key) => checkpoint[key] !== lastCheckpoint?.[key]);
|
|
715
|
+
if (!documents.length && moved) {
|
|
716
|
+
const res = await gql2(context, PRODUCT_LIST_QUERY2(barcodeField), { options: { take: 1 } });
|
|
717
|
+
documents.push(...(res.data?.products?.items ?? []).map(toProductDocument));
|
|
718
|
+
}
|
|
719
|
+
return { documents, checkpoint };
|
|
372
720
|
}
|
|
373
721
|
}
|
|
374
|
-
};
|
|
722
|
+
});
|
|
375
723
|
|
|
376
|
-
// src/
|
|
377
|
-
var
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
type: "password",
|
|
396
|
-
placeholder: "vendure-auth-token from login",
|
|
397
|
-
required: true
|
|
398
|
-
}
|
|
399
|
-
],
|
|
400
|
-
getHeaders: (credentials) => ({
|
|
401
|
-
Authorization: `Bearer ${credentials.auth_token}`
|
|
402
|
-
})
|
|
403
|
-
},
|
|
404
|
-
schemas: {
|
|
405
|
-
products: vendureProductSchema
|
|
406
|
-
},
|
|
407
|
-
traits: {
|
|
408
|
-
product: vendureProductTraits
|
|
409
|
-
},
|
|
410
|
-
sync: {
|
|
411
|
-
products: vendureProductSync
|
|
724
|
+
// src/reconcile/stock.ts
|
|
725
|
+
var PAGE_SIZE = 1e3;
|
|
726
|
+
var VARIANT_STOCK_QUERY = `
|
|
727
|
+
query VariantStock($options: ProductVariantListOptions) {
|
|
728
|
+
productVariants(options: $options) {
|
|
729
|
+
items { id stockLevels { stockLocationId stockOnHand stockAllocated } }
|
|
730
|
+
totalItems
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
`;
|
|
734
|
+
var levelsKey = (levels = []) => levels.map((l) => JSON.stringify([l.stockLocationId, l.stockOnHand, l.stockAllocated])).sort().join();
|
|
735
|
+
var vendureStockReconcile = {
|
|
736
|
+
async *fetchPages(context) {
|
|
737
|
+
for (let skip = 0, total = 1; skip < total; skip += PAGE_SIZE) {
|
|
738
|
+
const body = await gql2(context, VARIANT_STOCK_QUERY, { options: { take: PAGE_SIZE, skip, sort: { id: "ASC" } } });
|
|
739
|
+
const { items, totalItems } = body.data.productVariants;
|
|
740
|
+
total = totalItems;
|
|
741
|
+
yield new Map(items.map((v) => [v.id, v.stockLevels]));
|
|
742
|
+
}
|
|
412
743
|
},
|
|
413
|
-
|
|
414
|
-
|
|
744
|
+
overlay(doc, stock) {
|
|
745
|
+
const variants2 = doc.variants ?? [];
|
|
746
|
+
let changed = false;
|
|
747
|
+
const next = variants2.map((v) => {
|
|
748
|
+
const levels = stock.get(v.id);
|
|
749
|
+
if (!levels || levelsKey(levels) === levelsKey(v.stockLevels)) return v;
|
|
750
|
+
changed = true;
|
|
751
|
+
return { ...v, stockLevels: levels };
|
|
752
|
+
});
|
|
753
|
+
return changed ? { variants: next } : void 0;
|
|
415
754
|
}
|
|
416
755
|
};
|
|
756
|
+
|
|
757
|
+
// src/reconcile/ids.ts
|
|
758
|
+
var PAGE_SIZE2 = 1e3;
|
|
759
|
+
var ID_LIST_QUERY = `
|
|
760
|
+
query GetProductIds($options: ProductListOptions) {
|
|
761
|
+
products(options: $options) { items { id variants { id } } totalItems }
|
|
762
|
+
}
|
|
763
|
+
`;
|
|
764
|
+
async function* fetchPages(context) {
|
|
765
|
+
for (let skip = 0, total = 1; skip < total; skip += PAGE_SIZE2) {
|
|
766
|
+
const res = await gql2(context, ID_LIST_QUERY, { options: { take: PAGE_SIZE2, skip, sort: { id: "ASC" } } });
|
|
767
|
+
const { items, totalItems } = res.data.products;
|
|
768
|
+
total = totalItems;
|
|
769
|
+
yield items.map((p) => ({ id: String(p.id), variantIds: (p.variants ?? []).map((v) => String(v.id)) }));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function variantIds(doc) {
|
|
773
|
+
return (doc.variants ?? []).map((v) => String(v.id));
|
|
774
|
+
}
|
|
775
|
+
function createFetchByIds(barcodeField) {
|
|
776
|
+
return async function fetchByIds(ids, context) {
|
|
777
|
+
if (!ids.length) return [];
|
|
778
|
+
const res = await gql2(context, PRODUCT_LIST_QUERY2(barcodeField), { options: { take: ids.length, filter: { id: { in: ids } } } });
|
|
779
|
+
return (res.data?.products?.items ?? []).map(toProductDocument);
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/reconcile/prices.ts
|
|
784
|
+
var PAGE_SIZE3 = 1e3;
|
|
785
|
+
var VENDURE_PRICE_RECONCILE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
786
|
+
var PRICE_LIST_QUERY = `
|
|
787
|
+
query GetVariantPrices($options: ProductVariantListOptions) {
|
|
788
|
+
productVariants(options: $options) { items { id productId price priceWithTax currencyCode } totalItems }
|
|
789
|
+
}
|
|
790
|
+
`;
|
|
791
|
+
var fingerprintOf = (entries) => [...entries].sort().join("|");
|
|
792
|
+
async function* fetchPages2(context) {
|
|
793
|
+
const byProduct = /* @__PURE__ */ new Map();
|
|
794
|
+
for (let skip = 0, total = 1; skip < total; skip += PAGE_SIZE3) {
|
|
795
|
+
const res = await gql2(context, PRICE_LIST_QUERY, { options: { take: PAGE_SIZE3, skip, sort: { id: "ASC" } } });
|
|
796
|
+
const { items, totalItems } = res.data.productVariants;
|
|
797
|
+
total = totalItems;
|
|
798
|
+
const touched = /* @__PURE__ */ new Set();
|
|
799
|
+
for (const variant of items) {
|
|
800
|
+
const productId = String(variant.productId);
|
|
801
|
+
touched.add(productId);
|
|
802
|
+
const entries = byProduct.get(productId) ?? [];
|
|
803
|
+
byProduct.set(productId, entries);
|
|
804
|
+
entries.push(`${variant.id}:${variant.currencyCode}:${variant.price}:${variant.priceWithTax}`);
|
|
805
|
+
}
|
|
806
|
+
yield new Map([...touched].map((id) => [id, fingerprintOf(byProduct.get(id) ?? [])]));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function fingerprint(doc) {
|
|
810
|
+
const entries = (doc.variants ?? []).map(
|
|
811
|
+
(v) => `${v.id}:${v.currencyCode}:${v.price}:${v.priceWithTax}`
|
|
812
|
+
);
|
|
813
|
+
return fingerprintOf(entries);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/store-settings.ts
|
|
817
|
+
import { StoreSettingsError } from "@tallyui/core";
|
|
818
|
+
var CHANNEL_AND_CATEGORIES_QUERY = `
|
|
819
|
+
query StoreSettingsChannel {
|
|
820
|
+
activeChannel { defaultCurrencyCode pricesIncludeTax defaultTaxZone { id } }
|
|
821
|
+
taxCategories { items { id isDefault } }
|
|
822
|
+
}
|
|
823
|
+
`;
|
|
824
|
+
var TAX_RATES_QUERY = `
|
|
825
|
+
query StoreSettingsTaxRates($zoneId: String!) {
|
|
826
|
+
taxRates(options: { take: 1000, filter: { zoneId: { eq: $zoneId } } }) {
|
|
827
|
+
items { enabled value category { id } customerGroup { id } }
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
`;
|
|
831
|
+
async function query(context, source, variables) {
|
|
832
|
+
try {
|
|
833
|
+
return await gql2(context, source, variables);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
throw new StoreSettingsError("failed", error instanceof Error ? error.message : String(error));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
var vendureStoreSettings = async (context, _choice) => {
|
|
839
|
+
const head = await query(context, CHANNEL_AND_CATEGORIES_QUERY);
|
|
840
|
+
const channel = head.data?.activeChannel;
|
|
841
|
+
if (!channel) throw new StoreSettingsError("failed", "Vendure returned no active channel");
|
|
842
|
+
const categories = head.data?.taxCategories?.items ?? [];
|
|
843
|
+
let rateItems = [];
|
|
844
|
+
if (channel.defaultTaxZone) {
|
|
845
|
+
const ratesBody = await query(context, TAX_RATES_QUERY, { zoneId: channel.defaultTaxZone.id });
|
|
846
|
+
rateItems = ratesBody.data?.taxRates?.items ?? [];
|
|
847
|
+
}
|
|
848
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const rate of rateItems) {
|
|
850
|
+
if (!rate.enabled || rate.customerGroup || !rate.category) continue;
|
|
851
|
+
byCategory.set(rate.category.id, Math.round(rate.value * 1e4));
|
|
852
|
+
}
|
|
853
|
+
const defaultCategory = categories.find((c) => c.isDefault) ?? categories[0];
|
|
854
|
+
const taxRatesPpm = {
|
|
855
|
+
default: defaultCategory ? byCategory.get(defaultCategory.id) ?? 0 : 0
|
|
856
|
+
};
|
|
857
|
+
for (const [categoryId, ppm] of byCategory) taxRatesPpm[categoryId] = ppm;
|
|
858
|
+
for (const category of categories) taxRatesPpm[category.id] ??= 0;
|
|
859
|
+
return {
|
|
860
|
+
currency: channel.defaultCurrencyCode,
|
|
861
|
+
pricesIncludeTax: channel.pricesIncludeTax,
|
|
862
|
+
taxRatesPpm
|
|
863
|
+
};
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
// src/global-settings.ts
|
|
867
|
+
var GLOBAL_SETTINGS_QUERY = `
|
|
868
|
+
query GlobalStockSettings {
|
|
869
|
+
globalSettings { trackInventory outOfStockThreshold }
|
|
870
|
+
}
|
|
871
|
+
`;
|
|
872
|
+
async function vendureGlobalStockSettings(context) {
|
|
873
|
+
const res = await gql2(context, GLOBAL_SETTINGS_QUERY);
|
|
874
|
+
return res.data.globalSettings;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/index.ts
|
|
878
|
+
import { ConnectorUnauthorizedError as ConnectorUnauthorizedError5 } from "@tallyui/core";
|
|
879
|
+
var VENDURE_IDS_PER_REQUEST = 1e3;
|
|
880
|
+
var createVendureConnector = (options = {}) => {
|
|
881
|
+
const idFeed = createReconcileFeed({ fetchByIds: createFetchByIds(options.barcodeField) });
|
|
882
|
+
return {
|
|
883
|
+
id: "vendure",
|
|
884
|
+
name: "Vendure",
|
|
885
|
+
description: "Connect to Vendure backends via the Admin GraphQL API",
|
|
886
|
+
icon: void 0,
|
|
887
|
+
auth: vendureAuth,
|
|
888
|
+
schemas: {
|
|
889
|
+
products: vendureProductSchema
|
|
890
|
+
},
|
|
891
|
+
traits: {
|
|
892
|
+
product: createVendureProductTraits(
|
|
893
|
+
options.barcodeField,
|
|
894
|
+
options.stockLocationId,
|
|
895
|
+
options.pricesIncludeTax,
|
|
896
|
+
options.globalTrackInventory,
|
|
897
|
+
options.globalOutOfStockThreshold
|
|
898
|
+
)
|
|
899
|
+
},
|
|
900
|
+
sync: {
|
|
901
|
+
products: createVendureProductSync(options.barcodeField)
|
|
902
|
+
},
|
|
903
|
+
replication: {
|
|
904
|
+
// One replication per collection: the product, variant and id-reconcile feeds
|
|
905
|
+
// share it (ADR-060). reconcile is last so its fetch wins duplicates.
|
|
906
|
+
products: combinePullAdapters({
|
|
907
|
+
products: createVendureProductReplication(options.barcodeField, options.updatedAtSkewMs),
|
|
908
|
+
variants: createVendureVariantFeedReplication(options.barcodeField, options.updatedAtSkewMs),
|
|
909
|
+
reconcile: idFeed.adapter
|
|
910
|
+
}, { legacyKey: "products" })
|
|
911
|
+
},
|
|
912
|
+
reconcile: {
|
|
913
|
+
stock: vendureStockReconcile,
|
|
914
|
+
ids: { fetchPages, variantIds, enqueue: idFeed.enqueue, refetchBatchSize: VENDURE_IDS_PER_REQUEST },
|
|
915
|
+
prices: { fetchPages: fetchPages2, fingerprint, enqueue: idFeed.enqueue, refetchBatchSize: VENDURE_IDS_PER_REQUEST }
|
|
916
|
+
},
|
|
917
|
+
storeSettings: vendureStoreSettings,
|
|
918
|
+
// Re-reads a restored session's capabilities; with an API key and no sign-in, the only reader.
|
|
919
|
+
capabilities: (context) => readVendureCapabilities(context.baseUrl, context.headers, { signal: context.signal })
|
|
920
|
+
};
|
|
921
|
+
};
|
|
922
|
+
var vendureConnector = createVendureConnector();
|
|
417
923
|
export {
|
|
924
|
+
ConnectorUnauthorizedError5 as ConnectorUnauthorizedError,
|
|
925
|
+
VendureTimezoneConfigError,
|
|
926
|
+
createVendureConnector,
|
|
927
|
+
createVendureProductReplication,
|
|
928
|
+
createVendureVariantFeedReplication,
|
|
929
|
+
vendureAuth,
|
|
418
930
|
vendureConnector,
|
|
931
|
+
vendureGlobalStockSettings,
|
|
419
932
|
vendureProductReplication,
|
|
420
933
|
vendureProductSchema,
|
|
421
934
|
vendureProductSync,
|
|
422
|
-
vendureProductTraits
|
|
935
|
+
vendureProductTraits,
|
|
936
|
+
vendureSignIn,
|
|
937
|
+
vendureStockReconcile,
|
|
938
|
+
vendureStoreSettings
|
|
423
939
|
};
|