@apollo/federation-internals 2.7.8 → 2.8.0-alpha.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.
Files changed (56) hide show
  1. package/dist/directiveAndTypeSpecification.d.ts +13 -1
  2. package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
  3. package/dist/directiveAndTypeSpecification.js +2 -2
  4. package/dist/directiveAndTypeSpecification.js.map +1 -1
  5. package/dist/error.d.ts +6 -0
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/error.js +12 -0
  8. package/dist/error.js.map +1 -1
  9. package/dist/extractSubgraphsFromSupergraph.d.ts +1 -1
  10. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  11. package/dist/extractSubgraphsFromSupergraph.js +62 -7
  12. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  13. package/dist/federation.d.ts +15 -2
  14. package/dist/federation.d.ts.map +1 -1
  15. package/dist/federation.js +394 -4
  16. package/dist/federation.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/operations.d.ts +10 -8
  22. package/dist/operations.d.ts.map +1 -1
  23. package/dist/operations.js +37 -14
  24. package/dist/operations.js.map +1 -1
  25. package/dist/specs/contextSpec.d.ts +20 -0
  26. package/dist/specs/contextSpec.d.ts.map +1 -0
  27. package/dist/specs/contextSpec.js +62 -0
  28. package/dist/specs/contextSpec.js.map +1 -0
  29. package/dist/specs/federationSpec.d.ts +5 -2
  30. package/dist/specs/federationSpec.d.ts.map +1 -1
  31. package/dist/specs/federationSpec.js +9 -1
  32. package/dist/specs/federationSpec.js.map +1 -1
  33. package/dist/specs/joinSpec.d.ts +6 -0
  34. package/dist/specs/joinSpec.d.ts.map +1 -1
  35. package/dist/specs/joinSpec.js +11 -1
  36. package/dist/specs/joinSpec.js.map +1 -1
  37. package/dist/supergraphs.d.ts +4 -0
  38. package/dist/supergraphs.d.ts.map +1 -1
  39. package/dist/supergraphs.js +35 -2
  40. package/dist/supergraphs.js.map +1 -1
  41. package/dist/utils.d.ts +3 -0
  42. package/dist/utils.d.ts.map +1 -1
  43. package/dist/utils.js +39 -1
  44. package/dist/utils.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/directiveAndTypeSpecification.ts +8 -1
  47. package/src/error.ts +42 -0
  48. package/src/extractSubgraphsFromSupergraph.ts +76 -14
  49. package/src/federation.ts +593 -10
  50. package/src/index.ts +1 -0
  51. package/src/operations.ts +48 -21
  52. package/src/specs/contextSpec.ts +87 -0
  53. package/src/specs/federationSpec.ts +10 -1
  54. package/src/specs/joinSpec.ts +27 -3
  55. package/src/supergraphs.ts +37 -1
  56. 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.7", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@sourceAPI", "@sourceType", "@sourceField"])';
1243
-
1244
- // This is the federation @link for tests that go through the asFed2SubgraphDocument function.
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';