# Collection hook

{% hint style="success" %}
This is the official documentation of the `@forestadmin/agent` Node.js agent.
{% endhint %}

Forest Admin allows customizing at a very low level the behavior of any given Collection via the usage of Collection Hooks.

{% hint style="info" %}
Collection Hooks are a very powerful feature and require special care when using them.
{% endhint %}

### 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 an easy 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.

### Context object reference

All hook contexts provide access to:

* `collection` - the current collection, which can be queried using the [Forest Admin Query Interface](https://docs.forestadmin.com/developer-guide-agents-nodejs/data-sources/getting-started/queries)
* `dataSource` - the composite data source containing all collections
* `caller` - information about the user performing the operation

#### The `caller` object

The `caller` object contains information about the current user:

| Property    | Description   |
| ----------- | ------------- |
| `id`        | User ID       |
| `email`     | User email    |
| `firstName` | First name    |
| `lastName`  | Last name     |
| `team`      | Team name     |
| `role`      | Role name     |
| `tags`      | Custom tags   |
| `timezone`  | User timezone |

#### Error methods

All contexts provide methods to throw errors that will be displayed in the Forest Admin UI:

| Method                          | Description                |
| ------------------------------- | -------------------------- |
| `throwValidationError(message)` | Display a validation error |
| `throwForbiddenError(message)`  | Display a forbidden error  |
| `throwError(message)`           | Display a generic error    |

#### Hook-specific context properties

Each hook type provides additional properties:

| Hook          | Position     | Properties                                                   |
| ------------- | ------------ | ------------------------------------------------------------ |
| **List**      | Before       | `filter` (paginated filter), `projection` (fields to return) |
| **List**      | After        | Same as Before + `records` (returned records)                |
| **Create**    | Before       | `data` (data to create)                                      |
| **Create**    | After        | `data` + `records` (created records)                         |
| **Update**    | Before       | `filter`, `patch` (read-only)                                |
| **Update**    | After        | `filter`, `patch` (read-only)                                |
| **Delete**    | Before/After | `filter` (filter for records to delete)                      |
| **Aggregate** | Before       | `filter`, `aggregation`, `limit`                             |
| **Aggregate** | After        | Same as Before + `aggregateResult`                           |

{% hint style="warning" %}
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 Admin [query interface](https://docs.forestadmin.com/developer-guide-agents-nodejs/data-sources/getting-started/queries) will not trigger them.
{% endhint %}

### 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.

```javascript
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 Admin 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 would be the following: Each time a new `User` is created in the database, I want to send him an email.

```javascript
user.addHook('After', 'Create', async (context, responseBuilder) => {
  // The result of the create function always return 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.',
  });
});
```

### Advanced examples

#### Preventing deletion of protected records

```javascript
agent.customizeCollection('users', collection => {
  collection.addHook('Before', 'Delete', async context => {
    const records = await context.collection.list(context.filter, [
      'protected',
      'email',
    ]);

    const protectedRecords = records.filter(r => r.protected);
    if (protectedRecords.length > 0) {
      context.throwForbiddenError(
        `Cannot delete protected users: ${protectedRecords
          .map(r => r.email)
          .join(', ')}`,
      );
    }
  });
});
```

#### Querying other collections via dataSource

```javascript
agent.customizeCollection('orders', collection => {
  collection.addHook('Before', 'Create', async context => {
    // Access another collection via dataSource
    const customersCollection = context.dataSource.getCollection('customers');

    for (const order of context.data) {
      const [customer] = await customersCollection.list(
        {
          conditionTree: { field: 'id', operator: 'Equal', value: order.customerId },
        },
        ['creditLimit', 'currentBalance'],
      );

      if (customer.currentBalance + order.amount > customer.creditLimit) {
        context.throwValidationError(
          `Order exceeds credit limit for customer ${order.customerId}`,
        );
      }
    }
  });
});
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.forestadmin.com/developer-guide-agents-nodejs/agent-customization/hooks/collection-hook.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
