UNPKG

@pothos/plugin-errors

Version:

A Pothos plugin for adding typed errors into your schema

415 lines (357 loc) 12.4 kB
import './global-types'; import SchemaBuilder, { BasePlugin, type ImplementableObjectRef, type Normalize, type PothosOutputFieldConfig, type PothosOutputFieldType, PothosSchemaError, type SchemaTypes, sortClasses, type TypeParam, typeBrandKey, unwrapOutputFieldType, } from '@pothos/core'; import type { GraphQLFieldResolver, GraphQLIsTypeOfFn } from 'graphql'; import type { ErrorFieldOptions, GetTypeName } from './types'; export * from './types'; const pluginName = 'errors'; export default pluginName; export function capitalize(s: string) { return `${s.slice(0, 1).toUpperCase()}${s.slice(1)}`; } export const defaultGetResultName: GetTypeName = ({ parentTypeName, fieldName }) => `${parentTypeName}${capitalize(fieldName)}Success`; export const defaultGetListItemResultName: GetTypeName = ({ parentTypeName, fieldName }) => `${parentTypeName}${capitalize(fieldName)}ItemSuccess`; export const defaultGetUnionName: GetTypeName = ({ parentTypeName, fieldName }) => `${parentTypeName}${capitalize(fieldName)}Result`; export const defaultGetListItemUnionName: GetTypeName = ({ parentTypeName, fieldName }) => `${parentTypeName}${capitalize(fieldName)}ItemResult`; export const unwrapError = Symbol.for('Pothos.unwrapErrors'); function createErrorProxy(target: {}, ref: unknown, state: { wrapped: boolean }): {} { return new Proxy(target, { get(err, val, receiver) { if (val === unwrapError) { return () => { state.wrapped = false; }; } if (val === typeBrandKey) { return ref; } return Reflect.get(err, val, receiver) as unknown; }, getPrototypeOf(err) { const proto = Reflect.getPrototypeOf(err) as {}; if (!state.wrapped || !proto) { return proto; } return createErrorProxy(proto, ref, state); }, }); } const errorTypeMap = new WeakMap<{}, new (...args: never[]) => Error>(); export class PothosErrorsPlugin<Types extends SchemaTypes> extends BasePlugin<Types> { override wrapIsTypeOf( isTypeOf: GraphQLIsTypeOfFn<unknown, Types['Context']> | undefined, ): GraphQLIsTypeOfFn<unknown, Types['Context']> | undefined { if (isTypeOf) { return (parent, context, info) => { if (typeof parent === 'object' && parent) { (parent as { [unwrapError]?: () => void })[unwrapError]?.(); } return isTypeOf(parent, context, info); }; } return isTypeOf; } override onOutputFieldConfig( fieldConfig: PothosOutputFieldConfig<Types>, ): PothosOutputFieldConfig<Types> | null { const errorOptions = fieldConfig.pothosOptions.errors; const itemErrorOptions = fieldConfig.pothosOptions.itemErrors; const errorBuilderOptions = this.builder.options.errors; if (!errorOptions && !itemErrorOptions) { return fieldConfig; } const parentTypeName = this.buildCache.getTypeConfig(fieldConfig.parentType).name; const itemErrorTypes = itemErrorOptions && sortClasses([ ...new Set([ ...(itemErrorOptions?.types ?? []), ...(errorBuilderOptions?.defaultTypes ?? []), ]), ]); const errorTypes = errorOptions && sortClasses([ ...new Set([...(errorOptions?.types ?? []), ...(errorBuilderOptions?.defaultTypes ?? [])]), ]); let resultType = fieldConfig.pothosOptions.type; if (itemErrorOptions) { if (!Array.isArray(fieldConfig.pothosOptions.type) || fieldConfig.type.kind !== 'List') { throw new PothosSchemaError( `Field ${parentTypeName}.${fieldConfig.name} must return a list when 'itemErrors' is set`, ); } const itemFieldType = fieldConfig.type.type; const itemType = fieldConfig.pothosOptions.type[0]; resultType = [ this.createResultType( parentTypeName, fieldConfig.name, itemType, itemFieldType, itemErrorOptions, `Field ${parentTypeName}.${fieldConfig.name} list items must be an ObjectType when 'directResult' is set to true`, defaultGetListItemResultName, defaultGetListItemUnionName, errorBuilderOptions?.defaultItemResultOptions, errorBuilderOptions?.defaultItemUnionOptions, ), ]; if (!errorOptions) { return { ...fieldConfig, extensions: { ...fieldConfig.extensions, pothosItemErrors: itemErrorTypes, }, type: { ...fieldConfig.type, type: { kind: 'Union', ref: resultType[0], nullable: fieldConfig.type.type.nullable, }, }, }; } } const unionType = this.createResultType( parentTypeName, fieldConfig.name, resultType, fieldConfig.type, errorOptions!, `Field ${parentTypeName}.${fieldConfig.name} must return an ObjectType when 'directResult' is set to true`, defaultGetResultName, defaultGetUnionName, errorBuilderOptions?.defaultResultOptions, errorBuilderOptions?.defaultUnionOptions, ); return { ...fieldConfig, extensions: { ...fieldConfig.extensions, pothosErrors: errorTypes, pothosItemErrors: itemErrorTypes, }, type: { kind: 'Union', ref: unionType, nullable: fieldConfig.type.nullable, }, }; } override wrapResolve( resolver: GraphQLFieldResolver<unknown, Types['Context'], object>, fieldConfig: PothosOutputFieldConfig<Types>, ): GraphQLFieldResolver<unknown, Types['Context'], object> { const pothosErrors = fieldConfig.extensions?.pothosErrors as (typeof Error)[] | undefined; const pothosItemErrors = fieldConfig.extensions?.pothosItemErrors as | (typeof Error)[] | undefined; if (!pothosErrors && !pothosItemErrors) { return resolver; } return async (source, args, context, info) => { if (fieldConfig.kind === 'Subscription' && errorTypeMap.has(source as {})) { return source; } try { const result = (await resolver(source, args, context, info)) as never; if (pothosItemErrors && result && typeof result === 'object' && Symbol.iterator in result) { return yieldErrors(result, pothosItemErrors); } if ( pothosItemErrors && result && typeof result === 'object' && Symbol.asyncIterator in result ) { console.log(result, yieldAsyncErrors); return yieldAsyncErrors(result, pothosItemErrors); } return result; } catch (error: unknown) { return wrapOrThrow(error, pothosErrors ?? []); } }; } override wrapSubscribe( subscribe: GraphQLFieldResolver<unknown, Types['Context'], object>, fieldConfig: PothosOutputFieldConfig<Types>, ): GraphQLFieldResolver<unknown, Types['Context'], object> | undefined { const pothosErrors = fieldConfig.extensions?.pothosErrors as (typeof Error)[] | undefined; if (!pothosErrors) { return subscribe; } return (...args) => { async function* yieldSubscribeErrors() { try { const iter = (await subscribe(...args)) as AsyncIterableIterator<unknown>; if (!iter) { return iter; } for await (const value of iter) { yield value; } } catch (error: unknown) { yield wrapOrThrow(error, pothosErrors ?? []); } } return yieldSubscribeErrors(); }; } createResultType( parentTypeName: string, fieldName: string, type: TypeParam<Types>, fieldType: PothosOutputFieldType<Types>, errorOptions: ErrorFieldOptions<Types, TypeParam<Types>, unknown, false>, directResultError: string, defaultResultName: GetTypeName, defaultUnionName: GetTypeName, builderResultOptions: Normalize< Omit<PothosSchemaTypes.ObjectTypeOptions<Types, {}>, 'interfaces' | 'isTypeOf'> & { name?: GetTypeName; } > = {} as never, builderUnionOptions: Normalize< Omit<PothosSchemaTypes.UnionTypeOptions<Types>, 'resolveType' | 'types'> & { name?: GetTypeName; } > = {} as never, ) { const errorBuilderOptions = this.builder.options.errors; const { name: getResultName = defaultResultName, ...defaultResultOptions } = builderResultOptions ?? {}; const { name: getUnionName = defaultUnionName, ...defaultUnionOptions } = builderUnionOptions ?? {}; const { types = [], result: { name: resultName = getResultName({ parentTypeName, fieldName, }), fields: resultFieldOptions, ...resultObjectOptions } = {} as never, union: { name: unionName = getUnionName({ parentTypeName, fieldName, }), ...unionOptions } = {} as never, dataField: { name: dataFieldName = 'data', ...dataField } = {} as never, } = errorOptions; const errorTypes = sortClasses([ ...new Set([...types, ...(errorBuilderOptions?.defaultTypes ?? [])]), ]); const directResult = (errorOptions as { directResult?: boolean }).directResult ?? errorBuilderOptions?.directResult ?? false; const typeRef = unwrapOutputFieldType(fieldType); const typeName = this.builder.configStore.getTypeConfig(typeRef).name; return this.runUnique(resultName, () => { let resultType: ImplementableObjectRef<Types, unknown>; if (directResult && !Array.isArray(fieldType)) { resultType = type as ImplementableObjectRef<Types, unknown>; const resultConfig = this.builder.configStore.getTypeConfig(resultType); if (resultConfig.graphqlKind !== 'Object') { throw new PothosSchemaError(directResultError); } } else { resultType = this.builder.objectRef<unknown>(resultName); resultType.implement({ ...defaultResultOptions, ...resultObjectOptions, fields: (t) => ({ ...resultFieldOptions?.(t), [dataFieldName]: t.field({ ...dataField, type, nullable: fieldType.kind === 'List' ? { items: fieldType.type.nullable, list: false } : false, resolve: (data) => data as never, }), }), }); } const getDataloader = this.buildCache.getTypeConfig(unwrapOutputFieldType(fieldType)) .extensions?.getDataloader; return this.builder.unionType(unionName, { types: [...errorTypes, resultType], resolveType: (obj) => (errorTypeMap.get(obj as {}) as never) ?? resultType, ...defaultUnionOptions, ...unionOptions, extensions: { ...unionOptions.extensions, getDataloader, pothosIndirectInclude: { getType: () => typeName, path: directResult ? [] : [{ type: resultName, name: dataFieldName }], }, }, }); }); } } SchemaBuilder.registerPlugin(pluginName, PothosErrorsPlugin, { v3: (options) => ({ errorOptions: undefined, errors: options?.errorOptions, }), }); function wrapOrThrow(error: unknown, pothosErrors: ErrorConstructor[]) { for (const errorType of pothosErrors) { if (error instanceof errorType) { const result = createErrorProxy(error, errorType, { wrapped: true }); errorTypeMap.set(result, errorType); return result; } } throw error; } function* yieldErrors(result: Iterable<unknown>, pothosErrors: ErrorConstructor[]) { try { for (const item of result) { if (item instanceof Error) { yield wrapOrThrow(item, pothosErrors); } else { yield item; } } } catch (error: unknown) { yield wrapOrThrow(error, pothosErrors); } } async function* yieldAsyncErrors(result: AsyncIterable<unknown>, pothosErrors: ErrorConstructor[]) { try { for await (const item of result) { if (item instanceof Error) { yield wrapOrThrow(item, pothosErrors); } else { yield item; } } } catch (error: unknown) { yield wrapOrThrow(error, pothosErrors); } }