feat: add pagination to get bookings endpoints [v2] (#21173)

* feat: add pagination to get bookings endpoints [v2]

* add decorators for pagination

* fix: ensure correct values

* code review comment

* fixup! code review comment
This commit is contained in:
Morgan
2025-05-12 10:09:56 +02:00
committed by GitHub
parent 1277f5e9f7
commit 77054d92df
10 changed files with 242 additions and 13 deletions
@@ -182,7 +182,7 @@ export class BookingsController_2024_08_13 {
): Promise<GetBookingsOutput_2024_08_13> {
const profile = this.usersService.getUserMainProfile(user);
const bookings = await this.bookingsService.getBookings(queryParams, {
const { bookings, pagination } = await this.bookingsService.getBookings(queryParams, {
email: user.email,
id: user.id,
orgId: profile?.organizationId,
@@ -191,6 +191,7 @@ export class BookingsController_2024_08_13 {
return {
status: SUCCESS_STATUS,
data: bookings,
pagination,
};
}
@@ -376,7 +376,7 @@ export class BookingsService_2024_08_13 {
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
}
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
const fetchedBookings: { bookings: { id: number }[]; totalCount: number } = await getAllUserBookings({
bookingListingByStatus: queryParams.status || [],
skip: queryParams.skip ?? 0,
take: queryParams.take ?? 100,
@@ -432,7 +432,29 @@ export class BookingsService_2024_08_13 {
}
}
return formattedBookings;
const skip = Math.abs(queryParams?.skip ?? 0);
const take = Math.abs(queryParams?.take ?? 100);
const itemsPerPage = take;
const totalPages = itemsPerPage !== 0 ? Math.ceil(fetchedBookings.totalCount / itemsPerPage) : 0;
const currentPage = Math.floor(skip / itemsPerPage) + 1;
const hasNextPage = skip + itemsPerPage < fetchedBookings.totalCount;
const hasPreviousPage = skip > 0;
return {
bookings: formattedBookings,
pagination: {
totalItems: fetchedBookings.totalCount,
// clamp remainingItems between 0 and totalCount
remainingItems: Math.min(
Math.max(fetchedBookings.totalCount - (skip + take), 0),
fetchedBookings.totalCount
),
itemsPerPage: itemsPerPage,
currentPage: currentPage,
totalPages: totalPages,
hasNextPage: hasNextPage,
hasPreviousPage: hasPreviousPage,
},
};
}
async getAttendeeEmail(queryParamsAttendeeEmail: string, user: { id: number }) {
@@ -45,7 +45,7 @@ export class OrganizationsBookingsController {
): Promise<GetBookingsOutput_2024_08_13> {
const { userIds, ...restParams } = queryParams;
const bookings = await this.bookingsService.getBookings(
const { bookings, pagination } = await this.bookingsService.getBookings(
{ ...restParams },
{ email: user.email, id: user.id, orgId },
userIds
@@ -54,6 +54,7 @@ export class OrganizationsBookingsController {
return {
status: SUCCESS_STATUS,
data: bookings,
pagination,
};
}
}
@@ -54,7 +54,7 @@ export class OrganizationsTeamsBookingsController {
@Param("orgId", ParseIntPipe) orgId: number,
@GetUser() user: UserWithProfile
): Promise<GetBookingsOutput_2024_08_13> {
const bookings = await this.bookingsService.getBookings(
const { bookings, pagination } = await this.bookingsService.getBookings(
{ ...queryParams, teamId },
{ email: user.email, id: user.id, orgId }
);
@@ -62,6 +62,7 @@ export class OrganizationsTeamsBookingsController {
return {
status: SUCCESS_STATUS,
data: bookings,
pagination,
};
}
@@ -40,7 +40,7 @@ export class OrganizationsUsersBookingsController {
@Param("userId", ParseIntPipe) userId: number,
@Query() query: GetBookingsInput_2024_08_13
): Promise<GetBookingsOutput_2024_08_13> {
const bookings = await this.organizationUsersBookingsService.getOrganizationUserBookings(
const { bookings, pagination } = await this.organizationUsersBookingsService.getOrganizationUserBookings(
orgId,
userId,
query
@@ -49,6 +49,7 @@ export class OrganizationsUsersBookingsController {
return {
status: SUCCESS_STATUS,
data: bookings,
pagination,
};
}
}
@@ -29,6 +29,7 @@ import {
RecurringBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
PaginationMetaDto,
} from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
@@ -373,17 +374,26 @@ describe("Organizations UsersBookings Endpoints 2024-08-13", () => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.pagination).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
const pagination: PaginationMetaDto = responseBody.pagination;
expect(data.length).toEqual(3);
expect(data.find((booking) => booking.id === createdPersonalBooking.id)).toBeDefined();
expect(data.find((booking) => booking.id === createdTeamBooking.id)).toBeDefined();
expect(
data.find((booking) => booking.id === createdPersonalBookingUsingUsername.id)
).toBeDefined();
expect(pagination.totalItems).toEqual(3);
expect(pagination.remainingItems).toEqual(0);
expect(pagination.hasNextPage).toEqual(false);
expect(pagination.hasPreviousPage).toEqual(false);
expect(pagination.itemsPerPage).toEqual(100);
expect(pagination.totalPages).toEqual(1);
expect(pagination.currentPage).toEqual(1);
});
});
});
+65 -1
View File
@@ -1689,6 +1689,7 @@
"description": "The number of items to return",
"example": 10,
"schema": {
"default": 100,
"type": "number"
}
},
@@ -1699,6 +1700,7 @@
"description": "The number of items to skip",
"example": 0,
"schema": {
"default": 0,
"type": "number"
}
},
@@ -5603,6 +5605,7 @@
"description": "The number of items to return",
"example": 10,
"schema": {
"default": 100,
"type": "number"
}
},
@@ -5613,6 +5616,7 @@
"description": "The number of items to skip",
"example": 0,
"schema": {
"default": 0,
"type": "number"
}
}
@@ -6978,6 +6982,7 @@
"description": "The number of items to return",
"example": 10,
"schema": {
"default": 100,
"type": "number"
}
},
@@ -6988,6 +6993,7 @@
"description": "The number of items to skip",
"example": 0,
"schema": {
"default": 0,
"type": "number"
}
},
@@ -24137,6 +24143,60 @@
"data"
]
},
"PaginationMetaDto": {
"type": "object",
"properties": {
"totalItems": {
"type": "number",
"minimum": 0,
"description": "The total number of items available across all pages, matching the query criteria.",
"example": 123
},
"remainingItems": {
"type": "number",
"minimum": 0,
"description": "The number of items remaining to be fetched *after* the current page. Calculated as: `totalItems - (skip + itemsPerPage)`.",
"example": 103
},
"itemsPerPage": {
"type": "number",
"minimum": 1,
"description": "The maximum number of items requested per page.",
"example": 10
},
"currentPage": {
"type": "number",
"minimum": 1,
"description": "The current page number being returned.",
"example": 2
},
"totalPages": {
"type": "number",
"minimum": 0,
"description": "The total number of pages available.",
"example": 13
},
"hasNextPage": {
"type": "boolean",
"description": "Indicates if there is a subsequent page available after the current one.",
"example": true
},
"hasPreviousPage": {
"type": "boolean",
"description": "Indicates if there is a preceding page available before the current one.",
"example": true
}
},
"required": [
"totalItems",
"remainingItems",
"itemsPerPage",
"currentPage",
"totalPages",
"hasNextPage",
"hasPreviousPage"
]
},
"GetBookingsOutput_2024_08_13": {
"type": "object",
"properties": {
@@ -24168,13 +24228,17 @@
},
"description": "Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects"
},
"pagination": {
"$ref": "#/components/schemas/PaginationMetaDto"
},
"error": {
"type": "object"
}
},
"required": [
"status",
"data"
"data",
"pagination"
]
},
"RescheduleBookingInput_2024_08_13": {
+64 -1
View File
@@ -1617,6 +1617,7 @@
"description": "The number of items to return",
"example": 10,
"schema": {
"default": 100,
"type": "number"
}
},
@@ -1627,6 +1628,7 @@
"description": "The number of items to skip",
"example": 0,
"schema": {
"default": 0,
"type": "number"
}
},
@@ -5357,6 +5359,7 @@
"description": "The number of items to return",
"example": 10,
"schema": {
"default": 100,
"type": "number"
}
},
@@ -5367,6 +5370,7 @@
"description": "The number of items to skip",
"example": 0,
"schema": {
"default": 0,
"type": "number"
}
}
@@ -6666,6 +6670,7 @@
"description": "The number of items to return",
"example": 10,
"schema": {
"default": 100,
"type": "number"
}
},
@@ -6676,6 +6681,7 @@
"description": "The number of items to skip",
"example": 0,
"schema": {
"default": 0,
"type": "number"
}
},
@@ -22123,6 +22129,60 @@
},
"required": ["status", "data"]
},
"PaginationMetaDto": {
"type": "object",
"properties": {
"totalItems": {
"type": "number",
"minimum": 0,
"description": "The total number of items available across all pages, matching the query criteria.",
"example": 123
},
"remainingItems": {
"type": "number",
"minimum": 0,
"description": "The number of items remaining to be fetched *after* the current page. Calculated as: `totalItems - (skip + itemsPerPage)`.",
"example": 103
},
"itemsPerPage": {
"type": "number",
"minimum": 1,
"description": "The maximum number of items requested per page.",
"example": 10
},
"currentPage": {
"type": "number",
"minimum": 1,
"description": "The current page number being returned.",
"example": 2
},
"totalPages": {
"type": "number",
"minimum": 0,
"description": "The total number of pages available.",
"example": 13
},
"hasNextPage": {
"type": "boolean",
"description": "Indicates if there is a subsequent page available after the current one.",
"example": true
},
"hasPreviousPage": {
"type": "boolean",
"description": "Indicates if there is a preceding page available before the current one.",
"example": true
}
},
"required": [
"totalItems",
"remainingItems",
"itemsPerPage",
"currentPage",
"totalPages",
"hasNextPage",
"hasPreviousPage"
]
},
"GetBookingsOutput_2024_08_13": {
"type": "object",
"properties": {
@@ -22151,11 +22211,14 @@
},
"description": "Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects"
},
"pagination": {
"$ref": "#/components/schemas/PaginationMetaDto"
},
"error": {
"type": "object"
}
},
"required": ["status", "data"]
"required": ["status", "data", "pagination"]
},
"RescheduleBookingInput_2024_08_13": {
"type": "object",
@@ -258,7 +258,7 @@ export class GetBookingsInput_2024_08_13 {
sortUpdatedAt?: SortOrderType;
// note(Lauris): pagination
@ApiProperty({ required: false, description: "The number of items to return", example: 10 })
@ApiProperty({ required: false, description: "The number of items to return", example: 10, default: 100 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(1)
@@ -266,7 +266,7 @@ export class GetBookingsInput_2024_08_13 {
@IsOptional()
take?: number;
@ApiProperty({ required: false, description: "The number of items to skip", example: 0 })
@ApiProperty({ required: false, description: "The number of items to skip", example: 0, default: 0 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(0)
@@ -1,5 +1,6 @@
import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger";
import { IsEnum, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
import { IsBoolean, IsEnum, IsInt, Min, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import {
@@ -9,6 +10,68 @@ import {
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
export class PaginationMetaDto {
@ApiProperty({
description: "The total number of items available across all pages, matching the query criteria.",
example: 123,
minimum: 0,
})
@IsInt()
@Min(0)
totalItems!: number;
@ApiProperty({
description:
"The number of items remaining to be fetched *after* the current page. Calculated as: `totalItems - (skip + itemsPerPage)`.",
example: 103, // e.g., if totalItems=123, skip=10, itemsPerPage=10 -> 123 - (10 + 10) = 103
minimum: 0,
})
@IsInt()
@Min(0)
remainingItems!: number;
@ApiProperty({
description: "The maximum number of items requested per page.",
example: 10,
minimum: 1,
})
@IsInt()
@Min(1) // Typically, you request at least 1 item per page
itemsPerPage!: number;
@ApiProperty({
description: "The current page number being returned.",
example: 2, // e.g., if skip=10, itemsPerPage=10 -> page 2
minimum: 1,
})
@IsInt()
@Min(1) // Page numbers usually start from 1
currentPage!: number;
@ApiProperty({
description: "The total number of pages available.",
example: 13, // e.g., if totalItems=123, itemsPerPage=10 -> 13 pages
minimum: 0, // Can be 0 if totalItems is 0
})
@IsInt()
@Min(0)
totalPages!: number;
@ApiProperty({
description: "Indicates if there is a subsequent page available after the current one.",
example: true,
})
@IsBoolean()
hasNextPage!: boolean;
@ApiProperty({
description: "Indicates if there is a preceding page available before the current one.",
example: true,
})
@IsBoolean()
hasPreviousPage!: boolean;
}
@ApiExtraModels(
BookingOutput_2024_08_13,
RecurringBookingOutput_2024_08_13,
@@ -33,13 +96,16 @@ export class GetBookingsOutput_2024_08_13 {
description:
"Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects",
})
@ValidateNested({ each: true })
data!: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
| GetRecurringSeatedBookingOutput_2024_08_13
)[];
error?: Error;
@ApiProperty({ type: () => PaginationMetaDto }) // Crucial for Swagger
@Type(() => PaginationMetaDto)
@ValidateNested()
pagination!: PaginationMetaDto;
}