docs(api): type pagination counts as integers with encoded bounds

page and limit were declared as number in the spec, which permits
fractional values for what are discrete counts. The query params now
declare integer schemas with their minimum, maximum, and default, and
the pagination meta counters are integers too. Runtime behavior is
unchanged; parsePagination already clamped. Also hoists a repeated
inline require in the service spec to a top-level import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
isra el
2026-08-09 12:48:54 +03:00
co-authored by Claude Opus 5
parent 28e11cee47
commit 44c72eeeed
4 changed files with 34 additions and 31 deletions
+20 -15
View File
@@ -222,19 +222,24 @@
"name": "limit",
"required": false,
"in": "query",
"description": "Messages per page. Default 50, maximum 100.",
"schema": {
"type": "number"
}
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 50
},
"description": "Messages per page. Default 50, maximum 100."
},
{
"name": "page",
"required": false,
"in": "query",
"description": "Page to return. Default 1. Mutually exclusive with cursor.",
"schema": {
"type": "number"
}
"type": "integer",
"minimum": 1,
"default": 1
},
"description": "Page to return. Default 1. Mutually exclusive with cursor."
},
{
"name": "order",
@@ -1525,19 +1530,19 @@
"type": "object",
"properties": {
"page": {
"type": "number",
"type": "integer",
"description": "Current page number"
},
"limit": {
"type": "number",
"type": "integer",
"description": "Number of items per page"
},
"total": {
"type": "number",
"type": "integer",
"description": "Total number of items"
},
"totalPages": {
"type": "number",
"type": "integer",
"description": "Total number of pages"
},
"nextCursor": {
@@ -1563,7 +1568,7 @@
"type": "object",
"properties": {
"limit": {
"type": "number",
"type": "integer",
"description": "Number of items per page."
},
"nextCursor": {
@@ -1901,19 +1906,19 @@
"type": "object",
"properties": {
"page": {
"type": "number",
"type": "integer",
"description": "Current page number"
},
"limit": {
"type": "number",
"type": "integer",
"description": "Number of items per page"
},
"total": {
"type": "number",
"type": "integer",
"description": "Total number of items"
},
"totalPages": {
"type": "number",
"type": "integer",
"description": "Total number of pages"
}
},
+8 -8
View File
@@ -507,13 +507,13 @@ export class GatewayController {
@ApiQuery({
name: 'page',
required: false,
type: Number,
schema: { type: 'integer', minimum: 1, default: 1 },
description: 'Page to return. Default 1. Mutually exclusive with cursor.',
})
@ApiQuery({
name: 'limit',
required: false,
type: Number,
schema: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
description: 'Messages per page. Default 50, maximum 100.',
})
@ApiQuery({
@@ -549,13 +549,13 @@ export class GatewayController {
@ApiQuery({
name: 'page',
required: false,
type: Number,
schema: { type: 'integer', minimum: 1, default: 1 },
description: 'Page to return. Default 1.',
})
@ApiQuery({
name: 'limit',
required: false,
type: Number,
schema: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
description: 'Messages per page. Default 50, maximum 100.',
})
@UseGuards(AuthGuard, CanModifyDevice)
@@ -587,13 +587,13 @@ export class GatewayController {
@ApiQuery({
name: 'page',
required: false,
type: Number,
schema: { type: 'integer', minimum: 1, default: 1 },
description: 'Page to return. Default 1.',
})
@ApiQuery({
name: 'limit',
required: false,
type: Number,
schema: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
description: 'Messages per page. Default 50, maximum 100.',
})
@UseGuards(AuthGuard, CanModifyDevice)
@@ -624,13 +624,13 @@ export class GatewayController {
@ApiQuery({
name: 'page',
required: false,
type: Number,
schema: { type: 'integer', minimum: 1, default: 1 },
description: 'Page to return. Default 1.',
})
@ApiQuery({
name: 'limit',
required: false,
type: Number,
schema: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
description: 'Messages per page. Default 50, maximum 100.',
})
@ApiQuery({
+5 -5
View File
@@ -929,28 +929,28 @@ export class RetrieveSMSDTO {
export class PaginationMetaDTO {
@ApiProperty({
type: Number,
type: 'integer',
required: true,
description: 'Current page number',
})
page: number;
@ApiProperty({
type: Number,
type: 'integer',
required: true,
description: 'Number of items per page',
})
limit: number;
@ApiProperty({
type: Number,
type: 'integer',
required: true,
description: 'Total number of items',
})
total: number;
@ApiProperty({
type: Number,
type: 'integer',
required: true,
description: 'Total number of pages',
})
@@ -991,7 +991,7 @@ export class MessageDTO extends RetrieveSMSDTO {
}
export class CursorPaginationMetaDTO {
@ApiProperty({ type: Number, description: 'Number of items per page.' })
@ApiProperty({ type: 'integer', description: 'Number of items per page.' })
limit: number
@ApiProperty({
+1 -3
View File
@@ -17,6 +17,7 @@ import * as firebaseAdmin from 'firebase-admin'
import { SMSType } from './sms-type.enum'
import { WebhookEvent } from '../webhook/webhook-event.enum'
import { RegisterDeviceInputDTO, SendBulkSMSInputDTO, SendSMSInputDTO } from './gateway.dto'
import { decodeCursor } from './cursor'
import { User } from '../users/schemas/user.schema'
import { UserRole } from '../users/user-roles.enum'
import { BatchResponse } from 'firebase-admin/messaging'
@@ -1550,7 +1551,6 @@ describe('GatewayService', () => {
}
pages++
if (!result.meta.nextCursor) break
const { decodeCursor } = require('./cursor')
cursor = decodeCursor(result.meta.nextCursor)
}
expect(seen.size).toBe(150)
@@ -1567,7 +1567,6 @@ describe('GatewayService', () => {
offsetIds.push(...r.data.map((m: any) => String(m._id)))
}
const { decodeCursor } = require('./cursor')
const keysetIds: string[] = []
let cursor: any = undefined
while (true) {
@@ -1582,7 +1581,6 @@ describe('GatewayService', () => {
it('asc keyset walk sees rows inserted behind the head mid-walk', async () => {
seed(deviceA, 10, new Date('2026-08-01T00:00:00Z'))
const { decodeCursor } = require('./cursor')
const first = await service.getMessagesForUser(user, { order: 'asc' } as any, 1, 5)
// New rows land after the cursor position while we are mid-walk