@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/dist/error.d.ts +5 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +10 -0
- package/dist/error.js.map +1 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +72 -1
- package/dist/federation.js.map +1 -1
- package/dist/operations.d.ts +6 -4
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +28 -18
- package/dist/operations.js.map +1 -1
- package/package.json +1 -1
- package/src/error.ts +36 -0
- package/src/federation.ts +140 -1
- package/src/operations.ts +36 -19
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
|
|
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
|
-
|
|
101
|
-
if (!this.
|
|
102
|
-
this.
|
|
100
|
+
addAttachment(key: string, value: string) {
|
|
101
|
+
if (!this.attachments) {
|
|
102
|
+
this.attachments = new Map();
|
|
103
103
|
}
|
|
104
|
-
this.
|
|
104
|
+
this.attachments.set(key, value);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
return this.
|
|
107
|
+
getAttachment(key: string): string | undefined {
|
|
108
|
+
return this.attachments?.get(key);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
protected
|
|
112
|
-
if (this.
|
|
113
|
-
for (const [k, v] of this.
|
|
114
|
-
elt.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|