Skip to content

Commit b014ded

Browse files
authored
feat: api v2 event types ordering - user, team, org (calcom#25177)
* feat: api-v2-event-types-ordering * sort team and org event types * revert: remove accidental changes to api-auth.strategy.ts * docs: add ordering documentation and test for event types endpoints - Added test assertion to verify event types are returned in descending order by ID (newest first) - Added API documentation to user event types endpoint describing default ordering behavior - Added API documentation to team event types endpoint describing default ordering behavior - Added API documentation to organization event types endpoints describing default ordering behavior Addresses PR feedback to document and test the ordering behavior introduced in the API v2 event types ordering feature. * feat: add optional sortCreatedAt parameter to event types endpoints - Add sortCreatedAt query parameter (SortOrderType: "asc" | "desc") to all event types endpoints - Define SortOrder enum and SortOrderType in pagination.input.ts for reusability - When not provided, no explicit ordering is applied (backward compatible) - Update user, team, and organization event types endpoints - Add comprehensive e2e tests for all sorting scenarios - Fix circular dependency in platform-types import - Thread sortCreatedAt through all service layers - Use spread pattern for conditional orderBy to avoid empty array issues Addresses PR feedback to make ordering opt-in rather than changing default behavior
1 parent 46b88a3 commit b014ded

File tree

13 files changed

+209
-34
lines changed

13 files changed

+209
-34
lines changed

apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,9 @@ describe("Event types Endpoints", () => {
640640
hiddenEventType = responseBody.data;
641641
});
642642

643-
it(`/GET/event-types by username`, async () => {
643+
it(`/GET/event-types by username with sortCreatedAt=desc`, async () => {
644644
const response = await request(app.getHttpServer())
645-
.get(`/api/v2/event-types?username=${user.username}`)
645+
.get(`/api/v2/event-types?username=${user.username}&sortCreatedAt=desc`)
646646
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
647647
.set("Authorization", `Bearer ${apiKeyString}`)
648648
.expect(200);
@@ -653,6 +653,12 @@ describe("Event types Endpoints", () => {
653653
expect(responseBody.data).toBeDefined();
654654
expect(responseBody.data?.length).toEqual(2);
655655

656+
// Verify ordering: event types are returned newest to oldest when sortCreatedAt=desc
657+
// hiddenEventType was created after eventType, so it should have a higher ID and appear first
658+
expect(responseBody.data[0].id).toBeGreaterThan(responseBody.data[1].id);
659+
expect(responseBody.data[0].id).toEqual(hiddenEventType.id);
660+
expect(responseBody.data[1].id).toEqual(eventType.id);
661+
656662
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
657663
const fetchedHiddenEventType = responseBody.data?.find((et) => et.id === hiddenEventType.id);
658664

@@ -711,6 +717,46 @@ describe("Event types Endpoints", () => {
711717
expect(fetchedEventType?.hidden).toEqual(false);
712718
});
713719

720+
it(`/GET/event-types by username with sortCreatedAt=asc`, async () => {
721+
const response = await request(app.getHttpServer())
722+
.get(`/api/v2/event-types?username=${user.username}&sortCreatedAt=asc`)
723+
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
724+
.set("Authorization", `Bearer ${apiKeyString}`)
725+
.expect(200);
726+
727+
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
728+
729+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
730+
expect(responseBody.data).toBeDefined();
731+
expect(responseBody.data?.length).toEqual(2);
732+
733+
// Verify ordering: event types are returned oldest to newest when sortCreatedAt=asc
734+
// eventType was created before hiddenEventType, so it should have a lower ID and appear first
735+
expect(responseBody.data[0].id).toBeLessThan(responseBody.data[1].id);
736+
expect(responseBody.data[0].id).toEqual(eventType.id);
737+
expect(responseBody.data[1].id).toEqual(hiddenEventType.id);
738+
});
739+
740+
it(`/GET/event-types by username without sortCreatedAt parameter`, async () => {
741+
const response = await request(app.getHttpServer())
742+
.get(`/api/v2/event-types?username=${user.username}`)
743+
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
744+
.set("Authorization", `Bearer ${apiKeyString}`)
745+
.expect(200);
746+
747+
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
748+
749+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
750+
expect(responseBody.data).toBeDefined();
751+
expect(responseBody.data?.length).toEqual(2);
752+
753+
// Without sortCreatedAt, no specific order is guaranteed
754+
// Just verify both event types are present
755+
const ids = responseBody.data.map((et) => et.id);
756+
expect(ids).toContain(eventType.id);
757+
expect(ids).toContain(hiddenEventType.id);
758+
});
759+
714760
it(`/GET/event-types by username should not return hidden event type if no auth provided`, async () => {
715761
const response = await request(app.getHttpServer())
716762
.get(`/api/v2/event-types?username=${user.username}`)

apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ export class EventTypesController_2024_06_14 {
155155
summary: "Get all event types",
156156
description: `Hidden event types are returned only if authentication is provided and it belongs to the event type owner.
157157
158+
Use the optional \`sortCreatedAt\` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.
159+
158160
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
159161
`,
160162
})

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
33
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
44
import { Injectable } from "@nestjs/common";
55

6+
import type { SortOrderType } from "@calcom/platform-types";
67
import type { Prisma } from "@calcom/prisma/client";
78

89
@Injectable()
@@ -67,21 +68,23 @@ export class EventTypesRepository_2024_06_14 {
6768
});
6869
}
6970

70-
async getUserEventTypes(userId: number) {
71+
async getUserEventTypes(userId: number, sortCreatedAt?: SortOrderType) {
7172
return this.dbRead.prisma.eventType.findMany({
7273
where: {
7374
userId,
7475
},
76+
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
7577
include: { users: true, schedule: true, destinationCalendar: true },
7678
});
7779
}
7880

79-
async getUserEventTypesPublic(userId: number) {
81+
async getUserEventTypesPublic(userId: number, sortCreatedAt?: SortOrderType) {
8082
return this.dbRead.prisma.eventType.findMany({
8183
where: {
8284
userId,
8385
hidden: false,
8486
},
87+
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
8588
include: { users: true, schedule: true, destinationCalendar: true },
8689
});
8790
}

apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
getEventTypesPublic,
2323
EventTypesPublic,
2424
} from "@calcom/platform-libraries/event-types";
25-
import type { GetEventTypesQuery_2024_06_14 } from "@calcom/platform-types";
25+
import type { GetEventTypesQuery_2024_06_14, SortOrderType } from "@calcom/platform-types";
2626
import type { EventType } from "@calcom/prisma/client";
2727

2828
@Injectable()
@@ -160,15 +160,16 @@ export class EventTypesService_2024_06_14 {
160160
orgSlug?: string;
161161
orgId?: number;
162162
authUser?: AuthOptionalUser;
163+
sortCreatedAt?: SortOrderType;
163164
}) {
164165
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
165166
if (!user) {
166167
return [];
167168
}
168169
if (params.authUser?.id !== user.id) {
169-
return await this.getUserEventTypesPublic(user.id);
170+
return await this.getUserEventTypesPublic(user.id, params.sortCreatedAt);
170171
}
171-
return await this.getUserEventTypes(user.id);
172+
return await this.getUserEventTypes(user.id, params.sortCreatedAt);
172173
}
173174

174175
async getUserToCreateEvent(user: UserWithProfile) {
@@ -211,16 +212,16 @@ export class EventTypesService_2024_06_14 {
211212
};
212213
}
213214

214-
async getUserEventTypes(userId: number) {
215-
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId);
215+
async getUserEventTypes(userId: number, sortCreatedAt?: SortOrderType) {
216+
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId, sortCreatedAt);
216217

217218
return eventTypes.map((eventType) => {
218219
return { ownerId: userId, ...eventType };
219220
});
220221
}
221222

222-
async getUserEventTypesPublic(userId: number) {
223-
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId);
223+
async getUserEventTypesPublic(userId: number, sortCreatedAt?: SortOrderType) {
224+
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId, sortCreatedAt);
224225

225226
return eventTypes.map((eventType) => {
226227
return { ownerId: userId, ...eventType };
@@ -237,7 +238,7 @@ export class EventTypesService_2024_06_14 {
237238
}
238239

239240
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14, authUser?: AuthOptionalUser) {
240-
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
241+
const { username, eventSlug, usernames, orgSlug, orgId, sortCreatedAt } = queryParams;
241242
if (username && eventSlug) {
242243
const eventType = await this.getEventTypeByUsernameAndSlug({
243244
username,
@@ -255,6 +256,7 @@ export class EventTypesService_2024_06_14 {
255256
orgSlug,
256257
orgId,
257258
authUser,
259+
sortCreatedAt,
258260
});
259261
}
260262

@@ -264,7 +266,7 @@ export class EventTypesService_2024_06_14 {
264266
}
265267

266268
if (authUser?.id) {
267-
return await this.getUserEventTypes(authUser.id);
269+
return await this.getUserEventTypes(authUser.id, sortCreatedAt);
268270
}
269271

270272
return [];

apps/api/v2/src/modules/organizations/event-types/organizations-event-types.controller.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
4747
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
4848
import {
4949
CreateTeamEventTypeInput_2024_06_14,
50+
GetOrganizationEventTypesQuery_2024_06_14,
5051
GetTeamEventTypesQuery_2024_06_14,
51-
SkipTakePagination,
5252
TeamEventTypeOutput_2024_06_14,
5353
UpdateTeamEventTypeInput_2024_06_14,
5454
} from "@calcom/platform-types";
@@ -159,12 +159,16 @@ export class OrganizationsEventTypesController {
159159

160160
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
161161
@Get("/teams/:teamId/event-types")
162-
@ApiOperation({ summary: "Get team event types" })
162+
@ApiOperation({
163+
summary: "Get team event types",
164+
description:
165+
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
166+
})
163167
async getTeamEventTypes(
164168
@Param("teamId", ParseIntPipe) teamId: number,
165169
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
166170
): Promise<GetTeamEventTypesOutput> {
167-
const { eventSlug, hostsLimit } = queryParams;
171+
const { eventSlug, hostsLimit, sortCreatedAt } = queryParams;
168172

169173
if (eventSlug) {
170174
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(
@@ -179,7 +183,7 @@ export class OrganizationsEventTypesController {
179183
};
180184
}
181185

182-
const eventTypes = await this.organizationsEventTypesService.getTeamEventTypes(teamId);
186+
const eventTypes = await this.organizationsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
183187

184188
return {
185189
status: SUCCESS_STATUS,
@@ -191,16 +195,21 @@ export class OrganizationsEventTypesController {
191195
@PlatformPlan("ESSENTIALS")
192196
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
193197
@Get("/teams/event-types")
194-
@ApiOperation({ summary: "Get all team event types" })
198+
@ApiOperation({
199+
summary: "Get all team event types",
200+
description:
201+
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
202+
})
195203
async getTeamsEventTypes(
196204
@Param("orgId", ParseIntPipe) orgId: number,
197-
@Query() queryParams: SkipTakePagination
205+
@Query() queryParams: GetOrganizationEventTypesQuery_2024_06_14
198206
): Promise<GetTeamEventTypesOutput> {
199-
const { skip, take } = queryParams;
207+
const { skip, take, sortCreatedAt } = queryParams;
200208
const eventTypes = await this.organizationsEventTypesService.getOrganizationsTeamsEventTypes(
201209
orgId,
202210
skip,
203-
take
211+
take,
212+
sortCreatedAt
204213
);
205214

206215
return {

apps/api/v2/src/modules/organizations/event-types/organizations-event-types.repository.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
22
import { Injectable } from "@nestjs/common";
33

4+
import type { SortOrderType } from "@calcom/platform-types";
5+
46
@Injectable()
57
export class OrganizationsEventTypesRepository {
68
constructor(private readonly dbRead: PrismaReadService) {}
7-
async getOrganizationTeamsEventTypes(orgId: number, skip: number, take: number) {
9+
async getOrganizationTeamsEventTypes(
10+
orgId: number,
11+
skip: number,
12+
take: number,
13+
sortCreatedAt?: SortOrderType
14+
) {
815
return this.dbRead.prisma.eventType.findMany({
916
where: {
1017
team: {
1118
parentId: orgId,
1219
},
1320
},
21+
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
1422
skip,
1523
take,
1624
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },

apps/api/v2/src/modules/organizations/event-types/services/organizations-event-types.service.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
2+
3+
import type { SortOrderType } from "@calcom/platform-types";
24
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
35
import {
46
TransformedCreateTeamEventTypeInput,
@@ -83,16 +85,22 @@ export class OrganizationsEventTypesService {
8385
return this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventTypeSlug, hostsLimit);
8486
}
8587

86-
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
87-
return await this.teamsEventTypesService.getTeamEventTypes(teamId);
88+
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType): Promise<DatabaseTeamEventType[]> {
89+
return await this.teamsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
8890
}
8991

9092
async getOrganizationsTeamsEventTypes(
9193
orgId: number,
9294
skip = 0,
93-
take = 250
95+
take = 250,
96+
sortCreatedAt?: SortOrderType
9497
): Promise<DatabaseTeamEventType[]> {
95-
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
98+
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(
99+
orgId,
100+
skip,
101+
take,
102+
sortCreatedAt
103+
);
96104
}
97105

98106
async updateOrganizationTeamEventType(

apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,16 @@ export class TeamsEventTypesController {
136136
}
137137

138138
@Get("/")
139-
@ApiOperation({ summary: "Get a team event type" })
139+
@ApiOperation({
140+
summary: "Get team event types",
141+
description:
142+
'Use the optional `sortCreatedAt` query parameter to order results by creation date (by ID). Accepts "asc" (oldest first) or "desc" (newest first). When not provided, no explicit ordering is applied.',
143+
})
140144
async getTeamEventTypes(
141145
@Param("teamId", ParseIntPipe) teamId: number,
142146
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
143147
): Promise<GetTeamEventTypesOutput> {
144-
const { eventSlug, hostsLimit } = queryParams;
148+
const { eventSlug, hostsLimit, sortCreatedAt } = queryParams;
145149

146150
if (eventSlug) {
147151
const eventType = await this.teamsEventTypesService.getTeamEventTypeBySlug(
@@ -156,7 +160,7 @@ export class TeamsEventTypesController {
156160
};
157161
}
158162

159-
const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId);
163+
const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId, sortCreatedAt);
160164

161165
return {
162166
status: SUCCESS_STATUS,

apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { UsersService } from "@/modules/users/services/users.service";
1111
import { UserWithProfile } from "@/modules/users/users.repository";
1212
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
1313

14+
import type { SortOrderType } from "@calcom/platform-types";
15+
1416
import { createEventType, updateEventType } from "@calcom/platform-libraries/event-types";
1517

1618
@Injectable()
@@ -101,8 +103,8 @@ export class TeamsEventTypesService {
101103
return eventType;
102104
}
103105

104-
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
105-
return await this.teamsEventTypesRepository.getTeamEventTypes(teamId);
106+
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType): Promise<DatabaseTeamEventType[]> {
107+
return await this.teamsEventTypesRepository.getTeamEventTypes(teamId, sortCreatedAt);
106108
}
107109

108110
async updateTeamEventType(

apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
22
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
33
import { Injectable } from "@nestjs/common";
44

5+
import type { SortOrderType } from "@calcom/platform-types";
6+
57
@Injectable()
68
export class TeamsEventTypesRepository {
79
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
@@ -79,11 +81,12 @@ export class TeamsEventTypesRepository {
7981
});
8082
}
8183

82-
async getTeamEventTypes(teamId: number) {
84+
async getTeamEventTypes(teamId: number, sortCreatedAt?: SortOrderType) {
8385
return this.dbRead.prisma.eventType.findMany({
8486
where: {
8587
teamId,
8688
},
89+
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
8790
include: {
8891
users: true,
8992
schedule: true,

0 commit comments

Comments
 (0)