@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
@@ -1,984 +0,0 @@
1
- import { t as __export } from "./chunk-Bp6m_JJh.js";
2
- import { n as manifest, t as ShopifyConfigDef } from "./manifest-BFfx2LkS.js";
3
- import { Auth, Connector, ConnectorError, Cursor, Fetch, Metrics, Resource, Telemetry, Webhook } from "@useairfoil/connector-kit";
4
- import { Clock, Config, Context, Data, Duration, Effect, Layer, Option, Random, Redacted, Schedule, Schema, Stream } from "effect";
5
- import { HttpClient, HttpClientRequest, HttpClientResponse, HttpServerResponse } from "effect/unstable/http";
6
- import * as Schema$1 from "effect/Schema";
7
- import { createHmac, timingSafeEqual } from "node:crypto";
8
-
9
- //#region src/auth.ts
10
- var ShopifyAuthError = class extends Data.TaggedError("ShopifyAuthError") {};
11
- var ShopifyAuth = class extends Context.Service()("@useairfoil/producer-shopify/ShopifyAuth") {};
12
- const TokenResponseSchema = Schema.Struct({
13
- access_token: Schema.String,
14
- expires_in: Schema.Number
15
- });
16
- const ErrorResponseSchema = Schema.Struct({ error: Schema.optional(Schema.String) });
17
- const normalizedShopDomain = (shopDomain) => shopDomain.replace(/^https?:\/\//i, "").replace(/\/+$/g, "");
18
- const errorFor = (reason) => {
19
- switch (reason) {
20
- case "shop_not_permitted": return new ShopifyAuthError({
21
- reason,
22
- message: "The Shopify app and store must belong to the same Shopify organization"
23
- });
24
- case "invalid_client": return new ShopifyAuthError({
25
- reason,
26
- message: "The Shopify client ID or client secret is invalid"
27
- });
28
- case "exchange_failed": return new ShopifyAuthError({
29
- reason,
30
- message: "Shopify authentication failed"
31
- });
32
- }
33
- };
34
- const decodeRejectedResponse = (response) => HttpClientResponse.schemaBodyJson(ErrorResponseSchema)(response).pipe(Effect.map((body) => {
35
- switch (body.error) {
36
- case "shop_not_permitted": return "shop_not_permitted";
37
- case "invalid_client": return "invalid_client";
38
- default: return "exchange_failed";
39
- }
40
- }), Effect.catch(() => Effect.succeed("exchange_failed")));
41
- const make$2 = Effect.fnUntraced(function* (config) {
42
- const client = yield* HttpClient.HttpClient;
43
- const endpoint = `https://${normalizedShopDomain(config.shopDomain)}/admin/oauth/access_token`;
44
- return yield* Auth.makeTokenCache({ acquire: Effect.scoped(Effect.gen(function* () {
45
- const request = HttpClientRequest.post(endpoint).pipe(HttpClientRequest.bodyUrlParams({
46
- grant_type: "client_credentials",
47
- client_id: config.clientId,
48
- client_secret: Redacted.value(config.clientSecret)
49
- }));
50
- const response = yield* client.execute(request).pipe(Effect.mapError(() => errorFor("exchange_failed")));
51
- const successfulResponse = yield* HttpClientResponse.filterStatusOk(response).pipe(Effect.catchTag("HttpClientError", () => decodeRejectedResponse(response).pipe(Effect.flatMap((reason) => Effect.fail(errorFor(reason))))));
52
- const body = yield* HttpClientResponse.schemaBodyJson(TokenResponseSchema)(successfulResponse).pipe(Effect.mapError(() => errorFor("exchange_failed")));
53
- return {
54
- value: Redacted.make(body.access_token),
55
- expiresIn: Duration.seconds(body.expires_in)
56
- };
57
- })) });
58
- });
59
- const layer$2 = (config) => Layer.effect(ShopifyAuth)(make$2(config));
60
-
61
- //#endregion
62
- //#region src/schemas.ts
63
- const PageInfoSchema = Schema$1.Struct({
64
- hasNextPage: Schema$1.Boolean,
65
- endCursor: Schema$1.NullOr(Schema$1.String)
66
- });
67
- const ProductStatusSchema = Schema$1.Literals([
68
- "ACTIVE",
69
- "ARCHIVED",
70
- "DRAFT",
71
- "UNLISTED"
72
- ]);
73
- const RestProductStatusSchema = Schema$1.Literals([
74
- "active",
75
- "archived",
76
- "draft"
77
- ]);
78
- const ProductVariantInventoryPolicySchema = Schema$1.Literals(["CONTINUE", "DENY"]);
79
- const RestProductVariantInventoryPolicySchema = Schema$1.Literals(["continue", "deny"]);
80
- const ProductFeaturedMediaSchema = Schema$1.Any;
81
- const ProductOptionSchema = Schema$1.Struct({
82
- id: Schema$1.String,
83
- name: Schema$1.String,
84
- position: Schema$1.Number,
85
- values: Schema$1.Array(Schema$1.String)
86
- });
87
- const ProductVariantSchema = Schema$1.Struct({
88
- id: Schema$1.String,
89
- legacyResourceId: Schema$1.String,
90
- title: Schema$1.String,
91
- sku: Schema$1.NullOr(Schema$1.String),
92
- barcode: Schema$1.NullOr(Schema$1.String),
93
- price: Schema$1.String,
94
- compareAtPrice: Schema$1.NullOr(Schema$1.String),
95
- inventoryPolicy: ProductVariantInventoryPolicySchema,
96
- taxable: Schema$1.Boolean,
97
- createdAt: Schema$1.String,
98
- updatedAt: Schema$1.String
99
- });
100
- const ProductWebhookImageSchema = Schema$1.Struct({
101
- src: Schema$1.String,
102
- alt: Schema$1.NullOr(Schema$1.String)
103
- });
104
- const ProductWebhookOptionSchema = Schema$1.Struct({
105
- id: Schema$1.Number,
106
- name: Schema$1.String,
107
- position: Schema$1.Number,
108
- values: Schema$1.Array(Schema$1.String)
109
- });
110
- const ProductWebhookVariantSchema = Schema$1.Struct({
111
- admin_graphql_api_id: Schema$1.String,
112
- id: Schema$1.Number,
113
- title: Schema$1.String,
114
- price: Schema$1.Union([Schema$1.String, Schema$1.Number]),
115
- inventory_policy: RestProductVariantInventoryPolicySchema,
116
- compare_at_price: Schema$1.NullOr(Schema$1.Union([Schema$1.String, Schema$1.Number])),
117
- created_at: Schema$1.String,
118
- updated_at: Schema$1.String,
119
- taxable: Schema$1.Boolean,
120
- barcode: Schema$1.NullOr(Schema$1.String),
121
- sku: Schema$1.NullOr(Schema$1.String)
122
- });
123
- const ProductWebhookVariantGidSchema = Schema$1.Struct({
124
- admin_graphql_api_id: Schema$1.String,
125
- updated_at: Schema$1.String
126
- });
127
- const ProductSchema = Schema$1.Struct({
128
- id: Schema$1.String,
129
- legacyResourceId: Schema$1.String,
130
- title: Schema$1.String,
131
- handle: Schema$1.String,
132
- descriptionHtml: Schema$1.String,
133
- productType: Schema$1.String,
134
- vendor: Schema$1.String,
135
- status: ProductStatusSchema,
136
- tags: Schema$1.Array(Schema$1.String),
137
- createdAt: Schema$1.String,
138
- updatedAt: Schema$1.String,
139
- publishedAt: Schema$1.NullOr(Schema$1.String),
140
- templateSuffix: Schema$1.NullOr(Schema$1.String),
141
- featuredMedia: Schema$1.NullOr(ProductFeaturedMediaSchema),
142
- options: Schema$1.Array(ProductOptionSchema),
143
- variants: Schema$1.Array(ProductVariantSchema)
144
- });
145
- const ProductWebhookPayloadSchema = Schema$1.Struct({
146
- id: Schema$1.Number,
147
- admin_graphql_api_id: Schema$1.String,
148
- body_html: Schema$1.NullOr(Schema$1.String),
149
- created_at: Schema$1.NullOr(Schema$1.String),
150
- handle: Schema$1.String,
151
- image: Schema$1.optional(Schema$1.NullOr(ProductWebhookImageSchema)),
152
- images: Schema$1.optional(Schema$1.Array(Schema$1.Any)),
153
- options: Schema$1.Array(ProductWebhookOptionSchema),
154
- product_type: Schema$1.String,
155
- published_at: Schema$1.NullOr(Schema$1.String),
156
- published_scope: Schema$1.optional(Schema$1.String),
157
- status: RestProductStatusSchema,
158
- tags: Schema$1.String,
159
- template_suffix: Schema$1.NullOr(Schema$1.String),
160
- title: Schema$1.String,
161
- updated_at: Schema$1.String,
162
- variants: Schema$1.Array(ProductWebhookVariantSchema),
163
- variant_gids: Schema$1.Array(ProductWebhookVariantGidSchema),
164
- vendor: Schema$1.String
165
- });
166
- const ProductDeleteWebhookPayloadSchema = Schema$1.Struct({ id: Schema$1.Number });
167
- const ProductEventSchema = Schema$1.Union([Schema$1.Struct({
168
- _tag: Schema$1.Literal("upsert"),
169
- payload: ProductWebhookPayloadSchema
170
- }), Schema$1.Struct({
171
- _tag: Schema$1.Literal("delete"),
172
- id: Schema$1.String,
173
- version: Schema$1.String
174
- })]);
175
- const MoneySchema = Schema$1.Struct({
176
- amount: Schema$1.String,
177
- currency_code: Schema$1.String
178
- });
179
- const MoneyBagSchema = Schema$1.Struct({
180
- shop_money: MoneySchema,
181
- presentment_money: MoneySchema
182
- });
183
- const CartLineItemSchema = Schema$1.Struct({
184
- id: Schema$1.Union([Schema$1.String, Schema$1.Number]),
185
- properties: Schema$1.NullOr(Schema$1.Unknown),
186
- quantity: Schema$1.Number,
187
- variant_id: Schema$1.Union([Schema$1.String, Schema$1.Number]),
188
- key: Schema$1.String,
189
- discounted_price: Schema$1.String,
190
- discounts: Schema$1.Array(Schema$1.Unknown),
191
- gift_card: Schema$1.Boolean,
192
- grams: Schema$1.Number,
193
- line_price: Schema$1.String,
194
- original_line_price: Schema$1.String,
195
- original_price: Schema$1.String,
196
- price: Schema$1.String,
197
- product_id: Schema$1.Union([Schema$1.String, Schema$1.Number]),
198
- sku: Schema$1.NullOr(Schema$1.String),
199
- taxable: Schema$1.Boolean,
200
- title: Schema$1.String,
201
- total_discount: Schema$1.String,
202
- vendor: Schema$1.String,
203
- discounted_price_set: MoneyBagSchema,
204
- line_price_set: MoneyBagSchema,
205
- original_line_price_set: MoneyBagSchema,
206
- price_set: MoneyBagSchema,
207
- total_discount_set: MoneyBagSchema,
208
- parent_relationship: Schema$1.NullOr(Schema$1.Unknown)
209
- });
210
- const CartWebhookPayloadSchema = Schema$1.Struct({
211
- id: Schema$1.String,
212
- token: Schema$1.String,
213
- line_items: Schema$1.Array(CartLineItemSchema),
214
- note: Schema$1.NullOr(Schema$1.String),
215
- updated_at: Schema$1.String,
216
- created_at: Schema$1.String
217
- });
218
- const CartEventSchema = Schema$1.Struct({
219
- id: Schema$1.String,
220
- token: Schema$1.String,
221
- topic: Schema$1.Literals(["carts/create", "carts/update"]),
222
- lineItems: Schema$1.Array(CartLineItemSchema),
223
- note: Schema$1.NullOr(Schema$1.String),
224
- updatedAt: Schema$1.String,
225
- createdAt: Schema$1.String
226
- });
227
- const WebhookPayloadSchema = Schema$1.Unknown;
228
- const splitTags = (value) => {
229
- if (value.trim() === "") return [];
230
- return value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
231
- };
232
- const normalizeProductStatus = (status) => {
233
- switch (status) {
234
- case "active": return "ACTIVE";
235
- case "archived": return "ARCHIVED";
236
- case "draft": return "DRAFT";
237
- }
238
- };
239
- const normalizeInventoryPolicy = (policy) => {
240
- switch (policy) {
241
- case "continue": return "CONTINUE";
242
- case "deny": return "DENY";
243
- }
244
- };
245
- const normalizeProductOption = (restOption) => {
246
- return {
247
- id: `gid://shopify/ProductOption/${restOption.id}`,
248
- name: restOption.name,
249
- position: restOption.position,
250
- values: restOption.values
251
- };
252
- };
253
- const normalizeProductVariant = (restVariant) => {
254
- return {
255
- id: restVariant.admin_graphql_api_id,
256
- legacyResourceId: String(restVariant.id),
257
- title: restVariant.title,
258
- sku: restVariant.sku,
259
- barcode: restVariant.barcode,
260
- price: String(restVariant.price),
261
- compareAtPrice: restVariant.compare_at_price === null ? null : String(restVariant.compare_at_price),
262
- inventoryPolicy: normalizeInventoryPolicy(restVariant.inventory_policy),
263
- taxable: restVariant.taxable,
264
- createdAt: restVariant.created_at,
265
- updatedAt: restVariant.updated_at
266
- };
267
- };
268
- const normalizeProductFeaturedMedia = (image) => {
269
- if (image === null || image === void 0) return null;
270
- return { image: {
271
- url: image.src,
272
- altText: image.alt
273
- } };
274
- };
275
- const ShopifyNormalize = {
276
- productWebhook: (payload) => ({
277
- id: payload.admin_graphql_api_id,
278
- legacyResourceId: String(payload.id),
279
- title: payload.title,
280
- handle: payload.handle,
281
- descriptionHtml: payload.body_html ?? "",
282
- productType: payload.product_type,
283
- vendor: payload.vendor,
284
- status: normalizeProductStatus(payload.status),
285
- tags: splitTags(payload.tags),
286
- createdAt: payload.created_at,
287
- updatedAt: payload.updated_at,
288
- publishedAt: payload.published_at,
289
- templateSuffix: payload.template_suffix,
290
- featuredMedia: normalizeProductFeaturedMedia(payload.image),
291
- options: payload.options.map(normalizeProductOption),
292
- variants: payload.variants.map(normalizeProductVariant)
293
- }),
294
- cartWebhook: (payload, topic) => ({
295
- id: payload.id,
296
- token: payload.token,
297
- topic,
298
- lineItems: payload.line_items,
299
- note: payload.note,
300
- updatedAt: payload.updated_at,
301
- createdAt: payload.created_at
302
- })
303
- };
304
-
305
- //#endregion
306
- //#region src/throttle.ts
307
- const ThrottleStatusSchema = Schema.Struct({
308
- maximumAvailable: Schema.Number,
309
- currentlyAvailable: Schema.Number,
310
- restoreRate: Schema.Number
311
- });
312
- const GraphQLErrorSchema = Schema.Struct({ extensions: Schema.optional(Schema.Struct({ code: Schema.optional(Schema.String) })) });
313
- const GraphQLEnvelopeSchema = Schema.Struct({
314
- errors: Schema.optional(Schema.Array(GraphQLErrorSchema)),
315
- extensions: Schema.optional(Schema.Struct({ cost: Schema.optional(Schema.Struct({
316
- requestedQueryCost: Schema.optional(Schema.Number),
317
- throttleStatus: Schema.optional(ThrottleStatusSchema)
318
- })) }))
319
- });
320
- const decodeEnvelope = Schema.decodeUnknownOption(GraphQLEnvelopeSchema);
321
- const readThrottleCost = (body) => decodeEnvelope(body).pipe(Option.flatMap((envelope) => {
322
- const cost = envelope.extensions?.cost;
323
- const status = cost?.throttleStatus;
324
- const requestedQueryCost = cost?.requestedQueryCost;
325
- return status === void 0 || requestedQueryCost === void 0 ? Option.none() : Option.some({
326
- requestedQueryCost,
327
- status
328
- });
329
- }), Option.getOrUndefined);
330
- const retryableCodes = new Set(["THROTTLED", "INTERNAL_SERVER_ERROR"]);
331
- /** Retries only when every GraphQL error is transient. */
332
- const retryableErrorCode = (body) => decodeEnvelope(body).pipe(Option.flatMap((envelope) => {
333
- const codes = envelope.errors?.map((error) => error.extensions?.code) ?? [];
334
- if (!(codes.length > 0 && codes.every((code) => code !== void 0 && retryableCodes.has(code)))) return Option.none();
335
- return Option.some(codes.includes("THROTTLED") ? "THROTTLED" : "INTERNAL_SERVER_ERROR");
336
- }), Option.getOrUndefined);
337
- const backoff$1 = (baseDelay, attempt) => Random.nextBetween(.8, 1.2).pipe(Effect.map((jitter) => Duration.times(baseDelay, 2 ** attempt * jitter)));
338
- /** Uses Shopify's cost deficit for throttling and backoff for internal errors. */
339
- const retryDelay = (code, body, attempt, options) => {
340
- if (code === "INTERNAL_SERVER_ERROR") return backoff$1(options.baseDelay, attempt);
341
- const cost = readThrottleCost(body);
342
- if (cost === void 0 || cost.status.restoreRate <= 0) return backoff$1(options.baseDelay, attempt);
343
- const deficitMillis = Math.max(0, cost.requestedQueryCost - cost.status.currentlyAvailable) / cost.status.restoreRate * 1e3;
344
- return Effect.succeed(Duration.max(Duration.millis(Math.ceil(deficitMillis)), options.baseDelay));
345
- };
346
-
347
- //#endregion
348
- //#region src/transient-retry.ts
349
- const parseRetryAfter = (header, atMillis) => {
350
- if (header === void 0 || header.trim() === "") return void 0;
351
- const seconds = Number(header);
352
- if (Number.isFinite(seconds)) return Duration.seconds(Math.max(seconds, 0));
353
- const parsedDate = Date.parse(header);
354
- return Number.isNaN(parsedDate) ? void 0 : Duration.millis(Math.max(parsedDate - atMillis, 0));
355
- };
356
- const otherTransientStatuses = new Set([
357
- 408,
358
- 500,
359
- 502,
360
- 503,
361
- 504
362
- ]);
363
- const backoff = (baseDelay, attempt) => Random.nextBetween(.8, 1.2).pipe(Effect.map((jitter) => Duration.times(baseDelay, 2 ** attempt * jitter)));
364
- const delayFor = (response, attempt, options) => {
365
- if (response.status !== 429) return backoff(options.baseDelay, attempt);
366
- return Clock.currentTimeMillis.pipe(Effect.map((now) => parseRetryAfter(response.headers["retry-after"], now) ?? options.retryAfterFallback));
367
- };
368
- const retryReason = (status) => status === 429 ? "rate_limit" : status === 408 ? "timeout" : "server_error";
369
- /** Retries transient responses and honors Shopify's `Retry-After` header on 429. */
370
- const withTransientRetry = (client, options) => {
371
- const loop = (effect, remaining, attempt) => Effect.flatMap(effect, (response) => {
372
- if (!(response.status === 429 || otherTransientStatuses.has(response.status)) || remaining <= 0) return Effect.succeed(response);
373
- return delayFor(response, attempt, options).pipe(Effect.tap(() => Metrics.recordApiRetry({
374
- connector: manifest.name,
375
- reason: retryReason(response.status)
376
- })), Effect.flatMap(Effect.sleep), Effect.flatMap(() => loop(effect, remaining - 1, attempt + 1)));
377
- });
378
- return HttpClient.transform(client, (effect) => loop(effect, options.maxRetries, 0));
379
- };
380
-
381
- //#endregion
382
- //#region src/api.ts
383
- var api_exports = /* @__PURE__ */ __export({
384
- ShopifyApiClient: () => ShopifyApiClient,
385
- layer: () => layer$1,
386
- layerConfig: () => layerConfig$1,
387
- make: () => make$1
388
- });
389
- var ShopifyApiClient = class extends Context.Service()("@useairfoil/producer-shopify/ShopifyApiClient") {};
390
- const ProductsQuery = `#graphql
391
- query AirfoilProducts($first: Int!, $after: String) {
392
- products(first: $first, after: $after, sortKey: UPDATED_AT, reverse: true) {
393
- nodes {
394
- id
395
- legacyResourceId
396
- title
397
- handle
398
- descriptionHtml
399
- productType
400
- vendor
401
- status
402
- tags
403
- createdAt
404
- updatedAt
405
- publishedAt
406
- templateSuffix
407
- featuredMedia {
408
- ... on MediaImage {
409
- image {
410
- url
411
- altText
412
- }
413
- }
414
- }
415
- options(first: 100) {
416
- id
417
- name
418
- position
419
- values
420
- }
421
- variants(first: 25) {
422
- nodes {
423
- id
424
- legacyResourceId
425
- title
426
- sku
427
- barcode
428
- price
429
- compareAtPrice
430
- inventoryPolicy
431
- taxable
432
- createdAt
433
- updatedAt
434
- }
435
- pageInfo {
436
- hasNextPage
437
- endCursor
438
- }
439
- }
440
- }
441
- pageInfo {
442
- hasNextPage
443
- endCursor
444
- }
445
- }
446
- }
447
- `;
448
- const ProductByIdQuery = `#graphql
449
- query AirfoilProductById($id: ID!) {
450
- product(id: $id) {
451
- id
452
- legacyResourceId
453
- title
454
- handle
455
- descriptionHtml
456
- productType
457
- vendor
458
- status
459
- tags
460
- createdAt
461
- updatedAt
462
- publishedAt
463
- templateSuffix
464
- featuredMedia {
465
- ... on MediaImage {
466
- image {
467
- url
468
- altText
469
- }
470
- }
471
- }
472
- options(first: 100) {
473
- id
474
- name
475
- position
476
- values
477
- }
478
- variants(first: 25) {
479
- nodes {
480
- id
481
- legacyResourceId
482
- title
483
- sku
484
- barcode
485
- price
486
- compareAtPrice
487
- inventoryPolicy
488
- taxable
489
- createdAt
490
- updatedAt
491
- }
492
- pageInfo {
493
- hasNextPage
494
- endCursor
495
- }
496
- }
497
- }
498
- }
499
- `;
500
- const ProductVariantsQuery = `#graphql
501
- query AirfoilProductVariants($id: ID!, $first: Int!, $after: String) {
502
- product(id: $id) {
503
- variants(first: $first, after: $after) {
504
- nodes {
505
- id
506
- legacyResourceId
507
- title
508
- sku
509
- barcode
510
- price
511
- compareAtPrice
512
- inventoryPolicy
513
- taxable
514
- createdAt
515
- updatedAt
516
- }
517
- pageInfo {
518
- hasNextPage
519
- endCursor
520
- }
521
- }
522
- }
523
- }
524
- `;
525
- const ProductsAccessQuery = `#graphql
526
- query AirfoilProductsAccess {
527
- products(first: 1) {
528
- nodes {
529
- id
530
- }
531
- }
532
- }
533
- `;
534
- const ShopIdentityQuery = `#graphql
535
- query AirfoilShopIdentity {
536
- shop {
537
- id
538
- }
539
- }
540
- `;
541
- const ShopIdentitySchema = Schema.Struct({ shop: Schema.Struct({ id: Schema.String }) });
542
- const ProductsAccessSchema = Schema.Struct({ products: Schema.Struct({ nodes: Schema.Array(Schema.Struct({ id: Schema.String })) }) });
543
- const LegacyResourceIdSchema = Schema.Union([Schema.String, Schema.Number]);
544
- const GraphQLProductVariantNodeSchema = Schema.Struct({
545
- id: Schema.String,
546
- legacyResourceId: LegacyResourceIdSchema,
547
- title: Schema.String,
548
- sku: Schema.NullOr(Schema.String),
549
- barcode: Schema.NullOr(Schema.String),
550
- price: Schema.String,
551
- compareAtPrice: Schema.NullOr(Schema.String),
552
- inventoryPolicy: ProductVariantInventoryPolicySchema,
553
- taxable: Schema.Boolean,
554
- createdAt: Schema.String,
555
- updatedAt: Schema.String
556
- });
557
- const GraphQLProductNodeSchema = Schema.Struct({
558
- id: Schema.String,
559
- legacyResourceId: LegacyResourceIdSchema,
560
- title: Schema.String,
561
- handle: Schema.String,
562
- descriptionHtml: Schema.String,
563
- productType: Schema.String,
564
- vendor: Schema.String,
565
- status: ProductStatusSchema,
566
- tags: Schema.Array(Schema.String),
567
- createdAt: Schema.String,
568
- updatedAt: Schema.String,
569
- publishedAt: Schema.NullOr(Schema.String),
570
- templateSuffix: Schema.NullOr(Schema.String),
571
- featuredMedia: Schema.NullOr(Schema.Any),
572
- options: Schema.Array(ProductOptionSchema),
573
- variants: Schema.Struct({
574
- nodes: Schema.Array(GraphQLProductVariantNodeSchema),
575
- pageInfo: PageInfoSchema
576
- })
577
- });
578
- const GraphQLProductsDataSchema = Schema.Struct({ products: Schema.Struct({
579
- nodes: Schema.Array(GraphQLProductNodeSchema),
580
- pageInfo: PageInfoSchema
581
- }) });
582
- const GraphQLProductByIdDataSchema = Schema.Struct({ product: Schema.NullOr(GraphQLProductNodeSchema) });
583
- const GraphQLProductVariantsDataSchema = Schema.Struct({ product: Schema.NullOr(Schema.Struct({ variants: Schema.Struct({
584
- nodes: Schema.Array(GraphQLProductVariantNodeSchema),
585
- pageInfo: PageInfoSchema
586
- }) })) });
587
- const normalizeProductNode = (node, variants) => ({
588
- id: node.id,
589
- legacyResourceId: String(node.legacyResourceId),
590
- title: node.title,
591
- handle: node.handle,
592
- descriptionHtml: node.descriptionHtml,
593
- productType: node.productType,
594
- vendor: node.vendor,
595
- status: node.status,
596
- tags: node.tags,
597
- createdAt: node.createdAt,
598
- updatedAt: node.updatedAt,
599
- publishedAt: node.publishedAt,
600
- templateSuffix: node.templateSuffix,
601
- featuredMedia: node.featuredMedia,
602
- options: node.options,
603
- variants: variants.map((variant) => ({
604
- ...variant,
605
- legacyResourceId: String(variant.legacyResourceId)
606
- }))
607
- });
608
- const graphqlEndpoint = (config) => {
609
- return `https://${config.shopDomain.replace(/^https?:\/\//i, "").replace(/\/+$/g, "")}/admin/api/${config.apiVersion}/graphql.json`;
610
- };
611
- const hasGraphqlErrors = (body) => {
612
- if (typeof body !== "object" || body === null) return false;
613
- const errors = body.errors;
614
- return Array.isArray(errors) && errors.length > 0;
615
- };
616
- const summarizeBody = (body) => {
617
- try {
618
- return JSON.stringify(body);
619
- } catch {
620
- return String(body);
621
- }
622
- };
623
- const make$1 = Effect.fnUntraced(function* (config) {
624
- const auth = yield* ShopifyAuth;
625
- const retryBaseDelay = Duration.millis(config.retryBaseDelayMs);
626
- const graphqlRetryBaseDelay = Duration.millis(config.graphqlRetryBaseDelayMs);
627
- const requestTimeout = Duration.seconds(config.requestTimeoutSeconds);
628
- const transportRetrySchedule = Schedule.exponential(retryBaseDelay).pipe(Schedule.jittered, Schedule.upTo({ times: config.transportMaxRetries }), Schedule.tap(() => Metrics.recordApiRetry({
629
- connector: manifest.name,
630
- reason: "transport"
631
- })));
632
- const client = (yield* HttpClient.HttpClient).pipe(HttpClient.mapRequestEffect((request) => auth.get.pipe(Effect.map((token) => HttpClientRequest.setHeader(request, "X-Shopify-Access-Token", Redacted.value(token))))), HttpClient.mapRequest(HttpClientRequest.acceptJson), (c) => withTransientRetry(c, {
633
- maxRetries: config.responseMaxRetries,
634
- baseDelay: retryBaseDelay,
635
- retryAfterFallback: Duration.seconds(config.retryAfterFallbackSeconds)
636
- }), HttpClient.retryTransient({
637
- retryOn: "errors-only",
638
- schedule: transportRetrySchedule
639
- }));
640
- const endpoint = graphqlEndpoint(config);
641
- const fetchGraphQL = (options) => {
642
- const attempt = (remainingRetries) => Effect.gen(function* () {
643
- const request = yield* HttpClientRequest.post(endpoint).pipe(HttpClientRequest.bodyJson({
644
- query: options.query,
645
- variables: options.variables ?? {}
646
- }), Effect.mapError((cause) => new ConnectorError({
647
- message: "Failed to encode Shopify GraphQL request",
648
- cause
649
- })));
650
- const { body, status } = yield* Effect.scoped(client.execute(request).pipe(Effect.tapError((error) => Telemetry.annotateError("api_http", error)), Effect.mapError((error) => {
651
- if (error._tag === "ShopifyAuthError") return new ConnectorError({
652
- message: error.message,
653
- cause: error
654
- });
655
- return new ConnectorError({
656
- message: "Shopify GraphQL request failed",
657
- cause: error
658
- });
659
- }), Effect.flatMap((response) => response.json.pipe(Effect.tapError((error) => Telemetry.annotateError("api_json", error)), Effect.mapError((error) => new ConnectorError({
660
- message: "Shopify GraphQL returned invalid JSON",
661
- cause: error
662
- })), Effect.map((body$1) => ({
663
- body: body$1,
664
- status: response.status
665
- }))))));
666
- if (status < 200 || status >= 300) {
667
- const error = {
668
- status,
669
- body,
670
- operationName: options.operationName
671
- };
672
- yield* Effect.logWarning("Shopify GraphQL returned non-2xx status").pipe(Effect.annotateLogs({
673
- operationName: options.operationName,
674
- status,
675
- body: summarizeBody(body)
676
- }));
677
- yield* Telemetry.annotateError("api_status", error);
678
- return yield* Effect.fail(new ConnectorError({
679
- message: "Shopify GraphQL returned non-2xx status",
680
- cause: error
681
- }));
682
- }
683
- if (hasGraphqlErrors(body)) {
684
- const retryableCode = retryableErrorCode(body);
685
- if (retryableCode !== void 0 && remainingRetries > 0) {
686
- const attemptIndex = config.graphqlMaxRetries - remainingRetries;
687
- const wait = yield* retryDelay(retryableCode, body, attemptIndex, { baseDelay: graphqlRetryBaseDelay });
688
- yield* Effect.logWarning("Shopify GraphQL request returned a retryable error, retrying").pipe(Effect.annotateLogs({
689
- operationName: options.operationName,
690
- code: retryableCode,
691
- remainingRetries,
692
- waitMillis: Duration.toMillis(wait)
693
- }));
694
- yield* Metrics.recordApiRetry({
695
- connector: manifest.name,
696
- reason: retryableCode === "THROTTLED" ? "rate_limit" : "server_error"
697
- });
698
- yield* Effect.sleep(wait);
699
- return yield* attempt(remainingRetries - 1);
700
- }
701
- yield* Effect.logWarning("Shopify GraphQL returned errors").pipe(Effect.annotateLogs({
702
- operationName: options.operationName,
703
- body: summarizeBody(body)
704
- }));
705
- yield* Telemetry.annotateError("api_graphql", body);
706
- return yield* Effect.fail(new ConnectorError({
707
- message: "Shopify GraphQL returned errors",
708
- cause: body
709
- }));
710
- }
711
- const data = typeof body === "object" && body !== null ? body.data : void 0;
712
- return yield* Schema.decodeUnknownEffect(options.schema)(data).pipe(Effect.tapError((error) => Telemetry.annotateError("api_decode", error)), Effect.mapError((error) => new ConnectorError({
713
- message: "Shopify GraphQL response schema decode failed",
714
- cause: error
715
- })));
716
- }).pipe(Effect.withSpan(Telemetry.SpanName.apiFetch, {
717
- kind: "client",
718
- attributes: { [Telemetry.Attr.apiPath]: `graphql:${options.operationName}` }
719
- }));
720
- return attempt(config.graphqlMaxRetries).pipe(Effect.timeout(requestTimeout), Effect.mapError((error) => error instanceof ConnectorError ? error : new ConnectorError({
721
- message: "Shopify GraphQL request timed out",
722
- cause: error
723
- })));
724
- };
725
- const nextPageCursor = (pageInfo) => {
726
- if (!pageInfo.hasNextPage) return Effect.succeed(Option.none());
727
- if (pageInfo.endCursor === null) return Effect.fail(new ConnectorError({ message: "Shopify GraphQL pageInfo.endCursor is required when hasNextPage is true" }));
728
- return Effect.succeed(Option.some(pageInfo.endCursor));
729
- };
730
- const fetchRemainingProductVariants = (productId, initialCursor) => Stream.paginate(initialCursor, (after) => fetchGraphQL({
731
- operationName: "AirfoilProductVariants",
732
- query: ProductVariantsQuery,
733
- variables: {
734
- id: productId,
735
- first: 100,
736
- after
737
- },
738
- schema: GraphQLProductVariantsDataSchema
739
- }).pipe(Effect.flatMap((data) => {
740
- if (data.product === null) return Effect.fail(new ConnectorError({ message: "Shopify product disappeared while fetching variants" }));
741
- const variants = data.product.variants;
742
- return nextPageCursor(variants.pageInfo).pipe(Effect.map((cursor) => [variants.nodes, cursor]));
743
- }))).pipe(Stream.runCollect);
744
- const loadProductVariants = (node) => nextPageCursor(node.variants.pageInfo).pipe(Effect.flatMap(Option.match({
745
- onNone: () => Effect.succeed(node.variants.nodes),
746
- onSome: (cursor) => fetchRemainingProductVariants(node.id, cursor).pipe(Effect.map((remaining) => [...node.variants.nodes, ...remaining]))
747
- })));
748
- const fetchProducts = (options) => fetchGraphQL({
749
- operationName: "AirfoilProducts",
750
- query: ProductsQuery,
751
- variables: {
752
- first: options.first,
753
- after: options.after ?? null
754
- },
755
- schema: GraphQLProductsDataSchema
756
- }).pipe(Effect.flatMap((data) => nextPageCursor(data.products.pageInfo).pipe(Effect.andThen(Effect.forEach(data.products.nodes, (node) => loadProductVariants(node).pipe(Effect.map((variants) => normalizeProductNode(node, variants))))), Effect.map((items) => ({
757
- items,
758
- endCursor: data.products.pageInfo.endCursor,
759
- hasMore: data.products.pageInfo.hasNextPage
760
- })))));
761
- const fetchProductById = (id) => fetchGraphQL({
762
- operationName: "AirfoilProductById",
763
- query: ProductByIdQuery,
764
- variables: { id },
765
- schema: GraphQLProductByIdDataSchema
766
- }).pipe(Effect.flatMap((data) => {
767
- const node = data.product;
768
- if (node === null) return Effect.fail(new ConnectorError({ message: "Shopify product not found while refreshing from webhook" }));
769
- return loadProductVariants(node).pipe(Effect.map((variants) => normalizeProductNode(node, variants)));
770
- }));
771
- return {
772
- checkConnection: fetchGraphQL({
773
- operationName: "AirfoilShopIdentity",
774
- query: ShopIdentityQuery,
775
- schema: ShopIdentitySchema
776
- }).pipe(Effect.asVoid),
777
- checkProductsAccess: fetchGraphQL({
778
- operationName: "AirfoilProductsAccess",
779
- query: ProductsAccessQuery,
780
- schema: ProductsAccessSchema
781
- }).pipe(Effect.asVoid),
782
- fetchGraphQL,
783
- fetchProducts,
784
- fetchProductById
785
- };
786
- });
787
- const layer$1 = (config) => Layer.effect(ShopifyApiClient)(make$1(config));
788
- const layerConfig$1 = (config) => Layer.effect(ShopifyApiClient)(Config.unwrap(config).pipe(Effect.flatMap(make$1)));
789
-
790
- //#endregion
791
- //#region src/connector.ts
792
- var connector_exports = /* @__PURE__ */ __export({
793
- ShopifyConfigDef: () => ShopifyConfigDef,
794
- ShopifyConnector: () => ShopifyConnector,
795
- layer: () => layer,
796
- layerConfig: () => layerConfig,
797
- make: () => make,
798
- manifest: () => manifest
799
- });
800
- const verifyWebhookSignature = (options) => Effect.try({
801
- try: () => {
802
- if (!options.signature) throw new Error("Missing x-shopify-hmac-sha256 header");
803
- const digest = createHmac("sha256", options.secret).update(Buffer.from(options.rawBody)).digest();
804
- const provided = Buffer.from(options.signature, "base64");
805
- if (provided.length !== digest.length || !timingSafeEqual(digest, provided)) throw new Error("Invalid Shopify webhook signature");
806
- },
807
- catch: (cause) => new ConnectorError({
808
- message: "Shopify webhook verification failed",
809
- cause
810
- })
811
- });
812
- const decodeWebhookPayload = (schema) => Schema.decodeUnknownEffect(schema);
813
- const make = Effect.fnUntraced(function* (config) {
814
- const api = yield* ShopifyApiClient;
815
- const Products = Resource.entity({
816
- name: "products",
817
- schema: ProductSchema,
818
- key: "id",
819
- version: "updatedAt",
820
- check: api.checkProductsAccess,
821
- backfill: Fetch.page({
822
- pageCursor: Cursor.string(),
823
- cutoff: Cursor.isoDateTime(),
824
- fetch: ({ pageCursor, cutoff }) => api.fetchProducts({
825
- first: 50,
826
- after: typeof pageCursor === "string" ? pageCursor : void 0
827
- }).pipe(Effect.map((page) => ({
828
- mutations: page.items.filter((row) => Date.parse(row.updatedAt) <= Date.parse(String(cutoff))).map(Resource.upsert),
829
- nextPageCursor: page.endCursor ?? void 0,
830
- hasMore: page.hasMore
831
- })))
832
- }),
833
- webhook: Resource.webhook({
834
- schema: ProductEventSchema,
835
- handler: ({ payload }) => {
836
- if (payload._tag === "delete") return Effect.succeed([Resource.delete({
837
- key: `gid://shopify/Product/${payload.id}`,
838
- version: payload.version
839
- })]);
840
- const product = payload.payload;
841
- if (product.created_at !== null && product.variants.length === product.variant_gids.length) return Effect.succeed([Resource.upsert(ShopifyNormalize.productWebhook({
842
- ...product,
843
- created_at: product.created_at
844
- }))]);
845
- return api.fetchProductById(product.admin_graphql_api_id).pipe(Effect.map((row) => [Resource.upsert(row)]));
846
- }
847
- })
848
- });
849
- const CartEvents = Resource.entity({
850
- name: "cart_events",
851
- schema: CartEventSchema,
852
- key: "id",
853
- version: "updatedAt",
854
- check: api.checkConnection,
855
- webhook: Resource.webhook({
856
- schema: Schema.Struct({
857
- ...CartWebhookPayloadSchema.fields,
858
- topic: Schema.Literals(["carts/create", "carts/update"])
859
- }),
860
- handler: ({ payload }) => Effect.succeed([Resource.upsert(ShopifyNormalize.cartWebhook(payload, payload.topic))])
861
- })
862
- });
863
- const webhookRoute = Webhook.route({
864
- path: "/webhooks/shopify",
865
- ackMode: "after-publish",
866
- schema: Schema.Unknown,
867
- handler: ({ request, rawBody, payload: json, to }) => Effect.gen(function* () {
868
- const verificationError = yield* verifyWebhookSignature({
869
- rawBody,
870
- signature: request.headers["x-shopify-hmac-sha256"] ?? null,
871
- secret: Redacted.value(config.webhookSecret)
872
- }).pipe(Effect.match({
873
- onFailure: (error) => error,
874
- onSuccess: () => void 0
875
- }));
876
- if (verificationError) return HttpServerResponse.jsonUnsafe({
877
- ok: false,
878
- error: verificationError.message
879
- }, { status: 401 });
880
- const topic = request.headers["x-shopify-topic"] ?? "";
881
- switch (topic) {
882
- case "products/create":
883
- case "products/update": {
884
- const payload = yield* decodeWebhookPayload(ProductWebhookPayloadSchema)(json).pipe(Effect.mapError((cause) => new ConnectorError({
885
- message: `Invalid Shopify webhook payload for ${topic}`,
886
- cause
887
- })), Effect.match({
888
- onFailure: (error) => ({
889
- _tag: "Error",
890
- error
891
- }),
892
- onSuccess: (value) => ({
893
- _tag: "Success",
894
- value
895
- })
896
- }));
897
- if (payload._tag === "Error") return HttpServerResponse.jsonUnsafe({
898
- ok: false,
899
- error: payload.error.message
900
- }, { status: 400 });
901
- yield* to(Products, {
902
- _tag: "upsert",
903
- payload: payload.value
904
- });
905
- break;
906
- }
907
- case "products/delete": {
908
- const triggeredAt = request.headers["x-shopify-triggered-at"];
909
- if (!triggeredAt) return HttpServerResponse.jsonUnsafe({
910
- ok: false,
911
- error: "Missing x-shopify-triggered-at header"
912
- }, { status: 400 });
913
- const payload = yield* decodeWebhookPayload(ProductDeleteWebhookPayloadSchema)(json).pipe(Effect.mapError((cause) => new ConnectorError({
914
- message: "Invalid Shopify webhook payload for products/delete",
915
- cause
916
- })), Effect.match({
917
- onFailure: (error) => ({
918
- _tag: "Error",
919
- error
920
- }),
921
- onSuccess: (value) => ({
922
- _tag: "Success",
923
- value
924
- })
925
- }));
926
- if (payload._tag === "Error") return HttpServerResponse.jsonUnsafe({
927
- ok: false,
928
- error: payload.error.message
929
- }, { status: 400 });
930
- yield* to(Products, {
931
- _tag: "delete",
932
- id: String(payload.value.id),
933
- version: triggeredAt
934
- });
935
- break;
936
- }
937
- case "carts/create":
938
- case "carts/update": {
939
- const payload = yield* decodeWebhookPayload(CartWebhookPayloadSchema)(json).pipe(Effect.mapError((cause) => new ConnectorError({
940
- message: `Invalid Shopify webhook payload for ${topic}`,
941
- cause
942
- })), Effect.match({
943
- onFailure: (error) => ({
944
- _tag: "Error",
945
- error
946
- }),
947
- onSuccess: (value) => ({
948
- _tag: "Success",
949
- value
950
- })
951
- }));
952
- if (payload._tag === "Error") return HttpServerResponse.jsonUnsafe({
953
- ok: false,
954
- error: payload.error.message
955
- }, { status: 400 });
956
- yield* to(CartEvents, {
957
- ...payload.value,
958
- topic
959
- });
960
- break;
961
- }
962
- default: yield* Effect.logWarning("Ignoring unknown Shopify webhook topic").pipe(Effect.annotateLogs({ topic }));
963
- }
964
- return HttpServerResponse.jsonUnsafe({ ok: true });
965
- })
966
- });
967
- return Connector.define({
968
- name: "producer-shopify",
969
- title: "Shopify",
970
- resources: [Products, CartEvents],
971
- webhooks: [webhookRoute]
972
- });
973
- });
974
- var ShopifyConnector = class extends Context.Service()("@useairfoil/producer-shopify/ShopifyConnector") {};
975
- const layer = (config) => {
976
- const authLayer = layer$2(config);
977
- const apiLayer = layer$1(config).pipe(Layer.provide(authLayer));
978
- return Layer.effect(ShopifyConnector)(make(config).pipe(Effect.annotateLogs({ component: "producer-shopify" }))).pipe(Layer.provide(apiLayer));
979
- };
980
- const layerConfig = (config) => Layer.unwrap(Config.unwrap(config).pipe(Effect.map(layer)));
981
-
982
- //#endregion
983
- export { ProductWebhookPayloadSchema as _, CartEventSchema as a, MoneyBagSchema as c, ProductDeleteWebhookPayloadSchema as d, ProductOptionSchema as f, ProductVariantSchema as g, ProductVariantInventoryPolicySchema as h, api_exports as i, MoneySchema as l, ProductStatusSchema as m, connector_exports as n, CartLineItemSchema as o, ProductSchema as p, layerConfig as r, CartWebhookPayloadSchema as s, ShopifyConnector as t, PageInfoSchema as u, ShopifyNormalize as v, WebhookPayloadSchema as y };
984
- //# sourceMappingURL=src-DqmiLrYS.js.map