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)' })