@apollo/federation-internals 2.7.8 → 2.8.0-alpha.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/dist/directiveAndTypeSpecification.d.ts +13 -1
- package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
- package/dist/directiveAndTypeSpecification.js +2 -2
- package/dist/directiveAndTypeSpecification.js.map +1 -1
- package/dist/error.d.ts +6 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +12 -0
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +62 -7
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +15 -2
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +394 -4
- package/dist/federation.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +10 -8
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +37 -14
- package/dist/operations.js.map +1 -1
- package/dist/specs/contextSpec.d.ts +20 -0
- package/dist/specs/contextSpec.d.ts.map +1 -0
- package/dist/specs/contextSpec.js +62 -0
- package/dist/specs/contextSpec.js.map +1 -0
- package/dist/specs/federationSpec.d.ts +5 -2
- package/dist/specs/federationSpec.d.ts.map +1 -1
- package/dist/specs/federationSpec.js +9 -1
- package/dist/specs/federationSpec.js.map +1 -1
- package/dist/specs/joinSpec.d.ts +6 -0
- package/dist/specs/joinSpec.d.ts.map +1 -1
- package/dist/specs/joinSpec.js +11 -1
- package/dist/specs/joinSpec.js.map +1 -1
- package/dist/supergraphs.d.ts +4 -0
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +35 -2
- package/dist/supergraphs.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +39 -1
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/directiveAndTypeSpecification.ts +8 -1
- package/src/error.ts +42 -0
- package/src/extractSubgraphsFromSupergraph.ts +76 -14
- package/src/federation.ts +593 -10
- package/src/index.ts +1 -0
- package/src/operations.ts +48 -21
- package/src/specs/contextSpec.ts +87 -0
- package/src/specs/federationSpec.ts +10 -1
- package/src/specs/joinSpec.ts +27 -3
- package/src/supergraphs.ts +37 -1
- package/src/utils.ts +38 -0
package/src/federation.ts
CHANGED
|
@@ -27,8 +27,17 @@ import {
|
|
|
27
27
|
SchemaElement,
|
|
28
28
|
sourceASTs,
|
|
29
29
|
UnionType,
|
|
30
|
+
ArgumentDefinition,
|
|
31
|
+
InputType,
|
|
32
|
+
OutputType,
|
|
33
|
+
WrapperType,
|
|
34
|
+
isNonNullType,
|
|
35
|
+
isLeafType,
|
|
36
|
+
isListType,
|
|
37
|
+
isWrapperType,
|
|
38
|
+
possibleRuntimeTypes,
|
|
30
39
|
} from "./definitions";
|
|
31
|
-
import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues } from "./utils";
|
|
40
|
+
import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues, assertUnreachable } from "./utils";
|
|
32
41
|
import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
33
42
|
import { specifiedSDLRules } from "graphql/validation/specifiedRules";
|
|
34
43
|
import {
|
|
@@ -48,7 +57,7 @@ import {
|
|
|
48
57
|
} from "graphql";
|
|
49
58
|
import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
|
|
50
59
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
51
|
-
import { parseSelectionSet, SelectionSet } from './operations';
|
|
60
|
+
import { FragmentSelection, hasSelectionWithPredicate, parseOperationAST, parseSelectionSet, Selection, SelectionSet } from './operations';
|
|
52
61
|
import { TAG_VERSIONS } from "./specs/tagSpec";
|
|
53
62
|
import {
|
|
54
63
|
errorCodeDef,
|
|
@@ -336,6 +345,386 @@ function fieldSetTargetDescription(directive: Directive<any, {fields: any}>): st
|
|
|
336
345
|
return `${targetKind} "${directive.parent?.coordinate}"`;
|
|
337
346
|
}
|
|
338
347
|
|
|
348
|
+
export function parseContext(input: string) {
|
|
349
|
+
const regex = /^(?:[\n\r\t ,]|#[^\n\r]*(?![^\n\r]))*\$(?:[\n\r\t ,]|#[^\n\r]*(?![^\n\r]))*([A-Za-z_]\w*(?!\w))([\s\S]*)$/;
|
|
350
|
+
const match = input.match(regex);
|
|
351
|
+
if (!match) {
|
|
352
|
+
return { context: undefined, selection: undefined };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const [, context, selection] = match;
|
|
356
|
+
return {
|
|
357
|
+
context,
|
|
358
|
+
selection,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const wrapResolvedType = ({
|
|
363
|
+
originalType,
|
|
364
|
+
resolvedType,
|
|
365
|
+
}: {
|
|
366
|
+
originalType: OutputType,
|
|
367
|
+
resolvedType: InputType,
|
|
368
|
+
}): InputType | undefined => {
|
|
369
|
+
const stack = [];
|
|
370
|
+
let unwrappedType: NamedType | WrapperType = originalType;
|
|
371
|
+
while(unwrappedType.kind === 'NonNullType' || unwrappedType.kind === 'ListType') {
|
|
372
|
+
stack.push(unwrappedType.kind);
|
|
373
|
+
unwrappedType = unwrappedType.ofType;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let type: NamedType | WrapperType = resolvedType;
|
|
377
|
+
while(stack.length > 0) {
|
|
378
|
+
const kind = stack.pop();
|
|
379
|
+
if (kind === 'ListType') {
|
|
380
|
+
type = new ListType(type);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return type;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const validateFieldValueType = ({
|
|
387
|
+
currentType,
|
|
388
|
+
selectionSet,
|
|
389
|
+
errorCollector,
|
|
390
|
+
metadata,
|
|
391
|
+
fromContextParent,
|
|
392
|
+
}: {
|
|
393
|
+
currentType: CompositeType,
|
|
394
|
+
selectionSet: SelectionSet,
|
|
395
|
+
errorCollector: GraphQLError[],
|
|
396
|
+
metadata: FederationMetadata,
|
|
397
|
+
fromContextParent: ArgumentDefinition<FieldDefinition<ObjectType | InterfaceType | UnionType>>,
|
|
398
|
+
}): { resolvedType: InputType | undefined } => {
|
|
399
|
+
const selections = selectionSet.selections();
|
|
400
|
+
|
|
401
|
+
// ensure that type is not an interfaceObject
|
|
402
|
+
const interfaceObjectDirective = metadata.interfaceObjectDirective();
|
|
403
|
+
if (currentType.kind === 'ObjectType' && isFederationDirectiveDefinedInSchema(interfaceObjectDirective) && (currentType.appliedDirectivesOf(interfaceObjectDirective).length > 0)) {
|
|
404
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
405
|
+
`Context "is used in "${fromContextParent.coordinate}" but the selection is invalid: One of the types in the selection is an interfaceObject: "${currentType.name}"`,
|
|
406
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
407
|
+
));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const typesArray = selections.map((selection): { resolvedType: InputType | undefined } => {
|
|
411
|
+
if (selection.kind !== 'FieldSelection') {
|
|
412
|
+
return { resolvedType: undefined };
|
|
413
|
+
}
|
|
414
|
+
const { element, selectionSet: childSelectionSet } = selection;
|
|
415
|
+
assert(element.definition.type, 'Element type definition should exist');
|
|
416
|
+
const type = element.definition.type;
|
|
417
|
+
if (childSelectionSet) {
|
|
418
|
+
assert(isCompositeType(type), 'Child selection sets should only exist on composite types');
|
|
419
|
+
const { resolvedType } = validateFieldValueType({
|
|
420
|
+
currentType: type,
|
|
421
|
+
selectionSet: childSelectionSet,
|
|
422
|
+
errorCollector,
|
|
423
|
+
metadata,
|
|
424
|
+
fromContextParent,
|
|
425
|
+
});
|
|
426
|
+
if (!resolvedType) {
|
|
427
|
+
return { resolvedType: undefined };
|
|
428
|
+
}
|
|
429
|
+
return { resolvedType: wrapResolvedType({ originalType: type, resolvedType}) };
|
|
430
|
+
}
|
|
431
|
+
assert(isLeafType(baseType(type)), 'Expected a leaf type');
|
|
432
|
+
return {
|
|
433
|
+
resolvedType: wrapResolvedType({
|
|
434
|
+
originalType: type,
|
|
435
|
+
resolvedType: baseType(type) as InputType
|
|
436
|
+
})
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
return typesArray.reduce((acc, { resolvedType }) => {
|
|
440
|
+
if (acc.resolvedType?.toString() === resolvedType?.toString()) {
|
|
441
|
+
return { resolvedType };
|
|
442
|
+
}
|
|
443
|
+
return { resolvedType: undefined };
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const validateSelectionFormat = ({
|
|
448
|
+
context,
|
|
449
|
+
selection,
|
|
450
|
+
fromContextParent,
|
|
451
|
+
errorCollector,
|
|
452
|
+
} : {
|
|
453
|
+
context: string,
|
|
454
|
+
selection: string,
|
|
455
|
+
fromContextParent: ArgumentDefinition<FieldDefinition<ObjectType | InterfaceType | UnionType>>,
|
|
456
|
+
errorCollector: GraphQLError[],
|
|
457
|
+
}): {
|
|
458
|
+
selectionType: 'error' | 'field',
|
|
459
|
+
} | {
|
|
460
|
+
selectionType: 'inlineFragment',
|
|
461
|
+
typeConditions: Set<string>,
|
|
462
|
+
} => {
|
|
463
|
+
// we only need to parse the selection once, not do it for each location
|
|
464
|
+
try {
|
|
465
|
+
const node = parseOperationAST(selection.trim().startsWith('{') ? selection : `{${selection}}`);
|
|
466
|
+
const selections = node.selectionSet.selections;
|
|
467
|
+
if (selections.length === 0) {
|
|
468
|
+
// a selection must be made.
|
|
469
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
470
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: no selection is made`,
|
|
471
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
472
|
+
));
|
|
473
|
+
return { selectionType: 'error' };
|
|
474
|
+
}
|
|
475
|
+
const firstSelectionKind = selections[0].kind;
|
|
476
|
+
if (firstSelectionKind === 'Field') {
|
|
477
|
+
// if the first selection is a field, there should be only one
|
|
478
|
+
if (selections.length !== 1) {
|
|
479
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
480
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: multiple selections are made`,
|
|
481
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
482
|
+
));
|
|
483
|
+
return { selectionType: 'error' };
|
|
484
|
+
}
|
|
485
|
+
return { selectionType: 'field' };
|
|
486
|
+
} else if (firstSelectionKind === 'InlineFragment') {
|
|
487
|
+
const inlineFragmentTypeConditions: Set<string> = new Set();
|
|
488
|
+
if (!selections.every((s) => s.kind === 'InlineFragment')) {
|
|
489
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
490
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: multiple fields could be selected`,
|
|
491
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
492
|
+
));
|
|
493
|
+
return { selectionType: 'error' };
|
|
494
|
+
}
|
|
495
|
+
selections.forEach((s) => {
|
|
496
|
+
assert(s.kind === 'InlineFragment', 'Expected an inline fragment');
|
|
497
|
+
const { typeCondition }= s;
|
|
498
|
+
if (typeCondition) {
|
|
499
|
+
inlineFragmentTypeConditions.add(typeCondition.name.value);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
if (inlineFragmentTypeConditions.size !== selections.length) {
|
|
503
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
504
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type conditions have same name`,
|
|
505
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
506
|
+
));
|
|
507
|
+
return { selectionType: 'error' };
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
selectionType: 'inlineFragment',
|
|
511
|
+
typeConditions: inlineFragmentTypeConditions,
|
|
512
|
+
};
|
|
513
|
+
} else if (firstSelectionKind === 'FragmentSpread') {
|
|
514
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
515
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: fragment spread is not allowed`,
|
|
516
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
517
|
+
));
|
|
518
|
+
return { selectionType: 'error' };
|
|
519
|
+
} else {
|
|
520
|
+
assertUnreachable(firstSelectionKind);
|
|
521
|
+
}
|
|
522
|
+
} catch (err) {
|
|
523
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
524
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: ${err.message}`,
|
|
525
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
526
|
+
));
|
|
527
|
+
|
|
528
|
+
return { selectionType: 'error' };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// implementation of spec https://spec.graphql.org/draft/#IsValidImplementationFieldType()
|
|
533
|
+
function isValidImplementationFieldType(fieldType: InputType, implementedFieldType: InputType): boolean {
|
|
534
|
+
if (isNonNullType(fieldType)) {
|
|
535
|
+
if (isNonNullType(implementedFieldType)) {
|
|
536
|
+
return isValidImplementationFieldType(fieldType.ofType, implementedFieldType.ofType);
|
|
537
|
+
} else {
|
|
538
|
+
return isValidImplementationFieldType(fieldType.ofType, implementedFieldType);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (isListType(fieldType) && isListType(implementedFieldType)) {
|
|
542
|
+
return isValidImplementationFieldType(fieldType.ofType, implementedFieldType.ofType);
|
|
543
|
+
}
|
|
544
|
+
return !isWrapperType(fieldType) &&
|
|
545
|
+
!isWrapperType(implementedFieldType) &&
|
|
546
|
+
fieldType.name === implementedFieldType.name;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function selectionSetHasDirectives(selectionSet: SelectionSet): boolean {
|
|
550
|
+
return hasSelectionWithPredicate(selectionSet, (s: Selection) => {
|
|
551
|
+
if (s.kind === 'FieldSelection') {
|
|
552
|
+
return s.element.appliedDirectives.length > 0;
|
|
553
|
+
}
|
|
554
|
+
else if (s.kind === 'FragmentSelection') {
|
|
555
|
+
return s.element.appliedDirectives.length > 0;
|
|
556
|
+
} else {
|
|
557
|
+
assertUnreachable(s);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function selectionSetHasAlias(selectionSet: SelectionSet): boolean {
|
|
563
|
+
return hasSelectionWithPredicate(selectionSet, (s: Selection) => {
|
|
564
|
+
if (s.kind === 'FieldSelection') {
|
|
565
|
+
return s.element.alias !== undefined;
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function validateFieldValue({
|
|
572
|
+
context,
|
|
573
|
+
selection,
|
|
574
|
+
fromContextParent,
|
|
575
|
+
setContextLocations,
|
|
576
|
+
errorCollector,
|
|
577
|
+
metadata,
|
|
578
|
+
} : {
|
|
579
|
+
context: string,
|
|
580
|
+
selection: string,
|
|
581
|
+
fromContextParent: ArgumentDefinition<FieldDefinition<ObjectType | InterfaceType | UnionType>>,
|
|
582
|
+
setContextLocations: (ObjectType | InterfaceType | UnionType)[],
|
|
583
|
+
errorCollector: GraphQLError[],
|
|
584
|
+
metadata: FederationMetadata,
|
|
585
|
+
}): void {
|
|
586
|
+
const expectedType = fromContextParent.type;
|
|
587
|
+
assert(expectedType, 'Expected a type');
|
|
588
|
+
const validateSelectionFormatResults =
|
|
589
|
+
validateSelectionFormat({ context, selection, fromContextParent, errorCollector });
|
|
590
|
+
const selectionType = validateSelectionFormatResults.selectionType;
|
|
591
|
+
|
|
592
|
+
// if there was an error, just return, we've already added it to the errorCollector
|
|
593
|
+
if (selectionType === 'error') {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const usedTypeConditions = new Set<string>;
|
|
598
|
+
for (const location of setContextLocations) {
|
|
599
|
+
// for each location, we need to validate that the selection will result in exactly one field being selected
|
|
600
|
+
// the number of selection sets created will be the same
|
|
601
|
+
let selectionSet: SelectionSet;
|
|
602
|
+
try {
|
|
603
|
+
selectionSet = parseSelectionSet({ parentType: location, source: selection});
|
|
604
|
+
} catch (e) {
|
|
605
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
606
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid for type ${location.name}. Error: ${e.message}`,
|
|
607
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
608
|
+
));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (selectionSetHasDirectives(selectionSet)) {
|
|
612
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
613
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: directives are not allowed in the selection`,
|
|
614
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
615
|
+
));
|
|
616
|
+
}
|
|
617
|
+
if (selectionSetHasAlias(selectionSet)) {
|
|
618
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
619
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: aliases are not allowed in the selection`,
|
|
620
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
621
|
+
));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (selectionType === 'field') {
|
|
625
|
+
const { resolvedType } = validateFieldValueType({
|
|
626
|
+
currentType: location,
|
|
627
|
+
selectionSet,
|
|
628
|
+
errorCollector,
|
|
629
|
+
metadata,
|
|
630
|
+
fromContextParent,
|
|
631
|
+
});
|
|
632
|
+
if (resolvedType === undefined || !isValidImplementationFieldType(resolvedType, expectedType!)) {
|
|
633
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
634
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection "${resolvedType}" does not match the expected type "${expectedType?.toString()}"`,
|
|
635
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
636
|
+
));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
} else if (selectionType === 'inlineFragment') {
|
|
640
|
+
// ensure that each location maps to exactly one fragment
|
|
641
|
+
const selections: FragmentSelection[] = [];
|
|
642
|
+
for (const selection of selectionSet.selections()) {
|
|
643
|
+
if (selection.kind !== 'FragmentSelection') {
|
|
644
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
645
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: selection should only contain a single field or at least one inline fragment}"`,
|
|
646
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
647
|
+
));
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const { typeCondition } = selection.element;
|
|
652
|
+
if (!typeCondition) {
|
|
653
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
654
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: inline fragments must have type conditions"`,
|
|
655
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
656
|
+
));
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (typeCondition.kind === 'ObjectType') {
|
|
661
|
+
if (possibleRuntimeTypes(location).includes(typeCondition)) {
|
|
662
|
+
selections.push(selection);
|
|
663
|
+
usedTypeConditions.add(typeCondition.name);
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
667
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type conditions must be an object type"`,
|
|
668
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
669
|
+
));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (selections.length === 0) {
|
|
674
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
675
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: no type condition matches the location "${location.coordinate}"`,
|
|
676
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
677
|
+
));
|
|
678
|
+
return;
|
|
679
|
+
} else {
|
|
680
|
+
for (const selection of selections) {
|
|
681
|
+
let { resolvedType } = validateFieldValueType({
|
|
682
|
+
currentType: selection.element.typeCondition!,
|
|
683
|
+
selectionSet: selection.selectionSet,
|
|
684
|
+
errorCollector,
|
|
685
|
+
metadata,
|
|
686
|
+
fromContextParent,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
if (resolvedType === undefined) {
|
|
690
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
691
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection does not match the expected type "${expectedType?.toString()}"`,
|
|
692
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
693
|
+
));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Because other subgraphs may define members of the location type,
|
|
698
|
+
// it's always possible that none of the type conditions map, so we
|
|
699
|
+
// must remove any surrounding non-null wrapper if present.
|
|
700
|
+
if (isNonNullType(resolvedType)) {
|
|
701
|
+
resolvedType = resolvedType.ofType;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!isValidImplementationFieldType(resolvedType!, expectedType!)) {
|
|
705
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
706
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection "${resolvedType?.toString()}" does not match the expected type "${expectedType?.toString()}"`,
|
|
707
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
708
|
+
));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (validateSelectionFormatResults.selectionType === 'inlineFragment') {
|
|
717
|
+
for (const typeCondition of validateSelectionFormatResults.typeConditions) {
|
|
718
|
+
if (!usedTypeConditions.has(typeCondition)) {
|
|
719
|
+
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
|
|
720
|
+
`Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type condition "${typeCondition}" is never used.`,
|
|
721
|
+
{ nodes: sourceASTs(fromContextParent) }
|
|
722
|
+
));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
339
728
|
function validateAllFieldSet<TParent extends SchemaElement<any, any>>({
|
|
340
729
|
definition,
|
|
341
730
|
targetTypeExtractor,
|
|
@@ -404,7 +793,13 @@ export function collectUsedFields(metadata: FederationMetadata): Set<FieldDefini
|
|
|
404
793
|
},
|
|
405
794
|
usedFields,
|
|
406
795
|
);
|
|
407
|
-
|
|
796
|
+
|
|
797
|
+
// also for @fromContext
|
|
798
|
+
collectUsedFieldsForFromContext<CompositeType>(
|
|
799
|
+
metadata,
|
|
800
|
+
usedFields,
|
|
801
|
+
);
|
|
802
|
+
|
|
408
803
|
// Collects all fields used to satisfy an interface constraint
|
|
409
804
|
for (const itfType of metadata.schema.interfaceTypes()) {
|
|
410
805
|
const runtimeTypes = itfType.possibleRuntimeTypes();
|
|
@@ -421,6 +816,80 @@ export function collectUsedFields(metadata: FederationMetadata): Set<FieldDefini
|
|
|
421
816
|
return usedFields;
|
|
422
817
|
}
|
|
423
818
|
|
|
819
|
+
function collectUsedFieldsForFromContext<TParent extends SchemaElement<any, any>>(
|
|
820
|
+
metadata: FederationMetadata,
|
|
821
|
+
usedFieldDefs: Set<FieldDefinition<CompositeType>>
|
|
822
|
+
) {
|
|
823
|
+
const fromContextDirective = metadata.fromContextDirective();
|
|
824
|
+
const contextDirective = metadata.contextDirective();
|
|
825
|
+
|
|
826
|
+
// if one of the directives is not defined, there's nothing to validate
|
|
827
|
+
if (!isFederationDirectiveDefinedInSchema(fromContextDirective) || !isFederationDirectiveDefinedInSchema(contextDirective)) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// build the list of context entry points
|
|
832
|
+
const entryPoints = new Map<string, Set<CompositeType>>();
|
|
833
|
+
for (const application of contextDirective.applications()) {
|
|
834
|
+
const type = application.parent;
|
|
835
|
+
if (!type) {
|
|
836
|
+
// Means the application is wrong: we ignore it here as later validation will detect it
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const context = application.arguments().name;
|
|
840
|
+
if (!entryPoints.has(context)) {
|
|
841
|
+
entryPoints.set(context, new Set());
|
|
842
|
+
}
|
|
843
|
+
entryPoints.get(context)!.add(type as CompositeType);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
for (const application of fromContextDirective.applications()) {
|
|
847
|
+
const type = application.parent as TParent;
|
|
848
|
+
if (!type) {
|
|
849
|
+
// Means the application is wrong: we ignore it here as later validation will detect it
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const fieldValue = application.arguments().field;
|
|
854
|
+
const { context, selection } = parseContext(fieldValue);
|
|
855
|
+
|
|
856
|
+
if (!context) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// now we need to collect all the fields used for every type that they could be used for
|
|
861
|
+
const contextTypes = entryPoints.get(context);
|
|
862
|
+
if (!contextTypes) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
for (const contextType of contextTypes) {
|
|
867
|
+
try {
|
|
868
|
+
// helper function
|
|
869
|
+
const fieldAccessor = (t: CompositeType, f: string) => {
|
|
870
|
+
const field = t.field(f);
|
|
871
|
+
if (field) {
|
|
872
|
+
usedFieldDefs.add(field);
|
|
873
|
+
if (isInterfaceType(t)) {
|
|
874
|
+
for (const implType of t.possibleRuntimeTypes()) {
|
|
875
|
+
const implField = implType.field(f);
|
|
876
|
+
if (implField) {
|
|
877
|
+
usedFieldDefs.add(implField);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return field;
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
parseSelectionSet({ parentType: contextType, source: selection, fieldAccessor });
|
|
886
|
+
} catch (e) {
|
|
887
|
+
// ignore the error, it will be caught later
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
424
893
|
function collectUsedFieldsForDirective<TParent extends SchemaElement<any, any>>(
|
|
425
894
|
definition: DirectiveDefinition<{fields: any}>,
|
|
426
895
|
targetTypeExtractor: (element: TParent) => CompositeType | undefined,
|
|
@@ -459,7 +928,6 @@ function validateAllExternalFieldsUsed(metadata: FederationMetadata, errorCollec
|
|
|
459
928
|
if (!metadata.isFieldExternal(field) || metadata.isFieldUsed(field)) {
|
|
460
929
|
continue;
|
|
461
930
|
}
|
|
462
|
-
|
|
463
931
|
errorCollector.push(ERRORS.EXTERNAL_UNUSED.err(
|
|
464
932
|
`Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
|
|
465
933
|
+ ' the field declaration has no use and should be removed (or the field should not be @external).',
|
|
@@ -585,7 +1053,6 @@ function validateShareableNotRepeatedOnSameDeclaration(
|
|
|
585
1053
|
}
|
|
586
1054
|
}
|
|
587
1055
|
}
|
|
588
|
-
|
|
589
1056
|
export class FederationMetadata {
|
|
590
1057
|
private _externalTester?: ExternalTester;
|
|
591
1058
|
private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
|
|
@@ -799,6 +1266,14 @@ export class FederationMetadata {
|
|
|
799
1266
|
return this.getPost20FederationDirective(FederationDirectiveName.SOURCE_FIELD);
|
|
800
1267
|
}
|
|
801
1268
|
|
|
1269
|
+
fromContextDirective(): Post20FederationDirectiveDefinition<{ field: string }> {
|
|
1270
|
+
return this.getPost20FederationDirective(FederationDirectiveName.FROM_CONTEXT);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
contextDirective(): Post20FederationDirectiveDefinition<{ name: string }> {
|
|
1274
|
+
return this.getPost20FederationDirective(FederationDirectiveName.CONTEXT);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
802
1277
|
allFederationDirectives(): DirectiveDefinition[] {
|
|
803
1278
|
const baseDirectives: DirectiveDefinition[] = [
|
|
804
1279
|
this.keyDirective(),
|
|
@@ -852,6 +1327,16 @@ export class FederationMetadata {
|
|
|
852
1327
|
baseDirectives.push(sourceFieldDirective);
|
|
853
1328
|
}
|
|
854
1329
|
|
|
1330
|
+
const contextDirective = this.contextDirective();
|
|
1331
|
+
if (isFederationDirectiveDefinedInSchema(contextDirective)) {
|
|
1332
|
+
baseDirectives.push(contextDirective);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const fromContextDirective = this.fromContextDirective();
|
|
1336
|
+
if (isFederationDirectiveDefinedInSchema(fromContextDirective)) {
|
|
1337
|
+
baseDirectives.push(fromContextDirective);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
855
1340
|
return baseDirectives;
|
|
856
1341
|
}
|
|
857
1342
|
|
|
@@ -1084,6 +1569,106 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
1084
1569
|
metadata,
|
|
1085
1570
|
});
|
|
1086
1571
|
|
|
1572
|
+
// validate @context and @fromContext
|
|
1573
|
+
const contextDirective = metadata.contextDirective();
|
|
1574
|
+
const contextToTypeMap = new Map<string, (ObjectType | InterfaceType | UnionType)[]>();
|
|
1575
|
+
for (const application of contextDirective.applications()) {
|
|
1576
|
+
const parent = application.parent;
|
|
1577
|
+
const name = application.arguments().name as string;
|
|
1578
|
+
|
|
1579
|
+
if (name.includes('_')) {
|
|
1580
|
+
errorCollector.push(ERRORS.CONTEXT_NAME_INVALID.err(
|
|
1581
|
+
`Context name "${name}" may not contain an underscore.`,
|
|
1582
|
+
{ nodes: sourceASTs(application) }
|
|
1583
|
+
));
|
|
1584
|
+
}
|
|
1585
|
+
const types = contextToTypeMap.get(name);
|
|
1586
|
+
if (types) {
|
|
1587
|
+
types.push(parent);
|
|
1588
|
+
} else {
|
|
1589
|
+
contextToTypeMap.set(name, [parent]);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const fromContextDirective = metadata.fromContextDirective();
|
|
1594
|
+
for (const application of fromContextDirective.applications()) {
|
|
1595
|
+
const { field } = application.arguments();
|
|
1596
|
+
const { context, selection } = parseContext(field);
|
|
1597
|
+
|
|
1598
|
+
// error if parent's parent is a directive definition
|
|
1599
|
+
if (application.parent.parent.kind === 'DirectiveDefinition') {
|
|
1600
|
+
errorCollector.push(ERRORS.CONTEXT_NOT_SET.err(
|
|
1601
|
+
`@fromContext argument cannot be used on a directive definition "${application.parent.coordinate}".`,
|
|
1602
|
+
{ nodes: sourceASTs(application) }
|
|
1603
|
+
));
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const parent = application.parent as ArgumentDefinition<FieldDefinition<ObjectType | InterfaceType | UnionType>>;
|
|
1608
|
+
|
|
1609
|
+
// error if parent's parent is an interface
|
|
1610
|
+
if (parent?.parent?.parent?.kind !== 'ObjectType') {
|
|
1611
|
+
errorCollector.push(ERRORS.CONTEXT_NOT_SET.err(
|
|
1612
|
+
`@fromContext argument cannot be used on a field that exists on an abstract type "${application.parent.coordinate}".`,
|
|
1613
|
+
{ nodes: sourceASTs(application) }
|
|
1614
|
+
));
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// error if the parent's parent implements an interface containing the field
|
|
1619
|
+
const objectType = parent.parent.parent;
|
|
1620
|
+
for (const implementedInterfaceType of objectType.interfaces()) {
|
|
1621
|
+
const implementedInterfaceField = implementedInterfaceType.field(parent.parent.name);
|
|
1622
|
+
if (implementedInterfaceField) {
|
|
1623
|
+
errorCollector.push(ERRORS.CONTEXT_NOT_SET.err(
|
|
1624
|
+
`@fromContext argument cannot be used on a field implementing an interface field "${implementedInterfaceField.coordinate}".`,
|
|
1625
|
+
{ nodes: sourceASTs(application) }
|
|
1626
|
+
));
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (parent.defaultValue !== undefined) {
|
|
1631
|
+
errorCollector.push(ERRORS.CONTEXT_NOT_SET.err(
|
|
1632
|
+
`@fromContext arguments may not have a default value: "${parent.coordinate}".`,
|
|
1633
|
+
{ nodes: sourceASTs(application) }
|
|
1634
|
+
));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (!context || !selection) {
|
|
1638
|
+
errorCollector.push(ERRORS.NO_CONTEXT_IN_SELECTION.err(
|
|
1639
|
+
`@fromContext argument does not reference a context "${field}".`,
|
|
1640
|
+
{ nodes: sourceASTs(application) }
|
|
1641
|
+
));
|
|
1642
|
+
} else {
|
|
1643
|
+
const locations = contextToTypeMap.get(context);
|
|
1644
|
+
if (!locations) {
|
|
1645
|
+
errorCollector.push(ERRORS.CONTEXT_NOT_SET.err(
|
|
1646
|
+
`Context "${context}" is used at location "${parent.coordinate}" but is never set.`,
|
|
1647
|
+
{ nodes: sourceASTs(application) }
|
|
1648
|
+
));
|
|
1649
|
+
} else {
|
|
1650
|
+
validateFieldValue({
|
|
1651
|
+
context,
|
|
1652
|
+
selection,
|
|
1653
|
+
fromContextParent: parent,
|
|
1654
|
+
setContextLocations: locations,
|
|
1655
|
+
errorCollector,
|
|
1656
|
+
metadata,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// validate that there is at least one resolvable key on the type
|
|
1661
|
+
const keyDirective = metadata.keyDirective();
|
|
1662
|
+
const keyApplications = objectType.appliedDirectivesOf(keyDirective);
|
|
1663
|
+
if (!keyApplications.some(app => app.arguments().resolvable || app.arguments().resolvable === undefined)) {
|
|
1664
|
+
errorCollector.push(ERRORS.CONTEXT_NO_RESOLVABLE_KEY.err(
|
|
1665
|
+
`Object "${objectType.coordinate}" has no resolvable key but has an a field with a contextual argument.`,
|
|
1666
|
+
{ nodes: sourceASTs(objectType) }
|
|
1667
|
+
));
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1087
1672
|
validateNoExternalOnInterfaceFields(metadata, errorCollector);
|
|
1088
1673
|
validateAllExternalFieldsUsed(metadata, errorCollector);
|
|
1089
1674
|
validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata, errorCollector);
|
|
@@ -1093,7 +1678,6 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
1093
1678
|
// validation functions for subgraph schemas by overriding the
|
|
1094
1679
|
// validateSubgraphSchema method.
|
|
1095
1680
|
validateKnownFeatures(schema, errorCollector);
|
|
1096
|
-
|
|
1097
1681
|
// If tag is redefined by the user, make sure the definition is compatible with what we expect
|
|
1098
1682
|
const tagDirective = metadata.tagDirective();
|
|
1099
1683
|
if (tagDirective) {
|
|
@@ -1239,10 +1823,9 @@ export function setSchemaAsFed2Subgraph(schema: Schema, useLatest: boolean = fal
|
|
|
1239
1823
|
|
|
1240
1824
|
// This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
|
|
1241
1825
|
// subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
|
|
1242
|
-
export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
|
|
1826
|
+
export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@sourceAPI", "@sourceType", "@sourceField", "@context", "@fromContext"])';
|
|
1827
|
+
// This is the full @link declaration that is added when upgrading fed v1 subgraphs to v2 version. It should only be used by tests.
|
|
1828
|
+
export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
|
|
1246
1829
|
|
|
1247
1830
|
// This is the federation @link for tests that go through the SchemaUpgrader.
|
|
1248
1831
|
export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED = '@link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './specs/joinSpec';
|
|
|
12
12
|
export * from './specs/tagSpec';
|
|
13
13
|
export * from './specs/inaccessibleSpec';
|
|
14
14
|
export * from './specs/federationSpec';
|
|
15
|
+
export * from './specs/contextSpec';
|
|
15
16
|
export * from './supergraphs';
|
|
16
17
|
export * from './error';
|
|
17
18
|
export * from './schemaUpgrader';
|