@apollo/federation-internals 2.2.3 → 2.3.0-beta.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/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 +5 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +21 -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 +11 -2
- 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 +11 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +46 -1
- 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 +2 -0
- package/dist/supergraphs.js.map +1 -1
- package/dist/tagSpec.d.ts +1 -0
- package/dist/tagSpec.d.ts.map +1 -1
- package/dist/tagSpec.js +9 -1
- package/dist/tagSpec.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -6
- package/dist/types.js.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +2 -3
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +0 -1
- package/src/__tests__/matchers/toMatchString.ts +5 -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 +47 -16
- package/src/extractSubgraphsFromSupergraph.ts +40 -9
- package/src/federation.ts +178 -73
- package/src/federationSpec.ts +16 -2
- package/src/joinSpec.ts +40 -11
- package/src/operations.ts +59 -1
- package/src/schemaUpgrader.ts +13 -0
- package/src/supergraphs.ts +2 -0
- package/src/tagSpec.ts +10 -1
- package/src/types.ts +12 -15
- package/src/utils.ts +1 -1
- 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 });
|
|
@@ -93,7 +94,9 @@ const legacyFederationDirectives = [
|
|
|
93
94
|
requiresDirectiveSpec,
|
|
94
95
|
providesDirectiveSpec,
|
|
95
96
|
externalDirectiveSpec,
|
|
96
|
-
|
|
97
|
+
// This should really be v0.1 instead of v0.2, but we can't change this to
|
|
98
|
+
// v0.1 without checking whether anyone relied on the v0.2 behavior.
|
|
99
|
+
TAG_VERSIONS.find(new FeatureVersion(0, 2))!.tagDirectiveSpec,
|
|
97
100
|
extendsDirectiveSpec,
|
|
98
101
|
];
|
|
99
102
|
|
|
@@ -153,6 +156,16 @@ export class FederationSpecDefinition extends FeatureDefinition {
|
|
|
153
156
|
}),
|
|
154
157
|
}));
|
|
155
158
|
}
|
|
159
|
+
|
|
160
|
+
if (version >= (new FeatureVersion(2, 3))) {
|
|
161
|
+
this.registerDirective(createDirectiveSpecification({
|
|
162
|
+
name: FederationDirectiveName.INTERFACE_OBJECT,
|
|
163
|
+
locations: [DirectiveLocation.OBJECT],
|
|
164
|
+
}));
|
|
165
|
+
this.registerDirective(
|
|
166
|
+
TAG_VERSIONS.find(new FeatureVersion(0, 3))!.tagDirectiveSpec
|
|
167
|
+
);
|
|
168
|
+
}
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
private registerDirective(spec: DirectiveSpecification) {
|
|
@@ -195,6 +208,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
|
|
|
195
208
|
export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
|
|
196
209
|
.add(new FederationSpecDefinition(new FeatureVersion(2, 0)))
|
|
197
210
|
.add(new FederationSpecDefinition(new FeatureVersion(2, 1)))
|
|
198
|
-
.add(new FederationSpecDefinition(new FeatureVersion(2, 2)))
|
|
211
|
+
.add(new FederationSpecDefinition(new FeatureVersion(2, 2)))
|
|
212
|
+
.add(new FederationSpecDefinition(new FeatureVersion(2, 3)));
|
|
199
213
|
|
|
200
214
|
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);
|
package/src/operations.ts
CHANGED
|
@@ -134,6 +134,15 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
134
134
|
return newField;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
withUpdatedAlias(newAlias: string | undefined): Field<TArgs> {
|
|
138
|
+
const newField = new Field<TArgs>(this.definition, this.args, this.variableDefinitions, newAlias);
|
|
139
|
+
for (const directive of this.appliedDirectives) {
|
|
140
|
+
newField.applyDirective(directive.definition!, directive.arguments());
|
|
141
|
+
}
|
|
142
|
+
this.copyAttachementsTo(newField);
|
|
143
|
+
return newField;
|
|
144
|
+
}
|
|
145
|
+
|
|
137
146
|
appliesTo(type: ObjectType | InterfaceType): boolean {
|
|
138
147
|
const definition = type.field(this.name);
|
|
139
148
|
return !!definition && this.selects(definition);
|
|
@@ -141,7 +150,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
141
150
|
|
|
142
151
|
selects(definition: FieldDefinition<any>, assumeValid: boolean = false): boolean {
|
|
143
152
|
// We've already validated that the field selects the definition on which it was built.
|
|
144
|
-
if (definition
|
|
153
|
+
if (definition === this.definition) {
|
|
145
154
|
return true;
|
|
146
155
|
}
|
|
147
156
|
|
|
@@ -317,6 +326,10 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
317
326
|
return this.withUpdatedTypes(newSourceType, this.typeCondition);
|
|
318
327
|
}
|
|
319
328
|
|
|
329
|
+
withUpdatedCondition(newCondition: CompositeType | undefined): FragmentElement {
|
|
330
|
+
return this.withUpdatedTypes(this.sourceType, newCondition);
|
|
331
|
+
}
|
|
332
|
+
|
|
320
333
|
withUpdatedTypes(newSourceType: CompositeType, newCondition: CompositeType | undefined): FragmentElement {
|
|
321
334
|
// Note that we pass the type-condition name instead of the type itself, to ensure that if `newSourceType` was from a different
|
|
322
335
|
// schema (typically, the supergraph) than `this.sourceType` (typically, a subgraph), then the new condition uses the
|
|
@@ -1019,6 +1032,22 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1019
1032
|
return this._cachedSelections;
|
|
1020
1033
|
}
|
|
1021
1034
|
|
|
1035
|
+
fieldsInSet(): { path: string[], field: FieldSelection, directParent: SelectionSet }[] {
|
|
1036
|
+
const fields = new Array<{ path: string[], field: FieldSelection, directParent: SelectionSet }>();
|
|
1037
|
+
for (const selection of this.selections()) {
|
|
1038
|
+
if (selection.kind === 'FieldSelection') {
|
|
1039
|
+
fields.push({ path: [], field: selection, directParent: this });
|
|
1040
|
+
} else {
|
|
1041
|
+
const condition = selection.element().typeCondition;
|
|
1042
|
+
const header = condition ? [`... on ${condition}`] : [];
|
|
1043
|
+
for (const { path, field, directParent } of selection.selectionSet.fieldsInSet()) {
|
|
1044
|
+
fields.push({ path: header.concat(path), field, directParent });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return fields;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1022
1051
|
usedVariables(): Variables {
|
|
1023
1052
|
let variables: Variables = [];
|
|
1024
1053
|
for (const byResponseName of this._selections.values()) {
|
|
@@ -1200,6 +1229,23 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1200
1229
|
return toAdd;
|
|
1201
1230
|
}
|
|
1202
1231
|
|
|
1232
|
+
/**
|
|
1233
|
+
* If this selection contains a selection of a field with provided response name at top level, removes it.
|
|
1234
|
+
*
|
|
1235
|
+
* @return whether a selection was removed.
|
|
1236
|
+
*/
|
|
1237
|
+
removeTopLevelField(responseName: string): boolean {
|
|
1238
|
+
// It's a bug to try to remove from a frozen selection set
|
|
1239
|
+
assert(!this.isFrozen(), () => `Cannot remove from frozen selection: ${this}`);
|
|
1240
|
+
|
|
1241
|
+
const wasRemoved = this._selections.delete(responseName);
|
|
1242
|
+
if (wasRemoved) {
|
|
1243
|
+
--this._selectionCount;
|
|
1244
|
+
this._cachedSelections = undefined;
|
|
1245
|
+
}
|
|
1246
|
+
return wasRemoved;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1203
1249
|
addPath(path: OperationPath, onPathEnd?: (finalSelectionSet: SelectionSet | undefined) => void) {
|
|
1204
1250
|
let previousSelections: SelectionSet = this;
|
|
1205
1251
|
let currentSelections: SelectionSet | undefined = this;
|
|
@@ -1524,6 +1570,10 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1524
1570
|
this.selectionSet = isLeafType(type) ? undefined : (initialSelectionSet ? initialSelectionSet.cloneIfFrozen() : new SelectionSet(type as CompositeType));
|
|
1525
1571
|
}
|
|
1526
1572
|
|
|
1573
|
+
get parentType(): CompositeType {
|
|
1574
|
+
return this.field.parentType;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1527
1577
|
protected us(): FieldSelection {
|
|
1528
1578
|
return this;
|
|
1529
1579
|
}
|
|
@@ -1732,6 +1782,10 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1732
1782
|
return new FieldSelection(this.field, newSubSelection);
|
|
1733
1783
|
}
|
|
1734
1784
|
|
|
1785
|
+
withUpdatedField(newField: Field<any>): FieldSelection {
|
|
1786
|
+
return new FieldSelection(newField, this.selectionSet);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1735
1789
|
equals(that: Selection): boolean {
|
|
1736
1790
|
if (this === that) {
|
|
1737
1791
|
return true;
|
|
@@ -1821,6 +1875,10 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
|
|
|
1821
1875
|
|
|
1822
1876
|
abstract withUpdatedSubSelection(newSubSelection: SelectionSet | undefined): FragmentSelection;
|
|
1823
1877
|
|
|
1878
|
+
get parentType(): CompositeType {
|
|
1879
|
+
return this.element().parentType;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1824
1882
|
protected us(): FragmentSelection {
|
|
1825
1883
|
return this;
|
|
1826
1884
|
}
|