feathers-hooks-common

repository·master·Indexed 41 days ago

https://github.com/feathersjs-ecosystem/feathers-hooks-common

A collection of reusable, high-level hooks for FeathersJS services focusing on data manipulation, caching, and efficient data joining. Features include the fastJoin hook for GraphQL-like imperative API joins with batch-loading support, the populate hook for recursive relationship population, and utilities for dynamic hook parameters and field filtering via the keep hook. Version 8.2.1 is compatible with Feathers v5.

Tokens
31K
Snippets
83
Records
106
Agent score
47%

What's inside feathers-hooks-common

  1. Overview of feathers-hooks-common

    master

    feathers-hooks-common is a library providing a collection of reusable Feathers hooks for FeathersJS applications. It is designed to extend the functionality of Feathers services by providing common logic through the standard Feathers hook pattern.

    The documentation is organized into several key areas:

    • Hooks API: Details on the available hooks you can use in your services.
    • Utilities API: Details on utility methods provided by the library.
    • Migration Guides: Instructions for upgrading to newer versions.
    • In-depth Guides: Detailed usage instructions for specific hooks.
  2. Perform recursive operations with fastJoin

    master

    You can perform recursive operations by nesting joins within a resolver. To do this, the resolver function must return the data that is to be recursively operated on.

    Syntax for Recursive Joins:

    comments: {
      resolver: (args) => async (record) => {
        record.comments = await service.find(...);
        return record.comments; // CRITICAL: Must return the data to be operated on
      },
      joins: {
        author: ... // This will operate on the returned comments
      }
    }
    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; // Return the comments so the nested 'joins' can run on them
          },
          joins: {
            author: $select => async comment => {
              comment.author = (await users.find({
                query: { id: comment.userId, $select: $select },
                paginate: false
              }))[0];
            }
          }
        }
      }
    };
  3. Configure fastJoin resolvers and queries

    master

    In fastJoin, the joins object defines the operations to perform on each record. Each key in joins represents a property name used in the query to control execution.

    Resolver Structure:

    • joins: {}: Defines the operations.
    • property_name:: The name of the field to be added/modified.
    • (...args) =>: Allows passing parameters from the query to the resolver.
    • async (record, context) => ...: The resolver function receives the record and the Feathers context. It must modify the record (e.g., post.author = ...).

    Querying and Shaping Results:

    • Shaping: The query object determines which resolvers run. If a property is not in the query, the resolver for that property is skipped, and the field will not appear in the result.
    • Parameters: You can pass arrays to the query to provide arguments to the resolver (e.g., $select fields).
    • Dynamic Queries: Use context => query to allow the client to control which fields are joined.
    // Shaping the result with a query
    const query = {
      author: true, // Only runs the 'author' resolver
      starers: [['id', 'name']] // Runs 'starers' and passes ['id', 'name'] as $select
    };
    
    module.exports = {
      after: {
        all: [fastJoin(postResolvers, context => query)]
      }
    };
  4. Re-run client-side validation on the server with validate

    master

    You can share validation logic between your front-end and back-end by exporting validation functions from a common module. The validate hook can execute these functions on the server.

    • Synchronous Validation: A function that takes values and returns an errors object. If the object is empty, validation passes. If it contains keys, those keys represent the field names and values represent the error messages.
    • Asynchronous Validation: A function that returns a Promise. Resolve with null if validation passes (or with sanitized values), and reject with a Feathers error if validation fails.

    This pattern ensures that the exact same rules applied to the user in the browser are enforced by the server.

    // /common/usersClientValidations
    
    // Sync validation: returns an object of errors
    clientValidations.signup = values => {
      const errors = {};
      if (!values.email) errors.email = 'Email is required.';
      return errors;
    };
    
    // Async validation: returns a Promise
    clientValidations.signupAsync = values =>
      new Promise((resolve, reject) => {
        const errs = {};
        // Logic to check if email is taken...
        if (emailTaken) {
          errs.email = 'Already taken.';
          // Reject with an error object containing the field errors
          return reject(new errors.BadRequest('Values already taken.', { errors: errs }));
        }
        resolve(null);
      });
  5. Understand safe mutations in feathers-hooks-common

    master

    Most hooks in feathers-hooks-common have been updated to follow a safe mutation pattern. Instead of mutating the existing context object (e.g., adding or deleting properties directly), hooks now replace the object on the context with a new object.

    This design prevents side effects where params or params.query are unintentionally modified in nested hooks as they are passed through the hook chain.

  6. How actOnDefault and actOnDispatch work together

    master

    In Feathers, hooks typically mutate context.data (for before hooks) or context.result (for after hooks). This is the 'default' behavior.

    actOnDispatch allows you to run a series of hooks that instead mutate context.dispatch. context.dispatch is an optional, writable property that contains a "safe" version of the data intended for clients (e.g., via REST or Socket.io). If context.dispatch is not set, Feathers sends context.result to the client instead.

    actOnDefault is used to "undo" actOnDispatch by running hooks that mutate the standard context.data or context.result instead of the dispatch object. This allows for nested hook execution where some hooks target the client-facing data and others target the internal service data.

    ```js
    const { actOnDefault, actOnDispatch } = require('feathers-hooks-common');
    
    module.exports = {
      after: {
        find: [
          hook1(), 
          actOnDispatch(
            hook2(), 
            actOnDefault(hook3()), 
            hook4()
          ), 
          hook5()
        ]
      }
    };

    In this example:

    • hook1, hook3, and hook5 mutate context.result (default behavior).
    • hook2 and hook4 mutate context.dispatch.
  7. Understand the added properties in populated results

    master

    When using populate, the resulting objects may contain additional metadata properties:

    • _include: An array of property names that contain joined items.
    • _elapsed: An object containing the time taken (in nanoseconds) for each include and the total time. (Requires profile: true).
    • _computed: An array of property names containing values computed by a serialize hook.

    These properties are used by the depopulate hook to clean up the object (e.g., before calling service.patch()).

  8. Use persistent cache with fastJoin and BatchLoaders

    master

    By default, BatchLoaders use a per-request cache. To share cached records across different requests, use a persistent cache (like @feathers-plus/cache) with your BatchLoader.

    Requirements:

    1. Create a CacheMap (e.g., using CacheMap({ max: 100 })).
    2. Pass the cacheMap to the BatchLoader options.
    3. Crucial: The cache hook must be registered in both the before and after hooks of the service to ensure both reads and writes are cached.

    When configured correctly, the number of service calls for subsequent requests can drop to 0.

    const { cache, fastJoin, makeCallingParams } = require('feathers-hooks-common');
    const BatchLoader = require('@feathers-plus/batch-loader');
    const CacheMap = require('@feathers-plus/cache');
    
    const cacheMapUsers = CacheMap({ max: 100 });
    
    const userBatchLoader = new BatchLoader(
      async keys => { /* ... fetch logic ... */ },
      { cacheMap: cacheMapUsers }
    );
    
    module.exports = {
      before: {
        all: cache(cacheMapUsers)
      },
      after: {
        all: [cache(cacheMapUsers), fastJoin(postResolvers, () => query)]
      }
    };
  9. Make hook parameters dynamic based on request context

    master

    By default, hook parameters are evaluated when the module is required, making them static. If you need parameters to change based on the current request or circumstances (e.g., checking a variable or user permissions), you cannot pass a function directly to the hook factory, nor can you call the factory inside the module export definition.

    To make parameters dynamic, you must wrap the hook call in a function that accepts the context. This function should call the hook factory (e.g., disallow()) inside its body using the dynamic value, and then immediately invoke the resulting hook with the context.

    function disallowWhat() {
      return someVariableCircumstance() ? 'rest' : 'external';
    }
    
    // Correct way to use dynamic parameters:
    module.exports = {
      before: {
        all: context => disallow(disallowWhat())(context)
      }
    };
  10. Select different populate schemas based on client needs

    master

    You can use a function for the schema option in populate to return different inclusion sets based on the incoming request. This allows clients to request specific data shapes (e.g., an 'Accounting' view vs a 'Receiving' view).

    1. Client side: Use paramsForServer to pass a schema identifier.
    2. Server side: Use paramsFromClient to extract the identifier and a function in populate to select the correct schema.
    // 1. Client side
    import { paramsForServer } from 'feathers-hooks-common';
    // Request a specific schema named 'po-acct'
    purchaseOrders.get(id, paramsForServer({ schema: 'po-acct' }));
    
    // 2. Server side
    import { paramsFromClient } from 'feathers-hooks-common';
    
    const poSchemas = {
      'po-acct': { include: [ /* Accounting specific includes */ ] },
      'po-rec': { include: [ /* Receiving specific includes */ ] }
    };
    
    purchaseOrders.before({
      all: paramsFromClient('schema')
    });
    
    purchaseOrders.after({
      all: populate({ 
        schema: (hook) => poSchemas[hook.params.schema] 
      })
    });