40227-vm/backend/docs/campuses.md
2026-06-12 10:56:13 +02:00

6.0 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), timezone (TEXT, not null — a validated IANA zone, e.g. America/Phoenix; used by the daily zone check-in to compute the campus-local "today" server-side, see zone-checkin.md), 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.

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