@apollo/federation-internals 2.0.0-preview.8 → 2.0.0-preview.9
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/CHANGELOG.md +6 -0
- package/dist/buildSchema.js +1 -1
- package/dist/buildSchema.js.map +1 -1
- package/dist/coreSpec.d.ts +11 -5
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +68 -27
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts +7 -5
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +26 -13
- package/dist/definitions.js.map +1 -1
- package/dist/directiveAndTypeSpecification.d.ts +11 -1
- package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
- package/dist/directiveAndTypeSpecification.js +67 -19
- package/dist/directiveAndTypeSpecification.js.map +1 -1
- package/dist/error.d.ts +3 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +6 -0
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +6 -0
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +15 -5
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +77 -57
- package/dist/federation.js.map +1 -1
- package/dist/federationSpec.d.ts +3 -3
- package/dist/federationSpec.d.ts.map +1 -1
- package/dist/federationSpec.js +34 -26
- package/dist/federationSpec.js.map +1 -1
- package/dist/inaccessibleSpec.d.ts +3 -2
- package/dist/inaccessibleSpec.d.ts.map +1 -1
- package/dist/inaccessibleSpec.js +13 -4
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts.map +1 -1
- package/dist/introspection.js +8 -3
- package/dist/introspection.js.map +1 -1
- package/dist/joinSpec.d.ts +4 -1
- package/dist/joinSpec.d.ts.map +1 -1
- package/dist/joinSpec.js +3 -0
- package/dist/joinSpec.js.map +1 -1
- package/dist/operations.d.ts +1 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +16 -1
- package/dist/operations.js.map +1 -1
- package/dist/{sharing.d.ts → precompute.d.ts} +1 -1
- package/dist/precompute.d.ts.map +1 -0
- package/dist/{sharing.js → precompute.js} +1 -1
- package/dist/precompute.js.map +1 -0
- package/dist/schemaUpgrader.d.ts.map +1 -1
- package/dist/schemaUpgrader.js +1 -2
- package/dist/schemaUpgrader.js.map +1 -1
- package/dist/suggestions.d.ts +1 -1
- package/dist/suggestions.d.ts.map +1 -1
- package/dist/suggestions.js.map +1 -1
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +1 -0
- package/dist/supergraphs.js.map +1 -1
- package/dist/tagSpec.d.ts +6 -2
- package/dist/tagSpec.d.ts.map +1 -1
- package/dist/tagSpec.js +30 -14
- package/dist/tagSpec.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/removeInaccessibleElements.test.ts +59 -6
- package/src/__tests__/subgraphValidation.test.ts +339 -0
- package/src/buildSchema.ts +1 -1
- package/src/coreSpec.ts +88 -33
- package/src/definitions.ts +29 -14
- package/src/directiveAndTypeSpecification.ts +80 -20
- package/src/error.ts +18 -0
- package/src/extractSubgraphsFromSupergraph.ts +6 -0
- package/src/federation.ts +89 -72
- package/src/federationSpec.ts +36 -29
- package/src/inaccessibleSpec.ts +16 -4
- package/src/index.ts +1 -0
- package/src/introspection.ts +8 -3
- package/src/joinSpec.ts +14 -3
- package/src/operations.ts +15 -0
- package/src/{sharing.ts → precompute.ts} +1 -2
- package/src/schemaUpgrader.ts +4 -7
- package/src/suggestions.ts +1 -1
- package/src/supergraphs.ts +1 -0
- package/src/tagSpec.ts +42 -16
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/sharing.d.ts.map +0 -1
- package/dist/sharing.js.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { DirectiveLocation, GraphQLError } from "graphql";
|
|
1
|
+
import { ASTNode, DirectiveLocation, GraphQLError } from "graphql";
|
|
2
2
|
import {
|
|
3
3
|
ArgumentDefinition,
|
|
4
4
|
DirectiveDefinition,
|
|
5
|
+
EnumType,
|
|
5
6
|
InputType,
|
|
7
|
+
isEnumType,
|
|
6
8
|
isNonNullType,
|
|
7
9
|
isObjectType,
|
|
8
10
|
isUnionType,
|
|
@@ -49,13 +51,16 @@ export function createDirectiveSpecification({
|
|
|
49
51
|
name: string,
|
|
50
52
|
locations: DirectiveLocation[],
|
|
51
53
|
repeatable?: boolean,
|
|
52
|
-
argumentFct?: (schema: Schema) => ArgumentSpecification[],
|
|
54
|
+
argumentFct?: (schema: Schema, nameInSchema?: string) => { args: ArgumentSpecification[], errors: GraphQLError[] },
|
|
53
55
|
}): DirectiveSpecification {
|
|
54
56
|
return {
|
|
55
57
|
name,
|
|
56
58
|
checkOrAdd: (schema: Schema, nameInSchema?: string, asBuiltIn?: boolean) => {
|
|
57
|
-
const args = argumentFct ? argumentFct(schema) : [];
|
|
58
59
|
const actualName = nameInSchema ?? name;
|
|
60
|
+
const {args, errors} = argumentFct ? argumentFct(schema, actualName) : { args: [], errors: []};
|
|
61
|
+
if (errors.length > 0) {
|
|
62
|
+
return errors;
|
|
63
|
+
}
|
|
59
64
|
const existing = schema.directive(actualName);
|
|
60
65
|
if (existing) {
|
|
61
66
|
return ensureSameDirectiveStructure({name: actualName, locations, repeatable, args}, existing);
|
|
@@ -131,7 +136,7 @@ export function createObjectTypeSpecification({
|
|
|
131
136
|
errors = errors.concat(ensureSameArguments(
|
|
132
137
|
{ name, args },
|
|
133
138
|
existingField,
|
|
134
|
-
`field ${existingField.coordinate}`,
|
|
139
|
+
`field "${existingField.coordinate}"`,
|
|
135
140
|
));
|
|
136
141
|
}
|
|
137
142
|
return errors;
|
|
@@ -198,6 +203,44 @@ export function createUnionTypeSpecification({
|
|
|
198
203
|
}
|
|
199
204
|
}
|
|
200
205
|
|
|
206
|
+
export function createEnumTypeSpecification({
|
|
207
|
+
name,
|
|
208
|
+
values,
|
|
209
|
+
}: {
|
|
210
|
+
name: string,
|
|
211
|
+
values: { name: string, description?: string}[],
|
|
212
|
+
}): TypeSpecification {
|
|
213
|
+
return {
|
|
214
|
+
name,
|
|
215
|
+
checkOrAdd: (schema: Schema, nameInSchema?: string, asBuiltIn?: boolean) => {
|
|
216
|
+
const actualName = nameInSchema ?? name;
|
|
217
|
+
const existing = schema.type(actualName);
|
|
218
|
+
const expectedValueNames = values.map((v) => v.name).sort((n1, n2) => n1.localeCompare(n2));
|
|
219
|
+
if (existing) {
|
|
220
|
+
let errors = ensureSameTypeKind('EnumType', existing);
|
|
221
|
+
if (errors.length > 0) {
|
|
222
|
+
return errors;
|
|
223
|
+
}
|
|
224
|
+
assert(isEnumType(existing), 'Should be an enum type');
|
|
225
|
+
const actualValueNames = existing.values.map(v => v.name).sort((n1, n2) => n1.localeCompare(n2));
|
|
226
|
+
if (!arrayEquals(expectedValueNames, actualValueNames)) {
|
|
227
|
+
errors = errors.concat(ERRORS.TYPE_DEFINITION_INVALID.err({
|
|
228
|
+
message: `Invalid definition of type ${name}: expected values [${expectedValueNames}] but found [${actualValueNames}].`,
|
|
229
|
+
nodes: existing.sourceAST
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
return errors;
|
|
233
|
+
} else {
|
|
234
|
+
const type = schema.addType(new EnumType(actualName, asBuiltIn));
|
|
235
|
+
for (const {name, description} of values) {
|
|
236
|
+
type.addValue(name).description = description;
|
|
237
|
+
}
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
201
244
|
function ensureSameTypeKind(expected: NamedType['kind'], actual: NamedType): GraphQLError[] {
|
|
202
245
|
return expected === actual.kind
|
|
203
246
|
? []
|
|
@@ -216,18 +259,19 @@ function ensureSameDirectiveStructure(
|
|
|
216
259
|
},
|
|
217
260
|
actual: DirectiveDefinition<any>,
|
|
218
261
|
): GraphQLError[] {
|
|
219
|
-
|
|
262
|
+
const directiveName = `"@${expected.name}"`
|
|
263
|
+
let errors = ensureSameArguments(expected, actual, `directive ${directiveName}`);
|
|
220
264
|
// It's ok to say you'll never repeat a repeatable directive. It's not ok to repeat one that isn't.
|
|
221
265
|
if (!expected.repeatable && actual.repeatable) {
|
|
222
266
|
errors = errors.concat(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
|
|
223
|
-
message: `Invalid definition for directive ${
|
|
267
|
+
message: `Invalid definition for directive ${directiveName}: ${directiveName} should${expected.repeatable ? "" : " not"} be repeatable`,
|
|
224
268
|
nodes: actual.sourceAST
|
|
225
269
|
}));
|
|
226
270
|
}
|
|
227
271
|
// Similarly, it's ok to say that you will never use a directive in some locations, but not that you will use it in places not allowed by what is expected.
|
|
228
272
|
if (!actual.locations.every(loc => expected.locations.includes(loc))) {
|
|
229
273
|
errors = errors.concat(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
|
|
230
|
-
message: `Invalid
|
|
274
|
+
message: `Invalid definition for directive ${directiveName}: ${directiveName} should have locations ${expected.locations.join(', ')}, but found (non-subset) ${actual.locations.join(', ')}`,
|
|
231
275
|
nodes: actual.sourceAST
|
|
232
276
|
}));
|
|
233
277
|
}
|
|
@@ -241,17 +285,24 @@ function ensureSameArguments(
|
|
|
241
285
|
},
|
|
242
286
|
actual: { argument(name: string): ArgumentDefinition<any> | undefined, arguments(): readonly ArgumentDefinition<any>[] },
|
|
243
287
|
what: string,
|
|
288
|
+
containerSourceAST?: ASTNode,
|
|
244
289
|
): GraphQLError[] {
|
|
245
290
|
const expectedArguments = expected.args ?? [];
|
|
246
|
-
const
|
|
247
|
-
if (expectedArguments.length !== foundArguments.length) {
|
|
248
|
-
return [ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
|
|
249
|
-
message: `Invalid definition for ${what}: should have ${expectedArguments.length} arguments but ${foundArguments.length} found`,
|
|
250
|
-
})];
|
|
251
|
-
}
|
|
252
|
-
let errors: GraphQLError[] = [];
|
|
291
|
+
const errors: GraphQLError[] = [];
|
|
253
292
|
for (const { name, type, defaultValue } of expectedArguments) {
|
|
254
|
-
const actualArgument = actual.argument(name)
|
|
293
|
+
const actualArgument = actual.argument(name);
|
|
294
|
+
if (!actualArgument) {
|
|
295
|
+
// Not declaring an optional argument is ok: that means you won't be able to pass a non-default value in your schema, but we allow you that.
|
|
296
|
+
// But missing a required argument it not ok.
|
|
297
|
+
if (isNonNullType(type) && defaultValue === undefined) {
|
|
298
|
+
errors.push(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
|
|
299
|
+
message: `Invalid definition for ${what}: missing required argument "${name}"`,
|
|
300
|
+
nodes: containerSourceAST
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
255
306
|
let actualType = actualArgument.type!;
|
|
256
307
|
if (isNonNullType(actualType) && !isNonNullType(type)) {
|
|
257
308
|
// It's ok to redefine an optional argument as mandatory. For instance, if you want to force people on your team to provide a "deprecation reason", you can
|
|
@@ -260,13 +311,22 @@ function ensureSameArguments(
|
|
|
260
311
|
actualType = actualType.ofType;
|
|
261
312
|
}
|
|
262
313
|
if (!sameType(type, actualType)) {
|
|
263
|
-
errors
|
|
264
|
-
message: `Invalid definition
|
|
314
|
+
errors.push(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
|
|
315
|
+
message: `Invalid definition for ${what}: argument "${name}" should have type "${type}" but found type "${actualArgument.type!}"`,
|
|
265
316
|
nodes: actualArgument.sourceAST
|
|
266
317
|
}));
|
|
267
|
-
} else if (!isNonNullType(
|
|
268
|
-
errors
|
|
269
|
-
message: `Invalid definition
|
|
318
|
+
} else if (!isNonNullType(actualArgument.type!) && !valueEquals(defaultValue, actualArgument.defaultValue)) {
|
|
319
|
+
errors.push(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
|
|
320
|
+
message: `Invalid definition for ${what}: argument "${name}" should have default value ${valueToString(defaultValue)} but found default value ${valueToString(actualArgument.defaultValue)}`,
|
|
321
|
+
nodes: actualArgument.sourceAST
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const actualArgument of actual.arguments()) {
|
|
326
|
+
// If it's an expect argument, we already validated it. But we still need to reject unkown argument.
|
|
327
|
+
if (!expectedArguments.some((arg) => arg.name === actualArgument.name)) {
|
|
328
|
+
errors.push(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
|
|
329
|
+
message: `Invalid definition for ${what}: unknown/unsupported argument "${actualArgument.name}"`,
|
|
270
330
|
nodes: actualArgument.sourceAST
|
|
271
331
|
}));
|
|
272
332
|
}
|
package/src/error.ts
CHANGED
|
@@ -359,6 +359,21 @@ const SATISFIABILITY_ERROR = makeCodeDefinition(
|
|
|
359
359
|
'Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs.',
|
|
360
360
|
);
|
|
361
361
|
|
|
362
|
+
const OVERRIDE_FROM_SELF_ERROR = makeCodeDefinition(
|
|
363
|
+
'OVERRIDE_FROM_SELF_ERROR',
|
|
364
|
+
'Field with `@override` directive has "from" location that references its own subgraph.',
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const OVERRIDE_SOURCE_HAS_OVERRIDE = makeCodeDefinition(
|
|
368
|
+
'OVERRIDE_SOURCE_HAS_OVERRIDE',
|
|
369
|
+
'Field which is overridden to another subgraph is also marked @override.',
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE = makeCodeDefinition(
|
|
373
|
+
'OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE',
|
|
374
|
+
'The @override directive cannot be used on external fields, nor to override fields with either @external, @provides, or @requires.',
|
|
375
|
+
);
|
|
376
|
+
|
|
362
377
|
export const ERROR_CATEGORIES = {
|
|
363
378
|
DIRECTIVE_FIELDS_MISSING_EXTERNAL,
|
|
364
379
|
DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
|
|
@@ -418,6 +433,9 @@ export const ERRORS = {
|
|
|
418
433
|
REFERENCED_INACCESSIBLE,
|
|
419
434
|
REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH,
|
|
420
435
|
SATISFIABILITY_ERROR,
|
|
436
|
+
OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE,
|
|
437
|
+
OVERRIDE_FROM_SELF_ERROR,
|
|
438
|
+
OVERRIDE_SOURCE_HAS_OVERRIDE,
|
|
421
439
|
};
|
|
422
440
|
|
|
423
441
|
const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
|
|
@@ -181,6 +181,12 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
|
|
|
181
181
|
if (args.external) {
|
|
182
182
|
subgraphField.applyDirective(subgraph.metadata().externalDirective());
|
|
183
183
|
}
|
|
184
|
+
if (args.usedOverridden) {
|
|
185
|
+
subgraphField.applyDirective(subgraph.metadata().externalDirective(), {'reason': '[overridden]'});
|
|
186
|
+
}
|
|
187
|
+
if (args.override) {
|
|
188
|
+
subgraphField.applyDirective(subgraph.metadata().overrideDirective(), {'from': args.override});
|
|
189
|
+
}
|
|
184
190
|
}
|
|
185
191
|
}
|
|
186
192
|
}
|
package/src/federation.ts
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
ERRORS,
|
|
53
53
|
withModifiedErrorMessage,
|
|
54
54
|
} from "./error";
|
|
55
|
-
import { computeShareables } from "./
|
|
55
|
+
import { computeShareables } from "./precompute";
|
|
56
56
|
import {
|
|
57
57
|
CoreSpecDefinition,
|
|
58
58
|
FeatureVersion,
|
|
@@ -71,8 +71,7 @@ import {
|
|
|
71
71
|
externalDirectiveSpec,
|
|
72
72
|
extendsDirectiveSpec,
|
|
73
73
|
shareableDirectiveSpec,
|
|
74
|
-
|
|
75
|
-
inaccessibleDirectiveSpec,
|
|
74
|
+
overrideDirectiveSpec,
|
|
76
75
|
FEDERATION2_SPEC_DIRECTIVES,
|
|
77
76
|
ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES,
|
|
78
77
|
FEDERATION2_ONLY_SPEC_DIRECTIVES,
|
|
@@ -80,6 +79,7 @@ import {
|
|
|
80
79
|
import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
|
|
81
80
|
import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
|
|
82
81
|
import { didYouMean, suggestionList } from "./suggestions";
|
|
82
|
+
import { inaccessibleDirectiveSpec } from "./inaccessibleSpec";
|
|
83
83
|
|
|
84
84
|
const linkSpec = LINK_VERSIONS.latest();
|
|
85
85
|
const tagSpec = TAG_VERSIONS.latest();
|
|
@@ -263,53 +263,49 @@ function validateAllFieldSet<TParent extends SchemaElement<any, any>>(
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
export function
|
|
267
|
-
const
|
|
266
|
+
export function collectUsedFields(metadata: FederationMetadata): Set<FieldDefinition<CompositeType>> {
|
|
267
|
+
const usedFields = new Set<FieldDefinition<CompositeType>>();
|
|
268
268
|
|
|
269
269
|
// Collects all external fields used by a key, requires or provides
|
|
270
|
-
|
|
271
|
-
metadata,
|
|
270
|
+
collectUsedFieldsForDirective<CompositeType>(
|
|
272
271
|
metadata.keyDirective(),
|
|
273
272
|
type => type,
|
|
274
|
-
|
|
273
|
+
usedFields,
|
|
275
274
|
);
|
|
276
|
-
|
|
277
|
-
metadata,
|
|
275
|
+
collectUsedFieldsForDirective<FieldDefinition<CompositeType>>(
|
|
278
276
|
metadata.requiresDirective(),
|
|
279
277
|
field => field.parent!,
|
|
280
|
-
|
|
278
|
+
usedFields,
|
|
281
279
|
);
|
|
282
|
-
|
|
283
|
-
metadata,
|
|
280
|
+
collectUsedFieldsForDirective<FieldDefinition<CompositeType>>(
|
|
284
281
|
metadata.providesDirective(),
|
|
285
282
|
field => {
|
|
286
283
|
const type = baseType(field.type!);
|
|
287
284
|
return isCompositeType(type) ? type : undefined;
|
|
288
285
|
},
|
|
289
|
-
|
|
286
|
+
usedFields,
|
|
290
287
|
);
|
|
291
288
|
|
|
292
|
-
// Collects all
|
|
289
|
+
// Collects all fields used to satisfy an interface constraint
|
|
293
290
|
for (const itfType of metadata.schema.types<InterfaceType>('InterfaceType')) {
|
|
294
291
|
const runtimeTypes = itfType.possibleRuntimeTypes();
|
|
295
292
|
for (const field of itfType.fields()) {
|
|
296
293
|
for (const runtimeType of runtimeTypes) {
|
|
297
294
|
const implemField = runtimeType.field(field.name);
|
|
298
|
-
if (implemField
|
|
299
|
-
|
|
295
|
+
if (implemField) {
|
|
296
|
+
usedFields.add(implemField);
|
|
300
297
|
}
|
|
301
298
|
}
|
|
302
299
|
}
|
|
303
300
|
}
|
|
304
301
|
|
|
305
|
-
return
|
|
302
|
+
return usedFields;
|
|
306
303
|
}
|
|
307
304
|
|
|
308
|
-
function
|
|
309
|
-
metadata: FederationMetadata,
|
|
305
|
+
function collectUsedFieldsForDirective<TParent extends SchemaElement<any, any>>(
|
|
310
306
|
definition: DirectiveDefinition<{fields: any}>,
|
|
311
307
|
targetTypeExtractor: (element: TParent) => CompositeType | undefined,
|
|
312
|
-
|
|
308
|
+
usedFieldDefs: Set<FieldDefinition<CompositeType>>
|
|
313
309
|
) {
|
|
314
310
|
for (const application of definition.applications()) {
|
|
315
311
|
const type = targetTypeExtractor(application.parent! as TParent);
|
|
@@ -327,8 +323,7 @@ function collectUsedExternaFieldsForDirective<TParent extends SchemaElement<any,
|
|
|
327
323
|
directive: application as Directive<any, {fields: any}>,
|
|
328
324
|
includeInterfaceFieldsImplementations: true,
|
|
329
325
|
validate: false,
|
|
330
|
-
}).
|
|
331
|
-
.forEach((field) => usedExternalCoordinates.add(field.coordinate));
|
|
326
|
+
}).forEach((field) => usedFieldDefs.add(field));
|
|
332
327
|
}
|
|
333
328
|
}
|
|
334
329
|
|
|
@@ -337,23 +332,20 @@ function collectUsedExternaFieldsForDirective<TParent extends SchemaElement<any,
|
|
|
337
332
|
* interface implementation. Otherwise, the field declaration is somewhat useless.
|
|
338
333
|
*/
|
|
339
334
|
function validateAllExternalFieldsUsed(metadata: FederationMetadata, errorCollector: GraphQLError[]): void {
|
|
340
|
-
const allUsedExternals = collectUsedExternalFieldsCoordinates(metadata);
|
|
341
335
|
for (const type of metadata.schema.types()) {
|
|
342
336
|
if (!isObjectType(type) && !isInterfaceType(type)) {
|
|
343
337
|
continue;
|
|
344
338
|
}
|
|
345
339
|
for (const field of type.fields()) {
|
|
346
|
-
if (!metadata.isFieldExternal(field) ||
|
|
340
|
+
if (!metadata.isFieldExternal(field) || metadata.isFieldUsed(field)) {
|
|
347
341
|
continue;
|
|
348
342
|
}
|
|
349
343
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}));
|
|
356
|
-
}
|
|
344
|
+
errorCollector.push(ERRORS.EXTERNAL_UNUSED.err({
|
|
345
|
+
message: `Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
|
|
346
|
+
+ ' the field declaration has no use and should be removed (or the field should not be @external).',
|
|
347
|
+
nodes: field.sourceAST,
|
|
348
|
+
}));
|
|
357
349
|
}
|
|
358
350
|
}
|
|
359
351
|
}
|
|
@@ -371,10 +363,6 @@ function validateNoExternalOnInterfaceFields(metadata: FederationMetadata, error
|
|
|
371
363
|
}
|
|
372
364
|
}
|
|
373
365
|
|
|
374
|
-
function isFieldSatisfyingInterface(field: FieldDefinition<ObjectType | InterfaceType>): boolean {
|
|
375
|
-
return field.parent.interfaces().some(itf => itf.field(field.name));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
366
|
/**
|
|
379
367
|
* Register errors when, for an interface field, some of the implementations of that field are @external
|
|
380
368
|
* _and_ not all of those field implementation have the same type (which otherwise allowed because field
|
|
@@ -385,7 +373,7 @@ function isFieldSatisfyingInterface(field: FieldDefinition<ObjectType | Interfac
|
|
|
385
373
|
*/
|
|
386
374
|
function validateInterfaceRuntimeImplementationFieldsTypes(
|
|
387
375
|
itf: InterfaceType,
|
|
388
|
-
metadata: FederationMetadata,
|
|
376
|
+
metadata: FederationMetadata,
|
|
389
377
|
errorCollector: GraphQLError[],
|
|
390
378
|
): void {
|
|
391
379
|
const requiresDirective = federationMetadata(itf.schema())?.requiresDirective();
|
|
@@ -426,6 +414,7 @@ function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<Obje
|
|
|
426
414
|
export class FederationMetadata {
|
|
427
415
|
private _externalTester?: ExternalTester;
|
|
428
416
|
private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
|
|
417
|
+
private _fieldUsedPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
|
|
429
418
|
private _isFed2Schema?: boolean;
|
|
430
419
|
|
|
431
420
|
constructor(readonly schema: Schema) {
|
|
@@ -435,6 +424,7 @@ export class FederationMetadata {
|
|
|
435
424
|
this._externalTester = undefined;
|
|
436
425
|
this._sharingPredicate = undefined;
|
|
437
426
|
this._isFed2Schema = undefined;
|
|
427
|
+
this._fieldUsedPredicate = undefined;
|
|
438
428
|
}
|
|
439
429
|
|
|
440
430
|
isFed2Schema(): boolean {
|
|
@@ -463,6 +453,18 @@ export class FederationMetadata {
|
|
|
463
453
|
return this._sharingPredicate;
|
|
464
454
|
}
|
|
465
455
|
|
|
456
|
+
private fieldUsedPredicate(): (field: FieldDefinition<CompositeType>) => boolean {
|
|
457
|
+
if (!this._fieldUsedPredicate) {
|
|
458
|
+
const usedFields = collectUsedFields(this);
|
|
459
|
+
this._fieldUsedPredicate = (field: FieldDefinition<CompositeType>) => !!usedFields.has(field);
|
|
460
|
+
}
|
|
461
|
+
return this._fieldUsedPredicate;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
isFieldUsed(field: FieldDefinition<CompositeType>): boolean {
|
|
465
|
+
return this.fieldUsedPredicate()(field);
|
|
466
|
+
}
|
|
467
|
+
|
|
466
468
|
isFieldExternal(field: FieldDefinition<any> | InputFieldDefinition) {
|
|
467
469
|
return this.externalTester().isExternal(field);
|
|
468
470
|
}
|
|
@@ -534,11 +536,15 @@ export class FederationMetadata {
|
|
|
534
536
|
return this.getFederationDirective(keyDirectiveSpec.name);
|
|
535
537
|
}
|
|
536
538
|
|
|
539
|
+
overrideDirective(): DirectiveDefinition<{from: string}> {
|
|
540
|
+
return this.getFederationDirective(overrideDirectiveSpec.name);
|
|
541
|
+
}
|
|
542
|
+
|
|
537
543
|
extendsDirective(): DirectiveDefinition<Record<string, never>> {
|
|
538
544
|
return this.getFederationDirective(extendsDirectiveSpec.name);
|
|
539
545
|
}
|
|
540
546
|
|
|
541
|
-
externalDirective(): DirectiveDefinition<
|
|
547
|
+
externalDirective(): DirectiveDefinition<{reason: string}> {
|
|
542
548
|
return this.getFederationDirective(externalDirectiveSpec.name);
|
|
543
549
|
}
|
|
544
550
|
|
|
@@ -555,7 +561,7 @@ export class FederationMetadata {
|
|
|
555
561
|
}
|
|
556
562
|
|
|
557
563
|
tagDirective(): DirectiveDefinition<{name: string}> {
|
|
558
|
-
return this.getFederationDirective(tagDirectiveSpec.name);
|
|
564
|
+
return this.getFederationDirective(tagSpec.tagDirectiveSpec.name);
|
|
559
565
|
}
|
|
560
566
|
|
|
561
567
|
inaccessibleDirective(): DirectiveDefinition<{}> {
|
|
@@ -563,7 +569,7 @@ export class FederationMetadata {
|
|
|
563
569
|
}
|
|
564
570
|
|
|
565
571
|
allFederationDirectives(): DirectiveDefinition[] {
|
|
566
|
-
const baseDirectives = [
|
|
572
|
+
const baseDirectives: DirectiveDefinition[] = [
|
|
567
573
|
this.keyDirective(),
|
|
568
574
|
this.externalDirective(),
|
|
569
575
|
this.requiresDirective(),
|
|
@@ -572,7 +578,7 @@ export class FederationMetadata {
|
|
|
572
578
|
this.extendsDirective(),
|
|
573
579
|
];
|
|
574
580
|
return this.isFed2Schema()
|
|
575
|
-
? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective())
|
|
581
|
+
? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective(), this.overrideDirective())
|
|
576
582
|
: baseDirectives;
|
|
577
583
|
}
|
|
578
584
|
|
|
@@ -618,19 +624,21 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
618
624
|
}
|
|
619
625
|
}
|
|
620
626
|
|
|
621
|
-
onMissingDirectiveDefinition(schema: Schema, name: string): DirectiveDefinition | undefined {
|
|
627
|
+
onMissingDirectiveDefinition(schema: Schema, name: string, args?: {[key: string]: any}): DirectiveDefinition | GraphQLError[] | undefined {
|
|
622
628
|
if (name === linkDirectiveDefaultName) {
|
|
623
|
-
|
|
624
|
-
|
|
629
|
+
const url = args && (args['url'] as string | undefined);
|
|
630
|
+
const as = url && url.startsWith(linkSpec.identity) ? (args['as'] as string | undefined) : undefined;
|
|
631
|
+
const errors = linkSpec.addDefinitionsToSchema(schema, as);
|
|
632
|
+
return errors.length > 0 ? errors : schema.directive(name);
|
|
625
633
|
}
|
|
626
|
-
return super.onMissingDirectiveDefinition(schema, name);
|
|
634
|
+
return super.onMissingDirectiveDefinition(schema, name, args);
|
|
627
635
|
}
|
|
628
636
|
|
|
629
637
|
ignoreParsedField(type: NamedType, fieldName: string): boolean {
|
|
630
638
|
// Historically, federation 1 has accepted invalid schema, including some where the Query type included
|
|
631
639
|
// the definition of `_entities` (so `_entities(representations: [_Any!]!): [_Entity]!`) but _without_
|
|
632
640
|
// defining the `_Any` or `_Entity` type. So while we want to be stricter for fed2 (so this kind of
|
|
633
|
-
// really weird case can be fixed), we want fed2 to accept as much fed1 schema as possible.
|
|
641
|
+
// really weird case can be fixed), we want fed2 to accept as much fed1 schema as possible.
|
|
634
642
|
//
|
|
635
643
|
// So, to avoid this problem, we ignore the _entities and _service fields if we parse them from
|
|
636
644
|
// a fed1 input schema. Those will be added back anyway (along with the proper types) post-parsing.
|
|
@@ -648,8 +656,8 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
648
656
|
}
|
|
649
657
|
}
|
|
650
658
|
|
|
651
|
-
onDirectiveDefinitionAndSchemaParsed(schema: Schema) {
|
|
652
|
-
completeSubgraphSchema(schema);
|
|
659
|
+
onDirectiveDefinitionAndSchemaParsed(schema: Schema): GraphQLError[] {
|
|
660
|
+
return completeSubgraphSchema(schema);
|
|
653
661
|
}
|
|
654
662
|
|
|
655
663
|
onInvalidation(schema: Schema) {
|
|
@@ -788,7 +796,6 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
788
796
|
const federationFeature = metadata.federationFeature();
|
|
789
797
|
assert(federationFeature, 'Fed2 subgraph _must_ link to the federation feature')
|
|
790
798
|
const directiveNameInSchema = federationFeature.directiveNameInSchema(unknownDirectiveName);
|
|
791
|
-
console.log(`For ${unknownDirectiveName}, name in schema = ${directiveNameInSchema}`);
|
|
792
799
|
if (directiveNameInSchema.startsWith(federationFeature.nameInSchema + '__')) {
|
|
793
800
|
// There is no import for that directive
|
|
794
801
|
return withModifiedErrorMessage(
|
|
@@ -804,7 +811,7 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
804
811
|
}
|
|
805
812
|
} else {
|
|
806
813
|
return withModifiedErrorMessage(
|
|
807
|
-
error,
|
|
814
|
+
error,
|
|
808
815
|
`${error.message} If you meant the "@${unknownDirectiveName}" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specifcation v2.`
|
|
809
816
|
);
|
|
810
817
|
}
|
|
@@ -813,7 +820,7 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
813
820
|
const suggestions = suggestionList(unknownDirectiveName, FEDERATION2_ONLY_SPEC_DIRECTIVES.map((spec) => spec.name));
|
|
814
821
|
if (suggestions.length > 0) {
|
|
815
822
|
return withModifiedErrorMessage(
|
|
816
|
-
error,
|
|
823
|
+
error,
|
|
817
824
|
`${error.message}${didYouMean(suggestions.map((s) => '@' + s))} If so, note that ${suggestions.length === 1 ? 'it is a federation 2 directive' : 'they are federation 2 directives'} but this schema is a federation 1 one. To be a federation 2 schema, it needs to @link to the federation specifcation v2.`
|
|
818
825
|
);
|
|
819
826
|
}
|
|
@@ -851,7 +858,10 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
851
858
|
assert(spec.url.version.satisfies(linkSpec.version), `Fed2 schema must use @link with version >= 1.0, but schema uses ${spec.url}`);
|
|
852
859
|
} else {
|
|
853
860
|
const alias = findUnusedNamedForLinkDirective(schema);
|
|
854
|
-
linkSpec.addToSchema(schema, alias);
|
|
861
|
+
const errors = linkSpec.addToSchema(schema, alias);
|
|
862
|
+
if (errors.length > 0) {
|
|
863
|
+
throw ErrGraphQLValidationFailed(errors);
|
|
864
|
+
}
|
|
855
865
|
spec = linkSpec;
|
|
856
866
|
core = schema.coreFeatures;
|
|
857
867
|
assert(core, 'Schema should now be a core schema');
|
|
@@ -865,12 +875,15 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
865
875
|
import: FEDERATION2_SPEC_DIRECTIVES.map((spec) => `@${spec.name}`),
|
|
866
876
|
}
|
|
867
877
|
);
|
|
868
|
-
completeSubgraphSchema(schema);
|
|
878
|
+
const errors = completeSubgraphSchema(schema);
|
|
879
|
+
if (errors.length > 0) {
|
|
880
|
+
throw ErrGraphQLValidationFailed(errors);
|
|
881
|
+
}
|
|
869
882
|
}
|
|
870
883
|
|
|
871
884
|
// This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
|
|
872
885
|
// subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
|
|
873
|
-
export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible"])';
|
|
886
|
+
export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override"])';
|
|
874
887
|
|
|
875
888
|
export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
|
|
876
889
|
const fed2LinkExtension: SchemaExtensionNode = {
|
|
@@ -961,22 +974,25 @@ export function newEmptyFederation2Schema(): Schema {
|
|
|
961
974
|
return schema;
|
|
962
975
|
}
|
|
963
976
|
|
|
964
|
-
function completeSubgraphSchema(schema: Schema) {
|
|
977
|
+
function completeSubgraphSchema(schema: Schema): GraphQLError[] {
|
|
965
978
|
const coreFeatures = schema.coreFeatures;
|
|
966
979
|
if (coreFeatures) {
|
|
967
980
|
const fedFeature = coreFeatures.getByIdentity(federationIdentity);
|
|
968
981
|
if (fedFeature) {
|
|
969
|
-
completeFed2SubgraphSchema(schema);
|
|
982
|
+
return completeFed2SubgraphSchema(schema);
|
|
970
983
|
} else {
|
|
971
|
-
completeFed1SubgraphSchema(schema);
|
|
984
|
+
return completeFed1SubgraphSchema(schema);
|
|
972
985
|
}
|
|
973
986
|
} else {
|
|
974
987
|
const fedLink = schema.schemaDefinition.appliedDirectivesOf(linkDirectiveDefaultName).find(isFedSpecLinkDirective);
|
|
975
988
|
if (fedLink) {
|
|
976
|
-
linkSpec.addToSchema(schema);
|
|
977
|
-
|
|
989
|
+
const errors = linkSpec.addToSchema(schema);
|
|
990
|
+
if (errors.length > 0) {
|
|
991
|
+
return errors;
|
|
992
|
+
}
|
|
993
|
+
return completeFed2SubgraphSchema(schema);
|
|
978
994
|
} else {
|
|
979
|
-
completeFed1SubgraphSchema(schema);
|
|
995
|
+
return completeFed1SubgraphSchema(schema);
|
|
980
996
|
}
|
|
981
997
|
}
|
|
982
998
|
}
|
|
@@ -986,15 +1002,16 @@ function isFedSpecLinkDirective(directive: Directive<SchemaDefinition>): directi
|
|
|
986
1002
|
return directive.name === linkDirectiveDefaultName && args['url'] && (args['url'] as string).startsWith(federationIdentity);
|
|
987
1003
|
}
|
|
988
1004
|
|
|
989
|
-
function completeFed1SubgraphSchema(schema: Schema) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1005
|
+
function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
|
|
1006
|
+
return [
|
|
1007
|
+
fieldSetTypeSpec.checkOrAdd(schema, '_' + fieldSetTypeSpec.name),
|
|
1008
|
+
keyDirectiveSpec.checkOrAdd(schema),
|
|
1009
|
+
requiresDirectiveSpec.checkOrAdd(schema),
|
|
1010
|
+
providesDirectiveSpec.checkOrAdd(schema),
|
|
1011
|
+
extendsDirectiveSpec.checkOrAdd(schema),
|
|
1012
|
+
externalDirectiveSpec.checkOrAdd(schema),
|
|
1013
|
+
tagSpec.tagDirectiveSpec.checkOrAdd(schema),
|
|
1014
|
+
].flat();
|
|
998
1015
|
}
|
|
999
1016
|
|
|
1000
1017
|
function completeFed2SubgraphSchema(schema: Schema) {
|
|
@@ -1006,13 +1023,13 @@ function completeFed2SubgraphSchema(schema: Schema) {
|
|
|
1006
1023
|
|
|
1007
1024
|
const spec = FEDERATION_VERSIONS.find(fedFeature.url.version);
|
|
1008
1025
|
if (!spec) {
|
|
1009
|
-
|
|
1026
|
+
return [ERRORS.UNKNOWN_FEDERATION_LINK_VERSION.err({
|
|
1010
1027
|
message: `Invalid version ${fedFeature.url.version} for the federation feature in @link direction on schema`,
|
|
1011
1028
|
nodes: fedFeature.directive.sourceAST
|
|
1012
|
-
});
|
|
1029
|
+
})];
|
|
1013
1030
|
}
|
|
1014
1031
|
|
|
1015
|
-
spec.addElementsToSchema(schema);
|
|
1032
|
+
return spec.addElementsToSchema(schema);
|
|
1016
1033
|
}
|
|
1017
1034
|
|
|
1018
1035
|
export function parseFieldSetArgument({
|