diff --git a/package-lock.json b/package-lock.json index 20ef354..22d8c48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2070,9 +2070,9 @@ "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==" }, "node_modules/@types/eslint": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.2.tgz", - "integrity": "sha512-KubbADPkfoU75KgKeKLsFHXnU4ipH7wYg0TRT33NK3N3yiu7jlFAAoygIWBV+KbuHx/G+AvuGX6DllnK35gfJA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.0.tgz", + "integrity": "sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2115,9 +2115,9 @@ "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==" }, "node_modules/@types/node": { - "version": "16.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", - "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==" + "version": "16.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", + "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.0", @@ -9892,9 +9892,9 @@ } }, "node_modules/terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", + "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", "dependencies": { "commander": "^2.20.0", "source-map": "~0.7.2", @@ -9905,15 +9905,22 @@ }, "engines": { "node": ">=10" + }, + "peerDependencies": { + "acorn": "^8.5.0" + }, + "peerDependenciesMeta": { + "acorn": { + "optional": true + } } }, "node_modules/terser-webpack-plugin": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz", - "integrity": "sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz", + "integrity": "sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==", "dependencies": { "jest-worker": "^27.0.6", - "p-limit": "^3.1.0", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", "source-map": "^0.6.1", @@ -9941,20 +9948,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/terser/node_modules/source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", @@ -10313,9 +10306,9 @@ } }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10552,9 +10545,9 @@ } }, "node_modules/webpack": { - "version": "5.61.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.61.0.tgz", - "integrity": "sha512-fPdTuaYZ/GMGFm4WrPi2KRCqS1vDp773kj9S0iI5Uc//5cszsFEDgHNaX4Rj1vobUiU1dFIV3mA9k1eHeluFpw==", + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.1.tgz", + "integrity": "sha512-b4FHmRgaaAjP+aVOVz41a9Qa5SmkUPQ+u8FntTQ1roPHahSComB6rXnLwc976VhUY4CqTaLu5mCswuHiNhOfVw==", "dependencies": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.50", @@ -10579,7 +10572,7 @@ "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", "watchpack": "^2.2.0", - "webpack-sources": "^3.2.0" + "webpack-sources": "^3.2.2" }, "bin": { "webpack": "bin/webpack.js" @@ -10684,9 +10677,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.1.tgz", - "integrity": "sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", + "integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==", "engines": { "node": ">=10.13.0" } @@ -11088,9 +11081,9 @@ } }, "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", "engines": { "node": ">=10" }, @@ -12887,9 +12880,9 @@ "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==" }, "@types/eslint": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.2.tgz", - "integrity": "sha512-KubbADPkfoU75KgKeKLsFHXnU4ipH7wYg0TRT33NK3N3yiu7jlFAAoygIWBV+KbuHx/G+AvuGX6DllnK35gfJA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.0.tgz", + "integrity": "sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ==", "requires": { "@types/estree": "*", "@types/json-schema": "*" @@ -12932,9 +12925,9 @@ "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==" }, "@types/node": { - "version": "16.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", - "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==" + "version": "16.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", + "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -19141,9 +19134,9 @@ } }, "terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", + "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", "requires": { "commander": "^2.20.0", "source-map": "~0.7.2", @@ -19158,26 +19151,15 @@ } }, "terser-webpack-plugin": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz", - "integrity": "sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz", + "integrity": "sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==", "requires": { "jest-worker": "^27.0.6", - "p-limit": "^3.1.0", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", "source-map": "^0.6.1", "terser": "^5.7.2" - }, - "dependencies": { - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - } } }, "test-exclude": { @@ -19446,9 +19428,9 @@ } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==" + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==" }, "uglify-js": { "version": "3.13.4", @@ -19641,9 +19623,9 @@ "dev": true }, "webpack": { - "version": "5.61.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.61.0.tgz", - "integrity": "sha512-fPdTuaYZ/GMGFm4WrPi2KRCqS1vDp773kj9S0iI5Uc//5cszsFEDgHNaX4Rj1vobUiU1dFIV3mA9k1eHeluFpw==", + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.1.tgz", + "integrity": "sha512-b4FHmRgaaAjP+aVOVz41a9Qa5SmkUPQ+u8FntTQ1roPHahSComB6rXnLwc976VhUY4CqTaLu5mCswuHiNhOfVw==", "requires": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.50", @@ -19668,7 +19650,7 @@ "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", "watchpack": "^2.2.0", - "webpack-sources": "^3.2.0" + "webpack-sources": "^3.2.2" } }, "webpack-cli": { @@ -19725,9 +19707,9 @@ } }, "webpack-sources": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.1.tgz", - "integrity": "sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", + "integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==" }, "whatwg-url": { "version": "8.5.0", @@ -20077,9 +20059,9 @@ }, "dependencies": { "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==" }, "decamelize": { "version": "4.0.0", diff --git a/packages/hooks/src/base.ts b/packages/hooks/src/base.ts index 9441e91..8194fa5 100644 --- a/packages/hooks/src/base.ts +++ b/packages/hooks/src/base.ts @@ -1,4 +1,4 @@ -import { Middleware } from './compose'; +import { AsyncMiddleware } from './compose'; import { copyToSelf, copyProperties } from './utils'; export const HOOKS: string = Symbol('@feathersjs/hooks') as any; @@ -30,7 +30,7 @@ export type HookDefaultsInitializer = (self?: any, args?: any[], context?: HookC export class HookManager { _parent?: this|null = null; _params: string[]|null = null; - _middleware: Middleware[]|null = null; + _middleware: AsyncMiddleware[]|null = null; _props: HookContextData|null = null; _defaults?: HookDefaultsInitializer; @@ -40,13 +40,13 @@ export class HookManager { return this; } - middleware (middleware?: Middleware[]) { + middleware (middleware?: AsyncMiddleware[]) { this._middleware = middleware?.length ? middleware : null; return this; } - getMiddleware (): Middleware[]|null { + getMiddleware (): AsyncMiddleware[]|null { const previous = this._parent?.getMiddleware(); if (previous && this._middleware) { @@ -56,7 +56,7 @@ export class HookManager { return previous || this._middleware; } - collectMiddleware (self: any, _args: any[]): Middleware[] { + collectMiddleware (self: any, _args: any[]): AsyncMiddleware[] { const otherMiddleware = getMiddleware(self); const middleware = this.getMiddleware(); @@ -178,7 +178,7 @@ export class HookManager { } } -export type HookOptions = HookManager|Middleware[]|null; +export type HookOptions = HookManager|AsyncMiddleware[]|null; export function convertOptions (options: HookOptions = null) { if (!options) { @@ -200,13 +200,13 @@ export function setManager (target: T, manager: HookManager) { return target; } -export function getMiddleware (target: any): Middleware[]|null { +export function getMiddleware (target: any): AsyncMiddleware[]|null { const manager = getManager(target); return manager ? manager.getMiddleware() : null; } -export function setMiddleware (target: T, middleware: Middleware[]) { +export function setMiddleware (target: T, middleware: AsyncMiddleware[]) { const manager = new HookManager().middleware(middleware); return setManager(target, manager); diff --git a/packages/hooks/src/compose.ts b/packages/hooks/src/compose.ts index 7fcf83b..f486b7e 100644 --- a/packages/hooks/src/compose.ts +++ b/packages/hooks/src/compose.ts @@ -1,9 +1,10 @@ // TypeScript port of koa-compose (https://github.com/koajs/compose) export type NextFunction = () => Promise; -export type Middleware = (context: T, next: NextFunction) => Promise; +export type AsyncMiddleware = (context: T, next: NextFunction) => Promise; +export type Middleware = AsyncMiddleware; -export function compose (middleware: Middleware[]) { +export function compose (middleware: AsyncMiddleware[]) { if (!Array.isArray(middleware)) { throw new TypeError('Middleware stack must be an array!'); } @@ -14,7 +15,7 @@ export function compose (middleware: Middleware[]) { } } - return function (this: any, context: T, next?: Middleware) { + return function (this: any, context: T, next?: AsyncMiddleware) { // last called middleware # let index: number = -1; @@ -27,7 +28,7 @@ export function compose (middleware: Middleware[]) { index = i; - let fn: Middleware|undefined = middleware[i]; + let fn: AsyncMiddleware|undefined = middleware[i]; if (i === middleware.length) { fn = next; diff --git a/packages/hooks/src/hooks.ts b/packages/hooks/src/hooks.ts index 1a02fa4..a587f93 100644 --- a/packages/hooks/src/hooks.ts +++ b/packages/hooks/src/hooks.ts @@ -1,4 +1,4 @@ -import { compose, Middleware } from './compose'; +import { compose, AsyncMiddleware } from './compose'; import { HookContext, setManager, HookContextData, HookOptions, convertOptions, setMiddleware } from './base'; @@ -23,7 +23,7 @@ export function functionHooks (fn: F, managerOrMiddleware: HookOptions) { // Initialize the context const context = manager.initializeContext(this, args, base); // Assemble the hook chain - const hookChain: Middleware[] = [ + const hookChain: AsyncMiddleware[] = [ // Return `ctx.result` or the context (ctx, next) => next().then(() => returnContext ? ctx : ctx.result) ]; @@ -68,7 +68,7 @@ export type HookMap = { [L in keyof O]?: HookOptions; } -export function objectHooks (_obj: any, hooks: HookMap|Middleware[]) { +export function objectHooks (_obj: any, hooks: HookMap|AsyncMiddleware[]) { const obj = typeof _obj === 'function' ? _obj.prototype : _obj; if (Array.isArray(hooks)) { diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index a12bf5a..a130e8a 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -1,4 +1,4 @@ -import { Middleware } from './compose'; +import { AsyncMiddleware } from './compose'; import { HookManager, HookContextData, HookContext, HookContextConstructor, HookOptions } from './base'; @@ -7,6 +7,7 @@ import { functionHooks, hookDecorator, objectHooks, HookMap } from './hooks'; export * from './hooks'; export * from './compose'; export * from './base'; +export * from './regular'; export interface WrapperAddon { original: F; @@ -27,7 +28,7 @@ export type MiddlewareOptions = { * @param mw The list of middleware * @param options Middleware options (params, default, props) */ -export function middleware (mw?: Middleware[], options?: MiddlewareOptions) { +export function middleware (mw?: AsyncMiddleware[], options?: MiddlewareOptions) { const manager = new HookManager().middleware(mw); if (options) { @@ -66,7 +67,7 @@ export function hooks ( * @param hookMap A map of middleware settings where the * key is the method name. */ -export function hooks (obj: O|(new (...args: any[]) => O), hookMap: HookMap|Middleware[]): O; +export function hooks (obj: O|(new (...args: any[]) => O), hookMap: HookMap|AsyncMiddleware[]): O; /** * Decorate a class method with hooks. diff --git a/packages/hooks/src/regular.ts b/packages/hooks/src/regular.ts new file mode 100644 index 0000000..85ac4ff --- /dev/null +++ b/packages/hooks/src/regular.ts @@ -0,0 +1,58 @@ +import { compose } from './compose'; +import { HookContext } from './base'; + +export type RegularMiddleware = (context: T) => Promise | any; +export interface RegularHookMap { + before?: RegularMiddleware[], + after?: RegularMiddleware[], + error?: RegularMiddleware[] +} + +const runHook = (hook: RegularMiddleware, 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: RegularMiddleware) { + return (context: any, next: any) => { + return runHook(hook, context, 'before').then(next); + }; +} + +export function fromAfterHook (hook: RegularMiddleware) { + return (context: any, next: any) => { + return next().then(() => runHook(hook, context, 'after')); + } +} + +export function fromErrorHook (hook: RegularMiddleware) { + return (context: any, next: any) => { + return next().catch((error: any) => { + if (context.error !== error || context.result !== undefined) { + (context as any).original = { ...context }; + context.error = error; + delete context.result; + } + + return runHook(hook, context, 'error').then(() => { + if (context.result === undefined && context.error !== undefined) { + throw context.error; + } + }); + }); + } +} + +export function collect ({ before = [], after = [], error = [] }: RegularHookMap) { + const beforeHooks = before.map(fromBeforeHook); + const afterHooks = [...after].reverse().map(fromAfterHook); + const errorHooks: any = error.map(fromErrorHook); + + return compose([...errorHooks, ...afterHooks, ...beforeHooks]); +} \ No newline at end of file diff --git a/packages/hooks/test/collect.test.ts b/packages/hooks/test/collect.test.ts new file mode 100644 index 0000000..c5508f8 --- /dev/null +++ b/packages/hooks/test/collect.test.ts @@ -0,0 +1,138 @@ +import * as assert from 'assert'; +import { hooks, collect, HookContext, NextFunction, middleware } from '../src'; + +describe('collect utility for regular hooks', () => { + it('collects hooks in order', async () => { + class DummyClass { + async create (data: any) { + data.id = 1; + return data; + } + } + hooks(DummyClass, { + create: middleware([ + collect({ + before: [ + (ctx: any) => { + ctx.data.log.push('collect-1 : before : 1'); + }, + (ctx: any) => { + ctx.data.log.push('collect-1 : before : 2'); + } + ], + after: [ + (ctx: any) => { + ctx.data.log.push('collect-1 : after : 1'); + }, + (ctx: any) => { + ctx.data.log.push('collect-1 : after : 2'); + } + ], + error: [] + }), + async (ctx: HookContext, next: NextFunction) => { + ctx.data.log.push('async : before'); + await next(); + ctx.data.log.push('async : after'); + }, + collect({ + before: [ + (ctx: any) => { + ctx.data.log.push('collect-2 : before : 3'); + }, + (ctx: any) => { + ctx.data.log.push('collect-2 : before : 4'); + } + ], + after: [ + (ctx: any) => { + ctx.data.log.push('collect-2 : after : 3'); + }, + (ctx: any) => { + ctx.data.log.push('collect-2 : after : 4'); + } + ], + error: [] + }) + ]).params('data') + }); + + const service = new DummyClass(); + const value = await service.create({ name: 'David', log: [] }); + + assert.deepStrictEqual(value.log, [ + 'collect-1 : before : 1', + 'collect-1 : before : 2', + 'async : before', + 'collect-2 : before : 3', + 'collect-2 : before : 4', + 'collect-2 : after : 3', + 'collect-2 : after : 4', + 'async : after', + 'collect-1 : after : 1', + 'collect-1 : after : 2' + ]); + }); + + it('error hooks', async () => { + class DummyClass { + async create (name: string) { + if (name !== 'after') { + throw new Error(`Error in method with ${name}`); + } + } + } + + const collection = collect({ + before: [ + ctx => { + if (ctx.arguments[0] === 'before') { + throw new Error('in before hook'); + } + } + ], + after: [ + ctx => { + if (ctx.arguments[0] === 'after') { + throw new Error('in after hook'); + } + } + ], + error: [ + ctx => { + if (ctx.arguments[0] === 'error') { + throw new Error('in error hook'); + } + + if (ctx.arguments[0] === 'result') { + ctx.result = 'result from error hook'; + } + } + ] + }); + + hooks(DummyClass, { + create: middleware([collection]).params('data') + }); + + const service = new DummyClass(); + + await assert.rejects(() => service.create('test'), { + message: 'Error in method with test' + }); + + await assert.rejects(() => service.create('before'), { + message: 'in before hook' + }); + + await assert.rejects(() => service.create('after'), { + message: 'in after hook' + }); + + await assert.rejects(() => service.create('error'), { + message: 'in error hook' + }); + + assert.strictEqual(await service.create('result'), 'result from error hook'); + }); +}); diff --git a/readme.md b/readme.md index 8def8c7..6385845 100644 --- a/readme.md +++ b/readme.md @@ -3,17 +3,52 @@ [![Node CI](https://github.com/feathersjs/hooks/workflows/Node%20CI/badge.svg)](https://github.com/feathersjs/hooks/actions?query=workflow%3A%22Node+CI%22) [![Deno CI](https://github.com/feathersjs/hooks/actions/workflows/deno.yml/badge.svg)](https://github.com/feathersjs/hooks/actions/workflows/deno.yml) -`@feathersjs/hooks` brings middleware to any async JavaScript or TypeScript function. It allows to create composable and reusable workflows that can add +`@feathersjs/hooks` brings middleware-like functionality to any async JavaScript or TypeScript function. It allows creation of composable and reusable workflows to handle functionality like -- Logging +- Logging - Profiling - Validation -- Caching/Debouncing +- Caching / Debouncing - Permissions -- Data pre- and postprocessing +- Data pre- and post-processing - etc. -To a function or class without having to change its original code while also keeping everything cleanly separated and testable. See the [⚓ release post for a quick overview](https://blog.feathersjs.com/async-middleware-for-javascript-and-typescript-31a0f74c0d30). +This functionality can be added without having to change the original function. The pattern also keeps everything cleanly separated and testable. + +```ts +import { hooks, middleware } from '@feathersjs/hooks') + +// We're going to wrap `sayHi` with hook middleware. +class Hello { + async sayHi (name) { + return `Hi ${name}` + } +} + +// This logRuntime hook will be used as middleware +const logRuntime = async (context, next) => { + const start = new Date().getTime(); + + await next(); // In this example, `next` is `sayHi`. + + const duration = new Date().getTime() - start; + console.log(`Function '${context.method || '[no name]'}' returned '${context.result}' after ${duration}ms`); +} + +// The `hooks` utility wraps `logRuntime` around `sayHi`. +hooks(Hello, { sayHi: middleware([ logRuntime ]) }); + +// Calling `sayHi` will start by calling the `logRuntime` hook. +(async () => { + const hi = new Hello(); + + console.log(await hi.sayHi('Dave')); +})(); +``` + +The Hooks middleware pattern was originally implemented directly in [FeathersJS](https://www.feathersjs.com). Having been recognized as a powerful pattern with more broad-scale usefulness, it has been extracted from FeathersJS into this standalone utility. + +See the [⚓ release post for a quick overview](https://blog.feathersjs.com/async-middleware-for-javascript-and-typescript-31a0f74c0d30). @@ -21,28 +56,33 @@ To a function or class without having to change its original code while also kee - [Node](#node) - [Deno](#deno) - [Browser](#browser) -- [Quick Example](#quick-example) - - [JavaScript](#javascript) - - [TypeScript](#typescript) - [Documentation](#documentation) - - [Middleware](#middleware) - - [Function hooks](#function-hooks) - - [Object hooks](#object-hooks) - - [Class hooks](#class-hooks) - - [JavaScript](#javascript-1) - - [TypeScript](#typescript-1) + - [Intro to Async Hooks](#intro-to-async-hooks) + - [The `hooks` Function](#the-hooks-function) + - [with a Function](#example-with-a-function) + - [with a Class or Object](#example-with-a-class) + - [`@hooks` Decorator](#the-hooks-decorator) + - [The `middleware` Manager](#the-middleware-manager) + - [params(...names)](#paramsnames) + - [props(properties)](#propsproperties) + - [defaults(callback)](#defaultscallback) + - [Global hooks](#global-hooks) + - [on an Object](#global-hooks-on-an-object) + - [on a Class](#global-hooks-on-a-class) + - [JavaScript Example](#global-hooks---javascript-example) + - [TypeScript Example](#global-hooks---typescript-example) - [Hook Context](#hook-context) - [Context properties](#context-properties) - [Arguments](#arguments) - [Using named parameters](#using-named-parameters) - [Default values](#default-values) - [Modifying the result](#modifying-the-result) - - [Calling the original](#calling-the-original) + - [Calling the original function](#calling-the-original-function) - [Customizing and returning the context](#customizing-and-returning-the-context) - - [Options](#options) - - [params(...names)](#paramsnames) - - [props(properties)](#propsproperties) - - [defaults(callback)](#defaultscallback) + - [Flow Control with Multiple Hooks](#flow-control-with-multiple-hooks) + - [Async Hook Flow](#async-hook-flow) + - [Regular Hooks](#regular-hooks) + - [The `collect` utility](#the-collect-utility) - [Best practises](#best-practises) - [More Examples](#more-examples) - [Cache](#cache) @@ -56,7 +96,7 @@ To a function or class without having to change its original code while also kee ## Node -``` +```shell npm install @feathersjs/hooks --save yarn add @feathersjs/hooks ``` @@ -65,7 +105,7 @@ yarn add @feathersjs/hooks feathersjs/hooks releases are published to [deno.land/x/hooks](https://deno.land/x/hooks): -```js +```ts import { hooks } from 'https://deno.land/x/hooks@x.x.x/deno/index.ts'; ``` @@ -79,63 +119,103 @@ import { hooks } from 'https://deno.land/x/hooks@x.x.x/deno/index.ts'; Which will make a `hooks` global variable available. -# Quick Example +# Documentation -## JavaScript +## Intro to Async Hooks -The following example logs information about a function call: +The fundamental building block of `@feathersjs/hooks` is the "Async Hook". An "Async Hook" is an `async` function that accepts two arguments: -```js -const { hooks } = require('@feathersjs/hooks'); +- A [`context` object](#hook-context) containing the arguments for the function call. +- An asynchronous `next` function. Somewhere in the body of a hook function, there is a call to `await next()`, which calls the `next` hook OR the original function if all other hooks have run. -const logRuntime = async (context, next) => { - const start = new Date().getTime(); +In its simplest form, an Async Hook looks like this: +```js +const myAsyncHook = async (context, next) => { + // Code before `await next()` runs before the main function await next(); + // Code after `await next()` runs after the main function. +} +``` - const end = new Date().getTime(); +Any Async Hook can be wrapped around another function, essentially becoming a middleware function. Calling `await next()` will either call the next middleware in the chain or the original function if all middleware have run. In the next section you'll learn how to wrap hooks around other functions. - console.log(`Function '${context.method || '[no name]'}' returned '${context.result}' after ${end - start}ms`); +## The `hooks` Function + +`hooks(fn, middleware[]|manager)` returns a new function that wraps `fn` with `middleware` + +The `hooks` function wraps one or more [Async Hooks](#intro-to-async-hooks) around another function, setting up the hooks as middleware. The following examples all show the default functionality of passing an array of hooks as the second argument. Learn about additional functionality in the section about [Middleware Managers](#the-middleware-manager) + +### Example with a Function + +The example below demonstrates the concept of wrapping the `make_request` function with the `verify_auth` hook function. + +```ts +import { hooks } from '@feathersjs/hooks' + +const make_request = () => { /* make a request to the database server */ } + +const verify_auth = (context, next) => { + /* Do auth verification before calling `await next()` */ + await next() } -// Hooks can be used with a function like this: -const sayHello = hooks(async name => { - return `Hello ${name}!`; -}, [ - logRuntime -]); +const request_with_middleware = hooks(make_request, [verify_auth]) +``` + +In the above example, calling `request_with_middleware` will call the `verify_auth` function before calling `make_request`. The `verify_auth` function will have a `context.arguments` array containing the original arguments for the function call. A hook can modify the context object before calling `await next()`. (In this case, the `next` function IS the `make_request` function.) Alternatively, `verify_auth` could throw an error to prevent the request from ever getting to the `make_request` function. Check the [hook context](#hook-context) section to learn how to turn the `context.arguments` array into named parameters. + +> __Important:__ A wrapped function will _always_ return a Promise even if it was not originally `async`. + +We've seen how to wrap a single function, but the `hooks` utility is more powerful. It can also wrap [object methods](#object-hooks) and [class methods](#class-hooks). The following example shows how to use it with a class. + +### Example with a Class -// And on an object or class like this +The following example updates a class's `sayHi` method to log information about a function call. This syntax also works on plain objects. + +```js +const { hooks } = require('@feathersjs/hooks'); + +// This class has a `sayHi` instance method we're going to wrap with hooks. +// This would also work with an object containing a `sayHi` method. class Hello { async sayHi (name) { return `Hi ${name}` } } -hooks(Hello, { - sayHi: [ - logRuntime - ] -}); +// This logRuntime hook will be used as middleware +const logRuntime = async (context, next) => { + // Code before `await next()` runs before the original function + const start = new Date().getTime(); -(async () => { - console.log(await sayHello('David')); + await next(); + + // Code after `await next()` runs after the original function. + const end = new Date().getTime(); + console.log(`Function '${context.method || '[no name]'}' returned '${context.result}' after ${end - start}ms`); +} + +// Enhance class (or object) methods using an object of method names as the 2nd argument +hooks(Hello, { sayHi: [ logRuntime ] }); +// You can now use the wrapped instance methods inside any async function. +(async () => { const hi = new Hello(); console.log(await hi.sayHi('Dave')); })(); ``` -## TypeScript +### The `@hooks` Decorator -In addition to the normal JavaScript use, with the `experimentalDecorators` option in `tsconfig.json` enabled +With TypeScript, you can use `hooks` the same was as shown in the above JavaScript example, or you can use decorators. Using decorators requires the `experimentalDecorators` option in `tsconfig.json` to be enabled. ```json "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ ``` -Hooks can be registered using a decorator: +Now hooks can be registered using the `@hooks` decorator: ```ts import { hooks, HookContext, NextFunction } from '@feathersjs/hooks'; @@ -146,14 +226,11 @@ const logRuntime = async (context: HookContext, next: NextFunction) => { await next(); const end = new Date().getTime(); - console.log(`Function '${context.method || '[no name]'}' returned '${context.result}' after ${end - start}ms`); } class Hello { - @hooks([ - logRuntime - ]) + @hooks([ logRuntime ]) // the @hooks decorator async sayHi (name: string) { return `Hi ${name}`; } @@ -166,123 +243,73 @@ class Hello { })(); ``` -# Documentation - -## Middleware +## The `middleware` Manager -Middleware functions (or hook functions) take a `context` and an asynchronous `next` function as their parameters. The `context` contains information about the function call (like the arguments, the result or `this` context) and the `next` function can be called to continue to the next hook or the original function. +You can use a `middleware` manager, instead of a plain array of hook functions, to enable additional functionality. -A middleware function can do things before calling `await next()` and after all following middleware functions and the function call itself return. It can also `try/catch` the `await next()` call to handle and modify errors. This is the same control flow that the web framework [KoaJS](https://koajs.com/) uses for handling HTTP requests and response. +In all previous examples, when calling `hooks` with an array in the second argument ﹣ either directly like `hooks(someFn, [])` or as the value of an object key like `hooks(someObj, { prop: [] })` ﹣ the array gets wrapped into an internal middleware `Manager`. -Each hook function wraps _around_ all other functions (like an onion). This means that the first registered middleware function will run first before `await next()` and as the very last after all following hooks. +The `middleware` function creates a middleware Manager which has three important methods: -![Feathers hooks image](https://user-images.githubusercontent.com/338316/72454734-44e8d680-3776-11ea-90ed-c81b2d98e8e5.png) - -The following example: +- `params()` +- `props()` +- `defaults()` ```js -const { hooks } = require('@feathersjs/hooks'); - -const sayHello = async message => { - console.log(`Hello ${message}!`) -}; - -const hook1 = async (ctx, next) => { - console.log('hook1 before'); - await next(); - console.log('hook1 after') -} - -const hook2 = async (ctx, next) => { - console.log('hook2 before'); - await next(); - console.log('hook2 after') -} - -const hook3 = async (ctx, next) => { - console.log('hook3 before'); - await next(); - console.log('hook3 after') -} +const { hooks, middleware } = require('@feathersjs/hooks'); -const sayHelloWithHooks = hooks(sayHello, [ - hook1, - hook2, - hook3 -]); +const sayHiWithHooks = hooks(sayHi, middleware([ hook1, hook2, hook3 ])); (async () => { - await sayHelloWithHooks('David'); + await sayHiWithHooks('David'); })(); ``` -Would print: - -``` -hook1 before -hook2 before -hook3 before -Hello David -hook3 after -hook2 after -hook1 after -``` - -This order also applies when using hooks on [objects](#object-hooks) and [classes and with inheritance](#class-hooks). +### params(...names) -## Function hooks - -`hooks(fn, middleware[]|manager)` returns a new function that wraps `fn` with `middleware` +Supplies names for original function arguments. Instead of appearing in `params.arguments`, the arguments will be named in the order provided. ```js -const { hooks, middleware } = require('@feathersjs/hooks'); +const sayHiWithHooks = hooks(sayHi, + middleware([ hook1, hook2, hook3 ]).params('name', 'age') +); +``` -const sayHello = async name => { - return `Hello ${name}!`; -}; +### props(properties) -const wrappedSayHello = hooks(sayHello, middleware([ - async (context, next) => { - console.log(context.someProperty); - await next(); - } -]).params('name')); +Initializes properties on the `context` -(async () => { - console.log(await wrappedSayHello('David')); -})(); +```js +const sayHiWithHooks = hooks(sayHi, + middleware([ hook1, hook2, hook3 ]).params('name').props({ customProperty: true }) +); ``` -> __Important:__ A wrapped function will _always_ return a Promise even it was not originally `async`. +> __Note:__ `.props` must not contain any of the field names defined in `.params`. -## Object hooks +### defaults(callback) -`hooks(obj, middlewareMap)` takes an object and wraps the functions indicated in `middlewareMap`. It will modify the existing Object `obj`: +Calls a `callback(self, arguments, context)` that returns default values which will be set if the property on the hook context is `undefined`. Applies to both, `params` and other properties. ```js -const { hooks, middleware } = require('@feathersjs/hooks'); +const sayHi = async name => `Hello ${name}`; + +const sayHiWithHooks = hooks(sayHi, + middleware([]) + .params('name') + .defaults((self, args, context) => { + return { + name: 'Unknown human' + } + }) +); +``` -const o = { - async sayHi (name, quote) { - return `Hi ${name} ${quote}`; - } +## Global Hooks - async sayHello (name) { - return `Hello ${name}!`; - } -} +Sometimes you want to run a set of hooks on all of the methods in a class or object. -hooks(o, { - sayHello: [ logRuntime ], - sayHi: [ logRuntime ] -}); - -// With additional options -hooks(o, { - sayHello: middleware([ logRuntime ]).params('name', 'quote'), - sayHi: middleware([ logRuntime ]).params('name') -}); -``` +### Global Hooks on an Object Hooks can also be registered at the object level which will run before any specific hooks on a hook enabled function: @@ -293,32 +320,36 @@ const o = { async sayHi (name) { return `Hi ${name}!`; } - async sayHello (name) { return `Hello ${name}!`; } } -// This hook will run first for every hook enabled method on the object +// This hook will run first for every hook-enabled method on the object hooks(o, [ async (context, next) => { console.log('Top level hook'); await next(); } ]); +// The global hooks only run if you enable hooks on the method: +hooks(o, { + sayHello: middleware([ logRuntime ]).params('name', 'quote'), + sayHi: [] +}); hooks(o, { sayHi: [ logRuntime ] }); ``` -## Class hooks +### Global Hooks on a Class Similar to object hooks, class hooks modify the class (or class prototype). Just like for objects it is possible to register hooks that are global to the class or object. Registering hooks also works with inheritance. > __Note:__ Object or class level global hooks will only run if the method itself has been enabled for hooks. This can be done by registering hooks with an empty array. -### JavaScript +#### Global Hooks - JavaScript Example ```js const { hooks } = require('@feathersjs/hooks'); @@ -336,7 +367,7 @@ class HappyHelloSayer extends HelloSayer { } } -// To add hooks at the class level we need to use the prototype object +// Add global hooks to the class using its prototype hooks(HelloSayer.prototype, [ async (context, next) => { console.log('Hook on HelloSayer'); @@ -351,7 +382,7 @@ hooks(HappyHelloSayer.prototype, [ } ]); -// Methods can also be wrapped directly on the class +// Enabling hooks on sayHello also allows the global hooks to run. hooks(HelloSayer, { sayHello: [async (context, next) => { console.log('Hook on HelloSayer.sayHello'); @@ -366,7 +397,7 @@ hooks(HelloSayer, { })(); ``` -### TypeScript +#### Global Hooks - TypeScript Example Using decorators in TypeScript also respects inheritance: @@ -466,16 +497,6 @@ const sayHello = async (firstName, lastName) => { return `Hello ${firstName} ${lastName}!`; }; -const manager = middleware([ - async (context, next) => { - // Now we can modify `context.lastName` instead - context.lastName = 'X'; - await next(); - } -]).params('firstName', 'lastName'); -const wrappedSayHello = hooks(sayHello, manager); - -// Or all together const wrappedSayHello = hooks(sayHello, middleware([ async (context, next) => { // Now we can modify `context.lastName` instead @@ -489,10 +510,11 @@ const wrappedSayHello = hooks(sayHello, middleware([ })(); ``` -> __Note:__ When using named parameters, `context.arguments` is read only. +> __Note:__ When using named parameters, `context.arguments` is read only to preserve the order of named params. ### Default values +You can add default values using the manager's `.defaults()` method. See [manager.defaults()](#defaultscallback) > __Note:__ Even if your original function contains a default value, it is important to specify it because the middleware runs before and the value will be `undefined` without a default value. @@ -505,7 +527,7 @@ In a hook function, `context.result` can be See the [cache example](#cache) for how this can be used. -### Calling the original +### Calling the original function The original function without any hooks is available as `fn.original`: @@ -535,7 +557,7 @@ const o = hooks({ ### Customizing and returning the context -To add additional data to the context an instance of a hook context created via `fn.createContext(data)` can be passed as the last argument of a hook-enabled function call. In that case, the up to date context object with all the information (like `context.result`) will be returned: +Once a function has been wrapped with `hooks`, the wrapped function will have a `createContext` method. This method can be used to create a custom context object. This custom context can then be passed as the last argument of a hook-enabled function call. In that case, the up-to-date context object - with all the information (like `context.result`) - will be returned: ```js const { hooks, HookContext } = require('@feathersjs/hooks'); @@ -557,78 +579,142 @@ const customContext = sayHello.createContext({ (async () => { const finalContext = await sayHello('Dave', customContext); - + console.log(finalContext); })(); ``` -## Options +## Flow Control with Multiple Hooks + +### Async Hook Flow + +Middleware functions (or hook functions) take a `context` and an asynchronous `next` function as their parameters. The `context` contains information about the function call (like the arguments, the result or `this` context) and the `next` function can be called to continue to the next hook or the original function. -Instead an array of middleware, a chainable middleware manager that allows to set additional options can be passed like this: +A middleware function can do things before calling `await next()` and after all following middleware functions and the function call itself return. It can also `try/catch` the `await next()` call to handle and modify errors. This is the same control flow that the web framework [KoaJS](https://koajs.com/) uses for handling HTTP requests and response. + +Each hook function wraps _around_ all other functions (like an onion). This means that the first registered middleware function will run first before `await next()` and as the very last after all following hooks. + +![Feathers hooks image](https://user-images.githubusercontent.com/338316/72454734-44e8d680-3776-11ea-90ed-c81b2d98e8e5.png) + +The following example uses hooks named `one`, `two`, and `three` to demonstrate how execution order works: ```js -const { hooks, middleware } = require('@feathersjs/hooks'); +const { hooks } = require('@feathersjs/hooks'); -// Initialize middleware manager -const manager = middleware([ - hook1, - hook2, - hook3 -]); -const sayHelloWithHooks = hooks(sayHello, manager); +const sayHello = async message => { + console.log(`HELLO, ${message}!`) +}; -// Or all together -const sayHelloWithHooks = hooks(sayHello, middleware([ - hook1, - hook2, - hook3 -])); +const one = async (ctx, next) => { + console.log('one before'); + await next(); + console.log('one after') +} + +const two = async (ctx, next) => { + console.log('two before'); + await next(); + console.log('two after') +} + +const three = async (ctx, next) => { + console.log('three before'); + await next(); + console.log('three after') +} + +const sayHelloWithHooks = hooks(sayHello, [ + one, + two, + three +]); (async () => { - await sayHelloWithHooks('David'); + await sayHelloWithHooks('DAVID'); })(); ``` -### params(...names) - -Inititalizes a list of named parameters. +Would print: -```js -const sayHelloWithHooks = hooks(sayHello, middleware([ - hook1, - hook2, - hook3 -]).params('name')); +```console +one before +two before +three before +HELLO, DAVID! +three after +two after +one after ``` -### props(properties) +This order also applies when using hooks on [objects](#object-hooks) and [classes and with inheritance](#class-hooks). -Initializes properties on the `context` +### Regular Hooks + +You may have noticed that after-hook execution order is the reverse compared to before-hook execution order. This is due to how the hooks wrap around each other. If you prefer that the flow of the hooks matches the flow of the page, you can use Regular Hooks. Regular Hooks are similar to Async Hooks, but they do not receive a `next` function as the second argument. This means there is no `async next()` in the middle of the function body. This allows the code execution to match the natural reading flow on the page: top to bottom. Here's what a regular hook looks like: ```js -const sayHelloWithHooks = hooks(sayHello, middleware([ - hook1, - hook2, - hook3 -]).params('name').props({ - customProperty: true -})); +// A Regular Hook is just an async function that receives the context object. +const regularHook = async (context) => { + // All code goes here. +} ``` -> __Note:__ `.props` can not contain any of the field names defined in `.params`. +With @feathersjs/hooks, the `collect` utility enables the use of Regular Hooks. -### defaults(callback) +> Longtime FeathersJS developers will recognize Regular Hooks. They're the same type of hooks that have been around since the beginning. -Calls a `callback(self, arguments, context)` that returns default values which will be set if the property on the hook context is `undefined`. Applies to both, `params` and other properties. +#### The `collect` utility -```js -const sayHello = async name => `Hello ${name}`; +The `collect` utility enables Regular Hooks functionality. It gathers hooks into `before`, `after`, and `error` hooks. Here's what it looks like. + +```ts +import { hooks } from '@feathersjs/hooks' +import { discard } from 'feathers-hooks-common' + +const make_request = () => { /* make a request to the database server */ } + +const verify_auth = (context) => { + /* Do auth verification, here */ +} +const handle_error = (context) => { + /* Do some error handling */ +} -const sayHelloWithHooks = hooks(sayHello, middleware([]).params('name').defaults(() => { - return { - name: 'Unknown human' +const request_with_middleware = hooks(make_request, middleware( + collect({ + before: [verify_auth], + after: [discard('password')], + error: [handle_error] + }) +)) +``` + +Or with a class: + +```ts +import { hooks } from '@feathersjs/hooks' +import { discard } from 'feathers-hooks-common' + +class DbAdapter { + create() { + /* create data in the db */ } -})); +} + +const verify_auth = (context) => { + /* Do auth verification, here */ +} +const handle_error = (context) => { + /* Do some error handling */ +} + +const request_with_middleware = hooks(DbAdapter, middleware({ + create: collect({ + before: [verify_auth], + after: [discard('password')], + error: [handle_error] + }) +})) ``` # Best practises @@ -671,7 +757,7 @@ const cache = () => { setInterval(() => { cacheData = {}; }, 5000); - + return async (context, next) => { const key = JSON.stringify(context); @@ -683,7 +769,7 @@ const cache = () => { } await next(); - + // Set the cached value to the result cacheData[key] = context.result; } @@ -716,7 +802,7 @@ const deleteInvoice = hooks(async (id, user) => { ## Cleaning up GraphQL resolvers -The above examples can both be useful for speeding up and locking down existing [GraphQL resolvers](https://graphql.org/learn/execution/): +The above examples can both be useful for speeding up and locking down existing [GraphQL resolvers](https://graphql.org/learn/execution/): ```js const { hooks } = require('@feathersjs/hooks'); @@ -746,6 +832,6 @@ const resolvers = { # License -Copyright (c) 2020 +Copyright (c) 2021 Licensed under the [MIT license](LICENSE).