From 9a1fff7a5e568e38f5acbcf59b5da32edc54352b Mon Sep 17 00:00:00 2001 From: isra el Date: Sat, 8 Mar 2025 18:48:32 +0300 Subject: [PATCH] feat: improve retrieve sms messages query performance and add pagination --- api/src/gateway/gateway.controller.ts | 11 +- api/src/gateway/gateway.dto.ts | 37 +++++ api/src/gateway/gateway.service.ts | 33 +++- api/src/gateway/schemas/sms.schema.ts | 3 + .../dashboard/(components)/received-sms.tsx | 144 +++++++++++++++++- 5 files changed, 218 insertions(+), 10 deletions(-) diff --git a/api/src/gateway/gateway.controller.ts b/api/src/gateway/gateway.controller.ts index 2d104ad..6bdf1e0 100644 --- a/api/src/gateway/gateway.controller.ts +++ b/api/src/gateway/gateway.controller.ts @@ -113,13 +113,20 @@ export class GatewayController { @ApiOperation({ summary: 'Get received SMS from a device' }) @ApiResponse({ status: 200, type: RetrieveSMSResponseDTO }) + @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items per page (default: 50, max: 100)' }) @UseGuards(AuthGuard, CanModifyDevice) // deprecate getReceivedSMS route in favor of get-received-sms @Get(['/devices/:id/getReceivedSMS', '/devices/:id/get-received-sms']) async getReceivedSMS( @Param('id') deviceId: string, + @Request() req, ): Promise { - const data = await this.gatewayService.getReceivedSMS(deviceId) - return { data } + // Extract page and limit from query params, with defaults and max values + const page = req.query.page ? parseInt(req.query.page, 10) : 1; + const limit = req.query.limit ? Math.min(parseInt(req.query.limit, 10), 100) : 50; + + const result = await this.gatewayService.getReceivedSMS(deviceId, page, limit) + return result; } } diff --git a/api/src/gateway/gateway.dto.ts b/api/src/gateway/gateway.dto.ts index 6096ae7..dde4e23 100644 --- a/api/src/gateway/gateway.dto.ts +++ b/api/src/gateway/gateway.dto.ts @@ -195,6 +195,36 @@ export class RetrieveSMSDTO { updatedAt: Date } +export class PaginationMetaDTO { + @ApiProperty({ + type: Number, + required: true, + description: 'Current page number', + }) + page: number; + + @ApiProperty({ + type: Number, + required: true, + description: 'Number of items per page', + }) + limit: number; + + @ApiProperty({ + type: Number, + required: true, + description: 'Total number of items', + }) + total: number; + + @ApiProperty({ + type: Number, + required: true, + description: 'Total number of pages', + }) + totalPages: number; +} + export class RetrieveSMSResponseDTO { @ApiProperty({ type: [RetrieveSMSDTO], @@ -202,4 +232,11 @@ export class RetrieveSMSResponseDTO { description: 'The received SMS data', }) data: RetrieveSMSDTO[] + + @ApiProperty({ + type: PaginationMetaDTO, + required: true, + description: 'Pagination metadata', + }) + meta?: PaginationMetaDTO } diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts index 87c763b..27bcb7a 100644 --- a/api/src/gateway/gateway.service.ts +++ b/api/src/gateway/gateway.service.ts @@ -437,7 +437,7 @@ export class GatewayService { return sms } - async getReceivedSMS(deviceId: string): Promise { + async getReceivedSMS(deviceId: string, page = 1, limit = 50): Promise<{ data: any[], meta: any }> { const device = await this.deviceModel.findById(deviceId) if (!device) { @@ -450,20 +450,47 @@ export class GatewayService { ) } + // Calculate skip value for pagination + const skip = (page - 1) * limit; + + // Get total count for pagination metadata + const total = await this.smsModel.countDocuments({ + device: device._id, + type: SMSType.RECEIVED, + }); + // @ts-ignore - return await this.smsModel + const data = await this.smsModel .find( { device: device._id, type: SMSType.RECEIVED, }, null, - { sort: { receivedAt: -1 }, limit: 200 }, + { + sort: { receivedAt: -1 }, + limit: limit, + skip: skip + }, ) .populate({ path: 'device', select: '_id brand model buildId enabled', }) + .lean() // Use lean() to return plain JavaScript objects instead of Mongoose documents + + // Calculate pagination metadata + const totalPages = Math.ceil(total / limit); + + return { + meta: { + page, + limit, + total, + totalPages, + }, + data, + }; } async getStatsForUser(user: User) { diff --git a/api/src/gateway/schemas/sms.schema.ts b/api/src/gateway/schemas/sms.schema.ts index 2600f4e..286c082 100644 --- a/api/src/gateway/schemas/sms.schema.ts +++ b/api/src/gateway/schemas/sms.schema.ts @@ -62,3 +62,6 @@ export class SMS { } export const SMSSchema = SchemaFactory.createForClass(SMS) + + +SMSSchema.index({ device: 1, type: 1, receivedAt: -1 }) diff --git a/web/app/(app)/dashboard/(components)/received-sms.tsx b/web/app/(app)/dashboard/(components)/received-sms.tsx index 5bee23f..aeac12f 100644 --- a/web/app/(app)/dashboard/(components)/received-sms.tsx +++ b/web/app/(app)/dashboard/(components)/received-sms.tsx @@ -267,6 +267,7 @@ export default function ReceivedSms() { const handleTabChange = (tab: string) => { setCurrentTab(tab) + setPage(1) } const [currentTab, setCurrentTab] = useState('') @@ -277,19 +278,38 @@ export default function ReceivedSms() { } }, [devices]) + const [page, setPage] = useState(1) + const [limit, setLimit] = useState(20) + const { - data: receivedSms, + data: receivedSmsResponse, isLoading: isLoadingReceivedSms, error: receivedSmsError, } = useQuery({ - queryKey: ['received-sms', currentTab], + queryKey: ['received-sms', currentTab, page, limit], enabled: !!currentTab, queryFn: () => httpBrowserClient - .get(ApiEndpoints.gateway.getReceivedSMS(currentTab)) + .get( + `${ApiEndpoints.gateway.getReceivedSMS( + currentTab + )}?page=${page}&limit=${limit}` + ) .then((res) => res.data), }) + const receivedSms = receivedSmsResponse?.data || [] + const pagination = receivedSmsResponse?.meta || { + page: 1, + limit: 20, + total: 0, + totalPages: 1, + } + + const handlePageChange = (newPage: number) => { + setPage(newPage) + } + if (isLoadingDevices) return (
@@ -347,15 +367,129 @@ export default function ReceivedSms() { Error: {receivedSmsError.message}
)} - {!isLoadingReceivedSms && !receivedSms?.data?.length && ( + {!isLoadingReceivedSms && !receivedSms?.length && (
No messages found
)} - {receivedSms?.data?.map((sms) => ( + {receivedSms?.map((sms) => ( ))} + + {pagination.totalPages > 1 && ( +
+ + +
+ {/* First page */} + {pagination.totalPages > 1 && ( + + )} + + {/* Ellipsis if needed */} + {page > 4 && pagination.totalPages > 7 && ( + ... + )} + + {/* Middle pages */} + {Array.from( + { length: Math.min(6, pagination.totalPages - 2) }, + (_, i) => { + let pageToShow + + if (pagination.totalPages <= 8) { + // If we have 8 or fewer pages, show pages 2 through 7 (or fewer) + pageToShow = i + 2 + } else if (page <= 4) { + // Near the start + pageToShow = i + 2 + } else if (page >= pagination.totalPages - 3) { + // Near the end + pageToShow = pagination.totalPages - 7 + i + } else { + // Middle - center around current page + pageToShow = page - 2 + i + } + + // Ensure page is within bounds and not the first or last page + if ( + pageToShow > 1 && + pageToShow < pagination.totalPages + ) { + return ( + + ) + } + return null + } + )} + + {/* Ellipsis if needed */} + {page < pagination.totalPages - 3 && + pagination.totalPages > 7 && ( + ... + )} + + {/* Last page */} + {pagination.totalPages > 1 && ( + + )} +
+ + +
+ )} ))}