40227-vm/backend/docs/campuses.md
2026-06-12 06:55:35 +02:00

107 lines
5.8 KiB
Markdown

# Campuses Backend
## Purpose
`campuses` is the per-organization catalog of school campuses (branding tokens, contact details,
online/active flags). This doc covers the **authenticated CRUD surface** at `/api/campuses`. The
public read-only surface at `GET /api/public/campuses` is a separate slice documented in
`campus-catalog.md` — it is not re-documented here. Note that `src/db/api/campuses.ts` is the
repository for **this** authenticated slice; the public catalog slice queries `db.campuses` directly
and does not go through this repository.
## Slice Files (by layer)
- Route: `src/routes/campuses.ts``createCrudRouter(controller, { permission: 'campuses' })`.
- Controller: `src/api/controllers/campuses.controller.ts`
`createCrudController(service, { csvFields: ['id', 'name', 'code', 'address', 'phone', 'email'] })`.
- Service (BLL): `src/services/campuses.ts`
`createCrudService(DbApi, { notFoundCode: 'campusesNotFound' })`.
- Repository (DAL): `src/db/api/campuses.ts` (`CampusesDBApi`) — entity-specific
`create`/`bulkImport`/`update`/`findBy`/`findAll`; `remove`/`deleteByIds`/`findAllAutocomplete`
delegate to `db/api/shared/repository.ts` (autocomplete via `autocompleteByField`).
- Model: `src/db/models/campuses.ts`.
- Shared used: CRUD factories (`services/shared/crud-service.ts`,
`api/controllers/shared/crud-controller.ts`, `api/http/crud-router.ts`), repository helpers
(`db/api/shared/repository.ts``removeRecord`, `deleteRecordsByIds`, `autocompleteByField`),
`shared/constants/pagination.ts` (`resolvePagination`), `shared/errors/validation`
(`ValidationError`), `db/utils` (`uuid`, `ilike`).
## API
The standard generic-CRUD surface (all under `/api/campuses`, JWT + `${METHOD}_CAMPUSES` permission,
all `200`) — see `backend-architecture.md` for the shared contract:
- `POST /` — body `{ data }`, returns `true`.
- `POST /bulk-import` — multipart CSV file, returns `true`.
- `PUT /:id` — body `{ data, id }` (the service reads the id from the **body**, not the path param),
returns `true`.
- `DELETE /:id` — returns `true`.
- `POST /deleteByIds` — body `{ data: string[] }`, returns `true`.
- `GET /` — query filters, returns `{ rows, count }`; `?filetype=csv` streams a CSV of `csvFields`.
- `GET /count` — returns `{ rows: [], count }`.
- `GET /autocomplete``?query&limit&offset`, returns `[{ id, label }]` where `label` is `name`.
- `GET /:id` — returns the record with eager associations (see Data Contract).
`csvFields`: `id`, `name`, `code`, `address`, `phone`, `email`.
## Access Rules
- JWT required; the whole router is guarded by `checkCrudPermissions('campuses')`, deriving
`READ_CAMPUSES` / `CREATE_CAMPUSES` / `UPDATE_CAMPUSES` / `DELETE_CAMPUSES` per HTTP method.
- Access is granted by role permission or per-user `custom_permissions` (see `permissions.md`).
- The public `GET /api/public/campuses` surface has no JWT and no permission check — see
`campus-catalog.md`.
## Tenant Scope
- `findAll` scopes `where.organizationId` to `currentUser.organizationId` (only when the caller has
both `currentUser.organizations.id` and `currentUser.organizationId`); a `globalAccess` caller has
the `organizationId` filter deleted, so it sees all tenants.
- `create` assigns the organization from `currentUser.organizationId` via `setOrganization`.
- `update` only reassigns the organization when `data.organization` is provided: a `globalAccess`
caller may set it to the supplied value; otherwise it is forced back to `currentUser.organizationId`.
## Data Contract
Model columns (`paranoid`, soft-delete via `deletedAt`, `freezeTableName`):
- `id` (UUID PK), `name` (TEXT, not null), `code` (TEXT, not null), `address`, `phone`, `email`,
`mascot`, `color`, `bgGradient`, `borderColor`, `textColor`, `bgLight`, `description` (all TEXT,
nullable), `isOnline` (BOOLEAN, not null, default `false`), `active` (BOOLEAN, not null, default
`false`), `importHash` (STRING(255), unique, nullable), `organizationId` (UUID, nullable),
`createdById`, `updatedById`, `createdAt` / `updatedAt` / `deletedAt`.
Associations: `belongsTo` organization (`organization`, fk `organizationId`), createdBy/updatedBy
(users); `hasMany` `staff_campus`, `classes_campus`, `timetables_campus`,
`attendance_sessions_campus`, `messages_campus`, `documents_campus` (all keyed on
`campusId`, `constraints: false`).
`findBy` (backing `GET /:id`) returns the plain campus plus all eight `hasMany` collections and the
`organization`, fetched in a single `Promise.all`. `findAll` eager-loads only `organization`.
List filters (`CampusesFilter`): `id`, `name`, `code`, `address`, `phone`, `email` (all ilike),
`active`, `organization` (`|`-separated org ids, matched on `organizationId`), `createdAtRange`, plus
`field`/`sort` ordering and `limit`/`page` pagination. Default order is `createdAt desc`.
## Behavior / Notes
- `create` and `bulkImport` both throw `ValidationError` when `name` or `code` is missing (`name` and
`code` are not-null columns); `bulkImport` validates per row.
- `create`/`update` manage the organization link via `setOrganization` rather than writing
`organizationId` directly; `update` applies only the fields present in the body (each guarded by
`!== undefined`).
- `bulkImport` offsets `createdAt` per row by `BULK_IMPORT_TIMESTAMP_STEP_MS` to preserve order.
- List pagination uses the shared `resolvePagination` defaults; the query uses `distinct: true`
alongside the `organization` include.
## Tests
None yet.
## Related
- Public read surface: `campus-catalog.md` (`GET /api/public/campuses`, shares the
`src/db/models/campuses.ts` model but not the `src/db/api/campuses.ts` repository).
- Generic-CRUD contract: `backend-architecture.md`.
- Related slices: `staff`, `classes`, `timetables`, `attendance_sessions`, `messages` (all child records keyed on `campusId`), `permissions.md`.