40227-vm/backend/docs/documents.md
2026-06-10 18:27:19 +02:00

112 lines
6.4 KiB
Markdown

# Documents Backend
## Purpose
`documents` stores file/document metadata records (with related `file` uploads) attached to
school entities such as students, staff, classes, invoices, organizations, and campuses. The
slice is hand-written (not the generic CRUD factory): the service returns trimmed DTOs via
`toDocumentDto`, supports CSV export and CSV bulk import, and resolves related `organization`,
`campus`, and `file` on single-record reads.
## Slice Files (by layer)
- Route: `src/routes/documents.ts` (wires CRUD plus `bulk-import`, `count`, `autocomplete`,
`deleteByIds`; applies `checkCrudPermissions('documents')` to every route).
- Controller: `src/api/controllers/documents.controller.ts` (custom — maps DTOs, handles CSV
export and file upload).
- Service (BLL): `src/services/documents.ts` (exports `toDocumentDto`; wraps writes in
transactions; parses CSV buffers for bulk import).
- Repository (DAL): `src/db/api/documents.ts`.
- Model: `src/db/models/documents.ts`.
- Shared used: `db/api/shared/repository.ts` (`removeRecord`, `deleteRecordsByIds`,
`autocompleteByField`), `db/api/file.ts` (`FileDBApi.replaceRelationFiles`), `db/utils.ts`
(`Utils.uuid`, `Utils.ilike`), `shared/constants/pagination.ts` (`resolvePagination`),
`shared/constants/database.ts` (`BULK_IMPORT_TIMESTAMP_STEP_MS`), `shared/csv.ts` (`toCsv`),
`middlewares/upload.ts` (`processFile`), `middlewares/check-permissions.ts`,
`shared/errors/validation.ts` (`ValidationError`).
## API
All routes are mounted under `/api/documents` and require JWT authentication (mounted with
`authenticated` in `src/index.ts`). Every route additionally passes
`checkCrudPermissions('documents')`, which checks the permission `${METHOD}_DOCUMENTS`
(see `permissions.md`).
- `POST /api/documents` -> `201`, the created document DTO. Request body: `{ data: <DocumentInput> }`.
- `POST /api/documents/bulk-import` -> `200` `true`. Multipart file upload (`processFile`); a CSV
buffer is parsed into rows. Missing file raises `ValidationError('importer.errors.invalidFileEmpty')`.
- `PUT /api/documents/:id` -> `200`, the updated document DTO. The controller calls
`Service.update(req.body.data, req.body.id, ...)` (it reads `req.body.id`, not `req.params.id`).
- `DELETE /api/documents/:id` -> `200` `true`.
- `POST /api/documents/deleteByIds` -> `200` `true`. Request body: `{ data: string[] }`.
- `GET /api/documents` -> `200` `{ rows, count }` where `rows` are DTOs. When `?filetype=csv`,
responds with a CSV attachment of fields `id, entity_reference, name, notes, uploaded_at`.
- `GET /api/documents/count` -> `200` `{ rows: [], count }` (count-only).
- `GET /api/documents/autocomplete` -> `200` array of `{ id, label }` matched on `name`.
- `GET /api/documents/:id` -> `200`, a single record (plain) with eager-resolved `organization`,
`campus`, and `file`. This response is NOT passed through `toDocumentDto`.
## Access Rules
Authorization is by CRUD permission only: `checkCrudPermissions('documents')` requires the
effective role (or a custom per-user permission) to hold `${METHOD}_DOCUMENTS`. There is no
additional role-name gate or owner check inside the service. The self-access bypass in
`check-permissions.ts` (matching `req.params.id`/`req.body.id` to the current user id) does not
meaningfully apply to documents since those ids are document ids, not user ids.
## Tenant Scope
- `findAll` scopes to `currentUser.organizationId` only when the user has a loaded
`organizations` association and an `organizationId`. Callers with `globalAccess` (from
`currentUser.app_role.globalAccess`) have the `organizationId` constraint removed, so they read
across organizations.
- On `create`, the document's organization is forced to `currentUser.organizationId`
(`setOrganization`), regardless of input.
- On `update`, organization is only changed when `data.organization` is provided: global-access
users may set the provided organization; non-global users are pinned back to
`currentUser.organizationId`.
- `campus` is set from input on create and update.
## Data Contract
Model columns (`src/db/models/documents.ts`): `id` (UUID PK), `entity_type` (ENUM: `student`,
`staff`, `class`, `invoice`, `organization`, `campus`, `other`), `entity_reference` (text),
`name` (text), `category` (ENUM: `policy`, `report`, `id`, `medical`, `consent`, `invoice`,
`receipt`, `other`), `uploaded_at` (date), `notes` (text), `importHash` (unique), `createdAt`,
`updatedAt`, `deletedAt` (paranoid soft-delete), `campusId`, `organizationId`, `createdById`,
`updatedById`. Associations: `belongsTo organizations` (as `organization`), `belongsTo campuses`
(as `campus`), `hasMany file` (as `file`, scoped relation upload), `belongsTo users` (as
`createdBy`/`updatedBy`).
`toDocumentDto` exposes only: `id`, `entity_type`, `entity_reference`, `name`, `category`,
`uploaded_at`, `notes`, `organizationId`, `campusId`, `createdById`, `updatedById`, `createdAt`,
`updatedAt`. It deliberately omits `importHash`, `deletedAt`, and eager relations.
List filters (`findAll`): `id`, `entity_reference`, `name`, `notes` (all ILIKE),
`uploaded_atRange`, `active`, `entity_type`, `category`, `campus` (filter-only inner join on id
or name, `|`-separated), `organization` (`|`-separated ids), `createdAtRange`, plus `field`/`sort`
ordering (defaults to `createdAt desc`) and `limit`/`page` pagination.
## Behavior / Notes
- All mutations (`create`, `bulkImport`, `update`, `deleteByIds`, `remove`) run inside a manual
Sequelize transaction (`db.sequelize.transaction()`), committing on success and rolling back on
error.
- `update` raises `ValidationError('documentsNotFound')` when the record does not exist.
- Bulk import parses the uploaded CSV (`csv-parser`) into rows, then `bulkCreate`s with
`ignoreDuplicates: true` and per-row `createdAt` staggered by `BULK_IMPORT_TIMESTAMP_STEP_MS`
to preserve ordering. Related files are attached per row via `replaceRelationFiles`.
- The list query selects only scalar columns (no eager org/file load); the `campus` filter join
selects no attributes (filter-only, inner join).
## Tests
None yet (no `documents` unit/e2e test under `src/`).
## Related
- Frontend: `frontend/docs/policies-integration.md` (the handbook/policies workflow reads and
mutates policy documents via `GET /api/documents?category=policy`, `POST`, `PUT`, `DELETE`).
- Backend slices: `permissions.md` (the `${METHOD}_DOCUMENTS` permission gate), and the `file`
upload relation used by `replaceRelationFiles`.