@apollo/federation-internals 2.1.4 → 2.2.1
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 +11 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +7 -0
- package/dist/buildSchema.js.map +1 -1
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +1 -1
- package/dist/coreSpec.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 +13 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +100 -39
- 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 +59 -2
- package/src/buildSchema.ts +9 -0
- package/src/coreSpec.ts +3 -5
- package/src/error.ts +7 -0
- package/src/extractSubgraphsFromSupergraph.ts +17 -2
- package/src/federation.ts +150 -56
- 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>[]]) {
|
|
@@ -607,54 +641,66 @@ export class FederationMetadata {
|
|
|
607
641
|
}
|
|
608
642
|
}
|
|
609
643
|
|
|
610
|
-
|
|
611
|
-
|
|
644
|
+
// Should only be be called for "legacy" directives, those that existed in 2.0. This
|
|
645
|
+
// allow to avoiding have to double-check the directive exists every time when we
|
|
646
|
+
// know it will always exists (note that even though we accept fed1 schema as inputs,
|
|
647
|
+
// those are almost immediately converted to fed2 ones by the `SchemaUpgrader`, so
|
|
648
|
+
// we include @shareable or @override in those "legacy" directives).
|
|
649
|
+
private getLegacyFederationDirective<TApplicationArgs extends {[key: string]: any}>(
|
|
650
|
+
name: FederationDirectiveName
|
|
612
651
|
): DirectiveDefinition<TApplicationArgs> {
|
|
613
|
-
const directive = this.
|
|
652
|
+
const directive = this.getFederationDirective<TApplicationArgs>(name);
|
|
614
653
|
assert(directive, `The provided schema does not have federation directive @${name}`);
|
|
615
|
-
return directive
|
|
654
|
+
return directive;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private getFederationDirective<TApplicationArgs extends {[key: string]: any}>(
|
|
658
|
+
name: FederationDirectiveName
|
|
659
|
+
): DirectiveDefinition<TApplicationArgs> | undefined {
|
|
660
|
+
return this.schema.directive(this.federationDirectiveNameInSchema(name)) as DirectiveDefinition<TApplicationArgs> | undefined;
|
|
616
661
|
}
|
|
617
662
|
|
|
618
663
|
keyDirective(): DirectiveDefinition<{fields: any, resolvable?: boolean}> {
|
|
619
|
-
return this.
|
|
664
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.KEY);
|
|
620
665
|
}
|
|
621
666
|
|
|
622
667
|
overrideDirective(): DirectiveDefinition<{from: string}> {
|
|
623
|
-
return this.
|
|
668
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.OVERRIDE);
|
|
624
669
|
}
|
|
625
670
|
|
|
626
671
|
extendsDirective(): DirectiveDefinition<Record<string, never>> {
|
|
627
|
-
return this.
|
|
672
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.EXTENDS);
|
|
628
673
|
}
|
|
629
674
|
|
|
630
675
|
externalDirective(): DirectiveDefinition<{reason: string}> {
|
|
631
|
-
return this.
|
|
676
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.EXTERNAL);
|
|
632
677
|
}
|
|
633
678
|
|
|
634
679
|
requiresDirective(): DirectiveDefinition<{fields: any}> {
|
|
635
|
-
return this.
|
|
680
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.REQUIRES);
|
|
636
681
|
}
|
|
637
682
|
|
|
638
683
|
providesDirective(): DirectiveDefinition<{fields: any}> {
|
|
639
|
-
return this.
|
|
684
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.PROVIDES);
|
|
640
685
|
}
|
|
641
686
|
|
|
642
687
|
shareableDirective(): DirectiveDefinition<{}> {
|
|
643
|
-
return this.
|
|
688
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.SHAREABLE);
|
|
644
689
|
}
|
|
645
690
|
|
|
646
691
|
tagDirective(): DirectiveDefinition<{name: string}> {
|
|
647
|
-
return this.
|
|
692
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.TAG);
|
|
648
693
|
}
|
|
649
694
|
|
|
650
|
-
composeDirective(): DirectiveDefinition<{name: string}> {
|
|
651
|
-
return this.getFederationDirective(
|
|
695
|
+
composeDirective(): DirectiveDefinition<{name: string}> | FederationDirectiveNotDefinedInSchema<{name: string}> {
|
|
696
|
+
return this.getFederationDirective<{name: string}>(FederationDirectiveName.COMPOSE_DIRECTIVE) ?? {
|
|
697
|
+
name: FederationDirectiveName.COMPOSE_DIRECTIVE,
|
|
698
|
+
applications: () => new Array<Directive<any, {name: string}>>(),
|
|
699
|
+
};
|
|
652
700
|
}
|
|
653
701
|
|
|
654
702
|
inaccessibleDirective(): DirectiveDefinition<{}> {
|
|
655
|
-
return this.
|
|
656
|
-
inaccessibleSpec.inaccessibleDirectiveSpec.name
|
|
657
|
-
);
|
|
703
|
+
return this.getLegacyFederationDirective(FederationDirectiveName.INACCESSIBLE);
|
|
658
704
|
}
|
|
659
705
|
|
|
660
706
|
allFederationDirectives(): DirectiveDefinition[] {
|
|
@@ -666,9 +712,18 @@ export class FederationMetadata {
|
|
|
666
712
|
this.tagDirective(),
|
|
667
713
|
this.extendsDirective(),
|
|
668
714
|
];
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
715
|
+
if (!this.isFed2Schema()) {
|
|
716
|
+
return baseDirectives;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
baseDirectives.push(this.shareableDirective());
|
|
720
|
+
baseDirectives.push(this.inaccessibleDirective());
|
|
721
|
+
baseDirectives.push(this.overrideDirective());
|
|
722
|
+
const composeDirective = this.composeDirective();
|
|
723
|
+
if (isFederationDirectiveDefinedInSchema(composeDirective)) {
|
|
724
|
+
baseDirectives.push(composeDirective);
|
|
725
|
+
}
|
|
726
|
+
return baseDirectives;
|
|
672
727
|
}
|
|
673
728
|
|
|
674
729
|
// Note that a subgraph may have no "entities" and so no _EntityType.
|
|
@@ -685,7 +740,7 @@ export class FederationMetadata {
|
|
|
685
740
|
}
|
|
686
741
|
|
|
687
742
|
fieldSetType(): ScalarType {
|
|
688
|
-
return this.schema.type(this.federationTypeNameInSchema(
|
|
743
|
+
return this.schema.type(this.federationTypeNameInSchema(FederationTypeName.FIELD_SET)) as ScalarType;
|
|
689
744
|
}
|
|
690
745
|
|
|
691
746
|
allFederationTypes(): NamedType[] {
|
|
@@ -702,6 +757,17 @@ export class FederationMetadata {
|
|
|
702
757
|
}
|
|
703
758
|
}
|
|
704
759
|
|
|
760
|
+
export type FederationDirectiveNotDefinedInSchema<TApplicationArgs extends {[key: string]: any}> = {
|
|
761
|
+
name: string,
|
|
762
|
+
applications: () => readonly Directive<any, TApplicationArgs>[],
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function isFederationDirectiveDefinedInSchema<TApplicationArgs extends {[key: string]: any}>(
|
|
766
|
+
definition: DirectiveDefinition<TApplicationArgs> | FederationDirectiveNotDefinedInSchema<TApplicationArgs>
|
|
767
|
+
): definition is DirectiveDefinition<TApplicationArgs> {
|
|
768
|
+
return definition instanceof DirectiveDefinition;
|
|
769
|
+
}
|
|
770
|
+
|
|
705
771
|
export class FederationBlueprint extends SchemaBlueprint {
|
|
706
772
|
constructor(private readonly withRootTypeRenaming: boolean) {
|
|
707
773
|
super();
|
|
@@ -873,6 +939,28 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
873
939
|
validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errorCollector);
|
|
874
940
|
}
|
|
875
941
|
|
|
942
|
+
// While @shareable is "repeatable", this is only so one can use it on both a main
|
|
943
|
+
// type definition _and_ possible other type extensions. But putting 2 @shareable
|
|
944
|
+
// on the same type definition or field is both useless, and suggest some miscomprehension,
|
|
945
|
+
// so we reject it with an (hopefully helpful) error message.
|
|
946
|
+
for (const objectType of schema.objectTypes()) {
|
|
947
|
+
validateShareableNotRepeatedOnSameDeclaration(objectType, metadata, errorCollector);
|
|
948
|
+
for (const field of objectType.fields()) {
|
|
949
|
+
validateShareableNotRepeatedOnSameDeclaration(field, metadata, errorCollector);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// Additionally, reject using @shareable on an interface field, as that does not actually
|
|
953
|
+
// make sense.
|
|
954
|
+
for (const shareableApplication of metadata.shareableDirective().applications()) {
|
|
955
|
+
const element = shareableApplication.parent;
|
|
956
|
+
if (element instanceof FieldDefinition && !isObjectType(element.parent)) {
|
|
957
|
+
errorCollector.push(ERRORS.INVALID_SHAREABLE_USAGE.err(
|
|
958
|
+
`Invalid use of @shareable on field "${element.coordinate}": only object type fields can be marked with @shareable`,
|
|
959
|
+
{ nodes: sourceASTs(shareableApplication, element.parent) },
|
|
960
|
+
));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
876
964
|
return errorCollector;
|
|
877
965
|
}
|
|
878
966
|
|
|
@@ -883,7 +971,7 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
883
971
|
onUnknownDirectiveValidationError(schema: Schema, unknownDirectiveName: string, error: GraphQLError): GraphQLError {
|
|
884
972
|
const metadata = federationMetadata(schema);
|
|
885
973
|
assert(metadata, `This method should only have been called on a subgraph schema`)
|
|
886
|
-
if (
|
|
974
|
+
if (ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES.includes(unknownDirectiveName)) {
|
|
887
975
|
// The directive name is "unknown" but it is a default federation directive name. So it means one of a few things
|
|
888
976
|
// happened:
|
|
889
977
|
// 1. it's a fed1 schema but the directive is a fed2 only one (only possible case for fed1 schema).
|
|
@@ -914,7 +1002,7 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
914
1002
|
}
|
|
915
1003
|
} else if (!metadata.isFed2Schema()) {
|
|
916
1004
|
// 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,
|
|
1005
|
+
const suggestions = suggestionList(unknownDirectiveName, ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES);
|
|
918
1006
|
if (suggestions.length > 0) {
|
|
919
1007
|
return withModifiedErrorMessage(
|
|
920
1008
|
error,
|
|
@@ -971,7 +1059,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
971
1059
|
core.coreItself.nameInSchema,
|
|
972
1060
|
{
|
|
973
1061
|
url: federationSpec.url.toString(),
|
|
974
|
-
import:
|
|
1062
|
+
import: federationSpec.directiveSpecs().map((spec) => `@${spec.name}`),
|
|
975
1063
|
}
|
|
976
1064
|
);
|
|
977
1065
|
const errors = completeSubgraphSchema(schema);
|
|
@@ -982,7 +1070,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
982
1070
|
|
|
983
1071
|
// This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
|
|
984
1072
|
// 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
|
|
1073
|
+
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
1074
|
|
|
987
1075
|
export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
|
|
988
1076
|
const fed2LinkExtension: SchemaExtensionNode = {
|
|
@@ -998,7 +1086,7 @@ export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
|
|
|
998
1086
|
{
|
|
999
1087
|
kind: Kind.ARGUMENT,
|
|
1000
1088
|
name: { kind: Kind.NAME, value: 'import' },
|
|
1001
|
-
value: { kind: Kind.LIST, values:
|
|
1089
|
+
value: { kind: Kind.LIST, values: federationSpec.directiveSpecs().map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
|
|
1002
1090
|
}]
|
|
1003
1091
|
}]
|
|
1004
1092
|
};
|
|
@@ -1111,8 +1199,8 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
|
|
|
1111
1199
|
// Note that, in a perfect world, we'd do this within the `SchemaUpgrader`. But the way the code
|
|
1112
1200
|
// is organised, this method is called before we reach the `SchemaUpgrader`, and it doesn't seem
|
|
1113
1201
|
// worth refactoring things drastically for that minor convenience.
|
|
1114
|
-
for (const
|
|
1115
|
-
const directive = schema.directive(
|
|
1202
|
+
for (const name of [FederationDirectiveName.KEY, FederationDirectiveName.PROVIDES, FederationDirectiveName.REQUIRES]) {
|
|
1203
|
+
const directive = schema.directive(name);
|
|
1116
1204
|
if (!directive) {
|
|
1117
1205
|
continue;
|
|
1118
1206
|
}
|
|
@@ -1153,15 +1241,9 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
|
|
|
1153
1241
|
}
|
|
1154
1242
|
}
|
|
1155
1243
|
|
|
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();
|
|
1244
|
+
return FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, '_' + spec.name))
|
|
1245
|
+
.concat(FEDERATION1_DIRECTIVES.map((spec) => spec.checkOrAdd(schema)))
|
|
1246
|
+
.flat();
|
|
1165
1247
|
}
|
|
1166
1248
|
|
|
1167
1249
|
function completeFed2SubgraphSchema(schema: Schema) {
|
|
@@ -1224,7 +1306,7 @@ export function parseFieldSetArgument({
|
|
|
1224
1306
|
if (msg.endsWith('.')) {
|
|
1225
1307
|
msg = msg.slice(0, msg.length - 1);
|
|
1226
1308
|
}
|
|
1227
|
-
if (directive.name ===
|
|
1309
|
+
if (directive.name === FederationDirectiveName.KEY) {
|
|
1228
1310
|
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
1311
|
} else {
|
|
1230
1312
|
msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
|
|
@@ -1594,11 +1676,13 @@ class ExternalTester {
|
|
|
1594
1676
|
private readonly fakeExternalFields = new Set<string>();
|
|
1595
1677
|
private readonly providedFields = new Set<string>();
|
|
1596
1678
|
private readonly externalDirective: DirectiveDefinition<{}>;
|
|
1679
|
+
private readonly externalFieldsOnType = new Set<string>();
|
|
1597
1680
|
|
|
1598
1681
|
constructor(readonly schema: Schema) {
|
|
1599
1682
|
this.externalDirective = this.metadata().externalDirective();
|
|
1600
1683
|
this.collectFakeExternals();
|
|
1601
1684
|
this.collectProvidedFields();
|
|
1685
|
+
this.collectExternalsOnType();
|
|
1602
1686
|
}
|
|
1603
1687
|
|
|
1604
1688
|
private metadata(): FederationMetadata {
|
|
@@ -1637,8 +1721,18 @@ class ExternalTester {
|
|
|
1637
1721
|
}
|
|
1638
1722
|
}
|
|
1639
1723
|
|
|
1724
|
+
private collectExternalsOnType() {
|
|
1725
|
+
for (const type of this.schema.objectTypes()) {
|
|
1726
|
+
if (type.hasAppliedDirective(this.externalDirective)) {
|
|
1727
|
+
for (const field of type.fields()) {
|
|
1728
|
+
this.externalFieldsOnType.add(field.coordinate);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1640
1734
|
isExternal(field: FieldDefinition<any> | InputFieldDefinition) {
|
|
1641
|
-
return field.hasAppliedDirective(this.externalDirective) && !this.isFakeExternal(field);
|
|
1735
|
+
return (field.hasAppliedDirective(this.externalDirective) || this.externalFieldsOnType.has(field.coordinate)) && !this.isFakeExternal(field);
|
|
1642
1736
|
}
|
|
1643
1737
|
|
|
1644
1738
|
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);
|