Common hooks
When it makes sense to do so, some plug-ins include their own hooks. The following plug-ins come bundled with useful hooks:
feathers-hooks
(see note below)feathers-mongoose
feathers-authentication
The next version of feathers-hooks (1.6.0) will export feathers-hooks-common instead of the previous bundled hooks. This will provide backward compatibility.
Feathers-hooks in Feathers 3.0 will become part of core and you will have to import feathers-hooks-common separately.
dr;tl Start using feathers-hooks-common now.
feathers-hooks-common
Useful hooks for use with Feathersjs services.
- Data Items
- Query Params
- Authorization
- Database
- Utilities
- Running hooks conditionally
- Utilities for Writing Hooks
- Things that are deprecated
Data items
populate
populate(fieldName: string, { service: ServiceName, field = fieldName }): HookFunc
The populate
hook uses a property from the result (or every item if it is a list) to retrieve a single related object from a service and add it to the original object.
- Used as an after hook on any service method.
- Supports multiple result items, including paginated
find
. - Supports an array of keys in
field
.
const hooks = require('feathers-hooks-common');
// Given a `user_id` in a message, retrieve the user and
// add it in the `user` field.
app.service('messages').after(
hooks.populate('user', {
service: 'users',
field: 'user_id'
})
);
/*
If the object from the message service is
{ _id: '1...1', senderId: 'a...a', text: 'Jane, are you there?' }
when the hook is run
hooks.populate('senderId', { service: '/users', field: 'user' })
the result will contain
{ _id: '1...1', senderId : 'a...a', text: 'Jane, are you there?',
user: { _id: 'a...a', name: 'John Doe'} }
If `senderId` is an array of keys, then `user` will be an array of populated items.
*/
Options
fieldName
[required] - The field name you want to populate the related object on to.service
[required] - The service you want to populate the object from.field
[optional. default:fieldName
] - The field you want to look up the related object by from the service. That is, the foreign key we have for the service.
If field
is an array of keys, then fieldName
will contain an array of service objects.
If field
is not provided, then fieldName
is used,
in which case the value(s) of fieldName
will first be used to look up the service,
and then fieldName
will be set to the service object(s) selected.
remove
remove(fieldName: string, ...fieldNames?: string[]): HookFunc
Remove the given fields either from the data submitted or from the result. If the data is an array or a paginated find
result the hook will remove the field(s) for every item.
- Used as a
before
hook forcreate
,update
orpatch
. - Used as an
after
hook. - Field names support dot notation e.g.
name.address.city
. - Supports multiple data items, including paginated
find
.
const hooks = require('feathers-hooks-common');
// Remove the hashed `password` and `salt` field after all method calls
app.service('users').after(hooks.remove('password', 'salt'));
// Remove _id for `create`, `update` and `patch`
app.service('users').before({
create: hooks.remove('_id'),
update: hooks.remove('_id'),
patch: hooks.remove('_id')
})
ProTip: This hook will only fire when
params.provider
has a value, i.e. when it is an external request over REST or Sockets.
Options
fieldName
[required] - The first field that you want to remove from the object(s).fieldNames
[optional] - Other fields that you want to remove.
pluck
pluck(fieldName: string, ...fieldNames?: string[]): HookFunc
Discard all other fields except for the provided fields either from the data submitted or from the result. If the data is an array or a paginated find
result the hook will remove the field(s) for every item.
- Used as a
before
hook forcreate
,update
orpatch
. - Used as an
after
hook. - Field names support dot notation.
- Supports multiple data items, including paginated
find
.
const hooks = require('feathers-hooks-common');
// Only retain the hashed `password` and `salt` field after all method calls
app.service('users').after(hooks.pluck('password', 'salt'));
// Only keep the _id for `create`, `update` and `patch`
app.service('users').before({
create: hooks.pluck('_id'),
update: hooks.pluck('_id'),
patch: hooks.pluck('_id')
})
ProTip: This hook will only fire when
params.provider
has a value, i.e. when it is an external request over REST or Sockets.
Options
fieldName
[required] - The fields that you want to retain from the object(s).fieldNames
[optional] - The other fields that you want to retain.All other fields will be discarded.
lowerCase
lowerCase(fieldName: string, ...fieldNames?: string[]): HookFunc
Lower cases the given fields either in the data submitted or in the result. If the data is an array or a paginated find
result the hook will lowercase the field(s) for every item.
- Used as a
before
hook forcreate
,update
orpatch
. - Used as an
after
hook. - Field names support dot notation.
- Supports multiple data items, including paginated
find
.
const hooks = require('feathers-hooks-common');
// lowercase the `email` and `password` field before a user is created
app.service('users').before({
create: hooks.lowerCase('email', 'username')
});
Options
fieldName
[required] - The fields that you want to lowercase from the retrieved object(s).fieldNames
[optional] - The other fields that you want to lowercase.
setCreatedAt
setCreatedAt(fieldName = 'createdAt', ...fieldNames?: string[])
Add the fields with the current date-time.
- Used as a
before
hook forcreate
,update
orpatch
. - Used as an
after
hook. - Field names support dot notation.
- Supports multiple data items, including paginated
find
.
const hooks = require('feathers-hooks-common');
// set the `createdAt` field before a user is created
app.service('users').before({
create: [ hooks.setCreatedAt('createdAt') ]
};
Options
fieldName
[optional. default:createdAt
] - The field that you want to add with the current date-time to the retrieved object(s).fieldNames
[optional] - Other fields to add with the current date-time.
setUpdatedAt
setUpdatedAt(fieldName = 'updatedAt', ...fieldNames?: string[]): HookFunc
Add or update the fields with the current date-time.
- Used as a
before
hook forcreate
,update
orpatch
. - Used as an
after
hook. - Field names support dot notation.
- Supports multiple data items, including paginated
find
.
const hooks = require('feathers-hooks-common');
// set the `updatedAt` field before a user is created
app.service('users').before({
create: [ hooks.setCreatedAt('updatedAt') ]
};
Options
fieldName
[optional. default:updatedAt
] - The fields that you want to add or update in the retrieved object(s).fieldNames
[optional] - Other fields to add or update with the current date-time.
Query Params
removeQuery
removeQuery(...fieldNames?: string[]): HookFunc
Remove the given fields from the query params.
- Used as a
before
hook. - Field names support dot notation
- Supports multiple data items, including paginated
find
.
const hooks = require('feathers-hooks-common');
// Remove _id from the query for all service methods
app.service('users').before({
all: hooks.removeQuery('_id')
});
ProTip: This hook will only fire when
params.provider
has a value, i.e. when it is an external request over REST or Sockets.
Options
fieldNames
[optional] - The fields that you want to remove from the query object.
pluckQuery
pluckQuery(...fieldNames?: string[]): HookFunc
Discard all other fields except for the given fields from the query params.
- Used as a
before
hook. - Field names support dot notation.
- Supports multiple data items, including paginated
find
.
const hooks = require('feathers-hooks-common');
// Discard all other fields except for _id from the query
// for all service methods
app.service('users').before({
all: hooks.pluckQuery('_id')
});
ProTip: This hook will only fire when
params.provider
has a value, i.e. when it is an external request over REST or Sockets.
Options
fieldNames
[optional] - The fields that you want to retain from the query object. All other fields will be discarded.
Authorization
Most hooks dealing with authentication and authorization come bundled with feathers-authentication
.
disable
disable(provider?: string, ...providers: string[]): HookFunc
Disables access to a service method completely or for specific providers. All providers (REST, Socket.io and Primus) set the params.provider
property which is what disable
checks for.
- Used as a
before
hook. - Disable service completely, for all external providers, or for certain providers.
const hooks = require('feathers-hooks-common');
app.service('users').before({
// Users can not be created by external access
create: hooks.disable('external'),
// A user can not be deleted through these two web socket providers
remove: hooks.disable('socketio', 'primus'),
// Disable calling `update` completely (e.g. to only support `patch`)
update: hooks.disable(),
});
ProTip: Service methods that are not implemented do not need to be disabled.
Options
provider
[optional. default: _disables everything] - The transport that you want to disable this service method for. Options are:socketio
- will disable the method for the Socket.IO providerprimus
- will disable the method for the Primus providerrest
- will disable the method for the REST providerexternal
- will disable access from all providers making a service method only usable internally.
providers
[optional] - Other transports you want to disable.
Database
softDelete
softDelete(fieldName = 'deleted'): HookFunc
On remove
, marks an object as deleted rather than removing the object.
Instead of remove
, a patch
is performed to mark the object as deleted.
On find
, removes such marked objects from the results.
- Used as a
before
hook forremove
to mark objects as deleted. - Used as a
before
hook forfind
to ignore objects marked as deleted. - Field names support dot notation.
- Supports multiple data items, including paginated
find
.
const hooks = require('feathers-hooks-common');
app.service('stockItems').before({
// Ignore any objects marked as deleted.
find: hooks.softDelete(),
// Mark an object as deleted rather than physically deleting it.
remove: hooks.softDelete(),
});
Options
fieldName
[optional. default:deleted
] - The name of the field holding the deleted flag.
Utilities
setSlug
setSlug(slug: string, fieldName = 'query.' + slug): HookFunc
A service may have a slug in its URL, e.g. storeId
in
app.use('/stores/:storeId/candies', new Service());
.
The service gets slightly different values depending on the transport used by the client.
transport | hook.data.storeId |
hook.params.query |
code run on client |
---|---|---|---|
socketio | undefined |
{ size: 'large', storeId: '123' } |
candies.create({ name: 'Gummi', qty: 100 }, { query: { size: 'large', storeId: '123' } }) |
rest | :storeId |
... same as above | ... same as above |
raw HTTP | 123 |
{ size: 'large' } |
fetch('/stores/123/candies?size=large', .. |
This hook normalizes the difference between the transports. A hook of
all: [ hooks.setSlug('storeId') ]
provides a normalized hook.params.query
of
{ size: 'large', storeId: '123' }
for the above cases.
- Used as a
before
hook. - Field names support dot notation.
const hooks = require('feathers-hooks-common');
app.service('stores').before({
create: [ hooks.setSlug('storeId') ]
});
Options
slug
[required] - The slug as it appears in the route, e.g.storeId
for/stores/:storeId/candies
.fieldName
[optional. default:query[slugId]
] - The field to contain the slug value.
debug
debug(label?: string)
Display current info about the hook to console.
- Used as a
before
orafter
hook.
const hooks = require('feathers-hooks-common');
hooks.debug('step 1')
// * step 1
// type: before, method: create
// data: { name: 'Joe Doe' }
// query: { sex: 'm' }
// result: { assigned: true }
Options
label
[optional] - Label to identify the debug listing.
Utilities to promisify functions
Wrap functions so they return Promises.
fnPromisifyCallback
fnPromisifyCallback(callbackFunc: CallbackFunc, paramsCount?: number): PromiseFunc
Wrap a function calling a callback into one that returns a Promise.
- Promise is rejected if the function throws.
const fnPromisifyCallback = require('feathers-hooks-common/promisify').fnPromisifyCallback;
function tester(data, a, b, cb) {
if (data === 3) { throw new Error('error thrown'); }
cb(data === 1 ? null : 'bad', data);
}
const wrappedTester = fnPromisifyCallback(tester, 3); // because func call requires 3 params
wrappedTester(1, 2, 3); // tester(1, 2, 3, wrapperCb)
wrappedTester(1, 2); // tester(1, 2, undefined, wrapperCb)
wrappedTester(); // tester(undefined, undefined undefined, wrapperCb)
wrappedTester(1, 2, 3, 4, 5); // tester(1, 2, 3, wrapperCb)
wrappedTester(1, 2, 3).then( ... )
.catch(err => { console.log(err instanceof Error ? err.message : err); });
Options
callbackFunc
[required] - A function which uses a callback as its last param.paramsCount
[optional. default: count is obtained by parsing func signature] - The number of parameterscallbackFunc
expects. This count does not include the callback param itself.
The wrapped function will always be called with that many params, preventing potential bugs.
The function signature is parsed to obtain the count if the number of params is not provided.
Parsing function signatures
Parsing function signatures turns out to be surprisingly difficult. We are using the best signature parser as of Oct 2016. It however has problems with some situations which are unlikely to occur in practice.
These bugs all involve params which have defaults, when their calculation involves parenthesis or commas, e.g.
function abc(a, b = () => {}, c) {}
function abc(a, b = (x, y) => {}, c) {}
function abc(a, b = 5 * (1 + 2), c) {}
const abc = (a, b = 'x,y'.indexOf('y'), c) {};
As an aside, these cases all go away if you transpile your code with Babel.
Running hooks conditionally
There are times when you may want to run a hook conditionally, perhaps depending on the provider, the user authorization, if the user created the record, etc.
A custom service may be designed to always be called with the create
method,
with a data value specifying the action the service is to perform.
Certain actions may require authentication or authorization,
while others do not.
iff
iff(predicateFunc: function, hookFunc: HookFunc): HookFunc
Run a predicate function, which returns either a boolean, or a Promise which evaluates to a boolean. Run the hook if the result is truesy.
- Used as a
before
orafter
hook. - Predicate may be a sync or async function.
- Hook to run may be sync or async.
feathers-hooks
catches any errors thrown in the predicate or hook.
const hooks = require('feathers-hooks-common');
const isNotAdmin = adminRole => hook => hook.params.user.roles.indexOf(adminRole || 'admin') === -1;
app.service('workOrders').after({
// async predicate and hook
create: hooks.iff(
() => new Promise((resolve, reject) => { ... }),
hooks.populate('user', { field: 'authorisedByUserId', service: 'users' })
)
});
app.service('workOrders').after({
// sync predicate and hook
find: [ hooks.iff(isNotAdmin(), hooks.remove('budget')) ]
});
Options
predicateFunc
[required] - Function to determine if hookFunc should be run or not.predicateFunc
is called with the hook as its param. It returns either a boolean or a Promise that evaluates to a boolean.hookFunc
[required] - A hook function.
isNot
isNot(predicateFunc: function)
Negate the predicateFunc
.
- Used with
hooks.iff
. - Predicate may be a sync or async function.
feathers-hooks
catches any errors thrown in the predicate.
import hooks, { iff, isNot, isProvider } from 'feathers-hooks-common';
const isRequestor = () => hook => new Promise(resolve, reject) => ... );
app.service('workOrders').after({
iff(isNot(isRequestor()), hooks.remove( ... ))
});
Options
predicateFunc
[required] - A function which returns either a boolean or a Promise that resolves to a boolean.
isProvider
isProvider(provider: string, providers?: string[])
Check which transport called the service method.
All providers (REST, Socket.io and Primus) set the params.provider
property which is what isProvider
checks for.
- Used as a predicate function with
hooks.iff
.
import hooks, { iff, isProvider } from 'feathers-hooks-common';
app.service('users').after({
iff(isProvider('external'), hooks.remove( ... ))
});
Options
provider
[required] - The transport that you want this hook to run for. Options are:server
- Run the hook if the server called the service method.external
- Run the hook if any transport other than the server called the service method.socketio
- Run the hook if the Socket.IO provider called the service method.primus
- If the Primus provider.rest
- If the REST provider.
providers
[optional] - Other transports that you want this hook to run for.
Utilities for Writing Hooks
These utilities may be useful when you are writing your own hooks.
You can import them from feathers-hooks-common/lib/utils
.
checkContext
checkContext(hook: Hook, type: string|null, methods?: string[]|string, label: string): void
Restrict the hook to a hook type (before, after) and a set of hook methods (find, get, create, update, patch, remove).
const checkContext = require('feathers-hooks-common/lib/utils').checkContext;
function myHook(hook) {
checkContext(hook, 'before', ['create', 'remove']);
...
}
app.service('users').after({
create: [ myHook ] // throws
});
// checkContext(hook, 'before', ['update', 'patch'], 'hookName');
// checkContext(hook, null, ['update', 'patch']);
// checkContext(hook, 'before', null, 'hookName');
// checkContext(hook, 'before');
Options
hook
[required] - The hook provided to the hook function.type
[optional] - The hook may be run inbefore
orafter
.null
allows the hook to be run in either.methods
[optional] - The hook may be run for these methods.label
[optional] - The label to identify the hook in error messages, e.g. its name.
getItems, replaceItems
`getItems(hook: Hook): Item[]|Item
replaceItems(hook: Hook, items: Item[]|Item): void
'getItemsgets the data items in a hook. The items may be
hook.data,
hook.resultor
hook.result.datadepending on where the hook is used, the method its used with and if pagination is used.
undefined`, an object or an array of objects may be returned.
replaceItems
is the companion to getItems
. It updates the data items in the hook.
- Handles before and after hooks.
- Handles paginated and non-paginated results from
find
.
import { getItems, replaceItems } from 'feathers-hooks-common/lib/utils';
const insertCode = (code) => (hook) {
const items = getItems(hook);
!Array.isArray(items) ? items.code = code : (items.forEach(item => { item.code = code; }));
replaceItems(hook, items);
}
app.service('messages').before = {
create: insertCode('a')
};
Options
hook
[required] - The hook provided to the hook function.items
[required] - The updated item or array of items.
getByDot, setByDot
getByDot(obj: object, path: string): any
setByDot(obj: object, path: string, value: any, ifDelete: boolean): void
getItems
gets a value from an object using dot notation, e.g. employee.address.city
. It does not differentiate between non-existent paths and a value of undefined
.
setByDot
is the companion to getByDot
. It sets a value in an object using dot notation.
- Optionally deletes properties in object.
import { getByDot, setByDot } from 'feathers-hooks-common/lib/utils';
const setHomeCity = () => (hook) => {
const city = getByDot(hook.data, 'person.address.city');
setByDot(hook, 'data.person.home.city', city);
}
app.service('directories').before = {
create: setHomeCity()
};
Options
obj
[required] - The object we get data from or set data in.path
[required] - The path to the data, e.g.person.address.city
. Array notion is not supported, e.g.order.lineItems[1].quantity
.value
[required] - The value to set the data to.ifDelete
[optional. default:false
] - Delete the property at the end of the path ifvalue
isundefined
. Note that new empty inner objects will still be created, e.g.setByDot({}, 'a.b.c', undefined, true)
will result in{a: b: {} }
.
Things that are deprecated
A few things from feathers-hooks
have been deprecated and will be removed in a future version of feathers-hooks-common
.
- The hooks bundled with
feathers-hooks
are deprecated. Usefeathers-hooks-common
instead. - Many hooks allowed a predicate function as their last param, e.g.
remove('name', () => true)
. This allowed the hook to be conditionally run.iff(predicate, hookFunc)
should be used instead. - Instead of
restrictToRoles
use the expanded hooks bundled with the next version offeathers-authentication
. - The several validation hooks are deprecated. Use validate instead.