@apollo/federation-internals 2.1.4 → 2.2.0
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 +7 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +7 -0
- package/dist/buildSchema.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 +16 -2
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +1 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +76 -34
- package/dist/federation.js.map +1 -1
- package/dist/federationSpec.d.ts +24 -16
- package/dist/federationSpec.d.ts.map +1 -1
- package/dist/federationSpec.js +94 -68
- package/dist/federationSpec.js.map +1 -1
- package/dist/introspection.d.ts +1 -0
- package/dist/introspection.d.ts.map +1 -1
- package/dist/introspection.js +11 -1
- package/dist/introspection.js.map +1 -1
- package/dist/operations.d.ts +1 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +21 -2
- package/dist/operations.js.map +1 -1
- package/dist/schemaUpgrader.d.ts +7 -1
- package/dist/schemaUpgrader.d.ts.map +1 -1
- package/dist/schemaUpgrader.js +23 -3
- package/dist/schemaUpgrader.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +13 -1
- package/dist/utils.js.map +1 -1
- package/dist/validate.js +4 -1
- package/dist/validate.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +64 -1
- package/src/__tests__/schemaUpgrader.test.ts +3 -3
- package/src/__tests__/subgraphValidation.test.ts +60 -1
- package/src/buildSchema.ts +9 -0
- package/src/error.ts +7 -0
- package/src/extractSubgraphsFromSupergraph.ts +17 -2
- package/src/federation.ts +109 -49
- package/src/federationSpec.ts +102 -75
- package/src/introspection.ts +10 -0
- package/src/operations.ts +28 -3
- package/src/schemaUpgrader.ts +24 -2
- package/src/utils.ts +15 -0
- package/src/validate.ts +9 -2
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Directive,
|
|
8
8
|
DirectiveDefinition,
|
|
9
9
|
ErrGraphQLValidationFailed,
|
|
10
|
+
Extension,
|
|
10
11
|
FieldDefinition,
|
|
11
12
|
InputFieldDefinition,
|
|
12
13
|
InterfaceType,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
sourceASTs,
|
|
28
29
|
UnionType,
|
|
29
30
|
} from "./definitions";
|
|
30
|
-
import { assert, joinStrings, MultiMap, printHumanReadableList, OrderedMap } from "./utils";
|
|
31
|
+
import { assert, joinStrings, MultiMap, printHumanReadableList, OrderedMap, mapValues } from "./utils";
|
|
31
32
|
import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
32
33
|
import { specifiedSDLRules } from "graphql/validation/specifiedRules";
|
|
33
34
|
import {
|
|
@@ -46,7 +47,6 @@ import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFed
|
|
|
46
47
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
47
48
|
import { parseSelectionSet, selectionOfElement, SelectionSet } from './operations';
|
|
48
49
|
import { TAG_VERSIONS } from "./tagSpec";
|
|
49
|
-
import { INACCESSIBLE_VERSIONS } from "./inaccessibleSpec";
|
|
50
50
|
import {
|
|
51
51
|
errorCodeDef,
|
|
52
52
|
ErrorCodeDefinition,
|
|
@@ -69,18 +69,10 @@ import {
|
|
|
69
69
|
import {
|
|
70
70
|
FEDERATION_VERSIONS,
|
|
71
71
|
federationIdentity,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
externalDirectiveSpec,
|
|
77
|
-
extendsDirectiveSpec,
|
|
78
|
-
shareableDirectiveSpec,
|
|
79
|
-
overrideDirectiveSpec,
|
|
80
|
-
composeDirectiveSpec,
|
|
81
|
-
FEDERATION2_SPEC_DIRECTIVES,
|
|
82
|
-
ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES,
|
|
83
|
-
FEDERATION2_ONLY_SPEC_DIRECTIVES,
|
|
72
|
+
FederationDirectiveName,
|
|
73
|
+
FederationTypeName,
|
|
74
|
+
FEDERATION1_TYPES,
|
|
75
|
+
FEDERATION1_DIRECTIVES,
|
|
84
76
|
} from "./federationSpec";
|
|
85
77
|
import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
|
|
86
78
|
import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
|
|
@@ -88,7 +80,6 @@ import { didYouMean, suggestionList } from "./suggestions";
|
|
|
88
80
|
|
|
89
81
|
const linkSpec = LINK_VERSIONS.latest();
|
|
90
82
|
const tagSpec = TAG_VERSIONS.latest();
|
|
91
|
-
const inaccessibleSpec = INACCESSIBLE_VERSIONS.latest();
|
|
92
83
|
const federationSpec = FEDERATION_VERSIONS.latest();
|
|
93
84
|
|
|
94
85
|
// 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
|
|
@@ -115,6 +106,7 @@ const FEDERATION_SPECIFIC_VALIDATION_RULES = [
|
|
|
115
106
|
|
|
116
107
|
const FEDERATION_VALIDATION_RULES = specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES);
|
|
117
108
|
|
|
109
|
+
const ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES: string[] = Object.values(FederationDirectiveName);
|
|
118
110
|
|
|
119
111
|
function validateFieldSetSelections({
|
|
120
112
|
directiveName,
|
|
@@ -157,12 +149,12 @@ function validateFieldSetSelections({
|
|
|
157
149
|
if (metadata.isFieldFakeExternal(field)) {
|
|
158
150
|
onError(errorCode.err(
|
|
159
151
|
`field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
|
|
160
|
-
+ `(while it is marked @${
|
|
152
|
+
+ `(while it is marked @${FederationDirectiveName.EXTERNAL}, it is a @${FederationDirectiveName.KEY} field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`,
|
|
161
153
|
{ nodes: field.sourceAST }
|
|
162
154
|
));
|
|
163
155
|
} else {
|
|
164
156
|
onError(errorCode.err(
|
|
165
|
-
`field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${
|
|
157
|
+
`field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${FederationDirectiveName.EXTERNAL})`,
|
|
166
158
|
{ nodes: field.sourceAST }
|
|
167
159
|
));
|
|
168
160
|
}
|
|
@@ -488,6 +480,48 @@ function validateInterfaceRuntimeImplementationFieldsTypes(
|
|
|
488
480
|
}
|
|
489
481
|
}
|
|
490
482
|
|
|
483
|
+
function validateShareableNotRepeatedOnSameDeclaration(
|
|
484
|
+
element: ObjectType | FieldDefinition<ObjectType>,
|
|
485
|
+
metadata: FederationMetadata,
|
|
486
|
+
errorCollector: GraphQLError[],
|
|
487
|
+
) {
|
|
488
|
+
const shareableApplications: Directive[] = element.appliedDirectivesOf(metadata.shareableDirective());
|
|
489
|
+
if (shareableApplications.length <= 1) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
type ByExtensions = {
|
|
494
|
+
without: Directive<any, {}>[],
|
|
495
|
+
with: MultiMap<Extension<any>, Directive<any, {}>>,
|
|
496
|
+
};
|
|
497
|
+
const byExtensions = shareableApplications.reduce<ByExtensions>(
|
|
498
|
+
(acc, v) => {
|
|
499
|
+
const ext = v.ofExtension();
|
|
500
|
+
if (ext) {
|
|
501
|
+
acc.with.add(ext, v);
|
|
502
|
+
} else {
|
|
503
|
+
acc.without.push(v);
|
|
504
|
+
}
|
|
505
|
+
return acc;
|
|
506
|
+
},
|
|
507
|
+
{ without: [], with: new MultiMap() }
|
|
508
|
+
);
|
|
509
|
+
const groups = [ byExtensions.without ].concat(mapValues(byExtensions.with));
|
|
510
|
+
for (const group of groups) {
|
|
511
|
+
if (group.length > 1) {
|
|
512
|
+
const eltStr = element.kind === 'ObjectType'
|
|
513
|
+
? `the same type declaration of "${element.coordinate}"`
|
|
514
|
+
: `field "${element.coordinate}"`;
|
|
515
|
+
errorCollector.push(ERRORS.INVALID_SHAREABLE_USAGE.err(
|
|
516
|
+
`Invalid duplicate application of @shareable on ${eltStr}: `
|
|
517
|
+
+ '@shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration',
|
|
518
|
+
{ nodes: sourceASTs(...group) },
|
|
519
|
+
));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
491
525
|
const printFieldCoordinate = (f: FieldDefinition<CompositeType>): string => `"${f.coordinate}"`;
|
|
492
526
|
|
|
493
527
|
function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<ObjectType>[]]) {
|
|
@@ -608,7 +642,7 @@ export class FederationMetadata {
|
|
|
608
642
|
}
|
|
609
643
|
|
|
610
644
|
private getFederationDirective<TApplicationArgs extends {[key: string]: any}>(
|
|
611
|
-
name:
|
|
645
|
+
name: FederationDirectiveName
|
|
612
646
|
): DirectiveDefinition<TApplicationArgs> {
|
|
613
647
|
const directive = this.schema.directive(this.federationDirectiveNameInSchema(name));
|
|
614
648
|
assert(directive, `The provided schema does not have federation directive @${name}`);
|
|
@@ -616,45 +650,43 @@ export class FederationMetadata {
|
|
|
616
650
|
}
|
|
617
651
|
|
|
618
652
|
keyDirective(): DirectiveDefinition<{fields: any, resolvable?: boolean}> {
|
|
619
|
-
return this.getFederationDirective(
|
|
653
|
+
return this.getFederationDirective(FederationDirectiveName.KEY);
|
|
620
654
|
}
|
|
621
655
|
|
|
622
656
|
overrideDirective(): DirectiveDefinition<{from: string}> {
|
|
623
|
-
return this.getFederationDirective(
|
|
657
|
+
return this.getFederationDirective(FederationDirectiveName.OVERRIDE);
|
|
624
658
|
}
|
|
625
659
|
|
|
626
660
|
extendsDirective(): DirectiveDefinition<Record<string, never>> {
|
|
627
|
-
return this.getFederationDirective(
|
|
661
|
+
return this.getFederationDirective(FederationDirectiveName.EXTENDS);
|
|
628
662
|
}
|
|
629
663
|
|
|
630
664
|
externalDirective(): DirectiveDefinition<{reason: string}> {
|
|
631
|
-
return this.getFederationDirective(
|
|
665
|
+
return this.getFederationDirective(FederationDirectiveName.EXTERNAL);
|
|
632
666
|
}
|
|
633
667
|
|
|
634
668
|
requiresDirective(): DirectiveDefinition<{fields: any}> {
|
|
635
|
-
return this.getFederationDirective(
|
|
669
|
+
return this.getFederationDirective(FederationDirectiveName.REQUIRES);
|
|
636
670
|
}
|
|
637
671
|
|
|
638
672
|
providesDirective(): DirectiveDefinition<{fields: any}> {
|
|
639
|
-
return this.getFederationDirective(
|
|
673
|
+
return this.getFederationDirective(FederationDirectiveName.PROVIDES);
|
|
640
674
|
}
|
|
641
675
|
|
|
642
676
|
shareableDirective(): DirectiveDefinition<{}> {
|
|
643
|
-
return this.getFederationDirective(
|
|
677
|
+
return this.getFederationDirective(FederationDirectiveName.SHAREABLE);
|
|
644
678
|
}
|
|
645
679
|
|
|
646
680
|
tagDirective(): DirectiveDefinition<{name: string}> {
|
|
647
|
-
return this.getFederationDirective(
|
|
681
|
+
return this.getFederationDirective(FederationDirectiveName.TAG);
|
|
648
682
|
}
|
|
649
683
|
|
|
650
684
|
composeDirective(): DirectiveDefinition<{name: string}> {
|
|
651
|
-
return this.getFederationDirective(
|
|
685
|
+
return this.getFederationDirective(FederationDirectiveName.COMPOSE_DIRECTIVE);
|
|
652
686
|
}
|
|
653
687
|
|
|
654
688
|
inaccessibleDirective(): DirectiveDefinition<{}> {
|
|
655
|
-
return this.getFederationDirective(
|
|
656
|
-
inaccessibleSpec.inaccessibleDirectiveSpec.name
|
|
657
|
-
);
|
|
689
|
+
return this.getFederationDirective(FederationDirectiveName.INACCESSIBLE);
|
|
658
690
|
}
|
|
659
691
|
|
|
660
692
|
allFederationDirectives(): DirectiveDefinition[] {
|
|
@@ -685,7 +717,7 @@ export class FederationMetadata {
|
|
|
685
717
|
}
|
|
686
718
|
|
|
687
719
|
fieldSetType(): ScalarType {
|
|
688
|
-
return this.schema.type(this.federationTypeNameInSchema(
|
|
720
|
+
return this.schema.type(this.federationTypeNameInSchema(FederationTypeName.FIELD_SET)) as ScalarType;
|
|
689
721
|
}
|
|
690
722
|
|
|
691
723
|
allFederationTypes(): NamedType[] {
|
|
@@ -873,6 +905,28 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
873
905
|
validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errorCollector);
|
|
874
906
|
}
|
|
875
907
|
|
|
908
|
+
// While @shareable is "repeatable", this is only so one can use it on both a main
|
|
909
|
+
// type definition _and_ possible other type extensions. But putting 2 @shareable
|
|
910
|
+
// on the same type definition or field is both useless, and suggest some miscomprehension,
|
|
911
|
+
// so we reject it with an (hopefully helpful) error message.
|
|
912
|
+
for (const objectType of schema.objectTypes()) {
|
|
913
|
+
validateShareableNotRepeatedOnSameDeclaration(objectType, metadata, errorCollector);
|
|
914
|
+
for (const field of objectType.fields()) {
|
|
915
|
+
validateShareableNotRepeatedOnSameDeclaration(field, metadata, errorCollector);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Additionally, reject using @shareable on an interface field, as that does not actually
|
|
919
|
+
// make sense.
|
|
920
|
+
for (const shareableApplication of metadata.shareableDirective().applications()) {
|
|
921
|
+
const element = shareableApplication.parent;
|
|
922
|
+
if (element instanceof FieldDefinition && !isObjectType(element.parent)) {
|
|
923
|
+
errorCollector.push(ERRORS.INVALID_SHAREABLE_USAGE.err(
|
|
924
|
+
`Invalid use of @shareable on field "${element.coordinate}": only object type fields can be marked with @shareable`,
|
|
925
|
+
{ nodes: sourceASTs(shareableApplication, element.parent) },
|
|
926
|
+
));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
876
930
|
return errorCollector;
|
|
877
931
|
}
|
|
878
932
|
|
|
@@ -883,7 +937,7 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
883
937
|
onUnknownDirectiveValidationError(schema: Schema, unknownDirectiveName: string, error: GraphQLError): GraphQLError {
|
|
884
938
|
const metadata = federationMetadata(schema);
|
|
885
939
|
assert(metadata, `This method should only have been called on a subgraph schema`)
|
|
886
|
-
if (
|
|
940
|
+
if (ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES.includes(unknownDirectiveName)) {
|
|
887
941
|
// The directive name is "unknown" but it is a default federation directive name. So it means one of a few things
|
|
888
942
|
// happened:
|
|
889
943
|
// 1. it's a fed1 schema but the directive is a fed2 only one (only possible case for fed1 schema).
|
|
@@ -914,7 +968,7 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
914
968
|
}
|
|
915
969
|
} else if (!metadata.isFed2Schema()) {
|
|
916
970
|
// We could get here in the case where a fed1 schema has tried to use a fed2 directive but mispelled it.
|
|
917
|
-
const suggestions = suggestionList(unknownDirectiveName,
|
|
971
|
+
const suggestions = suggestionList(unknownDirectiveName, ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES);
|
|
918
972
|
if (suggestions.length > 0) {
|
|
919
973
|
return withModifiedErrorMessage(
|
|
920
974
|
error,
|
|
@@ -971,7 +1025,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
971
1025
|
core.coreItself.nameInSchema,
|
|
972
1026
|
{
|
|
973
1027
|
url: federationSpec.url.toString(),
|
|
974
|
-
import:
|
|
1028
|
+
import: federationSpec.directiveSpecs().map((spec) => `@${spec.name}`),
|
|
975
1029
|
}
|
|
976
1030
|
);
|
|
977
1031
|
const errors = completeSubgraphSchema(schema);
|
|
@@ -982,7 +1036,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
982
1036
|
|
|
983
1037
|
// This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
|
|
984
1038
|
// subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
|
|
985
|
-
export const
|
|
1039
|
+
export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.2", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective"])';
|
|
986
1040
|
|
|
987
1041
|
export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
|
|
988
1042
|
const fed2LinkExtension: SchemaExtensionNode = {
|
|
@@ -998,7 +1052,7 @@ export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
|
|
|
998
1052
|
{
|
|
999
1053
|
kind: Kind.ARGUMENT,
|
|
1000
1054
|
name: { kind: Kind.NAME, value: 'import' },
|
|
1001
|
-
value: { kind: Kind.LIST, values:
|
|
1055
|
+
value: { kind: Kind.LIST, values: federationSpec.directiveSpecs().map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
|
|
1002
1056
|
}]
|
|
1003
1057
|
}]
|
|
1004
1058
|
};
|
|
@@ -1111,8 +1165,8 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
|
|
|
1111
1165
|
// Note that, in a perfect world, we'd do this within the `SchemaUpgrader`. But the way the code
|
|
1112
1166
|
// is organised, this method is called before we reach the `SchemaUpgrader`, and it doesn't seem
|
|
1113
1167
|
// worth refactoring things drastically for that minor convenience.
|
|
1114
|
-
for (const
|
|
1115
|
-
const directive = schema.directive(
|
|
1168
|
+
for (const name of [FederationDirectiveName.KEY, FederationDirectiveName.PROVIDES, FederationDirectiveName.REQUIRES]) {
|
|
1169
|
+
const directive = schema.directive(name);
|
|
1116
1170
|
if (!directive) {
|
|
1117
1171
|
continue;
|
|
1118
1172
|
}
|
|
@@ -1153,15 +1207,9 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
|
|
|
1153
1207
|
}
|
|
1154
1208
|
}
|
|
1155
1209
|
|
|
1156
|
-
return
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
requiresDirectiveSpec.checkOrAdd(schema),
|
|
1160
|
-
providesDirectiveSpec.checkOrAdd(schema),
|
|
1161
|
-
extendsDirectiveSpec.checkOrAdd(schema),
|
|
1162
|
-
externalDirectiveSpec.checkOrAdd(schema),
|
|
1163
|
-
tagSpec.tagDirectiveSpec.checkOrAdd(schema),
|
|
1164
|
-
].flat();
|
|
1210
|
+
return FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, '_' + spec.name))
|
|
1211
|
+
.concat(FEDERATION1_DIRECTIVES.map((spec) => spec.checkOrAdd(schema)))
|
|
1212
|
+
.flat();
|
|
1165
1213
|
}
|
|
1166
1214
|
|
|
1167
1215
|
function completeFed2SubgraphSchema(schema: Schema) {
|
|
@@ -1224,7 +1272,7 @@ export function parseFieldSetArgument({
|
|
|
1224
1272
|
if (msg.endsWith('.')) {
|
|
1225
1273
|
msg = msg.slice(0, msg.length - 1);
|
|
1226
1274
|
}
|
|
1227
|
-
if (directive.name ===
|
|
1275
|
+
if (directive.name === FederationDirectiveName.KEY) {
|
|
1228
1276
|
msg = msg + ' (the field should 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).';
|
|
1229
1277
|
} else {
|
|
1230
1278
|
msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
|
|
@@ -1594,11 +1642,13 @@ class ExternalTester {
|
|
|
1594
1642
|
private readonly fakeExternalFields = new Set<string>();
|
|
1595
1643
|
private readonly providedFields = new Set<string>();
|
|
1596
1644
|
private readonly externalDirective: DirectiveDefinition<{}>;
|
|
1645
|
+
private readonly externalFieldsOnType = new Set<string>();
|
|
1597
1646
|
|
|
1598
1647
|
constructor(readonly schema: Schema) {
|
|
1599
1648
|
this.externalDirective = this.metadata().externalDirective();
|
|
1600
1649
|
this.collectFakeExternals();
|
|
1601
1650
|
this.collectProvidedFields();
|
|
1651
|
+
this.collectExternalsOnType();
|
|
1602
1652
|
}
|
|
1603
1653
|
|
|
1604
1654
|
private metadata(): FederationMetadata {
|
|
@@ -1637,8 +1687,18 @@ class ExternalTester {
|
|
|
1637
1687
|
}
|
|
1638
1688
|
}
|
|
1639
1689
|
|
|
1690
|
+
private collectExternalsOnType() {
|
|
1691
|
+
for (const type of this.schema.objectTypes()) {
|
|
1692
|
+
if (type.hasAppliedDirective(this.externalDirective)) {
|
|
1693
|
+
for (const field of type.fields()) {
|
|
1694
|
+
this.externalFieldsOnType.add(field.coordinate);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1640
1700
|
isExternal(field: FieldDefinition<any> | InputFieldDefinition) {
|
|
1641
|
-
return field.hasAppliedDirective(this.externalDirective) && !this.isFakeExternal(field);
|
|
1701
|
+
return (field.hasAppliedDirective(this.externalDirective) || this.externalFieldsOnType.has(field.coordinate)) && !this.isFakeExternal(field);
|
|
1642
1702
|
}
|
|
1643
1703
|
|
|
1644
1704
|
isFakeExternal(field: FieldDefinition<any> | InputFieldDefinition) {
|
package/src/federationSpec.ts
CHANGED
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
createDirectiveSpecification,
|
|
10
10
|
createScalarTypeSpecification,
|
|
11
11
|
DirectiveSpecification,
|
|
12
|
+
TypeSpecification,
|
|
12
13
|
} from "./directiveAndTypeSpecification";
|
|
13
14
|
import { DirectiveLocation, GraphQLError } from "graphql";
|
|
14
|
-
import { assert } from "./utils";
|
|
15
|
+
import { assert, MapWithCachedArrays } from "./utils";
|
|
15
16
|
import { TAG_VERSIONS } from "./tagSpec";
|
|
16
17
|
import { federationMetadata } from "./federation";
|
|
17
18
|
import { registerKnownFeature } from "./knownCoreFeatures";
|
|
@@ -19,10 +20,27 @@ import { INACCESSIBLE_VERSIONS } from "./inaccessibleSpec";
|
|
|
19
20
|
|
|
20
21
|
export const federationIdentity = 'https://specs.apollo.dev/federation';
|
|
21
22
|
|
|
22
|
-
export
|
|
23
|
+
export enum FederationTypeName {
|
|
24
|
+
FIELD_SET = 'FieldSet',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export enum FederationDirectiveName {
|
|
28
|
+
KEY = 'key',
|
|
29
|
+
EXTERNAL = 'external',
|
|
30
|
+
REQUIRES = 'requires',
|
|
31
|
+
PROVIDES = 'provides',
|
|
32
|
+
EXTENDS = 'extends',
|
|
33
|
+
SHAREABLE = 'shareable',
|
|
34
|
+
OVERRIDE = 'override',
|
|
35
|
+
TAG = 'tag',
|
|
36
|
+
INACCESSIBLE = 'inaccessible',
|
|
37
|
+
COMPOSE_DIRECTIVE = 'composeDirective',
|
|
38
|
+
}
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
const fieldSetTypeSpec = createScalarTypeSpecification({ name: FederationTypeName.FIELD_SET });
|
|
41
|
+
|
|
42
|
+
const keyDirectiveSpec = createDirectiveSpecification({
|
|
43
|
+
name: FederationDirectiveName.KEY,
|
|
26
44
|
locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
|
|
27
45
|
repeatable: true,
|
|
28
46
|
argumentFct: (schema) => ({
|
|
@@ -34,13 +52,13 @@ export const keyDirectiveSpec = createDirectiveSpecification({
|
|
|
34
52
|
}),
|
|
35
53
|
});
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
name:
|
|
55
|
+
const extendsDirectiveSpec = createDirectiveSpecification({
|
|
56
|
+
name: FederationDirectiveName.EXTENDS,
|
|
39
57
|
locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
|
|
40
58
|
});
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
name:
|
|
60
|
+
const externalDirectiveSpec = createDirectiveSpecification({
|
|
61
|
+
name: FederationDirectiveName.EXTERNAL,
|
|
44
62
|
locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION],
|
|
45
63
|
argumentFct: (schema) => ({
|
|
46
64
|
args: [{ name: 'reason', type: schema.stringType() }],
|
|
@@ -48,8 +66,8 @@ export const externalDirectiveSpec = createDirectiveSpecification({
|
|
|
48
66
|
}),
|
|
49
67
|
});
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
name:
|
|
69
|
+
const requiresDirectiveSpec = createDirectiveSpecification({
|
|
70
|
+
name: FederationDirectiveName.REQUIRES,
|
|
53
71
|
locations: [DirectiveLocation.FIELD_DEFINITION],
|
|
54
72
|
argumentFct: (schema) => ({
|
|
55
73
|
args: [fieldsArgument(schema)],
|
|
@@ -57,8 +75,8 @@ export const requiresDirectiveSpec = createDirectiveSpecification({
|
|
|
57
75
|
}),
|
|
58
76
|
});
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
name:
|
|
78
|
+
const providesDirectiveSpec = createDirectiveSpecification({
|
|
79
|
+
name: FederationDirectiveName.PROVIDES,
|
|
62
80
|
locations: [DirectiveLocation.FIELD_DEFINITION],
|
|
63
81
|
argumentFct: (schema) => ({
|
|
64
82
|
args: [fieldsArgument(schema)],
|
|
@@ -66,29 +84,21 @@ export const providesDirectiveSpec = createDirectiveSpecification({
|
|
|
66
84
|
}),
|
|
67
85
|
});
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
87
|
+
const legacyFederationTypes = [
|
|
88
|
+
fieldSetTypeSpec,
|
|
89
|
+
];
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
const legacyFederationDirectives = [
|
|
92
|
+
keyDirectiveSpec,
|
|
93
|
+
requiresDirectiveSpec,
|
|
94
|
+
providesDirectiveSpec,
|
|
95
|
+
externalDirectiveSpec,
|
|
96
|
+
TAG_VERSIONS.latest().tagDirectiveSpec,
|
|
97
|
+
extendsDirectiveSpec,
|
|
98
|
+
];
|
|
82
99
|
|
|
83
|
-
export const
|
|
84
|
-
|
|
85
|
-
locations: [DirectiveLocation.SCHEMA],
|
|
86
|
-
repeatable: true,
|
|
87
|
-
argumentFct: (schema) => ({
|
|
88
|
-
args: [{ name: 'name', type: schema.stringType() }],
|
|
89
|
-
errors: [],
|
|
90
|
-
}),
|
|
91
|
-
})
|
|
100
|
+
export const FEDERATION1_TYPES = legacyFederationTypes;
|
|
101
|
+
export const FEDERATION1_DIRECTIVES = legacyFederationDirectives;
|
|
92
102
|
|
|
93
103
|
function fieldsArgument(schema: Schema): ArgumentSpecification {
|
|
94
104
|
return { name: 'fields', type: fieldSetType(schema) };
|
|
@@ -100,50 +110,65 @@ function fieldSetType(schema: Schema): InputType {
|
|
|
100
110
|
return new NonNullType(metadata.fieldSetType());
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
overrideDirectiveSpec,
|
|
107
|
-
];
|
|
113
|
+
export class FederationSpecDefinition extends FeatureDefinition {
|
|
114
|
+
private readonly _directiveSpecs = new MapWithCachedArrays<string, DirectiveSpecification>();
|
|
115
|
+
private readonly _typeSpecs = new MapWithCachedArrays<string, TypeSpecification>();
|
|
108
116
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
];
|
|
117
|
+
constructor(version: FeatureVersion) {
|
|
118
|
+
super(new FeatureUrl(federationIdentity, 'federation', version));
|
|
112
119
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
providesDirectiveSpec,
|
|
117
|
-
externalDirectiveSpec,
|
|
118
|
-
TAG_VERSIONS.latest().tagDirectiveSpec,
|
|
119
|
-
extendsDirectiveSpec, // TODO: should we stop supporting that?
|
|
120
|
-
];
|
|
120
|
+
for (const type of legacyFederationTypes) {
|
|
121
|
+
this.registerType(type);
|
|
122
|
+
}
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
...FEDERATION2_ONLY_SPEC_DIRECTIVES,
|
|
126
|
-
...FEDERATION2_1_ONLY_SPEC_DIRECTIVES,
|
|
127
|
-
];
|
|
124
|
+
for (const directive of legacyFederationDirectives) {
|
|
125
|
+
this.registerDirective(directive);
|
|
126
|
+
}
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
this.registerDirective(createDirectiveSpecification({
|
|
129
|
+
name: FederationDirectiveName.SHAREABLE,
|
|
130
|
+
locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION],
|
|
131
|
+
repeatable: version >= (new FeatureVersion(2, 2)),
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
this.registerDirective(INACCESSIBLE_VERSIONS.latest().inaccessibleDirectiveSpec);
|
|
135
|
+
|
|
136
|
+
this.registerDirective(createDirectiveSpecification({
|
|
137
|
+
name: FederationDirectiveName.OVERRIDE,
|
|
138
|
+
locations: [DirectiveLocation.FIELD_DEFINITION],
|
|
139
|
+
argumentFct: (schema) => ({
|
|
140
|
+
args: [{ name: 'from', type: new NonNullType(schema.stringType()) }],
|
|
141
|
+
errors: [],
|
|
142
|
+
}),
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
if (version >= (new FeatureVersion(2, 1))) {
|
|
146
|
+
this.registerDirective(createDirectiveSpecification({
|
|
147
|
+
name: FederationDirectiveName.COMPOSE_DIRECTIVE,
|
|
148
|
+
locations: [DirectiveLocation.SCHEMA],
|
|
149
|
+
repeatable: true,
|
|
150
|
+
argumentFct: (schema) => ({
|
|
151
|
+
args: [{ name: 'name', type: schema.stringType() }],
|
|
152
|
+
errors: [],
|
|
153
|
+
}),
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
133
157
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
private registerDirective(spec: DirectiveSpecification) {
|
|
159
|
+
this._directiveSpecs.set(spec.name, spec);
|
|
160
|
+
}
|
|
137
161
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
super(new FeatureUrl(federationIdentity, 'federation', version));
|
|
162
|
+
private registerType(spec: TypeSpecification) {
|
|
163
|
+
this._typeSpecs.set(spec.name, spec);
|
|
141
164
|
}
|
|
142
165
|
|
|
143
|
-
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
directiveSpecs(): readonly DirectiveSpecification[] {
|
|
167
|
+
return this._directiveSpecs.values();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
typeSpecs(): readonly TypeSpecification[] {
|
|
171
|
+
return this._typeSpecs.values();
|
|
147
172
|
}
|
|
148
173
|
|
|
149
174
|
addElementsToSchema(schema: Schema): GraphQLError[] {
|
|
@@ -151,23 +176,25 @@ export class FederationSpecDefinition extends FeatureDefinition {
|
|
|
151
176
|
assert(feature, 'The federation specification should have been added to the schema before this is called');
|
|
152
177
|
|
|
153
178
|
let errors: GraphQLError[] = [];
|
|
154
|
-
|
|
179
|
+
for (const type of this.typeSpecs()) {
|
|
180
|
+
errors = errors.concat(this.addTypeSpec(schema, type));
|
|
181
|
+
}
|
|
155
182
|
|
|
156
|
-
for (const directive of this.
|
|
183
|
+
for (const directive of this.directiveSpecs()) {
|
|
157
184
|
errors = errors.concat(this.addDirectiveSpec(schema, directive));
|
|
158
185
|
}
|
|
159
186
|
return errors;
|
|
160
187
|
}
|
|
161
188
|
|
|
162
189
|
allElementNames(): string[] {
|
|
163
|
-
return this.
|
|
164
|
-
|
|
165
|
-
])
|
|
190
|
+
return this.directiveSpecs().map((spec) => `@${spec.name}`)
|
|
191
|
+
.concat(this.typeSpecs().map((spec) => spec.name));
|
|
166
192
|
}
|
|
167
193
|
}
|
|
168
194
|
|
|
169
195
|
export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
|
|
170
196
|
.add(new FederationSpecDefinition(new FeatureVersion(2, 0)))
|
|
171
|
-
.add(new FederationSpecDefinition(new FeatureVersion(2, 1)))
|
|
197
|
+
.add(new FederationSpecDefinition(new FeatureVersion(2, 1)))
|
|
198
|
+
.add(new FederationSpecDefinition(new FeatureVersion(2, 2)));
|
|
172
199
|
|
|
173
200
|
registerKnownFeature(FEDERATION_VERSIONS);
|
package/src/introspection.ts
CHANGED
|
@@ -2,6 +2,16 @@ import { DirectiveLocation } from "graphql";
|
|
|
2
2
|
import { EnumType, FieldDefinition, ListType, NonNullType, ObjectType, Schema } from "./definitions";
|
|
3
3
|
|
|
4
4
|
export const introspectionFieldNames = [ '__schema', '__type' ];
|
|
5
|
+
export const introspectionTypeNames = [
|
|
6
|
+
'__Schema',
|
|
7
|
+
'__Directive',
|
|
8
|
+
'__DirectiveLocation',
|
|
9
|
+
'__Type',
|
|
10
|
+
'__Field',
|
|
11
|
+
'__InputValue',
|
|
12
|
+
'__EnumValue',
|
|
13
|
+
'__TypeKind',
|
|
14
|
+
]
|
|
5
15
|
|
|
6
16
|
export function isIntrospectionName(name: string): boolean {
|
|
7
17
|
return name.startsWith('__');
|
package/src/operations.ts
CHANGED
|
@@ -690,10 +690,35 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
690
690
|
* @param type - the type at which we're looking at applying the fragment
|
|
691
691
|
*/
|
|
692
692
|
canApplyAtType(type: CompositeType): boolean {
|
|
693
|
-
|
|
693
|
+
const applyAtType =
|
|
694
694
|
sameType(this.typeCondition, type)
|
|
695
|
-
|| (isAbstractType(this.typeCondition) && !isUnionType(type) && isDirectSubtype(this.typeCondition, type))
|
|
696
|
-
|
|
695
|
+
|| (isAbstractType(this.typeCondition) && !isUnionType(type) && isDirectSubtype(this.typeCondition, type));
|
|
696
|
+
return applyAtType
|
|
697
|
+
&& this.validForSchema(type.schema());
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Checks whether this named fragment can be applied to the provided schema, which might be different
|
|
701
|
+
// from the one the named fragment originate from.
|
|
702
|
+
private validForSchema(schema: Schema): boolean {
|
|
703
|
+
if (schema === this.schema()) {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const typeInSchema = schema.type(this.typeCondition.name);
|
|
708
|
+
if (!typeInSchema || !isCompositeType(typeInSchema)) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// We try "rebasing" the selection into the provided schema and checks if that succeed.
|
|
713
|
+
try {
|
|
714
|
+
const rebasedSelection = new SelectionSet(typeInSchema);
|
|
715
|
+
rebasedSelection.mergeIn(this.selectionSet);
|
|
716
|
+
// If this succeed, it means the fragment could be applied to that schema and be valid.
|
|
717
|
+
return true;
|
|
718
|
+
} catch (e) {
|
|
719
|
+
// We don't really care what kind of error was triggered; only that it doesn't work.
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
697
722
|
}
|
|
698
723
|
|
|
699
724
|
toString(indent?: string): string {
|