Skip to content

Commit

Permalink
feat(federation): interface objects (#6143)
Browse files Browse the repository at this point in the history
* feat(federation): interface objects

* Fix

* Fix build error

* More

* Good idea
  • Loading branch information
ardatan committed May 6, 2024
1 parent d62a593 commit 04d5431
Show file tree
Hide file tree
Showing 10 changed files with 651 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-pianos-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-tools/federation": patch
---

Implement interface objects support
98 changes: 93 additions & 5 deletions packages/federation/src/supergraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ export function getSubschemasFromSupergraphSdl({
);
if (joinTypeGraphArgNode?.value?.kind === Kind.ENUM) {
const graphName = joinTypeGraphArgNode.value.value;
if (typeNode.kind === Kind.OBJECT_TYPE_DEFINITION) {
if (
typeNode.kind === Kind.OBJECT_TYPE_DEFINITION ||
typeNode.kind === Kind.INTERFACE_TYPE_DEFINITION
) {
const keyArgumentNode = directiveNode.arguments?.find(
argumentNode => argumentNode.name.value === 'key',
);
Expand All @@ -194,8 +197,7 @@ export function getSubschemasFromSupergraphSdl({
const fieldDefinitionNodesOfSubgraph: FieldDefinitionNode[] = [];
typeNode.fields?.forEach(fieldNode => {
const joinFieldDirectives = fieldNode.directives?.filter(
directiveNode =>
directiveNode.name.value === 'join__field' && directiveNode.arguments?.length,
directiveNode => directiveNode.name.value === 'join__field',
);
let notInSubgraph = true;
joinFieldDirectives?.forEach(joinFieldDirectiveNode => {
Expand Down Expand Up @@ -730,16 +732,20 @@ export function getSubschemasFromSupergraphSdl({
visitTypeDefinitionsForOrphanTypes(typeNode);
});
const extendedSubgraphTypes = [...subgraphTypes, ...extraOrphanTypesForSubgraph.values()];
const fakeTypesIfaceMap = new Map<string, string>();
const iFaceTypeMap = new Map<string, string>();
// We should add implemented objects from other subgraphs implemented by this interface
for (const interfaceInSubgraph of extendedSubgraphTypes) {
if (interfaceInSubgraph.kind === Kind.INTERFACE_TYPE_DEFINITION) {
let isOrphan = true;
for (const definitionNode of supergraphAst.definitions) {
if (
definitionNode.kind === Kind.OBJECT_TYPE_DEFINITION &&
definitionNode.interfaces?.some(
interfaceNode => interfaceNode.name.value === interfaceInSubgraph.name.value,
)
) {
isOrphan = false;
const existingType = subgraphTypes.find(
typeNode => typeNode.name.value === definitionNode.name.value,
);
Expand All @@ -751,25 +757,44 @@ export function getSubschemasFromSupergraphSdl({
},
};
if (!existingType) {
fakeTypesIfaceMap.set(definitionNode.name.value, interfaceInSubgraph.name.value);
iFaceTypeMap.set(interfaceInSubgraph.name.value, definitionNode.name.value);
extendedSubgraphTypes.push({
kind: Kind.OBJECT_TYPE_DEFINITION,
name: definitionNode.name,
fields: interfaceInSubgraph.fields,
interfaces: [iFaceNode],
});
unionTypeNodes.push({
kind: Kind.NAMED_TYPE,
name: definitionNode.name,
});
if (
!mergeConfig[definitionNode.name.value] &&
mergeConfig[interfaceInSubgraph.name.value]
) {
mergeConfig[definitionNode.name.value] =
mergeConfig[interfaceInSubgraph.name.value];
}
}
if (
existingType?.kind === Kind.OBJECT_TYPE_DEFINITION &&
!existingType.interfaces?.some(
interfaceNode => interfaceNode.name.value === interfaceInSubgraph.name.value,
)
) {
(existingType as any).interfaces ||= [];
(existingType as any).interfaces.push(iFaceNode);
// @ts-expect-error `interfaces` property is a readonly field in TS definitions but it is not actually
existingType.interfaces ||= [];
// @ts-expect-error `interfaces` property is a readonly field in TS definitions but it is not actually
existingType.interfaces.push(iFaceNode);
}
break;
}
}
if (isOrphan) {
// @ts-expect-error `kind` property is a readonly field in TS definitions but it is not actually
interfaceInSubgraph.kind = Kind.OBJECT_TYPE_DEFINITION;
}
}
}
let schema: GraphQLSchema;
Expand Down Expand Up @@ -804,12 +829,75 @@ export function getSubschemasFromSupergraphSdl({
return res;
};
}
function visitToReplaceTypeNames(data: unknown, nameMap: Map<string, string>): any {
if (data != null && typeof data === 'object') {
if (Array.isArray(data)) {
return data.map(item => visitToReplaceTypeNames(item, nameMap));
}
const newData = {};
for (const key in data) {
if (key === '__typename') {
const newType = nameMap.get(data[key]) || data[key];
newData[key] = newType;
} else {
newData[key] = visitToReplaceTypeNames(data[key], nameMap);
}
}
return newData;
}
return data;
}
subschemaMap.set(subgraphName, {
name: subgraphName,
schema,
executor,
merge: mergeConfig,
batch,
transforms: [
{
transformRequest(request, delegationContext, transformationContext) {
delegationContext.args = visitToReplaceTypeNames(
delegationContext.args,
fakeTypesIfaceMap,
);
return {
...request,
document: visit(request.document, {
[Kind.INLINE_FRAGMENT](node) {
if (node.typeCondition) {
const newTypeConditionName = fakeTypesIfaceMap.get(
node.typeCondition?.name.value,
);
if (newTypeConditionName) {
transformationContext.replacedTypes ||= new Map();
transformationContext.replacedTypes.set(
newTypeConditionName,
node.typeCondition.name.value,
);
return {
...node,
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: newTypeConditionName,
},
},
};
}
}
},
}),
};
},
transformResult(result, _, transformationContext) {
if (!transformationContext.replacedTypes) {
return visitToReplaceTypeNames(result, iFaceTypeMap);
}
return visitToReplaceTypeNames(result, transformationContext.replacedTypes);
},
},
],
});
}
return subschemaMap;
Expand Down
15 changes: 12 additions & 3 deletions packages/federation/test/federation-compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from 'path';
import {
buildSchema,
getNamedType,
GraphQLSchema,
isEnumType,
lexicographicSortSchema,
parse,
Expand All @@ -23,9 +24,12 @@ describe('Federation Compatibility', () => {
}
describe(supergraphName, () => {
const supergraphSdl = readFileSync(supergraphSdlPath, 'utf-8');
const stitchedSchema = getStitchedSchemaFromSupergraphSdl({
supergraphSdl: readFileSync(supergraphSdlPath, 'utf-8'),
batch: true,
let stitchedSchema: GraphQLSchema;
beforeAll(() => {
stitchedSchema = getStitchedSchemaFromSupergraphSdl({
supergraphSdl,
batch: true,
});
});
const tests: { query: string; expected: any }[] = JSON.parse(
readFileSync(join(supergraphFixturesDir, 'tests.json'), 'utf-8'),
Expand Down Expand Up @@ -71,6 +75,11 @@ describe('Federation Compatibility', () => {
);
const sortedInputSchema = lexicographicSortSchema(filteredInputSchema);
const sortedStitchedSchema = lexicographicSortSchema(stitchedSchema);
// For Stitching's sanity, if an interface is not implemented by any object type, it should be converted to an object type
// You can see the difference when you commented this condition out.
if (supergraphName === 'non-resolvable-interface-object') {
return;
}
expect(printSchema(sortedStitchedSchema).trim()).toBe(
printSchema(sortedInputSchema).trim(),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
query: Query
}

directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE

directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

scalar join__FieldSet

enum join__Graph {
A @join__graph(name: "a", url: "https://federation-compatibility.the-guild.dev/interface-object-with-requires/a")
B @join__graph(name: "b", url: "https://federation-compatibility.the-guild.dev/interface-object-with-requires/b")
}

scalar link__Import

enum link__Purpose {
"""
`SECURITY` features provide metadata necessary to securely resolve fields.
"""
SECURITY

"""
`EXECUTION` features provide metadata necessary for operation execution.
"""
EXECUTION
}

interface NodeWithName
@join__type(graph: A, key: "id")
@join__type(graph: B, key: "id", isInterfaceObject: true)
{
id: ID!
name: String @join__field(graph: A) @join__field(graph: B, external: true)
username: String @join__field(graph: B, requires: "name")
}

type Query
@join__type(graph: A)
@join__type(graph: B)
{
users: [NodeWithName!]! @join__field(graph: A)
anotherUsers: [NodeWithName] @join__field(graph: B)
}

type User implements NodeWithName
@join__implements(graph: A, interface: "NodeWithName")
@join__type(graph: A, key: "id")
{
id: ID!
name: String
age: Int
username: String @join__field
}

0 comments on commit 04d5431

Please sign in to comment.