4.8 KiB
4.8 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,hasFeaturePermission,getDisplayName);shared/constants/roles.ts(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. If the caller is a parent-scope user acting through a drilled child scope, the request is accepted as a no-op and returnsnull.
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.createpersists only when the active scope is the user's own scope. Parent users drilled into a child school/campus/classroom can complete the quiz there, but the backend does not create reportable quiz rows for that child scope.list: users withREAD_SAFETY_QUIZ_REPORTSsee scope-filtered results; everyone else sees only their own rows (filtered byuserId). Role-seeded permissions are only the baseline grants.custom_permissionscan grant the report permission andcustom_permissions_filtercan remove it for non-global users.
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. - Drilled child scopes are not treated as the user's own scope for personal saves, even though list visibility can use the active scope for reports.
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 user'sapp_role.name(defaulting toteacher);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
createfirst checks whether the caller is acting in their own scope. If not, it skips persistence and returnsnull. Otherwise it runs insidewithTransaction; trimmed string fields are persisted.listis paginated with shared defaults (resolvePagination).
Tests
src/services/personal_scope_results.test.tsverifies that parent users drilled into child scopes do not create safety quiz result rows.
Related
- Frontend:
frontend/docs/safety-quiz-integration.md. - Related slices:
personality-quiz-results.md,walkthrough-checkins.md,user-progress.md.