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>
This commit is contained in:
Anwar Sadath
2024-12-05 19:12:17 +05:30
committed by GitHub
co-authored by Praash
parent 3290ae526a
commit ae4474cd42
8 changed files with 221 additions and 31 deletions
@@ -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 },
});
}
}
@@ -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,
};
}
@@ -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<number> {
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<string, { time: string }[]> },
duration?: number,
eventTypeId?: number,
slotFormat?: SlotFormat
): Promise<Record<string, { startTime: string; endTime: string }[]>> {
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<string, { startTime: string; endTime: string }[]>
>((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;
}, {});
}
}
+42 -11
View File
@@ -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"
}
]
}
+39 -11
View File
@@ -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"
}
]
}
+1
View File
@@ -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";
@@ -0,0 +1,4 @@
export enum SlotFormat {
Range = "range",
Time = "time",
}
+30 -1
View File
@@ -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 {