Introduction to feathers-hooks-common
feathers-hooks-common is a library providing a collection of reusable hooks and utility methods for FeathersJS applications to simplify the development of real-time applications and REST APIs.website·Indexed 36 days ago
https://hooks-common.feathersjs.com/A collection of useful hooks and utility functions for managing service lifecycles in FeathersJS. Includes hooks for data alteration, caching, and access control, as well as utilities for constructing calling parameters and managing dynamic hook parameters via fastJoin.
feathers-hooks-common is a library providing a collection of reusable hooks and utility methods for FeathersJS applications to simplify the development of real-time applications and REST APIs.fastJoin hook combines rows from multiple tables based on relationships. It is significantly more performant than the populate hook because it batches service calls and caches records. It uses a resolver-based API where you define how to fetch related data and can optionally provide a query to shape the result (similar to GraphQL).const { fastJoin } = require('feathers-hooks-common');
const postResolvers = {
joins: {
author: (...args) => async (post, { app }) => {
post.author = (await app.service('users').find({
query: { id: post.userId }
}))[0];
},
starers: $select => async (post, { app }) => {
post.starers = await app.service('users').find({
query: { id: { $in: post.starIds }, $select: $select || ['name'] },
paginate: false
});
}
}
};
module.exports = {
after: {
all: [fastJoin(postResolvers)]
}
};feathers-hooks-common have been updated to avoid direct mutation of objects on the context. Instead, they replace the object (e.g., params or params.query) with a new object. This prevents bugs where nested hooks unexpectedly modify shared state.context object. This allows you to use logic or context-specific data to determine the hook's behavior during runtime.// Example: Dynamic 'disallow' hook
function disallowWhat() {
return someVariableCircumstance() ? 'rest' : 'external';
}
module.exports = {
before: {
all: context => disallow(disallowWhat())(context)
}
};
// Example: Dynamic 'keep' hook based on user record
module.exports = {
after: {
get: context => keep(...context.params.user.public)(context)
}
};iffElse hook executes one set of hooks if a predicate is true and another set if it is false. The predicate can be a boolean, a Promise, or a function receiving the context. Like iff, these functions are not bound to the service via this; use hook.service instead.const { iffElse, populate, serialize } = require('feathers-hooks-common');
module.exports = {
after: {
create: iffElse(
() => { /* predicate logic */ },
[populate(poAccting), serialize(...)],
[populate(poReceiving), serialize(...)]
)
}
};When using fastJoin, you can define recursive operations by nesting joins within a resolver. The resolver function must return the data that is to be recursively operated on; if the return value is omitted, recursive joins will not execute.
In the example below, the comments resolver fetches related comments for a post. Because it returns post.comments, fastJoin can then apply the nested author resolver to each comment record in that array.
const postResolvers = {
joins: {
comments: {
resolver: ($select, $limit, $sort) => async post => {
post.comments = await comments.find({
query: { postId: post.id, $select: $select, $limit: $limit || 5, [$sort]: { createdAt: -1 } },
paginate: false
});
return post.comments; // Critical: must return data for recursive operation
},
joins: {
author: $select => async comment => {
comment.author = (await users.find({
query: { id: comment.userId, $select: $select },
paginate: false
}))[0];
},
},
},
}
};userId is joined.// Define a reusable resolver for comments
const commentResolvers = {
joins: {
author: $select => async comment => {
comment.author = (
await users.find({
query: { id: comment.userId, $select: $select || ['name'] },
paginate: false
})
)[0];
}
}
};
// Reference the reusable resolver in the post configuration
const postResolvers = {
joins: {
comments: {
resolver: ($select, $limit, $sort) => async post => {
post.comments = await comments.find({
query: { postId: post.id, $select: $select, $limit: $limit || 5, [$sort]: { createdAt: -1 } },
paginate: false
});
return post.comments;
},
joins: commentResolvers // Reference the external resolver object
}
}
};The populate hook accepts an options object with the following properties:
Hook Options:
schema (Required): Object or function (hook, options) => Object defining how to populate.checkPermissions (Optional): Function (hook, service, permissions, depth) => boolean to validate if a user can perform the join.profile (Optional): Boolean. If true, includes performance profile information in the result.Include Schema Properties:
service (Required): The service to join.nameAs (Optional): The key where results are placed (default is service name). Supports dot notation.parentField / childField (Required if no query/select): Fields used to link records.query (Optional): Object injected into the service.find call.select (Optional): Function (hook, parent, depth) => Object to generate the query.asArray (Optional): Boolean. Forces a single result into an array.paginate (Optional): Boolean or number. Controls pagination for the joined service.provider (Optional): Set to undefined to make the call appear as if it came from the server.useInnerPopulate (Optional): If true, runs populate hooks defined on the child service.include (Optional): Array of further includes for recursive population.disablePagination hook disables pagination for the find method if context.query.$limit is set to -1 or '-1'. This works across all transports, including REST.const { disablePagination } = require('feathers-hooks-common');
module.exports = {
before: {
find: disablePagination()
}
};disallow hook prevents access to service methods based on the transport used. If no transports are specified, all access is disallowed.// Example: Disallow REST and Socket.IO, but allow internal server calls
const { disallow } = require('feathers-hooks-common');
module.exports = {
before: {
all: disallow(['rest', 'socketio'])
}
};The cache hook maintains a persistent cache for a service to speed up future requests. It must be registered in both the before and after hooks.
get method retrieves records from the cache. Other methods remove entries from the cache in the before hook and add them in the after hook. All records from a find call are cached.get(), set(), delete(), and clear(). While a standard JavaScript Map can be used, it grows without limit; for production, @feathers-plus/cache (a LRU cache) is recommended.makeCacheKey option to convert them to strings.const CacheMap = require('@feathers-plus/cache');
const mongoose = require('mongoose');
const { cache } = require('feathers-hooks-common');
const cacheMap = CacheMap({ max: 100 });
// Convert MongoDB ObjectId to string for the cache key
const makeCacheKey = key => (key instanceof mongoose.Types.ObjectId ? key.toString() : key);
module.exports = {
before: {
all: cache(cacheMap, undefined, { makeCacheKey })
},
after: {
all: cache(cacheMap, undefined, { makeCacheKey })
}
};fastJoin alongside @feathers-plus/batch-loader to resolve complex relationships (like nested arrays or calculated fields) while avoiding the 'N+1' query problem. By initializing batch-loaders in the before hook and using them within joins resolvers, you can consolidate multiple record lookups into a single database call using the $in operator.const { fastJoin, makeCallingParams } = require('feathers-hooks-common');
const BatchLoader = require('@feathers-plus/batch-loader');
const { getResultsByKey, getUniqueKeys } = BatchLoader;
const postResolvers = {
// Initialize loaders in the before hook to be available in the context
before: context => {
context._loaders = { user: {} };
context._loaders.user.id = new BatchLoader(
async (keys, context) => {
const result = await users.find(
makeCallingParams(context, { id: { $in: getUniqueKeys(keys) } }, undefined, {
paginate: false
})
);
// Map results back to the requested keys
return getResultsByKey(keys, result, user => user.id, '!');
},
{ context }
);
},
joins: {
// Resolve a single record
author: () => async (post, context) =>
(post.userRecord = await context._loaders.user.id.load(post.userId)),
// Resolve multiple records from an array of IDs
starers: () => async (post, context) =>
!post.starIds
? null
: (post.starIdsRecords = await context._loaders.user.id.loadMany(post.starIds))
}
};
module.exports = {
after: {
all: [fastJoin(postResolvers, context => ({ author: true, starers: true }))]
}
};