because I got bored of customising my CV for every job
1import { PrismaService } from "@cv/system";
2import { Injectable } from "@nestjs/common";
3
4/**
5 * Draft entity representation - ID is null if no match found
6 */
7export interface DraftEntity {
8 id: string | null;
9 name: string;
10}
11
12/**
13 * Resolved job experience with draft entities
14 */
15export interface ResolvedJobExperience {
16 company: DraftEntity;
17 role: DraftEntity;
18 level: DraftEntity;
19 skills: DraftEntity[];
20 startDate: Date;
21 endDate: Date | null;
22 description: string | null;
23}
24
25/**
26 * Resolved education with draft entities
27 */
28export interface ResolvedEducation {
29 institution: DraftEntity;
30 degree: string;
31 fieldOfStudy: string | null;
32 skills: DraftEntity[];
33 startDate: Date;
34 endDate: Date | null;
35 description: string | null;
36}
37
38/**
39 * Service for resolving entity names to existing database entities
40 * Returns stubs with null IDs for unmatched names
41 */
42@Injectable()
43export class EntityResolverService {
44 constructor(private readonly prisma: PrismaService) {}
45
46 /**
47 * Resolve a company name to an existing entity
48 */
49 async resolveCompany(name: string): Promise<DraftEntity> {
50 const match = await this.prisma.company.findFirst({
51 where: { name: { equals: name, mode: "insensitive" } },
52 });
53
54 return match ? { id: match.id, name: match.name } : { id: null, name };
55 }
56
57 /**
58 * Resolve a role name to an existing entity
59 */
60 async resolveRole(name: string): Promise<DraftEntity> {
61 const match = await this.prisma.role.findFirst({
62 where: { name: { equals: name, mode: "insensitive" } },
63 });
64
65 return match ? { id: match.id, name: match.name } : { id: null, name };
66 }
67
68 /**
69 * Resolve a level name to an existing entity
70 * Falls back to "Mid-level" if not provided
71 */
72 async resolveLevel(name?: string): Promise<DraftEntity> {
73 const searchName = name || "Mid-level";
74 const match = await this.prisma.level.findFirst({
75 where: { name: { equals: searchName, mode: "insensitive" } },
76 });
77
78 return match
79 ? { id: match.id, name: match.name }
80 : { id: null, name: searchName };
81 }
82
83 /**
84 * Resolve a skill name to an existing entity
85 */
86 async resolveSkill(name: string): Promise<DraftEntity> {
87 const trimmed = name.trim();
88 if (!trimmed) {
89 return { id: null, name: "" };
90 }
91
92 const match = await this.prisma.skill.findFirst({
93 where: { name: { equals: trimmed, mode: "insensitive" } },
94 });
95
96 return match
97 ? { id: match.id, name: match.name }
98 : { id: null, name: trimmed };
99 }
100
101 /**
102 * Resolve an institution name to an existing entity
103 */
104 async resolveInstitution(name: string): Promise<DraftEntity> {
105 const match = await this.prisma.institution.findFirst({
106 where: { name: { equals: name, mode: "insensitive" } },
107 });
108
109 return match ? { id: match.id, name: match.name } : { id: null, name };
110 }
111
112 /**
113 * Resolve multiple skills in parallel
114 */
115 async resolveSkills(names: string[]): Promise<DraftEntity[]> {
116 const uniqueNames = [...new Set(names.filter((s) => s?.trim()))];
117
118 return Promise.all(uniqueNames.map((name) => this.resolveSkill(name)));
119 }
120}