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

6.9 KiB

Roles Backend

Purpose

roles is the catalog of access roles. It is wired through the shared generic-CRUD router, controller, and service factories, but its repository is custom: a role is not a plain record, it carries a many-to-many link to permissions, and the repository manages that link on every create/update and eager-loads it on reads. Roles are what users reference as their app role, so this slice underpins the permission checks documented in permissions.md and auth-profile.md.

Slice Files (by layer)

  • Route: src/routes/roles.tscreateCrudRouter(controller, { permission: 'roles' }).
  • Controller: src/api/controllers/roles.controller.tscreateCrudController(service, { csvFields: ['id', 'name'] }).
  • Service (BLL): src/services/roles.tscreateCrudService(DbApi, { notFoundCode: 'rolesNotFound' }).
  • Repository (DAL): src/db/api/roles.ts (RolesDBApi) — custom; see Behavior. remove/deleteByIds delegate to db/api/shared/repository.ts; create/update/findBy/findAll/findAllAutocomplete are entity-specific (autocomplete is implemented inline, not via the shared autocompleteByField).
  • Model: src/db/models/roles.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), shared/constants/pagination.ts (resolvePagination), shared/config (config.roles.super_admin), db/utils (uuid, ilike).

API

The standard generic-CRUD surface (all under /api/roles, JWT + ${METHOD}_ROLES 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('roles'), deriving READ_ROLES / CREATE_ROLES / UPDATE_ROLES / DELETE_ROLES per HTTP method.
  • Access is granted by role permission or per-user custom_permissions (see permissions.md).
  • Visibility gate on the super-admin role: in both findAll and findAllAutocomplete, when the caller does not have globalAccess, the where clause is reset to name <> config.roles.super_admin, so non-global callers never see the super-admin role.

Tenant Scope

  • roles has no organizationId column; the catalog is not tenant-scoped. The only access partition is the super-admin visibility gate above (driven by globalAccess, not by organization).

Data Contract

Model columns (paranoid, soft-delete via deletedAt, freezeTableName):

  • id (UUID PK), name (TEXT, nullable), globalAccess (BOOLEAN, not null, default false), importHash (STRING(255), unique, nullable), createdById, updatedById, createdAt / updatedAt / deletedAt.

Associations:

  • belongsToMany permissions as permissions — through table rolesPermissionsPermissions, foreign key roles_permissionsId, constraints: false.
  • belongsToMany permissions as permissions_filter — same through table and foreign key (rolesPermissionsPermissions / roles_permissionsId), used only for filtered list queries.
  • hasMany users as users_app_role (foreign key app_roleId) — the users whose app role is this role.
  • belongsTo users as createdBy / updatedBy.

findBy (backing GET /:id) returns the plain role plus two eager-loaded keys fetched in a single Promise.all: users_app_role (from getUsers_app_role) and permissions (from getPermissions).

List filters (RolesFilter): id, name (ilike), active, globalAccess, permissions (|-separated; matched by permission id or permission name ilike via the permissions_filter include), createdAtRange, plus field/sort ordering and limit/page pagination. Default order is createdAt desc.

Behavior / Notes

  • Role <-> permission linkage is the defining custom behavior. create builds the role row, then calls roles.setPermissions(data.permissions || []) to replace the through-table rows. update only touches the through table when data.permissions !== undefined, calling roles.setPermissions(data.permissions) (a full replace of the role's permission set). findBy reads them back with roles.getPermissions(). So a role's permissions are passed as a flat string[] of permission ids on create/update and returned as the eager permissions array on read — this is the main way this slice differs from a plain CRUD entity, whose repository only manages its own columns.
  • findAll always eager-loads permissions (required: false). When the permissions filter is present it additionally joins permissions_filter (the second alias over the same through table) with an Op.or of permission id Op.in and permission name ilike, and the list uses distinct: true to avoid row multiplication from the join.
  • The super-admin gate (see Access Rules) replaces the accumulated where clause rather than merging into it, so for a non-global caller other filters on the same query are overridden by the name <> super_admin condition (kept here for source accuracy).
  • bulkImport offsets createdAt per row by BULK_IMPORT_TIMESTAMP_STEP_MS to preserve order; it does not set permissions (only create/update do).
  • List pagination uses the shared resolvePagination defaults; findAllAutocomplete orders by name ASC and selects only id/name.
  • Note: RolesFilter accepts an active flag and findAll filters on an active column the roles model does not declare; it is currently inert (kept for source accuracy).
  • Seeded globalAccess roles: The seeder (20200430130760-user-roles.ts) sets globalAccess: true for both Super Administrator and Administrator roles. Users with these roles can access data across all organizations without an organizationId filter. Services use getOrganizationIdOrGlobal and hasGlobalAccess from services/shared/access.ts to check for and honor global access.

Tests

None yet.

  • Generic-CRUD contract: backend-architecture.md.
  • Related slices: permissions.md (the linked entity), users.md (references a role via app_roleId), auth-profile.md (how the signed-in user's role/permissions are resolved).