feathers-hooks-common Documentation

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.

Tokens
12.6K
Snippets
65
Records
75
Agent score
55%

What's inside feathers-hooks-common

  1. 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.
  2. Use fastJoin to aggregate related records

    The 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)]
      }
    };
  3. Understand safe mutations in feathers-hooks-common

    Most hooks in 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.
  4. Implement dynamic parameters in FeathersJS hooks

    Standard hook parameters are evaluated when the module is required. To make parameters dynamic (evaluated at each service call), wrap the hook call inside a function that accepts the 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)
      }
    };
  5. Conditionally execute hooks with iffElse

    The 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(...)]
        )
      }
    };
  6. Implement recursive joins using fastJoin resolvers

    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];
            },
          },
        },
      }
    };
  7. Reuse resolvers to keep fastJoin configurations DRY

    To avoid duplicating resolver logic across different services, define base resolvers for each table separately and reference them within other resolver definitions. This allows a common resolver (like a user author resolver) to be reused wherever a record containing a 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
        }
      }
    };
  8. Configure populate hook options and schema properties

    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.
  9. Disable pagination based on query limits

    The 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()
      }
    };
  10. Restrict service access using the disallow hook

    The 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'])
      }
    };
  11. Implement persistent caching for a service using the cache hook

    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.

    • Behavior: The 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.
    • Cache Storage: It accepts any object implementing 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.
    • MongoDB/Mongoose Note: Because these store keys as BSON objects, use the 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 })
      }
    };
  12. Implement efficient data joining with fastJoin and BatchLoader

    Use 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 }))]
      }
    };