@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.
- package/README.md +85 -248
- package/dist/index.d.mts +6993 -0
- package/dist/index.mjs +3 -0
- package/dist/main.d.mts +1 -0
- package/dist/main.mjs +698 -0
- package/dist/main.mjs.map +1 -0
- package/dist/manifest.d.mts +32 -0
- package/dist/manifest.mjs +51 -0
- package/dist/manifest.mjs.map +1 -0
- package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
- package/dist/src-Bgvuhcfk.mjs +2473 -0
- package/dist/src-Bgvuhcfk.mjs.map +1 -0
- package/package.json +19 -19
- package/src/{auth.ts → api/auth.ts} +15 -35
- package/src/api/client.ts +429 -0
- package/src/api/graphql.ts +221 -0
- package/src/api/http.ts +25 -0
- package/src/connector.ts +266 -163
- package/src/create-table.ts +5 -0
- package/src/demo/customers.ts +412 -0
- package/src/demo/index.ts +31 -0
- package/src/demo/setup.ts +63 -0
- package/src/demo/shared.ts +114 -0
- package/src/index.ts +35 -21
- package/src/main.ts +8 -8
- package/src/manifest.ts +4 -55
- package/src/resources/carts/row.ts +58 -0
- package/src/resources/carts/webhook.ts +98 -0
- package/src/resources/customers/graphql.ts +175 -0
- package/src/resources/customers/row.ts +112 -0
- package/src/resources/customers/webhook.ts +30 -0
- package/src/resources/orders/graphql.ts +419 -0
- package/src/resources/orders/row.ts +180 -0
- package/src/resources/orders/webhook.ts +248 -0
- package/src/resources/products/graphql.ts +142 -0
- package/src/resources/products/row.ts +92 -0
- package/src/resources/products/webhook.ts +130 -0
- package/src/resources/refunds/graphql.ts +196 -0
- package/src/resources/refunds/row.ts +97 -0
- package/src/resources/refunds/webhook.ts +123 -0
- package/src/resources/shared.ts +293 -0
- package/src/sandbox.ts +3 -3
- package/src/start.ts +3 -5
- package/src/tables.ts +15 -0
- package/dist/index.d.ts +0 -965
- package/dist/index.js +0 -4
- package/dist/main.d.ts +0 -1
- package/dist/main.js +0 -49
- package/dist/main.js.map +0 -1
- package/dist/manifest-BFfx2LkS.js +0 -91
- package/dist/manifest-BFfx2LkS.js.map +0 -1
- package/dist/manifest-CPOEExQU.d.ts +0 -34
- package/dist/manifest.d.ts +0 -2
- package/dist/manifest.js +0 -3
- package/dist/src-DqmiLrYS.js +0 -984
- package/dist/src-DqmiLrYS.js.map +0 -1
- package/src/api.ts +0 -636
- package/src/schemas.ts +0 -319
- package/src/throttle.ts +0 -89
- package/src/transient-retry.ts +0 -74
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Auth } from "@useairfoil/connector-kit";
|
|
2
2
|
import { Config, Context, Data, Duration, Effect, Layer, Redacted, Schema } from "effect";
|
|
3
|
-
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/
|
|
3
|
+
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
4
4
|
|
|
5
|
-
import type { ShopifyConfig } from "
|
|
5
|
+
import type { ShopifyConfig } from "../manifest";
|
|
6
6
|
|
|
7
7
|
type ShopifyAuthConfig = Pick<ShopifyConfig, "shopDomain" | "clientId" | "clientSecret">;
|
|
8
8
|
|
|
@@ -25,52 +25,32 @@ const TokenResponseSchema = Schema.Struct({
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const ErrorResponseSchema = Schema.Struct({
|
|
28
|
-
error: Schema.
|
|
28
|
+
error: Schema.Literals(["shop_not_permitted", "invalid_client"]),
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
shopDomain.replace(/^https?:\/\//i, "").replace(/\/+$/g, "")
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
reason,
|
|
39
|
-
message: "The Shopify app and store must belong to the same Shopify organization",
|
|
40
|
-
});
|
|
41
|
-
case "invalid_client":
|
|
42
|
-
return new ShopifyAuthError({
|
|
43
|
-
reason,
|
|
44
|
-
message: "The Shopify client ID or client secret is invalid",
|
|
45
|
-
});
|
|
46
|
-
case "exchange_failed":
|
|
47
|
-
return new ShopifyAuthError({
|
|
48
|
-
reason,
|
|
49
|
-
message: "Shopify authentication failed",
|
|
50
|
-
});
|
|
51
|
-
}
|
|
31
|
+
export const shopUrl = (shopDomain: string): string =>
|
|
32
|
+
`https://${shopDomain.replace(/^https?:\/\//i, "").replace(/\/+$/g, "")}`;
|
|
33
|
+
|
|
34
|
+
const errorMessages: Record<ShopifyAuthErrorReason, string> = {
|
|
35
|
+
shop_not_permitted: "The Shopify app and store must belong to the same Shopify organization",
|
|
36
|
+
invalid_client: "The Shopify client ID or client secret is invalid",
|
|
37
|
+
exchange_failed: "Shopify authentication failed",
|
|
52
38
|
};
|
|
53
39
|
|
|
40
|
+
const errorFor = (reason: ShopifyAuthErrorReason): ShopifyAuthError =>
|
|
41
|
+
new ShopifyAuthError({ reason, message: errorMessages[reason] });
|
|
42
|
+
|
|
54
43
|
const decodeRejectedResponse = (
|
|
55
44
|
response: HttpClientResponse.HttpClientResponse,
|
|
56
45
|
): Effect.Effect<ShopifyAuthErrorReason> =>
|
|
57
46
|
HttpClientResponse.schemaBodyJson(ErrorResponseSchema)(response).pipe(
|
|
58
|
-
Effect.map((body) =>
|
|
59
|
-
switch (body.error) {
|
|
60
|
-
case "shop_not_permitted":
|
|
61
|
-
return "shop_not_permitted";
|
|
62
|
-
case "invalid_client":
|
|
63
|
-
return "invalid_client";
|
|
64
|
-
default:
|
|
65
|
-
return "exchange_failed";
|
|
66
|
-
}
|
|
67
|
-
}),
|
|
47
|
+
Effect.map((body) => body.error),
|
|
68
48
|
Effect.catch(() => Effect.succeed<ShopifyAuthErrorReason>("exchange_failed")),
|
|
69
49
|
);
|
|
70
50
|
|
|
71
51
|
export const make = Effect.fnUntraced(function* (config: ShopifyAuthConfig) {
|
|
72
52
|
const client = yield* HttpClient.HttpClient;
|
|
73
|
-
const endpoint =
|
|
53
|
+
const endpoint = `${shopUrl(config.shopDomain)}/admin/oauth/access_token`;
|
|
74
54
|
|
|
75
55
|
return yield* Auth.makeTokenCache({
|
|
76
56
|
acquire: Effect.scoped(
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { ConnectorError } from "@useairfoil/connector-kit";
|
|
2
|
+
import { Config, Context, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/http";
|
|
4
|
+
|
|
5
|
+
import type { ShopifyConfig } from "../manifest";
|
|
6
|
+
import type { Customer } from "../resources/customers/row";
|
|
7
|
+
import type { Order } from "../resources/orders/row";
|
|
8
|
+
import type { Product } from "../resources/products/row";
|
|
9
|
+
import type { Refund } from "../resources/refunds/row";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CustomerAddressesQuery,
|
|
13
|
+
CustomerByIdQuery,
|
|
14
|
+
CustomersQuery,
|
|
15
|
+
GraphQLCustomerAddressesSchema,
|
|
16
|
+
GraphQLCustomerByIdDataSchema,
|
|
17
|
+
type GraphQLCustomerNode,
|
|
18
|
+
GraphQLCustomersDataSchema,
|
|
19
|
+
normalizeCustomerNode,
|
|
20
|
+
} from "../resources/customers/graphql";
|
|
21
|
+
import {
|
|
22
|
+
GraphQLDiscountApplicationsSchema,
|
|
23
|
+
GraphQLLineItemsSchema,
|
|
24
|
+
type GraphQLOrderNode,
|
|
25
|
+
GraphQLOrdersDataSchema,
|
|
26
|
+
GraphQLShippingLinesSchema,
|
|
27
|
+
OrderDiscountApplicationsQuery,
|
|
28
|
+
OrderLineItemsQuery,
|
|
29
|
+
OrderShippingLinesQuery,
|
|
30
|
+
OrdersQuery,
|
|
31
|
+
fromOrderNode,
|
|
32
|
+
} from "../resources/orders/graphql";
|
|
33
|
+
import {
|
|
34
|
+
GraphQLProductByIdDataSchema,
|
|
35
|
+
type GraphQLProductNode,
|
|
36
|
+
GraphQLProductVariantNodeSchema,
|
|
37
|
+
GraphQLProductsDataSchema,
|
|
38
|
+
ProductByIdQuery,
|
|
39
|
+
ProductVariantsQuery,
|
|
40
|
+
ProductsQuery,
|
|
41
|
+
normalizeProductNode,
|
|
42
|
+
} from "../resources/products/graphql";
|
|
43
|
+
import {
|
|
44
|
+
GraphQLOrderAdjustmentsSchema,
|
|
45
|
+
GraphQLOrderRefundsDataSchema,
|
|
46
|
+
GraphQLRefundLineItemsSchema,
|
|
47
|
+
type GraphQLRefundNode,
|
|
48
|
+
GraphQLRefundShippingLinesSchema,
|
|
49
|
+
GraphQLRefundTransactionsSchema,
|
|
50
|
+
OrderRefundsQuery,
|
|
51
|
+
RefundLineItemsQuery,
|
|
52
|
+
RefundOrderAdjustmentsQuery,
|
|
53
|
+
RefundShippingLinesQuery,
|
|
54
|
+
RefundTransactionsQuery,
|
|
55
|
+
fromRefundNode,
|
|
56
|
+
} from "../resources/refunds/graphql";
|
|
57
|
+
import { type PageInfo, connection } from "../resources/shared";
|
|
58
|
+
import * as ShopifyAuth from "./auth";
|
|
59
|
+
import * as ShopifyGraphQL from "./graphql";
|
|
60
|
+
|
|
61
|
+
export type ShopifyPage<A> = {
|
|
62
|
+
readonly items: ReadonlyArray<A>;
|
|
63
|
+
readonly endCursor: string | null;
|
|
64
|
+
readonly hasMore: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type PageRequest = { readonly first: number; readonly after?: string };
|
|
68
|
+
|
|
69
|
+
export type ShopifyApiClientService = {
|
|
70
|
+
readonly checkConnection: Effect.Effect<void, ConnectorError>;
|
|
71
|
+
readonly checkProductsAccess: Effect.Effect<void, ConnectorError>;
|
|
72
|
+
readonly checkOrdersAccess: Effect.Effect<void, ConnectorError>;
|
|
73
|
+
readonly checkCustomersAccess: Effect.Effect<void, ConnectorError>;
|
|
74
|
+
readonly fetchGraphQL: <A>(options: {
|
|
75
|
+
readonly operationName: string;
|
|
76
|
+
readonly query: string;
|
|
77
|
+
readonly variables?: Record<string, unknown>;
|
|
78
|
+
readonly schema: Schema.Decoder<A>;
|
|
79
|
+
}) => Effect.Effect<A, ConnectorError>;
|
|
80
|
+
readonly fetchProducts: (
|
|
81
|
+
options: PageRequest,
|
|
82
|
+
) => Effect.Effect<ShopifyPage<Product>, ConnectorError>;
|
|
83
|
+
/** Gets every variant page, or none when the product is gone. */
|
|
84
|
+
readonly fetchProductById: (id: string) => Effect.Effect<Option.Option<Product>, ConnectorError>;
|
|
85
|
+
/** Gets every page of line items, shipping lines, and discounts. */
|
|
86
|
+
readonly fetchOrders: (options: PageRequest) => Effect.Effect<ShopifyPage<Order>, ConnectorError>;
|
|
87
|
+
readonly fetchRefunds: (
|
|
88
|
+
options: PageRequest,
|
|
89
|
+
) => Effect.Effect<ShopifyPage<Refund>, ConnectorError>;
|
|
90
|
+
readonly fetchCustomers: (
|
|
91
|
+
options: PageRequest,
|
|
92
|
+
) => Effect.Effect<ShopifyPage<Customer>, ConnectorError>;
|
|
93
|
+
/** Gets every address page, or none when the customer is gone. */
|
|
94
|
+
readonly fetchCustomerById: (
|
|
95
|
+
id: string,
|
|
96
|
+
) => Effect.Effect<Option.Option<Customer>, ConnectorError>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export class ShopifyApiClient extends Context.Service<ShopifyApiClient, ShopifyApiClientService>()(
|
|
100
|
+
"@useairfoil/producer-shopify/ShopifyApiClient",
|
|
101
|
+
) {}
|
|
102
|
+
|
|
103
|
+
// Protected fields make this fail without protected customer data access.
|
|
104
|
+
const CustomersAccessQuery = `#graphql
|
|
105
|
+
query AirfoilCustomersAccess {
|
|
106
|
+
customers(first: 1) {
|
|
107
|
+
nodes {
|
|
108
|
+
id
|
|
109
|
+
defaultEmailAddress { emailAddress }
|
|
110
|
+
defaultPhoneNumber { phoneNumber }
|
|
111
|
+
defaultAddress { address1 }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
const OrdersAccessQuery = `#graphql
|
|
118
|
+
query AirfoilOrdersAccess {
|
|
119
|
+
orders(first: 1) {
|
|
120
|
+
nodes {
|
|
121
|
+
id
|
|
122
|
+
email
|
|
123
|
+
phone
|
|
124
|
+
shippingAddress { address1 }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
const ProductsAccessQuery = `#graphql
|
|
131
|
+
query AirfoilProductsAccess {
|
|
132
|
+
products(first: 1) {
|
|
133
|
+
nodes {
|
|
134
|
+
id
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
const ShopIdentityQuery = `#graphql
|
|
141
|
+
query AirfoilShopIdentity {
|
|
142
|
+
shop {
|
|
143
|
+
id
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
const ShopIdentitySchema = Schema.Struct({
|
|
149
|
+
shop: Schema.Struct({ id: Schema.String }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const ProductsAccessSchema = Schema.Struct({
|
|
153
|
+
products: Schema.Struct({
|
|
154
|
+
nodes: Schema.Array(Schema.Struct({ id: Schema.String })),
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const OrdersAccessSchema = Schema.Struct({
|
|
159
|
+
orders: Schema.Struct({ nodes: Schema.Array(Schema.Struct({ id: Schema.String })) }),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const CustomersAccessSchema = Schema.Struct({
|
|
163
|
+
customers: Schema.Struct({ nodes: Schema.Array(Schema.Struct({ id: Schema.String })) }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Follow-up queries alias the parent as `node` and the list as `connection`.
|
|
167
|
+
const nestedPageSchema = <S extends Schema.Top>(connection: S) =>
|
|
168
|
+
Schema.Struct({ node: Schema.NullOr(Schema.Struct({ connection })) });
|
|
169
|
+
|
|
170
|
+
export const make = Effect.fnUntraced(function* (config: ShopifyConfig) {
|
|
171
|
+
const fetchGraphQL = yield* ShopifyGraphQL.make(config);
|
|
172
|
+
|
|
173
|
+
const nextPageCursor = (pageInfo: PageInfo): Option.Option<string> =>
|
|
174
|
+
pageInfo.hasNextPage ? Option.fromNullishOr(pageInfo.endCursor) : Option.none();
|
|
175
|
+
|
|
176
|
+
const fetchProductById = (id: string) =>
|
|
177
|
+
fetchGraphQL({
|
|
178
|
+
operationName: "AirfoilProductById",
|
|
179
|
+
query: ProductByIdQuery,
|
|
180
|
+
variables: { id },
|
|
181
|
+
schema: GraphQLProductByIdDataSchema,
|
|
182
|
+
}).pipe(
|
|
183
|
+
Effect.flatMap((data) => {
|
|
184
|
+
const node = data.product;
|
|
185
|
+
if (node === null) {
|
|
186
|
+
return Effect.succeed(Option.none());
|
|
187
|
+
}
|
|
188
|
+
return loadProduct(node).pipe(Effect.map(Option.some));
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const followConnection = <A>(
|
|
193
|
+
first: { readonly nodes: ReadonlyArray<A>; readonly pageInfo: PageInfo },
|
|
194
|
+
options: {
|
|
195
|
+
readonly id: string;
|
|
196
|
+
readonly operationName: string;
|
|
197
|
+
readonly query: string;
|
|
198
|
+
readonly schema: Schema.Decoder<{
|
|
199
|
+
readonly node: {
|
|
200
|
+
readonly connection: { readonly nodes: ReadonlyArray<A>; readonly pageInfo: PageInfo };
|
|
201
|
+
} | null;
|
|
202
|
+
}>;
|
|
203
|
+
},
|
|
204
|
+
): Effect.Effect<ReadonlyArray<A>, ConnectorError> =>
|
|
205
|
+
Option.match(nextPageCursor(first.pageInfo), {
|
|
206
|
+
onNone: () => Effect.succeed(first.nodes),
|
|
207
|
+
onSome: (initialCursor) =>
|
|
208
|
+
Stream.paginate(initialCursor, (after) =>
|
|
209
|
+
fetchGraphQL({
|
|
210
|
+
operationName: options.operationName,
|
|
211
|
+
query: options.query,
|
|
212
|
+
variables: { id: options.id, first: 100, after },
|
|
213
|
+
schema: options.schema,
|
|
214
|
+
}).pipe(
|
|
215
|
+
Effect.flatMap(({ node }) =>
|
|
216
|
+
node === null
|
|
217
|
+
? Effect.fail(
|
|
218
|
+
new ConnectorError({
|
|
219
|
+
message: `Shopify record disappeared during ${options.operationName}`,
|
|
220
|
+
}),
|
|
221
|
+
)
|
|
222
|
+
: Effect.succeed([
|
|
223
|
+
node.connection.nodes,
|
|
224
|
+
nextPageCursor(node.connection.pageInfo),
|
|
225
|
+
] as const),
|
|
226
|
+
),
|
|
227
|
+
),
|
|
228
|
+
).pipe(
|
|
229
|
+
Stream.runCollect,
|
|
230
|
+
Effect.map((rest) => [...first.nodes, ...rest]),
|
|
231
|
+
),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const loadOrder = (node: GraphQLOrderNode) =>
|
|
235
|
+
Effect.all({
|
|
236
|
+
discountApplications: followConnection(node.discountApplications, {
|
|
237
|
+
id: node.id,
|
|
238
|
+
operationName: "AirfoilOrderDiscountApplications",
|
|
239
|
+
query: OrderDiscountApplicationsQuery,
|
|
240
|
+
schema: nestedPageSchema(GraphQLDiscountApplicationsSchema),
|
|
241
|
+
}),
|
|
242
|
+
shippingLines: followConnection(node.shippingLines, {
|
|
243
|
+
id: node.id,
|
|
244
|
+
operationName: "AirfoilOrderShippingLines",
|
|
245
|
+
query: OrderShippingLinesQuery,
|
|
246
|
+
schema: nestedPageSchema(GraphQLShippingLinesSchema),
|
|
247
|
+
}),
|
|
248
|
+
lineItems: followConnection(node.lineItems, {
|
|
249
|
+
id: node.id,
|
|
250
|
+
operationName: "AirfoilOrderLineItems",
|
|
251
|
+
query: OrderLineItemsQuery,
|
|
252
|
+
schema: nestedPageSchema(GraphQLLineItemsSchema),
|
|
253
|
+
}),
|
|
254
|
+
}).pipe(Effect.map((nested) => fromOrderNode(node, nested)));
|
|
255
|
+
|
|
256
|
+
const loadRefund = (orderId: string, node: GraphQLRefundNode) =>
|
|
257
|
+
Effect.all({
|
|
258
|
+
transactions: followConnection(node.transactions, {
|
|
259
|
+
id: node.id,
|
|
260
|
+
operationName: "AirfoilRefundTransactions",
|
|
261
|
+
query: RefundTransactionsQuery,
|
|
262
|
+
schema: nestedPageSchema(GraphQLRefundTransactionsSchema),
|
|
263
|
+
}),
|
|
264
|
+
refundLineItems: followConnection(node.refundLineItems, {
|
|
265
|
+
id: node.id,
|
|
266
|
+
operationName: "AirfoilRefundLineItems",
|
|
267
|
+
query: RefundLineItemsQuery,
|
|
268
|
+
schema: nestedPageSchema(GraphQLRefundLineItemsSchema),
|
|
269
|
+
}),
|
|
270
|
+
refundShippingLines: followConnection(node.refundShippingLines, {
|
|
271
|
+
id: node.id,
|
|
272
|
+
operationName: "AirfoilRefundShippingLines",
|
|
273
|
+
query: RefundShippingLinesQuery,
|
|
274
|
+
schema: nestedPageSchema(GraphQLRefundShippingLinesSchema),
|
|
275
|
+
}),
|
|
276
|
+
orderAdjustments: followConnection(node.orderAdjustments, {
|
|
277
|
+
id: node.id,
|
|
278
|
+
operationName: "AirfoilRefundOrderAdjustments",
|
|
279
|
+
query: RefundOrderAdjustmentsQuery,
|
|
280
|
+
schema: nestedPageSchema(GraphQLOrderAdjustmentsSchema),
|
|
281
|
+
}),
|
|
282
|
+
}).pipe(Effect.map((nested) => fromRefundNode(orderId, node, nested)));
|
|
283
|
+
|
|
284
|
+
const loadCustomer = (node: GraphQLCustomerNode) =>
|
|
285
|
+
followConnection(node.addressesV2, {
|
|
286
|
+
id: node.id,
|
|
287
|
+
operationName: "AirfoilCustomerAddresses",
|
|
288
|
+
query: CustomerAddressesQuery,
|
|
289
|
+
schema: nestedPageSchema(GraphQLCustomerAddressesSchema),
|
|
290
|
+
}).pipe(Effect.map((addresses) => normalizeCustomerNode(node, addresses)));
|
|
291
|
+
|
|
292
|
+
const loadProduct = (node: GraphQLProductNode) =>
|
|
293
|
+
followConnection(node.variants, {
|
|
294
|
+
id: node.id,
|
|
295
|
+
operationName: "AirfoilProductVariants",
|
|
296
|
+
query: ProductVariantsQuery,
|
|
297
|
+
schema: nestedPageSchema(connection(GraphQLProductVariantNodeSchema)),
|
|
298
|
+
}).pipe(Effect.map((variants) => normalizeProductNode(node, variants)));
|
|
299
|
+
|
|
300
|
+
const toPage = <A>(
|
|
301
|
+
pageInfo: PageInfo,
|
|
302
|
+
items: Effect.Effect<ReadonlyArray<A>, ConnectorError>,
|
|
303
|
+
): Effect.Effect<ShopifyPage<A>, ConnectorError> =>
|
|
304
|
+
items.pipe(
|
|
305
|
+
Effect.map((loaded) => ({
|
|
306
|
+
items: loaded,
|
|
307
|
+
endCursor: pageInfo.endCursor,
|
|
308
|
+
hasMore: pageInfo.hasNextPage,
|
|
309
|
+
})),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const fetchProducts = (options: PageRequest) =>
|
|
313
|
+
fetchGraphQL({
|
|
314
|
+
operationName: "AirfoilProducts",
|
|
315
|
+
query: ProductsQuery,
|
|
316
|
+
variables: { first: options.first, after: options.after ?? null },
|
|
317
|
+
schema: GraphQLProductsDataSchema,
|
|
318
|
+
}).pipe(
|
|
319
|
+
Effect.flatMap(({ products }) =>
|
|
320
|
+
toPage(products.pageInfo, Effect.forEach(products.nodes, loadProduct)),
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const fetchOrders = (options: PageRequest) =>
|
|
325
|
+
fetchGraphQL({
|
|
326
|
+
operationName: "AirfoilOrders",
|
|
327
|
+
query: OrdersQuery,
|
|
328
|
+
variables: { first: options.first, after: options.after ?? null },
|
|
329
|
+
schema: GraphQLOrdersDataSchema,
|
|
330
|
+
}).pipe(
|
|
331
|
+
Effect.flatMap(({ orders }) =>
|
|
332
|
+
toPage(orders.pageInfo, Effect.forEach(orders.nodes, loadOrder)),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const fetchRefunds = (options: PageRequest) =>
|
|
337
|
+
fetchGraphQL({
|
|
338
|
+
operationName: "AirfoilOrderRefunds",
|
|
339
|
+
query: OrderRefundsQuery,
|
|
340
|
+
variables: { first: options.first, after: options.after ?? null },
|
|
341
|
+
schema: GraphQLOrderRefundsDataSchema,
|
|
342
|
+
}).pipe(
|
|
343
|
+
Effect.flatMap(({ orders }) =>
|
|
344
|
+
toPage(
|
|
345
|
+
orders.pageInfo,
|
|
346
|
+
Effect.forEach(orders.nodes, (order) =>
|
|
347
|
+
Effect.forEach(order.refunds, (refund) => loadRefund(order.id, refund)),
|
|
348
|
+
).pipe(Effect.map((refunds) => refunds.flat())),
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const fetchCustomers = (options: PageRequest) =>
|
|
354
|
+
fetchGraphQL({
|
|
355
|
+
operationName: "AirfoilCustomers",
|
|
356
|
+
query: CustomersQuery,
|
|
357
|
+
variables: { first: options.first, after: options.after ?? null },
|
|
358
|
+
schema: GraphQLCustomersDataSchema,
|
|
359
|
+
}).pipe(
|
|
360
|
+
Effect.flatMap(({ customers }) =>
|
|
361
|
+
toPage(customers.pageInfo, Effect.forEach(customers.nodes, loadCustomer)),
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const fetchCustomerById = (id: string) =>
|
|
366
|
+
fetchGraphQL({
|
|
367
|
+
operationName: "AirfoilCustomerById",
|
|
368
|
+
query: CustomerByIdQuery,
|
|
369
|
+
variables: { id },
|
|
370
|
+
schema: GraphQLCustomerByIdDataSchema,
|
|
371
|
+
}).pipe(
|
|
372
|
+
Effect.flatMap(({ customer }) =>
|
|
373
|
+
customer === null
|
|
374
|
+
? Effect.succeed(Option.none())
|
|
375
|
+
: loadCustomer(customer).pipe(Effect.map(Option.some)),
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const checkCustomersAccess = fetchGraphQL({
|
|
380
|
+
operationName: "AirfoilCustomersAccess",
|
|
381
|
+
query: CustomersAccessQuery,
|
|
382
|
+
schema: CustomersAccessSchema,
|
|
383
|
+
}).pipe(Effect.asVoid);
|
|
384
|
+
|
|
385
|
+
const checkOrdersAccess = fetchGraphQL({
|
|
386
|
+
operationName: "AirfoilOrdersAccess",
|
|
387
|
+
query: OrdersAccessQuery,
|
|
388
|
+
schema: OrdersAccessSchema,
|
|
389
|
+
}).pipe(Effect.asVoid);
|
|
390
|
+
|
|
391
|
+
const checkConnection = fetchGraphQL({
|
|
392
|
+
operationName: "AirfoilShopIdentity",
|
|
393
|
+
query: ShopIdentityQuery,
|
|
394
|
+
schema: ShopIdentitySchema,
|
|
395
|
+
}).pipe(Effect.asVoid);
|
|
396
|
+
|
|
397
|
+
const checkProductsAccess = fetchGraphQL({
|
|
398
|
+
operationName: "AirfoilProductsAccess",
|
|
399
|
+
query: ProductsAccessQuery,
|
|
400
|
+
schema: ProductsAccessSchema,
|
|
401
|
+
}).pipe(Effect.asVoid);
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
checkConnection,
|
|
405
|
+
checkProductsAccess,
|
|
406
|
+
checkOrdersAccess,
|
|
407
|
+
checkCustomersAccess,
|
|
408
|
+
fetchGraphQL,
|
|
409
|
+
fetchProducts,
|
|
410
|
+
fetchProductById,
|
|
411
|
+
fetchOrders,
|
|
412
|
+
fetchRefunds,
|
|
413
|
+
fetchCustomers,
|
|
414
|
+
fetchCustomerById,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
export const layer = (
|
|
419
|
+
config: ShopifyConfig,
|
|
420
|
+
): Layer.Layer<ShopifyApiClient, ConnectorError, HttpClient.HttpClient | ShopifyAuth.ShopifyAuth> =>
|
|
421
|
+
Layer.effect(ShopifyApiClient)(make(config));
|
|
422
|
+
|
|
423
|
+
export const layerConfig = (
|
|
424
|
+
config: Config.Wrap<ShopifyConfig>,
|
|
425
|
+
): Layer.Layer<
|
|
426
|
+
ShopifyApiClient,
|
|
427
|
+
ConnectorError | Config.ConfigError,
|
|
428
|
+
HttpClient.HttpClient | ShopifyAuth.ShopifyAuth
|
|
429
|
+
> => Layer.effect(ShopifyApiClient)(Config.unwrap(config).pipe(Effect.flatMap(make)));
|