@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/dist/error.d.ts +0 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +1 -2
- package/dist/error.js.map +1 -1
- package/dist/federation.d.ts +4 -3
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +101 -39
- package/dist/federation.js.map +1 -1
- package/dist/operations.d.ts +8 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +47 -5
- package/dist/operations.js.map +1 -1
- package/dist/values.js +2 -2
- package/dist/values.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/subgraphValidation.test.ts +55 -16
- package/src/error.ts +1 -2
- package/src/federation.ts +122 -61
- package/src/operations.ts +66 -6
- package/src/values.ts +2 -2
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollo/federation-internals",
|
|
3
|
-
"version": "2.1.2-alpha.
|
|
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": "
|
|
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
|
-
|
|
131
|
+
metadata: FederationMetadata,
|
|
124
132
|
onError: (error: GraphQLError) => void,
|
|
125
133
|
allowOnNonExternalLeafFields: boolean,
|
|
126
|
-
|
|
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 =
|
|
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 (
|
|
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 &&
|
|
179
|
+
if (fieldInImplem && metadata.isFieldExternal(fieldInImplem)) {
|
|
171
180
|
newHasExternalInParents = true;
|
|
172
181
|
break;
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
}
|
|
176
|
-
validateFieldSetSelections(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
isOnParentType
|
|
276
|
-
allowOnNonExternalLeafFields
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
827
|
+
validateAllFieldSet<FieldDefinition<CompositeType>>({
|
|
828
|
+
definition: metadata.requiresDirective(),
|
|
829
|
+
targetTypeExtractor: field => field.parent,
|
|
830
|
+
errorCollector,
|
|
785
831
|
metadata,
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
856
|
+
errorCollector,
|
|
812
857
|
metadata,
|
|
813
|
-
|
|
814
|
-
false,
|
|
815
|
-
);
|
|
858
|
+
});
|
|
816
859
|
|
|
817
|
-
validateNoExternalOnInterfaceFields(metadata,
|
|
818
|
-
validateAllExternalFieldsUsed(metadata,
|
|
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
|
-
|
|
868
|
+
errorCollector.push(error);
|
|
826
869
|
}
|
|
827
870
|
}
|
|
828
871
|
|
|
829
872
|
for (const itf of schema.interfaceTypes()) {
|
|
830
|
-
validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata,
|
|
873
|
+
validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errorCollector);
|
|
831
874
|
}
|
|
832
875
|
|
|
833
|
-
return
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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}"
|
|
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
|
|
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
|
-
|
|
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);
|