Skip to content

Commit

Permalink
Support directives on enum values and unions (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela authored Oct 2, 2024
1 parent 9794ab2 commit 780892d
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/short-oranges-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@theguild/federation-composition": minor
---

Support directives on enum values and unions
19 changes: 18 additions & 1 deletion __tests__/ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,20 @@ describe('union type', () => {
union Media @inaccessible = Movie | Book
`);
});

test('directives', () => {
expect(
createUnionTypeNode({
name: 'Media',
members: ['Book', 'Movie'],
ast: {
directives: [createDirective('custom')],
},
}),
).toEqualGraphQL(/* GraphQL */ `
union Media @custom = Movie | Book
`);
});
});

describe('input object type', () => {
Expand Down Expand Up @@ -996,6 +1010,9 @@ describe('enum object type', () => {
values: [
{
name: 'BOOK',
ast: {
directives: [createDirective('any')],
},
},
{
name: 'MOVIE',
Expand All @@ -1007,7 +1024,7 @@ describe('enum object type', () => {
}),
).toEqualGraphQL(/* GraphQL */ `
enum Media @custom {
BOOK
BOOK @any
MOVIE
}
`);
Expand Down
108 changes: 108 additions & 0 deletions __tests__/composition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2378,6 +2378,114 @@ testImplementations(api => {
`);
});

test('preserve directive on enum values if included in @composeDirective', () => {
const result = composeServices([
{
name: 'a',
typeDefs: parse(/* GraphQL */ `
extend schema
@link(
url: "https://specs.apollo.dev/federation/${version}"
import: ["@key", "@composeDirective"]
)
@link(url: "https://myspecs.dev/whatever/v1.0", import: ["@whatever"])
@composeDirective(name: "@whatever")
directive @whatever on ENUM_VALUE
enum UserType {
ADMIN @whatever
REGULAR
}
type User @key(fields: "id") {
id: ID!
name: String!
type: UserType!
}
type Query {
users: [User]
}
`),
},
]);

if (version === 'v2.0') {
assertCompositionFailure(result);
return;
}

assertCompositionSuccess(result);

expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
directive @whatever on ENUM_VALUE
`);

expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
enum UserType @join__type(graph: A) {
ADMIN @join__enumValue(graph: A) @whatever
REGULAR @join__enumValue(graph: A)
}
`);
});

test('preserve directive on union types if included in @composeDirective', () => {
const result = composeServices([
{
name: 'a',
typeDefs: parse(/* GraphQL */ `
extend schema
@link(
url: "https://specs.apollo.dev/federation/${version}"
import: ["@key", "@composeDirective"]
)
@link(url: "https://myspecs.dev/whatever/v1.0", import: ["@whatever"])
@composeDirective(name: "@whatever")
directive @whatever on UNION
type User @key(fields: "id") {
id: ID!
name: String!
}
type Admin @key(fields: "id") {
id: ID!
name: String!
}
union Whoever @whatever = User | Admin
type Query {
whoever: [Whoever]
}
`),
},
]);

if (version === 'v2.0') {
assertCompositionFailure(result);
return;
}

assertCompositionSuccess(result);

expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
directive @whatever on UNION
`);

expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ `
union Whoever
@join__type(graph: A)
@join__unionMember(graph: A, member: "Admin")
@join__unionMember(graph: A, member: "User")
@whatever =
| Admin
| User
`);
});

test('preserve directive on interface its field and argument if included in @composeDirective', () => {
const result = composeServices([
{
Expand Down
40 changes: 37 additions & 3 deletions src/subgraph/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ export interface UnionType {
inaccessible: boolean;
isDefinition: boolean;
description?: Description;
ast: {
directives: DirectiveNode[];
};
}

export interface EnumType {
Expand Down Expand Up @@ -211,6 +214,9 @@ export interface EnumValue {
tags: Set<string>;
description?: Description;
deprecated?: Deprecated;
ast: {
directives: DirectiveNode[];
};
}

export interface Argument {
Expand Down Expand Up @@ -724,6 +730,7 @@ export function createSubgraphStateBuilder(
if (composedDirectives.has(node.name.value)) {
const typeDef = typeNodeInfo.getTypeDef();
const fieldDef = typeNodeInfo.getFieldDef();
const enumValueDef = typeNodeInfo.getValueDef();
const argDef = typeNodeInfo.getArgumentDef();

if (!typeDef) {
Expand Down Expand Up @@ -795,12 +802,27 @@ export function createSubgraphStateBuilder(
}
case Kind.ENUM_TYPE_DEFINITION:
case Kind.ENUM_TYPE_EXTENSION: {
enumTypeBuilder.setDirective(typeDef.name.value, node);
if (enumValueDef) {
enumTypeBuilder.value.setDirective(
typeDef.name.value,
enumValueDef.name.value,
node,
);
} else {
enumTypeBuilder.setDirective(typeDef.name.value, node);
}
break;
}
case Kind.UNION_TYPE_DEFINITION:
case Kind.UNION_TYPE_EXTENSION: {
unionTypeBuilder.setDirective(typeDef.name.value, node);
break;
}

default:
// TODO: T07 support directives on other locations than OBJECT, FIELD_DEFINITION, ARGUMENT_DEFINITION
throw new Error(`Directives on "${typeDef.kind}" types are not supported yet`);
throw new Error(
`Directives on "${typeof typeDef === 'object' && typeDef !== null && 'kind' in typeDef ? (typeDef as any).kind : typeDef}" types are not supported yet`,
);
}
} else if (node.name.value === 'specifiedBy') {
const typeDef = typeNodeInfo.getTypeDef();
Expand Down Expand Up @@ -1786,6 +1808,9 @@ function unionTypeFactory(state: SubgraphState) {
setMember(typeName: string, member: string) {
getOrCreateUnionType(state, typeName).members.add(member);
},
setDirective(typeName: string, directive: DirectiveNode) {
getOrCreateUnionType(state, typeName).ast.directives.push(directive);
},
};
}

Expand Down Expand Up @@ -1830,6 +1855,9 @@ function enumTypeFactory(state: SubgraphState) {
setDescription(typeName: string, valueName: string, description: Description) {
getOrCreateEnumValue(state, typeName, valueName).description = description;
},
setDirective(typeName: string, valueName: string, directive: DirectiveNode) {
getOrCreateEnumValue(state, typeName, valueName).ast.directives.push(directive);
},
setInaccessible(typeName: string, valueName: string) {
getOrCreateEnumValue(state, typeName, valueName).inaccessible = true;
},
Expand Down Expand Up @@ -2089,6 +2117,9 @@ function getOrCreateUnionType(state: SubgraphState, typeName: string): UnionType
inaccessible: false,
tags: new Set(),
isDefinition: false,
ast: {
directives: [],
},
};

state.types.set(typeName, unionType);
Expand Down Expand Up @@ -2230,6 +2261,9 @@ function getOrCreateEnumValue(
name: enumValueName,
inaccessible: false,
tags: new Set(),
ast: {
directives: [],
},
};

enumType.values.set(enumValueName, enumValue);
Expand Down
13 changes: 13 additions & 0 deletions src/supergraph/composition/enum-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export function enumTypeBuilder(): TypeBuilder<EnumType, EnumTypeState> {
valueState.description = value.description;
}

value.ast.directives.forEach(directive => {
valueState.ast.directives.push(directive);
});

valueState.byGraph.set(graph.id, {
inaccessible: value.inaccessible,
version: graph.version,
Expand Down Expand Up @@ -114,6 +118,9 @@ export function enumTypeBuilder(): TypeBuilder<EnumType, EnumTypeState> {
inaccessible: value.inaccessible,
description: value.description,
deprecated: value.deprecated,
ast: {
directives: convertToConst(value.ast.directives),
},
})),
tags: Array.from(enumType.tags),
inaccessible: enumType.inaccessible,
Expand Down Expand Up @@ -185,6 +192,9 @@ type EnumValueState = {
inaccessible: boolean;
deprecated?: Deprecated;
description?: Description;
ast: {
directives: DirectiveNode[];
};
byGraph: MapByGraph<EnumValueStateInGraph>;
};

Expand Down Expand Up @@ -242,6 +252,9 @@ function getOrCreateEnumValue(enumTypeState: EnumTypeState, enumValueName: strin
tags: new Set(),
inaccessible: false,
byGraph: new Map(),
ast: {
directives: [],
},
};

enumTypeState.values.set(enumValueName, def);
Expand Down
16 changes: 15 additions & 1 deletion src/supergraph/composition/union-type.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { DirectiveNode } from 'graphql';
import { FederationVersion } from '../../specifications/federation.js';
import { Description, UnionType } from '../../subgraph/state.js';
import { createUnionTypeNode } from './ast.js';
import type { MapByGraph, TypeBuilder } from './common.js';
import { convertToConst, type MapByGraph, type TypeBuilder } from './common.js';

export function unionTypeBuilder(): TypeBuilder<UnionType, UnionTypeState> {
return {
Expand All @@ -23,6 +24,10 @@ export function unionTypeBuilder(): TypeBuilder<UnionType, UnionTypeState> {
unionTypeState.description = type.description;
}

type.ast.directives.forEach(directive => {
unionTypeState.ast.directives.push(directive);
});

unionTypeState.byGraph.set(graph.id, {
members: type.members,
version: graph.version,
Expand Down Expand Up @@ -50,6 +55,9 @@ export function unionTypeBuilder(): TypeBuilder<UnionType, UnionTypeState> {
})
.flat(1),
},
ast: {
directives: convertToConst(unionType.ast.directives),
},
});
},
};
Expand All @@ -64,6 +72,9 @@ export type UnionTypeState = {
inaccessible: boolean;
byGraph: MapByGraph<UnionTypeInGraph>;
members: Set<string>;
ast: {
directives: DirectiveNode[];
};
};

type UnionTypeInGraph = {
Expand All @@ -86,6 +97,9 @@ function getOrCreateUnionType(state: Map<string, UnionTypeState>, typeName: stri
inaccessible: false,
hasDefinition: false,
byGraph: new Map(),
ast: {
directives: [],
},
};

state.set(typeName, def);
Expand Down

0 comments on commit 780892d

Please sign in to comment.