From ae4474cd4204a7a10a10221d77cc7973acb49467 Mon Sep 17 00:00:00 2001 From: Anwar Sadath <37188392+asadath1395@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:12:17 +0530 Subject: [PATCH] feat: Add slotFormat query parameter to slots API V2 endpoint to return start and end time of a slot (#17873) * feat: Add formatAsStartAndEndTime query parameter to slots API V2 endpoint to return start and end time of a slot * Update apps/api/v2/src/modules/slots/controllers/slots.controller.ts Co-authored-by: Praash <99237795+Praashh@users.noreply.github.com> * Fix duration was allowed to be any number * Refactor code * Show slotFormat property as optional in swagger doc --------- Co-authored-by: Praash <99237795+Praashh@users.noreply.github.com> --- .../event-types.repository.ts | 7 +++ .../slots/controllers/slots.controller.ts | 56 ++++++++++++++++--- .../modules/slots/services/slots.service.ts | 50 ++++++++++++++++- apps/api/v2/swagger/documentation.json | 53 ++++++++++++++---- docs/api-reference/v2/openapi.json | 50 +++++++++++++---- packages/platform/enums/index.ts | 1 + packages/platform/enums/slot-format.enum.ts | 4 ++ packages/platform/types/slots.ts | 31 +++++++++- 8 files changed, 221 insertions(+), 31 deletions(-) create mode 100644 packages/platform/enums/slot-format.enum.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts index 799cdc5e50..4fbde6d2c1 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts @@ -78,4 +78,11 @@ export class EventTypesRepository_2024_04_15 { async deleteEventType(eventTypeId: number) { return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } }); } + + async getEventTypeWithDuration(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + select: { length: true }, + }); + } } diff --git a/apps/api/v2/src/modules/slots/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/controllers/slots.controller.ts index 9c4f321e6d..b381e936ca 100644 --- a/apps/api/v2/src/modules/slots/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/controllers/slots.controller.ts @@ -5,6 +5,7 @@ import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation } import { Response as ExpressResponse, Request as ExpressRequest } from "express"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { SlotFormat } from "@calcom/platform-enums"; import { getAvailableSlots } from "@calcom/platform-libraries"; import type { AvailableSlotsType } from "@calcom/platform-libraries"; import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types"; @@ -89,9 +90,31 @@ export class SlotsController { type: "array", items: { type: "object", - properties: { - time: { type: "string", format: "date-time", example: "2024-09-25T08:00:00.000Z" }, - }, + oneOf: [ + { + properties: { + time: { + type: "string", + format: "date-time", + example: "2024-09-25T08:00:00.000Z", + }, + }, + }, + { + properties: { + startTime: { + type: "string", + format: "date-time", + example: "2024-09-25T08:00:00.000Z", + }, + endTime: { + type: "string", + format: "date-time", + example: "2024-09-25T08:30:00.000Z", + }, + }, + }, + ], }, }, }, @@ -102,11 +125,18 @@ export class SlotsController { status: "success", data: { slots: { + // Default format (when slotFormat is 'time' or not provided) "2024-09-25": [{ time: "2024-09-25T08:00:00.000Z" }, { time: "2024-09-25T08:15:00.000Z" }], + // Alternative format (when slotFormat is 'range') "2024-09-26": [ - { time: "2024-09-26T08:00:00.000Z" }, - { time: "2024-09-26T08:15:00.000Z" }, - { time: "2024-09-26T08:30:00.000Z" }, + { + startTime: "2024-09-26T08:00:00.000Z", + endTime: "2024-09-26T08:30:00.000Z", + }, + { + startTime: "2024-09-26T08:15:00.000Z", + endTime: "2024-09-26T08:45:00.000Z", + }, ], }, }, @@ -129,8 +159,20 @@ export class SlotsController { }, }); + const transformedSlots = + query.slotFormat === SlotFormat.Range + ? await this.slotsService.formatSlots( + availableSlots, + query.duration, + query.eventTypeId, + query.slotFormat + ) + : availableSlots.slots; + return { - data: availableSlots, + data: { + slots: transformedSlots, + }, status: SUCCESS_STATUS, }; } diff --git a/apps/api/v2/src/modules/slots/services/slots.service.ts b/apps/api/v2/src/modules/slots/services/slots.service.ts index 4ba2e2c37c..7d076ec6a4 100644 --- a/apps/api/v2/src/modules/slots/services/slots.service.ts +++ b/apps/api/v2/src/modules/slots/services/slots.service.ts @@ -1,8 +1,9 @@ import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository"; import { SlotsRepository } from "@/modules/slots/slots.repository"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; import { v4 as uuid } from "uuid"; +import { SlotFormat } from "@calcom/platform-enums"; import { ReserveSlotInput } from "@calcom/platform-types"; @Injectable() @@ -54,4 +55,51 @@ export class SlotsService { const event = await this.eventTypeRepo.getEventTypeById(eventTypeId); return !!event?.teamId; } + + async getEventTypeWithDuration(eventTypeId: number) { + return await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId); + } + + async getDuration(duration?: number, eventTypeId?: number): Promise { + if (duration) { + return duration; + } + + if (eventTypeId) { + const eventType = await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId); + if (!eventType) { + throw new Error("Event type not found"); + } + return eventType.length; + } + + throw new Error("duration or eventTypeId is required"); + } + + async formatSlots( + availableSlots: { slots: Record }, + duration?: number, + eventTypeId?: number, + slotFormat?: SlotFormat + ): Promise> { + if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) { + throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'"); + } + + const slotDuration = await this.getDuration(duration, eventTypeId); + + return Object.entries(availableSlots.slots).reduce< + Record + >((acc, [date, slots]) => { + acc[date] = (slots as { time: string }[]).map((slot) => { + const startTime = new Date(slot.time); + const endTime = new Date(startTime.getTime() + slotDuration * 60000); + return { + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + }; + }); + return acc; + }, {}); + } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 3a2cf217ad..72b6a7404e 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -5041,6 +5041,20 @@ "schema": { "type": "number" } + }, + { + "name": "slotFormat", + "required": false, + "in": "query", + "description": "Format of slot times in response. Use 'range' to get start and end times.", + "example": "range", + "schema": { + "enum": [ + "range", + "time" + ], + "type": "string" + } } ], "responses": { @@ -5064,13 +5078,31 @@ "type": "array", "items": { "type": "object", - "properties": { - "time": { - "type": "string", - "format": "date-time", - "example": "2024-09-25T08:00:00.000Z" + "oneOf": [ + { + "properties": { + "time": { + "type": "string", + "format": "date-time", + "example": "2024-09-25T08:00:00.000Z" + } + } + }, + { + "properties": { + "startTime": { + "type": "string", + "format": "date-time", + "example": "2024-09-25T08:00:00.000Z" + }, + "endTime": { + "type": "string", + "format": "date-time", + "example": "2024-09-25T08:30:00.000Z" + } + } } - } + ] } } } @@ -5091,13 +5123,12 @@ ], "2024-09-26": [ { - "time": "2024-09-26T08:00:00.000Z" + "startTime": "2024-09-26T08:00:00.000Z", + "endTime": "2024-09-26T08:30:00.000Z" }, { - "time": "2024-09-26T08:15:00.000Z" - }, - { - "time": "2024-09-26T08:30:00.000Z" + "startTime": "2024-09-26T08:15:00.000Z", + "endTime": "2024-09-26T08:45:00.000Z" } ] } diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index b1191d750f..dfb70e5bc5 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -4787,6 +4787,17 @@ "schema": { "type": "number" } + }, + { + "name": "slotFormat", + "required": false, + "in": "query", + "description": "Format of slot times in response. Use 'range' to get start and end times.", + "example": "range", + "schema": { + "enum": ["range", "time"], + "type": "string" + } } ], "responses": { @@ -4810,13 +4821,31 @@ "type": "array", "items": { "type": "object", - "properties": { - "time": { - "type": "string", - "format": "date-time", - "example": "2024-09-25T08:00:00.000Z" + "oneOf": [ + { + "properties": { + "time": { + "type": "string", + "format": "date-time", + "example": "2024-09-25T08:00:00.000Z" + } + } + }, + { + "properties": { + "startTime": { + "type": "string", + "format": "date-time", + "example": "2024-09-25T08:00:00.000Z" + }, + "endTime": { + "type": "string", + "format": "date-time", + "example": "2024-09-25T08:30:00.000Z" + } + } } - } + ] } } } @@ -4837,13 +4866,12 @@ ], "2024-09-26": [ { - "time": "2024-09-26T08:00:00.000Z" + "startTime": "2024-09-26T08:00:00.000Z", + "endTime": "2024-09-26T08:30:00.000Z" }, { - "time": "2024-09-26T08:15:00.000Z" - }, - { - "time": "2024-09-26T08:30:00.000Z" + "startTime": "2024-09-26T08:15:00.000Z", + "endTime": "2024-09-26T08:45:00.000Z" } ] } diff --git a/packages/platform/enums/index.ts b/packages/platform/enums/index.ts index 756f02b39d..2be41e84f0 100644 --- a/packages/platform/enums/index.ts +++ b/packages/platform/enums/index.ts @@ -7,3 +7,4 @@ export * from "./event-types/scheduling-type"; export * from "./event-types/frequency"; export * from "./event-types/booker-layouts.enum"; export * from "./event-types/confirmation-policy.enum"; +export * from "./slot-format.enum"; diff --git a/packages/platform/enums/slot-format.enum.ts b/packages/platform/enums/slot-format.enum.ts new file mode 100644 index 0000000000..92effba369 --- /dev/null +++ b/packages/platform/enums/slot-format.enum.ts @@ -0,0 +1,4 @@ +export enum SlotFormat { + Range = "range", + Time = "time", +} diff --git a/packages/platform/types/slots.ts b/packages/platform/types/slots.ts index 3c6b1009b4..3b5d3d9618 100644 --- a/packages/platform/types/slots.ts +++ b/packages/platform/types/slots.ts @@ -1,6 +1,18 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Transform } from "class-transformer"; -import { IsArray, IsBoolean, IsDateString, IsInt, IsNumber, IsOptional, IsString } from "class-validator"; +import { + IsArray, + IsBoolean, + IsDateString, + IsInt, + IsNumber, + IsOptional, + IsString, + Min, + IsEnum, +} from "class-validator"; + +import { SlotFormat } from "@calcom/platform-enums"; export class GetAvailableSlotsInput { @IsDateString() @@ -43,6 +55,7 @@ export class GetAvailableSlotsInput { @Transform(({ value }: { value: string }) => value && parseInt(value)) @IsNumber() @IsOptional() + @Min(1, { message: "Duration must be a positive number" }) @ApiProperty({ description: "Only for dynamic events - length of returned slots." }) duration?: number; @@ -57,6 +70,22 @@ export class GetAvailableSlotsInput { @IsString() @IsOptional() orgSlug?: string; + + @IsString() + @IsEnum(SlotFormat, { + message: "slotFormat must be either 'range' or 'time'", + }) + @Transform(({ value }) => { + if (!value) return undefined; + return value.toLowerCase(); + }) + @IsOptional() + @ApiPropertyOptional({ + description: "Format of slot times in response. Use 'range' to get start and end times.", + example: "range", + enum: SlotFormat, + }) + slotFormat?: SlotFormat; } export class RemoveSelectedSlotInput {