@apollo/federation-internals 2.0.1 → 2.0.2-alpha.2

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 (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +17 -6
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/definitions.d.ts.map +1 -1
  6. package/dist/definitions.js +13 -6
  7. package/dist/definitions.js.map +1 -1
  8. package/dist/error.js +7 -7
  9. package/dist/error.js.map +1 -1
  10. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  11. package/dist/extractSubgraphsFromSupergraph.js +170 -152
  12. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  13. package/dist/federation.d.ts.map +1 -1
  14. package/dist/federation.js +15 -0
  15. package/dist/federation.js.map +1 -1
  16. package/dist/genErrorCodeDoc.js +12 -6
  17. package/dist/genErrorCodeDoc.js.map +1 -1
  18. package/dist/inaccessibleSpec.js +1 -1
  19. package/dist/inaccessibleSpec.js.map +1 -1
  20. package/dist/operations.d.ts +38 -4
  21. package/dist/operations.d.ts.map +1 -1
  22. package/dist/operations.js +107 -8
  23. package/dist/operations.js.map +1 -1
  24. package/dist/schemaUpgrader.d.ts +8 -1
  25. package/dist/schemaUpgrader.d.ts.map +1 -1
  26. package/dist/schemaUpgrader.js +40 -1
  27. package/dist/schemaUpgrader.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/definitions.test.ts +87 -0
  30. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +83 -0
  31. package/src/__tests__/operations.test.ts +119 -1
  32. package/src/__tests__/removeInaccessibleElements.test.ts +84 -1
  33. package/src/__tests__/schemaUpgrader.test.ts +38 -0
  34. package/src/__tests__/subgraphValidation.test.ts +74 -0
  35. package/src/buildSchema.ts +32 -5
  36. package/src/definitions.ts +20 -7
  37. package/src/error.ts +7 -7
  38. package/src/extractSubgraphsFromSupergraph.ts +229 -204
  39. package/src/federation.ts +50 -0
  40. package/src/genErrorCodeDoc.ts +13 -7
  41. package/src/inaccessibleSpec.ts +12 -2
  42. package/src/operations.ts +179 -8
  43. package/src/schemaUpgrader.ts +45 -0
  44. package/tsconfig.test.tsbuildinfo +1 -1
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -946,11 +946,13 @@ export class CoreFeature {
946
946
  }
947
947
 
948
948
  directiveNameInSchema(name: string): string {
949
- if (name === this.url.name) {
950
- return this.nameInSchema;
951
- }
952
949
  const elementImport = this.imports.find((i) => i.name.charAt(0) === '@' && i.name.slice(1) === name);
953
- return elementImport ? (elementImport.as?.slice(1) ?? name) : this.nameInSchema + '__' + name;
950
+ return elementImport
951
+ ? (elementImport.as?.slice(1) ?? name)
952
+ : (name === this.url.name
953
+ ? this.nameInSchema
954
+ : this.nameInSchema + '__' + name
955
+ );
954
956
  }
955
957
 
956
958
  typeNameInSchema(name: string): string {
@@ -2104,7 +2106,11 @@ export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
2104
2106
  }
2105
2107
 
2106
2108
  protected removeInnerElements(): void {
2107
- this._values.splice(0, this._values.length);
2109
+ // Make a copy, since EnumValue.remove() will modify this._values.
2110
+ const values = Array.from(this._values);
2111
+ for (const value of values) {
2112
+ value.remove();
2113
+ }
2108
2114
  }
2109
2115
 
2110
2116
  protected hasNonExtensionInnerElements(): boolean {
@@ -2762,7 +2768,7 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
2762
2768
  assert(false, `Directive definition ${this} can't reference other types (it's arguments can); shouldn't be asked to remove reference to ${type}`);
2763
2769
  }
2764
2770
 
2765
- /**
2771
+ /**
2766
2772
  * Removes this directive definition from its parent schema.
2767
2773
  *
2768
2774
  * After calling this method, this directive definition will be "detached": it will have no parent, schema, or
@@ -3213,9 +3219,16 @@ function copy(source: Schema, dest: Schema) {
3213
3219
  for (const type of typesToCopy(source, dest)) {
3214
3220
  dest.addType(newNamedType(type.kind, type.name));
3215
3221
  }
3222
+ // Directives can use other directives in their arguments. So, like types, we first shallow copy
3223
+ // directives so future references to any of them can be dereferenced. We'll copy the actual
3224
+ // definition later after all directives are defined.
3216
3225
  for (const directive of directivesToCopy(source, dest)) {
3217
- copyDirectiveDefinitionInner(directive, dest.addDirectiveDefinition(directive.name));
3226
+ dest.addDirectiveDefinition(directive.name);
3218
3227
  }
3228
+ for (const directive of directivesToCopy(source, dest)) {
3229
+ copyDirectiveDefinitionInner(directive, dest.directive(directive.name)!);
3230
+ }
3231
+
3219
3232
  copySchemaDefinitionInner(source.schemaDefinition, dest.schemaDefinition);
3220
3233
  for (const type of typesToCopy(source, dest)) {
3221
3234
  copyNamedTypeInner(type, dest.type(type.name)!);
package/src/error.ts CHANGED
@@ -183,7 +183,7 @@ const REQUIRES_UNSUPPORTED_ON_INTERFACE = DIRECTIVE_UNSUPPORTED_ON_INTERFACE.cre
183
183
 
184
184
  const EXTERNAL_UNUSED = makeCodeDefinition(
185
185
  'EXTERNAL_UNUSED',
186
- 'An `@external` field is not being used by any instance of `@key`, `@requires`, `@provides` or to satisfy an interface implememtation.',
186
+ 'An `@external` field is not being used by any instance of `@key`, `@requires`, `@provides` or to satisfy an interface implementation.',
187
187
  { addedIn: FED1_CODE },
188
188
  );
189
189
 
@@ -191,7 +191,7 @@ const TYPE_WITH_ONLY_UNUSED_EXTERNAL = makeCodeDefinition(
191
191
  'TYPE_WITH_ONLY_UNUSED_EXTERNAL',
192
192
  'A federation 1 schema has a composite type comprised only of unused external fields.'
193
193
  + ` Note that this error can _only_ be raised for federation 1 schema as federation 2 schema do not allow unused external fields (and errors with code ${EXTERNAL_UNUSED.code} will be raised in that case).`
194
- + ' But when federation 1 schema are automatically migrated to federation 2 ones, unused external fields are automaticaly removed, and in rare case this can leave a type empty. If that happens, an error with this code will be raised',
194
+ + ' But when federation 1 schema are automatically migrated to federation 2 ones, unused external fields are automatically removed, and in rare case this can leave a type empty. If that happens, an error with this code will be raised',
195
195
  );
196
196
 
197
197
  const PROVIDES_ON_NON_OBJECT_FIELD = makeCodeDefinition(
@@ -245,7 +245,7 @@ const NO_QUERIES = makeCodeDefinition(
245
245
 
246
246
  const INTERFACE_FIELD_NO_IMPLEM = makeCodeDefinition(
247
247
  'INTERFACE_FIELD_NO_IMPLEM',
248
- 'After subgraph merging, an implemenation is missing a field of one of the interface it implements (which can happen for valid subgraphs).'
248
+ 'After subgraph merging, an implementation is missing a field of one of the interface it implements (which can happen for valid subgraphs).'
249
249
  );
250
250
 
251
251
  const TYPE_KIND_MISMATCH = makeCodeDefinition(
@@ -267,12 +267,12 @@ const EXTERNAL_ARGUMENT_MISSING = makeCodeDefinition(
267
267
 
268
268
  const EXTERNAL_ARGUMENT_TYPE_MISMATCH = makeCodeDefinition(
269
269
  'EXTERNAL_ARGUMENT_TYPE_MISMATCH',
270
- 'An `@external` field declares an argument with a type that is incompatible with the corresponding argument in the declaration(s) of that field in other subgtaphs.',
270
+ 'An `@external` field declares an argument with a type that is incompatible with the corresponding argument in the declaration(s) of that field in other subgraphs.',
271
271
  );
272
272
 
273
273
  const EXTERNAL_ARGUMENT_DEFAULT_MISMATCH = makeCodeDefinition(
274
274
  'EXTERNAL_ARGUMENT_DEFAULT_MISMATCH',
275
- 'An `@external` field declares an argument with a default that is incompatible with the corresponding argument in the declaration(s) of that field in other subgtaphs.',
275
+ 'An `@external` field declares an argument with a default that is incompatible with the corresponding argument in the declaration(s) of that field in other subgraphs.',
276
276
  );
277
277
 
278
278
  const EXTERNAL_ON_INTERFACE = makeCodeDefinition(
@@ -510,7 +510,7 @@ const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorC
510
510
  */
511
511
  export const REMOVED_ERRORS = [
512
512
  ['KEY_FIELDS_MISSING_ON_BASE', 'Keys can now use any field from any other subgraph.'],
513
- ['KEY_FIELDS_MISSING_EXTERNAL', 'Using `@external` for key fields is now decouraged, unless the field is truly meant to be external.'],
513
+ ['KEY_FIELDS_MISSING_EXTERNAL', 'Using `@external` for key fields is now discouraged, unless the field is truly meant to be external.'],
514
514
  ['KEY_MISSING_ON_BASE', 'Each subgraph is now free to declare a key only if it needs it.'],
515
515
  ['MULTIPLE_KEYS_ON_EXTENSION', 'Every subgraph can have multiple keys, as necessary.'],
516
516
  ['KEY_NOT_SPECIFIED', 'Each subgraph can declare key independently of any other subgraph.'],
@@ -528,5 +528,5 @@ export const REMOVED_ERRORS = [
528
528
  ['VALUE_TYPE_NO_ENTITY', 'There is no strong different between entity and value types in the model (they are just usage pattern) and a type can have keys in one subgraph but not another.'],
529
529
  ['VALUE_TYPE_UNION_TYPES_MISMATCH', 'Subgraph definitions for an union are now merged by composition'],
530
530
  ['PROVIDES_FIELDS_SELECT_INVALID_TYPE', '@provides can now be used on field of interface, union and list types'],
531
- ['RESERVED_FIELD_USED', 'This error was previously not correctly enforced: the _service and _entities, if present, were overriden; this is still the case'],
531
+ ['RESERVED_FIELD_USED', 'This error was previously not correctly enforced: the _service and _entities, if present, were overridden; this is still the case'],
532
532
  ];
@@ -72,244 +72,269 @@ function collectEmptySubgraphs(supergraph: Schema, joinSpec: JoinSpecDefinition)
72
72
  return [subgraphs, graphEnumNameToSubgraphName];
73
73
  }
74
74
 
75
+ class SubgraphExtractionError {
76
+ constructor(
77
+ readonly originalError: any,
78
+ readonly subgraph: Subgraph,
79
+ ) {
80
+ }
81
+ }
82
+
75
83
  export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
76
84
  const [coreFeatures, joinSpec] = validateSupergraph(supergraph);
77
85
  const isFed1 = joinSpec.version.equals(new FeatureVersion(0, 1));
86
+ try {
87
+ // We first collect the subgraphs (creating an empty schema that we'll populate next for each).
88
+ const [subgraphs, graphEnumNameToSubgraphName] = collectEmptySubgraphs(supergraph, joinSpec);
89
+ const typeDirective = joinSpec.typeDirective(supergraph);
90
+ const implementsDirective = joinSpec.implementsDirective(supergraph);
78
91
 
79
- // We first collect the subgraphs (creating an empty schema that we'll populate next for each).
80
- const [subgraphs, graphEnumNameToSubgraphName] = collectEmptySubgraphs(supergraph, joinSpec);
81
- const typeDirective = joinSpec.typeDirective(supergraph);
82
- const implementsDirective = joinSpec.implementsDirective(supergraph);
83
-
84
- // Next, we iterate on all types and add it to the proper subgraphs (along with any @key).
85
- // Note that we first add all types empty and populate the types next. This avoids having to care about the iteration
86
- // order if we have fields than depends on other types.
87
- for (const type of filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition)) {
88
- const typeApplications = type.appliedDirectivesOf(typeDirective);
89
- if (!typeApplications.length) {
90
- // Imply the type is in all subgraphs (technically, some subgraphs may not have had this type, but adding it
91
- // in that case is harmless because it will be unreachable anyway).
92
- subgraphs.values().map(sg => sg.schema).forEach(schema => schema.addType(newNamedType(type.kind, type.name)));
93
- } else {
94
- for (const application of typeApplications) {
95
- const args = application.arguments();
96
- const subgraphName = graphEnumNameToSubgraphName.get(args.graph)!;
97
- const schema = subgraphs.get(subgraphName)!.schema;
98
- // We can have more than one type directive for a given subgraph
99
- let subgraphType = schema.type(type.name);
100
- if (!subgraphType) {
101
- subgraphType = schema.addType(newNamedType(type.kind, type.name));
102
- }
103
- if (args.key) {
104
- const { resolvable } = args;
105
- const directive = subgraphType.applyDirective('key', {'fields': args.key, resolvable});
106
- if (args.extension) {
107
- directive.setOfExtension(subgraphType.newExtension());
92
+ // Next, we iterate on all types and add it to the proper subgraphs (along with any @key).
93
+ // Note that we first add all types empty and populate the types next. This avoids having to care about the iteration
94
+ // order if we have fields than depends on other types.
95
+ for (const type of filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition)) {
96
+ const typeApplications = type.appliedDirectivesOf(typeDirective);
97
+ if (!typeApplications.length) {
98
+ // Imply the type is in all subgraphs (technically, some subgraphs may not have had this type, but adding it
99
+ // in that case is harmless because it will be unreachable anyway).
100
+ subgraphs.values().map(sg => sg.schema).forEach(schema => schema.addType(newNamedType(type.kind, type.name)));
101
+ } else {
102
+ for (const application of typeApplications) {
103
+ const args = application.arguments();
104
+ const subgraphName = graphEnumNameToSubgraphName.get(args.graph)!;
105
+ const schema = subgraphs.get(subgraphName)!.schema;
106
+ // We can have more than one type directive for a given subgraph
107
+ let subgraphType = schema.type(type.name);
108
+ if (!subgraphType) {
109
+ subgraphType = schema.addType(newNamedType(type.kind, type.name));
110
+ }
111
+ if (args.key) {
112
+ const { resolvable } = args;
113
+ const directive = subgraphType.applyDirective('key', {'fields': args.key, resolvable});
114
+ if (args.extension) {
115
+ directive.setOfExtension(subgraphType.newExtension());
116
+ }
108
117
  }
109
118
  }
110
119
  }
111
120
  }
112
- }
113
121
 
114
- const ownerDirective = joinSpec.ownerDirective(supergraph);
115
- const fieldDirective = joinSpec.fieldDirective(supergraph);
116
- // We can now populate all those types (with relevant @provides and @requires on fields).
117
- for (const type of filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition)) {
118
- switch (type.kind) {
119
- case 'ObjectType':
120
- // @ts-expect-error: we fall-through the inputObjectType for fields.
121
- case 'InterfaceType':
122
- const addedInterfaces = [];
123
- const implementsApplications = implementsDirective ? type.appliedDirectivesOf(implementsDirective) : [];
124
- for (const application of implementsApplications) {
125
- const args = application.arguments();
126
- const subgraph = subgraphs.get(graphEnumNameToSubgraphName.get(args.graph)!)!;
127
- const schema = subgraph.schema;
128
- (schema.type(type.name)! as (ObjectType | InterfaceType)).addImplementedInterface(args.interface);
129
- addedInterfaces.push(args.interface);
130
- }
131
- for (const implementations of type.interfaceImplementations()) {
132
- // If the object/interface implements an interface but we had no @join__implements for it (which will
133
- // always be the case for join v0.1 in particular), then that means the object/interface should implement
134
- // the interface in all subgraphs (which contains both types).
135
- const name = implementations.interface.name;
136
- if (!addedInterfaces.includes(name)) {
137
- for (const subgraph of subgraphs) {
138
- const subgraphType = subgraph.schema.type(type.name);
139
- const subgraphItf = subgraph.schema.type(name);
140
- if (subgraphType && subgraphItf) {
141
- (subgraphType as (ObjectType | InterfaceType)).addImplementedInterface(name);
142
- }
143
- }
122
+ const ownerDirective = joinSpec.ownerDirective(supergraph);
123
+ const fieldDirective = joinSpec.fieldDirective(supergraph);
124
+ // We can now populate all those types (with relevant @provides and @requires on fields).
125
+ for (const type of filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition)) {
126
+ switch (type.kind) {
127
+ case 'ObjectType':
128
+ // @ts-expect-error: we fall-through the inputObjectType for fields.
129
+ case 'InterfaceType':
130
+ const addedInterfaces = [];
131
+ const implementsApplications = implementsDirective ? type.appliedDirectivesOf(implementsDirective) : [];
132
+ for (const application of implementsApplications) {
133
+ const args = application.arguments();
134
+ const subgraph = subgraphs.get(graphEnumNameToSubgraphName.get(args.graph)!)!;
135
+ const schema = subgraph.schema;
136
+ (schema.type(type.name)! as (ObjectType | InterfaceType)).addImplementedInterface(args.interface);
137
+ addedInterfaces.push(args.interface);
144
138
  }
145
- }
146
- // Fall-through on purpose.
147
- case 'InputObjectType':
148
- for (const field of type.fields()) {
149
- const fieldApplications = field.appliedDirectivesOf(fieldDirective);
150
- if (!fieldApplications.length) {
151
- // The meaning of having no join__field depends on whether the parent type has a join__owner.
152
- // If it does, it means the field is only on that owner subgraph. Otherwise, we kind of don't
153
- // know, so we add it to all subgraphs that have the parent type and, if the field base type
154
- // is a named type, know that field type.
155
- const ownerApplications = ownerDirective ? type.appliedDirectivesOf(ownerDirective) : [];
156
- if (!ownerApplications.length) {
157
- const fieldBaseType = baseType(field.type!);
139
+ for (const implementations of type.interfaceImplementations()) {
140
+ // If the object/interface implements an interface but we had no @join__implements for it (which will
141
+ // always be the case for join v0.1 in particular), then that means the object/interface should implement
142
+ // the interface in all subgraphs (which contains both types).
143
+ const name = implementations.interface.name;
144
+ if (!addedInterfaces.includes(name)) {
158
145
  for (const subgraph of subgraphs) {
159
- if (subgraph.schema.type(fieldBaseType.name)) {
160
- addSubgraphField(field, subgraph);
146
+ const subgraphType = subgraph.schema.type(type.name);
147
+ const subgraphItf = subgraph.schema.type(name);
148
+ if (subgraphType && subgraphItf) {
149
+ (subgraphType as (ObjectType | InterfaceType)).addImplementedInterface(name);
161
150
  }
162
151
  }
163
- } else {
164
- assert(ownerApplications.length == 1, () => `Found multiple join__owner directives on type ${type}`)
165
- const subgraph = subgraphs.get(graphEnumNameToSubgraphName.get(ownerApplications[0].arguments().graph)!)!;
166
- const subgraphField = addSubgraphField(field, subgraph);
167
- assert(subgraphField, () => `Found join__owner directive on ${type} but no corresponding join__type`);
168
152
  }
169
- } else {
170
- for (const application of fieldApplications) {
171
- const args = application.arguments();
172
- const subgraph = subgraphs.get(graphEnumNameToSubgraphName.get(args.graph)!)!;
173
- const subgraphField = addSubgraphField(field, subgraph, args.type);
174
- assert(subgraphField, () => `Found join__field directive for graph ${subgraph.name} on field ${field.coordinate} but no corresponding join__type on ${type}`);
175
- if (args.requires) {
176
- subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': args.requires});
177
- }
178
- if (args.provides) {
179
- subgraphField.applyDirective(subgraph.metadata().providesDirective(), {'fields': args.provides});
180
- }
181
- if (args.external) {
182
- subgraphField.applyDirective(subgraph.metadata().externalDirective());
183
- }
184
- if (args.usedOverridden) {
185
- subgraphField.applyDirective(subgraph.metadata().externalDirective(), {'reason': '[overridden]'});
153
+ }
154
+ // Fall-through on purpose.
155
+ case 'InputObjectType':
156
+ for (const field of type.fields()) {
157
+ const fieldApplications = field.appliedDirectivesOf(fieldDirective);
158
+ if (!fieldApplications.length) {
159
+ // The meaning of having no join__field depends on whether the parent type has a join__owner.
160
+ // If it does, it means the field is only on that owner subgraph. Otherwise, we kind of don't
161
+ // know, so we add it to all subgraphs that have the parent type and, if the field base type
162
+ // is a named type, know that field type.
163
+ const ownerApplications = ownerDirective ? type.appliedDirectivesOf(ownerDirective) : [];
164
+ if (!ownerApplications.length) {
165
+ const fieldBaseType = baseType(field.type!);
166
+ for (const subgraph of subgraphs) {
167
+ if (subgraph.schema.type(fieldBaseType.name)) {
168
+ addSubgraphField(field, subgraph);
169
+ }
170
+ }
171
+ } else {
172
+ assert(ownerApplications.length == 1, () => `Found multiple join__owner directives on type ${type}`)
173
+ const subgraph = subgraphs.get(graphEnumNameToSubgraphName.get(ownerApplications[0].arguments().graph)!)!;
174
+ const subgraphField = addSubgraphField(field, subgraph);
175
+ assert(subgraphField, () => `Found join__owner directive on ${type} but no corresponding join__type`);
186
176
  }
187
- if (args.override) {
188
- subgraphField.applyDirective(subgraph.metadata().overrideDirective(), {'from': args.override});
177
+ } else {
178
+ for (const application of fieldApplications) {
179
+ const args = application.arguments();
180
+ const subgraph = subgraphs.get(graphEnumNameToSubgraphName.get(args.graph)!)!;
181
+ const subgraphField = addSubgraphField(field, subgraph, args.type);
182
+ assert(subgraphField, () => `Found join__field directive for graph ${subgraph.name} on field ${field.coordinate} but no corresponding join__type on ${type}`);
183
+ if (args.requires) {
184
+ subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': args.requires});
185
+ }
186
+ if (args.provides) {
187
+ subgraphField.applyDirective(subgraph.metadata().providesDirective(), {'fields': args.provides});
188
+ }
189
+ if (args.external) {
190
+ subgraphField.applyDirective(subgraph.metadata().externalDirective());
191
+ }
192
+ if (args.usedOverridden) {
193
+ subgraphField.applyDirective(subgraph.metadata().externalDirective(), {'reason': '[overridden]'});
194
+ }
195
+ if (args.override) {
196
+ subgraphField.applyDirective(subgraph.metadata().overrideDirective(), {'from': args.override});
197
+ }
189
198
  }
190
199
  }
191
200
  }
192
- }
193
- break;
194
- case 'EnumType':
195
- // TODO: it's not guaranteed that every enum value was in every subgraph declaring the enum and we should preserve
196
- // that info with the join spec. But for now, we add every values to all subgraphs (having the enum)
197
- for (const subgraph of subgraphs) {
198
- const subgraphEnum = subgraph.schema.type(type.name);
199
- if (!subgraphEnum) {
200
- continue;
201
- }
202
- assert(isEnumType(subgraphEnum), () => `${subgraphEnum} should be an enum but found a ${subgraphEnum.kind}`);
203
- for (const value of type.values) {
204
- subgraphEnum.addValue(value.name);
205
- }
206
- }
207
- break;
208
- case 'UnionType':
209
- // TODO: Same as for enums. We need to know in which subgraph each member is defined.
210
- // But for now, we also add every members to all subgraphs (as long as the subgraph has both the union type
211
- // and the member in question).
212
- for (const subgraph of subgraphs) {
213
- const subgraphUnion = subgraph.schema.type(type.name);
214
- if (!subgraphUnion) {
215
- continue;
216
- }
217
- assert(isUnionType(subgraphUnion), () => `${subgraphUnion} should be an enum but found a ${subgraphUnion.kind}`);
218
- for (const memberType of type.types()) {
219
- const subgraphType = subgraph.schema.type(memberType.name);
220
- if (subgraphType) {
221
- subgraphUnion.addType(subgraphType as ObjectType);
201
+ break;
202
+ case 'EnumType':
203
+ // TODO: it's not guaranteed that every enum value was in every subgraph declaring the enum and we should preserve
204
+ // that info with the join spec. But for now, we add every values to all subgraphs (having the enum)
205
+ for (const subgraph of subgraphs) {
206
+ const subgraphEnum = subgraph.schema.type(type.name);
207
+ if (!subgraphEnum) {
208
+ continue;
209
+ }
210
+ assert(isEnumType(subgraphEnum), () => `${subgraphEnum} should be an enum but found a ${subgraphEnum.kind}`);
211
+ for (const value of type.values) {
212
+ subgraphEnum.addValue(value.name);
222
213
  }
223
- }
224
- }
225
- break;
226
- }
227
- }
228
-
229
- for (const subgraph of subgraphs) {
230
- if (isFed1) {
231
- // The join spec in fed1 was not including external fields. Let's make sure we had them or we'll get validation
232
- // errors later.
233
- addExternalFields(subgraph, supergraph, isFed1);
234
- }
235
- removeInactiveProvidesAndRequires(subgraph.schema);
236
-
237
- // We now do an additional path on all types because we sometimes added types to subgraphs without
238
- // being sure that the subgraph had the type in the first place (especially with the 0.1 join spec), and because
239
- // we later might not have added any fields/members to said type, they may be empty (indicating they clearly
240
- // didn't belong to the subgraph in the first) and we need to remove them.
241
- // Note that need to do this _after_ the `addExternalFields` call above since it may have added (external) fields
242
- // to some of the types.
243
- for (const type of subgraph.schema.types()) {
244
- switch (type.kind) {
245
- case 'ObjectType':
246
- case 'InterfaceType':
247
- case 'InputObjectType':
248
- if (!type.hasFields()) {
249
- // Note that we have to use removeRecursive or this could leave the subgraph invalid. But if the
250
- // type was not in this subgraphs, nothing that depends on it should be either.
251
- type.removeRecursive();
252
214
  }
253
215
  break;
254
216
  case 'UnionType':
255
- if (type.membersCount() === 0) {
256
- type.removeRecursive();
217
+ // TODO: Same as for enums. We need to know in which subgraph each member is defined.
218
+ // But for now, we also add every members to all subgraphs (as long as the subgraph has both the union type
219
+ // and the member in question).
220
+ for (const subgraph of subgraphs) {
221
+ const subgraphUnion = subgraph.schema.type(type.name);
222
+ if (!subgraphUnion) {
223
+ continue;
224
+ }
225
+ assert(isUnionType(subgraphUnion), () => `${subgraphUnion} should be an enum but found a ${subgraphUnion.kind}`);
226
+ for (const memberType of type.types()) {
227
+ const subgraphType = subgraph.schema.type(memberType.name);
228
+ if (subgraphType) {
229
+ subgraphUnion.addType(subgraphType as ObjectType);
230
+ }
231
+ }
257
232
  }
258
233
  break;
259
234
  }
260
235
  }
261
- }
262
236
 
263
- // TODO: Not sure that code is needed anymore (any field necessary to validate an interface will have been marked
264
- // external)?
265
- if (isFed1) {
266
- // We now make a pass on every field of every interface and check that all implementers do have that field (even if
267
- // external). If not (which can happen because, again, the v0.1 spec had no information on where an interface was
268
- // truly defined, so we've so far added them everywhere with all their fields, but some fields may have been part
269
- // of an extension and be only in a few subgraphs), we remove the field or the subgraph would be invalid.
270
237
  for (const subgraph of subgraphs) {
271
- for (const itf of subgraph.schema.interfaceTypes()) {
272
- // We only look at objects because interfaces are handled by this own loop in practice.
273
- const implementations = itf.possibleRuntimeTypes();
274
- for (const field of itf.fields()) {
275
- if (!implementations.every(implem => implem.field(field.name))) {
276
- field.remove();
277
- }
238
+ if (isFed1) {
239
+ // The join spec in fed1 was not including external fields. Let's make sure we had them or we'll get validation
240
+ // errors later.
241
+ addExternalFields(subgraph, supergraph, isFed1);
242
+ }
243
+ removeInactiveProvidesAndRequires(subgraph.schema);
244
+
245
+ // We now do an additional path on all types because we sometimes added types to subgraphs without
246
+ // being sure that the subgraph had the type in the first place (especially with the 0.1 join spec), and because
247
+ // we later might not have added any fields/members to said type, they may be empty (indicating they clearly
248
+ // didn't belong to the subgraph in the first) and we need to remove them.
249
+ // Note that need to do this _after_ the `addExternalFields` call above since it may have added (external) fields
250
+ // to some of the types.
251
+ for (const type of subgraph.schema.types()) {
252
+ switch (type.kind) {
253
+ case 'ObjectType':
254
+ case 'InterfaceType':
255
+ case 'InputObjectType':
256
+ if (!type.hasFields()) {
257
+ // Note that we have to use removeRecursive or this could leave the subgraph invalid. But if the
258
+ // type was not in this subgraphs, nothing that depends on it should be either.
259
+ type.removeRecursive();
260
+ }
261
+ break;
262
+ case 'UnionType':
263
+ if (type.membersCount() === 0) {
264
+ type.removeRecursive();
265
+ }
266
+ break;
278
267
  }
279
- // And it may be that the interface wasn't part of the subgraph at all!
280
- if (!itf.hasFields()) {
281
- itf.remove();
268
+ }
269
+ }
270
+
271
+ // TODO: Not sure that code is needed anymore (any field necessary to validate an interface will have been marked
272
+ // external)?
273
+ if (isFed1) {
274
+ // We now make a pass on every field of every interface and check that all implementers do have that field (even if
275
+ // external). If not (which can happen because, again, the v0.1 spec had no information on where an interface was
276
+ // truly defined, so we've so far added them everywhere with all their fields, but some fields may have been part
277
+ // of an extension and be only in a few subgraphs), we remove the field or the subgraph would be invalid.
278
+ for (const subgraph of subgraphs) {
279
+ for (const itf of subgraph.schema.interfaceTypes()) {
280
+ // We only look at objects because interfaces are handled by this own loop in practice.
281
+ const implementations = itf.possibleRuntimeTypes();
282
+ for (const field of itf.fields()) {
283
+ if (!implementations.every(implem => implem.field(field.name))) {
284
+ field.remove();
285
+ }
286
+ }
287
+ // And it may be that the interface wasn't part of the subgraph at all!
288
+ if (!itf.hasFields()) {
289
+ itf.remove();
290
+ }
282
291
  }
283
292
  }
284
293
  }
285
- }
286
294
 
287
- // We're done with the subgraphs, so call validate (which, amongst other things, sets up the _entities query field, which ensures
288
- // all entities in all subgraphs are reachable from a query and so are properly included in the "query graph" later).
289
- for (const subgraph of subgraphs) {
290
- try {
291
- subgraph.validate();
292
- } catch (e) {
293
- // There is 2 reasons this could happen:
294
- // 1. if the subgraph is a Fed1 one, because fed2 has stricter validation than fed1, this could be due to the supergraph having been generated by fed1 and
295
- // containing something invalid that fed1 accepted and fed2 didn't (for instance, an invalid `@provides` selection).
296
- // 2. otherwise, this would be a bug (because fed1 compatibility excluded, we shouldn't extract invalid subgraphs from valid supergraphs).
297
- // We throw essentially the same thing in both cases, but adapt the message slightly.
298
- if (isFed1) {
299
- // Note that this could be a bug with the code handling fed1 as well, but it's more helpful to ask users to recompose their subgraphs with fed2 as either
300
- // it'll solve the issue and that's good, or we'll hit the other message anyway.
301
- const msg = `Error extracting subgraph "${subgraph.name}" from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n`
302
- + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph';
303
- throw new Error(`${msg}.\n\nDetails:\n${errorToString(e)}`);
304
- } else {
305
- const msg = `Unexpected error extracting subgraph ${subgraph.name} from the supergraph: this is either a bug, or the supergraph has been corrupted`;
306
- const dumpMsg = maybeDumpSubgraphSchema(subgraph);
307
- throw new Error(`${msg}.\n\nDetails:\n${errorToString(e)}\n\n${dumpMsg}`);
295
+ // We're done with the subgraphs, so call validate (which, amongst other things, sets up the _entities query field, which ensures
296
+ // all entities in all subgraphs are reachable from a query and so are properly included in the "query graph" later).
297
+ for (const subgraph of subgraphs) {
298
+ try {
299
+ subgraph.validate();
300
+ } catch (e) {
301
+ // This is going to be caught directly by the enclosing try-catch, but this is so we indicate the subgraph having the issue.
302
+ throw new SubgraphExtractionError(e, subgraph);
308
303
  }
309
304
  }
310
- }
311
305
 
312
- return subgraphs;
306
+ return subgraphs;
307
+ } catch (e) {
308
+ let error = e;
309
+ let subgraph: Subgraph | undefined = undefined;
310
+ // We want this catch to capture all errors happening during extraction, but the most common
311
+ // case is likely going to be fed2 validation that fed1 didn't enforced, and those will be
312
+ // throw when validating the extracted subgraphs, and n that case we use
313
+ // `SubgraphExtractionError` to pass the subgraph that errored out, which allows us
314
+ // to provide a bit more context in those cases.
315
+ if (e instanceof SubgraphExtractionError) {
316
+ error = e.originalError;
317
+ subgraph = e.subgraph;
318
+ }
319
+
320
+ // There is 2 reasons this could happen:
321
+ // 1. if the supergraph is a Fed1 one, because fed2 has stricter validations than fed1, this could be due to the supergraph
322
+ // containing something invalid that fed1 accepted and fed2 didn't (for instance, an invalid `@provides` selection).
323
+ // 2. otherwise, this would be a bug (because fed1 compatibility excluded, we shouldn't extract invalid subgraphs from valid supergraphs).
324
+ // We throw essentially the same thing in both cases, but adapt the message slightly.
325
+ const impacted = subgraph ? `subgraph "${subgraph.name}"` : 'subgraphs';
326
+ if (isFed1) {
327
+ // Note that this could be a bug with the code handling fed1 as well, but it's more helpful to ask users to recompose their subgraphs with fed2 as either
328
+ // it'll solve the issue and that's good, or we'll hit the other message anyway.
329
+ const msg = `Error extracting ${impacted} from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n`
330
+ + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph';
331
+ throw new Error(`${msg}.\n\nDetails:\n${errorToString(error)}`);
332
+ } else {
333
+ const msg = `Unexpected error extracting ${impacted} from the supergraph: this is either a bug, or the supergraph has been corrupted`;
334
+ const dumpMsg = subgraph ? '\n\n' + maybeDumpSubgraphSchema(subgraph) : '';
335
+ throw new Error(`${msg}.\n\nDetails:\n${errorToString(error)}${dumpMsg}`);
336
+ }
337
+ }
313
338
  }
314
339
 
315
340
  const DEBUG_SUBGRAPHS_ENV_VARIABLE_NAME = 'APOLLO_FEDERATION_DEBUG_SUBGRAPHS';
@@ -401,8 +426,8 @@ function copyType(type: Type, subgraph: Schema, subgraphName: string): Type {
401
426
  return new NonNullType(copyType(type.ofType, subgraph, subgraphName) as NullableType);
402
427
  default:
403
428
  const subgraphType = subgraph.type(type.name);
404
- assert(subgraphType, () => `Cannot find type ${type.name} in subgraph ${subgraphName}`);
405
- return subgraphType!;
429
+ assert(subgraphType, () => `Cannot find type "${type.name}" in subgraph "${subgraphName}"`);
430
+ return subgraphType;
406
431
  }
407
432
  }
408
433