@apollo/federation-internals 2.0.0-alpha.6 → 2.0.0-preview.10

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 (127) hide show
  1. package/CHANGELOG.md +43 -3
  2. package/dist/buildSchema.d.ts +7 -3
  3. package/dist/buildSchema.d.ts.map +1 -1
  4. package/dist/buildSchema.js +94 -61
  5. package/dist/buildSchema.js.map +1 -1
  6. package/dist/coreSpec.d.ts +39 -9
  7. package/dist/coreSpec.d.ts.map +1 -1
  8. package/dist/coreSpec.js +232 -42
  9. package/dist/coreSpec.js.map +1 -1
  10. package/dist/definitions.d.ts +71 -51
  11. package/dist/definitions.d.ts.map +1 -1
  12. package/dist/definitions.js +326 -231
  13. package/dist/definitions.js.map +1 -1
  14. package/dist/directiveAndTypeSpecification.d.ts +48 -0
  15. package/dist/directiveAndTypeSpecification.d.ts.map +1 -0
  16. package/dist/directiveAndTypeSpecification.js +253 -0
  17. package/dist/directiveAndTypeSpecification.js.map +1 -0
  18. package/dist/error.d.ts +21 -1
  19. package/dist/error.d.ts.map +1 -1
  20. package/dist/error.js +63 -3
  21. package/dist/error.js.map +1 -1
  22. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  23. package/dist/extractSubgraphsFromSupergraph.js +42 -97
  24. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  25. package/dist/federation.d.ts +102 -46
  26. package/dist/federation.d.ts.map +1 -1
  27. package/dist/federation.js +762 -234
  28. package/dist/federation.js.map +1 -1
  29. package/dist/federationSpec.d.ts +23 -0
  30. package/dist/federationSpec.d.ts.map +1 -0
  31. package/dist/federationSpec.js +117 -0
  32. package/dist/federationSpec.js.map +1 -0
  33. package/dist/inaccessibleSpec.d.ts +5 -1
  34. package/dist/inaccessibleSpec.d.ts.map +1 -1
  35. package/dist/inaccessibleSpec.js +31 -3
  36. package/dist/inaccessibleSpec.js.map +1 -1
  37. package/dist/index.d.ts +4 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +9 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/introspection.d.ts.map +1 -1
  42. package/dist/introspection.js +8 -3
  43. package/dist/introspection.js.map +1 -1
  44. package/dist/joinSpec.d.ts +6 -1
  45. package/dist/joinSpec.d.ts.map +1 -1
  46. package/dist/joinSpec.js +22 -0
  47. package/dist/joinSpec.js.map +1 -1
  48. package/dist/knownCoreFeatures.d.ts +4 -0
  49. package/dist/knownCoreFeatures.d.ts.map +1 -0
  50. package/dist/knownCoreFeatures.js +16 -0
  51. package/dist/knownCoreFeatures.js.map +1 -0
  52. package/dist/operations.d.ts +9 -1
  53. package/dist/operations.d.ts.map +1 -1
  54. package/dist/operations.js +27 -5
  55. package/dist/operations.js.map +1 -1
  56. package/dist/precompute.d.ts +3 -0
  57. package/dist/precompute.d.ts.map +1 -0
  58. package/dist/precompute.js +51 -0
  59. package/dist/precompute.js.map +1 -0
  60. package/dist/print.d.ts +11 -9
  61. package/dist/print.d.ts.map +1 -1
  62. package/dist/print.js +32 -22
  63. package/dist/print.js.map +1 -1
  64. package/dist/schemaUpgrader.d.ts +108 -0
  65. package/dist/schemaUpgrader.d.ts.map +1 -0
  66. package/dist/schemaUpgrader.js +497 -0
  67. package/dist/schemaUpgrader.js.map +1 -0
  68. package/dist/suggestions.d.ts +1 -1
  69. package/dist/suggestions.d.ts.map +1 -1
  70. package/dist/suggestions.js.map +1 -1
  71. package/dist/supergraphs.d.ts.map +1 -1
  72. package/dist/supergraphs.js +3 -3
  73. package/dist/supergraphs.js.map +1 -1
  74. package/dist/tagSpec.d.ts +7 -2
  75. package/dist/tagSpec.d.ts.map +1 -1
  76. package/dist/tagSpec.js +36 -16
  77. package/dist/tagSpec.js.map +1 -1
  78. package/dist/utils.d.ts +7 -0
  79. package/dist/utils.d.ts.map +1 -1
  80. package/dist/utils.js +34 -1
  81. package/dist/utils.js.map +1 -1
  82. package/dist/validate.d.ts.map +1 -1
  83. package/dist/validate.js +19 -11
  84. package/dist/validate.js.map +1 -1
  85. package/dist/validation/KnownTypeNamesInFederationRule.d.ts.map +1 -1
  86. package/dist/validation/KnownTypeNamesInFederationRule.js +1 -2
  87. package/dist/validation/KnownTypeNamesInFederationRule.js.map +1 -1
  88. package/dist/values.d.ts +1 -0
  89. package/dist/values.d.ts.map +1 -1
  90. package/dist/values.js +3 -2
  91. package/dist/values.js.map +1 -1
  92. package/package.json +4 -4
  93. package/src/__tests__/coreSpec.test.ts +100 -0
  94. package/src/__tests__/definitions.test.ts +98 -17
  95. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +64 -0
  96. package/src/__tests__/federation.test.ts +31 -0
  97. package/src/__tests__/operations.test.ts +2 -3
  98. package/src/__tests__/removeInaccessibleElements.test.ts +59 -6
  99. package/src/__tests__/schemaUpgrader.test.ts +169 -0
  100. package/src/__tests__/subgraphValidation.test.ts +422 -21
  101. package/src/__tests__/values.test.ts +2 -4
  102. package/src/buildSchema.ts +154 -84
  103. package/src/coreSpec.ts +294 -55
  104. package/src/definitions.ts +415 -275
  105. package/src/directiveAndTypeSpecification.ts +353 -0
  106. package/src/error.ts +143 -5
  107. package/src/extractSubgraphsFromSupergraph.ts +56 -122
  108. package/src/federation.ts +991 -302
  109. package/src/federationSpec.ts +146 -0
  110. package/src/inaccessibleSpec.ts +39 -11
  111. package/src/index.ts +4 -0
  112. package/src/introspection.ts +8 -3
  113. package/src/joinSpec.ts +35 -4
  114. package/src/knownCoreFeatures.ts +13 -0
  115. package/src/operations.ts +37 -7
  116. package/src/precompute.ts +65 -0
  117. package/src/print.ts +63 -48
  118. package/src/schemaUpgrader.ts +653 -0
  119. package/src/suggestions.ts +1 -1
  120. package/src/supergraphs.ts +4 -3
  121. package/src/tagSpec.ts +50 -18
  122. package/src/utils.ts +63 -0
  123. package/src/validate.ts +27 -16
  124. package/src/validation/KnownTypeNamesInFederationRule.ts +1 -7
  125. package/src/values.ts +7 -3
  126. package/tsconfig.test.tsbuildinfo +1 -1
  127. package/tsconfig.tsbuildinfo +1 -1
@@ -12,16 +12,24 @@ import {
12
12
  ListTypeNode,
13
13
  NamedTypeNode,
14
14
  parse,
15
- printError,
16
15
  TypeNode,
17
16
  VariableDefinitionNode,
18
17
  VariableNode
19
18
  } from "graphql";
20
- import { CoreDirectiveArgs, CoreSpecDefinition, CORE_VERSIONS, FeatureUrl, isCoreSpecDirectiveApplication, removeFeatureElements } from "./coreSpec";
21
- import { arrayEquals, assert, mapValues, MapWithCachedArrays, setValues } from "./utils";
19
+ import {
20
+ CoreImport,
21
+ CoreOrLinkDirectiveArgs,
22
+ CoreSpecDefinition,
23
+ extractCoreFeatureImports,
24
+ FeatureUrl,
25
+ findCoreSpecVersion,
26
+ isCoreSpecDirectiveApplication,
27
+ removeFeatureElements,
28
+ } from "./coreSpec";
29
+ import { assert, mapValues, MapWithCachedArrays, setValues } from "./utils";
22
30
  import { withDefaultValues, valueEquals, valueToString, valueToAST, variablesInValue, valueFromAST, valueNodeToConstValueNode } from "./values";
23
31
  import { removeInaccessibleElements } from "./inaccessibleSpec";
24
- import { printSchema, Options, defaultPrintOptions } from './print';
32
+ import { defaultPrintOptions, printSchema } from './print';
25
33
  import { sameType } from './types';
26
34
  import { addIntrospectionFields, introspectionFieldNames, isIntrospectionName } from "./introspection";
27
35
  import { err } from '@apollo/core-schema';
@@ -30,12 +38,16 @@ import { validateSDL } from "graphql/validation/validate";
30
38
  import { SDLValidationRule } from "graphql/validation/ValidationContext";
31
39
  import { specifiedSDLRules } from "graphql/validation/specifiedRules";
32
40
  import { validateSchema } from "./validate";
41
+ import { createDirectiveSpecification, createScalarTypeSpecification, DirectiveSpecification, TypeSpecification } from "./directiveAndTypeSpecification";
42
+ import { didYouMean, suggestionList } from "./suggestions";
43
+ import { withModifiedErrorMessage } from "./error";
33
44
 
34
45
  const validationErrorCode = 'GraphQLValidationFailed';
46
+ const DEFAULT_VALIDATION_ERROR_MESSAGE = 'The schema is not a valid GraphQL schema.';
35
47
 
36
- export const ErrGraphQLValidationFailed = (causes: GraphQLError[]) =>
48
+ export const ErrGraphQLValidationFailed = (causes: GraphQLError[], message: string = DEFAULT_VALIDATION_ERROR_MESSAGE) =>
37
49
  err(validationErrorCode, {
38
- message: 'The schema is not a valid GraphQL schema',
50
+ message,
39
51
  causes
40
52
  });
41
53
 
@@ -61,11 +73,11 @@ export function printGraphQLErrorsOrRethrow(e: Error): string {
61
73
  if (!causes) {
62
74
  throw e;
63
75
  }
64
- return causes.map(e => printError(e)).join('\n\n');
76
+ return causes.map(e => e.toString()).join('\n\n');
65
77
  }
66
78
 
67
79
  export function printErrors(errors: GraphQLError[]): string {
68
- return errors.map(e => printError(e)).join('\n\n');
80
+ return errors.map(e => e.toString()).join('\n\n');
69
81
  }
70
82
 
71
83
  export const typenameFieldName = '__typename';
@@ -93,6 +105,10 @@ function checkDefaultSchemaRoot(type: NamedType): SchemaRootKind | undefined {
93
105
  }
94
106
  }
95
107
 
108
+ export function isSchemaRootType(type: NamedType): boolean {
109
+ return isObjectType(type) && type.isRootType();
110
+ }
111
+
96
112
  export type Type = NamedType | WrapperType;
97
113
  export type NamedType = ScalarType | ObjectType | InterfaceType | UnionType | EnumType | InputObjectType;
98
114
  export type OutputType = ScalarType | ObjectType | InterfaceType | UnionType | EnumType | ListType<any> | NonNullType<any>;
@@ -131,7 +147,7 @@ export function isScalarType(type: Type): type is ScalarType {
131
147
  }
132
148
 
133
149
  export function isCustomScalarType(type: Type): boolean {
134
- return isScalarType(type) && !graphQLBuiltIns.defaultGraphQLBuiltInTypes.includes(type.name);
150
+ return isScalarType(type) && !graphQLBuiltInTypes.includes(type.name);
135
151
  }
136
152
 
137
153
  export function isIntType(type: Type): boolean {
@@ -198,6 +214,22 @@ export function isInputType(type: Type): type is InputType {
198
214
  }
199
215
  }
200
216
 
217
+ export function isTypeOfKind<T extends Type>(type: Type, kind: T['kind']): type is T {
218
+ return type.kind === kind;
219
+ }
220
+
221
+ export function filterTypesOfKind<T extends Type>(types: readonly Type[], kind: T['kind']): T[] {
222
+ return types.reduce(
223
+ (acc: T[], type: Type) => {
224
+ if (isTypeOfKind(type, kind)) {
225
+ acc.push(type);
226
+ }
227
+ return acc;
228
+ },
229
+ [],
230
+ );
231
+ }
232
+
201
233
  export function baseType(type: Type): NamedType {
202
234
  return isWrapperType(type) ? type.baseType() : type;
203
235
  }
@@ -367,8 +399,8 @@ export class DirectiveTargetElement<T extends DirectiveTargetElement<T>> {
367
399
  }
368
400
  }
369
401
 
370
- export function sourceASTs(...elts: ({ sourceAST?: ASTNode } | undefined)[]): ASTNode[] {
371
- return elts.map(elt => elt?.sourceAST).filter(elt => elt !== undefined) as ASTNode[];
402
+ export function sourceASTs<TNode extends ASTNode = ASTNode>(...elts: ({ sourceAST?: TNode } | undefined)[]): TNode[] {
403
+ return elts.map(elt => elt?.sourceAST).filter((elt): elt is TNode => elt !== undefined);
372
404
  }
373
405
 
374
406
  // Not exposed: mostly about avoid code duplication between SchemaElement and Directive (which is not a SchemaElement as it can't
@@ -477,7 +509,8 @@ export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>
477
509
 
478
510
  applyDirective<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(
479
511
  nameOrDefOrDirective: Directive<TOwnType, TApplicationArgs> | DirectiveDefinition<TApplicationArgs> | string,
480
- args?: TApplicationArgs
512
+ args?: TApplicationArgs,
513
+ asFirstDirective: boolean = false,
481
514
  ): Directive<TOwnType, TApplicationArgs> {
482
515
  let toAdd: Directive<TOwnType, TApplicationArgs>;
483
516
  if (nameOrDefOrDirective instanceof Directive) {
@@ -490,9 +523,15 @@ export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>
490
523
  let name: string;
491
524
  if (typeof nameOrDefOrDirective === 'string') {
492
525
  this.checkUpdate();
493
- const def = this.schema().directive(nameOrDefOrDirective);
526
+ const def = this.schema().directive(nameOrDefOrDirective) ?? this.schema().blueprint.onMissingDirectiveDefinition(this.schema(), nameOrDefOrDirective, args);
494
527
  if (!def) {
495
- throw new GraphQLError(`Cannot apply unknown directive "@${nameOrDefOrDirective}"`);
528
+ throw this.schema().blueprint.onGraphQLJSValidationError(
529
+ this.schema(),
530
+ new GraphQLError(`Unknown directive "@${nameOrDefOrDirective}".`)
531
+ );
532
+ }
533
+ if (Array.isArray(def)) {
534
+ throw ErrGraphQLValidationFailed(def);
496
535
  }
497
536
  name = nameOrDefOrDirective;
498
537
  } else {
@@ -503,7 +542,11 @@ export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>
503
542
  Element.prototype['setParent'].call(toAdd, this);
504
543
  }
505
544
  // TODO: we should typecheck arguments or our TApplicationArgs business is just a lie.
506
- this._appliedDirectives.push(toAdd);
545
+ if (asFirstDirective) {
546
+ this._appliedDirectives.unshift(toAdd);
547
+ } else {
548
+ this._appliedDirectives.push(toAdd);
549
+ }
507
550
  DirectiveDefinition.prototype['addReferencer'].call(toAdd.definition!, toAdd);
508
551
  this.onModification();
509
552
  return toAdd;
@@ -636,6 +679,18 @@ abstract class BaseNamedType<TReferencer, TOwnType extends NamedType & NamedSche
636
679
  return extension;
637
680
  }
638
681
 
682
+ removeExtensions() {
683
+ if (this._extensions.size === 0) {
684
+ return;
685
+ }
686
+
687
+ this._extensions.clear();
688
+ for (const directive of this._appliedDirectives) {
689
+ directive.removeOfExtension();
690
+ }
691
+ this.removeInnerElementsExtensions();
692
+ }
693
+
639
694
  isIntrospectionType(): boolean {
640
695
  return isIntrospectionName(this.name);
641
696
  }
@@ -649,6 +704,7 @@ abstract class BaseNamedType<TReferencer, TOwnType extends NamedType & NamedSche
649
704
  }
650
705
 
651
706
  protected abstract hasNonExtensionInnerElements(): boolean;
707
+ protected abstract removeInnerElementsExtensions(): void;
652
708
 
653
709
  protected isElementBuiltIn(): boolean {
654
710
  return this.isBuiltIn;
@@ -773,6 +829,10 @@ abstract class BaseExtensionMember<TExtended extends ExtendableElement> extends
773
829
  return this._extension;
774
830
  }
775
831
 
832
+ removeOfExtension() {
833
+ this._extension = undefined;
834
+ }
835
+
776
836
  setOfExtension(extension: Extension<TExtended> | undefined) {
777
837
  this.checkUpdate();
778
838
  // See similar comment on FieldDefinition.setOfExtension for why we have to cast.
@@ -792,231 +852,105 @@ abstract class BaseExtensionMember<TExtended extends ExtendableElement> extends
792
852
  protected abstract removeInner(): void;
793
853
  }
794
854
 
795
- function sortedMemberNames(u: UnionType): string[] {
796
- return u.members().map(m => m.type.name).sort((n1, n2) => n1.localeCompare(n2));
797
- }
798
-
799
- export class BuiltIns {
800
- readonly defaultGraphQLBuiltInTypes: readonly string[] = [ 'Int', 'Float', 'String', 'Boolean', 'ID' ];
801
- private readonly defaultGraphQLBuiltInDirectives: readonly string[] = [ 'include', 'skip', 'deprecated', 'specifiedBy' ];
802
-
803
- addBuiltInTypes(schema: Schema) {
804
- this.defaultGraphQLBuiltInTypes.forEach(t => this.addBuiltInScalar(schema, t));
805
- }
806
-
807
- addBuiltInDirectives(schema: Schema) {
808
- for (const name of ['include', 'skip']) {
809
- this.addBuiltInDirective(schema, name)
810
- .addLocations(DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT)
811
- .addArgument('if', new NonNullType(schema.booleanType()));
812
- }
813
- this.addBuiltInDirective(schema, 'deprecated')
814
- .addLocations(
815
- DirectiveLocation.FIELD_DEFINITION,
816
- DirectiveLocation.ENUM_VALUE,
817
- DirectiveLocation.ARGUMENT_DEFINITION,
818
- DirectiveLocation.INPUT_FIELD_DEFINITION,
819
- ).addArgument('reason', schema.stringType(), 'No longer supported');
820
- this.addBuiltInDirective(schema, 'specifiedBy')
821
- .addLocations(DirectiveLocation.SCALAR)
822
- .addArgument('url', new NonNullType(schema.stringType()));
823
- }
824
-
825
- isGraphQLBuiltIn(element: NamedType | DirectiveDefinition | FieldDefinition<any>): boolean {
826
- if (isIntrospectionName(element.name)) {
827
- return true;
828
- }
829
- if (element instanceof FieldDefinition) {
830
- return false;
831
- } else if (element instanceof DirectiveDefinition) {
832
- return this.defaultGraphQLBuiltInDirectives.includes(element.name);
833
- } else {
834
- return this.defaultGraphQLBuiltInTypes.includes(element.name);
835
- }
836
- }
837
-
838
- prepareValidation(_: Schema) {
839
- // No-op for graphQL built-ins, but overriden for federation built-ins.
840
- }
841
-
842
- onValidation(schema: Schema, unvalidatedDirectives?: string[]): GraphQLError[] {
843
- const errors: GraphQLError[] = [];
844
- // We make sure that if any of the built-ins has been redefined, then the redefinition is
845
- // the same as the built-in one.
846
- for (const type of schema.builtInTypes(undefined, true)) {
847
- const maybeRedefined = schema.type(type.name)!;
848
- if (!maybeRedefined.isBuiltIn) {
849
- this.ensureSameTypeStructure(type, maybeRedefined, errors);
850
- }
851
- }
852
-
853
- for (const directive of schema.builtInDirectives(true)) {
854
- if (unvalidatedDirectives && unvalidatedDirectives.includes(directive.name)) {
855
- continue;
856
- }
857
- const maybeRedefined = schema.directive(directive.name)!;
858
- if (!maybeRedefined.isBuiltIn) {
859
- this.ensureSameDirectiveStructure(directive, maybeRedefined, errors);
860
- }
861
- }
862
- return errors;
863
- }
864
-
865
- validationRules(): readonly SDLValidationRule[] {
866
- return specifiedSDLRules;
867
- }
868
-
869
- maybeUpdateSubgraphDocument(_: Schema, document: DocumentNode): DocumentNode {
870
- return document;
871
- }
872
-
873
- private ensureSameDirectiveStructure(builtIn: DirectiveDefinition<any>, manuallyDefined: DirectiveDefinition<any>, errors: GraphQLError[]) {
874
- this.ensureSameArguments(builtIn, manuallyDefined, `directive ${builtIn}`, errors);
875
- // It's ok to say you'll never repeat a built-in that is repeatable. It's not ok to repeat one that isn't.
876
- if (!builtIn.repeatable && manuallyDefined.repeatable) {
877
- errors.push(error(`Invalid redefinition of built-in directive ${builtIn}: ${builtIn} should${builtIn.repeatable ? "" : " not"} be repeatable`));
878
- }
879
- // Similarly, it's ok to say that you will never use a directive in some locations, but not that you will use it in places not allowed by the built-in.
880
- if (!manuallyDefined.locations.every(loc => builtIn.locations.includes(loc))) {
881
- errors.push(error(`Invalid redefinition of built-in directive ${builtIn}: ${builtIn} should have locations ${builtIn.locations.join(', ')}, but found (non-subset) ${manuallyDefined.locations.join(', ')}`));
882
- }
855
+ export class SchemaBlueprint {
856
+ onMissingDirectiveDefinition(_schema: Schema, _name: string, _args?: {[key: string]: any}): DirectiveDefinition | GraphQLError[] | undefined {
857
+ // No-op by default, but used for federation.
858
+ return undefined;
883
859
  }
884
860
 
885
- private ensureSameArguments(
886
- builtIn: { arguments(): readonly ArgumentDefinition<any>[] },
887
- manuallyDefined: { argument(name: string): ArgumentDefinition<any> | undefined, arguments(): readonly ArgumentDefinition<any>[] },
888
- what: string,
889
- errors: GraphQLError[]
890
- ) {
891
- const expectedArguments = builtIn.arguments();
892
- const foundArguments = manuallyDefined.arguments();
893
- if (expectedArguments.length !== foundArguments.length) {
894
- errors.push(error(`Invalid redefinition of built-in ${what}: should have ${expectedArguments.length} arguments but ${foundArguments.length} found in redefinition`));
895
- return;
896
- }
897
- for (const expectedArgument of expectedArguments) {
898
- const foundArgument = manuallyDefined.argument(expectedArgument.name)!;
899
- const expectedType = expectedArgument.type!;
900
- let actualType = foundArgument.type!;
901
- if (isNonNullType(actualType) && !isNonNullType(expectedType)) {
902
- // It's ok to redefine an optional argument as mandatory. For instance, if you want to force people on your team to provide a "deprecation reason", you can
903
- // redefine @deprecated as `directive @deprecated(reason: String!)...` to get validation. In other words, you are allowed to always pass an argument that
904
- // is optional if you so wish.
905
- actualType = actualType.ofType;
906
- }
907
- if (!sameType(expectedType, actualType)) {
908
- errors.push(error(`Invalid redefinition of built-in ${what}: ${expectedArgument.coordinate} should have type ${expectedArgument.type!} but found type ${foundArgument.type!}`));
909
- } else if (!isNonNullType(actualType) && !valueEquals(expectedArgument.defaultValue, foundArgument.defaultValue)) {
910
- errors.push(error(`Invalid redefinition of built-in ${what}: ${expectedArgument.coordinate} should have default value ${valueToString(expectedArgument.defaultValue)} but found default value ${valueToString(foundArgument.defaultValue)}`));
911
- }
912
- }
861
+ onDirectiveDefinitionAndSchemaParsed(_: Schema): GraphQLError[] {
862
+ // No-op by default, but used for federation.
863
+ return [];
913
864
  }
914
865
 
915
- private ensureSameTypeStructure(builtIn: NamedType, manuallyDefined: NamedType, errors: GraphQLError[]) {
916
- if (builtIn.kind !== manuallyDefined.kind) {
917
- errors.push(error(`Invalid redefinition of built-in type ${builtIn}: ${builtIn} should be a ${builtIn.kind} type but redefined as a ${manuallyDefined.kind}`));
918
- return;
919
- }
920
-
921
- switch (builtIn.kind) {
922
- case 'ScalarType':
923
- // Nothing more to check for scalars.
924
- return;
925
- case 'ObjectType':
926
- const redefinedObject = manuallyDefined as ObjectType;
927
- for (const builtInField of builtIn.fields()) {
928
- const redefinedField = redefinedObject.field(builtInField.name);
929
- if (!redefinedField) {
930
- errors.push(error(`Invalid redefinition of built-in type ${builtIn}: redefinition is missing field ${builtInField}`));
931
- return;
932
- }
933
- // We allow adding non-nullability because we've seen redefinition of the federation _Service type with type String! for the `sdl` field
934
- // and we don't want to break backward compatibility as this doesn't feel too harmful.
935
- let rType = redefinedField.type!;
936
- if (!isNonNullType(builtInField.type!) && isNonNullType(rType)) {
937
- rType = rType.ofType;
938
- }
939
- if (!sameType(builtInField.type!, rType)) {
940
- errors.push(error(`Invalid redefinition of field ${builtInField} of built-in type ${builtIn}: should have type ${builtInField.type} but redefined with type ${redefinedField.type}`));
941
- return;
942
- }
943
- this.ensureSameArguments(builtInField, redefinedField, `field ${builtInField.coordinate}`, errors);
944
- }
945
- break;
946
- case 'UnionType':
947
- const redefinedUnion = manuallyDefined as UnionType;
948
- const builtInMembers = sortedMemberNames(builtIn);
949
- const redefinedMembers = sortedMemberNames(redefinedUnion);
950
- if (!arrayEquals(builtInMembers, redefinedMembers)) {
951
- errors.push(error(`Invalid redefinition of built-in type ${builtIn}: redefinition has members [${redefinedMembers}] but should have members [${builtInMembers}]`));
952
- }
953
- break;
954
- default:
955
- // Let's not bother with the rest until we actually need it.
956
- errors.push(error(`Invalid redefinition of built-in type ${builtIn}: cannot redefine ${builtIn.kind} built-in types`));
957
- }
866
+ ignoreParsedField(_type: NamedType, _fieldName: string): boolean {
867
+ // No-op by default, but used for federation.
868
+ return false;
958
869
  }
959
870
 
960
- protected addBuiltInScalar(schema: Schema, name: string): ScalarType {
961
- return schema.addType(new ScalarType(name, true));
871
+ onConstructed(_: Schema) {
872
+ // No-op by default, but used for federation.
962
873
  }
963
874
 
964
- protected addBuiltInObject(schema: Schema, name: string): ObjectType {
965
- return schema.addType(new ObjectType(name, true));
875
+ onAddedCoreFeature(_schema: Schema, _feature: CoreFeature) {
876
+ // No-op by default, but used for federation.
966
877
  }
967
878
 
968
- protected addBuiltInUnion(schema: Schema, name: string): UnionType {
969
- return schema.addType(new UnionType(name, true));
879
+ onInvalidation(_: Schema) {
880
+ // No-op by default, but used for federation.
970
881
  }
971
882
 
972
- protected addBuiltInDirective(schema: Schema, name: string): DirectiveDefinition {
973
- return schema.addDirectiveDefinition(new DirectiveDefinition(name, true));
883
+ onValidation(_schema: Schema): GraphQLError[] {
884
+ // No-op by default, but used for federation.
885
+ return []
974
886
  }
975
887
 
976
- protected addBuiltInField(parentType: ObjectType, name: string, type: OutputType): FieldDefinition<ObjectType> {
977
- return parentType.addField(new FieldDefinition(name, true), type);
888
+ validationRules(): readonly SDLValidationRule[] {
889
+ return specifiedSDLRules;
978
890
  }
979
891
 
980
- protected getTypedDirective<TApplicationArgs extends {[key: string]: any}>(
981
- schema: Schema,
982
- name: string
983
- ): DirectiveDefinition<TApplicationArgs> {
984
- const directive = schema.directive(name);
985
- if (!directive) {
986
- throw new Error(`The provided schema has not be built with the ${name} directive built-in`);
892
+ /**
893
+ * Allows to intercept some graphQL-js error messages when we can provide additional guidance to users.
894
+ */
895
+ onGraphQLJSValidationError(schema: Schema, error: GraphQLError): GraphQLError {
896
+ // For now, the main additional guidance we provide is around directives, where we could provide additional help in 2 main ways:
897
+ // - if a directive name is likely misspelled (somehow, graphQL-js has methods to offer suggestions on likely mispelling, but don't use this (at the
898
+ // time of this writting) for directive names).
899
+ // - for fed 2 schema, if a federation directive is refered under it's "default" naming but is not properly imported (not enforced
900
+ // in the method but rather in the `FederationBlueprint`).
901
+ //
902
+ // Note that intercepting/parsing error messages to modify them is never ideal, but pragmatically, it's probably better than rewriting the relevant
903
+ // rules entirely (in that later case, our "copied" rule would stop getting any potential graphQL-js made improvements for instance). And while such
904
+ // parsing is fragile, in that it'll break if the original message change, we have unit tests to surface any such breakage so it's not really a risk.
905
+ const matcher = /^Unknown directive "@(?<directive>[_A-Za-z][_0-9A-Za-z]*)"\.$/.exec(error.message);
906
+ const name = matcher?.groups?.directive;
907
+ if (!name) {
908
+ return error;
909
+ }
910
+
911
+ const allDefinedDirectiveNames = schema.allDirectives().map((d) => d.name);
912
+ const suggestions = suggestionList(name, allDefinedDirectiveNames);
913
+ if (suggestions.length === 0) {
914
+ return this.onUnknownDirectiveValidationError(schema, name, error);
915
+ } else {
916
+ return withModifiedErrorMessage(error, `${error.message}${didYouMean(suggestions.map((s) => '@' + s))}`);
987
917
  }
988
- return directive as DirectiveDefinition<TApplicationArgs>;
989
- }
990
-
991
- includeDirective(schema: Schema): DirectiveDefinition<{if: boolean}> {
992
- return this.getTypedDirective(schema, 'include');
993
918
  }
994
919
 
995
- skipDirective(schema: Schema): DirectiveDefinition<{if: boolean}> {
996
- return this.getTypedDirective(schema, 'skip');
997
- }
998
-
999
- deprecatedDirective(schema: Schema): DirectiveDefinition<{reason?: string}> {
1000
- return this.getTypedDirective(schema, 'deprecated');
1001
- }
1002
-
1003
- specifiedByDirective(schema: Schema): DirectiveDefinition<{url: string}> {
1004
- return this.getTypedDirective(schema, 'specifiedBy');
920
+ onUnknownDirectiveValidationError(_schema: Schema, _unknownDirectiveName: string, error: GraphQLError): GraphQLError {
921
+ return error;
1005
922
  }
1006
923
  }
1007
924
 
925
+ export const defaultSchemaBlueprint = new SchemaBlueprint();
926
+
1008
927
  export class CoreFeature {
1009
928
  constructor(
1010
929
  readonly url: FeatureUrl,
1011
930
  readonly nameInSchema: string,
1012
931
  readonly directive: Directive<SchemaDefinition>,
932
+ readonly imports: CoreImport[],
1013
933
  readonly purpose?: string,
1014
934
  ) {
1015
935
  }
1016
936
 
1017
937
  isFeatureDefinition(element: NamedType | DirectiveDefinition): boolean {
1018
938
  return element.name.startsWith(this.nameInSchema + '__')
1019
- || (element.kind === 'DirectiveDefinition' && element.name === this.nameInSchema);
939
+ || (element.kind === 'DirectiveDefinition' && element.name === this.nameInSchema)
940
+ || !!this.imports.find((i) => element.name === (i.as ?? i.name));
941
+ }
942
+
943
+ directiveNameInSchema(name: string): string {
944
+ if (name === this.url.name) {
945
+ return this.nameInSchema;
946
+ }
947
+ const elementImport = this.imports.find((i) => i.name.charAt(0) === '@' && i.name.slice(1) === name);
948
+ return elementImport ? (elementImport.as?.slice(1) ?? name) : this.nameInSchema + '__' + name;
949
+ }
950
+
951
+ typeNameInSchema(name: string): string {
952
+ const elementImport = this.imports.find((i) => i.name === name);
953
+ return elementImport ? (elementImport.as ?? name) : this.nameInSchema + '__' + name;
1020
954
  }
1021
955
  }
1022
956
 
@@ -1027,9 +961,9 @@ export class CoreFeatures {
1027
961
 
1028
962
  constructor(readonly coreItself: CoreFeature) {
1029
963
  this.add(coreItself);
1030
- const coreDef = CORE_VERSIONS.find(coreItself.url.version);
964
+ const coreDef = findCoreSpecVersion(coreItself.url);
1031
965
  if (!coreDef) {
1032
- throw error(`Schema uses unknown version ${coreItself.url.version} of the core spec (known versions: ${CORE_VERSIONS.versions().join(', ')})`);
966
+ throw error(`Schema uses unknown version ${coreItself.url.version} of the ${coreItself.url.name} spec`);
1033
967
  }
1034
968
  this.coreDefinition = coreDef;
1035
969
  }
@@ -1054,14 +988,17 @@ export class CoreFeatures {
1054
988
  if (directive.definition?.name !== this.coreItself.nameInSchema) {
1055
989
  return undefined;
1056
990
  }
1057
- const args = (directive as Directive<SchemaDefinition, CoreDirectiveArgs>).arguments();
1058
- const url = FeatureUrl.parse(args.feature);
991
+ const typedDirective = directive as Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>
992
+ const args = typedDirective.arguments();
993
+ const url = this.coreDefinition.extractFeatureUrl(args);
1059
994
  const existing = this.byIdentity.get(url.identity);
1060
995
  if (existing) {
1061
996
  throw error(`Duplicate inclusion of feature ${url.identity}`);
1062
997
  }
1063
- const feature = new CoreFeature(url, args.as ?? url.name, directive, args.for);
998
+ const imports = extractCoreFeatureImports(url, typedDirective);
999
+ const feature = new CoreFeature(url, args.as ?? url.name, directive, imports, args.for);
1064
1000
  this.add(feature);
1001
+ directive.schema().blueprint.onAddedCoreFeature(directive.schema(), feature);
1065
1002
  return feature;
1066
1003
  }
1067
1004
 
@@ -1069,9 +1006,62 @@ export class CoreFeatures {
1069
1006
  this.byAlias.set(feature.nameInSchema, feature);
1070
1007
  this.byIdentity.set(feature.url.identity, feature);
1071
1008
  }
1009
+
1010
+ sourceFeature(element: DirectiveDefinition | NamedType): CoreFeature | undefined {
1011
+ const isDirective = element instanceof DirectiveDefinition;
1012
+ const splitted = element.name.split('__');
1013
+ if (splitted.length > 1) {
1014
+ return this.byAlias.get(splitted[0]);
1015
+ } else {
1016
+ const directFeature = this.byAlias.get(element.name);
1017
+ if (directFeature && isDirective) {
1018
+ return directFeature;
1019
+ }
1020
+
1021
+ // Let's see if it's an import. If not, it's not associated to a declared feature.
1022
+ const importName = isDirective ? '@' + element.name : element.name;
1023
+ const allFeatures = [this.coreItself, ...this.byIdentity.values()];
1024
+ for (const feature of allFeatures) {
1025
+ for (const { as } of feature.imports) {
1026
+ if (as === importName) {
1027
+ return feature;
1028
+ }
1029
+ }
1030
+ }
1031
+ return undefined;
1032
+ }
1033
+ }
1072
1034
  }
1073
1035
 
1074
- const toASTPrintOptions: Options = { ...defaultPrintOptions, showNonGraphQLBuiltIns: true };
1036
+ const graphQLBuiltInTypes: readonly string[] = [ 'Int', 'Float', 'String', 'Boolean', 'ID' ];
1037
+ const graphQLBuiltInTypesSpecifications: readonly TypeSpecification[] = graphQLBuiltInTypes.map((name) => createScalarTypeSpecification({ name }));
1038
+
1039
+ const graphQLBuiltInDirectivesSpecifications: readonly DirectiveSpecification[] = [
1040
+ createDirectiveSpecification({
1041
+ name: 'include',
1042
+ locations: [DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT],
1043
+ argumentFct: (schema) => ({ args: [{ name: 'if', type: new NonNullType(schema.booleanType()) }], errors: [] })
1044
+ }),
1045
+ createDirectiveSpecification({
1046
+ name: 'skip',
1047
+ locations: [DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT],
1048
+ argumentFct: (schema) => ({ args: [{ name: 'if', type: new NonNullType(schema.booleanType()) }], errors: [] })
1049
+ }),
1050
+ createDirectiveSpecification({
1051
+ name: 'deprecated',
1052
+ locations: [DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE, DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION],
1053
+ argumentFct: (schema) => ({ args: [{ name: 'reason', type: schema.stringType(), defaultValue: 'No longer supported' }], errors: [] })
1054
+ }),
1055
+ createDirectiveSpecification({
1056
+ name: 'specifiedBy',
1057
+ locations: [DirectiveLocation.SCALAR],
1058
+ argumentFct: (schema) => ({ args: [{ name: 'url', type: new NonNullType(schema.stringType()) }], errors: [] })
1059
+ }),
1060
+ ];
1061
+
1062
+
1063
+ // A coordinate is up to 3 "graphQL name" ([_A-Za-z][_0-9A-Za-z]*).
1064
+ const coordinateRegexp = /^@?[_A-Za-z][_0-9A-Za-z]*(\.[_A-Za-z][_0-9A-Za-z]*)?(\([_A-Za-z][_0-9A-Za-z]*:\))?$/;
1075
1065
 
1076
1066
  export class Schema {
1077
1067
  private _schemaDefinition: SchemaDefinition;
@@ -1081,16 +1071,17 @@ export class Schema {
1081
1071
  private readonly _directives = new MapWithCachedArrays<string, DirectiveDefinition>();
1082
1072
  private _coreFeatures?: CoreFeatures;
1083
1073
  private isConstructed: boolean = false;
1084
- private isValidated: boolean = false;
1074
+ public isValidated: boolean = false;
1085
1075
 
1086
1076
  private cachedDocument?: DocumentNode;
1087
1077
  private apiSchema?: Schema;
1088
1078
 
1089
- constructor(readonly builtIns: BuiltIns = graphQLBuiltIns) {
1079
+ constructor(readonly blueprint: SchemaBlueprint = defaultSchemaBlueprint) {
1090
1080
  this._schemaDefinition = new SchemaDefinition();
1091
1081
  Element.prototype['setParent'].call(this._schemaDefinition, this);
1092
- builtIns.addBuiltInTypes(this);
1093
- builtIns.addBuiltInDirectives(this);
1082
+ graphQLBuiltInTypesSpecifications.forEach((spec) => spec.checkOrAdd(this, undefined, true));
1083
+ graphQLBuiltInDirectivesSpecifications.forEach((spec) => spec.checkOrAdd(this, undefined, true));
1084
+ blueprint.onConstructed(this);
1094
1085
  this.isConstructed = true;
1095
1086
  }
1096
1087
 
@@ -1135,10 +1126,8 @@ export class Schema {
1135
1126
  }
1136
1127
  }
1137
1128
 
1138
- private forceSetCachedDocument(document: DocumentNode, addNonGraphQLBuiltIns: boolean = true) {
1139
- this.cachedDocument = addNonGraphQLBuiltIns
1140
- ? this.builtIns.maybeUpdateSubgraphDocument(this, document)
1141
- : document;
1129
+ private forceSetCachedDocument(document: DocumentNode) {
1130
+ this.cachedDocument = document;
1142
1131
  }
1143
1132
 
1144
1133
  isCoreSchema(): boolean {
@@ -1152,7 +1141,7 @@ export class Schema {
1152
1141
  toAST(): DocumentNode {
1153
1142
  if (!this.cachedDocument) {
1154
1143
  // As we're not building the document from a file, having locations info might be more confusing that not.
1155
- this.forceSetCachedDocument(parse(printSchema(this, toASTPrintOptions), { noLocation: true }), false);
1144
+ this.forceSetCachedDocument(parse(printSchema(this), { noLocation: true }));
1156
1145
  }
1157
1146
  return this.cachedDocument!;
1158
1147
  }
@@ -1185,8 +1174,8 @@ export class Schema {
1185
1174
 
1186
1175
  // Some subgraphs, especially federation 1 ones, cannot be properly converted to a GraphQLSchema because they are invalid graphQL.
1187
1176
  // And the main culprit is type extensions that don't have a corresponding definition. So to avoid that problem, we print
1188
- // up the AST without extensions. Another issue is the non graph built-ins which needs to be explicitely defined.
1189
- const ast = parse(printSchema(this, { ...toASTPrintOptions, mergeTypesAndExtensions: true }), { noLocation: true });
1177
+ // up the AST without extensions.
1178
+ const ast = parse(printSchema(this, { ...defaultPrintOptions, mergeTypesAndExtensions: true }), { noLocation: true });
1190
1179
  return buildGraphqlSchemaFromAST(ast);
1191
1180
  }
1192
1181
 
@@ -1197,23 +1186,42 @@ export class Schema {
1197
1186
  /**
1198
1187
  * All the types defined on this schema, excluding the built-in types.
1199
1188
  */
1200
- types<T extends NamedType>(kind?: T['kind'], includeNonGraphQLBuiltIns: boolean = false): readonly T[] {
1201
- const allKinds = this._types.values();
1202
- const forKind = (kind ? allKinds.filter(t => t.kind === kind) : allKinds) as readonly T[];
1203
- return includeNonGraphQLBuiltIns
1204
- ? this.builtInTypes(kind).filter(t => !graphQLBuiltIns.isGraphQLBuiltIn(t)).concat(forKind)
1205
- : forKind;
1189
+ types(): readonly NamedType[] {
1190
+ return this._types.values();
1191
+ }
1192
+
1193
+ interfaceTypes(): readonly InterfaceType[] {
1194
+ return filterTypesOfKind<InterfaceType>(this.types(), 'InterfaceType');
1195
+ }
1196
+
1197
+ objectTypes(): readonly ObjectType[] {
1198
+ return filterTypesOfKind<ObjectType>(this.types(), 'ObjectType');
1199
+ }
1200
+
1201
+ unionTypes(): readonly UnionType[] {
1202
+ return filterTypesOfKind<UnionType>(this.types(), 'UnionType');
1203
+ }
1204
+
1205
+ scalarTypes(): readonly ScalarType[] {
1206
+ return filterTypesOfKind<ScalarType>(this.types(), 'ScalarType');
1207
+ }
1208
+
1209
+ inputTypes(): readonly InputObjectType[] {
1210
+ return filterTypesOfKind<InputObjectType>(this.types(), 'InputObjectType');
1211
+ }
1212
+
1213
+ enumTypes(): readonly EnumType[] {
1214
+ return filterTypesOfKind<EnumType>(this.types(), 'EnumType');
1206
1215
  }
1207
1216
 
1208
1217
  /**
1209
1218
  * All the built-in types for this schema (those that are not displayed when printing the schema).
1210
1219
  */
1211
- builtInTypes<T extends NamedType>(kind?: T['kind'], includeShadowed: boolean = false): readonly T[] {
1220
+ builtInTypes(includeShadowed: boolean = false): readonly NamedType[] {
1212
1221
  const allBuiltIns = this._builtInTypes.values();
1213
- const forKind = (kind ? allBuiltIns.filter(t => t.kind === kind) : allBuiltIns) as readonly T[];
1214
1222
  return includeShadowed
1215
- ? forKind
1216
- : forKind.filter(t => !this.isShadowedBuiltInType(t));
1223
+ ? allBuiltIns
1224
+ : allBuiltIns.filter(t => !this.isShadowedBuiltInType(t));
1217
1225
  }
1218
1226
 
1219
1227
  private isShadowedBuiltInType(type: NamedType) {
@@ -1223,8 +1231,8 @@ export class Schema {
1223
1231
  /**
1224
1232
  * All the types, including the built-in ones.
1225
1233
  */
1226
- allTypes<T extends NamedType>(kind?: T['kind']): readonly T[] {
1227
- return this.builtInTypes(kind).concat(this.types(kind));
1234
+ allTypes(): readonly NamedType[] {
1235
+ return this.builtInTypes().concat(this.types());
1228
1236
  }
1229
1237
 
1230
1238
  /**
@@ -1262,9 +1270,12 @@ export class Schema {
1262
1270
 
1263
1271
  addType<T extends NamedType>(type: T): T {
1264
1272
  const existing = this.type(type.name);
1265
- // Like for directive, we let use shadow built-in types, but validation will ensure the definition is compatible.
1266
- if (existing && !existing.isBuiltIn) {
1267
- throw error(`Type ${type} already exists in this schema`);
1273
+ if (existing) {
1274
+ // Like for directive, we let user shadow built-in types, but the definition must be valid.
1275
+ if (existing.isBuiltIn) {
1276
+ } else {
1277
+ throw error(`Type ${type} already exists in this schema`);
1278
+ }
1268
1279
  }
1269
1280
  if (type.isAttached()) {
1270
1281
  // For convenience, let's not error out on adding an already added type.
@@ -1297,10 +1308,8 @@ export class Schema {
1297
1308
  /**
1298
1309
  * All the directive defined on this schema, excluding the built-in directives.
1299
1310
  */
1300
- directives(includeNonGraphQLBuiltIns: boolean = false): readonly DirectiveDefinition[] {
1301
- return includeNonGraphQLBuiltIns
1302
- ? this.builtInDirectives().filter(d => !graphQLBuiltIns.isGraphQLBuiltIn(d)).concat(this._directives.values())
1303
- : this._directives.values();
1311
+ directives(): readonly DirectiveDefinition[] {
1312
+ return this._directives.values();
1304
1313
  }
1305
1314
 
1306
1315
  /**
@@ -1322,7 +1331,11 @@ export class Schema {
1322
1331
 
1323
1332
  directive(name: string): DirectiveDefinition | undefined {
1324
1333
  const directive = this._directives.get(name);
1325
- return directive ? directive : this._builtInDirectives.get(name);
1334
+ return directive ? directive : this.builtInDirective(name);
1335
+ }
1336
+
1337
+ builtInDirective(name: string): DirectiveDefinition | undefined {
1338
+ return this._builtInDirectives.get(name);
1326
1339
  }
1327
1340
 
1328
1341
  *allNamedSchemaElement(): Generator<NamedSchemaElement<any, any, any>, void, undefined> {
@@ -1374,6 +1387,7 @@ export class Schema {
1374
1387
 
1375
1388
  invalidate() {
1376
1389
  this.isValidated = false;
1390
+ this.blueprint.onInvalidation(this);
1377
1391
  }
1378
1392
 
1379
1393
  validate() {
@@ -1381,22 +1395,20 @@ export class Schema {
1381
1395
  return;
1382
1396
  }
1383
1397
 
1384
- // This needs to run _before_ graphQL validation: otherwise, the _Entity union will be empty and fail validation.
1385
1398
  this.runWithBuiltInModificationAllowed(() => {
1386
- this.builtIns.prepareValidation(this);
1387
1399
  addIntrospectionFields(this);
1388
1400
  });
1389
1401
 
1390
- // TODO: we should ensure first that there is no undefined types (or maybe throw properly when printing the AST
1391
- // and catching that properly).
1392
- let errors = validateSDL(this.toAST(), undefined, this.builtIns.validationRules());
1402
+ // TODO: we check that all types are properly set (aren't undefined) in `validateSchema`, but `validateSDL` will error out beforehand. We should
1403
+ // probably extract that part of `validateSchema` and run `validateSDL` conditionally on that first check.
1404
+ let errors = validateSDL(this.toAST(), undefined, this.blueprint.validationRules()).map((e) => this.blueprint.onGraphQLJSValidationError(this, e));
1393
1405
  errors = errors.concat(validateSchema(this));
1394
1406
 
1395
1407
  // We avoid adding federation-specific validations if the base schema is not proper graphQL as the later can easily trigger
1396
1408
  // the former (for instance, someone mistyping the 'fields' argument name of a @key).
1397
1409
  if (errors.length === 0) {
1398
1410
  this.runWithBuiltInModificationAllowed(() => {
1399
- errors = this.builtIns.onValidation(this);
1411
+ errors = this.blueprint.onValidation(this);
1400
1412
  });
1401
1413
  }
1402
1414
 
@@ -1407,8 +1419,8 @@ export class Schema {
1407
1419
  this.isValidated = true;
1408
1420
  }
1409
1421
 
1410
- clone(builtIns?: BuiltIns): Schema {
1411
- const cloned = new Schema(builtIns ?? this.builtIns);
1422
+ clone(builtIns?: SchemaBlueprint): Schema {
1423
+ const cloned = new Schema(builtIns ?? this.blueprint);
1412
1424
  copy(this, cloned);
1413
1425
  if (this.isValidated) {
1414
1426
  // TODO: when we do actual validation, no point in redoing it, but we should
@@ -1417,6 +1429,81 @@ export class Schema {
1417
1429
  }
1418
1430
  return cloned;
1419
1431
  }
1432
+
1433
+ private getBuiltInDirective<TApplicationArgs extends {[key: string]: any}>(
1434
+ schema: Schema,
1435
+ name: string
1436
+ ): DirectiveDefinition<TApplicationArgs> {
1437
+ const directive = schema.directive(name);
1438
+ assert(directive, `The provided schema has not be built with the ${name} directive built-in`);
1439
+ return directive as DirectiveDefinition<TApplicationArgs>;
1440
+ }
1441
+
1442
+ includeDirective(schema: Schema): DirectiveDefinition<{if: boolean}> {
1443
+ return this.getBuiltInDirective(schema, 'include');
1444
+ }
1445
+
1446
+ skipDirective(schema: Schema): DirectiveDefinition<{if: boolean}> {
1447
+ return this.getBuiltInDirective(schema, 'skip');
1448
+ }
1449
+
1450
+ deprecatedDirective(schema: Schema): DirectiveDefinition<{reason?: string}> {
1451
+ return this.getBuiltInDirective(schema, 'deprecated');
1452
+ }
1453
+
1454
+ specifiedByDirective(schema: Schema): DirectiveDefinition<{url: string}> {
1455
+ return this.getBuiltInDirective(schema, 'specifiedBy');
1456
+ }
1457
+
1458
+ /**
1459
+ * Gets an element of the schema given its "schema coordinate".
1460
+ *
1461
+ * Note that the syntax for schema coordinates is the one from the upcoming GraphQL spec: https://github.com/graphql/graphql-spec/pull/794.
1462
+ */
1463
+ elementByCoordinate(coordinate: string): NamedSchemaElement<any, any, any> | undefined {
1464
+ if (!coordinate.match(coordinateRegexp)) {
1465
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1466
+ }
1467
+
1468
+ const argStartIdx = coordinate.indexOf('(');
1469
+ const start = argStartIdx < 0 ? coordinate : coordinate.slice(0, argStartIdx);
1470
+ // Argument syntax is `foo(argName:)`, so the arg name start after the open parenthesis and go until the final ':)'.
1471
+ const argName = argStartIdx < 0 ? undefined : coordinate.slice(argStartIdx + 1, coordinate.length - 2);
1472
+ const splittedStart = start.split('.');
1473
+ const typeOrDirectiveName = splittedStart[0];
1474
+ const fieldOrEnumName = splittedStart[1];
1475
+ const isDirective = typeOrDirectiveName.startsWith('@');
1476
+ if (isDirective) {
1477
+ if (fieldOrEnumName) {
1478
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1479
+ }
1480
+ const directive = this.directive(typeOrDirectiveName.slice(1));
1481
+ return argName ? directive?.argument(argName) : directive;
1482
+ } else {
1483
+ const type = this.type(typeOrDirectiveName);
1484
+ if (!type || !fieldOrEnumName) {
1485
+ return type;
1486
+ }
1487
+ switch (type.kind) {
1488
+ case 'ObjectType':
1489
+ case 'InterfaceType':
1490
+ const field = type.field(fieldOrEnumName);
1491
+ return argName ? field?.argument(argName) : field;
1492
+ case 'InputObjectType':
1493
+ if (argName) {
1494
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1495
+ }
1496
+ return type.field(fieldOrEnumName);
1497
+ case 'EnumType':
1498
+ if (argName) {
1499
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1500
+ }
1501
+ return type.value(fieldOrEnumName);
1502
+ default:
1503
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1504
+ }
1505
+ }
1506
+ }
1420
1507
  }
1421
1508
 
1422
1509
  export class RootType extends BaseExtensionMember<SchemaDefinition> {
@@ -1444,20 +1531,26 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
1444
1531
 
1445
1532
  applyDirective<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(
1446
1533
  nameOrDefOrDirective: Directive<SchemaDefinition, TApplicationArgs> | DirectiveDefinition<TApplicationArgs> | string,
1447
- args?: TApplicationArgs
1534
+ args?: TApplicationArgs,
1535
+ asFirstDirective: boolean = false,
1448
1536
  ): Directive<SchemaDefinition, TApplicationArgs> {
1449
- const applied = super.applyDirective(nameOrDefOrDirective, args) as Directive<SchemaDefinition, TApplicationArgs>;
1537
+ const applied = super.applyDirective(nameOrDefOrDirective, args, asFirstDirective) as Directive<SchemaDefinition, TApplicationArgs>;
1450
1538
  const schema = this.schema();
1451
1539
  const coreFeatures = schema.coreFeatures;
1452
1540
  if (isCoreSpecDirectiveApplication(applied)) {
1453
1541
  if (coreFeatures) {
1454
- throw error(`Invalid duplicate application of the @core feature`);
1542
+ throw error(`Invalid duplicate application of @core/@link`);
1455
1543
  }
1456
- const schemaDirective = applied as Directive<SchemaDefinition, CoreDirectiveArgs>;
1544
+ const schemaDirective = applied as Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>;
1457
1545
  const args = schemaDirective.arguments();
1458
- const url = FeatureUrl.parse(args.feature);
1459
- const core = new CoreFeature(url, args.as ?? 'core', schemaDirective, args.for);
1546
+ const url = FeatureUrl.parse((args.url ?? args.feature)!);
1547
+ const imports = extractCoreFeatureImports(url, schemaDirective);
1548
+ const core = new CoreFeature(url, args.as ?? url.name, schemaDirective, imports, args.for);
1460
1549
  Schema.prototype['markAsCoreSchema'].call(schema, core);
1550
+ // We also any core features that may have been added before we saw the @link for link itself
1551
+ this.appliedDirectives
1552
+ .filter((a) => a !== applied)
1553
+ .forEach((other) => CoreFeatures.prototype['maybeAddFeature'].call(schema.coreFeatures, other));
1461
1554
  } else if (coreFeatures) {
1462
1555
  CoreFeatures.prototype['maybeAddFeature'].call(coreFeatures, applied);
1463
1556
  }
@@ -1551,6 +1644,10 @@ export class ScalarType extends BaseNamedType<OutputTypeReferencer | InputTypeRe
1551
1644
  return false; // No inner elements
1552
1645
  }
1553
1646
 
1647
+ protected removeInnerElementsExtensions(): void {
1648
+ // No inner elements
1649
+ }
1650
+
1554
1651
  protected removeInnerElements(): void {
1555
1652
  // No inner elements
1556
1653
  }
@@ -1657,18 +1754,15 @@ abstract class FieldBasedType<T extends (ObjectType | InterfaceType) & NamedSche
1657
1754
  /**
1658
1755
  * All the fields of this type, excluding the built-in ones.
1659
1756
  */
1660
- fields(includeNonGraphQLBuiltIns: boolean = false): readonly FieldDefinition<T>[] {
1661
- if (includeNonGraphQLBuiltIns) {
1662
- return this.allFields().filter(f => !graphQLBuiltIns.isGraphQLBuiltIn(f));
1663
- }
1757
+ fields(): readonly FieldDefinition<T>[] {
1664
1758
  if (!this._cachedNonBuiltInFields) {
1665
1759
  this._cachedNonBuiltInFields = this._fields.values().filter(f => !f.isBuiltIn);
1666
1760
  }
1667
1761
  return this._cachedNonBuiltInFields;
1668
1762
  }
1669
1763
 
1670
- hasFields(includeNonGraphQLBuiltIns: boolean = false): boolean {
1671
- return this.fields(includeNonGraphQLBuiltIns).length > 0;
1764
+ hasFields(): boolean {
1765
+ return this.fields().length > 0;
1672
1766
  }
1673
1767
 
1674
1768
  /**
@@ -1761,6 +1855,11 @@ abstract class FieldBasedType<T extends (ObjectType | InterfaceType) & NamedSche
1761
1855
  return this.interfaceImplementations().some(itf => itf.ofExtension() === undefined)
1762
1856
  || this.fields().some(f => f.ofExtension() === undefined);
1763
1857
  }
1858
+
1859
+ protected removeInnerElementsExtensions(): void {
1860
+ this.interfaceImplementations().forEach(itf => itf.removeOfExtension());
1861
+ this.fields().forEach(f => f.removeOfExtension());
1862
+ }
1764
1863
  }
1765
1864
 
1766
1865
  export class ObjectType extends FieldBasedType<ObjectType, ObjectTypeReferencer> {
@@ -1949,6 +2048,10 @@ export class UnionType extends BaseNamedType<OutputTypeReferencer, UnionType> {
1949
2048
  protected removeReferenceRecursive(ref: OutputTypeReferencer): void {
1950
2049
  ref.removeRecursive();
1951
2050
  }
2051
+
2052
+ protected removeInnerElementsExtensions(): void {
2053
+ this.members().forEach(m => m.removeOfExtension());
2054
+ }
1952
2055
  }
1953
2056
 
1954
2057
  export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
@@ -1956,11 +2059,16 @@ export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
1956
2059
  protected readonly _values: EnumValue[] = [];
1957
2060
 
1958
2061
  get values(): readonly EnumValue[] {
1959
- return this._values;
2062
+ // Because our abstractions are mutable, and removal is done by calling
2063
+ // `remove()` on the element to remove, it's not unlikely someone mauy
2064
+ // try to iterate on the result of this method and call `remove()` on
2065
+ // some of the return value based on some condition. But this will break
2066
+ // in an error-prone way if we don't copy, so we do.
2067
+ return Array.from(this._values);
1960
2068
  }
1961
2069
 
1962
2070
  value(name: string): EnumValue | undefined {
1963
- return this._values.find(v => v.name == name);
2071
+ return this._values.find(v => v.name === name);
1964
2072
  }
1965
2073
 
1966
2074
  addValue(value: EnumValue): EnumValue;
@@ -2007,6 +2115,10 @@ export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
2007
2115
  protected removeReferenceRecursive(ref: OutputTypeReferencer): void {
2008
2116
  ref.removeRecursive();
2009
2117
  }
2118
+
2119
+ protected removeInnerElementsExtensions(): void {
2120
+ this._values.forEach(v => v.removeOfExtension());
2121
+ }
2010
2122
  }
2011
2123
 
2012
2124
  export class InputObjectType extends BaseNamedType<InputTypeReferencer, InputObjectType> {
@@ -2090,6 +2202,10 @@ export class InputObjectType extends BaseNamedType<InputTypeReferencer, InputObj
2090
2202
  ref.removeRecursive();
2091
2203
  }
2092
2204
  }
2205
+
2206
+ protected removeInnerElementsExtensions(): void {
2207
+ this.fields().forEach(f => f.removeOfExtension());
2208
+ }
2093
2209
  }
2094
2210
 
2095
2211
  class BaseWrapperType<T extends Type> {
@@ -2208,6 +2324,10 @@ export class FieldDefinition<TParent extends CompositeType> extends NamedSchemaE
2208
2324
  return this._extension;
2209
2325
  }
2210
2326
 
2327
+ removeOfExtension() {
2328
+ this._extension = undefined;
2329
+ }
2330
+
2211
2331
  setOfExtension(extension: Extension<TParent> | undefined) {
2212
2332
  this.checkUpdate();
2213
2333
  // It seems typescript "expand" `TParent` below into `ObjectType | Interface`, so it essentially lose the context that
@@ -2302,6 +2422,10 @@ export class InputFieldDefinition extends NamedSchemaElementWithType<InputType,
2302
2422
  return this._extension;
2303
2423
  }
2304
2424
 
2425
+ removeOfExtension() {
2426
+ this._extension = undefined;
2427
+ }
2428
+
2305
2429
  setOfExtension(extension: Extension<InputObjectType> | undefined) {
2306
2430
  this.checkUpdate();
2307
2431
  // It seems typescript "expand" `TParent` below into `ObjectType | Interface`, so it essentially lose the context that
@@ -2328,6 +2452,7 @@ export class InputFieldDefinition extends NamedSchemaElementWithType<InputType,
2328
2452
  return [];
2329
2453
  }
2330
2454
  this.onModification();
2455
+ this.removeAppliedDirectives();
2331
2456
  InputObjectType.prototype['removeFieldInternal'].call(this._parent, this);
2332
2457
  this._parent = undefined;
2333
2458
  this.type = undefined;
@@ -2384,6 +2509,7 @@ export class ArgumentDefinition<TParent extends FieldDefinition<any> | Directive
2384
2509
  return [];
2385
2510
  }
2386
2511
  this.onModification();
2512
+ this.removeAppliedDirectives();
2387
2513
  if (this._parent instanceof FieldDefinition) {
2388
2514
  FieldDefinition.prototype['removeArgumentInternal'].call(this._parent, this.name);
2389
2515
  } else {
@@ -2414,6 +2540,10 @@ export class EnumValue extends NamedSchemaElement<EnumValue, EnumType, never> {
2414
2540
  return this._extension;
2415
2541
  }
2416
2542
 
2543
+ removeOfExtension() {
2544
+ this._extension = undefined;
2545
+ }
2546
+
2417
2547
  setOfExtension(extension: Extension<EnumType> | undefined) {
2418
2548
  this.checkUpdate();
2419
2549
  if (extension && !this._parent?.extensions().has(extension)) {
@@ -2438,6 +2568,7 @@ export class EnumValue extends NamedSchemaElement<EnumValue, EnumType, never> {
2438
2568
  return [];
2439
2569
  }
2440
2570
  this.onModification();
2571
+ this.removeAppliedDirectives();
2441
2572
  EnumType.prototype['removeValueInternal'].call(this._parent, this);
2442
2573
  this._parent = undefined;
2443
2574
  // Enum values have nothing that can reference them outside of their parents
@@ -2622,6 +2753,9 @@ export class Directive<
2622
2753
  }
2623
2754
 
2624
2755
  get definition(): DirectiveDefinition | undefined {
2756
+ if (!this.isAttached()) {
2757
+ return undefined;
2758
+ }
2625
2759
  const doc = this.schema();
2626
2760
  return doc.directive(this.name);
2627
2761
  }
@@ -2681,6 +2815,10 @@ export class Directive<
2681
2815
  return this._extension;
2682
2816
  }
2683
2817
 
2818
+ removeOfExtension() {
2819
+ this._extension = undefined;
2820
+ }
2821
+
2684
2822
  setOfExtension(extension: Extension<any> | undefined) {
2685
2823
  this.checkUpdate();
2686
2824
  if (extension) {
@@ -2726,9 +2864,9 @@ export class Directive<
2726
2864
  this.onModification();
2727
2865
  const coreFeatures = this.schema().coreFeatures;
2728
2866
  if (coreFeatures && this.name === coreFeatures.coreItself.nameInSchema) {
2729
- // We're removing a @core directive application, so we remove it from the list of core features. And
2867
+ // We're removing a @core/@link directive application, so we remove it from the list of core features. And
2730
2868
  // if it is @core itself, we clean all features (to avoid having things too inconsistent).
2731
- const url = FeatureUrl.parse(this._args['feature']!);
2869
+ const url = FeatureUrl.parse(this._args[coreFeatures.coreDefinition.urlArgName()]!);
2732
2870
  if (url.identity === coreFeatures.coreItself.url.identity) {
2733
2871
  // Note that we unmark first because the loop after that will nuke our parent.
2734
2872
  Schema.prototype['unmarkAsCoreSchema'].call(this.schema());
@@ -2933,8 +3071,6 @@ export function variableDefinitionFromAST(schema: Schema, definitionNode: Variab
2933
3071
  return def;
2934
3072
  }
2935
3073
 
2936
- export const graphQLBuiltIns = new BuiltIns();
2937
-
2938
3074
  function addReferenceToType(referencer: SchemaElement<any, any>, type: Type) {
2939
3075
  switch (type.kind) {
2940
3076
  case 'ListType':
@@ -3043,7 +3179,7 @@ function copySchemaDefinitionInner(source: SchemaDefinition, dest: SchemaDefinit
3043
3179
  // Same as copyAppliedDirectives, but as the directive applies to the schema definition, we need to remember if the application
3044
3180
  // is for the extension or not.
3045
3181
  for (const directive of source.appliedDirectives) {
3046
- copyOfExtension(extensionsMap, directive, dest.applyDirective(directive.name, { ...directive.arguments() }));
3182
+ copyOfExtension(extensionsMap, directive, copyAppliedDirective(directive, dest));
3047
3183
  }
3048
3184
  dest.description = source.description;
3049
3185
  dest.sourceAST = source.sourceAST;
@@ -3054,7 +3190,7 @@ function copyNamedTypeInner(source: NamedType, dest: NamedType) {
3054
3190
  // Same as copyAppliedDirectives, but as the directive applies to the type, we need to remember if the application
3055
3191
  // is for the extension or not.
3056
3192
  for (const directive of source.appliedDirectives) {
3057
- copyOfExtension(extensionsMap, directive, dest.applyDirective(directive.name, { ...directive.arguments() }));
3193
+ copyOfExtension(extensionsMap, directive, copyAppliedDirective(directive, dest));
3058
3194
  }
3059
3195
  dest.description = source.description;
3060
3196
  dest.sourceAST = source.sourceAST;
@@ -3099,9 +3235,13 @@ function copyNamedTypeInner(source: NamedType, dest: NamedType) {
3099
3235
  }
3100
3236
 
3101
3237
  function copyAppliedDirectives(source: SchemaElement<any, any>, dest: SchemaElement<any, any>) {
3102
- for (const directive of source.appliedDirectives) {
3103
- dest.applyDirective(directive.name, { ...directive.arguments() });
3104
- }
3238
+ source.appliedDirectives.forEach((d) => copyAppliedDirective(d, dest));
3239
+ }
3240
+
3241
+ function copyAppliedDirective(source: Directive<any, any>, dest: SchemaElement<any, any>): Directive<any, any> {
3242
+ const res = dest.applyDirective(source.name, { ...source.arguments() });
3243
+ res.sourceAST = source.sourceAST
3244
+ return res;
3105
3245
  }
3106
3246
 
3107
3247
  function copyFieldDefinitionInner<P extends ObjectType | InterfaceType>(source: FieldDefinition<P>, dest: FieldDefinition<P>) {