$ 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(... whitelist)
source
A hook for passing params from the client to the server.
- Used as a
before
hook.
ProTip Use the
paramsFromClient
hook instead. It does exactly the same thing asclient
but 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(... 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:
combine
is 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(label)
source
Display current info about the hook to console.
- Used as a
before
orafter
hook.
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()
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(...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
before
hook.
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()
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
before
hook.
app.service('users').before({
update: hooks.disableMultiItemChange(),
});
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
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 { 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
remove
hook 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.
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
before
orafter
hook. - Hooks to run may be sync, Promises or callbacks.
feathers-hooks
catches 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
this
set to the service. Usehook.service
instead.
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
hook
is passed to all the hook functions, and they are run in parallel. - Hooks to run may be sync or Promises only.
feathers-hooks
catches 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(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
before
orafter
hook. - Predicate may be a sync or async function.
- Hooks to run may be sync, Promises or callbacks.
feathers-hooks
catches 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,predicate
is 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
this
set to the service. Usehook.service
instead.
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
before
orafter
hook. - Predicate may be a sync or async function.
- Hooks to run may be sync, Promises or callbacks.
feathers-hooks
catches 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,predicate
is 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 whenpredicate
is truthy.falseHooks
(optional) - Zero or more hook functions run whenpredicate
is 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
this
set to the service. Usehook.service
instead.
isNot(predicate)
source
Negate the predicate
.
- Used as a predicate with conditional hooks.
- 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:
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(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(... 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
before
hook forcreate
,update
orpatch
. - Used as an
after
hook. - 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(... whitelist)
source
A hook, on the server, for passing params
from the client to the server.
- Used as a
before
hook. - 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(... 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
before
hook forcreate
,update
orpatch
. - Used as an
after
hook. - 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.provider
has 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(... fieldNames)
source
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 { 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.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.
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
populate
hook.
- 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' }
// ]}
schema
(required, object or function) How to populate the items. Details are below.- Function signature
(hook: Hook, options: Object): Object
hook
The hook.options
Theoptions
passed 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 apermissions
property is found.- Function signature
(hook: Hook, service: string, permissions: any, depth: number): boolean
hook
The hook.service
The name of the service being included, e.g. users, messages.permissions
The value of the permissions property.depth
How 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.
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 withcomment
items.permissions
(optional, any type of value) Who is allowed to perform this populate. SeecheckPermissions
above.include
(optional) Which services to join to the data.
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
include
to 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. SeecheckPermissions
above.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 usequery
orselect
to 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
hook
The hook.parentItem
The parent item to which we are joining.depth
How 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.false
No pagination. The default.true
Use the configuration provided when the service was configured/- A number. The maximum number of items to include.
provider
[optional]find
calls 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: undefined
and 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.params
by using theclient
hook, along with something likequery: { ..., $client: { paramsProp1: ..., paramsProp2: ... } }
.
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: [ { ... }, { ... } ]
} }
_include
The property names containing joined items._elapsed
The 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._computed
The property names containing values computed by theserialize
hook.
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.
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.
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] }),
});
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(... fieldNames)
source
Prevents the specified fields from being patched.
- Used as a
before
hook 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
validateSchema
if you would rather specify which fields are allowed to change.
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
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 { 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.provider
has 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(... fieldNames)
source
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 { 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.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.
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
hook
The 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_include
and_elapsed
are 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,_include
and_elapsed
.computed
[optional, object with functions] The new names you want added and how to compute their values.- Object is like
{ name: func, ...}
name
The name of the field to add to the items.func
Function with signature(item, hook)
.item
The item with all its initial values, plus all of its included items. The function can still reference values which will be later removed byonly
andexclude
.hook
The hook passed to serialize.
- Object is like
The populate example above produced the result
{ id: 9, title: 'The unbearable ligthness of FeathersJS', author: 5, yearBorn: 1990,
authorItem: { id: 5, email: '[email protected]', 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'],
}
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(fieldName = 'createdAt', ... fieldNames)
source
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
.
ProTip
setCreatedAt
will be deprecated, so usesetNow
instead.
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(... fieldNames)
source
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 { 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
setNow
rather thansetCreatedAt
orsetUpdatedAt
.
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
before
hook. - 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.storeId
for/stores/:storeId/candies
.fieldName
(optional, default:query[slugId]
) - The field to contain the slug value.
setUpdatedAt(fieldName = 'updatedAt', ...fieldNames)
source
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
.
ProTip
setUpdatedAt
will be deprecated, so usesetNow
instead.
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(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
after
hook forfind
. - SProvides extensive MongoDB-like selection capabilities.
ProTip
sifter
filters the result of afind
call. Therefore more records will be physically read than needed. You can use the Feathers database adaptersquery
to 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 themongoQueryObj
syntax is available at sift.
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.all
hook 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(... 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
hook
is passed to all the hook functions, and they are run in parallel. - Hooks to run may be sync or Promises only.
feathers-hooks
catches 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(name)
source
Stash current value of record before mutating it.
- Used as a
before
hook forget
,update
,patch
orremove
. - An
id
is 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(transformer, getObject)
source
Traverse and transform objects in place by visiting every node on a recursive walk.
- Used as a
before
orafter
hook. - Supports multiple data items, including paginated
find
. - Any object in the hook may be traversed, including the query object.
transformer
has 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.data
orhook.result
) - Function with signature (hook) which returns the object to traverse.
unless(predicate, ...hookFuncs)
source
Resolve the predicate to a boolean. Run the hooks sequentially if the result is falsey.
- Used as a
before
orafter
hook. - Predicate may be a sync or async function.
- Hooks to run may be sync, Promises or callbacks.
feathers-hooks
catches 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,predicate
is 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(validator)
source
Call a validation function from a before
hook. The function may be sync or return a Promise.
- Used as a
before
hook forcreate
,update
orpatch
.
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
callbackToPromise
if 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.
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.
// 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()
]
};
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(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
before
orafter
hook. - The hook will throw if the data does not match the JSON-Schema.
error.errors
will, 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
ajv
with new keywords, formats or schemas, then instead of passing theAjv
constructor, you may pass in an instance ofAjv
as the second parameter. In this case you need to passajv
options to theajv
instance whennew
ing, 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) - Theajv
validator. Could be either theAjv
constructor or an instance of it.options
(optional) - Options.- Any
ajv
options. Only effective when the second parameter is theAjv
constructor. 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 }: newFormattedMessages
currentFormattedMessages
- 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
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 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.
See also promiseToCallback.
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 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.
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.
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.
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.
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.
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) Theparams
object to pass to the server, including anyquery
prop.whitelist
(optional) Names of the props inparams
to transfer to the server. This is a security feature. All props are transferred if no whitelist is specified.
See paramsFromClient
.
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
catch
chain 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.
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.