import { getManager, HookContextData, HookManager, HookMap as BaseHookMap, hooks, Middleware, collect } from '@feathersjs/hooks' import { Service, ServiceOptions, HookContext, FeathersService, HookMap, AroundHookFunction, HookFunction, HookType } from './declarations' import { defaultServiceArguments, getHookMethods } from './service' type ConvertedMap = { [type in HookType]: ReturnType } type HookStore = { around: { [method: string]: AroundHookFunction[] } before: { [method: string]: HookFunction[] } after: { [method: string]: HookFunction[] } error: { [method: string]: HookFunction[] } collected: { [method: string]: AroundHookFunction[] } collectedAll: { before?: AroundHookFunction[]; after?: AroundHookFunction[] } } type HookEnabled = { __hooks: HookStore } const types: HookType[] = ['before', 'after', 'error', 'around'] const isType = (value: any): value is HookType => types.includes(value) // Converts different hook registration formats into the // same internal format export function convertHookData(input: any) { const result: { [method: string]: HookFunction[] | AroundHookFunction[] } = {} if (Array.isArray(input)) { result.all = input } else if (typeof input !== 'object') { result.all = [input] } else { for (const key of Object.keys(input)) { const value = input[key] result[key] = Array.isArray(value) ? value : [value] } } return result } export function collectHooks(target: HookEnabled, method: string) { const { collected, collectedAll, around } = target.__hooks return [ ...(around.all || []), ...(around[method] || []), ...(collectedAll.before || []), ...(collected[method] || []), ...(collectedAll.after || []) ] as AroundHookFunction[] } // Add `.hooks` functionality to an object export function enableHooks(object: any) { const store: HookStore = { around: {}, before: {}, after: {}, error: {}, collected: {}, collectedAll: {} } Object.defineProperty(object, '__hooks', { configurable: true, value: store, writable: true }) return function registerHooks(this: HookEnabled, input: HookMap) { const store = this.__hooks const map = Object.keys(input).reduce((map, type) => { if (!isType(type)) { throw new Error(`'${type}' is not a valid hook type`) } map[type] = convertHookData(input[type]) return map }, {} as ConvertedMap) const types = Object.keys(map) as HookType[] types.forEach((type) => Object.keys(map[type]).forEach((method) => { const mapHooks = map[type][method] const storeHooks: any[] = (store[type][method] ||= []) storeHooks.push(...mapHooks) if (method === 'all') { if (store.before[method] || store.error[method]) { const beforeAll = collect({ before: store.before[method] || [], error: store.error[method] || [] }) store.collectedAll.before = [beforeAll] } if (store.after[method]) { const afterAll = collect({ after: store.after[method] || [] }) store.collectedAll.after = [afterAll] } } else { if (store.before[method] || store.after[method] || store.error[method]) { const collected = collect({ before: store.before[method] || [], after: store.after[method] || [], error: store.error[method] || [] }) store.collected[method] = [collected] } } }) ) return this } } export function createContext(service: Service, method: string, data: HookContextData = {}) { const createContext = (service as any)[method].createContext if (typeof createContext !== 'function') { throw new Error(`Can not create context for method ${method}`) } return createContext(data) as HookContext } export class FeathersHookManager extends HookManager { constructor( public app: A, public method: string ) { super() this._middleware = [] } collectMiddleware(self: any, args: any[]): Middleware[] { const appHooks = collectHooks(this.app as any as HookEnabled, this.method) const middleware = super.collectMiddleware(self, args) const methodHooks = collectHooks(self, this.method) return [...appHooks, ...middleware, ...methodHooks] } initializeContext(self: any, args: any[], context: HookContext) { const ctx = super.initializeContext(self, args, context) ctx.params = ctx.params || {} return ctx } middleware(mw: Middleware[]) { this._middleware.push(...mw) return this } } export function hookMixin(this: A, service: FeathersService, path: string, options: ServiceOptions) { if (typeof service.hooks === 'function') { return service } const hookMethods = getHookMethods(service, options) const serviceMethodHooks = hookMethods.reduce((res, method) => { const params = (defaultServiceArguments as any)[method] || ['data', 'params'] res[method] = new FeathersHookManager(this, method).params(...params).props({ app: this, path, method, service, event: null, type: 'around', get statusCode() { return this.http?.status }, set statusCode(value: number) { this.http = this.http || {} this.http.status = value } }) return res }, {} as BaseHookMap) const registerHooks = enableHooks(service) hooks(service, serviceMethodHooks) service.hooks = function (this: any, hookOptions: any) { if (hookOptions.before || hookOptions.after || hookOptions.error || hookOptions.around) { return registerHooks.call(this, hookOptions) } if (Array.isArray(hookOptions)) { return hooks(this, hookOptions) } Object.keys(hookOptions).forEach((method) => { const manager = getManager(this[method]) if (!(manager instanceof FeathersHookManager)) { throw new Error(`Method ${method} is not a Feathers hooks enabled service method`) } manager.middleware(hookOptions[method]) }) return this } return service }