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): update organization and vacancy entities

+403 -117
+6 -6
apps/server/src/modules/organization/organization-role.entity.ts
··· 11 11 @Field(() => String, { nullable: true }) 12 12 description: string | null; 13 13 14 - @Field(() => String) 15 - color: string; 14 + @Field(() => String, { nullable: true }) 15 + color: string | null; 16 16 17 17 @Field(() => Date) 18 18 createdAt: Date; ··· 24 24 id: string; 25 25 name: string; 26 26 description?: string | null; 27 - color: string; 27 + color?: string | null; 28 28 createdAt: Date; 29 29 updatedAt: Date; 30 30 }) { 31 31 this.id = data.id; 32 32 this.name = data.name; 33 33 this.description = data.description ?? null; 34 - this.color = data.color; 34 + this.color = data.color ?? null; 35 35 this.createdAt = data.createdAt; 36 36 this.updatedAt = data.updatedAt; 37 37 } ··· 40 40 id: string; 41 41 name: string; 42 42 description?: string | null; 43 - color: string; 43 + color?: string | null; 44 44 createdAt: Date; 45 45 updatedAt: Date; 46 46 }): OrganizationRole { ··· 48 48 id: domainRole.id, 49 49 name: domainRole.name, 50 50 description: domainRole.description ?? null, 51 - color: domainRole.color, 51 + color: domainRole.color ?? null, 52 52 createdAt: domainRole.createdAt, 53 53 updatedAt: domainRole.updatedAt, 54 54 });
+4 -2
apps/server/src/modules/organization/organization-role.mapper.ts
··· 1 1 import { Injectable } from "@nestjs/common"; 2 2 import type { OrganizationRole as PrismaOrganizationRole } from "@prisma/client"; 3 - import type { BaseMapper } from "../base/mapper.interface"; 3 + import type { BaseMapper } from "@/modules/base/mapper.interface"; 4 4 import { OrganizationRole } from "./organization-role.entity"; 5 5 6 6 @Injectable() ··· 11 11 toDomain(prismaRole: PrismaOrganizationRole): OrganizationRole; 12 12 toDomain(prismaRole: PrismaOrganizationRole | null): OrganizationRole | null; 13 13 toDomain(prismaRole: PrismaOrganizationRole | null): OrganizationRole | null { 14 - if (prismaRole === null) return null; 14 + if (prismaRole === null) { 15 + return null; 16 + } 15 17 return new OrganizationRole({ 16 18 id: prismaRole.id, 17 19 name: prismaRole.name,
+5 -9
apps/server/src/modules/organization/organization-role.service.ts
··· 1 - import { Injectable, Logger, NotFoundException } from "@nestjs/common"; 2 - import { PrismaService } from "../database/prisma.service"; 1 + import { Injectable, Logger } from "@nestjs/common"; 2 + import { notFound } from "@/modules/base/not-found.util"; 3 + import { PrismaService } from "@/modules/database/prisma.service"; 3 4 import { OrganizationRole } from "./organization-role.entity"; 4 5 import { OrganizationRoleMapper } from "./organization-role.mapper"; 5 6 ··· 26 27 this.logger.log(`Finding organization role by id or fail: ${id}`); 27 28 28 29 const role = await this.findById(id); 29 - if (!role) { 30 - throw new NotFoundException(`Organization role with id ${id} not found`); 31 - } 32 - return role; 30 + return role ?? notFound("Organization role", "id", id); 33 31 } 34 32 35 33 async findAll(): Promise<OrganizationRole[]> { ··· 83 81 return this.organizationRoleMapper.toDomain(role); 84 82 } 85 83 86 - async delete(id: string): Promise<boolean> { 84 + async delete(id: string): Promise<void> { 87 85 this.logger.log(`Deleting organization role: ${id}`); 88 86 89 87 await this.prisma["organizationRole"].delete({ 90 88 where: { id }, 91 89 }); 92 - 93 - return true; 94 90 } 95 91 }
+4 -2
apps/server/src/modules/organization/organization.mapper.ts
··· 1 1 import { Injectable } from "@nestjs/common"; 2 2 import type { Organization as PrismaOrganization } from "@prisma/client"; 3 - import type { BaseMapper } from "../base/mapper.interface"; 3 + import type { BaseMapper } from "@/modules/base/mapper.interface"; 4 4 import { Organization } from "./organization.entity"; 5 5 6 6 @Injectable() ··· 11 11 toDomain(prismaOrganization: PrismaOrganization): Organization; 12 12 toDomain(prismaOrganization: PrismaOrganization | null): Organization | null; 13 13 toDomain(prismaOrganization: PrismaOrganization | null): Organization | null { 14 - if (prismaOrganization === null) return null; 14 + if (prismaOrganization === null) { 15 + return null; 16 + } 15 17 return new Organization({ 16 18 id: prismaOrganization.id, 17 19 name: prismaOrganization.name,
+10 -4
apps/server/src/modules/organization/organization.module.ts
··· 1 1 import { Module } from "@nestjs/common"; 2 - import { DatabaseModule } from "../database/database.module"; 2 + import { AuthModule } from "@/modules/auth/auth.module"; 3 + import { BaseModule } from "@/modules/base/base.module"; 4 + import { DatabaseModule } from "@/modules/database/database.module"; 5 + import { OrganizationResolver } from "./graphql/organization.resolver"; 6 + import { UserFieldResolver } from "./graphql/user-field.resolver"; 3 7 import { OrganizationMapper } from "./organization.mapper"; 4 - import { OrganizationResolver } from "./organization.resolver"; 5 8 import { OrganizationService } from "./organization.service"; 6 9 import { OrganizationRoleMapper } from "./organization-role.mapper"; 7 10 import { OrganizationRoleService } from "./organization-role.service"; 8 - import { UserFieldResolver } from "./user-field.resolver"; 11 + import { MembershipSeedService } from "./seed/membership.seed"; 12 + import { OrganizationSeedService } from "./seed/organization.seed"; 9 13 10 14 @Module({ 11 - imports: [DatabaseModule], 15 + imports: [DatabaseModule, BaseModule, AuthModule], 12 16 providers: [ 13 17 OrganizationService, 14 18 OrganizationRoleService, ··· 16 20 UserFieldResolver, 17 21 OrganizationMapper, 18 22 OrganizationRoleMapper, 23 + OrganizationSeedService, 24 + MembershipSeedService, 19 25 ], 20 26 exports: [ 21 27 OrganizationService,
+104 -20
apps/server/src/modules/organization/organization.service.ts
··· 1 1 import { Injectable, Logger } from "@nestjs/common"; 2 - import { PrismaService } from "../database/prisma.service"; 2 + import type { Prisma } from "@prisma/client"; 3 + import { notFound } from "@/modules/base/not-found.util"; 4 + import { PrismaService } from "@/modules/database/prisma.service"; 3 5 import type { 4 6 CreateOrganizationDto, 5 7 UpdateOrganizationDto, ··· 7 9 import { Organization } from "./organization.entity"; 8 10 import { OrganizationMapper } from "./organization.mapper"; 9 11 12 + export type Membership = { 13 + id: string; 14 + organizationId: string; 15 + userId: string; 16 + organizationRoleId: string; 17 + createdAt: Date; 18 + updatedAt: Date; 19 + user: { 20 + id: string; 21 + name: string; 22 + email: string; 23 + createdAt: Date; 24 + }; 25 + role: { 26 + id: string; 27 + name: string; 28 + description: string | null; 29 + color: string | null; 30 + createdAt: Date; 31 + updatedAt: Date; 32 + }; 33 + }; 34 + 35 + export type MembershipSortField = "createdAt" | "userName" | "roleName"; 36 + export type MembershipSortOrder = "asc" | "desc"; 37 + 10 38 @Injectable() 11 39 export class OrganizationService { 12 40 private readonly logger = new Logger(OrganizationService.name); ··· 29 57 async findForUser(userId: string): Promise<Organization[]> { 30 58 this.logger.log(`Finding organizations for user: ${userId}`); 31 59 32 - const userOrganizations = await this.prisma.userOrganization.findMany({ 60 + const memberships = await this.prisma["membership"].findMany({ 33 61 where: { userId }, 34 62 include: { 35 63 organization: true, ··· 38 66 }); 39 67 40 68 return this.organizationMapper.mapToDomain( 41 - userOrganizations.map((uo) => uo.organization), 69 + memberships.map((m) => m.organization), 42 70 ); 43 71 } 44 72 45 - async findUsersByOrganizationId(organizationId: string) { 46 - this.logger.log(`Finding users for organization: ${organizationId}`); 73 + async findMembershipsByOrganization( 74 + organizationId: string, 75 + sortBy: MembershipSortField = "createdAt", 76 + sortOrder: MembershipSortOrder = "asc", 77 + searchTerm?: string, 78 + ): Promise<Membership[]> { 79 + this.logger.log( 80 + `Finding memberships for organization: ${organizationId}, sortBy: ${sortBy}, sortOrder: ${sortOrder}, searchTerm: ${searchTerm}`, 81 + ); 82 + 83 + const whereClause: Prisma.MembershipWhereInput = { organizationId }; 84 + 85 + // Add search functionality for user names 86 + if (searchTerm?.trim()) { 87 + whereClause.user = { 88 + name: { 89 + contains: searchTerm.trim(), 90 + mode: "insensitive", 91 + }, 92 + }; 93 + } 47 94 48 - const userOrganizations = await this.prisma.userOrganization.findMany({ 49 - where: { organizationId }, 95 + const memberships = await this.prisma["membership"].findMany({ 96 + where: whereClause, 50 97 include: { 51 98 user: { 52 99 select: { ··· 56 103 createdAt: true, 57 104 }, 58 105 }, 59 - role: true, 106 + role: { 107 + select: { 108 + id: true, 109 + name: true, 110 + description: true, 111 + color: true, 112 + createdAt: true, 113 + updatedAt: true, 114 + }, 115 + }, 60 116 }, 61 - orderBy: { createdAt: "asc" }, 117 + orderBy: 118 + sortBy === "userName" 119 + ? { user: { name: sortOrder } } 120 + : sortBy === "roleName" 121 + ? { role: { name: sortOrder } } 122 + : { createdAt: sortOrder }, 62 123 }); 63 124 64 - return userOrganizations.map((uo) => ({ 65 - id: uo.id, 66 - joinedAt: uo.createdAt, 67 - user: uo.user, 68 - role: uo.role, 69 - })); 125 + return memberships as Membership[]; 126 + } 127 + 128 + async countMembershipsByOrganization( 129 + organizationId: string, 130 + searchTerm?: string, 131 + ): Promise<number> { 132 + this.logger.log( 133 + `Getting membership count for organization: ${organizationId}, searchTerm: ${searchTerm}`, 134 + ); 135 + 136 + const whereClause: Prisma.MembershipWhereInput = { organizationId }; 137 + 138 + // Add search functionality for user names 139 + if (searchTerm?.trim()) { 140 + whereClause.user = { 141 + name: { 142 + contains: searchTerm.trim(), 143 + mode: "insensitive", 144 + }, 145 + }; 146 + } 147 + 148 + return this.prisma["membership"].count({ 149 + where: whereClause, 150 + }); 70 151 } 71 152 72 153 async create( ··· 109 190 return this.organizationMapper.toDomain(organization); 110 191 } 111 192 112 - async delete(id: string): Promise<boolean> { 193 + async findByIdOrFail(id: string): Promise<Organization> { 194 + const organization = await this.findById(id); 195 + return organization ?? notFound("Organization", "id", id); 196 + } 197 + 198 + async delete(id: string): Promise<void> { 113 199 this.logger.log(`Deleting organization: ${id}`); 114 200 115 201 await this.prisma.organization.delete({ 116 202 where: { id }, 117 203 }); 118 - 119 - return true; 120 204 } 121 205 122 206 async addUserToOrganization( ··· 129 213 // Default to member role if no role specified 130 214 const defaultRoleId = roleId || "member_role_id"; 131 215 132 - await this.prisma.userOrganization.create({ 216 + await this.prisma["membership"].create({ 133 217 data: { 134 218 organizationId, 135 219 userId, ··· 148 232 `Removing user ${userId} from organization ${organizationId}`, 149 233 ); 150 234 151 - await this.prisma.userOrganization.deleteMany({ 235 + await this.prisma["membership"].deleteMany({ 152 236 where: { 153 237 organizationId, 154 238 userId,
+31 -15
apps/server/src/modules/vacancies/vacancy.entity.ts
··· 1 - import { BaseEntity } from "../base/base.entity"; 1 + import { BaseEntity } from "@/modules/base/base.entity"; 2 2 3 3 export class Vacancy extends BaseEntity { 4 - userId: string; 5 4 title: string; 6 - company: string; 5 + ownerId: string; 6 + companyId: string; 7 + roleId: string; 8 + levelId?: string; 9 + jobTypeId?: string; 7 10 description?: string; 8 11 requirements?: string; 9 12 location?: string; 10 - salary?: string; 11 - jobType?: string; 13 + minSalary?: number; 14 + maxSalary?: number; 12 15 applicationUrl?: string; 13 16 deadline?: Date; 14 17 isActive: boolean; 18 + isPublic: boolean; 15 19 16 20 constructor( 17 21 id: string, 18 - userId: string, 19 22 title: string, 20 - company: string, 23 + ownerId: string, 24 + companyId: string, 25 + roleId: string, 21 26 createdAt: Date, 22 27 updatedAt: Date, 28 + levelId?: string, 29 + jobTypeId?: string, 23 30 description?: string, 24 31 requirements?: string, 25 32 location?: string, 26 - salary?: string, 27 - jobType?: string, 33 + minSalary?: number, 34 + maxSalary?: number, 28 35 applicationUrl?: string, 29 36 deadline?: Date, 30 37 isActive: boolean = true, 38 + isPublic: boolean = false, 31 39 ) { 32 40 super(id, createdAt, updatedAt); 33 - this.userId = userId; 34 41 this.title = title; 35 - this.company = company; 42 + this.ownerId = ownerId; 43 + this.companyId = companyId; 44 + this.roleId = roleId; 36 45 this.isActive = isActive; 46 + this.isPublic = isPublic; 37 47 48 + if (levelId !== undefined) { 49 + this.levelId = levelId; 50 + } 51 + if (jobTypeId !== undefined) { 52 + this.jobTypeId = jobTypeId; 53 + } 38 54 if (description !== undefined) { 39 55 this.description = description; 40 56 } ··· 44 60 if (location !== undefined) { 45 61 this.location = location; 46 62 } 47 - if (salary !== undefined) { 48 - this.salary = salary; 63 + if (minSalary !== undefined) { 64 + this.minSalary = minSalary; 49 65 } 50 - if (jobType !== undefined) { 51 - this.jobType = jobType; 66 + if (maxSalary !== undefined) { 67 + this.maxSalary = maxSalary; 52 68 } 53 69 if (applicationUrl !== undefined) { 54 70 this.applicationUrl = applicationUrl;
+12 -6
apps/server/src/modules/vacancies/vacancy.mapper.ts
··· 1 1 import { Injectable } from "@nestjs/common"; 2 2 import type { Vacancy as PrismaVacancy } from "@prisma/client"; 3 - import type { BaseMapper } from "../base/mapper.interface"; 3 + import type { BaseMapper } from "@/modules/base/mapper.interface"; 4 4 import { Vacancy } from "./vacancy.entity"; 5 5 6 6 @Injectable() ··· 9 9 toDomain(prismaVacancy: PrismaVacancy): Vacancy; 10 10 toDomain(prismaVacancy: PrismaVacancy | null): Vacancy | null; 11 11 toDomain(prismaVacancy: PrismaVacancy | null): Vacancy | null { 12 - if (prismaVacancy === null) return null; 12 + if (prismaVacancy === null) { 13 + return null; 14 + } 13 15 return new Vacancy( 14 16 prismaVacancy.id, 15 - prismaVacancy.userId, 16 17 prismaVacancy.title, 17 - prismaVacancy.company, 18 + prismaVacancy.ownerId, 19 + prismaVacancy.companyId, 20 + prismaVacancy.roleId, 18 21 prismaVacancy.createdAt, 19 22 prismaVacancy.updatedAt, 23 + prismaVacancy.levelId ?? undefined, 24 + prismaVacancy.jobTypeId ?? undefined, 20 25 prismaVacancy.description ?? undefined, 21 26 prismaVacancy.requirements ?? undefined, 22 27 prismaVacancy.location ?? undefined, 23 - prismaVacancy.salary ?? undefined, 24 - prismaVacancy.jobType ?? undefined, 28 + prismaVacancy.minSalary ?? undefined, 29 + prismaVacancy.maxSalary ?? undefined, 25 30 prismaVacancy.applicationUrl ?? undefined, 26 31 prismaVacancy.deadline ?? undefined, 27 32 prismaVacancy.isActive, 33 + prismaVacancy.isPublic, 28 34 ); 29 35 } 30 36
+26 -4
apps/server/src/modules/vacancies/vacancy.module.ts
··· 1 1 import { Module } from "@nestjs/common"; 2 - import { DatabaseModule } from "../database/database.module"; 2 + import { AuthModule } from "@/modules/auth/auth.module"; 3 + import { BaseModule } from "@/modules/base/base.module"; 4 + import { DatabaseModule } from "@/modules/database/database.module"; 5 + import { CompanyModule } from "@/modules/job-experience/company/company.module"; 6 + import { LevelModule } from "@/modules/job-experience/level/level.module"; 7 + import { RoleModule } from "@/modules/job-experience/role/role.module"; 8 + import { SkillModule } from "@/modules/job-experience/skill/skill.module"; 9 + import { VacancyUserFieldResolver } from "./graphql/user-field.resolver"; 10 + import { VacancyResolver } from "./graphql/vacancy.resolver"; 11 + import { VacancySeedService } from "./seed/vacancy.seed"; 3 12 import { VacancyMapper } from "./vacancy.mapper"; 4 - import { VacancyResolver } from "./vacancy.resolver"; 5 13 import { VacancyService } from "./vacancy.service"; 6 14 7 15 @Module({ 8 - imports: [DatabaseModule], 9 - providers: [VacancyService, VacancyResolver, VacancyMapper], 16 + imports: [ 17 + DatabaseModule, 18 + BaseModule, 19 + CompanyModule, 20 + RoleModule, 21 + LevelModule, 22 + SkillModule, 23 + AuthModule, 24 + ], 25 + providers: [ 26 + VacancyService, 27 + VacancyResolver, 28 + VacancyMapper, 29 + VacancyUserFieldResolver, 30 + VacancySeedService, 31 + ], 10 32 exports: [VacancyService, VacancyMapper], 11 33 }) 12 34 export class VacancyModule {}
+201 -49
apps/server/src/modules/vacancies/vacancy.service.ts
··· 1 - import { Injectable, Logger, NotFoundException } from "@nestjs/common"; 2 - import { PrismaService } from "../database/prisma.service"; 1 + import { Injectable, NotFoundException } from "@nestjs/common"; 2 + import type { Prisma } from "@prisma/client"; 3 + import { notFound } from "@/modules/base/not-found.util"; 4 + import { PaginationService } from "@/modules/base/pagination.service"; 5 + import type { 6 + PaginationOptions, 7 + PaginationResult, 8 + } from "@/modules/base/pagination.types"; 9 + import { PrismaService } from "@/modules/database/prisma.service"; 10 + import { VacancyFilterInput } from "./graphql/vacancy-filter.input"; 3 11 import { Vacancy } from "./vacancy.entity"; 4 12 import { VacancyMapper } from "./vacancy.mapper"; 5 13 6 14 export interface CreateVacancyDto { 7 15 title: string; 8 - company: string; 16 + companyId: string; 17 + roleId: string; 18 + levelId?: string; 19 + jobTypeId?: string; 9 20 description?: string; 10 21 requirements?: string; 11 22 location?: string; 12 - salary?: string; 13 - jobType?: string; 23 + minSalary?: number; 24 + maxSalary?: number; 14 25 applicationUrl?: string; 15 26 deadline?: Date; 16 27 isActive?: boolean; 28 + isPublic?: boolean; 17 29 } 18 30 19 31 export type UpdateVacancyDto = Partial<CreateVacancyDto>; 20 32 21 33 @Injectable() 22 34 export class VacancyService { 23 - private readonly logger = new Logger(VacancyService.name); 24 - 25 35 constructor( 26 36 private readonly prisma: PrismaService, 27 37 private readonly vacancyMapper: VacancyMapper, 38 + private readonly paginationService: PaginationService, 28 39 ) {} 29 40 30 41 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({ 42 + const vacancy = await this.prisma["vacancy"].findUnique({ 34 43 where: { id }, 35 44 }); 36 45 ··· 38 47 } 39 48 40 49 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({ 50 + const vacancy = await this.prisma["vacancy"].findFirst({ 44 51 where: { 45 52 id, 46 - userId, 53 + ownerId: userId, 47 54 }, 48 55 }); 49 56 50 57 return this.vacancyMapper.toDomain(vacancy); 51 58 } 52 59 60 + private buildWhereClause( 61 + filter?: VacancyFilterInput, 62 + userId?: string, 63 + ): Prisma.VacancyWhereInput { 64 + const where: Prisma.VacancyWhereInput = {}; 65 + 66 + if (userId) { 67 + where.ownerId = userId; 68 + } 69 + 70 + if (!filter) { 71 + return where; 72 + } 73 + 74 + const andConditions: Prisma.VacancyWhereInput[] = []; 75 + 76 + // Search term filter 77 + if (filter.searchTerm) { 78 + const searchConditions: Prisma.VacancyWhereInput[] = [ 79 + { title: { contains: filter.searchTerm, mode: "insensitive" } }, 80 + { description: { contains: filter.searchTerm, mode: "insensitive" } }, 81 + { 82 + requirements: { contains: filter.searchTerm, mode: "insensitive" }, 83 + }, 84 + { location: { contains: filter.searchTerm, mode: "insensitive" } }, 85 + ]; 86 + 87 + if (where.OR) { 88 + const existingORConditions: Prisma.VacancyWhereInput[] = Array.isArray( 89 + where.OR, 90 + ) 91 + ? where.OR 92 + : [where.OR]; 93 + andConditions.push({ OR: existingORConditions }); 94 + delete where.OR; 95 + } 96 + andConditions.push({ OR: searchConditions }); 97 + } 98 + 99 + // Company filter 100 + if (filter.companies && filter.companies.length > 0) { 101 + where.companyId = { in: filter.companies }; 102 + } 103 + 104 + // Location filter 105 + if (filter.locations && filter.locations.length > 0) { 106 + where.location = { in: filter.locations }; 107 + } 108 + 109 + // Level filter 110 + if (filter.levels && filter.levels.length > 0) { 111 + where.levelId = { in: filter.levels }; 112 + } 113 + 114 + // Salary range filter - find vacancies where the salary range overlaps with the filter 115 + if (filter.minSalary !== undefined || filter.maxSalary !== undefined) { 116 + // Vacancy overlaps if: (vacancy.minSalary <= filter.maxSalary OR vacancy.minSalary is null) 117 + // AND (vacancy.maxSalary >= filter.minSalary OR vacancy.maxSalary is null) 118 + if (filter.minSalary !== undefined && filter.minSalary !== null) { 119 + andConditions.push({ 120 + OR: [{ maxSalary: { gte: filter.minSalary } }, { maxSalary: null }], 121 + }); 122 + } 123 + 124 + if (filter.maxSalary !== undefined && filter.maxSalary !== null) { 125 + andConditions.push({ 126 + OR: [{ minSalary: { lte: filter.maxSalary } }, { minSalary: null }], 127 + }); 128 + } 129 + } 130 + 131 + // Public filter 132 + if (filter.isPublic !== undefined && filter.isPublic !== null) { 133 + where.isPublic = filter.isPublic; 134 + } 135 + 136 + // Combine all AND conditions 137 + if (andConditions.length > 0) { 138 + const existingAND = where.AND 139 + ? Array.isArray(where.AND) 140 + ? where.AND 141 + : [where.AND] 142 + : []; 143 + where.AND = [...existingAND, ...andConditions]; 144 + } 145 + 146 + return where; 147 + } 148 + 149 + async findMany( 150 + filter?: VacancyFilterInput, 151 + userId?: string, 152 + options: PaginationOptions = {}, 153 + ): Promise<PaginationResult<Vacancy>> { 154 + const where = this.buildWhereClause(filter, userId); 155 + const queryOptions = this.paginationService.buildQueryOptions( 156 + where, 157 + { createdAt: "desc" }, 158 + options, 159 + ); 160 + 161 + const [items, totalCount] = await Promise.all([ 162 + this.prisma["vacancy"].findMany({ 163 + ...queryOptions, 164 + include: { 165 + company: true, 166 + role: true, 167 + level: true, 168 + skills: true, 169 + }, 170 + }), 171 + this.prisma["vacancy"].count({ where }), 172 + ]); 173 + 174 + const domainVacancies = this.vacancyMapper.mapToDomain(items); 175 + return this.paginationService.buildPaginationResult( 176 + domainVacancies, 177 + totalCount, 178 + options, 179 + ); 180 + } 181 + 53 182 async findForUser(userId: string): Promise<Vacancy[]> { 54 - this.logger.log(`Finding vacancies for user: ${userId}`); 183 + const result = await this.findMany(undefined, userId); 184 + return result.edges.map((edge) => edge.node); 185 + } 55 186 56 - const vacancies = await this.prisma.vacancy.findMany({ 57 - where: { userId }, 58 - orderBy: { createdAt: "desc" }, 59 - }); 187 + async findForUserWithFilters( 188 + userId: string, 189 + filter?: VacancyFilterInput, 190 + ): Promise<Vacancy[]> { 191 + const result = await this.findMany(filter, userId); 192 + return result.edges.map((edge) => edge.node); 193 + } 60 194 61 - return this.vacancyMapper.mapToDomain(vacancies); 195 + async findManyForUserWithFilters( 196 + userId: string, 197 + filter?: VacancyFilterInput, 198 + options: PaginationOptions = {}, 199 + ): Promise<PaginationResult<Vacancy>> { 200 + return this.findMany(filter, userId, options); 62 201 } 63 202 64 203 async create( 65 204 userId: string, 66 205 createVacancyDto: CreateVacancyDto, 67 206 ): Promise<Vacancy> { 68 - this.logger.log(`Creating vacancy for user: ${userId}`); 69 - 70 - const vacancy = await this.prisma.vacancy.create({ 207 + const vacancy = await this.prisma["vacancy"].create({ 71 208 data: { 72 - userId, 209 + ownerId: userId, // Set owner to the user creating the vacancy 73 210 title: createVacancyDto.title, 74 - company: createVacancyDto.company, 211 + companyId: createVacancyDto.companyId, 212 + roleId: createVacancyDto.roleId, 213 + levelId: createVacancyDto.levelId ?? null, 214 + jobTypeId: createVacancyDto.jobTypeId ?? null, 75 215 description: createVacancyDto.description ?? null, 76 216 requirements: createVacancyDto.requirements ?? null, 77 217 location: createVacancyDto.location ?? null, 78 - salary: createVacancyDto.salary ?? null, 79 - jobType: createVacancyDto.jobType ?? null, 218 + minSalary: createVacancyDto.minSalary ?? null, 219 + maxSalary: createVacancyDto.maxSalary ?? null, 80 220 applicationUrl: createVacancyDto.applicationUrl ?? null, 81 221 deadline: createVacancyDto.deadline ?? null, 82 222 isActive: createVacancyDto.isActive ?? true, 223 + isPublic: createVacancyDto.isPublic ?? false, 83 224 }, 84 225 }); 85 226 ··· 91 232 userId: string, 92 233 updateVacancyDto: UpdateVacancyDto, 93 234 ): 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) { 235 + // First check if the vacancy exists and the user is the owner 236 + const existingVacancy = await this.findById(id); 237 + if (!existingVacancy || existingVacancy.ownerId !== userId) { 99 238 throw new NotFoundException( 100 - `Vacancy with ID ${id} not found or does not belong to user`, 239 + `Vacancy with ID ${id} not found or user is not the owner`, 101 240 ); 102 241 } 103 242 104 - const vacancy = await this.prisma.vacancy.update({ 243 + const vacancy = await this.prisma["vacancy"].update({ 105 244 where: { id }, 106 245 data: { 107 246 ...(updateVacancyDto.title !== undefined && { 108 247 title: updateVacancyDto.title, 109 248 }), 110 - ...(updateVacancyDto.company !== undefined && { 111 - company: updateVacancyDto.company, 249 + ...(updateVacancyDto.companyId !== undefined && { 250 + companyId: updateVacancyDto.companyId, 251 + }), 252 + ...(updateVacancyDto.roleId !== undefined && { 253 + roleId: updateVacancyDto.roleId, 254 + }), 255 + ...(updateVacancyDto.levelId !== undefined && { 256 + levelId: updateVacancyDto.levelId ?? null, 257 + }), 258 + ...(updateVacancyDto.jobTypeId !== undefined && { 259 + jobTypeId: updateVacancyDto.jobTypeId ?? null, 112 260 }), 113 261 ...(updateVacancyDto.description !== undefined && { 114 262 description: updateVacancyDto.description ?? null, ··· 119 267 ...(updateVacancyDto.location !== undefined && { 120 268 location: updateVacancyDto.location ?? null, 121 269 }), 122 - ...(updateVacancyDto.salary !== undefined && { 123 - salary: updateVacancyDto.salary ?? null, 270 + ...(updateVacancyDto.minSalary !== undefined && { 271 + minSalary: updateVacancyDto.minSalary ?? null, 124 272 }), 125 - ...(updateVacancyDto.jobType !== undefined && { 126 - jobType: updateVacancyDto.jobType ?? null, 273 + ...(updateVacancyDto.maxSalary !== undefined && { 274 + maxSalary: updateVacancyDto.maxSalary ?? null, 127 275 }), 128 276 ...(updateVacancyDto.applicationUrl !== undefined && { 129 277 applicationUrl: updateVacancyDto.applicationUrl ?? null, ··· 133 281 }), 134 282 ...(updateVacancyDto.isActive !== undefined && { 135 283 isActive: updateVacancyDto.isActive, 284 + }), 285 + ...(updateVacancyDto.isPublic !== undefined && { 286 + isPublic: updateVacancyDto.isPublic, 136 287 }), 137 288 }, 138 289 }); ··· 140 291 return this.vacancyMapper.toDomain(vacancy); 141 292 } 142 293 143 - async delete(id: string, userId: string): Promise<boolean> { 144 - this.logger.log(`Deleting vacancy: ${id} for user: ${userId}`); 294 + async findByIdOrFail(id: string): Promise<Vacancy> { 295 + const vacancy = await this.findById(id); 296 + return vacancy ?? notFound("Vacancy", "id", id); 297 + } 145 298 146 - // First check if the vacancy exists and belongs to the user 147 - const existingVacancy = await this.findByIdAndUser(id, userId); 148 - if (!existingVacancy) { 299 + async delete(id: string, userId: string): Promise<void> { 300 + // First check if the vacancy exists and the user is the owner 301 + const existingVacancy = await this.findById(id); 302 + if (!existingVacancy || existingVacancy.ownerId !== userId) { 149 303 throw new NotFoundException( 150 - `Vacancy with ID ${id} not found or does not belong to user`, 304 + `Vacancy with ID ${id} not found or user is not the owner`, 151 305 ); 152 306 } 153 307 154 - await this.prisma.vacancy.delete({ 308 + await this.prisma["vacancy"].delete({ 155 309 where: { id }, 156 310 }); 157 - 158 - return true; 159 311 } 160 312 }