diff --git a/package-lock.json b/package-lock.json index 0eb0556dc9..fcdc117786 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "dependencies": { "@babel/core": "^7.17.10", "@babel/preset-env": "^7.17.10", - "@feathersjs/hooks": "^0.7.4", + "@feathersjs/hooks": "^0.7.5", "@types/axios": "^0.14.0", "@types/bcryptjs": "^2.4.2", "@types/config": "^0.0.41", @@ -1767,9 +1767,9 @@ "dev": true }, "node_modules/@feathersjs/hooks": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@feathersjs/hooks/-/hooks-0.7.4.tgz", - "integrity": "sha512-TrnvC+bBpb8xg83SkKZ+/0yXLfJFV5Y93iwDbI0fE3yg32VC6bugnBHMHhNlNJywvjA0JJKE96kyMOavp+uZIA==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@feathersjs/hooks/-/hooks-0.7.5.tgz", + "integrity": "sha512-xHbWPCJ5upr9eDdRSuJgWSKgCLN07YLvVjS7PdDqWrEz/iz5YBqoIQOzHhNPsWXOA6T7v+ArifrTUitG+ZzIRA==", "engines": { "node": ">= 14" } @@ -3478,9 +3478,9 @@ } }, "node_modules/@types/node": { - "version": "17.0.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.39.tgz", - "integrity": "sha512-JDU3YLlnPK3WDao6/DlXLOgSNpG13ct+CwIO17V8q0/9fWJyeMJJ/VyZ1lv8kDprihvZMydzVwf0tQOqGiY2Nw==" + "version": "17.0.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.40.tgz", + "integrity": "sha512-UXdBxNGqTMtm7hCwh9HtncFVLrXoqA3oJW30j6XWp5BH/wu3mVeaxo7cq5benFdBw34HB3XDT2TRPI7rXZ+mDg==" }, "node_modules/@types/node-fetch": { "version": "2.6.1", @@ -7296,9 +7296,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.145", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.145.tgz", - "integrity": "sha512-g4VQCi61gA0t5fJHsalxAc8NpvxC/CEwLAGLfJ+DmkRXTEyntJA7H01771uVD6X6nnViv3GToPgb0QOVA8ivOQ==" + "version": "1.4.146", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.146.tgz", + "integrity": "sha512-4eWebzDLd+hYLm4csbyMU2EbBnqhwl8Oe9eF/7CBDPWcRxFmqzx4izxvHH+lofQxzieg8UbB8ZuzNTxeukzfTg==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -7500,11 +7500,11 @@ } }, "node_modules/error-stack-parser": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz", - "integrity": "sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.3.tgz", + "integrity": "sha512-F9KypcaAvLzI4yXneZzOvzZoqakhbjuAGFK0aLy33tYaDqdu6v+lzrN/TTG/mM48Op624zZZ2RpXRx3wA0+zmg==", "dependencies": { - "stackframe": "^1.1.1" + "stackframe": "^1.3.4" } }, "node_modules/es-abstract": { @@ -7687,9 +7687,9 @@ } }, "node_modules/eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz", - "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", @@ -16816,9 +16816,9 @@ } }, "node_modules/stackframe": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", - "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==" + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, "node_modules/statuses": { "version": "2.0.1", @@ -17402,9 +17402,9 @@ } }, "node_modules/ts-node": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", - "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", + "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -17562,9 +17562,9 @@ } }, "node_modules/typescript": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", - "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19731,9 +19731,9 @@ } }, "@feathersjs/hooks": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@feathersjs/hooks/-/hooks-0.7.4.tgz", - "integrity": "sha512-TrnvC+bBpb8xg83SkKZ+/0yXLfJFV5Y93iwDbI0fE3yg32VC6bugnBHMHhNlNJywvjA0JJKE96kyMOavp+uZIA==" + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@feathersjs/hooks/-/hooks-0.7.5.tgz", + "integrity": "sha512-xHbWPCJ5upr9eDdRSuJgWSKgCLN07YLvVjS7PdDqWrEz/iz5YBqoIQOzHhNPsWXOA6T7v+ArifrTUitG+ZzIRA==" }, "@gar/promisify": { "version": "1.1.3", @@ -21191,9 +21191,9 @@ } }, "@types/node": { - "version": "17.0.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.39.tgz", - "integrity": "sha512-JDU3YLlnPK3WDao6/DlXLOgSNpG13ct+CwIO17V8q0/9fWJyeMJJ/VyZ1lv8kDprihvZMydzVwf0tQOqGiY2Nw==" + "version": "17.0.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.40.tgz", + "integrity": "sha512-UXdBxNGqTMtm7hCwh9HtncFVLrXoqA3oJW30j6XWp5BH/wu3mVeaxo7cq5benFdBw34HB3XDT2TRPI7rXZ+mDg==" }, "@types/node-fetch": { "version": "2.6.1", @@ -24313,9 +24313,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.145", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.145.tgz", - "integrity": "sha512-g4VQCi61gA0t5fJHsalxAc8NpvxC/CEwLAGLfJ+DmkRXTEyntJA7H01771uVD6X6nnViv3GToPgb0QOVA8ivOQ==" + "version": "1.4.146", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.146.tgz", + "integrity": "sha512-4eWebzDLd+hYLm4csbyMU2EbBnqhwl8Oe9eF/7CBDPWcRxFmqzx4izxvHH+lofQxzieg8UbB8ZuzNTxeukzfTg==" }, "elliptic": { "version": "6.5.4", @@ -24465,11 +24465,11 @@ } }, "error-stack-parser": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz", - "integrity": "sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.3.tgz", + "integrity": "sha512-F9KypcaAvLzI4yXneZzOvzZoqakhbjuAGFK0aLy33tYaDqdu6v+lzrN/TTG/mM48Op624zZZ2RpXRx3wA0+zmg==", "requires": { - "stackframe": "^1.1.1" + "stackframe": "^1.3.4" } }, "es-abstract": { @@ -24609,9 +24609,9 @@ } }, "eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz", - "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.0", @@ -31777,9 +31777,9 @@ } }, "stackframe": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", - "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==" + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, "statuses": { "version": "2.0.1", @@ -32216,9 +32216,9 @@ } }, "ts-node": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", - "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", + "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -32326,9 +32326,9 @@ } }, "typescript": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", - "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==" + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==" }, "uglify-js": { "version": "3.15.5", diff --git a/packages/feathers/package.json b/packages/feathers/package.json index 9a910079fc..9817c140f6 100644 --- a/packages/feathers/package.json +++ b/packages/feathers/package.json @@ -58,7 +58,7 @@ }, "dependencies": { "@feathersjs/commons": "^5.0.0-pre.22", - "@feathersjs/hooks": "^0.7.4", + "@feathersjs/hooks": "^0.7.5", "events": "^3.3.0" }, "devDependencies": { diff --git a/packages/feathers/src/application.ts b/packages/feathers/src/application.ts index ad888fe698..be991fe96f 100644 --- a/packages/feathers/src/application.ts +++ b/packages/feathers/src/application.ts @@ -1,9 +1,9 @@ import version from './version' import { EventEmitter } from 'events' import { stripSlashes, createDebug } from '@feathersjs/commons' -import { HOOKS, hooks, middleware } from '@feathersjs/hooks' +import { hooks, middleware } from '@feathersjs/hooks' import { eventHook, eventMixin } from './events' -import { hookMixin } from './hooks/index' +import { hookMixin } from './hooks' import { wrapService, getServiceOptions, protectedMethods } from './service' import { FeathersApplication, @@ -13,10 +13,9 @@ import { ServiceInterface, Application, FeathersService, - AroundHookMap, ApplicationHookOptions } from './declarations' -import { enableRegularHooks } from './hooks/regular' +import { enableHooks } from './hooks' const debug = createDebug('@feathersjs/feathers') @@ -29,15 +28,11 @@ export class Feathers mixins: ServiceMixin>[] = [hookMixin, eventMixin] version: string = version _isSetup = false - appHooks: AroundHookMap, any> = { - [HOOKS]: [eventHook as any] - } - private regularHooks: (this: any, allHooks: any) => any + protected registerHooks: (this: any, allHooks: any) => any constructor() { super() - this.regularHooks = enableRegularHooks(this) hooks(this, { setup: middleware().params('server').props({ app: this @@ -46,6 +41,10 @@ export class Feathers app: this }) }) + this.registerHooks = enableHooks(this) + this.registerHooks({ + around: [eventHook] + }) } get(name: L): Settings[L] { @@ -130,19 +129,16 @@ export class Feathers hooks(hookMap: ApplicationHookOptions) { const untypedMap = hookMap as any - if (untypedMap.before || untypedMap.after || untypedMap.error) { - this.regularHooks(untypedMap) + if (untypedMap.before || untypedMap.after || untypedMap.error || untypedMap.around) { + // regular hooks for all service methods + this.registerHooks(untypedMap) } else if (untypedMap.setup || untypedMap.teardown) { + // .setup and .teardown application hooks hooks(this, untypedMap) - } else if (Array.isArray(hookMap)) { - this.appHooks[HOOKS].push(...(hookMap as any)) } else { - const methodHookMap = hookMap as AroundHookMap, any> - - Object.keys(methodHookMap).forEach((key) => { - const methodHooks = this.appHooks[key] || [] - - this.appHooks[key] = methodHooks.concat(methodHookMap[key]) + // Other registration formats are just `around` hooks + this.registerHooks({ + around: untypedMap }) } diff --git a/packages/feathers/src/declarations.ts b/packages/feathers/src/declarations.ts index 861562ff3b..a63416bac0 100644 --- a/packages/feathers/src/declarations.ts +++ b/packages/feathers/src/declarations.ts @@ -127,11 +127,6 @@ export interface FeathersApplication { */ _isSetup: boolean - /** - * Contains all registered application level hooks. - */ - appHooks: AroundHookMap, any> - /** * Retrieve an application setting by name * @@ -332,18 +327,18 @@ export interface HookContext extends BaseHookContext = ( +export type HookFunction = ( this: S, context: HookContext ) => Promise | void> | HookContext | void -export type Hook = RegularHookFunction +export type Hook = HookFunction -type RegularHookMethodMap = { - [L in keyof S]?: SelfOrArray> -} & { all?: SelfOrArray> } +type HookMethodMap = { + [L in keyof S]?: SelfOrArray> +} & { all?: SelfOrArray> } -type RegularHookTypeMap = SelfOrArray> | RegularHookMethodMap +type HookTypeMap = SelfOrArray> | HookMethodMap // New @feathersjs/hook typings export type AroundHookFunction = ( @@ -353,13 +348,13 @@ export type AroundHookFunction = ( export type AroundHookMap = { [L in keyof S]?: AroundHookFunction[] -} +} & { all?: AroundHookFunction[] } export type HookMap = { around?: AroundHookMap - before?: RegularHookTypeMap - after?: RegularHookTypeMap - error?: RegularHookTypeMap + before?: HookTypeMap + after?: HookTypeMap + error?: HookTypeMap } export type HookOptions = AroundHookMap | AroundHookFunction[] | HookMap diff --git a/packages/feathers/src/hooks.ts b/packages/feathers/src/hooks.ts new file mode 100644 index 0000000000..02697b702b --- /dev/null +++ b/packages/feathers/src/hooks.ts @@ -0,0 +1,230 @@ +import { + getManager, + HookContextData, + HookManager, + HookMap as BaseHookMap, + hooks, + Middleware, + collect +} from '@feathersjs/hooks' +import { + Service, + ServiceOptions, + HookContext, + FeathersService, + HookMap, + AroundHookFunction, + HookFunction +} from './declarations' +import { defaultServiceArguments, defaultServiceMethods, getHookMethods } from './service' + +export function collectHooks(target: any, method: string) { + return target.__hooks.hooks[method] || [] +} + +// 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 +} + +type HookTypes = 'before' | 'after' | 'error' | 'around' + +type ConvertedMap = { [type in HookTypes]: ReturnType } + +type HookStore = { + around: { [method: string]: AroundHookFunction[] } + before: { [method: string]: HookFunction[] } + after: { [method: string]: HookFunction[] } + error: { [method: string]: HookFunction[] } + hooks: { [method: string]: AroundHookFunction[] } +} + +const types: HookTypes[] = ['before', 'after', 'error', 'around'] + +const isType = (value: any): value is HookTypes => types.includes(value) + +const createMap = (input: HookMap, methods: string[]) => { + const map = {} as ConvertedMap + + Object.keys(input).forEach((type) => { + if (!isType(type)) { + throw new Error(`'${type}' is not a valid hook type`) + } + + const data = convertHookData(input[type]) + + Object.keys(data).forEach((method) => { + if (method !== 'all' && !methods.includes(method) && !defaultServiceMethods.includes(method)) { + throw new Error(`'${method}' is not a valid hook method`) + } + }) + + map[type] = data + }) + + return map +} + +const updateStore = (store: HookStore, map: ConvertedMap) => + Object.keys(store.hooks).forEach((method) => { + Object.keys(map).forEach((key) => { + const type = key as HookTypes + const allHooks = map[type].all || [] + const methodHooks = map[type][method] || [] + + if (allHooks.length || methodHooks.length) { + const list = [...allHooks, ...methodHooks] as any + const hooks = (store[type][method] ||= []) + + hooks.push(...list) + } + }) + + const collected = collect({ + before: store.before[method] || [], + after: store.after[method] || [], + error: store.error[method] || [] + }) + + store.hooks[method] = [...(store.around[method] || []), collected] + }) + +// Add `.hooks` functionality to an object +export function enableHooks(object: any, methods: string[] = defaultServiceMethods) { + const store: HookStore = { + around: {}, + before: {}, + after: {}, + error: {}, + hooks: {} + } + + for (const method of methods) { + store.hooks[method] = [] + } + + Object.defineProperty(object, '__hooks', { + configurable: true, + value: store, + writable: true + }) + + return function registerHooks(this: any, input: HookMap) { + const store = this.__hooks + const map = createMap(input, methods) + + updateStore(store, map) + + 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, 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: null, + 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, hookMethods) + + 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 +} diff --git a/packages/feathers/src/hooks/index.ts b/packages/feathers/src/hooks/index.ts deleted file mode 100644 index 144ce8e166..0000000000 --- a/packages/feathers/src/hooks/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - getManager, - HookContextData, - HookManager, - HookMap, - HOOKS, - hooks, - Middleware -} from '@feathersjs/hooks' -import { Service, ServiceOptions, HookContext, FeathersService, Application } from '../declarations' -import { defaultServiceArguments, getHookMethods } from '../service' -import { collectRegularHooks, enableRegularHooks } from './regular' - -export { - fromBeforeHook, - fromBeforeHooks, - fromAfterHook, - fromAfterHooks, - fromErrorHook, - fromErrorHooks -} from './regular' - -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 app = this.app as any as Application - const appHooks = app.appHooks[HOOKS].concat(app.appHooks[this.method] || []) - const regularAppHooks = collectRegularHooks(this.app, this.method) - const middleware = super.collectMiddleware(self, args) - const regularHooks = collectRegularHooks(self, this.method) - - return [...appHooks, ...regularAppHooks, ...middleware, ...regularHooks] - } - - 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: null, - get statusCode() { - return this.http?.status - }, - set statusCode(value: number) { - this.http = this.http || {} - this.http.status = value - } - }) - - return res - }, {} as HookMap) - - const handleRegularHooks = enableRegularHooks(service, hookMethods) - - hooks(service, serviceMethodHooks) - - service.hooks = function (this: any, hookOptions: any) { - if (hookOptions.before || hookOptions.after || hookOptions.error) { - return handleRegularHooks.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 -} diff --git a/packages/feathers/src/hooks/regular.ts b/packages/feathers/src/hooks/regular.ts deleted file mode 100644 index df067269b7..0000000000 --- a/packages/feathers/src/hooks/regular.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { AroundHookFunction, RegularHookFunction, HookMap } from '../declarations' -import { defaultServiceMethods } from '../service' - -const runHook = (hook: RegularHookFunction, context: any, type?: string) => { - if (type) context.type = type - return Promise.resolve(hook.call(context.self, context)).then((res: any) => { - if (type) context.type = null - if (res && res !== context) { - Object.assign(context, res) - } - }) -} - -export function fromBeforeHook(hook: RegularHookFunction): AroundHookFunction { - return (context, next) => { - return runHook(hook, context, 'before').then(next) - } -} - -export function fromAfterHook(hook: RegularHookFunction): AroundHookFunction { - return (context, next) => { - return next().then(() => runHook(hook, context, 'after')) - } -} - -export function fromErrorHook(hook: RegularHookFunction): AroundHookFunction { - return (context, next) => { - return next().catch((error: any) => { - if (context.error !== error || context.result !== undefined) { - context.original = { ...context } - context.error = error - delete context.result - } - - return runHook(hook, context, 'error').then(() => { - if (context.result === undefined && context.error !== undefined) { - throw context.error - } - }) - }) - } -} - -const RunHooks = - (hooks: RegularHookFunction[]) => - (context: any) => { - return hooks.reduce((promise, hook) => { - return promise.then(() => runHook(hook, context)) - }, Promise.resolve(undefined)) - } - -export function fromBeforeHooks(hooks: RegularHookFunction[]) { - return fromBeforeHook(RunHooks(hooks)) -} - -export function fromAfterHooks(hooks: RegularHookFunction[]) { - return fromAfterHook(RunHooks(hooks)) -} - -export function fromErrorHooks(hooks: RegularHookFunction[]) { - return fromErrorHook(RunHooks(hooks)) -} - -export function collectRegularHooks(target: any, method: string) { - return target.__hooks.hooks[method] || [] -} - -// Converts different hook registration formats into the -// same internal format -export function convertHookData(input: any) { - const result: { [method: string]: RegularHookFunction[] } = {} - - 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 -} - -type RegularType = 'before' | 'after' | 'error' - -type RegularMap = { [type in RegularType]: ReturnType } - -type RegularAdapter = AroundHookFunction & { hooks: RegularHookFunction[] } - -type RegularStore = { - before: { [method: string]: RegularAdapter } - after: { [method: string]: RegularAdapter } - error: { [method: string]: RegularAdapter } - hooks: { [method: string]: AroundHookFunction[] } -} - -const types: RegularType[] = ['before', 'after', 'error'] - -const isType = (value: any): value is RegularType => types.includes(value) - -const wrappers = { - before: fromBeforeHooks, - after: fromAfterHooks, - error: fromErrorHooks -} - -const createStore = (methods: string[]) => { - const store: RegularStore = { - before: {}, - after: {}, - error: {}, - hooks: {} - } - - for (const method of methods) { - store.hooks[method] = [] - } - - return store -} - -const setStore = (object: any, store: RegularStore) => { - Object.defineProperty(object, '__hooks', { - configurable: true, - value: store, - writable: true - }) -} - -const getStore = (object: any): RegularStore => object.__hooks - -const createMap = (input: HookMap, methods: string[]) => { - const map = {} as RegularMap - - Object.keys(input).forEach((type) => { - if (!isType(type)) { - throw new Error(`'${type}' is not a valid hook type`) - } - - const data = convertHookData(input[type]) - - Object.keys(data).forEach((method) => { - if (method !== 'all' && !methods.includes(method) && !defaultServiceMethods.includes(method)) { - throw new Error(`'${method}' is not a valid hook method`) - } - }) - - map[type] = data - }) - - return map -} - -const createAdapter = (type: RegularType) => { - const hooks: RegularHookFunction[] = [] - const hook = wrappers[type](hooks) - const adapter = Object.assign(hook, { hooks }) - - return adapter -} - -const updateStore = (store: RegularStore, map: RegularMap) => { - Object.keys(store.hooks).forEach((method) => { - let adapted = false - - Object.keys(map).forEach((key) => { - const type = key as RegularType - const allHooks = map[type].all || [] - const methodHooks = map[type][method] || [] - - if (allHooks.length || methodHooks.length) { - const adapter = (store[type][method] ||= ((adapted = true), createAdapter(type))) - - adapter.hooks.push(...allHooks, ...methodHooks) - } - }) - - if (adapted) { - store.hooks[method] = [store.error[method], store.before[method], store.after[method]].filter( - (hook) => hook - ) - } - }) -} - -// Add `.hooks` functionality to an object -export function enableRegularHooks(object: any, methods: string[] = defaultServiceMethods) { - const store = createStore(methods) - - setStore(object, store) - - return function regularHooks(this: any, input: HookMap) { - const store = getStore(this) - const map = createMap(input, methods) - - updateStore(store, map) - - return this - } -} diff --git a/packages/feathers/src/index.ts b/packages/feathers/src/index.ts index 1febd548ec..c37bfef951 100644 --- a/packages/feathers/src/index.ts +++ b/packages/feathers/src/index.ts @@ -11,7 +11,7 @@ export function feathers() { feathers.setDebug = setDebug export { version, Feathers } -export * from './hooks/index' +export * from './hooks' export * from './declarations' export * from './service' diff --git a/packages/feathers/src/service.ts b/packages/feathers/src/service.ts index 8dfe5ed4b9..b62562e9bc 100644 --- a/packages/feathers/src/service.ts +++ b/packages/feathers/src/service.ts @@ -24,7 +24,7 @@ export const defaultEventMap = { export const protectedMethods = Object.keys(Object.prototype) .concat(Object.keys(EventEmitter.prototype)) - .concat(['all', 'before', 'after', 'error', 'hooks', 'setup', 'teardown', 'publish']) + .concat(['all', 'around', 'before', 'after', 'error', 'hooks', 'setup', 'teardown', 'publish']) export function getHookMethods(service: any, options: ServiceOptions) { const { methods } = options diff --git a/packages/feathers/test/hooks/async.test.ts b/packages/feathers/test/hooks/around.test.ts similarity index 83% rename from packages/feathers/test/hooks/async.test.ts rename to packages/feathers/test/hooks/around.test.ts index b124e92134..5a1a209b9b 100644 --- a/packages/feathers/test/hooks/async.test.ts +++ b/packages/feathers/test/hooks/around.test.ts @@ -1,8 +1,8 @@ import assert from 'assert' import { feathers, Params, ServiceInterface } from '../../src' -describe('`async` hooks', () => { - it('async hooks can set hook.result which will skip service method', async () => { +describe('`around` hooks', () => { + it('around hooks can set hook.result which will skip service method', async () => { const app = feathers().use('/dummy', { async get() { assert.ok(false, 'This should never run') @@ -31,6 +31,60 @@ describe('`async` hooks', () => { }) }) + it('works with traditional registration format, all syntax and app hooks', async () => { + const app = feathers().use('/dummy', { + async get() { + assert.ok(false, 'This should never run') + } + }) + const service = app.service('dummy') + + app.hooks([ + async function (this: any, hook, next) { + hook.result = { + id: hook.id, + app: 'Set from app around all' + } + + await next() + } + ]) + + service.hooks({ + around: { + all: [ + async (hook, next) => { + hook.result = { + ...hook.result, + all: 'Set from around all' + } + + await next() + } + ], + get: [ + async (hook, next) => { + hook.result = { + ...hook.result, + get: 'Set from around get' + } + + await next() + } + ] + } + }) + + const data = await service.get(10, {}) + + assert.deepStrictEqual(data, { + id: 10, + app: 'Set from app around all', + all: 'Set from around all', + get: 'Set from around get' + }) + }) + it('gets mixed into a service and modifies data', async () => { const dummyService = { async create(data: any, params: any) { @@ -211,7 +265,7 @@ describe('`async` hooks', () => { await service.create({ some: 'thing' }) }) - it('async hooks run in the correct order', async () => { + it('around hooks run in the correct order', async () => { interface DummyParams extends Params<{ name: string }> { items: string[] } @@ -255,7 +309,7 @@ describe('`async` hooks', () => { await service.get(10) }) - it('async all hooks (#11)', async () => { + it('around all hooks (#11)', async () => { interface DummyParams extends Params { asyncAllObject: boolean asyncAllMethodArray: boolean @@ -301,7 +355,7 @@ describe('`async` hooks', () => { await service.find() }) - it('async hooks have service as context and keep it in service method (#17)', async () => { + it('around hooks have service as context and keep it in service method (#17)', async () => { interface DummyParams extends Params { test: number } diff --git a/packages/schema/package.json b/packages/schema/package.json index d95b138283..515fc16489 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -56,7 +56,7 @@ "@feathersjs/errors": "^5.0.0-pre.22", "@feathersjs/feathers": "^5.0.0-pre.22", "@feathersjs/commons": "^5.0.0-pre.22", - "@feathersjs/hooks": "^0.7.4", + "@feathersjs/hooks": "^0.7.5", "@types/json-schema": "^7.0.11", "ajv": "^8.11.0", "json-schema": "^0.4.0",