nexus/tests/plugin.spec.ts

439 lines
11 KiB
TypeScript

import { buildSchema, graphql, GraphQLSchema, lexicographicSortSchema, printSchema } from 'graphql'
import {
interfaceType,
list,
makeSchema,
MiddlewareFn,
nonNull,
objectType,
plugin,
queryField,
booleanArg,
inputObjectType,
} from '../src/core'
import { nullabilityGuardPlugin } from '../src/plugins'
import { EXAMPLE_SDL } from './_sdl'
const nullGuardPlugin = nullabilityGuardPlugin({
shouldGuard: true,
fallbackValues: {
ID: () => 'UNKNOWN_ID',
Float: () => 0,
Boolean: () => false,
String: () => '',
UUID: () => '63bcbf07-c5e7-4c89-ad07-74030a09f5f6',
},
onGuarded() {},
})
describe('plugin', () => {
it('is applied to the resolver for every field in the schema', async () => {
const lifecycleCalls: string[] = []
const beforeCalls: string[] = []
const afterCalls: string[] = []
const schema = makeSchema({
outputs: false,
types: [buildSchema(EXAMPLE_SDL)],
plugins: [
plugin({
name: 'Lifecycle test',
onBeforeBuild: () => lifecycleCalls.push('onBeforeBuild'),
onInstall: () => {
lifecycleCalls.push('onInstall')
},
onCreateFieldResolver({ fieldConfig }) {
return async (root, args, ctx, info, next) => {
beforeCalls.push(`${info.parentType.name}:${info.fieldName}`)
const val = await next(root, args, ctx, info)
afterCalls.push(`${info.parentType.name}:${info.fieldName} ${val}`)
return val
}
},
onAfterBuild: (schema) => {
lifecycleCalls.push('onAfterBuild')
expect(schema).toBeInstanceOf(GraphQLSchema)
},
}),
nullGuardPlugin,
],
features: {
abstractTypeStrategies: {
__typename: true,
},
},
})
const result = await graphql(
schema,
`
{
user {
id
name
email
phone
}
posts(filters: { order: DESC }) {
id
uuid
author {
id
}
}
}
`
)
expect(lifecycleCalls).toMatchSnapshot()
expect(beforeCalls).toMatchSnapshot()
expect(afterCalls).toMatchSnapshot()
expect(result).toMatchSnapshot()
})
it('throws when the plugin is included in the types but not the plugins array', () => {
expect.assertions(1)
try {
makeSchema({
outputs: false,
types: [buildSchema(EXAMPLE_SDL), nullGuardPlugin],
})
} catch (e) {
expect(e.message).toEqual(
`Nexus plugin NullabilityGuard was seen in the "types" config, but should instead be provided to the "plugins" array.`
)
}
})
it('does not throw if the plugin is seen in the types as well as the plugins array', () => {
makeSchema({
outputs: false,
types: [buildSchema(EXAMPLE_SDL), nullGuardPlugin],
plugins: [nullGuardPlugin],
features: {
abstractTypeStrategies: {
__typename: true,
},
},
})
})
it('has an onMissingType, which will be called in order when we encounter a missing type', () => {
const schema = makeSchema({
outputs: false,
types: [
objectType({
name: 'User',
definition(t) {
t.id('id')
},
}),
queryField('users', {
type: 'UserConnection',
resolve: () => [],
}),
],
plugins: [
plugin({
name: 'DynamicConnection',
onMissingType(name, builder) {
if (name === 'ConnectionInfo') {
return objectType({
name,
definition(t) {
t.boolean('hasNextPage')
t.boolean('hasPrevPage')
},
})
}
const exec = /^(.*?)(Connection|Edge|Node)$/.exec(name)
if (!exec) {
return null
}
const [, typeName, fieldType] = exec
switch (fieldType) {
case 'Edge': {
return objectType({
name: name,
definition(t) {
t.string('cursor')
t.field('node', { type: typeName })
},
})
}
case 'Connection': {
return objectType({
name: name,
definition(t) {
t.field('edges', {
type: list(`${typeName}Edge`),
})
t.field('connectionInfo', {
type: 'ConnectionInfo',
})
},
})
}
}
},
}),
],
})
expect(printSchema(lexicographicSortSchema(schema))).toMatchSnapshot()
})
it('composes the onCreateFieldResolve fns', async () => {
const calls: string[] = []
const testResolve = (name: string) => (): MiddlewareFn => (root, args, ctx, info, next) => {
calls.push(`Before:${name}`)
return plugin.completeValue(next(root, args, ctx, info), (val) => {
calls.push(`After:${name} ${val}`)
return val + 1
})
}
const schema = makeSchema({
outputs: false,
types: [
queryField('testCompose', {
type: 'Int',
resolve: () => {
calls.push('calls:resolver')
return 1
},
}),
],
plugins: [
plugin({
name: 'a',
onCreateFieldResolver: testResolve('a'),
}),
plugin({
name: 'b',
onCreateFieldResolver: testResolve('b'),
}),
plugin({
name: 'c',
onCreateFieldResolver: testResolve('c'),
}),
],
})
await graphql(
schema,
`
{
testCompose
}
`
)
expect(calls).toMatchSnapshot()
})
it('has an onObjectDefinition option, which receives the object metadata', async () => {
//
const schema = makeSchema({
outputs: false,
features: {
abstractTypeStrategies: {
__typename: true,
},
},
types: [
interfaceType({
name: 'Node',
resolveType(n) {
return n.__typename
},
definition(t) {
t.field('id', {
type: nonNull('ID'),
resolve: () => {
throw new Error('Abstract')
},
})
},
}),
objectType({
name: 'AddsNode',
// @ts-ignore
node: 'id',
definition(t) {
t.string('name')
},
}),
queryField('getNode', {
type: 'Node',
resolve: () => ({ __typename: 'AddsNode', name: 'test', id: 'abc' }),
}),
],
plugins: [
plugin({
name: 'Node',
onObjectDefinition(t, config) {
const node = (config as any).node as any
if (node) {
t.implements('Node')
t.field('id', {
type: nonNull('ID'),
resolve: (root) => `${config.name}:${root[node]}`,
})
}
},
}),
],
})
const result = await graphql(
schema,
`
{
getNode {
__typename
id
... on AddsNode {
name
}
}
}
`
)
expect(result.data?.getNode).toEqual({ __typename: 'AddsNode', id: 'AddsNode:abc', name: 'test' })
})
it('has an onAddOutputField / onAddInputField / onAddArg option, which receives the field metadata, and can modify the field', async () => {
//
const schema = makeSchema({
outputs: false,
features: {
abstractTypeStrategies: {
__typename: true,
},
},
types: [
queryField('ok', {
type: 'Boolean',
// @ts-ignore
listTest: true,
args: {
// @ts-ignore
filter: booleanArg({ listTest: true }),
input: inputObjectType({
name: 'SomeType',
definition(t) {
// @ts-ignore
t.boolean('inputField', { listTest: true })
},
}),
},
resolve: () => [true],
}),
],
plugins: [
plugin({
name: 'Node',
onAddOutputField(field) {
// @ts-ignore
if (field.listTest) {
field.type = list(field.type)
}
},
onAddInputField(field) {
// @ts-ignore
if (field.listTest) {
field.type = list(field.type)
}
},
onAddArg(arg) {
// @ts-ignore
if (arg.listTest) {
arg.type = list(arg.type)
}
},
}),
],
})
expect(printSchema(lexicographicSortSchema(schema))).toMatchSnapshot()
})
it('has an plugin.inputObjectTypeDefTypes field where extra properties for inputObjectType can be added', async () => {
const spy = jest.spyOn(console, 'error').mockImplementation()
makeSchema({
outputs: false,
features: {
abstractTypeStrategies: {
__typename: true,
},
},
types: [
queryField('ok', {
type: 'Boolean',
// @ts-ignore
listTest: true,
args: {
input: inputObjectType({
name: 'SomeType',
definition(t) {
// @ts-ignore
t.boolean('inputField', { listTest: true })
},
// @ts-ignore
extraData: true,
}),
},
resolve: () => [true],
}),
],
plugins: [
plugin({
name: 'Node',
inputObjectTypeDefTypes: 'extraData?: bool',
onAddArg: (field) => {
expect(field.type.config.extraData).toBe(true)
},
}),
],
})
expect(spy).toBeCalledTimes(0)
})
it('has a plugin.completeValue fn which is used to efficiently complete a value which is possibly a promise', async () => {
const calls: string[] = []
const testResolve = (name: string) => (): MiddlewareFn => async (root, args, ctx, info, next) => {
calls.push(`Before:${name}`)
return plugin.completeValue(next(root, args, ctx, info), (val) => {
calls.push(`After:${name} ${val}`)
return val + 1
})
}
const schema = makeSchema({
outputs: false,
types: [
queryField('testCompose', {
type: 'Int',
resolve: async () => {
calls.push('calls:resolver')
return 1
},
}),
],
plugins: [
plugin({
name: 'a',
onCreateFieldResolver: testResolve('a'),
}),
plugin({
name: 'b',
onCreateFieldResolver: testResolve('b'),
}),
plugin({
name: 'c',
onCreateFieldResolver: testResolve('c'),
}),
],
})
await graphql(
schema,
`
{
testCompose
}
`
)
expect(calls).toMatchSnapshot()
})
})