Skip to main content

Structured CV Migration Documentation

Migration Complete. The legacy ParsedCV, CVField, CVFieldValue, and CVGroupFields models have been removed from the Prisma schema and the backend. The parsedCV field no longer exists on CVType. The 11 structured CV tables (+ CVSkill join table) are the sole source of truth. This document is preserved as a historical reference and ongoing implementation guide.

Overview

This document covers the migration from the legacy schemaData JSON blob approach to typed structured GraphQL fields and mutations for CV data management. The migration affects both the read path (how CV data is fetched and displayed) and the write path (how CV edits are saved).


Background

Legacy Approach (schemaData)

Previously, all CV data was stored as a single JSON blob called schemaData inside ParsedCV. The frontend would:

  1. Read: Query parsedCV.schemaData, parse the JSON, and extract fields like personalInfo, experience, education, skills, meta, etc.
  2. Write: Build a BackendCvSchema object from form data via profileToSchema(), then send it as a JSON blob via the UPDATE_CV_SCHEMA mutation.

New Approach (Structured Fields)

The backend now exposes 11 normalized GraphQL field resolvers on CVType, each backed by its own database table:

FieldTypeDescription
profile1:1Personal info (name, email, phone, title, location, etc.)
jobPreference1:1Job preferences (desired titles, salary, work site, availability)
workExperiencesArrayWork experience entries
educationsArrayEducation entries
certificationsArrayCertification entries
languagesArrayLanguage entries
projectsArrayProject entries
publicationsArrayPublication entries
awardsArrayAward entries
volunteersArrayVolunteer entries
cvMetadata1:1Parsing quality info (confidence, extraction method, etc.)
cvSkillsArrayCV-skill join table (separate from structured tables)

The backend also exposes 30 typed write mutations (jobseeker-only) for granular CV updates, including 3 skill mutations (addMyCvSkill, updateMyCvSkill, removeMyCvSkill).


Architecture

File Structure

graphql/operations/
schema/cv/fragments.ts # GraphQL fragments for structured fields
cv/structuredMutations.ts # 30 typed mutation definitions (27 jobseeker + 3 company-scoped skill)
cv/mutations.ts # CREATE_MY_CV, SET_MY_ACTIVE_CV

lib/types/
structuredCv.ts # TypeScript interfaces for structured CV types
structuredCvInputs.ts # Input types for mutation variables
cvbuilder.ts # Form types (Experience, Education, etc.) with _structuredId
cv.ts # Legacy types + TMyCvsQuery, TSetMyActiveCvData (with StructuredCvFields)

lib/utils/cv/
structuredCvToProfileForm.ts # Structured fields -> ProfileFormData (read)
profileFormToInputs.ts # ProfileFormData -> mutation inputs (write)
structuredCvMapper.ts # Resolvers for structured CV data

lib/utils/
resolveAvatarUrl.ts # Avatar URL resolver with initials fallback

lib/hooks/
useStructuredCvMutations.ts # Central save orchestrator hook (diff + dispatch)

Data Flow

READ PATH (All Pages):
GraphQL query (with ...CvStructuredFields fragment)
-> structured fields on CVType (profile, workExperiences, etc.)
-> structuredCvToProfileForm() maps to ProfileFormData
-> Form/preview components render the data

WRITE PATH (Jobseeker):
ProfileFormData (from form)
-> profileFormToInputs.ts maps to mutation inputs
-> useStructuredCvMutations diff engine compares current vs original
-> Typed mutations dispatched in parallel (add/update/remove)

WRITE PATH (Company):
ProfileFormData (from form)
-> profileFormToInputs.ts maps to mutation inputs
-> Company-scoped mutations for skills (addCvSkill, updateCvSkill, removeCvSkill)
-> Jobseeker "My" mutations NOT usable (ownership check fails)
-> TODO: Backend needs company-scoped variants for all other tables

Flowcharts

Read Path — How CV Data Reaches the UI

Write Path — How Edits Are Saved

CV Lifecycle — Create, View, Edit, Switch

Avatar Resolution Flow

Diff Algorithm — _structuredId Tracking


GraphQL Fragments

CV_STRUCTURED_FIELDS (Full)

Used for detail/edit views. Includes all 11 structured field types plus cvSkills.

CV_STRUCTURED_LIST_FIELDS (Lightweight)

Used for list/table views. Includes only profile, jobPreference, and cvSkills.

Location: graphql/operations/schema/cv/fragments.ts

Individual fragments:

  • CV_PROFILE_FIELDS, CV_JOB_PREFERENCE_FIELDS
  • CV_WORK_EXPERIENCE_FIELDS, CV_EDUCATION_FIELDS, CV_CERTIFICATION_FIELDS
  • CV_LANGUAGE_FIELDS, CV_PROJECT_FIELDS, CV_PUBLICATION_FIELDS
  • CV_AWARD_FIELDS, CV_VOLUNTEER_FIELDS, CV_METADATA_FIELDS

Typed Mutations

Location: graphql/operations/cv/structuredMutations.ts

All mutations use the "My" prefix (jobseeker-only, checks CV ownership against logged-in user):

SectionAddUpdateRemove
ProfileUPDATE_MY_CV_PROFILE
Job PreferenceUPDATE_MY_CV_JOB_PREFERENCEDELETE_MY_CV_JOB_PREFERENCE
Work ExperienceADD_MY_CV_WORK_EXPERIENCEUPDATE_MY_CV_WORK_EXPERIENCEREMOVE_MY_CV_WORK_EXPERIENCE
EducationADD_MY_CV_EDUCATIONUPDATE_MY_CV_EDUCATIONREMOVE_MY_CV_EDUCATION
CertificationADD_MY_CV_CERTIFICATIONUPDATE_MY_CV_CERTIFICATIONREMOVE_MY_CV_CERTIFICATION
LanguageADD_MY_CV_LANGUAGEUPDATE_MY_CV_LANGUAGEREMOVE_MY_CV_LANGUAGE
ProjectADD_MY_CV_PROJECTUPDATE_MY_CV_PROJECTREMOVE_MY_CV_PROJECT
PublicationADD_MY_CV_PUBLICATIONUPDATE_MY_CV_PUBLICATIONREMOVE_MY_CV_PUBLICATION
AwardADD_MY_CV_AWARDUPDATE_MY_CV_AWARDREMOVE_MY_CV_AWARD
VolunteerADD_MY_CV_VOLUNTEERUPDATE_MY_CV_VOLUNTEERREMOVE_MY_CV_VOLUNTEER
SkillADD_MY_CV_SKILLUPDATE_MY_CV_SKILLREMOVE_MY_CV_SKILL

Important: Input types use Create prefix (e.g., CreateCvWorkExperienceInput) not Add.

Skill mutations: addMyCvSkill, updateMyCvSkill, removeMyCvSkill are implemented on both backend and frontend. The useStructuredCvMutations hook handles skill saves via diff (add/update/remove). Company-scoped variants (addCvSkill, updateCvSkill, removeCvSkill) are not yet available — see BACKEND_CV_SKILL_MUTATIONS.md.


Key Components

useStructuredCvMutations Hook

Location: lib/hooks/useStructuredCvMutations.ts

Central save orchestrator that:

  1. Registers mutations for Profile, JobPreference, WorkExperience, Education, Certification, Language, and Skill via useMutation
  2. Exposes saveStructuredCv(cvId, currentForm, originalSnapshot)
  3. Computes diffs and dispatches mutations in parallel

Diff algorithm (diffArraySection):

  • Items in current WITH _structuredId matching original -> UPDATE
  • Items in current WITHOUT _structuredId (or new) -> ADD
  • Items in original's _structuredId set but NOT in current -> REMOVE

Execution: Profile + JobPreference always overwrite. Array sections use computed diffs. All dispatched via Promise.allSettled().

Note: No schemaData sync — the hook purely uses typed structured mutations. The backend handles any backward-compatibility sync internally.

structuredCvToProfileForm

Location: lib/utils/cv/structuredCvToProfileForm.ts

Maps StructuredCvFields -> Partial<ProfileFormData> with _structuredId threading. Key mappings:

  • profile -> personal fields (firstName, email, phone, etc.)
  • profile.profileImageUrl -> profileImageUrl (raw value, no avatar fallback — form components handle empty state)
  • jobPreference -> preference fields (preferredTitles, expectedSalary, etc.)
  • workExperiences[].id -> experiences[]._structuredId
  • educations[].id -> education[]._structuredId
  • certifications[].id -> certificates[]._structuredId
  • languages[].id -> languages[]._structuredId
  • cvSkills -> skills[] (name, proficiency mapped to numeric level)

profileFormToInputs

Location: lib/utils/cv/profileFormToInputs.ts

Mapper functions:

  • profileFormToProfileInput(form) -> UpdateCvProfileInput
  • profileFormToJobPrefInput(form) -> UpdateCvJobPreferenceInput
  • experienceToInput(exp) -> CreateCvWorkExperienceInput (maps title -> jobTitle, company -> companyName, technicalSkills -> technologies)
  • educationToInput(edu) -> CreateCvEducationInput (maps title -> degree, institute -> institution)
  • certificateToInput(cert) -> CreateCvCertificationInput (maps title -> name, institute -> issuingOrganization, year -> issueDate)
  • languageToInput(lang) -> CreateCvLanguageInput (maps name -> language, numeric level -> string proficiency)

Includes toISODateOrNull() helper for date normalization (YYYY-MM-DD format required by backend).

Important: technologies must be [] not null — Prisma array fields reject null values.

_structuredId Convention

Array item types (Experience, Education, Certificate, LanguageItem) have an optional _structuredId?: string field that stores the backend database ID. The _ prefix:

  • Distinguishes it from frontend-generated IDs
  • Prevents accidental inclusion in legacy mutations
  • Enables the diff engine to track add/update/remove operations

resolveAvatarUrl Utility

Location: lib/utils/resolveAvatarUrl.ts

Resolves avatar image URLs with initials fallback:

  • Empty/null URL → generates ui-avatars.com URL from the name parameter
  • Relative paths → prepends NEXT_PUBLIC_IMAGE_BASE_URL
  • S3 URLs on localhost → proxied through /s3-proxy/ to avoid CORS

Usage: Always pass name as the second argument for meaningful initials fallback:

resolveAvatarUrl(user.profileImageUrl, fullName)  // ✓ shows "JD" initials
resolveAvatarUrl(user.profileImageUrl) // ✗ shows "?" character

Migration Status by Page

Jobseeker CV Edit Page

File: app/jobseeker/resume/edit/[cvId]/page.tsx

PathStatusMethod
ReadStructured fieldsstructuredCvToProfileForm()
WriteTyped mutationsuseStructuredCvMutations.saveStructuredCv()

Fully migrated. No schemaData usage.

Jobseeker Resume Preview Page

File: app/jobseeker/resume/page.tsx

PathStatusMethod
ReadStructured fieldsstructuredCvToProfileForm() + profileFormToPreview()

Fully migrated. Uses MY_CVS query (which includes ...CvStructuredFields) to find the active CV, then maps structured data through structuredCvToProfileForm()profileFormToPreview()JobseekerCVPreview component.

Previously: Used schemaToPreviewForm() which read from activeCV.parsedCV.schemaData (JSON blob). This 230-line function and all its helpers have been removed.

Company-Side Talent Edit (UpdateCandidateForm)

File: app/company/resume/candidates/_components/UpdateCandidateForm.tsx

PathStatusMethod
ReadStructured fieldsstructuredCvToProfileForm(structured)
Write (skills)Company-scoped structured mutationsuseStructuredCvMutations({ companyScoped: true })addCvSkill, updateCvSkill, removeCvSkill
Write (other)Legacy UPDATE_CV_SCHEMAprofileToSchema()updateCvSchema() (backend needs company-scoped variants for other tables)

Partially migrated: Skills use company-scoped structured mutations with diff logic. Other fields (profile, experiences, education, etc.) still use UPDATE_CV_SCHEMA because company-scoped "My" variants don't exist yet.

Company-Side Talent View (TalentDetails)

File: app/company/resume/talents/_components/TalentDetails.tsx

PathStatusMethod
ReadStructured fieldsstructured.profile, structured.jobPreference

Migrated from reading schemaData.personalInfo/schemaData.meta to reading structured profile and jobPreference fields. The personalData (avatar, name, title, email, location) and salary/preferences now come from structured fields with user fallback.

Company-Side Talent List Components

FileChange
TalentSidebar.tsxUses resolveAvatarUrl(avatarUrl, fullName) for avatar display
TalentQuickView.tsxUses resolveAvatarUrl(avatarUrl, fullName) for avatar display
TalentsTable.tsxUses resolveAvatarUrl(avatarUrl, talentName) with name prop on TWAvatar

Avatar Components (Profile Image Fix)

FileChange
app/onboarding/jobseeker/steps/components/AvatarSection.tsxUses resolveAvatarUrl(user.profileImageUrl, fullName) + name prop
app/(shared)/userprofile/components/AvatarSection.tsxUses resolveAvatarUrl(user.profileImageUrl, fullName) + name prop

Paths NOT Changed

FileReason
app/onboarding/jobseeker/steps/SummaryStep.tsxCREATE path — uses CREATE_MY_CV
lib/hooks/useCreateCv.tsCREATE path — uses CREATE_MY_CV
app/company/resume/talents/_components/ImportTalentModal.tsxBulk import — low ROI, backend sync keeps data consistent
lib/hooks/useAddCandidate.tsCREATE path — could be migrated to structured mutations

Mutation Response Changes

SET_MY_ACTIVE_CV (graphql/operations/cv/mutations.ts)

  • Now returns ...CvStructuredFields fragment (profile, workExperiences, cvSkills, etc.)
  • Removed schemaData from parsedCV block
  • Ensures Apollo cache is populated with structured data after switching active CV

CREATE_MY_CV (graphql/operations/cv/mutations.ts)

  • Returns ...CvStructuredFields fragment
  • Ensures structured data is available in cache immediately after CV creation

MY_CVS Query (graphql/operations/cv/queries.ts)

  • Includes ...CvStructuredFields fragment on each CV

GET_CV_BY_ID Query (graphql/operations/cv/queries.ts)

  • Includes ...CvStructuredFields fragment

Type Changes

TMyCvsQuery (lib/types/cv.ts)

  • CV items now include Partial<StructuredCvFields> (profile, workExperiences, cvSkills, etc.)

TSetMyActiveCvData (lib/types/cv.ts)

  • Added Partial<StructuredCvFields> for structured field access
  • No parsedCV — backend removed it from schema

Query Changes

GET_TALENT (graphql/operations/talents/queries.ts)

  • Added source field on talent (needed for needsRegistration check)
  • Added isActive field on user (needed for needsRegistration check)
  • Uses CV_STRUCTURED_FIELDS fragment on activeCV

GET_COMPANY_TALENTS (graphql/operations/talents/queries.ts)

  • Upgraded from CV_STRUCTURED_LIST_FIELDS to CV_STRUCTURED_FIELDS

GET_CANDIDATE_DETAILED (graphql/operations/candidate/queries.ts)

  • Uses CV_STRUCTURED_FIELDS and ...CvStructuredFields on both CVs

Data Source Priority

When prefilling the edit form, data is resolved in this priority order:

  1. Structured CV fields (primary) — from activeCV.profile, .workExperiences, .cvSkills, etc.
  2. user.qualifications (fallback for arrays) — JSON field containing skills, experience, education, certifications, languages
  3. user.* fields (fallback for scalars) — firstName, lastName, email, title, location, etc.

This ensures data is shown even when structured tables are empty (e.g., newly created CVs before parsing completes).


Bug Fixes Applied

1. Input Type Naming

  • Issue: Frontend used AddCvWorkExperienceInput but backend expects CreateCvWorkExperienceInput
  • Fix: Updated all input types to use Create prefix

2. Date Format Validation

  • Issue: Backend requires ISO 8601 dates (YYYY-MM-DD) but form data had various formats ("May 2024", "2018-01-01T00:00:00.000Z", etc.)
  • Fix: Added toISODateOrNull() helper in profileFormToInputs.ts that normalizes all date formats

3. "My" Mutations Are Jobseeker-Only

  • Issue: updateMyCvProfile, addMyCvWorkExperience, etc. check CV ownership — company users get "CV not found"
  • Fix: Reverted company-side write path to UPDATE_CV_SCHEMA. Jobseeker edit page uses typed mutations successfully.

4. schemaData Sent as String Instead of Object

  • Issue: JSON.stringify(schema) was passed to schemaData field which expects JSONObject type
  • Fix: Pass the raw object directly: { schemaData: schema } not { schemaData: JSON.stringify(schema) }

5. Profile Image Not Showing on View Page

  • Issue: TalentDetails.tsx was reading schemaData.meta.profileImageUrl which was removed from the query
  • Fix: Updated to read from structured.profile.profileImageUrl with user.profileImageUrl fallback

6. Hardcoded "Active" Status

  • Issue: talentRowBuilder.ts and candidateRowBuilder.ts defaulted to "Active" when user.status was null
  • Fix: Changed fallback to empty string "", and TalentsTable.tsx now shows "—" for empty status

7. Profile Image Question Mark on Multiple Pages

  • Issue: When a user had no profile image, a "?" character was displayed instead of proper initials or upload UI
  • Root causes:
    • resolveAvatarUrl(url) called without name param → generated ui-avatars.com/?name=? URL
    • structuredCvToProfileForm used resolveAvatarUrl(profile.profileImageUrl, null) for form data → injected a ui-avatars URL into profileImageUrl, which made ProfileForm.tsx show an <img> with "?" instead of the ImageUploadCrop component
  • Fixes:
    • All avatar display components now pass fullName to resolveAvatarUrl(url, fullName)
    • structuredCvToProfileForm now uses profile.profileImageUrl ?? "" (raw value, no avatar fallback)
    • Components fixed: onboarding AvatarSection, shared AvatarSection, TalentSidebar, TalentQuickView, TalentsTable

8. SET_MY_ACTIVE_CV and CREATE_MY_CV Missing Structured Fields

  • Issue: Mutation responses didn't include ...CvStructuredFields, so Apollo cache wasn't populated with structured data (profile, skills, etc.) after switching/creating CVs
  • Fix: Added CV_STRUCTURED_FIELDS fragment to both mutations. Verified response now includes all structured fields (e.g., 36 cvSkills returned correctly)

9. Jobseeker Resume Preview Page Using Legacy schemaData

  • Issue: /jobseeker/resume page read from activeCV.parsedCV.schemaData via a 230-line schemaToPreviewForm() function. This showed incomplete/outdated data since writes now go through structured mutations.
  • Fix: Replaced with structuredCvToProfileForm() + compact profileFormToPreview() mapper. Page now finds active CV from myCvs query (which has structured fields) instead of WHO_AM_I.activeCV.schemaData.

10. technologies Null Error in Work Experience Updates

  • Issue: experienceToInput() sent technologies: null when no technical skills existed. Prisma rejects null for array fields: "Argument technologies must not be null"
  • Fix: Changed to technologies: exp.technicalSkills ?? [] (empty array instead of null)

Known Limitations & Backend Issues

1. Company-Side Write Path — Partial Migration

Company-scoped skill mutations (addCvSkill, updateCvSkill, removeCvSkill) are fully integrated (backend + frontend). UpdateCandidateForm uses useStructuredCvMutations({ companyScoped: true }) with saveSkillsOnly() for diff-based skill saves. However, company-scoped variants for all other tables (updateCvProfile, addCvWorkExperience, etc.) do not exist yet — these still go through UPDATE_CV_SCHEMA.

3. cv(id:) Resolver Returns Empty cvSkills

Backend bug: The cv(id:) GraphQL resolver returns cvSkills: [] while myCvs and setMyActiveCv correctly return all skills for the same CV. The frontend query is correct (uses ...CvStructuredFields which includes cvSkills { ...CVSkillFields }). This affects the jobseeker CV edit page which uses GET_CV_BY_ID.

4. updateMyCvJobPreference Upsert Missing CV Relation

Backend Prisma bug: When upserting a cvJobPreference record, the create block passes cvId as a scalar field but Prisma requires the CV relation to be connected via connect: { id: cvId }. Error: "Argument CV is missing."

5. CREATE Paths

CREATE_MY_CV (onboarding, useCreateCv) creates CVs via the CREATE mutation. The backend populates structured tables during CV parsing. Typed mutations apply to existing CVs only.

6. Qualifications Fallback

When structured tables are empty for a CV, the form falls back to user.qualifications JSON for array fields. This ensures data continuity for newly created CVs before parsing completes.


Verification Checklist

  1. npm run build passes with no type errors
  2. Jobseeker CV Editor: Edit CV -> save -> verify typed mutation calls in Network tab (not updateCvSchema)
  3. Jobseeker Resume Preview: View /jobseeker/resume -> verify all fields display correctly from structured data
  4. Company Talent View: Open talent details -> verify profile image, name, salary display correctly from structured fields
  5. Company Talent Edit: Edit talent skills -> save -> verify addCvSkill/updateCvSkill/removeCvSkill calls in Network tab
  6. Talents List: Verify avatar shows initials (not "?") when no profile image exists
  7. Profile Image: Verify CV edit page shows upload component (not question mark) when no image exists
  8. Add Candidate: Add new candidate with CV -> verify structured fields are populated via backend auto-sync
  9. Active CV Switch: Switch active CV -> verify all structured fields populate correctly in preview

Files Changed (Complete List)

Core Migration Files (Created/Heavily Modified)

FilePurpose
graphql/operations/cv/structuredMutations.ts30 typed mutation definitions (27 jobseeker + 3 company-scoped skill)
graphql/operations/schema/cv/fragments.tsCV_STRUCTURED_FIELDS and sub-fragments
lib/types/structuredCv.tsTypeScript interfaces for 11 structured types
lib/types/structuredCvInputs.tsInput types for mutation variables
lib/utils/cv/structuredCvToProfileForm.tsStructured → ProfileFormData mapper
lib/utils/cv/profileFormToInputs.tsProfileFormData → mutation inputs mapper
lib/utils/cv/structuredCvMapper.tsresolveStructuredCv() for structured CV data
lib/hooks/useStructuredCvMutations.tsCentral save orchestrator with diff engine

GraphQL Queries/Mutations Modified

FileChanges
graphql/operations/cv/mutations.tsAdded CvStructuredFields to CREATE_MY_CV and SET_MY_ACTIVE_CV
graphql/operations/cv/queries.tsBoth queries include CvStructuredFields
graphql/operations/talents/queries.tsUpgraded to CV_STRUCTURED_FIELDS, added source/isActive fields
graphql/operations/candidate/queries.tsUses CvStructuredFields (no parsedCV)

Type Definitions Modified

FileChanges
lib/types/cv.tsTMyCvsQuery, TSetMyActiveCvData, TWhoAmIQuery updated with StructuredCvFields
lib/types/cvbuilder.tsAdded _structuredId to Experience, Education, Certificate

Page Components Modified

FileChanges
app/jobseeker/resume/edit/[cvId]/page.tsxFull structured read/write migration
app/jobseeker/resume/page.tsxMigrated from schemaData to structured fields for preview
app/company/resume/candidates/_components/UpdateCandidateForm.tsxStructured read + schemaData fallback
app/company/resume/talents/_components/TalentDetails.tsxStructured field reads for profile/preferences
app/company/resume/talents/_components/TalentSidebar.tsxresolveAvatarUrl with fullName
app/company/resume/talents/_components/TalentQuickView.tsxresolveAvatarUrl with fullName
app/company/resume/talents/_components/TalentsTable.tsxresolveAvatarUrl with talentName
app/onboarding/jobseeker/steps/components/AvatarSection.tsxresolveAvatarUrl with fullName
app/(shared)/userprofile/components/AvatarSection.tsxresolveAvatarUrl with fullName
app/onboarding/jobseeker/steps/SummaryStep.tsxMade schemaData optional in local WhoAmIQuery type
components/cv/ProfileForm.tsxProfile image uses raw URL (no resolveAvatarUrl injection)

Utility Files Modified

FileChanges
lib/utils/resolveAvatarUrl.tsEnhanced with name-based initials, S3 proxy, URL encoding
lib/utils/candidates/talentRowBuilder.tsChanged status fallback from "Active" to ""
lib/utils/candidates/candidateRowBuilder.tsChanged status fallback from "Active" to ""