08 — Form Specifications (Bulk Module)
- 1. CSV column contract (import — students entity)
- 2. Error-string catalogue (exact)
- 3. Export "form" (SS5)
The only "form" in this module is the CSV itself plus the import confirm decision. This file specifies the CSV column contract field by field (from the adapter) and the complete error-string catalogue (exact strings from source). Every string below is quoted from code — the client must display these verbatim.
1. CSV column contract (import — students entity)
Source: students-import.adapter.ts:17-26 (columns array) + :38-144
(validation/resolution). Header names are exact, case-sensitive, no
normalization — the server parses with columns:true
(bulk-import.service.ts:26-30) which keys rows by the literal header.
1.1 Field-by-field
| # | Column | Required | Format / matching | Adapter source |
|---|---|---|---|---|
| 1 | firstName | yes | any non-empty string; → CreateUserDto.firstName (students-import.adapter.ts:66-70; create-user.dto.ts:5-8) | :18 |
| 2 | lastName | yes | any non-empty string; → CreateUserDto.lastName (create-user.dto.ts:15-17) | :19 |
| 3 | email | yes | must match /^[^\s@]+@[^\s@]+\.[^\s@]+$/ (students-import.adapter.ts:11,46-47); must not already be registered (:55-56 via UsersService.findByEmail) | :20 |
| 4 | admissionNumber | yes | any non-empty string; must not already exist (:49-54 via StudentRepository.findByAdmissionNumber) | :21 |
| 5 | grade | yes | matched against existing Grade by name OR code (:118-121, $or) | :22 |
| 6 | section | yes | matched against existing Section by name (:122-125) | :23 |
| 7 | academicYear | yes | matched against existing AcademicYear by name (:113-117) | :24 |
| 8 | rollNumber | no | blank → omitted (` |
1.2 Reference resolution rule (critical)
The adapter never creates academicYear/grade/section/class — they must
exist in the tenant before import. Class lookup is derived:
grade + section (+ academicYear) → classRepo.findOne (:131-142).
"Not found" rows are rejected, not auto-created (03 J2).
1.3 Value handling
- Cell values are trimmed by the parser (
trim:true,bulk-import.service.ts:29) and again in the adapter (:41-44); required check is!valueafter trim → empty string fails. - Row keys (headers) are not trimmed/normalized — a
" firstName "header fails every lookup (06 §2.2; 14 §2). - Extra/unknown columns are ignored (parser keeps them in the row object but
the adapter only reads its
columnskeys).
1.4 Template copy (client-generated (forward-looking))
| Row | Content |
|---|---|
| header | firstName,lastName,email,admissionNumber,grade,section,academicYear,rollNumber |
| example | Amara,Osei,amara.o@school.example,A-2026-001,Grade 7,B,2026-2027,01 |
Downloadable template is client-generated — there is no server template endpoint (04 §6).
2. Error-string catalogue (exact)
2.1 Request-level (file rejected, no rows touched)
| Trigger | Code | Exact string | Source |
|---|---|---|---|
multipart field file missing | 400 VALIDATION_ERROR | CSV file is required (multipart field "file"). | bulk.controller.ts:44-45 |
| CSV unparsable | 400 VALIDATION_ERROR | Malformed CSV: could not parse file. | bulk-import.service.ts:32 |
| zero data rows | 400 VALIDATION_ERROR | CSV must include a header row and data. | bulk-import.service.ts:35 |
| unknown entity | 404 RESOURCE_NOT_FOUND | No import adapter for entity "X". (X = path param) | bulk-import.service.ts:19 |
2.2 Row-level (per ImportRowError.errors[])
| Group | Exact string | Source |
|---|---|---|
| required missing | Missing required column "X". (X = column name) | students-import.adapter.ts:43 |
| format | Invalid email format. | :47 |
| duplicate | Admission number "X" already exists. | :54 |
| duplicate | Email "X" already registered. | :56 |
| reference | Academic year "X" not found. | :117 |
| reference | Grade "X" not found. | :121 |
| reference | Section "X" not found. | :125 |
| reference | No class found for grade "X" section "Y". | :140 |
| create-time | arbitrary message from the thrown Error (err.message) | bulk-import.service.ts:57-62 |
2.3 Client-side (pre-upload, mirroring server strings)
| Condition | Copy | Mirrors |
|---|---|---|
| unparsable file | This file could not be read as CSV. | bulk-import.service.ts:32 |
| header only / empty | CSV must include a header row and data. | :35 |
| missing required header | Missing required column "X". | students-import.adapter.ts:43 |
| whitespace-damaged header | Header " firstName" has leading/trailing space — the server will not match it. | :26-30 (headers unnormalized) |
| non-UTF-8 encoding | File is not UTF-8 — re-encode and retry. | bulk.controller.ts:47 |
3. Export "form" (SS5)
No input form: entity select → download. Columns produced
(students-import.adapter.ts:92-97): admissionNumber, rollNumber,
status, admissionDate (ISO string; '' when unset); rows sorted by
admissionNumber asc (:90-91). Export is a roster snapshot, not an import
template (04 §6).