@apollo/federation-internals 2.4.0-alpha.0 → 2.4.0
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 +25 -0
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +2 -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 +131 -88
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +774 -581
- 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 +148 -119
- package/src/coreSpec.ts +1 -0
- package/src/definitions.ts +53 -57
- package/src/federation.ts +27 -23
- package/src/operations.ts +1120 -811
- 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
|
|
|
@@ -242,124 +242,6 @@ describe('fragments optimization', () => {
|
|
|
242
242
|
});
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
describe('selection set freezing', () => {
|
|
246
|
-
const schema = parseSchema(`
|
|
247
|
-
type Query {
|
|
248
|
-
t: T
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
type T {
|
|
252
|
-
a: Int
|
|
253
|
-
b: Int
|
|
254
|
-
}
|
|
255
|
-
`);
|
|
256
|
-
|
|
257
|
-
const tField = schema.schemaDefinition.rootType('query')!.field('t')!;
|
|
258
|
-
|
|
259
|
-
it('throws if one tries to modify a frozen selection set', () => {
|
|
260
|
-
// Note that we use parseOperation to help us build selection/selection sets because it's more readable/convenient
|
|
261
|
-
// thant to build the object "programmatically".
|
|
262
|
-
const s1 = parseOperation(schema, `{ t { a } }`).selectionSet;
|
|
263
|
-
const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
|
|
264
|
-
|
|
265
|
-
const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
|
|
266
|
-
|
|
267
|
-
// Control: check we can add to the selection set while not yet frozen
|
|
268
|
-
expect(s.isFrozen()).toBeFalsy();
|
|
269
|
-
expect(() => s.mergeIn(s1)).not.toThrow();
|
|
270
|
-
|
|
271
|
-
s.freeze();
|
|
272
|
-
expect(s.isFrozen()).toBeTruthy();
|
|
273
|
-
expect(() => s.mergeIn(s2)).toThrowError('Cannot add to frozen selection: { t { a } }');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('it does not clone non-frozen selections when adding to another one', () => {
|
|
277
|
-
// This test is a bit debatable because what it tests is not so much a behaviour
|
|
278
|
-
// we *need* absolutely to preserve, but rather test how things happens to
|
|
279
|
-
// behave currently and illustrate the current contrast between frozen and
|
|
280
|
-
// non-frozen selection set.
|
|
281
|
-
// That is, this test show what happens if the test
|
|
282
|
-
// "it automaticaly clones frozen selections when adding to another one"
|
|
283
|
-
// is done without freezing.
|
|
284
|
-
|
|
285
|
-
const s1 = parseOperation(schema, `{ t { a } }`).selectionSet;
|
|
286
|
-
const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
|
|
287
|
-
const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
|
|
288
|
-
|
|
289
|
-
s.mergeIn(s1);
|
|
290
|
-
s.mergeIn(s2);
|
|
291
|
-
|
|
292
|
-
expect(s.toString()).toBe('{ t { a b } }');
|
|
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
|
-
});
|
|
309
|
-
|
|
310
|
-
it('it automaticaly clones frozen field selections when merging to another one', () => {
|
|
311
|
-
const s1 = parseOperation(schema, `{ t { a } }`).selectionSet.freeze();
|
|
312
|
-
const s2 = parseOperation(schema, `{ t { b } }`).selectionSet.freeze();
|
|
313
|
-
const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
|
|
314
|
-
|
|
315
|
-
s.mergeIn(s1);
|
|
316
|
-
s.mergeIn(s2);
|
|
317
|
-
|
|
318
|
-
// We check S is what we expect...
|
|
319
|
-
expect(s.toString()).toBe('{ t { a b } }');
|
|
320
|
-
|
|
321
|
-
// ... but more importantly for this test, that s1/s2 were not modified.
|
|
322
|
-
expect(s1.toString()).toBe('{ t { a } }');
|
|
323
|
-
expect(s2.toString()).toBe('{ t { b } }');
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('it automaticaly clones frozen fragment selections when merging to another one', () => {
|
|
327
|
-
// Note: needlessly complex queries, but we're just ensuring the cloning story works when fragments are involved
|
|
328
|
-
const s1 = parseOperation(schema, `{ ... on Query { t { ... on T { a } } } }`).selectionSet.freeze();
|
|
329
|
-
const s2 = parseOperation(schema, `{ ... on Query { t { ... on T { b } } } }`).selectionSet.freeze();
|
|
330
|
-
const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
|
|
331
|
-
|
|
332
|
-
s.mergeIn(s1);
|
|
333
|
-
s.mergeIn(s2);
|
|
334
|
-
|
|
335
|
-
expect(s.toString()).toBe('{ ... on Query { t { ... on T { a b } } } }');
|
|
336
|
-
|
|
337
|
-
expect(s1.toString()).toBe('{ ... on Query { t { ... on T { a } } } }');
|
|
338
|
-
expect(s2.toString()).toBe('{ ... on Query { t { ... on T { b } } } }');
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('it automaticaly clones frozen field selections when adding to another one', () => {
|
|
342
|
-
const s1 = parseOperation(schema, `{ t { a } }`).selectionSet.freeze();
|
|
343
|
-
const s2 = parseOperation(schema, `{ t { b } }`).selectionSet.freeze();
|
|
344
|
-
const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
|
|
345
|
-
const tSelection = new FieldSelection(new Field(tField));
|
|
346
|
-
s.add(tSelection);
|
|
347
|
-
|
|
348
|
-
// Note that this test both checks the auto-cloning for the `add` method, but
|
|
349
|
-
// also shows that freezing dose apply deeply (since we freeze the whole `s1`/`s2`
|
|
350
|
-
// but only add some sub-selection of it).
|
|
351
|
-
tSelection.selectionSet!.add(s1.selections()[0].selectionSet!.selections()[0]);
|
|
352
|
-
tSelection.selectionSet!.add(s2.selections()[0].selectionSet!.selections()[0]);
|
|
353
|
-
|
|
354
|
-
// We check S is what we expect...
|
|
355
|
-
expect(s.toString()).toBe('{ t { a b } }');
|
|
356
|
-
|
|
357
|
-
// ... but more importantly for this test, that s1/s2 were not modified.
|
|
358
|
-
expect(s1.toString()).toBe('{ t { a } }');
|
|
359
|
-
expect(s2.toString()).toBe('{ t { b } }');
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
|
|
363
245
|
describe('validations', () => {
|
|
364
246
|
test.each([
|
|
365
247
|
{ directive: '@defer', rootKind: 'mutation' },
|
|
@@ -517,3 +399,150 @@ describe('empty branches removal', () => {
|
|
|
517
399
|
)).toBe('{ u }');
|
|
518
400
|
});
|
|
519
401
|
});
|
|
402
|
+
|
|
403
|
+
describe('basic operations', () => {
|
|
404
|
+
const schema = parseSchema(`
|
|
405
|
+
type Query {
|
|
406
|
+
t: T
|
|
407
|
+
i: I
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
type T {
|
|
411
|
+
v1: Int
|
|
412
|
+
v2: String
|
|
413
|
+
v3: I
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
interface I {
|
|
417
|
+
x: Int
|
|
418
|
+
y: Int
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
type A implements I {
|
|
422
|
+
x: Int
|
|
423
|
+
y: Int
|
|
424
|
+
a1: String
|
|
425
|
+
a2: String
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
type B implements I {
|
|
429
|
+
x: Int
|
|
430
|
+
y: Int
|
|
431
|
+
b1: Int
|
|
432
|
+
b2: T
|
|
433
|
+
}
|
|
434
|
+
`);
|
|
435
|
+
|
|
436
|
+
const operation = parseOperation(schema, `
|
|
437
|
+
{
|
|
438
|
+
t {
|
|
439
|
+
v1
|
|
440
|
+
v3 {
|
|
441
|
+
x
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
i {
|
|
445
|
+
... on A {
|
|
446
|
+
a1
|
|
447
|
+
a2
|
|
448
|
+
}
|
|
449
|
+
... on B {
|
|
450
|
+
y
|
|
451
|
+
b2 {
|
|
452
|
+
v2
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
`);
|
|
458
|
+
|
|
459
|
+
test('forEachElement', () => {
|
|
460
|
+
// We collect a pair of (parent type, field-or-fragment).
|
|
461
|
+
const actual: [string, string][] = [];
|
|
462
|
+
operation.selectionSet.forEachElement((elt) => actual.push([elt.parentType.name, elt.toString()]));
|
|
463
|
+
expect(actual).toStrictEqual([
|
|
464
|
+
['Query', 't'],
|
|
465
|
+
['T', 'v1'],
|
|
466
|
+
['T', 'v3'],
|
|
467
|
+
['I', 'x'],
|
|
468
|
+
['Query', 'i'],
|
|
469
|
+
['I', '... on A'],
|
|
470
|
+
['A', 'a1'],
|
|
471
|
+
['A', 'a2'],
|
|
472
|
+
['I', '... on B'],
|
|
473
|
+
['B', 'y'],
|
|
474
|
+
['B', 'b2'],
|
|
475
|
+
['T', 'v2'],
|
|
476
|
+
]);
|
|
477
|
+
})
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
describe('MutableSelectionSet', () => {
|
|
482
|
+
test('memoizer', () => {
|
|
483
|
+
const schema = parseSchema(`
|
|
484
|
+
type Query {
|
|
485
|
+
t: T
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
type T {
|
|
489
|
+
v1: Int
|
|
490
|
+
v2: String
|
|
491
|
+
v3: Int
|
|
492
|
+
v4: Int
|
|
493
|
+
}
|
|
494
|
+
`);
|
|
495
|
+
|
|
496
|
+
type Value = {
|
|
497
|
+
count: number
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
let calls = 0;
|
|
501
|
+
const sets: string[] = [];
|
|
502
|
+
|
|
503
|
+
const queryType = schema.schemaDefinition.rootType('query')!;
|
|
504
|
+
const ss = MutableSelectionSet.emptyWithMemoized<Value>(
|
|
505
|
+
queryType,
|
|
506
|
+
(s) => {
|
|
507
|
+
sets.push(s.toString());
|
|
508
|
+
return { count: ++calls };
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
expect(ss.memoized().count).toBe(1);
|
|
513
|
+
// Calling a 2nd time with no change to make sure we're not re-generating the value.
|
|
514
|
+
expect(ss.memoized().count).toBe(1);
|
|
515
|
+
|
|
516
|
+
ss.updates().add(parseOperation(schema, `{ t { v1 } }`).selectionSet);
|
|
517
|
+
|
|
518
|
+
expect(ss.memoized().count).toBe(2);
|
|
519
|
+
expect(sets).toStrictEqual(['{}', '{ t { v1 } }']);
|
|
520
|
+
|
|
521
|
+
ss.updates().add(parseOperation(schema, `{ t { v3 } }`).selectionSet);
|
|
522
|
+
|
|
523
|
+
expect(ss.memoized().count).toBe(3);
|
|
524
|
+
expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }']);
|
|
525
|
+
|
|
526
|
+
// Still making sure we don't re-compute without updates.
|
|
527
|
+
expect(ss.memoized().count).toBe(3);
|
|
528
|
+
|
|
529
|
+
const cloned = ss.clone();
|
|
530
|
+
expect(cloned.memoized().count).toBe(3);
|
|
531
|
+
|
|
532
|
+
cloned.updates().add(parseOperation(schema, `{ t { v2 } }`).selectionSet);
|
|
533
|
+
|
|
534
|
+
// The value of `ss` should not have be recomputed, so it should still be 3.
|
|
535
|
+
expect(ss.memoized().count).toBe(3);
|
|
536
|
+
// But that of the clone should have changed.
|
|
537
|
+
expect(cloned.memoized().count).toBe(4);
|
|
538
|
+
expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }']);
|
|
539
|
+
|
|
540
|
+
// And here we make sure that if we update the fist selection, we don't have v3 in the set received
|
|
541
|
+
ss.updates().add(parseOperation(schema, `{ t { v4 } }`).selectionSet);
|
|
542
|
+
// Here, only `ss` memoized value has been recomputed. But since both increment the same `calls` variable,
|
|
543
|
+
// the total count should be 5 (even if the previous count for `ss` was only 3).
|
|
544
|
+
expect(ss.memoized().count).toBe(5);
|
|
545
|
+
expect(cloned.memoized().count).toBe(4);
|
|
546
|
+
expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }', '{ t { v1 v3 v4 } }']);
|
|
547
|
+
});
|
|
548
|
+
});
|
package/src/coreSpec.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ASTNode, DirectiveLocation, GraphQLError, StringValueNode } from "graphql";
|
|
2
|
+
import { URL } from "url";
|
|
2
3
|
import { CoreFeature, Directive, DirectiveDefinition, EnumType, ErrGraphQLAPISchemaValidationFailed, ErrGraphQLValidationFailed, InputType, ListType, NamedType, NonNullType, ScalarType, Schema, SchemaDefinition, SchemaElement, sourceASTs } from "./definitions";
|
|
3
4
|
import { sameType } from "./types";
|
|
4
5
|
import { assert, firstOf } from './utils';
|
package/src/definitions.ts
CHANGED
|
@@ -32,7 +32,16 @@ import {
|
|
|
32
32
|
removeAllCoreFeatures,
|
|
33
33
|
} from "./coreSpec";
|
|
34
34
|
import { assert, mapValues, MapWithCachedArrays, removeArrayElement } from "./utils";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
withDefaultValues,
|
|
37
|
+
valueEquals,
|
|
38
|
+
valueToString,
|
|
39
|
+
valueToAST,
|
|
40
|
+
valueFromAST,
|
|
41
|
+
valueNodeToConstValueNode,
|
|
42
|
+
argumentsEquals,
|
|
43
|
+
collectVariablesInValue
|
|
44
|
+
} from "./values";
|
|
36
45
|
import { removeInaccessibleElements } from "./inaccessibleSpec";
|
|
37
46
|
import { printDirectiveDefinition, printSchema } from './print';
|
|
38
47
|
import { sameType } from './types';
|
|
@@ -343,54 +352,39 @@ export interface Named {
|
|
|
343
352
|
export type ExtendableElement = SchemaDefinition | NamedType;
|
|
344
353
|
|
|
345
354
|
export class DirectiveTargetElement<T extends DirectiveTargetElement<T>> {
|
|
346
|
-
|
|
355
|
+
readonly appliedDirectives: Directive<T>[];
|
|
347
356
|
|
|
348
|
-
constructor(
|
|
357
|
+
constructor(
|
|
358
|
+
private readonly _schema: Schema,
|
|
359
|
+
directives: readonly Directive<any>[] = [],
|
|
360
|
+
) {
|
|
361
|
+
this.appliedDirectives = directives.map((d) => this.attachDirective(d));
|
|
362
|
+
}
|
|
349
363
|
|
|
350
364
|
schema(): Schema {
|
|
351
365
|
return this._schema;
|
|
352
366
|
}
|
|
353
367
|
|
|
368
|
+
private attachDirective(directive: Directive<any>): Directive<T> {
|
|
369
|
+
// if the directive is not attached, we can assume we're fine just attaching it to use. Otherwise, we're "copying" it.
|
|
370
|
+
const toAdd = directive.isAttached()
|
|
371
|
+
? new Directive(directive.name, directive.arguments())
|
|
372
|
+
: directive;
|
|
373
|
+
|
|
374
|
+
Element.prototype['setParent'].call(toAdd, this);
|
|
375
|
+
return toAdd;
|
|
376
|
+
}
|
|
377
|
+
|
|
354
378
|
appliedDirectivesOf<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(nameOrDefinition: string | DirectiveDefinition<TApplicationArgs>): Directive<T, TApplicationArgs>[] {
|
|
355
379
|
const directiveName = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
|
|
356
380
|
return this.appliedDirectives.filter(d => d.name == directiveName) as Directive<T, TApplicationArgs>[];
|
|
357
381
|
}
|
|
358
382
|
|
|
359
|
-
get appliedDirectives(): readonly Directive<T>[] {
|
|
360
|
-
return this._appliedDirectives ?? [];
|
|
361
|
-
}
|
|
362
|
-
|
|
363
383
|
hasAppliedDirective(nameOrDefinition: string | DirectiveDefinition): boolean {
|
|
364
384
|
const directiveName = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
|
|
365
385
|
return this.appliedDirectives.some(d => d.name == directiveName);
|
|
366
386
|
}
|
|
367
387
|
|
|
368
|
-
applyDirective<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(
|
|
369
|
-
defOrDirective: Directive<T, TApplicationArgs> | DirectiveDefinition<TApplicationArgs>,
|
|
370
|
-
args?: TApplicationArgs
|
|
371
|
-
): Directive<T, TApplicationArgs> {
|
|
372
|
-
let toAdd: Directive<T, TApplicationArgs>;
|
|
373
|
-
if (defOrDirective instanceof Directive) {
|
|
374
|
-
if (defOrDirective.schema() != this.schema()) {
|
|
375
|
-
throw new Error(`Cannot add directive ${defOrDirective} to ${this} as it is attached to another schema`);
|
|
376
|
-
}
|
|
377
|
-
toAdd = defOrDirective;
|
|
378
|
-
if (args) {
|
|
379
|
-
toAdd.setArguments(args);
|
|
380
|
-
}
|
|
381
|
-
} else {
|
|
382
|
-
toAdd = new Directive<T, TApplicationArgs>(defOrDirective.name, args ?? Object.create(null));
|
|
383
|
-
}
|
|
384
|
-
Element.prototype['setParent'].call(toAdd, this);
|
|
385
|
-
// TODO: we should typecheck arguments or our TApplicationArgs business is just a lie.
|
|
386
|
-
if (this._appliedDirectives) {
|
|
387
|
-
this._appliedDirectives.push(toAdd);
|
|
388
|
-
} else {
|
|
389
|
-
this._appliedDirectives = [ toAdd ];
|
|
390
|
-
}
|
|
391
|
-
return toAdd;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
388
|
appliedDirectivesToDirectiveNodes() : ConstDirectiveNode[] | undefined {
|
|
395
389
|
if (this.appliedDirectives.length == 0) {
|
|
396
390
|
return undefined;
|
|
@@ -414,8 +408,10 @@ export class DirectiveTargetElement<T extends DirectiveTargetElement<T>> {
|
|
|
414
408
|
: ' ' + this.appliedDirectives.join(' ');
|
|
415
409
|
}
|
|
416
410
|
|
|
417
|
-
|
|
418
|
-
|
|
411
|
+
collectVariablesInAppliedDirectives(collector: VariableCollector) {
|
|
412
|
+
for (const applied of this.appliedDirectives) {
|
|
413
|
+
collector.collectInArguments(applied.arguments());
|
|
414
|
+
}
|
|
419
415
|
}
|
|
420
416
|
}
|
|
421
417
|
|
|
@@ -3069,7 +3065,7 @@ export class Directive<
|
|
|
3069
3065
|
// applied to a field that is part of an extension, the field will have its extension set, but not the underlying directive.
|
|
3070
3066
|
private _extension?: Extension<any>;
|
|
3071
3067
|
|
|
3072
|
-
constructor(readonly name: string, private _args: TArgs) {
|
|
3068
|
+
constructor(readonly name: string, private _args: TArgs = Object.create(null)) {
|
|
3073
3069
|
super();
|
|
3074
3070
|
}
|
|
3075
3071
|
|
|
@@ -3314,38 +3310,38 @@ export class Variable {
|
|
|
3314
3310
|
|
|
3315
3311
|
export type Variables = readonly Variable[];
|
|
3316
3312
|
|
|
3317
|
-
export
|
|
3318
|
-
|
|
3319
|
-
|
|
3313
|
+
export class VariableCollector {
|
|
3314
|
+
private readonly _variables = new Map<string, Variable>();
|
|
3315
|
+
|
|
3316
|
+
add(variable: Variable) {
|
|
3317
|
+
this._variables.set(variable.name, variable);
|
|
3320
3318
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3319
|
+
|
|
3320
|
+
addAll(variables: Variables) {
|
|
3321
|
+
for (const variable of variables) {
|
|
3322
|
+
this.add(variable);
|
|
3323
|
+
}
|
|
3323
3324
|
}
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3325
|
+
|
|
3326
|
+
collectInArguments(args: {[key: string]: any}) {
|
|
3327
|
+
for (const value of Object.values(args)) {
|
|
3328
|
+
collectVariablesInValue(value, this);
|
|
3328
3329
|
}
|
|
3329
3330
|
}
|
|
3330
|
-
return res;
|
|
3331
|
-
}
|
|
3332
3331
|
|
|
3333
|
-
|
|
3334
|
-
|
|
3332
|
+
variables() {
|
|
3333
|
+
return mapValues(this._variables);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
toString(): string {
|
|
3337
|
+
return this.variables().toString();
|
|
3338
|
+
}
|
|
3335
3339
|
}
|
|
3336
3340
|
|
|
3337
3341
|
export function isVariable(v: any): v is Variable {
|
|
3338
3342
|
return v instanceof Variable;
|
|
3339
3343
|
}
|
|
3340
3344
|
|
|
3341
|
-
export function variablesInArguments(args: {[key: string]: any}): Variables {
|
|
3342
|
-
let variables: Variables = [];
|
|
3343
|
-
for (const value of Object.values(args)) {
|
|
3344
|
-
variables = mergeVariables(variables, variablesInValue(value));
|
|
3345
|
-
}
|
|
3346
|
-
return variables;
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
3345
|
export class VariableDefinition extends DirectiveTargetElement<VariableDefinition> {
|
|
3350
3346
|
constructor(
|
|
3351
3347
|
schema: Schema,
|
package/src/federation.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
} from "graphql";
|
|
49
49
|
import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
|
|
50
50
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
51
|
-
import { parseSelectionSet,
|
|
51
|
+
import { parseSelectionSet, SelectionSet } from './operations';
|
|
52
52
|
import { TAG_VERSIONS } from "./tagSpec";
|
|
53
53
|
import {
|
|
54
54
|
errorCodeDef,
|
|
@@ -129,7 +129,7 @@ function validateFieldSetSelections({
|
|
|
129
129
|
allowFieldsWithArguments: boolean,
|
|
130
130
|
}): void {
|
|
131
131
|
for (const selection of selectionSet.selections()) {
|
|
132
|
-
const appliedDirectives = selection.element
|
|
132
|
+
const appliedDirectives = selection.element.appliedDirectives;
|
|
133
133
|
if (appliedDirectives.length > 0) {
|
|
134
134
|
onError(ERROR_CATEGORIES.DIRECTIVE_IN_FIELDS_ARG.get(directiveName).err(
|
|
135
135
|
`cannot have directive applications in the @${directiveName}(fields:) argument but found ${appliedDirectives.join(', ')}.`,
|
|
@@ -137,7 +137,7 @@ function validateFieldSetSelections({
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
if (selection.kind === 'FieldSelection') {
|
|
140
|
-
const field = selection.element
|
|
140
|
+
const field = selection.element.definition;
|
|
141
141
|
const isExternal = metadata.isFieldExternal(field);
|
|
142
142
|
if (!allowFieldsWithArguments && field.hasArguments()) {
|
|
143
143
|
onError(ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err(
|
|
@@ -1817,12 +1817,17 @@ class ExternalTester {
|
|
|
1817
1817
|
private collectProvidedFields() {
|
|
1818
1818
|
for (const provides of this.metadata().providesDirective().applications()) {
|
|
1819
1819
|
const parent = provides.parent as FieldDefinition<CompositeType>;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1820
|
+
const parentType = baseType(parent.type!);
|
|
1821
|
+
// If `parentType` is not a composite, that means an invalid @provides, but we ignore such errors
|
|
1822
|
+
// for now (also why we pass 'validate: false'). Proper errors will be thrown later during validation.
|
|
1823
|
+
if (isCompositeType(parentType)) {
|
|
1824
|
+
collectTargetFields({
|
|
1825
|
+
parentType,
|
|
1826
|
+
directive: provides as Directive<any, {fields: any}>,
|
|
1827
|
+
includeInterfaceFieldsImplementations: true,
|
|
1828
|
+
validate: false,
|
|
1829
|
+
}).forEach((f) => this.providedFields.add(f.coordinate));
|
|
1830
|
+
}
|
|
1826
1831
|
}
|
|
1827
1832
|
}
|
|
1828
1833
|
|
|
@@ -1854,7 +1859,7 @@ class ExternalTester {
|
|
|
1854
1859
|
|
|
1855
1860
|
selectsAnyExternalField(selectionSet: SelectionSet): boolean {
|
|
1856
1861
|
for (const selection of selectionSet.selections()) {
|
|
1857
|
-
if (selection.kind === 'FieldSelection' && this.isExternal(selection.element
|
|
1862
|
+
if (selection.kind === 'FieldSelection' && this.isExternal(selection.element.definition)) {
|
|
1858
1863
|
return true;
|
|
1859
1864
|
}
|
|
1860
1865
|
if (selection.selectionSet) {
|
|
@@ -1966,7 +1971,7 @@ function selectsNonExternalLeafField(selection: SelectionSet): boolean {
|
|
|
1966
1971
|
return selection.selections().some(s => {
|
|
1967
1972
|
if (s.kind === 'FieldSelection') {
|
|
1968
1973
|
// If it's external, we're good and don't need to recurse.
|
|
1969
|
-
if (isExternalOrHasExternalImplementations(s.
|
|
1974
|
+
if (isExternalOrHasExternalImplementations(s.element.definition)) {
|
|
1970
1975
|
return false;
|
|
1971
1976
|
}
|
|
1972
1977
|
// Otherwise, we select a non-external if it's a leaf, or the sub-selection does.
|
|
@@ -1978,25 +1983,24 @@ function selectsNonExternalLeafField(selection: SelectionSet): boolean {
|
|
|
1978
1983
|
}
|
|
1979
1984
|
|
|
1980
1985
|
function withoutNonExternalLeafFields(selectionSet: SelectionSet): SelectionSet {
|
|
1981
|
-
|
|
1982
|
-
for (const selection of selectionSet.selections()) {
|
|
1986
|
+
return selectionSet.lazyMap((selection) => {
|
|
1983
1987
|
if (selection.kind === 'FieldSelection') {
|
|
1984
|
-
if (isExternalOrHasExternalImplementations(selection.
|
|
1988
|
+
if (isExternalOrHasExternalImplementations(selection.element.definition)) {
|
|
1985
1989
|
// That field is external, so we can add the selection back entirely.
|
|
1986
|
-
|
|
1987
|
-
continue;
|
|
1990
|
+
return selection;
|
|
1988
1991
|
}
|
|
1989
1992
|
}
|
|
1990
|
-
// Note that for fragments will always be true (and we just recurse), while
|
|
1991
|
-
// for fields, we'll only get here if the field is not external, and so
|
|
1992
|
-
// we want to add the selection only if it's not a leaf and even then, only
|
|
1993
|
-
// the part where we've recursed.
|
|
1994
1993
|
if (selection.selectionSet) {
|
|
1994
|
+
// Note that for fragments this will always be true (and we just recurse), while
|
|
1995
|
+
// for fields, we'll only get here if the field is not external, and so
|
|
1996
|
+
// we want to add the selection only if it's not a leaf and even then, only
|
|
1997
|
+
// the part where we've recursed.
|
|
1995
1998
|
const updated = withoutNonExternalLeafFields(selection.selectionSet);
|
|
1996
1999
|
if (!updated.isEmpty()) {
|
|
1997
|
-
|
|
2000
|
+
return selection.withUpdatedSelectionSet(updated);
|
|
1998
2001
|
}
|
|
1999
2002
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2003
|
+
// We skip that selection.
|
|
2004
|
+
return undefined;
|
|
2005
|
+
});
|
|
2002
2006
|
}
|