# Delete Handler

> **Prerequisite**: this reference applies only when the handler file already exists on disk — the
> CLI generated it from a standard `delete` or `deleteById` 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 `*-delete-*.handler.ts` under `@app/<pkg>/<module>/application/delete/`. The primary
archetype is `delete-by-id`, which accepts an id, confirms the row exists, persists the deletion,
and returns the deleted entity so the caller can render confirmation UI without an extra query.

## Canonical signature

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

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

@Injectable()
export class IamDeleteTagByIdHandler {
  constructor(
    private readonly eventEmitter: EventEmitter2,
    private readonly deleteByIdService: IamDeleteTagByIdService,
    private readonly findByIdService: IamFindTagByIdService,
  ) {}

  @EmitEvent('iam.tag.deleted')
  @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`);
    }

    await this.deleteByIdService.main(id, constraint, handlerMeta);

    return tag;
  }
}
```

Role of each moving part:

- **`@EmitEvent('iam.tag.deleted')`** — fires after a successful `main()`. If the find returns empty
  and `NotFoundException` is thrown, no event is emitted.
- **`@Format(IamTagFieldSchema)`** — applied because the delete handler **returns** the deleted
  entity, not a void or boolean. The decorator reshapes the returned object using the same schema
  that find/paginate handlers use.
- **`find` before `delete`** — the find confirms existence (so the handler can throw
  `NotFoundException` instead of silently no-op-ing), and the returned reference is what the handler
  ultimately sends back.
- **Return value is the pre-delete snapshot** — the service is called after the find, so by the time
  the handler returns, the row is gone. Returning the snapshot preserves the caller's ability to
  display "tag 'foo' was deleted" without a second query.

## `NotFoundException` pattern

Same contract as update and find-by-id: the handler owns the "not found" semantics. The delete
service would happily run a zero-row DELETE without complaint, masking a client error. The handler's
find-first step makes the failure loud.

## Cascading considerations

Catalyst services are thin and do not orchestrate cascades. When a delete must also tear down
dependent rows, the options are:

1. **Database-level cascade** — declared in the Sequelize model or the YAML relation
   (`onDelete: CASCADE`). Preferred when the relation is stable and every delete should trigger the
   cascade.
2. **Domain-level cascade inside the handler** — call multiple services in sequence from `main()`,
   typically with explicit order to respect referential integrity. Use this when the cascade depends
   on domain rules (e.g., only delete children in state `draft`).
3. **Event-driven cascade** — other handlers listen to the emitted `*.deleted` event and perform
   their own cleanup. Decouples modules but is harder to reason about; reserve for
   cross-bounded-context teardown.

Do not mix strategies for the same relation. Pick one explicitly.

## Expected errors

- **`NotFoundException`** — missing row after the find.
- **`ConflictException`** / `BadRequestException` — when a domain rule forbids deletion (e.g., a tag
  in use by active sessions cannot be deleted). Perform that check between the find and the delete
  service call.

## Archetype-specific anti-patterns

- **Returning `void` or `boolean`.** The catalyst contract returns the deleted entity. Callers rely
  on it.
- **Deleting before validating existence.** Losing the distinction between "wasn't there" and "was
  deleted successfully" produces silent client bugs.
- **Manual `eventEmitter.emit(...)` after delete.** Let `@EmitEvent` do it. A manual call fires
  twice or fires before the delete completes.
- **Soft-delete logic in the handler.** If the entity supports soft delete, express that in the
  domain model and the delete service; the handler is the same either way.
