40227-vm/backend/docs/campuses.md
2026-06-10 18:27:19 +02:00

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.tscreateCrudRouter(controller, { permission: 'campuses' }).
  • Controller: src/api/controllers/campuses.controller.tscreateCrudController(service, { csvFields: ['id', 'name', 'code', 'address', 'phone', 'email'] }).
  • Service (BLL): src/services/campuses.tscreateCrudService(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.tsremoveRecord, 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 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

  • 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.

  • 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: students, staff, classes, timetables, attendance_sessions, invoices, messages, documents (all child records keyed on campusId), permissions.md.