@apollo/federation-internals 2.1.2-alpha.0 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollo/federation-internals",
3
- "version": "2.1.2-alpha.0",
3
+ "version": "2.1.2-alpha.2",
4
4
  "description": "Apollo Federation internal utilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,5 +32,5 @@
32
32
  "peerDependencies": {
33
33
  "graphql": "^16.5.0"
34
34
  },
35
- "gitHead": "4ee2da1aaa93f08f1b9ff173b6ab9b895e3a53fc"
35
+ "gitHead": "c7bb87702c5913f5a0ecc5c2a28d2f7228e2988c"
36
36
  }
@@ -60,22 +60,6 @@ describe('fieldset-based directives', () => {
60
60
  ]);
61
61
  });
62
62
 
63
- it('rejects field defined with arguments in @requires', () => {
64
- const subgraph = gql`
65
- type Query {
66
- t: T
67
- }
68
-
69
- type T {
70
- f(x: Int): Int @external
71
- g: Int @requires(fields: "f")
72
- }
73
- `
74
- expect(buildForErrors(subgraph)).toStrictEqual([
75
- ['REQUIRES_FIELDS_HAS_ARGS', '[S] On field "T.g", for @requires(fields: "f"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @requires)']
76
- ]);
77
- });
78
-
79
63
  it('rejects @provides on non-external fields', () => {
80
64
  const subgraph = gql`
81
65
  type Query {
@@ -459,6 +443,61 @@ describe('fieldset-based directives', () => {
459
443
  ['PROVIDES_FIELDS_MISSING_EXTERNAL', '[S] On field "Query.t", for @provides(fields: "f(x: 3)"): field "T.f" should not be part of a @provides since it is already provided by this subgraph (it is not marked @external)'],
460
444
  ]);
461
445
  });
446
+
447
+ it('rejects aliases in @key', () => {
448
+ const subgraph = gql`
449
+ type Query {
450
+ t: T
451
+ }
452
+
453
+ type T @key(fields: "foo: id") {
454
+ id: ID!
455
+ }
456
+ `
457
+ expect(buildForErrors(subgraph)).toStrictEqual([
458
+ [ 'KEY_INVALID_FIELDS', '[S] On type "T", for @key(fields: "foo: id"): Cannot use alias "foo" in "foo: id": aliases are not currently supported in @key' ],
459
+ ]);
460
+ });
461
+
462
+ it('rejects aliases in @provides', () => {
463
+ const subgraph = gql`
464
+ type Query {
465
+ t: T @provides(fields: "bar: x")
466
+ }
467
+
468
+ type T @key(fields: "id") {
469
+ id: ID!
470
+ x: Int @external
471
+ }
472
+ `
473
+ expect(buildForErrors(subgraph)).toStrictEqual([
474
+ [ 'PROVIDES_INVALID_FIELDS', '[S] On field "Query.t", for @provides(fields: "bar: x"): Cannot use alias "bar" in "bar: x": aliases are not currently supported in @provides' ],
475
+ ]);
476
+ });
477
+
478
+ it('rejects aliases in @requires', () => {
479
+ const subgraph = gql`
480
+ type Query {
481
+ t: T
482
+ }
483
+
484
+ type T {
485
+ x: X @external
486
+ y: Int @external
487
+ g: Int @requires(fields: "foo: y")
488
+ h: Int @requires(fields: "x { m: a n: b }")
489
+ }
490
+
491
+ type X {
492
+ a: Int
493
+ b: Int
494
+ }
495
+ `
496
+ expect(buildForErrors(subgraph)).toStrictEqual([
497
+ [ 'REQUIRES_INVALID_FIELDS', '[S] On field "T.g", for @requires(fields: "foo: y"): Cannot use alias "foo" in "foo: y": aliases are not currently supported in @requires' ],
498
+ [ 'REQUIRES_INVALID_FIELDS', '[S] On field "T.h", for @requires(fields: "x { m: a n: b }"): Cannot use alias "m" in "m: a": aliases are not currently supported in @requires' ],
499
+ ]);
500
+ });
462
501
  });
463
502
 
464
503
  describe('root types', () => {
package/src/error.ts CHANGED
@@ -223,7 +223,6 @@ const FIELDS_HAS_ARGS = makeFederationDirectiveErrorCodeCategory(
223
223
 
224
224
  const KEY_FIELDS_HAS_ARGS = FIELDS_HAS_ARGS.createCode('key');
225
225
  const PROVIDES_FIELDS_HAS_ARGS = FIELDS_HAS_ARGS.createCode('provides');
226
- const REQUIRES_FIELDS_HAS_ARGS = FIELDS_HAS_ARGS.createCode('requires');
227
226
 
228
227
  const DIRECTIVE_FIELDS_MISSING_EXTERNAL = makeFederationDirectiveErrorCodeCategory(
229
228
  'FIELDS_MISSING_EXTERNAL',
@@ -545,7 +544,6 @@ export const ERRORS = {
545
544
  UNKNOWN_LINK_VERSION,
546
545
  KEY_FIELDS_HAS_ARGS,
547
546
  PROVIDES_FIELDS_HAS_ARGS,
548
- REQUIRES_FIELDS_HAS_ARGS,
549
547
  PROVIDES_MISSING_EXTERNAL,
550
548
  REQUIRES_MISSING_EXTERNAL,
551
549
  KEY_UNSUPPORTED_ON_INTERFACE,
@@ -644,4 +642,5 @@ export const REMOVED_ERRORS = [
644
642
  ['RESERVED_FIELD_USED', 'This error was previously not correctly enforced: the _service and _entities, if present, were overridden; this is still the case'],
645
643
 
646
644
  ['NON_REPEATABLE_DIRECTIVE_ARGUMENTS_MISMATCH', 'Since federation 2.1.0, the case this error used to cover is now a warning (with code `INCONSISTENT_NON_REPEATABLE_DIRECTIVE_ARGUMENTS`) instead of an error'],
645
+ ['REQUIRES_FIELDS_HAS_ARGS', 'Since federation 2.1.1, using fields with arguments in a @requires is fully supported'],
647
646
  ];
package/src/federation.ts CHANGED
@@ -116,14 +116,23 @@ const FEDERATION_SPECIFIC_VALIDATION_RULES = [
116
116
  const FEDERATION_VALIDATION_RULES = specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES);
117
117
 
118
118
 
119
- function validateFieldSetSelections(
119
+ function validateFieldSetSelections({
120
+ directiveName,
121
+ selectionSet,
122
+ hasExternalInParents,
123
+ metadata,
124
+ onError,
125
+ allowOnNonExternalLeafFields,
126
+ allowFieldsWithArguments,
127
+ }: {
120
128
  directiveName: string,
121
129
  selectionSet: SelectionSet,
122
130
  hasExternalInParents: boolean,
123
- federationMetadata: FederationMetadata,
131
+ metadata: FederationMetadata,
124
132
  onError: (error: GraphQLError) => void,
125
133
  allowOnNonExternalLeafFields: boolean,
126
- ): void {
134
+ allowFieldsWithArguments: boolean,
135
+ }): void {
127
136
  for (const selection of selectionSet.selections()) {
128
137
  const appliedDirectives = selection.element().appliedDirectives;
129
138
  if (appliedDirectives.length > 0) {
@@ -134,8 +143,8 @@ function validateFieldSetSelections(
134
143
 
135
144
  if (selection.kind === 'FieldSelection') {
136
145
  const field = selection.element().definition;
137
- const isExternal = federationMetadata.isFieldExternal(field);
138
- if (field.hasArguments()) {
146
+ const isExternal = metadata.isFieldExternal(field);
147
+ if (!allowFieldsWithArguments && field.hasArguments()) {
139
148
  onError(ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err(
140
149
  `field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`,
141
150
  { nodes: field.sourceAST },
@@ -145,7 +154,7 @@ function validateFieldSetSelections(
145
154
  const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents;
146
155
  if (!isExternal && mustBeExternal) {
147
156
  const errorCode = ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName);
148
- if (federationMetadata.isFieldFakeExternal(field)) {
157
+ if (metadata.isFieldFakeExternal(field)) {
149
158
  onError(errorCode.err(
150
159
  `field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
151
160
  + `(while it is marked @${externalDirectiveSpec.name}, it is a @${keyDirectiveSpec.name} field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`,
@@ -167,28 +176,53 @@ function validateFieldSetSelections(
167
176
  if (!newHasExternalInParents && isInterfaceType(parentType)) {
168
177
  for (const implem of parentType.possibleRuntimeTypes()) {
169
178
  const fieldInImplem = implem.field(field.name);
170
- if (fieldInImplem && federationMetadata.isFieldExternal(fieldInImplem)) {
179
+ if (fieldInImplem && metadata.isFieldExternal(fieldInImplem)) {
171
180
  newHasExternalInParents = true;
172
181
  break;
173
182
  }
174
183
  }
175
184
  }
176
- validateFieldSetSelections(directiveName, selection.selectionSet, newHasExternalInParents, federationMetadata, onError, allowOnNonExternalLeafFields);
185
+ validateFieldSetSelections({
186
+ directiveName,
187
+ selectionSet: selection.selectionSet,
188
+ hasExternalInParents: newHasExternalInParents,
189
+ metadata,
190
+ onError,
191
+ allowOnNonExternalLeafFields,
192
+ allowFieldsWithArguments,
193
+ });
177
194
  }
178
195
  } else {
179
- validateFieldSetSelections(directiveName, selection.selectionSet, hasExternalInParents, federationMetadata, onError, allowOnNonExternalLeafFields);
196
+ validateFieldSetSelections({
197
+ directiveName,
198
+ selectionSet: selection.selectionSet,
199
+ hasExternalInParents,
200
+ metadata,
201
+ onError,
202
+ allowOnNonExternalLeafFields,
203
+ allowFieldsWithArguments,
204
+ });
180
205
  }
181
206
  }
182
207
  }
183
208
 
184
- function validateFieldSet(
209
+ function validateFieldSet({
210
+ type,
211
+ directive,
212
+ metadata,
213
+ errorCollector,
214
+ allowOnNonExternalLeafFields,
215
+ allowFieldsWithArguments,
216
+ onFields,
217
+ }: {
185
218
  type: CompositeType,
186
219
  directive: Directive<any, {fields: any}>,
187
- federationMetadata: FederationMetadata,
220
+ metadata: FederationMetadata,
188
221
  errorCollector: GraphQLError[],
189
222
  allowOnNonExternalLeafFields: boolean,
223
+ allowFieldsWithArguments: boolean,
190
224
  onFields?: (field: FieldDefinition<any>) => void,
191
- ): void {
225
+ }): void {
192
226
  try {
193
227
  // Note that `parseFieldSetArgument` already properly format the error, hence the separate try-catch.
194
228
  // TODO: `parseFieldSetArgument` throws on the first issue found and never accumulate multiple
@@ -206,14 +240,15 @@ function validateFieldSet(
206
240
  }
207
241
  : undefined;
208
242
  const selectionSet = parseFieldSetArgument({parentType: type, directive, fieldAccessor});
209
- validateFieldSetSelections(
210
- directive.name,
243
+ validateFieldSetSelections({
244
+ directiveName: directive.name,
211
245
  selectionSet,
212
- false,
213
- federationMetadata,
214
- (error) => errorCollector.push(handleFieldSetValidationError(directive, error)),
246
+ hasExternalInParents: false,
247
+ metadata,
248
+ onError: (error) => errorCollector.push(handleFieldSetValidationError(directive, error)),
215
249
  allowOnNonExternalLeafFields,
216
- );
250
+ allowFieldsWithArguments,
251
+ });
217
252
  } catch (e) {
218
253
  if (e instanceof GraphQLError) {
219
254
  errorCollector.push(e);
@@ -267,15 +302,25 @@ function fieldSetTargetDescription(directive: Directive<any, {fields: any}>): st
267
302
  return `${targetKind} "${directive.parent?.coordinate}"`;
268
303
  }
269
304
 
270
- function validateAllFieldSet<TParent extends SchemaElement<any, any>>(
305
+ function validateAllFieldSet<TParent extends SchemaElement<any, any>>({
306
+ definition,
307
+ targetTypeExtractor,
308
+ errorCollector,
309
+ metadata,
310
+ isOnParentType = false,
311
+ allowOnNonExternalLeafFields = false,
312
+ allowFieldsWithArguments = false,
313
+ onFields,
314
+ }: {
271
315
  definition: DirectiveDefinition<{fields: any}>,
272
316
  targetTypeExtractor: (element: TParent) => CompositeType,
273
317
  errorCollector: GraphQLError[],
274
- federationMetadata: FederationMetadata,
275
- isOnParentType: boolean,
276
- allowOnNonExternalLeafFields: boolean,
318
+ metadata: FederationMetadata,
319
+ isOnParentType?: boolean,
320
+ allowOnNonExternalLeafFields?: boolean,
321
+ allowFieldsWithArguments?: boolean,
277
322
  onFields?: (field: FieldDefinition<any>) => void,
278
- ): void {
323
+ }): void {
279
324
  for (const application of definition.applications()) {
280
325
  const elt = application.parent as TParent;
281
326
  const type = targetTypeExtractor(elt);
@@ -289,14 +334,15 @@ function validateAllFieldSet<TParent extends SchemaElement<any, any>>(
289
334
  { nodes: sourceASTs(application).concat(isOnParentType ? [] : sourceASTs(type)) },
290
335
  ));
291
336
  }
292
- validateFieldSet(
337
+ validateFieldSet({
293
338
  type,
294
- application,
295
- federationMetadata,
339
+ directive: application,
340
+ metadata,
296
341
  errorCollector,
297
342
  allowOnNonExternalLeafFields,
343
+ allowFieldsWithArguments,
298
344
  onFields,
299
- );
345
+ });
300
346
  }
301
347
  }
302
348
 
@@ -717,7 +763,7 @@ export class FederationBlueprint extends SchemaBlueprint {
717
763
  }
718
764
 
719
765
  onValidation(schema: Schema): GraphQLError[] {
720
- const errors = super.onValidation(schema);
766
+ const errorCollector = super.onValidation(schema);
721
767
 
722
768
  // We rename all root type to their default names (we do here rather than in `prepareValidation` because
723
769
  // that can actually fail).
@@ -730,7 +776,7 @@ export class FederationBlueprint extends SchemaBlueprint {
730
776
  // composition error.
731
777
  const existing = schema.type(defaultName);
732
778
  if (existing) {
733
- errors.push(ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err(
779
+ errorCollector.push(ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err(
734
780
  `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): `
735
781
  + 'this is not supported by federation. '
736
782
  + 'If a root type does not use its default name, there should be no other type with that default name.',
@@ -748,19 +794,19 @@ export class FederationBlueprint extends SchemaBlueprint {
748
794
  // accepted, and some of those issues are fixed by `SchemaUpgrader`. So insofar as any fed 1 scheam is ultimately converted
749
795
  // to a fed 2 one before composition, then skipping some validation on fed 1 schema is fine.
750
796
  if (!metadata.isFed2Schema()) {
751
- return errors;
797
+ return errorCollector;
752
798
  }
753
799
 
754
800
  // We validate the @key, @requires and @provides.
755
801
  const keyDirective = metadata.keyDirective();
756
- validateAllFieldSet<CompositeType>(
757
- keyDirective,
758
- type => type,
759
- errors,
802
+ validateAllFieldSet<CompositeType>({
803
+ definition: keyDirective,
804
+ targetTypeExtractor: type => type,
805
+ errorCollector,
760
806
  metadata,
761
- true,
762
- true,
763
- field => {
807
+ isOnParentType: true,
808
+ allowOnNonExternalLeafFields: true,
809
+ onFields: field => {
764
810
  const type = baseType(field.type!);
765
811
  if (isUnionType(type) || isInterfaceType(type)) {
766
812
  let kind: string = type.kind;
@@ -770,7 +816,7 @@ export class FederationBlueprint extends SchemaBlueprint {
770
816
  );
771
817
  }
772
818
  }
773
- );
819
+ });
774
820
  // Note that we currently reject @requires where a leaf field of the selection is not external,
775
821
  // because if it's provided by the current subgraph, why "requires" it? That said, it's not 100%
776
822
  // nonsensical if you wanted a local field to be part of the subgraph fetch even if it's not
@@ -778,21 +824,20 @@ export class FederationBlueprint extends SchemaBlueprint {
778
824
  // rejecting it as it also make it less likely user misunderstand what @requires actually do.
779
825
  // But we could consider lifting that limitation if users comes with a good rational for allowing
780
826
  // it.
781
- validateAllFieldSet<FieldDefinition<CompositeType>>(
782
- metadata.requiresDirective(),
783
- field => field.parent,
784
- errors,
827
+ validateAllFieldSet<FieldDefinition<CompositeType>>({
828
+ definition: metadata.requiresDirective(),
829
+ targetTypeExtractor: field => field.parent,
830
+ errorCollector,
785
831
  metadata,
786
- false,
787
- false,
788
- );
832
+ allowFieldsWithArguments: true,
833
+ });
789
834
  // Note that like for @requires above, we error out if a leaf field of the selection is not
790
835
  // external in a @provides (we pass `false` for the `allowOnNonExternalLeafFields` parameter),
791
836
  // but contrarily to @requires, there is probably no reason to ever change this, as a @provides
792
837
  // of a field already provides is 100% nonsensical.
793
- validateAllFieldSet<FieldDefinition<CompositeType>>(
794
- metadata.providesDirective(),
795
- field => {
838
+ validateAllFieldSet<FieldDefinition<CompositeType>>({
839
+ definition: metadata.providesDirective(),
840
+ targetTypeExtractor: field => {
796
841
  if (metadata.isFieldExternal(field)) {
797
842
  throw ERRORS.EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE.err(
798
843
  `Cannot have both @provides and @external on field "${field.coordinate}"`,
@@ -808,29 +853,27 @@ export class FederationBlueprint extends SchemaBlueprint {
808
853
  }
809
854
  return type;
810
855
  },
811
- errors,
856
+ errorCollector,
812
857
  metadata,
813
- false,
814
- false,
815
- );
858
+ });
816
859
 
817
- validateNoExternalOnInterfaceFields(metadata, errors);
818
- validateAllExternalFieldsUsed(metadata, errors);
860
+ validateNoExternalOnInterfaceFields(metadata, errorCollector);
861
+ validateAllExternalFieldsUsed(metadata, errorCollector);
819
862
 
820
863
  // If tag is redefined by the user, make sure the definition is compatible with what we expect
821
864
  const tagDirective = metadata.tagDirective();
822
865
  if (tagDirective) {
823
866
  const error = tagSpec.checkCompatibleDirective(tagDirective);
824
867
  if (error) {
825
- errors.push(error);
868
+ errorCollector.push(error);
826
869
  }
827
870
  }
828
871
 
829
872
  for (const itf of schema.interfaceTypes()) {
830
- validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errors);
873
+ validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errorCollector);
831
874
  }
832
875
 
833
- return errors;
876
+ return errorCollector;
834
877
  }
835
878
 
836
879
  validationRules(): readonly SDLValidationRule[] {
@@ -1144,21 +1187,32 @@ export function parseFieldSetArgument({
1144
1187
  directive,
1145
1188
  fieldAccessor,
1146
1189
  validate,
1190
+ decorateValidationErrors = true,
1147
1191
  }: {
1148
1192
  parentType: CompositeType,
1149
- directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>,
1193
+ directive: Directive<SchemaElement<any, any>, {fields: any}>,
1150
1194
  fieldAccessor?: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined,
1151
1195
  validate?: boolean,
1196
+ decorateValidationErrors?: boolean,
1152
1197
  }): SelectionSet {
1153
1198
  try {
1154
- return parseSelectionSet({
1199
+ const selectionSet = parseSelectionSet({
1155
1200
  parentType,
1156
1201
  source: validateFieldSetValue(directive),
1157
1202
  fieldAccessor,
1158
1203
  validate,
1159
1204
  });
1205
+ if (validate ?? true) {
1206
+ selectionSet.forEachElement((elt) => {
1207
+ if (elt.kind === 'Field' && elt.alias) {
1208
+ // Note that this will be caught by the surrounding catch and "decorated".
1209
+ throw new GraphQLError(`Cannot use alias "${elt.alias}" in "${elt}": aliases are not currently supported in @${directive.name}`);
1210
+ }
1211
+ });
1212
+ }
1213
+ return selectionSet;
1160
1214
  } catch (e) {
1161
- if (!(e instanceof GraphQLError)) {
1215
+ if (!(e instanceof GraphQLError) || !decorateValidationErrors) {
1162
1216
  throw e;
1163
1217
  }
1164
1218
 
@@ -1226,7 +1280,7 @@ export function collectTargetFields({
1226
1280
  return fields;
1227
1281
  }
1228
1282
 
1229
- function validateFieldSetValue(directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>): string {
1283
+ function validateFieldSetValue(directive: Directive<SchemaElement<any, any>, {fields: any}>): string {
1230
1284
  const fields = directive.arguments().fields;
1231
1285
  const nodes = directive.sourceAST;
1232
1286
  if (typeof fields !== 'string') {
@@ -1501,6 +1555,13 @@ export class Subgraph {
1501
1555
  export type SubgraphASTNode = ASTNode & { subgraph: string };
1502
1556
 
1503
1557
  export function addSubgraphToASTNode(node: ASTNode, subgraph: string): SubgraphASTNode {
1558
+ // We won't override a existing subgraph info: it's not like the subgraph an ASTNode can come
1559
+ // from can ever change and this allow the provided to act as a "default" rather than a
1560
+ // hard setter, which is convenient in `addSubgraphToError` below if some of the AST of
1561
+ // the provided error already have a subgraph "origin".
1562
+ if ('subgraph' in (node as any)) {
1563
+ return node as SubgraphASTNode;
1564
+ }
1504
1565
  return {
1505
1566
  ...node,
1506
1567
  subgraph
package/src/operations.ts CHANGED
@@ -62,6 +62,8 @@ function haveSameDirectives<TElement extends OperationElement>(op1: TElement, op
62
62
  }
63
63
 
64
64
  abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> extends DirectiveTargetElement<T> {
65
+ private attachements?: Map<string, string>;
66
+
65
67
  constructor(
66
68
  schema: Schema,
67
69
  private readonly variablesInElement: Variables
@@ -74,6 +76,25 @@ abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> e
74
76
  }
75
77
 
76
78
  abstract updateForAddingTo(selection: SelectionSet): T;
79
+
80
+ addAttachement(key: string, value: string) {
81
+ if (!this.attachements) {
82
+ this.attachements = new Map();
83
+ }
84
+ this.attachements.set(key, value);
85
+ }
86
+
87
+ getAttachement(key: string): string | undefined {
88
+ return this.attachements?.get(key);
89
+ }
90
+
91
+ protected copyAttachementsTo(elt: AbstractOperationElement<any>) {
92
+ if (this.attachements) {
93
+ for (const [k, v] of this.attachements.entries()) {
94
+ elt.addAttachement(k, v);
95
+ }
96
+ }
97
+ }
77
98
  }
78
99
 
79
100
  export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> extends AbstractOperationElement<Field<TArgs>> {
@@ -86,7 +107,6 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
86
107
  readonly alias?: string
87
108
  ) {
88
109
  super(definition.schema(), variablesInArguments(args));
89
- this.validate();
90
110
  }
91
111
 
92
112
  get name(): string {
@@ -106,6 +126,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
106
126
  for (const directive of this.appliedDirectives) {
107
127
  newField.applyDirective(directive.definition!, directive.arguments());
108
128
  }
129
+ this.copyAttachementsTo(newField);
109
130
  return newField;
110
131
  }
111
132
 
@@ -152,7 +173,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
152
173
  return true;
153
174
  }
154
175
 
155
- private validate() {
176
+ validate() {
156
177
  validate(this.name === this.definition.name, () => `Field name "${this.name}" cannot select field "${this.definition.coordinate}: name mismatch"`);
157
178
 
158
179
  // We need to make sure the field has valid values for every non-optional argument.
@@ -161,7 +182,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
161
182
  if (appliedValue === undefined) {
162
183
  validate(
163
184
  argDef.defaultValue !== undefined || isNullableType(argDef.type!),
164
- () => `Missing mandatory value "${argDef.name}" in field selection "${this}"`);
185
+ () => `Missing mandatory value for argument "${argDef.name}" of field "${this.definition.coordinate}" in selection "${this}"`);
165
186
  } else {
166
187
  validate(
167
188
  isValidValue(appliedValue, argDef, this.variableDefinitions),
@@ -276,6 +297,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
276
297
  for (const directive of this.appliedDirectives) {
277
298
  newFragment.applyDirective(directive.definition!, directive.arguments());
278
299
  }
300
+ this.copyAttachementsTo(newFragment);
279
301
  return newFragment;
280
302
  }
281
303
 
@@ -326,6 +348,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
326
348
  }
327
349
 
328
350
  const updated = new FragmentElement(this.sourceType, this.typeCondition);
351
+ this.copyAttachementsTo(updated);
329
352
  updatedDirectives.forEach((d) => updated.applyDirective(d.definition!, d.arguments()));
330
353
  return updated;
331
354
  }
@@ -386,6 +409,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
386
409
  }
387
410
 
388
411
  const updated = new FragmentElement(this.sourceType, this.typeCondition);
412
+ this.copyAttachementsTo(updated);
389
413
  const deferDirective = this.schema().deferDirective();
390
414
  // Re-apply all the non-defer directives
391
415
  this.appliedDirectives.filter((d) => d.name !== deferDirective.name).forEach((d) => updated.applyDirective(d.definition!, d.arguments()));
@@ -897,7 +921,7 @@ export class SelectionSet extends Freezable<SelectionSet> {
897
921
  this._cachedSelections = selections;
898
922
  }
899
923
  assert(this._cachedSelections, 'Cache should have been populated');
900
- if (reversedOrder) {
924
+ if (reversedOrder && this._cachedSelections.length > 1) {
901
925
  const reversed = new Array(this._selectionCount);
902
926
  for (let i = 0; i < this._selectionCount; i++) {
903
927
  reversed[i] = this._cachedSelections[this._selectionCount - i - 1];
@@ -1238,8 +1262,10 @@ export class SelectionSet extends Freezable<SelectionSet> {
1238
1262
  // If __typename is selected however, we put it first. It's a detail but as __typename is a bit special it looks better,
1239
1263
  // and it happens to mimic prior behavior on the query plan side so it saves us from changing tests for no good reasons.
1240
1264
  const typenameSelection = this._selections.get(typenameFieldName);
1265
+ const isNonAliasedTypenameSelection =
1266
+ (s: Selection) => s.kind === 'FieldSelection' && !s.field.alias && s.field.name === typenameFieldName;
1241
1267
  if (typenameSelection) {
1242
- return typenameSelection.concat(this.selections().filter(s => s.kind != 'FieldSelection' || s.field.name !== typenameFieldName));
1268
+ return typenameSelection.concat(this.selections().filter(s => !isNonAliasedTypenameSelection(s)));
1243
1269
  } else {
1244
1270
  return this.selections();
1245
1271
  }
@@ -1258,10 +1284,29 @@ export class SelectionSet extends Freezable<SelectionSet> {
1258
1284
  });
1259
1285
  }
1260
1286
 
1287
+ /**
1288
+ * Calls the provided callback on all the "elements" (including nested ones) of this selection set.
1289
+ * The specific order of traversal should not be relied on.
1290
+ */
1291
+ forEachElement(callback: (elt: OperationElement) => void) {
1292
+ const stack = this.selections().concat();
1293
+ while (stack.length > 0) {
1294
+ const selection = stack.pop()!;
1295
+ callback(selection.element());
1296
+ // Note: we reserve to preserver ordering (since the stack re-reverse). Not a big cost in general
1297
+ // and make output a bit more intuitive.
1298
+ selection.selectionSet?.selections(true).forEach((s) => stack.push(s));
1299
+ }
1300
+ }
1301
+
1261
1302
  clone(): SelectionSet {
1262
1303
  const cloned = new SelectionSet(this.parentType);
1263
1304
  for (const selection of this.selections()) {
1264
- cloned.add(selection.clone());
1305
+ const clonedSelection = selection.clone();
1306
+ // Note: while we could used cloned.add() directly, this does some checks (in `updatedForAddingTo` in particular)
1307
+ // which we can skip when we clone (since we know the inputs have already gone through that).
1308
+ cloned._selections.add(clonedSelection.key(), clonedSelection);
1309
+ ++cloned._selectionCount;
1265
1310
  }
1266
1311
  return cloned;
1267
1312
  }
@@ -1470,6 +1515,7 @@ export class FieldSelection extends Freezable<FieldSelection> {
1470
1515
  }
1471
1516
 
1472
1517
  validate() {
1518
+ this.field.validate();
1473
1519
  // Note that validation is kind of redundant since `this.selectionSet.validate()` will check that it isn't empty. But doing it
1474
1520
  // allow to provide much better error messages.
1475
1521
  validate(
@@ -1520,6 +1566,10 @@ export class FieldSelection extends Freezable<FieldSelection> {
1520
1566
  };
1521
1567
  }
1522
1568
 
1569
+ withUpdatedSubSelection(newSubSelection: SelectionSet | undefined): FieldSelection {
1570
+ return new FieldSelection(this.field, newSubSelection);
1571
+ }
1572
+
1523
1573
  equals(that: Selection): boolean {
1524
1574
  if (this === that) {
1525
1575
  return true;
@@ -1602,6 +1652,8 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
1602
1652
 
1603
1653
  abstract updateForAddingTo(selectionSet: SelectionSet): FragmentSelection;
1604
1654
 
1655
+ abstract withUpdatedSubSelection(newSubSelection: SelectionSet | undefined): FragmentSelection;
1656
+
1605
1657
  protected us(): FragmentSelection {
1606
1658
  return this;
1607
1659
  }
@@ -1805,6 +1857,10 @@ class InlineFragmentSelection extends FragmentSelection {
1805
1857
  : new InlineFragmentSelection(newFragment, updatedSubSelections);
1806
1858
  }
1807
1859
 
1860
+ withUpdatedSubSelection(newSubSelection: SelectionSet | undefined): InlineFragmentSelection {
1861
+ return new InlineFragmentSelection(this.fragmentElement, newSubSelection);
1862
+ }
1863
+
1808
1864
  toString(expandFragments: boolean = true, indent?: string): string {
1809
1865
  return (indent ?? '') + this.fragmentElement + ' ' + this.selectionSet.toString(expandFragments, true, indent);
1810
1866
  }
@@ -1917,6 +1973,10 @@ class FragmentSpreadSelection extends FragmentSelection {
1917
1973
  return this._element.appliedDirectives.slice(this.namedFragment.appliedDirectives.length);
1918
1974
  }
1919
1975
 
1976
+ withUpdatedSubSelection(_: SelectionSet | undefined): InlineFragmentSelection {
1977
+ assert(false, `Unssupported`);
1978
+ }
1979
+
1920
1980
  toString(expandFragments: boolean = true, indent?: string): string {
1921
1981
  if (expandFragments) {
1922
1982
  return (indent ?? '') + this._element + ' ' + this.selectionSet.toString(true, true, indent);