import { CVParserService as CVParser, type ParsedCVData } from "@cv/ai-parser"; import type { User } from "@cv/auth"; 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 { const provider = await this.providerResolver.resolveForUser(user); return new CVParser(provider); } async parseFile( user: User, buffer: Buffer, mimeType: string, originalName: string, ): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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, }; }), ); } }