@distilled.cloud/core 1.0.0-rc.12 → 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/src/graphql.ts
CHANGED
|
@@ -1,205 +1,81 @@
|
|
|
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
|
-
import {
|
|
9
|
-
Kind,
|
|
10
|
-
parseType,
|
|
11
|
-
print,
|
|
12
|
-
type FieldNode,
|
|
13
|
-
type SelectionNode,
|
|
14
|
-
type VariableDefinitionNode,
|
|
15
|
-
} from "graphql";
|
|
16
|
-
import type { GraphQLModel, GraphQLField } from "./codegen/graphql-client.ts";
|
|
17
|
-
|
|
18
|
-
export type { GraphQLModel } from "./codegen/graphql-client.ts";
|
|
19
|
-
|
|
20
|
-
/** A generated field's arguments, GraphQL output reference, and error tags. */
|
|
21
|
-
export interface Field<Args, Ref extends string, E extends string = never> {
|
|
22
|
-
readonly args: Args;
|
|
23
|
-
readonly type: Ref;
|
|
24
|
-
readonly errors: E;
|
|
25
|
-
}
|
|
26
60
|
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
scalars: Record<string, unknown>;
|
|
30
|
-
errors: Record<string, GraphQLIssue>;
|
|
31
|
-
possibleTypes: Record<string, string>;
|
|
32
|
-
query: string;
|
|
33
|
-
mutation: string;
|
|
34
|
-
subscription: string;
|
|
35
|
-
globalErrors: string;
|
|
36
|
-
}
|
|
61
|
+
export const QuerySymbol = Symbol.for("@distilled.cloud/graphql/Query");
|
|
62
|
+
const inspect = Symbol.for("nodejs.util.inspect.custom");
|
|
37
63
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: T;
|
|
43
|
-
type Fields<C extends Schema, N extends string> = N extends keyof C["types"]
|
|
44
|
-
? C["types"][N]
|
|
45
|
-
: {};
|
|
46
|
-
type TypeOf<F> = F extends Field<any, infer T, any> ? T : never;
|
|
47
|
-
type ArgsOf<F> = F extends Field<infer A, any, any> ? A : never;
|
|
48
|
-
type TagsOf<F> = F extends Field<any, any, infer E> ? E : never;
|
|
49
|
-
type Where<A> = {} extends A ? { readonly where?: A } : { readonly where: A };
|
|
50
|
-
type Directives = { readonly $include?: boolean; readonly $skip?: boolean };
|
|
51
|
-
type FieldSelection<C extends Schema, F> =
|
|
52
|
-
Named<TypeOf<F>> extends keyof C["scalars"]
|
|
53
|
-
?
|
|
54
|
-
| ({} extends ArgsOf<F> ? boolean : never)
|
|
55
|
-
| (Where<ArgsOf<F>> & Directives & { readonly select?: true })
|
|
56
|
-
:
|
|
57
|
-
| (keyof ArgsOf<F> extends never
|
|
58
|
-
? Selection<C, Named<TypeOf<F>>>
|
|
59
|
-
: never)
|
|
60
|
-
| (Where<ArgsOf<F>> &
|
|
61
|
-
Directives & { readonly select: Selection<C, Named<TypeOf<F>>> });
|
|
62
|
-
type Alias<C extends Schema, N extends string> = {
|
|
63
|
-
[K in keyof Fields<C, N>]: {
|
|
64
|
-
readonly [P in K]: FieldSelection<C, Fields<C, N>[K]>;
|
|
65
|
-
};
|
|
66
|
-
}[keyof Fields<C, N>];
|
|
67
|
-
|
|
68
|
-
/** Mobius-style field selection. Arguments are passed in `where`. */
|
|
69
|
-
export type Selection<C extends Schema, N extends string> = {
|
|
70
|
-
readonly [K in keyof Fields<C, N>]?: FieldSelection<C, Fields<C, N>[K]>;
|
|
71
|
-
} & {
|
|
72
|
-
readonly __typename?: boolean;
|
|
73
|
-
readonly __alias?: Readonly<Record<string, Alias<C, N>>>;
|
|
74
|
-
readonly __on?: N extends keyof C["possibleTypes"]
|
|
75
|
-
? { readonly [K in C["possibleTypes"][N]]?: Selection<C, K> }
|
|
76
|
-
: never;
|
|
77
|
-
};
|
|
78
|
-
type Selected<Q> = Q extends { readonly select: infer P } ? P : Q;
|
|
79
|
-
type Keys<Q> = {
|
|
80
|
-
[K in keyof Q]: Q[K] extends false | undefined ? never : K;
|
|
81
|
-
}[keyof Q];
|
|
82
|
-
type Simplify<A> = { [K in keyof A]: A[K] };
|
|
83
|
-
type AliasValue<C extends Schema, N extends string, Q> = {
|
|
84
|
-
[K in Extract<Keys<Q>, keyof Fields<C, N>>]: Result<
|
|
85
|
-
C,
|
|
86
|
-
TypeOf<Fields<C, N>[K]>,
|
|
87
|
-
Selected<Q[K]>
|
|
88
|
-
>;
|
|
89
|
-
}[Extract<Keys<Q>, keyof Fields<C, N>>];
|
|
90
|
-
type AliasResult<C extends Schema, N extends string, Q> = Q extends {
|
|
91
|
-
readonly __alias: infer A;
|
|
64
|
+
/** A plan that cannot be compiled (a programming error, raised as a defect). */
|
|
65
|
+
export class GqlError {
|
|
66
|
+
readonly _tag = "GqlError" as const;
|
|
67
|
+
constructor(readonly message: string) {}
|
|
92
68
|
}
|
|
93
|
-
? Simplify<
|
|
94
|
-
{
|
|
95
|
-
[
|
|
96
|
-
K in keyof A as Conditional<A[K][keyof A[K]]> extends true ? never : K
|
|
97
|
-
]: AliasValue<C, N, A[K]>;
|
|
98
|
-
} & {
|
|
99
|
-
[
|
|
100
|
-
K in keyof A as Conditional<A[K][keyof A[K]]> extends true ? K : never
|
|
101
|
-
]?: AliasValue<C, N, A[K]>;
|
|
102
|
-
}
|
|
103
|
-
>
|
|
104
|
-
: {};
|
|
105
|
-
type Conditional<Q> = boolean extends Q
|
|
106
|
-
? true
|
|
107
|
-
: Q extends { readonly $include: infer I }
|
|
108
|
-
? [I] extends [true]
|
|
109
|
-
? Q extends { readonly $skip: infer S }
|
|
110
|
-
? [S] extends [false]
|
|
111
|
-
? false
|
|
112
|
-
: true
|
|
113
|
-
: false
|
|
114
|
-
: true
|
|
115
|
-
: Q extends { readonly $skip: infer S }
|
|
116
|
-
? [S] extends [false]
|
|
117
|
-
? false
|
|
118
|
-
: true
|
|
119
|
-
: false;
|
|
120
|
-
type ObjectFields<C extends Schema, N extends string, Q> = {
|
|
121
|
-
[
|
|
122
|
-
K in Extract<Keys<Q>, keyof Fields<C, N>> as Conditional<Q[K]> extends true
|
|
123
|
-
? never
|
|
124
|
-
: K
|
|
125
|
-
]: Result<C, TypeOf<Fields<C, N>[K]>, Selected<Q[K]>>;
|
|
126
|
-
} & {
|
|
127
|
-
[
|
|
128
|
-
K in Extract<Keys<Q>, keyof Fields<C, N>> as Conditional<Q[K]> extends true
|
|
129
|
-
? K
|
|
130
|
-
: never
|
|
131
|
-
]?: Result<C, TypeOf<Fields<C, N>[K]>, Selected<Q[K]>>;
|
|
132
|
-
};
|
|
133
|
-
type ObjectResult<C extends Schema, N extends string, Q> = Simplify<
|
|
134
|
-
ObjectFields<C, N, Q> &
|
|
135
|
-
(Q extends { readonly __typename: true } ? { __typename: N } : {}) &
|
|
136
|
-
AliasResult<C, N, Q>
|
|
137
|
-
>;
|
|
138
|
-
type Branch<
|
|
139
|
-
C extends Schema,
|
|
140
|
-
N extends string,
|
|
141
|
-
Q,
|
|
142
|
-
T extends string,
|
|
143
|
-
> = T extends unknown
|
|
144
|
-
? Simplify<
|
|
145
|
-
Omit<ObjectResult<C, N, Q>, "__typename"> &
|
|
146
|
-
(Q extends { readonly __on: infer O }
|
|
147
|
-
? T extends keyof O
|
|
148
|
-
? ObjectResult<C, T, O[T]>
|
|
149
|
-
: {}
|
|
150
|
-
: {}) & { __typename: T }
|
|
151
|
-
>
|
|
152
|
-
: never;
|
|
153
|
-
type Value<C extends Schema, T extends string, Q> = T extends `[${infer I}]`
|
|
154
|
-
? Array<Result<C, I, Q>>
|
|
155
|
-
: T extends keyof C["scalars"]
|
|
156
|
-
? C["scalars"][T]
|
|
157
|
-
: T extends keyof C["possibleTypes"]
|
|
158
|
-
? Branch<C, T, Q, C["possibleTypes"][T]>
|
|
159
|
-
: ObjectResult<C, T, Q>;
|
|
160
|
-
/** Selected output, preserving list and nullable wrappers. */
|
|
161
|
-
export type Result<
|
|
162
|
-
C extends Schema,
|
|
163
|
-
Ref extends string,
|
|
164
|
-
Q,
|
|
165
|
-
> = Ref extends `${infer T}!` ? Value<C, T, Q> : Value<C, Ref, Q> | null;
|
|
166
|
-
type SelectionTags<C extends Schema, N extends string, Q> = string extends N
|
|
167
|
-
? string
|
|
168
|
-
: Q extends object
|
|
169
|
-
?
|
|
170
|
-
| {
|
|
171
|
-
[K in Extract<Keys<Q>, keyof Fields<C, N>>]:
|
|
172
|
-
| TagsOf<Fields<C, N>[K]>
|
|
173
|
-
| SelectionTags<
|
|
174
|
-
C,
|
|
175
|
-
Named<TypeOf<Fields<C, N>[K]>>,
|
|
176
|
-
Selected<Q[K]>
|
|
177
|
-
>;
|
|
178
|
-
}[Extract<Keys<Q>, keyof Fields<C, N>>]
|
|
179
|
-
| (Q extends { readonly __on: infer O }
|
|
180
|
-
? { [K in keyof O & string]: SelectionTags<C, K, O[K]> }[keyof O &
|
|
181
|
-
string]
|
|
182
|
-
: never)
|
|
183
|
-
| (Q extends { readonly __alias: infer A }
|
|
184
|
-
? { [K in keyof A]: SelectionTags<C, N, A[K]> }[keyof A]
|
|
185
|
-
: never)
|
|
186
|
-
: never;
|
|
187
|
-
/** Known selected errors plus provider-wide errors and an honest fallback. */
|
|
188
|
-
export type Errors<C extends Schema, N extends string, Q> =
|
|
189
|
-
| (SelectionTags<C, N, Q> extends infer Tags
|
|
190
|
-
? C["errors"][Extract<C["globalErrors"] | Tags, keyof C["errors"]>]
|
|
191
|
-
: never)
|
|
192
|
-
| UnknownGraphQLError;
|
|
193
|
-
type Root<C extends Schema, K extends OperationKind> = K extends "query"
|
|
194
|
-
? C["query"]
|
|
195
|
-
: C["mutation"];
|
|
196
|
-
type OperationSelection<
|
|
197
|
-
C extends Schema,
|
|
198
|
-
N extends string,
|
|
199
|
-
K extends keyof Fields<C, N>,
|
|
200
|
-
Q,
|
|
201
|
-
> = { [P in K]: { select: Q } };
|
|
202
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
|
+
|
|
78
|
+
/** Fields every classified GraphQL error carries. */
|
|
203
79
|
export const errorFields = {
|
|
204
80
|
message: S.String,
|
|
205
81
|
code: S.optional(S.String),
|
|
@@ -210,8 +86,11 @@ export const errorFields = {
|
|
|
210
86
|
extensions: S.optional(S.Unknown),
|
|
211
87
|
traceId: S.optional(S.String),
|
|
212
88
|
status: S.optional(S.Number),
|
|
89
|
+
/** Server retry hint in seconds. */
|
|
213
90
|
retryAfter: S.optional(S.Number),
|
|
214
91
|
};
|
|
92
|
+
|
|
93
|
+
/** The shape shared by every classified GraphQL error. */
|
|
215
94
|
export interface GraphQLIssue {
|
|
216
95
|
readonly _tag: string;
|
|
217
96
|
readonly message: string;
|
|
@@ -224,23 +103,28 @@ export interface GraphQLIssue {
|
|
|
224
103
|
readonly extensions?: unknown;
|
|
225
104
|
readonly traceId?: string;
|
|
226
105
|
readonly status?: number;
|
|
227
|
-
/** Server retry hint in seconds. */
|
|
228
106
|
readonly retryAfter?: number;
|
|
229
107
|
}
|
|
108
|
+
|
|
109
|
+
export type GraphQLIssueProps = Omit<GraphQLIssue, "_tag">;
|
|
110
|
+
|
|
111
|
+
/** A GraphQL error no declared matcher recognizes. */
|
|
230
112
|
export class UnknownGraphQLError extends S.TaggedError<UnknownGraphQLError>()(
|
|
231
113
|
"UnknownGraphQLError",
|
|
232
114
|
{
|
|
233
115
|
...errorFields,
|
|
116
|
+
/** Root field the error path points at, when the path is known. */
|
|
234
117
|
coordinate: S.optional(S.String),
|
|
235
118
|
},
|
|
236
119
|
) {}
|
|
237
|
-
|
|
120
|
+
|
|
121
|
+
/** One response carried errors with different tags; none is hidden. */
|
|
238
122
|
export class GraphQLFailure<
|
|
239
123
|
E extends GraphQLIssue = GraphQLIssue,
|
|
240
124
|
> extends Data.TaggedError("GraphQLFailure")<{
|
|
241
125
|
readonly errors: readonly [E, ...E[]];
|
|
242
126
|
readonly data: unknown;
|
|
243
|
-
readonly status
|
|
127
|
+
readonly status?: number;
|
|
244
128
|
}> {
|
|
245
129
|
get message(): string {
|
|
246
130
|
return this.errors
|
|
@@ -251,622 +135,1225 @@ export class GraphQLFailure<
|
|
|
251
135
|
.join("; ");
|
|
252
136
|
}
|
|
253
137
|
}
|
|
138
|
+
|
|
139
|
+
/** The request never produced a GraphQL response (network, HTTP, non-JSON). */
|
|
254
140
|
export class GraphQLTransportError extends Data.TaggedError(
|
|
255
141
|
"GraphQLTransportError",
|
|
256
142
|
)<{
|
|
257
143
|
readonly message: string;
|
|
258
|
-
readonly cause?: unknown;
|
|
259
144
|
readonly status?: number;
|
|
145
|
+
/** Server retry hint in seconds. */
|
|
260
146
|
readonly retryAfter?: number;
|
|
147
|
+
readonly cause?: unknown;
|
|
261
148
|
}> {}
|
|
262
|
-
|
|
149
|
+
|
|
150
|
+
/** A paginated connection reported more pages without a new cursor. */
|
|
151
|
+
export class GraphQLPaginationError extends Data.TaggedError(
|
|
152
|
+
"GraphQLPaginationError",
|
|
153
|
+
)<{
|
|
263
154
|
readonly message: string;
|
|
264
|
-
readonly
|
|
265
|
-
readonly cause?: unknown;
|
|
155
|
+
readonly cursor: string | null;
|
|
266
156
|
}> {}
|
|
267
|
-
export class GraphQLRequestError extends Data.TaggedError(
|
|
268
|
-
"GraphQLRequestError",
|
|
269
|
-
)<{ readonly message: string }> {}
|
|
270
|
-
export type ClientError =
|
|
271
|
-
| GraphQLTransportError
|
|
272
|
-
| GraphQLDecodeError
|
|
273
|
-
| GraphQLRequestError;
|
|
274
|
-
type IssueOf<E> = E extends GraphQLFailure<infer I> ? I : never;
|
|
275
|
-
/** True only when every issue has an allowed tag; mixed failures are never hidden. */
|
|
276
|
-
export const isErrorTag = (
|
|
277
|
-
error: unknown,
|
|
278
|
-
tags: string | readonly string[],
|
|
279
|
-
): boolean => {
|
|
280
|
-
const allowed = typeof tags === "string" ? [tags] : tags;
|
|
281
|
-
return (
|
|
282
|
-
error instanceof GraphQLFailure &&
|
|
283
|
-
error.errors.length > 0 &&
|
|
284
|
-
error.errors.every((issue) => allowed.includes(issue._tag))
|
|
285
|
-
);
|
|
286
|
-
};
|
|
287
|
-
/** Recover only an aggregate whose every issue matches. The original aggregate is also supplied. */
|
|
288
|
-
export const catchTags =
|
|
289
|
-
<const T extends readonly string[] | string, B, E2, R2>(
|
|
290
|
-
tags: T,
|
|
291
|
-
handler: (
|
|
292
|
-
error: GraphQLIssue & { readonly _tag: T extends string ? T : T[number] },
|
|
293
|
-
failure: GraphQLFailure,
|
|
294
|
-
) => Effect.Effect<B, E2, R2>,
|
|
295
|
-
) =>
|
|
296
|
-
<A, E, R>(
|
|
297
|
-
self: Effect.Effect<A, E, R>,
|
|
298
|
-
): Effect.Effect<A | B, E | E2, R | R2> =>
|
|
299
|
-
self.pipe(
|
|
300
|
-
Effect.catch((error): Effect.Effect<B, E | E2, R2> =>
|
|
301
|
-
isErrorTag(error, tags)
|
|
302
|
-
? handler(
|
|
303
|
-
(error as GraphQLFailure).errors[0] as any,
|
|
304
|
-
error as GraphQLFailure,
|
|
305
|
-
)
|
|
306
|
-
: Effect.fail(error),
|
|
307
|
-
),
|
|
308
|
-
);
|
|
309
157
|
|
|
310
|
-
|
|
311
|
-
export interface
|
|
312
|
-
readonly
|
|
313
|
-
readonly
|
|
314
|
-
readonly
|
|
315
|
-
|
|
158
|
+
/** Recognizes a raw GraphQL error. Every present property must match. */
|
|
159
|
+
export interface ErrorMatcher {
|
|
160
|
+
readonly code?: string;
|
|
161
|
+
readonly message?: string;
|
|
162
|
+
readonly messageIncludes?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Runtime description of one generated error class. */
|
|
166
|
+
export interface ErrorSpec<E extends GraphQLIssue = GraphQLIssue> {
|
|
167
|
+
readonly tag: E["_tag"];
|
|
168
|
+
readonly make: (props: GraphQLIssueProps) => E;
|
|
169
|
+
readonly matchers: ReadonlyArray<ErrorMatcher>;
|
|
170
|
+
/** Queries failing only with retryable errors are retried. */
|
|
171
|
+
readonly retryable: boolean;
|
|
172
|
+
/** Applies to every root, including errors that carry no path. */
|
|
173
|
+
readonly global: boolean;
|
|
316
174
|
}
|
|
175
|
+
|
|
176
|
+
export const errorSpec = <E extends GraphQLIssue>(
|
|
177
|
+
make: new (props: GraphQLIssueProps) => E,
|
|
178
|
+
tag: E["_tag"],
|
|
179
|
+
matchers: ReadonlyArray<ErrorMatcher>,
|
|
180
|
+
options: { readonly retryable?: boolean; readonly global?: boolean } = {},
|
|
181
|
+
): ErrorSpec<E> => ({
|
|
182
|
+
tag,
|
|
183
|
+
make: (props) => new make(props),
|
|
184
|
+
matchers,
|
|
185
|
+
retryable: options.retryable ?? false,
|
|
186
|
+
global: options.global ?? false,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/** An entry of a GraphQL response's `errors` array, as sent on the wire. */
|
|
190
|
+
export interface RawGraphQLError {
|
|
191
|
+
readonly message: string;
|
|
192
|
+
readonly path?: ReadonlyArray<string | number>;
|
|
193
|
+
readonly locations?: ReadonlyArray<{
|
|
194
|
+
readonly line: number;
|
|
195
|
+
readonly column: number;
|
|
196
|
+
}>;
|
|
197
|
+
readonly extensions?: Record<string, unknown>;
|
|
198
|
+
readonly [key: string]: unknown;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** What a transport returns once it has a GraphQL response body. */
|
|
317
202
|
export interface GraphQLResponse {
|
|
318
|
-
readonly
|
|
319
|
-
readonly
|
|
320
|
-
readonly
|
|
203
|
+
readonly data?: unknown;
|
|
204
|
+
readonly errors?: ReadonlyArray<RawGraphQLError>;
|
|
205
|
+
readonly status?: number;
|
|
206
|
+
readonly headers?: Readonly<Record<string, string | undefined>>;
|
|
321
207
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
readonly
|
|
334
|
-
readonly
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
208
|
+
|
|
209
|
+
export type ArgMeta = Record<string, string>;
|
|
210
|
+
|
|
211
|
+
export type FieldMeta = {
|
|
212
|
+
readonly name: string;
|
|
213
|
+
readonly kind: "scalar" | "object" | "list" | "connection";
|
|
214
|
+
readonly of?: TypeMeta;
|
|
215
|
+
readonly argTypes?: ArgMeta;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export type TypeMeta = {
|
|
219
|
+
readonly name: string;
|
|
220
|
+
readonly fields: Record<string, FieldMeta>;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const scalarField = (name: string): FieldMeta => ({
|
|
224
|
+
name,
|
|
225
|
+
kind: "scalar",
|
|
226
|
+
});
|
|
227
|
+
export const objectField = (name: string, objectType: TypeMeta): FieldMeta => ({
|
|
228
|
+
name,
|
|
229
|
+
kind: "object",
|
|
230
|
+
of: objectType,
|
|
231
|
+
});
|
|
232
|
+
export const listField = (
|
|
233
|
+
name: string,
|
|
234
|
+
itemType: TypeMeta,
|
|
235
|
+
argTypes?: ArgMeta,
|
|
236
|
+
): FieldMeta => ({
|
|
237
|
+
name,
|
|
238
|
+
kind: "list",
|
|
239
|
+
of: itemType,
|
|
240
|
+
argTypes,
|
|
241
|
+
});
|
|
242
|
+
/** Relay connection: GraphQL `edges { node }` presented as `Query<Item[]>`. */
|
|
243
|
+
export const connectionField = (
|
|
244
|
+
name: string,
|
|
245
|
+
nodeType: TypeMeta,
|
|
246
|
+
argTypes?: ArgMeta,
|
|
247
|
+
): FieldMeta => ({
|
|
248
|
+
name,
|
|
249
|
+
kind: "connection",
|
|
250
|
+
of: nodeType,
|
|
251
|
+
argTypes,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
export type TypeRef =
|
|
255
|
+
| { readonly tag: "scalar" }
|
|
256
|
+
| { readonly tag: "object"; readonly meta: TypeMeta }
|
|
257
|
+
| { readonly tag: "list"; readonly of: TypeRef };
|
|
258
|
+
|
|
259
|
+
const objectRef = (meta: TypeMeta): TypeRef => ({ tag: "object", meta });
|
|
260
|
+
const listRef = (itemType: TypeRef): TypeRef => ({
|
|
261
|
+
tag: "list",
|
|
262
|
+
of: itemType,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const fieldResult = (parent: TypeRef, field: FieldMeta): TypeRef => {
|
|
266
|
+
const inner = (): TypeRef => {
|
|
267
|
+
if (field.kind === "scalar") return { tag: "scalar" };
|
|
268
|
+
if (field.kind === "object") return objectRef(field.of!);
|
|
269
|
+
return listRef(objectRef(field.of!)); // list | connection
|
|
270
|
+
};
|
|
271
|
+
return parent.tag === "list"
|
|
272
|
+
? listRef(fieldResult(parent.of, field))
|
|
273
|
+
: inner();
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const unwrapList = (typeRef: TypeRef): TypeRef => {
|
|
277
|
+
if (typeRef.tag !== "list") throw new GqlError("expected a list Query");
|
|
278
|
+
return typeRef.of;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const fieldsOf = (typeRef: TypeRef): Record<string, FieldMeta> => {
|
|
282
|
+
if (typeRef.tag === "object") return typeRef.meta.fields;
|
|
283
|
+
if (typeRef.tag === "list") return fieldsOf(typeRef.of);
|
|
284
|
+
return {};
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export interface SelNode {
|
|
288
|
+
field: string;
|
|
289
|
+
alias: string | undefined;
|
|
290
|
+
args: Record<string, string> | undefined;
|
|
291
|
+
children: Map<string, SelNode>;
|
|
292
|
+
isScalar: boolean;
|
|
293
|
+
isList: boolean;
|
|
339
294
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
readonly
|
|
295
|
+
|
|
296
|
+
export interface CompiledOperation {
|
|
297
|
+
readonly document: string;
|
|
298
|
+
readonly operationName: string;
|
|
299
|
+
readonly variables: Record<string, unknown>;
|
|
300
|
+
readonly kind: "query" | "mutation";
|
|
301
|
+
readonly tree: SelNode;
|
|
343
302
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
303
|
+
|
|
304
|
+
export class GqlTransport extends Context.Service<
|
|
305
|
+
GqlTransport,
|
|
306
|
+
{
|
|
307
|
+
readonly execute: (
|
|
308
|
+
req: CompiledOperation,
|
|
309
|
+
) => Effect.Effect<GraphQLResponse, GraphQLTransportError, any>;
|
|
310
|
+
}
|
|
311
|
+
>()("@distilled.cloud/graphql/Transport") {}
|
|
312
|
+
|
|
313
|
+
const log = (..._args: Array<unknown>): void => {};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* GraphQL fields copied onto a Query so `user.name` type-checks.
|
|
317
|
+
* `Query<User>` contains `projects: Query<Project[]>`, which contains
|
|
318
|
+
* `owner: Query<User>` again — TypeScript leaves the cycle lazy. Fields
|
|
319
|
+
* inherit the errors of the root they were read from.
|
|
320
|
+
*/
|
|
321
|
+
type QueryFields<Value, Error> = [NonNullable<Value>] extends [
|
|
322
|
+
ReadonlyArray<infer Item>,
|
|
323
|
+
]
|
|
324
|
+
? {
|
|
325
|
+
readonly [Field in keyof NonNullable<Item>]: Query<
|
|
326
|
+
NonNullable<Item>[Field] | Nullish<Item>,
|
|
327
|
+
Error
|
|
328
|
+
>;
|
|
329
|
+
} & {
|
|
330
|
+
(args: Record<string, unknown>): Query<Value, Error>;
|
|
331
|
+
}
|
|
332
|
+
: [NonNullable<Value>] extends [object]
|
|
333
|
+
? {
|
|
334
|
+
readonly [Field in keyof NonNullable<Value>]: Query<
|
|
335
|
+
NonNullable<Value>[Field] | Nullish<Value>,
|
|
336
|
+
Error
|
|
337
|
+
>;
|
|
338
|
+
}
|
|
339
|
+
: {};
|
|
340
|
+
|
|
341
|
+
/** A field read through a `null` parent is `null`. */
|
|
342
|
+
type Nullish<Value> = Extract<Value, null | undefined>;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* A lazy GraphQL selection. `Value` is the plain data after `Query.fn` runs;
|
|
346
|
+
* `Error` is the union of typed errors the roots it reads can return.
|
|
347
|
+
*/
|
|
348
|
+
export type Query<Value, Error = never> = QueryNode<Value, Error> &
|
|
349
|
+
QueryFields<Value, Error>;
|
|
350
|
+
|
|
351
|
+
/** Strip Query/Effect wrappers from a returned plan down to plain data. */
|
|
352
|
+
export type UnwrapPlan<Plan> =
|
|
353
|
+
Plan extends QueryNode<infer Value, any>
|
|
354
|
+
? Value
|
|
355
|
+
: Plan extends Effect.Effect<infer Success, any, any>
|
|
356
|
+
? Success
|
|
357
|
+
: Plan extends ReadonlyArray<infer Element>
|
|
358
|
+
? ReadonlyArray<UnwrapPlan<Element>>
|
|
359
|
+
: Plan extends object
|
|
360
|
+
? { readonly [Key in keyof Plan]: UnwrapPlan<Plan[Key]> }
|
|
361
|
+
: Plan;
|
|
362
|
+
|
|
363
|
+
/** Typed errors of every Query and Effect in a returned plan. */
|
|
364
|
+
export type PlanError<Plan> =
|
|
365
|
+
Plan extends QueryNode<any, infer Error>
|
|
366
|
+
? Error
|
|
367
|
+
: Plan extends Effect.Effect<any, infer Error, any>
|
|
368
|
+
? Error
|
|
369
|
+
: Plan extends ReadonlyArray<infer Element>
|
|
370
|
+
? PlanError<Element>
|
|
371
|
+
: Plan extends object
|
|
372
|
+
? { readonly [Key in keyof Plan]: PlanError<Plan[Key]> }[keyof Plan]
|
|
373
|
+
: never;
|
|
374
|
+
|
|
375
|
+
/** Failures of evaluating a plan whose roots declare `Error`. */
|
|
376
|
+
export type QueryError<Error> =
|
|
377
|
+
| Error
|
|
378
|
+
| GraphQLFailure<Extract<Error, GraphQLIssue> | UnknownGraphQLError>
|
|
379
|
+
| UnknownGraphQLError
|
|
380
|
+
| GraphQLTransportError;
|
|
381
|
+
|
|
382
|
+
type Expr =
|
|
383
|
+
| RootExpr
|
|
384
|
+
| PropExpr
|
|
385
|
+
| ItemExpr
|
|
386
|
+
| MapValueExpr
|
|
387
|
+
| MapItemsExpr
|
|
388
|
+
| FilterExpr
|
|
389
|
+
| FlatMapExpr
|
|
390
|
+
| LiteralExpr;
|
|
391
|
+
|
|
392
|
+
type RootExpr = {
|
|
393
|
+
readonly _tag: "Root";
|
|
394
|
+
readonly op: "query" | "mutation";
|
|
395
|
+
readonly field: string;
|
|
396
|
+
readonly args: Record<string, unknown> | undefined;
|
|
397
|
+
readonly argTypes: ArgMeta | undefined;
|
|
398
|
+
readonly type: TypeRef;
|
|
399
|
+
/** Relay connection: paint `edges { node }`, extract an array of nodes. */
|
|
400
|
+
readonly connection?: boolean;
|
|
401
|
+
/** Errors this root field can return, including global errors. */
|
|
402
|
+
readonly errors: ReadonlyArray<ErrorSpec>;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
type PropExpr = {
|
|
406
|
+
readonly _tag: "Prop";
|
|
407
|
+
readonly parent: Expr;
|
|
408
|
+
readonly field: FieldMeta;
|
|
409
|
+
readonly type: TypeRef;
|
|
410
|
+
readonly args?: Record<string, unknown>;
|
|
350
411
|
};
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
412
|
+
|
|
413
|
+
type ItemExpr = {
|
|
414
|
+
readonly _tag: "Item";
|
|
415
|
+
readonly list: Expr;
|
|
416
|
+
readonly type: TypeRef;
|
|
354
417
|
};
|
|
355
418
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
419
|
+
type MapValueExpr = {
|
|
420
|
+
readonly _tag: "MapValue";
|
|
421
|
+
readonly parent: Expr;
|
|
422
|
+
readonly mapFn: (value: unknown) => unknown;
|
|
423
|
+
readonly type: TypeRef;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
type MapItemsExpr = {
|
|
427
|
+
readonly _tag: "MapItems";
|
|
428
|
+
readonly parent: Expr;
|
|
429
|
+
readonly mapped: unknown;
|
|
430
|
+
/** Map one object (`null` stays `null`) instead of every list item. */
|
|
431
|
+
readonly single?: boolean;
|
|
432
|
+
readonly type: TypeRef;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
type FilterExpr = {
|
|
436
|
+
readonly _tag: "Filter";
|
|
437
|
+
readonly parent: Expr;
|
|
438
|
+
readonly predicate: Expr;
|
|
439
|
+
readonly type: TypeRef;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
type FlatMapExpr = {
|
|
443
|
+
readonly _tag: "FlatMap";
|
|
444
|
+
readonly parent: Expr;
|
|
445
|
+
readonly flatMapFn: (value: unknown) => unknown;
|
|
446
|
+
readonly type: TypeRef;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
type LiteralExpr = {
|
|
450
|
+
readonly _tag: "Literal";
|
|
451
|
+
readonly value: unknown;
|
|
452
|
+
readonly type: TypeRef;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
export class QueryNode<out Value = unknown, out Error = never> {
|
|
456
|
+
readonly [QuerySymbol] = QuerySymbol;
|
|
457
|
+
declare readonly valueType: Value;
|
|
458
|
+
declare readonly errorType: Error;
|
|
459
|
+
constructor(readonly expr: Expr) {
|
|
460
|
+
return proxy(this) as QueryNode<Value, Error>;
|
|
367
461
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
462
|
+
pipe<Self>(this: Self): Self;
|
|
463
|
+
pipe<Self, Out1>(this: Self, step1: (_: Self) => Out1): Out1;
|
|
464
|
+
pipe<Self, Out1, Out2>(
|
|
465
|
+
this: Self,
|
|
466
|
+
step1: (_: Self) => Out1,
|
|
467
|
+
step2: (_: Out1) => Out2,
|
|
468
|
+
): Out2;
|
|
469
|
+
pipe<Self, Out1, Out2, Out3>(
|
|
470
|
+
this: Self,
|
|
471
|
+
step1: (_: Self) => Out1,
|
|
472
|
+
step2: (_: Out1) => Out2,
|
|
473
|
+
step3: (_: Out2) => Out3,
|
|
474
|
+
): Out3;
|
|
475
|
+
pipe<Self, Out1, Out2, Out3, Out4>(
|
|
476
|
+
this: Self,
|
|
477
|
+
step1: (_: Self) => Out1,
|
|
478
|
+
step2: (_: Out1) => Out2,
|
|
479
|
+
step3: (_: Out2) => Out3,
|
|
480
|
+
step4: (_: Out3) => Out4,
|
|
481
|
+
): Out4;
|
|
482
|
+
pipe<Self, Out1, Out2, Out3, Out4, Out5>(
|
|
483
|
+
this: Self,
|
|
484
|
+
step1: (_: Self) => Out1,
|
|
485
|
+
step2: (_: Out1) => Out2,
|
|
486
|
+
step3: (_: Out2) => Out3,
|
|
487
|
+
step4: (_: Out3) => Out4,
|
|
488
|
+
step5: (_: Out4) => Out5,
|
|
489
|
+
): Out5;
|
|
490
|
+
pipe<Self, Out1, Out2, Out3, Out4, Out5, Out6>(
|
|
491
|
+
this: Self,
|
|
492
|
+
step1: (_: Self) => Out1,
|
|
493
|
+
step2: (_: Out1) => Out2,
|
|
494
|
+
step3: (_: Out2) => Out3,
|
|
495
|
+
step4: (_: Out3) => Out4,
|
|
496
|
+
step5: (_: Out4) => Out5,
|
|
497
|
+
step6: (_: Out5) => Out6,
|
|
498
|
+
): Out6;
|
|
499
|
+
pipe<Self, Out1, Out2, Out3, Out4, Out5, Out6, Out7>(
|
|
500
|
+
this: Self,
|
|
501
|
+
step1: (_: Self) => Out1,
|
|
502
|
+
step2: (_: Out1) => Out2,
|
|
503
|
+
step3: (_: Out2) => Out3,
|
|
504
|
+
step4: (_: Out3) => Out4,
|
|
505
|
+
step5: (_: Out4) => Out5,
|
|
506
|
+
step6: (_: Out5) => Out6,
|
|
507
|
+
step7: (_: Out6) => Out7,
|
|
508
|
+
): Out7;
|
|
509
|
+
pipe<Self, Out1, Out2, Out3, Out4, Out5, Out6, Out7, Out8>(
|
|
510
|
+
this: Self,
|
|
511
|
+
step1: (_: Self) => Out1,
|
|
512
|
+
step2: (_: Out1) => Out2,
|
|
513
|
+
step3: (_: Out2) => Out3,
|
|
514
|
+
step4: (_: Out3) => Out4,
|
|
515
|
+
step5: (_: Out4) => Out5,
|
|
516
|
+
step6: (_: Out5) => Out6,
|
|
517
|
+
step7: (_: Out6) => Out7,
|
|
518
|
+
step8: (_: Out7) => Out8,
|
|
519
|
+
): Out8;
|
|
520
|
+
pipe(...steps: Array<(_: any) => any>): unknown {
|
|
521
|
+
return steps.reduce((current, step) => step(current), this as never);
|
|
375
522
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (type.kind === "INPUT_OBJECT") {
|
|
379
|
-
if (!object(value)) requestError(`${at} must be an input object`);
|
|
380
|
-
const input = value as Record<string, unknown>;
|
|
381
|
-
const result: Record<string, unknown> = {};
|
|
382
|
-
for (const key of Object.keys(input))
|
|
383
|
-
if (!type.inputFields?.[key])
|
|
384
|
-
requestError(`${at}: unknown input field ${key}`);
|
|
385
|
-
for (const [key, field] of Object.entries(type.inputFields ?? {})) {
|
|
386
|
-
if (input[key] === undefined) {
|
|
387
|
-
if (field.type.endsWith("!") && field.defaultValue === undefined)
|
|
388
|
-
requestError(`${at}.${key} is required`);
|
|
389
|
-
} else
|
|
390
|
-
result[key] = encodeInput(
|
|
391
|
-
model,
|
|
392
|
-
field.type,
|
|
393
|
-
input[key],
|
|
394
|
-
`${at}.${key}`,
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
return result;
|
|
523
|
+
[inspect](): string {
|
|
524
|
+
return printExpr(this.expr);
|
|
398
525
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
requestError(`${at}: invalid ${base}`);
|
|
402
|
-
} else if (type.scalar === "string" || base === "String" || base === "ID") {
|
|
403
|
-
if (typeof value !== "string") requestError(`${at} must be a string`);
|
|
404
|
-
} else if (type.scalar === "number" || base === "Int" || base === "Float") {
|
|
405
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
406
|
-
requestError(`${at} must be a finite number`);
|
|
407
|
-
if (
|
|
408
|
-
base === "Int" &&
|
|
409
|
-
(!Number.isInteger(value) ||
|
|
410
|
-
(value as number) < -2147483648 ||
|
|
411
|
-
(value as number) > 2147483647)
|
|
412
|
-
)
|
|
413
|
-
requestError(`${at} must be a GraphQL Int`);
|
|
414
|
-
} else if (type.scalar === "boolean" || base === "Boolean") {
|
|
415
|
-
if (typeof value !== "boolean") requestError(`${at} must be a boolean`);
|
|
526
|
+
toString(): string {
|
|
527
|
+
return this[inspect]();
|
|
416
528
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export const isQuery = (value: unknown): value is Query<unknown> =>
|
|
532
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
533
|
+
value !== null &&
|
|
534
|
+
QuerySymbol in value;
|
|
535
|
+
|
|
536
|
+
const make = <Value, Error = never>(expr: Expr): Query<Value, Error> =>
|
|
537
|
+
new QueryNode<Value, Error>(expr) as unknown as Query<Value, Error>;
|
|
538
|
+
|
|
539
|
+
const itemQuery = (list: QueryNode): QueryNode =>
|
|
540
|
+
new QueryNode({
|
|
541
|
+
_tag: "Item",
|
|
542
|
+
list: list.expr,
|
|
543
|
+
type: unwrapList(list.expr.type),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
function proxy(self: QueryNode<unknown, unknown>): Query<unknown> {
|
|
547
|
+
const callable = Object.assign(function (bag?: Record<string, unknown>) {
|
|
548
|
+
return bag == null ? self : applyBag(self, bag);
|
|
549
|
+
}, self);
|
|
550
|
+
return new Proxy(callable, {
|
|
551
|
+
has: (_, prop) =>
|
|
552
|
+
prop === QuerySymbol ||
|
|
553
|
+
prop === inspect ||
|
|
554
|
+
prop in self ||
|
|
555
|
+
(typeof prop === "string" && prop in fieldsOf(self.expr.type)),
|
|
556
|
+
get: (_target, prop) => {
|
|
557
|
+
if (prop === QuerySymbol) return QuerySymbol;
|
|
558
|
+
if (prop === inspect) return self[inspect].bind(self);
|
|
559
|
+
if (prop === "pipe") return self.pipe.bind(self);
|
|
560
|
+
if (typeof prop === "string" && prop in self) {
|
|
561
|
+
const member = (self as unknown as Record<string, unknown>)[prop];
|
|
562
|
+
return typeof member === "function" ? member.bind(self) : member;
|
|
563
|
+
}
|
|
564
|
+
if (typeof prop !== "string") return undefined;
|
|
565
|
+
const field = fieldsOf(self.expr.type)[prop];
|
|
566
|
+
if (!field) return undefined;
|
|
567
|
+
return make({
|
|
568
|
+
_tag: "Prop",
|
|
569
|
+
parent: self.expr,
|
|
570
|
+
field,
|
|
571
|
+
type: fieldResult(self.expr.type, field),
|
|
572
|
+
});
|
|
573
|
+
},
|
|
574
|
+
apply: (_target, _thisArg, args) => applyBag(self, args[0] ?? {}),
|
|
575
|
+
}) as unknown as Query<unknown>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const applyBag = <Value, Error>(
|
|
579
|
+
self: QueryNode<Value, Error>,
|
|
580
|
+
bag: Record<string, unknown>,
|
|
581
|
+
): Query<Value, Error> => {
|
|
582
|
+
const argTypes =
|
|
583
|
+
self.expr._tag === "Prop"
|
|
584
|
+
? self.expr.field.argTypes
|
|
585
|
+
: self.expr._tag === "Root"
|
|
586
|
+
? self.expr.argTypes
|
|
587
|
+
: undefined;
|
|
588
|
+
const args: Record<string, unknown> = {};
|
|
589
|
+
for (const [argName, argValue] of Object.entries(bag)) {
|
|
590
|
+
if (argTypes && argName in argTypes) args[argName] = argValue;
|
|
591
|
+
}
|
|
592
|
+
if (self.expr._tag === "Prop") {
|
|
593
|
+
return make<Value, Error>({
|
|
594
|
+
...self.expr,
|
|
595
|
+
args: { ...self.expr.args, ...args },
|
|
596
|
+
});
|
|
434
597
|
}
|
|
598
|
+
if (self.expr._tag === "Root") {
|
|
599
|
+
return make<Value, Error>({
|
|
600
|
+
...self.expr,
|
|
601
|
+
args: { ...self.expr.args, ...args },
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return self as unknown as Query<Value, Error>;
|
|
435
605
|
};
|
|
436
606
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
607
|
+
export const root = <Value, Error = never>(
|
|
608
|
+
op: "query" | "mutation",
|
|
609
|
+
field: string,
|
|
610
|
+
meta: TypeMeta,
|
|
611
|
+
args?: Record<string, unknown>,
|
|
612
|
+
argTypes?: ArgMeta,
|
|
613
|
+
errors: ReadonlyArray<ErrorSpec> = [],
|
|
614
|
+
): Query<Value, Error> =>
|
|
615
|
+
make({
|
|
616
|
+
_tag: "Root",
|
|
617
|
+
op,
|
|
618
|
+
field,
|
|
619
|
+
args,
|
|
620
|
+
argTypes,
|
|
621
|
+
type: objectRef(meta),
|
|
622
|
+
errors,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
export const rootList = <Item, Error = never>(
|
|
626
|
+
op: "query" | "mutation",
|
|
627
|
+
field: string,
|
|
628
|
+
meta: TypeMeta,
|
|
629
|
+
args?: Record<string, unknown>,
|
|
630
|
+
argTypes?: ArgMeta,
|
|
631
|
+
errors: ReadonlyArray<ErrorSpec> = [],
|
|
632
|
+
): Query<ReadonlyArray<Item>, Error> =>
|
|
633
|
+
make({
|
|
634
|
+
_tag: "Root",
|
|
635
|
+
op,
|
|
636
|
+
field,
|
|
637
|
+
args,
|
|
638
|
+
argTypes,
|
|
639
|
+
type: listRef(objectRef(meta)),
|
|
640
|
+
errors,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
/** Root Relay connection: typed as `Query<Item[]>`, document uses `edges { node }`. */
|
|
644
|
+
export const rootConnection = <Item, Error = never>(
|
|
645
|
+
op: "query" | "mutation",
|
|
646
|
+
field: string,
|
|
647
|
+
meta: TypeMeta,
|
|
648
|
+
args?: Record<string, unknown>,
|
|
649
|
+
argTypes?: ArgMeta,
|
|
650
|
+
errors: ReadonlyArray<ErrorSpec> = [],
|
|
651
|
+
): Query<ReadonlyArray<Item>, Error> =>
|
|
652
|
+
make({
|
|
653
|
+
_tag: "Root",
|
|
654
|
+
op,
|
|
655
|
+
field,
|
|
656
|
+
args,
|
|
657
|
+
argTypes,
|
|
658
|
+
type: listRef(objectRef(meta)),
|
|
659
|
+
connection: true,
|
|
660
|
+
errors,
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
/** Root field whose GraphQL type is a scalar, enum, or list of those. */
|
|
664
|
+
export const rootLeaf = <Value, Error = never>(
|
|
665
|
+
op: "query" | "mutation",
|
|
666
|
+
field: string,
|
|
667
|
+
list: boolean,
|
|
668
|
+
args?: Record<string, unknown>,
|
|
669
|
+
argTypes?: ArgMeta,
|
|
670
|
+
errors: ReadonlyArray<ErrorSpec> = [],
|
|
671
|
+
): Query<Value, Error> =>
|
|
672
|
+
make({
|
|
673
|
+
_tag: "Root",
|
|
674
|
+
op,
|
|
675
|
+
field,
|
|
676
|
+
args,
|
|
677
|
+
argTypes,
|
|
678
|
+
type: list ? listRef({ tag: "scalar" }) : { tag: "scalar" },
|
|
679
|
+
errors,
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const printExpr = (expr: Expr): string => {
|
|
683
|
+
switch (expr._tag) {
|
|
684
|
+
case "Root":
|
|
685
|
+
return expr.args
|
|
686
|
+
? `${expr.field}(${JSON.stringify(expr.args)})`
|
|
687
|
+
: expr.field;
|
|
688
|
+
case "Prop":
|
|
689
|
+
return `${printExpr(expr.parent)}.${expr.field.name}`;
|
|
690
|
+
case "Item":
|
|
691
|
+
return "$item";
|
|
692
|
+
case "MapValue":
|
|
693
|
+
return `map(${printExpr(expr.parent)})`;
|
|
694
|
+
case "MapItems":
|
|
695
|
+
return `mapItems(${printExpr(expr.parent)})`;
|
|
696
|
+
case "Filter":
|
|
697
|
+
return `filter(${printExpr(expr.parent)})`;
|
|
698
|
+
case "FlatMap":
|
|
699
|
+
return `flatMap(${printExpr(expr.parent)})`;
|
|
700
|
+
case "Literal":
|
|
701
|
+
return `of(${String(expr.value)})`;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const emptySel = (field: string): SelNode => ({
|
|
706
|
+
field,
|
|
707
|
+
alias: undefined,
|
|
708
|
+
args: undefined,
|
|
709
|
+
children: new Map(),
|
|
710
|
+
isScalar: false,
|
|
711
|
+
isList: false,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
const stableStringify = (value: unknown): string => {
|
|
715
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
716
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
717
|
+
const keys = Object.keys(value as object).sort();
|
|
718
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify((value as Record<string, unknown>)[key])}`).join(",")}}`;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
type Forest = {
|
|
722
|
+
kind: "query" | "mutation" | undefined;
|
|
723
|
+
roots: SelNode;
|
|
724
|
+
vars: Map<string, { type: string; value: unknown }>;
|
|
725
|
+
varI: number;
|
|
726
|
+
aliasI: number;
|
|
727
|
+
rootAlias: WeakMap<object, string>;
|
|
728
|
+
/** Response key of each root → the root field and its declared errors. */
|
|
729
|
+
rootErrors: Map<string, RootErrors>;
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
interface RootErrors {
|
|
733
|
+
readonly field: string;
|
|
734
|
+
readonly errors: ReadonlyArray<ErrorSpec>;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const bindArgs = (
|
|
738
|
+
forest: Forest,
|
|
739
|
+
args: Record<string, unknown> | undefined,
|
|
740
|
+
argTypes: ArgMeta | undefined,
|
|
741
|
+
): Record<string, string> | undefined => {
|
|
742
|
+
if (args === undefined) return undefined;
|
|
743
|
+
const bound: Record<string, string> = {};
|
|
744
|
+
let hasArgs = false;
|
|
745
|
+
for (const [name, value] of Object.entries(args)) {
|
|
746
|
+
if (value === undefined) continue;
|
|
747
|
+
forest.vars.set(`v${forest.varI}`, {
|
|
748
|
+
type: argTypes?.[name] ?? "String",
|
|
749
|
+
value,
|
|
456
750
|
});
|
|
457
|
-
|
|
751
|
+
bound[name] = `v${forest.varI++}`;
|
|
752
|
+
hasArgs = true;
|
|
753
|
+
}
|
|
754
|
+
return hasArgs ? bound : undefined;
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const ensureChild = (
|
|
758
|
+
forest: Forest,
|
|
759
|
+
parent: SelNode,
|
|
760
|
+
field: string,
|
|
761
|
+
options: {
|
|
762
|
+
args?: Record<string, unknown>;
|
|
763
|
+
argTypes?: ArgMeta;
|
|
764
|
+
isScalar?: boolean;
|
|
765
|
+
isList?: boolean;
|
|
766
|
+
},
|
|
767
|
+
): SelNode => {
|
|
768
|
+
const key =
|
|
769
|
+
options.args === undefined
|
|
770
|
+
? field
|
|
771
|
+
: `${field}:${stableStringify(options.args)}`;
|
|
772
|
+
const existing = parent.children.get(key);
|
|
773
|
+
if (existing) return existing;
|
|
774
|
+
const taken = new Set(
|
|
775
|
+
[...parent.children.values()].map((c) => c.alias ?? c.field),
|
|
776
|
+
);
|
|
777
|
+
let alias: string | undefined;
|
|
778
|
+
if (taken.has(field)) {
|
|
779
|
+
do {
|
|
780
|
+
alias = `${field}_${forest.aliasI++}`;
|
|
781
|
+
} while (taken.has(alias));
|
|
782
|
+
}
|
|
783
|
+
const sel: SelNode = {
|
|
784
|
+
field,
|
|
785
|
+
alias,
|
|
786
|
+
args: bindArgs(forest, options.args, options.argTypes),
|
|
787
|
+
children: new Map(),
|
|
788
|
+
isScalar: options.isScalar ?? false,
|
|
789
|
+
isList: options.isList ?? false,
|
|
458
790
|
};
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (
|
|
475
|
-
|
|
476
|
-
`
|
|
791
|
+
parent.children.set(key, sel);
|
|
792
|
+
return sel;
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
type Path = Array<string | "*">;
|
|
796
|
+
|
|
797
|
+
const paintConnection = (forest: Forest, connection: SelNode): Path => {
|
|
798
|
+
const edges = ensureChild(forest, connection, "edges", { isList: true });
|
|
799
|
+
ensureChild(forest, edges, "node", {});
|
|
800
|
+
return ["edges", "node"];
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const paintAbs = (forest: Forest, expr: Expr): Path => {
|
|
804
|
+
switch (expr._tag) {
|
|
805
|
+
case "Root": {
|
|
806
|
+
if (forest.kind !== undefined && forest.kind !== expr.op) {
|
|
807
|
+
throw new GqlError(
|
|
808
|
+
`cannot mix ${forest.kind} and ${expr.op} in one document`,
|
|
477
809
|
);
|
|
478
|
-
const field: GraphQLField | undefined =
|
|
479
|
-
fieldName === "__typename"
|
|
480
|
-
? { type: "String!", args: {}, errors: [] }
|
|
481
|
-
: type.fields?.[fieldName];
|
|
482
|
-
if (!field) return requestError(`Unknown field ${parent}.${fieldName}`);
|
|
483
|
-
const wrapper =
|
|
484
|
-
object(selected) &&
|
|
485
|
-
("where" in selected ||
|
|
486
|
-
"select" in selected ||
|
|
487
|
-
"$include" in selected ||
|
|
488
|
-
"$skip" in selected)
|
|
489
|
-
? selected
|
|
490
|
-
: undefined;
|
|
491
|
-
const args = wrapper?.where ?? {};
|
|
492
|
-
if (!object(args))
|
|
493
|
-
return requestError(`${parent}.${fieldName}.where must be an object`);
|
|
494
|
-
for (const arg of Object.keys(args))
|
|
495
|
-
if (!field.args[arg])
|
|
496
|
-
return requestError(
|
|
497
|
-
`Unknown argument ${parent}.${fieldName}(${arg})`,
|
|
498
|
-
);
|
|
499
|
-
const arguments_: NonNullable<FieldNode["arguments"]>[number][] = [];
|
|
500
|
-
for (const [arg, definition] of Object.entries(field.args)) {
|
|
501
|
-
if (args[arg] === undefined) {
|
|
502
|
-
if (
|
|
503
|
-
definition.type.endsWith("!") &&
|
|
504
|
-
definition.defaultValue === undefined
|
|
505
|
-
)
|
|
506
|
-
return requestError(`${parent}.${fieldName}(${arg}) is required`);
|
|
507
|
-
} else
|
|
508
|
-
arguments_.push({
|
|
509
|
-
kind: Kind.ARGUMENT,
|
|
510
|
-
name: nameNode(arg),
|
|
511
|
-
value: variable(
|
|
512
|
-
definition.type,
|
|
513
|
-
encodeInput(
|
|
514
|
-
model,
|
|
515
|
-
definition.type,
|
|
516
|
-
args[arg],
|
|
517
|
-
`${parent}.${fieldName}(${arg})`,
|
|
518
|
-
),
|
|
519
|
-
),
|
|
520
|
-
});
|
|
521
810
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
arguments: [
|
|
532
|
-
{
|
|
533
|
-
kind: Kind.ARGUMENT,
|
|
534
|
-
name: nameNode("if"),
|
|
535
|
-
value: variable("Boolean!", value),
|
|
536
|
-
},
|
|
537
|
-
],
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
const outputType = model.types[named(field.type)];
|
|
542
|
-
const leaf =
|
|
543
|
-
fieldName === "__typename" ||
|
|
544
|
-
outputType?.kind === "SCALAR" ||
|
|
545
|
-
outputType?.kind === "ENUM";
|
|
546
|
-
const sub = wrapper ? wrapper.select : selected;
|
|
547
|
-
let child: ReturnType<typeof walk> = { nodes: [], positions: [] };
|
|
548
|
-
if (leaf) {
|
|
549
|
-
if (sub !== true && sub !== undefined)
|
|
550
|
-
return requestError(
|
|
551
|
-
`${parent}.${fieldName} is a leaf and takes no selection`,
|
|
552
|
-
);
|
|
553
|
-
} else child = walk(named(field.type), sub);
|
|
554
|
-
nodes.push({
|
|
555
|
-
kind: Kind.FIELD,
|
|
556
|
-
name: nameNode(fieldName),
|
|
557
|
-
...(alias ? { alias: nameNode(alias) } : {}),
|
|
558
|
-
arguments: arguments_,
|
|
559
|
-
directives,
|
|
560
|
-
...(!leaf
|
|
561
|
-
? {
|
|
562
|
-
selectionSet: {
|
|
563
|
-
kind: Kind.SELECTION_SET,
|
|
564
|
-
selections: child.nodes,
|
|
565
|
-
},
|
|
566
|
-
}
|
|
567
|
-
: {}),
|
|
811
|
+
forest.kind = expr.op;
|
|
812
|
+
const node = ensureChild(forest, forest.roots, expr.field, {
|
|
813
|
+
args: expr.args,
|
|
814
|
+
argTypes: expr.argTypes,
|
|
815
|
+
// Leaf roots (`projectDelete: Boolean!`) take no sub-selection.
|
|
816
|
+
isScalar:
|
|
817
|
+
expr.type.tag === "scalar" ||
|
|
818
|
+
(expr.type.tag === "list" && expr.type.of.tag === "scalar"),
|
|
819
|
+
isList: expr.type.tag === "list" && !expr.connection,
|
|
568
820
|
});
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
conditional: wrapper?.$include === false || wrapper?.$skip === true,
|
|
821
|
+
const key = node.alias ?? node.field;
|
|
822
|
+
forest.rootAlias.set(expr, key);
|
|
823
|
+
forest.rootErrors.set(key, { field: expr.field, errors: expr.errors });
|
|
824
|
+
return expr.connection ? [key, ...paintConnection(forest, node)] : [key];
|
|
825
|
+
}
|
|
826
|
+
case "Prop": {
|
|
827
|
+
const parentPath = paintAbs(forest, expr.parent);
|
|
828
|
+
const parentSel = selAt(forest.roots, parentPath);
|
|
829
|
+
const node = ensureChild(forest, parentSel, expr.field.name, {
|
|
830
|
+
args: expr.args,
|
|
831
|
+
argTypes: expr.field.argTypes,
|
|
832
|
+
isScalar: expr.field.kind === "scalar",
|
|
833
|
+
isList: expr.field.kind === "list",
|
|
583
834
|
});
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return requestError(`${parent}.__alias must be an object`);
|
|
589
|
-
for (const [alias, fields] of Object.entries(value)) {
|
|
590
|
-
if (!object(fields) || Object.keys(fields).length !== 1)
|
|
591
|
-
return requestError(`Alias ${alias} must select exactly one field`);
|
|
592
|
-
const [field, selected] = Object.entries(fields)[0]!;
|
|
593
|
-
addField(field, selected, alias);
|
|
594
|
-
}
|
|
595
|
-
} else if (key === "__on") {
|
|
596
|
-
if (!object(value))
|
|
597
|
-
return requestError(`${parent}.__on must be an object`);
|
|
598
|
-
for (const [branch, fields] of Object.entries(value)) {
|
|
599
|
-
if (!type.possibleTypes?.includes(branch))
|
|
600
|
-
return requestError(
|
|
601
|
-
`${branch} is not a possible type of ${parent}`,
|
|
602
|
-
);
|
|
603
|
-
const children = walk(branch, fields);
|
|
604
|
-
nodes.push({
|
|
605
|
-
kind: Kind.INLINE_FRAGMENT,
|
|
606
|
-
typeCondition: { kind: Kind.NAMED_TYPE, name: nameNode(branch) },
|
|
607
|
-
selectionSet: {
|
|
608
|
-
kind: Kind.SELECTION_SET,
|
|
609
|
-
selections: children.nodes,
|
|
610
|
-
},
|
|
611
|
-
});
|
|
612
|
-
positions.push({
|
|
613
|
-
field: "__fragment",
|
|
614
|
-
key: "",
|
|
615
|
-
coordinate: parent,
|
|
616
|
-
type: branch,
|
|
617
|
-
errors: [],
|
|
618
|
-
children: children.positions,
|
|
619
|
-
branches: {},
|
|
620
|
-
conditional: false,
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
} else addField(key, value);
|
|
835
|
+
const path = [...parentPath, node.alias ?? node.field];
|
|
836
|
+
return expr.field.kind === "connection"
|
|
837
|
+
? [...path, ...paintConnection(forest, node)]
|
|
838
|
+
: path;
|
|
624
839
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
840
|
+
default:
|
|
841
|
+
throw new GqlError(`paintAbs: ${expr._tag}`);
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const selAt = (roots: SelNode, path: Path): SelNode => {
|
|
846
|
+
let current = roots;
|
|
847
|
+
for (const segment of path) {
|
|
848
|
+
if (segment === "*") continue;
|
|
849
|
+
const child = [...current.children.values()].find(
|
|
850
|
+
(candidate) => (candidate.alias ?? candidate.field) === segment,
|
|
851
|
+
);
|
|
852
|
+
if (!child) {
|
|
853
|
+
throw new GqlError(`internal: missing ${String(segment)}`);
|
|
854
|
+
}
|
|
855
|
+
current = child;
|
|
856
|
+
}
|
|
857
|
+
return current;
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const connectionNodeSel = (connection: SelNode): SelNode => {
|
|
861
|
+
const edges = [...connection.children.values()].find(
|
|
862
|
+
(c) => c.field === "edges",
|
|
863
|
+
);
|
|
864
|
+
const node = edges
|
|
865
|
+
? [...edges.children.values()].find((c) => c.field === "node")
|
|
866
|
+
: undefined;
|
|
867
|
+
if (!node) throw new GqlError("internal: connection missing edges.node");
|
|
868
|
+
return node;
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const relSel = (expr: Expr, listSel: SelNode): SelNode => {
|
|
872
|
+
switch (expr._tag) {
|
|
873
|
+
case "Item":
|
|
874
|
+
return listSel;
|
|
875
|
+
case "Prop": {
|
|
876
|
+
const parent = relSel(expr.parent, listSel);
|
|
877
|
+
const child = [...parent.children.values()].find(
|
|
878
|
+
(c) => c.field === expr.field.name,
|
|
879
|
+
);
|
|
880
|
+
if (!child) {
|
|
881
|
+
throw new GqlError(`missing ${expr.field.name} in list selection`);
|
|
882
|
+
}
|
|
883
|
+
return expr.field.kind === "connection"
|
|
884
|
+
? connectionNodeSel(child)
|
|
885
|
+
: child;
|
|
886
|
+
}
|
|
887
|
+
default:
|
|
888
|
+
return listSel;
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
const paintRel = (forest: Forest, expr: Expr, listSel: SelNode): void => {
|
|
893
|
+
switch (expr._tag) {
|
|
894
|
+
case "Item":
|
|
895
|
+
case "Literal":
|
|
896
|
+
return;
|
|
897
|
+
case "Prop": {
|
|
898
|
+
paintRel(forest, expr.parent, listSel);
|
|
899
|
+
const child = ensureChild(
|
|
900
|
+
forest,
|
|
901
|
+
relSel(expr.parent, listSel),
|
|
902
|
+
expr.field.name,
|
|
903
|
+
{
|
|
904
|
+
args: expr.args,
|
|
905
|
+
argTypes: expr.field.argTypes,
|
|
906
|
+
isScalar: expr.field.kind === "scalar",
|
|
907
|
+
isList: expr.field.kind === "list",
|
|
908
|
+
},
|
|
909
|
+
);
|
|
910
|
+
if (expr.field.kind === "connection") paintConnection(forest, child);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
case "MapValue":
|
|
914
|
+
case "FlatMap":
|
|
915
|
+
paintRel(forest, expr.parent, listSel);
|
|
916
|
+
return;
|
|
917
|
+
case "MapItems":
|
|
918
|
+
paintRel(forest, expr.parent, listSel);
|
|
919
|
+
visitRel(expr.mapped, forest, relSel(expr.parent, listSel));
|
|
920
|
+
return;
|
|
921
|
+
case "Filter":
|
|
922
|
+
paintRel(forest, expr.parent, listSel);
|
|
923
|
+
paintRel(forest, expr.predicate, relSel(expr.parent, listSel));
|
|
924
|
+
return;
|
|
925
|
+
case "Root":
|
|
926
|
+
throw new GqlError("root inside list item");
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const visitExpr = (forest: Forest, expr: Expr): void => {
|
|
931
|
+
switch (expr._tag) {
|
|
932
|
+
case "Root":
|
|
933
|
+
case "Prop":
|
|
934
|
+
paintAbs(forest, expr);
|
|
935
|
+
return;
|
|
936
|
+
case "Item":
|
|
937
|
+
return;
|
|
938
|
+
case "Literal":
|
|
939
|
+
return;
|
|
940
|
+
case "MapValue":
|
|
941
|
+
case "FlatMap":
|
|
942
|
+
visitExpr(forest, expr.parent);
|
|
943
|
+
return;
|
|
944
|
+
case "MapItems": {
|
|
945
|
+
visitExpr(forest, expr.parent);
|
|
946
|
+
const path = paintAbs(forest, graphqlBase(expr.parent));
|
|
947
|
+
visitRel(expr.mapped, forest, selAt(forest.roots, path));
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
case "Filter": {
|
|
951
|
+
visitExpr(forest, expr.parent);
|
|
952
|
+
const path = paintAbs(forest, graphqlBase(expr.parent));
|
|
953
|
+
paintRel(forest, expr.predicate, selAt(forest.roots, path));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const graphqlBase = (expr: Expr): Expr => {
|
|
960
|
+
switch (expr._tag) {
|
|
961
|
+
case "Root":
|
|
962
|
+
case "Prop":
|
|
963
|
+
case "Item":
|
|
964
|
+
return expr;
|
|
965
|
+
case "MapValue":
|
|
966
|
+
case "MapItems":
|
|
967
|
+
case "Filter":
|
|
968
|
+
case "FlatMap":
|
|
969
|
+
return graphqlBase(expr.parent);
|
|
970
|
+
case "Literal":
|
|
971
|
+
throw new GqlError("literal has no graphql base");
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const visitRel = (plan: unknown, forest: Forest, listSel: SelNode): void => {
|
|
976
|
+
if (isQuery(plan)) {
|
|
977
|
+
paintRel(forest, (plan as QueryNode).expr, listSel);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (plan !== null && typeof plan === "object" && !Effect.isEffect(plan)) {
|
|
981
|
+
for (const nested of Object.values(plan as Record<string, unknown>)) {
|
|
982
|
+
visitRel(nested, forest, listSel);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Recurse through a builder return value. Query nodes contribute GraphQL
|
|
989
|
+
* fields to `forest`; Effects are ignored until interpret (FlatMap).
|
|
990
|
+
*/
|
|
991
|
+
const visitPlan = (plan: unknown, forest: Forest): void => {
|
|
992
|
+
if (isQuery(plan)) {
|
|
993
|
+
log("visit", printExpr((plan as QueryNode).expr));
|
|
994
|
+
visitExpr(forest, (plan as QueryNode).expr);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (Effect.isEffect(plan)) return;
|
|
998
|
+
if (plan !== null && typeof plan === "object" && !Array.isArray(plan)) {
|
|
999
|
+
for (const nested of Object.values(plan as Record<string, unknown>)) {
|
|
1000
|
+
visitPlan(nested, forest);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const validate = (node: SelNode): string | undefined => {
|
|
1006
|
+
for (const child of node.children.values()) {
|
|
1007
|
+
if (!child.isScalar && child.children.size === 0) {
|
|
1008
|
+
return `field "${child.field}" has no sub-selection`;
|
|
1009
|
+
}
|
|
1010
|
+
const inner = validate(child);
|
|
1011
|
+
if (inner) return inner;
|
|
1012
|
+
}
|
|
1013
|
+
return undefined;
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const printChildren = (node: SelNode, indent: number): string => {
|
|
1017
|
+
const pad = " ".repeat(indent);
|
|
1018
|
+
const lines: Array<string> = [];
|
|
1019
|
+
for (const child of node.children.values()) {
|
|
1020
|
+
let head = child.alias ? `${child.alias}: ${child.field}` : child.field;
|
|
1021
|
+
if (child.args && Object.keys(child.args).length > 0) {
|
|
1022
|
+
const printed = Object.entries(child.args)
|
|
1023
|
+
.map(([argName, varName]) => `${argName}: $${varName}`)
|
|
1024
|
+
.join(", ");
|
|
1025
|
+
head += `(${printed})`;
|
|
1026
|
+
}
|
|
1027
|
+
if (child.isScalar) lines.push(`${pad}${head}`);
|
|
1028
|
+
else {
|
|
1029
|
+
lines.push(`${pad}${head} {`);
|
|
1030
|
+
lines.push(printChildren(child, indent + 1));
|
|
1031
|
+
lines.push(`${pad}}`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return lines.join("\n");
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const emptyForest = (): Forest => ({
|
|
1038
|
+
kind: undefined,
|
|
1039
|
+
roots: emptySel(""),
|
|
1040
|
+
vars: new Map(),
|
|
1041
|
+
varI: 0,
|
|
1042
|
+
aliasI: 0,
|
|
1043
|
+
rootAlias: new WeakMap(),
|
|
1044
|
+
rootErrors: new Map(),
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const emit = (forest: Forest): Compiled => {
|
|
1048
|
+
const err = validate(forest.roots);
|
|
1049
|
+
if (err) throw new GqlError(err);
|
|
1050
|
+
const kind = forest.kind ?? "query";
|
|
1051
|
+
const varDefs = [...forest.vars.entries()]
|
|
1052
|
+
.map(([name, binding]) => `$${name}: ${binding.type}`)
|
|
1053
|
+
.join(", ");
|
|
1054
|
+
const header = varDefs.length > 0 ? `${kind} Gql(${varDefs})` : `${kind} Gql`;
|
|
1055
|
+
const variables: Record<string, unknown> = {};
|
|
1056
|
+
for (const [name, binding] of forest.vars) variables[name] = binding.value;
|
|
643
1057
|
return {
|
|
644
|
-
|
|
1058
|
+
document: `${header} {\n${printChildren(forest.roots, 1)}\n}`,
|
|
1059
|
+
operationName: "Gql",
|
|
645
1060
|
variables,
|
|
646
|
-
operationName,
|
|
647
1061
|
kind,
|
|
648
|
-
|
|
649
|
-
|
|
1062
|
+
tree: forest.roots,
|
|
1063
|
+
rootAlias: forest.rootAlias,
|
|
1064
|
+
rootErrors: forest.rootErrors,
|
|
650
1065
|
};
|
|
651
1066
|
};
|
|
652
1067
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1068
|
+
type Compiled = CompiledOperation & {
|
|
1069
|
+
readonly rootAlias: WeakMap<object, string>;
|
|
1070
|
+
readonly rootErrors: ReadonlyMap<string, RootErrors>;
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
const nodesFromConnection = (value: unknown): unknown => {
|
|
1074
|
+
if (value == null) return value;
|
|
1075
|
+
if (Array.isArray(value)) return value.map(nodesFromConnection);
|
|
1076
|
+
const edges = (value as Record<string, unknown>).edges;
|
|
1077
|
+
if (!Array.isArray(edges)) return [];
|
|
1078
|
+
return edges.map((edge) =>
|
|
1079
|
+
edge == null ? edge : (edge as Record<string, unknown>).node,
|
|
1080
|
+
);
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const readField = (parent: unknown, name: string): unknown => {
|
|
1084
|
+
if (parent == null) return parent;
|
|
1085
|
+
if (Array.isArray(parent)) {
|
|
1086
|
+
return parent.map((item) =>
|
|
1087
|
+
item == null ? item : (item as Record<string, unknown>)[name],
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
return (parent as Record<string, unknown>)[name];
|
|
658
1091
|
};
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
ref: string,
|
|
1092
|
+
|
|
1093
|
+
const extractAbs = (
|
|
1094
|
+
expr: Expr,
|
|
663
1095
|
data: unknown,
|
|
664
|
-
|
|
665
|
-
branches: Readonly<Record<string, readonly Position[]>>,
|
|
666
|
-
path: ReadonlyArray<string | number>,
|
|
1096
|
+
rootAlias: WeakMap<object, string>,
|
|
667
1097
|
): unknown => {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1098
|
+
switch (expr._tag) {
|
|
1099
|
+
case "Root": {
|
|
1100
|
+
const raw = (data as Record<string, unknown>)[
|
|
1101
|
+
rootAlias.get(expr) ?? expr.field
|
|
1102
|
+
];
|
|
1103
|
+
return expr.connection ? nodesFromConnection(raw) : raw;
|
|
1104
|
+
}
|
|
1105
|
+
case "Prop": {
|
|
1106
|
+
const value = readField(
|
|
1107
|
+
extractAbs(expr.parent, data, rootAlias),
|
|
1108
|
+
expr.field.name,
|
|
1109
|
+
);
|
|
1110
|
+
return expr.field.kind === "connection"
|
|
1111
|
+
? nodesFromConnection(value)
|
|
1112
|
+
: value;
|
|
1113
|
+
}
|
|
1114
|
+
default:
|
|
1115
|
+
throw new GqlError(`extractAbs: ${expr._tag}`);
|
|
672
1116
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
const extractRel = (expr: Expr, item: unknown): unknown => {
|
|
1120
|
+
switch (expr._tag) {
|
|
1121
|
+
case "Item":
|
|
1122
|
+
return item;
|
|
1123
|
+
case "Prop": {
|
|
1124
|
+
const value = readField(extractRel(expr.parent, item), expr.field.name);
|
|
1125
|
+
return expr.field.kind === "connection"
|
|
1126
|
+
? nodesFromConnection(value)
|
|
1127
|
+
: value;
|
|
1128
|
+
}
|
|
1129
|
+
default:
|
|
1130
|
+
throw new GqlError(`extractRel: ${expr._tag}`);
|
|
684
1131
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (
|
|
700
|
-
primitive === "string | number" &&
|
|
701
|
-
typeof data !== "string" &&
|
|
702
|
-
typeof data !== "number"
|
|
703
|
-
)
|
|
704
|
-
return decodeFailure(`Expected ${base}`, path);
|
|
705
|
-
if (
|
|
706
|
-
base === "BigInt" &&
|
|
707
|
-
((typeof data === "number" && !Number.isSafeInteger(data)) ||
|
|
708
|
-
(typeof data === "string" && !/^-?\d+$/.test(data)))
|
|
709
|
-
)
|
|
710
|
-
return decodeFailure(`Invalid ${base}`, path);
|
|
711
|
-
if (
|
|
712
|
-
["string", "number", "boolean"].includes(primitive ?? "") &&
|
|
713
|
-
typeof data !== primitive
|
|
714
|
-
)
|
|
715
|
-
return decodeFailure(`Expected ${base}`, path);
|
|
716
|
-
if (
|
|
717
|
-
typeof data === "number" &&
|
|
718
|
-
(!Number.isFinite(data) ||
|
|
719
|
-
(base === "Int" &&
|
|
720
|
-
(!Number.isInteger(data) || data < -2147483648 || data > 2147483647)))
|
|
721
|
-
)
|
|
722
|
-
return decodeFailure(`Invalid ${base}`, path);
|
|
723
|
-
return data;
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const inItem = (expr: Expr): boolean => {
|
|
1135
|
+
switch (expr._tag) {
|
|
1136
|
+
case "Item":
|
|
1137
|
+
return true;
|
|
1138
|
+
case "Prop":
|
|
1139
|
+
case "MapValue":
|
|
1140
|
+
case "MapItems":
|
|
1141
|
+
case "Filter":
|
|
1142
|
+
case "FlatMap":
|
|
1143
|
+
return inItem(expr.parent);
|
|
1144
|
+
default:
|
|
1145
|
+
return false;
|
|
724
1146
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
const interpretExpr = (
|
|
1150
|
+
expr: Expr,
|
|
1151
|
+
data: unknown,
|
|
1152
|
+
rootAlias: WeakMap<object, string>,
|
|
1153
|
+
item: unknown,
|
|
1154
|
+
): unknown => {
|
|
1155
|
+
switch (expr._tag) {
|
|
1156
|
+
case "Literal":
|
|
1157
|
+
return expr.value;
|
|
1158
|
+
case "Item":
|
|
1159
|
+
return item;
|
|
1160
|
+
case "Root":
|
|
1161
|
+
case "Prop":
|
|
1162
|
+
return inItem(expr)
|
|
1163
|
+
? extractRel(expr, item)
|
|
1164
|
+
: extractAbs(expr, data, rootAlias);
|
|
1165
|
+
case "MapValue":
|
|
1166
|
+
return expr.mapFn(interpretExpr(expr.parent, data, rootAlias, item));
|
|
1167
|
+
case "Filter": {
|
|
1168
|
+
const list = interpretExpr(expr.parent, data, rootAlias, item);
|
|
1169
|
+
const arr = Array.isArray(list) ? list : [];
|
|
1170
|
+
return arr.filter((row) =>
|
|
1171
|
+
Boolean(interpretExpr(expr.predicate, data, rootAlias, row)),
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
case "MapItems": {
|
|
1175
|
+
const list = interpretExpr(expr.parent, data, rootAlias, item);
|
|
1176
|
+
if (expr.single) {
|
|
1177
|
+
return list == null
|
|
1178
|
+
? null
|
|
1179
|
+
: interpretValueSync(expr.mapped, data, rootAlias, list);
|
|
1180
|
+
}
|
|
1181
|
+
const arr = Array.isArray(list) ? list : [];
|
|
1182
|
+
return arr.map((row) =>
|
|
1183
|
+
interpretValueSync(expr.mapped, data, rootAlias, row),
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
case "FlatMap":
|
|
1187
|
+
throw new GqlError("FlatMap must be interpreted as Effect");
|
|
729
1188
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
? (branches[data.__typename] ?? [])
|
|
741
|
-
: []),
|
|
742
|
-
];
|
|
743
|
-
const result: Record<string, unknown> = {};
|
|
744
|
-
const merged = new Map<string, Position>();
|
|
745
|
-
for (const position of active) {
|
|
746
|
-
if (position.conditional) continue;
|
|
747
|
-
const previous = merged.get(position.key);
|
|
748
|
-
merged.set(
|
|
749
|
-
position.key,
|
|
750
|
-
previous
|
|
751
|
-
? {
|
|
752
|
-
...position,
|
|
753
|
-
children: [...previous.children, ...position.children],
|
|
754
|
-
branches: Object.fromEntries(
|
|
755
|
-
[
|
|
756
|
-
...new Set([
|
|
757
|
-
...Object.keys(previous.branches),
|
|
758
|
-
...Object.keys(position.branches),
|
|
759
|
-
]),
|
|
760
|
-
].map((key) => [
|
|
761
|
-
key,
|
|
762
|
-
[
|
|
763
|
-
...(previous.branches[key] ?? []),
|
|
764
|
-
...(position.branches[key] ?? []),
|
|
765
|
-
],
|
|
766
|
-
]),
|
|
767
|
-
),
|
|
768
|
-
}
|
|
769
|
-
: position,
|
|
770
|
-
);
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
const interpretValueSync = (
|
|
1192
|
+
plan: unknown,
|
|
1193
|
+
data: unknown,
|
|
1194
|
+
rootAlias: WeakMap<object, string>,
|
|
1195
|
+
item: unknown,
|
|
1196
|
+
): unknown => {
|
|
1197
|
+
if (isQuery(plan)) {
|
|
1198
|
+
return interpretExpr((plan as QueryNode).expr, data, rootAlias, item);
|
|
771
1199
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1200
|
+
if (plan !== null && typeof plan === "object" && !Effect.isEffect(plan)) {
|
|
1201
|
+
const out: Record<string, unknown> = {};
|
|
1202
|
+
for (const [key, nested] of Object.entries(
|
|
1203
|
+
plan as Record<string, unknown>,
|
|
1204
|
+
)) {
|
|
1205
|
+
out[key] = interpretValueSync(nested, data, rootAlias, item);
|
|
1206
|
+
}
|
|
1207
|
+
return out;
|
|
1208
|
+
}
|
|
1209
|
+
return plan;
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
const interpretPlan = (
|
|
1213
|
+
plan: unknown,
|
|
1214
|
+
data: unknown,
|
|
1215
|
+
rootAlias: WeakMap<object, string>,
|
|
1216
|
+
): Effect.Effect<unknown, unknown, GqlTransport> =>
|
|
1217
|
+
Effect.gen(function* () {
|
|
1218
|
+
if (isQuery(plan)) {
|
|
1219
|
+
const expr = (plan as QueryNode).expr;
|
|
1220
|
+
if (expr._tag === "FlatMap") {
|
|
1221
|
+
const resolved = interpretExpr(expr.parent, data, rootAlias, undefined);
|
|
1222
|
+
const next = expr.flatMapFn(resolved);
|
|
1223
|
+
if (Effect.isEffect(next)) return yield* next;
|
|
1224
|
+
if (isQuery(next)) return yield* interpretPlan(next, data, rootAlias);
|
|
1225
|
+
return next;
|
|
1226
|
+
}
|
|
1227
|
+
return interpretExpr(expr, data, rootAlias, undefined);
|
|
1228
|
+
}
|
|
1229
|
+
if (Effect.isEffect(plan)) return yield* plan;
|
|
1230
|
+
if (plan !== null && typeof plan === "object" && !Array.isArray(plan)) {
|
|
1231
|
+
const out: Record<string, unknown> = {};
|
|
1232
|
+
for (const [key, nested] of Object.entries(
|
|
1233
|
+
plan as Record<string, unknown>,
|
|
1234
|
+
)) {
|
|
1235
|
+
out[key] = yield* interpretPlan(nested, data, rootAlias);
|
|
1236
|
+
}
|
|
1237
|
+
return out;
|
|
1238
|
+
}
|
|
1239
|
+
return plan;
|
|
1240
|
+
}) as Effect.Effect<unknown, unknown, GqlTransport>;
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Compile every Query in `plan` into one document, POST it, then replace
|
|
1244
|
+
* Query/Effect leaves with plain data (`UnwrapPlan`).
|
|
1245
|
+
*/
|
|
1246
|
+
const evaluatePlan = <Plan>(
|
|
1247
|
+
plan: Plan,
|
|
1248
|
+
): Effect.Effect<UnwrapPlan<Plan>, QueryError<PlanError<Plan>>, GqlTransport> =>
|
|
1249
|
+
Effect.gen(function* () {
|
|
1250
|
+
const forest = emptyForest();
|
|
1251
|
+
visitPlan(plan, forest);
|
|
1252
|
+
let data: unknown = {};
|
|
1253
|
+
let rootAlias: WeakMap<object, string> = new WeakMap();
|
|
1254
|
+
if (forest.roots.children.size > 0) {
|
|
1255
|
+
const compiled = emit(forest);
|
|
1256
|
+
log("document\n" + compiled.document);
|
|
1257
|
+
data = yield* execute(compiled);
|
|
1258
|
+
rootAlias = compiled.rootAlias;
|
|
1259
|
+
}
|
|
1260
|
+
return (yield* interpretPlan(plan, data, rootAlias)) as UnwrapPlan<Plan>;
|
|
1261
|
+
}) as Effect.Effect<
|
|
1262
|
+
UnwrapPlan<Plan>,
|
|
1263
|
+
QueryError<PlanError<Plan>>,
|
|
1264
|
+
GqlTransport
|
|
1265
|
+
>;
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* POST one compiled document and fail with its classified errors. Queries
|
|
1269
|
+
* are retried (bounded) while every failure is retryable; mutations never
|
|
1270
|
+
* are, since an error can follow a side effect that already happened.
|
|
1271
|
+
*/
|
|
1272
|
+
const execute = (
|
|
1273
|
+
compiled: Compiled,
|
|
1274
|
+
): Effect.Effect<unknown, ExecuteError, GqlTransport> => {
|
|
1275
|
+
const once = Effect.gen(function* () {
|
|
1276
|
+
const transport = yield* GqlTransport;
|
|
1277
|
+
const response = yield* transport.execute(compiled);
|
|
1278
|
+
const raw = response.errors ?? [];
|
|
1279
|
+
if (raw.length === 0) return response.data;
|
|
1280
|
+
const issues = raw.map((error) => classify(compiled, error, response));
|
|
1281
|
+
const [first, ...rest] = issues as [GraphQLIssue, ...GraphQLIssue[]];
|
|
1282
|
+
return yield* Effect.fail(
|
|
1283
|
+
rest.every((issue) => issue._tag === first._tag)
|
|
1284
|
+
? first
|
|
1285
|
+
: new GraphQLFailure({
|
|
1286
|
+
errors: [first, ...rest],
|
|
1287
|
+
data: response.data,
|
|
1288
|
+
status: response.status,
|
|
1289
|
+
}),
|
|
1290
|
+
);
|
|
1291
|
+
});
|
|
1292
|
+
return compiled.kind === "query"
|
|
1293
|
+
? once.pipe(
|
|
1294
|
+
Effect.retry({
|
|
1295
|
+
while: (error) => isRetryable(compiled, error),
|
|
1296
|
+
times: 5,
|
|
1297
|
+
schedule: Schedule.exponential("200 millis"),
|
|
1298
|
+
}),
|
|
778
1299
|
)
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
} else
|
|
790
|
-
Object.defineProperty(result, position.key, {
|
|
791
|
-
value: decodeData(
|
|
792
|
-
model,
|
|
793
|
-
position.type,
|
|
794
|
-
data[position.key],
|
|
795
|
-
position.children,
|
|
796
|
-
position.branches,
|
|
797
|
-
[...path, position.key],
|
|
798
|
-
),
|
|
799
|
-
enumerable: true,
|
|
800
|
-
configurable: true,
|
|
801
|
-
writable: true,
|
|
802
|
-
});
|
|
1300
|
+
: once;
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
type ExecuteError = GraphQLIssue | GraphQLFailure | GraphQLTransportError;
|
|
1304
|
+
|
|
1305
|
+
const isRetryable = (compiled: Compiled, error: ExecuteError): boolean => {
|
|
1306
|
+
if (error instanceof GraphQLTransportError) {
|
|
1307
|
+
return (
|
|
1308
|
+
error.status === undefined || error.status === 429 || error.status >= 500
|
|
1309
|
+
);
|
|
803
1310
|
}
|
|
804
|
-
|
|
1311
|
+
const retryable = new Set(
|
|
1312
|
+
[...compiled.rootErrors.values()].flatMap((root) =>
|
|
1313
|
+
root.errors.filter((spec) => spec.retryable).map((spec) => spec.tag),
|
|
1314
|
+
),
|
|
1315
|
+
);
|
|
1316
|
+
return error instanceof GraphQLFailure
|
|
1317
|
+
? error.errors.every((issue) => retryable.has(issue._tag))
|
|
1318
|
+
: retryable.has(error._tag);
|
|
805
1319
|
};
|
|
806
1320
|
|
|
807
|
-
|
|
1321
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
1322
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Pick the most specific matcher among the errors the root at `path[0]`
|
|
1326
|
+
* declares (or the global errors for a path-less error). A tie between
|
|
1327
|
+
* different tags is ambiguous and stays {@link UnknownGraphQLError}.
|
|
1328
|
+
*/
|
|
808
1329
|
const classify = (
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
raw: Record<string, any>,
|
|
1330
|
+
compiled: Compiled,
|
|
1331
|
+
raw: RawGraphQLError,
|
|
812
1332
|
response: GraphQLResponse,
|
|
813
|
-
classes: Readonly<Record<string, ErrorConstructor>>,
|
|
814
1333
|
): GraphQLIssue => {
|
|
815
|
-
const path = Array.isArray(raw.path)
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
?
|
|
822
|
-
:
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
)
|
|
828
|
-
positions.flatMap((p) => [
|
|
829
|
-
...p.children,
|
|
830
|
-
...(object(value) && typeof value.__typename === "string"
|
|
831
|
-
? (p.branches[value.__typename] ?? [])
|
|
832
|
-
: Object.values(p.branches).flat()),
|
|
833
|
-
]);
|
|
834
|
-
let valid = !!path?.length;
|
|
835
|
-
for (const segment of path ?? []) {
|
|
836
|
-
if (typeof segment === "number") {
|
|
837
|
-
currentData = Array.isArray(currentData)
|
|
838
|
-
? currentData[segment]
|
|
839
|
-
: undefined;
|
|
840
|
-
candidates = descendants(parents, currentData);
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
const matches = candidates.filter((p) => p.key === segment);
|
|
844
|
-
if (!matches.length) {
|
|
845
|
-
valid = false;
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
position = {
|
|
849
|
-
...matches[0]!,
|
|
850
|
-
errors: [...new Set(matches.flatMap((p) => p.errors))],
|
|
851
|
-
};
|
|
852
|
-
currentData = object(currentData) ? currentData[segment] : undefined;
|
|
853
|
-
parents = matches;
|
|
854
|
-
candidates = descendants(matches, currentData);
|
|
855
|
-
}
|
|
856
|
-
const extensions = object(raw.extensions) ? raw.extensions : {};
|
|
1334
|
+
const path = Array.isArray(raw.path) ? raw.path : undefined;
|
|
1335
|
+
const root =
|
|
1336
|
+
typeof path?.[0] === "string"
|
|
1337
|
+
? compiled.rootErrors.get(path[0])
|
|
1338
|
+
: undefined;
|
|
1339
|
+
const candidates = root
|
|
1340
|
+
? root.errors
|
|
1341
|
+
: uniqueByTag(
|
|
1342
|
+
[...compiled.rootErrors.values()].flatMap((entry) =>
|
|
1343
|
+
entry.errors.filter((spec) => spec.global),
|
|
1344
|
+
),
|
|
1345
|
+
);
|
|
1346
|
+
const extensions = isRecord(raw.extensions) ? raw.extensions : {};
|
|
857
1347
|
const code =
|
|
858
1348
|
typeof extensions.code === "string"
|
|
859
1349
|
? extensions.code
|
|
860
1350
|
: typeof extensions.errorCode === "string"
|
|
861
1351
|
? extensions.errorCode
|
|
862
1352
|
: undefined;
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
: undefined;
|
|
868
|
-
const props = {
|
|
869
|
-
message: raw.message,
|
|
1353
|
+
const message = typeof raw.message === "string" ? raw.message : "";
|
|
1354
|
+
const retryHeader = response.headers?.["retry-after"];
|
|
1355
|
+
const props: GraphQLIssueProps = {
|
|
1356
|
+
message,
|
|
870
1357
|
code,
|
|
871
1358
|
path,
|
|
872
1359
|
locations: raw.locations,
|
|
@@ -878,23 +1365,22 @@ const classify = (
|
|
|
878
1365
|
? raw.traceId
|
|
879
1366
|
: undefined,
|
|
880
1367
|
status: response.status,
|
|
881
|
-
retryAfter
|
|
1368
|
+
retryAfter:
|
|
1369
|
+
retryHeader && /^\d+(\.\d+)?$/.test(retryHeader)
|
|
1370
|
+
? Number(retryHeader)
|
|
1371
|
+
: undefined,
|
|
882
1372
|
};
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
...(valid ? (position?.errors ?? []) : []),
|
|
886
|
-
];
|
|
887
|
-
const matches = tags.flatMap((tag) =>
|
|
888
|
-
(model.errors[tag]?.matchers ?? [])
|
|
1373
|
+
const matches = candidates.flatMap((spec) =>
|
|
1374
|
+
spec.matchers
|
|
889
1375
|
.filter(
|
|
890
1376
|
(matcher) =>
|
|
891
1377
|
(matcher.code === undefined || matcher.code === code) &&
|
|
892
|
-
(matcher.message === undefined || matcher.message ===
|
|
1378
|
+
(matcher.message === undefined || matcher.message === message) &&
|
|
893
1379
|
(matcher.messageIncludes === undefined ||
|
|
894
|
-
|
|
1380
|
+
message.includes(matcher.messageIncludes)),
|
|
895
1381
|
)
|
|
896
1382
|
.map((matcher) => ({
|
|
897
|
-
|
|
1383
|
+
spec,
|
|
898
1384
|
score:
|
|
899
1385
|
(matcher.code ? 1 : 0) +
|
|
900
1386
|
(matcher.message ? 4 : 0) +
|
|
@@ -903,419 +1389,358 @@ const classify = (
|
|
|
903
1389
|
);
|
|
904
1390
|
matches.sort((a, b) => b.score - a.score);
|
|
905
1391
|
const best = matches[0];
|
|
906
|
-
|
|
907
|
-
best &&
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1392
|
+
if (
|
|
1393
|
+
best &&
|
|
1394
|
+
!matches.some(
|
|
1395
|
+
(match) => match.score === best.score && match.spec.tag !== best.spec.tag,
|
|
1396
|
+
)
|
|
1397
|
+
) {
|
|
1398
|
+
return best.spec.make(props);
|
|
1399
|
+
}
|
|
911
1400
|
return new UnknownGraphQLError({
|
|
912
1401
|
...props,
|
|
913
|
-
coordinate:
|
|
1402
|
+
coordinate: root?.field,
|
|
914
1403
|
});
|
|
915
1404
|
};
|
|
916
1405
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1406
|
+
const uniqueByTag = (specs: ReadonlyArray<ErrorSpec>): ErrorSpec[] => [
|
|
1407
|
+
...new Map(specs.map((spec) => [spec.tag, spec])).values(),
|
|
1408
|
+
];
|
|
1409
|
+
|
|
1410
|
+
/** `Query.of` / pure. */
|
|
1411
|
+
export const ofQuery = <Value>(value: Value): Query<Value> =>
|
|
1412
|
+
make({ _tag: "Literal", value, type: { tag: "scalar" } });
|
|
1413
|
+
|
|
1414
|
+
const filterImpl = (
|
|
1415
|
+
source: QueryNode,
|
|
1416
|
+
predicate: (item: QueryNode) => QueryNode,
|
|
1417
|
+
): Query<unknown> => {
|
|
1418
|
+
const proto = itemQuery(source);
|
|
1419
|
+
const predicateQuery = predicate(proto);
|
|
1420
|
+
log("filter pred", printExpr(predicateQuery.expr));
|
|
1421
|
+
return make({
|
|
1422
|
+
_tag: "Filter",
|
|
1423
|
+
parent: source.expr,
|
|
1424
|
+
predicate: predicateQuery.expr,
|
|
1425
|
+
type: source.expr.type,
|
|
1426
|
+
});
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
export function filterQuery<Item, Error = never>(
|
|
1430
|
+
predicate: (item: Query<Item>) => QueryNode<boolean, any>,
|
|
1431
|
+
): (
|
|
1432
|
+
source: QueryNode<readonly Item[] | null, Error>,
|
|
1433
|
+
) => Query<readonly Item[], Error>;
|
|
1434
|
+
export function filterQuery<Item, Error = never>(
|
|
1435
|
+
source: QueryNode<readonly Item[] | null, Error>,
|
|
1436
|
+
predicate: (item: Query<Item>) => QueryNode<boolean, any>,
|
|
1437
|
+
): Query<readonly Item[], Error>;
|
|
1438
|
+
export function filterQuery(sourceOrPredicate: any, predicate?: any): any {
|
|
1439
|
+
if (predicate === undefined) {
|
|
1440
|
+
return (source: QueryNode) => filterImpl(source, sourceOrPredicate);
|
|
1441
|
+
}
|
|
1442
|
+
return filterImpl(sourceOrPredicate, predicate);
|
|
922
1443
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
)
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* What `Query.map`'s callback receives: a Query per item for lists, a Query
|
|
1447
|
+
* for an object (so its fields get selected), the plain value for scalars.
|
|
1448
|
+
*/
|
|
1449
|
+
type MapInput<Value> = [NonNullable<Value>] extends [ReadonlyArray<infer Item>]
|
|
1450
|
+
? Query<Item>
|
|
1451
|
+
: [NonNullable<Value>] extends [object]
|
|
1452
|
+
? Query<NonNullable<Value>>
|
|
1453
|
+
: Value;
|
|
1454
|
+
|
|
1455
|
+
/** An object mapped through a `null` value stays `null`. */
|
|
1456
|
+
type MapOutput<Value, Mapped> = [NonNullable<Value>] extends [
|
|
1457
|
+
ReadonlyArray<unknown>,
|
|
1458
|
+
]
|
|
1459
|
+
? ReadonlyArray<UnwrapPlan<Mapped>>
|
|
1460
|
+
: [NonNullable<Value>] extends [object]
|
|
1461
|
+
? UnwrapPlan<Mapped> | Nullish<Value>
|
|
1462
|
+
: Mapped;
|
|
1463
|
+
|
|
1464
|
+
const mapImpl = (
|
|
1465
|
+
source: QueryNode,
|
|
1466
|
+
mapFn: (value: any) => unknown,
|
|
1467
|
+
): Query<unknown> => {
|
|
1468
|
+
if (source.expr.type.tag === "object") {
|
|
1469
|
+
const mapped = mapFn(
|
|
1470
|
+
new QueryNode({
|
|
1471
|
+
_tag: "Item",
|
|
1472
|
+
list: source.expr,
|
|
1473
|
+
type: source.expr.type,
|
|
1474
|
+
}),
|
|
1475
|
+
);
|
|
1476
|
+
log("map object", printExpr(source.expr));
|
|
1477
|
+
return make({
|
|
1478
|
+
_tag: "MapItems",
|
|
1479
|
+
parent: source.expr,
|
|
1480
|
+
mapped,
|
|
1481
|
+
single: true,
|
|
1482
|
+
type: { tag: "scalar" },
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
if (source.expr.type.tag === "list") {
|
|
1486
|
+
const proto = itemQuery(source);
|
|
1487
|
+
const mapped = mapFn(proto);
|
|
1488
|
+
log("map items", printExpr(source.expr));
|
|
1489
|
+
return make({
|
|
1490
|
+
_tag: "MapItems",
|
|
1491
|
+
parent: source.expr,
|
|
1492
|
+
mapped,
|
|
1493
|
+
type: source.expr.type,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
log("map value", printExpr(source.expr));
|
|
1497
|
+
return make({
|
|
1498
|
+
_tag: "MapValue",
|
|
1499
|
+
parent: source.expr,
|
|
1500
|
+
mapFn,
|
|
1501
|
+
type: { tag: "scalar" },
|
|
1502
|
+
});
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
export function mapQuery<Value, Mapped, Error = never>(
|
|
1506
|
+
mapFn: (value: NoInfer<MapInput<Value>>) => Mapped,
|
|
1507
|
+
): (source: QueryNode<Value, Error>) => Query<MapOutput<Value, Mapped>, Error>;
|
|
1508
|
+
export function mapQuery<Value, Mapped, Error = never>(
|
|
1509
|
+
source: QueryNode<Value, Error>,
|
|
1510
|
+
mapFn: (value: NoInfer<MapInput<Value>>) => Mapped,
|
|
1511
|
+
): Query<MapOutput<Value, Mapped>, Error>;
|
|
1512
|
+
export function mapQuery(sourceOrMapFn: any, mapFn?: any): any {
|
|
1513
|
+
if (mapFn === undefined) {
|
|
1514
|
+
return (source: QueryNode) => mapImpl(source, sourceOrMapFn);
|
|
1515
|
+
}
|
|
1516
|
+
return mapImpl(sourceOrMapFn, mapFn);
|
|
952
1517
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
(
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1518
|
+
|
|
1519
|
+
const flatMapImpl = (
|
|
1520
|
+
source: QueryNode,
|
|
1521
|
+
flatMapFn: (value: unknown) => unknown,
|
|
1522
|
+
): Query<unknown> => {
|
|
1523
|
+
log("flatMap", printExpr(source.expr));
|
|
1524
|
+
return make({
|
|
1525
|
+
_tag: "FlatMap",
|
|
1526
|
+
parent: source.expr,
|
|
1527
|
+
flatMapFn,
|
|
1528
|
+
type: { tag: "scalar" },
|
|
1529
|
+
});
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
export function flatMapQuery<
|
|
1533
|
+
Value,
|
|
1534
|
+
Result,
|
|
1535
|
+
Error,
|
|
1536
|
+
Requirements,
|
|
1537
|
+
SourceError = never,
|
|
1538
|
+
>(
|
|
1539
|
+
flatMapFn: (
|
|
1540
|
+
value: Value,
|
|
1541
|
+
) => Effect.Effect<Result, Error, Requirements> | QueryNode<Result, Error>,
|
|
1542
|
+
): (
|
|
1543
|
+
source: QueryNode<Value, SourceError>,
|
|
1544
|
+
) => Query<Result, SourceError | Error>;
|
|
1545
|
+
export function flatMapQuery<
|
|
1546
|
+
Value,
|
|
1547
|
+
Result,
|
|
1548
|
+
Error,
|
|
1549
|
+
Requirements,
|
|
1550
|
+
SourceError = never,
|
|
1551
|
+
>(
|
|
1552
|
+
source: QueryNode<Value, SourceError>,
|
|
1553
|
+
flatMapFn: (
|
|
1554
|
+
value: Value,
|
|
1555
|
+
) => Effect.Effect<Result, Error, Requirements> | QueryNode<Result, Error>,
|
|
1556
|
+
): Query<Result, SourceError | Error>;
|
|
1557
|
+
export function flatMapQuery(sourceOrFlatMapFn: any, flatMapFn?: any): any {
|
|
1558
|
+
if (flatMapFn === undefined) {
|
|
1559
|
+
return (source: QueryNode) => flatMapImpl(source, sourceOrFlatMapFn);
|
|
1560
|
+
}
|
|
1561
|
+
return flatMapImpl(sourceOrFlatMapFn, flatMapFn);
|
|
966
1562
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
):
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
)
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Builder → Effect. The inner function is *not* a generator; it returns a
|
|
1566
|
+
* plan of Query/Effect values. Query.fn visits the plan, POSTs one document,
|
|
1567
|
+
* then interprets Map/Filter/FlatMap/Of.
|
|
1568
|
+
*/
|
|
1569
|
+
export const queryFn =
|
|
1570
|
+
<Arguments extends Array<unknown>, Plan>(
|
|
1571
|
+
build: (...args: Arguments) => Plan,
|
|
1572
|
+
): ((
|
|
1573
|
+
...args: Arguments
|
|
1574
|
+
) => Effect.Effect<
|
|
1575
|
+
UnwrapPlan<Plan>,
|
|
1576
|
+
QueryError<PlanError<Plan>>,
|
|
1577
|
+
GqlTransport
|
|
1578
|
+
>) =>
|
|
1579
|
+
(...args) => {
|
|
1580
|
+
const plan = build(...args);
|
|
1581
|
+
return evaluatePlan(plan);
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
// ── Pagination ──────────────────────────────────────────────────────────────
|
|
1585
|
+
//
|
|
1586
|
+
// A connection lens hides `edges`/`pageInfo`. `Query.pages` re-evaluates the
|
|
1587
|
+
// query once per page: it sets the connection's `after` argument, paints
|
|
1588
|
+
// `pageInfo { hasNextPage endCursor }` next to `edges`, and stops when
|
|
1589
|
+
// `hasNextPage` is false.
|
|
1590
|
+
|
|
1591
|
+
/** The connection a paged query reads, below any list-level Map/Filter. */
|
|
1592
|
+
const connectionOf = (expr: Expr): RootExpr | PropExpr => {
|
|
1593
|
+
const base = graphqlBase(expr);
|
|
1594
|
+
const isConnection =
|
|
1595
|
+
(base._tag === "Root" && base.connection === true) ||
|
|
1596
|
+
(base._tag === "Prop" && base.field.kind === "connection");
|
|
1597
|
+
if (!isConnection || inItem(base)) {
|
|
1598
|
+
throw new GqlError(
|
|
1599
|
+
`Query.pages needs a Relay connection, got ${printExpr(base)}`,
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
const argTypes = base._tag === "Root" ? base.argTypes : base.field.argTypes;
|
|
1603
|
+
if (!argTypes || !("after" in argTypes)) {
|
|
1604
|
+
throw new GqlError(`${printExpr(base)} takes no "after" cursor`);
|
|
1605
|
+
}
|
|
1606
|
+
return base as RootExpr | PropExpr;
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
/** Rebuild `expr` with the connection's `after` argument set. */
|
|
1610
|
+
const withCursor = (
|
|
1611
|
+
expr: Expr,
|
|
1612
|
+
connection: Expr,
|
|
1613
|
+
after: string | undefined,
|
|
1614
|
+
): Expr => {
|
|
1615
|
+
if (expr === connection) {
|
|
1616
|
+
const current = expr as RootExpr | PropExpr;
|
|
1617
|
+
return { ...current, args: { ...current.args, after } } as Expr;
|
|
1618
|
+
}
|
|
1619
|
+
switch (expr._tag) {
|
|
1620
|
+
case "MapItems":
|
|
1621
|
+
case "Filter":
|
|
1622
|
+
case "MapValue":
|
|
1623
|
+
case "FlatMap":
|
|
1624
|
+
return { ...expr, parent: withCursor(expr.parent, connection, after) };
|
|
1625
|
+
default:
|
|
1626
|
+
throw new GqlError(`withCursor: ${expr._tag}`);
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
const readPath = (data: unknown, path: Path): unknown => {
|
|
1631
|
+
let current = data;
|
|
1632
|
+
for (const segment of path) {
|
|
1633
|
+
if (current == null) return undefined;
|
|
1634
|
+
if (Array.isArray(current)) {
|
|
1635
|
+
throw new GqlError("Query.pages cannot page a connection inside a list");
|
|
1636
|
+
}
|
|
1637
|
+
current = (current as Record<string, unknown>)[segment];
|
|
1638
|
+
}
|
|
1639
|
+
return current;
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
interface PageState {
|
|
1643
|
+
readonly after: string | undefined;
|
|
1644
|
+
readonly seen: ReadonlySet<string>;
|
|
1011
1645
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1646
|
+
|
|
1647
|
+
const fetchPage = <Item>(
|
|
1648
|
+
query: QueryNode<ReadonlyArray<Item> | null, unknown>,
|
|
1649
|
+
connection: RootExpr | PropExpr,
|
|
1650
|
+
state: PageState,
|
|
1651
|
+
) =>
|
|
1652
|
+
Effect.gen(function* () {
|
|
1653
|
+
const pageExpr = withCursor(query.expr, connection, state.after);
|
|
1654
|
+
const forest = emptyForest();
|
|
1655
|
+
visitExpr(forest, pageExpr);
|
|
1656
|
+
const nodePath = paintAbs(
|
|
1657
|
+
forest,
|
|
1658
|
+
withCursor(connection, connection, state.after),
|
|
1659
|
+
);
|
|
1660
|
+
const connectionPath = nodePath.slice(0, -2);
|
|
1661
|
+
const pageInfo = ensureChild(
|
|
1662
|
+
forest,
|
|
1663
|
+
selAt(forest.roots, connectionPath),
|
|
1664
|
+
"pageInfo",
|
|
1665
|
+
{},
|
|
1666
|
+
);
|
|
1667
|
+
ensureChild(forest, pageInfo, "hasNextPage", { isScalar: true });
|
|
1668
|
+
ensureChild(forest, pageInfo, "endCursor", { isScalar: true });
|
|
1669
|
+
const compiled = emit(forest);
|
|
1670
|
+
log("page\n" + compiled.document);
|
|
1671
|
+
const data = yield* execute(compiled);
|
|
1672
|
+
const items = (yield* interpretPlan(
|
|
1673
|
+
make(pageExpr),
|
|
1674
|
+
data,
|
|
1675
|
+
compiled.rootAlias,
|
|
1676
|
+
)) as ReadonlyArray<Item> | null | undefined;
|
|
1677
|
+
const info = readPath(data, [...connectionPath, "pageInfo"]) as
|
|
1678
|
+
| { hasNextPage?: boolean; endCursor?: string | null }
|
|
1679
|
+
| null
|
|
1680
|
+
| undefined;
|
|
1681
|
+
const cursor = info?.endCursor ?? null;
|
|
1682
|
+
if (info?.hasNextPage !== true) {
|
|
1683
|
+
return [items ?? [], Option.none<PageState>()] as const;
|
|
1684
|
+
}
|
|
1685
|
+
if (cursor === null || cursor === state.after || state.seen.has(cursor)) {
|
|
1686
|
+
return yield* new GraphQLPaginationError({
|
|
1687
|
+
message: `${printExpr(connection)} returned a non-advancing cursor`,
|
|
1688
|
+
cursor,
|
|
1048
1689
|
});
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
"GraphQL response has neither data nor errors",
|
|
1083
|
-
[],
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
if (response.status >= 400 && !rawErrors.length)
|
|
1087
|
-
throw new GraphQLTransportError({
|
|
1088
|
-
message: `HTTP ${response.status} without GraphQL errors`,
|
|
1089
|
-
status: response.status,
|
|
1090
|
-
});
|
|
1091
|
-
const errors = rawErrors.map((raw: Record<string, any>) =>
|
|
1092
|
-
classify(model, compiled, raw, response, classes),
|
|
1093
|
-
);
|
|
1094
|
-
let data = body.data;
|
|
1095
|
-
if (data !== undefined && data !== null)
|
|
1096
|
-
data = decodeData(
|
|
1097
|
-
model,
|
|
1098
|
-
compiled.rootType + "!",
|
|
1099
|
-
data,
|
|
1100
|
-
compiled.positions,
|
|
1101
|
-
{},
|
|
1102
|
-
[],
|
|
1103
|
-
);
|
|
1104
|
-
if ((data === undefined || data === null) && !errors.length)
|
|
1105
|
-
return decodeFailure("GraphQL success has no data", []);
|
|
1106
|
-
return {
|
|
1107
|
-
data,
|
|
1108
|
-
errors,
|
|
1109
|
-
status: response.status,
|
|
1110
|
-
extensions: body.extensions,
|
|
1111
|
-
};
|
|
1690
|
+
}
|
|
1691
|
+
return [
|
|
1692
|
+
items ?? [],
|
|
1693
|
+
Option.some({ after: cursor, seen: new Set([...state.seen, cursor]) }),
|
|
1694
|
+
] as const;
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Every page of a Relay connection, one request per page. `query` is a
|
|
1699
|
+
* connection lens (root or nested under objects), optionally mapped or
|
|
1700
|
+
* filtered: `Query.pages(Railway.project({ id }).services({ first: 50 }))`.
|
|
1701
|
+
* Starts from the query's own `after` argument, if any.
|
|
1702
|
+
*/
|
|
1703
|
+
export const pagesQuery = <Item, Error>(
|
|
1704
|
+
query: QueryNode<ReadonlyArray<Item> | null, Error>,
|
|
1705
|
+
): Stream.Stream<
|
|
1706
|
+
ReadonlyArray<Item>,
|
|
1707
|
+
QueryError<Error> | GraphQLPaginationError,
|
|
1708
|
+
GqlTransport
|
|
1709
|
+
> =>
|
|
1710
|
+
Stream.unwrap(
|
|
1711
|
+
Effect.sync(() => {
|
|
1712
|
+
const connection = connectionOf(query.expr);
|
|
1713
|
+
const after = connection.args?.after;
|
|
1714
|
+
return Stream.paginate<
|
|
1715
|
+
PageState,
|
|
1716
|
+
ReadonlyArray<Item>,
|
|
1717
|
+
QueryError<Error> | GraphQLPaginationError,
|
|
1718
|
+
GqlTransport
|
|
1719
|
+
>(
|
|
1720
|
+
{
|
|
1721
|
+
after: typeof after === "string" ? after : undefined,
|
|
1722
|
+
seen: new Set(),
|
|
1112
1723
|
},
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
error instanceof GraphQLFailure
|
|
1125
|
-
? error.errors.every(
|
|
1126
|
-
(issue) => model.errors[issue._tag]?.retryable === true,
|
|
1127
|
-
)
|
|
1128
|
-
: error instanceof GraphQLTransportError &&
|
|
1129
|
-
(error.status === undefined ||
|
|
1130
|
-
error.status === 429 ||
|
|
1131
|
-
error.status >= 500);
|
|
1132
|
-
const execute = (
|
|
1133
|
-
kind: OperationKind,
|
|
1134
|
-
selection: unknown,
|
|
1135
|
-
): Effect.Effect<any, ClientError | GraphQLFailure, R> => {
|
|
1136
|
-
const effect = executeReport(kind, selection).pipe(
|
|
1137
|
-
Effect.flatMap((report) =>
|
|
1138
|
-
report.errors.length
|
|
1139
|
-
? Effect.fail(
|
|
1140
|
-
new GraphQLFailure({
|
|
1141
|
-
errors: report.errors as [GraphQLIssue, ...GraphQLIssue[]],
|
|
1142
|
-
data: report.data,
|
|
1143
|
-
status: report.status,
|
|
1144
|
-
}),
|
|
1145
|
-
)
|
|
1146
|
-
: Effect.succeed(report.data),
|
|
1147
|
-
),
|
|
1148
|
-
);
|
|
1149
|
-
return kind === "query"
|
|
1150
|
-
? effect.pipe(
|
|
1151
|
-
Effect.retry({
|
|
1152
|
-
while: retryable,
|
|
1153
|
-
times: 5,
|
|
1154
|
-
schedule: Schedule.exponential("200 millis"),
|
|
1155
|
-
}),
|
|
1156
|
-
)
|
|
1157
|
-
: effect;
|
|
1158
|
-
};
|
|
1159
|
-
const operation = <
|
|
1160
|
-
K extends OperationKind,
|
|
1161
|
-
N extends keyof Fields<C, Root<C, K>> & string,
|
|
1162
|
-
>(
|
|
1163
|
-
kind: K,
|
|
1164
|
-
name: N,
|
|
1165
|
-
): Operation<C, K, N, R> => {
|
|
1166
|
-
const field =
|
|
1167
|
-
model.types[kind === "query" ? model.queryType : model.mutationType!]
|
|
1168
|
-
?.fields?.[name];
|
|
1169
|
-
const call = (args: unknown, selection?: unknown) =>
|
|
1170
|
-
execute(kind, {
|
|
1171
|
-
[name]: { where: args, select: selection ?? true },
|
|
1172
|
-
}).pipe(Effect.map((data) => data[name]));
|
|
1173
|
-
const pages = (args: any, selection: any) =>
|
|
1174
|
-
Stream.unwrap(
|
|
1175
|
-
Effect.try({
|
|
1176
|
-
try: () => {
|
|
1177
|
-
const connection = field && model.types[named(field.type)];
|
|
1178
|
-
if (
|
|
1179
|
-
kind !== "query" ||
|
|
1180
|
-
!field?.args.after ||
|
|
1181
|
-
!connection?.fields?.edges ||
|
|
1182
|
-
!connection.fields.pageInfo
|
|
1183
|
-
)
|
|
1184
|
-
return Stream.fromEffect(
|
|
1185
|
-
Effect.fail(
|
|
1186
|
-
new GraphQLRequestError({
|
|
1187
|
-
message: `${name} is not a paginated query connection`,
|
|
1188
|
-
}),
|
|
1189
|
-
),
|
|
1190
|
-
);
|
|
1191
|
-
// Cursor fields are internal additions; the caller's projection remains exact.
|
|
1192
|
-
const projection = compile(model, kind, {
|
|
1193
|
-
[name]: { where: args, select: selection },
|
|
1194
|
-
}).positions[0]!;
|
|
1195
|
-
let cursorKey = "pageInfo";
|
|
1196
|
-
let complete: Record<string, unknown>;
|
|
1197
|
-
if (
|
|
1198
|
-
selection.__alias &&
|
|
1199
|
-
Object.hasOwn(selection.__alias, "pageInfo")
|
|
1200
|
-
) {
|
|
1201
|
-
cursorKey = "_distilledPageInfo";
|
|
1202
|
-
while (
|
|
1203
|
-
Object.hasOwn(selection, cursorKey) ||
|
|
1204
|
-
Object.hasOwn(selection.__alias, cursorKey)
|
|
1205
|
-
)
|
|
1206
|
-
cursorKey += "_";
|
|
1207
|
-
complete = {
|
|
1208
|
-
...selection,
|
|
1209
|
-
__alias: {
|
|
1210
|
-
...selection.__alias,
|
|
1211
|
-
[cursorKey]: {
|
|
1212
|
-
pageInfo: { endCursor: true, hasNextPage: true },
|
|
1213
|
-
},
|
|
1214
|
-
},
|
|
1215
|
-
};
|
|
1216
|
-
} else {
|
|
1217
|
-
const {
|
|
1218
|
-
$include: _include,
|
|
1219
|
-
$skip: _skip,
|
|
1220
|
-
...pageFields
|
|
1221
|
-
} = selection.pageInfo?.select ?? selection.pageInfo ?? {};
|
|
1222
|
-
complete = {
|
|
1223
|
-
...selection,
|
|
1224
|
-
pageInfo: { ...pageFields, endCursor: true, hasNextPage: true },
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
|
-
return Stream.paginate(
|
|
1228
|
-
{
|
|
1229
|
-
after: args.after as string | undefined,
|
|
1230
|
-
seen: new Set<string>(),
|
|
1231
|
-
},
|
|
1232
|
-
(state) =>
|
|
1233
|
-
call({ ...args, after: state.after }, complete).pipe(
|
|
1234
|
-
Effect.flatMap((page) => {
|
|
1235
|
-
const cursor = page?.[cursorKey]?.endCursor;
|
|
1236
|
-
const more = page?.[cursorKey]?.hasNextPage;
|
|
1237
|
-
if (
|
|
1238
|
-
more &&
|
|
1239
|
-
(typeof cursor !== "string" ||
|
|
1240
|
-
cursor === state.after ||
|
|
1241
|
-
state.seen.has(cursor))
|
|
1242
|
-
)
|
|
1243
|
-
return Effect.fail(
|
|
1244
|
-
new GraphQLDecodeError({
|
|
1245
|
-
message: `${name} returned a non-advancing pagination cursor`,
|
|
1246
|
-
}),
|
|
1247
|
-
);
|
|
1248
|
-
const seen = new Set(state.seen);
|
|
1249
|
-
if (typeof cursor === "string") seen.add(cursor);
|
|
1250
|
-
const projected: any = decodeData(
|
|
1251
|
-
model,
|
|
1252
|
-
field.type,
|
|
1253
|
-
page,
|
|
1254
|
-
projection.children,
|
|
1255
|
-
projection.branches,
|
|
1256
|
-
[],
|
|
1257
|
-
);
|
|
1258
|
-
return Effect.succeed([
|
|
1259
|
-
[projected],
|
|
1260
|
-
more
|
|
1261
|
-
? Option.some({ after: cursor, seen })
|
|
1262
|
-
: Option.none(),
|
|
1263
|
-
] as const);
|
|
1264
|
-
}),
|
|
1265
|
-
),
|
|
1266
|
-
);
|
|
1267
|
-
},
|
|
1268
|
-
catch: (cause) =>
|
|
1269
|
-
cause instanceof GraphQLRequestError
|
|
1270
|
-
? cause
|
|
1271
|
-
: new GraphQLRequestError({ message: String(cause) }),
|
|
1272
|
-
}),
|
|
1724
|
+
(state) =>
|
|
1725
|
+
fetchPage(query, connection, state).pipe(
|
|
1726
|
+
Effect.map(([items, next]) => [[items], next] as const),
|
|
1727
|
+
) as Effect.Effect<
|
|
1728
|
+
readonly [
|
|
1729
|
+
ReadonlyArray<ReadonlyArray<Item>>,
|
|
1730
|
+
Option.Option<PageState>,
|
|
1731
|
+
],
|
|
1732
|
+
QueryError<Error> | GraphQLPaginationError,
|
|
1733
|
+
GqlTransport
|
|
1734
|
+
>,
|
|
1273
1735
|
);
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
),
|
|
1286
|
-
}) as unknown as Operation<C, K, N, R>;
|
|
1287
|
-
};
|
|
1288
|
-
return {
|
|
1289
|
-
query: <const Q extends Selection<C, C["query"]>>(
|
|
1290
|
-
selection: Q,
|
|
1291
|
-
): Effect.Effect<
|
|
1292
|
-
Result<C, `${C["query"]}!`, Q>,
|
|
1293
|
-
Failure<C, C["query"], Q>,
|
|
1294
|
-
R
|
|
1295
|
-
> => execute("query", selection) as any,
|
|
1296
|
-
mutation: <const Q extends Selection<C, C["mutation"]>>(
|
|
1297
|
-
selection: Q,
|
|
1298
|
-
): Effect.Effect<
|
|
1299
|
-
Result<C, `${C["mutation"]}!`, Q>,
|
|
1300
|
-
Failure<C, C["mutation"], Q>,
|
|
1301
|
-
R
|
|
1302
|
-
> => execute("mutation", selection) as any,
|
|
1303
|
-
report: {
|
|
1304
|
-
query: <const Q extends Selection<C, C["query"]>>(
|
|
1305
|
-
selection: Q,
|
|
1306
|
-
): Effect.Effect<
|
|
1307
|
-
Report<Result<C, `${C["query"]}!`, Q>, Errors<C, C["query"], Q>>,
|
|
1308
|
-
ClientError,
|
|
1309
|
-
R
|
|
1310
|
-
> => executeReport("query", selection) as any,
|
|
1311
|
-
mutation: <const Q extends Selection<C, C["mutation"]>>(
|
|
1312
|
-
selection: Q,
|
|
1313
|
-
): Effect.Effect<
|
|
1314
|
-
Report<Result<C, `${C["mutation"]}!`, Q>, Errors<C, C["mutation"], Q>>,
|
|
1315
|
-
ClientError,
|
|
1316
|
-
R
|
|
1317
|
-
> => executeReport("mutation", selection) as any,
|
|
1318
|
-
},
|
|
1319
|
-
operation,
|
|
1320
|
-
};
|
|
1321
|
-
};
|
|
1736
|
+
}),
|
|
1737
|
+
);
|
|
1738
|
+
|
|
1739
|
+
/** Every node of a Relay connection, across all pages. */
|
|
1740
|
+
export const itemsQuery = <Item, Error>(
|
|
1741
|
+
query: QueryNode<ReadonlyArray<Item> | null, Error>,
|
|
1742
|
+
): Stream.Stream<
|
|
1743
|
+
Item,
|
|
1744
|
+
QueryError<Error> | GraphQLPaginationError,
|
|
1745
|
+
GqlTransport
|
|
1746
|
+
> => pagesQuery(query).pipe(Stream.flatMap(Stream.fromIterable));
|