@tallyui/connector-vendure 1.0.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.
@@ -0,0 +1,69 @@
1
+ import { ProductTraits, CollectionSync, ReplicationAdapter, TallyConnector } from '@tallyui/core';
2
+ import { RxJsonSchema } from 'rxdb';
3
+
4
+ /**
5
+ * RxDB schema for Vendure products.
6
+ *
7
+ * Mirrors the Vendure GraphQL Admin API product shape. Key differences:
8
+ * - Uses `name` (like WooCommerce, unlike Medusa's `title`)
9
+ * - Prices are integers in cents on `variants[].priceWithTax`
10
+ * - Images use `assets[].preview` and `featuredAsset.preview`
11
+ * - Categories are `collections[].name`
12
+ * - Stock level is a string (`IN_STOCK`, `OUT_OF_STOCK`, `LOW_STOCK`)
13
+ * - No native barcode field (custom fields only)
14
+ */
15
+ declare const vendureProductSchema: RxJsonSchema<any>;
16
+
17
+ /**
18
+ * Vendure product trait implementations.
19
+ *
20
+ * Key differences from other connectors:
21
+ * - Product name is `name` (same as WooCommerce, unlike Medusa's `title`)
22
+ * - Price is on `variants[].priceWithTax` (integer cents, like Medusa)
23
+ * - Images use `featuredAsset.preview` and `assets[].preview`
24
+ * - Stock status is a string from the Shop API: 'IN_STOCK', 'OUT_OF_STOCK', 'LOW_STOCK'
25
+ * - Categories are `collections[].name` (Vendure's equivalent of categories)
26
+ * - No native sale price — Vendure handles sales via promotions at checkout
27
+ * - No native barcode field — uses custom fields if configured
28
+ */
29
+ declare const vendureProductTraits: ProductTraits;
30
+
31
+ /**
32
+ * Vendure product sync implementation.
33
+ *
34
+ * Uses the Admin GraphQL API. Vendure uses offset-based pagination
35
+ * with `take` and `skip` options.
36
+ */
37
+ declare const vendureProductSync: CollectionSync;
38
+
39
+ type VendureProductCheckpoint = {
40
+ skip: number;
41
+ updatedAt: string;
42
+ };
43
+ /**
44
+ * Replication adapter for Vendure products.
45
+ *
46
+ * Implements pull (GraphQL query with offset pagination and updatedAt
47
+ * filtering) and push (GraphQL updateProduct mutation). Designed for
48
+ * use with RxDB's replicateRxCollection.
49
+ */
50
+ declare const vendureProductReplication: ReplicationAdapter<any, VendureProductCheckpoint>;
51
+
52
+ /**
53
+ * Vendure connector for Tally UI.
54
+ *
55
+ * Connects to Vendure backends via the Admin GraphQL API.
56
+ * Products are stored in RxDB using a schema that mirrors the Vendure API shape.
57
+ *
58
+ * ```ts
59
+ * import { vendureConnector } from '@tallyui/connector-vendure';
60
+ * import { ConnectorProvider } from '@tallyui/core';
61
+ *
62
+ * <ConnectorProvider connector={vendureConnector}>
63
+ * <App />
64
+ * </ConnectorProvider>
65
+ * ```
66
+ */
67
+ declare const vendureConnector: TallyConnector;
68
+
69
+ export { vendureConnector, vendureProductReplication, vendureProductSchema, vendureProductSync, vendureProductTraits };
package/dist/index.js ADDED
@@ -0,0 +1,423 @@
1
+ // src/schemas/products.ts
2
+ var vendureProductSchema = {
3
+ version: 0,
4
+ primaryKey: "id",
5
+ type: "object",
6
+ properties: {
7
+ id: { type: "string", maxLength: 100 },
8
+ name: { type: "string" },
9
+ slug: { type: "string", maxLength: 255 },
10
+ description: { type: "string" },
11
+ enabled: { type: "boolean" },
12
+ featuredAsset: {
13
+ type: ["object", "null"],
14
+ properties: {
15
+ id: { type: "string" },
16
+ preview: { type: "string" }
17
+ }
18
+ },
19
+ assets: {
20
+ type: "array",
21
+ items: {
22
+ type: "object",
23
+ properties: {
24
+ id: { type: "string" },
25
+ preview: { type: "string" }
26
+ }
27
+ }
28
+ },
29
+ collections: {
30
+ type: "array",
31
+ items: {
32
+ type: "object",
33
+ properties: {
34
+ id: { type: "string" },
35
+ name: { type: "string" },
36
+ slug: { type: "string" }
37
+ }
38
+ }
39
+ },
40
+ facetValues: {
41
+ type: "array",
42
+ items: {
43
+ type: "object",
44
+ properties: {
45
+ id: { type: "string" },
46
+ name: { type: "string" },
47
+ code: { type: "string" },
48
+ facet: {
49
+ type: "object",
50
+ properties: {
51
+ id: { type: "string" },
52
+ name: { type: "string" }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ },
58
+ variants: {
59
+ type: "array",
60
+ items: {
61
+ type: "object",
62
+ properties: {
63
+ id: { type: "string" },
64
+ name: { type: "string" },
65
+ sku: { type: "string" },
66
+ price: { type: "number" },
67
+ priceWithTax: { type: "number" },
68
+ currencyCode: { type: "string" },
69
+ stockLevel: { type: "string" },
70
+ stockOnHand: { type: "number" },
71
+ trackInventory: { type: "string" },
72
+ featuredAsset: {
73
+ type: ["object", "null"],
74
+ properties: {
75
+ id: { type: "string" },
76
+ preview: { type: "string" }
77
+ }
78
+ },
79
+ options: {
80
+ type: "array",
81
+ items: {
82
+ type: "object",
83
+ properties: {
84
+ id: { type: "string" },
85
+ name: { type: "string" },
86
+ code: { type: "string" }
87
+ }
88
+ }
89
+ },
90
+ customFields: { type: "object" }
91
+ }
92
+ }
93
+ },
94
+ updatedAt: { type: "string", maxLength: 50 }
95
+ },
96
+ required: ["id"],
97
+ indexes: ["slug", "updatedAt"]
98
+ };
99
+
100
+ // src/traits/product.ts
101
+ var vendureProductTraits = {
102
+ getId: (doc) => String(doc.id),
103
+ getName: (doc) => doc.name ?? "",
104
+ getSku: (doc) => doc.variants?.[0]?.sku || void 0,
105
+ getPrice: (doc) => {
106
+ const amount = doc.variants?.[0]?.priceWithTax;
107
+ if (amount == null) return void 0;
108
+ return (amount / 100).toFixed(2);
109
+ },
110
+ getRegularPrice: (doc) => {
111
+ const amount = doc.variants?.[0]?.priceWithTax;
112
+ if (amount == null) return void 0;
113
+ return (amount / 100).toFixed(2);
114
+ },
115
+ getSalePrice: () => {
116
+ return void 0;
117
+ },
118
+ isOnSale: () => {
119
+ return false;
120
+ },
121
+ getImageUrl: (doc) => doc.featuredAsset?.preview || doc.assets?.[0]?.preview || void 0,
122
+ getImageUrls: (doc) => (doc.assets ?? []).map((a) => a.preview).filter(Boolean),
123
+ getDescription: (doc) => doc.description || void 0,
124
+ getStockStatus: (doc) => {
125
+ const variant = doc.variants?.[0];
126
+ if (!variant) return "unknown";
127
+ const level = variant.stockLevel;
128
+ if (level === "IN_STOCK" || level === "LOW_STOCK") return "instock";
129
+ if (level === "OUT_OF_STOCK") return "outofstock";
130
+ if (variant.stockOnHand != null) {
131
+ return variant.stockOnHand > 0 ? "instock" : "outofstock";
132
+ }
133
+ return "unknown";
134
+ },
135
+ getStockQuantity: (doc) => {
136
+ return doc.variants?.[0]?.stockOnHand ?? null;
137
+ },
138
+ hasVariants: (doc) => (doc.variants?.length ?? 0) > 1,
139
+ getType: (doc) => {
140
+ if ((doc.variants?.length ?? 0) > 1) return "variable";
141
+ return "simple";
142
+ },
143
+ getBarcode: (doc) => {
144
+ return doc.variants?.[0]?.customFields?.barcode || void 0;
145
+ },
146
+ getCategoryNames: (doc) => (doc.collections ?? []).map((c) => c.name).filter(Boolean)
147
+ };
148
+
149
+ // src/sync/products.ts
150
+ var PRODUCT_LIST_QUERY = `
151
+ query GetProducts($options: ProductListOptions) {
152
+ products(options: $options) {
153
+ items {
154
+ id
155
+ updatedAt
156
+ }
157
+ totalItems
158
+ }
159
+ }
160
+ `;
161
+ var PRODUCT_DETAIL_QUERY = `
162
+ query GetProduct($id: ID!) {
163
+ product(id: $id) {
164
+ id
165
+ createdAt
166
+ updatedAt
167
+ name
168
+ slug
169
+ description
170
+ enabled
171
+ featuredAsset { id preview }
172
+ assets { id preview }
173
+ collections { id name slug }
174
+ facetValues { id name code facet { id name } }
175
+ variants {
176
+ id
177
+ name
178
+ sku
179
+ price
180
+ priceWithTax
181
+ currencyCode
182
+ stockLevel
183
+ stockOnHand
184
+ trackInventory
185
+ featuredAsset { id preview }
186
+ options { id name code }
187
+ customFields
188
+ }
189
+ }
190
+ }
191
+ `;
192
+ var vendureProductSync = {
193
+ fetchAllIds: async (context) => {
194
+ const entries = [];
195
+ const take = 100;
196
+ let skip = 0;
197
+ let totalItems = Infinity;
198
+ while (skip < totalItems) {
199
+ const res = await gql(context, PRODUCT_LIST_QUERY, {
200
+ options: { take, skip }
201
+ });
202
+ const data = res.data?.products;
203
+ if (!data) break;
204
+ totalItems = data.totalItems;
205
+ for (const item of data.items ?? []) {
206
+ entries.push({
207
+ id: String(item.id),
208
+ dateModified: item.updatedAt
209
+ });
210
+ }
211
+ skip += take;
212
+ }
213
+ return entries;
214
+ },
215
+ fetchByIds: async (ids, context) => {
216
+ const products = [];
217
+ for (const id of ids) {
218
+ const res = await gql(context, PRODUCT_DETAIL_QUERY, { id });
219
+ if (res.data?.product) {
220
+ products.push(res.data.product);
221
+ }
222
+ }
223
+ return products;
224
+ },
225
+ fetchModifiedAfter: async (date, context) => {
226
+ const products = [];
227
+ const take = 100;
228
+ let skip = 0;
229
+ let totalItems = Infinity;
230
+ while (skip < totalItems) {
231
+ const res = await gql(context, PRODUCT_LIST_QUERY, {
232
+ options: {
233
+ take,
234
+ skip,
235
+ filter: {
236
+ updatedAt: { after: date }
237
+ }
238
+ }
239
+ });
240
+ const data = res.data?.products;
241
+ if (!data) break;
242
+ totalItems = data.totalItems;
243
+ for (const item of data.items ?? []) {
244
+ const detail = await gql(context, PRODUCT_DETAIL_QUERY, { id: item.id });
245
+ if (detail.data?.product) {
246
+ products.push(detail.data.product);
247
+ }
248
+ }
249
+ skip += take;
250
+ }
251
+ return products;
252
+ }
253
+ };
254
+ async function gql(context, query, variables) {
255
+ const res = await fetch(`${context.baseUrl}/admin-api`, {
256
+ method: "POST",
257
+ headers: {
258
+ "Content-Type": "application/json",
259
+ ...context.headers
260
+ },
261
+ body: JSON.stringify({ query, variables }),
262
+ signal: context.signal
263
+ });
264
+ if (!res.ok) throw new Error(`Vendure API error: ${res.status}`);
265
+ return res.json();
266
+ }
267
+
268
+ // src/replication/products.ts
269
+ var PRODUCT_LIST_QUERY2 = `
270
+ query GetProducts($options: ProductListOptions) {
271
+ products(options: $options) {
272
+ items {
273
+ id
274
+ createdAt
275
+ updatedAt
276
+ name
277
+ slug
278
+ description
279
+ enabled
280
+ featuredAsset { id preview }
281
+ assets { id preview }
282
+ collections { id name slug }
283
+ facetValues { id name code facet { id name } }
284
+ variants {
285
+ id
286
+ name
287
+ sku
288
+ price
289
+ priceWithTax
290
+ currencyCode
291
+ stockLevel
292
+ stockOnHand
293
+ trackInventory
294
+ featuredAsset { id preview }
295
+ options { id name code }
296
+ customFields
297
+ }
298
+ }
299
+ totalItems
300
+ }
301
+ }
302
+ `;
303
+ var UPDATE_PRODUCT_MUTATION = `
304
+ mutation UpdateProduct($input: UpdateProductInput!) {
305
+ updateProduct(input: $input) {
306
+ id
307
+ updatedAt
308
+ }
309
+ }
310
+ `;
311
+ async function gql2(context, query, variables) {
312
+ const res = await fetch(`${context.baseUrl}/admin-api`, {
313
+ method: "POST",
314
+ headers: {
315
+ "Content-Type": "application/json",
316
+ ...context.headers
317
+ },
318
+ body: JSON.stringify({ query, variables }),
319
+ signal: context.signal
320
+ });
321
+ if (!res.ok) throw new Error(`Vendure API error: ${res.status}`);
322
+ return res.json();
323
+ }
324
+ var vendureProductReplication = {
325
+ pull: {
326
+ async handler(lastCheckpoint, batchSize, context) {
327
+ const options = {
328
+ take: batchSize,
329
+ skip: lastCheckpoint?.skip ?? 0
330
+ };
331
+ if (lastCheckpoint?.updatedAt) {
332
+ options.filter = {
333
+ updatedAt: { after: lastCheckpoint.updatedAt }
334
+ };
335
+ }
336
+ const res = await gql2(context, PRODUCT_LIST_QUERY2, { options });
337
+ if (res.errors?.length) {
338
+ throw new Error(`Vendure GraphQL error: ${res.errors[0].message}`);
339
+ }
340
+ const data = res.data?.products;
341
+ const products = data?.items ?? [];
342
+ const documents = products.map((p) => ({ ...p, _deleted: false }));
343
+ const nextSkip = products.length < batchSize ? 0 : (lastCheckpoint?.skip ?? 0) + products.length;
344
+ const checkpoint = products.length > 0 ? {
345
+ skip: nextSkip,
346
+ updatedAt: products[products.length - 1].updatedAt
347
+ } : lastCheckpoint ?? { skip: 0, updatedAt: "" };
348
+ return { documents, checkpoint };
349
+ }
350
+ },
351
+ push: {
352
+ async handler(changeRows, context) {
353
+ const conflicts = [];
354
+ for (const row of changeRows) {
355
+ const doc = row.newDocumentState;
356
+ try {
357
+ const res = await gql2(context, UPDATE_PRODUCT_MUTATION, {
358
+ input: doc
359
+ });
360
+ if (res.errors?.length) {
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
+ }
369
+ }
370
+ }
371
+ return conflicts;
372
+ }
373
+ }
374
+ };
375
+
376
+ // src/index.ts
377
+ var vendureConnector = {
378
+ id: "vendure",
379
+ name: "Vendure",
380
+ description: "Connect to Vendure backends via the Admin GraphQL API",
381
+ icon: void 0,
382
+ auth: {
383
+ type: "Vendure Admin API",
384
+ fields: [
385
+ {
386
+ key: "url",
387
+ label: "Backend URL",
388
+ type: "url",
389
+ placeholder: "https://my-vendure-server.com",
390
+ required: true
391
+ },
392
+ {
393
+ key: "auth_token",
394
+ label: "Auth Token",
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
412
+ },
413
+ replication: {
414
+ products: vendureProductReplication
415
+ }
416
+ };
417
+ export {
418
+ vendureConnector,
419
+ vendureProductReplication,
420
+ vendureProductSchema,
421
+ vendureProductSync,
422
+ vendureProductTraits
423
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@tallyui/connector-vendure",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Vendure connector for Tally UI",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "source": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "source": "./src/index.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "license": "MIT",
21
+ "peerDependencies": {
22
+ "@tallyui/core": "0.2.0"
23
+ },
24
+ "devDependencies": {
25
+ "rxdb": "16.21.1"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "echo \"no tests yet\""
31
+ }
32
+ }