Skip to content

Commit

Permalink
WIP: Interface Object
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed May 2, 2024
1 parent 361052a commit a740869
Show file tree
Hide file tree
Showing 14 changed files with 1,326 additions and 7 deletions.
134 changes: 127 additions & 7 deletions packages/federation/src/supergraph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
buildASTSchema,
DefinitionNode,
DocumentNode,
EnumTypeDefinitionNode,
EnumValueDefinitionNode,
Expand Down Expand Up @@ -110,6 +111,7 @@ export function getSubschemasFromSupergraphSdl({
const typeNameCanonicalMap = new Map<string, string>();
const subgraphTypeNameExtraFieldsMap = new Map<string, Map<string, FieldDefinitionNode[]>>();
const orphanTypeMap = new Map<string, TypeDefinitionNode>();
const iFaceObjects = new Set<string>();
// TODO: Temporary fix to add missing join__type directives to Query
const subgraphNames: string[] = [];
visit(supergraphAst, {
Expand Down Expand Up @@ -171,6 +173,15 @@ export function getSubschemasFromSupergraphSdl({
const joinTypeGraphArgNode = directiveNode.arguments?.find(
argumentNode => argumentNode.name.value === 'graph',
);
const isInterfaceObjectArgNode = directiveNode.arguments?.find(
argumentNode => argumentNode.name.value === 'isInterfaceObject',
);
if (
isInterfaceObjectArgNode?.value?.kind === Kind.BOOLEAN &&
isInterfaceObjectArgNode.value
) {
iFaceObjects.add(typeNode.name.value);
}
if (joinTypeGraphArgNode?.value?.kind === Kind.ENUM) {
const graphName = joinTypeGraphArgNode.value.value;
if (typeNode.kind === Kind.OBJECT_TYPE_DEFINITION) {
Expand All @@ -194,10 +205,8 @@ 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 => {
const joinFieldGraphArgNode = joinFieldDirectiveNode.arguments?.find(
argumentNode => argumentNode.name.value === 'graph',
Expand All @@ -206,7 +215,6 @@ export function getSubschemasFromSupergraphSdl({
joinFieldGraphArgNode?.value?.kind === Kind.ENUM &&
joinFieldGraphArgNode.value.value === graphName
) {
notInSubgraph = false;
const isExternal = joinFieldDirectiveNode.arguments?.some(
argumentNode =>
argumentNode.name.value === 'external' &&
Expand Down Expand Up @@ -290,7 +298,6 @@ export function getSubschemasFromSupergraphSdl({
),
});
} else if (
notInSubgraph &&
typeNameKeysBySubgraphMap
.get(graphName)
?.get(typeNode.name.value)
Expand Down Expand Up @@ -726,7 +733,10 @@ export function getSubschemasFromSupergraphSdl({
const extendedSubgraphTypes = [...subgraphTypes, ...extraOrphanTypesForSubgraph.values()];
// We should add implemented objects from other subgraphs implemented by this interface
for (const interfaceInSubgraph of extendedSubgraphTypes) {
if (interfaceInSubgraph.kind === Kind.INTERFACE_TYPE_DEFINITION) {
if (
interfaceInSubgraph.kind === Kind.INTERFACE_TYPE_DEFINITION &&
!iFaceObjects.has(interfaceInSubgraph.name.value)
) {
for (const definitionNode of supergraphAst.definitions) {
if (
definitionNode.kind === Kind.OBJECT_TYPE_DEFINITION &&
Expand All @@ -751,6 +761,10 @@ export function getSubschemasFromSupergraphSdl({
fields: interfaceInSubgraph.fields,
interfaces: [iFaceNode],
});
unionTypeNodes.push({
kind: Kind.NAMED_TYPE,
name: definitionNode.name,
});
}
if (
existingType?.kind === Kind.OBJECT_TYPE_DEFINITION &&
Expand All @@ -767,14 +781,115 @@ export function getSubschemasFromSupergraphSdl({
}
}
let schema: GraphQLSchema;
const schemaAst: DocumentNode = {
let schemaAst: DocumentNode = {
kind: Kind.DOCUMENT,
definitions: [
...extendedSubgraphTypes,
entitiesUnionTypeDefinitionNode,
anyTypeDefinitionNode,
],
};
// Interface Objects
if (iFaceObjects.size) {
const newObjectDefs: ObjectTypeDefinitionNode[] = [];
const iFaceObjectImplementations = new Map<string, Set<string>>();
schemaAst = visit(schemaAst, {
[Kind.INTERFACE_TYPE_DEFINITION](node) {
if (iFaceObjects.has(node.name.value)) {
const newIfaceName = `iFace_${node.name.value}`;
const objectDef: ObjectTypeDefinitionNode = {
kind: Kind.OBJECT_TYPE_DEFINITION,
name: node.name,
fields: node.fields,
interfaces: [
{
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: newIfaceName,
},
},
],
};
newObjectDefs.push(objectDef);
return {
...node,
name: {
kind: Kind.NAME,
value: newIfaceName,
},
};
}
if (node.interfaces?.some(iFace => iFaceObjects.has(iFace.name.value))) {
return {
...node,
interfaces: node.interfaces?.map(iFace =>
iFaceObjects.has(iFace.name.value)
? {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: `iFace_${iFace.name.value}`,
},
}
: iFace,
),
};
}
},
[Kind.OBJECT_TYPE_DEFINITION](node) {
if (node.interfaces?.some(iFace => iFaceObjects.has(iFace.name.value))) {
const interfaceNodes: NamedTypeNode[] = [];
for (const iFace of node.interfaces || []) {
if (iFaceObjects.has(iFace.name.value)) {
const iFaceName = iFace.name.value;
let iFaceImplementations = iFaceObjectImplementations.get(iFaceName);
if (!iFaceImplementations) {
iFaceImplementations = new Set();
iFaceObjectImplementations.set(iFaceName, iFaceImplementations);
}
iFaceImplementations.add(node.name.value);
interfaceNodes.push({
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: `iFace_${iFace.name.value}`,
},
});
} else {
interfaceNodes.push(iFace);
}
}
return {
...node,
interfaces: interfaceNodes,
};
}
},
[Kind.NAMED_TYPE](node) {
if (iFaceObjects.has(node.name.value)) {
return {
...node,
name: {
kind: Kind.NAME,
value: `iFace_${node.name.value}`,
},
};
}
},
});
(schemaAst.definitions as DefinitionNode[]).push(...newObjectDefs);
for (const [iFaceName, implementations] of iFaceObjectImplementations) {
for (const implName of implementations) {
const implEntrypoints = mergeConfig[implName]?.entryPoints;
if (implEntrypoints?.length) {
const iFaceMergeTypeConfig = (mergeConfig[iFaceName] ||= {});
const iFaceEntrypoints = (iFaceMergeTypeConfig.entryPoints ||= []);
iFaceEntrypoints.push(...implEntrypoints);
}
}
}
}
try {
schema = buildASTSchema(schemaAst, {
assumeValidSDL: true,
Expand Down Expand Up @@ -822,6 +937,11 @@ export function getStitchedSchemaFromSupergraphSdl(opts: GetSubschemasFromSuperg
},
fieldConfigMerger: getFieldMergerFromSupergraphSdl(opts.supergraphSdl),
},
typeDefs: /* GraphQL */ `
type User {
username: String
}
`,
});
return filterInternalFieldsAndTypes(supergraphSchema);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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

type Category
@join__type(graph: PRICE, key: "id tag")
@join__type(graph: PRODUCTS, key: "id")
{
id: String!
tag: String
mainProduct: Product! @join__field(graph: PRODUCTS)
}

scalar join__FieldSet

enum join__Graph {
LINK @join__graph(name: "link", url: "https://federation-compatibility.the-guild.dev/complex-entity-call/link")
LIST @join__graph(name: "list", url: "https://federation-compatibility.the-guild.dev/complex-entity-call/list")
PRICE @join__graph(name: "price", url: "https://federation-compatibility.the-guild.dev/complex-entity-call/price")
PRODUCTS @join__graph(name: "products", url: "https://federation-compatibility.the-guild.dev/complex-entity-call/products")
}

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
}

type Price
@join__type(graph: PRICE)
{
price: Float!
}

type Product
@join__type(graph: LINK, key: "id")
@join__type(graph: LINK, key: "id pid")
@join__type(graph: LIST, key: "id pid")
@join__type(graph: PRICE, key: "id pid category{id tag}")
@join__type(graph: PRODUCTS, key: "id", extension: true)
{
id: String!
pid: String @join__field(graph: LINK, type: "String!") @join__field(graph: LIST, type: "String") @join__field(graph: PRICE, type: "String")
price: Price @join__field(graph: PRICE)
category: Category @join__field(graph: PRICE) @join__field(graph: PRODUCTS)
}

type ProductList
@join__type(graph: LIST, key: "products{id pid}")
@join__type(graph: PRICE, key: "products{id pid category{id tag}} selected{id}")
@join__type(graph: PRODUCTS, key: "products{id}")
{
products: [Product!]!
first: Product @join__field(graph: LIST) @join__field(graph: PRICE)
selected: Product @join__field(graph: LIST) @join__field(graph: PRICE)
}

type Query
@join__type(graph: LINK)
@join__type(graph: LIST)
@join__type(graph: PRICE)
@join__type(graph: PRODUCTS)
{
topProducts: ProductList! @join__field(graph: PRODUCTS)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[
{
"query": "\n query {\n topProducts {\n products {\n id\n pid\n price {\n price\n }\n category {\n mainProduct {\n id\n }\n id\n tag\n }\n }\n selected {\n id\n }\n first {\n id\n }\n }\n }\n ",
"expected": {
"data": {
"topProducts": {
"products": [
{
"id": "1",
"pid": "p1",
"price": {
"price": 100
},
"category": {
"mainProduct": {
"id": "1"
},
"id": "c1",
"tag": "t1"
}
},
{
"id": "2",
"pid": "p2",
"price": {
"price": 200
},
"category": {
"mainProduct": {
"id": "2"
},
"id": "c2",
"tag": "t2"
}
}
],
"selected": {
"id": "2"
},
"first": {
"id": "1"
}
}
}
}
}
]

0 comments on commit a740869

Please sign in to comment.