@apollo/federation-internals 2.0.0-preview.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +32 -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 +16 -8
  6. package/dist/coreSpec.d.ts.map +1 -1
  7. package/dist/coreSpec.js +205 -53
  8. package/dist/coreSpec.js.map +1 -1
  9. package/dist/definitions.d.ts +28 -11
  10. package/dist/definitions.d.ts.map +1 -1
  11. package/dist/definitions.js +185 -67
  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 +77 -20
  16. package/dist/directiveAndTypeSpecification.js.map +1 -1
  17. package/dist/error.d.ts +17 -0
  18. package/dist/error.d.ts.map +1 -1
  19. package/dist/error.js +54 -2
  20. package/dist/error.js.map +1 -1
  21. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  22. package/dist/extractSubgraphsFromSupergraph.js +7 -1
  23. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  24. package/dist/federation.d.ts +22 -5
  25. package/dist/federation.d.ts.map +1 -1
  26. package/dist/federation.js +143 -86
  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 +10 -2
  33. package/dist/inaccessibleSpec.d.ts.map +1 -1
  34. package/dist/inaccessibleSpec.js +634 -16
  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} +3 -3
  58. package/dist/precompute.js.map +1 -0
  59. package/dist/schemaUpgrader.d.ts.map +1 -1
  60. package/dist/schemaUpgrader.js +17 -7
  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 +2 -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/dist/validate.js +13 -7
  73. package/dist/validate.js.map +1 -1
  74. package/dist/values.d.ts +2 -2
  75. package/dist/values.d.ts.map +1 -1
  76. package/dist/values.js +13 -11
  77. package/dist/values.js.map +1 -1
  78. package/package.json +4 -4
  79. package/src/__tests__/coreSpec.test.ts +212 -0
  80. package/src/__tests__/definitions.test.ts +75 -0
  81. package/src/__tests__/removeInaccessibleElements.test.ts +2229 -137
  82. package/src/__tests__/schemaUpgrader.test.ts +3 -2
  83. package/src/__tests__/subgraphValidation.test.ts +419 -4
  84. package/src/__tests__/values.test.ts +315 -3
  85. package/src/buildSchema.ts +98 -51
  86. package/src/coreSpec.ts +277 -65
  87. package/src/definitions.ts +317 -92
  88. package/src/directiveAndTypeSpecification.ts +98 -21
  89. package/src/error.ts +119 -1
  90. package/src/extractSubgraphsFromSupergraph.ts +7 -1
  91. package/src/federation.ts +184 -102
  92. package/src/federationSpec.ts +56 -24
  93. package/src/inaccessibleSpec.ts +985 -39
  94. package/src/index.ts +2 -0
  95. package/src/introspection.ts +8 -3
  96. package/src/joinSpec.ts +33 -3
  97. package/src/knownCoreFeatures.ts +13 -0
  98. package/src/operations.ts +15 -0
  99. package/src/{sharing.ts → precompute.ts} +3 -6
  100. package/src/schemaUpgrader.ts +29 -13
  101. package/src/suggestions.ts +1 -1
  102. package/src/supergraphs.ts +2 -0
  103. package/src/tagSpec.ts +49 -16
  104. package/src/validate.ts +20 -9
  105. package/src/values.ts +39 -12
  106. package/tsconfig.test.tsbuildinfo +1 -1
  107. package/tsconfig.tsbuildinfo +1 -1
  108. package/dist/sharing.d.ts.map +0 -1
  109. 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, ErrGraphQLAPISchemaValidationFailed, ErrGraphQLValidationFailed, InputType, ListType, NamedType, NonNullType, ScalarType, Schema, SchemaDefinition, SchemaElement } 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));
242
- }
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;
403
+ names.push('Import');
249
404
  }
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,20 +703,77 @@ 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
 
551
- export function removeFeatureElements(schema: Schema, feature: CoreFeature) {
552
- // 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
- // actually using the type.
554
- const featureDirectives = schema.directives().filter(d => feature.isFeatureDefinition(d));
555
- featureDirectives.forEach(d => d.remove().forEach(application => application.remove()));
556
-
557
- const featureTypes = schema.types().filter(t => feature.isFeatureDefinition(t));
558
- featureTypes.forEach(type => {
559
- const references = type.remove();
560
- if (references.length > 0) {
561
- throw new GraphQLError(
562
- `Cannot remove elements of feature ${feature} as feature type ${type} is referenced by elements: ${references.join(', ')}`,
563
- references.map(r => r.sourceAST).filter(n => n !== undefined) as ASTNode[]
564
- );
706
+ registerKnownFeature(CORE_VERSIONS);
707
+ registerKnownFeature(LINK_VERSIONS);
708
+
709
+ export function removeAllCoreFeatures(schema: Schema) {
710
+ // Gather a list of core features up front, since we can't fetch them during
711
+ // removal. (Also note that core being a feature itself, this will remove core
712
+ // itself and mark the schema as 'not core').
713
+ const coreFeatures = [...(schema.coreFeatures?.allFeatures() ?? [])];
714
+
715
+ // Remove all feature elements, keeping track of any type references found
716
+ // along the way.
717
+ const typeReferences: {
718
+ feature: CoreFeature;
719
+ type: NamedType;
720
+ references: SchemaElement<any, any>[];
721
+ }[] = [];
722
+ for (const feature of coreFeatures) {
723
+ // Remove feature directive definitions and their applications.
724
+ const featureDirectiveDefs = schema.directives()
725
+ .filter(d => feature.isFeatureDefinition(d));
726
+ featureDirectiveDefs.forEach(def =>
727
+ def.remove().forEach(application => application.remove())
728
+ );
729
+
730
+ // Remove feature types.
731
+ const featureTypes = schema.types()
732
+ .filter(t => feature.isFeatureDefinition(t));
733
+ featureTypes.forEach(type => {
734
+ const references = type.remove();
735
+ if (references.length > 0) {
736
+ typeReferences.push({
737
+ feature,
738
+ type,
739
+ references,
740
+ });
565
741
  }
566
- });
742
+ });
743
+ }
744
+
745
+ // Now that we're finished with removals, for any referencers encountered,
746
+ // check whether they're still attached to the schema (and fail if they are).
747
+ //
748
+ // We wait for after all removals are done, since it means we don't have to
749
+ // worry about the ordering of removals (e.g. if one feature element refers
750
+ // to a different feature's element) or any circular references.
751
+ //
752
+ // Note that we fail for ALL type referencers, regardless of whether removing
753
+ // the type necessitates removal of the type referencer. E.g. even if some
754
+ // non-core object type were to implement some core feature interface type, we
755
+ // would still require removal of the non-core object type. Users don't have
756
+ // to enact this removal by removing the object type from their supergraph
757
+ // schema though; they could also just mark it @inaccessible (since this
758
+ // function is called after removeInaccessibleElements()).
759
+ //
760
+ // In the future, we could potentially relax this validation once we determine
761
+ // the appropriate semantics. (This validation has already been relaxed for
762
+ // directive applications, since feature directive definition removal does not
763
+ // necessitate removal of elements with directive applications.)
764
+ const errors: GraphQLError[] = [];
765
+ for (const { feature, type, references } of typeReferences) {
766
+ const referencesInSchema = references.filter(r => r.isAttached());
767
+ if (referencesInSchema.length > 0) {
768
+ errors.push(new GraphQLError(
769
+ `Cannot remove elements of feature ${feature} as feature type ${type}` +
770
+ ` is referenced by elements: ${referencesInSchema.join(', ')}`,
771
+ references.map(r => r.sourceAST)
772
+ .filter(n => n !== undefined) as ASTNode[]
773
+ ));
774
+ }
775
+ }
776
+ if (errors.length > 0) {
777
+ throw ErrGraphQLAPISchemaValidationFailed(errors);
778
+ }
567
779
  }