@distilled.cloud/core 1.0.0-rc.11 → 1.0.0-rc.13

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 (149) hide show
  1. package/lib/api.d.ts +4 -4
  2. package/lib/api.d.ts.map +1 -1
  3. package/lib/api.js +1 -1
  4. package/lib/api.js.map +1 -1
  5. package/lib/category.js +1 -1
  6. package/lib/category.js.map +1 -1
  7. package/lib/codegen/boolean-string-enums.d.ts +8 -8
  8. package/lib/codegen/boolean-string-enums.d.ts.map +1 -1
  9. package/lib/codegen/boolean-string-enums.js +84 -27
  10. package/lib/codegen/boolean-string-enums.js.map +1 -1
  11. package/lib/codegen/boolean-string-enums.test.js +130 -0
  12. package/lib/codegen/boolean-string-enums.test.js.map +1 -1
  13. package/lib/codegen/cli.js +2 -2
  14. package/lib/codegen/cli.js.map +1 -1
  15. package/lib/codegen/generator.d.ts +20 -3
  16. package/lib/codegen/generator.d.ts.map +1 -1
  17. package/lib/codegen/generator.js +36 -9
  18. package/lib/codegen/generator.js.map +1 -1
  19. package/lib/codegen/generator.test.d.ts +2 -0
  20. package/lib/codegen/generator.test.d.ts.map +1 -0
  21. package/lib/codegen/generator.test.js +126 -0
  22. package/lib/codegen/generator.test.js.map +1 -0
  23. package/lib/codegen/graphql-client.d.ts +7 -1
  24. package/lib/codegen/graphql-client.d.ts.map +1 -1
  25. package/lib/codegen/graphql-client.js +262 -125
  26. package/lib/codegen/graphql-client.js.map +1 -1
  27. package/lib/codegen/graphql-client.test.js +104 -236
  28. package/lib/codegen/graphql-client.test.js.map +1 -1
  29. package/lib/codegen/openapi-binary.test.d.ts +2 -0
  30. package/lib/codegen/openapi-binary.test.d.ts.map +1 -0
  31. package/lib/codegen/openapi-binary.test.js +67 -0
  32. package/lib/codegen/openapi-binary.test.js.map +1 -0
  33. package/lib/codegen/openapi.d.ts +7 -1
  34. package/lib/codegen/openapi.d.ts.map +1 -1
  35. package/lib/codegen/openapi.js +15 -4
  36. package/lib/codegen/openapi.js.map +1 -1
  37. package/lib/codegen/patches.d.ts +16 -1
  38. package/lib/codegen/patches.d.ts.map +1 -1
  39. package/lib/codegen/patches.js +70 -4
  40. package/lib/codegen/patches.js.map +1 -1
  41. package/lib/codegen/patches.test.js +48 -2
  42. package/lib/codegen/patches.test.js.map +1 -1
  43. package/lib/codegen/untagged-unions.test.d.ts +2 -0
  44. package/lib/codegen/untagged-unions.test.d.ts.map +1 -0
  45. package/lib/codegen/untagged-unions.test.js +80 -0
  46. package/lib/codegen/untagged-unions.test.js.map +1 -0
  47. package/lib/errors.d.ts +2 -1
  48. package/lib/errors.d.ts.map +1 -1
  49. package/lib/errors.js +5 -2
  50. package/lib/errors.js.map +1 -1
  51. package/lib/fixtures/xml/upstream-validator.json +397 -0
  52. package/lib/graphql.d.ts +296 -205
  53. package/lib/graphql.d.ts.map +1 -1
  54. package/lib/graphql.js +968 -631
  55. package/lib/graphql.js.map +1 -1
  56. package/lib/graphql.test.js +134 -763
  57. package/lib/graphql.test.js.map +1 -1
  58. package/lib/graphql.types.d.ts +10 -0
  59. package/lib/graphql.types.d.ts.map +1 -1
  60. package/lib/graphql.types.js +4 -44
  61. package/lib/graphql.types.js.map +1 -1
  62. package/lib/index.d.ts +27 -0
  63. package/lib/index.d.ts.map +1 -0
  64. package/lib/index.js +27 -0
  65. package/lib/index.js.map +1 -0
  66. package/lib/pagination.d.ts +2 -1
  67. package/lib/pagination.d.ts.map +1 -1
  68. package/lib/pagination.js +24 -16
  69. package/lib/pagination.js.map +1 -1
  70. package/lib/pagination.test.d.ts +2 -0
  71. package/lib/pagination.test.d.ts.map +1 -0
  72. package/lib/pagination.test.js +134 -0
  73. package/lib/pagination.test.js.map +1 -0
  74. package/lib/protocol-http.d.ts +12 -17
  75. package/lib/protocol-http.d.ts.map +1 -1
  76. package/lib/protocol-http.js +100 -45
  77. package/lib/protocol-http.js.map +1 -1
  78. package/lib/protocol-http.test.js +130 -0
  79. package/lib/protocol-http.test.js.map +1 -1
  80. package/lib/protocol-rest.d.ts +18 -1
  81. package/lib/protocol-rest.d.ts.map +1 -1
  82. package/lib/protocol-rest.js +24 -4
  83. package/lib/protocol-rest.js.map +1 -1
  84. package/lib/query.d.ts +29 -0
  85. package/lib/query.d.ts.map +1 -0
  86. package/lib/query.js +27 -0
  87. package/lib/query.js.map +1 -0
  88. package/lib/response-validation.d.ts +67 -0
  89. package/lib/response-validation.d.ts.map +1 -0
  90. package/lib/response-validation.js +76 -0
  91. package/lib/response-validation.js.map +1 -0
  92. package/lib/response-validation.test.d.ts +2 -0
  93. package/lib/response-validation.test.d.ts.map +1 -0
  94. package/lib/response-validation.test.js +67 -0
  95. package/lib/response-validation.test.js.map +1 -0
  96. package/lib/testing.d.ts +39 -0
  97. package/lib/testing.d.ts.map +1 -0
  98. package/lib/testing.js +41 -0
  99. package/lib/testing.js.map +1 -0
  100. package/lib/trait.d.ts +29 -9
  101. package/lib/trait.d.ts.map +1 -1
  102. package/lib/trait.js +8 -0
  103. package/lib/trait.js.map +1 -1
  104. package/lib/xml.d.ts +44 -0
  105. package/lib/xml.d.ts.map +1 -0
  106. package/lib/xml.js +315 -0
  107. package/lib/xml.js.map +1 -0
  108. package/lib/xml.test.d.ts +2 -0
  109. package/lib/xml.test.d.ts.map +1 -0
  110. package/lib/xml.test.js +211 -0
  111. package/lib/xml.test.js.map +1 -0
  112. package/package.json +4 -4
  113. package/src/api.ts +4 -4
  114. package/src/category.ts +1 -1
  115. package/src/codegen/boolean-string-enums.test.ts +147 -0
  116. package/src/codegen/boolean-string-enums.ts +84 -27
  117. package/src/codegen/cli.ts +2 -2
  118. package/src/codegen/generator.test.ts +151 -0
  119. package/src/codegen/generator.ts +70 -12
  120. package/src/codegen/graphql-client.test.ts +116 -283
  121. package/src/codegen/graphql-client.ts +326 -169
  122. package/src/codegen/openapi-binary.test.ts +98 -0
  123. package/src/codegen/openapi.ts +26 -4
  124. package/src/codegen/patches.test.ts +66 -1
  125. package/src/codegen/patches.ts +84 -4
  126. package/src/codegen/untagged-unions.test.ts +89 -0
  127. package/src/errors.ts +5 -2
  128. package/src/fixtures/xml/upstream-validator.json +397 -0
  129. package/src/graphql.test.ts +174 -948
  130. package/src/graphql.ts +1592 -1167
  131. package/src/graphql.types.ts +92 -163
  132. package/src/index.ts +26 -0
  133. package/src/pagination.test.ts +191 -0
  134. package/src/pagination.ts +35 -25
  135. package/src/protocol-http.test.ts +163 -0
  136. package/src/protocol-http.ts +122 -46
  137. package/src/protocol-rest.ts +62 -13
  138. package/src/query.ts +39 -0
  139. package/src/response-validation.test.ts +95 -0
  140. package/src/response-validation.ts +116 -0
  141. package/src/testing.ts +78 -0
  142. package/src/trait.ts +39 -8
  143. package/src/xml.test.ts +269 -0
  144. package/src/xml.ts +339 -0
  145. package/lib/graphql.fixture.d.ts +0 -249
  146. package/lib/graphql.fixture.d.ts.map +0 -1
  147. package/lib/graphql.fixture.js +0 -240
  148. package/lib/graphql.fixture.js.map +0 -1
  149. package/src/graphql.fixture.ts +0 -371
package/lib/graphql.js CHANGED
@@ -1,11 +1,80 @@
1
- /** GraphQL selections, typed failures and Effect execution for generated SDKs. */
1
+ /**
2
+ * GraphQL Query algebra — the runtime generated GraphQL SDKs target.
3
+ *
4
+ * ── Why this exists ────────────────────────────────────────────────────────
5
+ * A GraphQL document is a *selection*, not an RPC. Distilled used to bake a
6
+ * max-depth selection into every operation (`Railway.me({})` downloaded the
7
+ * world). This module instead treats every field as a lazy `Query<Value>`:
8
+ * reading `user.email` records that field, and nothing is posted until
9
+ * `Query.fn` evaluates the plan you returned.
10
+ *
11
+ * ── The two phases ─────────────────────────────────────────────────────────
12
+ * 1. Build (your function). `Railway.me()` / `user.email` / `Query.map` do
13
+ * not talk to the network. They construct an expression tree:
14
+ * Of | Map | Filter | FlatMap | Prop | Root
15
+ * The inner function of `Query.fn` is therefore a *builder*, not an
16
+ * Effect generator. No `yield*` inside it.
17
+ *
18
+ * 2. Evaluate (`Query.fn`). Walk the returned plan, paint every GraphQL
19
+ * field that appears onto one selection forest, POST that document
20
+ * once through {@link GqlTransport}, then interpret Map/Filter/FlatMap
21
+ * against the JSON. Effects reached via FlatMap (e.g.
22
+ * `usernameIsAvailable(name)`) run *after* the POST, with real values.
23
+ *
24
+ * ── Combinators ────────────────────────────────────────────────────────────
25
+ * - `Query.of(x)` — a literal; never selected.
26
+ * - `Query.map` on a list — callback receives `Query<Item>` so `p.name` is
27
+ * still a Query (selected). On a scalar, callback receives the value
28
+ * after extract (`stars => stars > 5`). Relay connections (`edges { node }`)
29
+ * are lists of the node type, so `projects({ first: 20 }).pipe(Query.map)`
30
+ * maps projects, not edges.
31
+ * - `Query.filter` — predicate is `Query<Item> => Query<boolean>`; the
32
+ * boolean Query is walked for fields, then run per row after the POST.
33
+ * - `Query.flatMap` — after extract, run `(value) => Effect | Query`.
34
+ * - `Query.pages` / `Query.items` — a Stream over every page / node of a
35
+ * Relay connection, one POST per page (see "Pagination").
36
+ *
37
+ * ── Types ──────────────────────────────────────────────────────────────────
38
+ * `Query<Value>` is `QueryNode<Value>` plus the GraphQL fields of `Value`,
39
+ * so `user.name` type-checks. That copy is a circular type alias
40
+ * (`Query<User>` contains `Query<Project[]>` which contains `Query<User>`
41
+ * again). TypeScript leaves the cycle lazy; there is no depth cap.
42
+ *
43
+ * ── Transport ──────────────────────────────────────────────────────────────
44
+ * {@link GqlTransport.execute} posts `{ query, variables, operationName }`.
45
+ * The Effect it returns may require Credentials / HttpClient; those leak
46
+ * into `Query.fn`'s requirements. It returns the parsed `data` and raw
47
+ * `errors`; `Query.fn` classifies the errors (see "Errors" below). Compile
48
+ * failures are {@link GqlError} defects.
49
+ *
50
+ * Generated SDKs export *roots* (`Railway.me`, `Railway.project`).
51
+ * Combinators live in `@distilled.cloud/core/query`.
52
+ */
53
+ import * as Context from "effect/Context";
2
54
  import * as Data from "effect/Data";
3
55
  import * as Effect from "effect/Effect";
4
56
  import * as Option from "effect/Option";
5
- import * as S from "effect/Schema";
6
57
  import * as Schedule from "effect/Schedule";
58
+ import * as S from "effect/Schema";
7
59
  import * as Stream from "effect/Stream";
8
- import { Kind, parseType, print, } from "graphql";
60
+ export const QuerySymbol = Symbol.for("@distilled.cloud/graphql/Query");
61
+ const inspect = Symbol.for("nodejs.util.inspect.custom");
62
+ /** A plan that cannot be compiled (a programming error, raised as a defect). */
63
+ export class GqlError {
64
+ message;
65
+ _tag = "GqlError";
66
+ constructor(message) {
67
+ this.message = message;
68
+ }
69
+ }
70
+ // ── Errors ──────────────────────────────────────────────────────────────────
71
+ //
72
+ // Every entry of a GraphQL `errors` array becomes one typed issue. Generated
73
+ // SDKs declare tagged error classes plus the matchers that recognize them,
74
+ // scoped to the root fields that can return them (or global). `Query.fn`
75
+ // classifies each entry against the roots its path points at, so the error
76
+ // channel is the union of the errors declared by the roots the plan reads.
77
+ /** Fields every classified GraphQL error carries. */
9
78
  export const errorFields = {
10
79
  message: S.String,
11
80
  code: S.optional(S.String),
@@ -14,14 +83,17 @@ export const errorFields = {
14
83
  extensions: S.optional(S.Unknown),
15
84
  traceId: S.optional(S.String),
16
85
  status: S.optional(S.Number),
86
+ /** Server retry hint in seconds. */
17
87
  retryAfter: S.optional(S.Number),
18
88
  };
89
+ /** A GraphQL error no declared matcher recognizes. */
19
90
  export class UnknownGraphQLError extends S.TaggedError()("UnknownGraphQLError", {
20
91
  ...errorFields,
92
+ /** Root field the error path points at, when the path is known. */
21
93
  coordinate: S.optional(S.String),
22
94
  }) {
23
95
  }
24
- /** All errors from one GraphQL execution, with the original partial response. */
96
+ /** One response carried errors with different tags; none is hidden. */
25
97
  export class GraphQLFailure extends Data.TaggedError("GraphQLFailure") {
26
98
  get message() {
27
99
  return this.errors
@@ -29,479 +101,741 @@ export class GraphQLFailure extends Data.TaggedError("GraphQLFailure") {
29
101
  .join("; ");
30
102
  }
31
103
  }
104
+ /** The request never produced a GraphQL response (network, HTTP, non-JSON). */
32
105
  export class GraphQLTransportError extends Data.TaggedError("GraphQLTransportError") {
33
106
  }
34
- export class GraphQLDecodeError extends Data.TaggedError("GraphQLDecodeError") {
107
+ /** A paginated connection reported more pages without a new cursor. */
108
+ export class GraphQLPaginationError extends Data.TaggedError("GraphQLPaginationError") {
35
109
  }
36
- export class GraphQLRequestError extends Data.TaggedError("GraphQLRequestError") {
110
+ export const errorSpec = (make, tag, matchers, options = {}) => ({
111
+ tag,
112
+ make: (props) => new make(props),
113
+ matchers,
114
+ retryable: options.retryable ?? false,
115
+ global: options.global ?? false,
116
+ });
117
+ export const scalarField = (name) => ({
118
+ name,
119
+ kind: "scalar",
120
+ });
121
+ export const objectField = (name, objectType) => ({
122
+ name,
123
+ kind: "object",
124
+ of: objectType,
125
+ });
126
+ export const listField = (name, itemType, argTypes) => ({
127
+ name,
128
+ kind: "list",
129
+ of: itemType,
130
+ argTypes,
131
+ });
132
+ /** Relay connection: GraphQL `edges { node }` presented as `Query<Item[]>`. */
133
+ export const connectionField = (name, nodeType, argTypes) => ({
134
+ name,
135
+ kind: "connection",
136
+ of: nodeType,
137
+ argTypes,
138
+ });
139
+ const objectRef = (meta) => ({ tag: "object", meta });
140
+ const listRef = (itemType) => ({
141
+ tag: "list",
142
+ of: itemType,
143
+ });
144
+ const fieldResult = (parent, field) => {
145
+ const inner = () => {
146
+ if (field.kind === "scalar")
147
+ return { tag: "scalar" };
148
+ if (field.kind === "object")
149
+ return objectRef(field.of);
150
+ return listRef(objectRef(field.of)); // list | connection
151
+ };
152
+ return parent.tag === "list"
153
+ ? listRef(fieldResult(parent.of, field))
154
+ : inner();
155
+ };
156
+ const unwrapList = (typeRef) => {
157
+ if (typeRef.tag !== "list")
158
+ throw new GqlError("expected a list Query");
159
+ return typeRef.of;
160
+ };
161
+ const fieldsOf = (typeRef) => {
162
+ if (typeRef.tag === "object")
163
+ return typeRef.meta.fields;
164
+ if (typeRef.tag === "list")
165
+ return fieldsOf(typeRef.of);
166
+ return {};
167
+ };
168
+ export class GqlTransport extends Context.Service()("@distilled.cloud/graphql/Transport") {
37
169
  }
38
- /** True only when every issue has an allowed tag; mixed failures are never hidden. */
39
- export const isErrorTag = (error, tags) => {
40
- const allowed = typeof tags === "string" ? [tags] : tags;
41
- return (error instanceof GraphQLFailure &&
42
- error.errors.length > 0 &&
43
- error.errors.every((issue) => allowed.includes(issue._tag)));
44
- };
45
- /** Recover only an aggregate whose every issue matches. The original aggregate is also supplied. */
46
- export const catchTags = (tags, handler) => (self) => self.pipe(Effect.catch((error) => isErrorTag(error, tags)
47
- ? handler(error.errors[0], error)
48
- : Effect.fail(error)));
49
- const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
50
- const named = (ref) => ref.replace(/[\[\]!]/g, "");
51
- const nameNode = (value) => ({ kind: Kind.NAME, value });
52
- const requestError = (message) => {
53
- throw new GraphQLRequestError({ message });
54
- };
55
- const assertName = (name) => {
56
- if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(name))
57
- requestError(`Invalid GraphQL name ${name}`);
58
- };
59
- /** Validate and encode inputs recursively. Undefined omits; null stays null. */
60
- const encodeInput = (model, ref, value, at) => {
61
- if (value === undefined)
62
- return undefined;
63
- if (value === null) {
64
- if (ref.endsWith("!"))
65
- requestError(`${at} must not be null`);
66
- return null;
67
- }
68
- const base = ref.endsWith("!") ? ref.slice(0, -1) : ref;
69
- if (base.startsWith("[")) {
70
- if (!Array.isArray(value))
71
- requestError(`${at} must be an array`);
72
- return value.map((v, i) => {
73
- if (v === undefined)
74
- requestError(`${at}[${i}] must not be undefined`);
75
- return encodeInput(model, base.slice(1, -1), v, `${at}[${i}]`);
76
- });
170
+ const log = (..._args) => { };
171
+ export class QueryNode {
172
+ expr;
173
+ [QuerySymbol] = QuerySymbol;
174
+ constructor(expr) {
175
+ this.expr = expr;
176
+ return proxy(this);
77
177
  }
78
- const type = model.types[base];
79
- if (!type)
80
- requestError(`${at}: unknown input type ${base}`);
81
- if (type.kind === "INPUT_OBJECT") {
82
- if (!object(value))
83
- requestError(`${at} must be an input object`);
84
- const input = value;
85
- const result = {};
86
- for (const key of Object.keys(input))
87
- if (!type.inputFields?.[key])
88
- requestError(`${at}: unknown input field ${key}`);
89
- for (const [key, field] of Object.entries(type.inputFields ?? {})) {
90
- if (input[key] === undefined) {
91
- if (field.type.endsWith("!") && field.defaultValue === undefined)
92
- requestError(`${at}.${key} is required`);
178
+ pipe(...steps) {
179
+ return steps.reduce((current, step) => step(current), this);
180
+ }
181
+ [inspect]() {
182
+ return printExpr(this.expr);
183
+ }
184
+ toString() {
185
+ return this[inspect]();
186
+ }
187
+ }
188
+ export const isQuery = (value) => (typeof value === "object" || typeof value === "function") &&
189
+ value !== null &&
190
+ QuerySymbol in value;
191
+ const make = (expr) => new QueryNode(expr);
192
+ const itemQuery = (list) => new QueryNode({
193
+ _tag: "Item",
194
+ list: list.expr,
195
+ type: unwrapList(list.expr.type),
196
+ });
197
+ function proxy(self) {
198
+ const callable = Object.assign(function (bag) {
199
+ return bag == null ? self : applyBag(self, bag);
200
+ }, self);
201
+ return new Proxy(callable, {
202
+ has: (_, prop) => prop === QuerySymbol ||
203
+ prop === inspect ||
204
+ prop in self ||
205
+ (typeof prop === "string" && prop in fieldsOf(self.expr.type)),
206
+ get: (_target, prop) => {
207
+ if (prop === QuerySymbol)
208
+ return QuerySymbol;
209
+ if (prop === inspect)
210
+ return self[inspect].bind(self);
211
+ if (prop === "pipe")
212
+ return self.pipe.bind(self);
213
+ if (typeof prop === "string" && prop in self) {
214
+ const member = self[prop];
215
+ return typeof member === "function" ? member.bind(self) : member;
93
216
  }
94
- else
95
- result[key] = encodeInput(model, field.type, input[key], `${at}.${key}`);
96
- }
97
- return result;
98
- }
99
- if (type.kind === "ENUM") {
100
- if (typeof value !== "string" || !type.enumValues?.includes(value))
101
- requestError(`${at}: invalid ${base}`);
102
- }
103
- else if (type.scalar === "string" || base === "String" || base === "ID") {
104
- if (typeof value !== "string")
105
- requestError(`${at} must be a string`);
106
- }
107
- else if (type.scalar === "number" || base === "Int" || base === "Float") {
108
- if (typeof value !== "number" || !Number.isFinite(value))
109
- requestError(`${at} must be a finite number`);
110
- if (base === "Int" &&
111
- (!Number.isInteger(value) ||
112
- value < -2147483648 ||
113
- value > 2147483647))
114
- requestError(`${at} must be a GraphQL Int`);
115
- }
116
- else if (type.scalar === "boolean" || base === "Boolean") {
117
- if (typeof value !== "boolean")
118
- requestError(`${at} must be a boolean`);
119
- }
120
- if (type.scalar === "string | number" &&
121
- typeof value !== "string" &&
122
- typeof value !== "number")
123
- requestError(`${at} must be a string or number`);
124
- if (base === "BigInt" &&
125
- ((typeof value === "number" && !Number.isSafeInteger(value)) ||
126
- (typeof value === "string" && !/^-?\d+$/.test(value))))
127
- requestError(`${at} must be a safe integer or an integer string`);
128
- // JSON custom scalars must still be transportable JSON; preserve scalar contents.
129
- try {
130
- return JSON.parse(JSON.stringify(value));
131
- }
132
- catch {
133
- return requestError(`${at} is not a JSON-encodable scalar`);
134
- }
135
- };
136
- /** Compile one selection to a GraphQL AST, variables, and an error/decoder path map. */
137
- export const compile = (model, kind, selection, operationName = "Distilled") => {
138
- const rootType = kind === "query" ? model.queryType : model.mutationType;
139
- if (!rootType)
140
- return requestError(`Schema has no ${kind} root`);
141
- assertName(operationName);
142
- const variables = {};
143
- const definitions = [];
144
- const variable = (ref, value) => {
145
- const id = `v${definitions.length}`;
146
- variables[id] = value;
147
- definitions.push({
148
- kind: Kind.VARIABLE_DEFINITION,
149
- variable: { kind: Kind.VARIABLE, name: nameNode(id) },
150
- type: parseType(ref),
217
+ if (typeof prop !== "string")
218
+ return undefined;
219
+ const field = fieldsOf(self.expr.type)[prop];
220
+ if (!field)
221
+ return undefined;
222
+ return make({
223
+ _tag: "Prop",
224
+ parent: self.expr,
225
+ field,
226
+ type: fieldResult(self.expr.type, field),
227
+ });
228
+ },
229
+ apply: (_target, _thisArg, args) => applyBag(self, args[0] ?? {}),
230
+ });
231
+ }
232
+ const applyBag = (self, bag) => {
233
+ const argTypes = self.expr._tag === "Prop"
234
+ ? self.expr.field.argTypes
235
+ : self.expr._tag === "Root"
236
+ ? self.expr.argTypes
237
+ : undefined;
238
+ const args = {};
239
+ for (const [argName, argValue] of Object.entries(bag)) {
240
+ if (argTypes && argName in argTypes)
241
+ args[argName] = argValue;
242
+ }
243
+ if (self.expr._tag === "Prop") {
244
+ return make({
245
+ ...self.expr,
246
+ args: { ...self.expr.args, ...args },
247
+ });
248
+ }
249
+ if (self.expr._tag === "Root") {
250
+ return make({
251
+ ...self.expr,
252
+ args: { ...self.expr.args, ...args },
151
253
  });
152
- return { kind: Kind.VARIABLE, name: nameNode(id) };
254
+ }
255
+ return self;
256
+ };
257
+ export const root = (op, field, meta, args, argTypes, errors = []) => make({
258
+ _tag: "Root",
259
+ op,
260
+ field,
261
+ args,
262
+ argTypes,
263
+ type: objectRef(meta),
264
+ errors,
265
+ });
266
+ export const rootList = (op, field, meta, args, argTypes, errors = []) => make({
267
+ _tag: "Root",
268
+ op,
269
+ field,
270
+ args,
271
+ argTypes,
272
+ type: listRef(objectRef(meta)),
273
+ errors,
274
+ });
275
+ /** Root Relay connection: typed as `Query<Item[]>`, document uses `edges { node }`. */
276
+ export const rootConnection = (op, field, meta, args, argTypes, errors = []) => make({
277
+ _tag: "Root",
278
+ op,
279
+ field,
280
+ args,
281
+ argTypes,
282
+ type: listRef(objectRef(meta)),
283
+ connection: true,
284
+ errors,
285
+ });
286
+ /** Root field whose GraphQL type is a scalar, enum, or list of those. */
287
+ export const rootLeaf = (op, field, list, args, argTypes, errors = []) => make({
288
+ _tag: "Root",
289
+ op,
290
+ field,
291
+ args,
292
+ argTypes,
293
+ type: list ? listRef({ tag: "scalar" }) : { tag: "scalar" },
294
+ errors,
295
+ });
296
+ const printExpr = (expr) => {
297
+ switch (expr._tag) {
298
+ case "Root":
299
+ return expr.args
300
+ ? `${expr.field}(${JSON.stringify(expr.args)})`
301
+ : expr.field;
302
+ case "Prop":
303
+ return `${printExpr(expr.parent)}.${expr.field.name}`;
304
+ case "Item":
305
+ return "$item";
306
+ case "MapValue":
307
+ return `map(${printExpr(expr.parent)})`;
308
+ case "MapItems":
309
+ return `mapItems(${printExpr(expr.parent)})`;
310
+ case "Filter":
311
+ return `filter(${printExpr(expr.parent)})`;
312
+ case "FlatMap":
313
+ return `flatMap(${printExpr(expr.parent)})`;
314
+ case "Literal":
315
+ return `of(${String(expr.value)})`;
316
+ }
317
+ };
318
+ const emptySel = (field) => ({
319
+ field,
320
+ alias: undefined,
321
+ args: undefined,
322
+ children: new Map(),
323
+ isScalar: false,
324
+ isList: false,
325
+ });
326
+ const stableStringify = (value) => {
327
+ if (value === null || typeof value !== "object")
328
+ return JSON.stringify(value);
329
+ if (Array.isArray(value))
330
+ return `[${value.map(stableStringify).join(",")}]`;
331
+ const keys = Object.keys(value).sort();
332
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
333
+ };
334
+ const bindArgs = (forest, args, argTypes) => {
335
+ if (args === undefined)
336
+ return undefined;
337
+ const bound = {};
338
+ let hasArgs = false;
339
+ for (const [name, value] of Object.entries(args)) {
340
+ if (value === undefined)
341
+ continue;
342
+ forest.vars.set(`v${forest.varI}`, {
343
+ type: argTypes?.[name] ?? "String",
344
+ value,
345
+ });
346
+ bound[name] = `v${forest.varI++}`;
347
+ hasArgs = true;
348
+ }
349
+ return hasArgs ? bound : undefined;
350
+ };
351
+ const ensureChild = (forest, parent, field, options) => {
352
+ const key = options.args === undefined
353
+ ? field
354
+ : `${field}:${stableStringify(options.args)}`;
355
+ const existing = parent.children.get(key);
356
+ if (existing)
357
+ return existing;
358
+ const taken = new Set([...parent.children.values()].map((c) => c.alias ?? c.field));
359
+ let alias;
360
+ if (taken.has(field)) {
361
+ do {
362
+ alias = `${field}_${forest.aliasI++}`;
363
+ } while (taken.has(alias));
364
+ }
365
+ const sel = {
366
+ field,
367
+ alias,
368
+ args: bindArgs(forest, options.args, options.argTypes),
369
+ children: new Map(),
370
+ isScalar: options.isScalar ?? false,
371
+ isList: options.isList ?? false,
153
372
  };
154
- const walk = (parent, input) => {
155
- if (!object(input))
156
- return requestError(`${parent} requires an object selection`);
157
- const type = model.types[parent];
158
- if (!type)
159
- return requestError(`Unknown GraphQL type ${parent}`);
160
- const nodes = [];
161
- const positions = [];
162
- const addField = (fieldName, selected, alias) => {
163
- if (selected === false || selected === undefined)
164
- return;
165
- assertName(fieldName);
166
- if (alias)
167
- assertName(alias);
168
- const key = alias ?? fieldName;
169
- if (positions.some((p) => p.key === key))
170
- return requestError(`Duplicate response key ${parent}.${key}; use distinct aliases`);
171
- const field = fieldName === "__typename"
172
- ? { type: "String!", args: {}, errors: [] }
173
- : type.fields?.[fieldName];
174
- if (!field)
175
- return requestError(`Unknown field ${parent}.${fieldName}`);
176
- const wrapper = object(selected) &&
177
- ("where" in selected ||
178
- "select" in selected ||
179
- "$include" in selected ||
180
- "$skip" in selected)
181
- ? selected
182
- : undefined;
183
- const args = wrapper?.where ?? {};
184
- if (!object(args))
185
- return requestError(`${parent}.${fieldName}.where must be an object`);
186
- for (const arg of Object.keys(args))
187
- if (!field.args[arg])
188
- return requestError(`Unknown argument ${parent}.${fieldName}(${arg})`);
189
- const arguments_ = [];
190
- for (const [arg, definition] of Object.entries(field.args)) {
191
- if (args[arg] === undefined) {
192
- if (definition.type.endsWith("!") &&
193
- definition.defaultValue === undefined)
194
- return requestError(`${parent}.${fieldName}(${arg}) is required`);
195
- }
196
- else
197
- arguments_.push({
198
- kind: Kind.ARGUMENT,
199
- name: nameNode(arg),
200
- value: variable(definition.type, encodeInput(model, definition.type, args[arg], `${parent}.${fieldName}(${arg})`)),
201
- });
202
- }
203
- const directives = [];
204
- for (const directive of ["include", "skip"]) {
205
- const value = wrapper?.[`$${directive}`];
206
- if (value !== undefined) {
207
- if (typeof value !== "boolean")
208
- return requestError(`$${directive} must be a boolean`);
209
- directives.push({
210
- kind: Kind.DIRECTIVE,
211
- name: nameNode(directive),
212
- arguments: [
213
- {
214
- kind: Kind.ARGUMENT,
215
- name: nameNode("if"),
216
- value: variable("Boolean!", value),
217
- },
218
- ],
219
- });
220
- }
221
- }
222
- const outputType = model.types[named(field.type)];
223
- const leaf = fieldName === "__typename" ||
224
- outputType?.kind === "SCALAR" ||
225
- outputType?.kind === "ENUM";
226
- const sub = wrapper ? wrapper.select : selected;
227
- let child = { nodes: [], positions: [] };
228
- if (leaf) {
229
- if (sub !== true && sub !== undefined)
230
- return requestError(`${parent}.${fieldName} is a leaf and takes no selection`);
373
+ parent.children.set(key, sel);
374
+ return sel;
375
+ };
376
+ const paintConnection = (forest, connection) => {
377
+ const edges = ensureChild(forest, connection, "edges", { isList: true });
378
+ ensureChild(forest, edges, "node", {});
379
+ return ["edges", "node"];
380
+ };
381
+ const paintAbs = (forest, expr) => {
382
+ switch (expr._tag) {
383
+ case "Root": {
384
+ if (forest.kind !== undefined && forest.kind !== expr.op) {
385
+ throw new GqlError(`cannot mix ${forest.kind} and ${expr.op} in one document`);
231
386
  }
232
- else
233
- child = walk(named(field.type), sub);
234
- nodes.push({
235
- kind: Kind.FIELD,
236
- name: nameNode(fieldName),
237
- ...(alias ? { alias: nameNode(alias) } : {}),
238
- arguments: arguments_,
239
- directives,
240
- ...(!leaf
241
- ? {
242
- selectionSet: {
243
- kind: Kind.SELECTION_SET,
244
- selections: child.nodes,
245
- },
246
- }
247
- : {}),
387
+ forest.kind = expr.op;
388
+ const node = ensureChild(forest, forest.roots, expr.field, {
389
+ args: expr.args,
390
+ argTypes: expr.argTypes,
391
+ // Leaf roots (`projectDelete: Boolean!`) take no sub-selection.
392
+ isScalar: expr.type.tag === "scalar" ||
393
+ (expr.type.tag === "list" && expr.type.of.tag === "scalar"),
394
+ isList: expr.type.tag === "list" && !expr.connection,
248
395
  });
249
- const branches = {};
250
- // Branch metadata is attached to sentinel positions by walk and removed from regular children.
251
- for (const p of child.positions)
252
- if (p.field === "__fragment")
253
- branches[p.type] = p.children;
254
- positions.push({
255
- field: fieldName,
256
- key,
257
- coordinate: `${parent}.${fieldName}`,
258
- type: field.type,
259
- errors: field.errors,
260
- children: child.positions.filter((p) => p.field !== "__fragment"),
261
- branches,
262
- typename: fieldName === "__typename",
263
- conditional: wrapper?.$include === false || wrapper?.$skip === true,
396
+ const key = node.alias ?? node.field;
397
+ forest.rootAlias.set(expr, key);
398
+ forest.rootErrors.set(key, { field: expr.field, errors: expr.errors });
399
+ return expr.connection ? [key, ...paintConnection(forest, node)] : [key];
400
+ }
401
+ case "Prop": {
402
+ const parentPath = paintAbs(forest, expr.parent);
403
+ const parentSel = selAt(forest.roots, parentPath);
404
+ const node = ensureChild(forest, parentSel, expr.field.name, {
405
+ args: expr.args,
406
+ argTypes: expr.field.argTypes,
407
+ isScalar: expr.field.kind === "scalar",
408
+ isList: expr.field.kind === "list",
264
409
  });
265
- };
266
- for (const [key, value] of Object.entries(input)) {
267
- if (key === "__alias") {
268
- if (!object(value))
269
- return requestError(`${parent}.__alias must be an object`);
270
- for (const [alias, fields] of Object.entries(value)) {
271
- if (!object(fields) || Object.keys(fields).length !== 1)
272
- return requestError(`Alias ${alias} must select exactly one field`);
273
- const [field, selected] = Object.entries(fields)[0];
274
- addField(field, selected, alias);
275
- }
276
- }
277
- else if (key === "__on") {
278
- if (!object(value))
279
- return requestError(`${parent}.__on must be an object`);
280
- for (const [branch, fields] of Object.entries(value)) {
281
- if (!type.possibleTypes?.includes(branch))
282
- return requestError(`${branch} is not a possible type of ${parent}`);
283
- const children = walk(branch, fields);
284
- nodes.push({
285
- kind: Kind.INLINE_FRAGMENT,
286
- typeCondition: { kind: Kind.NAMED_TYPE, name: nameNode(branch) },
287
- selectionSet: {
288
- kind: Kind.SELECTION_SET,
289
- selections: children.nodes,
290
- },
291
- });
292
- positions.push({
293
- field: "__fragment",
294
- key: "",
295
- coordinate: parent,
296
- type: branch,
297
- errors: [],
298
- children: children.positions,
299
- branches: {},
300
- conditional: false,
301
- });
302
- }
410
+ const path = [...parentPath, node.alias ?? node.field];
411
+ return expr.field.kind === "connection"
412
+ ? [...path, ...paintConnection(forest, node)]
413
+ : path;
414
+ }
415
+ default:
416
+ throw new GqlError(`paintAbs: ${expr._tag}`);
417
+ }
418
+ };
419
+ const selAt = (roots, path) => {
420
+ let current = roots;
421
+ for (const segment of path) {
422
+ if (segment === "*")
423
+ continue;
424
+ const child = [...current.children.values()].find((candidate) => (candidate.alias ?? candidate.field) === segment);
425
+ if (!child) {
426
+ throw new GqlError(`internal: missing ${String(segment)}`);
427
+ }
428
+ current = child;
429
+ }
430
+ return current;
431
+ };
432
+ const connectionNodeSel = (connection) => {
433
+ const edges = [...connection.children.values()].find((c) => c.field === "edges");
434
+ const node = edges
435
+ ? [...edges.children.values()].find((c) => c.field === "node")
436
+ : undefined;
437
+ if (!node)
438
+ throw new GqlError("internal: connection missing edges.node");
439
+ return node;
440
+ };
441
+ const relSel = (expr, listSel) => {
442
+ switch (expr._tag) {
443
+ case "Item":
444
+ return listSel;
445
+ case "Prop": {
446
+ const parent = relSel(expr.parent, listSel);
447
+ const child = [...parent.children.values()].find((c) => c.field === expr.field.name);
448
+ if (!child) {
449
+ throw new GqlError(`missing ${expr.field.name} in list selection`);
303
450
  }
304
- else
305
- addField(key, value);
451
+ return expr.field.kind === "connection"
452
+ ? connectionNodeSel(child)
453
+ : child;
306
454
  }
307
- if (type.possibleTypes?.length && !positions.some((p) => p.typename))
308
- addField("__typename", true);
309
- if (!nodes.length)
310
- return requestError(`${parent} has an empty selection`);
311
- return { nodes, positions };
312
- };
313
- const selected = walk(rootType, selection);
314
- const query = print({
315
- kind: Kind.DOCUMENT,
316
- definitions: [
317
- {
318
- kind: Kind.OPERATION_DEFINITION,
319
- operation: kind,
320
- name: nameNode(operationName),
321
- variableDefinitions: definitions,
322
- selectionSet: { kind: Kind.SELECTION_SET, selections: selected.nodes },
323
- },
324
- ],
325
- });
455
+ default:
456
+ return listSel;
457
+ }
458
+ };
459
+ const paintRel = (forest, expr, listSel) => {
460
+ switch (expr._tag) {
461
+ case "Item":
462
+ case "Literal":
463
+ return;
464
+ case "Prop": {
465
+ paintRel(forest, expr.parent, listSel);
466
+ const child = ensureChild(forest, relSel(expr.parent, listSel), expr.field.name, {
467
+ args: expr.args,
468
+ argTypes: expr.field.argTypes,
469
+ isScalar: expr.field.kind === "scalar",
470
+ isList: expr.field.kind === "list",
471
+ });
472
+ if (expr.field.kind === "connection")
473
+ paintConnection(forest, child);
474
+ return;
475
+ }
476
+ case "MapValue":
477
+ case "FlatMap":
478
+ paintRel(forest, expr.parent, listSel);
479
+ return;
480
+ case "MapItems":
481
+ paintRel(forest, expr.parent, listSel);
482
+ visitRel(expr.mapped, forest, relSel(expr.parent, listSel));
483
+ return;
484
+ case "Filter":
485
+ paintRel(forest, expr.parent, listSel);
486
+ paintRel(forest, expr.predicate, relSel(expr.parent, listSel));
487
+ return;
488
+ case "Root":
489
+ throw new GqlError("root inside list item");
490
+ }
491
+ };
492
+ const visitExpr = (forest, expr) => {
493
+ switch (expr._tag) {
494
+ case "Root":
495
+ case "Prop":
496
+ paintAbs(forest, expr);
497
+ return;
498
+ case "Item":
499
+ return;
500
+ case "Literal":
501
+ return;
502
+ case "MapValue":
503
+ case "FlatMap":
504
+ visitExpr(forest, expr.parent);
505
+ return;
506
+ case "MapItems": {
507
+ visitExpr(forest, expr.parent);
508
+ const path = paintAbs(forest, graphqlBase(expr.parent));
509
+ visitRel(expr.mapped, forest, selAt(forest.roots, path));
510
+ return;
511
+ }
512
+ case "Filter": {
513
+ visitExpr(forest, expr.parent);
514
+ const path = paintAbs(forest, graphqlBase(expr.parent));
515
+ paintRel(forest, expr.predicate, selAt(forest.roots, path));
516
+ return;
517
+ }
518
+ }
519
+ };
520
+ const graphqlBase = (expr) => {
521
+ switch (expr._tag) {
522
+ case "Root":
523
+ case "Prop":
524
+ case "Item":
525
+ return expr;
526
+ case "MapValue":
527
+ case "MapItems":
528
+ case "Filter":
529
+ case "FlatMap":
530
+ return graphqlBase(expr.parent);
531
+ case "Literal":
532
+ throw new GqlError("literal has no graphql base");
533
+ }
534
+ };
535
+ const visitRel = (plan, forest, listSel) => {
536
+ if (isQuery(plan)) {
537
+ paintRel(forest, plan.expr, listSel);
538
+ return;
539
+ }
540
+ if (plan !== null && typeof plan === "object" && !Effect.isEffect(plan)) {
541
+ for (const nested of Object.values(plan)) {
542
+ visitRel(nested, forest, listSel);
543
+ }
544
+ }
545
+ };
546
+ /**
547
+ * Recurse through a builder return value. Query nodes contribute GraphQL
548
+ * fields to `forest`; Effects are ignored until interpret (FlatMap).
549
+ */
550
+ const visitPlan = (plan, forest) => {
551
+ if (isQuery(plan)) {
552
+ log("visit", printExpr(plan.expr));
553
+ visitExpr(forest, plan.expr);
554
+ return;
555
+ }
556
+ if (Effect.isEffect(plan))
557
+ return;
558
+ if (plan !== null && typeof plan === "object" && !Array.isArray(plan)) {
559
+ for (const nested of Object.values(plan)) {
560
+ visitPlan(nested, forest);
561
+ }
562
+ }
563
+ };
564
+ const validate = (node) => {
565
+ for (const child of node.children.values()) {
566
+ if (!child.isScalar && child.children.size === 0) {
567
+ return `field "${child.field}" has no sub-selection`;
568
+ }
569
+ const inner = validate(child);
570
+ if (inner)
571
+ return inner;
572
+ }
573
+ return undefined;
574
+ };
575
+ const printChildren = (node, indent) => {
576
+ const pad = " ".repeat(indent);
577
+ const lines = [];
578
+ for (const child of node.children.values()) {
579
+ let head = child.alias ? `${child.alias}: ${child.field}` : child.field;
580
+ if (child.args && Object.keys(child.args).length > 0) {
581
+ const printed = Object.entries(child.args)
582
+ .map(([argName, varName]) => `${argName}: $${varName}`)
583
+ .join(", ");
584
+ head += `(${printed})`;
585
+ }
586
+ if (child.isScalar)
587
+ lines.push(`${pad}${head}`);
588
+ else {
589
+ lines.push(`${pad}${head} {`);
590
+ lines.push(printChildren(child, indent + 1));
591
+ lines.push(`${pad}}`);
592
+ }
593
+ }
594
+ return lines.join("\n");
595
+ };
596
+ const emptyForest = () => ({
597
+ kind: undefined,
598
+ roots: emptySel(""),
599
+ vars: new Map(),
600
+ varI: 0,
601
+ aliasI: 0,
602
+ rootAlias: new WeakMap(),
603
+ rootErrors: new Map(),
604
+ });
605
+ const emit = (forest) => {
606
+ const err = validate(forest.roots);
607
+ if (err)
608
+ throw new GqlError(err);
609
+ const kind = forest.kind ?? "query";
610
+ const varDefs = [...forest.vars.entries()]
611
+ .map(([name, binding]) => `$${name}: ${binding.type}`)
612
+ .join(", ");
613
+ const header = varDefs.length > 0 ? `${kind} Gql(${varDefs})` : `${kind} Gql`;
614
+ const variables = {};
615
+ for (const [name, binding] of forest.vars)
616
+ variables[name] = binding.value;
326
617
  return {
327
- query,
618
+ document: `${header} {\n${printChildren(forest.roots, 1)}\n}`,
619
+ operationName: "Gql",
328
620
  variables,
329
- operationName,
330
621
  kind,
331
- rootType,
332
- positions: selected.positions,
622
+ tree: forest.roots,
623
+ rootAlias: forest.rootAlias,
624
+ rootErrors: forest.rootErrors,
333
625
  };
334
626
  };
335
- const decodeFailure = (message, path) => {
336
- throw new GraphQLDecodeError({ message, path });
337
- };
338
- /** Validate only selected fields. Preserve the schema's nullability exactly. */
339
- const decodeData = (model, ref, data, positions, branches, path) => {
340
- if (data === null) {
341
- if (ref.endsWith("!"))
342
- return decodeFailure(`Non-null ${ref} was null`, path);
343
- return null;
344
- }
345
- if (data === undefined)
346
- return decodeFailure(`Missing selected ${ref}`, path);
347
- const base = ref.endsWith("!") ? ref.slice(0, -1) : ref;
348
- if (base.startsWith("[")) {
349
- if (!Array.isArray(data))
350
- return decodeFailure(`Expected ${ref} array`, path);
351
- return data.map((v, i) => decodeData(model, base.slice(1, -1), v, positions, branches, [
352
- ...path,
353
- i,
354
- ]));
355
- }
356
- const type = model.types[base];
357
- if (!type)
358
- return decodeFailure(`Unknown response type ${base}`, path);
359
- if (type.kind === "SCALAR") {
360
- const primitive = type.scalar ??
361
- {
362
- String: "string",
363
- ID: "string",
364
- Boolean: "boolean",
365
- Int: "number",
366
- Float: "number",
367
- }[base];
368
- if (primitive === "string | number" &&
369
- typeof data !== "string" &&
370
- typeof data !== "number")
371
- return decodeFailure(`Expected ${base}`, path);
372
- if (base === "BigInt" &&
373
- ((typeof data === "number" && !Number.isSafeInteger(data)) ||
374
- (typeof data === "string" && !/^-?\d+$/.test(data))))
375
- return decodeFailure(`Invalid ${base}`, path);
376
- if (["string", "number", "boolean"].includes(primitive ?? "") &&
377
- typeof data !== primitive)
378
- return decodeFailure(`Expected ${base}`, path);
379
- if (typeof data === "number" &&
380
- (!Number.isFinite(data) ||
381
- (base === "Int" &&
382
- (!Number.isInteger(data) || data < -2147483648 || data > 2147483647))))
383
- return decodeFailure(`Invalid ${base}`, path);
384
- return data;
385
- }
386
- if (type.kind === "ENUM") {
387
- if (typeof data !== "string" || !type.enumValues?.includes(data))
388
- return decodeFailure(`Invalid ${base} enum`, path);
389
- return data;
390
- }
391
- if (!object(data))
392
- return decodeFailure(`Expected ${base} object`, path);
393
- if (type.possibleTypes?.length &&
394
- (typeof data.__typename !== "string" ||
395
- !type.possibleTypes.includes(data.__typename)))
396
- return decodeFailure(`Missing or invalid ${base} __typename`, path);
397
- const active = [
398
- ...positions,
399
- ...(typeof data.__typename === "string"
400
- ? (branches[data.__typename] ?? [])
401
- : []),
402
- ];
403
- const result = {};
404
- const merged = new Map();
405
- for (const position of active) {
406
- if (position.conditional)
407
- continue;
408
- const previous = merged.get(position.key);
409
- merged.set(position.key, previous
410
- ? {
411
- ...position,
412
- children: [...previous.children, ...position.children],
413
- branches: Object.fromEntries([
414
- ...new Set([
415
- ...Object.keys(previous.branches),
416
- ...Object.keys(position.branches),
417
- ]),
418
- ].map((key) => [
419
- key,
420
- [
421
- ...(previous.branches[key] ?? []),
422
- ...(position.branches[key] ?? []),
423
- ],
424
- ])),
627
+ const nodesFromConnection = (value) => {
628
+ if (value == null)
629
+ return value;
630
+ if (Array.isArray(value))
631
+ return value.map(nodesFromConnection);
632
+ const edges = value.edges;
633
+ if (!Array.isArray(edges))
634
+ return [];
635
+ return edges.map((edge) => edge == null ? edge : edge.node);
636
+ };
637
+ const readField = (parent, name) => {
638
+ if (parent == null)
639
+ return parent;
640
+ if (Array.isArray(parent)) {
641
+ return parent.map((item) => item == null ? item : item[name]);
642
+ }
643
+ return parent[name];
644
+ };
645
+ const extractAbs = (expr, data, rootAlias) => {
646
+ switch (expr._tag) {
647
+ case "Root": {
648
+ const raw = data[rootAlias.get(expr) ?? expr.field];
649
+ return expr.connection ? nodesFromConnection(raw) : raw;
650
+ }
651
+ case "Prop": {
652
+ const value = readField(extractAbs(expr.parent, data, rootAlias), expr.field.name);
653
+ return expr.field.kind === "connection"
654
+ ? nodesFromConnection(value)
655
+ : value;
656
+ }
657
+ default:
658
+ throw new GqlError(`extractAbs: ${expr._tag}`);
659
+ }
660
+ };
661
+ const extractRel = (expr, item) => {
662
+ switch (expr._tag) {
663
+ case "Item":
664
+ return item;
665
+ case "Prop": {
666
+ const value = readField(extractRel(expr.parent, item), expr.field.name);
667
+ return expr.field.kind === "connection"
668
+ ? nodesFromConnection(value)
669
+ : value;
670
+ }
671
+ default:
672
+ throw new GqlError(`extractRel: ${expr._tag}`);
673
+ }
674
+ };
675
+ const inItem = (expr) => {
676
+ switch (expr._tag) {
677
+ case "Item":
678
+ return true;
679
+ case "Prop":
680
+ case "MapValue":
681
+ case "MapItems":
682
+ case "Filter":
683
+ case "FlatMap":
684
+ return inItem(expr.parent);
685
+ default:
686
+ return false;
687
+ }
688
+ };
689
+ const interpretExpr = (expr, data, rootAlias, item) => {
690
+ switch (expr._tag) {
691
+ case "Literal":
692
+ return expr.value;
693
+ case "Item":
694
+ return item;
695
+ case "Root":
696
+ case "Prop":
697
+ return inItem(expr)
698
+ ? extractRel(expr, item)
699
+ : extractAbs(expr, data, rootAlias);
700
+ case "MapValue":
701
+ return expr.mapFn(interpretExpr(expr.parent, data, rootAlias, item));
702
+ case "Filter": {
703
+ const list = interpretExpr(expr.parent, data, rootAlias, item);
704
+ const arr = Array.isArray(list) ? list : [];
705
+ return arr.filter((row) => Boolean(interpretExpr(expr.predicate, data, rootAlias, row)));
706
+ }
707
+ case "MapItems": {
708
+ const list = interpretExpr(expr.parent, data, rootAlias, item);
709
+ if (expr.single) {
710
+ return list == null
711
+ ? null
712
+ : interpretValueSync(expr.mapped, data, rootAlias, list);
425
713
  }
426
- : position);
714
+ const arr = Array.isArray(list) ? list : [];
715
+ return arr.map((row) => interpretValueSync(expr.mapped, data, rootAlias, row));
716
+ }
717
+ case "FlatMap":
718
+ throw new GqlError("FlatMap must be interpreted as Effect");
427
719
  }
428
- for (const position of merged.values()) {
429
- if (position.conditional)
430
- continue;
431
- if (position.typename) {
432
- if (typeof data[position.key] !== "string" ||
433
- (type.kind === "OBJECT" && data[position.key] !== base))
434
- return decodeFailure(`Invalid __typename for ${base}`, [
435
- ...path,
436
- position.key,
437
- ]);
438
- Object.defineProperty(result, position.key, {
439
- value: data[position.key],
440
- enumerable: true,
441
- configurable: true,
442
- writable: true,
443
- });
720
+ };
721
+ const interpretValueSync = (plan, data, rootAlias, item) => {
722
+ if (isQuery(plan)) {
723
+ return interpretExpr(plan.expr, data, rootAlias, item);
724
+ }
725
+ if (plan !== null && typeof plan === "object" && !Effect.isEffect(plan)) {
726
+ const out = {};
727
+ for (const [key, nested] of Object.entries(plan)) {
728
+ out[key] = interpretValueSync(nested, data, rootAlias, item);
444
729
  }
445
- else
446
- Object.defineProperty(result, position.key, {
447
- value: decodeData(model, position.type, data[position.key], position.children, position.branches, [...path, position.key]),
448
- enumerable: true,
449
- configurable: true,
450
- writable: true,
451
- });
730
+ return out;
452
731
  }
453
- return result;
732
+ return plan;
454
733
  };
455
- const classify = (model, compiled, raw, response, classes) => {
456
- const path = Array.isArray(raw.path)
457
- ? raw.path
458
- : undefined;
459
- let candidates = compiled.positions;
460
- let position;
461
- let currentData = object(response.body)
462
- ? response.body.data
463
- : undefined;
464
- let parents = [];
465
- const descendants = (positions, value) => positions.flatMap((p) => [
466
- ...p.children,
467
- ...(object(value) && typeof value.__typename === "string"
468
- ? (p.branches[value.__typename] ?? [])
469
- : Object.values(p.branches).flat()),
470
- ]);
471
- let valid = !!path?.length;
472
- for (const segment of path ?? []) {
473
- if (typeof segment === "number") {
474
- currentData = Array.isArray(currentData)
475
- ? currentData[segment]
476
- : undefined;
477
- candidates = descendants(parents, currentData);
478
- continue;
734
+ const interpretPlan = (plan, data, rootAlias) => Effect.gen(function* () {
735
+ if (isQuery(plan)) {
736
+ const expr = plan.expr;
737
+ if (expr._tag === "FlatMap") {
738
+ const resolved = interpretExpr(expr.parent, data, rootAlias, undefined);
739
+ const next = expr.flatMapFn(resolved);
740
+ if (Effect.isEffect(next))
741
+ return yield* next;
742
+ if (isQuery(next))
743
+ return yield* interpretPlan(next, data, rootAlias);
744
+ return next;
479
745
  }
480
- const matches = candidates.filter((p) => p.key === segment);
481
- if (!matches.length) {
482
- valid = false;
483
- break;
746
+ return interpretExpr(expr, data, rootAlias, undefined);
747
+ }
748
+ if (Effect.isEffect(plan))
749
+ return yield* plan;
750
+ if (plan !== null && typeof plan === "object" && !Array.isArray(plan)) {
751
+ const out = {};
752
+ for (const [key, nested] of Object.entries(plan)) {
753
+ out[key] = yield* interpretPlan(nested, data, rootAlias);
484
754
  }
485
- position = {
486
- ...matches[0],
487
- errors: [...new Set(matches.flatMap((p) => p.errors))],
488
- };
489
- currentData = object(currentData) ? currentData[segment] : undefined;
490
- parents = matches;
491
- candidates = descendants(matches, currentData);
492
- }
493
- const extensions = object(raw.extensions) ? raw.extensions : {};
755
+ return out;
756
+ }
757
+ return plan;
758
+ });
759
+ /**
760
+ * Compile every Query in `plan` into one document, POST it, then replace
761
+ * Query/Effect leaves with plain data (`UnwrapPlan`).
762
+ */
763
+ const evaluatePlan = (plan) => Effect.gen(function* () {
764
+ const forest = emptyForest();
765
+ visitPlan(plan, forest);
766
+ let data = {};
767
+ let rootAlias = new WeakMap();
768
+ if (forest.roots.children.size > 0) {
769
+ const compiled = emit(forest);
770
+ log("document\n" + compiled.document);
771
+ data = yield* execute(compiled);
772
+ rootAlias = compiled.rootAlias;
773
+ }
774
+ return (yield* interpretPlan(plan, data, rootAlias));
775
+ });
776
+ /**
777
+ * POST one compiled document and fail with its classified errors. Queries
778
+ * are retried (bounded) while every failure is retryable; mutations never
779
+ * are, since an error can follow a side effect that already happened.
780
+ */
781
+ const execute = (compiled) => {
782
+ const once = Effect.gen(function* () {
783
+ const transport = yield* GqlTransport;
784
+ const response = yield* transport.execute(compiled);
785
+ const raw = response.errors ?? [];
786
+ if (raw.length === 0)
787
+ return response.data;
788
+ const issues = raw.map((error) => classify(compiled, error, response));
789
+ const [first, ...rest] = issues;
790
+ return yield* Effect.fail(rest.every((issue) => issue._tag === first._tag)
791
+ ? first
792
+ : new GraphQLFailure({
793
+ errors: [first, ...rest],
794
+ data: response.data,
795
+ status: response.status,
796
+ }));
797
+ });
798
+ return compiled.kind === "query"
799
+ ? once.pipe(Effect.retry({
800
+ while: (error) => isRetryable(compiled, error),
801
+ times: 5,
802
+ schedule: Schedule.exponential("200 millis"),
803
+ }))
804
+ : once;
805
+ };
806
+ const isRetryable = (compiled, error) => {
807
+ if (error instanceof GraphQLTransportError) {
808
+ return (error.status === undefined || error.status === 429 || error.status >= 500);
809
+ }
810
+ const retryable = new Set([...compiled.rootErrors.values()].flatMap((root) => root.errors.filter((spec) => spec.retryable).map((spec) => spec.tag)));
811
+ return error instanceof GraphQLFailure
812
+ ? error.errors.every((issue) => retryable.has(issue._tag))
813
+ : retryable.has(error._tag);
814
+ };
815
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
816
+ /**
817
+ * Pick the most specific matcher among the errors the root at `path[0]`
818
+ * declares (or the global errors for a path-less error). A tie between
819
+ * different tags is ambiguous and stays {@link UnknownGraphQLError}.
820
+ */
821
+ const classify = (compiled, raw, response) => {
822
+ const path = Array.isArray(raw.path) ? raw.path : undefined;
823
+ const root = typeof path?.[0] === "string"
824
+ ? compiled.rootErrors.get(path[0])
825
+ : undefined;
826
+ const candidates = root
827
+ ? root.errors
828
+ : uniqueByTag([...compiled.rootErrors.values()].flatMap((entry) => entry.errors.filter((spec) => spec.global)));
829
+ const extensions = isRecord(raw.extensions) ? raw.extensions : {};
494
830
  const code = typeof extensions.code === "string"
495
831
  ? extensions.code
496
832
  : typeof extensions.errorCode === "string"
497
833
  ? extensions.errorCode
498
834
  : undefined;
499
- const retryHeader = response.headers["retry-after"];
500
- const retryAfter = retryHeader && /^\d+(\.\d+)?$/.test(retryHeader)
501
- ? Number(retryHeader)
502
- : undefined;
835
+ const message = typeof raw.message === "string" ? raw.message : "";
836
+ const retryHeader = response.headers?.["retry-after"];
503
837
  const props = {
504
- message: raw.message,
838
+ message,
505
839
  code,
506
840
  path,
507
841
  locations: raw.locations,
@@ -512,207 +846,210 @@ const classify = (model, compiled, raw, response, classes) => {
512
846
  ? raw.traceId
513
847
  : undefined,
514
848
  status: response.status,
515
- retryAfter,
849
+ retryAfter: retryHeader && /^\d+(\.\d+)?$/.test(retryHeader)
850
+ ? Number(retryHeader)
851
+ : undefined,
516
852
  };
517
- const tags = [
518
- ...model.globalErrors,
519
- ...(valid ? (position?.errors ?? []) : []),
520
- ];
521
- const matches = tags.flatMap((tag) => (model.errors[tag]?.matchers ?? [])
853
+ const matches = candidates.flatMap((spec) => spec.matchers
522
854
  .filter((matcher) => (matcher.code === undefined || matcher.code === code) &&
523
- (matcher.message === undefined || matcher.message === raw.message) &&
855
+ (matcher.message === undefined || matcher.message === message) &&
524
856
  (matcher.messageIncludes === undefined ||
525
- raw.message.includes(matcher.messageIncludes)))
857
+ message.includes(matcher.messageIncludes)))
526
858
  .map((matcher) => ({
527
- tag,
859
+ spec,
528
860
  score: (matcher.code ? 1 : 0) +
529
861
  (matcher.message ? 4 : 0) +
530
862
  (matcher.messageIncludes ? 2 : 0),
531
863
  })));
532
864
  matches.sort((a, b) => b.score - a.score);
533
865
  const best = matches[0];
534
- const tag = best && !matches.some((m) => m.score === best.score && m.tag !== best.tag)
535
- ? best.tag
536
- : undefined;
537
- if (tag && classes[tag])
538
- return new classes[tag](props);
866
+ if (best &&
867
+ !matches.some((match) => match.score === best.score && match.spec.tag !== best.spec.tag)) {
868
+ return best.spec.make(props);
869
+ }
539
870
  return new UnknownGraphQLError({
540
871
  ...props,
541
- coordinate: valid ? position?.coordinate : undefined,
872
+ coordinate: root?.field,
542
873
  });
543
874
  };
544
- /** Construct an Effect client; credentials and HTTP are resolved by the transport per call. */
545
- export const makeClient = (model, transport, classes) => {
546
- const executeReport = (kind, selection) => Effect.gen(function* () {
547
- const compiled = yield* Effect.try({
548
- try: () => compile(model, kind, selection),
549
- catch: (cause) => cause instanceof GraphQLRequestError
550
- ? cause
551
- : new GraphQLRequestError({ message: String(cause) }),
552
- });
553
- const response = yield* transport(compiled);
554
- return yield* Effect.try({
555
- try: () => {
556
- const body = response.body;
557
- if (!object(body))
558
- return decodeFailure("GraphQL response must be an object", []);
559
- if (body.errors !== undefined &&
560
- (!Array.isArray(body.errors) || !body.errors.length))
561
- return decodeFailure("GraphQL errors must be a nonempty array", []);
562
- const rawErrors = body.errors ?? [];
563
- for (const error of rawErrors) {
564
- if (!object(error) || typeof error.message !== "string")
565
- return decodeFailure("Invalid GraphQL error", []);
566
- if (error.path !== undefined &&
567
- (!Array.isArray(error.path) ||
568
- error.path.some((p) => typeof p !== "string" &&
569
- (typeof p !== "number" || !Number.isInteger(p) || p < 0))))
570
- return decodeFailure("Invalid GraphQL error path", []);
571
- }
572
- if (!("data" in body) && !rawErrors.length) {
573
- if (response.status >= 400)
574
- throw new GraphQLTransportError({
575
- message: `HTTP ${response.status} without a GraphQL response`,
576
- status: response.status,
577
- });
578
- return decodeFailure("GraphQL response has neither data nor errors", []);
579
- }
580
- if (response.status >= 400 && !rawErrors.length)
581
- throw new GraphQLTransportError({
582
- message: `HTTP ${response.status} without GraphQL errors`,
583
- status: response.status,
584
- });
585
- const errors = rawErrors.map((raw) => classify(model, compiled, raw, response, classes));
586
- let data = body.data;
587
- if (data !== undefined && data !== null)
588
- data = decodeData(model, compiled.rootType + "!", data, compiled.positions, {}, []);
589
- if ((data === undefined || data === null) && !errors.length)
590
- return decodeFailure("GraphQL success has no data", []);
591
- return {
592
- data,
593
- errors,
594
- status: response.status,
595
- extensions: body.extensions,
596
- };
597
- },
598
- catch: (cause) => cause instanceof GraphQLDecodeError ||
599
- cause instanceof GraphQLTransportError
600
- ? cause
601
- : new GraphQLDecodeError({
602
- message: "Invalid GraphQL response",
603
- cause,
604
- }),
605
- });
875
+ const uniqueByTag = (specs) => [
876
+ ...new Map(specs.map((spec) => [spec.tag, spec])).values(),
877
+ ];
878
+ /** `Query.of` / pure. */
879
+ export const ofQuery = (value) => make({ _tag: "Literal", value, type: { tag: "scalar" } });
880
+ const filterImpl = (source, predicate) => {
881
+ const proto = itemQuery(source);
882
+ const predicateQuery = predicate(proto);
883
+ log("filter pred", printExpr(predicateQuery.expr));
884
+ return make({
885
+ _tag: "Filter",
886
+ parent: source.expr,
887
+ predicate: predicateQuery.expr,
888
+ type: source.expr.type,
606
889
  });
607
- const retryable = (error) => error instanceof GraphQLFailure
608
- ? error.errors.every((issue) => model.errors[issue._tag]?.retryable === true)
609
- : error instanceof GraphQLTransportError &&
610
- (error.status === undefined ||
611
- error.status === 429 ||
612
- error.status >= 500);
613
- const execute = (kind, selection) => {
614
- const effect = executeReport(kind, selection).pipe(Effect.flatMap((report) => report.errors.length
615
- ? Effect.fail(new GraphQLFailure({
616
- errors: report.errors,
617
- data: report.data,
618
- status: report.status,
619
- }))
620
- : Effect.succeed(report.data)));
621
- return kind === "query"
622
- ? effect.pipe(Effect.retry({
623
- while: retryable,
624
- times: 5,
625
- schedule: Schedule.exponential("200 millis"),
626
- }))
627
- : effect;
628
- };
629
- const operation = (kind, name) => {
630
- const field = model.types[kind === "query" ? model.queryType : model.mutationType]
631
- ?.fields?.[name];
632
- const call = (args, selection) => execute(kind, {
633
- [name]: { where: args, select: selection ?? true },
634
- }).pipe(Effect.map((data) => data[name]));
635
- const pages = (args, selection) => Stream.unwrap(Effect.try({
636
- try: () => {
637
- const connection = field && model.types[named(field.type)];
638
- if (kind !== "query" ||
639
- !field?.args.after ||
640
- !connection?.fields?.edges ||
641
- !connection.fields.pageInfo)
642
- return Stream.fromEffect(Effect.fail(new GraphQLRequestError({
643
- message: `${name} is not a paginated query connection`,
644
- })));
645
- // Cursor fields are internal additions; the caller's projection remains exact.
646
- const projection = compile(model, kind, {
647
- [name]: { where: args, select: selection },
648
- }).positions[0];
649
- let cursorKey = "pageInfo";
650
- let complete;
651
- if (selection.__alias &&
652
- Object.hasOwn(selection.__alias, "pageInfo")) {
653
- cursorKey = "_distilledPageInfo";
654
- while (Object.hasOwn(selection, cursorKey) ||
655
- Object.hasOwn(selection.__alias, cursorKey))
656
- cursorKey += "_";
657
- complete = {
658
- ...selection,
659
- __alias: {
660
- ...selection.__alias,
661
- [cursorKey]: {
662
- pageInfo: { endCursor: true, hasNextPage: true },
663
- },
664
- },
665
- };
666
- }
667
- else {
668
- const { $include: _include, $skip: _skip, ...pageFields } = selection.pageInfo?.select ?? selection.pageInfo ?? {};
669
- complete = {
670
- ...selection,
671
- pageInfo: { ...pageFields, endCursor: true, hasNextPage: true },
672
- };
673
- }
674
- return Stream.paginate({
675
- after: args.after,
676
- seen: new Set(),
677
- }, (state) => call({ ...args, after: state.after }, complete).pipe(Effect.flatMap((page) => {
678
- const cursor = page?.[cursorKey]?.endCursor;
679
- const more = page?.[cursorKey]?.hasNextPage;
680
- if (more &&
681
- (typeof cursor !== "string" ||
682
- cursor === state.after ||
683
- state.seen.has(cursor)))
684
- return Effect.fail(new GraphQLDecodeError({
685
- message: `${name} returned a non-advancing pagination cursor`,
686
- }));
687
- const seen = new Set(state.seen);
688
- if (typeof cursor === "string")
689
- seen.add(cursor);
690
- const projected = decodeData(model, field.type, page, projection.children, projection.branches, []);
691
- return Effect.succeed([
692
- [projected],
693
- more
694
- ? Option.some({ after: cursor, seen })
695
- : Option.none(),
696
- ]);
697
- })));
698
- },
699
- catch: (cause) => cause instanceof GraphQLRequestError
700
- ? cause
701
- : new GraphQLRequestError({ message: String(cause) }),
890
+ };
891
+ export function filterQuery(sourceOrPredicate, predicate) {
892
+ if (predicate === undefined) {
893
+ return (source) => filterImpl(source, sourceOrPredicate);
894
+ }
895
+ return filterImpl(sourceOrPredicate, predicate);
896
+ }
897
+ const mapImpl = (source, mapFn) => {
898
+ if (source.expr.type.tag === "object") {
899
+ const mapped = mapFn(new QueryNode({
900
+ _tag: "Item",
901
+ list: source.expr,
902
+ type: source.expr.type,
702
903
  }));
703
- return Object.assign(call, {
704
- pages,
705
- items: (args, select) => pages(args, { edges: { node: select } }).pipe(Stream.flatMap((page) => Stream.fromIterable((page?.edges ?? []).flatMap((edge) => edge === null ? [] : [edge.node])))),
904
+ log("map object", printExpr(source.expr));
905
+ return make({
906
+ _tag: "MapItems",
907
+ parent: source.expr,
908
+ mapped,
909
+ single: true,
910
+ type: { tag: "scalar" },
706
911
  });
707
- };
708
- return {
709
- query: (selection) => execute("query", selection),
710
- mutation: (selection) => execute("mutation", selection),
711
- report: {
712
- query: (selection) => executeReport("query", selection),
713
- mutation: (selection) => executeReport("mutation", selection),
714
- },
715
- operation,
716
- };
912
+ }
913
+ if (source.expr.type.tag === "list") {
914
+ const proto = itemQuery(source);
915
+ const mapped = mapFn(proto);
916
+ log("map items", printExpr(source.expr));
917
+ return make({
918
+ _tag: "MapItems",
919
+ parent: source.expr,
920
+ mapped,
921
+ type: source.expr.type,
922
+ });
923
+ }
924
+ log("map value", printExpr(source.expr));
925
+ return make({
926
+ _tag: "MapValue",
927
+ parent: source.expr,
928
+ mapFn,
929
+ type: { tag: "scalar" },
930
+ });
931
+ };
932
+ export function mapQuery(sourceOrMapFn, mapFn) {
933
+ if (mapFn === undefined) {
934
+ return (source) => mapImpl(source, sourceOrMapFn);
935
+ }
936
+ return mapImpl(sourceOrMapFn, mapFn);
937
+ }
938
+ const flatMapImpl = (source, flatMapFn) => {
939
+ log("flatMap", printExpr(source.expr));
940
+ return make({
941
+ _tag: "FlatMap",
942
+ parent: source.expr,
943
+ flatMapFn,
944
+ type: { tag: "scalar" },
945
+ });
946
+ };
947
+ export function flatMapQuery(sourceOrFlatMapFn, flatMapFn) {
948
+ if (flatMapFn === undefined) {
949
+ return (source) => flatMapImpl(source, sourceOrFlatMapFn);
950
+ }
951
+ return flatMapImpl(sourceOrFlatMapFn, flatMapFn);
952
+ }
953
+ /**
954
+ * Builder → Effect. The inner function is *not* a generator; it returns a
955
+ * plan of Query/Effect values. Query.fn visits the plan, POSTs one document,
956
+ * then interprets Map/Filter/FlatMap/Of.
957
+ */
958
+ export const queryFn = (build) => (...args) => {
959
+ const plan = build(...args);
960
+ return evaluatePlan(plan);
717
961
  };
962
+ // ── Pagination ──────────────────────────────────────────────────────────────
963
+ //
964
+ // A connection lens hides `edges`/`pageInfo`. `Query.pages` re-evaluates the
965
+ // query once per page: it sets the connection's `after` argument, paints
966
+ // `pageInfo { hasNextPage endCursor }` next to `edges`, and stops when
967
+ // `hasNextPage` is false.
968
+ /** The connection a paged query reads, below any list-level Map/Filter. */
969
+ const connectionOf = (expr) => {
970
+ const base = graphqlBase(expr);
971
+ const isConnection = (base._tag === "Root" && base.connection === true) ||
972
+ (base._tag === "Prop" && base.field.kind === "connection");
973
+ if (!isConnection || inItem(base)) {
974
+ throw new GqlError(`Query.pages needs a Relay connection, got ${printExpr(base)}`);
975
+ }
976
+ const argTypes = base._tag === "Root" ? base.argTypes : base.field.argTypes;
977
+ if (!argTypes || !("after" in argTypes)) {
978
+ throw new GqlError(`${printExpr(base)} takes no "after" cursor`);
979
+ }
980
+ return base;
981
+ };
982
+ /** Rebuild `expr` with the connection's `after` argument set. */
983
+ const withCursor = (expr, connection, after) => {
984
+ if (expr === connection) {
985
+ const current = expr;
986
+ return { ...current, args: { ...current.args, after } };
987
+ }
988
+ switch (expr._tag) {
989
+ case "MapItems":
990
+ case "Filter":
991
+ case "MapValue":
992
+ case "FlatMap":
993
+ return { ...expr, parent: withCursor(expr.parent, connection, after) };
994
+ default:
995
+ throw new GqlError(`withCursor: ${expr._tag}`);
996
+ }
997
+ };
998
+ const readPath = (data, path) => {
999
+ let current = data;
1000
+ for (const segment of path) {
1001
+ if (current == null)
1002
+ return undefined;
1003
+ if (Array.isArray(current)) {
1004
+ throw new GqlError("Query.pages cannot page a connection inside a list");
1005
+ }
1006
+ current = current[segment];
1007
+ }
1008
+ return current;
1009
+ };
1010
+ const fetchPage = (query, connection, state) => Effect.gen(function* () {
1011
+ const pageExpr = withCursor(query.expr, connection, state.after);
1012
+ const forest = emptyForest();
1013
+ visitExpr(forest, pageExpr);
1014
+ const nodePath = paintAbs(forest, withCursor(connection, connection, state.after));
1015
+ const connectionPath = nodePath.slice(0, -2);
1016
+ const pageInfo = ensureChild(forest, selAt(forest.roots, connectionPath), "pageInfo", {});
1017
+ ensureChild(forest, pageInfo, "hasNextPage", { isScalar: true });
1018
+ ensureChild(forest, pageInfo, "endCursor", { isScalar: true });
1019
+ const compiled = emit(forest);
1020
+ log("page\n" + compiled.document);
1021
+ const data = yield* execute(compiled);
1022
+ const items = (yield* interpretPlan(make(pageExpr), data, compiled.rootAlias));
1023
+ const info = readPath(data, [...connectionPath, "pageInfo"]);
1024
+ const cursor = info?.endCursor ?? null;
1025
+ if (info?.hasNextPage !== true) {
1026
+ return [items ?? [], Option.none()];
1027
+ }
1028
+ if (cursor === null || cursor === state.after || state.seen.has(cursor)) {
1029
+ return yield* new GraphQLPaginationError({
1030
+ message: `${printExpr(connection)} returned a non-advancing cursor`,
1031
+ cursor,
1032
+ });
1033
+ }
1034
+ return [
1035
+ items ?? [],
1036
+ Option.some({ after: cursor, seen: new Set([...state.seen, cursor]) }),
1037
+ ];
1038
+ });
1039
+ /**
1040
+ * Every page of a Relay connection, one request per page. `query` is a
1041
+ * connection lens (root or nested under objects), optionally mapped or
1042
+ * filtered: `Query.pages(Railway.project({ id }).services({ first: 50 }))`.
1043
+ * Starts from the query's own `after` argument, if any.
1044
+ */
1045
+ export const pagesQuery = (query) => Stream.unwrap(Effect.sync(() => {
1046
+ const connection = connectionOf(query.expr);
1047
+ const after = connection.args?.after;
1048
+ return Stream.paginate({
1049
+ after: typeof after === "string" ? after : undefined,
1050
+ seen: new Set(),
1051
+ }, (state) => fetchPage(query, connection, state).pipe(Effect.map(([items, next]) => [[items], next])));
1052
+ }));
1053
+ /** Every node of a Relay connection, across all pages. */
1054
+ export const itemsQuery = (query) => pagesQuery(query).pipe(Stream.flatMap(Stream.fromIterable));
718
1055
  //# sourceMappingURL=graphql.js.map