@unisian/shopify-admin 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/access-token-rejected.d.mts +10 -0
  2. package/dist/access-token-rejected.mjs +12 -0
  3. package/dist/admin-api.d.mts +45 -0
  4. package/dist/admin-api.mjs +103 -0
  5. package/dist/admin-bulk-query-registry.d.mts +26 -0
  6. package/dist/admin-bulk-query-registry.mjs +8 -0
  7. package/dist/admin-operation-registry.d.mts +11 -0
  8. package/dist/admin-operation-registry.mjs +8 -0
  9. package/dist/admin-query-failed.d.mts +11 -0
  10. package/dist/admin-query-failed.mjs +13 -0
  11. package/dist/bulk/bulk-operation-refused.d.mts +15 -0
  12. package/dist/bulk/bulk-operation-refused.mjs +18 -0
  13. package/dist/bulk/bulk-operations-finish.d.mts +13 -0
  14. package/dist/bulk/bulk-operations-finish.mjs +26 -0
  15. package/dist/bulk/bulk-result-failed.d.mts +10 -0
  16. package/dist/bulk/bulk-result-failed.mjs +12 -0
  17. package/dist/bulk/bulk-results.mjs +131 -0
  18. package/dist/bulk/bulk-transport.d.mts +33 -0
  19. package/dist/bulk/bulk-transport.mjs +118 -0
  20. package/dist/bulk/bulk-upload-failed.d.mts +11 -0
  21. package/dist/bulk/bulk-upload-failed.mjs +13 -0
  22. package/dist/bulk/read-only-shopify-bulk.d.mts +26 -0
  23. package/dist/bulk/read-only-shopify-bulk.mjs +23 -0
  24. package/dist/bulk/shopify-bulk.d.mts +47 -0
  25. package/dist/bulk/shopify-bulk.mjs +123 -0
  26. package/dist/bulk.d.mts +8 -0
  27. package/dist/bulk.mjs +7 -0
  28. package/dist/delegate-token-refused.d.mts +10 -0
  29. package/dist/delegate-token-refused.mjs +12 -0
  30. package/dist/delegate-token.d.mts +17 -0
  31. package/dist/delegate-token.mjs +45 -0
  32. package/dist/index.d.mts +12 -0
  33. package/dist/index.mjs +11 -0
  34. package/dist/mutation-refused.d.mts +10 -0
  35. package/dist/mutation-refused.mjs +12 -0
  36. package/dist/operations.d.mts +12 -0
  37. package/dist/operations.mjs +1 -0
  38. package/dist/read-only-admin.d.mts +49 -0
  39. package/dist/read-only-admin.mjs +60 -0
  40. package/dist/shopify-layer.d.mts +11 -0
  41. package/dist/shopify-layer.mjs +8 -0
  42. package/dist/testing.d.mts +29 -0
  43. package/dist/testing.mjs +52 -0
  44. package/package.json +35 -1
@@ -0,0 +1,10 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/access-token-rejected.d.ts
3
+ declare const AccessTokenRejected_base: Schema.Class<AccessTokenRejected, Schema.TaggedStruct<"AccessTokenRejected", {
4
+ readonly operation: Schema.String;
5
+ readonly shop: Schema.String;
6
+ }>, import("effect/Cause").YieldableError>;
7
+ export declare class AccessTokenRejected extends AccessTokenRejected_base {
8
+ get message(): string;
9
+ }
10
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/access-token-rejected.ts
3
+ var AccessTokenRejected = class extends Schema.TaggedError()("AccessTokenRejected", {
4
+ operation: Schema.String,
5
+ shop: Schema.String
6
+ }) {
7
+ get message() {
8
+ return `Admin API rejected the access token for ${this.shop} (HTTP 401 on ${this.operation})`;
9
+ }
10
+ };
11
+ //#endregion
12
+ export { AccessTokenRejected };
@@ -0,0 +1,45 @@
1
+ import { AccessTokenRejected } from "./access-token-rejected.mjs";
2
+ import { AdminOperationRegistry } from "./admin-operation-registry.mjs";
3
+ import { AdminQueryFailed } from "./admin-query-failed.mjs";
4
+ import { AdminDocument, AdminOperations } from "./operations.mjs";
5
+ import * as Schema from "effect/Schema";
6
+ import * as Context from "effect/Context";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Layer from "effect/Layer";
9
+ import * as Redacted from "effect/Redacted";
10
+ import { HttpClient } from "effect/http";
11
+ import { AuthenticatedShop, ExchangeError, RefreshError, SessionOwner, ShopifyApp, ShopifyAuth } from "@unisian/shopify-auth";
12
+ import { VariablesArgsFor } from "@unisian/shopify-auth/kernel";
13
+ import { ShopDomain } from "@unisian/shopify-sessions";
14
+ //#region src/admin-api.d.ts
15
+ export interface AdminCredential {
16
+ readonly accessToken: Redacted.Redacted;
17
+ readonly shop: ShopDomain;
18
+ }
19
+ export interface AdminDocumentResult {
20
+ readonly data: Schema.Json | null;
21
+ readonly errors: readonly Schema.Json[];
22
+ readonly extensions: Schema.Json | null;
23
+ readonly status: number;
24
+ }
25
+ export declare const sendAdminDocument: (apiVersion: string, credential: AdminCredential, document: string, variables: Readonly<Record<string, Schema.Json>>) => Effect.Effect<AdminDocumentResult, AdminQueryFailed | AccessTokenRejected, HttpClient.HttpClient>;
26
+ export type VariablesArgs<Q extends AdminDocument> = VariablesArgsFor<AdminOperations[Q]["variables"]>;
27
+ export type AdminError = AdminQueryFailed | AccessTokenRejected | ExchangeError | RefreshError;
28
+ declare const ShopifyAdmin_base: Context.ServiceClass<ShopifyAdmin, "@unisian/shopify-admin/ShopifyAdmin", {
29
+ readonly graphql: <const Q extends AdminDocument>(document: Q, ...variables: VariablesArgs<Q>) => Effect.Effect<AdminOperations[Q]["data"], AdminError>;
30
+ }>;
31
+ export declare class ShopifyAdmin extends ShopifyAdmin_base {
32
+ static readonly make: (authenticated: AuthenticatedShop) => Effect.Effect<{
33
+ readonly graphql: <const Q extends AdminDocument>(document: Q, ...variables: VariablesArgs<Q>) => Effect.Effect<AdminOperations[Q]["data"], AdminError>;
34
+ }, never, AdminOperationRegistry | HttpClient.HttpClient | ShopifyApp | ShopifyAuth>;
35
+ static readonly layer: (authenticated: AuthenticatedShop) => Layer.Layer<ShopifyAdmin, never, AdminOperationRegistry | HttpClient.HttpClient | ShopifyApp | ShopifyAuth>;
36
+ static readonly makeForShop: (shop: ShopDomain) => Effect.Effect<{
37
+ readonly graphql: <const Q extends AdminDocument>(document: Q, ...variables: VariablesArgs<Q>) => Effect.Effect<AdminOperations[Q]["data"], AdminError>;
38
+ }, never, AdminOperationRegistry | HttpClient.HttpClient | SessionOwner | ShopifyApp>;
39
+ static readonly layerForShop: (shop: ShopDomain) => Layer.Layer<ShopifyAdmin, never, AdminOperationRegistry | HttpClient.HttpClient | SessionOwner | ShopifyApp>;
40
+ static readonly makeForToken: (credential: AdminCredential, apiVersion: string) => Effect.Effect<{
41
+ readonly graphql: <const Q extends AdminDocument>(document: Q, ...variables: VariablesArgs<Q>) => Effect.Effect<AdminOperations[Q]["data"], AdminError>;
42
+ }, never, AdminOperationRegistry | HttpClient.HttpClient>;
43
+ static readonly layerForToken: (credential: AdminCredential, apiVersion: string) => Layer.Layer<ShopifyAdmin, never, AdminOperationRegistry | HttpClient.HttpClient>;
44
+ }
45
+ //#endregion
@@ -0,0 +1,103 @@
1
+ import { AccessTokenRejected } from "./access-token-rejected.mjs";
2
+ import { AdminOperationRegistry } from "./admin-operation-registry.mjs";
3
+ import { AdminQueryFailed } from "./admin-query-failed.mjs";
4
+ import * as Schema from "effect/Schema";
5
+ import * as Context from "effect/Context";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Layer from "effect/Layer";
8
+ import * as Redacted from "effect/Redacted";
9
+ import * as Ref from "effect/Ref";
10
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
11
+ import { SessionOwner, ShopifyApp, ShopifyAuth, withoutRequest } from "@unisian/shopify-auth";
12
+ import { logRenewal, operationBody, operationData, operationFor } from "@unisian/shopify-auth/kernel";
13
+ //#region src/admin-api.ts
14
+ const adminRequest = (apiVersion, credential, body) => HttpClientRequest.post(`https://${credential.shop}/admin/api/${apiVersion}/graphql.json`).pipe(HttpClientRequest.acceptJson, HttpClientRequest.setHeader("X-Shopify-Access-Token", Redacted.value(credential.accessToken)), HttpClientRequest.bodyJsonUnsafe(body));
15
+ const AdminDocumentBody = Schema.Struct({
16
+ data: Schema.optional(Schema.NullOr(Schema.Json)),
17
+ errors: Schema.optional(Schema.Array(Schema.Json)),
18
+ extensions: Schema.optional(Schema.Json)
19
+ });
20
+ const sendAdminDocument = (apiVersion, credential, document, variables) => Effect.gen(function* sendAdminDocumentGen() {
21
+ const operation = "an ad hoc document";
22
+ const failed = (cause) => new AdminQueryFailed({
23
+ cause: withoutRequest(cause),
24
+ operation,
25
+ shop: credential.shop
26
+ });
27
+ const response = yield* (yield* HttpClient.HttpClient).execute(adminRequest(apiVersion, credential, {
28
+ query: document,
29
+ variables
30
+ })).pipe(Effect.mapError(failed));
31
+ if (response.status === 401) return yield* new AccessTokenRejected({
32
+ operation,
33
+ shop: credential.shop
34
+ });
35
+ const body = yield* HttpClientResponse.schemaBodyJson(AdminDocumentBody)(response).pipe(Effect.mapError(failed));
36
+ return {
37
+ data: body.data ?? null,
38
+ errors: body.errors ?? [],
39
+ extensions: body.extensions ?? null,
40
+ status: response.status
41
+ };
42
+ }).pipe(Effect.withSpan("sendAdminDocument"));
43
+ const adminQuery = (apiVersion, session, operation, variables) => Effect.gen(function* sendAdminQuery() {
44
+ const failed = (cause) => new AdminQueryFailed({
45
+ cause: withoutRequest(cause),
46
+ operation: operation.name,
47
+ shop: session.shop
48
+ });
49
+ const client = yield* HttpClient.HttpClient;
50
+ const body = yield* operationBody(operation, variables, failed);
51
+ const response = yield* client.execute(adminRequest(apiVersion, session, body)).pipe(Effect.mapError(failed));
52
+ if (response.status === 401) return yield* new AccessTokenRejected({
53
+ operation: operation.name,
54
+ shop: session.shop
55
+ });
56
+ return yield* operationData(operation.data, failed)(response);
57
+ }).pipe(Effect.withSpan("adminQuery", { attributes: { operation: operation.name } }));
58
+ const sendForShop = (shop) => Effect.gen(function* sendForShopGen() {
59
+ const { apiVersion } = yield* ShopifyApp;
60
+ const client = yield* HttpClient.HttpClient;
61
+ const owner = yield* SessionOwner;
62
+ const send = (operation, variables) => owner.stored(shop).pipe(Effect.tap(logRenewal), Effect.flatMap(({ session }) => adminQuery(apiVersion, session, operation, variables)), Effect.annotateLogs("shop", shop), Effect.provideService(HttpClient.HttpClient, client));
63
+ return send;
64
+ });
65
+ const sendForToken = (credential, apiVersion) => Effect.gen(function* sendForTokenGen() {
66
+ const client = yield* HttpClient.HttpClient;
67
+ const send = (operation, variables) => adminQuery(apiVersion, credential, operation, variables).pipe(Effect.annotateLogs("shop", credential.shop), Effect.provideService(HttpClient.HttpClient, client));
68
+ return send;
69
+ });
70
+ const graphqlOver = (registry, send) => (document, ...[variables]) => {
71
+ return operationFor(registry, document).pipe(Effect.flatMap((operation) => send(operation, variables ?? {})));
72
+ };
73
+ var ShopifyAdmin = class ShopifyAdmin extends Context.Service()("@unisian/shopify-admin/ShopifyAdmin") {
74
+ static make = (authenticated) => Effect.gen(function* make() {
75
+ const { apiVersion } = yield* ShopifyApp;
76
+ const registry = yield* AdminOperationRegistry;
77
+ const client = yield* HttpClient.HttpClient;
78
+ const auth = yield* ShopifyAuth;
79
+ const session = yield* Ref.make(authenticated.session);
80
+ const graphql = (document, ...[variables]) => {
81
+ return operationFor(registry, document).pipe(Effect.flatMap((operation) => {
82
+ const run = Ref.get(session).pipe(Effect.flatMap((current) => adminQuery(apiVersion, current, operation, variables ?? {})));
83
+ return run.pipe(Effect.catchTag("AccessTokenRejected", () => auth.reexchange(authenticated).pipe(Effect.flatMap((renewed) => Ref.set(session, renewed)), Effect.andThen(run))));
84
+ }), Effect.provideService(HttpClient.HttpClient, client));
85
+ };
86
+ return ShopifyAdmin.of({ graphql });
87
+ });
88
+ static layer = (authenticated) => Layer.effect(ShopifyAdmin, ShopifyAdmin.make(authenticated));
89
+ static makeForShop = (shop) => Effect.gen(function* makeForShop() {
90
+ const registry = yield* AdminOperationRegistry;
91
+ const send = yield* sendForShop(shop);
92
+ return ShopifyAdmin.of({ graphql: graphqlOver(registry, send) });
93
+ });
94
+ static layerForShop = (shop) => Layer.effect(ShopifyAdmin, ShopifyAdmin.makeForShop(shop));
95
+ static makeForToken = (credential, apiVersion) => Effect.gen(function* makeForToken() {
96
+ const registry = yield* AdminOperationRegistry;
97
+ const send = yield* sendForToken(credential, apiVersion);
98
+ return ShopifyAdmin.of({ graphql: graphqlOver(registry, send) });
99
+ });
100
+ static layerForToken = (credential, apiVersion) => Layer.effect(ShopifyAdmin, ShopifyAdmin.makeForToken(credential, apiVersion));
101
+ };
102
+ //#endregion
103
+ export { ShopifyAdmin, adminQuery, sendAdminDocument, sendForShop, sendForToken };
@@ -0,0 +1,26 @@
1
+ import * as Schema from "effect/Schema";
2
+ import * as Context from "effect/Context";
3
+ import * as Layer from "effect/Layer";
4
+ //#region src/admin-bulk-query-registry.d.ts
5
+ export interface BulkLines {
6
+ readonly line: Schema.Codec<unknown, unknown>;
7
+ readonly connections: Readonly<Record<string, BulkConnection>>;
8
+ }
9
+ export interface BulkConnection extends BulkLines {
10
+ readonly path: readonly string[];
11
+ readonly types: readonly string[];
12
+ }
13
+ export interface AdminBulkQuery extends BulkLines {
14
+ readonly name: string;
15
+ readonly kind: "bulk";
16
+ readonly document: readonly string[];
17
+ readonly slots: readonly string[];
18
+ readonly enums: readonly string[];
19
+ readonly parameters: Schema.Codec<unknown, unknown>;
20
+ readonly node: Schema.Codec<unknown, unknown>;
21
+ }
22
+ declare const AdminBulkQueryRegistry_base: Context.ServiceClass<AdminBulkQueryRegistry, "@unisian/shopify-admin/AdminBulkQueryRegistry", Readonly<Record<string, AdminBulkQuery>>>;
23
+ export declare class AdminBulkQueryRegistry extends AdminBulkQueryRegistry_base {
24
+ static readonly layer: (queries: Readonly<Record<string, AdminBulkQuery>>) => Layer.Layer<AdminBulkQueryRegistry, never, never>;
25
+ }
26
+ //#endregion
@@ -0,0 +1,8 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Layer from "effect/Layer";
3
+ //#region src/admin-bulk-query-registry.ts
4
+ var AdminBulkQueryRegistry = class AdminBulkQueryRegistry extends Context.Service()("@unisian/shopify-admin/AdminBulkQueryRegistry") {
5
+ static layer = (queries) => Layer.succeed(AdminBulkQueryRegistry, AdminBulkQueryRegistry.of(queries));
6
+ };
7
+ //#endregion
8
+ export { AdminBulkQueryRegistry };
@@ -0,0 +1,11 @@
1
+ import * as Schema from "effect/Schema";
2
+ import * as Context from "effect/Context";
3
+ import * as Layer from "effect/Layer";
4
+ import { GraphqlOperation } from "@unisian/shopify-auth/kernel";
5
+ //#region src/admin-operation-registry.d.ts
6
+ export type AdminOperation<Data extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>, Variables extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>> = GraphqlOperation<Data, Variables>;
7
+ declare const AdminOperationRegistry_base: Context.ServiceClass<AdminOperationRegistry, "@unisian/shopify-admin/AdminOperationRegistry", Readonly<Record<string, AdminOperation<Schema.Codec<unknown, unknown, never, never>, Schema.Codec<unknown, unknown, never, never>>>>>;
8
+ export declare class AdminOperationRegistry extends AdminOperationRegistry_base {
9
+ static readonly layer: (operations: Readonly<Record<string, AdminOperation>>) => Layer.Layer<AdminOperationRegistry, never, never>;
10
+ }
11
+ //#endregion
@@ -0,0 +1,8 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Layer from "effect/Layer";
3
+ //#region src/admin-operation-registry.ts
4
+ var AdminOperationRegistry = class AdminOperationRegistry extends Context.Service()("@unisian/shopify-admin/AdminOperationRegistry") {
5
+ static layer = (operations) => Layer.succeed(AdminOperationRegistry, AdminOperationRegistry.of(operations));
6
+ };
7
+ //#endregion
8
+ export { AdminOperationRegistry };
@@ -0,0 +1,11 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/admin-query-failed.d.ts
3
+ declare const AdminQueryFailed_base: Schema.Class<AdminQueryFailed, Schema.TaggedStruct<"AdminQueryFailed", {
4
+ readonly cause: Schema.Defect;
5
+ readonly operation: Schema.String;
6
+ readonly shop: Schema.String;
7
+ }>, import("effect/Cause").YieldableError>;
8
+ export declare class AdminQueryFailed extends AdminQueryFailed_base {
9
+ get message(): string;
10
+ }
11
+ //#endregion
@@ -0,0 +1,13 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/admin-query-failed.ts
3
+ var AdminQueryFailed = class extends Schema.TaggedError()("AdminQueryFailed", {
4
+ cause: Schema.Defect(),
5
+ operation: Schema.String,
6
+ shop: Schema.String
7
+ }) {
8
+ get message() {
9
+ return `Admin API ${this.operation} failed for ${this.shop}`;
10
+ }
11
+ };
12
+ //#endregion
13
+ export { AdminQueryFailed };
@@ -0,0 +1,15 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/bulk/bulk-operation-refused.d.ts
3
+ declare const BulkOperationRefused_base: Schema.Class<BulkOperationRefused, Schema.TaggedStruct<"BulkOperationRefused", {
4
+ readonly operation: Schema.String;
5
+ readonly shop: Schema.String;
6
+ readonly userErrors: Schema.$Array<Schema.Struct<{
7
+ readonly code: Schema.NullOr<Schema.String>;
8
+ readonly field: Schema.NullOr<Schema.$Array<Schema.String>>;
9
+ readonly message: Schema.String;
10
+ }>>;
11
+ }>, import("effect/Cause").YieldableError>;
12
+ export declare class BulkOperationRefused extends BulkOperationRefused_base {
13
+ get message(): string;
14
+ }
15
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/bulk/bulk-operation-refused.ts
3
+ var BulkOperationRefused = class extends Schema.TaggedError()("BulkOperationRefused", {
4
+ operation: Schema.String,
5
+ shop: Schema.String,
6
+ userErrors: Schema.Array(Schema.Struct({
7
+ code: Schema.NullOr(Schema.String),
8
+ field: Schema.NullOr(Schema.Array(Schema.String)),
9
+ message: Schema.String
10
+ }))
11
+ }) {
12
+ get message() {
13
+ const reasons = this.userErrors.map((error) => error.message).join("; ");
14
+ return `Shopify refused ${this.operation} for ${this.shop}: ${reasons || "no bulk operation came back"}`;
15
+ }
16
+ };
17
+ //#endregion
18
+ export { BulkOperationRefused };
@@ -0,0 +1,13 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/bulk/bulk-operations-finish.d.ts
3
+ export declare const BULK_OPERATIONS_FINISH_TOPIC = "bulk_operations/finish";
4
+ export declare const BulkOperationsFinish: Schema.Struct<{
5
+ readonly admin_graphql_api_id: Schema.String;
6
+ readonly completed_at: Schema.NullOr<Schema.String>;
7
+ readonly created_at: Schema.String;
8
+ readonly error_code: Schema.NullOr<Schema.Union<readonly [Schema.StringForLiteralAutocomplete, Schema.Literals<readonly ["access_denied", "internal_server_error", "timeout"]>]>>;
9
+ readonly status: Schema.Union<readonly [Schema.StringForLiteralAutocomplete, Schema.Literals<readonly ["canceled", "canceling", "completed", "created", "expired", "failed", "running"]>]>;
10
+ readonly type: Schema.Union<readonly [Schema.StringForLiteralAutocomplete, Schema.Literals<readonly ["mutation", "query"]>]>;
11
+ }>;
12
+ export type BulkOperationsFinish = typeof BulkOperationsFinish.Type;
13
+ //#endregion
@@ -0,0 +1,26 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/bulk/bulk-operations-finish.ts
3
+ const BULK_OPERATIONS_FINISH_TOPIC = "bulk_operations/finish";
4
+ const lowercase = (values) => Schema.Union([Schema.StringForLiteralAutocomplete, Schema.Literals(values)]);
5
+ const BulkOperationsFinish = Schema.Struct({
6
+ admin_graphql_api_id: Schema.String,
7
+ completed_at: Schema.NullOr(Schema.String),
8
+ created_at: Schema.String,
9
+ error_code: Schema.NullOr(lowercase([
10
+ "access_denied",
11
+ "internal_server_error",
12
+ "timeout"
13
+ ])),
14
+ status: lowercase([
15
+ "canceled",
16
+ "canceling",
17
+ "completed",
18
+ "created",
19
+ "expired",
20
+ "failed",
21
+ "running"
22
+ ]),
23
+ type: lowercase(["mutation", "query"])
24
+ });
25
+ //#endregion
26
+ export { BULK_OPERATIONS_FINISH_TOPIC, BulkOperationsFinish };
@@ -0,0 +1,10 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/bulk/bulk-result-failed.d.ts
3
+ declare const BulkResultFailed_base: Schema.Class<BulkResultFailed, Schema.TaggedStruct<"BulkResultFailed", {
4
+ readonly cause: Schema.Defect;
5
+ readonly operation: Schema.String;
6
+ }>, import("effect/Cause").YieldableError>;
7
+ export declare class BulkResultFailed extends BulkResultFailed_base {
8
+ get message(): string;
9
+ }
10
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import * as Schema from "effect/Schema";
2
+ //#region src/bulk/bulk-result-failed.ts
3
+ var BulkResultFailed = class extends Schema.TaggedError()("BulkResultFailed", {
4
+ cause: Schema.Defect(),
5
+ operation: Schema.String
6
+ }) {
7
+ get message() {
8
+ return `Reading the result file of bulk ${this.operation} failed`;
9
+ }
10
+ };
11
+ //#endregion
12
+ export { BulkResultFailed };
@@ -0,0 +1,131 @@
1
+ import { BulkResultFailed } from "./bulk-result-failed.mjs";
2
+ import * as Schema from "effect/Schema";
3
+ import * as Effect from "effect/Effect";
4
+ import { HttpClientRequest, HttpClientResponse } from "effect/http";
5
+ import { withoutRequest } from "@unisian/shopify-auth";
6
+ import * as Stream from "effect/Stream";
7
+ import * as Predicate from "effect/Predicate";
8
+ //#region src/bulk/bulk-results.ts
9
+ const ResultLine = Schema.Record(Schema.String, Schema.Json);
10
+ const decodeObject = Schema.decodeUnknownEffect(Schema.fromJsonString(ResultLine));
11
+ const parentIdOf = (line) => Predicate.isString(line.__parentId) ? line.__parentId : void 0;
12
+ const connectionFor = (parent, typename) => Object.entries(parent.connections).find(([, connection]) => Predicate.isString(typename) && connection.types.includes(typename));
13
+ const holdsConnections = (position) => Object.keys(position.connections).length > 0;
14
+ const unplaced = (parentId, typename) => `a ${JSON.stringify(typename ?? null)} line names parent ${parentId}, which no earlier line holds under a connection for that type`;
15
+ const releasedEarly = (parentId, typename) => `${unplaced(parentId, typename)}; nodes folds a root once the next root starts, unless a connection on it holds a multiple of 250 children, which is the order Shopify writes without groupObjects but doesn't promise: submit with { groupObjects: true }, or read the file with lines`;
16
+ const isJsonObject = Schema.is(ResultLine);
17
+ const resultText = (client, url) => url === null ? Stream.empty : client.execute(HttpClientRequest.get(url)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), HttpClientResponse.stream, Stream.decodeText, Stream.splitLines, Stream.filter((line) => line.trim() !== ""));
18
+ const bulkLines = (query) => (text) => text.pipe(Stream.mapError((cause) => new BulkResultFailed({
19
+ cause: withoutRequest(cause),
20
+ operation: query.name
21
+ })), Stream.mapAccumEffect(() => /* @__PURE__ */ new Map(), (parents, raw) => Effect.gen(function* bulkLine() {
22
+ const line = yield* decodeObject(raw);
23
+ const parentId = parentIdOf(line);
24
+ const parent = parentId === void 0 ? void 0 : parents.get(parentId);
25
+ const position = parentId === void 0 ? query : parent && connectionFor(parent, line.__typename)?.[1];
26
+ if (position === void 0) return yield* Effect.fail(unplaced(parentId ?? "", line.__typename));
27
+ const decoded = yield* Schema.decodeEffect(position.line)(line);
28
+ if (holdsConnections(position) && Predicate.isString(line.id)) parents.set(line.id, position);
29
+ return [parents, [decoded]];
30
+ }).pipe(Effect.mapError((cause) => new BulkResultFailed({
31
+ cause,
32
+ operation: query.name
33
+ })))));
34
+ const childRunLength = 250;
35
+ const startFolding = (line, position) => ({
36
+ children: new Map(Object.keys(position.connections).map((key) => [key, []])),
37
+ line,
38
+ position
39
+ });
40
+ const mayContinueLater = (folding) => [...folding.children.values()].some((children) => children.length > 0 && children.length % childRunLength === 0 || children.some(mayContinueLater));
41
+ const forget = (byId, folding) => {
42
+ if (Predicate.isString(folding.line.id)) byId.delete(folding.line.id);
43
+ for (const children of folding.children.values()) for (const child of children) forget(byId, child);
44
+ };
45
+ const placeAt = (node, path, edges) => {
46
+ const [key, ...rest] = path;
47
+ if (key === void 0) return node;
48
+ if (rest.length === 0) return {
49
+ ...node,
50
+ [key]: edges
51
+ };
52
+ const inner = node[key];
53
+ return isJsonObject(inner) ? {
54
+ ...node,
55
+ [key]: placeAt(inner, rest, edges)
56
+ } : node;
57
+ };
58
+ const materialise = (folding) => {
59
+ let node = Object.fromEntries(Object.entries(folding.line).filter(([field]) => field !== "__parentId"));
60
+ for (const [key, children] of folding.children) node = placeAt(node, folding.position.connections[key]?.path ?? [key], { edges: children.map((child) => ({ node: materialise(child) })) });
61
+ return node;
62
+ };
63
+ const remember = (fold, folding) => {
64
+ if (holdsConnections(folding.position) && Predicate.isString(folding.line.id)) fold.byId.set(folding.line.id, folding);
65
+ };
66
+ const releaseCurrent = (fold) => {
67
+ const { current } = fold;
68
+ if (current === void 0) return [];
69
+ if (mayContinueLater(current)) {
70
+ fold.held.push(current);
71
+ return [];
72
+ }
73
+ forget(fold.byId, current);
74
+ return [materialise(current)];
75
+ };
76
+ const bulkNodes = (query) => (text) => text.pipe(Stream.mapError((cause) => new BulkResultFailed({
77
+ cause: withoutRequest(cause),
78
+ operation: query.name
79
+ })), Stream.mapAccumEffect(() => ({
80
+ byId: /* @__PURE__ */ new Map(),
81
+ current: void 0,
82
+ held: []
83
+ }), (fold, raw) => Effect.gen(function* foldLine() {
84
+ const line = yield* decodeObject(raw);
85
+ const parentId = parentIdOf(line);
86
+ if (parentId === void 0) {
87
+ const released = releaseCurrent(fold);
88
+ fold.current = startFolding(line, query);
89
+ remember(fold, fold.current);
90
+ return [fold, released];
91
+ }
92
+ const parent = fold.byId.get(parentId);
93
+ const connection = parent && connectionFor(parent.position, line.__typename);
94
+ if (parent === void 0 || connection === void 0) return yield* Effect.fail(releasedEarly(parentId, line.__typename));
95
+ const [key, position] = connection;
96
+ const child = startFolding(line, position);
97
+ parent.children.get(key)?.push(child);
98
+ remember(fold, child);
99
+ return [fold, []];
100
+ }).pipe(Effect.mapError((cause) => new BulkResultFailed({
101
+ cause,
102
+ operation: query.name
103
+ }))), { onHalt: (fold) => [...fold.held, ...fold.current === void 0 ? [] : [fold.current]].map(materialise) }), Stream.mapEffect((node) => Schema.decodeEffect(query.node)(node).pipe(Effect.mapError((cause) => new BulkResultFailed({
104
+ cause,
105
+ operation: query.name
106
+ })))));
107
+ const ResultError = Schema.Struct({ message: Schema.String });
108
+ const UnreachedLine = Schema.fromJsonString(Schema.Struct({
109
+ __lineNumber: Schema.optionalKey(Schema.Int),
110
+ data: Schema.Record(Schema.String, Schema.NullOr(Schema.Struct({ userErrors: Schema.Array(ResultError) })))
111
+ }));
112
+ const unreached = (text) => Schema.decodeEffect(UnreachedLine)(text).pipe(Effect.map(({ __lineNumber, data }) => ({
113
+ __lineNumber,
114
+ errors: Object.values(data).flatMap((payload) => payload?.userErrors ?? [])
115
+ })), Effect.filterOrFail(({ errors }) => errors.length > 0));
116
+ const bulkResults = (name, data) => (text) => {
117
+ const decodeLine = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({
118
+ __lineNumber: Schema.optionalKey(Schema.Int),
119
+ data: Schema.optionalKey(Schema.NullOr(data)),
120
+ errors: Schema.optionalKey(Schema.Array(ResultError))
121
+ })));
122
+ return text.pipe(Stream.mapError((cause) => new BulkResultFailed({
123
+ cause: withoutRequest(cause),
124
+ operation: name
125
+ })), Stream.mapEffect((line) => decodeLine(line).pipe(Effect.catch((error) => unreached(line).pipe(Effect.mapError(() => error))), Effect.mapError((cause) => new BulkResultFailed({
126
+ cause,
127
+ operation: name
128
+ })))));
129
+ };
130
+ //#endregion
131
+ export { bulkLines, bulkNodes, bulkResults, resultText };
@@ -0,0 +1,33 @@
1
+ import "../admin-bulk-query-registry.mjs";
2
+ import { AdminBulkQueries, AdminBulkQueryDocument } from "../operations.mjs";
3
+ import { AdminError } from "../admin-api.mjs";
4
+ import "./bulk-operation-refused.mjs";
5
+ import { BulkResultFailed } from "./bulk-result-failed.mjs";
6
+ import * as Schema from "effect/Schema";
7
+ import * as Effect from "effect/Effect";
8
+ import { HttpClient } from "effect/http";
9
+ import * as Stream from "effect/Stream";
10
+ import { ShopDomain } from "@unisian/shopify-sessions";
11
+ //#region src/bulk/bulk-transport.d.ts
12
+ export declare const BulkOperation: Schema.Struct<{
13
+ readonly completedAt: Schema.NullOr<Schema.String>;
14
+ readonly createdAt: Schema.String;
15
+ readonly errorCode: Schema.NullOr<Schema.Union<readonly [Schema.StringForLiteralAutocomplete, Schema.Literals<readonly ["ACCESS_DENIED", "INTERNAL_SERVER_ERROR", "TIMEOUT"]>]>>;
16
+ readonly id: Schema.String;
17
+ readonly objectCount: Schema.FiniteFromString;
18
+ readonly partialDataUrl: Schema.NullOr<Schema.String>;
19
+ readonly status: Schema.Union<readonly [Schema.StringForLiteralAutocomplete, Schema.Literals<readonly ["CANCELED", "CANCELING", "COMPLETED", "CREATED", "EXPIRED", "FAILED", "RUNNING"]>]>;
20
+ readonly type: Schema.Union<readonly [Schema.StringForLiteralAutocomplete, Schema.Literals<readonly ["MUTATION", "QUERY"]>]>;
21
+ readonly url: Schema.NullOr<Schema.String>;
22
+ }>;
23
+ export type BulkOperation = typeof BulkOperation.Type;
24
+ export interface BulkQueryOptions {
25
+ readonly groupObjects?: boolean;
26
+ }
27
+ export type BulkQueryArgs<Q extends AdminBulkQueryDocument> = {} extends AdminBulkQueries[Q]["parameters"] ? [parameters?: AdminBulkQueries[Q]["parameters"], options?: BulkQueryOptions] : [parameters: AdminBulkQueries[Q]["parameters"], options?: BulkQueryOptions];
28
+ export interface BulkReads {
29
+ readonly operation: (id: string) => Effect.Effect<BulkOperation | null, AdminError>;
30
+ readonly lines: <const Q extends AdminBulkQueryDocument>(document: Q, url: string | null) => Stream.Stream<AdminBulkQueries[Q]["line"], BulkResultFailed>;
31
+ readonly nodes: <const Q extends AdminBulkQueryDocument>(document: Q, url: string | null) => Stream.Stream<AdminBulkQueries[Q]["node"], BulkResultFailed>;
32
+ }
33
+ //#endregion
@@ -0,0 +1,118 @@
1
+ import { AdminQueryFailed } from "../admin-query-failed.mjs";
2
+ import { sendForShop, sendForToken } from "../admin-api.mjs";
3
+ import { AdminBulkQueryRegistry } from "../admin-bulk-query-registry.mjs";
4
+ import { BulkOperationRefused } from "./bulk-operation-refused.mjs";
5
+ import { bulkLines, bulkNodes, resultText } from "./bulk-results.mjs";
6
+ import * as Schema from "effect/Schema";
7
+ import * as Effect from "effect/Effect";
8
+ import { HttpClient } from "effect/http";
9
+ import { withoutRequest } from "@unisian/shopify-auth";
10
+ import * as Stream from "effect/Stream";
11
+ import * as Predicate from "effect/Predicate";
12
+ //#region src/bulk/bulk-transport.ts
13
+ const shopifyEnum = (values) => Schema.Union([Schema.StringForLiteralAutocomplete, Schema.Literals(values)]);
14
+ const BulkOperation = Schema.Struct({
15
+ completedAt: Schema.NullOr(Schema.String),
16
+ createdAt: Schema.String,
17
+ errorCode: Schema.NullOr(shopifyEnum([
18
+ "ACCESS_DENIED",
19
+ "INTERNAL_SERVER_ERROR",
20
+ "TIMEOUT"
21
+ ])),
22
+ id: Schema.String,
23
+ objectCount: Schema.FiniteFromString,
24
+ partialDataUrl: Schema.NullOr(Schema.String),
25
+ status: shopifyEnum([
26
+ "CANCELED",
27
+ "CANCELING",
28
+ "COMPLETED",
29
+ "CREATED",
30
+ "EXPIRED",
31
+ "FAILED",
32
+ "RUNNING"
33
+ ]),
34
+ type: shopifyEnum(["MUTATION", "QUERY"]),
35
+ url: Schema.NullOr(Schema.String)
36
+ });
37
+ const bulkOperationFields = "id type status errorCode objectCount url partialDataUrl createdAt completedAt";
38
+ const UserErrors = Schema.Array(Schema.Struct({
39
+ code: Schema.NullOr(Schema.String),
40
+ field: Schema.NullOr(Schema.Array(Schema.String)),
41
+ message: Schema.String
42
+ }));
43
+ const Submitted = Schema.NullOr(Schema.Struct({
44
+ bulkOperation: Schema.NullOr(BulkOperation),
45
+ userErrors: UserErrors
46
+ }));
47
+ const RunQuery = {
48
+ data: Schema.Struct({ bulkOperationRunQuery: Submitted }),
49
+ document: `mutation UnisianBulkRunQuery($query: String!, $groupObjects: Boolean!) { bulkOperationRunQuery(query: $query, groupObjects: $groupObjects) { bulkOperation { ${bulkOperationFields} } userErrors { code field message } } }`,
50
+ kind: "mutation",
51
+ name: "UnisianBulkRunQuery",
52
+ variables: Schema.Struct({
53
+ groupObjects: Schema.Boolean,
54
+ query: Schema.String
55
+ })
56
+ };
57
+ const ReadOperation = {
58
+ data: Schema.Struct({ bulkOperation: Schema.NullOr(BulkOperation) }),
59
+ document: `query UnisianBulkOperation($id: ID!) { bulkOperation(id: $id) { ${bulkOperationFields} } }`,
60
+ kind: "query",
61
+ name: "UnisianBulkOperation",
62
+ variables: Schema.Struct({ id: Schema.String })
63
+ };
64
+ const enumValue = /^[_A-Za-z][_0-9A-Za-z]*$/u;
65
+ const EncodedParameters = Schema.Record(Schema.String, Schema.Json);
66
+ const isJsonList = Schema.is(Schema.Array(Schema.Json));
67
+ const literal = (value, bare) => {
68
+ if (isJsonList(value)) return `[${value.map((item) => literal(item, bare)).join(", ")}]`;
69
+ return bare && Predicate.isString(value) && enumValue.test(value) ? value : JSON.stringify(value ?? null);
70
+ };
71
+ const bulkQueryFor = (registry, document) => {
72
+ const query = registry[document];
73
+ return query ? Effect.succeed(query) : Effect.die(`No generated bulk query for this #graphql template; run pnpm codegen`);
74
+ };
75
+ const reads = ({ send, client, bulkQueries }) => ({
76
+ lines: (document, url) => {
77
+ return bulkQueryFor(bulkQueries, document).pipe(Effect.map((query) => resultText(client, url).pipe(bulkLines(query))), Stream.unwrap);
78
+ },
79
+ nodes: (document, url) => {
80
+ return bulkQueryFor(bulkQueries, document).pipe(Effect.map((query) => resultText(client, url).pipe(bulkNodes(query))), Stream.unwrap);
81
+ },
82
+ operation: (id) => send(ReadOperation, { id }).pipe(Effect.map(({ bulkOperation }) => bulkOperation))
83
+ });
84
+ const submitted = (operation, shop) => (payload) => payload?.bulkOperation && payload.userErrors.length === 0 ? Effect.succeed(payload.bulkOperation) : Effect.fail(new BulkOperationRefused({
85
+ operation,
86
+ shop,
87
+ userErrors: payload?.userErrors ?? []
88
+ }));
89
+ const renderBulkQuery = (query, parameters, shop) => Schema.encodeUnknownEffect(query.parameters)(parameters ?? {}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(EncodedParameters)), Effect.map((encoded) => query.document.map((part, index) => {
90
+ const slot = query.slots[index];
91
+ return slot === void 0 ? part : `${part}${literal(encoded[slot], query.enums.includes(slot))}`;
92
+ }).join("")), Effect.mapError((cause) => new AdminQueryFailed({
93
+ cause: withoutRequest(cause),
94
+ operation: query.name,
95
+ shop
96
+ })));
97
+ const submitQuery = ({ send, shop, bulkQueries }, guard) => (document, ...[parameters, options]) => bulkQueryFor(bulkQueries, document).pipe(Effect.tap((query) => Effect.annotateCurrentSpan("operation", query.name)), Effect.flatMap(guard), Effect.flatMap((query) => renderBulkQuery(query, parameters, shop)), Effect.flatMap((rendered) => send(RunQuery, {
98
+ groupObjects: options?.groupObjects ?? false,
99
+ query: rendered
100
+ })), Effect.flatMap(({ bulkOperationRunQuery }) => submitted("bulkOperationRunQuery", shop)(bulkOperationRunQuery)), Effect.withSpan("ShopifyBulk.query"));
101
+ const transportForShop = (shop) => Effect.gen(function* transportForShopGen() {
102
+ return {
103
+ bulkQueries: yield* AdminBulkQueryRegistry,
104
+ client: yield* HttpClient.HttpClient,
105
+ send: yield* sendForShop(shop),
106
+ shop
107
+ };
108
+ });
109
+ const transportForToken = (credential, apiVersion) => Effect.gen(function* transportForTokenGen() {
110
+ return {
111
+ bulkQueries: yield* AdminBulkQueryRegistry,
112
+ client: yield* HttpClient.HttpClient,
113
+ send: yield* sendForToken(credential, apiVersion),
114
+ shop: credential.shop
115
+ };
116
+ });
117
+ //#endregion
118
+ export { BulkOperation, Submitted, bulkOperationFields, reads, submitQuery, submitted, transportForShop, transportForToken };