Skip to content

Commit 8fe04a8

Browse files
authored
fix: allow interface subtype for implemented interface fields (#2764)
1 parent e14c41b commit 8fe04a8

File tree

12 files changed

+608
-197
lines changed

12 files changed

+608
-197
lines changed

composition-go/index.global.js

Lines changed: 143 additions & 143 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composition/src/ast/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,14 @@ export type ScalarTypeNode = ScalarTypeDefinitionNode | ScalarTypeExtensionNode;
290290
export type SchemaNode = SchemaDefinitionNode | SchemaExtensionNode;
291291
export type UnionTypeNode = UnionTypeDefinitionNode | UnionTypeExtensionNode;
292292

293+
export type ParentTypeNode =
294+
| EnumTypeNode
295+
| InputObjectTypeNode
296+
| InterfaceTypeNode
297+
| ObjectTypeNode
298+
| ScalarTypeNode
299+
| UnionTypeNode;
300+
293301
export type InterfaceNodeKind = Kind.INTERFACE_TYPE_DEFINITION | Kind.INTERFACE_TYPE_EXTENSION;
294302
export type ObjectNodeKind = Kind.OBJECT_TYPE_DEFINITION | Kind.OBJECT_TYPE_EXTENSION;
295303
export type CompositeOutputNodeKind = InterfaceNodeKind | ObjectNodeKind;

composition/src/normalization/types.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import {
1818
} from '../schema-building/types';
1919
import { type Graph } from '../resolvability-graph/graph';
2020
import { type InternalSubgraph } from '../subgraph/types';
21-
import { type DirectiveName, type TypeName } from '../types/types';
21+
import {
22+
type AbstractTypeName,
23+
type DirectiveName,
24+
type InterfaceTypeName,
25+
type SubgraphName,
26+
type TypeName,
27+
} from '../types/types';
2228

2329
export type NormalizationFailure = {
2430
errors: Array<Error>;
@@ -36,9 +42,10 @@ export type NormalizationSuccess = {
3642
entityInterfaces: Map<string, EntityInterfaceSubgraphData>;
3743
entityDataByTypeName: Map<string, EntityData>;
3844
fieldCoordsByNamedTypeName: Map<string, Set<string>>;
39-
originalTypeNameByRenamedTypeName: Map<string, string>;
45+
interfaceImplementationTypeNamesByInterfaceTypeName: Map<InterfaceTypeName, Set<InterfaceTypeName>>;
4046
isEventDrivenGraph: boolean;
4147
isVersionTwo: boolean;
48+
originalTypeNameByRenamedTypeName: Map<string, string>;
4249
keyFieldNamesByParentTypeName: Map<string, Set<string>>;
4350
keyFieldSetsByEntityTypeNameByKeyFieldCoords: Map<string, Map<string, Set<string>>>;
4451
operationTypes: Map<string, OperationTypeNode>;
@@ -63,11 +70,12 @@ export type BatchNormalizationFailure = {
6370

6471
export type BatchNormalizationSuccess = {
6572
success: true;
66-
authorizationDataByParentTypeName: Map<string, AuthorizationData>;
67-
concreteTypeNamesByAbstractTypeName: Map<string, Set<string>>;
68-
entityDataByTypeName: Map<string, EntityData>;
69-
fieldCoordsByNamedTypeName: Map<string, Set<string>>;
70-
internalSubgraphBySubgraphName: Map<string, InternalSubgraph>;
73+
authorizationDataByParentTypeName: Map<TypeName, AuthorizationData>;
74+
concreteTypeNamesByAbstractTypeName: Map<AbstractTypeName, Set<TypeName>>;
75+
entityDataByTypeName: Map<TypeName, EntityData>;
76+
fieldCoordsByNamedTypeName: Map<TypeName, Set<string>>;
77+
interfaceImplementationTypeNamesByInterfaceTypeName: Map<InterfaceTypeName, Set<InterfaceTypeName>>;
78+
internalSubgraphBySubgraphName: Map<SubgraphName, InternalSubgraph>;
7179
internalGraph: Graph;
7280
warnings: Array<Warning>;
7381
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { type TypeNode } from 'graphql';
2+
import { type InterfaceTypeName, type TypeName } from '../types/types';
3+
4+
export type IsTypeValidImplementationParams = {
5+
concreteTypeNamesByAbstractTypeName: Map<TypeName, Set<TypeName>>;
6+
implementationType: TypeNode;
7+
interfaceImplementationTypeNamesByInterfaceTypeName: Map<InterfaceTypeName, Set<InterfaceTypeName>>;
8+
originalType: TypeNode;
9+
};

composition/src/schema-building/utils.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ import {
3838
type SchemaData,
3939
} from './types';
4040
import { type MutableDefinitionNode, type MutableFieldNode, type MutableInputValueNode } from './ast';
41-
import { type ObjectTypeNode, setToNameNodeArray, stringToNameNode } from '../ast/utils';
41+
import {
42+
type InterfaceTypeNode,
43+
type ObjectTypeNode,
44+
type ParentTypeNode,
45+
setToNameNodeArray,
46+
stringToNameNode,
47+
} from '../ast/utils';
4248
import {
4349
incompatibleInputValueDefaultValuesError,
4450
invalidRepeatedFederatedDirectiveErrorMessage,
@@ -58,6 +64,7 @@ import {
5864
INPUT_FIELD,
5965
INPUT_NODE_KINDS,
6066
INT_SCALAR,
67+
INTERFACE_NODE_KINDS,
6168
MUTATION,
6269
OUTPUT_NODE_KINDS,
6370
PERSISTED_CLIENT_DIRECTIVES,
@@ -80,6 +87,7 @@ import {
8087
import { type InputNodeKind, type InvalidRequiredInputValueData, type OutputNodeKind } from '../utils/types';
8188
import { getDescriptionFromString } from '../v1/federation/utils';
8289
import { type DirectiveName, type FieldName, type SubgraphName, type TypeName } from '../types/types';
90+
import { type IsTypeValidImplementationParams } from './params';
8391

8492
export function newPersistedDirectivesData(): PersistedDirectivesData {
8593
return {
@@ -620,19 +628,30 @@ export enum MergeMethod {
620628
CONSISTENT,
621629
}
622630

623-
export function isTypeValidImplementation(
624-
originalType: TypeNode,
625-
implementationType: TypeNode,
626-
concreteTypeNamesByAbstractTypeName: Map<TypeName, Set<TypeName>>,
627-
): boolean {
631+
export function isTypeValidImplementation({
632+
concreteTypeNamesByAbstractTypeName,
633+
implementationType,
634+
interfaceImplementationTypeNamesByInterfaceTypeName,
635+
originalType,
636+
}: IsTypeValidImplementationParams): boolean {
628637
if (originalType.kind === Kind.NON_NULL_TYPE) {
629638
if (implementationType.kind !== Kind.NON_NULL_TYPE) {
630639
return false;
631640
}
632-
return isTypeValidImplementation(originalType.type, implementationType.type, concreteTypeNamesByAbstractTypeName);
641+
return isTypeValidImplementation({
642+
concreteTypeNamesByAbstractTypeName,
643+
implementationType: implementationType.type,
644+
interfaceImplementationTypeNamesByInterfaceTypeName,
645+
originalType: originalType.type,
646+
});
633647
}
634648
if (implementationType.kind === Kind.NON_NULL_TYPE) {
635-
return isTypeValidImplementation(originalType, implementationType.type, concreteTypeNamesByAbstractTypeName);
649+
return isTypeValidImplementation({
650+
concreteTypeNamesByAbstractTypeName,
651+
implementationType: implementationType.type,
652+
interfaceImplementationTypeNamesByInterfaceTypeName,
653+
originalType,
654+
});
636655
}
637656
switch (originalType.kind) {
638657
case Kind.NAMED_TYPE:
@@ -642,20 +661,19 @@ export function isTypeValidImplementation(
642661
if (originalTypeName === implementationTypeName) {
643662
return true;
644663
}
664+
const abstractTypes = interfaceImplementationTypeNamesByInterfaceTypeName.get(originalTypeName);
645665
const concreteTypes = concreteTypeNamesByAbstractTypeName.get(originalTypeName);
646-
if (!concreteTypes) {
647-
return false;
648-
}
649-
return concreteTypes.has(implementationTypeName);
666+
return !!(concreteTypes?.has(implementationTypeName) || abstractTypes?.has(implementationTypeName));
650667
}
651668
return false;
652669
default:
653670
if (implementationType.kind === Kind.LIST_TYPE) {
654-
return isTypeValidImplementation(
655-
originalType.type,
656-
implementationType.type,
671+
return isTypeValidImplementation({
657672
concreteTypeNamesByAbstractTypeName,
658-
);
673+
implementationType: implementationType.type,
674+
interfaceImplementationTypeNamesByInterfaceTypeName,
675+
originalType: originalType.type,
676+
});
659677
}
660678
return false;
661679
}
@@ -775,3 +793,7 @@ export function isInputNodeKind(kind: Kind): kind is InputNodeKind {
775793
export function isOutputNodeKind(kind: Kind): kind is OutputNodeKind {
776794
return OUTPUT_NODE_KINDS.has(kind);
777795
}
796+
797+
export function isInterfaceNode(node: ParentTypeNode): node is InterfaceTypeNode {
798+
return INTERFACE_NODE_KINDS.has(node.kind);
799+
}

composition/src/types/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export type AbstractTypeName = TypeName;
2+
13
export type ArgumentName = string;
24

35
export type ContractName = string;
@@ -11,6 +13,8 @@ export type FieldName = string;
1113

1214
export type FieldCoords = string;
1315

16+
export type InterfaceTypeName = string;
17+
1418
export type SubgraphName = string;
1519

1620
export type TypeName = string;

composition/src/utils/string-constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,6 @@ export const OUTPUT_NODE_KINDS = new Set<Kind>([
197197
Kind.UNION_TYPE_DEFINITION,
198198
]);
199199

200+
export const INTERFACE_NODE_KINDS = new Set<Kind>([Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION]);
201+
200202
export const NON_REPEATABLE_PERSISTED_DIRECTIVES = new Set<DirectiveName>([INACCESSIBLE, ONE_OF, SEMANTIC_NON_NULL]);

composition/src/utils/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type Kind } from 'graphql';
2-
import { type SubgraphName } from '../types/types';
2+
import { type ArgumentName, type SubgraphName } from '../types/types';
33

44
export type RootTypeName = 'Mutation' | 'Query' | 'Subscription';
55

@@ -15,7 +15,7 @@ export type InvalidFieldImplementation = {
1515
invalidImplementedArguments: InvalidArgumentImplementation[];
1616
isInaccessible: boolean;
1717
originalResponseType: string;
18-
unimplementedArguments: Set<string>;
18+
unimplementedArguments: Set<ArgumentName>;
1919
};
2020

2121
export type ImplementationErrors = {

composition/src/v1/federation/federation-factory.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,12 @@ import type {
237237
InvalidRequiredInputValueData,
238238
} from '../../utils/types';
239239
import {
240+
type ArgumentName,
240241
type ContractName,
241242
type DirectiveName,
242243
type FieldCoords,
243244
type FieldName,
245+
type InterfaceTypeName,
244246
type SubgraphName,
245247
type TypeName,
246248
} from '../../types/types';
@@ -279,6 +281,7 @@ export class FederationFactory {
279281
fieldCoordsByNamedTypeName: Map<TypeName, Set<FieldCoords>>;
280282
inaccessibleCoords = new Set<string>();
281283
inaccessibleRequiredInputValueErrorByCoords = new Map<string, Error>();
284+
interfaceImplementationTypeNamesByInterfaceTypeName: Map<InterfaceTypeName, Set<InterfaceTypeName>>;
282285
internalGraph: Graph;
283286
internalSubgraphBySubgraphName: Map<SubgraphName, InternalSubgraph>;
284287
invalidORScopesCoords = new Set<string>();
@@ -311,6 +314,7 @@ export class FederationFactory {
311314
entityDataByTypeName,
312315
entityInterfaceFederationDataByTypeName,
313316
fieldCoordsByNamedTypeName,
317+
interfaceImplementationTypeNamesByInterfaceTypeName,
314318
internalGraph,
315319
internalSubgraphBySubgraphName,
316320
options,
@@ -322,6 +326,7 @@ export class FederationFactory {
322326
this.entityDataByTypeName = entityDataByTypeName;
323327
this.entityInterfaceFederationDataByTypeName = entityInterfaceFederationDataByTypeName;
324328
this.fieldCoordsByNamedTypeName = fieldCoordsByNamedTypeName;
329+
this.interfaceImplementationTypeNamesByInterfaceTypeName = interfaceImplementationTypeNamesByInterfaceTypeName;
325330
this.internalGraph = internalGraph;
326331
this.internalSubgraphBySubgraphName = internalSubgraphBySubgraphName;
327332
this.warnings = warnings;
@@ -402,15 +407,17 @@ export class FederationFactory {
402407
invalidImplementedArguments: [],
403408
isInaccessible: false,
404409
originalResponseType: printTypeNode(interfaceField.node.type),
405-
unimplementedArguments: new Set<string>(),
410+
unimplementedArguments: new Set<ArgumentName>(),
406411
};
407412
// The implemented field type must be equally or more restrictive than the original interface field type
408413
if (
409-
!isTypeValidImplementation(
410-
interfaceField.node.type,
411-
fieldData.node.type,
412-
this.concreteTypeNamesByAbstractTypeName,
413-
)
414+
!isTypeValidImplementation({
415+
concreteTypeNamesByAbstractTypeName: this.concreteTypeNamesByAbstractTypeName,
416+
implementationType: fieldData.node.type,
417+
interfaceImplementationTypeNamesByInterfaceTypeName:
418+
this.interfaceImplementationTypeNamesByInterfaceTypeName,
419+
originalType: interfaceField.node.type,
420+
})
414421
) {
415422
hasErrors = true;
416423
hasNestedErrors = true;
@@ -3373,6 +3380,7 @@ function initializeFederationFactory({ options, subgraphs }: FederationParams):
33733380
entityDataByTypeName: result.entityDataByTypeName,
33743381
entityInterfaceFederationDataByTypeName,
33753382
fieldCoordsByNamedTypeName: result.fieldCoordsByNamedTypeName,
3383+
interfaceImplementationTypeNamesByInterfaceTypeName: result.interfaceImplementationTypeNamesByInterfaceTypeName,
33763384
internalSubgraphBySubgraphName: result.internalSubgraphBySubgraphName,
33773385
internalGraph: result.internalGraph,
33783386
options,

composition/src/v1/federation/params.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { ContractName, DirectiveName, FieldName, SubgraphName, TypeName } from '../../types/types';
1+
import type {
2+
ContractName,
3+
DirectiveName,
4+
FieldName,
5+
InterfaceTypeName,
6+
SubgraphName,
7+
TypeName,
8+
} from '../../types/types';
29
import type {
310
AuthorizationData,
411
EntityData,
@@ -47,6 +54,7 @@ export type FederationFactoryParams = {
4754
entityDataByTypeName: Map<TypeName, EntityData>;
4855
entityInterfaceFederationDataByTypeName: Map<TypeName, EntityInterfaceFederationData>;
4956
fieldCoordsByNamedTypeName: Map<TypeName, Set<string>>;
57+
interfaceImplementationTypeNamesByInterfaceTypeName: Map<InterfaceTypeName, Set<InterfaceTypeName>>;
5058
internalGraph: Graph;
5159
internalSubgraphBySubgraphName: Map<SubgraphName, InternalSubgraph>;
5260
warnings: Array<Warning>;

0 commit comments

Comments
 (0)