@apollo/federation-internals 2.1.0-alpha.1 → 2.1.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 +13 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +13 -3
- package/dist/buildSchema.js.map +1 -1
- package/dist/definitions.d.ts +47 -8
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +137 -23
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +1 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +2 -0
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +9 -0
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +5 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +13 -5
- package/dist/federation.js.map +1 -1
- package/dist/federationSpec.d.ts +13 -9
- package/dist/federationSpec.d.ts.map +1 -1
- package/dist/federationSpec.js +27 -5
- package/dist/federationSpec.js.map +1 -1
- package/dist/inaccessibleSpec.js +1 -2
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/operations.d.ts +48 -21
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +329 -48
- 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 +1 -1
- package/dist/print.js.map +1 -1
- package/dist/schemaUpgrader.js +2 -2
- package/dist/schemaUpgrader.js.map +1 -1
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +31 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/definitions.test.ts +18 -0
- package/src/__tests__/operations.test.ts +217 -99
- package/src/__tests__/subgraphValidation.test.ts +2 -0
- package/src/buildSchema.ts +19 -5
- package/src/definitions.ts +217 -29
- package/src/error.ts +7 -0
- package/src/extractSubgraphsFromSupergraph.ts +20 -0
- package/src/federation.ts +16 -5
- package/src/federationSpec.ts +32 -5
- package/src/inaccessibleSpec.ts +2 -5
- package/src/operations.ts +520 -71
- package/src/print.ts +1 -1
- package/src/schemaUpgrader.ts +2 -2
- package/src/utils.ts +40 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/definitions.ts
CHANGED
|
@@ -15,9 +15,11 @@ import {
|
|
|
15
15
|
TypeNode,
|
|
16
16
|
VariableDefinitionNode,
|
|
17
17
|
VariableNode,
|
|
18
|
-
TypeSystemDefinitionNode,
|
|
19
18
|
SchemaDefinitionNode,
|
|
20
|
-
TypeDefinitionNode
|
|
19
|
+
TypeDefinitionNode,
|
|
20
|
+
DefinitionNode,
|
|
21
|
+
DirectiveDefinitionNode,
|
|
22
|
+
DirectiveNode,
|
|
21
23
|
} from "graphql";
|
|
22
24
|
import {
|
|
23
25
|
CoreImport,
|
|
@@ -32,7 +34,7 @@ import {
|
|
|
32
34
|
import { assert, mapValues, MapWithCachedArrays, setValues } from "./utils";
|
|
33
35
|
import { withDefaultValues, valueEquals, valueToString, valueToAST, variablesInValue, valueFromAST, valueNodeToConstValueNode, argumentsEquals } from "./values";
|
|
34
36
|
import { removeInaccessibleElements } from "./inaccessibleSpec";
|
|
35
|
-
import { printSchema } from './print';
|
|
37
|
+
import { printDirectiveDefinition, printSchema } from './print';
|
|
36
38
|
import { sameType } from './types';
|
|
37
39
|
import { addIntrospectionFields, introspectionFieldNames, isIntrospectionName } from "./introspection";
|
|
38
40
|
import { err } from '@apollo/core-schema';
|
|
@@ -291,6 +293,32 @@ export const executableDirectiveLocations: DirectiveLocation[] = [
|
|
|
291
293
|
DirectiveLocation.VARIABLE_DEFINITION,
|
|
292
294
|
];
|
|
293
295
|
|
|
296
|
+
const executableDirectiveLocationsSet = new Set(executableDirectiveLocations);
|
|
297
|
+
|
|
298
|
+
export function isExecutableDirectiveLocation(loc: DirectiveLocation): boolean {
|
|
299
|
+
return executableDirectiveLocationsSet.has(loc);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const typeSystemDirectiveLocations: DirectiveLocation[] = [
|
|
303
|
+
DirectiveLocation.SCHEMA,
|
|
304
|
+
DirectiveLocation.SCALAR,
|
|
305
|
+
DirectiveLocation.OBJECT,
|
|
306
|
+
DirectiveLocation.FIELD_DEFINITION,
|
|
307
|
+
DirectiveLocation.ARGUMENT_DEFINITION,
|
|
308
|
+
DirectiveLocation.INTERFACE,
|
|
309
|
+
DirectiveLocation.UNION,
|
|
310
|
+
DirectiveLocation.ENUM,
|
|
311
|
+
DirectiveLocation.ENUM_VALUE,
|
|
312
|
+
DirectiveLocation.INPUT_OBJECT,
|
|
313
|
+
DirectiveLocation.INPUT_FIELD_DEFINITION,
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
const typeSystemDirectiveLocationsSet = new Set(typeSystemDirectiveLocations);
|
|
317
|
+
|
|
318
|
+
export function isTypeSystemDirectiveLocation(loc: DirectiveLocation): boolean {
|
|
319
|
+
return typeSystemDirectiveLocationsSet.has(loc);
|
|
320
|
+
}
|
|
321
|
+
|
|
294
322
|
/**
|
|
295
323
|
* Converts a type to an AST of a "reference" to that type, one corresponding to the type `toString()` (and thus never a type definition).
|
|
296
324
|
*
|
|
@@ -495,11 +523,37 @@ export class Extension<TElement extends ExtendableElement> {
|
|
|
495
523
|
}
|
|
496
524
|
}
|
|
497
525
|
|
|
526
|
+
type UnappliedDirective = {
|
|
527
|
+
nameOrDef: DirectiveDefinition<Record<string, any>> | string,
|
|
528
|
+
args: Record<string, any>,
|
|
529
|
+
extension?: Extension<any>,
|
|
530
|
+
directive: DirectiveNode,
|
|
531
|
+
};
|
|
532
|
+
|
|
498
533
|
// TODO: ideally, we should hide the ctor of this class as we rely in places on the fact the no-one external defines new implementations.
|
|
499
534
|
export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>, TParent extends SchemaElement<any, any> | Schema> extends Element<TParent> {
|
|
500
535
|
protected readonly _appliedDirectives: Directive<TOwnType>[] = [];
|
|
536
|
+
protected _unappliedDirectives: UnappliedDirective[] = [];
|
|
501
537
|
description?: string;
|
|
502
538
|
|
|
539
|
+
addUnappliedDirective({ nameOrDef, args, extension, directive }: UnappliedDirective) {
|
|
540
|
+
this._unappliedDirectives.push({
|
|
541
|
+
nameOrDef,
|
|
542
|
+
args: args ?? {},
|
|
543
|
+
extension,
|
|
544
|
+
directive,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
processUnappliedDirectives() {
|
|
549
|
+
for (const { nameOrDef, args, extension, directive } of this._unappliedDirectives) {
|
|
550
|
+
const d = this.applyDirective(nameOrDef, args);
|
|
551
|
+
d.setOfExtension(extension);
|
|
552
|
+
d.sourceAST = directive;
|
|
553
|
+
}
|
|
554
|
+
this._unappliedDirectives = [];
|
|
555
|
+
}
|
|
556
|
+
|
|
503
557
|
get appliedDirectives(): readonly Directive<TOwnType>[] {
|
|
504
558
|
return this._appliedDirectives;
|
|
505
559
|
}
|
|
@@ -697,7 +751,7 @@ abstract class BaseNamedType<TReferencer, TOwnType extends NamedType & NamedSche
|
|
|
697
751
|
}
|
|
698
752
|
|
|
699
753
|
hasNonExtensionElements(): boolean {
|
|
700
|
-
return this.preserveEmptyDefinition
|
|
754
|
+
return this.preserveEmptyDefinition
|
|
701
755
|
|| this._appliedDirectives.some(d => d.ofExtension() === undefined)
|
|
702
756
|
|| this.hasNonExtensionInnerElements();
|
|
703
757
|
}
|
|
@@ -916,6 +970,10 @@ export class SchemaBlueprint {
|
|
|
916
970
|
onUnknownDirectiveValidationError(_schema: Schema, _unknownDirectiveName: string, error: GraphQLError): GraphQLError {
|
|
917
971
|
return error;
|
|
918
972
|
}
|
|
973
|
+
|
|
974
|
+
applyDirectivesAfterParsing() {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
919
977
|
}
|
|
920
978
|
|
|
921
979
|
export const defaultSchemaBlueprint = new SchemaBlueprint();
|
|
@@ -1009,24 +1067,37 @@ export class CoreFeatures {
|
|
|
1009
1067
|
this.byIdentity.set(feature.url.identity, feature);
|
|
1010
1068
|
}
|
|
1011
1069
|
|
|
1012
|
-
sourceFeature(element: DirectiveDefinition | Directive | NamedType): CoreFeature | undefined {
|
|
1070
|
+
sourceFeature(element: DirectiveDefinition | Directive | NamedType): { feature: CoreFeature, nameInFeature: string, isImported: boolean } | undefined {
|
|
1013
1071
|
const isDirective = element instanceof DirectiveDefinition || element instanceof Directive;
|
|
1014
1072
|
const splitted = element.name.split('__');
|
|
1015
1073
|
if (splitted.length > 1) {
|
|
1016
|
-
|
|
1074
|
+
const feature = this.byAlias.get(splitted[0]);
|
|
1075
|
+
return feature ? {
|
|
1076
|
+
feature,
|
|
1077
|
+
nameInFeature: splitted[1],
|
|
1078
|
+
isImported: false,
|
|
1079
|
+
} : undefined;
|
|
1017
1080
|
} else {
|
|
1018
1081
|
const directFeature = this.byAlias.get(element.name);
|
|
1019
1082
|
if (directFeature && isDirective) {
|
|
1020
|
-
return
|
|
1083
|
+
return {
|
|
1084
|
+
feature: directFeature,
|
|
1085
|
+
nameInFeature: directFeature.imports.find(imp => imp.as === `@${element.name}`)?.name.slice(1) ?? element.name,
|
|
1086
|
+
isImported: true,
|
|
1087
|
+
};
|
|
1021
1088
|
}
|
|
1022
1089
|
|
|
1023
1090
|
// Let's see if it's an import. If not, it's not associated to a declared feature.
|
|
1024
1091
|
const importName = isDirective ? '@' + element.name : element.name;
|
|
1025
1092
|
const allFeatures = [this.coreItself, ...this.byIdentity.values()];
|
|
1026
1093
|
for (const feature of allFeatures) {
|
|
1027
|
-
for (const { as } of feature.imports) {
|
|
1028
|
-
if (as === importName) {
|
|
1029
|
-
return
|
|
1094
|
+
for (const { as, name } of feature.imports) {
|
|
1095
|
+
if ((as ?? name) === importName) {
|
|
1096
|
+
return {
|
|
1097
|
+
feature,
|
|
1098
|
+
nameInFeature: name.slice(1),
|
|
1099
|
+
isImported: true,
|
|
1100
|
+
};
|
|
1030
1101
|
}
|
|
1031
1102
|
}
|
|
1032
1103
|
}
|
|
@@ -1059,8 +1130,50 @@ const graphQLBuiltInDirectivesSpecifications: readonly DirectiveSpecification[]
|
|
|
1059
1130
|
locations: [DirectiveLocation.SCALAR],
|
|
1060
1131
|
argumentFct: (schema) => ({ args: [{ name: 'url', type: new NonNullType(schema.stringType()) }], errors: [] })
|
|
1061
1132
|
}),
|
|
1133
|
+
// TODO: currently inconditionally adding @defer as the list of built-in. It's probably fine, but double check if we want to not do so when @defer-support is
|
|
1134
|
+
// not enabled or something (it would probably be hard to handle it at that point anyway but well...).
|
|
1135
|
+
createDirectiveSpecification({
|
|
1136
|
+
name: 'defer',
|
|
1137
|
+
locations: [DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT],
|
|
1138
|
+
argumentFct: (schema) => ({
|
|
1139
|
+
args: [
|
|
1140
|
+
{ name: 'label', type: schema.stringType() },
|
|
1141
|
+
{ name: 'if', type: schema.booleanType() },
|
|
1142
|
+
],
|
|
1143
|
+
errors: [],
|
|
1144
|
+
})
|
|
1145
|
+
}),
|
|
1146
|
+
// Adding @stream too so that it's know and we don't error out if it is queries. It feels like it would be weird to do so for @stream but not
|
|
1147
|
+
// @defer when both are defined in the same spec. That said, that does *not* mean we currently _implement_ @stream, we don't, and so putting
|
|
1148
|
+
// it in a query will be a no-op at the moment (which technically is valid according to the spec so ...).
|
|
1149
|
+
createDirectiveSpecification({
|
|
1150
|
+
name: 'stream',
|
|
1151
|
+
locations: [DirectiveLocation.FIELD],
|
|
1152
|
+
argumentFct: (schema) => ({
|
|
1153
|
+
args: [
|
|
1154
|
+
{ name: 'label', type: schema.stringType() },
|
|
1155
|
+
{ name: 'initialCount', type: schema.intType(), defaultValue: 0 },
|
|
1156
|
+
{ name: 'if', type: schema.booleanType() },
|
|
1157
|
+
],
|
|
1158
|
+
errors: [],
|
|
1159
|
+
})
|
|
1160
|
+
}),
|
|
1062
1161
|
];
|
|
1063
1162
|
|
|
1163
|
+
export type DeferDirectiveArgs = {
|
|
1164
|
+
// TODO: we currently do not support variables for the defer label. Passing a label in a variable
|
|
1165
|
+
// feels like a weird use case in the first place, but we should probably fix this nonetheless (or
|
|
1166
|
+
// if we decide to have it be a known limitations, we should at least reject it cleanly).
|
|
1167
|
+
label?: string,
|
|
1168
|
+
if?: boolean | Variable,
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
export type StreamDirectiveArgs = {
|
|
1172
|
+
label?: string,
|
|
1173
|
+
initialCount: number,
|
|
1174
|
+
if?: boolean,
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1064
1177
|
|
|
1065
1178
|
// A coordinate is up to 3 "graphQL name" ([_A-Za-z][_0-9A-Za-z]*).
|
|
1066
1179
|
const coordinateRegexp = /^@?[_A-Za-z][_0-9A-Za-z]*(\.[_A-Za-z][_0-9A-Za-z]*)?(\([_A-Za-z][_0-9A-Za-z]*:\))?$/;
|
|
@@ -1162,7 +1275,7 @@ export class Schema {
|
|
|
1162
1275
|
return this.apiSchema;
|
|
1163
1276
|
}
|
|
1164
1277
|
|
|
1165
|
-
private emptyASTDefinitionsForExtensionsWithoutDefinition():
|
|
1278
|
+
private emptyASTDefinitionsForExtensionsWithoutDefinition(): DefinitionNode[] {
|
|
1166
1279
|
const nodes = [];
|
|
1167
1280
|
if (this.schemaDefinition.hasExtensionElements() && !this.schemaDefinition.hasNonExtensionElements()) {
|
|
1168
1281
|
const node: SchemaDefinitionNode = { kind: Kind.SCHEMA_DEFINITION, operationTypes: [] };
|
|
@@ -1180,7 +1293,9 @@ export class Schema {
|
|
|
1180
1293
|
return nodes;
|
|
1181
1294
|
}
|
|
1182
1295
|
|
|
1183
|
-
toGraphQLJSSchema(): GraphQLSchema {
|
|
1296
|
+
toGraphQLJSSchema(config?: { includeDefer?: boolean }): GraphQLSchema {
|
|
1297
|
+
const includeDefer = config?.includeDefer ?? false;
|
|
1298
|
+
|
|
1184
1299
|
let ast = this.toAST();
|
|
1185
1300
|
|
|
1186
1301
|
// Note that AST generated by `this.toAST()` may not be fully graphQL valid because, in federation subgraphs, we accept
|
|
@@ -1188,6 +1303,9 @@ export class Schema {
|
|
|
1188
1303
|
// we need to "fix" that problem. For that, we add empty definitions for every element that has extensions without
|
|
1189
1304
|
// definitions (which is also what `fed1` was effectively doing).
|
|
1190
1305
|
const additionalNodes = this.emptyASTDefinitionsForExtensionsWithoutDefinition();
|
|
1306
|
+
if (includeDefer) {
|
|
1307
|
+
additionalNodes.push(this.deferDirective().toAST());
|
|
1308
|
+
}
|
|
1191
1309
|
if (additionalNodes.length > 0) {
|
|
1192
1310
|
ast = {
|
|
1193
1311
|
kind: Kind.DOCUMENT,
|
|
@@ -1464,28 +1582,35 @@ export class Schema {
|
|
|
1464
1582
|
}
|
|
1465
1583
|
|
|
1466
1584
|
private getBuiltInDirective<TApplicationArgs extends {[key: string]: any}>(
|
|
1467
|
-
schema: Schema,
|
|
1468
1585
|
name: string
|
|
1469
1586
|
): DirectiveDefinition<TApplicationArgs> {
|
|
1470
|
-
const directive =
|
|
1587
|
+
const directive = this.directive(name);
|
|
1471
1588
|
assert(directive, `The provided schema has not be built with the ${name} directive built-in`);
|
|
1472
1589
|
return directive as DirectiveDefinition<TApplicationArgs>;
|
|
1473
1590
|
}
|
|
1474
1591
|
|
|
1475
|
-
includeDirective(
|
|
1476
|
-
return this.getBuiltInDirective(
|
|
1592
|
+
includeDirective(): DirectiveDefinition<{if: boolean}> {
|
|
1593
|
+
return this.getBuiltInDirective('include');
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
skipDirective(): DirectiveDefinition<{if: boolean}> {
|
|
1597
|
+
return this.getBuiltInDirective('skip');
|
|
1477
1598
|
}
|
|
1478
1599
|
|
|
1479
|
-
|
|
1480
|
-
return this.getBuiltInDirective(
|
|
1600
|
+
deprecatedDirective(): DirectiveDefinition<{reason?: string}> {
|
|
1601
|
+
return this.getBuiltInDirective('deprecated');
|
|
1481
1602
|
}
|
|
1482
1603
|
|
|
1483
|
-
|
|
1484
|
-
return this.getBuiltInDirective(
|
|
1604
|
+
specifiedByDirective(): DirectiveDefinition<{url: string}> {
|
|
1605
|
+
return this.getBuiltInDirective('specifiedBy');
|
|
1485
1606
|
}
|
|
1486
1607
|
|
|
1487
|
-
|
|
1488
|
-
return this.getBuiltInDirective(
|
|
1608
|
+
deferDirective(): DirectiveDefinition<DeferDirectiveArgs> {
|
|
1609
|
+
return this.getBuiltInDirective('defer');
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
streamDirective(): DirectiveDefinition<StreamDirectiveArgs> {
|
|
1613
|
+
return this.getBuiltInDirective('stream');
|
|
1489
1614
|
}
|
|
1490
1615
|
|
|
1491
1616
|
/**
|
|
@@ -1653,7 +1778,7 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
|
|
|
1653
1778
|
}
|
|
1654
1779
|
|
|
1655
1780
|
hasNonExtensionElements(): boolean {
|
|
1656
|
-
return this.preserveEmptyDefinition
|
|
1781
|
+
return this.preserveEmptyDefinition
|
|
1657
1782
|
|| this._appliedDirectives.some((d) => d.ofExtension() === undefined)
|
|
1658
1783
|
|| this.roots().some((r) => r.ofExtension() === undefined);
|
|
1659
1784
|
}
|
|
@@ -2776,6 +2901,9 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
|
|
|
2776
2901
|
return this.addLocations(...Object.values(DirectiveLocation));
|
|
2777
2902
|
}
|
|
2778
2903
|
|
|
2904
|
+
/**
|
|
2905
|
+
* Adds the subset of type system locations that correspond to type definitions.
|
|
2906
|
+
*/
|
|
2779
2907
|
addAllTypeLocations(): DirectiveDefinition {
|
|
2780
2908
|
return this.addLocations(
|
|
2781
2909
|
DirectiveLocation.SCALAR,
|
|
@@ -2802,6 +2930,14 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
|
|
|
2802
2930
|
return this;
|
|
2803
2931
|
}
|
|
2804
2932
|
|
|
2933
|
+
hasExecutableLocations(): boolean {
|
|
2934
|
+
return this.locations.some((loc) => isExecutableDirectiveLocation(loc));
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
hasTypeSystemLocations(): boolean {
|
|
2938
|
+
return this.locations.some((loc) => isTypeSystemDirectiveLocation(loc));
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2805
2941
|
applications(): readonly Directive<SchemaElement<any, any>, TApplicationArgs>[] {
|
|
2806
2942
|
return setValues(this._referencers);
|
|
2807
2943
|
}
|
|
@@ -2858,6 +2994,11 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
|
|
|
2858
2994
|
this.remove().forEach(ref => ref.remove());
|
|
2859
2995
|
}
|
|
2860
2996
|
|
|
2997
|
+
toAST(): DirectiveDefinitionNode {
|
|
2998
|
+
const doc = parse(printDirectiveDefinition(this));
|
|
2999
|
+
return doc.definitions[0] as DirectiveDefinitionNode;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
2861
3002
|
toString(): string {
|
|
2862
3003
|
return `@${this.name}`;
|
|
2863
3004
|
}
|
|
@@ -3054,7 +3195,7 @@ export function sameDirectiveApplications(applications1: Directive<any, any>[],
|
|
|
3054
3195
|
/**
|
|
3055
3196
|
* Checks whether a given array of directive applications (`maybeSubset`) is a sub-set of another array of directive applications (`applications`).
|
|
3056
3197
|
*
|
|
3057
|
-
* Sub-set here means that all of the applications in `maybeSubset` appears in `applications`.
|
|
3198
|
+
* Sub-set here means that all of the applications in `maybeSubset` appears in `applications`.
|
|
3058
3199
|
*/
|
|
3059
3200
|
export function isDirectiveApplicationsSubset(applications: Directive<any, any>[], maybeSubset: Directive<any, any>[]): boolean {
|
|
3060
3201
|
if (maybeSubset.length > applications.length) {
|
|
@@ -3306,6 +3447,34 @@ function *directivesToCopy(source: Schema, dest: Schema): Generator<DirectiveDef
|
|
|
3306
3447
|
yield* source.directives();
|
|
3307
3448
|
}
|
|
3308
3449
|
|
|
3450
|
+
/**
|
|
3451
|
+
* Creates, in the provided schema, a directive definition equivalent to the provided one.
|
|
3452
|
+
*
|
|
3453
|
+
* Note that this method assumes that:
|
|
3454
|
+
* - the provided schema does not already have a directive with the name of the definition to copy.
|
|
3455
|
+
* - if the copied definition has arguments, then the provided schema has existing types with
|
|
3456
|
+
* names matching any type used in copied definition.
|
|
3457
|
+
*/
|
|
3458
|
+
export function copyDirectiveDefinitionToSchema({
|
|
3459
|
+
definition,
|
|
3460
|
+
schema,
|
|
3461
|
+
copyDirectiveApplicationsInArguments = true,
|
|
3462
|
+
locationFilter,
|
|
3463
|
+
}: {
|
|
3464
|
+
definition: DirectiveDefinition,
|
|
3465
|
+
schema: Schema,
|
|
3466
|
+
copyDirectiveApplicationsInArguments: boolean,
|
|
3467
|
+
locationFilter?: (loc: DirectiveLocation) => boolean,
|
|
3468
|
+
}
|
|
3469
|
+
) {
|
|
3470
|
+
copyDirectiveDefinitionInner(
|
|
3471
|
+
definition,
|
|
3472
|
+
schema.addDirectiveDefinition(definition.name),
|
|
3473
|
+
copyDirectiveApplicationsInArguments,
|
|
3474
|
+
locationFilter,
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3309
3478
|
function copy(source: Schema, dest: Schema) {
|
|
3310
3479
|
// We shallow copy types first so any future reference to any of them can be dereferenced.
|
|
3311
3480
|
for (const type of typesToCopy(source, dest)) {
|
|
@@ -3458,22 +3627,41 @@ function copyWrapperTypeOrTypeRef(source: Type | undefined, destParent: Schema):
|
|
|
3458
3627
|
}
|
|
3459
3628
|
}
|
|
3460
3629
|
|
|
3461
|
-
function copyArgumentDefinitionInner<P extends FieldDefinition<any> | DirectiveDefinition>(
|
|
3630
|
+
function copyArgumentDefinitionInner<P extends FieldDefinition<any> | DirectiveDefinition>(
|
|
3631
|
+
source: ArgumentDefinition<P>,
|
|
3632
|
+
dest: ArgumentDefinition<P>,
|
|
3633
|
+
copyDirectiveApplications: boolean = true,
|
|
3634
|
+
) {
|
|
3462
3635
|
const type = copyWrapperTypeOrTypeRef(source.type, dest.schema()) as InputType;
|
|
3463
3636
|
dest.type = type;
|
|
3464
3637
|
dest.defaultValue = source.defaultValue;
|
|
3465
|
-
|
|
3638
|
+
if (copyDirectiveApplications) {
|
|
3639
|
+
copyAppliedDirectives(source, dest);
|
|
3640
|
+
}
|
|
3466
3641
|
dest.description = source.description;
|
|
3467
3642
|
dest.sourceAST = source.sourceAST;
|
|
3468
3643
|
}
|
|
3469
3644
|
|
|
3470
|
-
function copyDirectiveDefinitionInner(
|
|
3645
|
+
function copyDirectiveDefinitionInner(
|
|
3646
|
+
source: DirectiveDefinition,
|
|
3647
|
+
dest: DirectiveDefinition,
|
|
3648
|
+
copyDirectiveApplicationsInArguments: boolean = true,
|
|
3649
|
+
locationFilter?: (loc: DirectiveLocation) => boolean,
|
|
3650
|
+
) {
|
|
3651
|
+
let locations = source.locations;
|
|
3652
|
+
if (locationFilter) {
|
|
3653
|
+
locations = locations.filter((loc) => locationFilter(loc));
|
|
3654
|
+
}
|
|
3655
|
+
if (locations.length === 0) {
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3471
3659
|
for (const arg of source.arguments()) {
|
|
3472
3660
|
const type = copyWrapperTypeOrTypeRef(arg.type, dest.schema());
|
|
3473
|
-
copyArgumentDefinitionInner(arg, dest.addArgument(arg.name, type as InputType));
|
|
3661
|
+
copyArgumentDefinitionInner(arg, dest.addArgument(arg.name, type as InputType), copyDirectiveApplicationsInArguments);
|
|
3474
3662
|
}
|
|
3475
3663
|
dest.repeatable = source.repeatable;
|
|
3476
|
-
dest.addLocations(...
|
|
3664
|
+
dest.addLocations(...locations);
|
|
3477
3665
|
dest.sourceAST = source.sourceAST;
|
|
3478
3666
|
dest.description = source.description;
|
|
3479
3667
|
}
|
package/src/error.ts
CHANGED
|
@@ -459,6 +459,12 @@ const DOWNSTREAM_SERVICE_ERROR = makeCodeDefinition(
|
|
|
459
459
|
{ addedIn: FED1_CODE },
|
|
460
460
|
);
|
|
461
461
|
|
|
462
|
+
const DIRECTIVE_COMPOSITION_ERROR = makeCodeDefinition(
|
|
463
|
+
'DIRECTIVE_COMPOSITION_ERROR',
|
|
464
|
+
'Error when composing custom directives.',
|
|
465
|
+
{ addedIn: '2.1.0' },
|
|
466
|
+
);
|
|
467
|
+
|
|
462
468
|
export const ERROR_CATEGORIES = {
|
|
463
469
|
DIRECTIVE_FIELDS_MISSING_EXTERNAL,
|
|
464
470
|
DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
|
|
@@ -540,6 +546,7 @@ export const ERRORS = {
|
|
|
540
546
|
KEY_HAS_DIRECTIVE_IN_FIELDS_ARGS,
|
|
541
547
|
PROVIDES_HAS_DIRECTIVE_IN_FIELDS_ARGS,
|
|
542
548
|
REQUIRES_HAS_DIRECTIVE_IN_FIELDS_ARGS,
|
|
549
|
+
DIRECTIVE_COMPOSITION_ERROR,
|
|
543
550
|
};
|
|
544
551
|
|
|
545
552
|
const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
baseType,
|
|
3
3
|
CompositeType,
|
|
4
|
+
copyDirectiveDefinitionToSchema,
|
|
4
5
|
Directive,
|
|
5
6
|
FieldDefinition,
|
|
6
7
|
InputFieldDefinition,
|
|
7
8
|
InputObjectType,
|
|
8
9
|
InterfaceType,
|
|
10
|
+
isExecutableDirectiveLocation,
|
|
9
11
|
isEnumType,
|
|
10
12
|
isInterfaceType,
|
|
11
13
|
isObjectType,
|
|
@@ -234,6 +236,7 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
|
|
|
234
236
|
}
|
|
235
237
|
}
|
|
236
238
|
|
|
239
|
+
const allExecutableDirectives = supergraph.directives().filter((def) => def.hasExecutableLocations());
|
|
237
240
|
for (const subgraph of subgraphs) {
|
|
238
241
|
if (isFed1) {
|
|
239
242
|
// The join spec in fed1 was not including external fields. Let's make sure we had them or we'll get validation
|
|
@@ -266,6 +269,23 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
|
|
|
266
269
|
break;
|
|
267
270
|
}
|
|
268
271
|
}
|
|
272
|
+
|
|
273
|
+
// Lastly, we add all the "executable" directives from the supergraph to each subgraphs, as those may be part
|
|
274
|
+
// of a query and end up in any subgraph fetches. We do this "last" to make sure that if one of the directive
|
|
275
|
+
// use a type for an argument, that argument exists.
|
|
276
|
+
// Note that we don't bother with non-executable directives at the moment since we've don't extract their
|
|
277
|
+
// applications. It might become something we need later, but we don't so far.
|
|
278
|
+
for (const definition of allExecutableDirectives) {
|
|
279
|
+
// Note that we skip any potentially applied directives in the argument of the copied definition, because as said
|
|
280
|
+
// in the comment above, we haven't copied type-system directives. And so far, we really don't care about those
|
|
281
|
+
// applications.
|
|
282
|
+
copyDirectiveDefinitionToSchema({
|
|
283
|
+
definition,
|
|
284
|
+
schema: subgraph.schema,
|
|
285
|
+
copyDirectiveApplicationsInArguments: false,
|
|
286
|
+
locationFilter: (loc) => isExecutableDirectiveLocation(loc),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
269
289
|
}
|
|
270
290
|
|
|
271
291
|
// TODO: Not sure that code is needed anymore (any field necessary to validate an interface will have been marked
|
package/src/federation.ts
CHANGED
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
extendsDirectiveSpec,
|
|
77
77
|
shareableDirectiveSpec,
|
|
78
78
|
overrideDirectiveSpec,
|
|
79
|
+
composeDirectiveSpec,
|
|
79
80
|
FEDERATION2_SPEC_DIRECTIVES,
|
|
80
81
|
ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES,
|
|
81
82
|
FEDERATION2_ONLY_SPEC_DIRECTIVES,
|
|
@@ -599,6 +600,10 @@ export class FederationMetadata {
|
|
|
599
600
|
return this.getFederationDirective(tagSpec.tagDirectiveSpec.name);
|
|
600
601
|
}
|
|
601
602
|
|
|
603
|
+
composeDirective(): DirectiveDefinition<{name: string}> {
|
|
604
|
+
return this.getFederationDirective(composeDirectiveSpec.name);
|
|
605
|
+
}
|
|
606
|
+
|
|
602
607
|
inaccessibleDirective(): DirectiveDefinition<{}> {
|
|
603
608
|
return this.getFederationDirective(
|
|
604
609
|
inaccessibleSpec.inaccessibleDirectiveSpec.name
|
|
@@ -615,7 +620,7 @@ export class FederationMetadata {
|
|
|
615
620
|
this.extendsDirective(),
|
|
616
621
|
];
|
|
617
622
|
return this.isFed2Schema()
|
|
618
|
-
? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective(), this.overrideDirective())
|
|
623
|
+
? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective(), this.overrideDirective(), this.composeDirective())
|
|
619
624
|
: baseDirectives;
|
|
620
625
|
}
|
|
621
626
|
|
|
@@ -698,7 +703,9 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
698
703
|
}
|
|
699
704
|
|
|
700
705
|
onDirectiveDefinitionAndSchemaParsed(schema: Schema): GraphQLError[] {
|
|
701
|
-
|
|
706
|
+
const errors = completeSubgraphSchema(schema);
|
|
707
|
+
schema.schemaDefinition.processUnappliedDirectives();
|
|
708
|
+
return errors;
|
|
702
709
|
}
|
|
703
710
|
|
|
704
711
|
onInvalidation(schema: Schema) {
|
|
@@ -873,6 +880,10 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
873
880
|
}
|
|
874
881
|
return error;
|
|
875
882
|
}
|
|
883
|
+
|
|
884
|
+
applyDirectivesAfterParsing() {
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
876
887
|
}
|
|
877
888
|
|
|
878
889
|
function findUnusedNamedForLinkDirective(schema: Schema): string | undefined {
|
|
@@ -927,7 +938,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
927
938
|
|
|
928
939
|
// This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
|
|
929
940
|
// subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
|
|
930
|
-
export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.
|
|
941
|
+
export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.1", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective"])';
|
|
931
942
|
|
|
932
943
|
export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
|
|
933
944
|
const fed2LinkExtension: SchemaExtensionNode = {
|
|
@@ -1422,7 +1433,7 @@ export class Subgraph {
|
|
|
1422
1433
|
}
|
|
1423
1434
|
|
|
1424
1435
|
const core = this.schema.coreFeatures;
|
|
1425
|
-
return !core || core.sourceFeature(d)?.url.identity !== linkIdentity;
|
|
1436
|
+
return !core || core.sourceFeature(d)?.feature.url.identity !== linkIdentity;
|
|
1426
1437
|
}
|
|
1427
1438
|
|
|
1428
1439
|
private isPrintedType(t: NamedType): boolean {
|
|
@@ -1437,7 +1448,7 @@ export class Subgraph {
|
|
|
1437
1448
|
}
|
|
1438
1449
|
|
|
1439
1450
|
const core = this.schema.coreFeatures;
|
|
1440
|
-
return !core || core.sourceFeature(t)?.url.identity !== linkIdentity;
|
|
1451
|
+
return !core || core.sourceFeature(t)?.feature.url.identity !== linkIdentity;
|
|
1441
1452
|
}
|
|
1442
1453
|
|
|
1443
1454
|
private isPrintedDirectiveApplication(d: Directive): boolean {
|
package/src/federationSpec.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ArgumentSpecification,
|
|
9
9
|
createDirectiveSpecification,
|
|
10
10
|
createScalarTypeSpecification,
|
|
11
|
+
DirectiveSpecification,
|
|
11
12
|
} from "./directiveAndTypeSpecification";
|
|
12
13
|
import { DirectiveLocation, GraphQLError } from "graphql";
|
|
13
14
|
import { assert } from "./utils";
|
|
@@ -79,6 +80,16 @@ export const overrideDirectiveSpec = createDirectiveSpecification({
|
|
|
79
80
|
}),
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
export const composeDirectiveSpec = createDirectiveSpecification({
|
|
84
|
+
name: 'composeDirective',
|
|
85
|
+
locations: [DirectiveLocation.SCHEMA],
|
|
86
|
+
repeatable: true,
|
|
87
|
+
argumentFct: (schema) => ({
|
|
88
|
+
args: [{ name: 'name', type: schema.stringType() }],
|
|
89
|
+
errors: [],
|
|
90
|
+
}),
|
|
91
|
+
})
|
|
92
|
+
|
|
82
93
|
function fieldsArgument(schema: Schema): ArgumentSpecification {
|
|
83
94
|
return { name: 'fields', type: fieldSetType(schema) };
|
|
84
95
|
}
|
|
@@ -95,15 +106,24 @@ export const FEDERATION2_ONLY_SPEC_DIRECTIVES = [
|
|
|
95
106
|
overrideDirectiveSpec,
|
|
96
107
|
];
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
export const FEDERATION2_1_ONLY_SPEC_DIRECTIVES = [
|
|
110
|
+
composeDirectiveSpec,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const PRE_FEDERATION2_SPEC_DIRECTIVES = [
|
|
100
114
|
keyDirectiveSpec,
|
|
101
115
|
requiresDirectiveSpec,
|
|
102
116
|
providesDirectiveSpec,
|
|
103
117
|
externalDirectiveSpec,
|
|
104
118
|
TAG_VERSIONS.latest().tagDirectiveSpec,
|
|
105
119
|
extendsDirectiveSpec, // TODO: should we stop supporting that?
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Note that this is only used for federation 2+ (federation 1 adds the same directive, but not through a core spec).
|
|
123
|
+
export const FEDERATION2_SPEC_DIRECTIVES = [
|
|
124
|
+
...PRE_FEDERATION2_SPEC_DIRECTIVES,
|
|
106
125
|
...FEDERATION2_ONLY_SPEC_DIRECTIVES,
|
|
126
|
+
...FEDERATION2_1_ONLY_SPEC_DIRECTIVES,
|
|
107
127
|
];
|
|
108
128
|
|
|
109
129
|
// Note that this is meant to contain _all_ federation directive names ever supported, regardless of which version.
|
|
@@ -120,6 +140,12 @@ export class FederationSpecDefinition extends FeatureDefinition {
|
|
|
120
140
|
super(new FeatureUrl(federationIdentity, 'federation', version));
|
|
121
141
|
}
|
|
122
142
|
|
|
143
|
+
private allFedDirectives(): DirectiveSpecification[] {
|
|
144
|
+
return PRE_FEDERATION2_SPEC_DIRECTIVES
|
|
145
|
+
.concat(FEDERATION2_ONLY_SPEC_DIRECTIVES)
|
|
146
|
+
.concat(this.url.version >= (new FeatureVersion(2, 1)) ? FEDERATION2_1_ONLY_SPEC_DIRECTIVES : []);
|
|
147
|
+
}
|
|
148
|
+
|
|
123
149
|
addElementsToSchema(schema: Schema): GraphQLError[] {
|
|
124
150
|
const feature = this.featureInSchema(schema);
|
|
125
151
|
assert(feature, 'The federation specification should have been added to the schema before this is called');
|
|
@@ -127,20 +153,21 @@ export class FederationSpecDefinition extends FeatureDefinition {
|
|
|
127
153
|
let errors: GraphQLError[] = [];
|
|
128
154
|
errors = errors.concat(this.addTypeSpec(schema, fieldSetTypeSpec));
|
|
129
155
|
|
|
130
|
-
for (const directive of
|
|
156
|
+
for (const directive of this.allFedDirectives()) {
|
|
131
157
|
errors = errors.concat(this.addDirectiveSpec(schema, directive));
|
|
132
158
|
}
|
|
133
159
|
return errors;
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
allElementNames(): string[] {
|
|
137
|
-
return
|
|
163
|
+
return this.allFedDirectives().map((spec) => `@${spec.name}`).concat([
|
|
138
164
|
fieldSetTypeSpec.name,
|
|
139
165
|
])
|
|
140
166
|
}
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
|
|
144
|
-
.add(new FederationSpecDefinition(new FeatureVersion(2, 0)))
|
|
170
|
+
.add(new FederationSpecDefinition(new FeatureVersion(2, 0)))
|
|
171
|
+
.add(new FederationSpecDefinition(new FeatureVersion(2, 1)));
|
|
145
172
|
|
|
146
173
|
registerKnownFeature(FEDERATION_VERSIONS);
|
package/src/inaccessibleSpec.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
EnumType,
|
|
7
7
|
EnumValue,
|
|
8
8
|
ErrGraphQLAPISchemaValidationFailed,
|
|
9
|
-
executableDirectiveLocations,
|
|
10
9
|
FieldDefinition,
|
|
11
10
|
InputFieldDefinition,
|
|
12
11
|
InputObjectType,
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
isListType,
|
|
18
17
|
isNonNullType,
|
|
19
18
|
isScalarType,
|
|
19
|
+
isTypeSystemDirectiveLocation,
|
|
20
20
|
isVariable,
|
|
21
21
|
NamedType,
|
|
22
22
|
ObjectType,
|
|
@@ -682,11 +682,8 @@ function validateInaccessibleElements(
|
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
-
const executableDirectiveLocationSet = new Set(executableDirectiveLocations);
|
|
686
685
|
for (const directive of schema.allDirectives()) {
|
|
687
|
-
const typeSystemLocations = directive.locations.filter((loc) =>
|
|
688
|
-
!executableDirectiveLocationSet.has(loc)
|
|
689
|
-
);
|
|
686
|
+
const typeSystemLocations = directive.locations.filter((loc) => isTypeSystemDirectiveLocation(loc));
|
|
690
687
|
if (hasBuiltInName(directive)) {
|
|
691
688
|
// Built-in directives (and their descendants) aren't allowed to be
|
|
692
689
|
// @inaccessible, regardless of shadowing.
|