@apollo/federation-internals 2.1.0-alpha.4 → 2.1.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 (60) hide show
  1. package/CHANGELOG.md +1 -10
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +1 -1
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/coreSpec.d.ts +1 -4
  6. package/dist/coreSpec.d.ts.map +1 -1
  7. package/dist/coreSpec.js +1 -5
  8. package/dist/coreSpec.js.map +1 -1
  9. package/dist/definitions.d.ts +26 -29
  10. package/dist/definitions.d.ts.map +1 -1
  11. package/dist/definitions.js +202 -158
  12. package/dist/definitions.js.map +1 -1
  13. package/dist/error.d.ts +5 -0
  14. package/dist/error.d.ts.map +1 -1
  15. package/dist/error.js +47 -1
  16. package/dist/error.js.map +1 -1
  17. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  18. package/dist/extractSubgraphsFromSupergraph.js +126 -5
  19. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  20. package/dist/federation.d.ts +2 -2
  21. package/dist/federation.d.ts.map +1 -1
  22. package/dist/federation.js +6 -6
  23. package/dist/federation.js.map +1 -1
  24. package/dist/operations.d.ts +10 -4
  25. package/dist/operations.d.ts.map +1 -1
  26. package/dist/operations.js +42 -39
  27. package/dist/operations.js.map +1 -1
  28. package/dist/print.js.map +1 -1
  29. package/dist/schemaUpgrader.d.ts.map +1 -1
  30. package/dist/schemaUpgrader.js +4 -4
  31. package/dist/schemaUpgrader.js.map +1 -1
  32. package/dist/supergraphs.d.ts +1 -3
  33. package/dist/supergraphs.d.ts.map +1 -1
  34. package/dist/supergraphs.js +9 -22
  35. package/dist/supergraphs.js.map +1 -1
  36. package/dist/utils.d.ts +2 -1
  37. package/dist/utils.d.ts.map +1 -1
  38. package/dist/utils.js +12 -1
  39. package/dist/utils.js.map +1 -1
  40. package/package.json +2 -3
  41. package/src/__tests__/coreSpec.test.ts +1 -1
  42. package/src/__tests__/definitions.test.ts +13 -4
  43. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +9 -5
  44. package/src/__tests__/operations.test.ts +123 -2
  45. package/src/__tests__/removeInaccessibleElements.test.ts +1 -3
  46. package/src/__tests__/schemaUpgrader.test.ts +0 -1
  47. package/src/__tests__/subgraphValidation.test.ts +1 -2
  48. package/src/buildSchema.ts +1 -2
  49. package/src/coreSpec.ts +2 -7
  50. package/src/definitions.ts +173 -148
  51. package/src/error.ts +62 -0
  52. package/src/extractSubgraphsFromSupergraph.ts +178 -7
  53. package/src/federation.ts +4 -3
  54. package/src/operations.ts +79 -50
  55. package/src/print.ts +2 -2
  56. package/src/schemaUpgrader.ts +5 -4
  57. package/src/supergraphs.ts +16 -25
  58. package/src/utils.ts +15 -0
  59. package/tsconfig.test.tsbuildinfo +1 -1
  60. package/tsconfig.tsbuildinfo +1 -1
package/src/error.ts CHANGED
@@ -56,6 +56,62 @@ export function extractGraphQLErrorOptions(e: GraphQLError): GraphQLErrorOptions
56
56
  };
57
57
  }
58
58
 
59
+ class AggregateGraphQLError extends GraphQLError {
60
+ constructor(
61
+ code: String,
62
+ message: string,
63
+ readonly causes: GraphQLError[],
64
+ options?: GraphQLErrorOptions,
65
+ ) {
66
+ super(
67
+ message + '. Caused by:\n' + causes.map((c) => c.toString()).join('\n\n'),
68
+ {
69
+ ...options,
70
+ extensions: { code },
71
+ }
72
+ );
73
+ }
74
+
75
+ toString() {
76
+ let output = `[${this.extensions.code}] ${super.toString()}`
77
+ output += "\ncaused by:";
78
+ for (const cause of this.causes) {
79
+ output += "\n\n - ";
80
+ output += cause.toString().split("\n").join("\n ");
81
+ }
82
+ return output;
83
+ }
84
+ }
85
+
86
+ export function aggregateError(code: String, message: string, causes: GraphQLError[]): GraphQLError {
87
+ return new AggregateGraphQLError(code, message, causes);
88
+ }
89
+
90
+ /**
91
+ * Given an error, check if it is a graphQL error and potentially extract its causes if is aggregate.
92
+ * If the error is not a graphQL error, undefined is returned.
93
+ */
94
+ export function errorCauses(e: Error): GraphQLError[] | undefined {
95
+ if (e instanceof AggregateGraphQLError) {
96
+ return e.causes;
97
+ }
98
+ if (e instanceof GraphQLError) {
99
+ return [e];
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ export function printGraphQLErrorsOrRethrow(e: Error): string {
105
+ const causes = errorCauses(e);
106
+ if (!causes) {
107
+ throw e;
108
+ }
109
+ return causes.map(e => e.toString()).join('\n\n');
110
+ }
111
+
112
+ export function printErrors(errors: GraphQLError[]): string {
113
+ return errors.map(e => e.toString()).join('\n\n');
114
+ }
59
115
  /*
60
116
  * Most codes currently originate from the initial fed 2 release so we use this for convenience.
61
117
  * This can be changed later, inline versions everywhere, if that becomes irrelevant.
@@ -144,6 +200,11 @@ const TYPE_DEFINITION_INVALID = makeCodeDefinition(
144
200
  'A built-in or federation type has an invalid definition in the schema.',
145
201
  );
146
202
 
203
+ const UNSUPPORTED_LINKED_FEATURE = makeCodeDefinition(
204
+ 'UNSUPPORTED_LINKED_FEATURE',
205
+ 'Indicates that a feature used in a @link is either unsupported or is used with unsupported options.',
206
+ );
207
+
147
208
  const UNKNOWN_FEDERATION_LINK_VERSION = makeCodeDefinition(
148
209
  'UNKNOWN_FEDERATION_LINK_VERSION',
149
210
  'The version of federation in a @link directive on the schema is unknown.',
@@ -479,6 +540,7 @@ export const ERRORS = {
479
540
  INVALID_GRAPHQL,
480
541
  DIRECTIVE_DEFINITION_INVALID,
481
542
  TYPE_DEFINITION_INVALID,
543
+ UNSUPPORTED_LINKED_FEATURE,
482
544
  UNKNOWN_FEDERATION_LINK_VERSION,
483
545
  UNKNOWN_LINK_VERSION,
484
546
  KEY_FIELDS_HAS_ARGS,
@@ -34,6 +34,7 @@ import { validateSupergraph } from "./supergraphs";
34
34
  import { builtTypeReference } from "./buildSchema";
35
35
  import { isSubtype } from "./types";
36
36
  import { printSchema } from "./print";
37
+ import { parseSelectionSet } from "./operations";
37
38
  import fs from 'fs';
38
39
  import path from 'path';
39
40
  import { validateStringContainsBoolean } from "./utils";
@@ -82,6 +83,114 @@ class SubgraphExtractionError {
82
83
  }
83
84
  }
84
85
 
86
+ function collectFieldReachableTypesForSubgraph(
87
+ supergraph: Schema,
88
+ subgraphName: string,
89
+ addReachableType: (t: NamedType) => void,
90
+ fieldInfoInSubgraph: (f: FieldDefinition<any> | InputFieldDefinition, subgraphName: string) => { isInSubgraph: boolean, typesInFederationDirectives: NamedType[] },
91
+ typeInfoInSubgraph: (t: NamedType, subgraphName: string) => { isEntityWithKeyInSubgraph: boolean, typesInFederationDirectives: NamedType[] },
92
+ ): void {
93
+ const seenTypes = new Set<string>();
94
+ // The types reachable at "top-level" are both the root types, plus any entity type with a key in this subgraph.
95
+ const stack = supergraph.schemaDefinition.roots().map((root) => root.type as NamedType)
96
+ for (const type of supergraph.types()) {
97
+ const { isEntityWithKeyInSubgraph, typesInFederationDirectives } = typeInfoInSubgraph(type, subgraphName);
98
+ if (isEntityWithKeyInSubgraph) {
99
+ stack.push(type);
100
+ }
101
+ typesInFederationDirectives.forEach((t) => stack.push(t));
102
+ }
103
+ while (stack.length > 0) {
104
+ const type = stack.pop()!;
105
+ addReachableType(type);
106
+ if (seenTypes.has(type.name)) {
107
+ continue;
108
+ }
109
+ seenTypes.add(type.name);
110
+ switch (type.kind) {
111
+ // @ts-expect-error: we fall-through to ObjectType for fields and implemented interfaces.
112
+ case 'InterfaceType':
113
+ // If an interface if reachable, then all of its implementation are too (a field returning the interface could return any of the
114
+ // implementation at runtime typically).
115
+ type.allImplementations().forEach((t) => stack.push(t));
116
+ case 'ObjectType':
117
+ type.interfaces().forEach((t) => stack.push(t));
118
+ for (const field of type.fields()) {
119
+ const { isInSubgraph, typesInFederationDirectives } = fieldInfoInSubgraph(field, subgraphName);
120
+ if (isInSubgraph) {
121
+ field.arguments().forEach((arg) => stack.push(baseType(arg.type!)));
122
+ stack.push(baseType(field.type!));
123
+ typesInFederationDirectives.forEach((t) => stack.push(t));
124
+ }
125
+ }
126
+ break;
127
+ case 'InputObjectType':
128
+ for (const field of type.fields()) {
129
+ const { isInSubgraph, typesInFederationDirectives } = fieldInfoInSubgraph(field, subgraphName);
130
+ if (isInSubgraph) {
131
+ stack.push(baseType(field.type!));
132
+ typesInFederationDirectives.forEach((t) => stack.push(t));
133
+ }
134
+ }
135
+ break;
136
+ case 'UnionType':
137
+ type.members().forEach((m) => stack.push(m.type));
138
+ break;
139
+ }
140
+ }
141
+
142
+ for (const directive of supergraph.directives()) {
143
+ // In fed1 supergraphs, which is the only place this is called, only executable directive from subgraph only ever made
144
+ // it to the supergraph. Skipping anything else saves us from worrying about supergraph-specific directives too.
145
+ if (!directive.hasExecutableLocations()) {
146
+ continue;
147
+ }
148
+ directive.arguments().forEach((arg) => stack.push(baseType(arg.type!)));
149
+ }
150
+ }
151
+
152
+ function collectFieldReachableTypesForAllSubgraphs(
153
+ supergraph: Schema,
154
+ allSubgraphs: readonly string[],
155
+ fieldInfoInSubgraph: (f: FieldDefinition<any> | InputFieldDefinition, subgraphName: string) => { isInSubgraph: boolean, typesInFederationDirectives: NamedType[] },
156
+ typeInfoInSubgraph: (t: NamedType, subgraphName: string) => { isEntityWithKeyInSubgraph: boolean, typesInFederationDirectives: NamedType[] },
157
+ ): Map<string, Set<string>> {
158
+ const reachableTypesBySubgraphs = new Map<string, Set<string>>();
159
+ for (const subgraphName of allSubgraphs) {
160
+ const reachableTypes = new Set<string>();
161
+ collectFieldReachableTypesForSubgraph(
162
+ supergraph,
163
+ subgraphName,
164
+ (t) => reachableTypes.add(t.name),
165
+ fieldInfoInSubgraph,
166
+ typeInfoInSubgraph,
167
+ );
168
+ reachableTypesBySubgraphs.set(subgraphName, reachableTypes);
169
+ }
170
+ return reachableTypesBySubgraphs;
171
+ }
172
+
173
+ function typesUsedInFederationDirective(fieldSet: string | undefined, parentType: CompositeType): NamedType[] {
174
+ if (!fieldSet) {
175
+ return [];
176
+ }
177
+
178
+ const usedTypes: NamedType[] = [];
179
+ parseSelectionSet({
180
+ parentType,
181
+ source: fieldSet,
182
+ fieldAccessor: (type, fieldName) => {
183
+ const field = type.field(fieldName);
184
+ if (field) {
185
+ usedTypes.push(baseType(field.type!));
186
+ }
187
+ return field;
188
+ },
189
+ validate: false,
190
+ });
191
+ return usedTypes;
192
+ }
193
+
85
194
  export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
86
195
  const [coreFeatures, joinSpec] = validateSupergraph(supergraph);
87
196
  const isFed1 = joinSpec.version.equals(new FeatureVersion(0, 1));
@@ -90,6 +199,61 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
90
199
  const [subgraphs, graphEnumNameToSubgraphName] = collectEmptySubgraphs(supergraph, joinSpec);
91
200
  const typeDirective = joinSpec.typeDirective(supergraph);
92
201
  const implementsDirective = joinSpec.implementsDirective(supergraph);
202
+ const ownerDirective = joinSpec.ownerDirective(supergraph);
203
+ const fieldDirective = joinSpec.fieldDirective(supergraph);
204
+
205
+ const getSubgraph = (application: Directive<any, { graph: string }>) => graphEnumNameToSubgraphName.get(application.arguments().graph);
206
+
207
+ /*
208
+ * Fed2 supergraph have "provenance" information for all types and fields, so we can faithfully extract subgraph relatively easily.
209
+ * For fed1 supergraph however, only entity types are marked with `@join__type` and `@join__field`. Which mean that for value types,
210
+ * we cannot directly know in which subgraphs they were initially defined. One strategy consists in "extracting" value types into
211
+ * all subgraphs blindly: functionally, having some unused types in an extracted subgraph schema does not matter much. However, adding
212
+ * those useless types increases memory usage, and we've seen some case with lots of subgraphs and lots of value types where those
213
+ * unused types balloon up memory usage (from 100MB to 1GB in one example; obviously, this is made worst by the fact that javascript
214
+ * is pretty memory heavy in the first place). So to avoid that problem, for fed1 supergraph, we do a first pass where we collect
215
+ * for all the subgraphs the set of types that are actually reachable in that subgraph. As we extract do the actual type extraction,
216
+ * we use this to ignore non-reachable types for any given subgraph.
217
+ */
218
+ let includeTypeInSubgraph: (t: NamedType, name: string) => boolean = () => true;
219
+ if (isFed1) {
220
+ const reachableTypesBySubgraph = collectFieldReachableTypesForAllSubgraphs(
221
+ supergraph,
222
+ subgraphs.names(),
223
+ (f, name) => {
224
+ const fieldApplications: Directive<any, { graph: string, requires?: string, provides?: string }>[] = f.appliedDirectivesOf(fieldDirective);
225
+ if (fieldApplications.length) {
226
+ const application = fieldApplications.find((application) => getSubgraph(application) === name);
227
+ if (application) {
228
+ const args = application.arguments();
229
+ const typesInFederationDirectives =
230
+ typesUsedInFederationDirective(args.provides, baseType(f.type!) as CompositeType)
231
+ .concat(typesUsedInFederationDirective(args.requires, f.parent));
232
+ return { isInSubgraph: true, typesInFederationDirectives };
233
+ } else {
234
+ return { isInSubgraph: false, typesInFederationDirectives: [] };
235
+ }
236
+ } else {
237
+ // No field application depends on the "owner" directive on the type. If we have no owner, then the
238
+ // field is in all subgraph and we return true. Otherwise, the field is only in the owner subgraph.
239
+ // In any case, the field cannot have a requires or provides
240
+ const ownerApplications = ownerDirective ? f.parent.appliedDirectivesOf(ownerDirective) : [];
241
+ return { isInSubgraph: !ownerApplications.length || getSubgraph(ownerApplications[0]) == name, typesInFederationDirectives: [] };
242
+ }
243
+ },
244
+ (t, name) => {
245
+ const typeApplications: Directive<any, { graph: string, key?: string}>[] = t.appliedDirectivesOf(typeDirective);
246
+ const application = typeApplications.find((application) => (application.arguments().key && (getSubgraph(application) === name)));
247
+ if (application) {
248
+ const typesInFederationDirectives = typesUsedInFederationDirective(application.arguments().key, t as CompositeType);
249
+ return { isEntityWithKeyInSubgraph: true, typesInFederationDirectives };
250
+ } else {
251
+ return { isEntityWithKeyInSubgraph: false, typesInFederationDirectives: [] };
252
+ }
253
+ },
254
+ );
255
+ includeTypeInSubgraph = (t, name) => reachableTypesBySubgraph.get(name)?.has(t.name) ?? false;
256
+ }
93
257
 
94
258
  // Next, we iterate on all types and add it to the proper subgraphs (along with any @key).
95
259
  // Note that we first add all types empty and populate the types next. This avoids having to care about the iteration
@@ -97,13 +261,15 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
97
261
  for (const type of filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition)) {
98
262
  const typeApplications = type.appliedDirectivesOf(typeDirective);
99
263
  if (!typeApplications.length) {
100
- // Imply the type is in all subgraphs (technically, some subgraphs may not have had this type, but adding it
101
- // in that case is harmless because it will be unreachable anyway).
102
- subgraphs.values().map(sg => sg.schema).forEach(schema => schema.addType(newNamedType(type.kind, type.name)));
264
+ // Imply we don't know in which subgraph the type is, so we had it in all subgraph in which the type is reachable.
265
+ subgraphs
266
+ .values()
267
+ .filter((sg) => includeTypeInSubgraph(type, sg.name))
268
+ .map(sg => sg.schema).forEach(schema => schema.addType(newNamedType(type.kind, type.name)));
103
269
  } else {
104
270
  for (const application of typeApplications) {
105
271
  const args = application.arguments();
106
- const subgraphName = graphEnumNameToSubgraphName.get(args.graph)!;
272
+ const subgraphName = getSubgraph(application)!;
107
273
  const schema = subgraphs.get(subgraphName)!.schema;
108
274
  // We can have more than one type directive for a given subgraph
109
275
  let subgraphType = schema.type(type.name);
@@ -121,8 +287,6 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
121
287
  }
122
288
  }
123
289
 
124
- const ownerDirective = joinSpec.ownerDirective(supergraph);
125
- const fieldDirective = joinSpec.fieldDirective(supergraph);
126
290
  // We can now populate all those types (with relevant @provides and @requires on fields).
127
291
  for (const type of filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition)) {
128
292
  switch (type.kind) {
@@ -181,7 +345,14 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
181
345
  const args = application.arguments();
182
346
  const subgraph = subgraphs.get(graphEnumNameToSubgraphName.get(args.graph)!)!;
183
347
  const subgraphField = addSubgraphField(field, subgraph, args.type);
184
- assert(subgraphField, () => `Found join__field directive for graph ${subgraph.name} on field ${field.coordinate} but no corresponding join__type on ${type}`);
348
+ if (!subgraphField) {
349
+ // It's unlikely but possible that a fed1 supergraph has a `@provides` on a field of a value type,
350
+ // and that value type is actually unreachable. Because we trim unreachable types for fed1 supergraph
351
+ // (see comment on `includeTypeInSubgraph` above), it would mean we get `undefined` here. It's fine
352
+ // however: the type is unreachable in this subgraph, so ignoring that field application is fine too.
353
+ assert(!includeTypeInSubgraph(type, subgraph.name), () => `Found join__field directive for graph ${subgraph.name} on field ${field.coordinate} but no corresponding join__type on ${type}`);
354
+ continue;
355
+ }
185
356
  if (args.requires) {
186
357
  subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': args.requires});
187
358
  }
package/src/federation.ts CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  Directive,
8
8
  DirectiveDefinition,
9
9
  ErrGraphQLValidationFailed,
10
- errorCauses,
11
10
  FieldDefinition,
12
11
  InputFieldDefinition,
13
12
  InterfaceType,
@@ -22,6 +21,7 @@ import {
22
21
  ScalarType,
23
22
  Schema,
24
23
  SchemaBlueprint,
24
+ SchemaConfig,
25
25
  SchemaDefinition,
26
26
  SchemaElement,
27
27
  sourceASTs,
@@ -54,6 +54,7 @@ import {
54
54
  ERRORS,
55
55
  withModifiedErrorMessage,
56
56
  extractGraphQLErrorOptions,
57
+ errorCauses,
57
58
  } from "./error";
58
59
  import { computeShareables } from "./precompute";
59
60
  import {
@@ -1024,8 +1025,8 @@ export function buildSubgraph(
1024
1025
  return subgraph.validate();
1025
1026
  }
1026
1027
 
1027
- export function newEmptyFederation2Schema(): Schema {
1028
- const schema = new Schema(new FederationBlueprint(true));
1028
+ export function newEmptyFederation2Schema(config?: SchemaConfig): Schema {
1029
+ const schema = new Schema(new FederationBlueprint(true), config);
1029
1030
  setSchemaAsFed2Subgraph(schema);
1030
1031
  return schema;
1031
1032
  }
package/src/operations.ts CHANGED
@@ -831,10 +831,11 @@ class DeferNormalizer {
831
831
  const deferArgs = selection.element().deferDirectiveArgs();
832
832
  if (deferArgs) {
833
833
  hasDefers = true;
834
+ if (!deferArgs.label || deferArgs.if !== undefined) {
835
+ hasNonLabelledOrConditionalDefers = true;
836
+ }
834
837
  if (deferArgs.label) {
835
838
  this.usedLabels.add(deferArgs.label);
836
- } else {
837
- hasNonLabelledOrConditionalDefers = true;
838
839
  }
839
840
  }
840
841
  }
@@ -951,7 +952,6 @@ export class SelectionSet extends Freezable<SelectionSet> {
951
952
  if (names && names.length === 0) {
952
953
  return this;
953
954
  }
954
-
955
955
  const newFragments = updateSelectionSetFragments
956
956
  ? (names ? this.fragments?.without(names) : undefined)
957
957
  : this.fragments;
@@ -1052,7 +1052,7 @@ export class SelectionSet extends Freezable<SelectionSet> {
1052
1052
  * This is very similar to `mergeIn` except that it takes a direct array of selection, and the direct aliasing
1053
1053
  * remarks from `mergeInd` applies here too.
1054
1054
  */
1055
- addAll(selections: Selection[]): SelectionSet {
1055
+ addAll(selections: readonly Selection[]): SelectionSet {
1056
1056
  selections.forEach(s => this.add(s));
1057
1057
  return this;
1058
1058
  }
@@ -1200,9 +1200,6 @@ export class SelectionSet extends Freezable<SelectionSet> {
1200
1200
  validate(!this.isEmpty(), () => `Invalid empty selection set`);
1201
1201
  for (const selection of this.selections()) {
1202
1202
  selection.validate();
1203
- const selectionFragments = selection.namedFragments();
1204
- // We make this an assertion because this is a programming error. But validate is a convenient place for this in practice.
1205
- assert(!selectionFragments || selectionFragments === this.fragments, () => `Selection fragments (${selectionFragments}) for ${selection} does not match selection set one (${this.fragments})`);
1206
1203
  }
1207
1204
  }
1208
1205
 
@@ -1435,17 +1432,15 @@ export class FieldSelection extends Freezable<FieldSelection> {
1435
1432
  }
1436
1433
 
1437
1434
  filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
1438
- if (!predicate(this)) {
1439
- return undefined;
1440
- }
1441
1435
  if (!this.selectionSet) {
1442
- return this;
1436
+ return predicate(this) ? this : undefined;
1443
1437
  }
1444
1438
 
1445
1439
  const updatedSelectionSet = this.selectionSet.filter(predicate);
1446
- return this.selectionSet === updatedSelectionSet
1440
+ const thisWithFilteredSelectionSet = this.selectionSet === updatedSelectionSet
1447
1441
  ? this
1448
1442
  : new FieldSelection(this.field, updatedSelectionSet);
1443
+ return predicate(thisWithFilteredSelectionSet) ? thisWithFilteredSelectionSet : undefined;
1449
1444
  }
1450
1445
 
1451
1446
  protected freezeInternals(): void {
@@ -1605,6 +1600,8 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
1605
1600
 
1606
1601
  abstract withNormalizedDefer(normalizer: DeferNormalizer): FragmentSelection | SelectionSet;
1607
1602
 
1603
+ abstract updateForAddingTo(selectionSet: SelectionSet): FragmentSelection;
1604
+
1608
1605
  protected us(): FragmentSelection {
1609
1606
  return this;
1610
1607
  }
@@ -1624,40 +1621,15 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
1624
1621
  return mergeVariables(this.element().variables(), this.selectionSet.usedVariables());
1625
1622
  }
1626
1623
 
1627
- updateForAddingTo(selectionSet: SelectionSet): FragmentSelection {
1628
- const updatedFragment = this.element().updateForAddingTo(selectionSet);
1629
- if (this.element() === updatedFragment) {
1630
- return this.cloneIfFrozen();
1631
- }
1632
-
1633
- // Like for fields, we create a new selection that not only uses the updated fragment, but also ensures
1634
- // the underlying selection set uses the updated type as parent type.
1635
- const updatedCastedType = updatedFragment.castedType();
1636
- let updatedSelectionSet : SelectionSet | undefined;
1637
- if (this.selectionSet.parentType !== updatedCastedType) {
1638
- updatedSelectionSet = new SelectionSet(updatedCastedType);
1639
- // Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
1640
- // effect of the type change.
1641
- for (const selection of this.selectionSet.selections()) {
1642
- updatedSelectionSet.add(selection);
1643
- }
1644
- } else {
1645
- updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
1646
- }
1647
-
1648
- return new InlineFragmentSelection(updatedFragment, updatedSelectionSet);
1649
- }
1650
-
1651
1624
  filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
1652
- if (!predicate(this)) {
1653
- return undefined;
1654
- }
1655
1625
  // Note that we essentially expand all fragments as part of this.
1656
1626
  const selectionSet = this.selectionSet;
1657
1627
  const updatedSelectionSet = selectionSet.filter(predicate);
1658
- return updatedSelectionSet === selectionSet
1628
+ const thisWithFilteredSelectionSet = updatedSelectionSet === selectionSet
1659
1629
  ? this
1660
1630
  : new InlineFragmentSelection(this.element(), updatedSelectionSet);
1631
+
1632
+ return predicate(thisWithFilteredSelectionSet) ? thisWithFilteredSelectionSet : undefined;
1661
1633
  }
1662
1634
 
1663
1635
  protected freezeInternals() {
@@ -1713,6 +1685,31 @@ class InlineFragmentSelection extends FragmentSelection {
1713
1685
  this.selectionSet.validate();
1714
1686
  }
1715
1687
 
1688
+ updateForAddingTo(selectionSet: SelectionSet): FragmentSelection {
1689
+ const updatedFragment = this.element().updateForAddingTo(selectionSet);
1690
+ if (this.element() === updatedFragment) {
1691
+ return this.cloneIfFrozen();
1692
+ }
1693
+
1694
+ // Like for fields, we create a new selection that not only uses the updated fragment, but also ensures
1695
+ // the underlying selection set uses the updated type as parent type.
1696
+ const updatedCastedType = updatedFragment.castedType();
1697
+ let updatedSelectionSet : SelectionSet | undefined;
1698
+ if (this.selectionSet.parentType !== updatedCastedType) {
1699
+ updatedSelectionSet = new SelectionSet(updatedCastedType);
1700
+ // Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
1701
+ // effect of the type change.
1702
+ for (const selection of this.selectionSet.selections()) {
1703
+ updatedSelectionSet.add(selection);
1704
+ }
1705
+ } else {
1706
+ updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
1707
+ }
1708
+
1709
+ return new InlineFragmentSelection(updatedFragment, updatedSelectionSet);
1710
+ }
1711
+
1712
+
1716
1713
  get selectionSet(): SelectionSet {
1717
1714
  return this._selectionSet;
1718
1715
  }
@@ -1753,12 +1750,17 @@ class InlineFragmentSelection extends FragmentSelection {
1753
1750
  const spread = new FragmentSpreadSelection(this.element().parentType, fragments, candidate.name);
1754
1751
  // We use the fragment when the fragments condition is either the same, or a supertype of our current condition.
1755
1752
  // If it's the same type, then we don't really want to preserve the current condition, it is included in the
1756
- // spread and we can return it directive. But if the fragment condition is a superset, then we should preserve
1753
+ // spread and we can return it directly. But if the fragment condition is a superset, then we should preserve
1757
1754
  // our current condition since it restricts the selection more than the fragment actual does.
1758
1755
  if (sameType(typeCondition, candidate.typeCondition)) {
1756
+ // If we ignore the current condition, then we need to ensure any directive applied to it are preserved.
1757
+ this.fragmentElement.appliedDirectives.forEach((directive) => {
1758
+ spread.element().applyDirective(directive.definition!, directive.arguments());
1759
+ })
1759
1760
  return spread;
1760
1761
  }
1761
1762
  optimizedSelection = selectionSetOf(spread.element().parentType, spread);
1763
+ break;
1762
1764
  }
1763
1765
  }
1764
1766
  }
@@ -1785,7 +1787,6 @@ class InlineFragmentSelection extends FragmentSelection {
1785
1787
  if (updatedSubSelections === this.selectionSet && !hasDeferToRemove) {
1786
1788
  return this;
1787
1789
  }
1788
-
1789
1790
  const newFragment = hasDeferToRemove ? this.fragmentElement.withoutDefer() : this.fragmentElement;
1790
1791
  if (!newFragment) {
1791
1792
  return updatedSubSelections;
@@ -1877,13 +1878,23 @@ class FragmentSpreadSelection extends FragmentSelection {
1877
1878
  return this;
1878
1879
  }
1879
1880
 
1881
+ updateForAddingTo(_selectionSet: SelectionSet): FragmentSelection {
1882
+ // This is a little bit iffy, because the fragment could link to a schema (typically the supergraph API one)
1883
+ // that is different from the one of `_selectionSet` (say, a subgraph fetch selection in which we're trying to
1884
+ // reuse a user fragment). But in practice, we expand all fragments when we do query planning and only re-add
1885
+ // fragments back at the very end, so this should be fine. Importantly, we don't want this method to mistakenly
1886
+ // expand the spread, as that would compromise the code that optimize subgraph fetches to re-use named
1887
+ // fragments.
1888
+ return this;
1889
+ }
1890
+
1880
1891
  expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): FragmentSelection | readonly Selection[] {
1881
1892
  if (names && !names.includes(this.namedFragment.name)) {
1882
1893
  return this;
1883
1894
  }
1884
1895
 
1885
1896
  const expandedSubSelections = this.selectionSet.expandFragments(names, updateSelectionSetFragments);
1886
- return sameType(this._element.parentType, this.namedFragment.typeCondition)
1897
+ return sameType(this._element.parentType, this.namedFragment.typeCondition) && this._element.appliedDirectives.length === 0
1887
1898
  ? expandedSubSelections.selections()
1888
1899
  : new InlineFragmentSelection(this._element, expandedSubSelections);
1889
1900
  }
@@ -1920,9 +1931,13 @@ class FragmentSpreadSelection extends FragmentSelection {
1920
1931
  export function operationFromDocument(
1921
1932
  schema: Schema,
1922
1933
  document: DocumentNode,
1923
- operationName?: string,
1934
+ options?: {
1935
+ operationName?: string,
1936
+ validate?: boolean,
1937
+ }
1924
1938
  ) : Operation {
1925
1939
  let operation: OperationDefinitionNode | undefined;
1940
+ const operationName = options?.operationName;
1926
1941
  const fragments = new NamedFragments();
1927
1942
  // We do a first pass to collect the operation, and create all named fragment, but without their selection set yet.
1928
1943
  // This allow later to be able to access any fragment regardless of the order in which the fragments are defined.
@@ -1967,14 +1982,20 @@ export function operationFromDocument(
1967
1982
  }
1968
1983
  });
1969
1984
  fragments.validate();
1970
- return operationFromAST(schema, operation, fragments);
1985
+ return operationFromAST({schema, operation, fragments, validateInput: options?.validate});
1971
1986
  }
1972
1987
 
1973
- function operationFromAST(
1988
+ function operationFromAST({
1989
+ schema,
1990
+ operation,
1991
+ fragments,
1992
+ validateInput,
1993
+ }:{
1974
1994
  schema: Schema,
1975
1995
  operation: OperationDefinitionNode,
1976
- fragments: NamedFragments
1977
- ) : Operation {
1996
+ fragments: NamedFragments,
1997
+ validateInput?: boolean,
1998
+ }) : Operation {
1978
1999
  const rootType = schema.schemaDefinition.root(operation.operation);
1979
2000
  validate(rootType, () => `The schema has no "${operation.operation}" root type defined`);
1980
2001
  const variableDefinitions = operation.variableDefinitions ? variableDefinitionsFromAST(schema, operation.variableDefinitions) : new VariableDefinitions();
@@ -1985,14 +2006,22 @@ function operationFromAST(
1985
2006
  source: operation.selectionSet,
1986
2007
  variableDefinitions,
1987
2008
  fragments,
2009
+ validate: validateInput,
1988
2010
  }),
1989
2011
  variableDefinitions,
1990
2012
  operation.name?.value
1991
2013
  );
1992
2014
  }
1993
2015
 
1994
- export function parseOperation(schema: Schema, operation: string, operationName?: string): Operation {
1995
- return operationFromDocument(schema, parse(operation), operationName);
2016
+ export function parseOperation(
2017
+ schema: Schema,
2018
+ operation: string,
2019
+ options?: {
2020
+ operationName?: string,
2021
+ validate?: boolean,
2022
+ },
2023
+ ): Operation {
2024
+ return operationFromDocument(schema, parse(operation), options);
1996
2025
  }
1997
2026
 
1998
2027
  export function parseSelectionSet({
package/src/print.ts CHANGED
@@ -94,7 +94,7 @@ export function printSchema(schema: Schema, options: PrintOptions = defaultPrint
94
94
  return definitions.flat().join('\n\n');
95
95
  }
96
96
 
97
- function definitionAndExtensions<T extends ExtendableElement>(element: {extensions(): ReadonlySet<Extension<T>>}, options: PrintOptions): (Extension<any> | null | undefined)[] {
97
+ function definitionAndExtensions<T extends ExtendableElement>(element: {extensions(): readonly Extension<T>[]}, options: PrintOptions): (Extension<any> | null | undefined)[] {
98
98
  return options.mergeTypesAndExtensions ? [undefined] : [null, ...element.extensions()];
99
99
  }
100
100
 
@@ -102,7 +102,7 @@ function printSchemaDefinitionAndExtensions(schemaDefinition: SchemaDefinition,
102
102
  return printDefinitionAndExtensions(schemaDefinition, options, printSchemaDefinitionOrExtension);
103
103
  }
104
104
 
105
- function printDefinitionAndExtensions<T extends {extensions(): ReadonlySet<Extension<any>>}>(
105
+ function printDefinitionAndExtensions<T extends {extensions(): readonly Extension<any>[]}>(
106
106
  t: T,
107
107
  options: PrintOptions,
108
108
  printer: (t: T, options: PrintOptions, extension?: Extension<any> | null) => string | undefined
@@ -4,12 +4,11 @@ import {
4
4
  Kind,
5
5
  print as printAST,
6
6
  } from "graphql";
7
- import { ERRORS } from "./error";
7
+ import { errorCauses, ERRORS } from "./error";
8
8
  import {
9
9
  baseType,
10
10
  CompositeType,
11
11
  Directive,
12
- errorCauses,
13
12
  Extension,
14
13
  FieldDefinition,
15
14
  isCompositeType,
@@ -432,7 +431,8 @@ class SchemaUpgrader {
432
431
 
433
432
  private fixFederationDirectivesArguments() {
434
433
  for (const directive of [this.metadata.keyDirective(), this.metadata.requiresDirective(), this.metadata.providesDirective()]) {
435
- for (const application of directive.applications()) {
434
+ // Note that we may remove (to replace) some of the application we iterate on, so we need to copy the list we iterate on first.
435
+ for (const application of Array.from(directive.applications())) {
436
436
  const fields = application.arguments().fields;
437
437
  if (typeof fields !== 'string') {
438
438
  // The one case we have seen in practice is user passing an array of string, so we handle that. If it's something else,
@@ -697,7 +697,8 @@ class SchemaUpgrader {
697
697
  return;
698
698
  }
699
699
 
700
- for (const application of tagDirective.applications()) {
700
+ // Copying the list we iterate on as we remove in the loop.
701
+ for (const application of Array.from(tagDirective.applications())) {
701
702
  const element = application.parent;
702
703
  if (!(element instanceof FieldDefinition)) {
703
704
  continue;