Every field of every form, derived 1:1 from create-user.dto.ts,
update-user.dto.ts, update-user-preferences.dto.ts, and
user.schema.ts, plus upload fields and CSV column specs from
users.service.ts:233-282 and students-import.adapter.ts:17-26.
Validation column = server enforcement (class-validator / schema).
# Field Type Required Server validation Default Notes
1 firstNametext ✅ @IsString (create-user.dto.ts:7-8)— Schema required: true (user.schema.ts:16-17)
2 middleNametext — @IsOptional @IsString (:10-13)—
3 lastNametext ✅ @IsString (:15-17)— Schema required (:22-23)
4 displayNametext — @IsOptional @IsString (:19-22)firstName + " " + lastName (users.service.ts:62)Recomputed on update (:136-139)
5 emailemail ✅ @IsEmail (:24-26); schema lowercase+trim (user.schema.ts:28-29)— Unique per tenant (user.schema.ts:83); server 409 pre-check (users.service.ts:50-54)
6 phonetel — @IsOptional @IsString (:28-31); schema trim (:31-32)— Unique per tenant (partial index :84-90); 409 pre-check (users.service.ts:56-60)
7 genderselect — @IsOptional @IsString (:33-38)— Schema enum male/female/other/prefer_not_to_say (user.schema.ts:37-41); client constrains, server is string-typed (mismatch OQ-12)
8 dateOfBirthdate — optional, no decorator beyond Optional (:40-42) — Schema Date (:43-44); max = today
9 statusselect — @IsEnum(UserStatus) (:44-47)activeEnum active/inactive/suspended/invited (user.schema.ts:7-12); invited only via (planned) invite flow
10 languagetext — @IsOptional @IsString (:49-52)enSchema default en (:49-50)
11 timezonetext — @IsOptional @IsString (:54-57)UTCSchema default UTC (:52-53); use searchable AppDropdown
12 metadatajson — @IsOptional object (:59-61)— Schema Object (user.schema.ts:68-69); not exposed as a user field by default (proposed)
Tenant: never sent by client — derived from JWT (00-shared/07 §6 ;
repo injects tenantId — base.repository.ts:32-36).
Success: 201 envelope with user doc; then (if roles chosen) POST /rbac/members {userId, roles} (rbac.controller.ts:63-67).
Errors: 400 field-level; 409 email/phone (message text from
users.service.ts:50-60); 429 backoff; 5xx generic.
# Field Notes (same validation as §1 where present)
1 firstName / middleName / lastNameoptional @IsString (update-user.dto.ts:8-24); renaming recomputes displayName (users.service.ts:136-139)
2 displayNameexplicit override wins over computed (:136-139)
3 email@IsEmail (:26-29); change → uniqueness re-check vs. existing (users.service.ts:120-126)
4 phone@IsString (:30-33); change → uniqueness re-check (:128-134)
5 avatarFileId@IsString (:38-39) — not a user-facing field ; avatar managed via POST /users/:id/avatar
6 gender / dateOfBirth / status / language / timezone / metadatasame as §1 (:41-69); status enum-validated (:52-55)
Partial PATCH: only changed fields sent; $set merge + version increment
(base.repository.ts:57-66).
# Group Field Type Server validation Notes
1 notificationsemailboolean @IsOptional @IsObject group (update-user-preferences.dto.ts:4-12)Schema Boolean (user.schema.ts:56-66)
2 notificationspushboolean same
3 notificationssmsboolean same
4 thememoderadio @IsOptional @IsObject (:14-20)Schema enum light/dark/system (user.schema.ts:61)
5 themelanguagetext same
Contract warning: updatePreferences does $set: { preferences: dto }
(users.service.ts:161-163) — a full replace . Client always sends the
merged complete object; a partial payload wipes the other group (e.g.
sending only {theme} drops notifications).
Read shape: GET /users/:id/preferences → user.preferences ?? {}
(users.service.ts:168-171).
Field Value
Multipart field name file — FileInterceptor('file') (users.controller.ts:96)
Content type multipart/form-data (ApiConsumes :97)
Payload image buffer; filename prefix ${id}- server-side (users.service.ts:219)
Response { avatarFileId } (:230); previous avatar deleted best-effort (:227-229)
Validation none in code (no size/mime check server-side) — client: image types, ≤ 2 MB, square crop (OQ-10)
Storage path sl/{tenantId}/avatars/{uuid} blueprint (04-Modules/Users.md:66)
Endpoint parses rows keyed by lowercased header (users.service.ts:244-247):
Column (accepted headers) Required Server behavior
email✅ missing → Row N: missing email (:256-259); duplicate (existing user or earlier row) → Row N: email "x" already exists (:260-263)
firstname / first_name— fallback Unknown (:265)
lastname / last_name— fallback Unknown (:266)
phone— passed through if present (:268)
gender— passed through (:269)
language— default en (:270)
timezone— default UTC (:271)
Header row required + ≥ 1 data row (:238-243); empty file → errors
contains "CSV must have a header row and at least one data row".
Naive , split — quoted commas unsupported (OQ-7); empty lines skipped
(:237).
Success shape: { imported: number, errors: string[] } (:281).
Template download header (client): email,firstname,lastname,phone,gender, language,timezone.
Parser: csv-parse/sync {columns:true, skip_empty_lines:true, trim:true}
(bulk-import.service.ts:26-30) — supports quoted fields ; malformed →
400 VALIDATION_ERROR (:31-33); no rows → 400 (:34-35).
Report shape: {entity, totalRows, imported, failed, errors:[{rowNumber, errors[]}]}
(import-adapter.interface.ts:14-25); rowNumber is physical line
(index + 2, header = row 1 — bulk-import.service.ts:46).
students adapter columns (students-import.adapter.ts:17-26):
required firstName, lastName, email, admissionNumber, grade, section, academicYear; optional rollNumber. Validation: required presence
(:41-45), email regex (:46-47), admission-number dup (:49-54), email
registered dup (:55-56), ref resolution grade/section/academicYear/class
(:58,100-144).
No users adapter yet — entity='users' → 404 "No import adapter"
(bulk-import.service.ts:17-20) (planned).