@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.
- package/lib/api.d.ts +4 -4
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +1 -1
- package/lib/api.js.map +1 -1
- package/lib/category.js +1 -1
- package/lib/category.js.map +1 -1
- package/lib/codegen/boolean-string-enums.d.ts +8 -8
- package/lib/codegen/boolean-string-enums.d.ts.map +1 -1
- package/lib/codegen/boolean-string-enums.js +84 -27
- package/lib/codegen/boolean-string-enums.js.map +1 -1
- package/lib/codegen/boolean-string-enums.test.js +130 -0
- package/lib/codegen/boolean-string-enums.test.js.map +1 -1
- package/lib/codegen/cli.js +2 -2
- package/lib/codegen/cli.js.map +1 -1
- package/lib/codegen/generator.d.ts +20 -3
- package/lib/codegen/generator.d.ts.map +1 -1
- package/lib/codegen/generator.js +36 -9
- package/lib/codegen/generator.js.map +1 -1
- package/lib/codegen/generator.test.d.ts +2 -0
- package/lib/codegen/generator.test.d.ts.map +1 -0
- package/lib/codegen/generator.test.js +126 -0
- package/lib/codegen/generator.test.js.map +1 -0
- package/lib/codegen/graphql-client.d.ts +7 -1
- package/lib/codegen/graphql-client.d.ts.map +1 -1
- package/lib/codegen/graphql-client.js +262 -125
- package/lib/codegen/graphql-client.js.map +1 -1
- package/lib/codegen/graphql-client.test.js +104 -236
- package/lib/codegen/graphql-client.test.js.map +1 -1
- package/lib/codegen/openapi-binary.test.d.ts +2 -0
- package/lib/codegen/openapi-binary.test.d.ts.map +1 -0
- package/lib/codegen/openapi-binary.test.js +67 -0
- package/lib/codegen/openapi-binary.test.js.map +1 -0
- package/lib/codegen/openapi.d.ts +7 -1
- package/lib/codegen/openapi.d.ts.map +1 -1
- package/lib/codegen/openapi.js +15 -4
- package/lib/codegen/openapi.js.map +1 -1
- package/lib/codegen/patches.d.ts +16 -1
- package/lib/codegen/patches.d.ts.map +1 -1
- package/lib/codegen/patches.js +70 -4
- package/lib/codegen/patches.js.map +1 -1
- package/lib/codegen/patches.test.js +48 -2
- package/lib/codegen/patches.test.js.map +1 -1
- package/lib/codegen/untagged-unions.test.d.ts +2 -0
- package/lib/codegen/untagged-unions.test.d.ts.map +1 -0
- package/lib/codegen/untagged-unions.test.js +80 -0
- package/lib/codegen/untagged-unions.test.js.map +1 -0
- package/lib/errors.d.ts +2 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +5 -2
- package/lib/errors.js.map +1 -1
- package/lib/fixtures/xml/upstream-validator.json +397 -0
- package/lib/graphql.d.ts +296 -205
- package/lib/graphql.d.ts.map +1 -1
- package/lib/graphql.js +968 -631
- package/lib/graphql.js.map +1 -1
- package/lib/graphql.test.js +134 -763
- package/lib/graphql.test.js.map +1 -1
- package/lib/graphql.types.d.ts +10 -0
- package/lib/graphql.types.d.ts.map +1 -1
- package/lib/graphql.types.js +4 -44
- package/lib/graphql.types.js.map +1 -1
- package/lib/index.d.ts +27 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +27 -0
- package/lib/index.js.map +1 -0
- package/lib/pagination.d.ts +2 -1
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +24 -16
- package/lib/pagination.js.map +1 -1
- package/lib/pagination.test.d.ts +2 -0
- package/lib/pagination.test.d.ts.map +1 -0
- package/lib/pagination.test.js +134 -0
- package/lib/pagination.test.js.map +1 -0
- package/lib/protocol-http.d.ts +12 -17
- package/lib/protocol-http.d.ts.map +1 -1
- package/lib/protocol-http.js +100 -45
- package/lib/protocol-http.js.map +1 -1
- package/lib/protocol-http.test.js +130 -0
- package/lib/protocol-http.test.js.map +1 -1
- package/lib/protocol-rest.d.ts +18 -1
- package/lib/protocol-rest.d.ts.map +1 -1
- package/lib/protocol-rest.js +24 -4
- package/lib/protocol-rest.js.map +1 -1
- package/lib/query.d.ts +29 -0
- package/lib/query.d.ts.map +1 -0
- package/lib/query.js +27 -0
- package/lib/query.js.map +1 -0
- package/lib/response-validation.d.ts +67 -0
- package/lib/response-validation.d.ts.map +1 -0
- package/lib/response-validation.js +76 -0
- package/lib/response-validation.js.map +1 -0
- package/lib/response-validation.test.d.ts +2 -0
- package/lib/response-validation.test.d.ts.map +1 -0
- package/lib/response-validation.test.js +67 -0
- package/lib/response-validation.test.js.map +1 -0
- package/lib/testing.d.ts +39 -0
- package/lib/testing.d.ts.map +1 -0
- package/lib/testing.js +41 -0
- package/lib/testing.js.map +1 -0
- package/lib/trait.d.ts +29 -9
- package/lib/trait.d.ts.map +1 -1
- package/lib/trait.js +8 -0
- package/lib/trait.js.map +1 -1
- package/lib/xml.d.ts +44 -0
- package/lib/xml.d.ts.map +1 -0
- package/lib/xml.js +315 -0
- package/lib/xml.js.map +1 -0
- package/lib/xml.test.d.ts +2 -0
- package/lib/xml.test.d.ts.map +1 -0
- package/lib/xml.test.js +211 -0
- package/lib/xml.test.js.map +1 -0
- package/package.json +4 -4
- package/src/api.ts +4 -4
- package/src/category.ts +1 -1
- package/src/codegen/boolean-string-enums.test.ts +147 -0
- package/src/codegen/boolean-string-enums.ts +84 -27
- package/src/codegen/cli.ts +2 -2
- package/src/codegen/generator.test.ts +151 -0
- package/src/codegen/generator.ts +70 -12
- package/src/codegen/graphql-client.test.ts +116 -283
- package/src/codegen/graphql-client.ts +326 -169
- package/src/codegen/openapi-binary.test.ts +98 -0
- package/src/codegen/openapi.ts +26 -4
- package/src/codegen/patches.test.ts +66 -1
- package/src/codegen/patches.ts +84 -4
- package/src/codegen/untagged-unions.test.ts +89 -0
- package/src/errors.ts +5 -2
- package/src/fixtures/xml/upstream-validator.json +397 -0
- package/src/graphql.test.ts +174 -948
- package/src/graphql.ts +1592 -1167
- package/src/graphql.types.ts +92 -163
- package/src/index.ts +26 -0
- package/src/pagination.test.ts +191 -0
- package/src/pagination.ts +35 -25
- package/src/protocol-http.test.ts +163 -0
- package/src/protocol-http.ts +122 -46
- package/src/protocol-rest.ts +62 -13
- package/src/query.ts +39 -0
- package/src/response-validation.test.ts +95 -0
- package/src/response-validation.ts +116 -0
- package/src/testing.ts +78 -0
- package/src/trait.ts +39 -8
- package/src/xml.test.ts +269 -0
- package/src/xml.ts +339 -0
- package/lib/graphql.fixture.d.ts +0 -249
- package/lib/graphql.fixture.d.ts.map +0 -1
- package/lib/graphql.fixture.js +0 -240
- package/lib/graphql.fixture.js.map +0 -1
- package/src/graphql.fixture.ts +0 -371
package/lib/graphql.js
CHANGED
|
@@ -1,11 +1,80 @@
|
|
|
1
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
107
|
+
/** A paginated connection reported more pages without a new cursor. */
|
|
108
|
+
export class GraphQLPaginationError extends Data.TaggedError("GraphQLPaginationError") {
|
|
35
109
|
}
|
|
36
|
-
export
|
|
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
|
-
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
305
|
-
|
|
451
|
+
return expr.field.kind === "connection"
|
|
452
|
+
? connectionNodeSel(child)
|
|
453
|
+
: child;
|
|
306
454
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
618
|
+
document: `${header} {\n${printChildren(forest.roots, 1)}\n}`,
|
|
619
|
+
operationName: "Gql",
|
|
328
620
|
variables,
|
|
329
|
-
operationName,
|
|
330
621
|
kind,
|
|
331
|
-
|
|
332
|
-
|
|
622
|
+
tree: forest.roots,
|
|
623
|
+
rootAlias: forest.rootAlias,
|
|
624
|
+
rootErrors: forest.rootErrors,
|
|
333
625
|
};
|
|
334
626
|
};
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
|
732
|
+
return plan;
|
|
454
733
|
};
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
500
|
-
const
|
|
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
|
|
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
|
|
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 ===
|
|
855
|
+
(matcher.message === undefined || matcher.message === message) &&
|
|
524
856
|
(matcher.messageIncludes === undefined ||
|
|
525
|
-
|
|
857
|
+
message.includes(matcher.messageIncludes)))
|
|
526
858
|
.map((matcher) => ({
|
|
527
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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:
|
|
872
|
+
coordinate: root?.field,
|
|
542
873
|
});
|
|
543
874
|
};
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|