4.2 KiB
4.2 KiB
Safety Quiz Results Backend
Purpose
safety_quiz_results stores weekly safety/de-escalation quiz submissions per authenticated staff
user. The backend owns tenant scope, user ownership, the user display-name snapshot, the product
role snapshot, and persistence. Each submission is an append (create) — there is no update path.
Slice Files (by layer)
- Route:
src/routes/safety_quiz_results.ts(thin wiring;GET /,POST /). - Controller:
src/api/controllers/safety_quiz_results.controller.ts(custom — not the CRUD factory). - Service (BLL):
src/services/safety_quiz_results.ts. - Repository (DAL): queries run through
db.safety_quiz_resultsinside the service (no separatedb/api/safety_quiz_results.ts). - Model:
src/db/models/safety_quiz_results.ts. - Shared used:
db/with-transaction.ts(withTransaction);shared/constants/pagination.ts(resolvePagination);services/shared/access.ts(getOrganizationIdOrGlobal,getCampusId,assertAuthenticatedTenantUser,hasRoleAccess,getDisplayName);shared/constants/roles.ts(GENERATED_ROLE_TO_PRODUCT_ROLE,PRODUCT_ROLE_VALUES);shared/constants/safety-quiz.ts(SAFETY_QUIZ_REPORT_ROLE_NAMES);shared/errors/validation.ts(ValidationError).
API
All routes require JWT authentication. Base path mounted at /api/safety_quiz_results.
GET /api/safety_quiz_results->200{ rows, count }. Optional queryweek_of, pluslimit/page(paginated viaresolvePagination). Returns results visible to the current user (see Access Rules), ordered bycompleted_atdesc.POST /api/safety_quiz_results->201. Request body wrapped as{ data: <SafetyQuizInput> }. Returns the created result DTO.
Access Rules
- All operations require an authenticated tenant user (
assertAuthenticatedTenantUser). create: a staff user creates a result for themselves; ownership fields are filled from the authenticated user.list: users holding a role inSAFETY_QUIZ_REPORT_ROLE_NAMES(Super Administrator,Administrator,Platform Owner,Tenant Director,Campus Manager) or any role withglobalAccess(viahasRoleAccess) see all org-level results; everyone else sees only their own rows (filtered byuserId).
Tenant Scope
- Organization is resolved via
getOrganizationIdOrGlobal: global access users bypass the org filter and see results across all organizations; regular users are bound to their organization. - On create,
campusIdis set fromgetCampusId;userId,createdById,updatedByIdcome from the current user.
Data Contract
- Mutation input (
SafetyQuizInput):quiz_id,quiz_title,week_of(non-empty strings);scoreandtotal_questions(integers);answers(an array of integers). Invalid input raisesValidationError. - On create the backend fills
user_namefromgetDisplayName(currentUser)anduser_rolefrom the product-role mapping (GENERATED_ROLE_TO_PRODUCT_ROLE, defaulting toPRODUCT_ROLE_VALUES.TEACHER);completed_atis set to the current time. The frontend does not send name, role, or ownership fields. - DTO fields:
id,quiz_id,quiz_title,week_of,score,total_questions,answers,user_name,user_role,completed_at,organizationId,campusId,userId,createdAt,updatedAt. - Model columns:
quiz_id,quiz_title,week_of,user_name,user_role(all TEXT, not null);score,total_questions(INTEGER, not null);answers(JSONB, not null);completed_at(DATE, not null);importHash(unique); plus tenant/audit UUID columns (organizationId,campusId,userId,createdById,updatedById, all nullable). The model isparanoid(soft delete viadeletedAt) and usesfreezeTableName. - Associations:
belongsToorganizations (organization), campuses (campus), users (user,createdBy,updatedBy).
Behavior / Notes
createruns insidewithTransaction; trimmed string fields are persisted.listis paginated with shared defaults (resolvePagination).
Tests
None yet (no safety_quiz_results unit/e2e test in src/).
Related
- Frontend:
frontend/docs/safety-quiz-integration.md. - Related slices:
personality-quiz-results.md,walkthrough-checkins.md,user-progress.md.