@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/lib/graphql.test.js
CHANGED
|
@@ -1,780 +1,151 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return { body, status, headers: {} };
|
|
13
|
-
}), errorClasses);
|
|
14
|
-
return { client, requests };
|
|
15
|
-
};
|
|
16
|
-
const failure = async (effect) => {
|
|
17
|
-
const result = await Effect.runPromise(Effect.result(effect));
|
|
18
|
-
if (Result.isSuccess(result))
|
|
19
|
-
throw new Error("Expected a typed failure");
|
|
20
|
-
return result.failure;
|
|
21
|
-
};
|
|
22
|
-
const projectIdentity = {
|
|
23
|
-
project: { where: { id: "p1" }, select: { id: true } },
|
|
24
|
-
};
|
|
25
|
-
const servicesSelection = {
|
|
26
|
-
project: {
|
|
27
|
-
where: { id: "p1" },
|
|
28
|
-
select: {
|
|
29
|
-
id: true,
|
|
30
|
-
services: { select: { edges: { node: { id: true } } } },
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
describe("GraphQL selection compilation", () => {
|
|
35
|
-
test("only requests chosen fields and serializes arguments as variables", () => {
|
|
36
|
-
const compiled = G.compile(fixtureModel, "query", {
|
|
37
|
-
project: { where: { id: adversarialArgument }, select: { id: true } },
|
|
38
|
-
});
|
|
39
|
-
expect(compiled.query).toMatch(/project\s*\(\s*id\s*:\s*\$/);
|
|
40
|
-
expect(compiled.query).toMatch(/\{\s*id\s*\}/);
|
|
41
|
-
expect(compiled.query).not.toContain("services");
|
|
42
|
-
expect(compiled.query).not.toContain(adversarialArgument);
|
|
43
|
-
expect(Object.values(compiled.variables)).toEqual([adversarialArgument]);
|
|
44
|
-
expect(JSON.parse(JSON.stringify(compiled.variables))).toEqual(compiled.variables);
|
|
45
|
-
});
|
|
46
|
-
test("nested arguments keep null, omit undefined, and let server defaults apply", () => {
|
|
47
|
-
const compiled = G.compile(fixtureModel, "query", {
|
|
48
|
-
project: {
|
|
49
|
-
where: { id: "p1" },
|
|
50
|
-
select: {
|
|
51
|
-
services: {
|
|
52
|
-
where: { first: undefined, after: null },
|
|
53
|
-
select: { edges: { node: { id: true } } },
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
expect(compiled.query).not.toContain("first:");
|
|
59
|
-
expect(compiled.query).toMatch(/services\s*\(\s*after\s*:\s*\$/);
|
|
60
|
-
expect(Object.values(compiled.variables)).toEqual(["p1", null]);
|
|
61
|
-
});
|
|
62
|
-
test("input objects preserve explicit null and omit optional members", () => {
|
|
63
|
-
const compiled = G.compile(fixtureModel, "query", {
|
|
64
|
-
projects: {
|
|
65
|
-
where: {
|
|
66
|
-
filter: {
|
|
67
|
-
name: null,
|
|
68
|
-
statuses: undefined,
|
|
69
|
-
metadata: { text: adversarialArgument, absent: undefined },
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
select: { id: true },
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
expect(JSON.parse(JSON.stringify(Object.values(compiled.variables)[0]))).toEqual({ name: null, metadata: { text: adversarialArgument } });
|
|
76
|
-
expect(compiled.query).toContain("ProjectFilter");
|
|
77
|
-
});
|
|
78
|
-
test("aliases give repeated roots independent variables", () => {
|
|
79
|
-
const compiled = G.compile(fixtureModel, "query", {
|
|
80
|
-
__alias: {
|
|
81
|
-
production: { project: { where: { id: "p1" }, select: { id: true } } },
|
|
82
|
-
staging: { project: { where: { id: "p2" }, select: { name: true } } },
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
expect(compiled.query).toMatch(/production\s*:\s*project/);
|
|
86
|
-
expect(compiled.query).toMatch(/staging\s*:\s*project/);
|
|
87
|
-
expect(Object.values(compiled.variables)).toEqual(["p1", "p2"]);
|
|
88
|
-
});
|
|
89
|
-
test("supports finite selections through recursive schema relationships", () => {
|
|
90
|
-
const compiled = G.compile(fixtureModel, "query", {
|
|
91
|
-
project: {
|
|
92
|
-
where: { id: "p1" },
|
|
93
|
-
select: { parent: { parent: { parent: { name: true } } } },
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
expect(compiled.query.match(/parent/g)).toHaveLength(3);
|
|
97
|
-
expect(compiled.query).not.toContain("services");
|
|
98
|
-
});
|
|
99
|
-
test("rejects invalid selections before transport execution", () => {
|
|
100
|
-
expect(() => G.compile(fixtureModel, "query", {
|
|
101
|
-
project: { where: { id: "p1" }, select: { unknownField: true } },
|
|
102
|
-
})).toThrow();
|
|
103
|
-
expect(() => G.compile(fixtureModel, "query", {
|
|
104
|
-
project: { where: {}, select: { id: true } },
|
|
105
|
-
})).toThrow();
|
|
106
|
-
expect(() => G.compile(fixtureModel, "query", {
|
|
107
|
-
project: { where: { id: "p1" }, select: {} },
|
|
108
|
-
})).toThrow();
|
|
109
|
-
expect(() => G.compile(fixtureModel, "query", { "bad alias": true })).toThrow();
|
|
110
|
-
});
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import { GqlTransport, connectionField, listField, objectField, root, rootConnection, scalarField, } from "./graphql.js";
|
|
5
|
+
import { Query } from "./query.js";
|
|
6
|
+
const User = { name: "User", fields: {} };
|
|
7
|
+
const Project = { name: "Project", fields: {} };
|
|
8
|
+
Object.assign(User.fields, {
|
|
9
|
+
email: scalarField("email"),
|
|
10
|
+
name: scalarField("name"),
|
|
11
|
+
projects: listField("projects", Project, { first: "Int" }),
|
|
111
12
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
where: { id: "p1", name: "Updated" },
|
|
123
|
-
select: invalidProjection,
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
expect((await failure(client.query(invalidRoot)))._tag).toBe("GraphQLRequestError");
|
|
127
|
-
expect((await failure(client.query(invalidNested)))._tag).toBe("GraphQLRequestError");
|
|
128
|
-
expect((await failure(client.operation("query", "project")({ id: "p1" }, invalidProjection)))._tag).toBe("GraphQLRequestError");
|
|
129
|
-
expect((await failure(client.mutation(invalidMutation)))._tag).toBe("GraphQLRequestError");
|
|
130
|
-
expect(requests).toHaveLength(0);
|
|
131
|
-
});
|
|
132
|
-
test("sends multiple roots in one HTTP request", async () => {
|
|
133
|
-
const { client, requests } = harness({
|
|
134
|
-
data: { project: { id: "p1" }, ping: true },
|
|
135
|
-
});
|
|
136
|
-
const value = await Effect.runPromise(client.query({ ...projectIdentity, ping: true }));
|
|
137
|
-
expect(value).toEqual({ project: { id: "p1" }, ping: true });
|
|
138
|
-
expect(requests).toHaveLength(1);
|
|
139
|
-
});
|
|
140
|
-
test("__proto__ aliases are own properties without changing result prototypes", async () => {
|
|
141
|
-
const { client } = harness({ data: { ["__proto__"]: { id: "p1" } } });
|
|
142
|
-
const value = await Effect.runPromise(client.query({
|
|
143
|
-
__alias: {
|
|
144
|
-
["__proto__"]: {
|
|
145
|
-
project: { where: { id: "p1" }, select: { id: true } },
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
}));
|
|
149
|
-
expect(Object.hasOwn(value, "__proto__")).toBe(true);
|
|
150
|
-
expect(Object.getPrototypeOf(value)).toBe(Object.prototype);
|
|
151
|
-
expect(value["__proto__"]).toEqual({ id: "p1" });
|
|
152
|
-
});
|
|
153
|
-
test("decodes null field and nullable list items without inventing missing fields", async () => {
|
|
154
|
-
const { client } = harness({
|
|
155
|
-
data: { projects: [null, { id: "p1", description: null }] },
|
|
156
|
-
});
|
|
157
|
-
const value = await Effect.runPromise(client.query({ projects: { select: { id: true, description: true } } }));
|
|
158
|
-
expect(value).toEqual({
|
|
159
|
-
projects: [null, { id: "p1", description: null }],
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
test("decodes union fragments into the selected concrete branch", async () => {
|
|
163
|
-
const { client, requests } = harness({
|
|
164
|
-
data: {
|
|
165
|
-
search: [
|
|
166
|
-
{ __typename: "Project", name: "Production" },
|
|
167
|
-
{ __typename: "Service", status: "ACTIVE" },
|
|
168
|
-
],
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
const value = await Effect.runPromise(client.query({
|
|
172
|
-
search: {
|
|
173
|
-
where: { text: "prod" },
|
|
174
|
-
select: {
|
|
175
|
-
__typename: true,
|
|
176
|
-
__on: { Project: { name: true }, Service: { status: true } },
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
}));
|
|
180
|
-
expect(value.search).toEqual([
|
|
181
|
-
{ __typename: "Project", name: "Production" },
|
|
182
|
-
{ __typename: "Service", status: "ACTIVE" },
|
|
183
|
-
]);
|
|
184
|
-
expect(requests[0].query).toMatch(/\.\.\.\s*on\s+Project/);
|
|
185
|
-
expect(requests[0].query).toMatch(/\.\.\.\s*on\s+Service/);
|
|
186
|
-
});
|
|
187
|
-
test("interface selections include shared fields and concrete fragments", async () => {
|
|
188
|
-
const { client } = harness({
|
|
189
|
-
data: { node: { __typename: "Project", id: "p1", name: "Production" } },
|
|
190
|
-
});
|
|
191
|
-
const value = await Effect.runPromise(client.query({
|
|
192
|
-
node: {
|
|
193
|
-
where: { id: "p1" },
|
|
194
|
-
select: {
|
|
195
|
-
__typename: true,
|
|
196
|
-
id: true,
|
|
197
|
-
__on: { Project: { name: true } },
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
}));
|
|
201
|
-
expect(value.node).toEqual({
|
|
202
|
-
__typename: "Project",
|
|
203
|
-
id: "p1",
|
|
204
|
-
name: "Production",
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
test("interface and concrete selections merge nested fields at the same response path", async () => {
|
|
208
|
-
const { client } = harness({
|
|
13
|
+
Object.assign(Project.fields, {
|
|
14
|
+
name: scalarField("name"),
|
|
15
|
+
stars: scalarField("stars"),
|
|
16
|
+
});
|
|
17
|
+
const Mock = Layer.succeed(GqlTransport, {
|
|
18
|
+
execute: (request) => Effect.sync(() => {
|
|
19
|
+
expect(request.document).toContain("email");
|
|
20
|
+
expect(request.document).toContain("stars");
|
|
21
|
+
expect(request.document).toContain("name");
|
|
22
|
+
return {
|
|
209
23
|
data: {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const value = await Effect.runPromise(client.query({
|
|
217
|
-
node: {
|
|
218
|
-
where: { id: "p1" },
|
|
219
|
-
select: {
|
|
220
|
-
__typename: true,
|
|
221
|
-
parent: { id: true },
|
|
222
|
-
__on: { Project: { parent: { name: true } } },
|
|
24
|
+
me: {
|
|
25
|
+
email: "ada@railway.app",
|
|
26
|
+
projects: [
|
|
27
|
+
{ name: "engine", stars: 12 },
|
|
28
|
+
{ name: "bombe", stars: 3 },
|
|
29
|
+
],
|
|
223
30
|
},
|
|
224
31
|
},
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
__typename: "Project",
|
|
228
|
-
parent: { id: "parent-1", name: "Parent" },
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
test("include and skip directives allow omitted fields without false decode failures", async () => {
|
|
232
|
-
const { client, requests } = harness({ data: { project: { id: "p1" } } });
|
|
233
|
-
const include = false;
|
|
234
|
-
const skip = true;
|
|
235
|
-
const value = await Effect.runPromise(client.query({
|
|
236
|
-
project: {
|
|
237
|
-
where: { id: "p1" },
|
|
238
|
-
select: { id: true, name: { $skip: skip, select: true } },
|
|
239
|
-
},
|
|
240
|
-
ping: { $include: include, select: true },
|
|
241
|
-
}));
|
|
242
|
-
expect(value).toEqual({ project: { id: "p1" } });
|
|
243
|
-
expect(requests[0].query).toContain("@include");
|
|
244
|
-
expect(requests[0].query).toContain("@skip");
|
|
245
|
-
expect(Object.values(requests[0].variables)).toContain(false);
|
|
246
|
-
expect(Object.values(requests[0].variables)).toContain(true);
|
|
247
|
-
});
|
|
248
|
-
test("bad selected scalar types produce a decode error", async () => {
|
|
249
|
-
const { client } = harness({
|
|
250
|
-
data: { project: { id: { invalid: true } } },
|
|
251
|
-
});
|
|
252
|
-
expect((await failure(client.query(projectIdentity)))._tag).toBe("GraphQLDecodeError");
|
|
253
|
-
});
|
|
254
|
-
test("missing non-null selected fields produce a decode error", async () => {
|
|
255
|
-
const { client } = harness({ data: { project: {} } });
|
|
256
|
-
expect((await failure(client.query(projectIdentity)))._tag).toBe("GraphQLDecodeError");
|
|
257
|
-
});
|
|
258
|
-
test("malformed errors envelopes never masquerade as success", async () => {
|
|
259
|
-
const { client } = harness({
|
|
260
|
-
data: { ping: true },
|
|
261
|
-
errors: [{ path: ["ping"] }],
|
|
262
|
-
});
|
|
263
|
-
expect((await failure(client.query({ ping: true })))._tag).toBe("GraphQLDecodeError");
|
|
264
|
-
});
|
|
32
|
+
};
|
|
33
|
+
}),
|
|
265
34
|
});
|
|
266
|
-
describe("
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
},
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
{
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
expect(report.errors.map((error) => error._tag)).toEqual([
|
|
302
|
-
"ServiceDescriptionUnavailable",
|
|
303
|
-
"ProjectDescriptionUnavailable",
|
|
304
|
-
]);
|
|
305
|
-
});
|
|
306
|
-
test("erased concrete-type data leaves conflicting equal-rank error matches unknown", async () => {
|
|
307
|
-
const { client } = harness({
|
|
308
|
-
data: null,
|
|
309
|
-
errors: [
|
|
310
|
-
{
|
|
311
|
-
message: "Description unavailable",
|
|
312
|
-
path: ["search", 0, "label"],
|
|
313
|
-
extensions: { code: "DESCRIPTION_UNAVAILABLE" },
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
});
|
|
317
|
-
const report = await Effect.runPromise(client.report.query(descriptionSelection));
|
|
318
|
-
expect(report.errors[0]._tag).toBe("UnknownGraphQLError");
|
|
319
|
-
expect(report.errors[0].path).toEqual(["search", 0, "label"]);
|
|
320
|
-
});
|
|
321
|
-
test("strict execution aggregates a single field issue and preserves diagnostics", async () => {
|
|
322
|
-
const { client } = harness(partialProjectEnvelope);
|
|
323
|
-
const error = await failure(client.query({ ...servicesSelection, ping: true }));
|
|
324
|
-
expect(error._tag).toBe("GraphQLFailure");
|
|
325
|
-
if (error._tag !== "GraphQLFailure")
|
|
326
|
-
throw new Error("Expected GraphQLFailure");
|
|
327
|
-
expect(error.errors).toHaveLength(1);
|
|
328
|
-
expect(error.errors[0]._tag).toBe("ServicesUnavailable");
|
|
329
|
-
expect(error.errors[0].path).toEqual(["project", "services"]);
|
|
330
|
-
expect(error.errors[0].extensions).toEqual({
|
|
331
|
-
code: "UNAVAILABLE",
|
|
332
|
-
incident: "incident-1",
|
|
333
|
-
});
|
|
334
|
-
expect(error.data).toEqual(partialProjectEnvelope.data);
|
|
335
|
-
});
|
|
336
|
-
test("report mode preserves partial data and typed issues as a success", async () => {
|
|
337
|
-
const { client } = harness(partialProjectEnvelope);
|
|
338
|
-
const report = await Effect.runPromise(client.report.query({ ...servicesSelection, ping: true }));
|
|
339
|
-
expect(report.data).toEqual(partialProjectEnvelope.data);
|
|
340
|
-
expect(report.errors.map((error) => error._tag)).toEqual([
|
|
341
|
-
"ServicesUnavailable",
|
|
342
|
-
]);
|
|
343
|
-
});
|
|
344
|
-
test("multiple errors retain aliases, tags, and all unaffected data", async () => {
|
|
345
|
-
const { client } = harness(multipleErrorEnvelope);
|
|
346
|
-
const error = await failure(client.query({
|
|
347
|
-
__alias: {
|
|
348
|
-
production: {
|
|
349
|
-
project: { where: { id: "p1" }, select: { id: true } },
|
|
350
|
-
},
|
|
351
|
-
staging: {
|
|
352
|
-
project: {
|
|
353
|
-
where: { id: "p2" },
|
|
354
|
-
select: servicesSelection.project.select,
|
|
35
|
+
describe("Query.fn", () => {
|
|
36
|
+
test("builds one document from a returned plan and maps/filters after fetch", async () => {
|
|
37
|
+
const program = Query.fn(() => {
|
|
38
|
+
const me = root("query", "me", User);
|
|
39
|
+
return {
|
|
40
|
+
email: me.email,
|
|
41
|
+
projects: me.projects({ first: 5 }).pipe(Query.filter((project) => project.stars.pipe(Query.map((stars) => stars > 5))), Query.map((project) => ({
|
|
42
|
+
name: project.name,
|
|
43
|
+
stars: project.stars,
|
|
44
|
+
}))),
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
const result = await Effect.runPromise(program.pipe(Effect.provide(Mock)));
|
|
48
|
+
expect(result).toEqual({
|
|
49
|
+
email: "ada@railway.app",
|
|
50
|
+
projects: [{ name: "engine", stars: 12 }],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
test("Relay connections map as a list of nodes, one POST", async () => {
|
|
54
|
+
const Workspace = { name: "Workspace", fields: {} };
|
|
55
|
+
Object.assign(Workspace.fields, { name: scalarField("name") });
|
|
56
|
+
const layer = Layer.succeed(GqlTransport, {
|
|
57
|
+
execute: (request) => Effect.sync(() => {
|
|
58
|
+
expect(request.document).toContain("edges");
|
|
59
|
+
expect(request.document).toContain("node");
|
|
60
|
+
expect(request.document).toContain("name");
|
|
61
|
+
expect(request.document).not.toContain("pageInfo");
|
|
62
|
+
return {
|
|
63
|
+
data: {
|
|
64
|
+
projects: {
|
|
65
|
+
edges: [
|
|
66
|
+
{ node: { name: "engine" } },
|
|
67
|
+
{ node: { name: "bombe" } },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
355
70
|
},
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const report = await Effect.runPromise(client.report.query({
|
|
384
|
-
projects: { select: servicesSelection.project.select },
|
|
385
|
-
}));
|
|
386
|
-
expect(report.errors[0]._tag).toBe("ServicesUnavailable");
|
|
387
|
-
expect(report.errors[0].path).toEqual(["projects", 0, "services"]);
|
|
388
|
-
expect(report.data).toEqual({ projects: [null] });
|
|
389
|
-
});
|
|
390
|
-
test("overlapping interface and concrete selections retain nested error contracts", async () => {
|
|
391
|
-
const { client } = harness({
|
|
392
|
-
data: {
|
|
393
|
-
node: {
|
|
394
|
-
__typename: "Project",
|
|
395
|
-
parent: { id: "parent-1", services: null },
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
errors: [
|
|
399
|
-
{
|
|
400
|
-
message: "Services unavailable",
|
|
401
|
-
path: ["node", "parent", "services"],
|
|
402
|
-
extensions: { code: "UNAVAILABLE" },
|
|
403
|
-
},
|
|
404
|
-
],
|
|
405
|
-
});
|
|
406
|
-
const report = await Effect.runPromise(client.report.query({
|
|
407
|
-
node: {
|
|
408
|
-
where: { id: "p1" },
|
|
409
|
-
select: {
|
|
410
|
-
__typename: true,
|
|
411
|
-
parent: { id: true },
|
|
412
|
-
__on: {
|
|
413
|
-
Project: {
|
|
414
|
-
parent: {
|
|
415
|
-
services: { select: { edges: { node: { id: true } } } },
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
const program = Query.fn(() => {
|
|
75
|
+
const page = rootConnection("query", "projects", Workspace, { first: 20 }, { first: "Int" });
|
|
76
|
+
return page.pipe(Query.map((project) => project.name));
|
|
77
|
+
})();
|
|
78
|
+
const names = await Effect.runPromise(program.pipe(Effect.provide(layer)));
|
|
79
|
+
expect(names).toEqual(["engine", "bombe"]);
|
|
80
|
+
});
|
|
81
|
+
test("nested connection fields unwrap edges.node", async () => {
|
|
82
|
+
const Service = { name: "Service", fields: {} };
|
|
83
|
+
Object.assign(Service.fields, { name: scalarField("name") });
|
|
84
|
+
const Project = { name: "Project", fields: {} };
|
|
85
|
+
Object.assign(Project.fields, {
|
|
86
|
+
name: scalarField("name"),
|
|
87
|
+
services: connectionField("services", Service, { first: "Int" }),
|
|
88
|
+
});
|
|
89
|
+
const layer = Layer.succeed(GqlTransport, {
|
|
90
|
+
execute: (request) => Effect.sync(() => {
|
|
91
|
+
expect(request.document).toMatch(/services[\s\S]*edges[\s\S]*node/);
|
|
92
|
+
return {
|
|
93
|
+
data: {
|
|
94
|
+
project: {
|
|
95
|
+
name: "engine",
|
|
96
|
+
services: {
|
|
97
|
+
edges: [{ node: { name: "web" } }, { node: { name: "db" } }],
|
|
416
98
|
},
|
|
417
99
|
},
|
|
418
100
|
},
|
|
419
|
-
}
|
|
420
|
-
},
|
|
421
|
-
}));
|
|
422
|
-
expect(report.errors[0]._tag).toBe("ServicesUnavailable");
|
|
423
|
-
});
|
|
424
|
-
test("known codes at undeclared fields fall back to unknown", async () => {
|
|
425
|
-
const { client } = harness({
|
|
426
|
-
data: { project: null },
|
|
427
|
-
errors: [
|
|
428
|
-
{
|
|
429
|
-
message: "Unavailable",
|
|
430
|
-
path: ["project"],
|
|
431
|
-
extensions: { code: "UNAVAILABLE" },
|
|
432
|
-
},
|
|
433
|
-
],
|
|
434
|
-
});
|
|
435
|
-
const report = await Effect.runPromise(client.report.query(projectIdentity));
|
|
436
|
-
expect(report.errors[0]._tag).toBe("UnknownGraphQLError");
|
|
437
|
-
});
|
|
438
|
-
test("unrecognized and unselected error paths fall back to unknown", async () => {
|
|
439
|
-
const { client } = harness({
|
|
440
|
-
data: { project: null },
|
|
441
|
-
errors: [
|
|
442
|
-
{
|
|
443
|
-
message: "New provider error",
|
|
444
|
-
path: ["project"],
|
|
445
|
-
extensions: { code: "NEW_CODE" },
|
|
446
|
-
},
|
|
447
|
-
{
|
|
448
|
-
message: "Unknown field path",
|
|
449
|
-
path: ["neverSelected"],
|
|
450
|
-
extensions: { code: "NOT_FOUND" },
|
|
451
|
-
},
|
|
452
|
-
],
|
|
101
|
+
};
|
|
102
|
+
}),
|
|
453
103
|
});
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
"UnknownGraphQLError",
|
|
457
|
-
"UnknownGraphQLError",
|
|
458
|
-
]);
|
|
459
|
-
});
|
|
460
|
-
test("global request errors without paths are decoded even on HTTP 400", async () => {
|
|
461
|
-
const { client } = harness({
|
|
462
|
-
errors: [
|
|
463
|
-
{
|
|
464
|
-
message: "Authentication required",
|
|
465
|
-
extensions: { code: "UNAUTHENTICATED" },
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
-
}, 400);
|
|
469
|
-
const report = await Effect.runPromise(client.report.query(projectIdentity));
|
|
470
|
-
expect(report.errors[0]._tag).toBe("Unauthorized");
|
|
471
|
-
expect(report.status).toBe(400);
|
|
472
|
-
});
|
|
473
|
-
test("code and message matchers require both constraints", async () => {
|
|
474
|
-
const { client } = harness({
|
|
475
|
-
errors: [
|
|
476
|
-
{
|
|
477
|
-
message: "Not Authorized",
|
|
478
|
-
extensions: { code: "INTERNAL_SERVER_ERROR" },
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
message: "Resolver crashed",
|
|
482
|
-
extensions: { code: "INTERNAL_SERVER_ERROR" },
|
|
483
|
-
},
|
|
484
|
-
],
|
|
485
|
-
});
|
|
486
|
-
const report = await Effect.runPromise(client.report.query(projectIdentity));
|
|
487
|
-
expect(report.errors.map((error) => error._tag)).toEqual([
|
|
488
|
-
"Unauthorized",
|
|
489
|
-
"UnknownGraphQLError",
|
|
490
|
-
]);
|
|
491
|
-
});
|
|
492
|
-
test("a mutation with response errors is never automatically retried", async () => {
|
|
493
|
-
const { client, requests } = harness({
|
|
494
|
-
data: null,
|
|
495
|
-
errors: [
|
|
496
|
-
{
|
|
497
|
-
message: "Not found",
|
|
498
|
-
path: ["editProject"],
|
|
499
|
-
extensions: { code: "NOT_FOUND" },
|
|
500
|
-
},
|
|
501
|
-
],
|
|
502
|
-
});
|
|
503
|
-
const error = await failure(client.mutation({
|
|
504
|
-
editProject: {
|
|
505
|
-
where: { id: "p1", name: "Changed" },
|
|
506
|
-
select: { id: true },
|
|
507
|
-
},
|
|
508
|
-
}));
|
|
509
|
-
expect(error._tag).toBe("GraphQLFailure");
|
|
510
|
-
expect(requests).toHaveLength(1);
|
|
511
|
-
expect(requests[0].query).toMatch(/^mutation/);
|
|
512
|
-
});
|
|
513
|
-
test("catchTags recovers only when every issue has an allowed tag", async () => {
|
|
514
|
-
const { client } = harness({
|
|
515
|
-
data: { project: null },
|
|
516
|
-
errors: [
|
|
517
|
-
{
|
|
518
|
-
message: "Gone",
|
|
519
|
-
path: ["project"],
|
|
520
|
-
extensions: { code: "NOT_FOUND" },
|
|
521
|
-
},
|
|
522
|
-
],
|
|
523
|
-
});
|
|
524
|
-
const value = await Effect.runPromise(client
|
|
525
|
-
.query(projectIdentity)
|
|
526
|
-
.pipe(G.catchTags("ProjectNotFound", () => Effect.succeed("absent"))));
|
|
527
|
-
expect(value).toBe("absent");
|
|
528
|
-
});
|
|
529
|
-
test("catchTags never hides another issue in a mixed aggregate", async () => {
|
|
530
|
-
const { client } = harness({
|
|
531
|
-
data: { project: null },
|
|
532
|
-
errors: [
|
|
533
|
-
{
|
|
534
|
-
message: "Gone",
|
|
535
|
-
path: ["project"],
|
|
536
|
-
extensions: { code: "NOT_FOUND" },
|
|
537
|
-
},
|
|
538
|
-
{
|
|
539
|
-
message: "Authentication required",
|
|
540
|
-
extensions: { code: "UNAUTHENTICATED" },
|
|
541
|
-
},
|
|
542
|
-
],
|
|
543
|
-
});
|
|
544
|
-
let recovered = false;
|
|
545
|
-
const error = await failure(client.query(projectIdentity).pipe(G.catchTags("ProjectNotFound", () => Effect.sync(() => {
|
|
546
|
-
recovered = true;
|
|
547
|
-
return "absent";
|
|
548
|
-
}))));
|
|
549
|
-
expect(recovered).toBe(false);
|
|
550
|
-
expect(error._tag).toBe("GraphQLFailure");
|
|
551
|
-
if (error._tag !== "GraphQLFailure")
|
|
552
|
-
throw new Error("Expected GraphQLFailure");
|
|
553
|
-
expect(error.errors.map((issue) => issue._tag)).toEqual([
|
|
554
|
-
"ProjectNotFound",
|
|
555
|
-
"Unauthorized",
|
|
556
|
-
]);
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
describe("GraphQL connection pagination", () => {
|
|
560
|
-
const paginated = (responses) => {
|
|
561
|
-
const requests = [];
|
|
562
|
-
const client = G.makeClient(fixtureModel, (request) => Effect.sync(() => {
|
|
563
|
-
const page = responses[requests.length];
|
|
564
|
-
requests.push(request);
|
|
565
|
-
if (!page)
|
|
566
|
-
throw new Error("Paginator made an unexpected extra request");
|
|
567
|
-
// Respect response aliases, including cursor fields added by the client.
|
|
568
|
-
const document = parse(request.query);
|
|
569
|
-
const operation = document.definitions.find((node) => node.kind === Kind.OPERATION_DEFINITION);
|
|
570
|
-
if (operation.kind !== Kind.OPERATION_DEFINITION)
|
|
571
|
-
throw new Error("Expected an operation");
|
|
572
|
-
const root = operation.selectionSet.selections[0];
|
|
573
|
-
if (root.kind !== Kind.FIELD)
|
|
574
|
-
throw new Error("Expected a connection field");
|
|
575
|
-
const selected = {};
|
|
576
|
-
for (const child of root.selectionSet.selections) {
|
|
577
|
-
if (child.kind !== Kind.FIELD)
|
|
578
|
-
continue;
|
|
579
|
-
selected[child.alias?.value ?? child.name.value] =
|
|
580
|
-
page[child.name.value];
|
|
581
|
-
}
|
|
104
|
+
const program = Query.fn(() => {
|
|
105
|
+
const project = root("query", "project", Project, { id: "p1" }, { id: "String!" });
|
|
582
106
|
return {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
107
|
+
name: project.name,
|
|
108
|
+
services: project
|
|
109
|
+
.services({ first: 20 })
|
|
110
|
+
.pipe(Query.map((service) => service.name)),
|
|
586
111
|
};
|
|
587
|
-
})
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
]);
|
|
623
|
-
|
|
624
|
-
expect(Array.from(pages)).toEqual([{ edges: [{ node: { id: "s1" } }] }]);
|
|
625
|
-
expect(requests[0].query).toContain("pageInfo");
|
|
626
|
-
});
|
|
627
|
-
test("a user alias named pageInfo cannot collide with hidden pagination fields", async () => {
|
|
628
|
-
const { services, requests } = paginated([
|
|
629
|
-
{
|
|
630
|
-
edges: [{ node: { id: "s1" } }],
|
|
631
|
-
pageInfo: { endCursor: null, hasNextPage: false },
|
|
632
|
-
},
|
|
633
|
-
]);
|
|
634
|
-
const pages = await Effect.runPromise(Stream.runCollect(services.pages({}, {
|
|
635
|
-
__alias: { pageInfo: { edges: { node: { id: true } } } },
|
|
636
|
-
})));
|
|
637
|
-
expect(Array.from(pages)).toEqual([{ pageInfo: [{ node: { id: "s1" } }] }]);
|
|
638
|
-
expect(requests).toHaveLength(1);
|
|
639
|
-
});
|
|
640
|
-
test("wrapped pageInfo selections still fetch the internal pagination cursor", async () => {
|
|
641
|
-
const { services, requests } = paginated([
|
|
642
|
-
{ edges: [], pageInfo: { endCursor: "c1", hasNextPage: true } },
|
|
643
|
-
{ edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
|
644
|
-
]);
|
|
645
|
-
const pages = await Effect.runPromise(Stream.runCollect(services.pages({}, { pageInfo: { select: { hasNextPage: true } } })));
|
|
646
|
-
expect(Array.from(pages)).toEqual([
|
|
647
|
-
{ pageInfo: { hasNextPage: true } },
|
|
648
|
-
{ pageInfo: { hasNextPage: false } },
|
|
649
|
-
]);
|
|
650
|
-
expect(requests).toHaveLength(2);
|
|
651
|
-
});
|
|
652
|
-
test("conditional pageInfo projection cannot skip internal cursor fetching", async () => {
|
|
653
|
-
const { services, requests } = paginated([
|
|
654
|
-
{
|
|
655
|
-
edges: [{ node: { id: "s1" } }],
|
|
656
|
-
pageInfo: { endCursor: "c1", hasNextPage: true },
|
|
657
|
-
},
|
|
658
|
-
{
|
|
659
|
-
edges: [{ node: { id: "s2" } }],
|
|
660
|
-
pageInfo: { endCursor: null, hasNextPage: false },
|
|
661
|
-
},
|
|
662
|
-
]);
|
|
663
|
-
const pages = await Effect.runPromise(Stream.runCollect(services.pages({}, {
|
|
664
|
-
edges: { node: { id: true } },
|
|
665
|
-
pageInfo: { $skip: true, select: { hasNextPage: true } },
|
|
666
|
-
})));
|
|
667
|
-
expect(Array.from(pages)).toEqual([
|
|
668
|
-
{ edges: [{ node: { id: "s1" } }] },
|
|
669
|
-
{ edges: [{ node: { id: "s2" } }] },
|
|
670
|
-
]);
|
|
671
|
-
expect(requests).toHaveLength(2);
|
|
672
|
-
});
|
|
673
|
-
test("non-advancing cursors fail without hanging", async () => {
|
|
674
|
-
const { services, requests } = paginated([
|
|
675
|
-
{
|
|
676
|
-
edges: [{ node: { id: "s1" } }],
|
|
677
|
-
pageInfo: { endCursor: "c1", hasNextPage: true },
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
edges: [{ node: { id: "s1" } }],
|
|
681
|
-
pageInfo: { endCursor: "c1", hasNextPage: true },
|
|
682
|
-
},
|
|
683
|
-
]);
|
|
684
|
-
const error = await failure(Stream.runCollect(services.items({}, { id: true })));
|
|
685
|
-
expect(error._tag).toBe("GraphQLDecodeError");
|
|
686
|
-
expect(requests).toHaveLength(2);
|
|
687
|
-
});
|
|
688
|
-
test("invalid pagination input fails in the typed channel without making a request", async () => {
|
|
689
|
-
const { services, requests } = paginated([]);
|
|
690
|
-
const stream = services.items({ first: "bad" }, { id: true });
|
|
691
|
-
const error = await failure(Stream.runCollect(stream));
|
|
692
|
-
expect(error._tag).toBe("GraphQLRequestError");
|
|
693
|
-
expect(requests).toHaveLength(0);
|
|
694
|
-
});
|
|
695
|
-
});
|
|
696
|
-
describe("GraphQL safe query retries", () => {
|
|
697
|
-
const retryableModel = {
|
|
698
|
-
...fixtureModel,
|
|
699
|
-
errors: {
|
|
700
|
-
...fixtureModel.errors,
|
|
701
|
-
ServicesUnavailable: {
|
|
702
|
-
...fixtureModel.errors.ServicesUnavailable,
|
|
703
|
-
retryable: true,
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
};
|
|
707
|
-
const unavailable = (root = "services") => ({
|
|
708
|
-
data: null,
|
|
709
|
-
errors: [
|
|
710
|
-
{
|
|
711
|
-
message: "Services unavailable",
|
|
712
|
-
path: [root],
|
|
713
|
-
extensions: { code: "UNAVAILABLE" },
|
|
714
|
-
},
|
|
715
|
-
],
|
|
716
|
-
});
|
|
717
|
-
const retried = (response) => {
|
|
718
|
-
const requests = [];
|
|
719
|
-
const client = G.makeClient(retryableModel, (request) => Effect.sync(() => {
|
|
720
|
-
requests.push(request);
|
|
721
|
-
return { body: response(requests.length), status: 200, headers: {} };
|
|
722
|
-
}), errorClasses);
|
|
723
|
-
return { client, requests };
|
|
724
|
-
};
|
|
725
|
-
test("a safely retryable query reuses the document and variables", async () => {
|
|
726
|
-
const { client, requests } = retried((attempt) => attempt === 1 ? unavailable() : { data: { services: { edges: [] } } });
|
|
727
|
-
const result = await Effect.runPromise(client.query({
|
|
728
|
-
services: {
|
|
729
|
-
where: { first: 1 },
|
|
730
|
-
select: { edges: { node: { id: true } } },
|
|
731
|
-
},
|
|
732
|
-
}));
|
|
733
|
-
expect(result).toEqual({ services: { edges: [] } });
|
|
734
|
-
expect(requests).toHaveLength(2);
|
|
735
|
-
expect(requests[0].query).toBe(requests[1].query);
|
|
736
|
-
expect(requests[0].variables).toEqual(requests[1].variables);
|
|
737
|
-
});
|
|
738
|
-
test("retryable query failures stop after five retries", async () => {
|
|
739
|
-
const { client, requests } = retried(() => unavailable());
|
|
740
|
-
const error = await failure(client.query({ services: { select: { edges: { node: { id: true } } } } }));
|
|
741
|
-
expect(error._tag).toBe("GraphQLFailure");
|
|
742
|
-
expect(requests).toHaveLength(6);
|
|
743
|
-
}, 15_000);
|
|
744
|
-
test("an aggregate containing a nonretryable issue is never retried", async () => {
|
|
745
|
-
const { client, requests } = retried(() => ({
|
|
746
|
-
data: null,
|
|
747
|
-
errors: [
|
|
748
|
-
...unavailable().errors,
|
|
749
|
-
{ message: "Unauthorized", extensions: { code: "UNAUTHENTICATED" } },
|
|
750
|
-
],
|
|
751
|
-
}));
|
|
752
|
-
const error = await failure(client.query({ services: { select: { edges: { node: { id: true } } } } }));
|
|
753
|
-
expect(error._tag).toBe("GraphQLFailure");
|
|
754
|
-
expect(requests).toHaveLength(1);
|
|
755
|
-
});
|
|
756
|
-
test("mutations never retry even when the field failure is marked retryable", async () => {
|
|
757
|
-
const { client, requests } = retried(() => ({
|
|
758
|
-
data: null,
|
|
759
|
-
errors: [
|
|
760
|
-
{
|
|
761
|
-
message: "Services unavailable",
|
|
762
|
-
path: ["editProject", "services"],
|
|
763
|
-
extensions: { code: "UNAVAILABLE" },
|
|
764
|
-
},
|
|
765
|
-
],
|
|
766
|
-
}));
|
|
767
|
-
const error = await failure(client.mutation({
|
|
768
|
-
editProject: {
|
|
769
|
-
where: { id: "p1", name: "Updated" },
|
|
770
|
-
select: { services: { select: { edges: { node: { id: true } } } } },
|
|
771
|
-
},
|
|
772
|
-
}));
|
|
773
|
-
expect(error._tag).toBe("GraphQLFailure");
|
|
774
|
-
if (error._tag !== "GraphQLFailure")
|
|
775
|
-
throw new Error("Expected GraphQLFailure");
|
|
776
|
-
expect(error.errors[0]._tag).toBe("ServicesUnavailable");
|
|
777
|
-
expect(requests).toHaveLength(1);
|
|
112
|
+
})();
|
|
113
|
+
const result = await Effect.runPromise(program.pipe(Effect.provide(layer)));
|
|
114
|
+
expect(result).toEqual({ name: "engine", services: ["web", "db"] });
|
|
115
|
+
});
|
|
116
|
+
test("Query.map on an object selects its fields and keeps null", async () => {
|
|
117
|
+
const Deployment = { name: "Deployment", fields: {} };
|
|
118
|
+
Object.assign(Deployment.fields, {
|
|
119
|
+
id: scalarField("id"),
|
|
120
|
+
status: scalarField("status"),
|
|
121
|
+
});
|
|
122
|
+
const Service = { name: "Service", fields: {} };
|
|
123
|
+
Object.assign(Service.fields, {
|
|
124
|
+
name: scalarField("name"),
|
|
125
|
+
latestDeployment: objectField("latestDeployment", Deployment),
|
|
126
|
+
});
|
|
127
|
+
const documents = [];
|
|
128
|
+
const respond = (latestDeployment) => Layer.succeed(GqlTransport, {
|
|
129
|
+
execute: (request) => Effect.sync(() => {
|
|
130
|
+
documents.push(request.document);
|
|
131
|
+
return {
|
|
132
|
+
data: { service: { name: "web", latestDeployment } },
|
|
133
|
+
};
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
const program = Query.fn(() => {
|
|
137
|
+
const service = root("query", "service", Service);
|
|
138
|
+
return {
|
|
139
|
+
name: service.name,
|
|
140
|
+
deployment: service.latestDeployment.pipe(Query.map((deployment) => ({
|
|
141
|
+
id: deployment.id,
|
|
142
|
+
status: deployment.status,
|
|
143
|
+
}))),
|
|
144
|
+
};
|
|
145
|
+
})();
|
|
146
|
+
expect(await Effect.runPromise(program.pipe(Effect.provide(respond({ id: "d1", status: "SUCCESS" }))))).toEqual({ name: "web", deployment: { id: "d1", status: "SUCCESS" } });
|
|
147
|
+
expect(documents[0]).toMatch(/latestDeployment \{\s+id\s+status/);
|
|
148
|
+
expect(await Effect.runPromise(program.pipe(Effect.provide(respond(null))))).toEqual({ name: "web", deployment: null });
|
|
778
149
|
});
|
|
779
150
|
});
|
|
780
151
|
//# sourceMappingURL=graphql.test.js.map
|