because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(server): introduce VacancyMapper and inject into VacancyService

+304
+34
apps/server/src/modules/vacancies/vacancy.mapper.ts
··· 1 + import { Injectable } from "@nestjs/common"; 2 + import type { Vacancy as PrismaVacancy } from "@prisma/client"; 3 + import type { BaseMapper } from "../base/mapper.interface"; 4 + import { Vacancy } from "./vacancy.entity"; 5 + 6 + @Injectable() 7 + export class VacancyMapper implements BaseMapper<PrismaVacancy, Vacancy> { 8 + toDomain(prismaVacancy: null): null; 9 + toDomain(prismaVacancy: PrismaVacancy): Vacancy; 10 + toDomain(prismaVacancy: PrismaVacancy | null): Vacancy | null; 11 + toDomain(prismaVacancy: PrismaVacancy | null): Vacancy | null { 12 + if (prismaVacancy === null) return null; 13 + return new Vacancy( 14 + prismaVacancy.id, 15 + prismaVacancy.userId, 16 + prismaVacancy.title, 17 + prismaVacancy.company, 18 + prismaVacancy.createdAt, 19 + prismaVacancy.updatedAt, 20 + prismaVacancy.description ?? undefined, 21 + prismaVacancy.requirements ?? undefined, 22 + prismaVacancy.location ?? undefined, 23 + prismaVacancy.salary ?? undefined, 24 + prismaVacancy.jobType ?? undefined, 25 + prismaVacancy.applicationUrl ?? undefined, 26 + prismaVacancy.deadline ?? undefined, 27 + prismaVacancy.isActive, 28 + ); 29 + } 30 + 31 + mapToDomain(prismaVacancies: PrismaVacancy[]): Vacancy[] { 32 + return prismaVacancies.map((v) => this.toDomain(v)); 33 + } 34 + }
+12
apps/server/src/modules/vacancies/vacancy.module.ts
··· 1 + import { Module } from "@nestjs/common"; 2 + import { DatabaseModule } from "../database/database.module"; 3 + import { VacancyMapper } from "./vacancy.mapper"; 4 + import { VacancyResolver } from "./vacancy.resolver"; 5 + import { VacancyService } from "./vacancy.service"; 6 + 7 + @Module({ 8 + imports: [DatabaseModule], 9 + providers: [VacancyService, VacancyResolver, VacancyMapper], 10 + exports: [VacancyService, VacancyMapper], 11 + }) 12 + export class VacancyModule {}
+160
apps/server/src/modules/vacancies/vacancy.service.ts
··· 1 + import { Injectable, Logger, NotFoundException } from "@nestjs/common"; 2 + import { PrismaService } from "../database/prisma.service"; 3 + import { Vacancy } from "./vacancy.entity"; 4 + import { VacancyMapper } from "./vacancy.mapper"; 5 + 6 + export interface CreateVacancyDto { 7 + title: string; 8 + company: string; 9 + description?: string; 10 + requirements?: string; 11 + location?: string; 12 + salary?: string; 13 + jobType?: string; 14 + applicationUrl?: string; 15 + deadline?: Date; 16 + isActive?: boolean; 17 + } 18 + 19 + export type UpdateVacancyDto = Partial<CreateVacancyDto>; 20 + 21 + @Injectable() 22 + export class VacancyService { 23 + private readonly logger = new Logger(VacancyService.name); 24 + 25 + constructor( 26 + private readonly prisma: PrismaService, 27 + private readonly vacancyMapper: VacancyMapper, 28 + ) {} 29 + 30 + async findById(id: string): Promise<Vacancy | null> { 31 + this.logger.log(`Finding vacancy by id: ${id}`); 32 + 33 + const vacancy = await this.prisma.vacancy.findUnique({ 34 + where: { id }, 35 + }); 36 + 37 + return this.vacancyMapper.toDomain(vacancy); 38 + } 39 + 40 + async findByIdAndUser(id: string, userId: string): Promise<Vacancy | null> { 41 + this.logger.log(`Finding vacancy by id and user: ${id}, ${userId}`); 42 + 43 + const vacancy = await this.prisma.vacancy.findFirst({ 44 + where: { 45 + id, 46 + userId, 47 + }, 48 + }); 49 + 50 + return this.vacancyMapper.toDomain(vacancy); 51 + } 52 + 53 + async findForUser(userId: string): Promise<Vacancy[]> { 54 + this.logger.log(`Finding vacancies for user: ${userId}`); 55 + 56 + const vacancies = await this.prisma.vacancy.findMany({ 57 + where: { userId }, 58 + orderBy: { createdAt: "desc" }, 59 + }); 60 + 61 + return this.vacancyMapper.mapToDomain(vacancies); 62 + } 63 + 64 + async create( 65 + userId: string, 66 + createVacancyDto: CreateVacancyDto, 67 + ): Promise<Vacancy> { 68 + this.logger.log(`Creating vacancy for user: ${userId}`); 69 + 70 + const vacancy = await this.prisma.vacancy.create({ 71 + data: { 72 + userId, 73 + title: createVacancyDto.title, 74 + company: createVacancyDto.company, 75 + description: createVacancyDto.description ?? null, 76 + requirements: createVacancyDto.requirements ?? null, 77 + location: createVacancyDto.location ?? null, 78 + salary: createVacancyDto.salary ?? null, 79 + jobType: createVacancyDto.jobType ?? null, 80 + applicationUrl: createVacancyDto.applicationUrl ?? null, 81 + deadline: createVacancyDto.deadline ?? null, 82 + isActive: createVacancyDto.isActive ?? true, 83 + }, 84 + }); 85 + 86 + return this.vacancyMapper.toDomain(vacancy); 87 + } 88 + 89 + async update( 90 + id: string, 91 + userId: string, 92 + updateVacancyDto: UpdateVacancyDto, 93 + ): Promise<Vacancy> { 94 + this.logger.log(`Updating vacancy: ${id} for user: ${userId}`); 95 + 96 + // First check if the vacancy exists and belongs to the user 97 + const existingVacancy = await this.findByIdAndUser(id, userId); 98 + if (!existingVacancy) { 99 + throw new NotFoundException( 100 + `Vacancy with ID ${id} not found or does not belong to user`, 101 + ); 102 + } 103 + 104 + const vacancy = await this.prisma.vacancy.update({ 105 + where: { id }, 106 + data: { 107 + ...(updateVacancyDto.title !== undefined && { 108 + title: updateVacancyDto.title, 109 + }), 110 + ...(updateVacancyDto.company !== undefined && { 111 + company: updateVacancyDto.company, 112 + }), 113 + ...(updateVacancyDto.description !== undefined && { 114 + description: updateVacancyDto.description ?? null, 115 + }), 116 + ...(updateVacancyDto.requirements !== undefined && { 117 + requirements: updateVacancyDto.requirements ?? null, 118 + }), 119 + ...(updateVacancyDto.location !== undefined && { 120 + location: updateVacancyDto.location ?? null, 121 + }), 122 + ...(updateVacancyDto.salary !== undefined && { 123 + salary: updateVacancyDto.salary ?? null, 124 + }), 125 + ...(updateVacancyDto.jobType !== undefined && { 126 + jobType: updateVacancyDto.jobType ?? null, 127 + }), 128 + ...(updateVacancyDto.applicationUrl !== undefined && { 129 + applicationUrl: updateVacancyDto.applicationUrl ?? null, 130 + }), 131 + ...(updateVacancyDto.deadline !== undefined && { 132 + deadline: updateVacancyDto.deadline ?? null, 133 + }), 134 + ...(updateVacancyDto.isActive !== undefined && { 135 + isActive: updateVacancyDto.isActive, 136 + }), 137 + }, 138 + }); 139 + 140 + return this.vacancyMapper.toDomain(vacancy); 141 + } 142 + 143 + async delete(id: string, userId: string): Promise<boolean> { 144 + this.logger.log(`Deleting vacancy: ${id} for user: ${userId}`); 145 + 146 + // First check if the vacancy exists and belongs to the user 147 + const existingVacancy = await this.findByIdAndUser(id, userId); 148 + if (!existingVacancy) { 149 + throw new NotFoundException( 150 + `Vacancy with ID ${id} not found or does not belong to user`, 151 + ); 152 + } 153 + 154 + await this.prisma.vacancy.delete({ 155 + where: { id }, 156 + }); 157 + 158 + return true; 159 + } 160 + }
+98
apps/server/src/modules/vacancies/vacancy.type.ts
··· 1 + import { Field, ID, ObjectType } from "@nestjs/graphql"; 2 + import type { Vacancy as DomainVacancy } from "./vacancy.entity"; 3 + 4 + @ObjectType() 5 + export class Vacancy { 6 + @Field(() => ID) 7 + id: string; 8 + 9 + @Field(() => ID) 10 + userId: string; 11 + 12 + @Field(() => String) 13 + title: string; 14 + 15 + @Field(() => String) 16 + company: string; 17 + 18 + @Field(() => String, { nullable: true }) 19 + description: string | null; 20 + 21 + @Field(() => String, { nullable: true }) 22 + requirements: string | null; 23 + 24 + @Field(() => String, { nullable: true }) 25 + location: string | null; 26 + 27 + @Field(() => String, { nullable: true }) 28 + salary: string | null; 29 + 30 + @Field(() => String, { nullable: true }) 31 + jobType: string | null; 32 + 33 + @Field(() => String, { nullable: true }) 34 + applicationUrl: string | null; 35 + 36 + @Field(() => Date, { nullable: true }) 37 + deadline: Date | null; 38 + 39 + @Field(() => Boolean) 40 + isActive: boolean; 41 + 42 + @Field(() => Date) 43 + createdAt: Date; 44 + 45 + @Field(() => Date) 46 + updatedAt: Date; 47 + 48 + constructor(data: { 49 + id: string; 50 + userId: string; 51 + title: string; 52 + company: string; 53 + description?: string | null; 54 + requirements?: string | null; 55 + location?: string | null; 56 + salary?: string | null; 57 + jobType?: string | null; 58 + applicationUrl?: string | null; 59 + deadline?: Date | null; 60 + isActive: boolean; 61 + createdAt: Date; 62 + updatedAt: Date; 63 + }) { 64 + this.id = data.id; 65 + this.userId = data.userId; 66 + this.title = data.title; 67 + this.company = data.company; 68 + this.description = data.description ?? null; 69 + this.requirements = data.requirements ?? null; 70 + this.location = data.location ?? null; 71 + this.salary = data.salary ?? null; 72 + this.jobType = data.jobType ?? null; 73 + this.applicationUrl = data.applicationUrl ?? null; 74 + this.deadline = data.deadline ?? null; 75 + this.isActive = data.isActive; 76 + this.createdAt = data.createdAt; 77 + this.updatedAt = data.updatedAt; 78 + } 79 + 80 + static fromDomain(domainVacancy: DomainVacancy): Vacancy { 81 + return new Vacancy({ 82 + id: domainVacancy.id, 83 + userId: domainVacancy.userId, 84 + title: domainVacancy.title, 85 + company: domainVacancy.company, 86 + description: domainVacancy.description ?? null, 87 + requirements: domainVacancy.requirements ?? null, 88 + location: domainVacancy.location ?? null, 89 + salary: domainVacancy.salary ?? null, 90 + jobType: domainVacancy.jobType ?? null, 91 + applicationUrl: domainVacancy.applicationUrl ?? null, 92 + deadline: domainVacancy.deadline ?? null, 93 + isActive: domainVacancy.isActive, 94 + createdAt: domainVacancy.createdAt, 95 + updatedAt: domainVacancy.updatedAt, 96 + }); 97 + } 98 + }