@apollo/federation-internals 2.4.0-alpha.1 → 2.4.1
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/CHANGELOG.md +41 -0
- package/dist/coreSpec.js +1 -1
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts +15 -15
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +35 -55
- package/dist/definitions.js.map +1 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +19 -18
- package/dist/federation.js.map +1 -1
- package/dist/operations.d.ts +222 -88
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +934 -605
- package/dist/operations.js.map +1 -1
- package/dist/precompute.d.ts.map +1 -1
- package/dist/precompute.js +13 -10
- package/dist/precompute.js.map +1 -1
- package/dist/values.d.ts +3 -3
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +22 -28
- package/dist/values.js.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/operations.test.ts +727 -145
- package/src/__tests__/schemaUpgrader.test.ts +1 -1
- package/src/definitions.ts +53 -57
- package/src/federation.ts +27 -23
- package/src/operations.ts +1370 -855
- package/src/precompute.ts +18 -12
- package/src/values.ts +24 -30
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
SchemaRootKind,
|
|
5
5
|
} from '../../dist/definitions';
|
|
6
6
|
import { buildSchema } from '../../dist/buildSchema';
|
|
7
|
-
import {
|
|
7
|
+
import { MutableSelectionSet, Operation, operationFromDocument, parseOperation } from '../../dist/operations';
|
|
8
8
|
import './matchers';
|
|
9
9
|
import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, SelectionNode, SelectionSetNode } from 'graphql';
|
|
10
10
|
|
|
@@ -32,6 +32,34 @@ function astSSet(...selections: SelectionNode[]): SelectionSetNode {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
describe('fragments optimization', () => {
|
|
35
|
+
// Takes a query with fragments as inputs, expand all those fragments, and ensures that all the
|
|
36
|
+
// fragments gets optimized back, and that we get back the exact same query.
|
|
37
|
+
function testFragmentsRoundtrip({
|
|
38
|
+
schema,
|
|
39
|
+
query,
|
|
40
|
+
expanded,
|
|
41
|
+
}: {
|
|
42
|
+
schema: Schema,
|
|
43
|
+
query: string,
|
|
44
|
+
expanded: string,
|
|
45
|
+
}) {
|
|
46
|
+
const operation = parseOperation(schema, query);
|
|
47
|
+
// We call `trimUnsatisfiableBranches` because the selections we care about in the query planner
|
|
48
|
+
// will effectively have had gone through that function (and even if that function wasn't called,
|
|
49
|
+
// the query planning algorithm would still end up removing unsatisfiable branches anyway), so
|
|
50
|
+
// it is a more interesting test.
|
|
51
|
+
const withoutFragments = operation.expandAllFragments().trimUnsatisfiableBranches();
|
|
52
|
+
|
|
53
|
+
expect(withoutFragments.toString()).toMatchString(expanded);
|
|
54
|
+
|
|
55
|
+
// We force keeping all reused fragments, even if they are used only once, because the tests using
|
|
56
|
+
// this are just about testing the reuse of fragments and this make things shorter/easier to write.
|
|
57
|
+
// There is tests in `buildPlan.test.ts` that double-check that we don't reuse fragments used only
|
|
58
|
+
// once in actual query plans.
|
|
59
|
+
const optimized = withoutFragments.optimize(operation.selectionSet.fragments!, 1);
|
|
60
|
+
expect(optimized.toString()).toMatchString(operation.toString());
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
test('handles fragments using other fragments', () => {
|
|
36
64
|
const schema = parseSchema(`
|
|
37
65
|
type Query {
|
|
@@ -126,9 +154,8 @@ describe('fragments optimization', () => {
|
|
|
126
154
|
`);
|
|
127
155
|
|
|
128
156
|
const optimized = withoutFragments.optimize(operation.selectionSet.fragments!);
|
|
129
|
-
// Note that we
|
|
130
|
-
//
|
|
131
|
-
// make the query bigger).
|
|
157
|
+
// Note that while we didn't use `onU` for `t` in the query, it's technically ok to use
|
|
158
|
+
// it and it makes the query smaller, so it gets used.
|
|
132
159
|
expect(optimized.toString()).toMatchString(`
|
|
133
160
|
fragment OnT1 on T1 {
|
|
134
161
|
a
|
|
@@ -144,15 +171,17 @@ describe('fragments optimization', () => {
|
|
|
144
171
|
b
|
|
145
172
|
}
|
|
146
173
|
|
|
174
|
+
fragment OnU on U {
|
|
175
|
+
...OnI
|
|
176
|
+
...OnT1
|
|
177
|
+
...OnT2
|
|
178
|
+
}
|
|
179
|
+
|
|
147
180
|
{
|
|
148
181
|
t {
|
|
149
|
-
...
|
|
150
|
-
...OnT2
|
|
151
|
-
...OnI
|
|
182
|
+
...OnU
|
|
152
183
|
u {
|
|
153
|
-
...
|
|
154
|
-
...OnT1
|
|
155
|
-
...OnT2
|
|
184
|
+
...OnU
|
|
156
185
|
}
|
|
157
186
|
}
|
|
158
187
|
}
|
|
@@ -176,187 +205,537 @@ describe('fragments optimization', () => {
|
|
|
176
205
|
}
|
|
177
206
|
`);
|
|
178
207
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
query {
|
|
187
|
-
t1a {
|
|
188
|
-
...OnT1
|
|
208
|
+
testFragmentsRoundtrip({
|
|
209
|
+
schema,
|
|
210
|
+
query: `
|
|
211
|
+
fragment OnT1 on T1 {
|
|
189
212
|
t2 {
|
|
190
|
-
|
|
213
|
+
x
|
|
191
214
|
}
|
|
192
215
|
}
|
|
193
|
-
t2a {
|
|
194
|
-
...OnT1
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
`);
|
|
198
216
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
t1a {
|
|
203
|
-
... on T1 {
|
|
217
|
+
query {
|
|
218
|
+
t1a {
|
|
219
|
+
...OnT1
|
|
204
220
|
t2 {
|
|
205
|
-
|
|
221
|
+
y
|
|
206
222
|
}
|
|
207
223
|
}
|
|
208
|
-
|
|
209
|
-
|
|
224
|
+
t2a {
|
|
225
|
+
...OnT1
|
|
210
226
|
}
|
|
211
227
|
}
|
|
212
|
-
|
|
213
|
-
|
|
228
|
+
`,
|
|
229
|
+
expanded: `
|
|
230
|
+
{
|
|
231
|
+
t1a {
|
|
232
|
+
t2 {
|
|
233
|
+
x
|
|
234
|
+
y
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
t2a {
|
|
214
238
|
t2 {
|
|
215
239
|
x
|
|
216
240
|
}
|
|
217
241
|
}
|
|
218
242
|
}
|
|
243
|
+
`,
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('handles nested fragments with field intersection', () => {
|
|
248
|
+
const schema = parseSchema(`
|
|
249
|
+
type Query {
|
|
250
|
+
t: T
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type T {
|
|
254
|
+
a: A
|
|
255
|
+
b: Int
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
type A {
|
|
259
|
+
x: String
|
|
260
|
+
y: String
|
|
261
|
+
z: String
|
|
219
262
|
}
|
|
220
263
|
`);
|
|
221
264
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
265
|
+
|
|
266
|
+
// The subtlety here is that `FA` contains `__typename` and so after we're reused it, the
|
|
267
|
+
// selection will look like:
|
|
268
|
+
// {
|
|
269
|
+
// t {
|
|
270
|
+
// a {
|
|
271
|
+
// ...FA
|
|
272
|
+
// }
|
|
273
|
+
// }
|
|
274
|
+
// }
|
|
275
|
+
// But to recognize that `FT` can be reused from there, we need to be able to see that
|
|
276
|
+
// the `__typename` that `FT` wants is inside `FA` (and since FA applies on the parent type `A`
|
|
277
|
+
// directly, it is fine to reuse).
|
|
278
|
+
testFragmentsRoundtrip({
|
|
279
|
+
schema,
|
|
280
|
+
query: `
|
|
281
|
+
fragment FA on A {
|
|
282
|
+
__typename
|
|
226
283
|
x
|
|
284
|
+
y
|
|
227
285
|
}
|
|
228
|
-
}
|
|
229
286
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
y
|
|
287
|
+
fragment FT on T {
|
|
288
|
+
a {
|
|
289
|
+
__typename
|
|
290
|
+
...FA
|
|
235
291
|
}
|
|
236
292
|
}
|
|
237
|
-
|
|
238
|
-
|
|
293
|
+
|
|
294
|
+
query {
|
|
295
|
+
t {
|
|
296
|
+
...FT
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
`,
|
|
300
|
+
expanded: `
|
|
301
|
+
{
|
|
302
|
+
t {
|
|
303
|
+
a {
|
|
304
|
+
__typename
|
|
305
|
+
x
|
|
306
|
+
y
|
|
307
|
+
}
|
|
308
|
+
}
|
|
239
309
|
}
|
|
310
|
+
`,
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('handles fragment matching subset of field selection', () => {
|
|
315
|
+
const schema = parseSchema(`
|
|
316
|
+
type Query {
|
|
317
|
+
t: T
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
type T {
|
|
321
|
+
a: String
|
|
322
|
+
b: B
|
|
323
|
+
c: Int
|
|
324
|
+
d: D
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
type B {
|
|
328
|
+
x: String
|
|
329
|
+
y: String
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
type D {
|
|
333
|
+
m: String
|
|
334
|
+
n: String
|
|
240
335
|
}
|
|
241
336
|
`);
|
|
337
|
+
|
|
338
|
+
testFragmentsRoundtrip({
|
|
339
|
+
schema,
|
|
340
|
+
query: `
|
|
341
|
+
fragment FragT on T {
|
|
342
|
+
b {
|
|
343
|
+
__typename
|
|
344
|
+
x
|
|
345
|
+
}
|
|
346
|
+
c
|
|
347
|
+
d {
|
|
348
|
+
m
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
{
|
|
353
|
+
t {
|
|
354
|
+
...FragT
|
|
355
|
+
d {
|
|
356
|
+
n
|
|
357
|
+
}
|
|
358
|
+
a
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
`,
|
|
362
|
+
expanded: `
|
|
363
|
+
{
|
|
364
|
+
t {
|
|
365
|
+
b {
|
|
366
|
+
__typename
|
|
367
|
+
x
|
|
368
|
+
}
|
|
369
|
+
c
|
|
370
|
+
d {
|
|
371
|
+
m
|
|
372
|
+
n
|
|
373
|
+
}
|
|
374
|
+
a
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
`,
|
|
378
|
+
});
|
|
242
379
|
});
|
|
243
|
-
});
|
|
244
380
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
381
|
+
test('handles fragment matching subset of inline fragment selection', () => {
|
|
382
|
+
// Pretty much the same test than the previous one, but matching inside a fragment selection inside
|
|
383
|
+
// of inside a field selection.
|
|
384
|
+
const schema = parseSchema(`
|
|
385
|
+
type Query {
|
|
386
|
+
i: I
|
|
387
|
+
}
|
|
250
388
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
`);
|
|
389
|
+
interface I {
|
|
390
|
+
a: String
|
|
391
|
+
}
|
|
256
392
|
|
|
257
|
-
|
|
393
|
+
type T {
|
|
394
|
+
a: String
|
|
395
|
+
b: B
|
|
396
|
+
c: Int
|
|
397
|
+
d: D
|
|
398
|
+
}
|
|
258
399
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
|
|
400
|
+
type B {
|
|
401
|
+
x: String
|
|
402
|
+
y: String
|
|
403
|
+
}
|
|
264
404
|
|
|
265
|
-
|
|
405
|
+
type D {
|
|
406
|
+
m: String
|
|
407
|
+
n: String
|
|
408
|
+
}
|
|
409
|
+
`);
|
|
266
410
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
411
|
+
testFragmentsRoundtrip({
|
|
412
|
+
schema,
|
|
413
|
+
query: `
|
|
414
|
+
fragment FragT on T {
|
|
415
|
+
b {
|
|
416
|
+
__typename
|
|
417
|
+
x
|
|
418
|
+
}
|
|
419
|
+
c
|
|
420
|
+
d {
|
|
421
|
+
m
|
|
422
|
+
}
|
|
423
|
+
}
|
|
270
424
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
425
|
+
{
|
|
426
|
+
i {
|
|
427
|
+
... on T {
|
|
428
|
+
...FragT
|
|
429
|
+
d {
|
|
430
|
+
n
|
|
431
|
+
}
|
|
432
|
+
a
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
`,
|
|
437
|
+
expanded: `
|
|
438
|
+
{
|
|
439
|
+
i {
|
|
440
|
+
... on T {
|
|
441
|
+
b {
|
|
442
|
+
__typename
|
|
443
|
+
x
|
|
444
|
+
}
|
|
445
|
+
c
|
|
446
|
+
d {
|
|
447
|
+
m
|
|
448
|
+
n
|
|
449
|
+
}
|
|
450
|
+
a
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
`,
|
|
455
|
+
});
|
|
274
456
|
});
|
|
275
457
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// This next assertion is where differs from the case where `s1` is frozen. Namely,
|
|
295
|
-
// when we do `s.mergeIn(s1)`, then `s` directly references `s1` without cloning
|
|
296
|
-
// and thus the next modification (`s.mergeIn(s2)`) ends up modifying both `s` and `s1`.
|
|
297
|
-
// Note that we don't mean by this test that the fact that `s.mergeIn(s1)` does
|
|
298
|
-
// not clone `s1` is a behaviour one should *rely* on, but it currently done for
|
|
299
|
-
// efficiencies sake: query planning does a lot of selection set building through
|
|
300
|
-
// `SelectionSet::mergeIn` and `SelectionSet::add` and we often pass to those method
|
|
301
|
-
// newly constructed selections as input, so cloning them would wast CPU and early
|
|
302
|
-
// query planning benchmarking showed that this could add up on the more expansive
|
|
303
|
-
// plan computations. This is why freezing exists: it allows us to save cloning
|
|
304
|
-
// in general, but to protect those selection set we know should be immutable
|
|
305
|
-
// so they do get cloned in such situation.
|
|
306
|
-
expect(s1.toString()).toBe('{ t { a b } }');
|
|
307
|
-
expect(s2.toString()).toBe('{ t { b } }');
|
|
308
|
-
});
|
|
458
|
+
test('intersecting fragments', () => {
|
|
459
|
+
const schema = parseSchema(`
|
|
460
|
+
type Query {
|
|
461
|
+
t: T
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
type T {
|
|
465
|
+
a: String
|
|
466
|
+
b: B
|
|
467
|
+
c: Int
|
|
468
|
+
d: D
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
type B {
|
|
472
|
+
x: String
|
|
473
|
+
y: String
|
|
474
|
+
}
|
|
309
475
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
476
|
+
type D {
|
|
477
|
+
m: String
|
|
478
|
+
n: String
|
|
479
|
+
}
|
|
480
|
+
`);
|
|
314
481
|
|
|
315
|
-
|
|
316
|
-
|
|
482
|
+
testFragmentsRoundtrip({
|
|
483
|
+
schema,
|
|
484
|
+
// Note: the code that reuse fragments iterates on fragments in the order they are defined in the document, but when it reuse
|
|
485
|
+
// a fragment, it puts it at the beginning of the selection (somewhat random, it just feel often easier to read), so the net
|
|
486
|
+
// effect on this example is that `Frag2`, which will be reused after `Frag1` will appear first in the re-optimized selection.
|
|
487
|
+
// So we put it first in the input too so that input and output actually match (the `testFragmentsRoundtrip` compares strings,
|
|
488
|
+
// so it is sensible to ordering; we could theoretically use `Operation.equals` instead of string equality, which wouldn't
|
|
489
|
+
// really on ordering, but `Operation.equals` is not entirely trivial and comparing strings make problem a bit more obvious).
|
|
490
|
+
query: `
|
|
491
|
+
fragment Frag1 on T {
|
|
492
|
+
b {
|
|
493
|
+
x
|
|
494
|
+
}
|
|
495
|
+
c
|
|
496
|
+
d {
|
|
497
|
+
m
|
|
498
|
+
}
|
|
499
|
+
}
|
|
317
500
|
|
|
318
|
-
|
|
319
|
-
|
|
501
|
+
fragment Frag2 on T {
|
|
502
|
+
a
|
|
503
|
+
b {
|
|
504
|
+
__typename
|
|
505
|
+
x
|
|
506
|
+
}
|
|
507
|
+
d {
|
|
508
|
+
m
|
|
509
|
+
n
|
|
510
|
+
}
|
|
511
|
+
}
|
|
320
512
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
513
|
+
{
|
|
514
|
+
t {
|
|
515
|
+
...Frag2
|
|
516
|
+
...Frag1
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
`,
|
|
520
|
+
expanded: `
|
|
521
|
+
{
|
|
522
|
+
t {
|
|
523
|
+
a
|
|
524
|
+
b {
|
|
525
|
+
__typename
|
|
526
|
+
x
|
|
527
|
+
}
|
|
528
|
+
d {
|
|
529
|
+
m
|
|
530
|
+
n
|
|
531
|
+
}
|
|
532
|
+
c
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
`,
|
|
536
|
+
});
|
|
324
537
|
});
|
|
325
538
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
539
|
+
test('fragments whose application makes a type condition trivial', () => {
|
|
540
|
+
const schema = parseSchema(`
|
|
541
|
+
type Query {
|
|
542
|
+
t: T
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
interface I {
|
|
546
|
+
x: String
|
|
547
|
+
}
|
|
331
548
|
|
|
332
|
-
|
|
333
|
-
|
|
549
|
+
type T implements I {
|
|
550
|
+
x: String
|
|
551
|
+
a: String
|
|
552
|
+
}
|
|
553
|
+
`);
|
|
334
554
|
|
|
335
|
-
|
|
555
|
+
testFragmentsRoundtrip({
|
|
556
|
+
schema,
|
|
557
|
+
query: `
|
|
558
|
+
fragment FragI on I {
|
|
559
|
+
x
|
|
560
|
+
... on T {
|
|
561
|
+
a
|
|
562
|
+
}
|
|
563
|
+
}
|
|
336
564
|
|
|
337
|
-
|
|
338
|
-
|
|
565
|
+
{
|
|
566
|
+
t {
|
|
567
|
+
...FragI
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
`,
|
|
571
|
+
expanded: `
|
|
572
|
+
{
|
|
573
|
+
t {
|
|
574
|
+
x
|
|
575
|
+
a
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
`,
|
|
579
|
+
});
|
|
339
580
|
});
|
|
340
581
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
582
|
+
describe('applied directives', () => {
|
|
583
|
+
test('reuse fragments with directives on the fragment, but only when there is those directives', () => {
|
|
584
|
+
const schema = parseSchema(`
|
|
585
|
+
type Query {
|
|
586
|
+
t1: T
|
|
587
|
+
t2: T
|
|
588
|
+
t3: T
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
type T {
|
|
592
|
+
a: Int
|
|
593
|
+
b: Int
|
|
594
|
+
c: Int
|
|
595
|
+
d: Int
|
|
596
|
+
}
|
|
597
|
+
`);
|
|
598
|
+
|
|
599
|
+
testFragmentsRoundtrip({
|
|
600
|
+
schema,
|
|
601
|
+
query: `
|
|
602
|
+
fragment DirectiveOnDef on T @include(if: $cond1) {
|
|
603
|
+
a
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
query myQuery($cond1: Boolean!, $cond2: Boolean!) {
|
|
607
|
+
t1 {
|
|
608
|
+
...DirectiveOnDef
|
|
609
|
+
}
|
|
610
|
+
t2 {
|
|
611
|
+
... on T @include(if: $cond2) {
|
|
612
|
+
a
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
t3 {
|
|
616
|
+
...DirectiveOnDef @include(if: $cond2)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
`,
|
|
620
|
+
expanded: `
|
|
621
|
+
query myQuery($cond1: Boolean!, $cond2: Boolean!) {
|
|
622
|
+
t1 {
|
|
623
|
+
... on T @include(if: $cond1) {
|
|
624
|
+
a
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
t2 {
|
|
628
|
+
... on T @include(if: $cond2) {
|
|
629
|
+
a
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
t3 {
|
|
633
|
+
... on T @include(if: $cond1) @include(if: $cond2) {
|
|
634
|
+
a
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
`,
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test('reuse fragments with directives in the fragment selection, but only when there is those directives', () => {
|
|
643
|
+
const schema = parseSchema(`
|
|
644
|
+
type Query {
|
|
645
|
+
t1: T
|
|
646
|
+
t2: T
|
|
647
|
+
t3: T
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
type T {
|
|
651
|
+
a: Int
|
|
652
|
+
b: Int
|
|
653
|
+
c: Int
|
|
654
|
+
d: Int
|
|
655
|
+
}
|
|
656
|
+
`);
|
|
657
|
+
|
|
658
|
+
testFragmentsRoundtrip({
|
|
659
|
+
schema,
|
|
660
|
+
query: `
|
|
661
|
+
fragment DirectiveInDef on T {
|
|
662
|
+
a @include(if: $cond1)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
query myQuery($cond1: Boolean!, $cond2: Boolean!) {
|
|
666
|
+
t1 {
|
|
667
|
+
a
|
|
668
|
+
}
|
|
669
|
+
t2 {
|
|
670
|
+
...DirectiveInDef
|
|
671
|
+
}
|
|
672
|
+
t3 {
|
|
673
|
+
a @include(if: $cond2)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
`,
|
|
677
|
+
expanded: `
|
|
678
|
+
query myQuery($cond1: Boolean!, $cond2: Boolean!) {
|
|
679
|
+
t1 {
|
|
680
|
+
a
|
|
681
|
+
}
|
|
682
|
+
t2 {
|
|
683
|
+
a @include(if: $cond1)
|
|
684
|
+
}
|
|
685
|
+
t3 {
|
|
686
|
+
a @include(if: $cond2)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
`,
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test('reuse fragments with directives on spread, but only when there is those directives', () => {
|
|
694
|
+
const schema = parseSchema(`
|
|
695
|
+
type Query {
|
|
696
|
+
t1: T
|
|
697
|
+
t2: T
|
|
698
|
+
t3: T
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
type T {
|
|
702
|
+
a: Int
|
|
703
|
+
b: Int
|
|
704
|
+
c: Int
|
|
705
|
+
d: Int
|
|
706
|
+
}
|
|
707
|
+
`);
|
|
708
|
+
|
|
709
|
+
testFragmentsRoundtrip({
|
|
710
|
+
schema,
|
|
711
|
+
query: `
|
|
712
|
+
fragment NoDirectiveDef on T {
|
|
713
|
+
a
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
query myQuery($cond1: Boolean!) {
|
|
717
|
+
t1 {
|
|
718
|
+
...NoDirectiveDef
|
|
719
|
+
}
|
|
720
|
+
t2 {
|
|
721
|
+
...NoDirectiveDef @include(if: $cond1)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
`,
|
|
725
|
+
expanded: `
|
|
726
|
+
query myQuery($cond1: Boolean!) {
|
|
727
|
+
t1 {
|
|
728
|
+
a
|
|
729
|
+
}
|
|
730
|
+
t2 {
|
|
731
|
+
... on T @include(if: $cond1) {
|
|
732
|
+
a
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
`,
|
|
737
|
+
});
|
|
738
|
+
});
|
|
360
739
|
});
|
|
361
740
|
});
|
|
362
741
|
|
|
@@ -517,3 +896,206 @@ describe('empty branches removal', () => {
|
|
|
517
896
|
)).toBe('{ u }');
|
|
518
897
|
});
|
|
519
898
|
});
|
|
899
|
+
|
|
900
|
+
describe('basic operations', () => {
|
|
901
|
+
const schema = parseSchema(`
|
|
902
|
+
type Query {
|
|
903
|
+
t: T
|
|
904
|
+
i: I
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
type T {
|
|
908
|
+
v1: Int
|
|
909
|
+
v2: String
|
|
910
|
+
v3: I
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
interface I {
|
|
914
|
+
x: Int
|
|
915
|
+
y: Int
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
type A implements I {
|
|
919
|
+
x: Int
|
|
920
|
+
y: Int
|
|
921
|
+
a1: String
|
|
922
|
+
a2: String
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
type B implements I {
|
|
926
|
+
x: Int
|
|
927
|
+
y: Int
|
|
928
|
+
b1: Int
|
|
929
|
+
b2: T
|
|
930
|
+
}
|
|
931
|
+
`);
|
|
932
|
+
|
|
933
|
+
const operation = parseOperation(schema, `
|
|
934
|
+
{
|
|
935
|
+
t {
|
|
936
|
+
v1
|
|
937
|
+
v3 {
|
|
938
|
+
x
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
i {
|
|
942
|
+
... on A {
|
|
943
|
+
a1
|
|
944
|
+
a2
|
|
945
|
+
}
|
|
946
|
+
... on B {
|
|
947
|
+
y
|
|
948
|
+
b2 {
|
|
949
|
+
v2
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
`);
|
|
955
|
+
|
|
956
|
+
test('forEachElement', () => {
|
|
957
|
+
// We collect a pair of (parent type, field-or-fragment).
|
|
958
|
+
const actual: [string, string][] = [];
|
|
959
|
+
operation.selectionSet.forEachElement((elt) => actual.push([elt.parentType.name, elt.toString()]));
|
|
960
|
+
expect(actual).toStrictEqual([
|
|
961
|
+
['Query', 't'],
|
|
962
|
+
['T', 'v1'],
|
|
963
|
+
['T', 'v3'],
|
|
964
|
+
['I', 'x'],
|
|
965
|
+
['Query', 'i'],
|
|
966
|
+
['I', '... on A'],
|
|
967
|
+
['A', 'a1'],
|
|
968
|
+
['A', 'a2'],
|
|
969
|
+
['I', '... on B'],
|
|
970
|
+
['B', 'y'],
|
|
971
|
+
['B', 'b2'],
|
|
972
|
+
['T', 'v2'],
|
|
973
|
+
]);
|
|
974
|
+
})
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
describe('MutableSelectionSet', () => {
|
|
978
|
+
test('memoizer', () => {
|
|
979
|
+
const schema = parseSchema(`
|
|
980
|
+
type Query {
|
|
981
|
+
t: T
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
type T {
|
|
985
|
+
v1: Int
|
|
986
|
+
v2: String
|
|
987
|
+
v3: Int
|
|
988
|
+
v4: Int
|
|
989
|
+
}
|
|
990
|
+
`);
|
|
991
|
+
|
|
992
|
+
type Value = {
|
|
993
|
+
count: number
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
let calls = 0;
|
|
997
|
+
const sets: string[] = [];
|
|
998
|
+
|
|
999
|
+
const queryType = schema.schemaDefinition.rootType('query')!;
|
|
1000
|
+
const ss = MutableSelectionSet.emptyWithMemoized<Value>(
|
|
1001
|
+
queryType,
|
|
1002
|
+
(s) => {
|
|
1003
|
+
sets.push(s.toString());
|
|
1004
|
+
return { count: ++calls };
|
|
1005
|
+
}
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
expect(ss.memoized().count).toBe(1);
|
|
1009
|
+
// Calling a 2nd time with no change to make sure we're not re-generating the value.
|
|
1010
|
+
expect(ss.memoized().count).toBe(1);
|
|
1011
|
+
|
|
1012
|
+
ss.updates().add(parseOperation(schema, `{ t { v1 } }`).selectionSet);
|
|
1013
|
+
|
|
1014
|
+
expect(ss.memoized().count).toBe(2);
|
|
1015
|
+
expect(sets).toStrictEqual(['{}', '{ t { v1 } }']);
|
|
1016
|
+
|
|
1017
|
+
ss.updates().add(parseOperation(schema, `{ t { v3 } }`).selectionSet);
|
|
1018
|
+
|
|
1019
|
+
expect(ss.memoized().count).toBe(3);
|
|
1020
|
+
expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }']);
|
|
1021
|
+
|
|
1022
|
+
// Still making sure we don't re-compute without updates.
|
|
1023
|
+
expect(ss.memoized().count).toBe(3);
|
|
1024
|
+
|
|
1025
|
+
const cloned = ss.clone();
|
|
1026
|
+
expect(cloned.memoized().count).toBe(3);
|
|
1027
|
+
|
|
1028
|
+
cloned.updates().add(parseOperation(schema, `{ t { v2 } }`).selectionSet);
|
|
1029
|
+
|
|
1030
|
+
// The value of `ss` should not have be recomputed, so it should still be 3.
|
|
1031
|
+
expect(ss.memoized().count).toBe(3);
|
|
1032
|
+
// But that of the clone should have changed.
|
|
1033
|
+
expect(cloned.memoized().count).toBe(4);
|
|
1034
|
+
expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }']);
|
|
1035
|
+
|
|
1036
|
+
// And here we make sure that if we update the fist selection, we don't have v3 in the set received
|
|
1037
|
+
ss.updates().add(parseOperation(schema, `{ t { v4 } }`).selectionSet);
|
|
1038
|
+
// Here, only `ss` memoized value has been recomputed. But since both increment the same `calls` variable,
|
|
1039
|
+
// the total count should be 5 (even if the previous count for `ss` was only 3).
|
|
1040
|
+
expect(ss.memoized().count).toBe(5);
|
|
1041
|
+
expect(cloned.memoized().count).toBe(4);
|
|
1042
|
+
expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }', '{ t { v1 v3 v4 } }']);
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
describe('unsatisfiable branches removal', () => {
|
|
1047
|
+
const schema = parseSchema(`
|
|
1048
|
+
type Query {
|
|
1049
|
+
i: I
|
|
1050
|
+
j: J
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
interface I {
|
|
1054
|
+
a: Int
|
|
1055
|
+
b: Int
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
interface J {
|
|
1059
|
+
b: Int
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
type T1 implements I & J {
|
|
1063
|
+
a: Int
|
|
1064
|
+
b: Int
|
|
1065
|
+
c: Int
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
type T2 implements I {
|
|
1069
|
+
a: Int
|
|
1070
|
+
b: Int
|
|
1071
|
+
d: Int
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
type T3 implements J {
|
|
1075
|
+
a: Int
|
|
1076
|
+
b: Int
|
|
1077
|
+
d: Int
|
|
1078
|
+
}
|
|
1079
|
+
`);
|
|
1080
|
+
|
|
1081
|
+
const withoutUnsatisfiableBranches = (op: string) => {
|
|
1082
|
+
return parseOperation(schema, op).trimUnsatisfiableBranches().toString(false, false)
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
it.each([
|
|
1087
|
+
'{ i { a } }',
|
|
1088
|
+
'{ i { ... on T1 { a b c } } }',
|
|
1089
|
+
])('is identity if there is no unsatisfiable branches', (op) => {
|
|
1090
|
+
expect(withoutUnsatisfiableBranches(op)).toBe(op);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
it.each([
|
|
1094
|
+
{ input: '{ i { ... on I { a } } }', output: '{ i { a } }' },
|
|
1095
|
+
{ input: '{ i { ... on T1 { ... on I { a b } } } }', output: '{ i { ... on T1 { a b } } }' },
|
|
1096
|
+
{ input: '{ i { ... on I { a ... on T2 { d } } } }', output: '{ i { a ... on T2 { d } } }' },
|
|
1097
|
+
{ input: '{ i { ... on T2 { ... on I { a ... on J { b } } } } }', output: '{ i { ... on T2 { a } } }' },
|
|
1098
|
+
])('removes unsatisfiable branches', ({input, output}) => {
|
|
1099
|
+
expect(withoutUnsatisfiableBranches(input)).toBe(output);
|
|
1100
|
+
});
|
|
1101
|
+
});
|