Something went wrong. Try again.
because I got bored of customising my CV for every job
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179import { CVParserService as CVParser, type ParsedCVData } from "@cv/ai-parser";import type { User } from "@cv/core";import { TEXT_EXTRACTOR_REGISTRY, type TextExtractorRegistry, validateFile,} from "@cv/file-upload";import { Inject, Injectable } from "@nestjs/common";import { AIProviderResolverService } from "./ai-provider-resolver.service";import { EntityResolverService, type ResolvedEducation, type ResolvedJobExperience,} from "./entity-resolver.service";
/** * Parsed CV data with resolved entities */export interface ParsedCVDataWithResolution { jobExperiences: ResolvedJobExperience[]; education: ResolvedEducation[];}
@Injectable()export class CVParserService { constructor( @Inject(TEXT_EXTRACTOR_REGISTRY) private readonly textExtractorRegistry: TextExtractorRegistry, private readonly entityResolver: EntityResolverService, private readonly providerResolver: AIProviderResolverService, ) {}
/** Create a per-request CVParser using the user's resolved provider */ private async createParser(user: User): Promise<CVParser> { const provider = await this.providerResolver.resolveForUser(user); return new CVParser(provider); }
async parseFile( user: User, buffer: Buffer, mimeType: string, originalName: string, ): Promise<ParsedCVData> { const validation = validateFile({ buffer, mimeType, originalName, sizeBytes: buffer.length, });
if (!validation.valid) { throw new Error(`File validation failed: ${validation.error}`); }
const extraction = await this.textExtractorRegistry.extract( buffer, mimeType, );
if (!extraction.success) { throw new Error(`Text extraction failed: ${extraction.error}`); }
if (!extraction.text || extraction.text.trim().length === 0) { throw new Error("Could not extract any text from the file"); }
const parser = await this.createParser(user); return parser.parseCVText(extraction.text); }
async parseStory(user: User, storyText: string): Promise<ParsedCVData> { if (!storyText || storyText.trim().length === 0) { throw new Error("Story text cannot be empty"); }
if (storyText.trim().length > 50000) { throw new Error("Story text is too long (max 50,000 characters)"); }
const parser = await this.createParser(user); return parser.parseCVText(storyText); }
/** * Parse story text and resolve entities to existing database records * Returns draft data with entity IDs where matches were found */ async parseStoryWithResolution( user: User, storyText: string, ): Promise<ParsedCVDataWithResolution> { const parsed = await this.parseStory(user, storyText); return this.resolveEntities(parsed); }
/** * Parse file and resolve entities to existing database records */ async parseFileWithResolution( user: User, buffer: Buffer, mimeType: string, originalName: string, ): Promise<ParsedCVDataWithResolution> { const parsed = await this.parseFile(user, buffer, mimeType, originalName); return this.resolveEntities(parsed); }
/** * Resolve entity names to existing database records */ private async resolveEntities( parsed: ParsedCVData, ): Promise<ParsedCVDataWithResolution> { const [jobExperiences, education] = await Promise.all([ this.resolveJobExperiences(parsed.jobExperiences), this.resolveEducation(parsed.education), ]);
return { jobExperiences, education }; }
/** * Resolve job experience entities */ private async resolveJobExperiences( jobs: ParsedCVData["jobExperiences"], ): Promise<ResolvedJobExperience[]> { return Promise.all( jobs.map(async (job) => { const [company, role, level, skills] = await Promise.all([ this.entityResolver.resolveCompany(job.companyName), this.entityResolver.resolveRole(job.roleName), this.entityResolver.resolveLevel(job.levelName), this.entityResolver.resolveSkills(job.skills ?? []), ]);
return { company, role, level, skills, startDate: new Date(job.startDate), endDate: job.endDate ? new Date(job.endDate) : null, description: job.description ?? null, }; }), ); }
/** * Resolve education entities */ private async resolveEducation( education: ParsedCVData["education"], ): Promise<ResolvedEducation[]> { return Promise.all( education.map(async (edu) => { const [institution, skills] = await Promise.all([ this.entityResolver.resolveInstitution(edu.institutionName), this.entityResolver.resolveSkills(edu.skills ?? []), ]);
return { institution, degree: edu.degree, fieldOfStudy: edu.fieldOfStudy ?? null, skills, startDate: new Date(edu.startDate), endDate: edu.endDate ? new Date(edu.endDate) : null, description: edu.description ?? null, }; }), ); }}