Skip to content

Instantly share code, notes, and snippets.

@jasonkuhrt
Created March 17, 2021 12:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jasonkuhrt/04d184543efb00f558e0ac730e8ab1b1 to your computer and use it in GitHub Desktop.
Save jasonkuhrt/04d184543efb00f558e0ac730e8ab1b1 to your computer and use it in GitHub Desktop.
Abstraction for defining result fields in Nexus
/**
* This does not show surrounding code like referenced types.
*/
resultMutationField('inviteUserToProject', {
input(t) {
t.nonNull.id('userHandle')
t.nonNull.id('projectId')
t.nonNull.field('role', {
type: 'ProjectRole',
})
},
successType: 'ProjectMembership',
errorTypes: [
'ClientErrorUserNotFound',
'ClientErrorProjectNotFound',
'ClientErrorInviteUserToProjectAlreadyMember',
'ClientErrorInviteUserToProjectAreOwner',
],
aggregateErrors: true,
async resolve(_, args, ctx) {
// ...
}
})
import { upperFirst } from 'lodash'
import { core, extendType, inputObjectType, list, nonNull, objectType, unionType } from 'nexus'
interface ResultMutationFieldConfig<FieldName extends string = any> {
/**
* The success-case type returned by this mutation resolver. This type will be wrapped in the result type which in
* turn becomes the actual mutation field type.
*
* Because this type will be in a union it _must_ be an Object type as per the GraphQL spec.
*/
successType: core.GetGen<'objectNames'> | core.NexusObjectTypeDef<any>
/**
* The types of errors that this mutation may return.
*
* The list may contain a mix of either Object type reference _or_ Object type definitions. If definitions given
* they will be added to the GraphQL schema for you (via being included in the returned array of this function
* call).
*/
errorTypes: (core.GetGen<'objectNames'> | core.NexusObjectTypeDef<any>)[]
/**
* The input for this mutation.
*
* May be an InputObject type reference _or_ definition. If definition given, then the automatically created
* InputObject name is `${mutation field name}Input`.
*
* Made available on `args` under the `input` key.
*
* Optional but generally use this, as a good GraphQL API has as few idiosyncracies as possible.
*/
input?: core.GetGen<'allInputTypes'> | core.NexusInputObjectTypeConfig<string>['definition']
/**
* Custom arguments to add to this query field.
*
* Warning: if `input` given it will overwrite the `input` from this configuration.
*/
args?: core.ArgsRecord
/**
* The resolver for this mutation field.
*/
resolve: core.FieldResolver<'Mutation', FieldName>
/**
* Can this mutation return multiple errors at a time?
*
* When enabled additional type definitions are added to the GraphQL schema to support returning multiple errors.
* Since the GraphQL spec does not support putting arrays into unions it requires a new wrapper object to house a list
* of errors.
*
* @default false
*/
aggregateErrors?: boolean
/**
* The type name prefix to use. By default is the given field name capitalized (first character).
*
* Generally use this sparingly as a good GraphQL API has as few idiosyncracies as possible.
*/
typeNamePrefix?: string
}
/**
* Create a mutation field with a result-style return type that captures the set of possible errors that can happpen for
* this mutation.
*
* @param - The name of this mutation field.
* @param - Configuration For this mutation field.
* @returns A list of Nexus type definitions ready to be handed over to `makeSchema`.
*/
export function resultMutationField<FieldName extends string>(
name: FieldName,
config: ResultMutationFieldConfig<FieldName>
) {
return resultFieldDo(name, { ...config, rootObjectType: 'Mutation' })
}
interface ResultQueryFieldConfig<FieldName extends string = any> {
/**
* The success-case type returned by this query resolver. This type will be wrapped in the result type which in turn
* becomes the actual query field type.
*
* Because this type will be in a union it _must_ be an Object type as per the GraphQL spec.
*/
successType: core.GetGen<'objectNames'> | core.NexusObjectTypeDef<any>
/**
* The types of errors that this query may return.
*
* The list may contain a mix of either Object type reference _or_ Object type definitions. If definitions given
* they will be added to the GraphQL schema for you (via being included in the returned array of this function
* call).
*/
errorTypes: (core.GetGen<'objectNames'> | core.NexusObjectTypeDef<any>)[]
/**
* The input for this query.
*
* May be an InputObject type reference _or_ definition. If definition given, then the automatically created
* InputObject name is `${query field name}Input`.
*
* Made available on `args` under the `input` key.
*
* Optional but generally use this, as a good GraphQL API has as few idiosyncracies as possible.
*/
input?: core.GetGen<'allInputTypes'> | core.NexusInputObjectTypeConfig<string>['definition']
/**
* Custom arguments to add to this query field.
*
* Warning: if `input` given it will overwrite the `input` from this configuration.
*/
args?: core.ArgsRecord
/**
* The resolver for this query field.
*/
resolve: core.FieldResolver<'Query', FieldName>
/**
* Can this mutation return multiple errors at a time?
*
* When enabled additional type definitions are added to the GraphQL schema to support returning multiple errors.
* Since the GraphQL spec does not support putting arrays into unions it requires a new wrapper object to house a list
* of errors.
*
* @default false
*/
aggregateErrors?: boolean
/**
* The type name prefix to use. By default is the given field name capitalized (first character).
*
* Generally use this sparingly as a good GraphQL API has as few idiosyncracies as possible.
*/
typeNamePrefix?: string
}
/**
* Create a query field with a result-style return type that captures the set of possible errors that can happpen for
* this query.
*
* @param - The name of this query field.
* @param - Configuration For this query field.
* @returns A list of Nexus type definitions ready to be handed over to `makeSchema`.
*/
export function resultQueryField<FieldName extends string>(name: FieldName, config: ResultQueryFieldConfig<FieldName>) {
return resultFieldDo(name, { ...config, rootObjectType: 'Query' })
}
/**
* Core logic for all public result-field functions.
*/
function resultFieldDo(
name: string,
{
successType,
resolve,
input,
args,
errorTypes,
aggregateErrors = false,
typeNamePrefix,
rootObjectType,
}: (ResultMutationFieldConfig | ResultQueryFieldConfig) & {
rootObjectType: 'Mutation' | 'Query'
}
) {
const typeNamePrefix_ = typeNamePrefix ?? upperFirst(name)
const typeNameResult = `${typeNamePrefix_}Result`
const typeNameErrorAggregate = `${typeNamePrefix_}Errors`
const typeNameError = `${typeNamePrefix_}Error`
const successTypeName = typeof successType === 'string' ? successType : successType.name
const errorTypeNameReferences = errorTypes.map((error) => {
// Get the name from any _definitions_ given
return typeof error === 'string' ? error : error.name
})
const inputReference = typeof input === 'string' ? input : `${typeNamePrefix_}Input`
const conventionArgs = input
? {
input: nonNull(inputReference as any),
}
: {}
const args_ = { ...conventionArgs, ...args }
const types: any[] = [
extendType({
type: rootObjectType,
definition(t) {
t.field(name, {
type: typeNameResult as any,
args: args_ as any,
resolve: resolve as any,
})
},
}),
unionType({
name: typeNameResult,
definition(t) {
const aggregateErrorOrInlineErrors = aggregateErrors ? [typeNameErrorAggregate] : errorTypeNameReferences
t.members(successTypeName as any, ...(aggregateErrorOrInlineErrors as any))
},
}),
]
// Add error type object-references if any
// For convenience in case the user is using writing Nexus type defs inline
types.push(...(errorTypes.filter((error) => typeof error !== 'string') as any))
// Add result type object-reference if being used
// For convenience in case the user is using writing Nexus type defs inline
if (typeof successType !== 'string') {
types.push(successType)
}
// Add aggregate error structure if enabled
if (aggregateErrors) {
types.push(
objectType({
name: typeNameErrorAggregate,
isTypeOf(model) {
return 'errors' in model
},
definition(t) {
t.field('errors', {
type: nonNull(list(nonNull(typeNameError as any))),
})
},
}),
unionType({
name: typeNameError,
definition(t) {
t.members(...errorTypeNameReferences)
},
})
)
}
// Add inline input-type definition if any
if (input && typeof input !== 'string') {
types.push(
inputObjectType({
name: inputReference,
definition(t) {
input(t)
},
})
)
}
return types
}
union InviteUserToProjectError =
ClientErrorInviteUserToProjectAlreadyMember
| ClientErrorInviteUserToProjectAreOwner
| ClientErrorProjectNotFound
| ClientErrorUserNotFound
type InviteUserToProjectErrors {
errors: [InviteUserToProjectError!]!
}
input InviteUserToProjectInput {
projectId: ID!
role: ProjectRole!
userHandle: ID!
}
union InviteUserToProjectResult = InviteUserToProjectErrors | ProjectMembership
type Mutation {
inviteUserToProject(input: InviteUserToProjectInput!): InviteUserToProjectResult!
}
type ClientErrorInviteUserToProjectAlreadyMember implements ClientError & Error {
message: String!
path: [String!]
}
type ClientErrorInviteUserToProjectAreOwner implements ClientError & Error {
message: String!
path: [String!]
}
type ClientErrorProjectNotFound implements ClientError & Error {
message: String!
path: [String!]
}
type ClientErrorUserNotFound implements ClientError & Error {
message: String!
path: [String!]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment