7.9 KiB
7.9 KiB
Communications Backend
Purpose
The communications area exposes two product-focused flows instead of generic CRUD:
- Parent/guardian direct messages through
direct_messages. - Internal alert events through
communication_events.
Slice Files (by layer)
- Route:
src/routes/communications.ts(thin wiring;GET /events,POST /events). Mounted at/api/communicationsbehind theauthenticatedmiddleware insrc/index.ts. - Controller:
src/api/controllers/communications.controller.ts(custom —listEvents,createEvent). - Service (BLL):
src/services/communications.ts(+src/services/communications.types.ts). Contains validation, scope resolution, and DTO mappers. - Repository (DAL): queries run through
db.communication_eventsinside the service (no separatedb/apifile). - Models:
src/db/models/communication_events.ts. - Shared used:
services/shared/access.ts,shared/constants/communications.ts(event-type values and manager role list),shared/constants/roles.ts(ROLE_NAMES),shared/constants/pagination.ts(resolvePagination),shared/errors/forbidden.ts,shared/errors/validation.ts.
Direct messages:
- Route:
src/routes/direct_messages.ts, mounted at/api/direct_messages, authenticated and gated byREAD_PARENT_COMM. - Controller:
src/api/controllers/direct_messages.controller.ts. - Service:
src/services/direct_messages.ts. - Model:
src/db/models/direct_messages.ts.
API
All routes require JWT authentication.
GET /api/communications/events->200{ rows, count }. Optional querytype; supportslimit/page.POST /api/communications/events->201the created event DTO. Body isreq.body.data.PATCH /api/communications/events/:id->200the updated event DTO. Body isreq.body.data.DELETE /api/communications/events/:id->204. Soft-deletes a wrongly-created alert without creating a user-facing notification.POST /api/communications/events/:id/cancel->201the cancellation notification DTO. Body isreq.body.datawith optionalreason.GET /api/direct_messages/contacts->200{ rows }, contacts available through a shared student.GET /api/direct_messages/conversations->200{ rows }, one row perotherUserId + studentId.GET /api/direct_messages/thread/:otherUserId?studentId=:studentId->200the isolated thread for that staff/guardian/student context; marks incoming messages in that context as read.POST /api/direct_messages/send->200the created message. Body isreq.body.datawithrecipientId,body, andstudentId.
Event DTO fields: id, title, date (from event_date), type (from event_type), targetLevel, roles, organizationId, schoolId, campusId, classId, canceledEventId, createdById, updatedById, createdAt, updatedAt.
Direct-message contact/conversation rows include conversationKey, userId, name, role, studentId, and studentName. conversationKey is derived from userId + studentId; it is a client key, not a stored column.
Access Rules
- All endpoints require an authenticated user. Tenant-scoped alerts require a tenant context; platform-scope alerts are allowed for global-access managers.
POST /eventsadditionally requiresMANAGE_INTERNAL_COMM.custom_permissionscan grant it andcustom_permissions_filtercan remove it for non-global users.PATCH /events/:id,DELETE /events/:id, andPOST /events/:id/cancelare allowed for the original creator or a manager withMANAGE_INTERNAL_COMMin the alert's scope. Global admins can mutate tenant alerts from platform root, but they do not see other users' tenant alerts in the root list; they see them through tenant drill-down.- Alert create accepts exact targets:
system,all,organization,school, andcampus. Class-level create/targeting is intentionally not supported; class-scope users read campus-level alerts. - Global root managers can create
system,all, and selected organization/school/campus alerts. Organization managers can target their own organization, schools, or campuses. School managers can target their own school or campuses. Campus managers can target their own campus only. - Direct messages require
READ_PARENT_COMM. The granted audience isoffice_manager,teacher, andguardian. - Direct-message access is membership-based and student-context based:
- Guardians can find the teacher and office manager connected to each linked student.
- Teachers can find guardians for students in their class.
- Office managers can find guardians for students on their campus.
- Threads and sends are allowed only when the requested
studentIdmatches one of those contact rows.
- The frontend does not send organization, campus, creator, or updater fields. The backend fills them from the authenticated user.
Tenant Scope
- Internal alerts are stored with an exact target.
targetLevel = systemwith a null tenant chain is visible only in the platform/system scope.targetLevel = allwith a null tenant chain is a platform-wide broadcast. - List visibility includes the current scope and descendant targets. Platform-root users see platform alerts plus tenant-target alerts they created. When they drill into a tenant, they see that tenant scope like a scoped viewer. Organization users see their organization alerts plus school/campus alerts inside that organization. School users see their school alerts plus campus alerts inside that school. Campus/class users see their campus alerts only.
- Parent alerts do not automatically propagate down: an organization alert is not a campus alert unless the sender also targets that campus. Class-scope users read campus-level alerts because class content is campus-level.
- Selecting multiple tenant audiences creates multiple
communication_eventsrows with the same title/date/type and different exact target stamps. - Direct messages are not tenant-broadcast records. Contacts and threads are resolved through the current user's student links, class, or campus.
Data Contract
- Event input (
EventInput):title(required),date(required, stored toevent_date),type(required; must be one ofmeeting,drill,event,deadline),targets(optional array of{ level, id }; omitted targets default to the creator's exact scope),roles(legacy metadata; it is not used for visibility). communication_eventsmodel:title(text),event_date(DATEONLY),event_type(ENUM of the four types),targetLevel(text:system,all,organization,school,campus),roles(JSONB, default[]), nullableorganizationId, nullableschoolId, nullablecampusId, nullableclassId, nullablecanceledEventId,createdById(not null), nullableupdatedById,paranoidsoft deletes,belongsToassociations toorganizations,campuses,users(createdBy, updatedBy).- List pagination: both lists use
resolvePagination(limit, page).
Behavior / Notes
- Direct-message conversations are separated by student. The same guardian and teacher can have separate threads for two different students because reads/writes filter by
sender/recipient pair + studentId. - Event list orders by
event_date asc, thencreatedAt desc.createEventcreates one row per exact target.cancelEventcreates a new cancellation notification with the same target stamp and soft-deletes the original alert;deleteEventonly soft-deletes the original alert. - Validation failures throw
ValidationError; access failures throwForbiddenError.
Tests
src/services/direct_messages.test.tscovers contact discovery through linked students, guardian/staff contact visibility, student-separated conversations, ambiguous same-counterpart thread rejection, and persistedstudentIdcontext when sending.
Related
- Frontend:
frontend/docs/communications-integration.md. - Related backend slice: direct messages (
src/services/direct_messages.ts) backs the guardian/staff Messages UI.