Skip to content

Commit 97cabf9

Browse files
pagination: add unified PageInfo, PaginatedResponse factory, and pagination utilities
1 parent 83b25ab commit 97cabf9

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { PageInfo } from '../pagination/page-info.model';
3+
4+
/**
5+
* Helper factory to create a Swagger-described paginated response class for a given item type.
6+
* Example: export class PaginatedJobResponse extends PaginatedResponse(JobDto) {}
7+
*/
8+
export function PaginatedResponse<T extends new (...args: any[]) => any>(ItemType: T) {
9+
class PaginatedResponseClass {
10+
@ApiProperty({ isArray: true, type: ItemType })
11+
items!: InstanceType<T>[];
12+
13+
@ApiProperty({ type: () => PageInfo })
14+
pageInfo!: PageInfo;
15+
16+
@ApiProperty({ description: 'Total number of matching items' })
17+
totalCount!: number;
18+
19+
@ApiProperty({ description: 'Optional contextual metadata (e.g., company, tag)', required: false })
20+
meta?: Record<string, any> | null;
21+
}
22+
return PaginatedResponseClass;
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
import { Field, ObjectType } from '@nestjs/graphql';
2+
import { ApiProperty } from '@nestjs/swagger';
23

34
@ObjectType()
45
export class PageInfo {
6+
@ApiProperty({ required: false, nullable: true, description: 'Cursor of last item in current page' })
57
@Field(() => String, { nullable: true })
68
endCursor?: string;
79

10+
@ApiProperty({ description: 'Is there another page after this one?' })
811
@Field(() => Boolean)
912
hasNextPage: boolean;
1013

14+
@ApiProperty({ description: 'Is there a page before this one?' })
1115
@Field(() => Boolean)
1216
hasPreviousPage: boolean;
1317

18+
@ApiProperty({ required: false, nullable: true, description: 'Cursor of first item in current page' })
1419
@Field(() => String, { nullable: true })
1520
startCursor?: string;
21+
22+
// Retained for backward compatibility in REST where totalCount previously lived under pageInfo
23+
@ApiProperty({
24+
description: '(Deprecated – use top-level totalCount) Total number of matching items',
25+
required: false,
26+
})
27+
@Field(() => Number, { nullable: true })
28+
totalCount?: number;
1629
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,60 @@
11
import { ArgsType } from '@nestjs/graphql';
2+
import { Field } from '../decorators/field.decorator';
23

34
@ArgsType()
45
export class PaginationArgs {
6+
@Field({
7+
name: 'skip',
8+
description: 'Number of records to skip (offset based pagination).',
9+
isInt: { message: 'skip must be an integer.' },
10+
optional: true,
11+
inQuery: true,
12+
example: 0,
13+
type: Number,
14+
})
515
skip?: number;
616

17+
@Field({
18+
name: 'after',
19+
description: 'Cursor to start after (forward cursor pagination).',
20+
isString: { message: 'after must be a string cursor.' },
21+
optional: true,
22+
inQuery: true,
23+
example: 'cursor123',
24+
type: String,
25+
})
726
after?: string;
827

28+
@Field({
29+
name: 'before',
30+
description: 'Cursor to end before (backward cursor pagination).',
31+
isString: { message: 'before must be a string cursor.' },
32+
optional: true,
33+
inQuery: true,
34+
example: 'cursor122',
35+
type: String,
36+
})
937
before?: string;
1038

39+
@Field({
40+
name: 'first',
41+
description: 'Max number of items to return going forward from the cursor.',
42+
isInt: { message: 'first must be an integer.' },
43+
optional: true,
44+
inQuery: true,
45+
example: 25,
46+
type: Number,
47+
})
1148
first?: number;
1249

50+
@Field({
51+
name: 'last',
52+
description: 'Max number of items to return going backward from the cursor.',
53+
isInt: { message: 'last must be an integer.' },
54+
optional: true,
55+
inQuery: true,
56+
example: 25,
57+
type: Number,
58+
})
1359
last?: number;
1460
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { PageInfo } from './page-info.model';
2+
3+
export interface BuildPaginationArgs {
4+
skip: number;
5+
take: number;
6+
totalCount: number;
7+
currentItemsLength: number;
8+
}
9+
10+
export function buildPageInfo({ skip, take, totalCount, currentItemsLength }: BuildPaginationArgs): PageInfo {
11+
return {
12+
hasNextPage: skip + take < totalCount,
13+
hasPreviousPage: skip > 0,
14+
startCursor: String(skip),
15+
endCursor: String(skip + currentItemsLength),
16+
} as PageInfo;
17+
}
18+
19+
export function buildPaginatedResult<T>(params: {
20+
items: T[];
21+
skip: number;
22+
take: number;
23+
totalCount: number;
24+
meta?: Record<string, any> | null;
25+
}) {
26+
const { items, skip, take, totalCount, meta = null } = params;
27+
return {
28+
items,
29+
pageInfo: buildPageInfo({ skip, take, totalCount, currentItemsLength: items.length }),
30+
totalCount,
31+
meta,
32+
};
33+
}

0 commit comments

Comments
 (0)