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

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

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

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, 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

  • 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. students, guardians, staff, campuses).