@apollo/federation-internals 2.2.2 → 2.3.0-alpha.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/dist/definitions.d.ts +2 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +14 -2
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +3 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +17 -12
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +31 -5
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +14 -6
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +141 -62
- package/dist/federation.js.map +1 -1
- package/dist/federationSpec.d.ts +2 -1
- package/dist/federationSpec.d.ts.map +1 -1
- package/dist/federationSpec.js +9 -1
- package/dist/federationSpec.js.map +1 -1
- package/dist/joinSpec.d.ts +9 -1
- package/dist/joinSpec.d.ts.map +1 -1
- package/dist/joinSpec.js +24 -2
- package/dist/joinSpec.js.map +1 -1
- package/dist/operations.d.ts +12 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +59 -5
- package/dist/operations.js.map +1 -1
- package/dist/schemaUpgrader.d.ts.map +1 -1
- package/dist/schemaUpgrader.js +9 -0
- package/dist/schemaUpgrader.js.map +1 -1
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +1 -0
- package/dist/supergraphs.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +0 -1
- package/src/__tests__/schemaUpgrader.test.ts +43 -0
- package/src/__tests__/subgraphValidation.test.ts +126 -57
- package/src/__tests__/testUtils.ts +28 -0
- package/src/__tests__/values.test.ts +1 -1
- package/src/definitions.ts +12 -0
- package/src/error.ts +34 -16
- package/src/extractSubgraphsFromSupergraph.ts +40 -9
- package/src/federation.ts +178 -73
- package/src/federationSpec.ts +10 -1
- package/src/joinSpec.ts +40 -11
- package/src/operations.ts +76 -8
- package/src/schemaUpgrader.ts +13 -0
- package/src/supergraphs.ts +1 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
sourceASTs,
|
|
29
29
|
UnionType,
|
|
30
30
|
} from "./definitions";
|
|
31
|
-
import { assert,
|
|
31
|
+
import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues } from "./utils";
|
|
32
32
|
import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
33
33
|
import { specifiedSDLRules } from "graphql/validation/specifiedRules";
|
|
34
34
|
import {
|
|
@@ -40,8 +40,11 @@ import {
|
|
|
40
40
|
PossibleTypeExtensionsRule,
|
|
41
41
|
print as printAST,
|
|
42
42
|
Source,
|
|
43
|
-
SchemaExtensionNode,
|
|
44
43
|
GraphQLErrorOptions,
|
|
44
|
+
SchemaDefinitionNode,
|
|
45
|
+
OperationTypeNode,
|
|
46
|
+
OperationTypeDefinitionNode,
|
|
47
|
+
ConstDirectiveNode,
|
|
45
48
|
} from "graphql";
|
|
46
49
|
import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
|
|
47
50
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
@@ -302,6 +305,7 @@ function validateAllFieldSet<TParent extends SchemaElement<any, any>>({
|
|
|
302
305
|
isOnParentType = false,
|
|
303
306
|
allowOnNonExternalLeafFields = false,
|
|
304
307
|
allowFieldsWithArguments = false,
|
|
308
|
+
allowOnInterface = false,
|
|
305
309
|
onFields,
|
|
306
310
|
}: {
|
|
307
311
|
definition: DirectiveDefinition<{fields: any}>,
|
|
@@ -311,13 +315,14 @@ function validateAllFieldSet<TParent extends SchemaElement<any, any>>({
|
|
|
311
315
|
isOnParentType?: boolean,
|
|
312
316
|
allowOnNonExternalLeafFields?: boolean,
|
|
313
317
|
allowFieldsWithArguments?: boolean,
|
|
318
|
+
allowOnInterface?: boolean,
|
|
314
319
|
onFields?: (field: FieldDefinition<any>) => void,
|
|
315
320
|
}): void {
|
|
316
321
|
for (const application of definition.applications()) {
|
|
317
322
|
const elt = application.parent as TParent;
|
|
318
323
|
const type = targetTypeExtractor(elt);
|
|
319
324
|
const parentType = isOnParentType ? type : (elt.parent as NamedType);
|
|
320
|
-
if (isInterfaceType(parentType)) {
|
|
325
|
+
if (isInterfaceType(parentType) && !allowOnInterface) {
|
|
321
326
|
const code = ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name);
|
|
322
327
|
errorCollector.push(code.err(
|
|
323
328
|
isOnParentType
|
|
@@ -438,43 +443,64 @@ function validateNoExternalOnInterfaceFields(metadata: FederationMetadata, error
|
|
|
438
443
|
}
|
|
439
444
|
}
|
|
440
445
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const implemField = type.field(field.name);
|
|
463
|
-
if (!implemField) continue;
|
|
464
|
-
if (implemField.sourceAST) {
|
|
465
|
-
nodes.push(implemField.sourceAST);
|
|
446
|
+
function validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata: FederationMetadata, errorCollector: GraphQLError[]): void {
|
|
447
|
+
for (const itfType of metadata.schema.interfaceTypes()) {
|
|
448
|
+
const implementations = itfType.possibleRuntimeTypes();
|
|
449
|
+
for (const keyApplication of itfType.appliedDirectivesOf(metadata.keyDirective())) {
|
|
450
|
+
// Note that we will always have validated all @key fields at this point, so not bothering with extra validation
|
|
451
|
+
const fields = parseFieldSetArgument({parentType: itfType, directive: keyApplication, validate: false});
|
|
452
|
+
const isResolvable = !(keyApplication.arguments().resolvable === false);
|
|
453
|
+
const implementationsWithKeyButNotResolvable = new Array<ObjectType>();
|
|
454
|
+
const implementationsMissingKey = new Array<ObjectType>();
|
|
455
|
+
for (const type of implementations) {
|
|
456
|
+
const matchingApp = type.appliedDirectivesOf(metadata.keyDirective()).find((app) => {
|
|
457
|
+
const appFields = parseFieldSetArgument({parentType: type, directive: app, validate: false});
|
|
458
|
+
return fields.equals(appFields);
|
|
459
|
+
});
|
|
460
|
+
if (matchingApp) {
|
|
461
|
+
if (isResolvable && matchingApp.arguments().resolvable === false) {
|
|
462
|
+
implementationsWithKeyButNotResolvable.push(type);
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
implementationsMissingKey.push(type);
|
|
466
|
+
}
|
|
466
467
|
}
|
|
467
|
-
|
|
468
|
-
|
|
468
|
+
|
|
469
|
+
if (implementationsMissingKey.length > 0) {
|
|
470
|
+
const typesString = printHumanReadableList(
|
|
471
|
+
implementationsMissingKey.map((i) => `"${i.coordinate}"`),
|
|
472
|
+
{
|
|
473
|
+
prefix: 'type',
|
|
474
|
+
prefixPlural: 'types',
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
errorCollector.push(ERRORS.INTERFACE_KEY_NOT_ON_IMPLEMENTATION.err(
|
|
478
|
+
`Key ${keyApplication} on interface type "${itfType.coordinate}" is missing on implementation ${typesString}.`,
|
|
479
|
+
{ nodes: sourceASTs(...implementationsMissingKey) },
|
|
480
|
+
));
|
|
481
|
+
} else if (implementationsWithKeyButNotResolvable.length > 0) {
|
|
482
|
+
const typesString = printHumanReadableList(
|
|
483
|
+
implementationsWithKeyButNotResolvable.map((i) => `"${i.coordinate}"`),
|
|
484
|
+
{
|
|
485
|
+
prefix: 'type',
|
|
486
|
+
prefixPlural: 'types',
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
errorCollector.push(ERRORS.INTERFACE_KEY_NOT_ON_IMPLEMENTATION.err(
|
|
490
|
+
`Key ${keyApplication} on interface type "${itfType.coordinate}" should be resolvable on all implementation types, but is declared with argument "@key(resolvable:)" set to false in ${typesString}.`,
|
|
491
|
+
{ nodes: sourceASTs(...implementationsWithKeyButNotResolvable) },
|
|
492
|
+
));
|
|
469
493
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function validateInterfaceObjectsAreOnEntities(metadata: FederationMetadata, errorCollector: GraphQLError[]): void {
|
|
499
|
+
for (const application of metadata.interfaceObjectDirective().applications()) {
|
|
500
|
+
if (!isEntityType(application.parent)) {
|
|
501
|
+
errorCollector.push(ERRORS.INTERFACE_OBJECT_USAGE_ERROR.err(
|
|
502
|
+
`The @interfaceObject directive can only be applied to entity types but type "${application.parent.coordinate}" has no @key in this subgraph.`,
|
|
503
|
+
{ nodes: application.parent.sourceAST }
|
|
478
504
|
));
|
|
479
505
|
}
|
|
480
506
|
}
|
|
@@ -521,13 +547,6 @@ function validateShareableNotRepeatedOnSameDeclaration(
|
|
|
521
547
|
}
|
|
522
548
|
}
|
|
523
549
|
|
|
524
|
-
|
|
525
|
-
const printFieldCoordinate = (f: FieldDefinition<CompositeType>): string => `"${f.coordinate}"`;
|
|
526
|
-
|
|
527
|
-
function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<ObjectType>[]]) {
|
|
528
|
-
return `${joinStrings(implems.map(printFieldCoordinate))} ${implems.length == 1 ? 'has' : 'have'} type "${type}"`;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
550
|
export class FederationMetadata {
|
|
532
551
|
private _externalTester?: ExternalTester;
|
|
533
552
|
private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
|
|
@@ -606,6 +625,11 @@ export class FederationMetadata {
|
|
|
606
625
|
return this.sharingPredicate()(field);
|
|
607
626
|
}
|
|
608
627
|
|
|
628
|
+
isInterfaceObjectType(type: NamedType): type is ObjectType {
|
|
629
|
+
return isObjectType(type)
|
|
630
|
+
&& hasAppliedDirective(type, this.interfaceObjectDirective());
|
|
631
|
+
}
|
|
632
|
+
|
|
609
633
|
federationDirectiveNameInSchema(name: string): string {
|
|
610
634
|
if (this.isFed2Schema()) {
|
|
611
635
|
const coreFeatures = this.schema.coreFeatures;
|
|
@@ -660,6 +684,15 @@ export class FederationMetadata {
|
|
|
660
684
|
return this.schema.directive(this.federationDirectiveNameInSchema(name)) as DirectiveDefinition<TApplicationArgs> | undefined;
|
|
661
685
|
}
|
|
662
686
|
|
|
687
|
+
private getPost20FederationDirective<TApplicationArgs extends {[key: string]: any}>(
|
|
688
|
+
name: FederationDirectiveName
|
|
689
|
+
): Post20FederationDirectiveDefinition<TApplicationArgs> {
|
|
690
|
+
return this.getFederationDirective<TApplicationArgs>(name) ?? {
|
|
691
|
+
name,
|
|
692
|
+
applications: () => new Array<Directive<any, TApplicationArgs>>(),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
663
696
|
keyDirective(): DirectiveDefinition<{fields: any, resolvable?: boolean}> {
|
|
664
697
|
return this.getLegacyFederationDirective(FederationDirectiveName.KEY);
|
|
665
698
|
}
|
|
@@ -692,17 +725,18 @@ export class FederationMetadata {
|
|
|
692
725
|
return this.getLegacyFederationDirective(FederationDirectiveName.TAG);
|
|
693
726
|
}
|
|
694
727
|
|
|
695
|
-
composeDirective():
|
|
696
|
-
return this.
|
|
697
|
-
name: FederationDirectiveName.COMPOSE_DIRECTIVE,
|
|
698
|
-
applications: () => new Array<Directive<any, {name: string}>>(),
|
|
699
|
-
};
|
|
728
|
+
composeDirective(): Post20FederationDirectiveDefinition<{name: string}> {
|
|
729
|
+
return this.getPost20FederationDirective(FederationDirectiveName.COMPOSE_DIRECTIVE);
|
|
700
730
|
}
|
|
701
731
|
|
|
702
732
|
inaccessibleDirective(): DirectiveDefinition<{}> {
|
|
703
733
|
return this.getLegacyFederationDirective(FederationDirectiveName.INACCESSIBLE);
|
|
704
734
|
}
|
|
705
735
|
|
|
736
|
+
interfaceObjectDirective(): Post20FederationDirectiveDefinition<{}> {
|
|
737
|
+
return this.getPost20FederationDirective(FederationDirectiveName.INTERFACE_OBJECT);
|
|
738
|
+
}
|
|
739
|
+
|
|
706
740
|
allFederationDirectives(): DirectiveDefinition[] {
|
|
707
741
|
const baseDirectives: DirectiveDefinition[] = [
|
|
708
742
|
this.keyDirective(),
|
|
@@ -723,6 +757,11 @@ export class FederationMetadata {
|
|
|
723
757
|
if (isFederationDirectiveDefinedInSchema(composeDirective)) {
|
|
724
758
|
baseDirectives.push(composeDirective);
|
|
725
759
|
}
|
|
760
|
+
const interfaceObjectDirective = this.interfaceObjectDirective();
|
|
761
|
+
if (isFederationDirectiveDefinedInSchema(interfaceObjectDirective)) {
|
|
762
|
+
baseDirectives.push(interfaceObjectDirective);
|
|
763
|
+
}
|
|
764
|
+
|
|
726
765
|
return baseDirectives;
|
|
727
766
|
}
|
|
728
767
|
|
|
@@ -762,12 +801,20 @@ export type FederationDirectiveNotDefinedInSchema<TApplicationArgs extends {[key
|
|
|
762
801
|
applications: () => readonly Directive<any, TApplicationArgs>[],
|
|
763
802
|
}
|
|
764
803
|
|
|
804
|
+
export type Post20FederationDirectiveDefinition<TApplicationArgs extends {[key: string]: any}> =
|
|
805
|
+
DirectiveDefinition<TApplicationArgs>
|
|
806
|
+
| FederationDirectiveNotDefinedInSchema<TApplicationArgs>;
|
|
807
|
+
|
|
765
808
|
export function isFederationDirectiveDefinedInSchema<TApplicationArgs extends {[key: string]: any}>(
|
|
766
|
-
definition:
|
|
809
|
+
definition: Post20FederationDirectiveDefinition<TApplicationArgs>
|
|
767
810
|
): definition is DirectiveDefinition<TApplicationArgs> {
|
|
768
811
|
return definition instanceof DirectiveDefinition;
|
|
769
812
|
}
|
|
770
813
|
|
|
814
|
+
export function hasAppliedDirective(type: NamedType, definition: Post20FederationDirectiveDefinition<any>): boolean {
|
|
815
|
+
return isFederationDirectiveDefinedInSchema(definition) && type.hasAppliedDirective(definition);
|
|
816
|
+
}
|
|
817
|
+
|
|
771
818
|
export class FederationBlueprint extends SchemaBlueprint {
|
|
772
819
|
constructor(private readonly withRootTypeRenaming: boolean) {
|
|
773
820
|
super();
|
|
@@ -872,6 +919,7 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
872
919
|
metadata,
|
|
873
920
|
isOnParentType: true,
|
|
874
921
|
allowOnNonExternalLeafFields: true,
|
|
922
|
+
allowOnInterface: metadata.federationFeature()!.url.version.compareTo(new FeatureVersion(2, 3)) >= 0,
|
|
875
923
|
onFields: field => {
|
|
876
924
|
const type = baseType(field.type!);
|
|
877
925
|
if (isUnionType(type) || isInterfaceType(type)) {
|
|
@@ -925,6 +973,8 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
925
973
|
|
|
926
974
|
validateNoExternalOnInterfaceFields(metadata, errorCollector);
|
|
927
975
|
validateAllExternalFieldsUsed(metadata, errorCollector);
|
|
976
|
+
validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata, errorCollector);
|
|
977
|
+
validateInterfaceObjectsAreOnEntities(metadata, errorCollector);
|
|
928
978
|
|
|
929
979
|
// If tag is redefined by the user, make sure the definition is compatible with what we expect
|
|
930
980
|
const tagDirective = metadata.tagDirective();
|
|
@@ -935,10 +985,6 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
935
985
|
}
|
|
936
986
|
}
|
|
937
987
|
|
|
938
|
-
for (const itf of schema.interfaceTypes()) {
|
|
939
|
-
validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errorCollector);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
988
|
// While @shareable is "repeatable", this is only so one can use it on both a main
|
|
943
989
|
// type definition _and_ possible other type extensions. But putting 2 @shareable
|
|
944
990
|
// on the same type definition or field is both useless, and suggest some miscomprehension,
|
|
@@ -1070,15 +1116,22 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
1070
1116
|
|
|
1071
1117
|
// This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
|
|
1072
1118
|
// subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
|
|
1073
|
-
export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1119
|
+
export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Given a document that is assumed to _not_ be a fed2 schema (it does not have a `@link` to the federation spec),
|
|
1123
|
+
* returns an equivalent document that `@link` to the last known federation spec.
|
|
1124
|
+
*
|
|
1125
|
+
* @param document - the document to "augment".
|
|
1126
|
+
* @param options.addAsSchemaExtension - defines whethere the added `@link` is added as a schema extension (`extend schema`) or
|
|
1127
|
+
* added to the schema definition. Defaults to `true` (added as an extension), as this mimics what we tends to write manually.
|
|
1128
|
+
*/
|
|
1129
|
+
export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAsSchemaExtension: boolean }): DocumentNode {
|
|
1130
|
+
const directiveToAdd: ConstDirectiveNode = ({
|
|
1131
|
+
kind: Kind.DIRECTIVE,
|
|
1132
|
+
name: { kind: Kind.NAME, value: linkDirectiveDefaultName },
|
|
1133
|
+
arguments: [
|
|
1134
|
+
{
|
|
1082
1135
|
kind: Kind.ARGUMENT,
|
|
1083
1136
|
name: { kind: Kind.NAME, value: 'url' },
|
|
1084
1137
|
value: { kind: Kind.STRING, value: federationSpec.url.toString() }
|
|
@@ -1087,13 +1140,54 @@ export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
|
|
|
1087
1140
|
kind: Kind.ARGUMENT,
|
|
1088
1141
|
name: { kind: Kind.NAME, value: 'import' },
|
|
1089
1142
|
value: { kind: Kind.LIST, values: federationSpec.directiveSpecs().map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1143
|
+
}
|
|
1144
|
+
]
|
|
1145
|
+
});
|
|
1146
|
+
if (options?.addAsSchemaExtension ?? true) {
|
|
1147
|
+
return {
|
|
1148
|
+
kind: Kind.DOCUMENT,
|
|
1149
|
+
loc: document.loc,
|
|
1150
|
+
definitions: document.definitions.concat({
|
|
1151
|
+
kind: Kind.SCHEMA_EXTENSION,
|
|
1152
|
+
directives: [directiveToAdd]
|
|
1153
|
+
}),
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// We can't add a new schema definition if it already exists. If it doesn't we need to know if there is a mutation type or
|
|
1158
|
+
// not.
|
|
1159
|
+
const existingSchemaDefinition = document.definitions.find((d): d is SchemaDefinitionNode => d.kind == Kind.SCHEMA_DEFINITION);
|
|
1160
|
+
if (existingSchemaDefinition) {
|
|
1161
|
+
return {
|
|
1162
|
+
kind: Kind.DOCUMENT,
|
|
1163
|
+
loc: document.loc,
|
|
1164
|
+
definitions: document.definitions.filter((d) => d !== existingSchemaDefinition).concat([{
|
|
1165
|
+
...existingSchemaDefinition,
|
|
1166
|
+
directives: [directiveToAdd].concat(existingSchemaDefinition.directives ?? []),
|
|
1167
|
+
}]),
|
|
1168
|
+
}
|
|
1169
|
+
} else {
|
|
1170
|
+
const hasMutation = document.definitions.some((d) => d.kind === Kind.OBJECT_TYPE_DEFINITION && d.name.value === 'Mutation');
|
|
1171
|
+
const makeOpType = (opType: OperationTypeNode, name: string): OperationTypeDefinitionNode => ({
|
|
1172
|
+
kind: Kind.OPERATION_TYPE_DEFINITION,
|
|
1173
|
+
operation: opType,
|
|
1174
|
+
type: {
|
|
1175
|
+
kind: Kind.NAMED_TYPE,
|
|
1176
|
+
name: {
|
|
1177
|
+
kind: Kind.NAME,
|
|
1178
|
+
value: name,
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
return {
|
|
1183
|
+
kind: Kind.DOCUMENT,
|
|
1184
|
+
loc: document.loc,
|
|
1185
|
+
definitions: document.definitions.concat({
|
|
1186
|
+
kind: Kind.SCHEMA_DEFINITION,
|
|
1187
|
+
directives: [directiveToAdd],
|
|
1188
|
+
operationTypes: [ makeOpType(OperationTypeNode.QUERY, 'Query') ].concat(hasMutation ? makeOpType(OperationTypeNode.MUTATION, 'Mutation') : []),
|
|
1189
|
+
}),
|
|
1190
|
+
}
|
|
1097
1191
|
}
|
|
1098
1192
|
}
|
|
1099
1193
|
|
|
@@ -1123,13 +1217,21 @@ export function isFederationField(field: FieldDefinition<CompositeType>): boolea
|
|
|
1123
1217
|
}
|
|
1124
1218
|
|
|
1125
1219
|
export function isEntityType(type: NamedType): boolean {
|
|
1126
|
-
if (type
|
|
1220
|
+
if (!isObjectType(type) && !isInterfaceType(type)) {
|
|
1127
1221
|
return false;
|
|
1128
1222
|
}
|
|
1129
1223
|
const metadata = federationMetadata(type.schema());
|
|
1130
1224
|
return !!metadata && type.hasAppliedDirective(metadata.keyDirective());
|
|
1131
1225
|
}
|
|
1132
1226
|
|
|
1227
|
+
export function isInterfaceObjectType(type: NamedType): boolean {
|
|
1228
|
+
if (!isObjectType(type)) {
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
const metadata = federationMetadata(type.schema());
|
|
1232
|
+
return !!metadata && metadata.isInterfaceObjectType(type);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1133
1235
|
export function buildSubgraph(
|
|
1134
1236
|
name: string,
|
|
1135
1237
|
url: string,
|
|
@@ -1191,7 +1293,6 @@ function isFedSpecLinkDirective(directive: Directive<SchemaDefinition>): directi
|
|
|
1191
1293
|
}
|
|
1192
1294
|
|
|
1193
1295
|
function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
|
|
1194
|
-
|
|
1195
1296
|
// We special case @key, @requires and @provides because we've seen existing user schema where those
|
|
1196
1297
|
// have been defined in an invalid way, but in a way that fed1 wasn't rejecting. So for convenience,
|
|
1197
1298
|
// if we detect one of those case, we just remove the definition and let the code afteward add the
|
|
@@ -1482,6 +1583,10 @@ export const serviceTypeSpec = createObjectTypeSpecification({
|
|
|
1482
1583
|
export const entityTypeSpec = createUnionTypeSpecification({
|
|
1483
1584
|
name: '_Entity',
|
|
1484
1585
|
membersFct: (schema) => {
|
|
1586
|
+
// Please note that `_Entity` cannot use "interface entities" since interface types cannot be in unions.
|
|
1587
|
+
// It is ok in practice because _Entity is only use as return type for `_entities`, and even when interfaces
|
|
1588
|
+
// are involve, the result of an `_entities` call will always be an object type anyway, and since we force
|
|
1589
|
+
// all implementations of an interface entity to be entity themselves in a subgraph, we're fine.
|
|
1485
1590
|
return schema.objectTypes().filter(isEntityType).map((t) => t.name);
|
|
1486
1591
|
},
|
|
1487
1592
|
});
|
package/src/federationSpec.ts
CHANGED
|
@@ -35,6 +35,7 @@ export enum FederationDirectiveName {
|
|
|
35
35
|
TAG = 'tag',
|
|
36
36
|
INACCESSIBLE = 'inaccessible',
|
|
37
37
|
COMPOSE_DIRECTIVE = 'composeDirective',
|
|
38
|
+
INTERFACE_OBJECT = 'interfaceObject',
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const fieldSetTypeSpec = createScalarTypeSpecification({ name: FederationTypeName.FIELD_SET });
|
|
@@ -153,6 +154,13 @@ export class FederationSpecDefinition extends FeatureDefinition {
|
|
|
153
154
|
}),
|
|
154
155
|
}));
|
|
155
156
|
}
|
|
157
|
+
|
|
158
|
+
if (version >= (new FeatureVersion(2, 3))) {
|
|
159
|
+
this.registerDirective(createDirectiveSpecification({
|
|
160
|
+
name: FederationDirectiveName.INTERFACE_OBJECT,
|
|
161
|
+
locations: [DirectiveLocation.OBJECT],
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
private registerDirective(spec: DirectiveSpecification) {
|
|
@@ -195,6 +203,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
|
|
|
195
203
|
export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
|
|
196
204
|
.add(new FederationSpecDefinition(new FeatureVersion(2, 0)))
|
|
197
205
|
.add(new FederationSpecDefinition(new FeatureVersion(2, 1)))
|
|
198
|
-
.add(new FederationSpecDefinition(new FeatureVersion(2, 2)))
|
|
206
|
+
.add(new FederationSpecDefinition(new FeatureVersion(2, 2)))
|
|
207
|
+
.add(new FederationSpecDefinition(new FeatureVersion(2, 3)));
|
|
199
208
|
|
|
200
209
|
registerKnownFeature(FEDERATION_VERSIONS);
|
package/src/joinSpec.ts
CHANGED
|
@@ -64,11 +64,21 @@ export class JoinSpecDefinition extends FeatureDefinition {
|
|
|
64
64
|
if (!this.isV01()) {
|
|
65
65
|
joinType.addArgument('extension', new NonNullType(schema.booleanType()), false);
|
|
66
66
|
joinType.addArgument('resolvable', new NonNullType(schema.booleanType()), true);
|
|
67
|
+
|
|
68
|
+
if (this.version >= (new FeatureVersion(0, 3))) {
|
|
69
|
+
joinType.addArgument('isInterfaceObject', new NonNullType(schema.booleanType()), false);
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
const joinField = this.addDirective(schema, 'field').addLocations(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION);
|
|
70
74
|
joinField.repeatable = true;
|
|
71
|
-
|
|
75
|
+
// The `graph` argument used to be non-nullable, but @interfaceObject makes us add some field in
|
|
76
|
+
// the supergraph that don't "directly" come from any subgraph (they indirectly are inherited from
|
|
77
|
+
// an `@interfaceObject` type), and to indicate that, we use a `@join__field(graph: null)` annotation.
|
|
78
|
+
const graphArgType = this.version >= (new FeatureVersion(0, 3))
|
|
79
|
+
? graphEnum
|
|
80
|
+
: new NonNullType(graphEnum);
|
|
81
|
+
joinField.addArgument('graph', graphArgType);
|
|
72
82
|
joinField.addArgument('requires', joinFieldSet);
|
|
73
83
|
joinField.addArgument('provides', joinFieldSet);
|
|
74
84
|
if (!this.isV01()) {
|
|
@@ -87,6 +97,17 @@ export class JoinSpecDefinition extends FeatureDefinition {
|
|
|
87
97
|
joinImplements.addArgument('interface', new NonNullType(schema.stringType()));
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
if (this.version >= (new FeatureVersion(0, 3))) {
|
|
101
|
+
const joinUnionMember = this.addDirective(schema, 'unionMember').addLocations(DirectiveLocation.UNION);
|
|
102
|
+
joinUnionMember.repeatable = true;
|
|
103
|
+
joinUnionMember.addArgument('graph', new NonNullType(graphEnum));
|
|
104
|
+
joinUnionMember.addArgument('member', new NonNullType(schema.stringType()));
|
|
105
|
+
|
|
106
|
+
const joinEnumValue = this.addDirective(schema, 'enumValue').addLocations(DirectiveLocation.ENUM_VALUE);
|
|
107
|
+
joinEnumValue.repeatable = true;
|
|
108
|
+
joinEnumValue.addArgument('graph', new NonNullType(graphEnum));
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
if (this.isV01()) {
|
|
91
112
|
const joinOwner = this.addDirective(schema, 'owner').addLocations(DirectiveLocation.OBJECT);
|
|
92
113
|
joinOwner.addArgument('graph', new NonNullType(graphEnum));
|
|
@@ -153,7 +174,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
|
|
|
153
174
|
return this.directive(schema, 'graph')!;
|
|
154
175
|
}
|
|
155
176
|
|
|
156
|
-
typeDirective(schema: Schema): DirectiveDefinition<{graph: string, key?: string, extension?: boolean, resolvable?: boolean}> {
|
|
177
|
+
typeDirective(schema: Schema): DirectiveDefinition<{graph: string, key?: string, extension?: boolean, resolvable?: boolean, isInterfaceObject?: boolean}> {
|
|
157
178
|
return this.directive(schema, 'type')!;
|
|
158
179
|
}
|
|
159
180
|
|
|
@@ -162,7 +183,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
|
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
fieldDirective(schema: Schema): DirectiveDefinition<{
|
|
165
|
-
graph
|
|
186
|
+
graph?: string,
|
|
166
187
|
requires?: string,
|
|
167
188
|
provides?: string,
|
|
168
189
|
override?: string,
|
|
@@ -173,6 +194,14 @@ export class JoinSpecDefinition extends FeatureDefinition {
|
|
|
173
194
|
return this.directive(schema, 'field')!;
|
|
174
195
|
}
|
|
175
196
|
|
|
197
|
+
unionMemberDirective(schema: Schema): DirectiveDefinition<{graph: string, member: string}> | undefined {
|
|
198
|
+
return this.directive(schema, 'unionMember');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
enumValueDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined {
|
|
202
|
+
return this.directive(schema, 'enumValue');
|
|
203
|
+
}
|
|
204
|
+
|
|
176
205
|
ownerDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined {
|
|
177
206
|
return this.directive(schema, 'owner');
|
|
178
207
|
}
|
|
@@ -182,15 +211,15 @@ export class JoinSpecDefinition extends FeatureDefinition {
|
|
|
182
211
|
}
|
|
183
212
|
}
|
|
184
213
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// due to the query planner having an invalid understanding of the subgraph services API.
|
|
214
|
+
// The versions are as follows:
|
|
215
|
+
// - 0.1: this is the version used by federation 1 composition. Federation 2 is still able to read supergraphs
|
|
216
|
+
// using that verison for backward compatibility, but never writes this spec version is not expressive enough
|
|
217
|
+
// for federation 2 in general.
|
|
218
|
+
// - 0.2: this is the original version released with federation 2.
|
|
219
|
+
// - 0.3: adds the `isInterfaceObject` argument to `@join__type`, and make the `graph` in `@join__field` skippable.
|
|
192
220
|
export const JOIN_VERSIONS = new FeatureDefinitions<JoinSpecDefinition>(joinIdentity)
|
|
193
221
|
.add(new JoinSpecDefinition(new FeatureVersion(0, 1)))
|
|
194
|
-
.add(new JoinSpecDefinition(new FeatureVersion(0, 2)))
|
|
222
|
+
.add(new JoinSpecDefinition(new FeatureVersion(0, 2)))
|
|
223
|
+
.add(new JoinSpecDefinition(new FeatureVersion(0, 3)));
|
|
195
224
|
|
|
196
225
|
registerKnownFeature(JOIN_VERSIONS);
|