@apollo/federation-internals 2.0.0-preview.9 → 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 (63) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/coreSpec.d.ts +1 -1
  3. package/dist/coreSpec.d.ts.map +1 -1
  4. package/dist/coreSpec.js +34 -11
  5. package/dist/coreSpec.js.map +1 -1
  6. package/dist/definitions.d.ts +20 -8
  7. package/dist/definitions.d.ts.map +1 -1
  8. package/dist/definitions.js +97 -58
  9. package/dist/definitions.js.map +1 -1
  10. package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
  11. package/dist/directiveAndTypeSpecification.js +10 -1
  12. package/dist/directiveAndTypeSpecification.js.map +1 -1
  13. package/dist/error.d.ts +10 -0
  14. package/dist/error.d.ts.map +1 -1
  15. package/dist/error.js +22 -2
  16. package/dist/error.js.map +1 -1
  17. package/dist/extractSubgraphsFromSupergraph.js +1 -1
  18. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  19. package/dist/federation.d.ts +4 -1
  20. package/dist/federation.d.ts.map +1 -1
  21. package/dist/federation.js +33 -26
  22. package/dist/federation.js.map +1 -1
  23. package/dist/federationSpec.js +1 -1
  24. package/dist/federationSpec.js.map +1 -1
  25. package/dist/inaccessibleSpec.d.ts +7 -3
  26. package/dist/inaccessibleSpec.d.ts.map +1 -1
  27. package/dist/inaccessibleSpec.js +622 -32
  28. package/dist/inaccessibleSpec.js.map +1 -1
  29. package/dist/precompute.d.ts.map +1 -1
  30. package/dist/precompute.js +2 -2
  31. package/dist/precompute.js.map +1 -1
  32. package/dist/schemaUpgrader.d.ts.map +1 -1
  33. package/dist/schemaUpgrader.js +16 -5
  34. package/dist/schemaUpgrader.js.map +1 -1
  35. package/dist/supergraphs.d.ts.map +1 -1
  36. package/dist/supergraphs.js +1 -0
  37. package/dist/supergraphs.js.map +1 -1
  38. package/dist/validate.js +13 -7
  39. package/dist/validate.js.map +1 -1
  40. package/dist/values.d.ts +2 -2
  41. package/dist/values.d.ts.map +1 -1
  42. package/dist/values.js +13 -11
  43. package/dist/values.js.map +1 -1
  44. package/package.json +3 -3
  45. package/src/__tests__/coreSpec.test.ts +112 -0
  46. package/src/__tests__/removeInaccessibleElements.test.ts +2216 -177
  47. package/src/__tests__/subgraphValidation.test.ts +22 -5
  48. package/src/__tests__/values.test.ts +315 -3
  49. package/src/coreSpec.ts +70 -16
  50. package/src/definitions.ts +201 -83
  51. package/src/directiveAndTypeSpecification.ts +18 -1
  52. package/src/error.ts +62 -2
  53. package/src/extractSubgraphsFromSupergraph.ts +1 -1
  54. package/src/federation.ts +36 -26
  55. package/src/federationSpec.ts +2 -2
  56. package/src/inaccessibleSpec.ts +973 -55
  57. package/src/precompute.ts +2 -4
  58. package/src/schemaUpgrader.ts +25 -6
  59. package/src/supergraphs.ts +1 -0
  60. package/src/validate.ts +20 -9
  61. package/src/values.ts +39 -12
  62. package/tsconfig.test.tsbuildinfo +1 -1
  63. package/tsconfig.tsbuildinfo +1 -1
@@ -1,43 +1,92 @@
1
1
  import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
2
2
  import {
3
+ ArgumentDefinition,
4
+ CoreFeatures,
3
5
  DirectiveDefinition,
4
- ErrGraphQLValidationFailed,
6
+ EnumType,
7
+ EnumValue,
8
+ ErrGraphQLAPISchemaValidationFailed,
9
+ executableDirectiveLocations,
5
10
  FieldDefinition,
6
- isCompositeType,
7
- isInterfaceType,
8
- isObjectType,
11
+ InputFieldDefinition,
12
+ InputObjectType,
13
+ InputType,
14
+ InterfaceType,
15
+ isEnumType,
16
+ isInputObjectType,
17
+ isListType,
18
+ isNonNullType,
19
+ isScalarType,
20
+ isVariable,
21
+ NamedType,
22
+ ObjectType,
23
+ ScalarType,
9
24
  Schema,
25
+ SchemaDefinition,
26
+ SchemaElement,
27
+ UnionType,
10
28
  } from "./definitions";
11
29
  import { GraphQLError, DirectiveLocation } from "graphql";
12
30
  import { registerKnownFeature } from "./knownCoreFeatures";
13
31
  import { ERRORS } from "./error";
14
- import { createDirectiveSpecification } from "./directiveAndTypeSpecification";
32
+ import { createDirectiveSpecification, DirectiveSpecification } from "./directiveAndTypeSpecification";
33
+ import { assert } from "./utils";
15
34
 
16
35
  export const inaccessibleIdentity = 'https://specs.apollo.dev/inaccessible';
17
36
 
18
- export const inaccessibleLocations = [
19
- DirectiveLocation.FIELD_DEFINITION,
20
- DirectiveLocation.OBJECT,
21
- DirectiveLocation.INTERFACE,
22
- DirectiveLocation.UNION,
23
- ];
24
-
25
- export const inaccessibleDirectiveSpec = createDirectiveSpecification({
26
- name: 'inaccessible',
27
- locations: [...inaccessibleLocations],
28
- });
29
-
30
37
  export class InaccessibleSpecDefinition extends FeatureDefinition {
38
+ public readonly inaccessibleLocations: DirectiveLocation[];
39
+ public readonly inaccessibleDirectiveSpec: DirectiveSpecification;
40
+ private readonly printedInaccessibleDefinition: string;
41
+
31
42
  constructor(version: FeatureVersion) {
32
43
  super(new FeatureUrl(inaccessibleIdentity, 'inaccessible', version));
44
+ this.inaccessibleLocations = [
45
+ DirectiveLocation.FIELD_DEFINITION,
46
+ DirectiveLocation.OBJECT,
47
+ DirectiveLocation.INTERFACE,
48
+ DirectiveLocation.UNION,
49
+ ];
50
+ this.printedInaccessibleDefinition = 'directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION';
51
+ if (!this.isV01()) {
52
+ this.inaccessibleLocations.push(
53
+ DirectiveLocation.ARGUMENT_DEFINITION,
54
+ DirectiveLocation.SCALAR,
55
+ DirectiveLocation.ENUM,
56
+ DirectiveLocation.ENUM_VALUE,
57
+ DirectiveLocation.INPUT_OBJECT,
58
+ DirectiveLocation.INPUT_FIELD_DEFINITION,
59
+ );
60
+ this.printedInaccessibleDefinition = 'directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION';
61
+ }
62
+ this.inaccessibleDirectiveSpec = createDirectiveSpecification({
63
+ name: 'inaccessible',
64
+ locations: this.inaccessibleLocations,
65
+ });
66
+ }
67
+
68
+ isV01() {
69
+ return this.version.equals(new FeatureVersion(0, 1));
33
70
  }
34
71
 
35
72
  addElementsToSchema(schema: Schema): GraphQLError[] {
36
- return this.addDirectiveSpec(schema, inaccessibleDirectiveSpec);
73
+ return this.addDirectiveSpec(schema, this.inaccessibleDirectiveSpec);
37
74
  }
38
75
 
39
- inaccessibleDirective(schema: Schema): DirectiveDefinition<Record<string, never>> {
40
- return this.directive(schema, 'inaccessible')!;
76
+ inaccessibleDirective(schema: Schema): DirectiveDefinition<Record<string, never>> | undefined {
77
+ return this.directive(schema, 'inaccessible');
78
+ }
79
+
80
+ checkCompatibleDirective(definition: DirectiveDefinition): GraphQLError | undefined {
81
+ const hasUnknownArguments = Object.keys(definition.arguments()).length > 0;
82
+ const hasRepeatable = definition.repeatable;
83
+ const hasValidLocations = definition.locations.every(loc => this.inaccessibleLocations.includes(loc));
84
+ if (hasUnknownArguments || hasRepeatable || !hasValidLocations) {
85
+ return ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
86
+ message: `Found invalid @inaccessible directive definition. Please ensure the directive definition in your schema's definitions matches the following:\n\t${this.printedInaccessibleDefinition}`,
87
+ });
88
+ }
89
+ return undefined;
41
90
  }
42
91
 
43
92
  allElementNames(): string[] {
@@ -46,11 +95,16 @@ export class InaccessibleSpecDefinition extends FeatureDefinition {
46
95
  }
47
96
 
48
97
  export const INACCESSIBLE_VERSIONS = new FeatureDefinitions<InaccessibleSpecDefinition>(inaccessibleIdentity)
49
- .add(new InaccessibleSpecDefinition(new FeatureVersion(0, 1)));
98
+ .add(new InaccessibleSpecDefinition(new FeatureVersion(0, 1)))
99
+ .add(new InaccessibleSpecDefinition(new FeatureVersion(0, 2)));
50
100
 
51
101
  registerKnownFeature(INACCESSIBLE_VERSIONS);
52
102
 
53
103
  export function removeInaccessibleElements(schema: Schema) {
104
+ // Note it doesn't hurt to validate here, since we expect the schema to be
105
+ // validated already, and if it has been, it's cached/inexpensive.
106
+ schema.validate();
107
+
54
108
  const coreFeatures = schema.coreFeatures;
55
109
  if (!coreFeatures) {
56
110
  return;
@@ -60,57 +114,921 @@ export function removeInaccessibleElements(schema: Schema) {
60
114
  if (!inaccessibleFeature) {
61
115
  return;
62
116
  }
63
- const inaccessibleSpec = INACCESSIBLE_VERSIONS.find(inaccessibleFeature.url.version);
117
+ const inaccessibleSpec = INACCESSIBLE_VERSIONS.find(
118
+ inaccessibleFeature.url.version
119
+ );
64
120
  if (!inaccessibleSpec) {
65
- throw new GraphQLError(
66
- `Cannot remove inaccessible elements: the schema uses unsupported inaccessible spec version ${inaccessibleFeature.url.version} (supported versions: ${INACCESSIBLE_VERSIONS.versions().join(', ')})`);
121
+ throw ErrGraphQLAPISchemaValidationFailed([new GraphQLError(
122
+ `Cannot remove inaccessible elements: the schema uses unsupported` +
123
+ ` inaccessible spec version ${inaccessibleFeature.url.version}` +
124
+ ` (supported versions: ${INACCESSIBLE_VERSIONS.versions().join(', ')})`
125
+ )]);
67
126
  }
68
127
 
69
128
  const inaccessibleDirective = inaccessibleSpec.inaccessibleDirective(schema);
70
129
  if (!inaccessibleDirective) {
71
- throw new GraphQLError(
72
- `Invalid schema: declares ${inaccessibleSpec.url} spec but does not define a @inaccessible directive`
73
- );
130
+ throw ErrGraphQLAPISchemaValidationFailed([new GraphQLError(
131
+ `Invalid schema: declares ${inaccessibleSpec.url} spec but does not` +
132
+ ` define a @inaccessible directive.`
133
+ )]);
74
134
  }
75
135
 
76
- const errors = [];
77
- for (const type of schema.types()) {
78
- // @inaccessible can only be on composite types.
79
- if (!isCompositeType(type)) {
80
- continue;
81
- }
82
-
83
- if (type.hasAppliedDirective(inaccessibleDirective)) {
84
- const references = type.remove();
85
- for (const reference of references) {
86
- // If the type was referenced in a field, we need to make sure that field is also inaccessible or the resulting schema will be invalid (the
87
- // field type will be `undefined`).
88
- if (reference.kind === 'FieldDefinition') {
89
- if (!reference.hasAppliedDirective(inaccessibleDirective)) {
90
- // We ship the inaccessible type and it's invalid reference in the extensions so composition can extract those and add proper links
91
- // in the subgraphs for those elements.
136
+ const incompatibleError =
137
+ inaccessibleSpec.checkCompatibleDirective(inaccessibleDirective);
138
+ if (incompatibleError) {
139
+ throw ErrGraphQLAPISchemaValidationFailed([incompatibleError]);
140
+ }
141
+
142
+ validateInaccessibleElements(
143
+ schema,
144
+ coreFeatures,
145
+ inaccessibleSpec,
146
+ inaccessibleDirective,
147
+ );
148
+
149
+ removeInaccessibleElementsAssumingValid(
150
+ schema,
151
+ inaccessibleDirective,
152
+ )
153
+ }
154
+
155
+ // These are elements that may be hidden, by either @inaccessible or core
156
+ // feature definition hiding.
157
+ type HideableElement =
158
+ | ObjectType
159
+ | InterfaceType
160
+ | UnionType
161
+ | ScalarType
162
+ | EnumType
163
+ | InputObjectType
164
+ | DirectiveDefinition
165
+ | FieldDefinition<ObjectType | InterfaceType>
166
+ | ArgumentDefinition<
167
+ | DirectiveDefinition
168
+ | FieldDefinition<ObjectType | InterfaceType>>
169
+ | InputFieldDefinition
170
+ | EnumValue
171
+
172
+ // Validate the applications of @inaccessible in the schema. Some of these may
173
+ // technically be caught by Schema.validate() later, but we'd like to give
174
+ // clearer error messaging when possible.
175
+ function validateInaccessibleElements(
176
+ schema: Schema,
177
+ coreFeatures: CoreFeatures,
178
+ inaccessibleSpec: InaccessibleSpecDefinition,
179
+ inaccessibleDirective: DirectiveDefinition,
180
+ ): void {
181
+ function isInaccessible(element: SchemaElement<any, any>): boolean {
182
+ return element.hasAppliedDirective(inaccessibleDirective);
183
+ }
184
+
185
+ const featureList = [...coreFeatures.allFeatures()];
186
+ function isFeatureDefinition(
187
+ element: NamedType | DirectiveDefinition
188
+ ): boolean {
189
+ return featureList.some((feature) => feature.isFeatureDefinition(element));
190
+ }
191
+
192
+ function isInAPISchema(element: HideableElement): boolean {
193
+ // If this element is @inaccessible, it's not in the API schema.
194
+ if (
195
+ !(element instanceof DirectiveDefinition) &&
196
+ isInaccessible(element)
197
+ ) return false;
198
+
199
+ if (
200
+ (element instanceof ObjectType) ||
201
+ (element instanceof InterfaceType) ||
202
+ (element instanceof UnionType) ||
203
+ (element instanceof ScalarType) ||
204
+ (element instanceof EnumType) ||
205
+ (element instanceof InputObjectType) ||
206
+ (element instanceof DirectiveDefinition)
207
+ ) {
208
+ // These are top-level elements. If they're not @inaccessible, the only
209
+ // way they won't be in the API schema is if they're definitions of some
210
+ // core feature.
211
+ return !isFeatureDefinition(element);
212
+ } else if (
213
+ (element instanceof FieldDefinition) ||
214
+ (element instanceof ArgumentDefinition) ||
215
+ (element instanceof InputFieldDefinition) ||
216
+ (element instanceof EnumValue)
217
+ ) {
218
+ // While this element isn't marked @inaccessible, this element won't be in
219
+ // the API schema if its parent isn't.
220
+ return isInAPISchema(element.parent);
221
+ }
222
+ assert(false, "Unreachable code, element is of unknown type.");
223
+ }
224
+
225
+ function fetchInaccessibleElementsDeep(
226
+ element: HideableElement
227
+ ): HideableElement[] {
228
+ const inaccessibleElements: HideableElement[] = [];
229
+ if (isInaccessible(element)) {
230
+ inaccessibleElements.push(element);
231
+ }
232
+
233
+ if (
234
+ (element instanceof ObjectType) ||
235
+ (element instanceof InterfaceType) ||
236
+ (element instanceof InputObjectType)
237
+ ) {
238
+ for (const field of element.fields()) {
239
+ inaccessibleElements.push(
240
+ ...fetchInaccessibleElementsDeep(field),
241
+ );
242
+ }
243
+ return inaccessibleElements;
244
+ } else if (element instanceof EnumType) {
245
+ for (const enumValue of element.values) {
246
+ inaccessibleElements.push(
247
+ ...fetchInaccessibleElementsDeep(enumValue),
248
+ )
249
+ }
250
+ return inaccessibleElements;
251
+ } else if (
252
+ (element instanceof DirectiveDefinition) ||
253
+ (element instanceof FieldDefinition)
254
+ ) {
255
+ for (const argument of element.arguments()) {
256
+ inaccessibleElements.push(
257
+ ...fetchInaccessibleElementsDeep(argument),
258
+ )
259
+ }
260
+ return inaccessibleElements;
261
+ } else if (
262
+ (element instanceof UnionType) ||
263
+ (element instanceof ScalarType) ||
264
+ (element instanceof ArgumentDefinition) ||
265
+ (element instanceof InputFieldDefinition) ||
266
+ (element instanceof EnumValue)
267
+ ) {
268
+ return inaccessibleElements;
269
+ }
270
+ assert(false, "Unreachable code, element is of unknown type.");
271
+ }
272
+
273
+ const errors: GraphQLError[] = [];
274
+ let defaultValueReferencers: Map<
275
+ DefaultValueReference,
276
+ SchemaElementWithDefaultValue[]
277
+ > | undefined = undefined;
278
+ if (!inaccessibleSpec.isV01()) {
279
+ // Note that for inaccessible v0.1, enum values and input fields can't be
280
+ // @inaccessible, so there's no need to compute references (the inaccessible
281
+ // v0.1 spec also doesn't require default values to be valid, so it doesn't
282
+ // make sense to compute them).
283
+ defaultValueReferencers = computeDefaultValueReferencers(schema);
284
+ }
285
+
286
+ for (const type of schema.allTypes()) {
287
+ if (hasBuiltInName(type)) {
288
+ // Built-in types (and their descendants) aren't allowed to be
289
+ // @inaccessible, regardless of shadowing.
290
+ const inaccessibleElements = fetchInaccessibleElementsDeep(type);
291
+ if (inaccessibleElements.length > 0) {
292
+ errors.push(ERRORS.DISALLOWED_INACCESSIBLE.err({
293
+ message:
294
+ `Built-in type "${type.coordinate}" cannot use @inaccessible.`,
295
+ nodes: type.sourceAST,
296
+ extensions: {
297
+ inaccessible_elements: inaccessibleElements
298
+ .map((element) => element.coordinate),
299
+ inaccessible_referencers: [type.coordinate],
300
+ }
301
+ }));
302
+ }
303
+ } else if (isFeatureDefinition(type)) {
304
+ // Core feature types (and their descendants) aren't allowed to be
305
+ // @inaccessible.
306
+ const inaccessibleElements = fetchInaccessibleElementsDeep(type);
307
+ if (inaccessibleElements.length > 0) {
308
+ errors.push(ERRORS.DISALLOWED_INACCESSIBLE.err({
309
+ message:
310
+ `Core feature type "${type.coordinate}" cannot use @inaccessible.`,
311
+ nodes: type.sourceAST,
312
+ extensions: {
313
+ inaccessible_elements: inaccessibleElements
314
+ .map((element) => element.coordinate),
315
+ inaccessible_referencers: [type.coordinate],
316
+ }
317
+ }));
318
+ }
319
+ } else if (isInaccessible(type)) {
320
+ // Types can be referenced by other schema elements in a few ways:
321
+ // 1. Fields, arguments, and input fields may have the type as their base
322
+ // type.
323
+ // 2. Union types may have the type as a member (for object types).
324
+ // 3. Object and interface types may implement the type (for interface
325
+ // types).
326
+ // 4. Schemas may have the type as a root operation type (for object
327
+ // types).
328
+ //
329
+ // When a type is hidden, the referencer must follow certain rules for the
330
+ // schema to be valid. Respectively, these rules are:
331
+ // 1. The field/argument/input field must not be in the API schema.
332
+ // 2. The union type, if empty, must not be in the API schema.
333
+ // 3. No rules are imposed in this case.
334
+ // 4. The root operation type must not be the query type.
335
+ //
336
+ // We validate the 1st and 4th rules above, and leave the 2nd for when we
337
+ // look at accessible union types.
338
+ const referencers = type.referencers();
339
+ for (const referencer of referencers) {
340
+ if (
341
+ referencer instanceof FieldDefinition ||
342
+ referencer instanceof ArgumentDefinition ||
343
+ referencer instanceof InputFieldDefinition
344
+ ) {
345
+ if (isInAPISchema(referencer)) {
92
346
  errors.push(ERRORS.REFERENCED_INACCESSIBLE.err({
93
- message: `Field "${reference.coordinate}" returns @inaccessible type "${type}" without being marked @inaccessible itself.`,
94
- nodes: reference.sourceAST,
347
+ message:
348
+ `Type "${type.coordinate}" is @inaccessible but is referenced` +
349
+ ` by "${referencer.coordinate}", which is in the API schema.`,
350
+ nodes: type.sourceAST,
351
+ extensions: {
352
+ inaccessible_elements: [type.coordinate],
353
+ inaccessible_referencers: [referencer.coordinate],
354
+ }
355
+ }));
356
+ }
357
+ } else if (referencer instanceof SchemaDefinition) {
358
+ if (type === referencer.rootType('query')) {
359
+ errors.push(ERRORS.QUERY_ROOT_TYPE_INACCESSIBLE.err({
360
+ message:
361
+ `Type "${type.coordinate}" is @inaccessible but is the root` +
362
+ ` query type, which must be in the API schema.`,
363
+ nodes: type.sourceAST,
364
+ extensions: {
365
+ inaccessible_elements: [type.coordinate],
366
+ }
367
+ }));
368
+ }
369
+ }
370
+ }
371
+ } else {
372
+ // At this point, we know the type must be in the API schema. For types
373
+ // with children (all types except scalar), we check that at least one of
374
+ // the children is accessible.
375
+ if (
376
+ (type instanceof ObjectType) ||
377
+ (type instanceof InterfaceType) ||
378
+ (type instanceof InputObjectType)
379
+ ) {
380
+ let isEmpty = true;
381
+ for (const field of type.fields()) {
382
+ if (!isInaccessible(field)) isEmpty = false;
383
+ }
384
+ if (isEmpty) {
385
+ errors.push(ERRORS.ONLY_INACCESSIBLE_CHILDREN.err({
386
+ message:
387
+ `Type "${type.coordinate}" is in the API schema but all of its` +
388
+ ` ${(type instanceof InputObjectType) ? 'input ' : ''}fields` +
389
+ ` are @inaccessible.`,
390
+ nodes: type.sourceAST,
391
+ extensions: {
392
+ inaccessible_elements: type.fields()
393
+ .map((field) => field.coordinate),
394
+ inaccessible_referencers: [type.coordinate],
395
+ }
396
+ }));
397
+ }
398
+ } else if (type instanceof UnionType) {
399
+ let isEmpty = true;
400
+ for (const member of type.types()) {
401
+ if (!isInaccessible(member)) isEmpty = false;
402
+ }
403
+ if (isEmpty) {
404
+ errors.push(ERRORS.ONLY_INACCESSIBLE_CHILDREN.err({
405
+ message:
406
+ `Type "${type.coordinate}" is in the API schema but all of its` +
407
+ ` members are @inaccessible.`,
408
+ nodes: type.sourceAST,
409
+ extensions: {
410
+ inaccessible_elements: type.types()
411
+ .map((type) => type.coordinate),
412
+ inaccessible_referencers: [type.coordinate],
413
+ }
414
+ }));
415
+ }
416
+ } else if (type instanceof EnumType) {
417
+ let isEmpty = true;
418
+ for (const enumValue of type.values) {
419
+ if (!isInaccessible(enumValue)) isEmpty = false;
420
+ }
421
+ if (isEmpty) {
422
+ errors.push(ERRORS.ONLY_INACCESSIBLE_CHILDREN.err({
423
+ message:
424
+ `Type "${type.coordinate}" is in the API schema but all of its` +
425
+ ` values are @inaccessible.`,
426
+ nodes: type.sourceAST,
427
+ extensions: {
428
+ inaccessible_elements: type.values
429
+ .map((enumValue) => enumValue.coordinate),
430
+ inaccessible_referencers: [type.coordinate],
431
+ }
432
+ }));
433
+ }
434
+ }
435
+
436
+ // Descend into the type's children if needed.
437
+ if (
438
+ (type instanceof ObjectType) ||
439
+ (type instanceof InterfaceType)
440
+ ) {
441
+ const implementedInterfaces = type.interfaces();
442
+ const implementingTypes: (ObjectType | InterfaceType)[] = [];
443
+ if (type instanceof InterfaceType) {
444
+ for (const referencer of type.referencers()) {
445
+ if (
446
+ (referencer instanceof ObjectType) ||
447
+ (referencer instanceof InterfaceType)
448
+ ) {
449
+ implementingTypes.push(referencer);
450
+ }
451
+ }
452
+ }
453
+ for (const field of type.fields()) {
454
+ if (isInaccessible(field)) {
455
+ // Fields can be "referenced" by the corresponding fields of any
456
+ // interfaces their parent type implements. When a field is hidden
457
+ // (but its parent isn't), we check that such implemented fields
458
+ // aren't in the API schema.
459
+ for (const implementedInterface of implementedInterfaces) {
460
+ const implementedField = implementedInterface.field(field.name);
461
+ if (implementedField && isInAPISchema(implementedField)) {
462
+ errors.push(ERRORS.IMPLEMENTED_BY_INACCESSIBLE.err({
463
+ message:
464
+ `Field "${field.coordinate}" is @inaccessible but` +
465
+ ` implements the interface field` +
466
+ ` "${implementedField.coordinate}", which is in the API` +
467
+ ` schema.`,
468
+ nodes: field.sourceAST,
469
+ extensions: {
470
+ inaccessible_elements: [field.coordinate],
471
+ inaccessible_referencers: [implementedField.coordinate],
472
+ }
473
+ }));
474
+ }
475
+ }
476
+ } else {
477
+ // Descend into the field's arguments.
478
+ for (const argument of field.arguments()) {
479
+ if (isInaccessible(argument)) {
480
+ // When an argument is hidden (but its ancestors aren't), we
481
+ // check that it isn't a required argument of its field.
482
+ if (argument.isRequired()) {
483
+ errors.push(ERRORS.REQUIRED_INACCESSIBLE.err({
484
+ message:
485
+ `Argument "${argument.coordinate}" is @inaccessible but` +
486
+ ` is a required argument of its field.`,
487
+ nodes: argument.sourceAST,
488
+ extensions: {
489
+ inaccessible_elements: [argument.coordinate],
490
+ inaccessible_referencers: [argument.coordinate],
491
+ }
492
+ }));
493
+ }
494
+ // When an argument is hidden (but its ancestors aren't), we
495
+ // check that it isn't a required argument of any implementing
496
+ // fields in the API schema. This is because the GraphQL spec
497
+ // requires that any arguments of an implementing field that
498
+ // aren't in its implemented field are optional.
499
+ //
500
+ // You might be thinking that a required argument in an
501
+ // implementing field would necessitate that the implemented
502
+ // field would also require that argument (and thus the check
503
+ // above would also always error, removing the need for this
504
+ // one), but the GraphQL spec does not enforce this. E.g. it's
505
+ // valid GraphQL for the implementing and implemented arguments
506
+ // to be both non-nullable, but for just the implemented
507
+ // argument to have a default value. Not providing a value for
508
+ // the argument when querying the implemented type succeeds
509
+ // GraphQL operation validation, but results in input coercion
510
+ // failure for the field at runtime.
511
+ for (const implementingType of implementingTypes) {
512
+ const implementingField = implementingType.field(field.name);
513
+ assert(
514
+ implementingField,
515
+ "Schema should have been valid, but an implementing type" +
516
+ " did not implement one of this type's fields."
517
+ );
518
+ const implementingArgument = implementingField
519
+ .argument(argument.name);
520
+ assert(
521
+ implementingArgument,
522
+ "Schema should have been valid, but an implementing type" +
523
+ " did not implement one of this type's field's arguments."
524
+ );
525
+ if (
526
+ isInAPISchema(implementingArgument) &&
527
+ implementingArgument.isRequired()
528
+ ) {
529
+ errors.push(ERRORS.REQUIRED_INACCESSIBLE.err({
530
+ message:
531
+ `Argument "${argument.coordinate}" is @inaccessible` +
532
+ ` but is implemented by the required argument` +
533
+ ` "${implementingArgument.coordinate}", which is` +
534
+ ` in the API schema.`,
535
+ nodes: argument.sourceAST,
536
+ extensions: {
537
+ inaccessible_elements: [argument.coordinate],
538
+ inaccessible_referencers: [
539
+ implementingArgument.coordinate,
540
+ ],
541
+ }
542
+ }));
543
+ }
544
+ }
545
+
546
+ // Arguments can be "referenced" by the corresponding arguments
547
+ // of any interfaces their parent type implements. When an
548
+ // argument is hidden (but its ancestors aren't), we check that
549
+ // such implemented arguments aren't in the API schema.
550
+ for (const implementedInterface of implementedInterfaces) {
551
+ const implementedArgument = implementedInterface
552
+ .field(field.name)
553
+ ?.argument(argument.name);
554
+ if (
555
+ implementedArgument &&
556
+ isInAPISchema(implementedArgument)
557
+ ) {
558
+ errors.push(ERRORS.IMPLEMENTED_BY_INACCESSIBLE.err({
559
+ message:
560
+ `Argument "${argument.coordinate}" is @inaccessible` +
561
+ ` but implements the interface argument` +
562
+ ` "${implementedArgument.coordinate}", which is in` +
563
+ ` the API schema.`,
564
+ nodes: argument.sourceAST,
565
+ extensions: {
566
+ inaccessible_elements: [argument.coordinate],
567
+ inaccessible_referencers: [
568
+ implementedArgument.coordinate,
569
+ ],
570
+ }
571
+ }));
572
+ }
573
+ }
574
+ }
575
+ }
576
+ }
577
+ }
578
+ } else if (type instanceof InputObjectType) {
579
+ for (const inputField of type.fields()) {
580
+ if (isInaccessible(inputField)) {
581
+ // When an input field is hidden (but its parent isn't), we check
582
+ // that it isn't a required argument of its field.
583
+ if (inputField.isRequired()) {
584
+ errors.push(ERRORS.REQUIRED_INACCESSIBLE.err({
585
+ message:
586
+ `Input field "${inputField.coordinate}" is @inaccessible` +
587
+ ` but is a required input field of its type.`,
588
+ nodes: inputField.sourceAST,
589
+ extensions: {
590
+ inaccessible_elements: [inputField.coordinate],
591
+ inaccessible_referencers: [inputField.coordinate],
592
+ }
593
+ }));
594
+ }
595
+
596
+ // Input fields can be referenced by schema default values. When an
597
+ // input field is hidden (but its parent isn't), we check that the
598
+ // arguments/input fields with such default values aren't in the API
599
+ // schema.
600
+ assert(
601
+ defaultValueReferencers,
602
+ "Input fields can't be @inaccessible in v0.1, but default value" +
603
+ " referencers weren't computed (which is only skipped for v0.1)."
604
+ );
605
+ const referencers = defaultValueReferencers.get(inputField) ?? [];
606
+ for (const referencer of referencers) {
607
+ if (isInAPISchema(referencer)) {
608
+ errors.push(ERRORS.DEFAULT_VALUE_USES_INACCESSIBLE.err({
609
+ message:
610
+ `Input field "${inputField.coordinate}" is @inaccessible` +
611
+ ` but is used in the default value of` +
612
+ ` "${referencer.coordinate}", which is in the API schema.`,
613
+ nodes: type.sourceAST,
614
+ extensions: {
615
+ inaccessible_elements: [type.coordinate],
616
+ inaccessible_referencers: [referencer.coordinate],
617
+ }
618
+ }));
619
+ }
620
+ }
621
+ }
622
+ }
623
+ } else if (type instanceof EnumType) {
624
+ for (const enumValue of type.values) {
625
+ if (isInaccessible(enumValue)) {
626
+ // Enum values can be referenced by schema default values. When an
627
+ // enum value is hidden (but its parent isn't), we check that the
628
+ // arguments/input fields with such default values aren't in the API
629
+ // schema.
630
+ assert(
631
+ defaultValueReferencers,
632
+ "Enum values can't be @inaccessible in v0.1, but default value" +
633
+ " referencers weren't computed (which is only skipped for v0.1)."
634
+ );
635
+ const referencers = defaultValueReferencers.get(enumValue) ?? [];
636
+ for (const referencer of referencers) {
637
+ if (isInAPISchema(referencer)) {
638
+ errors.push(ERRORS.DEFAULT_VALUE_USES_INACCESSIBLE.err({
639
+ message:
640
+ `Enum value "${enumValue.coordinate}" is @inaccessible` +
641
+ ` but is used in the default value of` +
642
+ ` "${referencer.coordinate}", which is in the API schema.`,
643
+ nodes: type.sourceAST,
644
+ extensions: {
645
+ inaccessible_elements: [type.coordinate],
646
+ inaccessible_referencers: [referencer.coordinate],
647
+ }
648
+ }));
649
+ }
650
+ }
651
+ }
652
+ }
653
+ }
654
+ }
655
+ }
656
+
657
+ const executableDirectiveLocationSet = new Set(executableDirectiveLocations);
658
+ for (const directive of schema.allDirectives()) {
659
+ const typeSystemLocations = directive.locations.filter((loc) =>
660
+ !executableDirectiveLocationSet.has(loc)
661
+ );
662
+ if (hasBuiltInName(directive)) {
663
+ // Built-in directives (and their descendants) aren't allowed to be
664
+ // @inaccessible, regardless of shadowing.
665
+ const inaccessibleElements =
666
+ fetchInaccessibleElementsDeep(directive);
667
+ if (inaccessibleElements.length > 0) {
668
+ errors.push(ERRORS.DISALLOWED_INACCESSIBLE.err({
669
+ message:
670
+ `Built-in directive "${directive.coordinate}" cannot use` +
671
+ ` @inaccessible.`,
672
+ nodes: directive.sourceAST,
673
+ extensions: {
674
+ inaccessible_elements: inaccessibleElements
675
+ .map((element) => element.coordinate),
676
+ inaccessible_referencers: [directive.coordinate],
677
+ }
678
+ }));
679
+ }
680
+ } else if (isFeatureDefinition(directive)) {
681
+ // Core feature directives (and their descendants) aren't allowed to be
682
+ // @inaccessible.
683
+ const inaccessibleElements =
684
+ fetchInaccessibleElementsDeep(directive);
685
+ if (inaccessibleElements.length > 0) {
686
+ errors.push(ERRORS.DISALLOWED_INACCESSIBLE.err({
687
+ message:
688
+ `Core feature directive "${directive.coordinate}" cannot use` +
689
+ ` @inaccessible.`,
690
+ nodes: directive.sourceAST,
691
+ extensions: {
692
+ inaccessible_elements: inaccessibleElements
693
+ .map((element) => element.coordinate),
694
+ inaccessible_referencers: [directive.coordinate],
695
+ }
696
+ }));
697
+ }
698
+ } else if (typeSystemLocations.length > 0) {
699
+ // Directives that can appear on type-system locations (and their
700
+ // descendants) aren't allowed to be @inaccessible.
701
+ const inaccessibleElements =
702
+ fetchInaccessibleElementsDeep(directive);
703
+ if (inaccessibleElements.length > 0) {
704
+ errors.push(ERRORS.DISALLOWED_INACCESSIBLE.err({
705
+ message:
706
+ `Directive "${directive.coordinate}" cannot use @inaccessible` +
707
+ ` because it may be applied to these type-system locations:` +
708
+ ` ${typeSystemLocations.join(', ')}.`,
709
+ nodes: directive.sourceAST,
710
+ extensions: {
711
+ inaccessible_elements: inaccessibleElements
712
+ .map((element) => element.coordinate),
713
+ inaccessible_referencers: [directive.coordinate],
714
+ }
715
+ }));
716
+ }
717
+ } else {
718
+ // At this point, we know the directive must be in the API schema. Descend
719
+ // into the directive's arguments.
720
+ for (const argument of directive.arguments()) {
721
+ // When an argument is hidden (but its parent isn't), we check that it
722
+ // isn't a required argument of its directive.
723
+ if (argument.isRequired()) {
724
+ if (isInaccessible(argument)) {
725
+ errors.push(ERRORS.REQUIRED_INACCESSIBLE.err({
726
+ message:
727
+ `Argument "${argument.coordinate}" is @inaccessible but is a` +
728
+ ` required argument of its directive.`,
729
+ nodes: argument.sourceAST,
95
730
  extensions: {
96
- "inaccessible_element": type.coordinate,
97
- "inaccessible_reference": reference.coordinate,
731
+ inaccessible_elements: [argument.coordinate],
732
+ inaccessible_referencers: [argument.coordinate],
98
733
  }
99
734
  }));
100
735
  }
101
736
  }
102
- // Other references can be:
103
- // - the type may have been a root type: in that case the schema will simply not have a root for that kind.
104
- // - the type may have been part of a union: it will have been removed from that union. This can leave the union empty but ...
105
- // - the type may an interface that other types implements: those other will simply not implement the (non-existing) interface.
106
737
  }
107
- } else if (isObjectType(type) || isInterfaceType(type)) {
108
- const toRemove = (type.fields() as FieldDefinition<any>[]).filter(f => f.hasAppliedDirective(inaccessibleDirective));
109
- toRemove.forEach(f => f.remove());
110
738
  }
111
739
  }
112
740
 
113
741
  if (errors.length > 0) {
114
- throw ErrGraphQLValidationFailed(errors, `Schema has ${errors.length === 1 ? 'an invalid use' : 'invalid uses'} of the @inaccessible directive.`);
742
+ throw ErrGraphQLAPISchemaValidationFailed(errors);
743
+ }
744
+ }
745
+
746
+ type DefaultValueReference = InputFieldDefinition | EnumValue;
747
+ type SchemaElementWithDefaultValue =
748
+ | ArgumentDefinition<
749
+ | DirectiveDefinition
750
+ | FieldDefinition<ObjectType | InterfaceType>>
751
+ | InputFieldDefinition;
752
+
753
+ // Default values in a schema may contain references to selectable elements that
754
+ // are @inaccessible (input fields and enum values). For a given schema, this
755
+ // function returns a map from such selectable elements to the elements with
756
+ // default values referencing them. (The default values of built-ins and their
757
+ // descendants are skipped.)
758
+ //
759
+ // This function assumes default values are coercible to their location types
760
+ // (see the comments for addValueReferences() for details).
761
+ function computeDefaultValueReferencers(
762
+ schema: Schema,
763
+ ): Map<
764
+ DefaultValueReference,
765
+ SchemaElementWithDefaultValue[]
766
+ > {
767
+ const referencers = new Map<
768
+ DefaultValueReference,
769
+ SchemaElementWithDefaultValue[]
770
+ >();
771
+
772
+ function addReference(
773
+ reference: DefaultValueReference,
774
+ referencer: SchemaElementWithDefaultValue,
775
+ ) {
776
+ const referencerList = referencers.get(reference) ?? [];
777
+ if (referencerList.length === 0) {
778
+ referencers.set(reference, referencerList);
779
+ }
780
+ referencerList.push(referencer);
781
+ }
782
+
783
+ // Note that the fields/arguments/input fields for built-in schema elements
784
+ // can presumably only have types that are built-in types. Since built-ins and
785
+ // their children aren't allowed to be @inaccessible, this means we shouldn't
786
+ // have to worry about references within the default values of arguments and
787
+ // input fields of built-ins, which is why we skip them below.
788
+ for (const type of schema.allTypes()) {
789
+ if (hasBuiltInName(type)) continue;
790
+
791
+ // Scan object/interface field arguments.
792
+ if (
793
+ (type instanceof ObjectType) ||
794
+ (type instanceof InterfaceType)
795
+ ) {
796
+ for (const field of type.fields()) {
797
+ for (const argument of field.arguments()) {
798
+ for (
799
+ const reference of computeDefaultValueReferences(argument)
800
+ ) {
801
+ addReference(reference, argument);
802
+ }
803
+ }
804
+ }
805
+ }
806
+
807
+ // Scan input object fields.
808
+ if (type instanceof InputObjectType) {
809
+ for (const inputField of type.fields()) {
810
+ for (
811
+ const reference of computeDefaultValueReferences(inputField)
812
+ ) {
813
+ addReference(reference, inputField);
814
+ }
815
+ }
816
+ }
817
+ }
818
+
819
+ // Scan directive definition arguments.
820
+ for (const directive of schema.allDirectives()) {
821
+ if (hasBuiltInName(directive)) continue;
822
+ for (const argument of directive.arguments()) {
823
+ for (
824
+ const reference of computeDefaultValueReferences(argument)
825
+ ) {
826
+ addReference(reference, argument);
827
+ }
828
+ }
829
+ }
830
+
831
+ return referencers;
832
+ }
833
+
834
+ // For the given element, compute a list of input fields and enum values that
835
+ // are referenced in its default value (if any). This function assumes the
836
+ // default value is coercible to the element's type (see the comments for
837
+ // addValueReferences() for details).
838
+ function computeDefaultValueReferences(
839
+ element: SchemaElementWithDefaultValue,
840
+ ): DefaultValueReference[] {
841
+ const references: DefaultValueReference[] = [];
842
+ addValueReferences(
843
+ element.defaultValue,
844
+ getInputType(element),
845
+ references,
846
+ )
847
+ return references;
848
+ }
849
+
850
+ function getInputType(element: SchemaElementWithDefaultValue): InputType {
851
+ const type = element.type;
852
+ assert(
853
+ type,
854
+ "Schema should have been valid, but argument/input field did not have type."
855
+ );
856
+ return type;
857
+ }
858
+
859
+ // For the given GraphQL input value (represented in the format implicitly
860
+ // defined in buildValue()) and its type, add any references to input fields and
861
+ // enum values in that input value to the given references list.
862
+ //
863
+ // Note that this function requires the input value to be coercible to its type,
864
+ // similar to the "Values of Correct Type" validation in the GraphQL spec.
865
+ // However, there are two noteable differences:
866
+ // 1. Variable references are not allowed.
867
+ // 2. Scalar values are not required to be coercible (due to machine-specific
868
+ // differences in input coercion rules).
869
+ //
870
+ // As it turns out, building a Schema object validates this (and a bit more)
871
+ // already, so in the interests of not duplicating validations/keeping the logic
872
+ // centralized, this code assumes the input values it receives satisfy the above
873
+ // validations.
874
+ //
875
+ // Accordingly, this function's code is structured very similarly to the
876
+ // valueToString() function, which makes similar assumptions about its given
877
+ // value. If any inconsistencies/invalidities are discovered, they will be
878
+ // silently ignored.
879
+ function addValueReferences(
880
+ value: any,
881
+ type: InputType,
882
+ references: DefaultValueReference[],
883
+ ): void {
884
+ if (value === undefined || value === null) {
885
+ return;
886
+ }
887
+
888
+ if (isNonNullType(type)) {
889
+ return addValueReferences(value, type.ofType, references);
890
+ }
891
+
892
+ if (isScalarType(type)) {
893
+ // No need to look at scalar values.
894
+ return;
895
+ }
896
+
897
+ if (isVariable(value)) {
898
+ // Values in schemas shouldn't use variables, but we silently ignore it.
899
+ return;
900
+ }
901
+
902
+ if (Array.isArray(value)) {
903
+ if (isListType(type)) {
904
+ const itemType = type.ofType;
905
+ for (const item of value) {
906
+ addValueReferences(item, itemType, references);
907
+ }
908
+ } else {
909
+ // At this point a JS array can only be a list type, but we silently
910
+ // ignore when it's not.
911
+ }
912
+ return;
913
+ }
914
+
915
+ if (isListType(type)) {
916
+ // Note that GraphQL spec coerces non-list items into single-element lists.
917
+ return addValueReferences(value, type.ofType, references);
918
+ }
919
+
920
+ if (typeof value === 'object') {
921
+ if (isInputObjectType(type)) {
922
+ // Silently ignore object keys that aren't in the input object.
923
+ for (const field of type.fields()) {
924
+ const fieldValue = value[field.name];
925
+ if (fieldValue !== undefined) {
926
+ references.push(field);
927
+ addValueReferences(fieldValue, field.type!, references);
928
+ } else {
929
+ // Silently ignore when required input fields are omitted.
930
+ }
931
+ }
932
+ } else {
933
+ // At this point a JS object can only be an input object type, but we
934
+ // silently ignore when it's not.
935
+ }
936
+ return;
937
+ }
938
+
939
+ if (typeof value === 'string') {
940
+ if (isEnumType(type)) {
941
+ const enumValue = type.value(value);
942
+ if (enumValue !== undefined) {
943
+ references.push(enumValue);
944
+ } else {
945
+ // Silently ignore enum values that aren't in the enum type.
946
+ }
947
+ } else {
948
+ // At this point a JS string can only be an enum type, but we silently
949
+ // ignore when it's not.
950
+ }
951
+ return;
952
+ }
953
+
954
+ // This should be unreachable code, but we silently ignore when it's not.
955
+ return;
956
+ }
957
+
958
+ // Determine whether a given schema element has a built-in's name. Note that
959
+ // this is not the same as the isBuiltIn flag, due to shadowing definitions
960
+ // (which will not have the flag set).
961
+ function hasBuiltInName(element: NamedType | DirectiveDefinition): boolean {
962
+ const schema = element.schema();
963
+ if (
964
+ (element instanceof ObjectType) ||
965
+ (element instanceof InterfaceType) ||
966
+ (element instanceof UnionType) ||
967
+ (element instanceof ScalarType) ||
968
+ (element instanceof EnumType) ||
969
+ (element instanceof InputObjectType)
970
+ ) {
971
+ return schema.builtInTypes(true).some((type) =>
972
+ type.name === element.name
973
+ );
974
+ } else if (element instanceof DirectiveDefinition) {
975
+ return schema.builtInDirectives(true).some((directive) =>
976
+ directive.name === element.name
977
+ );
978
+ }
979
+ assert(false, "Unreachable code, element is of unknown type.")
980
+ }
981
+
982
+ // Remove schema elements marked with @inaccessible in the schema, assuming the
983
+ // schema has been validated with validateInaccessibleElements().
984
+ //
985
+ // Note the schema that results from this may not necessarily be valid GraphQL
986
+ // until core feature definitions have been removed by removeFeatureElements().
987
+ function removeInaccessibleElementsAssumingValid(
988
+ schema: Schema,
989
+ inaccessibleDirective: DirectiveDefinition,
990
+ ): void {
991
+ function isInaccessible(element: SchemaElement<any, any>): boolean {
992
+ return element.hasAppliedDirective(inaccessibleDirective);
993
+ }
994
+
995
+ for (const type of schema.types()) {
996
+ if (isInaccessible(type)) {
997
+ type.remove();
998
+ } else {
999
+ if ((type instanceof ObjectType) || (type instanceof InterfaceType)) {
1000
+ for (const field of type.fields()) {
1001
+ if (isInaccessible(field)) {
1002
+ field.remove();
1003
+ } else {
1004
+ for (const argument of field.arguments()) {
1005
+ if (isInaccessible(argument)) {
1006
+ argument.remove();
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ } else if (type instanceof InputObjectType) {
1012
+ for (const inputField of type.fields()) {
1013
+ if (isInaccessible(inputField)) {
1014
+ inputField.remove();
1015
+ }
1016
+ }
1017
+ } else if (type instanceof EnumType) {
1018
+ for (const enumValue of type.values) {
1019
+ if (isInaccessible(enumValue)) {
1020
+ enumValue.remove();
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ for (const directive of schema.directives()) {
1028
+ for (const argument of directive.arguments()) {
1029
+ if (isInaccessible(argument)) {
1030
+ argument.remove();
1031
+ }
1032
+ }
115
1033
  }
116
1034
  }