From a3cb1c21919d95cfa5d44100c820fc2f2b1d1511 Mon Sep 17 00:00:00 2001 From: daffl Date: Sat, 24 Feb 2024 15:27:17 -0800 Subject: [PATCH] Update decorator docs --- readme.md | 309 +++++++++++++++++++++++++++++------------------------- 1 file changed, 165 insertions(+), 144 deletions(-) diff --git a/readme.md b/readme.md index 96589dc..ee5a0ce 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ [![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-like functionality to any async JavaScript or TypeScript function. It allows creation of composable and reusable workflows to handle functionality like +`@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 - Profiling @@ -19,8 +19,8 @@ import { hooks } from '@feathersjs/hooks'; // We're going to wrap `sayHi` with hook middleware. class Hello { - async sayHi (name) { - return `Hi ${name}` + async sayHi(name) { + return `Hi ${name}`; } } @@ -31,12 +31,14 @@ const logRuntime = async (context, next) => { await next(); // In this example, `next` is `sayHi`. const duration = new Date().getTime() - start; - console.log(`Function '${context.method}' returned '${context.result}' after ${duration}ms`); -} + console.log( + `Function '${context.method}' returned '${context.result}' after ${duration}ms` + ); +}; // The `hooks` utility wraps `logRuntime` around `sayHi`. hooks(Hello, { - sayHi: [ logRuntime ] + sayHi: [logRuntime] }); // Calling `sayHi` will start by calling the `logRuntime` hook. @@ -115,7 +117,7 @@ The `@feathersjs/hooks` npm package works in any modern browser and is compatibl ## Intro to Async Hooks -The fundamental building block of `@feathersjs/hooks` is the "Async Hook". An "Async Hook" is an `async` function that accepts two arguments: +The fundamental building block of `@feathersjs/hooks` is the "Async Hook". An "Async Hook" is an `async` function that accepts two arguments: - 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. @@ -127,43 +129,45 @@ 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. -} +}; ``` -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. +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. ## 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) +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' +import { hooks } from '@feathersjs/hooks'; -const make_request = () => { /* make a request to the database server */ } +const make_request = () => { + /* make a request to the database server */ +}; const verify_auth = (context, next) => { /* Do auth verification before calling `await next()` */ - await next() -} + await next(); +}; -const request_with_middleware = hooks(make_request, [verify_auth]) +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. +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`. +> **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. +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 -The following example updates a class's `sayHi` method to log information about a function call. This syntax also works on plain objects. +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'); @@ -171,8 +175,8 @@ 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}` + async sayHi(name) { + return `Hi ${name}`; } } @@ -185,11 +189,15 @@ const logRuntime = async (context, 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`); -} + 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 ] }); +hooks(Hello, { sayHi: [logRuntime] }); // You can now use the wrapped instance methods inside any async function. (async () => { @@ -201,13 +209,7 @@ hooks(Hello, { sayHi: [ logRuntime ] }); ### TypeScript with the `@hooks` Decorator -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. */ -``` - -Now hooks can be registered using the `@hooks` decorator: +With TypeScript, you can use `hooks` the same was as shown in the above JavaScript example, or - as of TypeScript 5 - use the `@hooks` decorator: ```ts import { hooks, HookContext, NextFunction } from '@feathersjs/hooks'; @@ -218,12 +220,16 @@ 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`); -} + console.log( + `Function '${context.method || '[no name]'}' returned '${ + context.result + }' after ${end - start}ms` + ); +}; class Hello { - @hooks([ logRuntime ]) // the @hooks decorator - async sayHi (name: string) { + @hooks([logRuntime]) // the @hooks decorator + async sayHi(name: string) { return `Hi ${name}`; } } @@ -235,6 +241,8 @@ class Hello { })(); ``` +> **Note:** An `experimentalDecorators` compatible decorator for earlier TypeScript versions can be used via `import { legacyDecorator } from '@feathersjs/hooks'`. + ## The `middleware` Manager You can use a `middleware` manager, instead of a plain array of hook functions, to enable additional functionality. @@ -250,7 +258,7 @@ The `middleware` function creates a middleware Manager which has three important ```js const { hooks, middleware } = require('@feathersjs/hooks'); -const sayHiWithHooks = hooks(sayHi, middleware([ hook1, hook2, hook3 ])); +const sayHiWithHooks = hooks(sayHi, middleware([hook1, hook2, hook3])); (async () => { await sayHiWithHooks('David'); @@ -259,11 +267,12 @@ const sayHiWithHooks = hooks(sayHi, middleware([ hook1, hook2, hook3 ])); ### params(...names) -Supplies names for original function arguments. Instead of appearing in `params.arguments`, the arguments will be named in the order provided. +Supplies names for original function arguments. Instead of appearing in `params.arguments`, the arguments will be named in the order provided. ```js -const sayHiWithHooks = hooks(sayHi, - middleware([ hook1, hook2, hook3 ]).params('name', 'age') +const sayHiWithHooks = hooks( + sayHi, + middleware([hook1, hook2, hook3]).params('name', 'age') ); ``` @@ -272,27 +281,31 @@ const sayHiWithHooks = hooks(sayHi, Initializes properties on the `context` ```js -const sayHiWithHooks = hooks(sayHi, - middleware([ hook1, hook2, hook3 ]).params('name').props({ customProperty: true }) +const sayHiWithHooks = hooks( + sayHi, + middleware([hook1, hook2, hook3]) + .params('name') + .props({ customProperty: true }) ); ``` -> __Note:__ `.props` must not contain any of the field names defined in `.params`. +> **Note:** `.props` must not contain any of the field names defined in `.params`. ### 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 sayHi = async name => `Hello ${name}`; +const sayHi = async (name) => `Hello ${name}`; -const sayHiWithHooks = hooks(sayHi, +const sayHiWithHooks = hooks( + sayHi, middleware([]) .params('name') .defaults((self, args, context) => { return { name: 'Unknown human' - } + }; }) ); ``` @@ -339,7 +352,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. This can be done by registering hooks with an empty array. +> **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 Example @@ -347,13 +360,13 @@ Similar to object hooks, class hooks modify the class (or class prototype). Just const { hooks } = require('@feathersjs/hooks'); class HelloSayer { - async sayHello (name) { + async sayHello(name) { return `Hello ${name}`; } } class HappyHelloSayer extends HelloSayer { - async sayHello (name) { + async sayHello(name) { const baseHello = await super.sayHello(name); return baseHello + '!!!!! :)'; } @@ -376,10 +389,12 @@ hooks(HappyHelloSayer.prototype, [ // Enabling hooks on sayHello also allows the global hooks to run. 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 () => { @@ -403,17 +418,19 @@ import { hooks, HookContext, NextFunction } from '@feathersjs/hooks'; } ]) class HelloSayer { - @hooks(middleware([ - async (context: HookContext, next: NextFunction) => { - console.log('Hook on HelloSayer.sayHello'); - await next(); - } - ]).params('name')) - async sayHello (name: string) { + @hooks( + middleware([ + async (context: HookContext, next: NextFunction) => { + console.log('Hook on HelloSayer.sayHello'); + await next(); + } + ]).params('name') + ) + async sayHello(name: string) { return `Hello ${name}`; } - async otherMethod () { + async otherMethod() { return 'This will not run any hooks'; } } @@ -425,7 +442,7 @@ class HelloSayer { } ]) class HappyHelloSayer extends HelloSayer { - async sayHello (name: string) { + async sayHello(name: string) { const message = await super.sayHello(name); return `${message}!!!!! :)`; } @@ -438,7 +455,7 @@ 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. +> **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 @@ -489,26 +506,29 @@ const sayHello = async (firstName, lastName) => { return `Hello ${firstName} ${lastName}!`; }; -const wrappedSayHello = hooks(sayHello, middleware([ - async (context, next) => { - // Now we can modify `context.lastName` instead - context.lastName = 'X'; - await next(); - } -]).params('firstName', 'lastName')); +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 to preserve the order of named params. +> **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. +> **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 @@ -530,26 +550,29 @@ const emphasize = async (context, next) => { context.result += '!!!'; }; -const sayHello = hooks(async name => `Hello ${name}`, [ emphasize ]); +const sayHello = hooks(async (name) => `Hello ${name}`, [emphasize]); -const o = hooks({ - async sayHi(name) { - return `Hi ${name}`; +const o = hooks( + { + async sayHi(name) { + return `Hi ${name}`; + } + }, + { + sayHi: [emphasize] } -}, { - 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')) + console.log(await o.sayHi.original.call(o, 'David')); })(); ``` ### Customizing and returning the context -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: +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'); @@ -559,11 +582,14 @@ const customContextData = async (context, next) => { context.customProperty = 'Hi'; await next(); -} +}; -const sayHello = hooks(async message => { - return `Hello ${message}!`; -}, [ customContextData ]); +const sayHello = hooks( + async (message) => { + return `Hello ${message}!`; + }, + [customContextData] +); const customContext = sayHello.createContext({ message: 'Hi from context' @@ -593,33 +619,29 @@ The following example uses hooks named `one`, `two`, and `three` to demonstrate ```js const { hooks } = require('@feathersjs/hooks'); -const sayHello = async message => { - console.log(`HELLO, ${message}!`) +const sayHello = async (message) => { + console.log(`HELLO, ${message}!`); }; const one = async (ctx, next) => { console.log('one before'); await next(); - console.log('one after') -} + console.log('one after'); +}; const two = async (ctx, next) => { console.log('two before'); await next(); - console.log('two after') -} + console.log('two after'); +}; const three = async (ctx, next) => { console.log('three before'); await next(); - console.log('three after') -} + console.log('three after'); +}; -const sayHelloWithHooks = hooks(sayHello, [ - one, - two, - three -]); +const sayHelloWithHooks = hooks(sayHello, [one, two, three]); (async () => { await sayHelloWithHooks('DAVID'); @@ -642,13 +664,13 @@ This order also applies when using hooks on [objects](#object-hooks) and [classe ### 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: +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 // A Regular Hook is just an async function that receives the context object. const regularHook = async (context) => { // All code goes here. -} +}; ``` With @feathersjs/hooks, the `collect` utility enables the use of Regular Hooks. @@ -657,20 +679,22 @@ With @feathersjs/hooks, the `collect` utility enables the use of Regular Hooks. #### The `collect` utility -The `collect` utility enables Regular Hooks functionality. It gathers hooks into `before`, `after`, and `error` hooks. Here's what it looks like. +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' +import { hooks } from '@feathersjs/hooks'; +import { discard } from 'feathers-hooks-common'; -const make_request = () => { /* make a request to the database server */ } +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 request_with_middleware = hooks( make_request, @@ -681,14 +705,14 @@ const request_with_middleware = hooks( error: [handle_error] }) ]) -) +); ``` Or with a class: ```ts -import { hooks } from '@feathersjs/hooks' -import { discard } from 'feathers-hooks-common' +import { hooks } from '@feathersjs/hooks'; +import { discard } from 'feathers-hooks-common'; class DbAdapter { create() { @@ -698,23 +722,20 @@ class DbAdapter { const verify_auth = (context) => { /* Do auth verification, here */ -} +}; const handle_error = (context) => { /* Do some error handling */ -} +}; -const request_with_middleware = hooks( - DbAdapter, - { - create: middleware([ - collect({ - before: [verify_auth], - after: [discard('password')], - error: [handle_error] - }) - ]), - } -) +const request_with_middleware = hooks(DbAdapter, { + create: middleware([ + collect({ + before: [verify_auth], + after: [discard('password')], + error: [handle_error] + }) + ]) +}); ``` # Best practises @@ -732,14 +753,14 @@ const request_with_middleware = hooks( context.query = { ...context.query, active: true - } + }; await next(); - } + }; - const findUser = hooks(async query => { + const findUser = hooks(async (query) => { return collection.find(query); - }, middleware([ updateQuery ]).params('query')); + }, middleware([updateQuery]).params('query')); ``` # More Examples @@ -772,12 +793,15 @@ const cache = () => { // Set the cached value to the result cacheData[key] = context.result; - } -} + }; +}; -const getData = hooks(async url => { - return axios.get(url); -}, [ cache() ]); +const getData = hooks( + async (url) => { + return axios.get(url); + }, + [cache()] +); await getData('http://url-that-takes-long-to-respond'); ``` @@ -787,17 +811,17 @@ 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 checkPermission = name => async (context, next) => { +const checkPermission = (name) => async (context, next) => { if (!context.user.permissions.includes(name)) { throw new Error(`User does not have ${name} permission`); } await next(); -} +}; const deleteInvoice = hooks(async (id, user) => { return collection.delete(id); -}, middleware([ checkPermission('admin') ]).params('id', 'user')); +}, middleware([checkPermission('admin')]).params('id', 'user')); ``` ## Cleaning up GraphQL resolvers @@ -807,27 +831,24 @@ The above examples can both be useful for speeding up and locking down existing ```js const { hooks } = require('@feathersjs/hooks'); -const checkPermission = name => async (ctx, next) => { +const checkPermission = (name) => async (ctx, next) => { const { context } = ctx; if (!context.user.permissions.includes(name)) { throw new Error(`User does not have ${name} permission`); } await next(); -} +}; const resolvers = { Query: { human: hooks(async (obj, args, context, info) => { - return context.db.loadHumanByID(args.id).then( - userData => new Human(userData) - ) - }, middleware([ - cache(), - checkPermission('admin') - ]).params('obj', 'args', 'context', 'info')) + return context.db + .loadHumanByID(args.id) + .then((userData) => new Human(userData)); + }, middleware([cache(), checkPermission('admin')]).params('obj', 'args', 'context', 'info')) } -} +}; ``` # Contributing