-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: api v2 org ooo crud * chore: add guard isUserOOO on ooo controller * chore: add checks for ooo * fixup! chore: add checks for ooo * added e2e tests * fixup! added e2e tests
- Loading branch information
1 parent
c9c5493
commit b997c65
Showing
11 changed files
with
1,414 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository"; | ||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; | ||
import { Request } from "express"; | ||
|
||
@Injectable() | ||
export class IsUserOOO implements CanActivate { | ||
constructor(private oooRepo: UserOOORepository) {} | ||
|
||
async canActivate(context: ExecutionContext): Promise<boolean> { | ||
const request = context.switchToHttp().getRequest<Request>(); | ||
const oooId: string = request.params.oooId; | ||
const userId: string = request.params.userId; | ||
|
||
if (!userId) { | ||
throw new ForbiddenException("No user id found in request params."); | ||
} | ||
|
||
if (!oooId) { | ||
throw new ForbiddenException("No ooo entry id found in request params."); | ||
} | ||
|
||
const ooo = await this.oooRepo.getUserOOOByIdAndUserId(Number(oooId), Number(userId)); | ||
|
||
if (ooo) { | ||
return true; | ||
} | ||
|
||
throw new ForbiddenException("This OOO entry does not belong to this user."); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { BadRequestException } from "@nestjs/common"; | ||
import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger"; | ||
import { Transform } from "class-transformer"; | ||
import { IsDate, IsInt, IsOptional, IsString, IsEnum, isDate } from "class-validator"; | ||
|
||
export enum OutOfOfficeReason { | ||
UNSPECIFIED = "unspecified", | ||
VACATION = "vacation", | ||
TRAVEL = "travel", | ||
SICK_LEAVE = "sick", | ||
PUBLIC_HOLIDAY = "public_holiday", | ||
} | ||
|
||
export type OutOfOfficeReasonType = `${OutOfOfficeReason}`; | ||
|
||
const isDateString = (dateString: string) => { | ||
try { | ||
const isoDateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{3})?Z$/; | ||
return isoDateRegex.test(dateString); | ||
} catch { | ||
throw new BadRequestException("Invalid Date."); | ||
} | ||
}; | ||
|
||
export class CreateOutOfOfficeEntryDto { | ||
@Transform(({ value }: { value: string }) => { | ||
if (isDateString(value)) { | ||
const date = new Date(value); | ||
date.setUTCHours(0, 0, 0, 0); | ||
return date; | ||
} | ||
throw new BadRequestException("Invalid Date."); | ||
}) | ||
@IsDate() | ||
@ApiProperty({ | ||
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.", | ||
example: "2023-05-01T00:00:00.000Z", | ||
}) | ||
start!: Date; | ||
|
||
@Transform(({ value }: { value: string }) => { | ||
if (isDateString(value)) { | ||
const date = new Date(value); | ||
date.setUTCHours(23, 59, 59, 999); | ||
return date; | ||
} | ||
throw new BadRequestException("Invalid Date."); | ||
}) | ||
@IsDate() | ||
@ApiProperty({ | ||
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.", | ||
example: "2023-05-10T23:59:59.999Z", | ||
}) | ||
end!: Date; | ||
|
||
@IsString() | ||
@IsOptional() | ||
@ApiPropertyOptional({ | ||
description: "Optional notes for the out of office entry.", | ||
example: "Vacation in Hawaii", | ||
}) | ||
notes?: string; | ||
|
||
@IsInt() | ||
@IsOptional() | ||
@ApiPropertyOptional({ | ||
description: "The ID of the user covering for the out of office period, if applicable.", | ||
example: 2, | ||
}) | ||
toUserId?: number; | ||
|
||
@IsEnum(OutOfOfficeReason) | ||
@IsOptional() | ||
@ApiPropertyOptional({ | ||
description: "the reason for the out of office entry, if applicable", | ||
example: "vacation", | ||
}) | ||
reason?: OutOfOfficeReasonType; | ||
} | ||
|
||
export class UpdateOutOfOfficeEntryDto extends PartialType(CreateOutOfOfficeEntryDto) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { OutOfOfficeReason } from "@/modules/ooo/inputs/ooo.input"; | ||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; | ||
import { Expose, Type } from "class-transformer"; | ||
import { IsInt, IsEnum, ValidateNested, IsString, IsDateString, IsOptional } from "class-validator"; | ||
|
||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; | ||
|
||
export class UserOooOutputDto { | ||
@IsInt() | ||
@Expose() | ||
@ApiProperty({ | ||
description: "The ID of the user.", | ||
example: 2, | ||
}) | ||
readonly userId!: number; | ||
|
||
@IsInt() | ||
@IsOptional() | ||
@ApiPropertyOptional({ | ||
description: "The ID of the user covering for the out of office period, if applicable.", | ||
example: 2, | ||
}) | ||
@Expose() | ||
readonly toUserId?: number; | ||
|
||
@IsInt() | ||
@Expose() | ||
@ApiProperty({ | ||
description: "The ID of the ooo entry.", | ||
example: 2, | ||
}) | ||
readonly id!: number; | ||
|
||
@IsString() | ||
@Expose() | ||
@ApiProperty({ | ||
description: "The UUID of the ooo entry.", | ||
example: 2, | ||
}) | ||
readonly uuid!: string; | ||
|
||
@IsDateString() | ||
@ApiProperty({ | ||
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.", | ||
example: "2023-05-01T00:00:00.000Z", | ||
}) | ||
@Expose() | ||
start!: Date; | ||
|
||
@IsDateString() | ||
@ApiProperty({ | ||
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.", | ||
example: "2023-05-10T23:59:59.999Z", | ||
}) | ||
@Expose() | ||
end!: Date; | ||
|
||
@IsString() | ||
@IsOptional() | ||
@ApiPropertyOptional({ | ||
description: "Optional notes for the out of office entry.", | ||
example: "Vacation in Hawaii", | ||
}) | ||
@Expose() | ||
notes?: string; | ||
|
||
@IsEnum(OutOfOfficeReason) | ||
@IsOptional() | ||
@ApiPropertyOptional({ | ||
description: "the reason for the out of office entry, if applicable", | ||
example: "vacation", | ||
}) | ||
@Expose() | ||
reason?: OutOfOfficeReason; | ||
} | ||
|
||
export class UserOooOutputResponseDto { | ||
@Expose() | ||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) | ||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS]) | ||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; | ||
|
||
@Expose() | ||
@ValidateNested() | ||
@Type(() => UserOooOutputDto) | ||
data!: UserOooOutputDto; | ||
} | ||
|
||
export class UserOoosOutputResponseDto { | ||
@Expose() | ||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) | ||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS]) | ||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; | ||
|
||
@Expose() | ||
@ValidateNested() | ||
@Type(() => UserOooOutputDto) | ||
data!: UserOooOutputDto[]; | ||
} |
100 changes: 100 additions & 0 deletions
100
apps/api/v2/src/modules/ooo/repositories/ooo.repository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; | ||
import { Injectable } from "@nestjs/common"; | ||
import { v4 as uuidv4 } from "uuid"; | ||
|
||
import { Prisma } from "@calcom/prisma/client"; | ||
|
||
import { PrismaWriteService } from "../../prisma/prisma-write.service"; | ||
|
||
type OOOInputData = Omit<Prisma.OutOfOfficeEntryCreateInput, "user" | "toUser" | "reason" | "uuid"> & { | ||
toUserId?: number; | ||
userId: number; | ||
reasonId?: number; | ||
}; | ||
|
||
@Injectable() | ||
export class UserOOORepository { | ||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} | ||
|
||
async createUserOOO(data: OOOInputData) { | ||
const uuid = uuidv4(); | ||
return this.dbWrite.prisma.outOfOfficeEntry.create({ | ||
data: { ...data, uuid }, | ||
include: { reason: true }, | ||
}); | ||
} | ||
|
||
async updateUserOOO(oooId: number, data: Partial<OOOInputData>) { | ||
return this.dbWrite.prisma.outOfOfficeEntry.update({ | ||
where: { id: oooId }, | ||
data, | ||
include: { reason: true }, | ||
}); | ||
} | ||
|
||
async getUserOOOById(oooId: number) { | ||
return this.dbRead.prisma.outOfOfficeEntry.findFirst({ | ||
where: { id: oooId }, | ||
include: { reason: true }, | ||
}); | ||
} | ||
|
||
async getUserOOOByIdAndUserId(oooId: number, userId: number) { | ||
return this.dbRead.prisma.outOfOfficeEntry.findFirst({ | ||
where: { id: oooId, userId }, | ||
include: { reason: true }, | ||
}); | ||
} | ||
|
||
async getUserOOOPaginated(userId: number, skip: number, take: number) { | ||
return this.dbRead.prisma.outOfOfficeEntry.findMany({ | ||
where: { userId }, | ||
skip, | ||
take, | ||
include: { reason: true }, | ||
}); | ||
} | ||
|
||
async deleteUserOOO(oooId: number) { | ||
return this.dbWrite.prisma.outOfOfficeEntry.delete({ | ||
where: { id: oooId }, | ||
include: { reason: true }, | ||
}); | ||
} | ||
|
||
async findExistingOooRedirect(userId: number, start: Date, end: Date, toUserId?: number) { | ||
const existingOutOfOfficeEntry = await this.dbRead.prisma.outOfOfficeEntry.findFirst({ | ||
select: { | ||
userId: true, | ||
toUserId: true, | ||
}, | ||
where: { | ||
...(toUserId && { userId: toUserId }), | ||
toUserId: userId, | ||
// Check for time overlap or collision | ||
OR: [ | ||
// Outside of range | ||
{ | ||
AND: [{ start: { lte: end } }, { end: { gte: start } }], | ||
}, | ||
// Inside of range | ||
{ | ||
AND: [{ start: { gte: start } }, { end: { lte: end } }], | ||
}, | ||
], | ||
}, | ||
}); | ||
|
||
return existingOutOfOfficeEntry; | ||
} | ||
|
||
async getOooByUserIdAndTime(userId: number, start: Date, end: Date) { | ||
return await this.dbRead.prisma.outOfOfficeEntry.findFirst({ | ||
where: { | ||
userId, | ||
start, | ||
end, | ||
}, | ||
}); | ||
} | ||
} |
Oops, something went wrong.