5.9 KiB
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-specificcreate/bulkImport/update/findBy/findAll;remove/deleteByIds/findAllAutocompletedelegate todb/api/shared/repository.ts(autocomplete viaautocompleteByField). - 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 }, returnstrue.POST /bulk-import— multipart CSV file, returnstrue.PUT /:id— body{ data, id }(the service reads the id from the body, not the path param), returnstrue.DELETE /:id— returnstrue.POST /deleteByIds— body{ data: string[] }, returnstrue.GET /— query filters, returns{ rows, count };?filetype=csvstreams a CSV ofcsvFields.GET /count— returns{ rows: [], count }.GET /autocomplete—?query&limit&offset, returns[{ id, label }]wherelabelisname.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'), derivingREAD_CAMPUSES/CREATE_CAMPUSES/UPDATE_CAMPUSES/DELETE_CAMPUSESper HTTP method. - Access is granted by role permission or per-user
custom_permissions(seepermissions.md). - The public
GET /api/public/campusessurface has no JWT and no permission check — seecampus-catalog.md.
Tenant Scope
findAllscopeswhere.organizationIdtocurrentUser.organizationId(only when the caller has bothcurrentUser.organizations.idandcurrentUser.organizationId); aglobalAccesscaller has theorganizationIdfilter deleted, so it sees all tenants.createassigns the organization fromcurrentUser.organizationIdviasetOrganization.updateonly reassigns the organization whendata.organizationis provided: aglobalAccesscaller may set it to the supplied value; otherwise it is forced back tocurrentUser.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, defaultfalse),active(BOOLEAN, not null, defaultfalse),importHash(STRING(255), unique, nullable),organizationId(UUID, nullable),createdById,updatedById,createdAt/updatedAt/deletedAt.
Associations: belongsTo organization (organization, fk organizationId), createdBy/updatedBy
(users); hasMany students_campus, staff_campus, classes_campus, timetables_campus,
attendance_sessions_campus, invoices_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
createandbulkImportboth throwValidationErrorwhennameorcodeis missing (nameandcodeare not-null columns);bulkImportvalidates per row.create/updatemanage the organization link viasetOrganizationrather than writingorganizationIddirectly;updateapplies only the fields present in the body (each guarded by!== undefined).bulkImportoffsetscreatedAtper row byBULK_IMPORT_TIMESTAMP_STEP_MSto preserve order.- List pagination uses the shared
resolvePaginationdefaults; the query usesdistinct: truealongside theorganizationinclude.
Tests
None yet.
Related
- Public read surface:
campus-catalog.md(GET /api/public/campuses, shares thesrc/db/models/campuses.tsmodel but not thesrc/db/api/campuses.tsrepository). - Generic-CRUD contract:
backend-architecture.md. - Related slices:
students,staff,classes,timetables,attendance_sessions,invoices,messages,documents(all child records keyed oncampusId),permissions.md.