107 lines
5.8 KiB
Markdown
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`.
|