Backend Requirement: CV Skill CRUD Mutations
Status
| Scope | Status | Notes |
|---|---|---|
Jobseeker mutations (addMyCvSkill, updateMyCvSkill, removeMyCvSkill) | DONE | Backend implemented, frontend wired up |
Company-scoped mutations (addCvSkill, updateCvSkill, removeCvSkill) | DONE | Backend + frontend fully integrated |
Part 1: Jobseeker Mutations (DONE)
The My mutations are fully implemented on both backend and frontend. They verify cv.userId === currentUser.id (job seeker editing their own CV).
Mutations
| Operation | Returns | Description |
|---|---|---|
addMyCvSkill(cvId, input) | CVSkill! | Add a skill to a CV (find-or-create Skill record by name) |
updateMyCvSkill(id, input) | CVSkill! | Update proficiency/yearsExperience on an existing CVSkill |
removeMyCvSkill(id) | SuccessResponse! | Remove a CVSkill record |
Input Types
input CreateCvSkillInput {
skillName: String! # Backend finds or creates the Skill record (case-insensitive)
proficiency: String # NONE | BASIC | INTERMEDIATE | ADVANCED | EXPERT
yearsExperience: Int
}
input UpdateCvSkillInput {
proficiency: String # NONE | BASIC | INTERMEDIATE | ADVANCED | EXPERT
yearsExperience: Int
}
Frontend Files
| File | What was added |
|---|---|
graphql/operations/cv/structuredMutations.ts | ADD_MY_CV_SKILL, UPDATE_MY_CV_SKILL, REMOVE_MY_CV_SKILL |
lib/types/structuredCvInputs.ts | CreateCvSkillInput, UpdateCvSkillInput interfaces |
lib/types/skills.ts | Added _structuredId to SkillRequirementInput for diff tracking |
lib/utils/cv/profileFormToInputs.ts | skillToCreateInput(), skillToUpdateInput() mappers |
lib/utils/cv/structuredCvToProfileForm.ts | Skills carry _structuredId from CVSkill record ID |
lib/hooks/useStructuredCvMutations.ts | Skills diff + add/update/remove wired into save flow |
Part 2: Company-Scoped Mutations (DONE)
Context
The My mutations enforce cv.userId === currentUser.id, so they only work for job seekers editing their own CV. Company users editing a candidate's or talent's CV need mutations that verify company access instead of CV ownership.
The backend has company-scoped skill mutations using TalentAuthorizationService.checkWriteAccessToCV() for access control. The frontend is fully wired up via useStructuredCvMutations({ companyScoped: true }).
Available Mutations (Backend Implemented)
Same pattern as the My variants, but with company access checks via TalentAuthorizationService.checkWriteAccessToCV().
| Operation | Returns | Description |
|---|---|---|
addCvSkill(cvId, input) | CVSkill! | Add a skill to any CV the company has access to |
updateCvSkill(id, input) | CVSkill! | Update proficiency/yearsExperience on a CVSkill |
removeCvSkill(id) | SuccessResponse! | Remove a CVSkill record |
Input Types
Same as the My variants:
input CreateCvSkillInput {
skillName: String! # Find-or-create Skill record (case-insensitive)
proficiency: String # NONE | BASIC | INTERMEDIATE | ADVANCED | EXPERT
yearsExperience: Int
}
input UpdateCvSkillInput {
proficiency: String # NONE | BASIC | INTERMEDIATE | ADVANCED | EXPERT
yearsExperience: Int
}
Access Control
Instead of cv.userId === currentUser.id, verify that:
- The CV belongs to a talent in a talent pool owned by the user's company, OR
- The CV belongs to a candidate on a job owned by the user's company
This matches how other company-scoped CV operations (like updateCvSchema) verify access.
async addCvSkill(userId: string, cvId: string, input: CreateCvSkillInput) {
return this.prisma.$transaction(async (tx) => {
// 1. Verify company access to this CV (not ownership)
const cv = await tx.cV.findUniqueOrThrow({
where: { id: cvId },
include: { user: { include: { talents: true } } },
});
await this.verifyCompanyAccess(tx, userId, cv);
// 2. Find or create the Skill record
let skill = await tx.skill.findFirst({
where: { name: { equals: input.skillName, mode: 'insensitive' } },
});
if (!skill) {
skill = await tx.skill.create({ data: { name: input.skillName } });
}
// 3. Upsert CVSkill
const cvSkill = await tx.cVSkill.upsert({
where: { cvId_skillId: { cvId, skillId: skill.id } },
create: {
cvId,
skillId: skill.id,
proficiency: input.proficiency ?? 'INTERMEDIATE',
yearsExperience: input.yearsExperience,
},
update: {
proficiency: input.proficiency ?? undefined,
yearsExperience: input.yearsExperience ?? undefined,
},
include: { skill: true },
});
// 4. Sync schemaData
await this.serializeStructuredToSchemaData(tx, cvId);
return cvSkill;
});
}
Backend Files (DONE)
| File | Change |
|---|---|
dto/cv-structured-inputs.ts | Reuses CreateCvSkillInput, UpdateCvSkillInput |
cv-structured-mutation.resolver.ts | addCvSkill, updateCvSkill, removeCvSkill resolvers added |
cv-structured-mutation.service.ts | Service methods with TalentAuthorizationService.checkWriteAccessToCV() + transaction |
Frontend Files (DONE)
| File | Change |
|---|---|
graphql/operations/cv/structuredMutations.ts | Added ADD_CV_SKILL, UPDATE_CV_SKILL, REMOVE_CV_SKILL mutation definitions |
lib/hooks/useStructuredCvMutations.ts | Added companyScoped option + saveSkillsOnly() method |
app/company/resume/candidates/_components/UpdateCandidateForm.tsx | Uses useStructuredCvMutations({ companyScoped: true }) with diff-based skill saves |
Broader Context: Company-Scoped Structured Mutations
The same My-only limitation applies to all structured mutations, not just skills. Skills are now fully covered (both jobseeker and company-scoped). The remaining tables still need company-scoped variants:
| Jobseeker (exists) | Company (needed) |
|---|---|
updateMyCvProfile | updateCvProfile |
updateMyCvJobPreference | updateCvJobPreference |
addMyCvWorkExperience | addCvWorkExperience |
addMyCvSkill | addCvSkill |
| ... | ... |
Skills are done. The remaining tables are a larger effort but needed since parsedCV has been removed from the backend schema entirely.
Data Flow
Jobseeker (Working)
User edits skills in CV builder
|
v
Form state: { _structuredId, id, name, level, proficiency, mustHave, weight }
|
v
Diff algorithm compares original vs current skills (by _structuredId)
|
+--> Added skills --> addMyCvSkill(cvId, { skillName, proficiency })
+--> Changed skills --> updateMyCvSkill(id, { proficiency })
+--> Removed skills --> removeMyCvSkill(id)
|
v
Backend: CVSkill write + Skill find-or-create + serializeStructuredToSchemaData()
Company (DONE)
Company user edits candidate/talent skills
|
v
Same form state and diff logic
|
+--> Added skills --> addCvSkill(cvId, { skillName, proficiency })
+--> Changed skills --> updateCvSkill(id, { proficiency })
+--> Removed skills --> removeCvSkill(id)
|
v
Backend: Same logic but with company access check instead of ownership check