# Entity CRUD Pipeline


`src/http/` contains the engine that turns a `*.meta.ts` + a service into
the full canonical entity route set — every entity page the FE renders
flows through this pipeline. The SDK edition (`entity-router.mdx` in the
sdk dev-guide) mirrors it for microservices; this is the BE original.

## The pieces

| File | Role |
|---|---|
| `entity-router.ts` | Route registration: the canonical action set (list/get/create/update/delete/purge/restore/audit/duplicate/bulk/export/meta), permission-declared |
| `entity-service.ts` | The service contract every entity service implements |
| `entity-write.ts` | `runEntityWrite` — write-path orchestration inside one transaction |
| `entity-actions.ts` | Action metadata → `meta.actions` (which ops exist and are enabled) |
| `list-query.ts` | Structured parsing of list params (filters, connector, sort, search, page) |
| `meta-assembler.ts` | Assembles `GET /meta` — columns, labels, actions, collaboration flags |
| `protected-router.ts` | Route protection plumbing (auth + RBAC wiring) |
| `etag-middleware.ts` | ETag/304 handling on cacheable GETs |
| `error-handler.ts` / `api-errors.ts` | Central RFC 7807 mapping |
| `lib/bulk/` | `bulk-action-runner` — chunked bulk ops with per-item results |
| `lib/export/` | CSV/structured export engine behind the `export` action |
| `lib/audit/` | audit transformer — history rows → UI-ready timeline |

## The write envelope — `{entity, translations}`

BE write bodies carry **two** payloads: the entity itself plus optional
per-language field translations in the same request. `runEntityWrite`
runs the whole thing inside one transaction — entity write and
translation writes commit or roll back together. This is a deliberate
BE-vs-US asymmetry: translations live in the BE's system schema, so only
the gateway may piggyback them; a US entity route receives `{entity}`
only (see sdk `entity-router`).

## Read path

`GET /list` → `list-query.ts` parses the bracket-notation query into the
structured filter/sort/search/page shape the DAL consumes →
`service.list(query)` → rows serialized as-is (snake_case, no DTO
reshaping). `GET /meta` is assembled by `meta-assembler` from the static
`*.meta.ts` plus the *derived* action set — the FE builds its whole page
(columns, filters, buttons) from that single response.

List query parameters (`list-query.ts` is the single parser — routes must
not re-parse): `search` + `search_in` (CSV column allowlist),
`filters[i][field|op|value|connector]` (bracket notation),
`sort_key`/`sort_dir`, `page`/`page_size`, and `deleted_records` —
`EXCLUDED` | `ONLY` | `INCLUDED`, sent explicitly by the FE for soft-delete
entities (absent param = BE default, currently EXCLUDED).

## Error discipline

Domain errors come from the service layer (`ApiError`/`mapDalError`); the
controller maps them to RFC 7807 — it never invents codes. Optimistic
locking surfaces as `409` on `version` mismatch; soft-deleted records
404/restore per action semantics.
