diff --git a/packages/hooks/src/function.ts b/packages/hooks/src/function.ts index 2c030bb..1eacbde 100644 --- a/packages/hooks/src/function.ts +++ b/packages/hooks/src/function.ts @@ -39,7 +39,7 @@ export const functionHooks = ( ...getMiddleware(this), // The hook chain attached to this function ...getMiddleware(result), - // Runs the actual original method if `ctx.result` is not set + // Runs the actual original method if `ctx.result` is not already set (ctx, next) => { if (ctx.result === undefined) { return Promise.resolve(fn.apply(this, ctx.arguments)).then(result => { diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 03d820e..1e2137c 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -8,12 +8,16 @@ export * from './function'; export * from './compose'; export * from './base'; +export interface OriginalAddon { + original: F; +} + // hooks(fn, hooks, updateContext?) export function hooks ( fn: F, hooks: Array>, updateContext?: ContextUpdater -): F&((...rest: any[]) => Promise); +): F&((...rest: any[]) => Promise)&OriginalAddon; // hooks(object, methodHookMap, methodUpdateContextMap?) export function hooks (obj: T, hookMap: MiddlewareMap, contextMap?: ContextUpdaterMap): T; // @hooks(hooks) diff --git a/packages/hooks/test/function.test.ts b/packages/hooks/test/function.test.ts index e34b880..44e754e 100644 --- a/packages/hooks/test/function.test.ts +++ b/packages/hooks/test/function.test.ts @@ -40,6 +40,20 @@ describe('functionHooks', () => { assert.strictEqual(res, 'Hello There You'); }); + it('has fn.original', async () => { + const fn = hooks(hello, [ + async (ctx: HookContext, next: NextFunction) => { + ctx.arguments[0] += ' You'; + + await next(); + } + ]); + + assert.equal(typeof fn.original, 'function'); + + assert.equal(await fn.original('Dave'), 'Hello Dave'); + }); + it('can override context.result before, skips method call', async () => { const hello = async (_name: string) => { throw new Error('Should never get here'); diff --git a/packages/hooks/test/object.test.ts b/packages/hooks/test/object.test.ts index cca2a0a..90360b9 100644 --- a/packages/hooks/test/object.test.ts +++ b/packages/hooks/test/object.test.ts @@ -180,7 +180,7 @@ describe('objectHooks', () => { ctx.result += '!'; } ]); - + hooks(obj, { sayHi: [async (ctx: HookContext, next: NextFunction) => { await next(); diff --git a/readme.md b/readme.md index 2ea1c42..eb26fba 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,7 @@ - Data pre- and postprocessing - etc. -To a function or class without having to change its original code while also keeping things cleanly separated and testable. +To a function or class without having to change its original code while also keeping everything cleanly separated and testable. @@ -32,6 +32,7 @@ To a function or class without having to change its original code while also kee - [Context properties](#context-properties) - [Modifying the result](#modifying-the-result) - [Using named parameters](#using-named-parameters) + - [Calling the original](#calling-the-original) - [Customizing and returning the context](#customizing-and-returning-the-context) - [Best practises](#best-practises) - [More Examples](#more-examples) @@ -55,7 +56,7 @@ The following example logs information about a function call: ```js const { hooks } = require('@feathersjs/hooks'); -const logInformation = async (context, next) => { +const logRuntime = async (context, next) => { const start = new Date().getTime(); await next(); @@ -70,7 +71,7 @@ const sayHello = async name => { } // Hooks can be used with a function like this: -const hookSayHello = hooks(sayhello, [ logInformation ]); +const hookSayHello = hooks(sayhello, [ logRuntime ]); (async () => { console.log(await hookSayHello('David')); @@ -84,7 +85,7 @@ class Hello { } hooks(Hello, { - sayHi: [ logInformation ] + sayHi: [ logRuntime ] }); (async () => { @@ -96,18 +97,18 @@ hooks(Hello, { ### TypeScript -With the following options in the `tsconfig.json` enabled: +In addition to the normal JavaScript use, with the `experimentalDecorators` option in the `tsconfig.json` enabled ```json "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ ``` -Hooks can be used as a decorator: +Hooks can also be registered using a decorator: ```ts import { hooks, HookContext, NextFunction } from '@feathersjs/hooks'; -const logInformation = async (context: HookContext, next: NextFunction) => { +const logRuntime = async (context: HookContext, next: NextFunction) => { const start = new Date().getTime(); await next(); @@ -119,7 +120,7 @@ const logInformation = async (context: HookContext, next: NextFunction) => { class Hello { @hooks([ - logInformation + logRuntime ]) async sayHi (name: string) { return `Hi ${name}`; @@ -137,9 +138,9 @@ class Hello { ### Middleware -Middleware functions (or hook functions) take a `context` and an asynchronous `next` function as their parameters that allow it to wrap around another function. +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 actual function. -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 as in [KoaJS](https://koajs.com/). +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. @@ -244,14 +245,14 @@ const o = { } hooks(o, { - sayHello: [ logInformation ], - sayHi: [ logInformation ] + sayHello: [ logRuntime ], + sayHi: [ logRuntime ] }); // With `updateContext` and named parameters hooks(o, { - sayHello: [ logInformation ], - sayHi: [ logInformation ] + sayHello: [ logRuntime ], + sayHi: [ logRuntime ] }, { sayHello: withParams('name'), sayHi: withParams('name') @@ -282,13 +283,15 @@ hooks(o, [ ]); hooks(o, { - sayHi: [ logInformation ] + sayHi: [ logRuntime ] }); ``` ### Class hooks -Similar to object hooks, class hooks modify the class (or class prototype). Just like for objects it is possible to register global hooks. Registering hooks also works with inheritance: +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 @@ -360,6 +363,10 @@ class HelloSayer { async sayHello (name: string) { return `Hello ${name}`; } + + async otherMethod () { + return 'This will not run any hooks'; + } } @hooks([ @@ -382,6 +389,8 @@ class HappyHelloSayer extends HelloSayer { })(); ``` +> __Note:__ Decorators only work on classes and class methods, not on functions. Standalone (arrow) functions require the [JavaScript function style](#function-hooks) hook registration. + ## Hook Context The hook context is an object that contains information about the function call. @@ -429,6 +438,34 @@ const sayHello = hooks(async (message, punctuationMark) => { > __Note:__ When using named parameters, `context.arguments` is read only. +### Calling the original + +The original function without any hooks is available as `fn.original`: + +```js +const { hooks } = require('@feathersjs/hooks'); +const emphasize = async (context, next) => { + await next(); + + context.result += '!!!'; +}; +const sayHello = hooks(async name => `Hello ${name}`, [ emphasize ]); + +const o = hooks({ + async sayHi(name) { + return `Hi ${name}`; + } +}, { + sayHi: [ emphasize ] +}); + +(async () => { + console.log(await sayHello.original('Dave')); // Hello Dave + // Originals on object need to be called with an explicit `this` context + console.log(await o.sayHi.original.call(o, 'David')) +})(); +``` + ### Customizing and returning the context To add additional data to the context an instance of `HookContext` 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: