485 lines
11 KiB
TypeScript
485 lines
11 KiB
TypeScript
import { graphql, GraphQLID, GraphQLNonNull, GraphQLObjectType } from 'graphql'
|
|
import {
|
|
enumType,
|
|
interfaceType,
|
|
list,
|
|
makeSchema,
|
|
nullabilityGuardPluginCore,
|
|
nullabilityGuardPlugin,
|
|
objectType,
|
|
queryField,
|
|
unionType,
|
|
} from '../../src'
|
|
|
|
type NullabilityGuardConfig = nullabilityGuardPluginCore.NullabilityGuardConfig
|
|
|
|
const NODE_ENV = process.env.NODE_ENV
|
|
|
|
const onGuardedMock = jest.fn()
|
|
|
|
const defaultFallbacks: NullabilityGuardConfig['fallbackValues'] = {
|
|
ID: ({ info }) => `${info.parentType.name}:N/A`,
|
|
Int: () => -1,
|
|
String: () => '',
|
|
Boolean: () => false,
|
|
Float: () => null as any, // intended to fail
|
|
}
|
|
|
|
const nullPlugin = (config: NullabilityGuardConfig = {}) =>
|
|
nullabilityGuardPlugin({
|
|
onGuarded: onGuardedMock,
|
|
fallbackValues: {
|
|
...defaultFallbacks,
|
|
},
|
|
shouldGuard: true,
|
|
...config,
|
|
})
|
|
|
|
const types = [
|
|
new GraphQLObjectType({
|
|
name: 'SomeObjectType',
|
|
description: 'Showing that the defaults works for all resolvers, not just Nexus ones',
|
|
fields: () => ({
|
|
id: {
|
|
type: new GraphQLNonNull(GraphQLID),
|
|
},
|
|
}),
|
|
}),
|
|
interfaceType({
|
|
name: 'UserLike',
|
|
resolveType(o) {
|
|
return o.__typename
|
|
},
|
|
definition(t) {
|
|
t.id('id')
|
|
t.string('login')
|
|
},
|
|
}),
|
|
objectType({
|
|
name: 'User',
|
|
definition(t) {
|
|
t.implements('UserLike')
|
|
t.string('login')
|
|
t.list.field('usersList', {
|
|
type: 'User',
|
|
// @ts-ignore
|
|
skipNullGuard: true,
|
|
resolve: () => {
|
|
return [{ id: `A:1` }, { id: `B:2` }]
|
|
},
|
|
})
|
|
},
|
|
sourceType: '{ id: string }',
|
|
}),
|
|
objectType({
|
|
name: 'Account',
|
|
definition(t) {
|
|
t.id('id')
|
|
t.string('displayName')
|
|
},
|
|
sourceType: '{ id: string }',
|
|
}),
|
|
queryField('objType', {
|
|
type: 'SomeObjectType' as any,
|
|
resolve: () => ({} as any),
|
|
}),
|
|
queryField('getUser', {
|
|
type: 'User',
|
|
resolve: () => ({
|
|
id: 'User: 1',
|
|
}),
|
|
}),
|
|
queryField('getUserWithGuard', {
|
|
type: 'User',
|
|
resolve: () => ({
|
|
id: null as any,
|
|
}),
|
|
}),
|
|
queryField('intList', {
|
|
type: list('Int'),
|
|
resolve: () => [1, 2, null],
|
|
}),
|
|
queryField('userList', {
|
|
type: list('User'),
|
|
resolve: () => [null, Promise.resolve(null), null],
|
|
}),
|
|
queryField('interfaceType', {
|
|
type: 'UserLike',
|
|
resolve: () => null,
|
|
}),
|
|
queryField('enumType', {
|
|
type: 'SomeEnum',
|
|
resolve: () => null,
|
|
}),
|
|
queryField('shouldFail', {
|
|
type: 'Float',
|
|
resolve: () => null,
|
|
}),
|
|
enumType({
|
|
name: 'SomeEnum',
|
|
members: ['A', 'B', 'C'],
|
|
}),
|
|
]
|
|
|
|
const defaultSchema = makeSchema({
|
|
types,
|
|
plugins: [nullPlugin()],
|
|
outputs: false,
|
|
nonNullDefaults: {
|
|
output: true,
|
|
},
|
|
features: {
|
|
abstractTypeStrategies: {
|
|
resolveType: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
describe('nullabilityGuardPlugin', () => {
|
|
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
beforeEach(() => {
|
|
jest.resetAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
process.env.NODE_ENV = NODE_ENV
|
|
})
|
|
|
|
it('should trigger the nullability guard', async () => {
|
|
const { errors = [], data } = await graphql({
|
|
schema: defaultSchema,
|
|
source: `
|
|
{
|
|
getUserWithGuard {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toEqual([])
|
|
expect(data!.getUserWithGuard).toEqual({ id: 'User:N/A' })
|
|
expect(onGuardedMock).toBeCalledTimes(1)
|
|
})
|
|
|
|
it('should fill ints with a default', async () => {
|
|
const { errors = [], data } = await graphql({
|
|
schema: defaultSchema,
|
|
source: `
|
|
{
|
|
intList
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toEqual([])
|
|
expect(data!.intList).toEqual([1, 2, -1])
|
|
expect(onGuardedMock).toBeCalledTimes(1)
|
|
})
|
|
|
|
it('should fill with defaults', async () => {
|
|
const { errors = [], data } = await graphql({
|
|
schema: defaultSchema,
|
|
source: `
|
|
{
|
|
userList {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toEqual([])
|
|
expect(data!.userList).toEqual([{ id: 'User:N/A' }, { id: 'User:N/A' }, { id: 'User:N/A' }])
|
|
// Once for each null, once for each "id" field
|
|
expect(onGuardedMock).toBeCalledTimes(6)
|
|
})
|
|
|
|
it('should guard on GraphQLObjectType fields', async () => {
|
|
const { errors = [], data } = await graphql({
|
|
schema: defaultSchema,
|
|
source: `
|
|
{
|
|
objType {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toEqual([])
|
|
expect(data!.objType).toEqual({ id: 'SomeObjectType:N/A' })
|
|
expect(onGuardedMock).toBeCalledTimes(1)
|
|
})
|
|
|
|
it('should guard interface types', async () => {
|
|
const { errors = [], data } = await graphql({
|
|
schema: defaultSchema,
|
|
source: `
|
|
{
|
|
interfaceType {
|
|
__typename
|
|
id
|
|
login
|
|
... on User {
|
|
usersList {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toEqual([])
|
|
expect(data!.interfaceType).toEqual({
|
|
__typename: 'User',
|
|
id: 'User:N/A',
|
|
login: '',
|
|
usersList: [{ id: 'A:1' }, { id: 'B:2' }],
|
|
})
|
|
expect(onGuardedMock).toBeCalledTimes(3)
|
|
})
|
|
|
|
it('should guard union types', async () => {
|
|
const { errors = [], data } = await graphql({
|
|
schema: makeSchema({
|
|
outputs: false,
|
|
nonNullDefaults: {
|
|
output: true,
|
|
},
|
|
types: [
|
|
types,
|
|
unionType({
|
|
name: 'UserOrAccount',
|
|
definition(t) {
|
|
t.members('User', 'Account')
|
|
},
|
|
}),
|
|
queryField('unionType', {
|
|
type: 'UserOrAccount',
|
|
resolve: () => null,
|
|
}),
|
|
],
|
|
plugins: [nullPlugin()],
|
|
features: {
|
|
abstractTypeStrategies: {
|
|
resolveType: true,
|
|
},
|
|
},
|
|
}),
|
|
source: `
|
|
{
|
|
unionType {
|
|
__typename
|
|
... on User {
|
|
id
|
|
login
|
|
}
|
|
... on Account {
|
|
id
|
|
displayName
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toEqual([])
|
|
expect(data!.unionType).toEqual({
|
|
__typename: 'User',
|
|
id: 'User:N/A',
|
|
login: '',
|
|
})
|
|
expect(onGuardedMock).toBeCalledTimes(3)
|
|
expect(errSpy).toHaveBeenCalledTimes(1)
|
|
expect(errSpy.mock.calls[0][0].message).toContain(
|
|
'You have a faulty implementation for your union type "UserOrAccount". It is missing a `resolveType` implementation.'
|
|
)
|
|
})
|
|
|
|
it('should guard on enumType fields', async () => {
|
|
const { errors = [], data } = await graphql({
|
|
schema: defaultSchema,
|
|
source: `
|
|
{
|
|
enumType
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toEqual([])
|
|
expect(data!.enumType).toEqual('A')
|
|
expect(onGuardedMock).toBeCalledTimes(1)
|
|
})
|
|
|
|
it('should warn by default if onGuarded is not provided', async () => {
|
|
const warnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {})
|
|
const schema = makeSchema({
|
|
outputs: false,
|
|
nonNullDefaults: {
|
|
output: true,
|
|
},
|
|
types,
|
|
plugins: [nullPlugin({ onGuarded: undefined })],
|
|
features: {
|
|
abstractTypeStrategies: {
|
|
resolveType: true,
|
|
},
|
|
},
|
|
})
|
|
const { errors = [], data } = await graphql({
|
|
schema,
|
|
source: `
|
|
{
|
|
getUserWithGuard {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toEqual([])
|
|
expect(data!.getUserWithGuard).toEqual({ id: 'User:N/A' })
|
|
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
expect(warnSpy).toHaveBeenCalledWith('Nullability guard called for User.id')
|
|
})
|
|
|
|
it('should not catch by default unless the env is production', async () => {
|
|
const schema = makeSchema({
|
|
types,
|
|
outputs: false,
|
|
nonNullDefaults: {
|
|
output: true,
|
|
},
|
|
plugins: [nullPlugin({ shouldGuard: undefined })],
|
|
features: {
|
|
abstractTypeStrategies: {
|
|
resolveType: true,
|
|
},
|
|
},
|
|
})
|
|
const { errors = [], data } = await graphql({
|
|
schema,
|
|
source: `
|
|
{
|
|
getUserWithGuard {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toHaveLength(1)
|
|
expect(errors[0].message).toEqual('Cannot return null for non-nullable field User.id.')
|
|
expect(data).toBeNull()
|
|
process.env.NODE_ENV = 'production'
|
|
const schema2 = makeSchema({
|
|
types,
|
|
outputs: false,
|
|
nonNullDefaults: {
|
|
output: true,
|
|
},
|
|
plugins: [nullPlugin({ shouldGuard: undefined })],
|
|
features: {
|
|
abstractTypeStrategies: {
|
|
resolveType: true,
|
|
},
|
|
},
|
|
})
|
|
const { errors: errors2 = [], data: data2 } = await graphql({
|
|
schema: schema2,
|
|
source: `
|
|
{
|
|
getUserWithGuard {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
expect(errors2).toEqual([])
|
|
expect(data2!.getUserWithGuard).toEqual({ id: 'User:N/A' })
|
|
})
|
|
|
|
it('logs an error if scalars are missing', () => {
|
|
const { String, ...rest } = defaultFallbacks
|
|
makeSchema({
|
|
types,
|
|
outputs: false,
|
|
plugins: [
|
|
nullPlugin({
|
|
fallbackValues: {
|
|
...rest,
|
|
},
|
|
}),
|
|
],
|
|
features: {
|
|
abstractTypeStrategies: {
|
|
resolveType: true,
|
|
},
|
|
},
|
|
})
|
|
expect(errSpy).toHaveBeenCalledTimes(1)
|
|
expect(errSpy).toHaveBeenCalledWith(
|
|
'No nullability guard was provided for Scalar String. Provide one in the nullabilityGuard config to remove this warning.'
|
|
)
|
|
})
|
|
|
|
it('logs an error for unknown/unused scalars', () => {
|
|
makeSchema({
|
|
types,
|
|
outputs: false,
|
|
plugins: [
|
|
nullPlugin({
|
|
fallbackValues: {
|
|
...defaultFallbacks,
|
|
JSON: () => ({}),
|
|
},
|
|
}),
|
|
],
|
|
features: {
|
|
abstractTypeStrategies: {
|
|
resolveType: true,
|
|
},
|
|
},
|
|
})
|
|
expect(errSpy).toHaveBeenCalledTimes(1)
|
|
expect(errSpy).toHaveBeenCalledWith(
|
|
'Unknown type JSON provided in nullabilityGuard fallbackValues config.'
|
|
)
|
|
})
|
|
|
|
it('will still fail if it cant handle with a guard', async () => {
|
|
const { errors = [] } = await graphql({
|
|
schema: defaultSchema,
|
|
source: `
|
|
{
|
|
shouldFail
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toHaveLength(1)
|
|
expect(errors[0].message).toEqual('Cannot return null for non-nullable field Query.shouldFail.')
|
|
})
|
|
|
|
it('will return null for nullable list values', async () => {
|
|
const onGuardedMock = jest.fn()
|
|
const { errors = [], data } = await graphql({
|
|
schema: makeSchema({
|
|
types: [
|
|
queryField('nullableList', {
|
|
type: list('String'),
|
|
resolve: async () => null,
|
|
}),
|
|
],
|
|
outputs: false,
|
|
plugins: [
|
|
nullPlugin({
|
|
onGuarded: onGuardedMock,
|
|
}),
|
|
],
|
|
}),
|
|
source: `
|
|
{
|
|
nullableList
|
|
}
|
|
`,
|
|
})
|
|
expect(errors).toHaveLength(0)
|
|
expect(data!.nullableList).toEqual(null)
|
|
expect(onGuardedMock).toHaveBeenCalledTimes(0)
|
|
})
|
|
})
|