5.0 KiB
Organizations Backend
Purpose
organizations is the tenant table itself — each row is a tenant org record that every other
per-organization slice references via organizationId. It is a generic-CRUD slice assembled from
the shared factories; the backend is the source of truth for organization records.
Slice Files (by layer)
- Route:
src/routes/organizations.ts—createCrudRouter(controller, { permission: 'organizations' }). - Controller:
src/api/controllers/organizations.controller.ts—createCrudController(service, { csvFields }). - Service (BLL):
src/services/organizations.ts—createCrudService(DbApi, { notFoundCode: 'organizationsNotFound' }). - Repository (DAL):
src/db/api/organizations.ts(OrganizationsDBApi) — entity-specificcreate/bulkImport/update/findBy/findAll;remove/deleteByIds/findAllAutocompletedelegate todb/api/shared/repository.ts. - Model:
src/db/models/organizations.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),shared/constants/database.ts(BULK_IMPORT_TIMESTAMP_STEP_MS),db/utils.ts(Utils).
API
The standard generic-CRUD surface (all under /api/organizations, JWT + ${METHOD}_ORGANIZATIONS
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.
Access Rules
- JWT required; the whole router is guarded by
checkCrudPermissions('organizations'), derivingREAD_ORGANIZATIONS/CREATE_ORGANIZATIONS/UPDATE_ORGANIZATIONS/DELETE_ORGANIZATIONSper HTTP method. - Access is granted by role permission or per-user
custom_permissions(seepermissions.md).
Tenant Scope
This entity is the tenant table, so its tenant scoping is unusual relative to the other
slices. The model has no organizationId column of its own; rows are tenants, identified by id.
findAllstill applies the generic scoping pattern: it setswhere.organizationIdtocurrentUser.organizationId, then aglobalAccessrole deletes that key. Because theorganizationstable has noorganizationIdcolumn, a non-global user's query would filter on a non-existent column; onlyglobalAccessusers reliably list organizations.create/updatedo not set or reassign any organization (nosetOrganization); they only persistname(pluscreatedById/updatedById).
Data Contract
Model columns (paranoid, soft-delete via deletedAt):
id(UUID PK).name— TEXT, nullable.importHash(unique),createdById,updatedById, timestamps.
No ENUM columns.
Associations: belongsTo createdBy/updatedBy (users); hasMany (all keyed by the child's
organizationId): users_organizations, campuses_organization, academic_years_organization,
grades_organization, subjects_organization, students_organization, guardians_organization,
staff_organization, classes_organization, class_enrollments_organization,
class_subjects_organization, timetables_organization, timetable_periods_organization,
attendance_sessions_organization, attendance_records_organization, fee_plans_organization,
invoices_organization, payments_organization, assessments_organization,
assessment_results_organization, messages_organization, message_recipients_organization,
documents_organization. findBy/GET /:id eager-load all of these in a single Promise.all.
List filters (OrganizationsFilter): id, name, createdAtRange, plus field/sort ordering
and limit/page pagination. findAll runs no include, so list rows carry no eager
associations.
Behavior / Notes
bulkImportoffsetscreatedAtper row byBULK_IMPORT_TIMESTAMP_STEP_MSto preserve order.- List pagination uses the shared
resolvePaginationdefaults (page size 10, capped at 100). - Note:
OrganizationsFilteraccepts anactiveflag andfindAllfilters on anactivecolumn, but the model has noactivecolumn; this filter is currently inert (kept for source accuracy).
Tests
None yet.
Related
- Generic-CRUD contract:
backend-architecture.md; tenant scoping:permissions.md. Every per-organization slice references this table viaorganizationId(e.g.students,guardians,staff,campuses).