@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/index.ts CHANGED
@@ -10,8 +10,10 @@ export * from './debug';
10
10
  export * from './coreSpec';
11
11
  export * from './joinSpec';
12
12
  export * from './tagSpec';
13
+ export * from './inaccessibleSpec';
13
14
  export * from './federationSpec';
14
15
  export * from './supergraphs';
15
16
  export * from './extractSubgraphsFromSupergraph';
16
17
  export * from './error';
17
18
  export * from './schemaUpgrader';
19
+ export * from './suggestions';
@@ -35,13 +35,15 @@ export function addIntrospectionFields(schema: Schema) {
35
35
  typeType.addField('possibleTypes', new ListType(new NonNullType(typeType)));
36
36
  typeType.addField('enumValues', new ListType(new NonNullType(enumValueType)))
37
37
  .addArgument('includeDeprecated', schema.booleanType(), false);
38
- typeType.addField('inputFields', new ListType(new NonNullType(inputValueType)));
38
+ typeType.addField('inputFields', new ListType(new NonNullType(inputValueType)))
39
+ .addArgument('includeDeprecated', schema.booleanType(), false);
39
40
  typeType.addField('ofType', typeType);
40
41
  typeType.addField('specifiedByURL', schema.stringType());
41
42
 
42
43
  fieldType.addField('name', new NonNullType(schema.stringType()));
43
44
  fieldType.addField('description', schema.stringType());
44
- fieldType.addField('args', new NonNullType(new ListType(new NonNullType(inputValueType))));
45
+ fieldType.addField('args', new NonNullType(new ListType(new NonNullType(inputValueType))))
46
+ .addArgument('includeDeprecated', schema.booleanType(), false);
45
47
  fieldType.addField('type', new NonNullType(typeType));
46
48
  fieldType.addField('isDeprecated', new NonNullType(schema.booleanType()));
47
49
  fieldType.addField('deprecationReason', schema.stringType());
@@ -50,6 +52,8 @@ export function addIntrospectionFields(schema: Schema) {
50
52
  inputValueType.addField('description', schema.stringType());
51
53
  inputValueType.addField('type', new NonNullType(typeType));
52
54
  inputValueType.addField('defaultValue', schema.stringType());
55
+ inputValueType.addField('isDeprecated', new NonNullType(schema.booleanType()));
56
+ inputValueType.addField('deprecationReason', schema.stringType());
53
57
 
54
58
  enumValueType.addField('name', new NonNullType(schema.stringType()));
55
59
  enumValueType.addField('description', schema.stringType());
@@ -65,7 +69,8 @@ export function addIntrospectionFields(schema: Schema) {
65
69
  directiveType.addField('name', new NonNullType(schema.stringType()));
66
70
  directiveType.addField('description', schema.stringType());
67
71
  directiveType.addField('locations', new NonNullType(new ListType(new NonNullType(directiveLocationEnum))));
68
- directiveType.addField('args', new NonNullType(new ListType(new NonNullType(inputValueType))));
72
+ directiveType.addField('args', new NonNullType(new ListType(new NonNullType(inputValueType))))
73
+ .addArgument('includeDeprecated', schema.booleanType(), false);
69
74
  directiveType.addField('isRepeatable', new NonNullType(schema.booleanType()));
70
75
 
71
76
  const schemaType = schema.addType(new ObjectType('__Schema', true));
package/src/joinSpec.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { DirectiveLocation } from 'graphql';
1
+ import { DirectiveLocation, GraphQLError } from 'graphql';
2
2
  import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
3
3
  import {
4
4
  DirectiveDefinition,
@@ -8,6 +8,7 @@ import {
8
8
  NonNullType,
9
9
  } from "./definitions";
10
10
  import { Subgraph, Subgraphs } from "./federation";
11
+ import { registerKnownFeature } from './knownCoreFeatures';
11
12
  import { MultiMap } from "./utils";
12
13
 
13
14
  export const joinIdentity = 'https://specs.apollo.dev/join';
@@ -38,7 +39,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
38
39
  return this.version.equals(new FeatureVersion(0, 1));
39
40
  }
40
41
 
41
- addElementsToSchema(schema: Schema) {
42
+ addElementsToSchema(schema: Schema): GraphQLError[] {
42
43
  const joinGraph = this.addDirective(schema, 'graph').addLocations(DirectiveLocation.ENUM_VALUE);
43
44
  joinGraph.addArgument('name', new NonNullType(schema.stringType()));
44
45
  joinGraph.addArgument('url', new NonNullType(schema.stringType()));
@@ -73,6 +74,8 @@ export class JoinSpecDefinition extends FeatureDefinition {
73
74
  if (!this.isV01()) {
74
75
  joinField.addArgument('type', schema.stringType());
75
76
  joinField.addArgument('external', schema.booleanType());
77
+ joinField.addArgument('override', schema.stringType());
78
+ joinField.addArgument('usedOverridden', schema.booleanType());
76
79
  }
77
80
 
78
81
  if (!this.isV01()) {
@@ -88,6 +91,23 @@ export class JoinSpecDefinition extends FeatureDefinition {
88
91
  const joinOwner = this.addDirective(schema, 'owner').addLocations(DirectiveLocation.OBJECT);
89
92
  joinOwner.addArgument('graph', new NonNullType(graphEnum));
90
93
  }
94
+ return [];
95
+ }
96
+
97
+ allElementNames(): string[] {
98
+ const names = [
99
+ 'graph',
100
+ 'Graph',
101
+ 'FieldSet',
102
+ '@type',
103
+ '@field',
104
+ ];
105
+ if (this.isV01()) {
106
+ names.push('@owner');
107
+ } else {
108
+ names.push('@implements');
109
+ }
110
+ return names;
91
111
  }
92
112
 
93
113
  populateGraphEnum(schema: Schema, subgraphs: Subgraphs): Map<string, string> {
@@ -141,7 +161,15 @@ export class JoinSpecDefinition extends FeatureDefinition {
141
161
  return this.directive(schema, 'implements');
142
162
  }
143
163
 
144
- fieldDirective(schema: Schema): DirectiveDefinition<{graph: string, requires?: string, provides?: string, type?: string, external?: boolean}> {
164
+ fieldDirective(schema: Schema): DirectiveDefinition<{
165
+ graph: string,
166
+ requires?: string,
167
+ provides?: string,
168
+ override?: string,
169
+ type?: string,
170
+ external?: boolean,
171
+ usedOverridden?: boolean,
172
+ }> {
145
173
  return this.directive(schema, 'field')!;
146
174
  }
147
175
 
@@ -160,3 +188,5 @@ export class JoinSpecDefinition extends FeatureDefinition {
160
188
  export const JOIN_VERSIONS = new FeatureDefinitions<JoinSpecDefinition>(joinIdentity)
161
189
  .add(new JoinSpecDefinition(new FeatureVersion(0, 1)))
162
190
  .add(new JoinSpecDefinition(new FeatureVersion(0, 2)));
191
+
192
+ registerKnownFeature(JOIN_VERSIONS);
@@ -0,0 +1,13 @@
1
+ import { FeatureDefinition, FeatureDefinitions, FeatureUrl } from "./coreSpec";
2
+
3
+ const registeredFeatures: Map<string, FeatureDefinitions> = new Map();
4
+
5
+ export function registerKnownFeature(definitions: FeatureDefinitions) {
6
+ if (!registeredFeatures.has(definitions.identity)) {
7
+ registeredFeatures.set(definitions.identity, definitions);
8
+ }
9
+ }
10
+
11
+ export function coreFeatureDefinitionIfKnown(url: FeatureUrl): FeatureDefinition | undefined {
12
+ return registeredFeatures.get(url.identity)?.find(url.version);
13
+ }
package/src/operations.ts CHANGED
@@ -862,6 +862,21 @@ export class SelectionSet {
862
862
  }
863
863
  }
864
864
 
865
+ export function allFieldDefinitionsInSelectionSet(selection: SelectionSet): FieldDefinition<CompositeType>[] {
866
+ const stack = Array.from(selection.selections());
867
+ const allFields: FieldDefinition<CompositeType>[] = [];
868
+ while (stack.length > 0) {
869
+ const selection = stack.pop()!;
870
+ if (selection.kind === 'FieldSelection') {
871
+ allFields.push(selection.field.definition);
872
+ }
873
+ if (selection.selectionSet) {
874
+ stack.push(...selection.selectionSet.selections());
875
+ }
876
+ }
877
+ return allFields;
878
+ }
879
+
865
880
  export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet): SelectionSet {
866
881
  const selectionSet = new SelectionSet(element.parentType);
867
882
  selectionSet.add(selectionOfElement(element, subSelection));
@@ -5,9 +5,7 @@ import {
5
5
  federationMetadata,
6
6
  FieldDefinition,
7
7
  collectTargetFields,
8
- InterfaceType,
9
- ObjectType,
10
- Schema
8
+ Schema,
11
9
  } from ".";
12
10
 
13
11
  export function computeShareables(schema: Schema): (field: FieldDefinition<CompositeType>) => boolean {
@@ -33,7 +31,7 @@ export function computeShareables(schema: Schema): (field: FieldDefinition<Compo
33
31
  }
34
32
  };
35
33
 
36
- for (const type of schema.types<ObjectType>('ObjectType')) {
34
+ for (const type of schema.objectTypes()) {
37
35
  addKeyFields(type);
38
36
  const shareablesOnType = shareableDirective ? type.appliedDirectivesOf(shareableDirective) : [];
39
37
  for (const field of type.fields()) {
@@ -59,10 +57,9 @@ export function computeShareables(schema: Schema): (field: FieldDefinition<Compo
59
57
  }
60
58
  }
61
59
 
62
- for (const type of schema.types<InterfaceType>('InterfaceType')) {
60
+ for (const type of schema.interfaceTypes()) {
63
61
  addKeyFields(type);
64
62
  }
65
63
 
66
64
  return (field) => shareableFields.has(field.coordinate);
67
65
  }
68
-
@@ -11,7 +11,6 @@ import {
11
11
  errorCauses,
12
12
  Extension,
13
13
  FieldDefinition,
14
- InterfaceType,
15
14
  isCompositeType,
16
15
  isInterfaceType,
17
16
  isObjectType,
@@ -24,7 +23,6 @@ import {
24
23
  import {
25
24
  addSubgraphToError,
26
25
  collectTargetFields,
27
- collectUsedExternalFieldsCoordinates,
28
26
  federationMetadata,
29
27
  FederationMetadata,
30
28
  printSubgraphNames,
@@ -43,7 +41,7 @@ type UpgradeChanges = MultiMap<UpgradeChangeID, UpgradeChange>;
43
41
  export type UpgradeSuccess = {
44
42
  subgraphs: Subgraphs,
45
43
  changes: Map<string, UpgradeChanges>,
46
- errors?: never,
44
+ errors?: never,
47
45
  }
48
46
 
49
47
  export type UpgradeFailure = {
@@ -235,7 +233,7 @@ export function upgradeSubgraphsIfNecessary(inputs: Subgraphs): UpgradeResult {
235
233
  * 2. do not have a definition for the same type in the same subgraph (this is a GraphQL extension otherwise).
236
234
  *
237
235
  * Not that type extensions in federation 1 generally have a @key but in really the code consider something a type extension even without
238
- * it (which I'd argue is a unintended bug of fed1 since this leads to various problems) so we don't check for the presence of @key here.
236
+ * it (which I'd argue is a unintended bug of fed1 since this leads to various problems) so we don't check for the presence of @key here.
239
237
  */
240
238
  function isFederationTypeExtension(type: NamedType): boolean {
241
239
  const metadata = federationMetadata(type.schema());
@@ -280,8 +278,28 @@ class SchemaUpgrader {
280
278
  // later merge errors "AST" nodes ends up pointing to the original schema, the one that make sense to the user.
281
279
  this.schema = originalSubgraph.schema.clone();
282
280
  this.renameFederationTypes();
283
- setSchemaAsFed2Subgraph(this.schema);
281
+ // Setting this property before trying to switch the cloned schema to fed2 because on
282
+ // errors `addError` uses `this.subgraph.name`.
284
283
  this.subgraph = new Subgraph(originalSubgraph.name, originalSubgraph.url, this.schema);
284
+ try {
285
+ setSchemaAsFed2Subgraph(this.schema);
286
+ } catch (e) {
287
+ // This could error out if some directive definition clashes with a federation one while
288
+ // having an incompatible definition. Note that in that case, the choices for the user
289
+ // are either:
290
+ // 1. fix/remove the definition if they did meant the federation directive, just had an
291
+ // invalid definition.
292
+ // 2. but if they have their own directive whose name happens to clash with a federation
293
+ // directive one but is genuinely a different directive, they will have to move their
294
+ // schema to a fed2 one and use renaming.
295
+ const causes = errorCauses(e);
296
+ if (causes) {
297
+ causes.forEach((c) => this.addError(c));
298
+ } else {
299
+ // An unexpected exception, rethrow.
300
+ throw e;
301
+ }
302
+ }
285
303
  this.metadata = this.subgraph.metadata();
286
304
  }
287
305
 
@@ -435,7 +453,7 @@ class SchemaUpgrader {
435
453
  }
436
454
 
437
455
  private removeExternalOnInterface() {
438
- for (const itf of this.schema.types<InterfaceType>('InterfaceType')) {
456
+ for (const itf of this.schema.interfaceTypes()) {
439
457
  for (const field of itf.fields()) {
440
458
  const external = this.external(field);
441
459
  if (external) {
@@ -522,7 +540,7 @@ class SchemaUpgrader {
522
540
  continue;
523
541
  }
524
542
  assert(isCompositeType(typeInOther), () => `Type ${type} is of kind ${type.kind} in ${this.subgraph.name} but ${typeInOther.kind} in ${other.name}`);
525
- const keysInOther = typeInOther.appliedDirectivesOf(other.metadata().keyDirective());
543
+ const keysInOther = typeInOther.appliedDirectivesOf(other.metadata().keyDirective());
526
544
  if (keysInOther.length === 0) {
527
545
  continue;
528
546
  }
@@ -564,14 +582,12 @@ class SchemaUpgrader {
564
582
  }
565
583
 
566
584
  private removeUnusedExternals() {
567
- const usedExternalFieldsCoordinates = collectUsedExternalFieldsCoordinates(this.metadata);
568
-
569
585
  for (const type of this.schema.types()) {
570
586
  if (!isObjectType(type) && !isInterfaceType(type)) {
571
587
  continue;
572
588
  }
573
589
  for (const field of type.fields()) {
574
- if (this.metadata.isFieldExternal(field) && !usedExternalFieldsCoordinates.has(field.coordinate)) {
590
+ if (this.metadata.isFieldExternal(field) && !this.metadata.isFieldUsed(field)) {
575
591
  this.addChange(new UnusedExternalRemoval(field.coordinate));
576
592
  field.remove();
577
593
  }
@@ -593,7 +609,7 @@ class SchemaUpgrader {
593
609
  }
594
610
 
595
611
  private removeDirectivesOnInterface() {
596
- for (const type of this.schema.types<InterfaceType>('InterfaceType')) {
612
+ for (const type of this.schema.interfaceTypes()) {
597
613
  for (const application of type.appliedDirectivesOf(this.metadata.keyDirective())) {
598
614
  this.addChange(new KeyOnInterfaceRemoval(type.name));
599
615
  application.remove();
@@ -610,7 +626,7 @@ class SchemaUpgrader {
610
626
  }
611
627
 
612
628
  private removeProvidesOnNonComposite() {
613
- for (const type of this.schema.types<ObjectType>('ObjectType')) {
629
+ for (const type of this.schema.objectTypes()) {
614
630
  for (const field of type.fields()) {
615
631
  if (isCompositeType(baseType(field.type!))) {
616
632
  continue;
@@ -630,7 +646,7 @@ class SchemaUpgrader {
630
646
  // We add shareable:
631
647
  // - to every "value type" (in the fed1 sense of non-root type and non-entity) if it is used in any other subgraphs
632
648
  // - to any (non-external) field of an entity/root-type that is not a key field and if another subgraphs resolve it (fully or partially through @provides)
633
- for (const type of this.schema.types<ObjectType>('ObjectType')) {
649
+ for (const type of this.schema.objectTypes()) {
634
650
  if (type.hasAppliedDirective(keyDirective) || type.isRootType()) {
635
651
  for (const field of type.fields()) {
636
652
  // To know if the field is a "key" field which doesn't need shareable, we rely on whether the field is shareable in the original
@@ -5,7 +5,7 @@ import { mapKeys } from './utils';
5
5
  * Given an invalid input string and a list of valid options, returns a filtered
6
6
  * list of valid options sorted based on their similarity with the input.
7
7
  */
8
- export function suggestionList(input: string, options: string[]): string[] {
8
+ export function suggestionList(input: string, options: readonly string[]): string[] {
9
9
  const optionsByDistance = new Map<string, number>();
10
10
 
11
11
  const threshold = Math.floor(input.length * 0.4) + 1;
@@ -12,7 +12,9 @@ const SUPPORTED_FEATURES = new Set([
12
12
  'https://specs.apollo.dev/join/v0.1',
13
13
  'https://specs.apollo.dev/join/v0.2',
14
14
  'https://specs.apollo.dev/tag/v0.1',
15
+ 'https://specs.apollo.dev/tag/v0.2',
15
16
  'https://specs.apollo.dev/inaccessible/v0.1',
17
+ 'https://specs.apollo.dev/inaccessible/v0.2',
16
18
  ]);
17
19
 
18
20
  export function ErrUnsupportedFeature(feature: CoreFeature): Error {
package/src/tagSpec.ts CHANGED
@@ -1,29 +1,55 @@
1
1
  import { DirectiveLocation, GraphQLError } from "graphql";
2
2
  import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
3
3
  import { DirectiveDefinition, NonNullType, Schema } from "./definitions";
4
+ import { createDirectiveSpecification, DirectiveSpecification } from "./directiveAndTypeSpecification";
4
5
  import { ERRORS } from "./error";
6
+ import { registerKnownFeature } from "./knownCoreFeatures";
5
7
  import { sameType } from "./types";
6
8
 
7
9
  export const tagIdentity = 'https://specs.apollo.dev/tag';
8
10
 
9
- export const tagLocations = [
10
- DirectiveLocation.FIELD_DEFINITION,
11
- DirectiveLocation.OBJECT,
12
- DirectiveLocation.INTERFACE,
13
- DirectiveLocation.UNION,
14
- ];
15
-
16
- const printedTagDefinition = 'directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION';
17
-
18
11
  export class TagSpecDefinition extends FeatureDefinition {
12
+ public readonly tagLocations: DirectiveLocation[];
13
+ public readonly tagDirectiveSpec: DirectiveSpecification;
14
+ private readonly printedTagDefinition: string;
15
+
19
16
  constructor(version: FeatureVersion) {
20
17
  super(new FeatureUrl(tagIdentity, 'tag', version));
18
+ this.tagLocations = [
19
+ DirectiveLocation.FIELD_DEFINITION,
20
+ DirectiveLocation.OBJECT,
21
+ DirectiveLocation.INTERFACE,
22
+ DirectiveLocation.UNION,
23
+ ];
24
+ this.printedTagDefinition = 'directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION';
25
+ if (!this.isV01()) {
26
+ this.tagLocations.push(
27
+ DirectiveLocation.ARGUMENT_DEFINITION,
28
+ DirectiveLocation.SCALAR,
29
+ DirectiveLocation.ENUM,
30
+ DirectiveLocation.ENUM_VALUE,
31
+ DirectiveLocation.INPUT_OBJECT,
32
+ DirectiveLocation.INPUT_FIELD_DEFINITION,
33
+ );
34
+ this.printedTagDefinition = 'directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION';
35
+ }
36
+ this.tagDirectiveSpec = createDirectiveSpecification({
37
+ name:'tag',
38
+ locations: this.tagLocations,
39
+ repeatable: true,
40
+ argumentFct: (schema) => ({
41
+ args: [{ name: 'name', type: new NonNullType(schema.stringType()) }],
42
+ errors: [],
43
+ }),
44
+ });
21
45
  }
22
46
 
23
- addElementsToSchema(schema: Schema) {
24
- const directive = this.addDirective(schema, 'tag').addLocations(...tagLocations);
25
- directive.repeatable = true;
26
- directive.addArgument("name", new NonNullType(schema.stringType()));
47
+ private isV01() {
48
+ return this.version.equals(new FeatureVersion(0, 1));
49
+ }
50
+
51
+ addElementsToSchema(schema: Schema): GraphQLError[] {
52
+ return this.addDirectiveSpec(schema, this.tagDirectiveSpec);
27
53
  }
28
54
 
29
55
  tagDirective(schema: Schema): DirectiveDefinition<{name: string}> {
@@ -34,16 +60,23 @@ export class TagSpecDefinition extends FeatureDefinition {
34
60
  const hasUnknownArguments = Object.keys(definition.arguments()).length > 1;
35
61
  const nameArg = definition.argument('name');
36
62
  const hasValidNameArg = nameArg && sameType(nameArg.type!, new NonNullType(definition.schema().stringType()));
37
- const hasValidLocations = definition.locations.every(loc => tagLocations.includes(loc));
63
+ const hasValidLocations = definition.locations.every(loc => this.tagLocations.includes(loc));
38
64
  if (hasUnknownArguments || !hasValidNameArg || !hasValidLocations) {
39
65
  return ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
40
- message: `Found invalid @tag directive definition. Please ensure the directive definition in your schema's definitions matches the following:\n\t${printedTagDefinition}`,
66
+ message: `Found invalid @tag directive definition. Please ensure the directive definition in your schema's definitions matches the following:\n\t${this.printedTagDefinition}`,
41
67
  }
42
68
  );
43
69
  }
44
70
  return undefined;
45
71
  }
72
+
73
+ allElementNames(): string[] {
74
+ return ["@tag"];
75
+ }
46
76
  }
47
77
 
48
78
  export const TAG_VERSIONS = new FeatureDefinitions<TagSpecDefinition>(tagIdentity)
49
- .add(new TagSpecDefinition(new FeatureVersion(0, 1)));
79
+ .add(new TagSpecDefinition(new FeatureVersion(0, 1)))
80
+ .add(new TagSpecDefinition(new FeatureVersion(0, 2)));
81
+
82
+ registerKnownFeature(TAG_VERSIONS);
package/src/validate.ts CHANGED
@@ -129,13 +129,14 @@ class Validator {
129
129
  return this.errors;
130
130
  }
131
131
 
132
- private validateHasType(elt: { type?: Type, coordinate: string, sourceAST?: ASTNode }) {
132
+ private validateHasType(elt: { type?: Type, coordinate: string, sourceAST?: ASTNode }): boolean {
133
133
  // Note that this error can't happen if you parse the schema since it wouldn't be valid syntax, but it can happen for
134
134
  // programmatically constructed schema.
135
135
  if (!elt.type) {
136
136
  this.errors.push(new GraphQLError(`Element ${elt.coordinate} does not have a type set`, elt.sourceAST));
137
137
  this.hasMissingTypes = false;
138
138
  }
139
+ return !!elt.type;
139
140
  }
140
141
 
141
142
  private validateName(elt: { name: string, sourceAST?: ASTNode}) {
@@ -183,8 +184,7 @@ class Validator {
183
184
  // Note that we may not have validated the interface yet, so making sure we have a meaningful error
184
185
  // if the type is not set, even if that means a bit of cpu wasted since we'll re-check later (and
185
186
  // as many type as the interface is implemented); it's a cheap check anyway.
186
- this.validateHasType(itfField);
187
- if (!isSubtype(itfField.type!, field.type!)) {
187
+ if (this.validateHasType(itfField) && !isSubtype(itfField.type!, field.type!)) {
188
188
  this.errors.push(new GraphQLError(
189
189
  `Interface field ${itfField.coordinate} expects type ${itfField.type} but ${field.coordinate} of type ${field.type} is not a proper subtype.`,
190
190
  sourceASTs(itfField, field)
@@ -200,10 +200,8 @@ class Validator {
200
200
  ));
201
201
  continue;
202
202
  }
203
- // Same as above for the field
204
- this.validateHasType(itfArg);
205
203
  // Note that we could use contra-variance but as graphQL-js currently doesn't allow it, we mimic that.
206
- if (!sameType(itfArg.type!, arg.type!)) {
204
+ if (this.validateHasType(itfArg) && !sameType(itfArg.type!, arg.type!)) {
207
205
  this.errors.push(new GraphQLError(
208
206
  `Interface field argument ${itfArg.coordinate} expects type ${itfArg.type} but ${arg.coordinate} is type ${arg.type}.`,
209
207
  sourceASTs(itfArg, arg)
@@ -247,19 +245,29 @@ class Validator {
247
245
  }
248
246
  for (const field of type.fields()) {
249
247
  this.validateName(field);
250
- this.validateHasType(field);
248
+ if (!this.validateHasType(field)) {
249
+ continue;
250
+ }
251
251
  if (field.isRequired() && field.isDeprecated()) {
252
252
  this.errors.push(new GraphQLError(
253
253
  `Required input field ${field.coordinate} cannot be deprecated.`,
254
254
  sourceASTs(field.appliedDirectivesOf('deprecated')[0], field)
255
255
  ));
256
256
  }
257
+ if (field.defaultValue !== undefined && !isValidValue(field.defaultValue, field, new VariableDefinitions())) {
258
+ this.errors.push(new GraphQLError(
259
+ `Invalid default value (got: ${valueToString(field.defaultValue)}) provided for input field ${field.coordinate} of type ${field.type}.`,
260
+ sourceASTs(field)
261
+ ));
262
+ }
257
263
  }
258
264
  }
259
265
 
260
266
  private validateArg(arg: ArgumentDefinition<any>) {
261
267
  this.validateName(arg);
262
- this.validateHasType(arg);
268
+ if (!this.validateHasType(arg)) {
269
+ return;
270
+ }
263
271
  if (arg.isRequired() && arg.isDeprecated()) {
264
272
  this.errors.push(new GraphQLError(
265
273
  `Required argument ${arg.coordinate} cannot be deprecated.`,
@@ -305,7 +313,10 @@ class Validator {
305
313
  // Again, that implies that value is not required.
306
314
  continue;
307
315
  }
308
- if (!isValidValue(value, argument, this.emptyVariables)) {
316
+ // Note that we validate if the definition argument has a type set separatly
317
+ // and log an error if necesary, but we just want to avoid calling
318
+ // `isValidValue` if there is not type as it may throw.
319
+ if (argument.type && !isValidValue(value, argument, this.emptyVariables)) {
309
320
  const parent = application.parent;
310
321
  // The only non-named SchemaElement is the `schema` definition.
311
322
  const parentDesc = parent instanceof NamedSchemaElement
package/src/values.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ArgumentDefinition,
3
+ InputFieldDefinition,
3
4
  InputObjectType,
4
5
  InputType,
5
6
  isBooleanType,
@@ -38,16 +39,29 @@ import { assert, assertUnreachable } from './utils';
38
39
  const MAX_INT = 2147483647;
39
40
  const MIN_INT = -2147483648;
40
41
 
42
+ /**
43
+ * Converts a graphQL value into it's textual representation.
44
+ *
45
+ * @param v - the value to convert/display. This method assumes that it is a value graphQL
46
+ * value (essentially, one that could have been produced by `valueFromAST`/`valueFormASTUntyped`).
47
+ * If this is not the case, the behaviour is unspecified, and in particular this method may
48
+ * throw or produce an output that is not valid graphQL syntax.
49
+ * @param expectedType - the type of the value being converted. This is optional is only used to
50
+ * ensure enum values are displayed as such and not as strings. In other words, the type of
51
+ * the value should be provided when possible (when the value is known to be of a ype) but
52
+ * using this method without a type is useful to dispaly the value in error/debug messages
53
+ * where no type may be known. Note that if `v` is not a valid value for `expectedType`,
54
+ * this method will not throw but enum values may be represented by strings in the output.
55
+ * @return a textual representation of the value. It is guaranteed to be valid graphQL syntax
56
+ * if the input value is a valid graphQL value.
57
+ */
41
58
  export function valueToString(v: any, expectedType?: InputType): string {
42
59
  if (v === undefined || v === null) {
43
- if (expectedType && isNonNullType(expectedType)) {
44
- throw buildError(`Invalid undefined/null value for non-null type ${expectedType}`);
45
- }
46
60
  return "null";
47
61
  }
48
62
 
49
63
  if (expectedType && isNonNullType(expectedType)) {
50
- expectedType = expectedType.ofType;
64
+ return valueToString(v, expectedType.ofType);
51
65
  }
52
66
 
53
67
  if (expectedType && isCustomScalarType(expectedType)) {
@@ -61,18 +75,26 @@ export function valueToString(v: any, expectedType?: InputType): string {
61
75
 
62
76
  if (Array.isArray(v)) {
63
77
  let elementsType: InputType | undefined = undefined;
64
- if (expectedType) {
65
- if (!isListType(expectedType)) {
66
- throw buildError(`Invalid list value for non-list type ${expectedType}`);
67
- }
78
+ // If the expected type is not a list, we've been given an invalid type. We don't want this
79
+ // method to fail though, so we just ignore the provided type from that point one (passing
80
+ // `undefined` to the recursion).
81
+ if (expectedType && isListType(expectedType)) {
68
82
  elementsType = expectedType.ofType;
69
83
  }
70
84
  return '[' + v.map(e => valueToString(e, elementsType)).join(', ') + ']';
71
85
  }
72
86
 
87
+ // We know the value is not a list/array. But if the type is a list, we still want to print
88
+ // the value correctly, at least as long as it's a valid value for the element type, since
89
+ // list input coercions may allow this.
90
+ if (expectedType && isListType(expectedType)) {
91
+ return valueToString(v, expectedType.ofType);
92
+ }
93
+
73
94
  if (typeof v === 'object') {
74
95
  if (expectedType && !isInputObjectType(expectedType)) {
75
- throw buildError(`Invalid object value for non-input-object type ${expectedType} (isCustomScalar? ${isCustomScalarType(expectedType)})`);
96
+ // expectedType does not match the value, we ignore it for what remains.
97
+ expectedType = undefined;
76
98
  }
77
99
  return '{' + Object.keys(v).map(k => {
78
100
  const valueType = expectedType ? (expectedType as InputObjectType).field(k)?.type : undefined;
@@ -462,7 +484,7 @@ function areTypesCompatible(variableType: InputType, locationType: InputType): b
462
484
  return !isListType(variableType) && sameType(variableType, locationType);
463
485
  }
464
486
 
465
- export function isValidValue(value: any, argument: ArgumentDefinition<any>, variableDefinitions: VariableDefinitions): boolean {
487
+ export function isValidValue(value: any, argument: ArgumentDefinition<any> | InputFieldDefinition, variableDefinitions: VariableDefinitions): boolean {
466
488
  return isValidValueApplication(value, argument.type!, argument.defaultValue, variableDefinitions);
467
489
  }
468
490
 
@@ -499,8 +521,13 @@ function isValidValueApplication(value: any, locationType: InputType, locationDe
499
521
  if (typeof value !== 'object') {
500
522
  return false;
501
523
  }
502
- const isValid = locationType.fields().every(field => isValidValueApplication(value[field.name], field.type!, undefined, variableDefinitions));
503
- return isValid;
524
+ const valueKeys = new Set(Object.keys(value));
525
+ const fieldsAreValid = locationType.fields().every(field => {
526
+ valueKeys.delete(field.name);
527
+ return isValidValueApplication(value[field.name], field.type!, undefined, variableDefinitions)
528
+ });
529
+ const hasUnexpectedField = valueKeys.size !== 0
530
+ return fieldsAreValid && !hasUnexpectedField;
504
531
  }
505
532
 
506
533
  // TODO: we may have to handle some coercions (not sure it matters in our use case