# Update Handler

> **Prerequisite**: this reference applies only when the handler file already exists on disk — the
> CLI generated it from a standard `update` or `updateById` 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

File matches `*-update-*.handler.ts` under `@app/<pkg>/<module>/application/update/`. The primary
archetype is `update-by-id`, which accepts a partial payload, fetches the existing row, computes a
delta, and persists only the changed fields. Bulk updates (no `-by-id`) share the skeleton but skip
the find-first step.

## Canonical signature

Exact shape from `backend/src/@app/iam/tag/application/update/iam-update-tag-by-id.handler.ts`:

```typescript
/**
 * @aurora-catalyst-generated
 * @source cliter/iam/tag.aurora.yaml
 */
import { IamTag, IamUpdateTagByIdInput } from '@api/graphql';
import { IamFindTagByIdService, IamUpdateTagByIdService } from '@app/iam/tag';
import { IamTagFieldSchema } from '@app/iam/tag/domain';
import { ApplySchema, EmitEvent, HandlerMeta } from '@aurorajs.dev/core-back';
import { Obj, QueryStatement } from '@aurorajs.dev/core-common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class IamUpdateTagByIdHandler {
  constructor(
    private readonly eventEmitter: EventEmitter2,
    private readonly updateByIdService: IamUpdateTagByIdService,
    private readonly findByIdService: IamFindTagByIdService,
  ) {}

  @EmitEvent('iam.tag.updated')
  @ApplySchema(IamTagFieldSchema)
  async main(
    payload: IamUpdateTagByIdInput,
    constraint?: QueryStatement,
    handlerMeta?: HandlerMeta,
  ): Promise<IamTag> {
    const tag = await this.findByIdService.main(payload.id, constraint, handlerMeta);

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

    const dataToUpdate = Obj.diff(payload, tag);

    await this.updateByIdService.main(
      {
        ...dataToUpdate,
        id: payload.id,
      },
      constraint,
      handlerMeta,
    );

    return await this.findByIdService.main(payload.id, constraint, handlerMeta);
  }
}
```

Role of each moving part:

- **`constraint?: QueryStatement`** — extra criteria that scope the operation. Most commonly used
  for multi-tenant isolation (ensuring the updated row belongs to the caller's tenant). Pass it
  through to every service call unchanged.
- **`Obj.diff(payload, tag)`** — from `@aurorajs.dev/core-common`. Computes the subset of fields
  that differ between the incoming payload and the persisted row. The service therefore receives
  only the fields that actually change, which keeps the UPDATE statement minimal and plays well with
  auditing.
- **Two `findByIdService.main()` calls** — the first validates existence (throw on missing); the
  second returns the freshly-updated entity. Both receive the same `constraint`.
- **`@EmitEvent('iam.tag.updated')`** — fires after a successful `main()`. If `NotFoundException`
  throws, no event is emitted.

## `Obj.diff()` pattern

`Obj.diff(payload, existing)` answers the question "which fields in `payload` differ from
`existing`?". Apply it so the service receives:

- fields the client actually changed, plus
- the `id` (explicitly re-added via `{ ...dataToUpdate, id: payload.id }`).

The `id` is re-added because `Obj.diff` would omit it when both sides match, but the service needs
it as the target key.

## `NotFoundException` handling

The handler owns "not found" semantics. The service does not throw on missing rows; it would simply
perform a no-op UPDATE. Letting that happen silently masks client errors, so the handler fetches
first and throws if the row is absent. Same pattern in `delete-by-id` and `find-by-id`.

## Expected errors

- **`NotFoundException`** — missing row after the first `findByIdService.main()` call.
- **`ValidationException`** / field-level throws from `@ApplySchema` — surface automatically; no
  manual re-throw.
- **`BadRequestException`** / `ConflictException` — when business rules reject the transition (e.g.,
  status `archived` cannot be updated, or the new slug collides in-tenant).

## `HandlerMeta` flow

```
resolver.main()            constructs { timezone, repositoryOptions: { auditing } }
  ↓
handler.main(payload, constraint, handlerMeta)
  ↓
findByIdService.main(id, constraint, handlerMeta)  ← unchanged passthrough
  ↓
updateByIdService.main(payload, constraint, handlerMeta)  ← unchanged passthrough
```

The handler never mutates `handlerMeta`. It only forwards it. Any auditing/timezone effect happens
at the repository layer by reading the meta on the way in.

## Archetype-specific anti-patterns

- **Sending the full payload to the update service.** Use `Obj.diff()`. Writing all fields, changed
  or not, pollutes the audit log and masks real changes.
- **Omitting the second `findByIdService.main()` call.** The handler contract returns the persisted
  entity, not the payload-with-id. The second fetch accounts for server-side mutations (timestamps,
  derived fields).
- **Validation duplicated inside the update service.** `@ApplySchema` already validated the payload;
  any domain check lives in the handler. The service stays a thin persistence step.
- **Mutating `constraint` between calls.** Treat it as read-only and forward verbatim.
