CV Field Mapping Fixes & Schema Alignment
Date: 2026-03-31 Scope: Structured CV field mapping across all views (company talent, company candidate, jobseeker CV builder), edit guards, onboarding pre-fill, skill selector UX.
1. Problem Statement
The same user's CV showed different data depending on where it was viewed:
- Talent view (company side) showed data from
user.qualifications(legacy JSON blob) - Candidate view showed data from structured CV tables (
talent.activeCV) - Jobseeker view read from structured CV but several fields were missing in the mapping pipeline
Root causes:
- Structured CV fields were not fully mapped through the frontend pipeline
- TalentDetails bypassed structured CV data entirely for qualifications
cvSkillswas missing from key extraction/normalization functions- Several backend schema fields had no frontend representation
- Some frontend fields (e.g.,
Certificate.location) didn't exist in the backend
2. Data Flow Architecture (After Fix)
Backend Schema (12 structured tables)
│
▼
GraphQL Fragments (cv/fragments.ts)
│ CV_STRUCTURED_FIELDS ── all fields from all 12 tables
│ CV_STRUCTURED_LIST_FIELDS ── profile + jobPreference + skills (list views)
▼
TypeScript Types (structuredCv.ts, structuredCvInputs.ts)
│ StructuredCvFields ── composite of all 12 interfaces
│ Update/Create input types ── mirror backend input types
▼
Mappers
│ structuredCvToProfileForm.ts ── GraphQL → ProfileFormData (for editing)
│ structuredCvMapper.ts ── GraphQL → Qualifications (for display)
│ dataTransformers.ts ── Resolution layer (structured → schema → user fallback)
│ profileFormToInputs.ts ── ProfileFormData → mutation inputs (for saving)
▼
Components
│ ProfileForm.tsx ── Edit form (jobseeker + company add candidate)
│ ResumePreviewPage.tsx ── Read-only display
│ ExperienceCard / EducationCard / CertificateCard ── Inline edit cards
│ SkillsSelector / LanguagesSelectorWrapper ── Skill/language pickers
▼
Mutations (structuredMutations.ts)
27 jobseeker "My" mutations
3 company-scoped skill mutations
Data Source Priority
| View | Priority |
|---|---|
| Talent detail (company) | structured CV (activeCV) → user.qualifications (legacy JSON) |
| Candidate detail (company) | structured CV (talent.activeCV ?? candidate.cv) → user.qualifications |
| Jobseeker edit | structured CV via GET_CV_BY_ID → structuredCvToProfileForm() |
| Jobseeker preview | structured CV → structuredCvToProfileForm() |
3. Field Mapping Matrix
CvProfile → ProfileForm
| Backend Field | Form Field | Mutation Input | Sent? |
|---|---|---|---|
firstName | firstName | firstName | Yes |
middleName | middleName | middleName | Yes |
lastName | lastName | lastName | Yes |
email | email | email | Yes |
phone | phone | phone | Yes |
title | title | title | Yes |
location | location | location | Yes |
linkedinUrl | linkedin | linkedinUrl | Yes |
portfolioUrl | portfolio | portfolioUrl | Yes |
biography | description | biography | Yes |
profileImageUrl | profileImageUrl | profileImageUrl | Yes (stripped of base URL) |
gender | gender | gender | Yes |
dateOfBirth | birthDate | dateOfBirth | Yes (ISO date) |
willingToRelocate | willingToRelocate | willingToRelocate | Yes |
preferredLocation | preferredLocation | preferredLocation | Yes |
addressStreet | addressStreet | addressStreet | Yes |
addressCity | addressCity | addressCity | Yes |
addressState | addressState | addressState | Yes |
addressCountry | addressCountry | addressCountry | Yes |
addressZipCode | addressZipCode | addressZipCode | Yes |
currentRole | currentRole | currentRole | Yes |
industries | industries | industries | Yes (as [] not null — Prisma constraint) |
specializations | specializations | specializations | Yes (as [] not null) |
preferredLanguage | preferredLanguage | preferredLanguage | Yes |
yearsOfExperience | yearsOfExperience | yearsOfExperience | Yes |
fullName | — | — | Computed field, not sent |
CvJobPreference → ProfileForm
| Backend Field | Form Field | Sent? |
|---|---|---|
desiredJobTitles | preferredTitles | Yes |
preferredJobTypes | preferredJobTypes | Yes |
preferredWorkSite | preferredWorkSite | Yes |
availableIn | availableIn | Yes |
expectedSalary | expectedSalary | Yes |
salaryCurrency | salaryCurrency | Yes |
salaryFrequency | salaryFrequency | Yes |
CvWorkExperience → Experience
| Backend Field | Form Field | Sent to Mutation? |
|---|---|---|
jobTitle | title (form) / jobTitle (qualifications) | Yes |
companyName | company | Yes |
startDate | startDate | Yes |
endDate | endDate | Yes |
location | location | Yes |
description | description | Yes |
technologies | technicalSkills | Yes |
isCurrent | isCurrent | Yes |
achievements | achievements | Yes |
duration | duration | Yes |
order | Drag-and-drop position | Via reorder |
CvEducation → Education
| Backend Field | Form Field | Sent to Mutation? |
|---|---|---|
degree | title | Yes |
fieldOfStudy | fieldOfStudy | Yes |
institution | institute | Yes |
graduationYear | endDate (parsed as int) | Yes |
location | location | Yes |
gpa | gpa | Yes |
honors | honors | Yes |
relevantCoursework | relevantCoursework | Yes |
startDate | — | NOT IN BACKEND (see Backend Requirements) |
CvCertification → Certificate
| Backend Field | Form Field | Sent to Mutation? |
|---|---|---|
name | title | Yes |
issuingOrganization | institute | Yes |
issueDate | year | Yes |
expirationDate | expirationDate | Yes |
credentialId | credentialId | Yes |
verificationUrl | verificationUrl | Yes |
status | status | Yes |
Note:
locationwas removed from Certificate — the backendCvCertificationtype has nolocationfield.
CvLanguage → LanguageItem
| Backend Field | Form Field | Sent to Mutation? |
|---|---|---|
language | name | Yes |
proficiency | level (0-3 number) | Yes (converted to string) |
certification | certification | Yes |
CVSkill → SkillRequirementInput
| Backend Field | Form Field | Sent to Mutation? |
|---|---|---|
skill.name | name | Yes (as skillName) |
proficiency | level (0-4 number) | Yes (converted to enum string) |
yearsExperience | yearsExperience | Yes |
Sections with No Form UI
| Section | Fields | Status |
|---|---|---|
| CvProject | 9 fields | Mutations exist, zero form support |
| CvPublication | 8 fields | Mutations exist, zero form support |
| CvAward | 6 fields | Mutations exist, zero form support |
| CvVolunteer | 7 fields | Mutations exist, zero form support |
| CvMetadata | 12 fields | Read-only parsing metadata, no form |
4. Edit Guards (Company Side)
Company users can only edit talent/candidate CVs when:
- Talent
source === "MANUAL_ENTRY"(company added this talent manually) - Talent owner
isOnboarding !== true(user hasn't completed onboarding yet)
Once the CV owner completes onboarding, they own their profile data — the company can no longer edit it.
Implementation
| Component | Guard |
|---|---|
TalentDetails.tsx | canEdit computed from source + isOnboarding |
CandidateDetails.tsx | Same logic, checks resumeMode for talent vs candidate source |
talents/page.tsx | List view edit action checks source + isOnboarding |
| Both detail components | Edit button hidden, inline edit handlers set to undefined |
Why "My" Mutations Fail for Company Users
The backend structured CV mutations (addMyCvEducation, updateMyCvProfile, etc.) check ownership: cv.userId === currentUser.id. Company users don't own the talent's CV, so these mutations return "CV not found".
Only 3 company-scoped mutations exist: addCvSkill, updateCvSkill, removeCvSkill (use TalentAuthorizationService.checkWriteAccessToCV()).
5. Onboarding Pre-fill for Invited Talents
Flow
- Company adds talent with CV → talent record + activeCV created
- Company clicks "Invite to Register" → password reset email sent
- Talent sets password → logs in →
isOnboarding === false,activeCvIdexists - AuthGuard detects
activeCvId→ setsonboardingRole = "jobseeker"→ redirects to/onboarding/jobseeker(skips role selection) - Onboarding starts at Step 2 (Profile) with all fields pre-filled from active CV
- Talent reviews/edits → completes onboarding →
isOnboarding = true
Pre-filled Fields
- Full CV fetched via
GET_CV_BY_IDwithCV_STRUCTURED_FIELDS - Profile: name, title, phone, location, bio, links, image, gender, DOB
- Job preferences: salary, currency, frequency, availability, job types, work site
- Work experiences: all entries
- Education: all entries
- Certifications: all entries
- Languages: all entries
- Skills: all entries from CVSkill join table
Files Changed
components/auth/authGuard.tsx— auto-route whenactiveCvIdexistsapp/onboarding/jobseeker/page.tsx—GET_CV_BY_IDquery + pre-fill effect + start at step 2
6. Skill Selector UX
Main Skills Selector (SkillsSelector.tsx)
Before: Already-selected skills were hidden from dropdown.
After:
- Already-selected skills appear in dropdown as:
"React · Advanced ✓" - Clicking one scrolls to and highlights the skill row (2-second primary color pulse)
- User can then adjust level with +/- buttons on the highlighted row
Experience Technical Skills (ExperienceCard.tsx, AddExperienceCard.tsx)
Before: No filtering — same skill could be added multiple times. No search filtering in dropdown.
After:
- Already-added skills show as
"React ✓"withdisabled: true - Disabled options are greyed out (50% opacity) and unclickable
- Added
searchableprop — typing now filters the dropdown
TWSelect Disabled Option Support (TWSelect.tsx, TWSharedStyles.tsx)
Added disabled?: boolean to the Option interface. Disabled options:
- Render at 50% opacity with
cursor-default - Don't respond to click, hover, or keyboard Enter
- Work in both normal and virtualized (react-window) rendering paths
7. Backend Requirements
Company-Scoped Mutations Needed
For company users to edit talent CVs through the frontend, the backend needs company-scoped variants of all structured CV mutations:
| Mutation Needed | Same As | Access Check |
|---|---|---|
addCvWorkExperience | addMyCvWorkExperience | TalentAuthorizationService.checkWriteAccessToCV() |
updateCvWorkExperience | updateMyCvWorkExperience | Same |
removeCvWorkExperience | removeMyCvWorkExperience | Same |
addCvEducation | addMyCvEducation | Same |
updateCvEducation | updateMyCvEducation | Same |
removeCvEducation | removeMyCvEducation | Same |
addCvCertification | addMyCvCertification | Same |
updateCvCertification | updateMyCvCertification | Same |
removeCvCertification | removeMyCvCertification | Same |
addCvLanguage | addMyCvLanguage | Same |
updateCvLanguage | updateMyCvLanguage | Same |
removeCvLanguage | removeMyCvLanguage | Same |
updateCvProfile | updateMyCvProfile | Same |
updateCvJobPreference | updateMyCvJobPreference | Same |
These follow the exact pattern of existing company-scoped skill mutations (addCvSkill, updateCvSkill, removeCvSkill).
CvEducation Date Fields Needed
| Field Needed | Type | Input Types | Reason |
|---|---|---|---|
startDate | DateTime | CreateCvEducationInput, UpdateCvEducationInput | Frontend has start date picker but can't persist |
endDate | DateTime | Same | Currently uses graduationYear: Int which loses month precision |
8. Files Changed
GraphQL
graphql/operations/schema/cv/fragments.ts— AddedaddressStreet,addressZipCode,preferredLanguageto CvProfile fragmentgraphql/operations/talents/queries.ts— AddedisOnboardingtoGET_TALENTandGET_COMPANY_TALENTSgraphql/operations/candidate/queries.ts— AddedisActive,isOnboardingtoGET_CANDIDATE_DETAILED
Types
lib/types/structuredCv.ts— AddedaddressStreet,addressZipCodeto StructuredCvProfilelib/types/structuredCvInputs.ts— AddedaddressStreet,addressZipCode,preferredLanguageto UpdateCvProfileInputlib/types/cvbuilder.ts— Added all missing fields to Experience, Education, Certificate, ProfileFormDatalib/types/qualifications.ts— Added all missing fields to Experience, Education, Certificatecomponents/reusable/jobs/LanguagesSelectorWrapper.tsx— Addedcertificationto LanguageItemcomponents/ux/TWSharedStyles.tsx— Addeddisabledto Option interface
Mappers
lib/utils/cv/structuredCvToProfileForm.ts— Maps all new fields from structured CV to formlib/utils/cv/profileFormToInputs.ts— Sends all new fields to mutationslib/utils/cv/structuredCvMapper.ts— Includes all new fields in qualifications converterapp/company/resume/candidates/_components/utils/dataTransformers.ts— Fixed cvSkills, jobTitle, removed cert location
Components
app/company/resume/talents/_components/TalentDetails.tsx— Structured CV qualifications, edit guardapp/company/resume/candidates/_components/CandidateDetails.tsx— Edit guardapp/company/resume/talents/page.tsx— List edit guardapp/company/resume/candidates/_components/utils/useInlineResumeEdit.ts— Pass-through new fieldsapp/jobseeker/resume/page.tsx— Removed broken optimistic responseapp/onboarding/jobseeker/page.tsx— Pre-fill from active CV, skip upload stepcomponents/auth/authGuard.tsx— Auto-route invited talentscomponents/reusable/jobs/SkillsSelector.tsx— Selected skill UXcomponents/reusable/qualifications/ExperienceCard.tsx— Skill dedup + searchablecomponents/reusable/qualifications/AddExperinceCard.tsx— Samecomponents/reusable/qualifications/CertificateCard.tsx— Removed locationcomponents/reusable/qualifications/AddCertificationCard.tsx— Removed locationcomponents/cv/ProfileForm.tsx— Removed cert location mappingcomponents/ux/TWSelect.tsx— Disabled option support
Cleanup (removed cert location across)
lib/export/profileFormToExportable.tslib/hooks/useAddCandidate.tslib/utils/cv/cvMapper.tslib/utils/userProfileInputs.tsapp/onboarding/jobseeker/steps/SummaryStep.tsx
Tests
e2e/cv-field-mapping.spec.ts— 16 Playwright tests across 5 suites
9. E2E Test Coverage
Run: npx playwright test e2e/cv-field-mapping.spec.ts
| Suite | Tests | What It Verifies |
|---|---|---|
| Company Talent | 5 | List loads, detail shows profile, resume sections visible, sidebar salary, edit form populated |
| Company Candidate | 3 | List loads, detail shows structured CV, edit form populated |
| Jobseeker CV Builder | 4 | Resume list, preview sections, edit form all fields, save roundtrip |
| Cross-View Consistency | 2 | Same talent shows consistent data in detail vs edit form |
| Field Mapping Audit | 1 | Logs complete Schema→Form→Mutation→Display matrix for all 12 types |