@apollo/federation-internals 2.9.1 → 2.9.3

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.9.1",
3
+ "version": "2.9.3",
4
4
  "description": "Apollo Federation internal utilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/error.ts CHANGED
@@ -710,6 +710,36 @@ const CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS = makeCodeDefinition(
710
710
  { addedIn: '2.7.0' },
711
711
  );
712
712
 
713
+ const COST_APPLIED_TO_INTERFACE_FIELD = makeCodeDefinition(
714
+ 'COST_APPLIED_TO_INTERFACE_FIELD',
715
+ 'The `@cost` directive must be applied to concrete types',
716
+ { addedIn: '2.9.2' },
717
+ );
718
+
719
+ const LIST_SIZE_APPLIED_TO_NON_LIST = makeCodeDefinition(
720
+ 'LIST_SIZE_APPLIED_TO_NON_LIST',
721
+ 'The `@listSize` directive must be applied to list types',
722
+ { addedIn: '2.9.2' },
723
+ );
724
+
725
+ const LIST_SIZE_INVALID_ASSUMED_SIZE = makeCodeDefinition(
726
+ 'LIST_SIZE_INVALID_ASSUMED_SIZE',
727
+ 'The `@listSize` directive assumed size cannot be negative',
728
+ { addedIn: '2.9.2' },
729
+ );
730
+
731
+ const LIST_SIZE_INVALID_SLICING_ARGUMENT = makeCodeDefinition(
732
+ 'LIST_SIZE_INVALID_SLICING_ARGUMENT',
733
+ 'The `@listSize` directive must have existing integer slicing arguments',
734
+ { addedIn: '2.9.2' },
735
+ );
736
+
737
+ const LIST_SIZE_INVALID_SIZED_FIELD = makeCodeDefinition(
738
+ 'LIST_SIZE_INVALID_SIZED_FIELD',
739
+ 'The `@listSize` directive must reference existing list fields as sized fields',
740
+ { addedIn: '2.9.2' },
741
+ );
742
+
713
743
  export const ERROR_CATEGORIES = {
714
744
  DIRECTIVE_FIELDS_MISSING_EXTERNAL,
715
745
  DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
@@ -824,6 +854,12 @@ export const ERRORS = {
824
854
  SOURCE_FIELD_SELECTION_INVALID,
825
855
  SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD,
826
856
  CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS,
857
+ // Errors related to demand control
858
+ COST_APPLIED_TO_INTERFACE_FIELD,
859
+ LIST_SIZE_APPLIED_TO_NON_LIST,
860
+ LIST_SIZE_INVALID_ASSUMED_SIZE,
861
+ LIST_SIZE_INVALID_SIZED_FIELD,
862
+ LIST_SIZE_INVALID_SLICING_ARGUMENT,
827
863
  };
828
864
 
829
865
  const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
package/src/federation.ts CHANGED
@@ -36,6 +36,8 @@ import {
36
36
  isListType,
37
37
  isWrapperType,
38
38
  possibleRuntimeTypes,
39
+ isIntType,
40
+ Type,
39
41
  } from "./definitions";
40
42
  import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues, assertUnreachable } from "./utils";
41
43
  import { SDLValidationRule } from "graphql/validation/ValidationContext";
@@ -100,7 +102,7 @@ import {
100
102
  SourceFieldDirectiveArgs,
101
103
  SourceTypeDirectiveArgs,
102
104
  } from "./specs/sourceSpec";
103
- import { CostDirectiveArguments, ListSizeDirectiveArguments } from "./specs/costSpec";
105
+ import { COST_VERSIONS, CostDirectiveArguments, ListSizeDirectiveArguments, costIdentity } from "./specs/costSpec";
104
106
 
105
107
  const linkSpec = LINK_VERSIONS.latest();
106
108
  const tagSpec = TAG_VERSIONS.latest();
@@ -1055,6 +1057,123 @@ function validateShareableNotRepeatedOnSameDeclaration(
1055
1057
  }
1056
1058
  }
1057
1059
  }
1060
+
1061
+ function validateCostNotAppliedToInterface(application: Directive<SchemaElement<any, any>, CostDirectiveArguments>, errorCollector: GraphQLError[]) {
1062
+ const parent = application.parent;
1063
+ // @cost cannot be used on interfaces https://ibm.github.io/graphql-specs/cost-spec.html#sec-No-Cost-on-Interface-Fields
1064
+ if (parent instanceof FieldDefinition && parent.parent instanceof InterfaceType) {
1065
+ errorCollector.push(ERRORS.COST_APPLIED_TO_INTERFACE_FIELD.err(
1066
+ `@cost cannot be applied to interface "${parent.coordinate}"`,
1067
+ { nodes: sourceASTs(application, parent) }
1068
+ ));
1069
+ }
1070
+ }
1071
+
1072
+ function validateListSizeAppliedToList(
1073
+ application: Directive<SchemaElement<any, any>, ListSizeDirectiveArguments>,
1074
+ parent: FieldDefinition<CompositeType>,
1075
+ errorCollector: GraphQLError[],
1076
+ ) {
1077
+ const { sizedFields = [] } = application.arguments();
1078
+ // @listSize must be applied to a list https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-List-Size-Target
1079
+ if (!sizedFields.length && parent.type && !isListType(parent.type)) {
1080
+ errorCollector.push(ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err(
1081
+ `"${parent.coordinate}" is not a list`,
1082
+ { nodes: sourceASTs(application, parent) },
1083
+ ));
1084
+ }
1085
+ }
1086
+
1087
+ function validateAssumedSizeNotNegative(
1088
+ application: Directive<SchemaElement<any, any>, ListSizeDirectiveArguments>,
1089
+ parent: FieldDefinition<CompositeType>,
1090
+ errorCollector: GraphQLError[]
1091
+ ) {
1092
+ const { assumedSize } = application.arguments();
1093
+ // Validate assumed size, but we differ from https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Assumed-Size.
1094
+ // Assumed size is used as a backup for slicing arguments in the event they are both specified.
1095
+ // The spec aims to rule out cases when the assumed size will never be used because there is always
1096
+ // a slicing argument. Two applications which are compliant with that validation rule can be merged
1097
+ // into an application which is not compliant, thus we need to handle this case gracefully at runtime regardless.
1098
+ // We omit this check to keep the validations to those that will otherwise cause runtime failures.
1099
+ //
1100
+ // With all that said, assumed size should not be negative.
1101
+ if (assumedSize !== undefined && assumedSize !== null && assumedSize < 0) {
1102
+ errorCollector.push(ERRORS.LIST_SIZE_INVALID_ASSUMED_SIZE.err(
1103
+ `Assumed size of "${parent.coordinate}" cannot be negative`,
1104
+ { nodes: sourceASTs(application, parent) },
1105
+ ));
1106
+ }
1107
+ }
1108
+
1109
+ function isNonNullIntType(ty: Type): boolean {
1110
+ return isNonNullType(ty) && isIntType(ty.ofType)
1111
+ }
1112
+
1113
+ function validateSlicingArgumentsAreValidIntegers(
1114
+ application: Directive<SchemaElement<any, any>, ListSizeDirectiveArguments>,
1115
+ parent: FieldDefinition<CompositeType>,
1116
+ errorCollector: GraphQLError[]
1117
+ ) {
1118
+ const { slicingArguments = [] } = application.arguments();
1119
+ // Validate slicingArguments https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Slicing-Arguments-Target
1120
+ for (const slicingArgumentName of slicingArguments) {
1121
+ const slicingArgument = parent.argument(slicingArgumentName);
1122
+ if (!slicingArgument?.type) {
1123
+ // Slicing arguments must be one of the field's arguments
1124
+ errorCollector.push(ERRORS.LIST_SIZE_INVALID_SLICING_ARGUMENT.err(
1125
+ `Slicing argument "${slicingArgumentName}" is not an argument of "${parent.coordinate}"`,
1126
+ { nodes: sourceASTs(application, parent) }
1127
+ ));
1128
+ } else if (!isIntType(slicingArgument.type) && !isNonNullIntType(slicingArgument.type)) {
1129
+ // Slicing arguments must be Int or Int!
1130
+ errorCollector.push(ERRORS.LIST_SIZE_INVALID_SLICING_ARGUMENT.err(
1131
+ `Slicing argument "${slicingArgument.coordinate}" must be Int or Int!`,
1132
+ { nodes: sourceASTs(application, parent) }
1133
+ ));
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ function isNonNullListType(ty: Type): boolean {
1139
+ return isNonNullType(ty) && isListType(ty.ofType)
1140
+ }
1141
+
1142
+ function validateSizedFieldsAreValidLists(
1143
+ application: Directive<SchemaElement<any, any>, ListSizeDirectiveArguments>,
1144
+ parent: FieldDefinition<CompositeType>,
1145
+ errorCollector: GraphQLError[]
1146
+ ) {
1147
+ const { sizedFields = [] } = application.arguments();
1148
+ // Validate sizedFields https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Sized-Fields-Target
1149
+ if (sizedFields.length) {
1150
+ if (!parent.type || !isCompositeType(parent.type)) {
1151
+ // The output type must have fields
1152
+ errorCollector.push(ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err(
1153
+ `Sized fields cannot be used because "${parent.type}" is not a composite type`,
1154
+ { nodes: sourceASTs(application, parent)}
1155
+ ));
1156
+ } else {
1157
+ for (const sizedFieldName of sizedFields) {
1158
+ const sizedField = parent.type.field(sizedFieldName);
1159
+ if (!sizedField) {
1160
+ // Sized fields must be present on the output type
1161
+ errorCollector.push(ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err(
1162
+ `Sized field "${sizedFieldName}" is not a field on type "${parent.type.coordinate}"`,
1163
+ { nodes: sourceASTs(application, parent) }
1164
+ ));
1165
+ } else if (!sizedField.type || !(isListType(sizedField.type) || isNonNullListType(sizedField.type))) {
1166
+ // Sized fields must be lists
1167
+ errorCollector.push(ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err(
1168
+ `Sized field "${sizedField.coordinate}" is not a list`,
1169
+ { nodes: sourceASTs(application, parent) },
1170
+ ));
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ }
1176
+
1058
1177
  export class FederationMetadata {
1059
1178
  private _externalTester?: ExternalTester;
1060
1179
  private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
@@ -1689,7 +1808,7 @@ export class FederationBlueprint extends SchemaBlueprint {
1689
1808
  const keyApplications = objectType.appliedDirectivesOf(keyDirective);
1690
1809
  if (!keyApplications.some(app => app.arguments().resolvable || app.arguments().resolvable === undefined)) {
1691
1810
  errorCollector.push(ERRORS.CONTEXT_NO_RESOLVABLE_KEY.err(
1692
- `Object "${objectType.coordinate}" has no resolvable key but has an a field with a contextual argument.`,
1811
+ `Object "${objectType.coordinate}" has no resolvable key but has a field with a contextual argument.`,
1693
1812
  { nodes: sourceASTs(objectType) }
1694
1813
  ));
1695
1814
  }
@@ -1736,6 +1855,26 @@ export class FederationBlueprint extends SchemaBlueprint {
1736
1855
  }
1737
1856
  }
1738
1857
 
1858
+ const costFeature = schema.coreFeatures?.getByIdentity(costIdentity);
1859
+ const costSpec = costFeature && COST_VERSIONS.find(costFeature.url.version);
1860
+ const costDirective = costSpec?.costDirective(schema);
1861
+ const listSizeDirective = costSpec?.listSizeDirective(schema);
1862
+
1863
+ // Validate @cost
1864
+ for (const application of costDirective?.applications() ?? []) {
1865
+ validateCostNotAppliedToInterface(application, errorCollector);
1866
+ }
1867
+
1868
+ // Validate @listSize
1869
+ for (const application of listSizeDirective?.applications() ?? []) {
1870
+ const parent = application.parent;
1871
+ assert(parent instanceof FieldDefinition, "@listSize can only be applied to FIELD_DEFINITION");
1872
+ validateListSizeAppliedToList(application, parent, errorCollector);
1873
+ validateAssumedSizeNotNegative(application, parent, errorCollector);
1874
+ validateSlicingArgumentsAreValidIntegers(application, parent, errorCollector);
1875
+ validateSizedFieldsAreValidLists(application, parent, errorCollector);
1876
+ }
1877
+
1739
1878
  return errorCollector;
1740
1879
  }
1741
1880
 
@@ -2141,12 +2280,14 @@ export function parseFieldSetArgument({
2141
2280
  fieldAccessor,
2142
2281
  validate,
2143
2282
  decorateValidationErrors = true,
2283
+ normalize = false,
2144
2284
  }: {
2145
2285
  parentType: CompositeType,
2146
2286
  directive: Directive<SchemaElement<any, any>, {fields: any}>,
2147
2287
  fieldAccessor?: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined,
2148
2288
  validate?: boolean,
2149
2289
  decorateValidationErrors?: boolean,
2290
+ normalize?: boolean,
2150
2291
  }): SelectionSet {
2151
2292
  try {
2152
2293
  const selectionSet = parseSelectionSet({
@@ -2163,7 +2304,9 @@ export function parseFieldSetArgument({
2163
2304
  }
2164
2305
  });
2165
2306
  }
2166
- return selectionSet;
2307
+ return normalize
2308
+ ? selectionSet.normalize({ parentType, recursive: true })
2309
+ : selectionSet;
2167
2310
  } catch (e) {
2168
2311
  if (!(e instanceof GraphQLError) || !decorateValidationErrors) {
2169
2312
  throw e;
package/src/operations.ts CHANGED
@@ -69,7 +69,7 @@ function haveSameDirectives<TElement extends OperationElement>(op1: TElement, op
69
69
  }
70
70
 
71
71
  abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> extends DirectiveTargetElement<T> {
72
- private attachements?: Map<string, string>;
72
+ private attachments?: Map<string, string>;
73
73
 
74
74
  constructor(
75
75
  schema: Schema,
@@ -97,21 +97,21 @@ abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> e
97
97
 
98
98
  protected abstract collectVariablesInElement(collector: VariableCollector): void;
99
99
 
100
- addAttachement(key: string, value: string) {
101
- if (!this.attachements) {
102
- this.attachements = new Map();
100
+ addAttachment(key: string, value: string) {
101
+ if (!this.attachments) {
102
+ this.attachments = new Map();
103
103
  }
104
- this.attachements.set(key, value);
104
+ this.attachments.set(key, value);
105
105
  }
106
106
 
107
- getAttachement(key: string): string | undefined {
108
- return this.attachements?.get(key);
107
+ getAttachment(key: string): string | undefined {
108
+ return this.attachments?.get(key);
109
109
  }
110
110
 
111
- protected copyAttachementsTo(elt: AbstractOperationElement<any>) {
112
- if (this.attachements) {
113
- for (const [k, v] of this.attachements.entries()) {
114
- elt.addAttachement(k, v);
111
+ protected copyAttachmentsTo(elt: AbstractOperationElement<any>) {
112
+ if (this.attachments) {
113
+ for (const [k, v] of this.attachments.entries()) {
114
+ elt.addAttachment(k, v);
115
115
  }
116
116
  }
117
117
  }
@@ -170,6 +170,17 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
170
170
  baseType(): NamedType {
171
171
  return baseType(this.definition.type!);
172
172
  }
173
+
174
+ copy(): Field<TArgs> {
175
+ const newField = new Field<TArgs>(
176
+ this.definition,
177
+ this.args,
178
+ this.appliedDirectives,
179
+ this.alias,
180
+ );
181
+ this.copyAttachmentsTo(newField);
182
+ return newField;
183
+ }
173
184
 
174
185
  withUpdatedArguments(newArgs: TArgs): Field<TArgs> {
175
186
  const newField = new Field<TArgs>(
@@ -178,7 +189,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
178
189
  this.appliedDirectives,
179
190
  this.alias,
180
191
  );
181
- this.copyAttachementsTo(newField);
192
+ this.copyAttachmentsTo(newField);
182
193
  return newField;
183
194
  }
184
195
 
@@ -189,7 +200,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
189
200
  this.appliedDirectives,
190
201
  this.alias,
191
202
  );
192
- this.copyAttachementsTo(newField);
203
+ this.copyAttachmentsTo(newField);
193
204
  return newField;
194
205
  }
195
206
 
@@ -200,7 +211,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
200
211
  this.appliedDirectives,
201
212
  newAlias,
202
213
  );
203
- this.copyAttachementsTo(newField);
214
+ this.copyAttachmentsTo(newField);
204
215
  return newField;
205
216
  }
206
217
 
@@ -211,7 +222,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
211
222
  newDirectives,
212
223
  this.alias,
213
224
  );
214
- this.copyAttachementsTo(newField);
225
+ this.copyAttachmentsTo(newField);
215
226
  return newField;
216
227
  }
217
228
 
@@ -505,13 +516,13 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
505
516
  // schema (typically, the supergraph) than `this.sourceType` (typically, a subgraph), then the new condition uses the
506
517
  // definition of the proper schema (the supergraph in such cases, instead of the subgraph).
507
518
  const newFragment = new FragmentElement(newSourceType, newCondition?.name, this.appliedDirectives);
508
- this.copyAttachementsTo(newFragment);
519
+ this.copyAttachmentsTo(newFragment);
509
520
  return newFragment;
510
521
  }
511
522
 
512
523
  withUpdatedDirectives(newDirectives: Directive<OperationElement>[]): FragmentElement {
513
524
  const newFragment = new FragmentElement(this.sourceType, this.typeCondition, newDirectives);
514
- this.copyAttachementsTo(newFragment);
525
+ this.copyAttachmentsTo(newFragment);
515
526
  return newFragment;
516
527
  }
517
528
 
@@ -590,7 +601,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
590
601
  }
591
602
 
592
603
  const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
593
- this.copyAttachementsTo(updated);
604
+ this.copyAttachmentsTo(updated);
594
605
  return updated;
595
606
  }
596
607
 
@@ -655,7 +666,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
655
666
  .concat(new Directive<FragmentElement>(deferDirective.name, newDeferArgs));
656
667
 
657
668
  const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
658
- this.copyAttachementsTo(updated);
669
+ this.copyAttachmentsTo(updated);
659
670
  return updated;
660
671
  }
661
672
 
@@ -1182,6 +1193,11 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1182
1193
  }
1183
1194
  }
1184
1195
 
1196
+ collectVariables(collector: VariableCollector) {
1197
+ this.selectionSet.collectVariables(collector);
1198
+ this.collectVariablesInAppliedDirectives(collector);
1199
+ }
1200
+
1185
1201
  toFragmentDefinitionNode() : FragmentDefinitionNode {
1186
1202
  return {
1187
1203
  kind: Kind.FRAGMENT_DEFINITION,
@@ -1647,10 +1663,10 @@ export class SelectionSet {
1647
1663
  }
1648
1664
 
1649
1665
  return new FragmentSpreadSelection(this.parentType, namedFragments, fragmentDefinition, []);
1650
- } else if (selection.kind === 'FieldSelection') {
1651
- if (selection.selectionSet) {
1652
- selection = selection.withUpdatedSelectionSet(selection.selectionSet.minimizeSelectionSet(namedFragments, seenSelections)[0]);
1653
- }
1666
+ }
1667
+
1668
+ if (selection.selectionSet) {
1669
+ selection = selection.withUpdatedSelectionSet(selection.selectionSet.minimizeSelectionSet(namedFragments, seenSelections)[0]);
1654
1670
  }
1655
1671
  return selection;
1656
1672
  });
@@ -2103,10 +2119,10 @@ export class SelectionSet {
2103
2119
  // By default, we will print the selection the order in which things were added to it.
2104
2120
  // If __typename is selected however, we put it first. It's a detail but as __typename is a bit special it looks better,
2105
2121
  // and it happens to mimic prior behavior on the query plan side so it saves us from changing tests for no good reasons.
2106
- const isNonAliasedTypenameSelection = (s: Selection) => s.kind === 'FieldSelection' && !s.element.alias && s.element.name === typenameFieldName;
2107
- const typenameSelection = this._selections.find((s) => isNonAliasedTypenameSelection(s));
2122
+ const isPlainTypenameSelection = (s: Selection) => s.kind === 'FieldSelection' && s.isPlainTypenameField();
2123
+ const typenameSelection = this._selections.find((s) => isPlainTypenameSelection(s));
2108
2124
  if (typenameSelection) {
2109
- return [typenameSelection].concat(this.selections().filter(s => !isNonAliasedTypenameSelection(s)));
2125
+ return [typenameSelection].concat(this.selections().filter(s => !isPlainTypenameSelection(s)));
2110
2126
  } else {
2111
2127
  return this._selections;
2112
2128
  }
@@ -3042,6 +3058,19 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
3042
3058
  return this.element.definition.name === typenameFieldName;
3043
3059
  }
3044
3060
 
3061
+ // Is this a plain simple __typename without any directive or alias?
3062
+ isPlainTypenameField(): boolean {
3063
+ return this.element.definition.name === typenameFieldName
3064
+ && this.element.appliedDirectives.length == 0
3065
+ && !this.element.alias;
3066
+ }
3067
+
3068
+ withAttachment(key: string, value: string): FieldSelection {
3069
+ const updatedField = this.element.copy();
3070
+ updatedField.addAttachment(key, value);
3071
+ return this.withUpdatedElement(updatedField);
3072
+ }
3073
+
3045
3074
  withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
3046
3075
  if (this.element === field && this.selectionSet === selectionSet) {
3047
3076
  return this;