import { ProcedureHookError, ProcedureError, ProcedureValidationError, } from './errors.js' import { ProcedureCodes } from './procedure-codes.js' import { computeSchema } from './schema/compute-schema.js' import { Prettify, TJSONSchema, TSchemaLib } from './schema/types.js' export type TNoContextProvided = unknown export type TLocalContext = { error: ( code: ProcedureCodes & number, message: string, meta?: object, ) => ProcedureError } export type TProcedureRegistration< TContext = unknown, TExtendedConfig = unknown, > = { name: string config: { description?: string hook?: (ctx: TContext, args?: any) => any schema?: { args?: TJSONSchema data?: TJSONSchema } validation?: { args?: (args: any) => { errors?: any[] } } } & TExtendedConfig handler: (ctx: TContext, args?: any) => Promise } export function Procedures< TContext = TNoContextProvided, TExtendedConfig = unknown, >( /** * Optionally provided builder to register Procedures */ builder?: { onCreate?: ( procedure: Prettify<{ name: string handler: (ctx: Prettify, args?: any) => Promise config: Prettify< { description?: string hook?: ( ctx: Prettify, args?: any, ) => Promise schema?: { args?: TJSONSchema data?: TJSONSchema } validation?: { args?: (args: any) => { errors?: any[] } } } & TExtendedConfig > }>, ) => void }, ) { const procedures: Map< string, { name: string config: Prettify< { description?: string hook?: ( ctx: Prettify, args?: any, ) => Promise schema?: { args?: TJSONSchema data?: TJSONSchema } validation?: { args?: (args: any) => { errors?: any[] } } } & TExtendedConfig > handler: (ctx: Prettify, args: any) => Promise } > = new Map() function Create( name: TName, config: { description?: string hook?: ( ctx: Prettify, args: TSchemaLib, ) => Promise> schema?: { args?: TArgs data?: TData } } & TExtendedConfig, handler: ( ctx: Prettify, args: TSchemaLib, ) => Promise>, ) { const { jsonSchema, validations } = computeSchema(name, config.schema) const registeredProcedure = { name, config: { // ctx: config.hook, ??? why was this here ...config, description: config.description, schema: jsonSchema, validation: { args: validations.args, }, }, handler: async (ctx: Prettify, args: TSchemaLib) => { try { if (validations?.args) { const { errors } = validations.args(args) if (errors) { throw new ProcedureValidationError( name, `Validation error for ${name} - ${errors.map((e) => e.message).join(', ')}`, errors, ) } } const localCtx: TLocalContext = { error: ( code: ProcedureCodes & number, message: string, meta?: object, ) => { return new ProcedureError(name, code, message, meta) }, } let computedLocalHook: TLocalHook = {} as TLocalHook if (config.hook) { try { computedLocalHook = (await config.hook( { ...localCtx, ...ctx }, args, )) as TLocalHook } catch (error: any) { if (error instanceof ProcedureError) { throw error } const err = new ProcedureHookError( name, `Error in hook for ${name} - ${error?.message}`, ) err.stack = error.stack throw err } } return handler( { ...ctx, ...localCtx, ...computedLocalHook, } as Prettify, args, ) } catch (error: any) { if (error instanceof ProcedureHookError) { throw error } else if (error instanceof ProcedureError) { throw error } else { const err = new ProcedureError( name, ProcedureCodes.INTERNAL_ERROR, `Error in handler for ${name} - ${error?.message}`, ) err.stack = error.stack throw err } } }, } procedures.set(name, registeredProcedure) if (builder?.onCreate) { builder.onCreate(registeredProcedure) } const info = { name, ...registeredProcedure.config, } // return so can be called directly (ie: int/unit tests) return { [name]: registeredProcedure.handler, procedure: registeredProcedure.handler, info, } as { [K in TName]: ( ctx: Prettify, args: TSchemaLib, ) => Promise> } & { procedure: ( ctx: Prettify, args: TSchemaLib, ) => Promise> info: { name: TName description?: string hook?: ( ctx: Prettify, args: TSchemaLib, ) => Promise> schema: { args?: TArgs data?: TData } validation?: { args?: (args: any) => { errors?: any[] } } } & TExtendedConfig } } return { getProcedures: () => { return procedures }, Create, } }