From 56a44beb3388873f7bef12ac640f115beffceb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Berthommier?= Date: Sun, 8 Nov 2020 20:40:23 +0100 Subject: [PATCH] feat(hooks): Revert refactoring into separate hooks (PR #37) (#57) BREAKING CHANGES: This reverts back to the usage of v0.4.0 and before. --- packages/hooks/src/base.ts | 174 ++++++++++++++++++++-- packages/hooks/src/context.ts | 63 -------- packages/hooks/src/hooks.ts | 44 +++--- packages/hooks/src/index.ts | 27 +++- packages/hooks/src/utils.ts | 25 ++++ packages/hooks/test/benchmark.test.ts | 12 +- packages/hooks/test/class.test.ts | 25 +--- packages/hooks/test/decorator.test.ts | 10 +- packages/hooks/test/function.test.ts | 77 ++++++---- packages/hooks/test/object.test.ts | 29 ++-- readme.md | 206 +++++++++++++++----------- 11 files changed, 428 insertions(+), 264 deletions(-) delete mode 100644 packages/hooks/src/context.ts create mode 100644 packages/hooks/src/utils.ts diff --git a/packages/hooks/src/base.ts b/packages/hooks/src/base.ts index 9622e9d..17cc397 100644 --- a/packages/hooks/src/base.ts +++ b/packages/hooks/src/base.ts @@ -1,9 +1,13 @@ import { Middleware } from './compose'; +import { copyToSelf } from './utils'; export const HOOKS: string = Symbol('@feathersjs/hooks') as any; export type HookContextData = { [key: string]: any }; +/** + * The base hook context. + */ export class HookContext { result?: T; method?: string; @@ -18,11 +22,14 @@ export class HookContext { export type HookContextConstructor = new (data?: { [key: string]: any }) => HookContext; -export type HookDefaultsInitializer = (context: HookContext) => HookContextData; +export type HookDefaultsInitializer = (self?: any, args?: any[], context?: HookContext) => HookContextData; export class HookManager { _parent?: this|null = null; - _middleware: Middleware[] = []; + _params: string[]|null = null; + _middleware: Middleware[]|null = null; + _props: HookContextData|null = null; + _defaults: HookDefaultsInitializer; parent (parent: this) { this._parent = parent; @@ -30,31 +37,154 @@ export class HookManager { return this; } - middleware (middleware: Middleware[]) { - this._middleware = middleware; + middleware (middleware?: Middleware[]) { + this._middleware = middleware?.length ? middleware : null; return this; } - getContextClass (): HookContextConstructor { - return HookContext; - } + getMiddleware (): Middleware[]|null { + if (this._parent) { + const previous = this._parent.getMiddleware(); + + if (previous) { + if (this._middleware) { + return this._parent.getMiddleware().concat(this._middleware); + } - getMiddleware (): Middleware[] { - const previous = this._parent ? this._parent.getMiddleware() : []; + return previous; + } + } - return previous.concat(this._middleware); + return this._middleware; } - collectMiddleware (self: any, _args: any[]): Middleware[] { + collectMiddleware (self: any, _args: any[]): Middleware[] { const otherMiddleware = getMiddleware(self); + const middleware = this.getMiddleware(); + + if (otherMiddleware) { + if (middleware) { + return otherMiddleware.concat(middleware); + } + + return otherMiddleware; + } + + return this.getMiddleware(); + } + + props (props: HookContextData) { + if (!this._props) { + this._props = {}; + } + + Object.assign(this._props, props); + + return this; + } + + getProps (): HookContextData { + if (this._parent) { + const previous = this._parent.getProps(); + + if (previous) { + if (this._props) { + return Object.assign({}, previous, this._props); + } + + return previous; + } + } + + return this._props; + } + + params (...params: string[]) { + this._params = params; + + return this; + } - return otherMiddleware.concat(this.getMiddleware()); + getParams (): string[] { + if (this._parent) { + const previous = this._parent.getParams(); + + if (previous) { + if (this._params) { + return previous.concat(this._params); + } + + return previous; + } + } + + return this._params; + } + + defaults (defaults: HookDefaultsInitializer) { + this._defaults = defaults; + + return this; + } + + getDefaults (self: any, args: any[], context: HookContext): HookContextData { + const defaults = typeof this._defaults === 'function' ? this._defaults(self, args, context) : null; + + if (this._parent) { + const previous = this._parent.getDefaults(self, args, context); + + if (previous) { + if (this._props) { + return Object.assign({}, previous, this._props); + } + + return previous; + } + } + + return defaults; } + getContextClass (Base: HookContextConstructor = HookContext): HookContextConstructor { + const ContextClass = class ContextClass extends Base { + constructor (data: any) { + super(data); + + copyToSelf(this); + } + }; + const params = this.getParams(); + const props = this.getProps(); + + if (params) { + params.forEach((name, index) => { + if (props?.[name]) { + throw new Error(`Hooks can not have a property and param named '${name}'. Use .defaults instead.`); + } + + Object.defineProperty(ContextClass.prototype, name, { + enumerable: true, + get () { + return this?.arguments[index]; + }, + set (value: any) { + this.arguments[index] = value; + } + }); + }); + } + + if (props) { + Object.assign(ContextClass.prototype, props); + } + + return ContextClass; + } initializeContext (self: any, args: any[], context: HookContext): HookContext { const ctx = this._parent ? this._parent.initializeContext(self, args, context) : context; + const defaults = this.getDefaults(self, args, ctx); if (self) { ctx.self = self; @@ -62,13 +192,25 @@ export class HookManager { ctx.arguments = args; + if (defaults) { + for (const name of Object.keys(defaults)) { + if (ctx[name] === undefined) { + ctx[name] = defaults[name]; + } + } + } + return ctx; } } -export type HookOptions = HookManager|Middleware[]; +export type HookOptions = HookManager|Middleware[]|null; + +export function convertOptions (options: HookOptions = null) { + if (!options) { + return new HookManager() + } -export function convertOptions (options: HookOptions = []) { return Array.isArray(options) ? new HookManager().middleware(options) : options; } @@ -84,10 +226,10 @@ export function setManager (target: T, manager: HookManager) { return target; } -export function getMiddleware (target: any): Middleware[] { +export function getMiddleware (target: any): Middleware[]|null { const manager = getManager(target); - return manager ? manager.getMiddleware() : []; + return manager ? manager.getMiddleware() : null; } export function setMiddleware (target: T, middleware: Middleware[]) { diff --git a/packages/hooks/src/context.ts b/packages/hooks/src/context.ts deleted file mode 100644 index db0f9f6..0000000 --- a/packages/hooks/src/context.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { HookContext, HookContextData, HookDefaultsInitializer } from './base' -import { NextFunction } from './compose' - -/** - * Returns a hook that initializes named function parameters on the - * hook context. - * - * @param names A list of parameter names - */ -export function params (...names: string[]) { - const descriptors = names.reduce((result, name, index) => { - result[name] = { - enumerable: true, - get (this: any) { - return this.arguments[index]; - }, - - set (this: any, value) { - this.arguments[index] = value; - } - } - - return result; - }, {} as PropertyDescriptorMap); - - return async function contextParams (context: HookContext, next: NextFunction) { - Object.defineProperties(context, descriptors); - await next(); - }; -} - -/** - * Returns a hook that sets the given properties on the hook context. - * - * @param properties The properties to set. - */ -export function properties (properties: HookContextData) { - return async function contextProperties (context: HookContext, next: NextFunction) { - Object.assign(context, properties); - await next(); - } -} - -/** - * Returns a hook that calls a `callback(context)` function that - * returns default values which will be set on the context if they - * are currently `undefined`. - * - * @param initializer The initialization callback. - */ -export function defaults (initializer: HookDefaultsInitializer) { - return async function contextDefaults (context: HookContext, next: NextFunction) { - const defaults = await initializer(context); - - for (const name of Object.keys(defaults)) { - if (context[name] === undefined) { - context[name] = defaults[name]; - } - } - - await next(); - } -} diff --git a/packages/hooks/src/hooks.ts b/packages/hooks/src/hooks.ts index 374a96b..d3283fa 100644 --- a/packages/hooks/src/hooks.ts +++ b/packages/hooks/src/hooks.ts @@ -2,7 +2,6 @@ import { compose, Middleware } from './compose'; import { HookContext, setManager, HookContextData, HookOptions, convertOptions, setMiddleware } from './base'; -import { properties } from './context'; export function getOriginal (fn: any): any { return typeof fn.original === 'function' ? getOriginal(fn.original) : fn; @@ -40,23 +39,29 @@ export function functionHooks (fn: F, managerOrMiddleware: HookOptions) { // Assemble the hook chain const hookChain: Middleware[] = [ // Return `ctx.result` or the context - (ctx, next) => next().then(() => returnContext ? ctx : ctx.result), - // Create the hook chain by calling the `collectMiddleware function - ...manager.collectMiddleware(this, args), - // Runs the actual original method if `ctx.result` is not already set - (ctx, next) => { - if (ctx.result === undefined) { - return Promise.resolve(original.apply(this, ctx.arguments)).then(result => { - ctx.result = result; - - return next(); - }); - } - - return next(); - } + (ctx, next) => next().then(() => returnContext ? ctx : ctx.result) ]; + // Create the hook chain by calling the `collectMiddleware function + const mw = manager.collectMiddleware(this, args); + + if (mw) { + Array.prototype.push.apply(hookChain, mw); + } + + // Runs the actual original method if `ctx.result` is not already set + hookChain.push((ctx, next) => { + if (ctx.result === undefined) { + return Promise.resolve(original.apply(this, ctx.arguments)).then(result => { + ctx.result = result; + + return next(); + }); + } + + return next(); + }); + return compose(hookChain).call(this, context); }; @@ -92,9 +97,7 @@ export function objectHooks (_obj: any, hooks: HookMap|Middleware[]) { const manager = convertOptions(hooks[method]); - manager._middleware.unshift(properties({ method })); - - result[method] = functionHooks(fn, manager); + result[method] = functionHooks(fn, manager.props({ method })); return result; }, obj); @@ -116,8 +119,7 @@ export const hookDecorator = (managerOrMiddleware?: HookOptions) => { throw new Error(`Can not apply hooks. '${method}' is not a function`); } - manager._middleware.unshift(properties({ method })); - descriptor.value = functionHooks(fn, manager); + descriptor.value = functionHooks(fn, manager.props({ method })); return descriptor; }; diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 29fb180..2a3b594 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -4,7 +4,6 @@ import { } from './base'; import { functionHooks, hookDecorator, objectHooks, HookMap } from './hooks'; -export * as setContext from './context'; export * from './hooks'; export * from './compose'; export * from './base'; @@ -17,14 +16,34 @@ export interface WrapperAddon { export type WrappedFunction = F&((...rest: any[]) => Promise|Promise)&WrapperAddon; +export type MiddlewareOptions = { + params?: any; + defaults?: any; + props?: any; +}; + /** * Initializes a hook settings object with the given middleware. * @param mw The list of middleware */ -export function middleware (mw: Middleware[] = []) { - const manager = new HookManager(); +export function middleware (mw?: Middleware[], options?: MiddlewareOptions) { + const manager = new HookManager().middleware(mw); + + if (options) { + if (options.params) { + manager.params(options.params); + } + + if (options.defaults) { + manager.defaults(options.defaults); + } + + if (options.props) { + manager.props(options.props); + } + } - return manager.middleware(mw); + return manager; } /** diff --git a/packages/hooks/src/utils.ts b/packages/hooks/src/utils.ts new file mode 100644 index 0000000..5fff989 --- /dev/null +++ b/packages/hooks/src/utils.ts @@ -0,0 +1,25 @@ +const proto = Object.prototype as any; +// These are non-standard but offer a more reliable prototype based +// lookup for properties +const hasProtoDefinitions = typeof proto.__lookupGetter__ === 'function' && + typeof proto.__defineGetter__ === 'function' && + typeof proto.__defineSetter__ === 'function'; + +export function copyToSelf (target: any) { + // tslint:disable-next-line + for (const key in target) { + if (!target.hasOwnProperty(key)) { + const getter = hasProtoDefinitions ? target.constructor.prototype.__lookupGetter__(key) + : Object.getOwnPropertyDescriptor(target, key); + + if (getter && hasProtoDefinitions) { + target.__defineGetter__(key, getter); + target.__defineSetter__(key, target.constructor.prototype.__lookupSetter__(key)); + } else if (getter) { + Object.defineProperty(target, key, getter); + } else { + target[key] = target[key]; + } + } + } +} diff --git a/packages/hooks/test/benchmark.test.ts b/packages/hooks/test/benchmark.test.ts index ab7f8dd..ba4fd7b 100644 --- a/packages/hooks/test/benchmark.test.ts +++ b/packages/hooks/test/benchmark.test.ts @@ -1,11 +1,5 @@ import { strict as assert } from 'assert'; -import { - hooks, - HookContext, - NextFunction, - middleware, - setContext -} from '../src/'; +import { hooks, HookContext, NextFunction, middleware } from '../src/'; const CYCLES = 100000; const getRuntime = async (callback: () => Promise) => { @@ -50,12 +44,10 @@ describe('hook benchmark', () => { it('single hook, withParams and props', async () => { const hookHello = hooks(hello, middleware([ - setContext.params('name'), - setContext.properties({ dave: true }), async (_ctx: HookContext, next: NextFunction) => { await next(); } - ])); + ]).params('name').props({ dave: true })); const runtime = await getRuntime(() => hookHello('Dave')); diff --git a/packages/hooks/test/class.test.ts b/packages/hooks/test/class.test.ts index 667cfcd..c6fc4c4 100644 --- a/packages/hooks/test/class.test.ts +++ b/packages/hooks/test/class.test.ts @@ -1,11 +1,5 @@ import { strict as assert } from 'assert'; -import { - hooks, - middleware, - HookContext, - NextFunction, - setContext -} from '../src'; +import { hooks, middleware, HookContext, NextFunction } from '../src'; interface Dummy { sayHi (name: string): Promise; @@ -31,7 +25,6 @@ describe('class objectHooks', () => { it('hooking object on class adds to the prototype', async () => { hooks(DummyClass, { sayHi: middleware([ - setContext.params('name'), async (ctx: HookContext, next: NextFunction) => { assert.deepStrictEqual(ctx, new DummyClass.prototype.sayHi.Context({ arguments: ['David'], @@ -44,7 +37,7 @@ describe('class objectHooks', () => { ctx.result += '?'; } - ]), + ]).params('name'), addOne: middleware([ async (ctx: HookContext, next: NextFunction) => { @@ -97,7 +90,6 @@ describe('class objectHooks', () => { it('works with multiple context updaters', async () => { hooks(DummyClass, { sayHi: middleware([ - setContext.params('name'), async (ctx, next) => { assert.equal(ctx.name, 'Dave'); @@ -105,28 +97,27 @@ describe('class objectHooks', () => { await next(); } - ]) + ]).params('name') }); - class OtherDummy extends DummyClass {} + class OtherDummy extends DummyClass { + } hooks(OtherDummy, { - sayHi: [ - setContext.properties({ gna: 42 }), + sayHi: middleware([ async (ctx, next) => { assert.equal(ctx.name, 'Changed'); assert.equal(ctx.gna, 42); await next(); } - ] + ]).props({ gna: 42 }) }); const instance = new OtherDummy(); hooks(instance, { sayHi: middleware([ - setContext.properties({ app: 'ok' }), async (ctx, next) => { assert.equal(ctx.name, 'Changed'); assert.equal(ctx.gna, 42); @@ -134,7 +125,7 @@ describe('class objectHooks', () => { await next(); } - ]) + ]).props({ app: 'ok' }) }); assert.equal(await instance.sayHi('Dave'), 'Hi Changed'); diff --git a/packages/hooks/test/decorator.test.ts b/packages/hooks/test/decorator.test.ts index e679b4e..c378bcd 100644 --- a/packages/hooks/test/decorator.test.ts +++ b/packages/hooks/test/decorator.test.ts @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import { hooks, HookContext, NextFunction, setContext } from '../src'; +import { hooks, HookContext, NextFunction, middleware } from '../src'; describe('hookDecorator', () => { it('hook decorator on method and classes with inheritance', async () => { @@ -24,8 +24,7 @@ describe('hookDecorator', () => { ctx.result += ' ResultFromDummyClass'; }]) class DummyClass extends TopLevel { - @hooks([ - setContext.params('name'), + @hooks(middleware([ async (ctx: HookContext, next: NextFunction) => { assert.equal(ctx.method, 'sayHi'); assert.deepEqual(ctx.arguments, [expectedName]); @@ -35,7 +34,7 @@ describe('hookDecorator', () => { ctx.result += ' ResultFromMethodDecorator'; } - ]) + ]).params('name')) async sayHi (name: string) { return `Hi ${name}`; } @@ -53,7 +52,8 @@ describe('hookDecorator', () => { const instance = new DummyClass(); - assert.strictEqual(await instance.sayHi('David'), + assert.strictEqual( + await instance.sayHi('David'), `Hi ${expectedName} ResultFromMethodDecorator ResultFromDummyClass ResultFromTopLevel` ); }); diff --git a/packages/hooks/test/function.test.ts b/packages/hooks/test/function.test.ts index 726714b..6bd7fcb 100644 --- a/packages/hooks/test/function.test.ts +++ b/packages/hooks/test/function.test.ts @@ -6,8 +6,7 @@ import { HookContext, NextFunction, setMiddleware, - functionHooks, - setContext + functionHooks } from '../src'; describe('functionHooks', () => { @@ -16,7 +15,7 @@ describe('functionHooks', () => { }; it('returns a new function, registers hooks', () => { - const fn = hooks(hello, middleware()); + const fn = hooks(hello, []); assert.notDeepEqual(fn, hello); assert.ok(getManager(fn) !== null); @@ -158,16 +157,13 @@ describe('functionHooks', () => { }); it('chains context initializers', async () => { - const first = hooks(hello, middleware([ - setContext.properties({ testing: ' test value' }) - ])); + const first = hooks(hello, middleware([]).params('name')); const second = hooks(first, middleware([ - setContext.params('name'), async (ctx, next) => { ctx.name += ctx.testing; await next(); } - ])); + ]).props({ testing: ' test value' })); const result = await second('Dave'); @@ -176,7 +172,6 @@ describe('functionHooks', () => { it('creates context with params and converts to arguments', async () => { const fn = hooks(hello, middleware([ - setContext.params('name'), async (ctx, next) => { assert.equal(ctx.name, 'Dave'); @@ -184,15 +179,13 @@ describe('functionHooks', () => { await next(); } - ])); + ]).params('name')); assert.equal(await fn('Dave'), 'Hello Changed'); }); it('assigns props to context', async () => { const fn = hooks(hello, middleware([ - setContext.params('name'), - setContext.properties({ dev: true }), async (ctx, next) => { assert.equal(ctx.name, 'Dave'); assert.equal(ctx.dev, true); @@ -201,7 +194,25 @@ describe('functionHooks', () => { await next(); } - ])); + ]).params('name').props({ dev: true })); + + assert.equal(await fn('Dave'), 'Hello Changed'); + }); + + it('assigns props to context by options', async () => { + const fn = hooks(hello, middleware([ + async (ctx, next) => { + assert.equal(ctx.name, 'Dave'); + assert.equal(ctx.dev, true); + + ctx.name = 'Changed'; + + await next(); + } + ], { + params: ['name'], + props: { dev: true } + })); assert.equal(await fn('Dave'), 'Hello Changed'); }); @@ -216,10 +227,7 @@ describe('functionHooks', () => { await next(); }; - const fn = hooks(hello, middleware([ - setContext.params('name'), - modifyArgs - ])); + const fn = hooks(hello, middleware([ modifyArgs ]).params('name')); const customContext = fn.createContext(); const resultContext = await fn('Daffl', {}, customContext); @@ -235,7 +243,6 @@ describe('functionHooks', () => { it('can take and return an existing HookContext', async () => { const message = 'Custom message'; const fn = hooks(hello, middleware([ - setContext.params('name'), async (ctx, next) => { assert.equal(ctx.name, 'Dave'); assert.equal(ctx.message, message); @@ -243,7 +250,7 @@ describe('functionHooks', () => { ctx.name = 'Changed'; await next(); } - ])); + ]).params('name')); const customContext = fn.createContext({ message }); const resultContext: HookContext = await fn('Dave', {}, customContext); @@ -316,20 +323,36 @@ describe('functionHooks', () => { assert.equal((sayHi as any)[TEST], (hello as any)[TEST]); }); + it('context has own properties', async () => { + const fn = hooks(hello, middleware([]).params('name')); + + const customContext = fn.createContext({ message: 'Hi !' }); + const resultContext: HookContext = await fn('Dave', {}, customContext); + + assert.deepEqual(Object.keys(resultContext), ['message', 'name', 'arguments', 'result']); + }); + + it('same params and props throw an error', async () => { + const hello = async (name?: string) => { + return `Hello ${name}`; + }; + assert.throws(() => hooks(hello, middleware([]).params('name').props({ name: 'David' })), { + message: `Hooks can not have a property and param named 'name'. Use .defaults instead.` + }); + }); + it('creates context with default params', async () => { const fn = hooks(hello, middleware([ - setContext.params('name', 'params'), - setContext.defaults(() => { - return { - name: 'Bertho', - params: {} - } - }), async (ctx, next) => { assert.deepEqual(ctx.params, {}); await next(); - }]) + }]).params('name', 'params').defaults(() => { + return { + name: 'Bertho', + params: {} + } + }) ); assert.equal(await fn('Dave'), 'Hello Dave'); diff --git a/packages/hooks/test/object.test.ts b/packages/hooks/test/object.test.ts index 73ca6a8..5afee43 100644 --- a/packages/hooks/test/object.test.ts +++ b/packages/hooks/test/object.test.ts @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import { hooks, middleware, HookContext, NextFunction, setContext } from '../src'; +import { hooks, middleware, HookContext, NextFunction } from '../src'; interface HookableObject { test: string; @@ -52,23 +52,20 @@ describe('objectHooks', () => { it('hooks object and allows to customize context for method', async () => { const hookedObj = hooks(obj, { - sayHi: middleware([ - setContext.params('name'), - async (ctx: HookContext, next: NextFunction) => { - assert.deepStrictEqual(ctx, new (obj.sayHi as any).Context({ - arguments: ['David'], - method: 'sayHi', - name: 'David', - self: obj - })); - - ctx.name = 'Dave'; + sayHi: middleware([async (ctx: HookContext, next: NextFunction) => { + assert.deepStrictEqual(ctx, new (obj.sayHi as any).Context({ + arguments: ['David'], + method: 'sayHi', + name: 'David', + self: obj + })); - await next(); + ctx.name = 'Dave'; + + await next(); - ctx.result += '?'; - } - ]), + ctx.result += '?'; + }]).params('name'), addOne: middleware([async (ctx: HookContext, next: NextFunction) => { ctx.arguments[0] += 1; diff --git a/readme.md b/readme.md index b020ca3..5c36eaa 100644 --- a/readme.md +++ b/readme.md @@ -33,13 +33,15 @@ To a function or class without having to change its original code while also kee - [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) - [Customizing and returning the context](#customizing-and-returning-the-context) - - [Built in hooks](#built-in-hooks) - - [setContext.params(...names)](#setcontextparamsnames) - - [setContext.properties(props)](#setcontextpropertiesprops) - - [setContext.defaults(callback)](#setcontextdefaultscallback) + - [Options](#options) + - [params(...names)](#paramsnames) + - [props(properties)](#propsproperties) + - [defaults(callback)](#defaultscallback) - [Best practises](#best-practises) - [More Examples](#more-examples) - [Cache](#cache) @@ -234,21 +236,21 @@ This order also applies when using hooks on [objects](#object-hooks) and [classe ## Function hooks -`hooks(fn, middleware[])` returns a new function that wraps `fn` with `middleware` +`hooks(fn, middleware[]|manager)` returns a new function that wraps `fn` with `middleware` ```js -const { hooks } = require('@feathersjs/hooks'); +const { hooks, middleware } = require('@feathersjs/hooks'); const sayHello = async name => { return `Hello ${name}!`; }; -const wrappedSayHello = hooks(sayHello, [ +const wrappedSayHello = hooks(sayHello, middleware([ async (context, next) => { - console.log(context.arguments); + console.log(context.someProperty); await next(); } -]); +]).params('name')); (async () => { console.log(await wrappedSayHello('David')); @@ -262,7 +264,7 @@ const wrappedSayHello = hooks(sayHello, [ `hooks(obj, middlewareMap)` takes an object and wraps the functions indicated in `middlewareMap`. It will modify the existing Object `obj`: ```js -const { hooks } = require('@feathersjs/hooks'); +const { hooks, middleware } = require('@feathersjs/hooks'); const o = { async sayHi (name, quote) { @@ -278,6 +280,12 @@ hooks(o, { sayHello: [ logRuntime ], sayHi: [ logRuntime ] }); + +// With additional options +hooks(o, { + sayHello: middleware([ logRuntime ]).params('name', 'quote'), + sayHi: middleware([ logRuntime ]).params('name') +}); ``` Hooks can also be registered at the object level which will run before any specific hooks on a hook enabled function: @@ -312,7 +320,7 @@ hooks(o, { 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. +> __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 @@ -349,12 +357,10 @@ hooks(HappyHelloSayer.prototype, [ // Methods can also be wrapped directly on the class hooks(HelloSayer, { - sayHello: [ - async (context, next) => { - console.log('Hook on HelloSayer.sayHello'); - await next(); - } - ] + sayHello: [async (context, next) => { + console.log('Hook on HelloSayer.sayHello'); + await next(); + }] }); (async () => { @@ -368,8 +374,8 @@ hooks(HelloSayer, { Using decorators in TypeScript also respects inheritance: -```ts -import { hooks, setContext, HookContext, NextFunction } from '@feathersjs/hooks'; +```js +import { hooks, HookContext, NextFunction } from '@feathersjs/hooks'; @hooks([ async (context: HookContext, next: NextFunction) => { @@ -378,13 +384,12 @@ import { hooks, setContext, HookContext, NextFunction } from '@feathersjs/hooks' } ]) class HelloSayer { - @hooks([ - setContext.params('name'), + @hooks(middleware([ async (context: HookContext, next: NextFunction) => { console.log('Hook on HelloSayer.sayHello'); await next(); } - ]) + ]).params('name')) async sayHello (name: string) { return `Hello ${name}`; } @@ -418,7 +423,7 @@ class HappyHelloSayer extends HelloSayer { ## Hook Context -The hook `context` in a [middleware function](#middleware) is an object that contains information about the function call. +The hook `context` in a [middleware function](#middleware) is an object that contains information about the function call. ### Context properties @@ -428,11 +433,11 @@ The default properties available are: - `context.method` - The name of the function (if it belongs to an object or class) - `context.self` - The `this` context of the function being called (may not always be available e.g. for top level arrow functions) - `context.result` - The result of the method call -- `context[name]` - Value of a named parameter when [using named arguments](#contextparamsnames) +- `context[name]` - Value of a named parameter when [using named arguments](#using-named-parameters) ### Arguments -The function call arguments will be available as an array in `context.arguments`. The values can be modified to change what is passed to the original function call: +By default, the function call arguments will be available as an array in `context.arguments`. The values can be modified to change what is passed to the original function call: ```js const { hooks } = require('@feathersjs/hooks'); @@ -454,6 +459,47 @@ const wrappedSayHello = hooks(sayHello, [ })(); ``` +### Using named parameters + +It is also possible to turn the arguments into named parameters. In the above example we probably want to have `context.firstName` and `context.lastName` available. To do this, the [`context` option](#options) can be initialized like this: + +```js +const { hooks, middleware } = require('@feathersjs/hooks'); + +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 + context.lastName = 'X'; + await next(); + } +]).params('firstName', 'lastName')); + +(async () => { + console.log(await wrappedSayHello('David', 'L')); // Hello David X +})(); +``` + +> __Note:__ When using named parameters, `context.arguments` is read only. + +### Default values + + +> __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. + ### Modifying the result In a hook function, `context.result` can be @@ -520,74 +566,73 @@ const customContext = sayHello.createContext({ })(); ``` -## Built in hooks - -`@feathersjs/hooks` comes with three built in hooks that can be used to customize the context. +## Options -### setContext.params(...names) - -The `setContext.params(...names)` hook turns the method call arguments into named parameters on the context. The following example allows to read and set `context.firstName` and `context.lastName` in all subsequent hooks: +Instead an array of middleware, a chainable middleware manager that allows to set additional options can be passed like this: ```js -const { hooks, setContext } = require('@feathersjs/hooks'); - -const sayHello = async (firstName, lastName) => { - return `Hello ${firstName} ${lastName}!`; -}; +const { hooks, middleware } = require('@feathersjs/hooks'); -const wrappedSayHello = hooks(sayHello, [ - // Sets `context.firstName` and `context.lastName` - setContext.params('firstName', 'lastName'), - async (context, next) => { - // Now we can modify `context.lastName` instead - context.lastName = 'X'; - await next(); - } +// Initialize middleware manager +const manager = middleware([ + hook1, + hook2, + hook3 ]); +const sayHelloWithHooks = hooks(sayHello, manager); + +// Or all together +const sayHelloWithHooks = hooks(sayHello, middleware([ + hook1, + hook2, + hook3 +])); (async () => { - console.log(await wrappedSayHello('David', 'L')); // Hello David X + await sayHelloWithHooks('David'); })(); ``` -> __Note:__ `setContext.params` always needs to be registered before any other hook that is reading or writing that property. - -### setContext.properties(props) +### params(...names) -`setContext.properties(props)` returns a hook that always sets the given properties on the context: +Inititalizes a list of named parameters. ```js -const { version } = require('package.json'); -const { hooks, setContext } = require('@feathersjs/hooks'); +const sayHelloWithHooks = hooks(sayHello, middleware([ + hook1, + hook2, + hook3 +]).params('name')); +``` -const sayHello = async (firstName, lastName) => { - return `Hello ${firstName} ${lastName}!`; -}; +### props(properties) -const wrappedSayHello = hooks(sayHello, [ - // Sets `context.version` - setContext.properties({ version }) -]); +Initializes properties on the `context` + +```js +const sayHelloWithHooks = hooks(sayHello, middleware([ + hook1, + hook2, + hook3 +]).params('name').props({ + customProperty: true +})); ``` -### setContext.defaults(callback) +> __Note:__ `.props` can not contain any of the field names defined in `.params`. -`setContext.defaults(callback)` returns a hook that calls a `callback(context)` that returns default values which will be set if the property on the context is `undefined`: +### defaults(callback) + +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, setContext } = require('@feathersjs/hooks'); -const sayHello = async (name?: string) => `Hello ${name}`; +const sayHello = async name => `Hello ${name}`; -const sayHelloWithHooks = hooks(sayHello, [ - setContext.params('name'), - setContext.defaults({ +const sayHelloWithHooks = hooks(sayHello, middleware([]).params('name').defaults(() => { + return { name: 'Unknown human' - }) -]); - -(async () => { - console.log(await sayHello()); -})(); + } +})); ``` # Best practises @@ -612,10 +657,7 @@ const sayHelloWithHooks = hooks(sayHello, [ const findUser = hooks(async query => { return collection.find(query); - }, [ - contextPrams('query'), - updateQuery - ]); + }, middleware([ updateQuery ]).params('query')); ``` # More Examples @@ -663,8 +705,6 @@ await getData('http://url-that-takes-long-to-respond'); When passing e.g. a `user` object to a function call, hooks allow for a better separation of concerns by handling permissions in a hook: ```js -const { hooks, setContext } = require('@feathersjs/hooks'); - const checkPermission = name => async (context, next) => { if (!context.user.permissions.includes(name)) { throw new Error(`User does not have ${name} permission`); @@ -675,10 +715,7 @@ const checkPermission = name => async (context, next) => { const deleteInvoice = hooks(async (id, user) => { return collection.delete(id); -}, [ - setContext.params('id', 'user'), - checkPermission('admin') -]); +}, middleware([ checkPermission('admin') ]).params('id', 'user')); ``` ## Cleaning up GraphQL resolvers @@ -686,7 +723,7 @@ const deleteInvoice = hooks(async (id, user) => { The above examples can both be useful for speeding up and locking down existing [GraphQL resolvers](https://graphql.org/learn/execution/): ```js -const { hooks, setContext } = require('@feathersjs/hooks'); +const { hooks } = require('@feathersjs/hooks'); const checkPermission = name => async (ctx, next) => { const { context } = ctx; @@ -703,11 +740,10 @@ const resolvers = { return context.db.loadHumanByID(args.id).then( userData => new Human(userData) ) - }, [ - setContext.params('obj', 'args', 'context', 'info'), + }, middleware([ cache(), checkPermission('admin') - ]) + ]).params('obj', 'args', 'context', 'info')) } } ```