# Read Handlers

> **Prerequisite**: this reference applies only when the handler file already exists on disk — the
> CLI generated it from a standard `find`, `findById`, `get`, or `paginate` operation declared in
> the module's YAML. If the file does not exist, return to Step 0 of `SKILL.md`: edit the YAML, run
> `catalyst load back module --name=<bc>/<module> --force`, and the CLI emits the handler, service,
> resolver, controller, DTO, and GraphQL fragment together. Hand-writing the handler bypasses the
> generator and breaks on the next regeneration.

## When it applies

Any handler under `@app/<pkg>/<module>/application/{find,get,paginate}/`. Read handlers do not emit
events (there is no state change to broadcast) and do not mutate data. They shape the response via
the `@Format(FieldSchema)` decorator.

Four read archetypes exist, distinguished by what they return:

| Archetype          | File pattern                        | Returns              | `NotFoundException` on empty? |
| ------------------ | ----------------------------------- | -------------------- | ----------------------------- |
| Find one by id     | `*-find-*-by-id.handler.ts`         | `Entity`             | Yes                           |
| Find one by filter | `*-find-*.handler.ts` (no `-by-id`) | `Entity`             | Yes                           |
| Get many           | `*-get-*.handler.ts`                | `Entity[]`           | No (empty array is valid)     |
| Paginate           | `*-paginate-*.handler.ts`           | `Pagination<Entity>` | No (empty page is valid)      |

## Canonical signatures

All from `backend/src/@app/iam/tag/application/`.

### `find-by-id`

```typescript
import { IamTag } from '@api/graphql';
import { IamFindTagByIdService } from '@app/iam/tag';
import { IamTagFieldSchema } from '@app/iam/tag/domain';
import { Format, HandlerMeta } from '@aurorajs.dev/core-back';
import { QueryStatement } from '@aurorajs.dev/core-common';
import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class IamFindTagByIdHandler {
  constructor(private readonly findByIdService: IamFindTagByIdService) {}

  @Format(IamTagFieldSchema)
  async main(id: string, constraint?: QueryStatement, handlerMeta?: HandlerMeta): Promise<IamTag> {
    const tag = await this.findByIdService.main(id, constraint, handlerMeta);

    if (!tag) {
      throw new NotFoundException(`IamTag with id: ${id}, not found`);
    }

    return tag;
  }
}
```

### `find` (by filter)

```typescript
@Injectable()
export class IamFindTagHandler {
  constructor(private readonly findService: IamFindTagService) {}

  @Format(IamTagFieldSchema)
  async main(
    queryStatement?: QueryStatement,
    constraint?: QueryStatement,
    handlerMeta?: HandlerMeta,
  ): Promise<IamTag> {
    const tag = await this.findService.main(queryStatement, constraint, handlerMeta);

    if (!tag) {
      throw new NotFoundException(`IamTag not found`);
    }

    return tag;
  }
}
```

### `get`

```typescript
@Injectable()
export class IamGetTagsHandler {
  constructor(private readonly getService: IamGetTagsService) {}

  @Format(IamTagFieldSchema)
  async main(
    queryStatement?: QueryStatement,
    constraint?: QueryStatement,
    handlerMeta?: HandlerMeta,
  ): Promise<IamTag[]> {
    return await this.getService.main(queryStatement, constraint, handlerMeta);
  }
}
```

### `paginate`

```typescript
@Injectable()
export class IamPaginateTagsHandler {
  constructor(private readonly paginateService: IamPaginateTagsService) {}

  @Format(IamTagFieldSchema)
  async main(
    queryStatement?: QueryStatement,
    constraint?: QueryStatement,
    handlerMeta?: HandlerMeta,
  ): Promise<Pagination> {
    return await this.paginateService.main(queryStatement, constraint, handlerMeta);
  }
}
```

## Role of `@Format`

`@Format(FieldSchema)` shapes the handler's return value using the same schema that `@ApplySchema`
uses for write-side validation. It strips fields the schema marks as hidden, applies formatting
rules, and resolves virtual fields. Put it on every read handler — the generator always emits it,
and omitting it causes silent drift between read and write shapes.

## `QueryStatement` with `include` for relations

`queryStatement` is the client-facing filter/sort/pagination payload. `constraint` is the
server-side scope (tenant isolation, permission filter). The two are merged by the service; the
handler forwards them unchanged.

To fetch relations in a single query, include them:

```typescript
const queryStatement: QueryStatement = {
  where: { tenantId: handlerMeta.repositoryOptions?.tenantId },
  include: [{ association: 'owner' }, { association: 'roles' }],
  limit: 50,
  offset: 0,
};
```

Relations are declared in the aurora YAML; the field names (`'owner'`, `'roles'`) match the YAML
`aggregateProperties`. If you are hand-writing a handler that needs a relation that the
CLI-generated variant does not include, adjust the service-side default or pass the `include`
through a hand edit to the handler — both are accepted, though adjusting the service is preferred
because it keeps the handler regenerable.

## Pagination response shape

`Pagination` (imported from `@api/graphql`) has the shape:

```typescript
{
  total: number;
  count: number;
  rows: Entity[];
}
```

The paginate service fills all three fields. The handler does not reshape; it returns the service
output verbatim (after `@Format` post-processing).

## `NotFoundException` placement

Only `find-by-id` and `find` throw. `get` and `paginate` return empty arrays/pages because "nothing
matches the filter" is a valid, expected outcome — not an error.

## Archetype-specific anti-patterns

- **Emitting events from read handlers.** Reads do not change state; there is nothing to announce.
  Do not add `@EmitEvent` here.
- **Using `@ApplySchema` on reads.** That decorator is for validating incoming payloads. The
  read-side equivalent is `@Format`.
- **Re-querying the entity manually after the service call.** The service already returns it; a
  second call duplicates round-trips.
- **Throwing on empty results in `get` / `paginate`.** Treat emptiness as a normal result, not an
  error.
- **Leaking `constraint` details to the client.** `constraint` encodes server-side security scope;
  do not serialize it back in the response or echo it in error messages.

## Cross-reference

The internals of `QueryStatement` (full shape, the 40+ `Operator` enum values, `include` semantics,
and the current security gap where `formatRecord` does NOT recurse into included relations) are
documented in
[`catalyst-field-schema/references/query-statement.md`](../../catalyst-field-schema/references/query-statement.md).
Read it whenever composing a custom `where`, reaching for an `Operator`, or loading a relation via
`include` — especially when the relation loads an entity with masked fields (passwords, tokens).
