@useairfoil/producer-shopify 0.2.0 → 0.2.1

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 (60) hide show
  1. package/README.md +85 -248
  2. package/dist/index.d.mts +6993 -0
  3. package/dist/index.mjs +3 -0
  4. package/dist/main.d.mts +1 -0
  5. package/dist/main.mjs +698 -0
  6. package/dist/main.mjs.map +1 -0
  7. package/dist/manifest.d.mts +32 -0
  8. package/dist/manifest.mjs +51 -0
  9. package/dist/manifest.mjs.map +1 -0
  10. package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
  11. package/dist/src-Bgvuhcfk.mjs +2473 -0
  12. package/dist/src-Bgvuhcfk.mjs.map +1 -0
  13. package/package.json +19 -19
  14. package/src/{auth.ts → api/auth.ts} +15 -35
  15. package/src/api/client.ts +429 -0
  16. package/src/api/graphql.ts +221 -0
  17. package/src/api/http.ts +25 -0
  18. package/src/connector.ts +266 -163
  19. package/src/create-table.ts +5 -0
  20. package/src/demo/customers.ts +412 -0
  21. package/src/demo/index.ts +31 -0
  22. package/src/demo/setup.ts +63 -0
  23. package/src/demo/shared.ts +114 -0
  24. package/src/index.ts +35 -21
  25. package/src/main.ts +8 -8
  26. package/src/manifest.ts +4 -55
  27. package/src/resources/carts/row.ts +58 -0
  28. package/src/resources/carts/webhook.ts +98 -0
  29. package/src/resources/customers/graphql.ts +175 -0
  30. package/src/resources/customers/row.ts +112 -0
  31. package/src/resources/customers/webhook.ts +30 -0
  32. package/src/resources/orders/graphql.ts +419 -0
  33. package/src/resources/orders/row.ts +180 -0
  34. package/src/resources/orders/webhook.ts +248 -0
  35. package/src/resources/products/graphql.ts +142 -0
  36. package/src/resources/products/row.ts +92 -0
  37. package/src/resources/products/webhook.ts +130 -0
  38. package/src/resources/refunds/graphql.ts +196 -0
  39. package/src/resources/refunds/row.ts +97 -0
  40. package/src/resources/refunds/webhook.ts +123 -0
  41. package/src/resources/shared.ts +293 -0
  42. package/src/sandbox.ts +3 -3
  43. package/src/start.ts +3 -5
  44. package/src/tables.ts +15 -0
  45. package/dist/index.d.ts +0 -965
  46. package/dist/index.js +0 -4
  47. package/dist/main.d.ts +0 -1
  48. package/dist/main.js +0 -49
  49. package/dist/main.js.map +0 -1
  50. package/dist/manifest-BFfx2LkS.js +0 -91
  51. package/dist/manifest-BFfx2LkS.js.map +0 -1
  52. package/dist/manifest-CPOEExQU.d.ts +0 -34
  53. package/dist/manifest.d.ts +0 -2
  54. package/dist/manifest.js +0 -3
  55. package/dist/src-DqmiLrYS.js +0 -984
  56. package/dist/src-DqmiLrYS.js.map +0 -1
  57. package/src/api.ts +0 -636
  58. package/src/schemas.ts +0 -319
  59. package/src/throttle.ts +0 -89
  60. package/src/transient-retry.ts +0 -74
package/src/main.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
2
2
  import { Effect, Layer } from "effect";
3
- import { Command } from "effect/unstable/cli";
4
- import { FetchHttpClient } from "effect/unstable/http";
3
+ import { Command } from "effect/cli";
4
+ import { FetchHttpClient } from "effect/http";
5
5
 
6
6
  import packageJson from "../package.json";
7
+ import { createTableCommand } from "./create-table";
8
+ import { demoCommand } from "./demo";
7
9
  import { sandboxCommand } from "./sandbox";
8
10
  import { startCommand } from "./start";
9
11
 
10
12
  const BootstrapLayer = Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer);
11
13
 
12
14
  const program = Command.make("producer-shopify", {}, () => Effect.void).pipe(
13
- Command.withSubcommands([startCommand, sandboxCommand]),
15
+ Command.withSubcommands([startCommand, sandboxCommand, createTableCommand, demoCommand]),
14
16
  );
15
17
 
16
- Command.run(program, { version: packageJson.version }).pipe(
17
- Effect.provide(BootstrapLayer),
18
- Effect.scoped,
19
- NodeRuntime.runMain,
20
- );
18
+ const cli = Command.run(program, { version: packageJson.version });
19
+
20
+ NodeRuntime.runMain(cli.pipe(Effect.provide(BootstrapLayer), Effect.scoped));
package/src/manifest.ts CHANGED
@@ -5,11 +5,6 @@ export const ShopifyConfigDef = Manifest.defineConfig({
5
5
  runtimeKey: "SHOPIFY_SHOP_DOMAIN",
6
6
  description: "Shopify shop domain, for example example.myshopify.com.",
7
7
  }),
8
- apiVersion: Manifest.string({
9
- runtimeKey: "SHOPIFY_API_VERSION",
10
- description: "Shopify Admin API version used in GraphQL endpoint URLs.",
11
- default: "2026-07",
12
- }),
13
8
  clientId: Manifest.string({
14
9
  runtimeKey: "SHOPIFY_CLIENT_ID",
15
10
  description: "Client ID for the merchant-owned Shopify Dev Dashboard app.",
@@ -18,55 +13,6 @@ export const ShopifyConfigDef = Manifest.defineConfig({
18
13
  runtimeKey: "SHOPIFY_CLIENT_SECRET",
19
14
  description: "Client secret for the merchant-owned Shopify Dev Dashboard app.",
20
15
  }),
21
- responseMaxRetries: Manifest.number({
22
- runtimeKey: "SHOPIFY_RESPONSE_MAX_RETRIES",
23
- description: "Maximum retries for temporary HTTP responses.",
24
- default: 5,
25
- integer: true,
26
- minimum: 0,
27
- }),
28
- transportMaxRetries: Manifest.number({
29
- runtimeKey: "SHOPIFY_TRANSPORT_MAX_RETRIES",
30
- description: "Maximum retries for connection and transport failures.",
31
- default: 5,
32
- integer: true,
33
- minimum: 0,
34
- }),
35
- graphqlMaxRetries: Manifest.number({
36
- runtimeKey: "SHOPIFY_GRAPHQL_MAX_RETRIES",
37
- description: "Maximum retries for temporary GraphQL errors.",
38
- default: 5,
39
- integer: true,
40
- minimum: 0,
41
- }),
42
- retryBaseDelayMs: Manifest.number({
43
- runtimeKey: "SHOPIFY_RETRY_BASE_DELAY_MS",
44
- description: "Initial delay for HTTP and transport retries.",
45
- default: 200,
46
- integer: true,
47
- minimum: 1,
48
- }),
49
- graphqlRetryBaseDelayMs: Manifest.number({
50
- runtimeKey: "SHOPIFY_GRAPHQL_RETRY_BASE_DELAY_MS",
51
- description: "Initial delay for temporary GraphQL errors.",
52
- default: 500,
53
- integer: true,
54
- minimum: 1,
55
- }),
56
- retryAfterFallbackSeconds: Manifest.number({
57
- runtimeKey: "SHOPIFY_RETRY_AFTER_FALLBACK_SECONDS",
58
- description: "Retry delay used when Shopify omits Retry-After.",
59
- default: 1,
60
- integer: true,
61
- minimum: 1,
62
- }),
63
- requestTimeoutSeconds: Manifest.number({
64
- runtimeKey: "SHOPIFY_REQUEST_TIMEOUT_SECONDS",
65
- description: "Maximum request time including retries.",
66
- default: 120,
67
- integer: true,
68
- minimum: 1,
69
- }),
70
16
  webhookSecret: Manifest.secret({
71
17
  runtimeKey: "SHOPIFY_WEBHOOK_SECRET",
72
18
  description: "Shopify webhook HMAC secret.",
@@ -81,6 +27,9 @@ export const manifest = Manifest.define({
81
27
  config: ShopifyConfigDef.spec,
82
28
  resources: [
83
29
  { name: "products", capabilities: ["backfill", "webhook"] },
84
- { name: "cart_events", capabilities: ["webhook"] },
30
+ { name: "carts", capabilities: ["webhook"] },
31
+ { name: "customers", capabilities: ["backfill", "webhook"] },
32
+ { name: "orders", capabilities: ["backfill", "webhook"] },
33
+ { name: "refunds", capabilities: ["backfill", "webhook"] },
85
34
  ],
86
35
  });
@@ -0,0 +1,58 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import * as Schema from "effect/Schema";
3
+
4
+ import { field, moneyBag } from "../shared";
5
+
6
+ export const CartLineItemSchema = Schema.Struct({
7
+ id: Schema.String.pipe(field(102, "Unique cart line identifier.")),
8
+ properties: Schema.NullOr(Schema.String).pipe(
9
+ field(103, "Custom line properties stored as JSON."),
10
+ ),
11
+ quantity: Schema.Int.pipe(field(104, "Quantity of the cart line.")),
12
+ variantId: Schema.String.pipe(field(105, "Product variant identifier.")),
13
+ key: Schema.String.pipe(field(106, "Unique key for the cart line.")),
14
+ discountedPrice: Schema.String.pipe(field(107, "Price after discounts.")),
15
+ discounts: Schema.String.pipe(field(108, "Line discounts stored as JSON.")),
16
+ giftCard: Schema.Boolean.pipe(field(109, "Whether the line is a gift card.")),
17
+ grams: Schema.Int.pipe(field(110, "Weight of the line in grams.")),
18
+ linePrice: Schema.String.pipe(field(111, "Total price for the line.")),
19
+ originalLinePrice: Schema.String.pipe(field(112, "Line price before discounts.")),
20
+ originalPrice: Schema.String.pipe(field(113, "Unit price before discounts.")),
21
+ price: Schema.String.pipe(field(114, "Current unit price.")),
22
+ productId: Schema.String.pipe(field(115, "Product identifier.")),
23
+ sku: Schema.NullOr(Schema.String).pipe(field(116, "Stock keeping unit for the line.")),
24
+ taxable: Schema.Boolean.pipe(field(117, "Whether the line is taxable.")),
25
+ title: Schema.String.pipe(field(118, "Title of the cart line.")),
26
+ totalDiscount: Schema.String.pipe(field(119, "Total discount for the line.")),
27
+ vendor: Schema.String.pipe(field(120, "Vendor of the product.")),
28
+ discountedPriceSet: moneyBag(200).pipe(field(121, "Price after discounts.")),
29
+ linePriceSet: moneyBag(210).pipe(field(122, "Total price for the line.")),
30
+ originalLinePriceSet: moneyBag(220).pipe(field(123, "Line price before discounts.")),
31
+ priceSet: moneyBag(230).pipe(field(124, "Current unit price.")),
32
+ totalDiscountSet: moneyBag(240).pipe(field(125, "Total discount for the line.")),
33
+ parentRelationship: Schema.NullOr(Schema.String).pipe(
34
+ field(126, "Parent relationship stored as JSON."),
35
+ ),
36
+ });
37
+
38
+ export const CartSchema = Schema.Struct({
39
+ id: Schema.String.pipe(field(1, "Unique cart identifier.")),
40
+ token: Schema.String.pipe(field(2, "Token for the cart.")),
41
+ topic: Schema.Literals(["carts/create", "carts/update"]).pipe(
42
+ field(3, "Webhook topic that last changed the cart."),
43
+ ),
44
+ lineItems: Schema.Array(CartLineItemSchema.annotate({ fieldId: 101 })).pipe(
45
+ field(4, "Items in the cart."),
46
+ ),
47
+ note: Schema.NullOr(Schema.String).pipe(field(5, "Note added to the cart.")),
48
+ updatedAt: Schema.Date.pipe(field(6, "Time when the cart was last changed.")),
49
+ createdAt: Schema.Date.pipe(field(7, "Time when the cart was created.")),
50
+ })
51
+ .annotate({
52
+ description: "Latest state of each cart in a Shopify store.",
53
+ })
54
+ .pipe(Iceberg.identifier("id"));
55
+
56
+ export type CartLineItem = Schema.Schema.Type<typeof CartLineItemSchema>;
57
+
58
+ export type Cart = Schema.Schema.Type<typeof CartSchema>;
@@ -0,0 +1,98 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ import type { Cart, CartLineItem } from "./row";
4
+
5
+ import { RestMoneyBagSchema, fromRest, gid, normalizeAmount } from "../shared";
6
+
7
+ const JsonStringSchema = Schema.flip(Schema.fromJsonString(Schema.Unknown));
8
+
9
+ const CartWebhookLineItemSchema = Schema.Struct({
10
+ id: Schema.Number,
11
+ properties: Schema.Union([Schema.Null, JsonStringSchema]),
12
+ quantity: Schema.Int,
13
+ variant_id: Schema.Number,
14
+ key: Schema.String,
15
+ discounted_price: Schema.String,
16
+ discounts: JsonStringSchema,
17
+ gift_card: Schema.Boolean,
18
+ grams: Schema.Int,
19
+ line_price: Schema.String,
20
+ original_line_price: Schema.String,
21
+ original_price: Schema.String,
22
+ price: Schema.String,
23
+ product_id: Schema.Number,
24
+ sku: Schema.NullOr(Schema.String),
25
+ taxable: Schema.Boolean,
26
+ title: Schema.String,
27
+ total_discount: Schema.String,
28
+ vendor: Schema.String,
29
+ discounted_price_set: RestMoneyBagSchema,
30
+ line_price_set: RestMoneyBagSchema,
31
+ original_line_price_set: RestMoneyBagSchema,
32
+ price_set: RestMoneyBagSchema,
33
+ total_discount_set: RestMoneyBagSchema,
34
+ parent_relationship: Schema.Union([Schema.Null, JsonStringSchema]),
35
+ });
36
+
37
+ export const CartWebhookPayloadSchema = Schema.Struct({
38
+ id: Schema.String,
39
+ token: Schema.String,
40
+ line_items: Schema.Array(CartWebhookLineItemSchema),
41
+ note: Schema.NullOr(Schema.String),
42
+ updated_at: Schema.DateFromString,
43
+ created_at: Schema.DateFromString,
44
+ });
45
+
46
+ export const CartWebhookEventSchema = Schema.Struct({
47
+ id: Schema.String,
48
+ token: Schema.String,
49
+ line_items: Schema.Array(Schema.toType(CartWebhookLineItemSchema)),
50
+ note: Schema.NullOr(Schema.String),
51
+ updated_at: Schema.Date,
52
+ created_at: Schema.Date,
53
+ topic: Schema.Literals(["carts/create", "carts/update"]),
54
+ });
55
+
56
+ export type CartWebhookLineItem = Schema.Schema.Type<typeof CartWebhookLineItemSchema>;
57
+
58
+ export type CartWebhookPayload = Schema.Schema.Type<typeof CartWebhookPayloadSchema>;
59
+
60
+ export type CartWebhookEvent = Schema.Schema.Type<typeof CartWebhookEventSchema>;
61
+
62
+ const normalizeCartLineItem = (item: CartWebhookLineItem): CartLineItem => ({
63
+ id: String(item.id),
64
+ properties: item.properties,
65
+ quantity: item.quantity,
66
+ variantId: gid("ProductVariant", item.variant_id),
67
+ key: item.key,
68
+ discountedPrice: normalizeAmount(item.discounted_price),
69
+ discounts: item.discounts,
70
+ giftCard: item.gift_card,
71
+ grams: item.grams,
72
+ linePrice: normalizeAmount(item.line_price),
73
+ originalLinePrice: normalizeAmount(item.original_line_price),
74
+ originalPrice: normalizeAmount(item.original_price),
75
+ price: normalizeAmount(item.price),
76
+ productId: gid("Product", item.product_id),
77
+ sku: item.sku,
78
+ taxable: item.taxable,
79
+ title: item.title,
80
+ totalDiscount: normalizeAmount(item.total_discount),
81
+ vendor: item.vendor,
82
+ discountedPriceSet: fromRest.money(item.discounted_price_set),
83
+ linePriceSet: fromRest.money(item.line_price_set),
84
+ originalLinePriceSet: fromRest.money(item.original_line_price_set),
85
+ priceSet: fromRest.money(item.price_set),
86
+ totalDiscountSet: fromRest.money(item.total_discount_set),
87
+ parentRelationship: item.parent_relationship,
88
+ });
89
+
90
+ export const fromCartWebhook = (payload: CartWebhookEvent): Cart => ({
91
+ id: payload.id,
92
+ token: payload.token,
93
+ topic: payload.topic,
94
+ lineItems: payload.line_items.map(normalizeCartLineItem),
95
+ note: payload.note ?? "",
96
+ updatedAt: payload.updated_at,
97
+ createdAt: payload.created_at,
98
+ });
@@ -0,0 +1,175 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ import {
4
+ AddressFields,
5
+ GraphQLAddressSchema,
6
+ connection,
7
+ emptyText,
8
+ fromGraphQL,
9
+ normalizeAmount,
10
+ pageQuery,
11
+ } from "../shared";
12
+ import {
13
+ ConsentCollectedFromSchema,
14
+ type Customer,
15
+ CustomerStateSchema,
16
+ EmailMarketingStateSchema,
17
+ MarketingOptInLevelSchema,
18
+ ProductSubscriberStatusSchema,
19
+ SmsMarketingStateSchema,
20
+ } from "./row";
21
+
22
+ const CustomerAddressNodeFields = `id ${AddressFields}`;
23
+
24
+ export const CustomerFields = `
25
+ id
26
+ legacyResourceId
27
+ firstName
28
+ lastName
29
+ displayName
30
+ note
31
+ state
32
+ verifiedEmail
33
+ taxExempt
34
+ taxExemptions
35
+ tags
36
+ locale
37
+ numberOfOrders
38
+ amountSpent { amount currencyCode }
39
+ lastOrder { id }
40
+ productSubscriberStatus
41
+ dataSaleOptOut
42
+ defaultEmailAddress { emailAddress marketingState marketingOptInLevel marketingUpdatedAt }
43
+ defaultPhoneNumber { phoneNumber }
44
+ smsMarketingConsent { marketingState marketingOptInLevel consentUpdatedAt consentCollectedFrom }
45
+ defaultAddress { id }
46
+ addressesV2(first: 25) {
47
+ nodes { ${CustomerAddressNodeFields} }
48
+ pageInfo { hasNextPage endCursor }
49
+ }
50
+ createdAt
51
+ updatedAt
52
+ `;
53
+
54
+ export const CustomerAddressesQuery = `#graphql
55
+ query AirfoilCustomerAddresses($id: ID!, $first: Int!, $after: String) {
56
+ node: customer(id: $id) {
57
+ connection: addressesV2(first: $first, after: $after) {
58
+ nodes { ${CustomerAddressNodeFields} }
59
+ pageInfo { hasNextPage endCursor }
60
+ }
61
+ }
62
+ }
63
+ `;
64
+
65
+ export const CustomerByIdQuery = `#graphql
66
+ query AirfoilCustomerById($id: ID!) {
67
+ customer(id: $id) { ${CustomerFields} }
68
+ }
69
+ `;
70
+
71
+ const GraphQLCustomerAddressSchema = Schema.Struct({
72
+ id: Schema.String,
73
+ ...GraphQLAddressSchema.fields,
74
+ });
75
+
76
+ export const GraphQLCustomerAddressesSchema = connection(GraphQLCustomerAddressSchema);
77
+
78
+ export const GraphQLCustomerNodeSchema = Schema.Struct({
79
+ id: Schema.String,
80
+ legacyResourceId: Schema.String,
81
+ firstName: Schema.NullOr(Schema.String),
82
+ lastName: Schema.NullOr(Schema.String),
83
+ displayName: Schema.String,
84
+ note: Schema.NullOr(Schema.String),
85
+ state: CustomerStateSchema,
86
+ verifiedEmail: Schema.Boolean,
87
+ taxExempt: Schema.Boolean,
88
+ taxExemptions: Schema.Array(Schema.String),
89
+ tags: Schema.Array(Schema.String),
90
+ locale: Schema.String,
91
+ numberOfOrders: Schema.NumberFromString,
92
+ amountSpent: Schema.Struct({ amount: Schema.String, currencyCode: Schema.String }),
93
+ lastOrder: Schema.NullOr(Schema.Struct({ id: Schema.String })),
94
+ productSubscriberStatus: ProductSubscriberStatusSchema,
95
+ dataSaleOptOut: Schema.Boolean,
96
+ defaultEmailAddress: Schema.NullOr(
97
+ Schema.Struct({
98
+ emailAddress: Schema.String,
99
+ marketingState: EmailMarketingStateSchema,
100
+ marketingOptInLevel: Schema.NullOr(MarketingOptInLevelSchema),
101
+ marketingUpdatedAt: Schema.NullOr(Schema.DateFromString),
102
+ }),
103
+ ),
104
+ defaultPhoneNumber: Schema.NullOr(Schema.Struct({ phoneNumber: Schema.String })),
105
+ smsMarketingConsent: Schema.NullOr(
106
+ Schema.Struct({
107
+ marketingState: SmsMarketingStateSchema,
108
+ marketingOptInLevel: MarketingOptInLevelSchema,
109
+ consentUpdatedAt: Schema.NullOr(Schema.DateFromString),
110
+ consentCollectedFrom: Schema.NullOr(ConsentCollectedFromSchema),
111
+ }),
112
+ ),
113
+ defaultAddress: Schema.NullOr(Schema.Struct({ id: Schema.String })),
114
+ addressesV2: GraphQLCustomerAddressesSchema,
115
+ createdAt: Schema.DateFromString,
116
+ updatedAt: Schema.DateFromString,
117
+ });
118
+
119
+ export type GraphQLCustomerNode = Schema.Schema.Type<typeof GraphQLCustomerNodeSchema>;
120
+
121
+ export type GraphQLCustomerAddress = Schema.Schema.Type<typeof GraphQLCustomerAddressSchema>;
122
+
123
+ const addressId = (value: string): string => value.replace(/\?.*$/, "");
124
+
125
+ export const normalizeCustomerNode = (
126
+ node: GraphQLCustomerNode,
127
+ addresses: ReadonlyArray<GraphQLCustomerAddress>,
128
+ ): Customer => ({
129
+ id: node.id,
130
+ legacyResourceId: node.legacyResourceId,
131
+ firstName: emptyText(node.firstName),
132
+ lastName: emptyText(node.lastName),
133
+ displayName: node.displayName,
134
+ email: node.defaultEmailAddress?.emailAddress ?? "",
135
+ emailMarketingState: node.defaultEmailAddress?.marketingState ?? null,
136
+ emailMarketingOptInLevel: node.defaultEmailAddress?.marketingOptInLevel ?? null,
137
+ emailMarketingUpdatedAt: node.defaultEmailAddress?.marketingUpdatedAt ?? null,
138
+ phone: node.defaultPhoneNumber?.phoneNumber ?? "",
139
+ smsMarketingState: node.smsMarketingConsent?.marketingState ?? null,
140
+ smsMarketingOptInLevel: node.smsMarketingConsent?.marketingOptInLevel ?? null,
141
+ smsMarketingUpdatedAt: node.smsMarketingConsent?.consentUpdatedAt ?? null,
142
+ smsMarketingCollectedFrom: node.smsMarketingConsent?.consentCollectedFrom ?? null,
143
+ note: emptyText(node.note),
144
+ state: node.state,
145
+ verifiedEmail: node.verifiedEmail,
146
+ taxExempt: node.taxExempt,
147
+ taxExemptions: node.taxExemptions,
148
+ tags: node.tags,
149
+ locale: node.locale,
150
+ numberOfOrders: node.numberOfOrders,
151
+ amountSpent: {
152
+ amount: normalizeAmount(node.amountSpent.amount),
153
+ currencyCode: node.amountSpent.currencyCode,
154
+ },
155
+ lastOrderId: node.lastOrder?.id ?? "",
156
+ productSubscriberStatus: node.productSubscriberStatus,
157
+ dataSaleOptOut: node.dataSaleOptOut,
158
+ defaultAddressId: node.defaultAddress === null ? "" : addressId(node.defaultAddress.id),
159
+ addresses: addresses.map(({ id, ...value }) => ({
160
+ id: addressId(id),
161
+ ...fromGraphQL.address(value),
162
+ })),
163
+ createdAt: node.createdAt,
164
+ updatedAt: node.updatedAt,
165
+ });
166
+
167
+ export const CustomersQuery = pageQuery("AirfoilCustomers", "customers", CustomerFields);
168
+
169
+ export const GraphQLCustomersDataSchema = Schema.Struct({
170
+ customers: connection(GraphQLCustomerNodeSchema),
171
+ });
172
+
173
+ export const GraphQLCustomerByIdDataSchema = Schema.Struct({
174
+ customer: Schema.NullOr(GraphQLCustomerNodeSchema),
175
+ });
@@ -0,0 +1,112 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import * as Schema from "effect/Schema";
3
+
4
+ import { address, field } from "../shared";
5
+
6
+ export const CustomerStateSchema = Schema.Literals(["DECLINED", "DISABLED", "ENABLED", "INVITED"]);
7
+
8
+ export const EmailMarketingStateSchema = Schema.Literals([
9
+ "INVALID",
10
+ "NOT_SUBSCRIBED",
11
+ "PENDING",
12
+ "REDACTED",
13
+ "SUBSCRIBED",
14
+ "UNSUBSCRIBED",
15
+ ]);
16
+
17
+ export const SmsMarketingStateSchema = Schema.Literals([
18
+ "NOT_SUBSCRIBED",
19
+ "PENDING",
20
+ "SUBSCRIBED",
21
+ "UNSUBSCRIBED",
22
+ "REDACTED",
23
+ ]);
24
+
25
+ export const MarketingOptInLevelSchema = Schema.Literals([
26
+ "SINGLE_OPT_IN",
27
+ "CONFIRMED_OPT_IN",
28
+ "UNKNOWN",
29
+ ]);
30
+
31
+ export const ConsentCollectedFromSchema = Schema.Literals(["SHOPIFY", "OTHER"]);
32
+
33
+ export const ProductSubscriberStatusSchema = Schema.Literals([
34
+ "ACTIVE",
35
+ "CANCELLED",
36
+ "EXPIRED",
37
+ "FAILED",
38
+ "NEVER_SUBSCRIBED",
39
+ "PAUSED",
40
+ ]);
41
+
42
+ export const CustomerAddressSchema = Schema.Struct({
43
+ id: Schema.String.pipe(field(121, "Unique address identifier.")),
44
+ ...address(121).fields,
45
+ });
46
+
47
+ export const CustomerSchema = Schema.Struct({
48
+ id: Schema.String.pipe(field(1, "Unique customer identifier.")),
49
+ legacyResourceId: Schema.String.pipe(field(2, "Numeric customer identifier stored as text.")),
50
+ firstName: Schema.NullOr(Schema.String).pipe(field(3, "First name.")),
51
+ lastName: Schema.NullOr(Schema.String).pipe(field(4, "Last name.")),
52
+ displayName: Schema.String.pipe(field(5, "Full name shown in Shopify.")),
53
+ email: Schema.NullOr(Schema.String).pipe(field(6, "Default email address.")),
54
+ emailMarketingState: Schema.NullOr(EmailMarketingStateSchema).pipe(
55
+ field(7, "Email marketing consent state."),
56
+ ),
57
+ emailMarketingOptInLevel: Schema.NullOr(MarketingOptInLevelSchema).pipe(
58
+ field(8, "Email marketing opt-in level."),
59
+ ),
60
+ emailMarketingUpdatedAt: Schema.NullOr(Schema.Date).pipe(
61
+ field(9, "Time when email marketing consent last changed."),
62
+ ),
63
+ phone: Schema.NullOr(Schema.String).pipe(field(10, "Default phone number.")),
64
+ smsMarketingState: Schema.NullOr(SmsMarketingStateSchema).pipe(
65
+ field(11, "SMS marketing consent state."),
66
+ ),
67
+ smsMarketingOptInLevel: Schema.NullOr(MarketingOptInLevelSchema).pipe(
68
+ field(12, "SMS marketing opt-in level."),
69
+ ),
70
+ smsMarketingUpdatedAt: Schema.NullOr(Schema.Date).pipe(
71
+ field(13, "Time when SMS marketing consent last changed."),
72
+ ),
73
+ smsMarketingCollectedFrom: Schema.NullOr(ConsentCollectedFromSchema).pipe(
74
+ field(14, "Where SMS marketing consent was collected."),
75
+ ),
76
+ note: Schema.NullOr(Schema.String).pipe(field(15, "Merchant note about the customer.")),
77
+ state: CustomerStateSchema.pipe(field(16, "Customer account state.")),
78
+ verifiedEmail: Schema.Boolean.pipe(field(17, "Whether the email address is verified.")),
79
+ taxExempt: Schema.Boolean.pipe(field(18, "Whether the customer is exempt from tax.")),
80
+ taxExemptions: Schema.Array(Schema.String.annotate({ fieldId: 100 })).pipe(
81
+ field(19, "Tax exemptions that apply to the customer."),
82
+ ),
83
+ tags: Schema.Array(Schema.String.annotate({ fieldId: 101 })).pipe(
84
+ field(20, "Tags added to the customer."),
85
+ ),
86
+ locale: Schema.NullOr(Schema.String).pipe(field(21, "Customer language.")),
87
+ numberOfOrders: Schema.Int.pipe(field(22, "Number of orders the customer placed.")),
88
+ amountSpent: Schema.Struct({
89
+ amount: Schema.String.pipe(field(111, "Amount spent.")),
90
+ currencyCode: Schema.String.pipe(field(112, "Shop currency code.")),
91
+ }).pipe(field(23, "Total amount spent in the shop currency.")),
92
+ lastOrderId: Schema.NullOr(Schema.String).pipe(field(24, "Most recent order identifier.")),
93
+ productSubscriberStatus: Schema.NullOr(ProductSubscriberStatusSchema).pipe(
94
+ field(25, "Subscription status for products."),
95
+ ),
96
+ dataSaleOptOut: Schema.NullOr(Schema.Boolean).pipe(
97
+ field(26, "Whether the customer opted out of data sale."),
98
+ ),
99
+ defaultAddressId: Schema.NullOr(Schema.String).pipe(field(27, "Default address identifier.")),
100
+ addresses: Schema.Array(CustomerAddressSchema.annotate({ fieldId: 120 })).pipe(
101
+ field(28, "Saved addresses."),
102
+ ),
103
+ createdAt: Schema.Date.pipe(field(29, "Time when the customer was created.")),
104
+ updatedAt: Schema.Date.pipe(field(30, "Time when the customer was last changed.")),
105
+ _deleted: Schema.optional(Schema.Boolean).pipe(field(31, "Whether the customer was deleted.")),
106
+ })
107
+ .annotate({
108
+ description: "Customers in a Shopify store.",
109
+ })
110
+ .pipe(Iceberg.identifier("id"));
111
+
112
+ export type Customer = Schema.Schema.Type<typeof CustomerSchema>;
@@ -0,0 +1,30 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ // Customer webhooks leave fields out, so the handler reads the customer again.
4
+
5
+ export const CustomerWebhookPayloadSchema = Schema.Struct({
6
+ admin_graphql_api_id: Schema.String,
7
+ updated_at: Schema.DateFromString,
8
+ });
9
+
10
+ export const CustomerPurchaseSummaryPayloadSchema = Schema.Struct({
11
+ customerId: Schema.String,
12
+ occurredAt: Schema.DateFromString,
13
+ });
14
+
15
+ export const CustomerEmailConsentPayloadSchema = Schema.Struct({ customer_id: Schema.Number });
16
+
17
+ export const CustomerSmsConsentPayloadSchema = Schema.Struct({ id: Schema.Number });
18
+
19
+ export const CustomerTagsPayloadSchema = Schema.Struct({
20
+ customerId: Schema.String,
21
+ occurredAt: Schema.DateFromString,
22
+ });
23
+
24
+ export const CustomerDeleteWebhookPayloadSchema = Schema.Struct({ id: Schema.Number });
25
+
26
+ export const CustomerEventSchema = Schema.Struct({
27
+ _tag: Schema.Literals(["refresh", "delete"]),
28
+ id: Schema.String,
29
+ version: Schema.Date,
30
+ });