-
Notifications
You must be signed in to change notification settings - Fork 0
Data Models
All models use Mongoose with timestamps: true. Every CV section, Application, and Profile document carries a candidateId foreign key back to Candidate, plus a nullable deletedAt: Number (soft-delete, null = not deleted — see API Reference's restore endpoints). Visit has neither.
| Model | Key fields |
|---|---|
Candidate |
email, password (bcrypt hash, never returned by the API), firstName, lastName, gender, marital, birthday, address, phone, introduction ({vi,en}), socialMedia, slug (vanity public-profile URL, unique/sparse), cvFile ({ originalName, uploadedAt } metadata for the uploaded PDF résumé), isPublic (default true — gates whether GET /api/me/:slug-or-email returns anything at all), emailVerified (default false, informational only) |
generalInformation |
candidateId, positionDesired, career ({vi,en}), levelCurrent, levelDesired, salaryDesired, education, yearsOfExperience, workLocation, workForm, openToWork, careerGoal ({vi,en}), personalSkills[], professionalSkills[], professionalSkillsGroup[], 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
|
Application |
candidateId, company, position, appliedDate, status (applied|interview|offer|rejected, default applied), note, jobLink — private job-application tracker, never exposed via the public profile |
Profile |
candidateId, name, educationIds[], experienceIds[], projectIds[], certificateIds[], awardIds[], referenceIds[] — a named subset used to filter what a public share link shows |
Visit |
candidateId, ip, location — one document per public-profile visit (analytics only) |
Reusable sub-schemas (models/part/index.ts): foreignLanguageSchema, professionalSkillsSchema, personalSkills, socialMediaSchema, localizedTextSchema.
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.
-
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/:slug-or-email,GET /download-pdf) resolve each localized field down to a single string based on?lang=vi|en(defaultvi), falling back to whichever language actually has content if the requested one is empty.
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.
baseDeleteDocument (services/index.ts) sets deletedAt = Date.now() instead of actually removing the document. Every list/find path excludes deletedAt-set documents by default, so this is invisible to normal reads. baseRestoreDocument clears it back to null. baseUpdateDocument/basePatchDocument also refuse to touch an already-soft-deleted document (issue #136 fix — see Security); baseDeleteDocument/baseRestoreDocument themselves don't apply that exclusion, since restore specifically needs to find an already-deleted document. Account-level self-delete (DELETE /api/v1/candidate) is the one exception — it still hard-deletes and cascades across every collection.
Every write (create/update/delete/restore) on a CV section, application, or profile 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.