Skip to content

Commit

Permalink
fix(federation): Fed v1 support improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Mar 13, 2024
1 parent 34763b6 commit 2202768
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/real-lies-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-tools/federation": patch
---

Federation v1 support improvements
150 changes: 130 additions & 20 deletions packages/federation/src/supergraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import {
EnumValueDefinitionNode,
FieldDefinitionNode,
GraphQLSchema,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
InterfaceTypeExtensionNode,
Kind,
NamedTypeNode,
ObjectTypeDefinitionNode,
ObjectTypeExtensionNode,
parse,
print,
ScalarTypeDefinitionNode,
Expand Down Expand Up @@ -534,16 +537,116 @@ export function getSubschemasFromSupergraphSdl({
types: unionTypeNodes,
};

const extraOrphanTypesForSubgraph = new Set<TypeDefinitionNode>();
const extraOrphanTypesForSubgraph = new Map<string, TypeDefinitionNode>();
// eslint-disable-next-line no-inner-declarations
function visitTypeDefinitionsForOrphanTypes(node: TypeDefinitionNode) {
visit(node, {
[Kind.NAMED_TYPE](node) {
const orphanType = orphanTypeMap.get(node.name.value);
if (orphanType && !extraOrphanTypesForSubgraph.has(orphanType)) {
extraOrphanTypesForSubgraph.add(orphanType);
visitTypeDefinitionsForOrphanTypes(orphanType);
function visitNamedTypeNode(namedTypeNode: NamedTypeNode) {
const typeName = namedTypeNode.name.value;
if (specifiedTypeNames.includes(typeName)) {
return;
}
const orphanType = orphanTypeMap.get(typeName);
if (orphanType) {
if (!extraOrphanTypesForSubgraph.has(typeName)) {
extraOrphanTypesForSubgraph.set(typeName, {} as any);
const extraOrphanType = visitTypeDefinitionsForOrphanTypes(orphanType);
extraOrphanTypesForSubgraph.set(typeName, extraOrphanType);
}
} else if (!subgraphTypes.some(typeNode => typeNode.name.value === typeName)) {
console.log(`Orphan type ${typeName} not found in subgraph ${subgraphName}`);
return null;
}
return node;
}
function visitFieldDefs<TFieldDef extends InputValueDefinitionNode | FieldDefinitionNode>(
nodeFields?: readonly TFieldDef[] | undefined,
): TFieldDef[] {
const fields: TFieldDef[] = [];
for (const field of nodeFields || []) {
const isTypeNodeOk = visitNamedTypeNode(getNamedTypeNode(field.type) as NamedTypeNode);
if (!isTypeNodeOk) {
continue;
}
if (field.kind === Kind.FIELD_DEFINITION) {
const args: InputValueDefinitionNode[] = visitFieldDefs(field.arguments);
fields.push({
...field,
arguments: args,
});
} else {
fields.push(field);
}
}
return fields;
}
function visitObjectAndInterfaceDefs(
node:
| ObjectTypeDefinitionNode
| InterfaceTypeDefinitionNode
| ObjectTypeExtensionNode
| InterfaceTypeExtensionNode,
) {
const fields: FieldDefinitionNode[] = visitFieldDefs(node.fields);
const interfaces: NamedTypeNode[] = [];
for (const iface of node.interfaces || []) {
const isTypeNodeOk = visitNamedTypeNode(iface);
if (!isTypeNodeOk) {
continue;
}
interfaces.push(iface);
}
return {
...node,
fields,
interfaces,
};
}
return visit(node, {
[Kind.OBJECT_TYPE_DEFINITION]: visitObjectAndInterfaceDefs,
[Kind.OBJECT_TYPE_EXTENSION]: visitObjectAndInterfaceDefs,
[Kind.INTERFACE_TYPE_DEFINITION]: visitObjectAndInterfaceDefs,
[Kind.INTERFACE_TYPE_EXTENSION]: visitObjectAndInterfaceDefs,
[Kind.UNION_TYPE_DEFINITION](node) {
const types: NamedTypeNode[] = [];
for (const type of node.types || []) {
const isTypeNodeOk = visitNamedTypeNode(type);
if (!isTypeNodeOk) {
continue;
}
types.push(type);
}
return {
...node,
types,
};
},
[Kind.UNION_TYPE_EXTENSION](node) {
const types: NamedTypeNode[] = [];
for (const type of node.types || []) {
const isTypeNodeOk = visitNamedTypeNode(type);
if (!isTypeNodeOk) {
continue;
}
types.push(type);
}
return {
...node,
types,
};
},
[Kind.INPUT_OBJECT_TYPE_DEFINITION](node) {
const fields = visitFieldDefs(node.fields);
return {
...node,
fields,
};
},
[Kind.INPUT_OBJECT_TYPE_EXTENSION](node) {
const fields = visitFieldDefs(node.fields);
return {
...node,
fields,
};
},
});
}
Expand All @@ -558,21 +661,26 @@ export function getSubschemasFromSupergraphSdl({
}
visitTypeDefinitionsForOrphanTypes(typeNode);
});
const schema = buildASTSchema(
{
kind: Kind.DOCUMENT,
definitions: [
...subgraphTypes,
...extraOrphanTypesForSubgraph,
entitiesUnionTypeDefinitionNode,
anyTypeDefinitionNode,
],
},
{
let schema: GraphQLSchema;
const schemaAst: DocumentNode = {
kind: Kind.DOCUMENT,
definitions: [
...subgraphTypes,
...extraOrphanTypesForSubgraph.values(),
entitiesUnionTypeDefinitionNode,
anyTypeDefinitionNode,
],
};
try {
schema = buildASTSchema(schemaAst, {
assumeValidSDL: true,
assumeValid: true,
},
);
});
} catch (e: any) {
throw new Error(
`Error building schema for subgraph ${subgraphName}: ${e?.message || e.toString()}`,
);
}
let executor: Executor = onExecutor({ subgraphName, endpoint, subgraphSchema: schema });
if (globalThis.process?.env?.['DEBUG']) {
const origExecutor = executor;
Expand Down Expand Up @@ -651,3 +759,5 @@ const entitiesFieldDefinitionNode: FieldDefinitionNode = {
},
],
};

const specifiedTypeNames = ['ID', 'String', 'Float', 'Int', 'Boolean', '_Any', '_Entity'];
54 changes: 44 additions & 10 deletions packages/federation/test/__snapshots__/supergraphs.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -548,24 +548,38 @@ type Member implements Node {
email: String!
}
interface Node {
type ConcreteBoard implements Board & Node & SomeBoard {
id: ID!
isVisible: Boolean!
someEntity: SomeEntity
}
scalar Url @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc1738")
interface Node
union Mentionable = ReactionAuthor | OwnerChangedNotification
type OwnerChangedNotification implements AppNotification {
entityType: NotifiableEntityType
}
enum NotifiableEntityType {
ITEM
}
interface AppNotification {
entityType: NotifiableEntityType
}
enum NotifiableEntityType {
ITEM
interface Board
interface SomeBoard {
someEntity: SomeEntity
}
type SomeEntity implements Node {
id: ID!
}"
`;

Expand Down Expand Up @@ -614,27 +628,33 @@ type Member implements Node {
email: String!
}
interface Node {
type ConcreteBoard implements Board & Node & SomeBoard {
id: ID!
}
scalar Url @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc1738")
interface Node
union Mentionable = ReactionAuthor | OwnerChangedNotification
type OwnerChangedNotification implements AppNotification {
entityType: NotifiableEntityType
}
enum NotifiableEntityType {
ITEM
}
interface AppNotification {
entityType: NotifiableEntityType
}
enum NotifiableEntityType {
ITEM
}
interface Board
interface SomeBoard
union _Entity = ReactionAuthor | Handle | Member
union _Entity = ReactionAuthor | Handle | Member | ConcreteBoard
scalar _Any"
`;
Expand All @@ -652,11 +672,25 @@ type Member implements Node {
id: ID!
}
interface Node {
type SomeEntity implements Node {
id: ID!
}
union _Entity = Member
type ConcreteBoard implements Board & Node & SomeBoard {
id: ID!
isVisible: Boolean!
someEntity: SomeEntity
}
interface Node
interface Board
interface SomeBoard {
someEntity: SomeEntity
}
union _Entity = Member | SomeEntity | ConcreteBoard
scalar _Any"
`;
26 changes: 26 additions & 0 deletions packages/federation/test/fixtures/supergraphs/d.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@ type Member implements Node

union Mentionable = ReactionAuthor | OwnerChangedNotification

interface Board {
id: ID!
isVisible: Boolean!
}

interface SomeBoard {
# introducing this interface and field causes:
# Mesh - Supergraph 💥 Failed to generate the schema Error: Unknown type: "SomeEntity".
someEntity: SomeEntity
}

type SomeEntity implements Node
@join__owner(graph: PB_SERVICE_B)
@join__type(graph: PB_SERVICE_B, key: "id") {
id: ID! @join__field(graph: PB_SERVICE_B)
}

type ConcreteBoard implements Board & Node & SomeBoard
@join__owner(graph: PB_SERVICE_B)
@join__type(graph: PB_SERVICE_B, key: "id")
@join__type(graph: PB_BACKEND, key: "id") {
id: ID! @join__field(graph: PB_SERVICE_B)
isVisible: Boolean! @join__field(graph: PB_SERVICE_B)
someEntity: SomeEntity @join__field(graph: PB_SERVICE_B)
}

enum core__Purpose {
"""
`EXECUTION` features provide metadata necessary to for operation execution.
Expand Down

0 comments on commit 2202768

Please sign in to comment.