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:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user