@apollo/federation-internals 2.0.0-alpha.0 → 2.0.0-alpha.4
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 +18 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +3 -3
- package/dist/buildSchema.js.map +1 -1
- package/dist/coreSpec.d.ts +1 -1
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +2 -0
- package/dist/coreSpec.js.map +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/debug.js +7 -23
- package/dist/debug.js.map +1 -1
- package/dist/definitions.d.ts +28 -13
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +132 -71
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +87 -3
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +143 -5
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +60 -8
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +7 -6
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +227 -81
- package/dist/federation.js.map +1 -1
- package/dist/genErrorCodeDoc.d.ts +2 -0
- package/dist/genErrorCodeDoc.d.ts.map +1 -0
- package/dist/genErrorCodeDoc.js +55 -0
- package/dist/genErrorCodeDoc.js.map +1 -0
- package/dist/inaccessibleSpec.d.ts +1 -1
- package/dist/inaccessibleSpec.d.ts.map +1 -1
- package/dist/inaccessibleSpec.js +5 -5
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/joinSpec.d.ts.map +1 -1
- package/dist/joinSpec.js +6 -5
- package/dist/joinSpec.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +16 -16
- package/dist/operations.js.map +1 -1
- package/dist/print.d.ts +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +4 -4
- package/dist/print.js.map +1 -1
- package/dist/suggestions.js +1 -1
- package/dist/suggestions.js.map +1 -1
- package/dist/tagSpec.d.ts +2 -2
- package/dist/tagSpec.d.ts.map +1 -1
- package/dist/tagSpec.js +10 -2
- package/dist/tagSpec.js.map +1 -1
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +82 -1
- package/dist/utils.js.map +1 -1
- package/dist/validate.js.map +1 -1
- package/dist/values.d.ts +2 -1
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +29 -4
- package/dist/values.js.map +1 -1
- package/package.json +5 -6
- package/src/__tests__/definitions.test.ts +3 -3
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +432 -0
- package/src/__tests__/matchers/toMatchString.ts +2 -2
- package/src/__tests__/removeInaccessibleElements.test.ts +8 -8
- package/src/__tests__/subgraphValidation.test.ts +452 -0
- package/src/__tests__/utils.test.ts +92 -0
- package/src/buildSchema.ts +12 -11
- package/src/coreSpec.ts +12 -10
- package/src/debug.ts +8 -25
- package/src/definitions.ts +249 -115
- package/src/error.ts +334 -7
- package/src/extractSubgraphsFromSupergraph.ts +80 -19
- package/src/federation.ts +299 -138
- package/src/genErrorCodeDoc.ts +69 -0
- package/src/inaccessibleSpec.ts +13 -8
- package/src/joinSpec.ts +11 -8
- package/src/operations.ts +40 -38
- package/src/print.ts +8 -8
- package/src/suggestions.ts +1 -1
- package/src/tagSpec.ts +12 -7
- package/src/types.ts +1 -1
- package/src/utils.ts +109 -0
- package/src/validate.ts +4 -4
- package/src/values.ts +51 -9
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts
CHANGED
|
@@ -18,21 +18,40 @@ import {
|
|
|
18
18
|
baseType,
|
|
19
19
|
isInterfaceType,
|
|
20
20
|
isObjectType,
|
|
21
|
+
isListType,
|
|
22
|
+
isUnionType,
|
|
21
23
|
sourceASTs,
|
|
22
24
|
VariableDefinitions,
|
|
23
25
|
InterfaceType,
|
|
24
|
-
InputFieldDefinition
|
|
26
|
+
InputFieldDefinition,
|
|
27
|
+
isCompositeType
|
|
25
28
|
} from "./definitions";
|
|
26
|
-
import { assert } from "./utils";
|
|
29
|
+
import { assert, OrderedMap } from "./utils";
|
|
27
30
|
import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
28
31
|
import { specifiedSDLRules } from "graphql/validation/specifiedRules";
|
|
29
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
ASTNode,
|
|
34
|
+
DocumentNode,
|
|
35
|
+
GraphQLError,
|
|
36
|
+
Kind,
|
|
37
|
+
KnownTypeNamesRule,
|
|
38
|
+
parse,
|
|
39
|
+
PossibleTypeExtensionsRule,
|
|
40
|
+
print as printAST,
|
|
41
|
+
Source,
|
|
42
|
+
DirectiveLocation,
|
|
43
|
+
} from "graphql";
|
|
30
44
|
import { defaultPrintOptions, printDirectiveDefinition } from "./print";
|
|
31
45
|
import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
|
|
32
46
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
33
47
|
import { parseSelectionSet, SelectionSet } from './operations';
|
|
34
48
|
import { tagLocations, TAG_VERSIONS } from "./tagSpec";
|
|
35
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
errorCodeDef,
|
|
51
|
+
ErrorCodeDefinition,
|
|
52
|
+
ERRORS,
|
|
53
|
+
} from "./error";
|
|
54
|
+
import { ERROR_CATEGORIES } from ".";
|
|
36
55
|
|
|
37
56
|
export const entityTypeName = '_Entity';
|
|
38
57
|
export const serviceTypeName = '_Service';
|
|
@@ -44,16 +63,16 @@ export const extendsDirectiveName = 'extends';
|
|
|
44
63
|
export const externalDirectiveName = 'external';
|
|
45
64
|
export const requiresDirectiveName = 'requires';
|
|
46
65
|
export const providesDirectiveName = 'provides';
|
|
47
|
-
// TODO: so far, it seems we allow tag to appear without a corresponding
|
|
66
|
+
// TODO: so far, it seems we allow tag to appear without a corresponding definition, so we add it as a built-in.
|
|
48
67
|
// If we change our mind, we should change this.
|
|
49
68
|
export const tagDirectiveName = 'tag';
|
|
50
69
|
|
|
51
70
|
export const serviceFieldName = '_service';
|
|
52
71
|
export const entitiesFieldName = '_entities';
|
|
53
72
|
|
|
54
|
-
const tagSpec = TAG_VERSIONS.latest()
|
|
73
|
+
const tagSpec = TAG_VERSIONS.latest();
|
|
55
74
|
|
|
56
|
-
// We don't let user use this as a subgraph name. That allows us to use it in `query graphs` to name the source of roots
|
|
75
|
+
// We don't let user use this as a subgraph name. That allows us to use it in `query graphs` to name the source of roots
|
|
57
76
|
// in the "federated query graph" without worrying about conflict (see `FEDERATED_GRAPH_ROOT_SOURCE` in `querygraph.ts`).
|
|
58
77
|
// (note that we could deal with this in other ways, but having a graph named '_' feels like a terrible idea anyway, so
|
|
59
78
|
// disallowing it feels like more a good thing than a real restriction).
|
|
@@ -106,26 +125,51 @@ function validateFieldSetSelections(
|
|
|
106
125
|
for (const selection of selectionSet.selections()) {
|
|
107
126
|
if (selection.kind === 'FieldSelection') {
|
|
108
127
|
const field = selection.element().definition;
|
|
109
|
-
if (field.hasArguments()) {
|
|
110
|
-
throw new GraphQLError(`field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`, field.sourceAST);
|
|
111
|
-
}
|
|
112
|
-
// The field must be external if we don't allow non-external leaf fields, it's a leaft, and we haven't traversed an external field in parent chain leading here.
|
|
113
|
-
const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents;
|
|
114
128
|
const isExternal = externalTester.isExternal(field);
|
|
129
|
+
// We collect the field as external before any other validation to avoid getting a (confusing)
|
|
130
|
+
// "external unused" error on top of another error due to exiting that method too early.
|
|
115
131
|
if (isExternal) {
|
|
116
132
|
externalFieldCoordinatesCollector.push(field.coordinate);
|
|
117
|
-
}
|
|
133
|
+
}
|
|
134
|
+
if (field.hasArguments()) {
|
|
135
|
+
throw ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err({
|
|
136
|
+
message: `field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`,
|
|
137
|
+
nodes: field.sourceAST
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// The field must be external if we don't allow non-external leaf fields, it's a leaf, and we haven't traversed an external field in parent chain leading here.
|
|
141
|
+
const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents;
|
|
142
|
+
if (!isExternal && mustBeExternal) {
|
|
143
|
+
const errorCode = ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName);
|
|
118
144
|
if (externalTester.isFakeExternal(field)) {
|
|
119
|
-
throw
|
|
120
|
-
`field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
|
|
121
|
-
|
|
122
|
-
field.sourceAST
|
|
145
|
+
throw errorCode.err({
|
|
146
|
+
message: `field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
|
|
147
|
+
+ `(while it is marked @${externalDirectiveName}, it is a @${keyDirectiveName} field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`,
|
|
148
|
+
nodes: field.sourceAST
|
|
149
|
+
});
|
|
123
150
|
} else {
|
|
124
|
-
throw
|
|
151
|
+
throw errorCode.err({
|
|
152
|
+
message: `field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${externalDirectiveName})`,
|
|
153
|
+
nodes: field.sourceAST
|
|
154
|
+
});
|
|
125
155
|
}
|
|
126
156
|
}
|
|
127
157
|
if (selection.selectionSet) {
|
|
128
|
-
|
|
158
|
+
// When passing the 'hasExternalInParents', the field might be external himself, but we may also have
|
|
159
|
+
// the case where the field parent is an interface and some implementation of the field are external, in
|
|
160
|
+
// which case we should say we have an external on the path, because we may have one.
|
|
161
|
+
let newHasExternalInParents = hasExternalInParents || isExternal;
|
|
162
|
+
const parentType = field.parent;
|
|
163
|
+
if (!newHasExternalInParents && isInterfaceType(parentType)) {
|
|
164
|
+
for (const implem of parentType.possibleRuntimeTypes()) {
|
|
165
|
+
const fieldInImplem = implem.field(field.name);
|
|
166
|
+
if (fieldInImplem && externalTester.isExternal(fieldInImplem)) {
|
|
167
|
+
newHasExternalInParents = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
validateFieldSetSelections(directiveName, selection.selectionSet, newHasExternalInParents, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
|
|
129
173
|
}
|
|
130
174
|
} else {
|
|
131
175
|
validateFieldSetSelections(directiveName, selection.selectionSet, hasExternalInParents, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
|
|
@@ -136,69 +180,97 @@ function validateFieldSetSelections(
|
|
|
136
180
|
function validateFieldSet(
|
|
137
181
|
type: CompositeType,
|
|
138
182
|
directive: Directive<any, {fields: any}>,
|
|
139
|
-
targetDescription: string,
|
|
140
183
|
externalTester: ExternalTester,
|
|
141
184
|
externalFieldCoordinatesCollector: string[],
|
|
142
185
|
allowOnNonExternalLeafFields: boolean,
|
|
186
|
+
onFields?: (field: FieldDefinition<any>) => void,
|
|
143
187
|
): GraphQLError | undefined {
|
|
144
188
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const nodes = sourceASTs(directive);
|
|
154
|
-
if (e.nodes) {
|
|
155
|
-
nodes.push(...e.nodes);
|
|
156
|
-
}
|
|
157
|
-
let msg = e.message.trim();
|
|
158
|
-
// The rule for validating @requires in fed 1 was not properly recursive, so people upgrading
|
|
159
|
-
// may have a @require that selects some fields but without declaring those fields on the
|
|
160
|
-
// subgraph. As we fixed the validation, this will now fail, but we try here to provide some
|
|
161
|
-
// hint for those users for how to fix the problem.
|
|
162
|
-
// Note that this is a tad fragile to rely on the error message like that, but worth case, a
|
|
163
|
-
// future change make us not show the hint and that's not the end of the world.
|
|
164
|
-
if (msg.startsWith('Cannot query field')) {
|
|
165
|
-
if (msg.endsWith('.')) {
|
|
166
|
-
msg = msg.slice(0, msg.length - 1);
|
|
189
|
+
// Note that `parseFieldSetArgument` already properly format the error, hence the separate try-catch.
|
|
190
|
+
const fieldAcessor = onFields
|
|
191
|
+
? (type: CompositeType, fieldName: string) => {
|
|
192
|
+
const field = type.field(fieldName);
|
|
193
|
+
if (field) {
|
|
194
|
+
onFields(field);
|
|
195
|
+
}
|
|
196
|
+
return field;
|
|
167
197
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
198
|
+
: undefined;
|
|
199
|
+
const selectionSet = parseFieldSetArgument(type, directive, fieldAcessor);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
validateFieldSetSelections(directive.name, selectionSet, false, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
|
|
203
|
+
return undefined;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (!(e instanceof GraphQLError)) {
|
|
206
|
+
throw e;
|
|
207
|
+
}
|
|
208
|
+
const nodes = sourceASTs(directive);
|
|
209
|
+
if (e.nodes) {
|
|
210
|
+
nodes.push(...e.nodes);
|
|
172
211
|
}
|
|
212
|
+
const codeDef = errorCodeDef(e) ?? ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name);
|
|
213
|
+
return codeDef.err({
|
|
214
|
+
message: `${fieldSetErrorDescriptor(directive)}: ${e.message.trim()}`,
|
|
215
|
+
nodes,
|
|
216
|
+
originalError: e,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
if (e instanceof GraphQLError) {
|
|
221
|
+
return e;
|
|
222
|
+
} else {
|
|
223
|
+
throw e;
|
|
173
224
|
}
|
|
174
|
-
return new GraphQLError(`On ${targetDescription}, for ${directive}: ${msg}`, nodes);
|
|
175
225
|
}
|
|
176
226
|
}
|
|
177
227
|
|
|
228
|
+
function fieldSetErrorDescriptor(directive: Directive<any, {fields: any}>): string {
|
|
229
|
+
return `On ${fieldSetTargetDescription(directive)}, for ${directiveStrUsingASTIfPossible(directive)}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// This method is called to display @key, @provides or @requires directives in error message in place where the directive `fields`
|
|
233
|
+
// argument might be invalid because it was not a string in the underlying AST. If that's the case, we want to use the AST to
|
|
234
|
+
// print the directive or the message might be a bit confusing for the user.
|
|
235
|
+
function directiveStrUsingASTIfPossible(directive: Directive<any>): string {
|
|
236
|
+
return directive.sourceAST ? printAST(directive.sourceAST) : directive.toString();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function fieldSetTargetDescription(directive: Directive<any, {fields: any}>): string {
|
|
240
|
+
const targetKind = directive.parent instanceof FieldDefinition ? "field" : "type";
|
|
241
|
+
return `${targetKind} "${directive.parent?.coordinate}"`;
|
|
242
|
+
}
|
|
243
|
+
|
|
178
244
|
function validateAllFieldSet<TParent extends SchemaElement<any, any>>(
|
|
179
245
|
definition: DirectiveDefinition<{fields: any}>,
|
|
180
246
|
targetTypeExtractor: (element: TParent) => CompositeType,
|
|
181
|
-
targetDescriptionExtractor: (element: TParent) => string,
|
|
182
247
|
errorCollector: GraphQLError[],
|
|
183
248
|
externalTester: ExternalTester,
|
|
184
249
|
externalFieldCoordinatesCollector: string[],
|
|
185
250
|
isOnParentType: boolean,
|
|
186
251
|
allowOnNonExternalLeafFields: boolean,
|
|
252
|
+
onFields?: (field: FieldDefinition<any>) => void,
|
|
187
253
|
): void {
|
|
188
254
|
for (const application of definition.applications()) {
|
|
189
|
-
const elt = application.parent
|
|
255
|
+
const elt = application.parent as TParent;
|
|
190
256
|
const type = targetTypeExtractor(elt);
|
|
191
|
-
const targetDescription = targetDescriptionExtractor(elt);
|
|
192
257
|
const parentType = isOnParentType ? type : (elt.parent as NamedType);
|
|
193
258
|
if (isInterfaceType(parentType)) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
259
|
+
const code = ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name);
|
|
260
|
+
errorCollector.push(code.err({
|
|
261
|
+
message: isOnParentType
|
|
262
|
+
? `Cannot use ${definition.coordinate} on interface "${parentType.coordinate}": ${definition.coordinate} is not yet supported on interfaces`
|
|
263
|
+
: `Cannot use ${definition.coordinate} on ${fieldSetTargetDescription(application)} of parent type "${parentType}": ${definition.coordinate} is not yet supported within interfaces`,
|
|
264
|
+
nodes: sourceASTs(application).concat(isOnParentType ? [] : sourceASTs(type)),
|
|
265
|
+
}));
|
|
200
266
|
}
|
|
201
|
-
const error = validateFieldSet(
|
|
267
|
+
const error = validateFieldSet(
|
|
268
|
+
type,
|
|
269
|
+
application,
|
|
270
|
+
externalTester,
|
|
271
|
+
externalFieldCoordinatesCollector,
|
|
272
|
+
allowOnNonExternalLeafFields,
|
|
273
|
+
onFields);
|
|
202
274
|
if (error) {
|
|
203
275
|
errorCollector.push(error);
|
|
204
276
|
}
|
|
@@ -225,18 +297,18 @@ function validateAllExternalFieldsUsed(
|
|
|
225
297
|
}
|
|
226
298
|
|
|
227
299
|
if (!isFieldSatisfyingInterface(field)) {
|
|
228
|
-
errorCollector.push(
|
|
229
|
-
`Field ${field.coordinate} is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
|
|
300
|
+
errorCollector.push(ERRORS.EXTERNAL_UNUSED.err({
|
|
301
|
+
message: `Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
|
|
230
302
|
+ ' the field declaration has no use and should be removed (or the field should not be @external).',
|
|
231
|
-
field.sourceAST
|
|
232
|
-
));
|
|
303
|
+
nodes: field.sourceAST,
|
|
304
|
+
}));
|
|
233
305
|
}
|
|
234
306
|
}
|
|
235
307
|
}
|
|
236
308
|
}
|
|
237
309
|
|
|
238
310
|
function isFieldSatisfyingInterface(field: FieldDefinition<ObjectType | InterfaceType>): boolean {
|
|
239
|
-
return field.parent
|
|
311
|
+
return field.parent.interfaces().some(itf => itf.field(field.name));
|
|
240
312
|
}
|
|
241
313
|
|
|
242
314
|
export class FederationBuiltIns extends BuiltIns {
|
|
@@ -255,25 +327,25 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
255
327
|
const fieldSetType = new NonNullType(schema.type(fieldSetTypeName)!);
|
|
256
328
|
|
|
257
329
|
// Note that we allow @key on interfaces in the definition to not break backward compatibility, because it has historically unfortunately be declared this way, but
|
|
258
|
-
// @key is actually not
|
|
330
|
+
// @key is actually not supported on interfaces at the moment, so if if is "used" then it is rejected.
|
|
259
331
|
const keyDirective = this.addBuiltInDirective(schema, keyDirectiveName)
|
|
260
|
-
.addLocations(
|
|
332
|
+
.addLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE);
|
|
261
333
|
// TODO: I believe fed 1 does not mark key repeatable and relax validation to accept repeating non-repeatable directive.
|
|
262
334
|
// Do we want to perpetuate this? (Obviously, this is for historical reason and some graphQL implementations still do
|
|
263
|
-
// not support 'repeatable'. But since this code does not kick in within users' code, not sure we have to
|
|
335
|
+
// not support 'repeatable'. But since this code does not kick in within users' code, not sure we have to accommodate
|
|
264
336
|
// for those implementations. Besides, we _do_ accept if people re-defined @key as non-repeatable).
|
|
265
337
|
keyDirective.repeatable = true;
|
|
266
338
|
keyDirective.addArgument('fields', fieldSetType);
|
|
267
339
|
|
|
268
340
|
this.addBuiltInDirective(schema, extendsDirectiveName)
|
|
269
|
-
.addLocations(
|
|
341
|
+
.addLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE);
|
|
270
342
|
|
|
271
343
|
this.addBuiltInDirective(schema, externalDirectiveName)
|
|
272
|
-
.addLocations(
|
|
344
|
+
.addLocations(DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION);
|
|
273
345
|
|
|
274
346
|
for (const name of [requiresDirectiveName, providesDirectiveName]) {
|
|
275
347
|
this.addBuiltInDirective(schema, name)
|
|
276
|
-
.addLocations(
|
|
348
|
+
.addLocations(DirectiveLocation.FIELD_DEFINITION)
|
|
277
349
|
.addArgument('fields', fieldSetType);
|
|
278
350
|
}
|
|
279
351
|
|
|
@@ -312,7 +384,7 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
312
384
|
// Adds the _entities and _service fields to the root query type.
|
|
313
385
|
const queryRoot = schema.schemaDefinition.root("query");
|
|
314
386
|
const queryType = queryRoot ? queryRoot.type : schema.addType(new ObjectType("Query"));
|
|
315
|
-
|
|
387
|
+
const entityField = queryType.field(entitiesFieldName);
|
|
316
388
|
if (hasEntities) {
|
|
317
389
|
const anyType = schema.type(anyTypeName);
|
|
318
390
|
assert(anyType, `The schema should have the _Any type`);
|
|
@@ -348,13 +420,12 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
348
420
|
// composition error.
|
|
349
421
|
const existing = schema.type(defaultName);
|
|
350
422
|
if (existing) {
|
|
351
|
-
errors.push(
|
|
352
|
-
`
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
));
|
|
423
|
+
errors.push(ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err({
|
|
424
|
+
message: `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): `
|
|
425
|
+
+ 'this is not supported by federation. '
|
|
426
|
+
+ 'If a root type does not use its default name, there should be no other type with that default name.',
|
|
427
|
+
nodes: sourceASTs(type, existing),
|
|
428
|
+
}));
|
|
358
429
|
}
|
|
359
430
|
type.rename(defaultName);
|
|
360
431
|
}
|
|
@@ -368,24 +439,31 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
368
439
|
validateAllFieldSet<CompositeType>(
|
|
369
440
|
keyDirective,
|
|
370
441
|
type => type,
|
|
371
|
-
type => `type "${type}"`,
|
|
372
442
|
errors,
|
|
373
443
|
externalTester,
|
|
374
444
|
externalFieldsInFedDirectivesCoordinates,
|
|
375
445
|
true,
|
|
376
|
-
true
|
|
446
|
+
true,
|
|
447
|
+
field => {
|
|
448
|
+
if (isListType(field.type!) || isUnionType(field.type!) || isInterfaceType(field.type!)) {
|
|
449
|
+
let kind: string = field.type!.kind;
|
|
450
|
+
kind = kind.slice(0, kind.length - 'Type'.length);
|
|
451
|
+
throw ERRORS.KEY_FIELDS_SELECT_INVALID_TYPE.err({
|
|
452
|
+
message: `field "${field.coordinate}" is a ${kind} type which is not allowed in @key`
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
377
456
|
);
|
|
378
457
|
// Note that we currently reject @requires where a leaf field of the selection is not external,
|
|
379
458
|
// because if it's provided by the current subgraph, why "requires" it? That said, it's not 100%
|
|
380
|
-
//
|
|
459
|
+
// nonsensical if you wanted a local field to be part of the subgraph fetch even if it's not
|
|
381
460
|
// truly queried _for some reason_. But it's unclear such reasons exists, so for now we prefer
|
|
382
461
|
// rejecting it as it also make it less likely user misunderstand what @requires actually do.
|
|
383
462
|
// But we could consider lifting that limitation if users comes with a good rational for allowing
|
|
384
463
|
// it.
|
|
385
464
|
validateAllFieldSet<FieldDefinition<CompositeType>>(
|
|
386
465
|
this.requiresDirective(schema),
|
|
387
|
-
field => field.parent
|
|
388
|
-
field => `field "${field.coordinate}"`,
|
|
466
|
+
field => field.parent,
|
|
389
467
|
errors,
|
|
390
468
|
externalTester,
|
|
391
469
|
externalFieldsInFedDirectivesCoordinates,
|
|
@@ -394,20 +472,23 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
394
472
|
);
|
|
395
473
|
// Note that like for @requires above, we error out if a leaf field of the selection is not
|
|
396
474
|
// external in a @provides (we pass `false` for the `allowOnNonExternalLeafFields` parameter),
|
|
397
|
-
// but contrarily to @requires, there is
|
|
398
|
-
// of a field already provides is 100%
|
|
475
|
+
// but contrarily to @requires, there is probably no reason to ever change this, as a @provides
|
|
476
|
+
// of a field already provides is 100% nonsensical.
|
|
399
477
|
validateAllFieldSet<FieldDefinition<CompositeType>>(
|
|
400
478
|
this.providesDirective(schema),
|
|
401
479
|
field => {
|
|
480
|
+
if (externalTester.isExternal(field)) {
|
|
481
|
+
throw new GraphQLError(`Cannot have both @provides and @external on field "${field.coordinate}"`, field.sourceAST);
|
|
482
|
+
}
|
|
402
483
|
const type = baseType(field.type!);
|
|
403
|
-
if (!
|
|
404
|
-
throw
|
|
405
|
-
`Invalid @provides directive on field "${field.coordinate}": field has type "${field.type}" which is not
|
|
406
|
-
field.sourceAST
|
|
484
|
+
if (!isCompositeType(type)) {
|
|
485
|
+
throw ERRORS.PROVIDES_ON_NON_OBJECT_FIELD.err({
|
|
486
|
+
message: `Invalid @provides directive on field "${field.coordinate}": field has type "${field.type}" which is not a Composite Type`,
|
|
487
|
+
nodes: field.sourceAST,
|
|
488
|
+
});
|
|
407
489
|
}
|
|
408
490
|
return type;
|
|
409
491
|
},
|
|
410
|
-
field => `field ${field.coordinate}`,
|
|
411
492
|
errors,
|
|
412
493
|
externalTester,
|
|
413
494
|
externalFieldsInFedDirectivesCoordinates,
|
|
@@ -437,11 +518,11 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
437
518
|
return this.getTypedDirective(schema, keyDirectiveName);
|
|
438
519
|
}
|
|
439
520
|
|
|
440
|
-
extendsDirective(schema: Schema): DirectiveDefinition<
|
|
521
|
+
extendsDirective(schema: Schema): DirectiveDefinition<Record<string, never>> {
|
|
441
522
|
return this.getTypedDirective(schema, extendsDirectiveName);
|
|
442
523
|
}
|
|
443
524
|
|
|
444
|
-
externalDirective(schema: Schema): DirectiveDefinition<
|
|
525
|
+
externalDirective(schema: Schema): DirectiveDefinition<Record<string, never>> {
|
|
445
526
|
return this.getTypedDirective(schema, externalDirectiveName);
|
|
446
527
|
}
|
|
447
528
|
|
|
@@ -460,7 +541,7 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
460
541
|
maybeUpdateSubgraphDocument(schema: Schema, document: DocumentNode): DocumentNode {
|
|
461
542
|
document = super.maybeUpdateSubgraphDocument(schema, document);
|
|
462
543
|
|
|
463
|
-
|
|
544
|
+
const definitions = document.definitions.concat();
|
|
464
545
|
for (const directiveName of FEDERATION_DIRECTIVES) {
|
|
465
546
|
const directive = schema.directive(directiveName);
|
|
466
547
|
assert(directive, 'This method should only have been called on a schema with federation built-ins')
|
|
@@ -473,7 +554,7 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
473
554
|
}
|
|
474
555
|
|
|
475
556
|
return {
|
|
476
|
-
kind:
|
|
557
|
+
kind: Kind.DOCUMENT,
|
|
477
558
|
loc: document.loc,
|
|
478
559
|
definitions
|
|
479
560
|
};
|
|
@@ -495,7 +576,7 @@ export function isFederationTypeName(typeName: string): boolean {
|
|
|
495
576
|
}
|
|
496
577
|
|
|
497
578
|
export function isFederationField(field: FieldDefinition<CompositeType>): boolean {
|
|
498
|
-
if (field.parent === field.schema()
|
|
579
|
+
if (field.parent === field.schema().schemaDefinition.root("query")?.type) {
|
|
499
580
|
return FEDERATION_ROOT_FIELDS.includes(field.name);
|
|
500
581
|
}
|
|
501
582
|
return false;
|
|
@@ -509,14 +590,17 @@ export function isEntityType(type: NamedType): boolean {
|
|
|
509
590
|
return type.kind == "ObjectType" && type.hasAppliedDirective(keyDirectiveName);
|
|
510
591
|
}
|
|
511
592
|
|
|
512
|
-
function buildSubgraph(name: string, source: DocumentNode | string): Schema {
|
|
593
|
+
export function buildSubgraph(name: string, source: DocumentNode | string): Schema {
|
|
513
594
|
try {
|
|
514
595
|
return typeof source === 'string'
|
|
515
596
|
? buildSchema(new Source(source, name), federationBuiltIns)
|
|
516
597
|
: buildSchemaFromAST(source, federationBuiltIns);
|
|
517
598
|
} catch (e) {
|
|
518
599
|
if (e instanceof GraphQLError) {
|
|
519
|
-
|
|
600
|
+
// Note that `addSubgraphToError` only adds the provided code if the original error
|
|
601
|
+
// didn't have one, and the only one that will not have a code are GraphQL errors
|
|
602
|
+
// (since we assign specific codes to the federation errors).
|
|
603
|
+
throw addSubgraphToError(e, name, ERRORS.INVALID_GRAPHQL);
|
|
520
604
|
} else {
|
|
521
605
|
throw e;
|
|
522
606
|
}
|
|
@@ -528,22 +612,75 @@ export function parseFieldSetArgument(
|
|
|
528
612
|
directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>,
|
|
529
613
|
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
530
614
|
): SelectionSet {
|
|
531
|
-
|
|
615
|
+
try {
|
|
616
|
+
const selectionSet = parseSelectionSet(parentType, validateFieldSetValue(directive), new VariableDefinitions(), undefined, fieldAccessor);
|
|
617
|
+
selectionSet.validate();
|
|
618
|
+
return selectionSet;
|
|
619
|
+
} catch (e) {
|
|
620
|
+
if (!(e instanceof GraphQLError)) {
|
|
621
|
+
throw e;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const nodes = sourceASTs(directive);
|
|
625
|
+
if (e.nodes) {
|
|
626
|
+
nodes.push(...e.nodes);
|
|
627
|
+
}
|
|
628
|
+
let msg = e.message.trim();
|
|
629
|
+
// The rule for validating @requires in fed 1 was not properly recursive, so people upgrading
|
|
630
|
+
// may have a @require that selects some fields but without declaring those fields on the
|
|
631
|
+
// subgraph. As we fixed the validation, this will now fail, but we try here to provide some
|
|
632
|
+
// hint for those users for how to fix the problem.
|
|
633
|
+
// Note that this is a tad fragile to rely on the error message like that, but worth case, a
|
|
634
|
+
// future change make us not show the hint and that's not the end of the world.
|
|
635
|
+
if (msg.startsWith('Cannot query field')) {
|
|
636
|
+
if (msg.endsWith('.')) {
|
|
637
|
+
msg = msg.slice(0, msg.length - 1);
|
|
638
|
+
}
|
|
639
|
+
if (directive.name === keyDirectiveName) {
|
|
640
|
+
msg = msg + ' (the field should be either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).';
|
|
641
|
+
} else {
|
|
642
|
+
msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const codeDef = errorCodeDef(e) ?? ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name);
|
|
647
|
+
throw codeDef.err({
|
|
648
|
+
message: `${fieldSetErrorDescriptor(directive)}: ${msg}`,
|
|
649
|
+
nodes,
|
|
650
|
+
originalError: e,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
532
653
|
}
|
|
533
654
|
|
|
534
655
|
function validateFieldSetValue(directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>): string {
|
|
535
656
|
const fields = directive.arguments().fields;
|
|
657
|
+
const nodes = directive.sourceAST;
|
|
536
658
|
if (typeof fields !== 'string') {
|
|
537
|
-
throw
|
|
538
|
-
`Invalid value for argument ${directive.definition!.argument('fields')!.
|
|
539
|
-
|
|
540
|
-
);
|
|
659
|
+
throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({
|
|
660
|
+
message: `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`,
|
|
661
|
+
nodes,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
// While validating if the field is a string will work in most cases, this will not catch the case where the field argument was
|
|
665
|
+
// unquoted but parsed as an enum value (see federation/issues/850 in particular). So if we have the AST (which we will usually
|
|
666
|
+
// have in practice), use that to check that the argument was truly a string.
|
|
667
|
+
if (nodes && nodes.kind === 'Directive') {
|
|
668
|
+
for (const argNode of nodes.arguments ?? []) {
|
|
669
|
+
if (argNode.name.value === 'fields') {
|
|
670
|
+
if (argNode.value.kind !== 'StringValue') {
|
|
671
|
+
throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({
|
|
672
|
+
message: `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`,
|
|
673
|
+
nodes,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
541
679
|
}
|
|
680
|
+
|
|
542
681
|
return fields;
|
|
543
682
|
}
|
|
544
683
|
|
|
545
|
-
// 'ServiceDefinition' is originally defined in federation-js and we don't want to create a dependency
|
|
546
|
-
// of internals-js to that just for that interface.
|
|
547
684
|
export interface ServiceDefinition {
|
|
548
685
|
typeDefs: DocumentNode;
|
|
549
686
|
name: string;
|
|
@@ -568,16 +705,11 @@ export function subgraphsFromServiceList(serviceList: ServiceDefinition[]): Subg
|
|
|
568
705
|
return errors.length === 0 ? subgraphs : errors;
|
|
569
706
|
}
|
|
570
707
|
|
|
571
|
-
// Simple wrapper around a
|
|
708
|
+
// Simple wrapper around a Subgraph[] that ensures that 1) we never mistakenly get 2 subgraph with the same name,
|
|
572
709
|
// 2) keep the subgraphs sorted by name (makes iteration more predictable). It also allow convenient access to
|
|
573
710
|
// a subgraph by name so behave like a map<string, Subgraph> in most ways (but with the previously mentioned benefits).
|
|
574
711
|
export class Subgraphs {
|
|
575
|
-
private readonly subgraphs
|
|
576
|
-
|
|
577
|
-
private idx(name: string): number {
|
|
578
|
-
// Note: we could do a binary search if we ever worry that a linear scan is too costly.
|
|
579
|
-
return this.subgraphs.findIndex(s => s.name === name);
|
|
580
|
-
}
|
|
712
|
+
private readonly subgraphs = new OrderedMap<string, Subgraph>();
|
|
581
713
|
|
|
582
714
|
add(subgraph: Subgraph): Subgraph;
|
|
583
715
|
add(name: string, url: string, schema: Schema | DocumentNode | string): Subgraph;
|
|
@@ -587,51 +719,46 @@ export class Subgraphs {
|
|
|
587
719
|
: subgraphOrName;
|
|
588
720
|
|
|
589
721
|
if (toAdd.name === FEDERATION_RESERVED_SUBGRAPH_NAME) {
|
|
590
|
-
throw
|
|
722
|
+
throw ERRORS.INVALID_SUBGRAPH_NAME.err({ message: `Invalid name ${FEDERATION_RESERVED_SUBGRAPH_NAME} for a subgraph: this name is reserved` });
|
|
591
723
|
}
|
|
592
724
|
|
|
593
|
-
|
|
594
|
-
if (idx >= 0) {
|
|
725
|
+
if (this.subgraphs.has(toAdd.name)) {
|
|
595
726
|
throw new Error(`A subgraph named ${toAdd.name} already exists` + (toAdd.url ? ` (with url '${toAdd.url}')` : ''));
|
|
596
727
|
}
|
|
597
|
-
this.subgraphs.
|
|
598
|
-
this.subgraphs.sort();
|
|
728
|
+
this.subgraphs.add(toAdd.name, toAdd);
|
|
599
729
|
return toAdd;
|
|
600
730
|
}
|
|
601
731
|
|
|
602
732
|
get(name: string): Subgraph | undefined {
|
|
603
|
-
|
|
604
|
-
return idx >= 0 ? this.subgraphs[idx] : undefined;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
getByIdx(idx: number): Subgraph {
|
|
608
|
-
return this.subgraphs[idx];
|
|
733
|
+
return this.subgraphs.get(name);
|
|
609
734
|
}
|
|
610
735
|
|
|
611
736
|
size(): number {
|
|
612
|
-
return this.subgraphs.
|
|
737
|
+
return this.subgraphs.size;
|
|
613
738
|
}
|
|
614
739
|
|
|
615
740
|
names(): readonly string[] {
|
|
616
|
-
return this.subgraphs.
|
|
741
|
+
return this.subgraphs.keys();
|
|
617
742
|
}
|
|
618
743
|
|
|
619
744
|
values(): readonly Subgraph[] {
|
|
620
|
-
return this.subgraphs;
|
|
745
|
+
return this.subgraphs.values();
|
|
621
746
|
}
|
|
622
747
|
|
|
623
|
-
[Symbol.iterator]() {
|
|
624
|
-
|
|
748
|
+
*[Symbol.iterator]() {
|
|
749
|
+
for (const subgraph of this.subgraphs) {
|
|
750
|
+
yield subgraph;
|
|
751
|
+
}
|
|
625
752
|
}
|
|
626
753
|
|
|
627
754
|
toString(): string {
|
|
628
|
-
return '[' + this.subgraphs.
|
|
755
|
+
return '[' + this.subgraphs.keys().join(', ') + ']'
|
|
629
756
|
}
|
|
630
757
|
}
|
|
631
758
|
|
|
632
759
|
export class Subgraph {
|
|
633
760
|
constructor(
|
|
634
|
-
readonly name: string,
|
|
761
|
+
readonly name: string,
|
|
635
762
|
readonly url: string,
|
|
636
763
|
readonly schema: Schema,
|
|
637
764
|
validateSchema: boolean = true
|
|
@@ -655,16 +782,36 @@ export function addSubgraphToASTNode(node: ASTNode, subgraph: string): SubgraphA
|
|
|
655
782
|
};
|
|
656
783
|
}
|
|
657
784
|
|
|
658
|
-
export function addSubgraphToError(e: GraphQLError, subgraphName: string): GraphQLError {
|
|
659
|
-
const updatedCauses = errorCauses(e)!.map(cause =>
|
|
660
|
-
`[${subgraphName}] ${cause.message}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
cause
|
|
666
|
-
|
|
667
|
-
|
|
785
|
+
export function addSubgraphToError(e: GraphQLError, subgraphName: string, errorCode?: ErrorCodeDefinition): GraphQLError {
|
|
786
|
+
const updatedCauses = errorCauses(e)!.map(cause => {
|
|
787
|
+
const message = `[${subgraphName}] ${cause.message}`;
|
|
788
|
+
const nodes = cause.nodes
|
|
789
|
+
? cause.nodes.map(node => addSubgraphToASTNode(node, subgraphName))
|
|
790
|
+
: undefined;
|
|
791
|
+
|
|
792
|
+
const code = errorCodeDef(cause) ?? errorCode;
|
|
793
|
+
if (code) {
|
|
794
|
+
return code.err({
|
|
795
|
+
message,
|
|
796
|
+
nodes,
|
|
797
|
+
source: cause.source,
|
|
798
|
+
positions: cause.positions,
|
|
799
|
+
path: cause.path,
|
|
800
|
+
originalError: cause.originalError,
|
|
801
|
+
extensions: cause.extensions,
|
|
802
|
+
});
|
|
803
|
+
} else {
|
|
804
|
+
return new GraphQLError(
|
|
805
|
+
message,
|
|
806
|
+
nodes,
|
|
807
|
+
cause.source,
|
|
808
|
+
cause.positions,
|
|
809
|
+
cause.path,
|
|
810
|
+
cause.originalError,
|
|
811
|
+
cause.extensions
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
668
815
|
|
|
669
816
|
return ErrGraphQLValidationFailed(updatedCauses);
|
|
670
817
|
}
|
|
@@ -682,7 +829,7 @@ export class ExternalTester {
|
|
|
682
829
|
return;
|
|
683
830
|
}
|
|
684
831
|
for (const key of keyDirective.applications()) {
|
|
685
|
-
const parent = key.parent
|
|
832
|
+
const parent = key.parent as CompositeType;
|
|
686
833
|
if (!(key.ofExtension() || parent.hasAppliedDirective(extendsDirectiveName))) {
|
|
687
834
|
continue;
|
|
688
835
|
}
|
|
@@ -708,4 +855,18 @@ export class ExternalTester {
|
|
|
708
855
|
isFakeExternal(field: FieldDefinition<any> | InputFieldDefinition) {
|
|
709
856
|
return this.fakeExternalFields.has(field.coordinate);
|
|
710
857
|
}
|
|
858
|
+
|
|
859
|
+
selectsAnyExternalField(selectionSet: SelectionSet): boolean {
|
|
860
|
+
for (const selection of selectionSet.selections()) {
|
|
861
|
+
if (selection.kind === 'FieldSelection' && this.isExternal(selection.element().definition)) {
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
if (selection.selectionSet) {
|
|
865
|
+
if (this.selectsAnyExternalField(selection.selectionSet)) {
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
711
872
|
}
|