4.5 KiB
Timetables Backend
Purpose
timetables is a named, dated schedule (a campus/academic-year timetable with a draft/active/
archived status) that owns a set of timetable_periods. It is a generic-CRUD slice assembled
from the shared factories.
Slice Files (by layer)
- Route:
src/routes/timetables.ts—createCrudRouter(controller, { permission: 'timetables' }). - Controller:
src/api/controllers/timetables.controller.ts—createCrudController(service, { csvFields }). - Service (BLL):
src/services/timetables.ts—createCrudService(DbApi, { notFoundCode: 'timetablesNotFound' }). - Repository (DAL):
src/db/api/timetables.ts(TimetablesDBApi) — entity-specificcreate/bulkImport/update/findBy/findAll;remove/deleteByIds/findAllAutocompletedelegate todb/api/shared/repository.ts. - Model:
src/db/models/timetables.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),shared/constants/pagination.ts(resolvePagination),db/utils.ts(Utils.uuid/Utils.ilike),shared/constants/database.ts(BULK_IMPORT_TIMESTAMP_STEP_MS).
API
The standard generic-CRUD surface (all under /api/timetables, JWT +
${METHOD}_TIMETABLES permission, all 200) — see backend-architecture.md for the shared
9-endpoint 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), 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, effective_from, effective_to.
Access Rules
- JWT required; the whole router is guarded by
checkCrudPermissions('timetables'), derivingREAD_TIMETABLES/CREATE_TIMETABLES/UPDATE_TIMETABLES/DELETE_TIMETABLESper HTTP method. - Access is granted by role permission or per-user
custom_permissions(seepermissions.md).
Tenant Scope
findAllscopeswhere.organizationIdtocurrentUser.organizationId; aglobalAccessrole clears the org filter (sees all tenants).createassigns the organization fromcurrentUser.organizationId;updateonly reassigns organization forglobalAccessusers (otherwise it stays the caller's org).
Data Contract
Model columns (paranoid, soft-delete via deletedAt):
id(UUID PK).name— TEXT.effective_from,effective_to— DATE.status— ENUMdraft|active|archived.importHash(STRING(255), unique),academic_yearId,campusId,organizationId,createdById,updatedById, timestamps (all UUID FKs nullable).
Associations: belongsTo organization, campus, academic_year (academic_years),
createdBy/updatedBy (users); hasMany timetable_periods as timetable_periods_timetable.
findBy/GET /:id eager-load timetable_periods_timetable, organization, campus, and
academic_year in a single Promise.all.
List filters (TimetablesFilter): id, name (iLike), effective_fromRange,
effective_toRange, status, campus (id or name, |-separated), academic_year (id or
name), organization, createdAtRange, plus a calendar overlap pair calendarStart +
calendarEnd (matches rows whose effective_from or effective_to falls between the two),
and field/sort ordering and limit/page pagination.
Behavior / Notes
create/bulkImport/updateset associations via the Sequelizeset*mixins (no file relations on this entity).bulkImportoffsetscreatedAtper row byBULK_IMPORT_TIMESTAMP_STEP_MSto preserve order.- List pagination uses the shared
resolvePaginationdefaults (page size 10, capped at 100). - Note:
TimetablesFilteraccepts anactiveflag the model has no column for; it is applied towhere.activebut, with no such column, is currently inert (kept for source accuracy).
Tests
None yet.
Related
- Generic-CRUD contract:
backend-architecture.md; related slices:timetable_periods,academic_years,campuses,permissions.md.