import type { User as DomainUser } from "@cv/core"; import { AuthorizationService } from "@cv/core"; import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { PrismaService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Parent, Query, ResolveField, Resolver, } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; import { CVDataLoaderService } from "@/modules/cv-template/cv.dataloader"; import { CV } from "@/modules/cv-template/graphql/cv.type"; import { Vacancy } from "@/modules/vacancies/graphql/vacancy.type"; import { VacancyDataLoaderService } from "@/modules/vacancies/vacancy.dataloader"; import { Application as ApplicationEntity } from "../application.entity"; import { ApplicationMapper } from "../application.mapper"; import { ApplicationService } from "../application.service"; import { ApplicationStatusDataLoaderService } from "../application-status.dataloader"; import { CreateApplicationInput, UpdateApplicationStatusInput, } from "./application.input"; import { Application, ApplicationStatus } from "./application.type"; @Resolver(() => Application) @UseGuards(JwtAuthGuard, VerifiedScopeGuard) export class ApplicationResolver { constructor( private readonly applicationService: ApplicationService, private readonly applicationMapper: ApplicationMapper, private readonly statusDataLoader: ApplicationStatusDataLoaderService, private readonly vacancyDataLoader: VacancyDataLoaderService, private readonly cvDataLoader: CVDataLoaderService, private readonly prisma: PrismaService, private readonly authorizationService: AuthorizationService, ) {} @Query(() => Application, { nullable: true }) async application( @Args("id", { type: () => String }) id: string, @CurrentUser() user: DomainUser, ): Promise { const application = await this.applicationService.findById(id); if (!application) { return null; } await this.authorizationService.canView( user, application, ApplicationEntity, ); return Application.fromDomain(application); } @Mutation(() => Application) async createApplication( @Args("input") input: CreateApplicationInput, @CurrentUser() user: DomainUser, ): Promise { await this.authorizationService.canCreate(user, ApplicationEntity, { userId: user.id, }); const appliedStatus = await this.prisma.applicationStatus.findFirst({ where: { name: "Applied" }, }); if (!appliedStatus) { throw new Error("Application status 'Applied' not found"); } const existingApplication = await this.prisma.application.findFirst({ where: { userId: user.id, vacancyId: input.vacancyId, }, }); if (existingApplication) { throw new Error("User has already applied to this vacancy"); } const application = await this.prisma.application.create({ data: { userId: user.id, profileId: input.profileId, vacancyId: input.vacancyId, statusId: appliedStatus.id, cvId: input.cvId ?? null, coverLetter: input.coverLetter ?? null, }, include: this.applicationService.applicationInclude, }); const domainApplication = this.applicationMapper.toDomain(application); if (!domainApplication) { throw new Error("Failed to map application to domain"); } return Application.fromDomain(domainApplication); } @Mutation(() => Application) async updateApplicationStatus( @Args("input") input: UpdateApplicationStatusInput, @CurrentUser() user: DomainUser, ): Promise { const existingApplication = await this.applicationService.findByIdOrFail( input.applicationId, ); await this.authorizationService.canUpdate( user, existingApplication, ApplicationEntity, ); const status = await this.prisma.applicationStatus.findUnique({ where: { id: input.statusId }, }); if (!status) { throw new Error("Application status not found"); } const updatedApplication = new ApplicationEntity( existingApplication.id, existingApplication.userId, existingApplication.profileId, existingApplication.vacancy, input.statusId, { id: status.id, name: status.name, description: status.description, createdAt: status.createdAt, updatedAt: status.updatedAt, }, existingApplication.appliedAt, existingApplication.createdAt, new Date(), existingApplication.cv, existingApplication.coverLetter, ); const application = await this.applicationService.save(updatedApplication); return Application.fromDomain(application); } @Mutation(() => Boolean) async deleteApplication( @Args("id", { type: () => String }) id: string, @CurrentUser() user: DomainUser, ): Promise { const existingApplication = await this.applicationService.findByIdOrFail(id); await this.authorizationService.canDelete( user, existingApplication, ApplicationEntity, ); await this.applicationService.destroy(existingApplication); return true; } @ResolveField(() => ApplicationStatus) async status(@Parent() application: Application): Promise { const status = await this.statusDataLoader.load(application.statusId); if (!status) { throw new Error( `Application status with id ${application.statusId} not found`, ); } return new ApplicationStatus({ id: status.id, name: status.name, description: status.description, createdAt: status.createdAt, updatedAt: status.updatedAt, }); } @ResolveField(() => Vacancy) async vacancy(@Parent() application: Application): Promise { const vacancy = await this.vacancyDataLoader.load(application.vacancyId); if (!vacancy) { throw new Error(`Vacancy with id ${application.vacancyId} not found`); } return Vacancy.fromDomain(vacancy); } @ResolveField(() => CV, { nullable: true }) async cv(@Parent() application: Application): Promise { if (!application.cvId) { return null; } const cv = await this.cvDataLoader.load(application.cvId); return cv; } }