All Projects → feathersjs → Hooks

feathersjs / Hooks

Licence: mit
Async middleware for JavaScript and TypeScript

Programming Languages

typescript
32286 projects

Projects that are alternatives of or similar to Hooks

React Use Wizard
🧙 A React wizard (stepper) builder without the hassle, powered by hooks.
Stars: ✭ 162 (+38.46%)
Mutual labels:  async, hooks, hook
Use Onclickoutside
React hook for listening for clicks outside of an element.
Stars: ✭ 361 (+208.55%)
Mutual labels:  hooks, hook
Swr
React Hooks for data fetching
Stars: ✭ 20,348 (+17291.45%)
Mutual labels:  hook, hooks
Gear
A lightweight, composable and high performance web service framework for Go.
Stars: ✭ 544 (+364.96%)
Mutual labels:  middleware, hooks
hookr
PHP action and filter hook system
Stars: ✭ 39 (-66.67%)
Mutual labels:  hooks, hook
MouseInjectDetection
Simple method of checking whether or not mouse movement or buttons (<windows 10) are injected
Stars: ✭ 29 (-75.21%)
Mutual labels:  hooks, hook
React Query
⚛️ Hooks for fetching, caching and updating asynchronous data in React
Stars: ✭ 24,427 (+20777.78%)
Mutual labels:  async, hooks
useAudioPlayer
Custom React hook & context for controlling browser audio
Stars: ✭ 176 (+50.43%)
Mutual labels:  hooks, hook
Webhook
webhook is a lightweight incoming webhook server to run shell commands
Stars: ✭ 7,201 (+6054.7%)
Mutual labels:  hooks, hook
Before After Hook
wrap methods with before/after hooks
Stars: ✭ 49 (-58.12%)
Mutual labels:  async, hooks
Fontmod
Simple hook tool to change Win32 program font.
Stars: ✭ 1,064 (+809.4%)
Mutual labels:  hooks, hook
use-bus
React hook to subscribe and dispatch events accros React components
Stars: ✭ 51 (-56.41%)
Mutual labels:  hooks, hook
redux-tools
Redux tools to speed up development.
Stars: ✭ 16 (-86.32%)
Mutual labels:  hook, middleware
Radioactive State
☢ Make Your React App Truly Reactive!
Stars: ✭ 273 (+133.33%)
Mutual labels:  hooks, hook
rusty-hook
git hook manager, geared toward Rust projects
Stars: ✭ 93 (-20.51%)
Mutual labels:  hooks, hook
Transmittable Thread Local
📌 TransmittableThreadLocal (TTL), the missing Java™ std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.
Stars: ✭ 4,678 (+3898.29%)
Mutual labels:  async, middleware
React Selector Hooks
Collection of hook-based memoized selector factories for declarations outside of render.
Stars: ✭ 84 (-28.21%)
Mutual labels:  hooks, hook
transition-hook
☄️ An extremely light-weight react transition animation hook which is simpler and easier to use than react-transition-group
Stars: ✭ 250 (+113.68%)
Mutual labels:  hooks, hook
entangle
Global state management tool for react hooks inspired by RecoilJS and Jotai using proxies.
Stars: ✭ 26 (-77.78%)
Mutual labels:  hooks, hook
Redux Ecosystem Links
A categorized list of Redux-related addons, libraries, and utilities
Stars: ✭ 5,076 (+4238.46%)
Mutual labels:  async, middleware

@feathersjs/hooks

CI GitHub action

@feathersjs/hooks brings middleware to any async JavaScript or TypeScript function. It allows to create composable and reusable workflows that can add

  • Logging
  • Profiling
  • Validation
  • Caching/Debouncing
  • Permissions
  • Data pre- and postprocessing
  • 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.

Installation

Node

npm install @feathersjs/hooks --save
yarn add @feathersjs/hooks

Deno

import { hooks } from 'https://unpkg.com/@feathersjs/[email protected]/deno/index.ts';

Note: You might want to replace latest with the actual version you want to use (e.g. https://unpkg.com/@feathersjs/[email protected]^0.2.0/deno/index.ts)

Browser

@feathersjs/hooks is compatible with any module loader like Webpack and can be included in the browser directly via:

<script type="text/javascript" src="//unpkg.com/@feathersjs/[email protected]^0.2.0/dist/hooks.js"></script>

Which will make a hooks global variable available.

Quick Example

JavaScript

The following example logs information about a function call:

const { hooks } = require('@feathersjs/hooks');

const logRuntime = async (context, next) => {
  const start = new Date().getTime();

  await next();

  const end = new Date().getTime();

  console.log(`Function '${context.method || '[no name]'}' returned '${context.result}' after ${end - start}ms`);
}

// Hooks can be used with a function like this:
const sayHello = hooks(async name => {
  return `Hello ${name}!`;
}, [
  logRuntime
]);

// And on an object or class like this
class Hello {
  async sayHi (name) {
    return `Hi ${name}`
  }
}

hooks(Hello, {
  sayHi: [
    logRuntime
  ]
});

(async () => {
  console.log(await sayHello('David'));

  // The following would throw an error
  // await sayHello('   ');

  const hi = new Hello();

  console.log(await hi.sayHi('Dave'));
})();

TypeScript

In addition to the normal JavaScript use, with the experimentalDecorators option in tsconfig.json enabled

"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */

Hooks can be registered using a decorator:

import { hooks, HookContext, NextFunction } from '@feathersjs/hooks';

const logRuntime = async (context: HookContext, next: NextFunction) => {
  const start = new Date().getTime();

  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
  ])
  async sayHi (name: string) {
    return `Hi ${name}`;
  }
}

(async () => {
  const hi = new Hello();

  console.log(await hi.sayHi('David'));
  // The following would throw an error
  // console.log(await hi.sayHi('  '));
})();

Documentation

Middleware

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.

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 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

The following example:

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 sayHelloWithHooks = hooks(sayHello, [
  hook1,
  hook2,
  hook3
]);

(async () => {
  await sayHelloWithHooks('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 and classes and with inheritance.

Function hooks

hooks(fn, middleware[]|manager) returns a new function that wraps fn with middleware

const { hooks, middleware } = require('@feathersjs/hooks');

const sayHello = async name => {
  return `Hello ${name}!`;
};

const wrappedSayHello = hooks(sayHello, middleware([
  async (context, next) => {
    console.log(context.someProperty);
    await next();
  }
]).params('name'));

(async () => {
  console.log(await wrappedSayHello('David'));
})();

Important: A wrapped function will always return a Promise even it was not originally async.

Object hooks

hooks(obj, middlewareMap) takes an object and wraps the functions indicated in middlewareMap. It will modify the existing Object obj:

const { hooks, middleware } = require('@feathersjs/hooks');

const o = {
  async sayHi (name, quote) {
    return `Hi ${name} ${quote}`;
  }

  async sayHello (name) {
    return `Hello ${name}!`;
  }
}

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:

const { hooks } = require('@feathersjs/hooks');

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
hooks(o, [
  async (context, next) => {
    console.log('Top level hook');
    await next();
  }
]);

hooks(o, {
  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 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

const { hooks } = require('@feathersjs/hooks');

class HelloSayer {
  async sayHello (name) {
    return `Hello ${name}`;
  }
}

class HappyHelloSayer extends HelloSayer {
  async sayHello (name) {
    const baseHello = await super.sayHello(name);
    return baseHello + '!!!!! :)';
  }
}

// To add hooks at the class level we need to use the prototype object
hooks(HelloSayer.prototype, [
  async (context, next) => {
    console.log('Hook on HelloSayer');
    await next();
  }
]);

hooks(HappyHelloSayer.prototype, [
  async (context, next) => {
    console.log('Hook on HappyHelloSayer');
    await next();
  }
]);

// Methods can also be wrapped directly on the class
hooks(HelloSayer, {
  sayHello: [async (context, next) => {
    console.log('Hook on HelloSayer.sayHello');
    await next();
  }]
});

(async () => {
  const happy = new HappyHelloSayer();

  console.log(await happy.sayHello('David'));
})();

TypeScript

Using decorators in TypeScript also respects inheritance:

import { hooks, HookContext, NextFunction } from '@feathersjs/hooks';

@hooks([
  async (context: HookContext, next: NextFunction) => {
    console.log('Hook on HelloSayer');
    await next();
  }
])
class HelloSayer {
  @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 () {
    return 'This will not run any hooks';
  }
}

@hooks([
  async (context: HookContext, next: NextFunction) => {
    console.log('Hook on HappyHelloSayer');
    await next();
  }
])
class HappyHelloSayer extends HelloSayer {
  async sayHello (name: string) {
    const message = await super.sayHello(name);
    return `${message}!!!!! :)`;
  }
}

(async () => {
  const happy = new HappyHelloSayer();

  console.log(await happy.sayHello('David'));
})();

Note: Decorators only work on classes and class methods, not on functions. Standalone (arrow) functions require the JavaScript function style hook registration.

Hook Context

The hook context in a middleware function is an object that contains information about the function call.

Context properties

The default properties available are:

  • context.arguments - The arguments of the function as an array
  • 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

Arguments

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:

const { hooks } = require('@feathersjs/hooks');

const sayHello = async (firstName, lastName) => {
  return `Hello ${firstName} ${lastName}!`;
};

const wrappedSayHello = hooks(sayHello, [
  async (context, next) => {
    // Replace the `lastName`
    context.arguments[1] = 'X';
    await next();
  }
]);

(async () => {
  console.log(await wrappedSayHello('David', 'L')); // Hello David X
})();

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 can be initialized like this:

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

  • Set before calling await next() to skip the original function call. Other hooks will still run.
  • Modified after calling await next() to modify what is being returned by the function.

See the cache example for how this can be used.

Calling the original

The original function without any hooks is available as fn.original:

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 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:

const { hooks, HookContext } = require('@feathersjs/hooks');
const customContextData = async (context, next) => {
  console.log('Custom context message is', context.message);

  context.customProperty = 'Hi';

  await next();
}

const sayHello = hooks(async message => {
  return `Hello ${message}!`;
}, [ customContextData ]);

const customContext = sayHello.createContext({
  message: 'Hi from context'
});

(async () => {
  const finalContext = await sayHello('Dave', customContext);
  
  console.log(finalContext);
})();

Options

Instead an array of middleware, a chainable middleware manager that allows to set additional options can be passed like this:

const { hooks, middleware } = require('@feathersjs/hooks');

// 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 () => {
  await sayHelloWithHooks('David');
})();

params(...names)

Inititalizes a list of named parameters.

const sayHelloWithHooks = hooks(sayHello, middleware([
  hook1,
  hook2,
  hook3
]).params('name'));

props(properties)

Initializes properties on the context

const sayHelloWithHooks = hooks(sayHello, middleware([
  hook1,
  hook2,
  hook3
]).params('name').props({
  customProperty: true
}));

Note: .props can 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.

const sayHello = async name => `Hello ${name}`;

const sayHelloWithHooks = hooks(sayHello, middleware([]).params('name').defaults(() => {
  return {
    name: 'Unknown human'
  }
}));

Best practises

  • Hooks can be registered at any time by calling hooks again but registration should be kept in one place for better visibility.

  • Decorators make the flow even more visible by putting it right next to the code the hooks are affecting.

  • The context will always be the same object in the hook flow. You can set any property on it.

  • If a parameter is an object, modifying that object will change the original parameter. This can cause subtle issues that are difficult to debug. Using the spread operator to add the new property and replacing the context property helps to avoid many of those problems:

    const updateQuery = async (context, next) => {
      // NOT: context.query.newProperty = 'something';
    
      // Instead
      context.query = {
        ...context.query,
        active: true
      }
    
      await next();
    }
    
    const findUser = hooks(async query => {
      return collection.find(query);
    }, middleware([ updateQuery ]).params('query'));
    

More Examples

Cache

The following example is a simple hook that caches the results of a function call and uses the cached value. It will clear the cache every 5 seconds. This is useful for any kind of expensive method call like an external HTTP request:

const { hooks } = require('@feathersjs/hooks');
const cache = () => {
  let cacheData = {};

  // Reset entire cache every 5 seconds
  setInterval(() => {
    cacheData = {};
  }, 5000);
  
  return async (context, next) => {
    const key = JSON.stringify(context);

    if (cacheData[key]) {
      // Setting context.result before `await next()`
      // will skip the (expensive function call) and
      // make it return the cached value
      context.result = cacheData[key];
    }

    await next();
    
    // Set the cached value to the result
    cacheData[key] = context.result;
  }
}

const getData = hooks(async url => {
  return axios.get(url);
}, [ cache() ]);

await getData('http://url-that-takes-long-to-respond');

Permissions

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:

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'));

Cleaning up GraphQL resolvers

The above examples can both be useful for speeding up and locking down existing GraphQL resolvers:

const { hooks } = require('@feathersjs/hooks');

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'))
  }
}

License

Copyright (c) 2020

Licensed under the MIT license.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].