@apollo/federation-internals 2.0.0-preview.5 → 2.0.0-preview.9

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 (100) hide show
  1. package/CHANGELOG.md +15 -3
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +51 -41
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/coreSpec.d.ts +15 -7
  6. package/dist/coreSpec.d.ts.map +1 -1
  7. package/dist/coreSpec.js +171 -42
  8. package/dist/coreSpec.js.map +1 -1
  9. package/dist/definitions.d.ts +10 -5
  10. package/dist/definitions.d.ts.map +1 -1
  11. package/dist/definitions.js +98 -19
  12. package/dist/definitions.js.map +1 -1
  13. package/dist/directiveAndTypeSpecification.d.ts +11 -1
  14. package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
  15. package/dist/directiveAndTypeSpecification.js +67 -19
  16. package/dist/directiveAndTypeSpecification.js.map +1 -1
  17. package/dist/error.d.ts +7 -0
  18. package/dist/error.d.ts.map +1 -1
  19. package/dist/error.js +33 -1
  20. package/dist/error.js.map +1 -1
  21. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  22. package/dist/extractSubgraphsFromSupergraph.js +6 -0
  23. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  24. package/dist/federation.d.ts +18 -4
  25. package/dist/federation.d.ts.map +1 -1
  26. package/dist/federation.js +113 -63
  27. package/dist/federation.js.map +1 -1
  28. package/dist/federationSpec.d.ts +6 -2
  29. package/dist/federationSpec.d.ts.map +1 -1
  30. package/dist/federationSpec.js +47 -22
  31. package/dist/federationSpec.js.map +1 -1
  32. package/dist/inaccessibleSpec.d.ts +5 -1
  33. package/dist/inaccessibleSpec.d.ts.map +1 -1
  34. package/dist/inaccessibleSpec.js +31 -3
  35. package/dist/inaccessibleSpec.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/introspection.d.ts.map +1 -1
  41. package/dist/introspection.js +8 -3
  42. package/dist/introspection.js.map +1 -1
  43. package/dist/joinSpec.d.ts +5 -1
  44. package/dist/joinSpec.d.ts.map +1 -1
  45. package/dist/joinSpec.js +21 -0
  46. package/dist/joinSpec.js.map +1 -1
  47. package/dist/knownCoreFeatures.d.ts +4 -0
  48. package/dist/knownCoreFeatures.d.ts.map +1 -0
  49. package/dist/knownCoreFeatures.js +16 -0
  50. package/dist/knownCoreFeatures.js.map +1 -0
  51. package/dist/operations.d.ts +1 -0
  52. package/dist/operations.d.ts.map +1 -1
  53. package/dist/operations.js +16 -1
  54. package/dist/operations.js.map +1 -1
  55. package/dist/{sharing.d.ts → precompute.d.ts} +1 -1
  56. package/dist/precompute.d.ts.map +1 -0
  57. package/dist/{sharing.js → precompute.js} +1 -1
  58. package/dist/precompute.js.map +1 -0
  59. package/dist/schemaUpgrader.d.ts.map +1 -1
  60. package/dist/schemaUpgrader.js +1 -2
  61. package/dist/schemaUpgrader.js.map +1 -1
  62. package/dist/suggestions.d.ts +1 -1
  63. package/dist/suggestions.d.ts.map +1 -1
  64. package/dist/suggestions.js.map +1 -1
  65. package/dist/supergraphs.d.ts.map +1 -1
  66. package/dist/supergraphs.js +1 -0
  67. package/dist/supergraphs.js.map +1 -1
  68. package/dist/tagSpec.d.ts +7 -2
  69. package/dist/tagSpec.d.ts.map +1 -1
  70. package/dist/tagSpec.js +35 -14
  71. package/dist/tagSpec.js.map +1 -1
  72. package/package.json +3 -3
  73. package/src/__tests__/coreSpec.test.ts +100 -0
  74. package/src/__tests__/definitions.test.ts +75 -0
  75. package/src/__tests__/removeInaccessibleElements.test.ts +59 -6
  76. package/src/__tests__/schemaUpgrader.test.ts +3 -2
  77. package/src/__tests__/subgraphValidation.test.ts +402 -4
  78. package/src/buildSchema.ts +98 -51
  79. package/src/coreSpec.ts +208 -50
  80. package/src/definitions.ts +128 -21
  81. package/src/directiveAndTypeSpecification.ts +80 -20
  82. package/src/error.ts +58 -0
  83. package/src/extractSubgraphsFromSupergraph.ts +6 -0
  84. package/src/federation.ts +150 -78
  85. package/src/federationSpec.ts +56 -24
  86. package/src/inaccessibleSpec.ts +39 -11
  87. package/src/index.ts +2 -0
  88. package/src/introspection.ts +8 -3
  89. package/src/joinSpec.ts +33 -3
  90. package/src/knownCoreFeatures.ts +13 -0
  91. package/src/operations.ts +15 -0
  92. package/src/{sharing.ts → precompute.ts} +1 -2
  93. package/src/schemaUpgrader.ts +4 -7
  94. package/src/suggestions.ts +1 -1
  95. package/src/supergraphs.ts +1 -0
  96. package/src/tagSpec.ts +49 -16
  97. package/tsconfig.test.tsbuildinfo +1 -1
  98. package/tsconfig.tsbuildinfo +1 -1
  99. package/dist/sharing.d.ts.map +0 -1
  100. package/dist/sharing.js.map +0 -1
package/src/coreSpec.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import { ASTNode, DirectiveLocation, GraphQLError, StringValueNode } from "graphql";
2
2
  import { URL } from "url";
3
- import { CoreFeature, Directive, DirectiveDefinition, EnumType, ListType, NamedType, NonNullType, ScalarType, Schema, SchemaDefinition } from "./definitions";
3
+ import { CoreFeature, Directive, DirectiveDefinition, EnumType, ErrGraphQLValidationFailed, InputType, ListType, NamedType, NonNullType, ScalarType, Schema, SchemaDefinition } from "./definitions";
4
4
  import { sameType } from "./types";
5
5
  import { err } from '@apollo/core-schema';
6
6
  import { assert } from './utils';
7
7
  import { ERRORS } from "./error";
8
+ import { valueToString } from "./values";
9
+ import { coreFeatureDefinitionIfKnown, registerKnownFeature } from "./knownCoreFeatures";
10
+ import { didYouMean, suggestionList } from "./suggestions";
11
+ import { ArgumentSpecification, createDirectiveSpecification, createEnumTypeSpecification, createScalarTypeSpecification, DirectiveSpecification, TypeSpecification } from "./directiveAndTypeSpecification";
8
12
 
9
13
  export const coreIdentity = 'https://specs.apollo.dev/core';
10
14
  export const linkIdentity = 'https://specs.apollo.dev/link';
@@ -61,7 +65,9 @@ export abstract class FeatureDefinition {
61
65
  return nameInSchema != undefined && (directive.name === nameInSchema || directive.name.startsWith(`${nameInSchema}__`));
62
66
  }
63
67
 
64
- abstract addElementsToSchema(schema: Schema): void;
68
+ abstract addElementsToSchema(schema: Schema): GraphQLError[];
69
+
70
+ abstract allElementNames(): string[];
65
71
 
66
72
  protected nameInSchema(schema: Schema): string | undefined {
67
73
  const feature = this.featureInSchema(schema);
@@ -73,9 +79,9 @@ export abstract class FeatureDefinition {
73
79
  return feature ? feature.directiveNameInSchema(directiveName) : undefined;
74
80
  }
75
81
 
76
- protected typeNameInSchema(schema: Schema, directiveName: string): string | undefined {
82
+ protected typeNameInSchema(schema: Schema, typeName: string): string | undefined {
77
83
  const feature = this.featureInSchema(schema);
78
- return feature ? feature.typeNameInSchema(directiveName) : undefined;
84
+ return feature ? feature.typeNameInSchema(typeName) : undefined;
79
85
  }
80
86
 
81
87
  protected rootDirective<TApplicationArgs extends {[key: string]: any}>(schema: Schema): DirectiveDefinition<TApplicationArgs> | undefined {
@@ -101,6 +107,14 @@ export abstract class FeatureDefinition {
101
107
  return schema.addDirectiveDefinition(this.directiveNameInSchema(schema, name)!);
102
108
  }
103
109
 
110
+ protected addDirectiveSpec(schema: Schema, spec: DirectiveSpecification): GraphQLError[] {
111
+ return spec.checkOrAdd(schema, this.directiveNameInSchema(schema, spec.name));
112
+ }
113
+
114
+ protected addTypeSpec(schema: Schema, spec: TypeSpecification): GraphQLError[] {
115
+ return spec.checkOrAdd(schema, this.typeNameInSchema(schema, spec.name));
116
+ }
117
+
104
118
  protected addScalarType(schema: Schema, name: string): ScalarType {
105
119
  return schema.addType(new ScalarType(this.typeNameInSchema(schema, name)!));
106
120
  }
@@ -144,33 +158,115 @@ export type CoreImport = {
144
158
  as?: string,
145
159
  };
146
160
 
147
- export function extractCoreFeatureImports(directive: Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>): CoreImport[] {
161
+ export function extractCoreFeatureImports(url: FeatureUrl, directive: Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>): CoreImport[] {
162
+ // Note: up to this point, we've kind of cheated with typing and force-casted the arguments to `CoreOrLinkDirectiveArgs`, and while this
163
+ // graphQL type validations ensure this is "mostly" true, the `import' arg is an exception becuse it uses the `link__Import` scalar,
164
+ // and so there is no fine-grained graphQL-side validation of the values. So we'll need to double-check that the values are indeed
165
+ // either a string or a valid `CoreImport` value.
148
166
  const args = directive.arguments();
149
- if (!('import' in args)) {
167
+ if (!('import' in args) || !args.import) {
150
168
  return [];
151
169
  }
152
- const importArg = args.import;
153
- const imports: CoreImport[] = importArg ? importArg.map((a) => typeof a === 'string' ? { name: a } : a) : [];
154
- for (const i of imports) {
155
- if (!i.as) {
170
+ const importArgValue = args.import;
171
+ const definition = coreFeatureDefinitionIfKnown(url);
172
+ const knownElements = definition?.allElementNames();
173
+ const errors: GraphQLError[] = [];
174
+ const imports: CoreImport[] = [];
175
+
176
+ importArgLoop:
177
+ for (const elt of importArgValue) {
178
+ if (typeof elt === 'string') {
179
+ imports.push({ name: elt });
180
+ validateImportedName(elt, knownElements, errors, directive);
156
181
  continue;
157
182
  }
158
- if (i.name.charAt(0) === '@' && i.as.charAt(0) !== '@') {
159
- throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
160
- message: `Invalid @link import renaming: directive ${i.name} imported name should starts with a '@' character, but got "${i.as}"`,
183
+ if (typeof elt !== 'object') {
184
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
185
+ message: `Invalid sub-value ${valueToString(elt)} for @link(import:) argument: values should be either strings or input object values of the form { name: "<importedElement>", as: "<alias>" }.`,
161
186
  nodes: directive.sourceAST
162
- });
187
+ }));
188
+ continue;
189
+ }
190
+ let name: string | undefined;
191
+ for (const [key, value] of Object.entries(elt)) {
192
+ switch (key) {
193
+ case 'name':
194
+ if (typeof value !== 'string') {
195
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
196
+ message: `Invalid value for the "name" field for sub-value ${valueToString(elt)} of @link(import:) argument: must be a string.`,
197
+ nodes: directive.sourceAST
198
+ }));
199
+ continue importArgLoop;
200
+ }
201
+ name = value;
202
+ break;
203
+ case 'as':
204
+ if (typeof value !== 'string') {
205
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
206
+ message: `Invalid value for the "as" field for sub-value ${valueToString(elt)} of @link(import:) argument: must be a string.`,
207
+ nodes: directive.sourceAST
208
+ }));
209
+ continue importArgLoop;
210
+ }
211
+ break;
212
+ default:
213
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
214
+ message: `Unknown field "${key}" for sub-value ${valueToString(elt)} of @link(import:) argument.`,
215
+ nodes: directive.sourceAST
216
+ }));
217
+ continue importArgLoop;
218
+ }
163
219
  }
164
- if (i.name.charAt(0) !== '@' && i.as.charAt(0) === '@') {
165
- throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
166
- message: `Invalid @link import renaming: type ${i.name} imported name should not starts with a '@' character, but got "${i.as}" (or, if @${i.name} is a directive, then it should be refered to with a '@')`,
220
+ if (name) {
221
+ const i = elt as CoreImport;
222
+ imports.push(i);
223
+ if (i.as) {
224
+ if (i.name.charAt(0) === '@' && i.as.charAt(0) !== '@') {
225
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
226
+ message: `Invalid @link import renaming: directive "${i.name}" imported name should start with a '@' character, but got "${i.as}".`,
227
+ nodes: directive.sourceAST
228
+ }));
229
+ }
230
+ else if (i.name.charAt(0) !== '@' && i.as.charAt(0) === '@') {
231
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
232
+ message: `Invalid @link import renaming: type "${i.name}" imported name should not start with a '@' character, but got "${i.as}" (or, if @${i.name} is a directive, then it should be referred to with a '@').`,
233
+ nodes: directive.sourceAST
234
+ }));
235
+ }
236
+ }
237
+ validateImportedName(name, knownElements, errors, directive);
238
+ } else {
239
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
240
+ message: `Invalid sub-value ${valueToString(elt)} for @link(import:) argument: missing mandatory "name" field.`,
167
241
  nodes: directive.sourceAST
168
- });
242
+ }));
169
243
  }
170
244
  }
245
+
246
+ if (errors.length > 0) {
247
+ throw ErrGraphQLValidationFailed(errors);
248
+ }
171
249
  return imports;
172
250
  }
173
251
 
252
+ function validateImportedName(name: string, knownElements: string[] | undefined, errors: GraphQLError[], directive: Directive<SchemaDefinition>) {
253
+ if (knownElements && !knownElements.includes(name)) {
254
+ let details = '';
255
+ if (!name.startsWith('@') && knownElements.includes('@' + name)) {
256
+ details = ` Did you mean directive "@${name}"?`;
257
+ } else {
258
+ const suggestions = suggestionList(name, knownElements);
259
+ if (suggestions) {
260
+ details = didYouMean(suggestions);
261
+ }
262
+ }
263
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
264
+ message: `Cannot import unknown element "${name}".${details}`,
265
+ nodes: directive.sourceAST
266
+ }));
267
+ }
268
+ }
269
+
174
270
  export function isCoreSpecDirectiveApplication(directive: Directive<SchemaDefinition, any>): directive is Directive<SchemaDefinition, CoreOrLinkDirectiveArgs> {
175
271
  const definition = directive.definition;
176
272
  if (!definition) {
@@ -184,7 +280,7 @@ export function isCoreSpecDirectiveApplication(directive: Directive<SchemaDefini
184
280
  return false;
185
281
  }
186
282
  const urlArg = definition.argument('url') ?? definition.argument('feature');
187
- if (!urlArg || !sameType(urlArg.type!, new NonNullType(directive.schema().stringType()))) {
283
+ if (!urlArg || !isValidUrlArgumentType(urlArg.type!, directive.schema())) {
188
284
  return false;
189
285
  }
190
286
 
@@ -201,53 +297,112 @@ export function isCoreSpecDirectiveApplication(directive: Directive<SchemaDefini
201
297
  }
202
298
  }
203
299
 
300
+ function isValidUrlArgumentType(type: InputType, schema: Schema): boolean {
301
+ // Note that the 'url' arg is defined as nullable (mostly for future proofing reasons) but we allow use to provide a definition
302
+ // where it's non-nullable (and in practice, @core (which we never generate anymore, but recognize) definition technically uses
303
+ // with a non-nullable argument, and some fed2 previews did if for @link, so this ensure we handle reading schema generated
304
+ // by those versions just fine).
305
+ return sameType(type, schema.stringType())
306
+ || sameType(type, new NonNullType(schema.stringType()));
307
+ }
308
+
309
+ const linkPurposeTypeSpec = createEnumTypeSpecification({
310
+ name: 'Purpose',
311
+ values: corePurposes.map((name) => ({ name, description: purposesDescription(name)}))
312
+ });
313
+
314
+ const linkImportTypeSpec = createScalarTypeSpecification({ name: 'Import' });
315
+
204
316
  export class CoreSpecDefinition extends FeatureDefinition {
317
+ private readonly directiveDefinitionSpec: DirectiveSpecification;
318
+
205
319
  constructor(version: FeatureVersion, identity: string = linkIdentity, name: string = linkDirectiveDefaultName) {
206
320
  super(new FeatureUrl(identity, name, version));
321
+ this.directiveDefinitionSpec = createDirectiveSpecification({
322
+ name,
323
+ locations: [DirectiveLocation.SCHEMA],
324
+ repeatable: true,
325
+ argumentFct: (schema, nameInSchema) => this.createDefinitionArgumentSpecifications(schema, nameInSchema),
326
+ });
327
+ }
328
+
329
+ private createDefinitionArgumentSpecifications(schema: Schema, nameInSchema?: string): { args: ArgumentSpecification[], errors: GraphQLError[] } {
330
+ const args: ArgumentSpecification[] = [
331
+ { name: this.urlArgName(), type: schema.stringType() },
332
+ { name: 'as', type: schema.stringType() },
333
+ ];
334
+ if (this.supportPurposes()) {
335
+ const purposeName = `${nameInSchema ?? this.url.name}__${linkPurposeTypeSpec.name}`;
336
+ const errors = linkPurposeTypeSpec.checkOrAdd(schema, purposeName);
337
+ if (errors.length > 0) {
338
+ return { args, errors }
339
+ }
340
+ args.push({ name: 'for', type: schema.type(purposeName) as InputType });
341
+ }
342
+ if (this.supportImport()) {
343
+ const importName = `${nameInSchema ?? this.url.name}__${linkImportTypeSpec.name}`;
344
+ const errors = linkImportTypeSpec.checkOrAdd(schema, importName);
345
+ if (errors.length > 0) {
346
+ return { args, errors }
347
+ }
348
+ args.push({ name: 'import', type: new ListType(schema.type(importName)!) });
349
+ }
350
+ return { args, errors: [] };
207
351
  }
208
352
 
209
- addElementsToSchema(_: Schema): void {
353
+ addElementsToSchema(_: Schema): GraphQLError[] {
210
354
  // Core is special and the @core directive is added in `addToSchema` below
355
+ return [];
356
+ }
357
+
358
+ // TODO: we may want to allow some `import` as argument to this method. When we do, we need to watch for imports of
359
+ // `Purpose` and `Import` and add the types under their imported name.
360
+ addToSchema(schema: Schema, as?: string): GraphQLError[] {
361
+ const errors = this.addDefinitionsToSchema(schema, as);
362
+ if (errors.length > 0) {
363
+ return errors;
364
+ }
365
+
366
+ // Note: we don't use `applyFeatureToSchema` because it would complain the schema is not a core schema, which it isn't
367
+ // until the next line.
368
+ const args = { [this.urlArgName()]: this.toString() } as unknown as CoreOrLinkDirectiveArgs;
369
+ if (as) {
370
+ args.as = as;
371
+ }
372
+ schema.schemaDefinition.applyDirective(as ?? this.url.name, args, true);
373
+ return [];
211
374
  }
212
375
 
213
- addToSchema(schema: Schema, as?: string) {
214
- const existing = schema.coreFeatures;
215
- if (existing) {
216
- if (existing.coreItself.url.identity === this.identity) {
376
+ addDefinitionsToSchema(schema: Schema, as?: string): GraphQLError[] {
377
+ const existingCore = schema.coreFeatures;
378
+ if (existingCore) {
379
+ if (existingCore.coreItself.url.identity === this.identity) {
217
380
  // Already exists with the same version, let it be.
218
- return;
381
+ return [];
219
382
  } else {
220
- throw buildError(`Cannot add feature ${this} to the schema, it already uses ${existing.coreItself.url}`);
383
+ return [ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err({
384
+ message: `Cannot add feature ${this} to the schema, it already uses ${existingCore.coreItself.url}`
385
+ })];
221
386
  }
222
387
  }
223
388
 
224
389
  const nameInSchema = as ?? this.url.name;
225
- const core = schema.addDirectiveDefinition(nameInSchema).addLocations(DirectiveLocation.SCHEMA);
226
- core.repeatable = true;
227
- core.addArgument(this.urlArgName(), new NonNullType(schema.stringType()));
228
- core.addArgument('as', schema.stringType());
390
+ return this.directiveDefinitionSpec.checkOrAdd(schema, nameInSchema);
391
+ }
392
+
393
+ /**
394
+ * The list of all the element names that can be "imported" from this feature. Importantly, directive names
395
+ * must start with a `@`.
396
+ */
397
+ allElementNames(): string[] {
398
+ const names = [ `@${this.url.name}` ];
229
399
  if (this.supportPurposes()) {
230
- const purposeEnum = schema.addType(new EnumType(`${nameInSchema}__Purpose`));
231
- for (const purpose of corePurposes) {
232
- purposeEnum.addValue(purpose).description = purposesDescription(purpose);
233
- }
234
- core.addArgument('for', purposeEnum);
400
+ names.push('Purpose');
235
401
  }
236
402
  if (this.supportImport()) {
237
- if (schema.type(`${nameInSchema}__Import`)) {
238
- console.trace();
239
- }
240
- const importType = schema.addType(new ScalarType(`${nameInSchema}__Import`));
241
- core.addArgument('import', new ListType(importType));
403
+ names.push('Import');
242
404
  }
243
-
244
- // Note: we don't use `applyFeatureToSchema` because it would complain the schema is not a core schema, which it isn't
245
- // until the next line.
246
- const args = { [this.urlArgName()]: this.toString() } as unknown as CoreOrLinkDirectiveArgs;
247
- if (as) {
248
- args.as = as;
249
- }
250
- schema.schemaDefinition.applyDirective(nameInSchema, args);
405
+ return names;
251
406
  }
252
407
 
253
408
  private supportPurposes() {
@@ -280,7 +435,7 @@ export class CoreSpecDefinition extends FeatureDefinition {
280
435
  return feature.url.version;
281
436
  }
282
437
 
283
- applyFeatureToSchema(schema: Schema, feature: FeatureDefinition, as?: string, purpose?: CorePurpose) {
438
+ applyFeatureToSchema(schema: Schema, feature: FeatureDefinition, as?: string, purpose?: CorePurpose): GraphQLError[] {
284
439
  const coreDirective = this.coreDirective(schema);
285
440
  const args = {
286
441
  [this.urlArgName()]: feature.toString(),
@@ -290,7 +445,7 @@ export class CoreSpecDefinition extends FeatureDefinition {
290
445
  args.for = purpose;
291
446
  }
292
447
  schema.schemaDefinition.applyDirective(coreDirective, args);
293
- feature.addElementsToSchema(schema);
448
+ return feature.addElementsToSchema(schema);
294
449
  }
295
450
 
296
451
  extractFeatureUrl(args: CoreOrLinkDirectiveArgs): FeatureUrl {
@@ -548,6 +703,9 @@ export const CORE_VERSIONS = new FeatureDefinitions<CoreSpecDefinition>(coreIden
548
703
  export const LINK_VERSIONS = new FeatureDefinitions<CoreSpecDefinition>(linkIdentity)
549
704
  .add(new CoreSpecDefinition(new FeatureVersion(1, 0)));
550
705
 
706
+ registerKnownFeature(CORE_VERSIONS);
707
+ registerKnownFeature(LINK_VERSIONS);
708
+
551
709
  export function removeFeatureElements(schema: Schema, feature: CoreFeature) {
552
710
  // Removing directives first, so that when we remove types, the checks that there is no references don't fail due a directive of a the feature
553
711
  // actually using the type.
@@ -39,12 +39,15 @@ import { SDLValidationRule } from "graphql/validation/ValidationContext";
39
39
  import { specifiedSDLRules } from "graphql/validation/specifiedRules";
40
40
  import { validateSchema } from "./validate";
41
41
  import { createDirectiveSpecification, createScalarTypeSpecification, DirectiveSpecification, TypeSpecification } from "./directiveAndTypeSpecification";
42
+ import { didYouMean, suggestionList } from "./suggestions";
43
+ import { withModifiedErrorMessage } from "./error";
42
44
 
43
45
  const validationErrorCode = 'GraphQLValidationFailed';
46
+ const DEFAULT_VALIDATION_ERROR_MESSAGE = 'The schema is not a valid GraphQL schema.';
44
47
 
45
- export const ErrGraphQLValidationFailed = (causes: GraphQLError[]) =>
48
+ export const ErrGraphQLValidationFailed = (causes: GraphQLError[], message: string = DEFAULT_VALIDATION_ERROR_MESSAGE) =>
46
49
  err(validationErrorCode, {
47
- message: 'The schema is not a valid GraphQL schema',
50
+ message,
48
51
  causes
49
52
  });
50
53
 
@@ -381,7 +384,7 @@ export class DirectiveTargetElement<T extends DirectiveTargetElement<T>> {
381
384
  }
382
385
 
383
386
  export function sourceASTs(...elts: ({ sourceAST?: ASTNode } | undefined)[]): ASTNode[] {
384
- return elts.map(elt => elt?.sourceAST).filter(elt => elt !== undefined) as ASTNode[];
387
+ return elts.map(elt => elt?.sourceAST).filter((elt): elt is ASTNode => elt !== undefined);
385
388
  }
386
389
 
387
390
  // Not exposed: mostly about avoid code duplication between SchemaElement and Directive (which is not a SchemaElement as it can't
@@ -490,7 +493,8 @@ export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>
490
493
 
491
494
  applyDirective<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(
492
495
  nameOrDefOrDirective: Directive<TOwnType, TApplicationArgs> | DirectiveDefinition<TApplicationArgs> | string,
493
- args?: TApplicationArgs
496
+ args?: TApplicationArgs,
497
+ asFirstDirective: boolean = false,
494
498
  ): Directive<TOwnType, TApplicationArgs> {
495
499
  let toAdd: Directive<TOwnType, TApplicationArgs>;
496
500
  if (nameOrDefOrDirective instanceof Directive) {
@@ -503,9 +507,15 @@ export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>
503
507
  let name: string;
504
508
  if (typeof nameOrDefOrDirective === 'string') {
505
509
  this.checkUpdate();
506
- const def = this.schema().directive(nameOrDefOrDirective) ?? this.schema().blueprint.onMissingDirectiveDefinition(this.schema(), nameOrDefOrDirective);
510
+ const def = this.schema().directive(nameOrDefOrDirective) ?? this.schema().blueprint.onMissingDirectiveDefinition(this.schema(), nameOrDefOrDirective, args);
507
511
  if (!def) {
508
- throw new GraphQLError(`Cannot apply unknown directive "@${nameOrDefOrDirective}"`);
512
+ throw this.schema().blueprint.onGraphQLJSValidationError(
513
+ this.schema(),
514
+ new GraphQLError(`Unknown directive "@${nameOrDefOrDirective}".`)
515
+ );
516
+ }
517
+ if (Array.isArray(def)) {
518
+ throw ErrGraphQLValidationFailed(def);
509
519
  }
510
520
  name = nameOrDefOrDirective;
511
521
  } else {
@@ -516,7 +526,11 @@ export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>
516
526
  Element.prototype['setParent'].call(toAdd, this);
517
527
  }
518
528
  // TODO: we should typecheck arguments or our TApplicationArgs business is just a lie.
519
- this._appliedDirectives.push(toAdd);
529
+ if (asFirstDirective) {
530
+ this._appliedDirectives.unshift(toAdd);
531
+ } else {
532
+ this._appliedDirectives.push(toAdd);
533
+ }
520
534
  DirectiveDefinition.prototype['addReferencer'].call(toAdd.definition!, toAdd);
521
535
  this.onModification();
522
536
  return toAdd;
@@ -823,13 +837,14 @@ abstract class BaseExtensionMember<TExtended extends ExtendableElement> extends
823
837
  }
824
838
 
825
839
  export class SchemaBlueprint {
826
- onMissingDirectiveDefinition(_schema: Schema, _name: string): DirectiveDefinition | undefined {
840
+ onMissingDirectiveDefinition(_schema: Schema, _name: string, _args?: {[key: string]: any}): DirectiveDefinition | GraphQLError[] | undefined {
827
841
  // No-op by default, but used for federation.
828
842
  return undefined;
829
843
  }
830
844
 
831
- onDirectiveDefinitionAndSchemaParsed(_: Schema) {
845
+ onDirectiveDefinitionAndSchemaParsed(_: Schema): GraphQLError[] {
832
846
  // No-op by default, but used for federation.
847
+ return [];
833
848
  }
834
849
 
835
850
  ignoreParsedField(_type: NamedType, _fieldName: string): boolean {
@@ -857,6 +872,38 @@ export class SchemaBlueprint {
857
872
  validationRules(): readonly SDLValidationRule[] {
858
873
  return specifiedSDLRules;
859
874
  }
875
+
876
+ /**
877
+ * Allows to intercept some graphQL-js error messages when we can provide additional guidance to users.
878
+ */
879
+ onGraphQLJSValidationError(schema: Schema, error: GraphQLError): GraphQLError {
880
+ // For now, the main additional guidance we provide is around directives, where we could provide additional help in 2 main ways:
881
+ // - 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
882
+ // time of this writting) for directive names).
883
+ // - for fed 2 schema, if a federation directive is refered under it's "default" naming but is not properly imported (not enforced
884
+ // in the method but rather in the `FederationBlueprint`).
885
+ //
886
+ // Note that intercepting/parsing error messages to modify them is never ideal, but pragmatically, it's probably better than rewriting the relevant
887
+ // rules entirely (in that later case, our "copied" rule would stop getting any potential graphQL-js made improvements for instance). And while such
888
+ // 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.
889
+ const matcher = /^Unknown directive "@(?<directive>[_A-Za-z][_0-9A-Za-z]*)"\.$/.exec(error.message);
890
+ const name = matcher?.groups?.directive;
891
+ if (!name) {
892
+ return error;
893
+ }
894
+
895
+ const allDefinedDirectiveNames = schema.allDirectives().map((d) => d.name);
896
+ const suggestions = suggestionList(name, allDefinedDirectiveNames);
897
+ if (suggestions.length === 0) {
898
+ return this.onUnknownDirectiveValidationError(schema, name, error);
899
+ } else {
900
+ return withModifiedErrorMessage(error, `${error.message}${didYouMean(suggestions.map((s) => '@' + s))}`);
901
+ }
902
+ }
903
+
904
+ onUnknownDirectiveValidationError(_schema: Schema, _unknownDirectiveName: string, error: GraphQLError): GraphQLError {
905
+ return error;
906
+ }
860
907
  }
861
908
 
862
909
  export const defaultSchemaBlueprint = new SchemaBlueprint();
@@ -873,7 +920,8 @@ export class CoreFeature {
873
920
 
874
921
  isFeatureDefinition(element: NamedType | DirectiveDefinition): boolean {
875
922
  return element.name.startsWith(this.nameInSchema + '__')
876
- || (element.kind === 'DirectiveDefinition' && element.name === this.nameInSchema);
923
+ || (element.kind === 'DirectiveDefinition' && element.name === this.nameInSchema)
924
+ || !!this.imports.find((i) => element.name === (i.as ?? i.name));
877
925
  }
878
926
 
879
927
  directiveNameInSchema(name: string): string {
@@ -931,7 +979,7 @@ export class CoreFeatures {
931
979
  if (existing) {
932
980
  throw error(`Duplicate inclusion of feature ${url.identity}`);
933
981
  }
934
- const imports = extractCoreFeatureImports(typedDirective);
982
+ const imports = extractCoreFeatureImports(url, typedDirective);
935
983
  const feature = new CoreFeature(url, args.as ?? url.name, directive, imports, args.for);
936
984
  this.add(feature);
937
985
  directive.schema().blueprint.onAddedCoreFeature(directive.schema(), feature);
@@ -976,25 +1024,29 @@ const graphQLBuiltInDirectivesSpecifications: readonly DirectiveSpecification[]
976
1024
  createDirectiveSpecification({
977
1025
  name: 'include',
978
1026
  locations: [DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT],
979
- argumentFct: (schema) => [{ name: 'if', type: new NonNullType(schema.booleanType()) }]
1027
+ argumentFct: (schema) => ({ args: [{ name: 'if', type: new NonNullType(schema.booleanType()) }], errors: [] })
980
1028
  }),
981
1029
  createDirectiveSpecification({
982
1030
  name: 'skip',
983
1031
  locations: [DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT],
984
- argumentFct: (schema) => [{ name: 'if', type: new NonNullType(schema.booleanType()) }]
1032
+ argumentFct: (schema) => ({ args: [{ name: 'if', type: new NonNullType(schema.booleanType()) }], errors: [] })
985
1033
  }),
986
1034
  createDirectiveSpecification({
987
1035
  name: 'deprecated',
988
1036
  locations: [DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE, DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION],
989
- argumentFct: (schema) => [{ name: 'reason', type: schema.stringType(), defaultValue: 'No longer supported' }]
1037
+ argumentFct: (schema) => ({ args: [{ name: 'reason', type: schema.stringType(), defaultValue: 'No longer supported' }], errors: [] })
990
1038
  }),
991
1039
  createDirectiveSpecification({
992
1040
  name: 'specifiedBy',
993
1041
  locations: [DirectiveLocation.SCALAR],
994
- argumentFct: (schema) => [{ name: 'url', type: new NonNullType(schema.stringType()) }]
1042
+ argumentFct: (schema) => ({ args: [{ name: 'url', type: new NonNullType(schema.stringType()) }], errors: [] })
995
1043
  }),
996
1044
  ];
997
1045
 
1046
+
1047
+ // A coordinate is up to 3 "graphQL name" ([_A-Za-z][_0-9A-Za-z]*).
1048
+ const coordinateRegexp = /^@?[_A-Za-z][_0-9A-Za-z]*(\.[_A-Za-z][_0-9A-Za-z]*)?(\([_A-Za-z][_0-9A-Za-z]*:\))?$/;
1049
+
998
1050
  export class Schema {
999
1051
  private _schemaDefinition: SchemaDefinition;
1000
1052
  private readonly _builtInTypes = new MapWithCachedArrays<string, NamedType>();
@@ -1311,7 +1363,7 @@ export class Schema {
1311
1363
 
1312
1364
  // TODO: we check that all types are properly set (aren't undefined) in `validateSchema`, but `validateSDL` will error out beforehand. We should
1313
1365
  // probably extract that part of `validateSchema` and run `validateSDL` conditionally on that first check.
1314
- let errors = validateSDL(this.toAST(), undefined, this.blueprint.validationRules());
1366
+ let errors = validateSDL(this.toAST(), undefined, this.blueprint.validationRules()).map((e) => this.blueprint.onGraphQLJSValidationError(this, e));
1315
1367
  errors = errors.concat(validateSchema(this));
1316
1368
 
1317
1369
  // We avoid adding federation-specific validations if the base schema is not proper graphQL as the later can easily trigger
@@ -1364,6 +1416,56 @@ export class Schema {
1364
1416
  specifiedByDirective(schema: Schema): DirectiveDefinition<{url: string}> {
1365
1417
  return this.getBuiltInDirective(schema, 'specifiedBy');
1366
1418
  }
1419
+
1420
+ /**
1421
+ * Gets an element of the schema given its "schema coordinate".
1422
+ *
1423
+ * Note that the syntax for schema coordinates is the one from the upcoming GraphQL spec: https://github.com/graphql/graphql-spec/pull/794.
1424
+ */
1425
+ elementByCoordinate(coordinate: string): NamedSchemaElement<any, any, any> | undefined {
1426
+ if (!coordinate.match(coordinateRegexp)) {
1427
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1428
+ }
1429
+
1430
+ const argStartIdx = coordinate.indexOf('(');
1431
+ const start = argStartIdx < 0 ? coordinate : coordinate.slice(0, argStartIdx);
1432
+ // Argument syntax is `foo(argName:)`, so the arg name start after the open parenthesis and go until the final ':)'.
1433
+ const argName = argStartIdx < 0 ? undefined : coordinate.slice(argStartIdx + 1, coordinate.length - 2);
1434
+ const splittedStart = start.split('.');
1435
+ const typeOrDirectiveName = splittedStart[0];
1436
+ const fieldOrEnumName = splittedStart[1];
1437
+ const isDirective = typeOrDirectiveName.startsWith('@');
1438
+ if (isDirective) {
1439
+ if (fieldOrEnumName) {
1440
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1441
+ }
1442
+ const directive = this.directive(typeOrDirectiveName.slice(1));
1443
+ return argName ? directive?.argument(argName) : directive;
1444
+ } else {
1445
+ const type = this.type(typeOrDirectiveName);
1446
+ if (!type || !fieldOrEnumName) {
1447
+ return type;
1448
+ }
1449
+ switch (type.kind) {
1450
+ case 'ObjectType':
1451
+ case 'InterfaceType':
1452
+ const field = type.field(fieldOrEnumName);
1453
+ return argName ? field?.argument(argName) : field;
1454
+ case 'InputObjectType':
1455
+ if (argName) {
1456
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1457
+ }
1458
+ return type.field(fieldOrEnumName);
1459
+ case 'EnumType':
1460
+ if (argName) {
1461
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1462
+ }
1463
+ return type.value(fieldOrEnumName);
1464
+ default:
1465
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1466
+ }
1467
+ }
1468
+ }
1367
1469
  }
1368
1470
 
1369
1471
  export class RootType extends BaseExtensionMember<SchemaDefinition> {
@@ -1391,9 +1493,10 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
1391
1493
 
1392
1494
  applyDirective<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(
1393
1495
  nameOrDefOrDirective: Directive<SchemaDefinition, TApplicationArgs> | DirectiveDefinition<TApplicationArgs> | string,
1394
- args?: TApplicationArgs
1496
+ args?: TApplicationArgs,
1497
+ asFirstDirective: boolean = false,
1395
1498
  ): Directive<SchemaDefinition, TApplicationArgs> {
1396
- const applied = super.applyDirective(nameOrDefOrDirective, args) as Directive<SchemaDefinition, TApplicationArgs>;
1499
+ const applied = super.applyDirective(nameOrDefOrDirective, args, asFirstDirective) as Directive<SchemaDefinition, TApplicationArgs>;
1397
1500
  const schema = this.schema();
1398
1501
  const coreFeatures = schema.coreFeatures;
1399
1502
  if (isCoreSpecDirectiveApplication(applied)) {
@@ -1403,9 +1506,13 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
1403
1506
  const schemaDirective = applied as Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>;
1404
1507
  const args = schemaDirective.arguments();
1405
1508
  const url = FeatureUrl.parse((args.url ?? args.feature)!);
1406
- const imports = extractCoreFeatureImports(schemaDirective);
1509
+ const imports = extractCoreFeatureImports(url, schemaDirective);
1407
1510
  const core = new CoreFeature(url, args.as ?? url.name, schemaDirective, imports, args.for);
1408
1511
  Schema.prototype['markAsCoreSchema'].call(schema, core);
1512
+ // We also any core features that may have been added before we saw the @link for link itself
1513
+ this.appliedDirectives
1514
+ .filter((a) => a !== applied)
1515
+ .forEach((other) => CoreFeatures.prototype['maybeAddFeature'].call(schema.coreFeatures, other));
1409
1516
  } else if (coreFeatures) {
1410
1517
  CoreFeatures.prototype['maybeAddFeature'].call(coreFeatures, applied);
1411
1518
  }
@@ -2711,9 +2818,9 @@ export class Directive<
2711
2818
  this.onModification();
2712
2819
  const coreFeatures = this.schema().coreFeatures;
2713
2820
  if (coreFeatures && this.name === coreFeatures.coreItself.nameInSchema) {
2714
- // We're removing a @core directive application, so we remove it from the list of core features. And
2821
+ // We're removing a @core/@link directive application, so we remove it from the list of core features. And
2715
2822
  // if it is @core itself, we clean all features (to avoid having things too inconsistent).
2716
- const url = FeatureUrl.parse(this._args['feature']!);
2823
+ const url = FeatureUrl.parse(this._args[coreFeatures.coreDefinition.urlArgName()]!);
2717
2824
  if (url.identity === coreFeatures.coreItself.url.identity) {
2718
2825
  // Note that we unmark first because the loop after that will nuke our parent.
2719
2826
  Schema.prototype['unmarkAsCoreSchema'].call(this.schema());