because I got bored of customising my CV for every job
1import { AdminGuard, JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth";
2import type { NamedEntity } from "@cv/system";
3import { ClockService, UuidFactoryService } from "@cv/system";
4import { UseGuards } from "@nestjs/common";
5import { Args, Mutation, Query, Resolver } from "@nestjs/graphql";
6import { ApplicationStatusService } from "@/modules/application/application-status/application-status.service";
7import { ApplicationStatus } from "@/modules/application/application-status/application-status.entity";
8import { SkillService } from "@/modules/job-experience/skill/skill.service";
9import { Skill } from "@/modules/job-experience/skill/skill.entity";
10import { CompanyService } from "@/modules/job-experience/company/company.service";
11import { Company } from "@/modules/job-experience/company/company.entity";
12import { RoleService } from "@/modules/job-experience/role/role.service";
13import { Role } from "@/modules/job-experience/role/role.entity";
14import { LevelService } from "@/modules/job-experience/level/level.service";
15import { Level } from "@/modules/job-experience/level/level.entity";
16import { InstitutionService } from "@/modules/education/institution.service";
17import { Institution } from "@/modules/education/institution.entity";
18import { JobTypeService } from "@/modules/vacancies/job-type/job-type.service";
19import { JobType } from "@/modules/vacancies/job-type/job-type.entity";
20import { OrganizationService } from "@/modules/organization/organization.service";
21import { Organization } from "@/modules/organization/organization.entity";
22import { OrganizationRoleService } from "@/modules/organization/organization-role.service";
23import { OrganizationRole } from "@/modules/organization/organization-role.entity";
24import { AdminEntityType, AdminLookupEntity } from "./admin-lookup.type";
25
26type EntityLike = {
27 id: string;
28 name: string;
29 description?: string | null | undefined;
30 createdAt: Date;
31 updatedAt: Date;
32};
33
34interface LookupAdapter {
35 findMany(searchTerm?: string): Promise<EntityLike[]>;
36 findByIdOrFail(id: string): Promise<EntityLike>;
37 save(entity: EntityLike): Promise<EntityLike>;
38 destroy(entity: EntityLike): Promise<void>;
39 create(id: string, name: string, now: Date, description?: string): EntityLike;
40}
41
42const namedEntityAdapter = <T extends NamedEntity>(
43 service: {
44 findMany(filters?: { searchTerm?: string }): Promise<T[]>;
45 findByIdOrFail(id: string): Promise<T>;
46 save(entity: T): Promise<T>;
47 destroy(entity: T): Promise<void>;
48 },
49 EntityClass: new (
50 id: string,
51 name: string,
52 createdAt: Date,
53 updatedAt: Date,
54 description?: string,
55 ) => T,
56): LookupAdapter => ({
57 findMany: (searchTerm) =>
58 service.findMany(searchTerm ? { searchTerm } : undefined),
59 findByIdOrFail: (id) => service.findByIdOrFail(id),
60 save: (entity) => service.save(entity as T),
61 destroy: (entity) => service.destroy(entity as T),
62 create: (id, name, now, description) =>
63 new EntityClass(id, name, now, now, description),
64});
65
66@Resolver()
67@UseGuards(JwtAuthGuard, VerifiedScopeGuard, AdminGuard)
68export class AdminLookupResolver {
69 private readonly registry: Record<AdminEntityType, LookupAdapter>;
70
71 constructor(
72 private readonly uuid: UuidFactoryService,
73 private readonly clock: ClockService,
74 skillService: SkillService,
75 companyService: CompanyService,
76 roleService: RoleService,
77 levelService: LevelService,
78 institutionService: InstitutionService,
79 jobTypeService: JobTypeService,
80 applicationStatusService: ApplicationStatusService,
81 organizationService: OrganizationService,
82 organizationRoleService: OrganizationRoleService,
83 ) {
84 this.registry = {
85 [AdminEntityType.SKILL]: namedEntityAdapter(skillService, Skill),
86 [AdminEntityType.COMPANY]: namedEntityAdapter(companyService, Company),
87 [AdminEntityType.ROLE]: namedEntityAdapter(roleService, Role),
88 [AdminEntityType.LEVEL]: namedEntityAdapter(levelService, Level),
89 [AdminEntityType.INSTITUTION]: namedEntityAdapter(
90 institutionService,
91 Institution,
92 ),
93 [AdminEntityType.JOB_TYPE]: namedEntityAdapter(jobTypeService, JobType),
94 [AdminEntityType.APPLICATION_STATUS]: namedEntityAdapter(
95 applicationStatusService,
96 ApplicationStatus,
97 ),
98 [AdminEntityType.ORGANIZATION]: {
99 findMany: (searchTerm) =>
100 organizationService.findMany().then((orgs) =>
101 searchTerm
102 ? orgs.filter((o) =>
103 o.name.toLowerCase().includes(searchTerm.toLowerCase()),
104 )
105 : orgs,
106 ),
107 findByIdOrFail: (id) => organizationService.findByIdOrFail(id),
108 save: (entity) =>
109 organizationService.save(entity as unknown as Organization),
110 destroy: (entity) =>
111 organizationService.destroy(entity as unknown as Organization),
112 create: (id, name, now, description) =>
113 new Organization(id, name, now, now, description ?? null),
114 },
115 [AdminEntityType.ORGANIZATION_ROLE]: {
116 findMany: (searchTerm) =>
117 organizationRoleService.findAll().then((roles) =>
118 searchTerm
119 ? roles.filter((r) =>
120 r.name.toLowerCase().includes(searchTerm.toLowerCase()),
121 )
122 : roles,
123 ),
124 findByIdOrFail: (id) => organizationRoleService.findByIdOrFail(id),
125 save: async (entity) => {
126 const desc =
127 entity.description != null ? entity.description : undefined;
128 const existing = await organizationRoleService.findById(entity.id);
129 return existing
130 ? organizationRoleService.update(entity.id, {
131 name: entity.name,
132 ...(desc !== undefined ? { description: desc } : {}),
133 })
134 : organizationRoleService.create({
135 name: entity.name,
136 ...(desc !== undefined ? { description: desc } : {}),
137 });
138 },
139 destroy: (entity) => organizationRoleService.delete(entity.id),
140 create: (id, name, now, description) =>
141 new OrganizationRole(id, name, now, now, description ?? null),
142 },
143 };
144 }
145
146 @Query(() => [AdminLookupEntity])
147 async adminLookupEntities(
148 @Args("entityType", { type: () => AdminEntityType })
149 entityType: AdminEntityType,
150 @Args("searchTerm", { nullable: true }) searchTerm?: string,
151 ): Promise<AdminLookupEntity[]> {
152 const adapter = this.registry[entityType];
153 const entities = await adapter.findMany(searchTerm ?? undefined);
154 return entities.map(AdminLookupEntity.fromDomain);
155 }
156
157 @Mutation(() => AdminLookupEntity)
158 async adminCreateLookupEntity(
159 @Args("entityType", { type: () => AdminEntityType })
160 entityType: AdminEntityType,
161 @Args("name") name: string,
162 @Args("description", { nullable: true }) description?: string,
163 ): Promise<AdminLookupEntity> {
164 const adapter = this.registry[entityType];
165 const now = this.clock.now();
166 const entity = adapter.create(
167 this.uuid.generate(),
168 name,
169 now,
170 description,
171 );
172 const saved = await adapter.save(entity);
173 return AdminLookupEntity.fromDomain(saved);
174 }
175
176 @Mutation(() => AdminLookupEntity)
177 async adminUpdateLookupEntity(
178 @Args("entityType", { type: () => AdminEntityType })
179 entityType: AdminEntityType,
180 @Args("id") id: string,
181 @Args("name", { nullable: true }) name?: string,
182 @Args("description", { nullable: true }) description?: string,
183 ): Promise<AdminLookupEntity> {
184 const adapter = this.registry[entityType];
185 const existing = await adapter.findByIdOrFail(id);
186 const updated = adapter.create(
187 existing.id,
188 name ?? existing.name,
189 existing.createdAt,
190 description !== undefined
191 ? description
192 : (existing.description ?? undefined),
193 );
194 (updated as { updatedAt: Date }).updatedAt = this.clock.now();
195 const saved = await adapter.save(updated);
196 return AdminLookupEntity.fromDomain(saved);
197 }
198
199 @Mutation(() => Boolean)
200 async adminDeleteLookupEntity(
201 @Args("entityType", { type: () => AdminEntityType })
202 entityType: AdminEntityType,
203 @Args("id") id: string,
204 ): Promise<boolean> {
205 const adapter = this.registry[entityType];
206 const entity = await adapter.findByIdOrFail(id);
207 await adapter.destroy(entity);
208 return true;
209 }
210}