Skip to content

Data Models

_david edited this page Aug 21, 2026 · 1 revision

Data Models

All models use Mongoose with timestamps: true. Every CV section document carries a candidateId foreign key back to Candidate.

Model Key fields
Candidate email, password (bcrypt hash, never returned by the API), firstName, lastName, gender, marital, birthday, address, phone, introduction ({vi,en}), socialMedia
generalInformation candidateId, positionDesired, career ({vi,en}), levelCurrent, levelDesired, salaryDesired, education, yearsOfExperience, workLocation, workForm, careerGoal ({vi,en}), personalSkills[], professionalSkills[], foreignLanguages[]
Experience candidateId, company, position, startDate, endDate, isCurrent, description ({vi,en}), skills[]
Education candidateId, school, major, startDate, endDate, isCurrent, description ({vi,en})
Project candidateId, name, position, description ({vi,en}), technology[], startDate, endDate, isWorking, images[], link
Certificate candidateId, name, organization, startDate, endDate, isNoExpiration, link, images[], description ({vi,en})
Award candidateId, name, organization, issueDate, link, images[], description ({vi,en})
Reference candidateId, fullName, phone, company, position

Reusable sub-schemas (models/part/index.ts): foreignLanguageSchema, professionalSkillsSchema, personalSkills, socialMediaSchema, localizedTextSchema.

Multi-language resume content (added 2026-08-21)

Free-text fields that a candidate actually writes prose into are stored per-language:

{ vi: string, en: string }

Localized: Candidate.introduction, description on Education/Experience/Award/Certificate/Project, generalInformation.career / careerGoal.

Not localized (stays a plain string) — proper nouns and short labels: school, company, major, position, positionDesired, levelCurrent, levelDesired, education (degree level), workLocation, workForm. (positionDesired in particular shares its Joi validator with Experience.position — a job title, not a description — which is why it was deliberately excluded from localization.)

Reading vs. writing

  • Authenticated CRUD (GET/POST/PUT /education, etc.) always returns/accepts the full { vi, en } object — the owner edits both languages directly.
  • Public-facing reads (GET /api/me/:email, GET /download-pdf) resolve each localized field down to a single string based on ?lang=vi|en (default vi), falling back to whichever language actually has content if the requested one is empty.

Migrating existing data

npm run migrate:localize-text (src/scripts/migrate-localize-text-fields.ts) wraps any remaining plain-string values into { vi: <existing value>, en: '' }. It's a MongoDB aggregation-pipeline update matching only documents where the field is currently a string, so it's idempotent — safe to run repeatedly, a no-op once everything is migrated. This must run against the database before deploying a version of the app whose schema expects the {vi,en} shape — old plain-string documents won't read/cast correctly under the new schema otherwise.

Ownership model

Every write (create/update/delete) on a CV section is scoped to req.user._id (the authenticated candidate), never a client-supplied id — see Security for the incident history behind why this is emphasized here.

Clone this wiki locally