feat: allow interfaces to implement other interfaces (#496)
closes #389
This commit is contained in:
parent
10c5f8bc8c
commit
9bfdf2cdc5
101
src/builder.ts
101
src/builder.ts
|
|
@ -52,16 +52,12 @@ import { NexusExtendInputTypeConfig, NexusExtendInputTypeDef } from './definitio
|
||||||
import { NexusExtendTypeConfig, NexusExtendTypeDef } from './definitions/extendType'
|
import { NexusExtendTypeConfig, NexusExtendTypeDef } from './definitions/extendType'
|
||||||
import { NexusInputObjectTypeConfig } from './definitions/inputObjectType'
|
import { NexusInputObjectTypeConfig } from './definitions/inputObjectType'
|
||||||
import {
|
import {
|
||||||
|
Implemented,
|
||||||
InterfaceDefinitionBlock,
|
InterfaceDefinitionBlock,
|
||||||
NexusInterfaceTypeConfig,
|
NexusInterfaceTypeConfig,
|
||||||
NexusInterfaceTypeDef,
|
NexusInterfaceTypeDef,
|
||||||
} from './definitions/interfaceType'
|
} from './definitions/interfaceType'
|
||||||
import {
|
import { NexusObjectTypeConfig, NexusObjectTypeDef, ObjectDefinitionBlock } from './definitions/objectType'
|
||||||
Implemented,
|
|
||||||
NexusObjectTypeConfig,
|
|
||||||
NexusObjectTypeDef,
|
|
||||||
ObjectDefinitionBlock,
|
|
||||||
} from './definitions/objectType'
|
|
||||||
import { NexusScalarExtensions, NexusScalarTypeConfig } from './definitions/scalarType'
|
import { NexusScalarExtensions, NexusScalarTypeConfig } from './definitions/scalarType'
|
||||||
import { NexusUnionTypeConfig, UnionDefinitionBlock, UnionMembers } from './definitions/unionType'
|
import { NexusUnionTypeConfig, UnionDefinitionBlock, UnionMembers } from './definitions/unionType'
|
||||||
import {
|
import {
|
||||||
|
|
@ -701,6 +697,51 @@ export class SchemaBuilder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkForInterfaceCircularDependencies() {
|
||||||
|
const interfaces: Record<string, NexusInterfaceTypeConfig<any>> = {}
|
||||||
|
Object.keys(this.pendingTypeMap)
|
||||||
|
.map((key) => this.pendingTypeMap[key])
|
||||||
|
.filter(isNexusInterfaceTypeDef)
|
||||||
|
.forEach((type) => {
|
||||||
|
interfaces[type.name] = type.value
|
||||||
|
})
|
||||||
|
const alreadyChecked: Record<string, boolean> = {}
|
||||||
|
function walkType(obj: NexusInterfaceTypeConfig<any>, path: string[], visited: Record<string, boolean>) {
|
||||||
|
if (alreadyChecked[obj.name]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (visited[obj.name]) {
|
||||||
|
if (obj.name === path[path.length - 1]) {
|
||||||
|
throw new Error(`GraphQL Nexus: Interface ${obj.name} can't implement itself`)
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`GraphQL Nexus: Interface circular dependency detected ${[
|
||||||
|
...path.slice(path.lastIndexOf(obj.name)),
|
||||||
|
obj.name,
|
||||||
|
].join(' -> ')}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const definitionBlock = new InterfaceDefinitionBlock({
|
||||||
|
typeName: obj.name,
|
||||||
|
addInterfaces: (i) =>
|
||||||
|
i.forEach((config) => {
|
||||||
|
const name = typeof config === 'string' ? config : config.value.name
|
||||||
|
walkType(interfaces[name], [...path, obj.name], { ...visited, [obj.name]: true })
|
||||||
|
}),
|
||||||
|
setResolveType: () => {},
|
||||||
|
addField: () => {},
|
||||||
|
addDynamicOutputMembers: () => {},
|
||||||
|
warn: () => {},
|
||||||
|
})
|
||||||
|
obj.definition(definitionBlock)
|
||||||
|
alreadyChecked[obj.name] = true
|
||||||
|
}
|
||||||
|
Object.keys(interfaces).forEach((name) => {
|
||||||
|
walkType(interfaces[name], [], {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
buildNexusTypes() {
|
buildNexusTypes() {
|
||||||
// If Query isn't defined, set it to null so it falls through to "missingType"
|
// If Query isn't defined, set it to null so it falls through to "missingType"
|
||||||
if (!this.pendingTypeMap.Query) {
|
if (!this.pendingTypeMap.Query) {
|
||||||
|
|
@ -758,6 +799,7 @@ export class SchemaBuilder {
|
||||||
this.createSchemaExtension()
|
this.createSchemaExtension()
|
||||||
this.walkTypes()
|
this.walkTypes()
|
||||||
this.beforeBuildTypes()
|
this.beforeBuildTypes()
|
||||||
|
this.checkForInterfaceCircularDependencies()
|
||||||
this.buildNexusTypes()
|
this.buildNexusTypes()
|
||||||
return {
|
return {
|
||||||
finalConfig: this.config,
|
finalConfig: this.config,
|
||||||
|
|
@ -824,19 +866,9 @@ export class SchemaBuilder {
|
||||||
}
|
}
|
||||||
const objectTypeConfig: NexusGraphQLObjectTypeConfig = {
|
const objectTypeConfig: NexusGraphQLObjectTypeConfig = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
interfaces: () => interfaces.map((i) => this.getInterface(i)),
|
interfaces: () => this.buildInterfaceList(interfaces),
|
||||||
description: config.description,
|
description: config.description,
|
||||||
fields: () => {
|
fields: () => this.buildOutputFields(fields, objectTypeConfig, this.buildInterfaceFields(interfaces)),
|
||||||
const allInterfaces = interfaces.map((i) => this.getInterface(i))
|
|
||||||
const interfaceConfigs = allInterfaces.map((i) => i.toConfig())
|
|
||||||
const interfaceFieldsMap: GraphQLFieldConfigMap<any, any> = {}
|
|
||||||
interfaceConfigs.forEach((i) => {
|
|
||||||
Object.keys(i.fields).forEach((iFieldName) => {
|
|
||||||
interfaceFieldsMap[iFieldName] = i.fields[iFieldName]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return this.buildOutputFields(fields, objectTypeConfig, interfaceFieldsMap)
|
|
||||||
},
|
|
||||||
extensions: {
|
extensions: {
|
||||||
nexus: new NexusObjectTypeExtension(config),
|
nexus: new NexusObjectTypeExtension(config),
|
||||||
},
|
},
|
||||||
|
|
@ -848,9 +880,11 @@ export class SchemaBuilder {
|
||||||
const { name, description } = config
|
const { name, description } = config
|
||||||
let resolveType: AbstractTypeResolver<string> | undefined
|
let resolveType: AbstractTypeResolver<string> | undefined
|
||||||
const fields: NexusOutputFieldDef[] = []
|
const fields: NexusOutputFieldDef[] = []
|
||||||
|
const interfaces: Implemented[] = []
|
||||||
const definitionBlock = new InterfaceDefinitionBlock({
|
const definitionBlock = new InterfaceDefinitionBlock({
|
||||||
typeName: config.name,
|
typeName: config.name,
|
||||||
addField: (field) => fields.push(field),
|
addField: (field) => fields.push(field),
|
||||||
|
addInterfaces: (interfaceDefs) => interfaces.push(...interfaceDefs),
|
||||||
setResolveType: (fn) => (resolveType = fn),
|
setResolveType: (fn) => (resolveType = fn),
|
||||||
addDynamicOutputMembers: (block, isList) => this.addDynamicOutputMembers(block, isList, 'build'),
|
addDynamicOutputMembers: (block, isList) => this.addDynamicOutputMembers(block, isList, 'build'),
|
||||||
warn: consoleWarn,
|
warn: consoleWarn,
|
||||||
|
|
@ -870,9 +904,11 @@ export class SchemaBuilder {
|
||||||
}
|
}
|
||||||
const interfaceTypeConfig: NexusGraphQLInterfaceTypeConfig = {
|
const interfaceTypeConfig: NexusGraphQLInterfaceTypeConfig = {
|
||||||
name,
|
name,
|
||||||
|
interfaces: () => this.buildInterfaceList(interfaces),
|
||||||
resolveType,
|
resolveType,
|
||||||
description,
|
description,
|
||||||
fields: () => this.buildOutputFields(fields, interfaceTypeConfig, {}),
|
fields: () =>
|
||||||
|
this.buildOutputFields(fields, interfaceTypeConfig, this.buildInterfaceFields(interfaces)),
|
||||||
extensions: {
|
extensions: {
|
||||||
nexus: new NexusInterfaceTypeExtension(config),
|
nexus: new NexusInterfaceTypeExtension(config),
|
||||||
},
|
},
|
||||||
|
|
@ -1013,6 +1049,26 @@ export class SchemaBuilder {
|
||||||
return unionMembers
|
return unionMembers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected buildInterfaceList(interfaces: (string | NexusInterfaceTypeDef<any>)[]) {
|
||||||
|
const list: GraphQLInterfaceType[] = []
|
||||||
|
interfaces.forEach((i) => {
|
||||||
|
const type = this.getInterface(i)
|
||||||
|
list.push(type, ...type.getInterfaces())
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildInterfaceFields(interfaces: (string | NexusInterfaceTypeDef<any>)[]) {
|
||||||
|
const interfaceFieldsMap: GraphQLFieldConfigMap<any, any> = {}
|
||||||
|
interfaces.forEach((i) => {
|
||||||
|
const config = this.getInterface(i).toConfig()
|
||||||
|
Object.keys(config.fields).forEach((field) => {
|
||||||
|
interfaceFieldsMap[field] = config.fields[field]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return interfaceFieldsMap
|
||||||
|
}
|
||||||
|
|
||||||
protected buildOutputFields(
|
protected buildOutputFields(
|
||||||
fields: NexusOutputFieldDef[],
|
fields: NexusOutputFieldDef[],
|
||||||
typeConfig: NexusGraphQLInterfaceTypeConfig | NexusGraphQLObjectTypeConfig,
|
typeConfig: NexusGraphQLInterfaceTypeConfig | NexusGraphQLObjectTypeConfig,
|
||||||
|
|
@ -1380,6 +1436,13 @@ export class SchemaBuilder {
|
||||||
protected walkInterfaceType(obj: NexusInterfaceTypeConfig<any>) {
|
protected walkInterfaceType(obj: NexusInterfaceTypeConfig<any>) {
|
||||||
const definitionBlock = new InterfaceDefinitionBlock({
|
const definitionBlock = new InterfaceDefinitionBlock({
|
||||||
typeName: obj.name,
|
typeName: obj.name,
|
||||||
|
addInterfaces: (i) => {
|
||||||
|
i.forEach((j) => {
|
||||||
|
if (typeof j !== 'string') {
|
||||||
|
this.addType(j)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
setResolveType: () => {},
|
setResolveType: () => {},
|
||||||
addField: (f) => this.maybeTraverseOutputFieldType(f),
|
addField: (f) => this.maybeTraverseOutputFieldType(f),
|
||||||
addDynamicOutputMembers: (block, isList) => this.addDynamicOutputMembers(block, isList, 'walk'),
|
addDynamicOutputMembers: (block, isList) => this.addDynamicOutputMembers(block, isList, 'walk'),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import { GraphQLFieldResolver } from 'graphql'
|
import { GraphQLFieldResolver } from 'graphql'
|
||||||
import {
|
import { AllInputTypes, FieldResolver, GetGen, GetGen3, HasGen3, NeedsResolver } from '../typegenTypeHelpers'
|
||||||
AbstractTypeResolver,
|
|
||||||
AllInputTypes,
|
|
||||||
FieldResolver,
|
|
||||||
GetGen,
|
|
||||||
GetGen3,
|
|
||||||
HasGen3,
|
|
||||||
NeedsResolver,
|
|
||||||
} from '../typegenTypeHelpers'
|
|
||||||
import { ArgsRecord } from './args'
|
import { ArgsRecord } from './args'
|
||||||
import { AllNexusInputTypeDefs, AllNexusOutputTypeDefs } from './wrapping'
|
import { AllNexusInputTypeDefs, AllNexusOutputTypeDefs } from './wrapping'
|
||||||
import { BaseScalars } from './_types'
|
import { BaseScalars } from './_types'
|
||||||
|
|
@ -285,7 +277,3 @@ export class InputDefinitionBlock<TypeName extends string> {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AbstractOutputDefinitionBuilder<TypeName extends string> extends OutputDefinitionBuilder {
|
|
||||||
setResolveType(fn: AbstractTypeResolver<TypeName>): void
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { assertValidName } from 'graphql'
|
import { assertValidName } from 'graphql'
|
||||||
import { AbstractTypeResolver } from '../typegenTypeHelpers'
|
import { AbstractTypeResolver, GetGen } from '../typegenTypeHelpers'
|
||||||
import { AbstractOutputDefinitionBuilder, OutputDefinitionBlock } from './definitionBlocks'
|
import { OutputDefinitionBlock, OutputDefinitionBuilder } from './definitionBlocks'
|
||||||
import { NexusTypes, NonNullConfig, RootTypingDef, withNexusSymbol } from './_types'
|
import { NexusTypes, NonNullConfig, RootTypingDef, withNexusSymbol } from './_types'
|
||||||
|
|
||||||
|
export type Implemented = GetGen<'interfaceNames'> | NexusInterfaceTypeDef<any>
|
||||||
|
|
||||||
export type NexusInterfaceTypeConfig<TypeName extends string> = {
|
export type NexusInterfaceTypeConfig<TypeName extends string> = {
|
||||||
name: TypeName
|
name: TypeName
|
||||||
|
|
||||||
|
|
@ -31,8 +33,13 @@ export type NexusInterfaceTypeConfig<TypeName extends string> = {
|
||||||
rootTyping?: RootTypingDef
|
rootTyping?: RootTypingDef
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InterfaceDefinitionBuilder<TypeName extends string> extends OutputDefinitionBuilder {
|
||||||
|
setResolveType(fn: AbstractTypeResolver<TypeName>): void
|
||||||
|
addInterfaces(toAdd: Implemented[]): void
|
||||||
|
}
|
||||||
|
|
||||||
export class InterfaceDefinitionBlock<TypeName extends string> extends OutputDefinitionBlock<TypeName> {
|
export class InterfaceDefinitionBlock<TypeName extends string> extends OutputDefinitionBlock<TypeName> {
|
||||||
constructor(protected typeBuilder: AbstractOutputDefinitionBuilder<TypeName>) {
|
constructor(protected typeBuilder: InterfaceDefinitionBuilder<TypeName>) {
|
||||||
super(typeBuilder)
|
super(typeBuilder)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,6 +48,12 @@ export class InterfaceDefinitionBlock<TypeName extends string> extends OutputDef
|
||||||
resolveType(fn: AbstractTypeResolver<TypeName>) {
|
resolveType(fn: AbstractTypeResolver<TypeName>) {
|
||||||
this.typeBuilder.setResolveType(fn)
|
this.typeBuilder.setResolveType(fn)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param interfaceName
|
||||||
|
*/
|
||||||
|
implements(...interfaceName: Array<Implemented>) {
|
||||||
|
this.typeBuilder.addInterfaces(interfaceName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NexusInterfaceTypeDef<TypeName extends string> {
|
export class NexusInterfaceTypeDef<TypeName extends string> {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { assertValidName } from 'graphql'
|
import { assertValidName } from 'graphql'
|
||||||
import { FieldResolver, GetGen } from '../typegenTypeHelpers'
|
import { FieldResolver } from '../typegenTypeHelpers'
|
||||||
import { OutputDefinitionBlock, OutputDefinitionBuilder } from './definitionBlocks'
|
import { OutputDefinitionBlock, OutputDefinitionBuilder } from './definitionBlocks'
|
||||||
import { NexusInterfaceTypeDef } from './interfaceType'
|
import { Implemented } from './interfaceType'
|
||||||
import { NexusTypes, NonNullConfig, Omit, RootTypingDef, withNexusSymbol } from './_types'
|
import { NexusTypes, NonNullConfig, Omit, RootTypingDef, withNexusSymbol } from './_types'
|
||||||
|
|
||||||
export type Implemented = GetGen<'interfaceNames'> | NexusInterfaceTypeDef<any>
|
|
||||||
|
|
||||||
export interface FieldModification<TypeName extends string, FieldName extends string> {
|
export interface FieldModification<TypeName extends string, FieldName extends string> {
|
||||||
/**
|
/**
|
||||||
* The description to annotate the GraphQL SDL
|
* The description to annotate the GraphQL SDL
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,23 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`interfaceType can extend other interfaces 1`] = `
|
||||||
|
Object {
|
||||||
|
"data": Object {
|
||||||
|
"dog": Object {
|
||||||
|
"breed": "Puli",
|
||||||
|
"classification": "Canis familiaris",
|
||||||
|
"owner": "Mark",
|
||||||
|
"type": "Animal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`interfaceType can not implement itself 1`] = `"GraphQL Nexus: Interface Node can't implement itself"`;
|
||||||
|
|
||||||
|
exports[`interfaceType detects circular dependencies 1`] = `"GraphQL Nexus: Interface circular dependency detected NodeA -> NodeC -> NodeB -> NodeA"`;
|
||||||
|
|
||||||
exports[`interfaceType logs error when resolveType is not provided for an interface 1`] = `
|
exports[`interfaceType logs error when resolveType is not provided for an interface 1`] = `
|
||||||
Array [
|
Array [
|
||||||
[Error: Missing resolveType for the Node interface. Be sure to add one in the definition block for the type, or t.resolveType(() => null) if you don't want or need to implement.],
|
[Error: Missing resolveType for the Node interface. Be sure to add one in the definition block for the type, or t.resolveType(() => null) if you don't want or need to implement.],
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,119 @@ describe('interfaceType', () => {
|
||||||
)
|
)
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
it('can extend other interfaces', async () => {
|
||||||
|
const schema = makeSchema({
|
||||||
|
types: [
|
||||||
|
interfaceType({
|
||||||
|
name: 'LivingOrganism',
|
||||||
|
definition(t) {
|
||||||
|
t.string('type')
|
||||||
|
t.resolveType(() => null)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
interfaceType({
|
||||||
|
name: 'Animal',
|
||||||
|
definition(t) {
|
||||||
|
t.implements('LivingOrganism')
|
||||||
|
t.string('classification')
|
||||||
|
t.resolveType(() => null)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
interfaceType({
|
||||||
|
name: 'Pet',
|
||||||
|
definition(t) {
|
||||||
|
t.implements('Animal')
|
||||||
|
t.string('owner')
|
||||||
|
t.resolveType(() => null)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
objectType({
|
||||||
|
name: 'Dog',
|
||||||
|
definition(t) {
|
||||||
|
t.implements('Pet')
|
||||||
|
t.string('breed')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
queryField('dog', {
|
||||||
|
type: 'Dog',
|
||||||
|
resolve: () => ({
|
||||||
|
type: 'Animal',
|
||||||
|
classification: 'Canis familiaris',
|
||||||
|
owner: 'Mark',
|
||||||
|
breed: 'Puli',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
outputs: {
|
||||||
|
schema: path.join(__dirname, 'interfaceTypeTest.graphql'),
|
||||||
|
typegen: false,
|
||||||
|
},
|
||||||
|
shouldGenerateArtifacts: false,
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
await graphql(
|
||||||
|
schema,
|
||||||
|
`
|
||||||
|
{
|
||||||
|
dog {
|
||||||
|
type
|
||||||
|
classification
|
||||||
|
owner
|
||||||
|
breed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
it('can not implement itself', async () => {
|
||||||
|
expect(() =>
|
||||||
|
makeSchema({
|
||||||
|
types: [
|
||||||
|
interfaceType({
|
||||||
|
name: 'Node',
|
||||||
|
definition(t) {
|
||||||
|
t.id('id')
|
||||||
|
t.implements('Node')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
outputs: false,
|
||||||
|
shouldGenerateArtifacts: false,
|
||||||
|
})
|
||||||
|
).toThrowErrorMatchingSnapshot()
|
||||||
|
})
|
||||||
|
it('detects circular dependencies', async () => {
|
||||||
|
expect(() =>
|
||||||
|
makeSchema({
|
||||||
|
types: [
|
||||||
|
interfaceType({
|
||||||
|
name: 'NodeA',
|
||||||
|
definition(t) {
|
||||||
|
t.id('a')
|
||||||
|
t.implements('NodeC')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
interfaceType({
|
||||||
|
name: 'NodeB',
|
||||||
|
definition(t) {
|
||||||
|
t.id('b')
|
||||||
|
t.implements('NodeA')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
interfaceType({
|
||||||
|
name: 'NodeC',
|
||||||
|
definition(t) {
|
||||||
|
t.id('c')
|
||||||
|
t.implements('NodeB')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
outputs: false,
|
||||||
|
shouldGenerateArtifacts: false,
|
||||||
|
})
|
||||||
|
).toThrowErrorMatchingSnapshot()
|
||||||
|
})
|
||||||
it('logs error when resolveType is not provided for an interface', async () => {
|
it('logs error when resolveType is not provided for an interface', async () => {
|
||||||
const spy = jest.spyOn(console, 'error').mockImplementation()
|
const spy = jest.spyOn(console, 'error').mockImplementation()
|
||||||
makeSchema({
|
makeSchema({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue