Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

12 — API Mapping (Bulk Module)

Exact endpoints per screen. Wire contract per 00-shared/07: base /api/v1, Bearer JWT, success {success:true,message:"OK",data,meta?, timestamp,requestId}, error envelope with codes. Only shapes in code are used. (planned)/(forward-looking) marked. Sources: bulk.controller.ts, bulk-import.service.ts, import-adapter.interface.ts, students-import.adapter.ts, permissions.constants.ts, event-queue-map.ts, queue.constants.ts.


E1 — Import entity CSV (the module's core endpoint)

EndpointPOST /api/v1/bulk/import/:entity (bulk.controller.ts:35-48)
GuardJwtAuthGuard (bulk.controller.ts:30); RBAC permission guard (planned)user.import exists (permissions.constants.ts:10) but is not enforced on this route
Contentmultipart/form-data, field file (FileInterceptor('file') :38); required — missing → 400 VALIDATION_ERROR CSV file is required (multipart field "file"). (:43-46)
Entity paramfree string; resolved by adapterFor (bulk-import.service.ts:17-20); students only (students-import.adapter.ts:15); anything else → 404 RESOURCE_NOT_FOUND No import adapter for entity "X". (bulk-import.service.ts:19); other entities (planned) (bulk-import.service.ts:15-16, IMPLEMENTATION_PLAN.md:172)
Encodingfile buffer decoded UTF-8 server-side (bulk.controller.ts:47)
Parsecsv-parse/sync {columns:true, skip_empty_lines:true, trim:true} (bulk-import.service.ts:26-30); unparsable → 400 VALIDATION_ERROR Malformed CSV: could not parse file. (:31-33); zero data rows → 400 VALIDATION_ERROR CSV must include a header row and data. (:34-35)
Executionsynchronous per-row validate → create loop (:45-63); rowNumber = physical line (header = 1) (:46); one bad row never aborts the batch (bulk-import.service.spec.ts:53-65)
Response200 envelope; data = ImportReport {entity, totalRows, imported, failed, errors: [{rowNumber, errors[]}]} (import-adapter.interface.ts:14-25); invariant imported + failed === totalRows
Errors400 (missing file, malformed, empty — bulk.controller.ts:43-46, bulk-import.service.ts:31-35); 404 (unknown entity :19); 429 RATE_LIMITED (api tier 100/min, 00-shared/07 §4); 5xx
Row semantics (students)per row: dup checks (admissionNumber via studentRepo.findByAdmissionNumber :49-54; email via usersService.findByEmail :55-56); ref resolution (academicYear by name :113-117, grade by name-or-code :118-121, section by name :122-125, class by grade+section+year :131-142) → validate errors; else usersService.create (:66-71) + studentService.create (:76-84)
Side effectsUserCreated event → in-app queue user-created-notification (users.service.ts:64-76; event-queue-map.ts:10); StudentCreated event → in-app queue student-enrolled (student.service.ts:79-91; event-queue-map.ts:28); student auto-enrolled into class enrollment, status ACTIVE, admissionDate now (student.service.ts:63-78)
Race fallbackif usersService.create throws (duplicate), the adapter reuses the existing user id (students-import.adapter.ts:72-75) — row still imports as a student
Async/pollingnone exists — result returned in the same request; no bulk queue in queue.constants.ts:1-17; async workers with BullMQ progress + rollback (planned) (IMPLEMENTATION_PLAN.md:172); students-specific POST /api/v1/students/bulk-import (planned) (IMPLEMENTATION_PLAN.md:195)
ClientSS1-SS4 wizard; in-flight = indeterminate (10 §6); timeout → partial-upload guidance (06 §4.1a)

E2 — Export entity CSV

EndpointGET /api/v1/bulk/export/:entity (bulk.controller.ts:50-60)
GuardJwtAuthGuard (:30); RBAC (planned)
Responsetext/csv (:52); Content-Disposition: attachment; filename="<entity>.csv" (:55-58); body = csv-stringify of adapter rows with header (bulk-import.service.ts:67-71)
Students rowsadmissionNumber, rollNumber, status, admissionDate (ISO string, '' when unset), sorted by admissionNumber asc (students-import.adapter.ts:87-98)
Emptyheader-only CSV (stringify([],{header:true})) — still 200
Errors404 unknown entity (bulk-import.service.ts:19); 429; 5xx
ClientSS5; roster snapshot ≠ import template (04 §6)
EndpointPurposeSource
POST /api/v1/users/importseparate inline import engine (naive split(','); {imported, errors[]}; no events) — not part of this moduleusers.controller.ts:104-110; users.service.ts:233-282
POST /api/v1/students/bulk-import(planned) async students importIMPLEMENTATION_PLAN.md:195
POST /api/v1/files/upload-csv → worker(planned) queue-based CSV → per-row UserCreatedIMPLEMENTATION_PLAN.md 2.7 (users package 01 §4)
GET /api/v1/studentspost-import verification for SS4 "View students"students module

E4 — Permission & rate-limit posture

ConcernFact
Route guardJwtAuthGuard only (bulk.controller.ts:30)
user.importdefined (permissions.constants.ts:10); not bound to the bulk routes — RBAC enforcement (planned)
student.create / user.createexist (:7,26); same unbound status on these routes
student.importdoes not exist — intended import permission is user.import
Rate limitapi tier 100/min (00-shared/07 §4); one large import consumes a large share — client warns (10 §6)

E5 — Event fan-out (exact)

EventEmitterRoute (event-queue-map.ts)
UserCreatedUsersService.create per imported row (users.service.ts:64-76)in-app / user-created-notification (:10)
StudentCreatedStudentService.create per imported row (student.service.ts:79-91)in-app / student-enrolled (:28)

Contrast: the users module's inline import (users.service.ts:264-272) emits no events; the bulk path emits both per row. Imported users get in-app notifications; there is no email for them (email worker handles only UserRegistered / PasswordResetRequested — users package 01 §4).

Client contract summary

ConcernRule
AuthBearer JWT; 401 → single-flight refresh → replay; fail → session expiry (00-shared/06 §3.6)
Uploadmultipart field file; content-type multipart/form-data; never optimistic
Idempotencyre-upload safe via per-row dedup (students-import.adapter.ts:49-56); no Idempotency-Key needed
Offlinereads from last-good cache + banner; import/export blocked (00-shared/06 §3.5)
Error mapping00-shared/06 §5: 400 file-level, 404 entity, 429 backoff, 5xx generic + requestId; row-level errors live in the 200 body, not the envelope
Realtimenone for bulk; (forward-looking) completion topic when async lands (06 §4.3)