-
-
Save jgoux/be2e02596a028969dfb4bf14161e6ec8 to your computer and use it in GitHub Desktop.
Abstraction for defining result fields in Nexus
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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) { | |
// ... | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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