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 { NexusInputObjectTypeConfig } from './definitions/inputObjectType'
|
||||
import {
|
||||
Implemented,
|
||||
InterfaceDefinitionBlock,
|
||||
NexusInterfaceTypeConfig,
|
||||
NexusInterfaceTypeDef,
|
||||
} from './definitions/interfaceType'
|
||||
import {
|
||||
Implemented,
|
||||
NexusObjectTypeConfig,
|
||||
NexusObjectTypeDef,
|
||||
ObjectDefinitionBlock,
|
||||
} from './definitions/objectType'
|
||||
import { NexusObjectTypeConfig, NexusObjectTypeDef, ObjectDefinitionBlock } from './definitions/objectType'
|
||||
import { NexusScalarExtensions, NexusScalarTypeConfig } from './definitions/scalarType'
|
||||
import { NexusUnionTypeConfig, UnionDefinitionBlock, UnionMembers } from './definitions/unionType'
|
||||
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() {
|
||||
// If Query isn't defined, set it to null so it falls through to "missingType"
|
||||
if (!this.pendingTypeMap.Query) {
|
||||
|
|
@ -758,6 +799,7 @@ export class SchemaBuilder {
|
|||
this.createSchemaExtension()
|
||||
this.walkTypes()
|
||||
this.beforeBuildTypes()
|
||||
this.checkForInterfaceCircularDependencies()
|
||||
this.buildNexusTypes()
|
||||
return {
|
||||
finalConfig: this.config,
|
||||
|
|
@ -824,19 +866,9 @@ export class SchemaBuilder {
|
|||
}
|
||||
const objectTypeConfig: NexusGraphQLObjectTypeConfig = {
|
||||
name: config.name,
|
||||
interfaces: () => interfaces.map((i) => this.getInterface(i)),
|
||||
interfaces: () => this.buildInterfaceList(interfaces),
|
||||
description: config.description,
|
||||
fields: () => {
|
||||
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)
|
||||
},
|
||||
fields: () => this.buildOutputFields(fields, objectTypeConfig, this.buildInterfaceFields(interfaces)),
|
||||
extensions: {
|
||||
nexus: new NexusObjectTypeExtension(config),
|
||||
},
|
||||
|
|
@ -848,9 +880,11 @@ export class SchemaBuilder {
|
|||
const { name, description } = config
|
||||
let resolveType: AbstractTypeResolver<string> | undefined
|
||||
const fields: NexusOutputFieldDef[] = []
|
||||
const interfaces: Implemented[] = []
|
||||
const definitionBlock = new InterfaceDefinitionBlock({
|
||||
typeName: config.name,
|
||||
addField: (field) => fields.push(field),
|
||||
addInterfaces: (interfaceDefs) => interfaces.push(...interfaceDefs),
|
||||
setResolveType: (fn) => (resolveType = fn),
|
||||
addDynamicOutputMembers: (block, isList) => this.addDynamicOutputMembers(block, isList, 'build'),
|
||||
warn: consoleWarn,
|
||||
|
|
@ -870,9 +904,11 @@ export class SchemaBuilder {
|
|||
}
|
||||
const interfaceTypeConfig: NexusGraphQLInterfaceTypeConfig = {
|
||||
name,
|
||||
interfaces: () => this.buildInterfaceList(interfaces),
|
||||
resolveType,
|
||||
description,
|
||||
fields: () => this.buildOutputFields(fields, interfaceTypeConfig, {}),
|
||||
fields: () =>
|
||||
this.buildOutputFields(fields, interfaceTypeConfig, this.buildInterfaceFields(interfaces)),
|
||||
extensions: {
|
||||
nexus: new NexusInterfaceTypeExtension(config),
|
||||
},
|
||||
|
|
@ -1013,6 +1049,26 @@ export class SchemaBuilder {
|
|||
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(
|
||||
fields: NexusOutputFieldDef[],
|
||||
typeConfig: NexusGraphQLInterfaceTypeConfig | NexusGraphQLObjectTypeConfig,
|
||||
|
|
@ -1380,6 +1436,13 @@ export class SchemaBuilder {
|
|||
protected walkInterfaceType(obj: NexusInterfaceTypeConfig<any>) {
|
||||
const definitionBlock = new InterfaceDefinitionBlock({
|
||||
typeName: obj.name,
|
||||
addInterfaces: (i) => {
|
||||
i.forEach((j) => {
|
||||
if (typeof j !== 'string') {
|
||||
this.addType(j)
|
||||
}
|
||||
})
|
||||
},
|
||||
setResolveType: () => {},
|
||||
addField: (f) => this.maybeTraverseOutputFieldType(f),
|
||||
addDynamicOutputMembers: (block, isList) => this.addDynamicOutputMembers(block, isList, 'walk'),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
import { GraphQLFieldResolver } from 'graphql'
|
||||
import {
|
||||
AbstractTypeResolver,
|
||||
AllInputTypes,
|
||||
FieldResolver,
|
||||
GetGen,
|
||||
GetGen3,
|
||||
HasGen3,
|
||||
NeedsResolver,
|
||||
} from '../typegenTypeHelpers'
|
||||
import { AllInputTypes, FieldResolver, GetGen, GetGen3, HasGen3, NeedsResolver } from '../typegenTypeHelpers'
|
||||
import { ArgsRecord } from './args'
|
||||
import { AllNexusInputTypeDefs, AllNexusOutputTypeDefs } from './wrapping'
|
||||
import { BaseScalars } from './_types'
|
||||
|
|
@ -285,7 +277,3 @@ export class InputDefinitionBlock<TypeName extends string> {
|
|||
return config
|
||||
}
|
||||
}
|
||||
|
||||
export interface AbstractOutputDefinitionBuilder<TypeName extends string> extends OutputDefinitionBuilder {
|
||||
setResolveType(fn: AbstractTypeResolver<TypeName>): void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { assertValidName } from 'graphql'
|
||||
import { AbstractTypeResolver } from '../typegenTypeHelpers'
|
||||
import { AbstractOutputDefinitionBuilder, OutputDefinitionBlock } from './definitionBlocks'
|
||||
import { AbstractTypeResolver, GetGen } from '../typegenTypeHelpers'
|
||||
import { OutputDefinitionBlock, OutputDefinitionBuilder } from './definitionBlocks'
|
||||
import { NexusTypes, NonNullConfig, RootTypingDef, withNexusSymbol } from './_types'
|
||||
|
||||
export type Implemented = GetGen<'interfaceNames'> | NexusInterfaceTypeDef<any>
|
||||
|
||||
export type NexusInterfaceTypeConfig<TypeName extends string> = {
|
||||
name: TypeName
|
||||
|
||||
|
|
@ -31,8 +33,13 @@ export type NexusInterfaceTypeConfig<TypeName extends string> = {
|
|||
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> {
|
||||
constructor(protected typeBuilder: AbstractOutputDefinitionBuilder<TypeName>) {
|
||||
constructor(protected typeBuilder: InterfaceDefinitionBuilder<TypeName>) {
|
||||
super(typeBuilder)
|
||||
}
|
||||
/**
|
||||
|
|
@ -41,6 +48,12 @@ export class InterfaceDefinitionBlock<TypeName extends string> extends OutputDef
|
|||
resolveType(fn: AbstractTypeResolver<TypeName>) {
|
||||
this.typeBuilder.setResolveType(fn)
|
||||
}
|
||||
/**
|
||||
* @param interfaceName
|
||||
*/
|
||||
implements(...interfaceName: Array<Implemented>) {
|
||||
this.typeBuilder.addInterfaces(interfaceName)
|
||||
}
|
||||
}
|
||||
|
||||
export class NexusInterfaceTypeDef<TypeName extends string> {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { assertValidName } from 'graphql'
|
||||
import { FieldResolver, GetGen } from '../typegenTypeHelpers'
|
||||
import { FieldResolver } from '../typegenTypeHelpers'
|
||||
import { OutputDefinitionBlock, OutputDefinitionBuilder } from './definitionBlocks'
|
||||
import { NexusInterfaceTypeDef } from './interfaceType'
|
||||
import { Implemented } from './interfaceType'
|
||||
import { NexusTypes, NonNullConfig, Omit, RootTypingDef, withNexusSymbol } from './_types'
|
||||
|
||||
export type Implemented = GetGen<'interfaceNames'> | NexusInterfaceTypeDef<any>
|
||||
|
||||
export interface FieldModification<TypeName extends string, FieldName extends string> {
|
||||
/**
|
||||
* 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`] = `
|
||||
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.],
|
||||
|
|
|
|||
|
|
@ -45,6 +45,119 @@ describe('interfaceType', () => {
|
|||
)
|
||||
).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 () => {
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation()
|
||||
makeSchema({
|
||||
|
|
|
|||
Loading…
Reference in New Issue