because I got bored of customising my CV for every job
at main 210 lines 8.4 kB view raw
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}