nexus
Version:
Scalable, strongly typed GraphQL schema development
216 lines (204 loc) • 6.96 kB
text/typescript
import {
GraphQLNullableType,
GraphQLOutputType,
GraphQLResolveInfo,
isEnumType,
isInterfaceType,
isListType,
isNonNullType,
isObjectType,
isScalarType,
isUnionType,
isWrappingType,
} from 'graphql'
import { forEach } from 'iterall'
import type { GraphQLPossibleOutputs } from '../definitions/_types'
import type { NexusGraphQLNamedType } from '../extensions'
import { CreateFieldResolverInfo, plugin } from '../plugin'
import type { AllOutputTypes, GetGen, GetGen2 } from '../typegenTypeHelpers'
import { isPromiseLike, printedGenTyping } from '../utils'
export interface NullabilityPluginFallbackFn {
ctx: GetGen<'context'>
info: GraphQLResolveInfo
type: GraphQLPossibleOutputs
}
export interface NullabilityPluginOnGuardedConfig {
fallback: any
ctx: GetGen<'context'>
info: GraphQLResolveInfo
type: GraphQLNullableType
}
export type NullFallbackValues = Partial<{
[K in AllOutputTypes]: (obj: NullabilityPluginFallbackFn) => GetGen2<'rootTypes', K>
}>
export type NullabilityGuardConfig = {
/** Whether we should guard against non-null values. Defaults to "true" if NODE_ENV === "production", false otherwise. */
shouldGuard?: boolean
/**
* When a nullish value is "guarded", meaning it is coerced into an acceptable non-null value, this function
* will be called if supplied.
*/
onGuarded?: (obj: NullabilityPluginOnGuardedConfig) => void
/** A mapping of typename to the value that should be used in the case of a null value. */
fallbackValues?: NullFallbackValues
}
const fieldDefTypes = printedGenTyping({
name: 'skipNullGuard',
optional: true,
type: 'boolean',
description: `
The nullability guard can be helpful, but is also a potentially expensive operation for lists.
We need to iterate the entire list to check for null items to guard against. Set this to true
to skip the null guard on a specific field if you know there's no potential for unsafe types.
`,
})
export const nullabilityGuardPlugin = (pluginConfig: NullabilityGuardConfig) => {
const {
shouldGuard = process.env.NODE_ENV === 'production',
fallbackValues = {},
onGuarded = (obj: NullabilityPluginOnGuardedConfig) => {
console.warn(`Nullability guard called for ${obj.info.parentType.name}.${obj.info.fieldName}`)
},
} = pluginConfig
const finalPluginConfig: Required<NullabilityGuardConfig> = {
shouldGuard,
onGuarded,
fallbackValues,
}
return plugin({
name: 'NullabilityGuard',
description: 'If we have a nullable field, we want to guard against this being an issue in production.',
fieldDefTypes,
onCreateFieldResolver(config: CreateFieldResolverInfo<{ skipNullGuard: boolean }>) {
if (config.fieldConfig.extensions?.nexus?.config.skipNullGuard) {
return
}
const { type } = config.fieldConfig
const { outerNonNull, hasListNonNull } = nonNullInfo(type)
if (outerNonNull || hasListNonNull) {
return (root, args, ctx, info, next) => {
return plugin.completeValue(
next(root, args, ctx, info),
nonNullGuard(
ctx,
info,
isNonNullType(type) ? type.ofType : type,
config,
finalPluginConfig,
outerNonNull
)
)
}
}
},
onAfterBuild(schema) {
Object.keys(schema.getTypeMap()).forEach((typeName) => {
const type = schema.getType(typeName) as NexusGraphQLNamedType
if (isScalarType(type)) {
if (fallbackValues[type.name as keyof typeof fallbackValues]) {
return
}
console.error(
`No nullability guard was provided for Scalar ${type.name}. ` +
`Provide one in the nullabilityGuard config to remove this warning.`
)
}
})
if (pluginConfig.fallbackValues) {
Object.keys(pluginConfig.fallbackValues).forEach((name) => {
const type = schema.getType(name) as NexusGraphQLNamedType
if (!type) {
return console.error(`Unknown type ${name} provided in nullabilityGuard fallbackValues config.`)
}
})
}
},
})
}
const isNullish = (val: any): boolean => val === null || val === undefined || val !== val
const nonNullGuard = (
ctx: GetGen<'context'>,
info: GraphQLResolveInfo,
type: GraphQLOutputType,
config: CreateFieldResolverInfo,
pluginConfig: Required<NullabilityGuardConfig>,
outerNonNull: boolean
) => {
const { onGuarded, fallbackValues, shouldGuard } = pluginConfig
const guardResult = (fallback: any) => {
onGuarded({ ctx, info, type, fallback })
return shouldGuard ? fallback : null
}
return (val: any) => {
// If it's a list type, return [] if the value is null,
// otherwise recurse into resolving the individual type.
if (isListType(type)) {
if (isNullish(val)) {
return outerNonNull ? guardResult([]) : null
}
let hasPromise = false
const listMembers: any[] = []
const listCompleter = nonNullGuard(
ctx,
info,
isNonNullType(type.ofType) ? type.ofType.ofType : type.ofType,
config,
pluginConfig,
isNonNullType(type.ofType)
)
forEach(val as any, (item) => {
if (!hasPromise && isPromiseLike(item)) {
hasPromise = true
}
listMembers.push(plugin.completeValue(item, listCompleter))
})
return hasPromise ? Promise.all(listMembers) : listMembers
}
if (!isNullish(val) || outerNonNull === false) {
return val
}
const typeName = type.name as keyof typeof fallbackValues
const fallbackFn = fallbackValues[typeName]
const fallbackValue = typeof fallbackFn === 'function' ? fallbackFn({ type, info, ctx }) : null
if (!isNullish(fallbackValue)) {
return guardResult(fallbackValue)
}
// If it's an object, just return an empty object and let the scalar fallbacks take care of the rest
if (isObjectType(type)) {
return guardResult({})
}
// If it's an enum, return the first member
if (isEnumType(type)) {
return guardResult(type.getValues()[0].value)
}
// If It's a union or interface, return the first type
if (isUnionType(type) || isInterfaceType(type)) {
const possibleTypes = info.schema.getPossibleTypes(type)
return guardResult({ __typename: possibleTypes[0].name })
}
// Otherwise, fail?
return val
}
}
interface NonNullInfo {
outerNonNull: boolean
hasListNonNull: boolean
}
const nonNullInfo = (type: GraphQLOutputType): NonNullInfo => {
let outerNonNull = false
let hasListNonNull = false
if (isNonNullType(type)) {
outerNonNull = true
type = type.ofType
}
while (isWrappingType(type)) {
type = type.ofType
if (isNonNullType(type)) {
hasListNonNull = true
}
}
return {
outerNonNull,
hasListNonNull,
}
}