@apollo/federation-internals 2.9.1 → 2.9.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/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;
@@ -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
 
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
 
@@ -3042,6 +3053,12 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
3042
3053
  return this.element.definition.name === typenameFieldName;
3043
3054
  }
3044
3055
 
3056
+ withAttachment(key: string, value: string): FieldSelection {
3057
+ const updatedField = this.element.copy();
3058
+ updatedField.addAttachment(key, value);
3059
+ return this.withUpdatedElement(updatedField);
3060
+ }
3061
+
3045
3062
  withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
3046
3063
  if (this.element === field && this.selectionSet === selectionSet) {
3047
3064
  return this;