@useairfoil/producer-shopify 0.2.0 → 0.2.2

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.
Files changed (61) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +85 -248
  3. package/dist/index.d.mts +6993 -0
  4. package/dist/index.mjs +3 -0
  5. package/dist/main.d.mts +1 -0
  6. package/dist/main.mjs +700 -0
  7. package/dist/main.mjs.map +1 -0
  8. package/dist/manifest.d.mts +32 -0
  9. package/dist/manifest.mjs +51 -0
  10. package/dist/manifest.mjs.map +1 -0
  11. package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
  12. package/dist/src-YBU_KSf5.mjs +2472 -0
  13. package/dist/src-YBU_KSf5.mjs.map +1 -0
  14. package/package.json +27 -27
  15. package/src/{auth.ts → api/auth.ts} +15 -35
  16. package/src/api/client.ts +429 -0
  17. package/src/api/graphql.ts +221 -0
  18. package/src/api/http.ts +25 -0
  19. package/src/connector.ts +266 -163
  20. package/src/create-table.ts +10 -0
  21. package/src/demo/customers.ts +412 -0
  22. package/src/demo/index.ts +31 -0
  23. package/src/demo/setup.ts +63 -0
  24. package/src/demo/shared.ts +114 -0
  25. package/src/index.ts +35 -21
  26. package/src/main.ts +14 -8
  27. package/src/manifest.ts +4 -55
  28. package/src/resources/carts/row.ts +82 -0
  29. package/src/resources/carts/webhook.ts +98 -0
  30. package/src/resources/customers/graphql.ts +175 -0
  31. package/src/resources/customers/row.ts +140 -0
  32. package/src/resources/customers/webhook.ts +30 -0
  33. package/src/resources/orders/graphql.ts +419 -0
  34. package/src/resources/orders/row.ts +306 -0
  35. package/src/resources/orders/webhook.ts +248 -0
  36. package/src/resources/products/graphql.ts +142 -0
  37. package/src/resources/products/row.ts +122 -0
  38. package/src/resources/products/webhook.ts +130 -0
  39. package/src/resources/refunds/graphql.ts +196 -0
  40. package/src/resources/refunds/row.ts +137 -0
  41. package/src/resources/refunds/webhook.ts +123 -0
  42. package/src/resources/shared.ts +300 -0
  43. package/src/sandbox.ts +3 -3
  44. package/src/start.ts +4 -6
  45. package/src/tables.ts +15 -0
  46. package/dist/index.d.ts +0 -965
  47. package/dist/index.js +0 -4
  48. package/dist/main.d.ts +0 -1
  49. package/dist/main.js +0 -49
  50. package/dist/main.js.map +0 -1
  51. package/dist/manifest-BFfx2LkS.js +0 -91
  52. package/dist/manifest-BFfx2LkS.js.map +0 -1
  53. package/dist/manifest-CPOEExQU.d.ts +0 -34
  54. package/dist/manifest.d.ts +0 -2
  55. package/dist/manifest.js +0 -3
  56. package/dist/src-DqmiLrYS.js +0 -984
  57. package/dist/src-DqmiLrYS.js.map +0 -1
  58. package/src/api.ts +0 -636
  59. package/src/schemas.ts +0 -319
  60. package/src/throttle.ts +0 -89
  61. package/src/transient-retry.ts +0 -74
@@ -0,0 +1,412 @@
1
+ import type { Demo } from "@useairfoil/connector-kit";
2
+
3
+ import { type Address, gid } from "../resources/shared";
4
+ import {
5
+ amount,
6
+ catalog,
7
+ clock,
8
+ day,
9
+ handleOf,
10
+ hour,
11
+ isGiftCard,
12
+ money,
13
+ productNumber,
14
+ type Rows,
15
+ taxRate,
16
+ valuesOf,
17
+ variantNumber,
18
+ vendor,
19
+ welcomeCode,
20
+ } from "./shared";
21
+
22
+ /** One group is one customer with their orders, refunds, and sometimes an open cart. */
23
+ export const customerRows = (index: number, context: Demo.DemoContext): Rows => {
24
+ const { faker, now } = context;
25
+ const { at } = clock(context);
26
+
27
+ const rows: Rows = { customers: [], orders: [], refunds: [], carts: [] };
28
+
29
+ const customerNumber = 7_200_000_000_000 + index;
30
+ const customerId = gid("Customer", customerNumber);
31
+ const addressId = gid("MailingAddress", 9_300_000_000_000 + index);
32
+ const firstName = faker.person.firstName();
33
+ const lastName = faker.person.lastName();
34
+ const email = faker.internet.exampleEmail({ firstName, lastName }).toLowerCase();
35
+ const phone = faker.phone.number({ style: "international" });
36
+
37
+ const address: Address = {
38
+ firstName,
39
+ lastName,
40
+ name: `${firstName} ${lastName}`,
41
+ company: null,
42
+ address1: faker.location.streetAddress(),
43
+ address2: null,
44
+ city: faker.location.city(),
45
+ province: faker.location.state(),
46
+ provinceCode: faker.location.state({ abbreviated: true }),
47
+ country: "United States",
48
+ countryCode: "US",
49
+ zip: faker.location.zipCode("#####"),
50
+ phone,
51
+ latitude: faker.location.latitude(),
52
+ longitude: faker.location.longitude(),
53
+ };
54
+
55
+ const subscribed = faker.datatype.boolean({ probability: 0.4 });
56
+ const created = at(now.getTime() - faker.number.int({ min: 10 * day, max: 700 * day }));
57
+
58
+ // Pick purchase times between signup and now, oldest first.
59
+ const orderCount = faker.helpers.weightedArrayElement([
60
+ { weight: 20, value: 0 },
61
+ { weight: 40, value: 1 },
62
+ { weight: 20, value: 2 },
63
+ { weight: 10, value: 3 },
64
+ { weight: 10, value: faker.number.int({ min: 4, max: 6 }) },
65
+ ]);
66
+
67
+ const orderTimes = Array.from({ length: orderCount }, () =>
68
+ faker.date.between({ from: created, to: now }),
69
+ ).toSorted((a, b) => a.getTime() - b.getTime());
70
+
71
+ let spent = 0;
72
+ let lastChange = created;
73
+
74
+ orderTimes.forEach((orderCreated, n) => {
75
+ const orderNumber = 5_600_000_000_000 + index * 10 + n;
76
+ const orderId = gid("Order", orderNumber);
77
+ const saleId = gid("OrderTransaction", 6_700_000_000_000 + (index * 10 + n) * 2);
78
+ const discounted = n === 0 && faker.datatype.boolean({ probability: 0.25 });
79
+
80
+ const picks = faker.helpers.arrayElements(
81
+ catalog.map((item, p) => ({ item, p })),
82
+ { min: 1, max: 3 },
83
+ );
84
+
85
+ const lines = picks.map(({ item, p }, k) => {
86
+ const v = faker.number.int({ max: valuesOf(item).length - 1 });
87
+ const quantity = isGiftCard(item) ? 1 : faker.number.int({ min: 1, max: 2 });
88
+ const gross = item.price * quantity;
89
+ const discount = discounted ? Math.round(gross * 0.1) : 0;
90
+ const tax = isGiftCard(item) ? 0 : Math.round((gross - discount) * taxRate);
91
+
92
+ return { item, p, v, k, quantity, gross, discount, tax };
93
+ });
94
+
95
+ const discountTotal = lines.reduce((sum, line) => sum + line.discount, 0);
96
+ const subtotal = lines.reduce((sum, line) => sum + line.gross - line.discount, 0);
97
+ const tax = lines.reduce((sum, line) => sum + line.tax, 0);
98
+ const shipping = lines.every((line) => isGiftCard(line.item)) || subtotal >= 7500 ? 0 : 600;
99
+ const total = subtotal + shipping + tax;
100
+ const grams = lines.reduce((sum, line) => sum + line.item.grams * line.quantity, 0);
101
+
102
+ const cancelled = faker.datatype.boolean({ probability: 0.03 });
103
+
104
+ const refund = cancelled
105
+ ? null
106
+ : faker.helpers.maybe(() => faker.helpers.arrayElement(["full", "partial"] as const), {
107
+ probability: 0.06,
108
+ });
109
+
110
+ const refundedLines = refund === "full" ? lines : refund === "partial" ? lines.slice(0, 1) : [];
111
+ const refundShipping = refund === "full" ? shipping : 0;
112
+
113
+ const refundedSubtotal = refundedLines.reduce(
114
+ (sum, line) => sum + line.gross - line.discount,
115
+ 0,
116
+ );
117
+
118
+ const refundedTax = refundedLines.reduce((sum, line) => sum + line.tax, 0);
119
+ const refundedTotal = refundedSubtotal + refundedTax + refundShipping;
120
+
121
+ const cancelledAt = cancelled
122
+ ? at(orderCreated.getTime() + faker.number.int({ min: 60_000, max: day }))
123
+ : null;
124
+
125
+ const refundAt = refund
126
+ ? at(orderCreated.getTime() + faker.number.int({ min: 3 * day, max: 20 * day }))
127
+ : null;
128
+
129
+ const fulfilled = orderCreated.getTime() < now.getTime() - 5 * day;
130
+
131
+ const closedAt =
132
+ cancelledAt ??
133
+ (fulfilled
134
+ ? at(orderCreated.getTime() + faker.number.int({ min: 2 * day, max: 5 * day }))
135
+ : null);
136
+
137
+ const updatedAt = new Date(
138
+ Math.max(orderCreated.getTime(), closedAt?.getTime() ?? 0, refundAt?.getTime() ?? 0),
139
+ );
140
+
141
+ const current = cancelled ? 0 : 1;
142
+ const currentSubtotal = (subtotal - refundedSubtotal) * current;
143
+ const currentTax = (tax - refundedTax) * current;
144
+ const currentShipping = (shipping - refundShipping) * current;
145
+ const currentTotal = currentSubtotal + currentTax + currentShipping;
146
+ spent += currentTotal;
147
+ lastChange = updatedAt > lastChange ? updatedAt : lastChange;
148
+
149
+ const shippingLineId = gid("ShippingLine", 4_500_000_000_000 + index * 10 + n);
150
+
151
+ rows.orders.push({
152
+ id: orderId,
153
+ legacyResourceId: String(orderNumber),
154
+ name: `#${1001 + index * 10 + n}`,
155
+ email,
156
+ phone,
157
+ customerId,
158
+ customerLocale: "en",
159
+ customerAcceptsMarketing: subscribed,
160
+ billingAddress: address,
161
+ shippingAddress: lines.every((line) => isGiftCard(line.item)) ? null : address,
162
+ note: "",
163
+ customAttributes: [],
164
+ tags: [],
165
+ confirmationNumber: faker.string.alphanumeric({ length: 9, casing: "upper" }),
166
+ poNumber: "",
167
+ sourceName: "web",
168
+ sourceIdentifier: "",
169
+ // The Online Store sales channel.
170
+ appId: gid("App", 580111),
171
+ paymentGatewayNames: ["shopify_payments"],
172
+ test: false,
173
+ taxesIncluded: false,
174
+ taxExempt: false,
175
+ dutiesIncluded: false,
176
+ estimatedTaxes: false,
177
+ totalWeight: grams,
178
+ displayFinancialStatus: cancelled
179
+ ? "VOIDED"
180
+ : refund === "full"
181
+ ? "REFUNDED"
182
+ : refund === "partial"
183
+ ? "PARTIALLY_REFUNDED"
184
+ : "PAID",
185
+ cancelReason: cancelled ? "CUSTOMER" : null,
186
+ cancelledAt,
187
+ closedAt,
188
+ processedAt: orderCreated,
189
+ createdAt: orderCreated,
190
+ updatedAt,
191
+ currencyCode: "USD",
192
+ presentmentCurrencyCode: "USD",
193
+ discountCodes: discounted ? [welcomeCode] : [],
194
+ discountApplications: discounted
195
+ ? [
196
+ {
197
+ index: 0,
198
+ type: "DISCOUNT_CODE",
199
+ code: welcomeCode,
200
+ title: null,
201
+ description: null,
202
+ allocationMethod: "ACROSS",
203
+ targetSelection: "ALL",
204
+ targetType: "LINE_ITEM",
205
+ valueType: "PERCENTAGE",
206
+ value: "10",
207
+ },
208
+ ]
209
+ : [],
210
+ subtotalPriceSet: money(subtotal),
211
+ currentSubtotalPriceSet: money(currentSubtotal),
212
+ totalDiscountsSet: money(discountTotal),
213
+ currentTotalDiscountsSet: money(discountTotal * current),
214
+ totalShippingPriceSet: money(shipping),
215
+ currentShippingPriceSet: money(currentShipping),
216
+ totalTaxSet: money(tax),
217
+ currentTotalTaxSet: money(currentTax),
218
+ originalTotalDutiesSet: null,
219
+ currentTotalDutiesSet: null,
220
+ totalPriceSet: money(total),
221
+ currentTotalPriceSet: money(currentTotal),
222
+ taxLines: tax > 0 ? [{ title: "Sales Tax", rate: taxRate, priceSet: money(tax) }] : [],
223
+ shippingLines:
224
+ shipping === 0 && lines.every((line) => isGiftCard(line.item))
225
+ ? []
226
+ : [
227
+ {
228
+ id: shippingLineId,
229
+ title: shipping === 0 ? "Free Shipping" : "Standard Shipping",
230
+ code: shipping === 0 ? "Free Shipping" : "Standard Shipping",
231
+ source: "shopify",
232
+ carrierIdentifier: null,
233
+ phone: null,
234
+ isRemoved: false,
235
+ originalPriceSet: money(shipping),
236
+ discountedPriceSet: money(shipping),
237
+ currentDiscountedPriceSet: money(currentShipping),
238
+ taxLines: [],
239
+ discountAllocations: [],
240
+ },
241
+ ],
242
+ lineItems: lines.map((line) => {
243
+ const value = valuesOf(line.item)[line.v] ?? "Default Title";
244
+ const refundedQuantity = refundedLines.includes(line) ? line.quantity : 0;
245
+
246
+ return {
247
+ id: gid("LineItem", 14_000_000_000_000 + (index * 10 + n) * 10 + line.k),
248
+ name: line.item.option ? `${line.item.title} - ${value}` : line.item.title,
249
+ title: line.item.title,
250
+ variantTitle: line.item.option ? value : null,
251
+ sku: `NW-${handleOf(line.item).toUpperCase()}-${value.replace("/", "").toUpperCase()}`,
252
+ vendor,
253
+ productId: gid("Product", productNumber(line.p)),
254
+ variantId: gid("ProductVariant", variantNumber(line.p, line.v)),
255
+ quantity: line.quantity,
256
+ currentQuantity: (line.quantity - refundedQuantity) * current,
257
+ taxable: !isGiftCard(line.item),
258
+ requiresShipping: !isGiftCard(line.item),
259
+ isGiftCard: isGiftCard(line.item),
260
+ originalUnitPriceSet: money(line.item.price),
261
+ totalDiscountSet: money(line.discount),
262
+ taxLines:
263
+ line.tax > 0 ? [{ title: "Sales Tax", rate: taxRate, priceSet: money(line.tax) }] : [],
264
+ discountAllocations: discounted
265
+ ? [{ discountApplicationIndex: 0, amountSet: money(line.discount) }]
266
+ : [],
267
+ customAttributes: [],
268
+ };
269
+ }),
270
+ });
271
+
272
+ if (refund && refundAt) {
273
+ const refundNumber = 1_000_000_000_000 + index * 10 + n;
274
+
275
+ rows.refunds.push({
276
+ id: gid("Refund", refundNumber),
277
+ legacyResourceId: String(refundNumber),
278
+ orderId,
279
+ note: refund === "full" ? "Returned by customer" : "",
280
+ createdAt: refundAt,
281
+ processedAt: refundAt,
282
+ transactions: [
283
+ {
284
+ id: gid("OrderTransaction", 6_700_000_000_000 + (index * 10 + n) * 2 + 1),
285
+ kind: "REFUND",
286
+ status: "SUCCESS",
287
+ amount: amount(refundedTotal),
288
+ currencyCode: "USD",
289
+ gateway: "shopify_payments",
290
+ test: false,
291
+ errorCode: null,
292
+ parentTransactionId: saleId,
293
+ createdAt: refundAt,
294
+ processedAt: refundAt,
295
+ },
296
+ ],
297
+ refundLineItems: refundedLines.map((line) => ({
298
+ id: gid("RefundLineItem", 2_000_000_000_000 + (index * 10 + n) * 10 + line.k),
299
+ lineItemId: gid("LineItem", 14_000_000_000_000 + (index * 10 + n) * 10 + line.k),
300
+ quantity: line.quantity,
301
+ restockType: "RETURN",
302
+ locationId: gid("Location", 64_000_000_001),
303
+ subtotalSet: money(line.gross - line.discount),
304
+ totalTaxSet: money(line.tax),
305
+ })),
306
+ refundShippingLines:
307
+ refundShipping > 0
308
+ ? [
309
+ {
310
+ id: gid("RefundShippingLine", 3_000_000_000_000 + index * 10 + n),
311
+ shippingLineId,
312
+ subtotalAmountSet: money(refundShipping),
313
+ },
314
+ ]
315
+ : [],
316
+ orderAdjustments: [],
317
+ duties: [],
318
+ });
319
+ }
320
+ });
321
+
322
+ // Some shoppers leave a cart behind.
323
+ if (faker.datatype.boolean({ probability: 0.25 })) {
324
+ const token = `c1-${index.toString(16).padStart(8, "0")}${faker.string.hexadecimal({ length: 24, casing: "lower", prefix: "" })}`;
325
+ const cartCreated = at(now.getTime() - faker.number.int({ min: hour, max: 7 * day }));
326
+
327
+ rows.carts.push({
328
+ id: token,
329
+ token,
330
+ topic: "carts/update",
331
+ lineItems: faker.helpers
332
+ .arrayElements(
333
+ catalog.map((item, p) => ({ item, p })),
334
+ { min: 1, max: 2 },
335
+ )
336
+ .map(({ item, p }) => {
337
+ const v = faker.number.int({ max: valuesOf(item).length - 1 });
338
+ const quantity = faker.number.int({ min: 1, max: 2 });
339
+ const variant = variantNumber(p, v);
340
+
341
+ return {
342
+ id: String(variant),
343
+ properties: null,
344
+ quantity,
345
+ variantId: gid("ProductVariant", variant),
346
+ key: `${variant}:${faker.string.hexadecimal({ length: 32, casing: "lower", prefix: "" })}`,
347
+ discountedPrice: amount(item.price),
348
+ discounts: "[]",
349
+ giftCard: isGiftCard(item),
350
+ grams: item.grams,
351
+ linePrice: amount(item.price * quantity),
352
+ originalLinePrice: amount(item.price * quantity),
353
+ originalPrice: amount(item.price),
354
+ price: amount(item.price),
355
+ productId: gid("Product", productNumber(p)),
356
+ sku: `NW-${handleOf(item).toUpperCase()}-${(valuesOf(item)[v] ?? "").replace("/", "").toUpperCase()}`,
357
+ taxable: !isGiftCard(item),
358
+ title: item.title,
359
+ totalDiscount: "0",
360
+ vendor,
361
+ discountedPriceSet: money(item.price),
362
+ linePriceSet: money(item.price * quantity),
363
+ originalLinePriceSet: money(item.price * quantity),
364
+ priceSet: money(item.price),
365
+ totalDiscountSet: money(0),
366
+ parentRelationship: null,
367
+ };
368
+ }),
369
+ note: "",
370
+ updatedAt: at(cartCreated.getTime() + faker.number.int({ min: 0, max: hour })),
371
+ createdAt: cartCreated,
372
+ });
373
+ }
374
+
375
+ const subscribedAt = subscribed ? created : null;
376
+
377
+ rows.customers.push({
378
+ id: customerId,
379
+ legacyResourceId: String(customerNumber),
380
+ firstName,
381
+ lastName,
382
+ displayName: `${firstName} ${lastName}`,
383
+ email,
384
+ emailMarketingState: subscribed ? "SUBSCRIBED" : "NOT_SUBSCRIBED",
385
+ emailMarketingOptInLevel: subscribed ? "SINGLE_OPT_IN" : null,
386
+ emailMarketingUpdatedAt: subscribedAt,
387
+ phone,
388
+ smsMarketingState: "NOT_SUBSCRIBED",
389
+ smsMarketingOptInLevel: null,
390
+ smsMarketingUpdatedAt: null,
391
+ smsMarketingCollectedFrom: null,
392
+ note: "",
393
+ state: orderCount > 0 ? "ENABLED" : "DISABLED",
394
+ verifiedEmail: true,
395
+ taxExempt: false,
396
+ taxExemptions: [],
397
+ tags: orderCount >= 3 ? ["repeat"] : [],
398
+ locale: "en",
399
+ numberOfOrders: orderCount,
400
+ amountSpent: { amount: amount(spent), currencyCode: "USD" },
401
+ lastOrderId:
402
+ orderCount > 0 ? gid("Order", 5_600_000_000_000 + index * 10 + orderCount - 1) : "",
403
+ productSubscriberStatus: "NEVER_SUBSCRIBED",
404
+ dataSaleOptOut: false,
405
+ defaultAddressId: addressId,
406
+ addresses: [{ id: addressId, ...address }],
407
+ createdAt: created,
408
+ updatedAt: lastChange,
409
+ });
410
+
411
+ return rows;
412
+ };
@@ -0,0 +1,31 @@
1
+ import { ConnectorApp, Demo, RuntimeConfig } from "@useairfoil/connector-kit";
2
+ import { Command } from "effect/cli";
3
+
4
+ import { CartSchema } from "../resources/carts/row";
5
+ import { CustomerSchema } from "../resources/customers/row";
6
+ import { OrderSchema } from "../resources/orders/row";
7
+ import { ProductSchema } from "../resources/products/row";
8
+ import { RefundSchema } from "../resources/refunds/row";
9
+ import { customerRows } from "./customers";
10
+ import { setupRows } from "./setup";
11
+
12
+ export const demo = Demo.define({
13
+ name: "producer-shopify",
14
+ setup: {
15
+ tables: [Demo.table({ name: "products", rowSchema: ProductSchema, version: "updatedAt" })],
16
+ rows: setupRows,
17
+ },
18
+ group: {
19
+ tables: [
20
+ Demo.table({ name: "customers", rowSchema: CustomerSchema, version: "updatedAt" }),
21
+ Demo.table({ name: "orders", rowSchema: OrderSchema, version: "updatedAt" }),
22
+ Demo.table({ name: "refunds", rowSchema: RefundSchema, version: "processedAt" }),
23
+ Demo.table({ name: "carts", rowSchema: CartSchema, version: "updatedAt" }),
24
+ ],
25
+ rows: customerRows,
26
+ },
27
+ });
28
+
29
+ export const demoCommand = ConnectorApp.makeDemoCommand(demo).pipe(
30
+ Command.provide(RuntimeConfig.layerHosted()),
31
+ );
@@ -0,0 +1,63 @@
1
+ import type { Demo } from "@useairfoil/connector-kit";
2
+
3
+ import type { Product } from "../resources/products/row";
4
+
5
+ import { gid } from "../resources/shared";
6
+ import {
7
+ amount,
8
+ catalog,
9
+ day,
10
+ handleOf,
11
+ isGiftCard,
12
+ productNumber,
13
+ valuesOf,
14
+ variantNumber,
15
+ vendor,
16
+ } from "./shared";
17
+
18
+ /** The product catalog, written once before any customer. */
19
+ export const setupRows = ({ now }: Demo.DemoContext) => {
20
+ const created = new Date(now.getTime() - 800 * day);
21
+
22
+ const products: Array<Product> = catalog.map((item, p) => ({
23
+ id: gid("Product", productNumber(p)),
24
+ legacyResourceId: String(productNumber(p)),
25
+ title: item.title,
26
+ handle: handleOf(item),
27
+ descriptionHtml: `<p>The ${item.title.toLowerCase()} from ${vendor}.</p>`,
28
+ productType: item.type,
29
+ vendor,
30
+ status: "ACTIVE",
31
+ tags: [item.type.toLowerCase()],
32
+ createdAt: created,
33
+ updatedAt: created,
34
+ publishedAt: created,
35
+ templateSuffix: "",
36
+ featuredMedia: {
37
+ image: { url: `https://demo.example.com/images/${handleOf(item)}.jpg`, altText: item.title },
38
+ },
39
+ options: [
40
+ {
41
+ id: gid("ProductOption", productNumber(p)),
42
+ name: item.option?.name ?? "Title",
43
+ position: 1,
44
+ values: valuesOf(item),
45
+ },
46
+ ],
47
+ variants: valuesOf(item).map((value, v) => ({
48
+ id: gid("ProductVariant", variantNumber(p, v)),
49
+ legacyResourceId: String(variantNumber(p, v)),
50
+ title: value,
51
+ sku: `NW-${handleOf(item).toUpperCase()}-${value.replace("/", "").toUpperCase()}`,
52
+ barcode: null,
53
+ price: amount(item.price),
54
+ compareAtPrice: null,
55
+ inventoryPolicy: "DENY",
56
+ taxable: !isGiftCard(item),
57
+ createdAt: created,
58
+ updatedAt: created,
59
+ })),
60
+ }));
61
+
62
+ return { products };
63
+ };
@@ -0,0 +1,114 @@
1
+ import type { Demo } from "@useairfoil/connector-kit";
2
+
3
+ import { Duration } from "effect";
4
+
5
+ import type { Cart } from "../resources/carts/row";
6
+ import type { Customer } from "../resources/customers/row";
7
+ import type { Order } from "../resources/orders/row";
8
+ import type { Refund } from "../resources/refunds/row";
9
+
10
+ import { type MoneyBag, normalizeAmount } from "../resources/shared";
11
+
12
+ export const hour = Duration.toMillis(Duration.hours(1));
13
+ export const day = Duration.toMillis(Duration.days(1));
14
+ export const vendor = "Northwind Goods";
15
+ export const taxRate = 0.08;
16
+ export const welcomeCode = "WELCOME10";
17
+
18
+ // Amounts are in cents, weights in grams.
19
+ export const catalog = [
20
+ {
21
+ title: "Classic Cotton Tee",
22
+ type: "T-Shirts",
23
+ price: 2800,
24
+ grams: 180,
25
+ option: { name: "Size", values: ["S", "M", "L", "XL"] },
26
+ },
27
+ {
28
+ title: "Everyday Hoodie",
29
+ type: "Sweatshirts",
30
+ price: 6400,
31
+ grams: 520,
32
+ option: { name: "Size", values: ["S", "M", "L", "XL"] },
33
+ },
34
+ {
35
+ title: "Linen Button Shirt",
36
+ type: "Shirts",
37
+ price: 7200,
38
+ grams: 260,
39
+ option: { name: "Size", values: ["S", "M", "L"] },
40
+ },
41
+ {
42
+ title: "Wool Beanie",
43
+ type: "Hats",
44
+ price: 2600,
45
+ grams: 90,
46
+ option: { name: "Color", values: ["Charcoal", "Navy", "Rust"] },
47
+ },
48
+ { title: "Canvas Tote Bag", type: "Bags", price: 2200, grams: 300, option: null },
49
+ {
50
+ title: "Leather Card Wallet",
51
+ type: "Accessories",
52
+ price: 5400,
53
+ grams: 80,
54
+ option: { name: "Color", values: ["Black", "Tan"] },
55
+ },
56
+ {
57
+ title: "Stoneware Mug",
58
+ type: "Home",
59
+ price: 1800,
60
+ grams: 400,
61
+ option: { name: "Color", values: ["Cream", "Sage"] },
62
+ },
63
+ {
64
+ title: "Insulated Water Bottle",
65
+ type: "Home",
66
+ price: 3200,
67
+ grams: 450,
68
+ option: { name: "Color", values: ["Black", "White", "Sky"] },
69
+ },
70
+ {
71
+ title: "Merino Running Socks",
72
+ type: "Socks",
73
+ price: 1600,
74
+ grams: 60,
75
+ option: { name: "Size", values: ["S/M", "L/XL"] },
76
+ },
77
+ { title: "Gift Card", type: "Gift Cards", price: 5000, grams: 0, option: null },
78
+ ] as const;
79
+
80
+ type CatalogItem = (typeof catalog)[number];
81
+
82
+ // Shopify IDs are numbers; each kind gets its own range.
83
+ export const productNumber = (p: number) => 8_100_000_000_000 + p;
84
+ export const variantNumber = (p: number, v: number) => 44_000_000_000_000 + p * 10 + v;
85
+
86
+ export const handleOf = (item: CatalogItem) => item.title.toLowerCase().replaceAll(" ", "-");
87
+ export const isGiftCard = (item: CatalogItem) => item.type === "Gift Cards";
88
+
89
+ export const valuesOf = (item: CatalogItem): ReadonlyArray<string> =>
90
+ item.option?.values ?? ["Default Title"];
91
+
92
+ export const money = (cents: number): MoneyBag => {
93
+ const amount = normalizeAmount((cents / 100).toFixed(2));
94
+
95
+ return {
96
+ shopMoney: { amount, currencyCode: "USD" },
97
+ presentmentMoney: { amount, currencyCode: "USD" },
98
+ };
99
+ };
100
+
101
+ export const amount = (cents: number) => normalizeAmount((cents / 100).toFixed(2));
102
+
103
+ /** Date helpers for one run. `at` keeps a time from going past now. */
104
+ export const clock = ({ now }: Demo.DemoContext) => {
105
+ const at = (time: number) => new Date(Math.min(time, now.getTime()));
106
+ return { at };
107
+ };
108
+
109
+ export type Rows = {
110
+ readonly customers: Array<Customer>;
111
+ readonly orders: Array<Order>;
112
+ readonly refunds: Array<Refund>;
113
+ readonly carts: Array<Cart>;
114
+ };
package/src/index.ts CHANGED
@@ -1,36 +1,50 @@
1
- export * as ShopifyApiClient from "./api";
1
+ import { fromCartWebhook } from "./resources/carts/webhook";
2
+ import { fromProductWebhook } from "./resources/products/webhook";
3
+
4
+ export * as ShopifyApiClient from "./api/client";
2
5
  export * as ShopifyConnector from "./connector";
3
6
  export { manifest } from "./manifest";
7
+ export type { Customer } from "./resources/customers/row";
8
+ export { CustomerSchema } from "./resources/customers/row";
9
+ export type { Order } from "./resources/orders/row";
10
+ export { OrderSchema } from "./resources/orders/row";
11
+ export type { OrderWebhookPayload } from "./resources/orders/webhook";
12
+ export {
13
+ fromOrderWebhook,
14
+ OrderDeleteWebhookPayloadSchema,
15
+ OrderWebhookPayloadSchema,
16
+ } from "./resources/orders/webhook";
17
+ export type { Refund } from "./resources/refunds/row";
18
+ export { RefundSchema } from "./resources/refunds/row";
19
+ export type { RefundWebhookPayload } from "./resources/refunds/webhook";
20
+ export { fromRefundWebhook, RefundWebhookPayloadSchema } from "./resources/refunds/webhook";
21
+ export type { Cart, CartLineItem } from "./resources/carts/row";
22
+ export { CartLineItemSchema, CartSchema } from "./resources/carts/row";
23
+ export type { CartWebhookPayload } from "./resources/carts/webhook";
24
+ export { CartWebhookPayloadSchema } from "./resources/carts/webhook";
4
25
  export type {
5
- CartEvent,
6
- CartLineItem,
7
- CartWebhookPayload,
8
- Money,
9
- MoneyBag,
10
- PageInfo,
11
26
  Product,
12
- ProductDeleteWebhookPayload,
13
27
  ProductOption,
14
28
  ProductStatus,
15
29
  ProductVariant,
16
30
  ProductVariantInventoryPolicy,
17
- ProductWebhookPayload,
18
- WebhookPayload,
19
- } from "./schemas";
31
+ } from "./resources/products/row";
20
32
  export {
21
- CartEventSchema,
22
- CartLineItemSchema,
23
- CartWebhookPayloadSchema,
24
- MoneyBagSchema,
25
- MoneySchema,
26
- PageInfoSchema,
27
33
  ProductOptionSchema,
28
- ProductDeleteWebhookPayloadSchema,
29
34
  ProductSchema,
30
35
  ProductStatusSchema,
31
36
  ProductVariantInventoryPolicySchema,
32
37
  ProductVariantSchema,
38
+ } from "./resources/products/row";
39
+ export type { ProductWebhookPayload } from "./resources/products/webhook";
40
+ export {
41
+ ProductDeleteWebhookPayloadSchema,
33
42
  ProductWebhookPayloadSchema,
34
- ShopifyNormalize,
35
- WebhookPayloadSchema,
36
- } from "./schemas";
43
+ } from "./resources/products/webhook";
44
+ export type { PageInfo } from "./resources/shared";
45
+ export { PageInfoSchema } from "./resources/shared";
46
+
47
+ export const ShopifyNormalize = {
48
+ productWebhook: fromProductWebhook,
49
+ cartWebhook: fromCartWebhook,
50
+ } as const;