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.ts—createCrudRouter(controller, { permission: 'roles' }). - Controller:
src/api/controllers/roles.controller.ts—createCrudController(service, { csvFields: ['id', 'name'] }). - Service (BLL):
src/services/roles.ts—createCrudService(DbApi, { notFoundCode: 'rolesNotFound' }). - Repository (DAL):
src/db/api/roles.ts(RolesDBApi) — custom; see Behavior.remove/deleteByIdsdelegate todb/api/shared/repository.ts;create/update/findBy/findAll/findAllAutocompleteare entity-specific (autocomplete is implemented inline, not via the sharedautocompleteByField). - 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.ts—removeRecord,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 }, 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('roles'), derivingREAD_ROLES/CREATE_ROLES/UPDATE_ROLES/DELETE_ROLESper HTTP method. - Access is granted by role permission or per-user
custom_permissions(seepermissions.md). - Visibility gate on the super-admin role: in both
findAllandfindAllAutocomplete, when the caller does not haveglobalAccess, the where clause is reset toname <> config.roles.super_admin, so non-global callers never see the super-admin role.
Tenant Scope
roleshas noorganizationIdcolumn; the catalog is not tenant-scoped. The only access partition is the super-admin visibility gate above (driven byglobalAccess, not by organization).
Data Contract
Model columns (paranoid, soft-delete via deletedAt, freezeTableName):
id(UUID PK),name(TEXT, nullable),globalAccess(BOOLEAN, not null, defaultfalse),importHash(STRING(255), unique, nullable),createdById,updatedById,createdAt/updatedAt/deletedAt.
Associations:
belongsToManypermissionsaspermissions— through tablerolesPermissionsPermissions, foreign keyroles_permissionsId,constraints: false.belongsToManypermissionsaspermissions_filter— same through table and foreign key (rolesPermissionsPermissions/roles_permissionsId), used only for filtered list queries.hasManyusersasusers_app_role(foreign keyapp_roleId) — the users whose app role is this role.belongsTousersascreatedBy/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.
createbuilds the role row, then callsroles.setPermissions(data.permissions || [])to replace the through-table rows.updateonly touches the through table whendata.permissions !== undefined, callingroles.setPermissions(data.permissions)(a full replace of the role's permission set).findByreads them back withroles.getPermissions(). So a role's permissions are passed as a flatstring[]of permission ids on create/update and returned as the eagerpermissionsarray on read — this is the main way this slice differs from a plain CRUD entity, whose repository only manages its own columns. findAllalways eager-loadspermissions(required: false). When thepermissionsfilter is present it additionally joinspermissions_filter(the second alias over the same through table) with anOp.orof permission idOp.inand permission name ilike, and the list usesdistinct: trueto 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_admincondition (kept here for source accuracy). bulkImportoffsetscreatedAtper row byBULK_IMPORT_TIMESTAMP_STEP_MSto preserve order; it does not set permissions (onlycreate/updatedo).- List pagination uses the shared
resolvePaginationdefaults;findAllAutocompleteorders byname ASCand selects onlyid/name. - Note:
RolesFilteraccepts anactiveflag andfindAllfilters on anactivecolumn therolesmodel does not declare; it is currently inert (kept for source accuracy). - Seeded globalAccess roles: The seeder (
20200430130760-user-roles.ts) setsglobalAccess: truefor bothSuper AdministratorandAdministratorroles. Users with these roles can access data across all organizations without anorganizationIdfilter. Services usegetOrganizationIdOrGlobalandhasGlobalAccessfromservices/shared/access.tsto check for and honor global access.
Tests
None yet.
Related
- Generic-CRUD contract:
backend-architecture.md. - Related slices:
permissions.md(the linked entity),users.md(references a role viaapp_roleId),auth-profile.md(how the signed-in user's role/permissions are resolved).