Skip to main content
Forest provides extensive customization options for your collections, allowing you to tailor CRUD operations, implement business logic, and enhance data integrity through two features:
  • Collection hooks, execute custom code before or after CRUD operations
  • Collection overrides, completely replace the default behavior of CUD operations
These two features are almost identical, but they are executed at very different stages of the customizations. This means that Collection hooks will be executed even if you choose to use Collection override.

Collection hooks

Collection hooks allow you to execute custom code before or after CRUD operations, giving you the ability to enforce business rules, or integrate with external services. A request passes through the Before hook (validate, mutate, or abort), then the collection operation (List, Create, Update, Delete), then the After hook (enrich, side-effects), then the response; a Before hook that throws aborts the operation

Features

  • Pre and Post operation execution: Execute custom logic before or after a specific collection operation.
  • Flexible trigger points: Hook into any of the standard CRUD operations (list, create, update, delete, aggregate).
  • Contextual information: Access to a rich high-level context providing details about the operation, enabling precise and informed logic execution.

How it works

Any given Collection should implement all of the following functions:
  • list
  • create
  • update
  • delete
  • aggregate
The Collection hooks feature allows executing code before and/or after any of these functions, providing a way to interact with your Collections. To declare a hook on a Collection, the following information is required:
  • A lifecycle position (Before | After)
  • An action type (List | Create | Update | Delete | Aggregate)
  • A callback, that will receive a context matching the provided hook position and hook definition.
A single Collection can have multiple hooks with the same position and the same type. They will run in their declaration order.Collection hooks are only called when the Collection function is contacted by the UI. This means that any usage of the Forest query interface will not trigger them.

Basic use cases

In the following example, we want to prevent a set of users from updating any records of the Transactions table. We want to check if the user email is allowed to update a record via an external API call.
transaction.addHook('Before', 'Update', async context => {
  // context.caller contains information about the current user, the defined
  // timezone, etc.
  // In this case, context.caller.email is the email used in Forest by the user
  // that initiated the call
  const isAllowed = await myFunctionToCheckIfUserIsAllowed(context.caller.email);
  if (!isAllowed) {
    // Raising an error here will prevent the execution of the update function,
    // as well as any other hooks that may be defined afterwards.
    context.throwForbiddenError(`${context.caller.email} is not allowed!`);
  }
});
Another good example: each time a new User is created in the database, send them a welcome email.
user.addHook('After', 'Create', async (context, responseBuilder) => {
  // The result of the create function always returns an array of records
  const userEmail = context.records[0]?.email;
  await MyEmailSender.sendEmail({
    from: 'erlich@bachman.com',
    to: userEmail,
    message: 'Hey, a new account was created with this email.',
  });
});

Collection overrides

Collection overrides provide the ability to completely replace the default behavior of CUD operations. This feature allows for custom implementations of create, update, and delete operations, giving you full control over data handling.

Features

  • Complete control over CUD: Directly replace the standard behavior of create, update, and delete operations with custom logic.
  • Custom operation logic: Implement entirely custom workflows or integrate external services directly into your CUD operations.
  • Full operation context: Receive detailed low-level context about the operation, enabling complex logic and integrations.

How it works

Collection overrides allow you to define custom behavior that will entirely replace the default implementation of the create, update, and delete operations. To define an override for a Collection, you must specify the handler function that will be executed instead of the default operation. The custom handler function will receive a context object containing relevant information for the operation.

Setting up overrides

Custom Create operation

Unknown properties in returned records will be removed.
collection.overrideCreate(async context => {
  // Custom logic to handle creation
  // context.data contains the data intended for creation
  // Return an array of created records
});

Custom Update operation

collection.overrideUpdate(async context => {
  // Custom logic to handle update
  // context.filter to determine which records are targeted
  // context.patch contains the data for update
  // Perform update operation
});

Custom Delete operation

collection.overrideDelete(async context => {
  // Custom logic to handle deletion
  // context.filter to determine which records are targeted
  // Perform deletion operation
});
Overrides take precedence over the default operation. Ensure your custom handlers properly manage all necessary logic for the operation, as the default behavior will not be executed.

Basic use cases

Create over API

You might want to create the record with your custom API:
const { MissingFieldError } = require('@forestadmin/datasource-toolkit');

product.overrideCreate(async context => {
  const { data } = context;

  if (data.some(product => !product.name)) {
    throw new MissingFieldError('name', 'products');
  }

  const response = await fetch('https://my-product-api.com/products', {
    method: 'POST',
    body: data,
  });
  const products = await response.json();

  // structure is an array of Partial<Product>
  // [ { name: 'CoffeeMaker3000, price: "$300" } ]
  return products;
});

Modify data before update

You might want to modify payload data before updating your record:
product.overrideUpdate(async context => {
  const { patch } = context;

  // Execute data modification and validation only if one of name or slug was edited
  if (patch.name || patch.slug) {
    const name = patch.name || patch.slug.split('-')[0];
    const uuid = await fetch('https://my-product-api.com/slug', {
      method: 'GET',
      body: { name, slug },
    });

    patch.name = name;
    patch.slug = `${name}-${uuid}`;
  }

  await context.collection.update(context.filter, context.patch);
});