439 lines
11 KiB
TypeScript
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()
|
|
})
|
|
})
|