@apollo/federation-internals 2.0.0-preview.9 → 2.0.2-alpha.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 (78) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/coreSpec.d.ts +2 -1
  3. package/dist/coreSpec.d.ts.map +1 -1
  4. package/dist/coreSpec.js +37 -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 +102 -62
  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 +29 -9
  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 +48 -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/genErrorCodeDoc.js +12 -6
  26. package/dist/genErrorCodeDoc.js.map +1 -1
  27. package/dist/inaccessibleSpec.d.ts +9 -4
  28. package/dist/inaccessibleSpec.d.ts.map +1 -1
  29. package/dist/inaccessibleSpec.js +625 -32
  30. package/dist/inaccessibleSpec.js.map +1 -1
  31. package/dist/joinSpec.d.ts +2 -1
  32. package/dist/joinSpec.d.ts.map +1 -1
  33. package/dist/joinSpec.js +3 -0
  34. package/dist/joinSpec.js.map +1 -1
  35. package/dist/operations.d.ts +20 -1
  36. package/dist/operations.d.ts.map +1 -1
  37. package/dist/operations.js +52 -1
  38. package/dist/operations.js.map +1 -1
  39. package/dist/precompute.d.ts.map +1 -1
  40. package/dist/precompute.js +2 -2
  41. package/dist/precompute.js.map +1 -1
  42. package/dist/schemaUpgrader.d.ts +8 -1
  43. package/dist/schemaUpgrader.d.ts.map +1 -1
  44. package/dist/schemaUpgrader.js +56 -6
  45. package/dist/schemaUpgrader.js.map +1 -1
  46. package/dist/supergraphs.d.ts.map +1 -1
  47. package/dist/supergraphs.js +1 -0
  48. package/dist/supergraphs.js.map +1 -1
  49. package/dist/validate.js +13 -7
  50. package/dist/validate.js.map +1 -1
  51. package/dist/values.d.ts +2 -2
  52. package/dist/values.d.ts.map +1 -1
  53. package/dist/values.js +13 -11
  54. package/dist/values.js.map +1 -1
  55. package/package.json +3 -3
  56. package/src/__tests__/coreSpec.test.ts +112 -0
  57. package/src/__tests__/removeInaccessibleElements.test.ts +2252 -177
  58. package/src/__tests__/schemaUpgrader.test.ts +38 -0
  59. package/src/__tests__/subgraphValidation.test.ts +96 -5
  60. package/src/__tests__/values.test.ts +315 -3
  61. package/src/coreSpec.ts +74 -16
  62. package/src/definitions.ts +207 -87
  63. package/src/directiveAndTypeSpecification.ts +18 -1
  64. package/src/error.ts +69 -9
  65. package/src/extractSubgraphsFromSupergraph.ts +1 -1
  66. package/src/federation.ts +86 -26
  67. package/src/federationSpec.ts +2 -2
  68. package/src/genErrorCodeDoc.ts +13 -7
  69. package/src/inaccessibleSpec.ts +978 -56
  70. package/src/joinSpec.ts +5 -1
  71. package/src/operations.ts +68 -2
  72. package/src/precompute.ts +2 -4
  73. package/src/schemaUpgrader.ts +70 -6
  74. package/src/supergraphs.ts +1 -0
  75. package/src/validate.ts +20 -9
  76. package/src/values.ts +39 -12
  77. package/tsconfig.test.tsbuildinfo +1 -1
  78. package/tsconfig.tsbuildinfo +1 -1
package/src/joinSpec.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DirectiveLocation, GraphQLError } from 'graphql';
2
- import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
2
+ import { CorePurpose, FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
3
3
  import {
4
4
  DirectiveDefinition,
5
5
  EnumType,
@@ -176,6 +176,10 @@ export class JoinSpecDefinition extends FeatureDefinition {
176
176
  ownerDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined {
177
177
  return this.directive(schema, 'owner');
178
178
  }
179
+
180
+ get defaultCorePurpose(): CorePurpose | undefined {
181
+ return 'EXECUTION';
182
+ }
179
183
  }
180
184
 
181
185
  // Note: This declare a no-yet-agreed-upon join spec v0.2, that:
package/src/operations.ts CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  typenameFieldName,
43
43
  NamedType,
44
44
  } from "./definitions";
45
+ import { sameType } from "./types";
45
46
  import { assert, mapEntries, MapWithCachedArrays, MultiMap } from "./utils";
46
47
  import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
47
48
 
@@ -305,6 +306,37 @@ export function sameOperationPaths(p1: OperationPath, p2: OperationPath): boolea
305
306
  return true;
306
307
  }
307
308
 
309
+ export function concatOperationPaths(head: OperationPath, tail: OperationPath): OperationPath {
310
+ // While this is mainly a simple array concatenation, we optimize slightly by recognizing if the
311
+ // tail path starts by a fragment selection that is useless given the end of the head path.
312
+ if (head.length === 0) {
313
+ return tail;
314
+ }
315
+ if (tail.length === 0) {
316
+ return head;
317
+ }
318
+ const lastOfHead = head[head.length - 1];
319
+ const firstOfTail = tail[0];
320
+ if (isUselessFollowupElement(lastOfHead, firstOfTail)) {
321
+ tail = tail.slice(1);
322
+ }
323
+ return head.concat(tail);
324
+ }
325
+
326
+ function isUselessFollowupElement(first: OperationElement, followup: OperationElement): boolean {
327
+ const typeOfFirst = first.kind === 'Field'
328
+ ? baseType(first.definition.type!)
329
+ : first.typeCondition;
330
+
331
+ // The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
332
+ // is already that of the first element.
333
+ return !!typeOfFirst
334
+ && followup.kind === 'FragmentElement'
335
+ && !!followup.typeCondition
336
+ && followup.appliedDirectives.length === 0
337
+ && sameType(typeOfFirst, followup.typeCondition);
338
+ }
339
+
308
340
  export type RootOperationPath = {
309
341
  rootKind: SchemaRootKind,
310
342
  path: OperationPath
@@ -604,6 +636,23 @@ export class SelectionSet {
604
636
  return withExpanded;
605
637
  }
606
638
 
639
+ /**
640
+ * Returns the selection select from filtering out any selection that does not match the provided predicate.
641
+ *
642
+ * Please that this method will expand *ALL* fragments as the result of applying it's filtering. You should
643
+ * call `optimize` on the result if you want to re-apply some fragments.
644
+ */
645
+ filter(predicate: (selection: Selection) => boolean): SelectionSet {
646
+ const filtered = new SelectionSet(this.parentType, this.fragments);
647
+ for (const selection of this.selections()) {
648
+ const filteredSelection = selection.filter(predicate);
649
+ if (filteredSelection) {
650
+ filtered.add(filteredSelection);
651
+ }
652
+ }
653
+ return filtered;
654
+ }
655
+
607
656
  mergeIn(selectionSet: SelectionSet) {
608
657
  for (const selection of selectionSet.selections()) {
609
658
  this.add(selection);
@@ -940,6 +989,16 @@ export class FieldSelection {
940
989
  : new FieldSelection(this.field, optimizedSelection);
941
990
  }
942
991
 
992
+ filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
993
+ if (!predicate(this)) {
994
+ return undefined;
995
+ }
996
+ if (!this.selectionSet) {
997
+ return this;
998
+ }
999
+ return new FieldSelection(this.field, this.selectionSet.filter(predicate));
1000
+ }
1001
+
943
1002
  expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): FieldSelection {
944
1003
  const expandedSelection = this.selectionSet ? this.selectionSet.expandFragments(names, updateSelectionSetFragments) : undefined;
945
1004
  return this.selectionSet === expandedSelection
@@ -1055,7 +1114,6 @@ export abstract class FragmentSelection {
1055
1114
 
1056
1115
  abstract validate(): void;
1057
1116
 
1058
-
1059
1117
  usedVariables(): Variables {
1060
1118
  return mergeVariables(this.element().variables(), this.selectionSet.usedVariables());
1061
1119
  }
@@ -1065,6 +1123,15 @@ export abstract class FragmentSelection {
1065
1123
  return this.element() === updatedFragment ? this : new InlineFragmentSelection(updatedFragment, this.selectionSet);
1066
1124
  }
1067
1125
 
1126
+ filter(predicate: (selection: Selection) => boolean): InlineFragmentSelection | undefined {
1127
+ if (!predicate(this)) {
1128
+ return undefined;
1129
+ }
1130
+ // Note that we essentially expand all fragments as part of this.
1131
+ return new InlineFragmentSelection(this.element(), this.selectionSet.filter(predicate));
1132
+ }
1133
+
1134
+
1068
1135
  equals(that: Selection): boolean {
1069
1136
  if (this === that) {
1070
1137
  return true;
@@ -1144,7 +1211,6 @@ class InlineFragmentSelection extends FragmentSelection {
1144
1211
  };
1145
1212
  }
1146
1213
 
1147
-
1148
1214
  optimize(fragments: NamedFragments): FragmentSelection {
1149
1215
  const optimizedSelection = this.selectionSet.optimize(fragments);
1150
1216
  const typeCondition = this.element().typeCondition;
package/src/precompute.ts CHANGED
@@ -5,8 +5,6 @@ import {
5
5
  federationMetadata,
6
6
  FieldDefinition,
7
7
  collectTargetFields,
8
- InterfaceType,
9
- ObjectType,
10
8
  Schema,
11
9
  } from ".";
12
10
 
@@ -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,7 +57,7 @@ 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
 
@@ -7,11 +7,11 @@ import {
7
7
  import { ERRORS } from "./error";
8
8
  import {
9
9
  baseType,
10
+ CompositeType,
10
11
  Directive,
11
12
  errorCauses,
12
13
  Extension,
13
14
  FieldDefinition,
14
- InterfaceType,
15
15
  isCompositeType,
16
16
  isInterfaceType,
17
17
  isObjectType,
@@ -34,6 +34,7 @@ import {
34
34
  } from "./federation";
35
35
  import { assert, firstOf, MultiMap } from "./utils";
36
36
  import { FEDERATION_SPEC_TYPES } from "./federationSpec";
37
+ import { valueEquals } from "./values";
37
38
 
38
39
  export type UpgradeResult = UpgradeSuccess | UpgradeFailure;
39
40
 
@@ -67,6 +68,7 @@ export type UpgradeChange =
67
68
  | ProvidesOrRequiresOnInterfaceFieldRemoval
68
69
  | ProvidesOnNonCompositeRemoval
69
70
  | FieldsArgumentCoercionToString
71
+ | RemovedTagOnExternal
70
72
  ;
71
73
 
72
74
  export class ExternalOnTypeExtensionRemoval {
@@ -199,6 +201,16 @@ export class FieldsArgumentCoercionToString {
199
201
  }
200
202
  }
201
203
 
204
+ export class RemovedTagOnExternal {
205
+ readonly id = 'REMOVED_TAG_ON_EXTERNAL' as const;
206
+
207
+ constructor(readonly application: string, readonly element: string) {}
208
+
209
+ toString() {
210
+ return `Removed ${this.application} application on @external "${this.element}" as the @tag application is on another definition`;
211
+ }
212
+ }
213
+
202
214
  export function upgradeSubgraphsIfNecessary(inputs: Subgraphs): UpgradeResult {
203
215
  const changes: Map<string, UpgradeChanges> = new Map();
204
216
  if (inputs.values().every((s) => s.isFed2Subgraph())) {
@@ -266,6 +278,11 @@ function resolvesField(subgraph: Subgraph, field: FieldDefinition<ObjectType>):
266
278
  return !!f && (!metadata.isFieldExternal(f) || metadata.isFieldPartiallyExternal(f));
267
279
  }
268
280
 
281
+ function getField(schema: Schema, typeName: string, fieldName: string): FieldDefinition<CompositeType> | undefined {
282
+ const type = schema.type(typeName);
283
+ return type && isCompositeType(type) ? type.field(fieldName) : undefined;
284
+ }
285
+
269
286
  class SchemaUpgrader {
270
287
  private readonly changes = new MultiMap<UpgradeChangeID, UpgradeChange>();
271
288
  private readonly schema: Schema;
@@ -279,8 +296,28 @@ class SchemaUpgrader {
279
296
  // later merge errors "AST" nodes ends up pointing to the original schema, the one that make sense to the user.
280
297
  this.schema = originalSubgraph.schema.clone();
281
298
  this.renameFederationTypes();
282
- setSchemaAsFed2Subgraph(this.schema);
299
+ // Setting this property before trying to switch the cloned schema to fed2 because on
300
+ // errors `addError` uses `this.subgraph.name`.
283
301
  this.subgraph = new Subgraph(originalSubgraph.name, originalSubgraph.url, this.schema);
302
+ try {
303
+ setSchemaAsFed2Subgraph(this.schema);
304
+ } catch (e) {
305
+ // This could error out if some directive definition clashes with a federation one while
306
+ // having an incompatible definition. Note that in that case, the choices for the user
307
+ // are either:
308
+ // 1. fix/remove the definition if they did meant the federation directive, just had an
309
+ // invalid definition.
310
+ // 2. but if they have their own directive whose name happens to clash with a federation
311
+ // directive one but is genuinely a different directive, they will have to move their
312
+ // schema to a fed2 one and use renaming.
313
+ const causes = errorCauses(e);
314
+ if (causes) {
315
+ causes.forEach((c) => this.addError(c));
316
+ } else {
317
+ // An unexpected exception, rethrow.
318
+ throw e;
319
+ }
320
+ }
284
321
  this.metadata = this.subgraph.metadata();
285
322
  }
286
323
 
@@ -365,6 +402,8 @@ class SchemaUpgrader {
365
402
 
366
403
  this.addShareable();
367
404
 
405
+ this.removeTagOnExternal();
406
+
368
407
  // If we had errors during the upgrade, we throw them before trying to validate the resulting subgraph, because any invalidity in the
369
408
  // migrated subgraph may well due to those migration errors and confuse users.
370
409
  if (this.errors.length > 0) {
@@ -434,7 +473,7 @@ class SchemaUpgrader {
434
473
  }
435
474
 
436
475
  private removeExternalOnInterface() {
437
- for (const itf of this.schema.types<InterfaceType>('InterfaceType')) {
476
+ for (const itf of this.schema.interfaceTypes()) {
438
477
  for (const field of itf.fields()) {
439
478
  const external = this.external(field);
440
479
  if (external) {
@@ -590,7 +629,7 @@ class SchemaUpgrader {
590
629
  }
591
630
 
592
631
  private removeDirectivesOnInterface() {
593
- for (const type of this.schema.types<InterfaceType>('InterfaceType')) {
632
+ for (const type of this.schema.interfaceTypes()) {
594
633
  for (const application of type.appliedDirectivesOf(this.metadata.keyDirective())) {
595
634
  this.addChange(new KeyOnInterfaceRemoval(type.name));
596
635
  application.remove();
@@ -607,7 +646,7 @@ class SchemaUpgrader {
607
646
  }
608
647
 
609
648
  private removeProvidesOnNonComposite() {
610
- for (const type of this.schema.types<ObjectType>('ObjectType')) {
649
+ for (const type of this.schema.objectTypes()) {
611
650
  for (const field of type.fields()) {
612
651
  if (isCompositeType(baseType(field.type!))) {
613
652
  continue;
@@ -627,7 +666,7 @@ class SchemaUpgrader {
627
666
  // We add shareable:
628
667
  // - to every "value type" (in the fed1 sense of non-root type and non-entity) if it is used in any other subgraphs
629
668
  // - 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)
630
- for (const type of this.schema.types<ObjectType>('ObjectType')) {
669
+ for (const type of this.schema.objectTypes()) {
631
670
  if (type.hasAppliedDirective(keyDirective) || type.isRootType()) {
632
671
  for (const field of type.fields()) {
633
672
  // 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
@@ -651,4 +690,29 @@ class SchemaUpgrader {
651
690
  }
652
691
  }
653
692
  }
693
+
694
+ private removeTagOnExternal() {
695
+ const tagDirective = this.schema.directive('tag');
696
+ if (!tagDirective) {
697
+ return;
698
+ }
699
+
700
+ for (const application of tagDirective.applications()) {
701
+ const element = application.parent;
702
+ if (!(element instanceof FieldDefinition)) {
703
+ continue;
704
+ }
705
+ if (this.external(element)) {
706
+ const tagIsUsedInOtherDefinition = this.otherSubgraphs
707
+ .map((s) => getField(s.schema, element.parent.name, element.name))
708
+ .filter((f) => !(f && f.hasAppliedDirective('external')))
709
+ .some((f) => f && f.appliedDirectivesOf('tag').some((d) => valueEquals(application.arguments(), d.arguments())));
710
+
711
+ if (tagIsUsedInOtherDefinition) {
712
+ this.addChange(new RemovedTagOnExternal(application.toString(), element.coordinate));
713
+ application.remove();
714
+ }
715
+ }
716
+ }
717
+ }
654
718
  }
@@ -14,6 +14,7 @@ const SUPPORTED_FEATURES = new Set([
14
14
  'https://specs.apollo.dev/tag/v0.1',
15
15
  'https://specs.apollo.dev/tag/v0.2',
16
16
  'https://specs.apollo.dev/inaccessible/v0.1',
17
+ 'https://specs.apollo.dev/inaccessible/v0.2',
17
18
  ]);
18
19
 
19
20
  export function ErrUnsupportedFeature(feature: CoreFeature): Error {
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