Common Hooks
$ npm install feathers-hooks-common --save
feathers-hooks-common is a collection of common hooks and utilities.
Authentication hooks are documented separately.
Note: Many hooks are just a few lines of code to implement from scratch. If you can't find a hook here but are unsure how to implement it or have an idea for a generally useful hook create a new issue here.
client
client(... whitelist) source
A hook for passing params from the client to the server.
- Used as a
beforehook.
ProTip Use the
paramsFromClienthook instead. It does exactly the same thing asclientbut is less likely to be deprecated.
Only the hook.params.query object is transferred to the server from a Feathers client,
for security among other reasons.
However if you can include a hook.params.query.$client object, e.g.
service.find({
query: {
dept: 'a',
$client: {
populate: 'po-1',
serialize: 'po-mgr'
}
}
});
the client hook will move that data to hook.params on the server.
service.before({ all: [ client('populate', 'serialize', 'otherProp'), myHook ]});
// myHook's hook.params will be
// { query: { dept: 'a' }, populate: 'po-1', serialize: 'po-mgr' } }
Options:
whitelist(optional) Names of the potential props to transfer fromquery.client. Other props are ignored. This is a security feature.
ProTip You can use the same technique for service calls made on the server.
See Util: paramsForServer and paramsFromClient.
combine
combine(... hookFuncs) source
Sequentially execute multiple hooks within a custom hook function.
function (hook) { // an arrow func cannot be used because we need 'this'
// ...
hooks.combine(hook1, hook2, hook3).call(this, hook)
.then(hook => {});
}
Options:
hooks(optional) - The hooks to run.
ProTip:
combineis primarily intended to be used within your custom hooks, not when registering hooks.. Its more convenient to use the following when registering hooks:const workflow = [hook1(), hook2(), ...]; app.service(...).hooks({ before: { update: [...workflow], patch: [...workflow], }, });
debug
debug(label) source
Display current info about the hook to console.
- Used as a
beforeorafterhook.
const { debug } = require('feathers-hooks-common');
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.
dePopulate
dePopulate() source
Removes joined and computed properties, as well any profile information.
Populated and serialized items may, after dePopulate, be used in service.patch(id, items) calls.
- Used as a before or after hook on any service method.
- Supports multiple result items, including paginated
find. - Supports an array of keys in
field.
See also populate, serialize.
disallow
disallow(...providers) source
Disallows access to a service method completely or for specific providers. All providers (REST, Socket.io and Primus) set the hook.params.provider property, and disallow checks this.
- Used as a
beforehook.
app.service('users').before({
// Users can not be created by external access
create: hooks.disallow('external'),
// A user can not be deleted through the REST provider
remove: hooks.disallow('rest'),
// disallow calling `update` completely (e.g. to allow only `patch`)
update: hooks.disallow(),
// disallow the remove hook if the user is not an admin
remove: hooks.when(hook => !hook.params.user.isAdmin, hooks.disallow())
});
ProTip Service methods that are not implemented do not need to be disallowed.
Options:
providers(optional, default: disallows everything) - The transports that you want to disallow this service method for. Options are:socketio- will disallow the method for the Socket.IO providerprimus- will disallow the method for the Primus providerrest- will disallow the method for the REST providerexternal- will disallow access from all providers other than the server.server- will disallow access for the server
disableMultiItemChange
disableMultiItemChange() source
Disables update, patch and remove methods from using null as an id, e.g. remove(null). A null id affects all the items in the DB, so accidentally using it may have undesirable results.
- Used as a
beforehook.
app.service('users').before({
update: hooks.disableMultiItemChange(),
});
discard
discard(... fieldNames) source
Delete 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 Delete the field(s) for every item.
- Used as a
beforehook forcreate,updateorpatch. - Used as an
afterhook. - Field names support dot notation e.g.
name.address.city. - Supports multiple data items, including paginated
find.
const { discard } = require('feathers-hooks-common');
// Delete the hashed `password` and `salt` field after all method calls
app.service('users').after(discard('password', 'salt'));
// Delete _id for `create`, `update` and `patch`
app.service('users').before({
create: discard('_id', 'password'),
update: discard('_id'),
patch: discard('_id')
})
ProTip: This hook will always delete the fields, unlike the
removehook which only deletes the fields if the service call was made by a client.
ProTip: You can replace
remove('name')withiff(isProvider('external'), discard('name)). The latter does not contains any hidden "magic".
Options:
fieldNames(required) - One or more fields you want to remove from the object(s).
See also remove.
else
iff(...).else(...hookFuncs) source
iff().else() is similar to iff and iffElse.
Its syntax is more suitable for writing nested conditional hooks.
If the predicate in the iff() is falsey, run the hooks in else() sequentially.
- Used as a
beforeorafterhook. - Hooks to run may be sync, Promises or callbacks.
feathers-hookscatches any errors thrown in the predicate or hook.
service.before({
create:
hooks.iff(isProvider('server'),
hookA,
hooks.iff(isProvider('rest'), hook1, hook2, hook3)
.else(hook4, hook5),
hookB
)
.else(
hooks.iff(hook => hook.path === 'users', hook6, hook7)
)
});
or:
service.before({
create:
hooks.iff(isServer, [
hookA,
hooks.iff(isProvider('rest'), [hook1, hook2, hook3])
.else([hook4, hook5]),
hookB
])
.else([
hooks.iff(hook => hook.path === 'users', [hook6, hook7])
])
});
Options:
hookFuncs(optional) - Zero or more hook functions. They may include other conditional hooks. Or you can use an array of hook functions as the second parameter.
See also iff, iffElse, when, unless, isNot, isProvider.
This The predicate and hook functions in the if, else and iffElse hooks will not be called with
thisset to the service. Usehook.serviceinstead.
every
every(... hookFuncs) source
Run hook functions in parallel.
Return true if every hook function returned a truthy value.
- Used as a predicate function with conditional hooks.
- The current
hookis passed to all the hook functions, and they are run in parallel. - Hooks to run may be sync or Promises only.
feathers-hookscatches any errors thrown in the predicate.
service.before({
create: hooks.iff(hooks.every(hook1, hook2, ...), hookA, hookB, ...)
});
hooks.every(hook1, hook2, ...).call(this, currentHook)
.then(bool => { ... });
Options:
hookFuncs(required) Functions which take the current hook as a param and return a boolean result.
See also some.
iff
iff(predicate: boolean|Promise|function, ...hookFuncs: HookFunc[]): HookFunc source
Resolve the predicate to a boolean. Run the hooks sequentially if the result is truthy.
- Used as a
beforeorafterhook. - Predicate may be a sync or async function.
- Hooks to run may be sync, Promises or callbacks.
feathers-hookscatches any errors thrown in the predicate or hook.
const { iff, populate } = 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: iff(
() => new Promise((resolve, reject) => { ... }),
populate('user', { field: 'authorisedByUserId', service: 'users' })
)
});
app.service('workOrders').after({
// sync predicate and hook
find: [ iff(isNotAdmin(), hooks.remove('budget')) ]
});
or with the array syntax:
app.service('workOrders').after({
find: [ iff(isNotAdmin(), [hooks.remove('budget'), hooks.remove('password')]
});
Options:
predicate(required) - Determines if hookFuncs should be run or not. If a function,predicateis called with the hook as its param. It returns either a boolean or a Promise that evaluates to a booleanhookFuncs(optional) - Zero or more hook functions. They may include other conditional hooks. Or you can use an array of hook functions as the second parameter.
See also iffElse, else, when, unless, isNot, isProvider.
This The predicate and hook functions in the if, else and iffElse hooks will not be called with
thisset to the service. Usehook.serviceinstead.
iffElse
iffElse(predicate, trueHooks, falseHooks) source
Resolve the predicate to a boolean. Run the first set of hooks sequentially if the result is truthy, the second set otherwise.
- Used as a
beforeorafterhook. - Predicate may be a sync or async function.
- Hooks to run may be sync, Promises or callbacks.
feathers-hookscatches any errors thrown in the predicate or hook.
const { iffElse, populate, serialize } = require('feathers-hooks-common');
app.service('purchaseOrders').after({
create: iffElse(() => { ... },
[populate(poAccting), serialize( ... )],
[populate(poReceiving), serialize( ... )]
)
});
Options:
predicate(required) - Determines if hookFuncs should be run or not. If a function,predicateis called with the hook as its param. It returns either a boolean or a Promise that evaluates to a booleantrueHooks(optional) - Zero or more hook functions run whenpredicateis truthy.falseHooks(optional) - Zero or more hook functions run whenpredicateis false.
See also iff, else, when, unless, isNot, isProvider.
This The predicate and hook functions in the if, else and iffElse hooks will not be called with
thisset to the service. Usehook.serviceinstead.
isNot
isNot(predicate) source
Negate the predicate.
- Used as a predicate with conditional hooks.
- Predicate may be a sync or async function.
feathers-hookscatches 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:
predicate(required) - A function which returns either a boolean or a Promise that resolves to a boolean.
See also iff, iffElse, else, when, unless, isProvider.
isProvider
isProvider(provider) source
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 conditional hooks.
import { iff, isProvider, remove } from 'feathers-hooks-common';
app.service('users').after({
iff(isProvider('external'), 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.
See also iff, iffElse, else, when, unless, isNot, isProvider.
lowerCase
lowerCase(... fieldNames) source
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
beforehook forcreate,updateorpatch. - Used as an
afterhook. - Field names support dot notation.
- Supports multiple data items, including paginated
find.
const { lowerCase } = require('feathers-hooks-common');
// lowercase the `email` and `password` field before a user is created
app.service('users').before({
create: lowerCase('email', 'username')
});
Options:
fieldNames(required) - One or more fields that you want to lowercase from the retrieved object(s).
See also upperCase.
paramsFromClient
paramsFromClient(... whitelist) source
A hook, on the server, for passing params from the client to the server.
- Used as a
beforehook. - Companion to the client utility function
paramsForServer.
By default, only the hook.params.query object is transferred
to the server from a Feathers client,
for security among other reasons.
However you can explicitly transfer other params props with
the client utility function paramsForServer in conjunction with
the hook function paramsFromClient on the server.
// client
import { paramsForServer } from 'feathers-hooks-common';
service.patch(null, data, paramsForServer({
query: { dept: 'a' }, populate: 'po-1', serialize: 'po-mgr'
}));
// server
const { paramsFromClient } = require('feathers-hooks-common');
service.before({ all: [
paramsFromClient('populate', 'serialize', 'otherProp'), myHook
]});
// hook.params will now be
// { query: { dept: 'a' }, populate: 'po-1', serialize: 'po-mgr' } }
Options:
whitelist(optional) Names of the permitted props; other props are ignored. This is a security feature.
ProTip You can use the same technique for service calls made on the server.
See util: paramsForServer.
pluck
pluck(... fieldNames) source
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
beforehook forcreate,updateorpatch. - Used as an
afterhook. - Field names support dot notation.
- Supports multiple data items, including paginated
find.
const { pluck } = require('feathers-hooks-common');
// Only retain the hashed `password` and `salt` field after all method calls
app.service('users').after(pluck('password', 'salt'));
// Only keep the _id for `create`, `update` and `patch`
app.service('users').before({
create: pluck('_id'),
update: pluck('_id'),
patch: pluck('_id')
})
ProTip: This hook will only fire when
params.providerhas a value, i.e. when it is an external request over REST or Sockets.
Options:
fieldNames(required) - One or more fields that you want to retain from the object(s).
All other fields will be discarded.
pluckQuery
pluckQuery(... fieldNames) source
Discard all other fields except for the given fields from the query params.
- Used as a
beforehook. - Field names support dot notation.
- Supports multiple data items, including paginated
find.
const { pluckQuery } = require('feathers-hooks-common');
// Discard all other fields except for _id from the query
// for all service methods
app.service('users').before({
all: pluckQuery('_id')
});
ProTip: This hook will only fire when
params.providerhas 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.
populate
populate(options: Object): HookFunc source
Populates items recursively to any depth. Supports 1:1, 1:n and n:1 relationships.
- Used as a before or after hook on any service method.
- Supports multiple result items, including paginated
find. - Permissions control what a user may see.
- Provides performance profile information.
- Backward compatible with the old FeathersJS
populatehook.
Examples
- 1:1 relationship
// users like { _id: '111', name: 'John', roleId: '555' }
// roles like { _id: '555', permissions: ['foo', bar'] }
import { populate } from 'feathers-hooks-common';
const userRoleSchema = {
include: {
service: 'roles',
nameAs: 'role',
parentField: 'roleId',
childField: '_id'
}
};
app.service('users').hooks({
after: {
all: populate({ schema: userRoleSchema })
}
});
// result like
// { _id: '111', name: 'John', roleId: '555',
// role: { _id: '555', permissions: ['foo', bar'] } }
- 1:n relationship
// users like { _id: '111', name: 'John', roleIds: ['555', '666'] }
// roles like { _id: '555', permissions: ['foo', 'bar'] }
const userRolesSchema = {
include: {
service: 'roles',
nameAs: 'roles',
parentField: 'roleIds',
childField: '_id'
}
};
usersService.hooks({
after: {
all: populate({ schema: userRolesSchema })
}
});
// result like
// { _id: '111', name: 'John', roleIds: ['555', '666'], roles: [
// { _id: '555', permissions: ['foo', 'bar'] }
// { _id: '666', permissions: ['fiz', 'buz'] }
// ]}
- n:1 relationship
// posts like { _id: '111', body: '...' }
// comments like { _id: '555', text: '...', postId: '111' }
const postCommentsSchema = {
include: {
service: 'comments',
nameAs: 'comments',
parentField: '_id',
childField: 'postId'
}
};
postService.hooks({
after: {
all: populate({ schema: postCommentsSchema })
}
});
// result like
// { _id: '111', body: '...' }, comments: [
// { _id: '555', text: '...', postId: '111' }
// { _id: '666', text: '...', postId: '111' }
// ]}
- Multiple and recursive includes
const schema = {
service: '...',
permissions: '...',
include: [
{
service: 'users',
nameAs: 'authorItem',
parentField: 'author',
childField: 'id',
include: [ ... ],
},
{
service: 'comments',
parentField: 'id',
childField: 'postId',
query: {
$limit: 5,
$select: ['title', 'content', 'postId'],
$sort: {createdAt: -1}
},
select: (hook, parent, depth) => ({ $limit: 6 }),
asArray: true,
provider: undefined,
},
{
service: 'users',
permissions: '...',
nameAs: 'readers',
parentField: 'readers',
childField: 'id'
}
],
};
module.exports.after = {
all: populate({ schema, checkPermissions, profile: true })
};
- Flexible relationship, similar to the n:1 relationship example above
// posts like { _id: '111', body: '...' }
// comments like { _id: '555', text: '...', postId: '111' }
const postCommentsSchema = {
include: {
service: 'comments',
nameAs: 'comments',
select: (hook, parentItem) => ({ postId: parentItem._id }),
}
};
postService.hooks({
after: {
all: populate({ schema: postCommentsSchema })
}
});
// result like
// { _id: '111', body: '...' }, comments: [
// { _id: '555', text: '...', postId: '111' }
// { _id: '666', text: '...', postId: '111' }
// ]}
Options
schema(required, object or function) How to populate the items. Details are below.- Function signature
(hook: Hook, options: Object): Object hookThe hook.optionsTheoptionspassed to the populate hook.
- Function signature
checkPermissions[optional, default () => true] Function to check if the user is allowed to perform this populate, or include this type of item. Called whenever apermissionsproperty is found.- Function signature
(hook: Hook, service: string, permissions: any, depth: number): boolean hookThe hook.serviceThe name of the service being included, e.g. users, messages.permissionsThe value of the permissions property.depthHow deep the include is in the schema. Top of schema is 0.- Return truesy to allow the include.
- Function signature
profile[optional, default false] Iftrue, the populated result is to contain a performance profile. Must betrue, truesy is insufficient.
Schema
The data currently in the hook will be populated according to the schema. The schema starts with:
const schema = {
service: '...',
permissions: '...',
include: [ ... ]
};
service(optional) The name of the service this schema is to be used with. This can be used to prevent a schema designed to populate 'blog' items from being incorrectly used withcommentitems.permissions(optional, any type of value) Who is allowed to perform this populate. SeecheckPermissionsabove.include(optional) Which services to join to the data.
Include
The include array has an element for each service to join. They each may have:
{ service: 'comments',
nameAs: 'commentItems',
permissions: '...',
parentField: 'id',
childField: 'postId',
query: {
$limit: 5,
$select: ['title', 'content', 'postId'],
$sort: {createdAt: -1}
},
select: (hook, parent, depth) => ({ $limit: 6 }),
asArray: true,
paginate: false,
provider: undefined,
useInnerPopulate: false,
include: [ ... ]
}
ProTip Instead of setting
includeto a 1-element array, you can set it to the include object itself, e.g.include: { service: ..., nameAs: ..., ... }.
service[required, string] The name of the service providing the items.nameAs[optional, string, default is service] Where to place the items from the join. Dot notation is allowed.permissions[optional, any type of value] Who is allowed to perform this join. SeecheckPermissionsabove.parentField[required if neither query nor select, string] The name of the field in the parent item for the relation. Dot notation is allowed.childField[required if neither query nor select, string] The name of the field in the child item for the relation. Dot notation is allowed and will result in a query like{ 'name.first': 'John' }which is not suitable for all DBs. You may usequeryorselectto create a query suitable for your DB.query[optional, object] An object to inject into the query inservice.find({ query: { ... } }).select[optional, function] A function whose result is injected into the query.- Function signature
(hook: Hook, parentItem: Object, depth: number): Object hookThe hook.parentItemThe parent item to which we are joining.depthHow deep the include is in the schema. Top of schema is 0.
- Function signature
asArray[optional, boolean, default false] Force a single joined item to be stored as an array.paginate{optional, boolean or number, default false] Controls pagination for this service.falseNo pagination. The default.trueUse the configuration provided when the service was configured/- A number. The maximum number of items to include.
provider[optional]findcalls are made to obtain the items to be joined. These, by default, are initialized to look like they were made by the same provider as that getting the base record. So when populating the result of a call made viasocketio, all the join calls will look like they were made viasocketio. Alternative you can setprovider: undefinedand the calls for that join will look like they were made by the server. The hooks on the service may behave differently in different situations.useInnerPopulate[optional] Populate, when including records from a child service, ignores any populate hooks defined for that child service. The useInnerPopulate option will run those populate hooks. This allows the populate for a base record to include child records containing their own immediate child records, without the populate for the base record knowing what those grandchildren populates are.include[optional] The new items may themselves include other items. The includes are recursive.
Populate forms the query [childField]: parentItem[parentField] when the parent value is not an array.
This will include all child items having that value.
Populate forms the query [childField]: { $in: parentItem[parentField] } when the parent value is an array.
This will include all child items having any of those values.
A populate hook for, say, posts may include items from users.
Should the users hooks also include a populate,
that users populate hook will not be run for includes arising from posts.
ProTip The populate interface only allows you to directly manipulate
hook.params.query. You can manipulate the rest ofhook.paramsby using theclienthook, along with something likequery: { ..., $client: { paramsProp1: ..., paramsProp2: ... } }.
Added properties
Some additional properties are added to populated items. The result may look like:
{ ...
_include: [ 'post' ],
_elapsed: { post: 487947, total: 527118 },
post:
{ ...
_include: [ 'authorItem', 'commentsInfo', 'readersInfo' ],
_elapsed: { authorItem: 321973, commentsInfo: 469375, readersInfo: 479874, total: 487947 },
_computed: [ 'averageStars', 'views' ],
authorItem: { ... },
commentsInfo: [ { ... }, { ... } ],
readersInfo: [ { ... }, { ... } ]
} }
_includeThe property names containing joined items._elapsedThe elapsed time in nano-seconds (where 1,000,000 ns === 1 ms) taken to perform each include, as well as the total taken for them all. This delay is mostly attributed to your DB._computedThe property names containing values computed by theserializehook.
The depopulate hook uses these fields to remove all joined and computed values.
This allows you to then service.patch() the item in the hook.
Joining without using related fields
Populate can join child records to a parent record using the related columns
parentField and childField.
However populate's query and select options may be used to related the
records without needing to use the related columns.
This is a more flexible, non-SQL-like way of relating records.
It easily supports dynamic, run-time schemas since the select option may be
a function.
Populate examples
Selecting schema based on UI needs
Consider a Purchase Order item. An Accounting oriented UI will likely want to populate the PO with Invoice items. A Receiving oriented UI will likely want to populate with Receiving Slips.
Using a function for schema allows you to select an appropriate schema based on the need.
The following example shows how the client can ask for the type of schema it needs.
// on client
import { paramsForServer } from 'feathers-hooks-common';
purchaseOrders.get(id, paramsForServer({ schema: 'po-acct' })); // pass schema name to server
// or
purchaseOrders.get(id, paramsForServer({ schema: 'po-rec' }));
// on server
import { paramsFromClient } from 'feathers-hooks-common';
const poSchemas = {
'po-acct': /* populate schema for Accounting oriented PO e.g. { include: ... } */,
'po-rec': /* populate schema for Receiving oriented PO */
};
purchaseOrders.before({
all: paramsfromClient('schema')
});
purchaseOrders.after({
all: populate({ schema: hook => poSchemas[hook.params.schema] }),
});
Using permissions
For a simplistic example,
assume hook.params.users.permissions is an array of the service names the user may use,
e.g. ['invoices', 'billings'].
These can be used to control which types of items the user can see.
The following populate will only be performed for users whose user.permissions contains 'invoices'.
const schema = {
include: [
{
service: 'invoices',
permissions: 'invoices',
...
}
]
};
purchaseOrders.after({
all: populate(schema, (hook, service, permissions) => hook.params.user.permissions.includes(service))
});
See also dePopulate, serialize.
preventChanges
preventChanges(... fieldNames) source
Prevents the specified fields from being patched.
- Used as a
beforehook forpatch. - Field names support dot notation e.g.
name.address.city.
const { preventChanges } = require('feathers-hooks-common');
app.service('users').before({
patch: preventChanges('security.badge')
})
Options:
fieldNames(required) - One or more fields which may not be patched.
Consider using
verifySchemaif you would rather specify which fields are allowed to change.
remove
remove(... fieldNames) source
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
beforehook forcreate,updateorpatch. - Used as an
afterhook. - Field names support dot notation e.g.
name.address.city. - Supports multiple data items, including paginated
find.
const { remove } = require('feathers-hooks-common');
// Remove the hashed `password` and `salt` field after all method calls
app.service('users').after(remove('password', 'salt'));
// Remove _id for `create`, `update` and `patch`
app.service('users').before({
create: remove('_id', 'password'),
update: remove('_id'),
patch: remove('_id')
})
ProTip: This hook will only fire when
params.providerhas a value, i.e. when it is an external request over REST or Sockets.
Options:
fieldNames(required) - One or more fields you want to remove from the object(s).
See also discard.
removeQuery
removeQuery(... fieldNames) source
Remove the given fields from the query params.
- Used as a
beforehook. - Field names support dot notation
- Supports multiple data items, including paginated
find.
const { removeQuery } = require('feathers-hooks-common');
// Remove _id from the query for all service methods
app.service('users').before({
all: removeQuery('_id')
});
ProTip: This hook will only fire when
params.providerhas 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.
serialize
serialize(schema: Object|Function): HookFunc source
Remove selected information from populated items. Add new computed information.
Intended for use with the populate hook.
const schema = {
only: 'updatedAt',
computed: {
commentsCount: (recommendation, hook) => recommendation.post.commentsInfo.length,
},
post: {
exclude: ['id', 'createdAt', 'author', 'readers'],
authorItem: {
exclude: ['id', 'password', 'age'],
computed: {
isUnder18: (authorItem, hook) => authorItem.age < 18,
},
},
readersInfo: {
exclude: 'id',
},
commentsInfo: {
only: ['title', 'content'],
exclude: 'content',
},
},
};
purchaseOrders.after({
all: [ populate( ... ), serialize(schema) ]
});
Options
schema[required, object or function] How to serialize the items.- Function signature
(hook: Hook): Object hookThe hook.
- Function signature
The schema reflects the structure of the populated items.
The base items for the example above have included post items,
which themselves have included authorItem, readersInfo and commentsInfo items.
The schema for each set of items may have
only[optional, string or array of strings] The names of the fields to keep in each item. The names for included sets of items plus_includeand_elapsedare not removed byonly.exclude[optional, string or array of strings] The names of fields to drop in each item. You may drop, at your own risk, names of included sets of items,_includeand_elapsed.computed[optional, object with functions] The new names you want added and how to compute their values.- Object is like
{ name: func, ...} nameThe name of the field to add to the items.funcFunction with signature(item, hook).itemThe item with all its initial values, plus all of its included items. The function can still reference values which will be later removed byonlyandexclude.hookThe hook passed to serialize.
- Object is like
Serialize examples
A simple serialize
The populate example above produced the result
{ id: 9, title: 'The unbearable ligthness of FeathersJS', author: 5, yearBorn: 1990,
authorItem: { id: 5, email: 'john.doe@gmail.com', name: 'John Doe' },
_include: ['authorItem']
}
We could tailor the result more to what we need with:
const serializeSchema = {
only: ['title'],
authorItem: {
only: ['name']
computed: {
isOver18: (authorItem, hook) => new Date().getFullYear() - authorItem.yearBorn >= 18,
},
}
};
app.service('posts').before({
get: [ hooks.populate({ schema }), serialize(serializeSchema) ],
find: [ hooks.populate({ schema }), serialize(serializeSchema) ]
});
The result would now be
{ title: 'The unbearable ligthness of FeathersJS',
authorItem: { name: 'John Doe', isOver18: true, _computed: ['isOver18'] },
_include: ['authorItem'],
}
Using permissions
Consider an Employee item. The Payroll Manager would be permitted to see the salaries of other department heads. No other person would be allowed to see them.
Using a function for schema allows you to select an appropriate schema based on the need.
Assume hook.params.user.roles contains an array of roles which the user performs.
The Employee item can be serialized differently for the Payroll Manager than for anyone else.
const payrollSerialize = {
'payrollMgr': { /* serialization schema for Payroll Manager */},
'payroll': { /* serialization schema for others */}
};
employees.after({
all: [
populate( ... ),
serialize(hook => payrollSerialize[
hook.params.user.roles.contains('payrollMgr') ? 'payrollMgr' : 'payroll'
])
]
});
setCreatedAt
setCreatedAt(fieldName = 'createdAt', ... fieldNames) source
Add the fields with the current date-time.
- Used as a
beforehook forcreate,updateorpatch. - Used as an
afterhook. - Field names support dot notation.
- Supports multiple data items, including paginated
find.
ProTip
setCreatedAtwill be deprecated, so usesetNowinstead.
const { setCreatedAt } = require('feathers-hooks-common');
// set the `createdAt` field before a user is created
app.service('users').before({
create: [ setCreatedAt() ]
});
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.
See also setUpdatedAt.
setNow
setNow(... fieldNames) source
Add the fields with the current date-time.
- Used as a
beforehook forcreate,updateorpatch. - Used as an
afterhook. - Field names support dot notation.
- Supports multiple data items, including paginated
find.
const { setNow } = require('feathers-hooks-common');
app.service('users').before({
create: setNow('createdAt', 'updatedAt')
});
Options:
fieldNames(required, at least one) - The fields that you want to add with the current date-time to the retrieved object(s).
ProTip Use
setNowrather thansetCreatedAtorsetUpdatedAt.
setSlug
setSlug(slug, fieldName = 'query.' + slug) source
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
beforehook. - Field names support dot notation.
const { setSlug } = require('feathers-hooks-common');
app.service('stores').before({
create: [ setSlug('storeId') ]
});
Options:
slug(required) - The slug as it appears in the route, e.g.storeIdfor/stores/:storeId/candies.fieldName(optional, default:query[slugId]) - The field to contain the slug value.
setUpdatedAt
setUpdatedAt(fieldName = 'updatedAt', ...fieldNames) source
Add or update the fields with the current date-time.
- Used as a
beforehook forcreate,updateorpatch. - Used as an
afterhook. - Field names support dot notation.
- Supports multiple data items, including paginated
find.
ProTip
setUpdatedAtwill be deprecated, so usesetNowinstead.
const { setUpdatedAt } = require('feathers-hooks-common');
// set the `updatedAt` field before a user is created
app.service('users').before({
create: [ setUpdatedAt() ]
});
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.
See also setCreatedAt.
sifter
sifter(mongoQueryFunc)) source
All official Feathers database adapters support a common way for querying, sorting, limiting and selecting find method calls. These are limited to what is commonly supported by all the databases.
The sifter hook provides an extensive MongoDB-like selection capabilities,
and it may be used to more extensively select records.
- Used as an
afterhook forfind. - SProvides extensive MongoDB-like selection capabilities.
ProTip
sifterfilters the result of afindcall. Therefore more records will be physically read than needed. You can use the Feathers database adaptersqueryto reduce this number.
const sift = require('sift');
const { sifter } = require('feathers-hooks-common');
const selectCountry = hook => sift({ 'address.country': hook.params.country });
app.service('stores').after({
find: sifter(selectCountry),
});
const sift = require('sift');
const { sifter } = require('feathers-hooks-common');
const selectCountry = country => () => sift({ address : { country: country } });
app.service('stores').after({
find: sifter(selectCountry('Canada')),
});
Options:
mongoQueryFunc(required) - Function similar tohook => sift(mongoQueryObj). Information about themongoQueryObjsyntax is available at sift.
softDelete
softDelete(fieldName = 'deleted') source
Marks items as { deleted: true } instead of physically removing them.
This is useful when you want to discontinue use of, say, a department,
but you have historical information which continues to refer to the discontinued department.
- Used as a
before.allhook to handle all service methods. - Supports multiple data items, including paginated
find.
const { softDelete } = require('feathers-hooks-common');
const dept = app.service('departments');
dept.before({
all: softDelete(),
});
// will throw if item is marked deleted.
dept.get(0).then()
// methods can be run avoiding softDelete handling
dept.get(0, { query: { $disableSoftDelete: true }}).then()
Options:
fieldName(optional, default:deleted) - The name of the field holding the deleted flag.
some
some(... hookFuncs) source
Run hook functions in parallel.
Return true if any hook function returned a truthy value.
- Used as a predicate function with conditional hooks.
- The current
hookis passed to all the hook functions, and they are run in parallel. - Hooks to run may be sync or Promises only.
feathers-hookscatches any errors thrown in the predicate.
service.before({
create: hooks.iff(hooks.some(hook1, hook2, ...), hookA, hookB, ...)
});
hooks.some(hook1, hook2, ...).call(this, currentHook)
.then(bool => { ... });
Options:
hookFuncs(required) Functions which take the current hook as a param and return a boolean result.
See also every.
stashBefore
stashBefore(name) source
Stash current value of record before mutating it.
- Used as a
beforehook forget,update,patchorremove. - An
idis required in the method call.
service.before({
patch: stashBefore()
});
Options:
name(optional defaults to 'before') The name of the params property to contain the current record value.
traverse
traverse(transformer, getObject) source
Traverse and transform objects in place by visiting every node on a recursive walk.
- Used as a
beforeorafterhook. - Supports multiple data items, including paginated
find. - Any object in the hook may be traversed, including the query object.
transformerhas access to powerful methods and context.
// Trim strings
const trimmer = function (node) {
if (typeof node === 'string') { this.update(node.trim()); }
};
service.before({ create: traverse(trimmer) });
// REST HTTP request may use the string 'null' in its query string.
// Replace these strings with the value null.
const nuller = function (node) {
if (node === 'null') { this.update(null); }
};
service.before({ find: traverse(nuller, hook => hook.params.query) });
ProTip: GitHub's substack/js-traverse documents the extensive methods and context available to the transformer function.
Options:
transformer(required) - Called for every node and may change it in place.getObject(optional, defaults tohook.dataorhook.result) - Function with signature (hook) which returns the object to traverse.
unless
unless(predicate, ...hookFuncs) source
Resolve the predicate to a boolean. Run the hooks sequentially if the result is falsey.
- Used as a
beforeorafterhook. - Predicate may be a sync or async function.
- Hooks to run may be sync, Promises or callbacks.
feathers-hookscatches any errors thrown in the predicate or hook.
service.before({
create:
unless(isProvider('server'),
hookA,
unless(isProvider('rest'), hook1, hook2, hook3),
hookB
)
});
Options:
predicate(required) - Determines if hookFuncs should be run or not. If a function,predicateis called with the hook as its param. It returns either a boolean or a Promise that evaluates to a boolean.hookFuncs(optional) - Zero or more hook functions. They may include other conditional hook functions.
See also iff, iffElse, else, when, isNot, isProvider.
validate
validate(validator) source
Call a validation function from a before hook. The function may be sync or return a Promise.
- Used as a
beforehook forcreate,updateorpatch.
ProTip: If you have a different signature for the validator then pass a wrapper as the validator e.g.
(values) => myValidator(..., values, ...).
ProTip: Wrap your validator in
callbackToPromiseif it uses a callback.
const { callbackToPromise, validate } = require('feathers-hooks-common');
// function myCallbackValidator(values, cb) { ... }
const myValidator = callbackToPromise(myCallbackValidator, 1); // function requires 1 param
app.service('users').before({ create: validate(myValidator) });
Options:
validator(required) - Validation function with signaturefunction validator(formValues, hook).
Sync functions return either an error object like { fieldName1: 'message', ... } or null.
Validate will throw on an error object with throw new errors.BadRequest({ errors: errorObject });.
Promise functions should throw on an error or reject with
new errors.BadRequest('Error message', { errors: { fieldName1: 'message', ... } });
Their .then returns either sanitized values to replace hook.data, or null.
Example
Comprehensive validation may include the following:
- Object schema validation. Checking the item object contains the expected properties with values in the expected format. The values might get sanitized. Knowing the item is well formed makes further validation simpler.
- Re-running any validation supposedly already done on the front-end. It would an asset if the server can re-run the same code the front-end used.
- Performing any validation and sanitization unique to the server.
A full featured example of such a process appears below. It validates and sanitizes a new user before adding the user to the database.
- The form expects to be notified of errors in the format
{ email: 'Invalid email.', password: 'Password must be at least 8 characters.' }. - The form calls the server for async checking of selected fields when control leaves those fields. This for example could check that an email address is not already used by another user.
- The form does local sync validation when the form is submitted.
- The code performing the validations on the front-end is also used by the server.
- The server performs schema validation using Walmart's Joi.
- The server does further validation and sanitization.
Validation using Validate
// file /server/services/users/hooks/index.js
const auth = require('feathers-authentication').hooks;
const { callbackToPromise, remove, validate } = require('feathers-hooks-common');
const validateSchema = require('feathers-hooks-validate-joi');
const clientValidations = require('/common/usersClientValidations');
const serverValidations = require('/server/validations/usersServerValidations');
const schemas = require('/server/validations/schemas');
const serverValidationsSignup = callbackToPromise(serverValidations.signup, 1);
exports.before = {
create: [
validateSchema.form(schemas.signup, schemas.options), // schema validation
validate(clientValidations.signup), // re-run form sync validation
validate(values => clientValidations.signupAsync(values, 'someMoreParams')), // re-run form async
validate(serverValidationsSignup), // run server validation
remove('confirmPassword'),
auth.hashPassword()
]
};
Validation routines for front and back-end.
Validations used on front-end. They are re-run by the server.
// file /common/usersClientValidations
// Validations for front-end. Also re-run on server.
const clientValidations = {};
// sync validation of signup form on form submit
clientValidations.signup = values => {
const errors = {};
checkName(values.name, errors);
checkUsername(values.username, errors);
checkEmail(values.email, errors);
checkPassword(values.password, errors);
checkConfirmPassword(values.password, values.confirmPassword, errors);
return errors;
};
// async validation on exit from some fields on form
clientValidations.signupAsync = values =>
new Promise((resolve, reject) => {
const errs = {};
// set a dummy error
errs.email = 'Already taken.';
if (!Object.keys(errs).length) {
resolve(null); // 'null' as we did not sanitize 'values'
}
reject(new errors.BadRequest('Values already taken.', { errors: errs }));
});
module.exports = clientValidations;
function checkName(name, errors, fieldName = 'name') {
if (!/^[\\sa-zA-Z]{8,30}$/.test((name || '').trim())) {
errors[fieldName] = 'Name must be 8 or more letters or spaces.';
}
}
Schema definitions used by the server.
// file /server/validations/schemas
const Joi = require('joi');
const username = Joi.string().trim().alphanum().min(5).max(30).required();
const password = Joi.string().trim().regex(/^[\sa-zA-Z0-9]+$/, 'letters, numbers, spaces')
.min(8).max(30).required();
const email = Joi.string().trim().email().required();
module.exports = {
options: { abortEarly: false, convert: true, allowUnknown: false, stripUnknown: true },
signup: Joi.object().keys({
name: Joi.string().trim().min(8).max(30).required(),
username,
password,
confirmPassword: password.label('Confirm password'),
email
})
};
Validations run by the server.
// file /server/validations/usersServerValidations
// Validations on server. A callback function is used to show how the hook handles it.
module.exports = {
signup: (data, cb) => {
const formErrors = {};
const sanitized = {};
Object.keys(data).forEach(key => {
sanitized[key] = (data[key] || '').trim();
});
cb(Object.keys(formErrors).length > 0 ? formErrors : null, sanitized);
}
};
See also validateSchema.
validateSchema
validateSchema(schema, ajv, options) source
Validate an object using JSON-Schema through AJV
ProTip There are some good tutorials on using JSON-Schema with ajv.
- Used as a
beforeorafterhook. - The hook will throw if the data does not match the JSON-Schema.
error.errorswill, by default, contain an array of error messages.
ProTip You may customize the error message format with a custom formatting function. You could, for example, return
{ name1: message, name2: message }which could be more suitable for a UI.ProTip If you need to customize
ajvwith new keywords, formats or schemas, then instead of passing theAjvconstructor, you may pass in an instance ofAjvas the second parameter. In this case you need to passajvoptions to theajvinstance whennewing, rather than passing them in the third parameter ofvalidateSchema. See the second example below.
const Ajv = require('ajv');
const createSchema = { /* JSON-Schema */ };
module.before({
create: validateSchema(createSchema, Ajv)
});
const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true, $data: true });
ajv.addFormat('allNumbers', '^\d+$');
const createSchema = { /* JSON-Schema */ };
module.before({
create: validateSchema(createSchema, ajv)
});
Options:
schema(required) - The JSON-Schema.ajv(required) - Theajvvalidator. Could be either theAjvconstructor or an instance of it.options(optional) - Options.- Any
ajvoptions. Only effective when the second parameter is theAjvconstructor. addNewError(optional) - Custom message formatter. Its a reducing function which works similarly toArray.reduce(). Its signature is{ currentFormattedMessages: any, ajvError: AjvError, itemsLen: number, index: number }: newFormattedMessagescurrentFormattedMessages- Formatted messages so far. Initiallynull.ajvError- ajv error.itemsLen- How many data items there are. 1-based.index- Which item this is. 0-based.newFormattedMessages- The function returns the updated formatted messages.
- Any
when
Util: callbackToPromise
callbackToPromise(callbackFunc, paramsCount) source
Wrap a function calling a callback into one that returns a Promise.
- Promise is rejected if the function throws.
const { callbackToPromise } = require('feathers-hooks-common');
function tester(data, a, b, cb) {
if (data === 3) { throw new Error('error thrown'); }
cb(data === 1 ? null : 'bad', data);
}
const wrappedTester = callbackToPromise(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(required) - The number of parameterscallbackFuncexpects. This count does not include the callback param itself.
The wrapped function will always be called with that many params, preventing potential bugs.
See also promiseToCallback.
Util: checkContext
checkContext(hook, type, methods, label) source
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');
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 inbeforeorafter.nullallows 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.
Util: deleteByDot
deleteByDot(obj, path) source
deleteByDot deletes a property from an object using dot notation, e.g. employee.address.city.
import { deleteByDot } from 'feathers-hooks-common';
const discardPasscode = () => (hook) => {
deleteByDot(hook.data, 'security.passcode');
}
app.service('directories').before = {
find: discardPasscode()
};
Options:
obj(required) - The object containing the property we want to delete.path(required) - The path to the data, e.g.security.passcode. Array notion is not supported, e.g.order.lineItems[1].quantity.
See also existsByDot, getByDot, setByDot.
Util: existsByDot
existsByDot(obj, path) source
existsByDot checks if a property exists in an object using dot notation, e.g. employee.address.city.
Properties with a value of undefined are considered to exist.
import { discard, existsByDot, iff } from 'feathers-hooks-common';
const discardBadge = () => iff(!existsByDot('security.passcode'), discard('security.badge'));
app.service('directories').before = {
find: discardBadge()
};
Options:
obj(required) - The object containing the property.path(required) - The path to the property, e.g.security.passcode. Array notion is not supported, e.g.order.lineItems[1].quantity.
See also existsByDot, getByDot, setByDot.
Util: getByDot, setByDot
getByDot(obj, path) source
setByDot(obj, path, value, ifDelete) source
getByDot 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.
import { getByDot, setByDot } from 'feathers-hooks-common';
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.
See also existsByDot, deleteByDot.
Util: getItems, replaceItems
getItems(hook) source
replaceItems(hook, items) source
getItems gets the data items in a hook. The items may be hook.data, hook.result or hook.result.data depending 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';
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')
};
The common hooks usually mutate the items in place, so a replaceItems is not required.
const items = getItems(hook);
(Array.isArray(items) ? items : [items]).forEach(item => { item.setCreateAt = new Date(); });
Options:
hook(required) - The hook provided to the hook function.items(required) - The updated item or array of items.
Util: paramsForServer
paramsForServer(params, ... whitelist) source
A client utility to pass selected params properties to the server.
- Companion to the server-side hook
paramsFromClient.
By default, only the hook.params.query object is transferred
to the server from a Feathers client,
for security among other reasons.
However you can explicitly transfer other params props with
the client utility function paramsForServer in conjunction with
the hook function paramsFromClient on the server.
// client
import { paramsForServer } from 'feathers-hooks-common';
service.patch(null, data, paramsForServer({
query: { dept: 'a' }, populate: 'po-1', serialize: 'po-mgr'
}));
// server
const { paramsFromClient } = require('feathers-hooks-common');
service.before({ all: [
paramsFromClient('populate', 'serialize', 'otherProp'),
myHook
]});
// myHook's `hook.params` will now be
// { query: { dept: 'a' }, populate: 'po-1', serialize: 'po-mgr' } }
Options:
params(optional) Theparamsobject to pass to the server, including anyqueryprop.whitelist(optional) Names of the props inparamsto transfer to the server. This is a security feature. All props are transferred if no whitelist is specified.
See paramsFromClient.
Util: promiseToCallback
promiseToCallback(promise)(callbackFunc) source
Wrap a Promise into a function that calls a callback.
- The callback does not run in the Promise's scope. The Promise's
catchchain is not invoked if the callback throws.
import { promiseToCallback } from 'feathers-hooks-common'
function (cb) {
const promise = new Promise( ...).then( ... ).catch( ... );
...
promiseToCallback(promise)(cb);
promise.then( ... ); // this is still possible
}
Options:
promise(required) - A function returning a promise.
See also callbackToPromise.
FAQ: Coerce data types
A common need is converting fields coming in from query params. These fields are provided as string values by default and you may need them as numbers, boolenas, etc.
The validateSchema does a wide selection of
type coercions,
as well as checking for missing and unexpected fields.


