Skip to content

Commit ae4474c

Browse files
asadath1395Praashh
andauthored
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>
1 parent 3290ae5 commit ae4474c

File tree

8 files changed

+221
-31
lines changed

8 files changed

+221
-31
lines changed

apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts

+7
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,11 @@ export class EventTypesRepository_2024_04_15 {
7878
async deleteEventType(eventTypeId: number) {
7979
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
8080
}
81+
82+
async getEventTypeWithDuration(eventTypeId: number) {
83+
return this.dbRead.prisma.eventType.findUnique({
84+
where: { id: eventTypeId },
85+
select: { length: true },
86+
});
87+
}
8188
}

apps/api/v2/src/modules/slots/controllers/slots.controller.ts

+49-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation }
55
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
66

77
import { SUCCESS_STATUS } from "@calcom/platform-constants";
8+
import { SlotFormat } from "@calcom/platform-enums";
89
import { getAvailableSlots } from "@calcom/platform-libraries";
910
import type { AvailableSlotsType } from "@calcom/platform-libraries";
1011
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
@@ -89,9 +90,31 @@ export class SlotsController {
8990
type: "array",
9091
items: {
9192
type: "object",
92-
properties: {
93-
time: { type: "string", format: "date-time", example: "2024-09-25T08:00:00.000Z" },
94-
},
93+
oneOf: [
94+
{
95+
properties: {
96+
time: {
97+
type: "string",
98+
format: "date-time",
99+
example: "2024-09-25T08:00:00.000Z",
100+
},
101+
},
102+
},
103+
{
104+
properties: {
105+
startTime: {
106+
type: "string",
107+
format: "date-time",
108+
example: "2024-09-25T08:00:00.000Z",
109+
},
110+
endTime: {
111+
type: "string",
112+
format: "date-time",
113+
example: "2024-09-25T08:30:00.000Z",
114+
},
115+
},
116+
},
117+
],
95118
},
96119
},
97120
},
@@ -102,11 +125,18 @@ export class SlotsController {
102125
status: "success",
103126
data: {
104127
slots: {
128+
// Default format (when slotFormat is 'time' or not provided)
105129
"2024-09-25": [{ time: "2024-09-25T08:00:00.000Z" }, { time: "2024-09-25T08:15:00.000Z" }],
130+
// Alternative format (when slotFormat is 'range')
106131
"2024-09-26": [
107-
{ time: "2024-09-26T08:00:00.000Z" },
108-
{ time: "2024-09-26T08:15:00.000Z" },
109-
{ time: "2024-09-26T08:30:00.000Z" },
132+
{
133+
startTime: "2024-09-26T08:00:00.000Z",
134+
endTime: "2024-09-26T08:30:00.000Z",
135+
},
136+
{
137+
startTime: "2024-09-26T08:15:00.000Z",
138+
endTime: "2024-09-26T08:45:00.000Z",
139+
},
110140
],
111141
},
112142
},
@@ -129,8 +159,20 @@ export class SlotsController {
129159
},
130160
});
131161

162+
const transformedSlots =
163+
query.slotFormat === SlotFormat.Range
164+
? await this.slotsService.formatSlots(
165+
availableSlots,
166+
query.duration,
167+
query.eventTypeId,
168+
query.slotFormat
169+
)
170+
: availableSlots.slots;
171+
132172
return {
133-
data: availableSlots,
173+
data: {
174+
slots: transformedSlots,
175+
},
134176
status: SUCCESS_STATUS,
135177
};
136178
}

apps/api/v2/src/modules/slots/services/slots.service.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
22
import { SlotsRepository } from "@/modules/slots/slots.repository";
3-
import { Injectable, NotFoundException } from "@nestjs/common";
3+
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
44
import { v4 as uuid } from "uuid";
55

6+
import { SlotFormat } from "@calcom/platform-enums";
67
import { ReserveSlotInput } from "@calcom/platform-types";
78

89
@Injectable()
@@ -54,4 +55,51 @@ export class SlotsService {
5455
const event = await this.eventTypeRepo.getEventTypeById(eventTypeId);
5556
return !!event?.teamId;
5657
}
58+
59+
async getEventTypeWithDuration(eventTypeId: number) {
60+
return await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
61+
}
62+
63+
async getDuration(duration?: number, eventTypeId?: number): Promise<number> {
64+
if (duration) {
65+
return duration;
66+
}
67+
68+
if (eventTypeId) {
69+
const eventType = await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
70+
if (!eventType) {
71+
throw new Error("Event type not found");
72+
}
73+
return eventType.length;
74+
}
75+
76+
throw new Error("duration or eventTypeId is required");
77+
}
78+
79+
async formatSlots(
80+
availableSlots: { slots: Record<string, { time: string }[]> },
81+
duration?: number,
82+
eventTypeId?: number,
83+
slotFormat?: SlotFormat
84+
): Promise<Record<string, { startTime: string; endTime: string }[]>> {
85+
if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) {
86+
throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'");
87+
}
88+
89+
const slotDuration = await this.getDuration(duration, eventTypeId);
90+
91+
return Object.entries(availableSlots.slots).reduce<
92+
Record<string, { startTime: string; endTime: string }[]>
93+
>((acc, [date, slots]) => {
94+
acc[date] = (slots as { time: string }[]).map((slot) => {
95+
const startTime = new Date(slot.time);
96+
const endTime = new Date(startTime.getTime() + slotDuration * 60000);
97+
return {
98+
startTime: startTime.toISOString(),
99+
endTime: endTime.toISOString(),
100+
};
101+
});
102+
return acc;
103+
}, {});
104+
}
57105
}

apps/api/v2/swagger/documentation.json

+42-11
Original file line numberDiff line numberDiff line change
@@ -5041,6 +5041,20 @@
50415041
"schema": {
50425042
"type": "number"
50435043
}
5044+
},
5045+
{
5046+
"name": "slotFormat",
5047+
"required": false,
5048+
"in": "query",
5049+
"description": "Format of slot times in response. Use 'range' to get start and end times.",
5050+
"example": "range",
5051+
"schema": {
5052+
"enum": [
5053+
"range",
5054+
"time"
5055+
],
5056+
"type": "string"
5057+
}
50445058
}
50455059
],
50465060
"responses": {
@@ -5064,13 +5078,31 @@
50645078
"type": "array",
50655079
"items": {
50665080
"type": "object",
5067-
"properties": {
5068-
"time": {
5069-
"type": "string",
5070-
"format": "date-time",
5071-
"example": "2024-09-25T08:00:00.000Z"
5081+
"oneOf": [
5082+
{
5083+
"properties": {
5084+
"time": {
5085+
"type": "string",
5086+
"format": "date-time",
5087+
"example": "2024-09-25T08:00:00.000Z"
5088+
}
5089+
}
5090+
},
5091+
{
5092+
"properties": {
5093+
"startTime": {
5094+
"type": "string",
5095+
"format": "date-time",
5096+
"example": "2024-09-25T08:00:00.000Z"
5097+
},
5098+
"endTime": {
5099+
"type": "string",
5100+
"format": "date-time",
5101+
"example": "2024-09-25T08:30:00.000Z"
5102+
}
5103+
}
50725104
}
5073-
}
5105+
]
50745106
}
50755107
}
50765108
}
@@ -5091,13 +5123,12 @@
50915123
],
50925124
"2024-09-26": [
50935125
{
5094-
"time": "2024-09-26T08:00:00.000Z"
5095-
},
5096-
{
5097-
"time": "2024-09-26T08:15:00.000Z"
5126+
"startTime": "2024-09-26T08:00:00.000Z",
5127+
"endTime": "2024-09-26T08:30:00.000Z"
50985128
},
50995129
{
5100-
"time": "2024-09-26T08:30:00.000Z"
5130+
"startTime": "2024-09-26T08:15:00.000Z",
5131+
"endTime": "2024-09-26T08:45:00.000Z"
51015132
}
51025133
]
51035134
}

docs/api-reference/v2/openapi.json

+39-11
Original file line numberDiff line numberDiff line change
@@ -4787,6 +4787,17 @@
47874787
"schema": {
47884788
"type": "number"
47894789
}
4790+
},
4791+
{
4792+
"name": "slotFormat",
4793+
"required": false,
4794+
"in": "query",
4795+
"description": "Format of slot times in response. Use 'range' to get start and end times.",
4796+
"example": "range",
4797+
"schema": {
4798+
"enum": ["range", "time"],
4799+
"type": "string"
4800+
}
47904801
}
47914802
],
47924803
"responses": {
@@ -4810,13 +4821,31 @@
48104821
"type": "array",
48114822
"items": {
48124823
"type": "object",
4813-
"properties": {
4814-
"time": {
4815-
"type": "string",
4816-
"format": "date-time",
4817-
"example": "2024-09-25T08:00:00.000Z"
4824+
"oneOf": [
4825+
{
4826+
"properties": {
4827+
"time": {
4828+
"type": "string",
4829+
"format": "date-time",
4830+
"example": "2024-09-25T08:00:00.000Z"
4831+
}
4832+
}
4833+
},
4834+
{
4835+
"properties": {
4836+
"startTime": {
4837+
"type": "string",
4838+
"format": "date-time",
4839+
"example": "2024-09-25T08:00:00.000Z"
4840+
},
4841+
"endTime": {
4842+
"type": "string",
4843+
"format": "date-time",
4844+
"example": "2024-09-25T08:30:00.000Z"
4845+
}
4846+
}
48184847
}
4819-
}
4848+
]
48204849
}
48214850
}
48224851
}
@@ -4837,13 +4866,12 @@
48374866
],
48384867
"2024-09-26": [
48394868
{
4840-
"time": "2024-09-26T08:00:00.000Z"
4841-
},
4842-
{
4843-
"time": "2024-09-26T08:15:00.000Z"
4869+
"startTime": "2024-09-26T08:00:00.000Z",
4870+
"endTime": "2024-09-26T08:30:00.000Z"
48444871
},
48454872
{
4846-
"time": "2024-09-26T08:30:00.000Z"
4873+
"startTime": "2024-09-26T08:15:00.000Z",
4874+
"endTime": "2024-09-26T08:45:00.000Z"
48474875
}
48484876
]
48494877
}

packages/platform/enums/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from "./event-types/scheduling-type";
77
export * from "./event-types/frequency";
88
export * from "./event-types/booker-layouts.enum";
99
export * from "./event-types/confirmation-policy.enum";
10+
export * from "./slot-format.enum";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum SlotFormat {
2+
Range = "range",
3+
Time = "time",
4+
}

packages/platform/types/slots.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
22
import { Transform } from "class-transformer";
3-
import { IsArray, IsBoolean, IsDateString, IsInt, IsNumber, IsOptional, IsString } from "class-validator";
3+
import {
4+
IsArray,
5+
IsBoolean,
6+
IsDateString,
7+
IsInt,
8+
IsNumber,
9+
IsOptional,
10+
IsString,
11+
Min,
12+
IsEnum,
13+
} from "class-validator";
14+
15+
import { SlotFormat } from "@calcom/platform-enums";
416

517
export class GetAvailableSlotsInput {
618
@IsDateString()
@@ -43,6 +55,7 @@ export class GetAvailableSlotsInput {
4355
@Transform(({ value }: { value: string }) => value && parseInt(value))
4456
@IsNumber()
4557
@IsOptional()
58+
@Min(1, { message: "Duration must be a positive number" })
4659
@ApiProperty({ description: "Only for dynamic events - length of returned slots." })
4760
duration?: number;
4861

@@ -57,6 +70,22 @@ export class GetAvailableSlotsInput {
5770
@IsString()
5871
@IsOptional()
5972
orgSlug?: string;
73+
74+
@IsString()
75+
@IsEnum(SlotFormat, {
76+
message: "slotFormat must be either 'range' or 'time'",
77+
})
78+
@Transform(({ value }) => {
79+
if (!value) return undefined;
80+
return value.toLowerCase();
81+
})
82+
@IsOptional()
83+
@ApiPropertyOptional({
84+
description: "Format of slot times in response. Use 'range' to get start and end times.",
85+
example: "range",
86+
enum: SlotFormat,
87+
})
88+
slotFormat?: SlotFormat;
6089
}
6190

6291
export class RemoveSelectedSlotInput {

0 commit comments

Comments
 (0)