# Collection hook

{% hint style="success" %}
This is the official documentation of the `forestadmin-agent-django` and `forestadmin-agent-flask` Python agents.
{% 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-python/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                |
| --------------------------------- | -------------------------- |
| `throw_validation_error(message)` | Display a validation error |
| `throw_forbidden_error(message)`  | Display a forbidden error  |
| `throw_error(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-python/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.

```python
from forestadmin.datasource_toolkit.decorators.hook.context.update import (
    HookBeforeUpdateContext
)

async def transaction_before_update_fn(context: HookBeforeUpdateContext):
    is_allowed = my_function_to_check_user_is_allowed(context.caller.email)
    if not is_allowed:
        context.throw_forbidden_error(f"{context.caller.email} is not allowed")

agent.customize_collection("Transactions").add_hook(
    "Before", "Update", transaction_before_update_fn
)
```

Another good example would be the following: Each time a new `User` is created in the database, I want to send him an email.

```python
agent.customize_collection("User").add_hook(
    "After",
    "Create",
    lambda context: MyEmailSender.send_email(
        {
            "from": "erlich@bachman.com",
            "to": context.records[0]['email'],
            "message": "Hey, a new account was created with this email.",
        }
    ),
)
```

### Advanced examples

#### Preventing deletion of protected records

```python
async def before_delete_user(context):
    records = await context.collection.list(context.filter, ['protected', 'email'])

    protected_records = [r for r in records if r.get('protected')]
    if protected_records:
        emails = ', '.join(r['email'] for r in protected_records)
        context.throw_forbidden_error(f"Cannot delete protected users: {emails}")

agent.customize_collection("user").add_hook("Before", "Delete", before_delete_user)
```

#### Querying other collections via dataSource

```python
async def before_create_order(context):
    customers_collection = context.datasource.get_collection('customer')

    for order in context.data:
        filter = ConditionTreeLeaf('id', 'equal', order['customer_id'])
        customers = await customers_collection.list(filter, ['credit_limit', 'current_balance'])
        customer = customers[0]

        if customer['current_balance'] + order['amount'] > customer['credit_limit']:
            context.throw_validation_error(f"Order exceeds credit limit for customer {order['customer_id']}")

agent.customize_collection("order").add_hook("Before", "Create", before_create_order)
```
