Skip to content

Commit

Permalink
Federation package fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Oct 13, 2023
1 parent 9d1da45 commit 250715a
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 123 deletions.
5 changes: 5 additions & 0 deletions .changeset/popular-dots-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/federation': patch
---

Support `extend type` in subgraph SDL
5 changes: 5 additions & 0 deletions .changeset/wise-houses-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/federation': patch
---

Support supergraph with no join\_\_type directives on Query type
49 changes: 42 additions & 7 deletions packages/federation/src/subgraph.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { GraphQLResolveInfo, visit } from 'graphql';
import {
GraphQLResolveInfo,
Kind,
ObjectTypeDefinitionNode,
ObjectTypeExtensionNode,
visit,
} from 'graphql';
import { ValueOrPromise } from 'value-or-promise';
import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge';
import { IExecutableSchemaDefinition, makeExecutableSchema } from '@graphql-tools/schema';
Expand Down Expand Up @@ -44,14 +50,43 @@ export const SubgraphBaseSDL = /* GraphQL */ `
directive @extends on OBJECT | INTERFACE
`;

export function buildSubgraphSchema<TContext = any>(opts: IExecutableSchemaDefinition<TContext>) {
const typeDefs = mergeTypeDefs([SubgraphBaseSDL, opts.typeDefs]);
export function buildSubgraphSchema<TContext = any>(
optsOrModules:
| IExecutableSchemaDefinition<TContext>
| Pick<IExecutableSchemaDefinition<TContext>, 'typeDefs' | 'resolvers'>[],
) {
const opts = Array.isArray(optsOrModules)
? {
typeDefs: optsOrModules.map(opt => opt.typeDefs),
resolvers: optsOrModules.map(opt => opt.resolvers).flat(),
}
: optsOrModules;
const entityTypeNames: string[] = [];
visit(typeDefs, {
function handleEntity(node: ObjectTypeExtensionNode | ObjectTypeDefinitionNode) {
if (node.directives?.some(directive => directive.name.value === 'key')) {
entityTypeNames.push(node.name.value);
}
}
const typeDefs = visit(mergeTypeDefs([SubgraphBaseSDL, opts.typeDefs]), {
ObjectTypeDefinition: node => {
if (node.directives?.some(directive => directive.name.value === 'key')) {
entityTypeNames.push(node.name.value);
}
handleEntity(node);
},
ObjectTypeExtension: node => {
handleEntity(node);
return {
...node,
kind: Kind.OBJECT_TYPE_DEFINITION,
directives: [
...(node.directives || []),
{
kind: 'Directive',
name: {
kind: 'Name',
value: 'extends',
},
},
],
};
},
});
const entityTypeDefinition = `union _Entity = ${entityTypeNames.join(' | ')}`;
Expand Down
202 changes: 86 additions & 116 deletions packages/federation/src/supergraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Kind,
NamedTypeNode,
ObjectTypeDefinitionNode,
OperationTypeNode,
parse,
ScalarTypeDefinitionNode,
TypeDefinitionNode,
Expand Down Expand Up @@ -40,58 +39,55 @@ export function getSubschemasFromSupergraphSdl({
}: GetSubschemasFromSupergraphSdlOpts) {
const ast =
typeof supergraphSdl === 'string' ? parse(supergraphSdl, { noLocation: true }) : supergraphSdl;
const subgraphRootFieldDefinitionNodes = new Map<
string,
Map<OperationTypeNode, FieldDefinitionNode[]>
>();
const subgraphEndpointMap = new Map<string, string>();
const subgraphTypesMap = new Map<string, TypeDefinitionNode[]>();
const typeNameKeysBySubgraphMap = new Map<string, Map<string, string[]>>();
const typeNameFieldsKeyBySubgraphMap = new Map<string, Map<string, Map<string, string>>>();
const typeNameCanonicalMap = new Map<string, string>();
const rootTypeNames = new Map<OperationTypeNode, string>();
const subgraphTypeNameExtraFieldsMap = new Map<string, Map<string, FieldDefinitionNode[]>>();
function TypeWithFieldsVisitor(typeNode: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode) {
if ([...rootTypeNames.values()].includes(typeNode.name.value)) {
const operationTypeName = [...rootTypeNames.entries()].find(
([, rootTypeName]) => rootTypeName === typeNode.name.value,
)![0];
typeNode.fields?.forEach(fieldNode => {
fieldNode.directives?.forEach(directiveNode => {
if (directiveNode.name.value === 'join__field') {
directiveNode.arguments?.forEach(argumentNode => {
if (argumentNode.name.value === 'graph' && argumentNode.value?.kind === Kind.ENUM) {
const graphName = argumentNode.value.value;
let subgraphRootFieldDefinitionNodeMap =
subgraphRootFieldDefinitionNodes.get(graphName);
if (!subgraphRootFieldDefinitionNodeMap) {
subgraphRootFieldDefinitionNodeMap = new Map();
subgraphRootFieldDefinitionNodes.set(
graphName,
subgraphRootFieldDefinitionNodeMap,
);
}
let fieldDefinitionNodesOfSubgraph =
subgraphRootFieldDefinitionNodeMap.get(operationTypeName);
if (!fieldDefinitionNodesOfSubgraph) {
fieldDefinitionNodesOfSubgraph = [];
subgraphRootFieldDefinitionNodeMap.set(
operationTypeName,
fieldDefinitionNodesOfSubgraph,
);
}
fieldDefinitionNodesOfSubgraph.push({
...fieldNode,
directives: fieldNode.directives?.filter(
directiveNode => directiveNode.name.value !== 'join__field',
),
});
}
});
}
// TODO: Temporary fix to add missing join__type directives to Query
const subgraphNames: string[] = [];
visit(ast, {
EnumTypeDefinition(node) {
if (node.name.value === 'join__Graph') {
node.values?.forEach(valueNode => {
subgraphNames.push(valueNode.name.value);
});
});
}
},
});
// END TODO
function TypeWithFieldsVisitor(typeNode: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode) {
// TODO: Temporary fix to add missing join__type directives to Query
if (
typeNode.name.value === 'Query' &&
!typeNode.directives?.some(directiveNode => directiveNode.name.value === 'join__type')
) {
(typeNode as any).directives = [
...(typeNode.directives || []),
...subgraphNames.map(subgraphName => ({
kind: Kind.DIRECTIVE,
name: {
kind: Kind.NAME,
value: 'join__type',
},
arguments: [
{
kind: Kind.ARGUMENT,
name: {
kind: Kind.NAME,
value: 'graph',
},
value: {
kind: Kind.ENUM,
value: subgraphName,
},
},
],
})),
];
}
// END TODO
const fieldDefinitionNodesByGraphName = new Map<string, FieldDefinitionNode[]>();
typeNode.directives?.forEach(directiveNode => {
if (typeNode.kind === Kind.OBJECT_TYPE_DEFINITION) {
Expand Down Expand Up @@ -249,6 +245,9 @@ export function getSubschemasFromSupergraphSdl({
interfaces.push(interfaceNode);
}
});
if (typeNode.name.value === 'Query') {
fieldDefinitionNodesOfSubgraph.push(entitiesFieldDefinitionNode);
}
const objectTypedDefNodeForSubgraph: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode =
{
...typeNode,
Expand All @@ -270,11 +269,6 @@ export function getSubschemasFromSupergraphSdl({
});
}
visit(ast, {
SchemaDefinition(node) {
node.operationTypes?.forEach(operationTypeNode => {
rootTypeNames.set(operationTypeNode.operation, operationTypeNode.type.name.value);
});
},
ScalarTypeDefinition(node) {
node.directives?.forEach(directiveNode => {
if (directiveNode.name.value === 'join__type') {
Expand Down Expand Up @@ -482,66 +476,7 @@ export function getSubschemasFromSupergraphSdl({
kind: Kind.UNION_TYPE_DEFINITION,
types: unionTypeNodes,
};
let subgraphRootFieldDefinitionNodeMap = subgraphRootFieldDefinitionNodes.get(subgraphName);
if (!subgraphRootFieldDefinitionNodeMap) {
subgraphRootFieldDefinitionNodeMap = new Map();
subgraphRootFieldDefinitionNodes.set(subgraphName, subgraphRootFieldDefinitionNodeMap);
}
let queryFields = subgraphRootFieldDefinitionNodeMap.get('query' as OperationTypeNode);
if (!queryFields) {
queryFields = [];
subgraphRootFieldDefinitionNodeMap.set('query' as OperationTypeNode, queryFields);
}
queryFields.push({
kind: Kind.FIELD_DEFINITION,
name: {
kind: Kind.NAME,
value: '_entities',
},
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: '_Entity',
},
},
arguments: [
{
kind: Kind.INPUT_VALUE_DEFINITION,
name: {
kind: Kind.NAME,
value: 'representations',
},
type: {
kind: Kind.NON_NULL_TYPE,
type: {
kind: Kind.LIST_TYPE,
type: {
kind: Kind.NON_NULL_TYPE,
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: '_Any',
},
},
},
},
},
},
],
});
const rootTypes: TypeDefinitionNode[] = [];
for (const [operationType, fieldDefinitionNodes] of subgraphRootFieldDefinitionNodeMap) {
rootTypes.push({
kind: Kind.OBJECT_TYPE_DEFINITION,
name: {
kind: Kind.NAME,
value: rootTypeNames.get(operationType)!,
},
fields: fieldDefinitionNodes,
});
}

const subgraphTypes = subgraphTypesMap.get(subgraphName) || [];
const typeNameExtraFieldsMap = subgraphTypeNameExtraFieldsMap.get(subgraphName);
if (typeNameExtraFieldsMap) {
Expand All @@ -557,12 +492,7 @@ export function getSubschemasFromSupergraphSdl({
const schema = buildASTSchema(
{
kind: Kind.DOCUMENT,
definitions: [
...subgraphTypes,
entitiesUnionTypeDefinitionNode,
anyTypeDefinitionNode,
...rootTypes,
],
definitions: [...subgraphTypes, entitiesUnionTypeDefinitionNode, anyTypeDefinitionNode],
},
{
assumeValidSDL: true,
Expand Down Expand Up @@ -596,3 +526,43 @@ const anyTypeDefinitionNode: ScalarTypeDefinitionNode = {
},
kind: Kind.SCALAR_TYPE_DEFINITION,
};

const entitiesFieldDefinitionNode: FieldDefinitionNode = {
kind: Kind.FIELD_DEFINITION,
name: {
kind: Kind.NAME,
value: '_entities',
},
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: '_Entity',
},
},
arguments: [
{
kind: Kind.INPUT_VALUE_DEFINITION,
name: {
kind: Kind.NAME,
value: 'representations',
},
type: {
kind: Kind.NON_NULL_TYPE,
type: {
kind: Kind.LIST_TYPE,
type: {
kind: Kind.NON_NULL_TYPE,
type: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: '_Any',
},
},
},
},
},
},
],
};
57 changes: 57 additions & 0 deletions packages/federation/test/__snapshots__/subgraph.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`converts extensions in the subgraph SDL 1`] = `
"schema {
query: Query
}
directive @external on FIELD_DEFINITION | OBJECT
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @key(fields: _FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
directive @link(url: String!, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @override(from: String!) on FIELD_DEFINITION
directive @composeDirective(name: String!) repeatable on SCHEMA
directive @extends on OBJECT | INTERFACE
union _Entity = User
scalar _Any
scalar _FieldSet
scalar link__Import
enum link__Purpose {
SECURITY
EXECUTION
}
type _Service {
sdl: String!
}
type Query {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
users: [User!]!
}
type User @key(fields: "id") @extends {
id: ID!
name: String!
}"
`;
Loading

0 comments on commit 250715a

Please sign in to comment.