40227-vm/backend/docs/organizations.md
2026-06-12 06:55:35 +02:00

5.3 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.tscreateCrudRouter(controller, { permission: 'organizations' }).
  • Controller: src/api/controllers/organizations.controller.tscreateCrudController(service, { csvFields }).
  • Service (BLL): src/services/organizations.tscreateCrudService(DbApi, { notFoundCode: 'organizationsNotFound' }).
  • Repository (DAL): src/db/api/organizations.ts (OrganizationsDBApi) — entity-specific create/bulkImport/update/findBy/findAll; remove/deleteByIds/findAllAutocomplete delegate to db/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 }, 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. Gated by the §3.3 relational policy (assertCanDeleteOrganization in routes/organizations.ts): only super_admin / system_admin / owner may delete a company; a superintendent is blocked even though it holds DELETE_ORGANIZATIONS.
  • POST /deleteByIds — body { data: string[] }, returns true (same delete guard).
  • 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.

Access Rules

  • JWT required; the whole router is guarded by checkCrudPermissions('organizations'), deriving READ_ORGANIZATIONS / CREATE_ORGANIZATIONS / UPDATE_ORGANIZATIONS / DELETE_ORGANIZATIONS per HTTP method.
  • Access is granted by role permission or per-user custom_permissions (see permissions.md).
  • Companies are created by the provisioning flow, not this CRUD: creating an owner user auto-creates the organization (services/users.ts -> OrganizationsDBApi.create).

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.

  • findAll still applies the generic scoping pattern: it sets where.organizationId to currentUser.organizationId, then a globalAccess role deletes that key. Because the organizations table has no organizationId column, a non-global user's query would filter on a non-existent column; only globalAccess users reliably list organizations.
  • create/update do not set or reassign any organization (no setOrganization); they only persist name (plus createdById/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, staff_organization, classes_organization, class_enrollments_organization, class_subjects_organization, timetables_organization, timetable_periods_organization, attendance_sessions_organization, attendance_records_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

  • bulkImport offsets createdAt per row by BULK_IMPORT_TIMESTAMP_STEP_MS to preserve order.
  • List pagination uses the shared resolvePagination defaults (page size 10, capped at 100).
  • Note: OrganizationsFilter accepts an active flag and findAll filters on an active column, but the model has no active column; this filter is currently inert (kept for source accuracy).

Tests

None yet.

  • Generic-CRUD contract: backend-architecture.md; tenant scoping: permissions.md. Every per-organization slice references this table via organizationId (e.g. staff, campuses).