# 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: }`. - `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`.