@apollo/federation-internals 2.0.0 → 2.0.2-alpha.1

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 (57) 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/coreSpec.d.ts +1 -0
  6. package/dist/coreSpec.d.ts.map +1 -1
  7. package/dist/coreSpec.js +3 -0
  8. package/dist/coreSpec.js.map +1 -1
  9. package/dist/definitions.d.ts.map +1 -1
  10. package/dist/definitions.js +9 -5
  11. package/dist/definitions.js.map +1 -1
  12. package/dist/error.js +7 -7
  13. package/dist/error.js.map +1 -1
  14. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  15. package/dist/extractSubgraphsFromSupergraph.js +170 -152
  16. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  17. package/dist/federation.d.ts.map +1 -1
  18. package/dist/federation.js +15 -0
  19. package/dist/federation.js.map +1 -1
  20. package/dist/genErrorCodeDoc.js +12 -6
  21. package/dist/genErrorCodeDoc.js.map +1 -1
  22. package/dist/inaccessibleSpec.d.ts +2 -1
  23. package/dist/inaccessibleSpec.d.ts.map +1 -1
  24. package/dist/inaccessibleSpec.js +4 -1
  25. package/dist/inaccessibleSpec.js.map +1 -1
  26. package/dist/joinSpec.d.ts +2 -1
  27. package/dist/joinSpec.d.ts.map +1 -1
  28. package/dist/joinSpec.js +3 -0
  29. package/dist/joinSpec.js.map +1 -1
  30. package/dist/operations.d.ts +38 -4
  31. package/dist/operations.d.ts.map +1 -1
  32. package/dist/operations.js +107 -8
  33. package/dist/operations.js.map +1 -1
  34. package/dist/schemaUpgrader.d.ts +8 -1
  35. package/dist/schemaUpgrader.d.ts.map +1 -1
  36. package/dist/schemaUpgrader.js +40 -1
  37. package/dist/schemaUpgrader.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/__tests__/definitions.test.ts +66 -0
  40. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +83 -0
  41. package/src/__tests__/operations.test.ts +119 -1
  42. package/src/__tests__/removeInaccessibleElements.test.ts +84 -1
  43. package/src/__tests__/schemaUpgrader.test.ts +38 -0
  44. package/src/__tests__/subgraphValidation.test.ts +74 -0
  45. package/src/buildSchema.ts +32 -5
  46. package/src/coreSpec.ts +4 -0
  47. package/src/definitions.ts +15 -6
  48. package/src/error.ts +7 -7
  49. package/src/extractSubgraphsFromSupergraph.ts +229 -204
  50. package/src/federation.ts +50 -0
  51. package/src/genErrorCodeDoc.ts +13 -7
  52. package/src/inaccessibleSpec.ts +17 -3
  53. package/src/joinSpec.ts +5 -1
  54. package/src/operations.ts +179 -8
  55. package/src/schemaUpgrader.ts +45 -0
  56. package/tsconfig.test.tsbuildinfo +1 -1
  57. package/tsconfig.tsbuildinfo +1 -1
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
 
package/src/federation.ts CHANGED
@@ -1013,6 +1013,56 @@ function isFedSpecLinkDirective(directive: Directive<SchemaDefinition>): directi
1013
1013
  }
1014
1014
 
1015
1015
  function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1016
+
1017
+ // We special case @key, @requires and @provides because we've seen existing user schema where those
1018
+ // have been defined in an invalid way, but in a way that fed1 wasn't rejecting. So for convenience,
1019
+ // if we detect one of those case, we just remove the definition and let the code afteward add the
1020
+ // proper definition back.
1021
+ // Note that, in a perfect world, we'd do this within the `SchemaUpgrader`. But the way the code
1022
+ // is organised, this method is called before we reach the `SchemaUpgrader`, and it doesn't seem
1023
+ // worth refactoring things drastically for that minor convenience.
1024
+ for (const spec of [keyDirectiveSpec, providesDirectiveSpec, requiresDirectiveSpec]) {
1025
+ const directive = schema.directive(spec.name);
1026
+ if (!directive) {
1027
+ continue;
1028
+ }
1029
+
1030
+ // We shouldn't have applications at the time of this writing because `completeSubgraphSchema`, which calls this,
1031
+ // is only called:
1032
+ // 1. during schema parsing, by `FederationBluePrint.onDirectiveDefinitionAndSchemaParsed`, and that is called
1033
+ // before we process any directive applications.
1034
+ // 2. by `setSchemaAsFed2Subgraph`, but as the name imply, this trickles to `completeFed2SubgraphSchema`, not
1035
+ // this one method.
1036
+ // In other words, there is currently no way to create a full fed1 schema first, and get that method called
1037
+ // second. If that changes (no real reason but...), we'd have to modify this because when we remove the
1038
+ // definition to re-add the "correct" version, we'd have to re-attach existing applications (doable but not
1039
+ // done). This assert is so we notice it quickly if that ever happens (again, unlikely, because fed1 schema
1040
+ // is a backward compatibility thing and there is no reason to expand that too much in the future).
1041
+ assert(directive.applications().length === 0, `${directive} shouldn't have had validation at that places`);
1042
+
1043
+ // The patterns we recognize and "correct" (by essentially ignoring the definition)
1044
+ // are:
1045
+ // 1. if the definition has no arguments at all.
1046
+ // 2. if the `fields` argument is declared as nullable.
1047
+ // 3. if the `fields` argument type is named "FieldSet" instead of "_FieldSet".
1048
+ //
1049
+ // Note that they all correspong to things we've seen in use schema.
1050
+ const fieldType = directive.argument('fields')?.type?.toString();
1051
+ // Note that to be on the safe side, we check that `fields` is the only argument. That's
1052
+ // because while fed2 accepts the optional `resolvable` arg for @key, fed1 only ever
1053
+ // accepted that one argument for all those directives. But if the use had definited
1054
+ // more arguments _and_ provided value for such extra argument in some applications,
1055
+ // us removing the definition would create validation errors that would be hard to
1056
+ // understand for the user.
1057
+ const fieldTypeIsWrongInKnownWays = !!fieldType
1058
+ && directive.arguments().length === 1
1059
+ && (fieldType === 'String' || fieldType === '_FieldSet' || fieldType === 'FieldSet');
1060
+
1061
+ if (directive.arguments().length === 0 || fieldTypeIsWrongInKnownWays) {
1062
+ directive.remove();
1063
+ }
1064
+ }
1065
+
1016
1066
  return [
1017
1067
  fieldSetTypeSpec.checkOrAdd(schema, '_' + fieldSetTypeSpec.name),
1018
1068
  keyDirectiveSpec.checkOrAdd(schema),