While working on a recent Feathers.js project, I encountered a challenge related to modifying service results in an after hook. Specifically, I needed to append a calculated value (in my case, a rank) to each record. However, the way I initially approached the problem led to an unintended recursion issue, where the after hook triggered itself repeatedly.
In this note, I’ll walk through what I learned and share how I resolved the issue by using direct database queries instead of context.service.find
. This could be useful for anyone encountering similar recursion problems in Feathers.js after hooks.
context.service.find
In Feathers.js, after hooks are a great way to modify the data returned by a service before sending it back to the client. My goal was to return a list of records (e.g., performance times) and calculate a rank for each based on their position relative to others.
Initially, I used context.service.find
to query the database from within the after hook. However, this approach caused the after hook to re-trigger itself every time the service method was called, resulting in an infinite loop. Here's a simplified version of the problematic code:
module.exports = (options = {}) => {
return async (context) => {
// This causes recursion!
const totalRecordsBefore = await context.service.find({
query: {
intTime: { $lt: context.result.data[0].intTime }
}
});
let currentRank = totalRecordsBefore + 1;
context.result.data.forEach((record, index) => {
record.rank = currentRank + index;
});
return context;
};
};
Since context.service.find
internally triggers the service method again, the after hook kept calling itself, leading to the recursion issue.
The key realization was that Feathers.js hooks still have access to the underlying database model (e.g., Mongoose or Sequelize). By using the model directly for queries, I could bypass the service layer and avoid re-triggering the after hook.
Here’s how I adjusted the code to query the database directly using Mongoose’s Model.countDocuments()
instead of context.service.find
:
module.exports = (options = {}) => {
return async (context) => {
const { Model } = context.service; // Access the database model (e.g., Mongoose, Sequelize, etc.)
// Check if there are results to rank
if (context.params.query["ageGroup"] && context.result.total > 0) {
const firstRecord = context.result.data[0]; // Get the first record to base the rank on
// Build the query from context.params.query but remove $limit, $skip, $sort
const query = { ...context.params.query };
let nextRank = query.$skip;
delete query.$limit;
delete query.$skip;
delete query.$sort;
// Add the intTime condition dynamically to the query
query.intTime = { $lt: firstRecord.intTime };
// console.log(query);
// Query the database directly using the model (Mongoose example)
const totalRecordsBefore = await Model.countDocuments(query);
let currentRank = totalRecordsBefore + 1; // Start rank based on records before
// Loop through the results to add ranks
context.result.data.forEach((record, index) => {
// Assign the rank to the current record
nextRank++;
if (
index > 0 &&
record.intTime !== context.result.data[index - 1].intTime
) {
currentRank = nextRank;
}
record.rank = currentRank;
});
}
return context; // Return the modified context with ranked results
};
};
Using the model to query the database directly avoids the recursion issue because it doesn’t invoke the Feathers service method again. Here’s what I learned:
Model.countDocuments()
for Mongoose), I was able to fetch the necessary data without triggering the after hook multiple times.This experience taught me the importance of understanding how Feathers.js hooks interact with the service layer. In cases where you need to modify results in an after hook (like calculating ranks), using context.service.find
can lead to recursion problems. Instead, directly accessing the model to query the database provides a simple and effective solution.
I hope sharing this approach helps others who might run into similar issues with Feathers.js. If you're dealing with recursion or need to query data in an after hook, I recommend using the model directly for querying the database.