From 9f52fdfa11319330aec5bd61d4eefe940194d4a8 Mon Sep 17 00:00:00 2001 From: white Date: Mon, 11 Dec 2023 19:42:35 +0100 Subject: [PATCH] feat session: service, controller, entity --- .../create-recruitment-session.dto.ts | 25 +++++ .../recruitment-session-response.dto.ts | 9 ++ .../recruitment-session.controller.ts | 102 ++++++++++++++++++ .../recruitment-session.entity.ts | 30 ++++++ .../recruitment-session.service.ts | 34 ++++++ .../update-recruitment-session.dto.ts | 23 ++++ shared/src/abilities.ts | 6 +- shared/src/recruitment-session.ts | 82 ++++++++++++++ 8 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 api/src/recruitment-session/create-recruitment-session.dto.ts create mode 100644 api/src/recruitment-session/recruitment-session-response.dto.ts create mode 100644 api/src/recruitment-session/recruitment-session.controller.ts create mode 100644 api/src/recruitment-session/recruitment-session.entity.ts create mode 100644 api/src/recruitment-session/recruitment-session.service.ts create mode 100644 api/src/recruitment-session/update-recruitment-session.dto.ts create mode 100644 shared/src/recruitment-session.ts diff --git a/api/src/recruitment-session/create-recruitment-session.dto.ts b/api/src/recruitment-session/create-recruitment-session.dto.ts new file mode 100644 index 0000000..614ef24 --- /dev/null +++ b/api/src/recruitment-session/create-recruitment-session.dto.ts @@ -0,0 +1,25 @@ +import { RecruitmentSession, RecruitmentSessionState } from "@hkrecruitment/shared/recruitment-session"; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateRecruitmentSessionDto implements RecruitmentSession { + @ApiProperty() + state: RecruitmentSessionState; + + @ApiProperty() + slotDuration: number; + + @ApiProperty() + interviewStart: Date; + + @ApiProperty() + interviewEnd: Date; + + @ApiProperty() + days: [Date]; + + @ApiProperty() + createdAt: Date; + + @ApiProperty() + lastModified: Date; +} \ No newline at end of file diff --git a/api/src/recruitment-session/recruitment-session-response.dto.ts b/api/src/recruitment-session/recruitment-session-response.dto.ts new file mode 100644 index 0000000..d63a3fd --- /dev/null +++ b/api/src/recruitment-session/recruitment-session-response.dto.ts @@ -0,0 +1,9 @@ +import { RecruitmentSession, RecruitmentSessionState } from '@hkrecruitment/shared/recruitment-session'; +import { Exclude, Expose } from 'class-transformer'; + +@Exclude() + export class RecruitmentSessionResponseDto implements Partial { + @Expose() id: number; + @Expose() createdAt: Date; +} + \ No newline at end of file diff --git a/api/src/recruitment-session/recruitment-session.controller.ts b/api/src/recruitment-session/recruitment-session.controller.ts new file mode 100644 index 0000000..22d46fd --- /dev/null +++ b/api/src/recruitment-session/recruitment-session.controller.ts @@ -0,0 +1,102 @@ +import { + Body, + Controller, + BadRequestException, + NotFoundException, + ConflictException, + Param, + Post, + Delete, + Req, + Patch, + ForbiddenException, + } from '@nestjs/common'; +import { RecruitmentSessionService } from './recruitment-session.service'; +import { createRecruitmentSessionSchema, RecruitmentSession, RecruitmentSessionState, updateRecruitmentSessionSchema } from '@hkrecruitment/shared/recruitment-session'; +import { Action, AppAbility, checkAbility } from "@hkrecruitment/shared" +import { JoiValidate } from '../joi-validation/joi-validate.decorator'; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiCreatedResponse, + ApiOkResponse, + ApiTags, + ApiConflictResponse, + ApiNoContentResponse, +} from '@nestjs/swagger'; +import { CheckPolicies } from 'src/authorization/check-policies.decorator'; +import { CreateRecruitmentSessionDto } from './create-recruitment-session.dto'; +import { UpdateRecruitmentSessionDto } from './update-recruitment-session.dto'; +import * as Joi from 'joi'; +import { Ability } from 'src/authorization/ability.decorator'; +import { AuthenticatedRequest } from 'src/authorization/authenticated-request.types'; +import { plainToClass } from 'class-transformer'; +import { RecruitmentSessionResponseDto } from './recruitment-session-response.dto'; + +@ApiBearerAuth() +@ApiTags('recruitment-session') +@Controller('recruitment-session') +export class RecruitmentSessionController { + constructor(private readonly recruitmentSessionService: RecruitmentSessionService) {} + + // CREATE NEW RECRUITMENT SESSION + @ApiBadRequestResponse() + @ApiForbiddenResponse() + @ApiConflictResponse({ + description: 'The recruitment session cannot be created', // + }) + @ApiCreatedResponse() + @JoiValidate({ + body: createRecruitmentSessionSchema, + }) + @CheckPolicies((ability) => ability.can(Action.Create, 'RecruitmentSession')) + @Post() + async createRecruitmentSession(@Body() rSess: CreateRecruitmentSessionDto): Promise { + return this.recruitmentSessionService.createRecruitmentSession({...rSess}); + } + + // UPDATE A RECRUITMENT SESSION + @Patch(':session_id') + @ApiBadRequestResponse() + @ApiForbiddenResponse() + @ApiOkResponse() + @JoiValidate({ + param: Joi.number().positive().integer().required().label('session_id'), + body: updateRecruitmentSessionSchema, + }) + async updateRecruitmentSession( + @Param('session_id') sessionId: number, + @Body() updateRecruitmentSession: UpdateRecruitmentSessionDto, + @Ability() ability: AppAbility, + @Req() req: AuthenticatedRequest, + ): Promise { + const session = await this.recruitmentSessionService.findRecruitmentSessionById(sessionId); + + if (session === null) throw new NotFoundException(); + + const sessionToCheck = { + ...updateRecruitmentSession, + sessionId: session.id, + }; + if ( + !checkAbility(ability, Action.Update, sessionToCheck, 'RecruitmentSession', [ + 'applicantId', + ]) + ) + throw new ForbiddenException(); + + const updatedRecruitmentSession = await this.recruitmentSessionService.updateRecruitmentSession( + { + ...session, + ...updateRecruitmentSession, + lastModified: new Date(), + }, + ); + + return plainToClass(RecruitmentSessionResponseDto, updatedRecruitmentSession); + } + + +} \ No newline at end of file diff --git a/api/src/recruitment-session/recruitment-session.entity.ts b/api/src/recruitment-session/recruitment-session.entity.ts new file mode 100644 index 0000000..73f6d04 --- /dev/null +++ b/api/src/recruitment-session/recruitment-session.entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { RecruitmentSession as RecruitmentSessionInterface, RecruitmentSessionState } from '@hkrecruitment/shared/src/recruitment-session' + + +@Entity() +export class RecruitmentSession implements RecruitmentSessionInterface { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column() + state: RecruitmentSessionState; + + @Column() + slotDuration: number; + + @Column() + interviewStart: Date; + + @Column() + interviewEnd: Date; + + @Column() + days: [Date]; + + @Column() + createdAt: Date; + + @Column() + lastModified: Date; +} diff --git a/api/src/recruitment-session/recruitment-session.service.ts b/api/src/recruitment-session/recruitment-session.service.ts new file mode 100644 index 0000000..ec46aca --- /dev/null +++ b/api/src/recruitment-session/recruitment-session.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { RecruitmentSession } from "./recruitment-session.entity"; +import { CreateRecruitmentSessionDto } from "./create-recruitment-session.dto"; + + +@Injectable() +export class RecruitmentSessionService { + constructor( + @InjectRepository(RecruitmentSession) + private readonly recruitmentSessionRepository: Repository + ) {} + + async createRecruitmentSession(rSess: CreateRecruitmentSessionDto): Promise { + return await this.recruitmentSessionRepository.save(rSess); + } + + async findAllRecruitmentSessions(): Promise { + return await this.recruitmentSessionRepository.find(); + } + + async findRecruitmentSessionById(id: number): Promise { + return await this.recruitmentSessionRepository.findOne({where: {id} }); + } + + async deletRecruitmentSession(rSess: RecruitmentSession): Promise { + return await this.recruitmentSessionRepository.remove(rSess); + } + + async updateRecruitmentSession(rSess: RecruitmentSession): Promise { + return await this.recruitmentSessionRepository.save(rSess); + } +} \ No newline at end of file diff --git a/api/src/recruitment-session/update-recruitment-session.dto.ts b/api/src/recruitment-session/update-recruitment-session.dto.ts new file mode 100644 index 0000000..7d2d222 --- /dev/null +++ b/api/src/recruitment-session/update-recruitment-session.dto.ts @@ -0,0 +1,23 @@ +import { RecruitmentSession, RecruitmentSessionState } from '@hkrecruitment/shared/recruitment-session'; +import { ApiProperty } from '@nestjs/swagger'; + + +export class UpdateRecruitmentSessionDto implements Partial { + @ApiProperty({ required: false }) + state?: RecruitmentSessionState; + + @ApiProperty({ required: false }) + slotDuration?: number; + + @ApiProperty({ required: false }) + interviewStart?: Date; + + @ApiProperty({ required: false }) + interviewEnd?: Date; + + @ApiProperty({ required: false }) + days?: [Date]; + + @ApiProperty({ required: false }) + lastModified?: Date; +} diff --git a/shared/src/abilities.ts b/shared/src/abilities.ts index 56a3e5d..8fa7e5b 100644 --- a/shared/src/abilities.ts +++ b/shared/src/abilities.ts @@ -9,6 +9,7 @@ import { applyAbilitiesForPerson, Person, Role } from "./person"; import { Application, applyAbilitiesOnApplication } from "./application"; import { applyAbilitiesOnAvailability, Availability } from "./availability"; import { TimeSlot } from "./timeslot"; +import { RecruitmentSession } from "recruitment-session"; export interface UserAuth { sub: string; @@ -26,8 +27,9 @@ type SubjectsTypes = | Partial | Partial | Partial - | Partial; -type SubjectNames = "Person" | "Application" | "Availability" | "TimeSlot"; + | Partial + | Partial; +type SubjectNames = "Person" | "Application" | "Availability" | "TimeSlot" | "RecruitmentSession"; export type Subjects = SubjectsTypes | SubjectNames; export type AppAbility = PureAbility<[Action, Subjects]>; diff --git a/shared/src/recruitment-session.ts b/shared/src/recruitment-session.ts new file mode 100644 index 0000000..6ca8728 --- /dev/null +++ b/shared/src/recruitment-session.ts @@ -0,0 +1,82 @@ +import { AvailabilityState } from "availability"; +import { Action, ApplyAbilities } from "./abilities"; +import { Role } from "./person"; +import DateExtension from "@joi/date"; +import * as Joi from "joi"; + +const JoiDate = Joi.extend(DateExtension); + + +export enum RecruitmentSessionState { + Active = "active", + Concluded = "concluded", +} + + +// import BaseJoi from "joi"; + +export interface RecruitmentSession { + state: RecruitmentSessionState; + slotDuration: number; + interviewStart: Date; + interviewEnd: Date; + days: [Date]; + createdAt: Date; + lastModified: Date; +} + +/* Validation schemas */ + +export const createRecruitmentSessionSchema = Joi.object ({ + state: Joi.string() + .valid("active", "concluded") + .required(), + slotDuration: Joi.number() + .integer(), + interviewStart: JoiDate.date().format("YYYY-MM-DD HH:mm").required(), + interviewEnd: JoiDate.date().format("YYYY-MM-DD HH:mm").required(), + // days: + lastModified: JoiDate.date().format("YYYY-MM-DD HH:mm").required() +}).options({ + stripUnknown: true, + abortEarly: false, + presence: "required", +}); + +export const updateRecruitmentSessionSchema = Joi.object ({ + state: Joi.string() + .valid("active", "concluded") + .optional(), +slotDuration: Joi.number() + .integer(), + interviewStart: JoiDate.date().format("YYYY-MM-DD HH:mm").optional(), + interviewEnd: JoiDate.date().format("YYYY-MM-DD HH:mm").optional(), + // days: + // .optional(), + createdAt: JoiDate.date().format("YYYY-MM-DD HH:mm").optional(), + lastModified: JoiDate.date().format("YYYY-MM-DD HH:mm").optional() +}); + + + +/* Abilities */ + +export const applyAbilitiesOnRecruitmentSession: ApplyAbilities = ( + user, + { can, cannot } +) => { + can(Action.Manage, "RecruitmentSession"); + switch (user.role) { + case Role.Admin: + case Role.Supervisor: + can(Action.Manage, "RecruitmentSession"); + break; + case Role.Clerk: // puo o non puo ?????? + case Role.Member: + case Role.Applicant: + can(Action.Read, "RecruitmentSession"); + break; + default: + cannot(Action.Manage, "RecruitmentSession"); + } +}; \ No newline at end of file