diff --git a/packages/core/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts b/packages/core/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts deleted file mode 100644 index 1d106d8..0000000 --- a/packages/core/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { - CV_PARSER_SERVICE, - type CVParserService as CVParser, -} from "@cv/ai-parser"; -import { TEXT_EXTRACTOR_REGISTRY } from "@cv/file-upload"; -import { Test, type TestingModule } from "@nestjs/testing"; -import { CVParserService } from "../cv-parser.service"; -import { EntityResolverService } from "../entity-resolver.service"; -import { - mockJaneDoeParsedCV, - mockJohnSmithParsedCV, -} from "./fixtures/mock-ai-responses"; - -describe("CVParserService", () => { - let service: CVParserService; - let mockCVParser: jest.Mocked; - let mockTextExtractorRegistry: jest.Mocked<{ - extract: ( - buffer: Buffer, - mimeType: string, - ) => Promise<{ - success: boolean; - text?: string; - error?: string; - }>; - }>; - let mockEntityResolver: jest.Mocked; - - const fixturesPath = join(__dirname, "fixtures"); - const sampleTxtCV = readFileSync( - join(fixturesPath, "sample-cv.txt"), - "utf-8", - ); - const sampleMdCV = readFileSync(join(fixturesPath, "sample-cv.md"), "utf-8"); - - beforeEach(async () => { - mockCVParser = { - parseCVText: jest.fn(), - } as unknown as jest.Mocked; - - mockTextExtractorRegistry = { - extract: jest.fn(), - }; - - mockEntityResolver = { - resolveCompany: jest.fn(), - resolveRole: jest.fn(), - resolveLevel: jest.fn(), - resolveSkill: jest.fn(), - resolveSkills: jest.fn(), - resolveInstitution: jest.fn(), - } as unknown as jest.Mocked; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CVParserService, - { - provide: CV_PARSER_SERVICE, - useValue: mockCVParser, - }, - { - provide: TEXT_EXTRACTOR_REGISTRY, - useValue: mockTextExtractorRegistry, - }, - { - provide: EntityResolverService, - useValue: mockEntityResolver, - }, - ], - }).compile(); - - service = module.get(CVParserService); - }); - - describe("parseStory", () => { - it("parses a career narrative from plain text", async () => { - mockCVParser.parseCVText.mockResolvedValue(mockJohnSmithParsedCV); - - const result = await service.parseStory(sampleTxtCV); - - expect(mockCVParser.parseCVText).toHaveBeenCalledWith(sampleTxtCV); - expect(result).toEqual(mockJohnSmithParsedCV); - expect(result.personalInfo?.name).toBe("John Smith"); - expect(result.jobExperiences).toHaveLength(3); - expect(result.education).toHaveLength(1); - }); - - it("parses a career narrative from markdown", async () => { - mockCVParser.parseCVText.mockResolvedValue(mockJaneDoeParsedCV); - - const result = await service.parseStory(sampleMdCV); - - expect(mockCVParser.parseCVText).toHaveBeenCalledWith(sampleMdCV); - expect(result).toEqual(mockJaneDoeParsedCV); - expect(result.personalInfo?.name).toBe("Jane Doe"); - expect(result.jobExperiences).toHaveLength(3); - expect(result.education).toHaveLength(2); - }); - - it("throws error when story text is empty", async () => { - await expect(service.parseStory("")).rejects.toThrow( - "Story text cannot be empty", - ); - - expect(mockCVParser.parseCVText).not.toHaveBeenCalled(); - }); - - it("throws error when story text is only whitespace", async () => { - await expect(service.parseStory(" \n\t ")).rejects.toThrow( - "Story text cannot be empty", - ); - - expect(mockCVParser.parseCVText).not.toHaveBeenCalled(); - }); - - it("throws error when story text exceeds maximum length", async () => { - const longText = "a".repeat(50001); - - await expect(service.parseStory(longText)).rejects.toThrow( - "Story text is too long (max 50,000 characters)", - ); - - expect(mockCVParser.parseCVText).not.toHaveBeenCalled(); - }); - - it("accepts story text at maximum length boundary", async () => { - const maxLengthText = "a".repeat(50000); - mockCVParser.parseCVText.mockResolvedValue({ - jobExperiences: [], - education: [], - skills: [], - }); - - await service.parseStory(maxLengthText); - - expect(mockCVParser.parseCVText).toHaveBeenCalledWith(maxLengthText); - }); - - it("extracts job experiences with correct structure", async () => { - mockCVParser.parseCVText.mockResolvedValue(mockJohnSmithParsedCV); - - const result = await service.parseStory(sampleTxtCV); - - const firstJob = result.jobExperiences[0]; - expect(firstJob.companyName).toBe("Acme Corporation"); - expect(firstJob.roleName).toBe("Senior Software Engineer"); - expect(firstJob.levelName).toBe("Senior"); - expect(firstJob.startDate).toBe("2020-01-01"); - expect(firstJob.endDate).toBeNull(); - expect(firstJob.skills).toContain("TypeScript"); - expect(firstJob.skills).toContain("Kubernetes"); - }); - - it("extracts education with correct structure", async () => { - mockCVParser.parseCVText.mockResolvedValue(mockJohnSmithParsedCV); - - const result = await service.parseStory(sampleTxtCV); - - const education = result.education[0]; - expect(education.institutionName).toBe("Stanford University"); - expect(education.degree).toBe("Bachelor of Science"); - expect(education.fieldOfStudy).toBe("Computer Science"); - expect(education.startDate).toBe("2010-09-01"); - expect(education.endDate).toBe("2014-06-30"); - }); - - it("extracts overall skills list", async () => { - mockCVParser.parseCVText.mockResolvedValue(mockJohnSmithParsedCV); - - const result = await service.parseStory(sampleTxtCV); - - expect(result.skills).toContain("TypeScript"); - expect(result.skills).toContain("Docker"); - expect(result.skills).toContain("AWS"); - expect(result.skills.length).toBeGreaterThan(0); - }); - }); - - describe("parseFile", () => { - const mockBuffer = Buffer.from("mock file content"); - const mockMimeType = "text/plain"; - const mockFileName = "resume.txt"; - - it("parses a valid text file", async () => { - mockTextExtractorRegistry.extract.mockResolvedValue({ - success: true, - text: sampleTxtCV, - }); - mockCVParser.parseCVText.mockResolvedValue(mockJohnSmithParsedCV); - - const result = await service.parseFile( - mockBuffer, - mockMimeType, - mockFileName, - ); - - expect(mockTextExtractorRegistry.extract).toHaveBeenCalledWith( - mockBuffer, - mockMimeType, - ); - expect(mockCVParser.parseCVText).toHaveBeenCalledWith(sampleTxtCV); - expect(result).toEqual(mockJohnSmithParsedCV); - }); - - it("throws error when text extraction fails", async () => { - mockTextExtractorRegistry.extract.mockResolvedValue({ - success: false, - error: "Unsupported file format", - }); - - await expect( - service.parseFile(mockBuffer, mockMimeType, mockFileName), - ).rejects.toThrow("Text extraction failed: Unsupported file format"); - }); - - it("throws error when extracted text is empty", async () => { - mockTextExtractorRegistry.extract.mockResolvedValue({ - success: true, - text: "", - }); - - await expect( - service.parseFile(mockBuffer, mockMimeType, mockFileName), - ).rejects.toThrow("Could not extract any text from the file"); - }); - - it("throws error when extracted text is only whitespace", async () => { - mockTextExtractorRegistry.extract.mockResolvedValue({ - success: true, - text: " \n\t ", - }); - - await expect( - service.parseFile(mockBuffer, mockMimeType, mockFileName), - ).rejects.toThrow("Could not extract any text from the file"); - }); - }); - - describe("parseStoryWithResolution", () => { - beforeEach(() => { - mockCVParser.parseCVText.mockResolvedValue(mockJohnSmithParsedCV); - - mockEntityResolver.resolveCompany.mockImplementation(async (name) => ({ - id: name === "Acme Corporation" ? "company-1" : null, - name, - })); - - mockEntityResolver.resolveRole.mockImplementation(async (name) => ({ - id: name === "Senior Software Engineer" ? "role-1" : null, - name, - })); - - mockEntityResolver.resolveLevel.mockImplementation(async (name) => ({ - id: name === "Senior" ? "level-1" : null, - name: name ?? "Mid-level", - })); - - mockEntityResolver.resolveInstitution.mockImplementation( - async (name) => ({ - id: name === "Stanford University" ? "institution-1" : null, - name, - }), - ); - - mockEntityResolver.resolveSkills.mockImplementation(async (names) => - names.map((name) => ({ - id: ["TypeScript", "JavaScript"].includes(name) - ? `skill-${name}` - : null, - name, - })), - ); - }); - - it("resolves entities for parsed CV data", async () => { - const result = await service.parseStoryWithResolution(sampleTxtCV); - - expect(result.jobExperiences).toHaveLength(3); - expect(result.education).toHaveLength(1); - - const firstJob = result.jobExperiences[0]; - expect(firstJob.company.id).toBe("company-1"); - expect(firstJob.company.name).toBe("Acme Corporation"); - expect(firstJob.role.id).toBe("role-1"); - expect(firstJob.level.id).toBe("level-1"); - }); - - it("returns null IDs for unmatched entities", async () => { - const result = await service.parseStoryWithResolution(sampleTxtCV); - - const secondJob = result.jobExperiences[1]; - expect(secondJob.company.id).toBeNull(); - expect(secondJob.company.name).toBe("TechStart Inc"); - }); - - it("converts date strings to Date objects", async () => { - const result = await service.parseStoryWithResolution(sampleTxtCV); - - const firstJob = result.jobExperiences[0]; - expect(firstJob.startDate).toBeInstanceOf(Date); - expect(firstJob.endDate).toBeNull(); - - const lastJob = result.jobExperiences[2]; - expect(lastJob.endDate).toBeInstanceOf(Date); - }); - - it("resolves education institutions", async () => { - const result = await service.parseStoryWithResolution(sampleTxtCV); - - const education = result.education[0]; - expect(education.institution.id).toBe("institution-1"); - expect(education.institution.name).toBe("Stanford University"); - expect(education.degree).toBe("Bachelor of Science"); - }); - - it("resolves skills for both jobs and education", async () => { - const result = await service.parseStoryWithResolution(sampleTxtCV); - - const firstJob = result.jobExperiences[0]; - const typescriptSkill = firstJob.skills.find( - (s) => s.name === "TypeScript", - ); - expect(typescriptSkill?.id).toBe("skill-TypeScript"); - - const kubernetesSkill = firstJob.skills.find( - (s) => s.name === "Kubernetes", - ); - expect(kubernetesSkill?.id).toBeNull(); - }); - }); -}); diff --git a/packages/core/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts b/packages/core/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts deleted file mode 100644 index 54ae2e5..0000000 --- a/packages/core/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { PrismaService } from "../../database"; -import { Test, type TestingModule } from "@nestjs/testing"; -import { EntityResolverService } from "../entity-resolver.service"; - -describe("EntityResolverService", () => { - let service: EntityResolverService; - let mockPrisma: jest.Mocked; - - beforeEach(async () => { - mockPrisma = { - company: { - findFirst: jest.fn(), - }, - role: { - findFirst: jest.fn(), - }, - level: { - findFirst: jest.fn(), - }, - skill: { - findFirst: jest.fn(), - }, - institution: { - findFirst: jest.fn(), - }, - } as unknown as jest.Mocked; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - EntityResolverService, - { - provide: PrismaService, - useValue: mockPrisma, - }, - ], - }).compile(); - - service = module.get(EntityResolverService); - }); - - describe("resolveCompany", () => { - it("returns matched company with ID when found", async () => { - const mockCompany = { id: "company-123", name: "Acme Corporation" }; - (mockPrisma.company.findFirst as jest.Mock).mockResolvedValue( - mockCompany, - ); - - const result = await service.resolveCompany("Acme Corporation"); - - expect(mockPrisma.company.findFirst).toHaveBeenCalledWith({ - where: { name: { equals: "Acme Corporation", mode: "insensitive" } }, - }); - expect(result).toEqual({ id: "company-123", name: "Acme Corporation" }); - }); - - it("returns draft entity with null ID when not found", async () => { - (mockPrisma.company.findFirst as jest.Mock).mockResolvedValue(null); - - const result = await service.resolveCompany("Unknown Company"); - - expect(result).toEqual({ id: null, name: "Unknown Company" }); - }); - - it("performs case-insensitive matching", async () => { - const mockCompany = { id: "company-123", name: "ACME Corporation" }; - (mockPrisma.company.findFirst as jest.Mock).mockResolvedValue( - mockCompany, - ); - - const result = await service.resolveCompany("acme corporation"); - - expect(mockPrisma.company.findFirst).toHaveBeenCalledWith({ - where: { name: { equals: "acme corporation", mode: "insensitive" } }, - }); - expect(result.id).toBe("company-123"); - expect(result.name).toBe("ACME Corporation"); - }); - }); - - describe("resolveRole", () => { - it("returns matched role with ID when found", async () => { - const mockRole = { id: "role-456", name: "Senior Software Engineer" }; - (mockPrisma.role.findFirst as jest.Mock).mockResolvedValue(mockRole); - - const result = await service.resolveRole("Senior Software Engineer"); - - expect(mockPrisma.role.findFirst).toHaveBeenCalledWith({ - where: { - name: { equals: "Senior Software Engineer", mode: "insensitive" }, - }, - }); - expect(result).toEqual({ - id: "role-456", - name: "Senior Software Engineer", - }); - }); - - it("returns draft entity with null ID when not found", async () => { - (mockPrisma.role.findFirst as jest.Mock).mockResolvedValue(null); - - const result = await service.resolveRole("Chief Happiness Officer"); - - expect(result).toEqual({ id: null, name: "Chief Happiness Officer" }); - }); - }); - - describe("resolveLevel", () => { - it("returns matched level with ID when found", async () => { - const mockLevel = { id: "level-789", name: "Senior" }; - (mockPrisma.level.findFirst as jest.Mock).mockResolvedValue(mockLevel); - - const result = await service.resolveLevel("Senior"); - - expect(mockPrisma.level.findFirst).toHaveBeenCalledWith({ - where: { name: { equals: "Senior", mode: "insensitive" } }, - }); - expect(result).toEqual({ id: "level-789", name: "Senior" }); - }); - - it("defaults to Mid-level when no name provided", async () => { - const mockLevel = { id: "level-mid", name: "Mid-level" }; - (mockPrisma.level.findFirst as jest.Mock).mockResolvedValue(mockLevel); - - const result = await service.resolveLevel(undefined); - - expect(mockPrisma.level.findFirst).toHaveBeenCalledWith({ - where: { name: { equals: "Mid-level", mode: "insensitive" } }, - }); - expect(result).toEqual({ id: "level-mid", name: "Mid-level" }); - }); - - it("defaults to Mid-level when empty string provided", async () => { - (mockPrisma.level.findFirst as jest.Mock).mockResolvedValue(null); - - const result = await service.resolveLevel(""); - - expect(mockPrisma.level.findFirst).toHaveBeenCalledWith({ - where: { name: { equals: "Mid-level", mode: "insensitive" } }, - }); - expect(result).toEqual({ id: null, name: "Mid-level" }); - }); - - it("returns draft entity with null ID when level not found", async () => { - (mockPrisma.level.findFirst as jest.Mock).mockResolvedValue(null); - - const result = await service.resolveLevel("Principal"); - - expect(result).toEqual({ id: null, name: "Principal" }); - }); - }); - - describe("resolveSkill", () => { - it("returns matched skill with ID when found", async () => { - const mockSkill = { id: "skill-ts", name: "TypeScript" }; - (mockPrisma.skill.findFirst as jest.Mock).mockResolvedValue(mockSkill); - - const result = await service.resolveSkill("TypeScript"); - - expect(mockPrisma.skill.findFirst).toHaveBeenCalledWith({ - where: { name: { equals: "TypeScript", mode: "insensitive" } }, - }); - expect(result).toEqual({ id: "skill-ts", name: "TypeScript" }); - }); - - it("returns draft entity with null ID when not found", async () => { - (mockPrisma.skill.findFirst as jest.Mock).mockResolvedValue(null); - - const result = await service.resolveSkill("ObscureFramework"); - - expect(result).toEqual({ id: null, name: "ObscureFramework" }); - }); - - it("trims whitespace from skill name", async () => { - const mockSkill = { id: "skill-ts", name: "TypeScript" }; - (mockPrisma.skill.findFirst as jest.Mock).mockResolvedValue(mockSkill); - - const result = await service.resolveSkill(" TypeScript "); - - expect(mockPrisma.skill.findFirst).toHaveBeenCalledWith({ - where: { name: { equals: "TypeScript", mode: "insensitive" } }, - }); - expect(result).toEqual({ id: "skill-ts", name: "TypeScript" }); - }); - - it("returns empty draft entity for empty/whitespace-only input", async () => { - const result = await service.resolveSkill(" "); - - expect(mockPrisma.skill.findFirst).not.toHaveBeenCalled(); - expect(result).toEqual({ id: null, name: "" }); - }); - }); - - describe("resolveInstitution", () => { - it("returns matched institution with ID when found", async () => { - const mockInstitution = { - id: "inst-stanford", - name: "Stanford University", - }; - (mockPrisma.institution.findFirst as jest.Mock).mockResolvedValue( - mockInstitution, - ); - - const result = await service.resolveInstitution("Stanford University"); - - expect(mockPrisma.institution.findFirst).toHaveBeenCalledWith({ - where: { name: { equals: "Stanford University", mode: "insensitive" } }, - }); - expect(result).toEqual({ - id: "inst-stanford", - name: "Stanford University", - }); - }); - - it("returns draft entity with null ID when not found", async () => { - (mockPrisma.institution.findFirst as jest.Mock).mockResolvedValue(null); - - const result = await service.resolveInstitution("Unknown University"); - - expect(result).toEqual({ id: null, name: "Unknown University" }); - }); - }); - - describe("resolveSkills", () => { - it("resolves multiple skills in parallel", async () => { - (mockPrisma.skill.findFirst as jest.Mock) - .mockResolvedValueOnce({ id: "skill-ts", name: "TypeScript" }) - .mockResolvedValueOnce({ id: "skill-js", name: "JavaScript" }) - .mockResolvedValueOnce(null); - - const result = await service.resolveSkills([ - "TypeScript", - "JavaScript", - "Rust", - ]); - - expect(mockPrisma.skill.findFirst).toHaveBeenCalledTimes(3); - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ id: "skill-ts", name: "TypeScript" }); - expect(result[1]).toEqual({ id: "skill-js", name: "JavaScript" }); - expect(result[2]).toEqual({ id: null, name: "Rust" }); - }); - - it("deduplicates skill names before resolving", async () => { - (mockPrisma.skill.findFirst as jest.Mock).mockResolvedValue({ - id: "skill-ts", - name: "TypeScript", - }); - - const result = await service.resolveSkills([ - "TypeScript", - "TypeScript", - "TypeScript", - ]); - - expect(mockPrisma.skill.findFirst).toHaveBeenCalledTimes(1); - expect(result).toHaveLength(1); - }); - - it("filters out empty and whitespace-only skill names", async () => { - (mockPrisma.skill.findFirst as jest.Mock).mockResolvedValue({ - id: "skill-ts", - name: "TypeScript", - }); - - const result = await service.resolveSkills([ - "TypeScript", - "", - " ", - null as unknown as string, - undefined as unknown as string, - ]); - - expect(mockPrisma.skill.findFirst).toHaveBeenCalledTimes(1); - expect(result).toHaveLength(1); - expect(result[0]).toEqual({ id: "skill-ts", name: "TypeScript" }); - }); - - it("returns empty array for empty input", async () => { - const result = await service.resolveSkills([]); - - expect(mockPrisma.skill.findFirst).not.toHaveBeenCalled(); - expect(result).toEqual([]); - }); - - it("handles mixed matching and non-matching skills", async () => { - (mockPrisma.skill.findFirst as jest.Mock) - .mockResolvedValueOnce({ id: "skill-1", name: "React" }) - .mockResolvedValueOnce(null) - .mockResolvedValueOnce({ id: "skill-2", name: "Node.js" }) - .mockResolvedValueOnce(null); - - const result = await service.resolveSkills([ - "React", - "SomeObscureLib", - "Node.js", - "AnotherRareSkill", - ]); - - expect(result).toHaveLength(4); - expect(result[0].id).toBe("skill-1"); - expect(result[1].id).toBeNull(); - expect(result[2].id).toBe("skill-2"); - expect(result[3].id).toBeNull(); - }); - }); - - describe("fuzzy matching scenarios", () => { - it("matches company names case-insensitively", async () => { - const mockCompany = { id: "c-1", name: "Google LLC" }; - (mockPrisma.company.findFirst as jest.Mock).mockResolvedValue( - mockCompany, - ); - - const variations = [ - "google llc", - "GOOGLE LLC", - "Google LLC", - "GoOgLe LlC", - ]; - - for (const variation of variations) { - const result = await service.resolveCompany(variation); - expect(result.id).toBe("c-1"); - } - }); - - it("handles abbreviated vs full company names", async () => { - (mockPrisma.company.findFirst as jest.Mock).mockResolvedValue(null); - - const result = await service.resolveCompany("IBM"); - - expect(result.id).toBeNull(); - expect(result.name).toBe("IBM"); - }); - - it("preserves original name from database when matched", async () => { - const mockSkill = { id: "skill-1", name: "Node.js" }; - (mockPrisma.skill.findFirst as jest.Mock).mockResolvedValue(mockSkill); - - const result = await service.resolveSkill("nodejs"); - - expect(result.name).toBe("Node.js"); - }); - }); -}); diff --git a/packages/core/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts b/packages/core/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts deleted file mode 100644 index 80f7ba3..0000000 --- a/packages/core/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts +++ /dev/null @@ -1,184 +0,0 @@ -import type { ParsedCVData } from "@cv/ai-parser"; - -/** - * Mock AI response for the plain text CV (John Smith) - */ -export const mockJohnSmithParsedCV: ParsedCVData = { - personalInfo: { - name: "John Smith", - headline: undefined, - introduction: - "Experienced software engineer with 8+ years of experience building scalable web applications and distributed systems.", - city: undefined, - country: undefined, - phone: undefined, - website: undefined, - linkedInUrl: undefined, - }, - jobExperiences: [ - { - companyName: "Acme Corporation", - roleName: "Senior Software Engineer", - levelName: "Senior", - startDate: "2020-01-01", - endDate: null, - description: - "Led development of microservices architecture serving 10M+ users. Mentored team of 5 junior developers.", - skills: ["TypeScript", "Node.js", "Kubernetes", "PostgreSQL", "Redis"], - }, - { - companyName: "TechStart Inc", - roleName: "Software Engineer", - levelName: "Mid-level", - startDate: "2016-03-01", - endDate: "2019-12-31", - description: - "Built REST APIs and GraphQL endpoints for e-commerce platform. Optimized database queries.", - skills: ["JavaScript", "Python", "MySQL", "Docker", "AWS"], - }, - { - companyName: "WebDev Agency", - roleName: "Junior Developer", - levelName: "Junior", - startDate: "2014-06-01", - endDate: "2016-02-28", - description: "Developed responsive websites for various clients.", - skills: ["PHP", "JavaScript", "HTML", "CSS"], - }, - ], - education: [ - { - institutionName: "Stanford University", - degree: "Bachelor of Science", - fieldOfStudy: "Computer Science", - startDate: "2010-09-01", - endDate: "2014-06-30", - description: "GPA: 3.8/4.0, Dean's List", - skills: ["Algorithms", "Data Structures", "C++"], - }, - ], - skills: [ - "TypeScript", - "JavaScript", - "Python", - "Node.js", - "React", - "GraphQL", - "PostgreSQL", - "MongoDB", - "Redis", - "Docker", - "Kubernetes", - "AWS", - "Git", - ], -}; - -/** - * Mock AI response for the markdown CV (Jane Doe) - */ -export const mockJaneDoeParsedCV: ParsedCVData = { - personalInfo: { - name: "Jane Doe", - headline: undefined, - introduction: - "Passionate full-stack developer with 6 years of experience in building modern web applications.", - city: undefined, - country: undefined, - phone: undefined, - website: undefined, - linkedInUrl: undefined, - }, - jobExperiences: [ - { - companyName: "Global Tech Solutions", - roleName: "Lead Developer", - levelName: "Lead", - startDate: "2021-04-01", - endDate: null, - description: - "Architected and led development of customer portal used by 50K+ enterprises.", - skills: [ - "React", - "TypeScript", - "NestJS", - "PostgreSQL", - "RabbitMQ", - "AWS", - ], - }, - { - companyName: "StartupXYZ", - roleName: "Software Engineer", - levelName: "Mid-level", - startDate: "2018-08-01", - endDate: "2021-03-31", - description: "Built real-time collaboration features using WebSockets.", - skills: ["React", "React Native", "Node.js", "MongoDB", "Firebase"], - }, - { - companyName: "Digital Creations Ltd", - roleName: "Junior Software Developer", - levelName: "Junior", - startDate: "2017-01-01", - endDate: "2018-07-31", - description: "Created RESTful APIs for content management system.", - skills: ["Express.js", "JavaScript", "MySQL", "Jest"], - }, - ], - education: [ - { - institutionName: "Columbia University", - degree: "Master of Science", - fieldOfStudy: "Software Engineering", - startDate: "2015-09-01", - endDate: "2017-05-31", - description: "Focus: Distributed Systems and Cloud Computing", - skills: [], - }, - { - institutionName: "NYU", - degree: "Bachelor of Arts", - fieldOfStudy: "Computer Science", - startDate: "2011-09-01", - endDate: "2015-05-31", - description: "Minor: Mathematics, Graduated Magna Cum Laude", - skills: [], - }, - ], - skills: [ - "TypeScript", - "JavaScript", - "Python", - "Go", - "React", - "React Native", - "Next.js", - "Vue.js", - "Node.js", - "NestJS", - "Express", - "GraphQL", - "PostgreSQL", - "MongoDB", - "Redis", - "AWS", - "Docker", - "Kubernetes", - "Terraform", - ], -}; - -/** - * Mock AI response wrapped in markdown code block (common LLM output format) - */ -export const createMockAIJsonResponse = (data: ParsedCVData): string => - `\`\`\`json -${JSON.stringify(data, null, 2)} -\`\`\``; - -/** - * Mock AI response as raw JSON (alternative LLM output format) - */ -export const createMockAIRawJsonResponse = (data: ParsedCVData): string => - JSON.stringify(data); diff --git a/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.md b/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.md deleted file mode 100644 index 1dc52d2..0000000 --- a/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.md +++ /dev/null @@ -1,62 +0,0 @@ -# Jane Doe -**Full Stack Developer** | New York, NY | jane.doe@email.com - ---- - -## Summary -Passionate full-stack developer with 6 years of experience in building modern web applications. Strong background in React, Node.js, and cloud technologies. Committed to writing maintainable, well-tested code. - ---- - -## Experience - -### Lead Developer -**Global Tech Solutions** | April 2021 - Present - -- Architected and led development of customer portal used by 50K+ enterprises -- Established coding standards and code review processes for team of 8 engineers -- Migrated legacy monolith to event-driven microservices architecture - -**Technologies:** React, TypeScript, NestJS, PostgreSQL, RabbitMQ, AWS - -### Software Engineer -**StartupXYZ** | August 2018 - March 2021 - -- Built real-time collaboration features using WebSockets -- Developed mobile-responsive UI components with React Native -- Integrated third-party payment and authentication services - -**Technologies:** React, React Native, Node.js, MongoDB, Firebase - -### Junior Software Developer -**Digital Creations Ltd** | January 2017 - July 2018 - -- Created RESTful APIs for content management system -- Wrote unit and integration tests achieving 85% code coverage -- Participated in agile ceremonies and sprint planning - -**Technologies:** Express.js, JavaScript, MySQL, Jest - ---- - -## Education - -### Master of Science in Software Engineering -**Columbia University** | September 2015 - May 2017 -- Focus: Distributed Systems and Cloud Computing -- Thesis: "Scalable Event-Driven Architectures for Real-Time Applications" - -### Bachelor of Arts in Computer Science -**NYU** | September 2011 - May 2015 -- Minor: Mathematics -- Graduated Magna Cum Laude - ---- - -## Skills - -**Languages:** TypeScript, JavaScript, Python, Go -**Frontend:** React, React Native, Next.js, Vue.js -**Backend:** Node.js, NestJS, Express, GraphQL -**Databases:** PostgreSQL, MongoDB, Redis -**Cloud & DevOps:** AWS, Docker, Kubernetes, Terraform, CI/CD diff --git a/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt b/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt deleted file mode 100644 index 646fbda..0000000 --- a/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt +++ /dev/null @@ -1,42 +0,0 @@ -John Smith -Software Engineer | San Francisco, CA - -PROFESSIONAL SUMMARY -Experienced software engineer with 8+ years of experience building scalable web applications and distributed systems. Passionate about clean code, testing, and mentoring junior developers. - -WORK EXPERIENCE - -Senior Software Engineer -Acme Corporation -January 2020 - Present -- Led development of microservices architecture serving 10M+ users -- Mentored team of 5 junior developers -- Implemented CI/CD pipelines reducing deployment time by 60% -Skills: TypeScript, Node.js, Kubernetes, PostgreSQL, Redis - -Software Engineer -TechStart Inc -March 2016 - December 2019 -- Built REST APIs and GraphQL endpoints for e-commerce platform -- Optimized database queries improving response times by 40% -- Collaborated with product team on feature specifications -Skills: JavaScript, Python, MySQL, Docker, AWS - -Junior Developer -WebDev Agency -June 2014 - February 2016 -- Developed responsive websites for various clients -- Maintained legacy PHP applications -Skills: PHP, JavaScript, HTML, CSS - -EDUCATION - -Bachelor of Science in Computer Science -Stanford University -September 2010 - June 2014 -- GPA: 3.8/4.0 -- Dean's List -Skills: Algorithms, Data Structures, C++ - -SKILLS -TypeScript, JavaScript, Python, Node.js, React, GraphQL, PostgreSQL, MongoDB, Redis, Docker, Kubernetes, AWS, Git diff --git a/packages/core/src/modules/cv-parser/ai-provider-resolver.service.ts b/packages/core/src/modules/cv-parser/ai-provider-resolver.service.ts deleted file mode 100644 index ba7b2ce..0000000 --- a/packages/core/src/modules/cv-parser/ai-provider-resolver.service.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { - AI_PROVIDER, - type AIProvider, - AnthropicProvider, - OpenAIProvider, -} from "@cv/ai-provider"; -import type { User } from "../auth"; -import { - BadRequestException, - ForbiddenException, - Inject, - Injectable, - Optional, -} from "@nestjs/common"; -import { AiCallLogService } from "../admin/ai-call-log.service"; -import { LoggingAIProvider } from "../admin/logging-ai-provider"; -import { UserAiSettingsService } from "../user-settings/user-ai-settings.service"; - -const DEFAULT_BASE_URLS: Record = { - anthropic: "https://api.anthropic.com", - openai: "https://api.openai.com", -}; - -/** - * Resolves the correct AIProvider for a given user based on their AI preference. - */ -@Injectable() -export class AIProviderResolverService { - constructor( - @Inject(AI_PROVIDER) - @Optional() - private readonly globalProvider: AIProvider | undefined, - private readonly userAiSettingsService: UserAiSettingsService, - private readonly aiCallLogService: AiCallLogService, - ) {} - - async resolveForUser(user: User): Promise { - try { - return await this.resolveProvider(user); - } catch (err) { - this.aiCallLogService.record({ - id: randomUUID(), - timestamp: new Date().toISOString(), - providerName: "resolution", - durationMs: 0, - status: "error", - error: err instanceof Error ? err.message : String(err), - userId: user.id, - source: "resolution", - }); - throw err; - } - } - - private async resolveProvider(user: User): Promise { - const settings = await this.userAiSettingsService.getOrCreateSettings(user); - - if (settings.aiPreference === "NO_AI") { - throw new ForbiddenException( - "AI is disabled. Enable it in profile settings.", - ); - } - - if (settings.aiPreference === "PLATFORM") { - if (!this.globalProvider) { - throw new BadRequestException( - "Platform AI is not available. Configure your own provider in profile settings.", - ); - } - return new LoggingAIProvider(this.globalProvider, this.aiCallLogService, { - userId: user.id, - source: "platform", - }); - } - - const resolved = - await this.userAiSettingsService.resolveActiveProvider(user); - - return this.instantiateProvider(resolved, user.id); - } - - private instantiateProvider( - config: { - providerType: string; - decryptedApiKey: string; - model: string | null; - baseUrl: string | null; - }, - userId: string, - ): AIProvider { - const baseUrl = - config.baseUrl || DEFAULT_BASE_URLS[config.providerType] || ""; - - const providerConfig = { - baseUrl, - apiKey: config.decryptedApiKey, - ...(config.model != null && { model: config.model }), - }; - - const provider = (() => { - if (config.providerType === "anthropic") - return new AnthropicProvider(providerConfig); - if (config.providerType === "openai") - return new OpenAIProvider(providerConfig); - throw new BadRequestException( - `Unsupported provider type: ${config.providerType}`, - ); - })(); - - return new LoggingAIProvider(provider, this.aiCallLogService, { - userId, - source: "byok", - }); - } -} diff --git a/packages/core/src/modules/cv-parser/cv-parser.module.ts b/packages/core/src/modules/cv-parser/cv-parser.module.ts deleted file mode 100644 index e41009e..0000000 --- a/packages/core/src/modules/cv-parser/cv-parser.module.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { randomBytes } from "node:crypto"; -import { mkdirSync } from "node:fs"; -import { join } from "node:path"; -import { CVParserModule as CVParserCoreModule } from "@cv/ai-parser"; -import { AIModule } from "@cv/ai-provider"; -import { FileExtractionModule } from "@cv/file-upload"; -import { DatabaseModule } from "../database"; -import { Module } from "@nestjs/common"; -import { MulterModule } from "@nestjs/platform-express"; -import { diskStorage } from "multer"; -import { DataImportModule } from "../data-import/data-import.module"; -import { FileImportSource } from "../data-import/sources/file-import-source"; -import { EducationModule } from "../education/education.module"; -import { EmploymentModule } from "../job-experience/employment/employment.module"; -import { ProfileModule } from "../profile/profile.module"; -import { UserAiSettingsModule } from "../user-settings/user-ai-settings.module"; -import { AIProviderResolverService } from "./ai-provider-resolver.service"; -import { CVParserService } from "./cv-parser.service"; -import { EntityResolverService } from "./entity-resolver.service"; -import { FileUploadController } from "./file-upload.controller"; -import { ImportOnboardingStep } from "./onboarding/import.step"; - -@Module({ - imports: [ - CVParserCoreModule.forConfig(), - AIModule.forConfig(), - FileExtractionModule.forRoot(), - DatabaseModule, - UserAiSettingsModule, - ProfileModule, - EmploymentModule, - EducationModule, - DataImportModule, - MulterModule.register({ - storage: diskStorage({ - destination: (_req, _file, cb) => { - const tmpDir = join(process.cwd(), "tmp", "cv-uploads"); - mkdirSync(tmpDir, { recursive: true }); - cb(null, tmpDir); - }, - filename: (_req, file, cb) => { - const uniqueSuffix = `${Date.now()}-${randomBytes(8).toString("hex")}`; - const ext = file.originalname.split(".").pop() ?? ""; - cb(null, `${uniqueSuffix}.${ext}`); - }, - }), - limits: { - fileSize: 10 * 1024 * 1024, // 10MB - }, - fileFilter: (_req, file, cb) => { - const allowedMimes = [ - "application/pdf", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "text/plain", - "text/markdown", - ]; - - if (allowedMimes.includes(file.mimetype)) { - cb(null, true); - } else { - cb( - new Error( - `File type ${file.mimetype} not allowed. Supported: PDF, DOCX, TXT, MD`, - ), - false, - ); - } - }, - }), - ], - providers: [ - EntityResolverService, - AIProviderResolverService, - CVParserService, - ImportOnboardingStep, - FileImportSource, - ], - controllers: [FileUploadController], - exports: [CVParserService], -}) -export class CVParserModule {} diff --git a/packages/core/src/modules/cv-parser/cv-parser.service.ts b/packages/core/src/modules/cv-parser/cv-parser.service.ts deleted file mode 100644 index cf687a5..0000000 --- a/packages/core/src/modules/cv-parser/cv-parser.service.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { CVParserService as CVParser, type ParsedCVData } from "@cv/ai-parser"; -import type { User } from "../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, - }; - }), - ); - } -} diff --git a/packages/core/src/modules/cv-parser/entity-resolver.service.ts b/packages/core/src/modules/cv-parser/entity-resolver.service.ts deleted file mode 100644 index a8a4539..0000000 --- a/packages/core/src/modules/cv-parser/entity-resolver.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { PrismaService } from "../database"; -import { Injectable } from "@nestjs/common"; - -/** - * Draft entity representation - ID is null if no match found - */ -export interface DraftEntity { - id: string | null; - name: string; -} - -/** - * Resolved job experience with draft entities - */ -export interface ResolvedJobExperience { - company: DraftEntity; - role: DraftEntity; - level: DraftEntity; - skills: DraftEntity[]; - startDate: Date; - endDate: Date | null; - description: string | null; -} - -/** - * Resolved education with draft entities - */ -export interface ResolvedEducation { - institution: DraftEntity; - degree: string; - fieldOfStudy: string | null; - skills: DraftEntity[]; - startDate: Date; - endDate: Date | null; - description: string | null; -} - -/** - * Service for resolving entity names to existing database entities - * Returns stubs with null IDs for unmatched names - */ -@Injectable() -export class EntityResolverService { - constructor(private readonly prisma: PrismaService) {} - - /** - * Resolve a company name to an existing entity - */ - async resolveCompany(name: string): Promise { - const match = await this.prisma.company.findFirst({ - where: { name: { equals: name, mode: "insensitive" } }, - }); - - return match ? { id: match.id, name: match.name } : { id: null, name }; - } - - /** - * Resolve a role name to an existing entity - */ - async resolveRole(name: string): Promise { - const match = await this.prisma.role.findFirst({ - where: { name: { equals: name, mode: "insensitive" } }, - }); - - return match ? { id: match.id, name: match.name } : { id: null, name }; - } - - /** - * Resolve a level name to an existing entity - * Falls back to "Mid-level" if not provided - */ - async resolveLevel(name?: string): Promise { - const searchName = name || "Mid-level"; - const match = await this.prisma.level.findFirst({ - where: { name: { equals: searchName, mode: "insensitive" } }, - }); - - return match - ? { id: match.id, name: match.name } - : { id: null, name: searchName }; - } - - /** - * Resolve a skill name to an existing entity - */ - async resolveSkill(name: string): Promise { - const trimmed = name.trim(); - if (!trimmed) { - return { id: null, name: "" }; - } - - const match = await this.prisma.skill.findFirst({ - where: { name: { equals: trimmed, mode: "insensitive" } }, - }); - - return match - ? { id: match.id, name: match.name } - : { id: null, name: trimmed }; - } - - /** - * Resolve an institution name to an existing entity - */ - async resolveInstitution(name: string): Promise { - const match = await this.prisma.institution.findFirst({ - where: { name: { equals: name, mode: "insensitive" } }, - }); - - return match ? { id: match.id, name: match.name } : { id: null, name }; - } - - /** - * Resolve multiple skills in parallel - */ - async resolveSkills(names: string[]): Promise { - const uniqueNames = [...new Set(names.filter((s) => s?.trim()))]; - - return Promise.all(uniqueNames.map((name) => this.resolveSkill(name))); - } -} diff --git a/packages/core/src/modules/cv-parser/file-upload.controller.ts b/packages/core/src/modules/cv-parser/file-upload.controller.ts deleted file mode 100644 index a5e84f0..0000000 --- a/packages/core/src/modules/cv-parser/file-upload.controller.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { createHash } from "node:crypto"; -import { readFile, unlink } from "node:fs/promises"; -import type { User as DomainUser } from "../auth"; -import { validateFile } from "@cv/file-upload"; -import { - BadRequestException, - Body, - Controller, - Logger, - Post, - Req, - UploadedFile, - UseInterceptors, -} from "@nestjs/common"; -import { FileInterceptor } from "@nestjs/platform-express"; -import { FileImportSource } from "../data-import/sources/file-import-source"; -import { ImportService } from "../data-import/import.service"; - -@Controller("api/cv-parser") -export class FileUploadController { - private readonly logger = new Logger(FileUploadController.name); - - constructor( - private readonly importService: ImportService, - private readonly fileImportSource: FileImportSource, - ) {} - - @Post("upload") - @UseInterceptors(FileInterceptor("file")) - async upload( - @Req() req: { user: DomainUser }, - @Body("profileId") profileId: string, - @UploadedFile() file?: Express.Multer.File, - ) { - if (!file) { - throw new BadRequestException("No file provided"); - } - - if (!profileId) { - throw new BadRequestException("profileId is required"); - } - - const filePath = file.path; - - try { - const buffer = file.buffer ?? (await readFile(filePath)); - - const validation = validateFile({ - buffer, - mimeType: file.mimetype, - originalName: file.originalname, - sizeBytes: file.size, - }); - - if (!validation.valid) { - throw new Error(`File validation failed: ${validation.error}`); - } - - const fingerprint = createHash("sha256").update(buffer).digest("hex"); - - const userFile = await this.importService.createImport( - req.user, - profileId, - this.fileImportSource, - { - fileName: file.originalname, - mimeType: file.mimetype, - sizeBytes: file.size, - fingerprint, - }, - { buffer, mimeType: file.mimetype }, - ); - - return { - userFileId: userFile.id, - status: userFile.status, - fileName: userFile.fileName, - sizeBytes: userFile.sizeBytes, - }; - } catch (error) { - this.logger.error(`File upload failed: ${error instanceof Error ? error.message : error}`, error instanceof Error ? error.stack : undefined); - - const message = - error instanceof Error ? error.message : "Failed to process file"; - - throw new BadRequestException(message); - } finally { - if (filePath) { - try { - await unlink(filePath); - } catch (err) { - this.logger.warn(`Failed to clean up temporary file ${filePath}: ${err instanceof Error ? err.message : err}`); - } - } - } - } -} diff --git a/packages/core/src/modules/cv-parser/onboarding/import.step.ts b/packages/core/src/modules/cv-parser/onboarding/import.step.ts deleted file mode 100644 index 051c1ba..0000000 --- a/packages/core/src/modules/cv-parser/onboarding/import.step.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { User } from "../../auth"; -import { PrismaService } from "../../database"; -import { Injectable } from "@nestjs/common"; -import { OnboardingStep } from "../../onboarding/onboarding-step.decorator"; -import { - type OnboardingStepDefinition, - OnboardingStepStatus, -} from "../../onboarding/onboarding-step.interface"; - -@Injectable() -@OnboardingStep() -export class ImportOnboardingStep implements OnboardingStepDefinition { - readonly name = "import"; - readonly order = 1; - readonly dependsOn = ["ai-preference"]; - - constructor(private readonly prisma: PrismaService) {} - - async computeStatus(user: User): Promise { - const [jobCount, eduCount] = await Promise.all([ - this.prisma.userJobExperience.count({ - where: { profile: { userId: user.id } }, - }), - this.prisma.education.count({ - where: { profile: { userId: user.id } }, - }), - ]); - - return jobCount > 0 || eduCount > 0 - ? OnboardingStepStatus.COMPLETE - : OnboardingStepStatus.NOT_STARTED; - } -} diff --git a/packages/core/src/modules/cv-parser/types/draft.types.ts b/packages/core/src/modules/cv-parser/types/draft.types.ts deleted file mode 100644 index ac2d177..0000000 --- a/packages/core/src/modules/cv-parser/types/draft.types.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Field, ID, ObjectType } from "@nestjs/graphql"; -import type { - DraftEntity, - ResolvedEducation, - ResolvedJobExperience, -} from "../entity-resolver.service"; - -/** - * Draft entity - represents an entity that may or may not exist in the database - * If id is null, the entity needs to be created on save - */ -@ObjectType() -export class DraftEntityType { - @Field(() => ID, { nullable: true }) - id: string | null; - - @Field() - name: string; - - constructor(id: string | null, name: string) { - this.id = id; - this.name = name; - } - - static fromDraft(draft: DraftEntity): DraftEntityType { - return new DraftEntityType(draft.id, draft.name); - } -} - -/** - * Draft job experience - parsed from CV with resolved entities - */ -@ObjectType() -export class DraftJobExperienceType { - @Field(() => DraftEntityType) - company: DraftEntityType; - - @Field(() => DraftEntityType) - role: DraftEntityType; - - @Field(() => DraftEntityType) - level: DraftEntityType; - - @Field(() => [DraftEntityType]) - skills: DraftEntityType[]; - - @Field() - startDate: Date; - - @Field(() => Date, { nullable: true }) - endDate: Date | null; - - @Field(() => String, { nullable: true }) - description: string | null; - - constructor( - company: DraftEntityType, - role: DraftEntityType, - level: DraftEntityType, - skills: DraftEntityType[], - startDate: Date, - endDate: Date | null, - description: string | null, - ) { - this.company = company; - this.role = role; - this.level = level; - this.skills = skills; - this.startDate = startDate; - this.endDate = endDate; - this.description = description; - } - - static fromResolved(resolved: ResolvedJobExperience): DraftJobExperienceType { - return new DraftJobExperienceType( - DraftEntityType.fromDraft(resolved.company), - DraftEntityType.fromDraft(resolved.role), - DraftEntityType.fromDraft(resolved.level), - resolved.skills.map(DraftEntityType.fromDraft), - resolved.startDate, - resolved.endDate, - resolved.description, - ); - } -} - -/** - * Draft education - parsed from CV with resolved entities - */ -@ObjectType() -export class DraftEducationType { - @Field(() => DraftEntityType) - institution: DraftEntityType; - - @Field() - degree: string; - - @Field(() => String, { nullable: true }) - fieldOfStudy: string | null; - - @Field(() => [DraftEntityType]) - skills: DraftEntityType[]; - - @Field() - startDate: Date; - - @Field(() => Date, { nullable: true }) - endDate: Date | null; - - @Field(() => String, { nullable: true }) - description: string | null; - - constructor( - institution: DraftEntityType, - degree: string, - fieldOfStudy: string | null, - skills: DraftEntityType[], - startDate: Date, - endDate: Date | null, - description: string | null, - ) { - this.institution = institution; - this.degree = degree; - this.fieldOfStudy = fieldOfStudy; - this.skills = skills; - this.startDate = startDate; - this.endDate = endDate; - this.description = description; - } - - static fromResolved(resolved: ResolvedEducation): DraftEducationType { - return new DraftEducationType( - DraftEntityType.fromDraft(resolved.institution), - resolved.degree, - resolved.fieldOfStudy, - resolved.skills.map(DraftEntityType.fromDraft), - resolved.startDate, - resolved.endDate, - resolved.description, - ); - } -} - -/** - * Parsed CV data with resolved entities - */ -@ObjectType() -export class ParsedCVDataWithResolutionType { - @Field(() => [DraftJobExperienceType]) - jobExperiences: DraftJobExperienceType[]; - - @Field(() => [DraftEducationType]) - education: DraftEducationType[]; - - constructor( - jobExperiences: DraftJobExperienceType[], - education: DraftEducationType[], - ) { - this.jobExperiences = jobExperiences; - this.education = education; - } -} diff --git a/packages/core/src/modules/data-import/data-import-source.interface.ts b/packages/core/src/modules/data-import/data-import-source.interface.ts deleted file mode 100644 index a7c4442..0000000 --- a/packages/core/src/modules/data-import/data-import-source.interface.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ParsedCVData } from "@cv/ai-parser"; -import type { User } from "../auth"; - -export interface DataImportSource { - readonly name: string; - execute( - user: User, - params: Record, - onStatus: (message: string) => Promise, - ): Promise; -} diff --git a/packages/core/src/modules/data-import/data-import.module.ts b/packages/core/src/modules/data-import/data-import.module.ts deleted file mode 100644 index afbae43..0000000 --- a/packages/core/src/modules/data-import/data-import.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AuthorizationModule } from "../auth"; -import { DatabaseModule } from "../database"; -import { Module } from "@nestjs/common"; -import { EntityResolverService } from "../cv-parser/entity-resolver.service"; -import { ProfileModule } from "../profile/profile.module"; -import { ImportService } from "./import.service"; -import { ImportJobMapper } from "./import-job.mapper"; -import { ImportJobPolicy } from "./import-job.policy"; -import { ImportJobListener } from "./listeners/import-job.listener"; -import { UserFileMapper } from "./user-file.mapper"; -import { UserFilePolicy } from "./user-file.policy"; - -@Module({ - imports: [DatabaseModule, AuthorizationModule, ProfileModule], - providers: [ - ImportService, - EntityResolverService, - UserFileMapper, - UserFilePolicy, - ImportJobMapper, - ImportJobPolicy, - ImportJobListener, - ], - exports: [ImportService, UserFileMapper, ImportJobMapper], -}) -export class DataImportModule {} diff --git a/packages/core/src/modules/data-import/events/user-file-created.event.ts b/packages/core/src/modules/data-import/events/user-file-created.event.ts deleted file mode 100644 index 68195f4..0000000 --- a/packages/core/src/modules/data-import/events/user-file-created.event.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DomainEvent } from "../../events"; -import type { User } from "../../auth"; -import type { DataImportSource } from "../data-import-source.interface"; - -interface UserFileCreatedPayload { - userFileId: string; - user: User; - source: DataImportSource; - params: Record; -} - -export class UserFileCreatedEvent extends DomainEvent {} diff --git a/packages/core/src/modules/data-import/import-job.entity.ts b/packages/core/src/modules/data-import/import-job.entity.ts deleted file mode 100644 index de6b1bf..0000000 --- a/packages/core/src/modules/data-import/import-job.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseEntity } from "../../shared"; - -export class ImportJob extends BaseEntity { - constructor( - id: string, - public readonly userFileId: string, - public readonly source: string, - public readonly status: string, - public readonly statusMessage: string, - public readonly error: string | null, - public readonly startedAt: Date | null, - public readonly completedAt: Date | null, - createdAt: Date, - updatedAt: Date, - ) { - super(id, createdAt, updatedAt); - } -} diff --git a/packages/core/src/modules/data-import/import-job.mapper.ts b/packages/core/src/modules/data-import/import-job.mapper.ts deleted file mode 100644 index d07c68a..0000000 --- a/packages/core/src/modules/data-import/import-job.mapper.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { BaseMapper } from "../../shared"; -import { Injectable } from "@nestjs/common"; -import type { Prisma } from "@prisma/client"; - -type PrismaImportJob = Prisma.ImportJobGetPayload; - -import { ImportJob } from "./import-job.entity"; - -@Injectable() -export class ImportJobMapper implements BaseMapper { - toDomain(prisma: null): null; - toDomain(prisma: PrismaImportJob): ImportJob; - toDomain(prisma: PrismaImportJob | null): ImportJob | null; - toDomain(prisma: PrismaImportJob | null): ImportJob | null { - if (!prisma) return null; - - return new ImportJob( - prisma.id, - prisma.userFileId, - prisma.source, - prisma.status, - prisma.statusMessage, - prisma.error, - prisma.startedAt, - prisma.completedAt, - prisma.createdAt, - prisma.updatedAt, - ); - } - - mapToDomain(items: PrismaImportJob[]): ImportJob[] { - return items - .map((item) => this.toDomain(item)) - .filter((item): item is ImportJob => item !== null); - } -} diff --git a/packages/core/src/modules/data-import/import-job.policy.ts b/packages/core/src/modules/data-import/import-job.policy.ts deleted file mode 100644 index f586a2c..0000000 --- a/packages/core/src/modules/data-import/import-job.policy.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { IPolicy, User } from "../auth"; -import { Policy } from "../auth"; -import { Injectable } from "@nestjs/common"; -import { ImportJob } from "./import-job.entity"; - -/** - * ImportJob is an internal/admin-only entity. - * All user-facing operations go through UserFile instead. - */ -@Injectable() -@Policy(ImportJob) -export class ImportJobPolicy implements IPolicy { - view(_user: User, _resource: ImportJob): boolean { - return false; - } - - create(_user: User, _resource?: Partial): boolean { - return false; - } - - update(_user: User, _resource: ImportJob): boolean { - return false; - } - - delete(_user: User, _resource: ImportJob): boolean { - return false; - } -} diff --git a/packages/core/src/modules/data-import/import.service.ts b/packages/core/src/modules/data-import/import.service.ts deleted file mode 100644 index 480b0f7..0000000 --- a/packages/core/src/modules/data-import/import.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { User } from "../auth"; -import { PrismaService } from "../database"; -import { Injectable } from "@nestjs/common"; -import { EventService } from "../events"; -import type { DataImportSource } from "./data-import-source.interface"; -import { UserFileCreatedEvent } from "./events/user-file-created.event"; -import { UserFile } from "./user-file.entity"; -import { UserFileMapper } from "./user-file.mapper"; - -@Injectable() -export class ImportService { - constructor( - private readonly prisma: PrismaService, - private readonly userFileMapper: UserFileMapper, - private readonly eventService: EventService, - ) {} - - /** - * Find an existing completed UserFile with the same fingerprint for this user. - */ - async findDuplicateForUser( - userId: string, - fingerprint: string, - ): Promise { - const record = await this.prisma.userFile.findFirst({ - where: { - fingerprint, - profile: { userId }, - status: "completed", - }, - orderBy: { createdAt: "desc" }, - }); - return this.userFileMapper.toDomain(record); - } - - /** - * Create a UserFile and emit an event for async processing. - */ - async createImport( - user: User, - profileId: string, - source: DataImportSource, - file: { fileName: string; mimeType: string; sizeBytes: number; fingerprint?: string }, - params: Record, - ): Promise { - const record = await this.prisma.userFile.create({ - data: { - profile: { connect: { id: profileId } }, - fingerprint: file.fingerprint ?? null, - fileName: file.fileName, - mimeType: file.mimeType, - sizeBytes: file.sizeBytes, - source: source.name, - status: "pending", - statusMessage: "Queued for processing", - }, - }); - - this.eventService.emit( - new UserFileCreatedEvent({ userFileId: record.id, user, source, params }), - ); - - return this.userFileMapper.toDomain(record) as UserFile; - } - - /** - * Delete a UserFile and its cascading ImportJobs. - */ - async deleteUserFile(id: string): Promise { - await this.prisma.userFile.delete({ where: { id } }); - } - - async findUserFileById(id: string): Promise { - const record = await this.prisma.userFile.findUnique({ where: { id } }); - return this.userFileMapper.toDomain(record); - } - - async findUserFilesForUser(userId: string): Promise { - const records = await this.prisma.userFile.findMany({ - where: { profile: { userId } }, - orderBy: { createdAt: "desc" }, - }); - return this.userFileMapper.mapToDomain(records); - } -} diff --git a/packages/core/src/modules/data-import/listeners/import-job.listener.ts b/packages/core/src/modules/data-import/listeners/import-job.listener.ts deleted file mode 100644 index 4e0c3fb..0000000 --- a/packages/core/src/modules/data-import/listeners/import-job.listener.ts +++ /dev/null @@ -1,268 +0,0 @@ -import type { ParsedCVData } from "@cv/ai-parser"; -import { PrismaService } from "../../database"; -import { Injectable, Logger } from "@nestjs/common"; -import { OnEvent } from "@nestjs/event-emitter"; -import { - EntityResolverService, - type ResolvedEducation, - type ResolvedJobExperience, -} from "../../cv-parser/entity-resolver.service"; -import { ProfileService } from "../../profile/profile.service"; -import { UserFileCreatedEvent } from "../events/user-file-created.event"; - -interface ResolvedResult { - personalInfo?: { - name?: string | undefined; - headline?: string | undefined; - introduction?: string | undefined; - city?: string | undefined; - country?: string | undefined; - phone?: string | undefined; - website?: string | undefined; - linkedInUrl?: string | undefined; - }; - jobExperiences: ResolvedJobExperience[]; - education: ResolvedEducation[]; -} - -@Injectable() -export class ImportJobListener { - private readonly logger = new Logger(ImportJobListener.name); - - constructor( - private readonly prisma: PrismaService, - private readonly entityResolver: EntityResolverService, - private readonly profileService: ProfileService, - ) {} - - @OnEvent(UserFileCreatedEvent.eventName, { async: true }) - async handleUserFileCreated(event: UserFileCreatedEvent): Promise { - const job = await this.prisma.importJob.create({ - data: { - userFileId: event.payload.userFileId, - source: event.payload.source.name, - status: "pending", - statusMessage: "Queued", - }, - }); - - await this.processJob(job.id, event); - } - - private async processJob( - jobId: string, - event: UserFileCreatedEvent, - ): Promise { - const { userFileId, user, source, params } = event.payload; - const tag = `job=${jobId} source=${source.name}`; - - try { - await this.startJob(jobId, userFileId); - - const userFile = await this.prisma.userFile.findUniqueOrThrow({ - where: { id: userFileId }, - select: { profileId: true }, - }); - - const onStatus = async (message: string) => { - this.logger.log(`${tag} ${message}`); - await this.updateStatusMessage(jobId, userFileId, message); - }; - - const parsed: ParsedCVData = await source.execute(user, params, onStatus); - - this.logger.log( - `${tag} Parsed: ${parsed.jobExperiences.length} job(s), ` + - `${parsed.education.length} education(s), ${parsed.skills.length} skill(s)`, - ); - this.logger.debug(`${tag} Parsed result: ${JSON.stringify(parsed, null, 2)}`); - - await this.updateStatusMessage(jobId, userFileId, "Resolving entities"); - const resolved = await this.resolveEntities(parsed); - - if (parsed.personalInfo) { - resolved.personalInfo = parsed.personalInfo; - } - - this.logger.log(`${tag} Entity resolution complete`); - - await this.populateProfile(userFile.profileId, parsed, tag); - await this.completeJob(jobId, userFileId, resolved); - } catch (err) { - const message = err instanceof Error ? err.message : "Processing failed"; - this.logger.error(`${tag} Import failed: ${message}`, err instanceof Error ? err.stack : undefined); - await this.failJob(jobId, userFileId, message); - } - } - - private async resolveEntities(parsed: ParsedCVData): Promise { - const [jobExperiences, education] = await Promise.all([ - Promise.all( - parsed.jobExperiences.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, - }; - }), - ), - Promise.all( - parsed.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, - }; - }), - ), - ]); - - return { jobExperiences, education }; - } - - /** - * Auto-populate empty profile fields from imported personalInfo. - * Only fills fields that are currently empty -- never overwrites existing data. - */ - private async populateProfile( - profileId: string, - parsed: ParsedCVData, - tag: string, - ): Promise { - const info = parsed.personalInfo; - if (!info) return; - - try { - const existing = await this.profileService.findByIdOrFail(profileId); - const updates: Record = {}; - - const fieldMap: Record = { - fullName: info.name, - headline: info.headline, - summary: info.introduction, - city: info.city, - country: info.country, - phone: info.phone, - website: info.website, - linkedInUrl: info.linkedInUrl, - }; - - for (const [field, value] of Object.entries(fieldMap)) { - if (value && !existing[field as keyof typeof existing]) { - updates[field] = value; - } - } - - if (Object.keys(updates).length === 0) { - this.logger.debug(`${tag} Profile already has data, skipping auto-populate`); - return; - } - - await this.profileService.updateProfile(profileId, updates); - this.logger.log(`${tag} Auto-populated profile: ${Object.keys(updates).join(", ")}`); - } catch (err) { - this.logger.warn(`${tag} Failed to auto-populate profile (non-fatal): ${err instanceof Error ? err.message : err}`); - } - } - - private async startJob(jobId: string, userFileId: string): Promise { - await Promise.all([ - this.prisma.importJob.update({ - where: { id: jobId }, - data: { - status: "processing", - statusMessage: "Starting import", - startedAt: new Date(), - }, - }), - this.prisma.userFile.update({ - where: { id: userFileId }, - data: { status: "processing", statusMessage: "Starting import" }, - }), - ]); - } - - private async updateStatusMessage( - jobId: string, - userFileId: string, - message: string, - ): Promise { - await Promise.all([ - this.prisma.importJob.update({ - where: { id: jobId }, - data: { statusMessage: message }, - }), - this.prisma.userFile.update({ - where: { id: userFileId }, - data: { statusMessage: message }, - }), - ]); - } - - private async completeJob( - jobId: string, - userFileId: string, - result: ResolvedResult, - ): Promise { - await Promise.all([ - this.prisma.importJob.update({ - where: { id: jobId }, - data: { - status: "completed", - statusMessage: "Import completed", - completedAt: new Date(), - }, - }), - this.prisma.userFile.update({ - where: { id: userFileId }, - data: { - status: "completed", - statusMessage: "Import completed", - resultJson: JSON.stringify(result), - }, - }), - ]); - } - - private async failJob( - jobId: string, - userFileId: string, - error: string, - ): Promise { - await Promise.all([ - this.prisma.importJob.update({ - where: { id: jobId }, - data: { - status: "failed", - statusMessage: "Import failed", - error, - completedAt: new Date(), - }, - }), - this.prisma.userFile.update({ - where: { id: userFileId }, - data: { status: "failed", statusMessage: "Import failed", error }, - }), - ]); - } -} diff --git a/packages/core/src/modules/data-import/sources/file-import-source.ts b/packages/core/src/modules/data-import/sources/file-import-source.ts deleted file mode 100644 index 7ad9276..0000000 --- a/packages/core/src/modules/data-import/sources/file-import-source.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - CVParserService as CVParser, - type ExistingUserContext, - type ParsedCVData, -} from "@cv/ai-parser"; -import type { User } from "../../auth"; -import { PrismaService } from "../../database"; -import { TextExtractorRegistry, TEXT_EXTRACTOR_REGISTRY } from "@cv/file-upload"; -import { Inject, Injectable, Logger } from "@nestjs/common"; -import { EducationService } from "../../education/education.service"; -import { AIProviderResolverService } from "../../cv-parser/ai-provider-resolver.service"; -import { UserJobExperienceService } from "../../job-experience/employment/user-job-experience.service"; -import { ProfileService } from "../../profile/profile.service"; -import type { DataImportSource } from "../data-import-source.interface"; - -/** - * Build ExistingUserContext from domain models for AI prompt enrichment. - */ -const buildExistingUserContext = ( - profile: { fullName?: string | null; headline?: string | null } | null, - jobs: Array<{ company: { name: string }; role: { name: string }; startDate: Date; endDate?: Date }>, - educations: Array<{ institution: { name: string }; degree: string; startDate: Date; endDate: Date | null }>, -): ExistingUserContext | undefined => { - const context: ExistingUserContext = {}; - - if (profile?.fullName) context.name = profile.fullName; - if (profile?.headline) context.headline = profile.headline; - - if (jobs.length > 0) { - context.jobs = jobs.map((j) => { - const entry: { company: string; role: string; startDate: string; endDate?: string } = { - company: j.company.name, - role: j.role.name, - startDate: j.startDate.toISOString().slice(0, 10), - }; - if (j.endDate) entry.endDate = j.endDate.toISOString().slice(0, 10); - return entry; - }); - } - - if (educations.length > 0) { - context.education = educations.map((e) => { - const entry: { institution: string; degree: string; startDate: string; endDate?: string } = { - institution: e.institution.name, - degree: e.degree, - startDate: e.startDate.toISOString().slice(0, 10), - }; - if (e.endDate) entry.endDate = e.endDate.toISOString().slice(0, 10); - return entry; - }); - } - - const hasData = Object.keys(context).length > 0; - return hasData ? context : undefined; -}; - -/** - * Wraps the existing file extraction + AI parsing pipeline - * as a DataImportSource. - */ -@Injectable() -export class FileImportSource implements DataImportSource { - readonly name = "file-upload"; - private readonly logger = new Logger(FileImportSource.name); - - constructor( - @Inject(TEXT_EXTRACTOR_REGISTRY) - private readonly textExtractorRegistry: TextExtractorRegistry, - private readonly providerResolver: AIProviderResolverService, - private readonly profileService: ProfileService, - private readonly jobExperienceService: UserJobExperienceService, - private readonly educationService: EducationService, - private readonly prisma: PrismaService, - ) {} - - async execute( - user: User, - params: Record, - onStatus: (message: string) => Promise, - ): Promise { - const buffer = params["buffer"] as Buffer; - const mimeType = params["mimeType"] as string; - - await onStatus("Extracting text from file"); - - 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"); - } - - this.logger.log(`Extracted ${extraction.text.length} chars from ${mimeType}`); - this.logger.debug(`Extracted text preview: ${extraction.text.slice(0, 500)}`); - - await onStatus("Gathering existing profile data"); - - const existingContext = await this.gatherUserContext(user); - - await onStatus("Analyzing with AI"); - - const provider = await this.providerResolver.resolveForUser(user); - this.logger.log(`AI provider: ${provider.constructor.name}`); - - const parser = new CVParser(provider); - const result = await parser.parseCVText(extraction.text, existingContext); - - return result; - } - - private async gatherUserContext(user: User): Promise { - try { - const firstProfile = await this.prisma.profile.findFirst({ - where: { userId: user.id }, - select: { id: true }, - }); - - if (!firstProfile) return undefined; - - const [profile, jobs, educationResult] = await Promise.all([ - this.profileService.findByIdOrFail(firstProfile.id), - this.jobExperienceService.findForProfile(firstProfile.id), - this.educationService.findManyForProfile(firstProfile.id), - ]); - - const context = buildExistingUserContext( - profile, - jobs, - educationResult.edges.map((e) => e.node), - ); - - if (context) { - this.logger.debug(`User context: ${JSON.stringify(context)}`); - } - - return context; - } catch (err) { - this.logger.warn( - `Failed to gather user context (non-fatal): ${err instanceof Error ? err.message : err}`, - ); - return undefined; - } - } -} diff --git a/packages/core/src/modules/data-import/user-file.entity.ts b/packages/core/src/modules/data-import/user-file.entity.ts deleted file mode 100644 index 4e353cd..0000000 --- a/packages/core/src/modules/data-import/user-file.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BaseEntity } from "../../shared"; - -export class UserFile extends BaseEntity { - constructor( - id: string, - public readonly profileId: string, - public readonly fingerprint: string | null, - public readonly fileName: string, - public readonly mimeType: string, - public readonly sizeBytes: number, - public readonly source: string, - public readonly status: string, - public readonly statusMessage: string, - public readonly resultJson: string | null, - public readonly error: string | null, - createdAt: Date, - updatedAt: Date, - ) { - super(id, createdAt, updatedAt); - } -} diff --git a/packages/core/src/modules/data-import/user-file.mapper.ts b/packages/core/src/modules/data-import/user-file.mapper.ts deleted file mode 100644 index 935eda9..0000000 --- a/packages/core/src/modules/data-import/user-file.mapper.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { BaseMapper } from "../../shared"; -import { Injectable } from "@nestjs/common"; -import type { Prisma } from "@prisma/client"; - -type PrismaUserFile = Prisma.UserFileGetPayload; - -import { UserFile } from "./user-file.entity"; - -@Injectable() -export class UserFileMapper implements BaseMapper { - toDomain(prisma: null): null; - toDomain(prisma: PrismaUserFile): UserFile; - toDomain(prisma: PrismaUserFile | null): UserFile | null; - toDomain(prisma: PrismaUserFile | null): UserFile | null { - if (!prisma) return null; - - return new UserFile( - prisma.id, - prisma.profileId, - prisma.fingerprint, - prisma.fileName, - prisma.mimeType, - prisma.sizeBytes, - prisma.source, - prisma.status, - prisma.statusMessage, - prisma.resultJson, - prisma.error, - prisma.createdAt, - prisma.updatedAt, - ); - } - - mapToDomain(items: PrismaUserFile[]): UserFile[] { - return items - .map((item) => this.toDomain(item)) - .filter((item): item is UserFile => item !== null); - } -} diff --git a/packages/core/src/modules/data-import/user-file.policy.ts b/packages/core/src/modules/data-import/user-file.policy.ts deleted file mode 100644 index 10efd0f..0000000 --- a/packages/core/src/modules/data-import/user-file.policy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Policy, ProfileOwnedResourcePolicy } from "../auth"; -import { PrismaService } from "../database"; -import { Injectable } from "@nestjs/common"; -import { UserFile } from "./user-file.entity"; - -@Injectable() -@Policy(UserFile) -export class UserFilePolicy extends ProfileOwnedResourcePolicy { - constructor(prisma: PrismaService) { - super(prisma); - } -} diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index 97a53e4..76e960e 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -6,13 +6,6 @@ export default defineConfig({ globals: true, environment: "node", include: ["**/*.{test,spec}.{ts,tsx}"], - // Pre-existing zombie tests written for jest. They were never run (no - // jest dep was ever installed in this package). Migrate to vi.* later or - // delete; until then they are excluded so vitest stays green. - exclude: [ - "**/node_modules/**", - "**/dist/**", - "modules/cv-parser/__tests__/**", - ], + exclude: ["**/node_modules/**", "**/dist/**"], }, });