From ac8f03f69a767222e5fe7c2a179910dbba7a8b54 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 20 Aug 2026 09:05:54 +0000 Subject: [PATCH] feat(pagination): raise the page-size ceiling to 500 Rows-per-page was capped at 100 in three independent places: @Max on PaginationQueryDto, the same @Max repeated on ListWagonsQueryDto (which does not extend the base), and MAX_PAGE_SIZE in pagination.util. The first two reject with a 400, the third silently truncates, so a larger page size had to be lifted in all three or the endpoints that opted in would refuse it -- train schedules, routes, locomotives, wagons, audit, rule engine, built trains, batch board and the rest. No service overrides maxPageSize, so the util constant is the effective cap everywhere it is reached. Adds a spec pinning the three together: 500 validates, 501 rejects, and the util returns take: 500 rather than truncating. A fourth copy of the number lives in the backoffice data-table footer and is noted there. --- .../src/common/dto/page-size-cap.spec.ts | 32 +++++++++++++++++++ .../src/common/dto/pagination-query.dto.ts | 10 ++++-- .../src/common/utils/pagination.util.ts | 7 +++- .../wagons/dto/list-wagons-query.dto.ts | 6 ++-- 4 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 apps/edr-freight-api/src/common/dto/page-size-cap.spec.ts diff --git a/apps/edr-freight-api/src/common/dto/page-size-cap.spec.ts b/apps/edr-freight-api/src/common/dto/page-size-cap.spec.ts new file mode 100644 index 000000000..574fa0ae6 --- /dev/null +++ b/apps/edr-freight-api/src/common/dto/page-size-cap.spec.ts @@ -0,0 +1,32 @@ +import { plainToInstance } from 'class-transformer'; +import { validateSync } from 'class-validator'; + +import { PaginationQueryDto } from './pagination-query.dto'; +import { ListWagonsQueryDto } from '../../modules/wagons/dto/list-wagons-query.dto'; +import { normalizePagination } from '../utils/pagination.util'; + +/** + * The page-size ceiling is stated in three places that must agree: `@Max` on + * PaginationQueryDto, the same `@Max` repeated on ListWagonsQueryDto (which + * doesn't extend it), and `MAX_PAGE_SIZE` in pagination.util. A fourth copy + * lives outside this package — `MAX_PAGE_SIZE` in @edr/ui-common's data-table + * footer, which is what actually asks for the number. Drift between any of + * them shows up as a 400 on the largest rows-per-page option, so pin them. + */ +const errorsFor = (cls: any, pageSize: unknown) => + validateSync(plainToInstance(cls, { pageSize }), { whitelist: false }); + +describe('page size ceiling', () => { + it.each([PaginationQueryDto, ListWagonsQueryDto])('accepts 500 on %p', (cls) => { + expect(errorsFor(cls, 500)).toHaveLength(0); + }); + + it.each([PaginationQueryDto, ListWagonsQueryDto])('rejects 501 on %p', (cls) => { + expect(errorsFor(cls, 501)).not.toHaveLength(0); + }); + + it('does not truncate 500 in the service-side clamp', () => { + expect(normalizePagination({ page: 1, pageSize: 500 }).take).toBe(500); + expect(normalizePagination({ page: 1, pageSize: 501 }).take).toBe(500); + }); +}); diff --git a/apps/edr-freight-api/src/common/dto/pagination-query.dto.ts b/apps/edr-freight-api/src/common/dto/pagination-query.dto.ts index e705b019d..be04dadba 100644 --- a/apps/edr-freight-api/src/common/dto/pagination-query.dto.ts +++ b/apps/edr-freight-api/src/common/dto/pagination-query.dto.ts @@ -19,12 +19,18 @@ export class PaginationQueryDto { @Min(1) page?: number; - @ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 }) + /** + * Ceiling is 500, matching `MAX_PAGE_SIZE` in `common/utils/pagination.util.ts` + * and the backoffice table footer's largest option. The three have to agree: + * a lower value here turns the footer's top preset into a 400, a higher one + * lets a request through that the util then silently truncates. + */ + @ApiPropertyOptional({ default: 20, minimum: 1, maximum: 500 }) @IsOptional() @Transform(({ value }) => parseInt(String(value), 10) || 20) @IsInt() @Min(1) - @Max(100) + @Max(500) pageSize?: number; @ApiPropertyOptional({ diff --git a/apps/edr-freight-api/src/common/utils/pagination.util.ts b/apps/edr-freight-api/src/common/utils/pagination.util.ts index ca4ed35a1..c7097dde7 100644 --- a/apps/edr-freight-api/src/common/utils/pagination.util.ts +++ b/apps/edr-freight-api/src/common/utils/pagination.util.ts @@ -20,7 +20,12 @@ export interface NormalizedPage { } const DEFAULT_PAGE_SIZE = 20; -const MAX_PAGE_SIZE = 100; +/** + * Must stay in step with `@Max` on `PaginationQueryDto.pageSize` and with + * `MAX_PAGE_SIZE` in the backoffice's data-table footer — the DTO rejects, + * this clamps, and the footer is what actually asks for the number. + */ +const MAX_PAGE_SIZE = 500; /** Clamp raw query values into a safe page window (page ≥ 1, pageSize capped). */ export function normalizePagination( diff --git a/apps/edr-freight-api/src/modules/wagons/dto/list-wagons-query.dto.ts b/apps/edr-freight-api/src/modules/wagons/dto/list-wagons-query.dto.ts index 328a6eaa8..4f48c2a8b 100644 --- a/apps/edr-freight-api/src/modules/wagons/dto/list-wagons-query.dto.ts +++ b/apps/edr-freight-api/src/modules/wagons/dto/list-wagons-query.dto.ts @@ -72,12 +72,14 @@ export class ListWagonsQueryDto { @Min(1) page?: number; - @ApiPropertyOptional({ default: 10, minimum: 1, maximum: 100 }) + // 500 to match PaginationQueryDto — this DTO doesn't extend it, so the + // ceiling has to be repeated here or the wagons list alone rejects at 100. + @ApiPropertyOptional({ default: 10, minimum: 1, maximum: 500 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) - @Max(100) + @Max(500) pageSize?: number; @ApiPropertyOptional({ description: 'Registered on or after this day (YYYY-MM-DD)' })