@longzai-intelligence/pagination 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -207
- package/dist/hono.cjs +1 -0
- package/dist/hono.d.cts +15 -0
- package/dist/hono.d.mts +15 -0
- package/dist/hono.mjs +1 -0
- package/dist/{index-C8FaNzej.d.mts → index-C4IwIpFc.d.mts} +55 -1
- package/dist/{index-CMiyhlfm.d.cts → index-DpbbjAwO.d.cts} +55 -1
- package/dist/index.cjs +1 -33
- package/dist/index.d.cts +80 -2
- package/dist/index.d.mts +80 -2
- package/dist/index.mjs +1 -1
- package/dist/typeorm.cjs +1 -129
- package/dist/typeorm.d.cts +1 -1
- package/dist/typeorm.d.mts +1 -1
- package/dist/typeorm.mjs +1 -1
- package/dist/utils-B200OLT-.cjs +1 -0
- package/dist/utils-DsM7-PZL.mjs +1 -0
- package/package.json +19 -7
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-lint.log +0 -1
- package/.turbo/turbo-test$colon$coverage.log +0 -36
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/CHANGELOG.md +0 -18
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -16
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -176
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/adapters/index.html +0 -116
- package/coverage/src/adapters/typeorm.adapter.ts.html +0 -940
- package/coverage/src/index.html +0 -131
- package/coverage/src/index.ts.html +0 -280
- package/coverage/src/schemas/index.html +0 -161
- package/coverage/src/schemas/index.ts.html +0 -169
- package/coverage/src/schemas/pagination.schemas.ts.html +0 -343
- package/coverage/src/schemas/result.schemas.ts.html +0 -724
- package/coverage/src/schemas/sort.schemas.ts.html +0 -544
- package/coverage/src/typeorm.ts.html +0 -154
- package/coverage/src/types/index.html +0 -176
- package/coverage/src/types/index.ts.html +0 -199
- package/coverage/src/types/pagination.types.ts.html +0 -169
- package/coverage/src/types/result.types.ts.html +0 -127
- package/coverage/src/types/sort.types.ts.html +0 -142
- package/coverage/src/types/typeorm.d.ts.html +0 -196
- package/coverage/src/utils/index.html +0 -146
- package/coverage/src/utils/index.ts.html +0 -178
- package/coverage/src/utils/pagination.util.ts.html +0 -703
- package/coverage/src/utils/validation.util.ts.html +0 -535
- package/dist/utils-CV6Ovku6.mjs +0 -1
- package/dist/utils-D_qD2YAX.cjs +0 -1
- package/docs/specs/spec-20260413-typeorm-subpath-export/checklist.md +0 -33
- package/docs/specs/spec-20260413-typeorm-subpath-export/spec.md +0 -104
- package/docs/specs/spec-20260413-typeorm-subpath-export/tasks.md +0 -48
- package/eslint.config.ts +0 -3
- package/src/__tests__/index.test.ts +0 -72
- package/src/__tests__/pagination.util.test.ts +0 -251
- package/src/__tests__/result.schemas.test.ts +0 -142
- package/src/__tests__/sort.schemas.test.ts +0 -157
- package/src/__tests__/typeorm.adapter.test.ts +0 -212
- package/src/__tests__/validation.util.test.ts +0 -120
- package/src/adapters/typeorm/adapter.ts +0 -203
- package/src/adapters/typeorm/index.ts +0 -23
- package/src/adapters/typeorm/typeorm.d.ts +0 -37
- package/src/adapters/typeorm/types.ts +0 -95
- package/src/index.ts +0 -65
- package/src/schemas/index.ts +0 -28
- package/src/schemas/pagination.schemas.ts +0 -86
- package/src/schemas/result.schemas.ts +0 -213
- package/src/schemas/sort.schemas.ts +0 -153
- package/src/typeorm.ts +0 -23
- package/src/types/index.ts +0 -38
- package/src/types/pagination.types.ts +0 -28
- package/src/types/result.types.ts +0 -14
- package/src/types/sort.types.ts +0 -19
- package/src/utils/index.ts +0 -31
- package/src/utils/pagination.util.ts +0 -206
- package/src/utils/validation.util.ts +0 -150
- package/tsconfig/.cache/app.tsbuildinfo +0 -1
- package/tsconfig/.cache/node.tsbuildinfo +0 -1
- package/tsconfig/.cache/test.tsbuildinfo +0 -1
- package/tsconfig/app.json +0 -15
- package/tsconfig/node.json +0 -11
- package/tsconfig/test.json +0 -14
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -3
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 排序参数 Schema 测试
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
SortOrderSchema,
|
|
8
|
-
createSortParamsSchema,
|
|
9
|
-
SortParamsSchema,
|
|
10
|
-
createSortItemSchema,
|
|
11
|
-
SortItemSchema,
|
|
12
|
-
createMultiSortParamsSchema,
|
|
13
|
-
MultiSortParamsSchema,
|
|
14
|
-
} from '@/schemas/sort.schemas';
|
|
15
|
-
|
|
16
|
-
describe('SortOrderSchema', () => {
|
|
17
|
-
it('应接受有效的排序方向', () => {
|
|
18
|
-
expect(SortOrderSchema.parse('asc')).toBe('asc');
|
|
19
|
-
expect(SortOrderSchema.parse('desc')).toBe('desc');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('应拒绝无效的排序方向', () => {
|
|
23
|
-
expect(() => SortOrderSchema.parse('invalid')).toThrow();
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('createSortParamsSchema', () => {
|
|
28
|
-
it('应正确验证有效的排序参数', () => {
|
|
29
|
-
const schema = createSortParamsSchema(['name', 'createdAt', 'updatedAt'] as const);
|
|
30
|
-
const result = schema.parse({
|
|
31
|
-
sortBy: 'name',
|
|
32
|
-
sortOrder: 'asc',
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
expect(result.sortBy).toBe('name');
|
|
36
|
-
expect(result.sortOrder).toBe('asc');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('应支持可选的排序参数', () => {
|
|
40
|
-
const schema = createSortParamsSchema(['name', 'createdAt'] as const);
|
|
41
|
-
const result = schema.parse({});
|
|
42
|
-
|
|
43
|
-
expect(result.sortBy).toBeUndefined();
|
|
44
|
-
expect(result.sortOrder).toBeUndefined();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('应拒绝无效的排序字段', () => {
|
|
48
|
-
const schema = createSortParamsSchema(['name', 'createdAt'] as const);
|
|
49
|
-
|
|
50
|
-
expect(() =>
|
|
51
|
-
schema.parse({
|
|
52
|
-
sortBy: 'invalid',
|
|
53
|
-
sortOrder: 'asc',
|
|
54
|
-
}),
|
|
55
|
-
).toThrow();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('SortParamsSchema', () => {
|
|
60
|
-
it('应正确验证默认排序参数', () => {
|
|
61
|
-
const result = SortParamsSchema.parse({
|
|
62
|
-
sortBy: 'anyField',
|
|
63
|
-
sortOrder: 'desc',
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
expect(result.sortBy).toBe('anyField');
|
|
67
|
-
expect(result.sortOrder).toBe('desc');
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('createSortItemSchema', () => {
|
|
72
|
-
it('应正确验证有效的排序项', () => {
|
|
73
|
-
const schema = createSortItemSchema(['name', 'createdAt'] as const);
|
|
74
|
-
const result = schema.parse({
|
|
75
|
-
field: 'name',
|
|
76
|
-
order: 'desc',
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
expect(result.field).toBe('name');
|
|
80
|
-
expect(result.order).toBe('desc');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('应拒绝无效的排序字段', () => {
|
|
84
|
-
const schema = createSortItemSchema(['name', 'createdAt'] as const);
|
|
85
|
-
|
|
86
|
-
expect(() =>
|
|
87
|
-
schema.parse({
|
|
88
|
-
field: 'invalid',
|
|
89
|
-
order: 'asc',
|
|
90
|
-
}),
|
|
91
|
-
).toThrow();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('应拒绝缺少必需字段', () => {
|
|
95
|
-
const schema = createSortItemSchema(['name', 'createdAt'] as const);
|
|
96
|
-
|
|
97
|
-
expect(() => schema.parse({ field: 'name' })).toThrow();
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('SortItemSchema', () => {
|
|
102
|
-
it('应正确验证默认排序项', () => {
|
|
103
|
-
const result = SortItemSchema.parse({
|
|
104
|
-
field: 'anyField',
|
|
105
|
-
order: 'asc',
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
expect(result.field).toBe('anyField');
|
|
109
|
-
expect(result.order).toBe('asc');
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('createMultiSortParamsSchema', () => {
|
|
114
|
-
it('应正确验证有效的多字段排序参数', () => {
|
|
115
|
-
const schema = createMultiSortParamsSchema(['name', 'createdAt', 'updatedAt'] as const);
|
|
116
|
-
const result = schema.parse({
|
|
117
|
-
sort: [
|
|
118
|
-
{ field: 'name', order: 'asc' },
|
|
119
|
-
{ field: 'createdAt', order: 'desc' },
|
|
120
|
-
],
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
expect(result.sort).toHaveLength(2);
|
|
124
|
-
expect(result.sort?.[0]?.field).toBe('name');
|
|
125
|
-
expect(result.sort?.[1]?.field).toBe('createdAt');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('应支持可选的排序参数', () => {
|
|
129
|
-
const schema = createMultiSortParamsSchema(['name', 'createdAt'] as const);
|
|
130
|
-
const result = schema.parse({});
|
|
131
|
-
|
|
132
|
-
expect(result.sort).toBeUndefined();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('应拒绝无效的排序字段', () => {
|
|
136
|
-
const schema = createMultiSortParamsSchema(['name', 'createdAt'] as const);
|
|
137
|
-
|
|
138
|
-
expect(() =>
|
|
139
|
-
schema.parse({
|
|
140
|
-
sort: [{ field: 'invalid', order: 'asc' }],
|
|
141
|
-
}),
|
|
142
|
-
).toThrow();
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('MultiSortParamsSchema', () => {
|
|
147
|
-
it('应正确验证默认多字段排序参数', () => {
|
|
148
|
-
const result = MultiSortParamsSchema.parse({
|
|
149
|
-
sort: [
|
|
150
|
-
{ field: 'field1', order: 'asc' },
|
|
151
|
-
{ field: 'field2', order: 'desc' },
|
|
152
|
-
],
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
expect(result.sort).toHaveLength(2);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeORM 适配器测试
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
paginateWithRepository,
|
|
8
|
-
paginateWithQueryBuilder,
|
|
9
|
-
createPaginationQueryBuilder,
|
|
10
|
-
} from '@/adapters/typeorm';
|
|
11
|
-
import type { Repository, SelectQueryBuilder, FindOptionsOrder } from 'typeorm';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 测试实体类型
|
|
15
|
-
*
|
|
16
|
-
* 用于测试的简单实体定义
|
|
17
|
-
*/
|
|
18
|
-
type TestEntity = {
|
|
19
|
-
id: number;
|
|
20
|
-
name?: string;
|
|
21
|
-
status?: string;
|
|
22
|
-
createdAt?: Date;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 映射后的 DTO 类型
|
|
27
|
-
*/
|
|
28
|
-
type MappedDto = {
|
|
29
|
-
mapped: TestEntity;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 创建模拟的 Repository
|
|
34
|
-
*
|
|
35
|
-
* @returns 模拟的 Repository 对象
|
|
36
|
-
*/
|
|
37
|
-
function createMockRepository(): Repository<TestEntity> {
|
|
38
|
-
return {
|
|
39
|
-
findAndCount: vi.fn(),
|
|
40
|
-
createQueryBuilder: vi.fn(),
|
|
41
|
-
metadata: {
|
|
42
|
-
name: 'TestEntity',
|
|
43
|
-
},
|
|
44
|
-
} as Repository<TestEntity>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 创建模拟的 QueryBuilder
|
|
49
|
-
*
|
|
50
|
-
* @returns 模拟的 QueryBuilder 对象
|
|
51
|
-
*/
|
|
52
|
-
function createMockQueryBuilder(): SelectQueryBuilder<TestEntity> {
|
|
53
|
-
return {
|
|
54
|
-
skip: vi.fn().mockReturnThis(),
|
|
55
|
-
take: vi.fn().mockReturnThis(),
|
|
56
|
-
getManyAndCount: vi.fn(),
|
|
57
|
-
} as SelectQueryBuilder<TestEntity>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
describe('paginateWithRepository', () => {
|
|
61
|
-
it('应正确执行分页查询', async () => {
|
|
62
|
-
const mockRepository = createMockRepository();
|
|
63
|
-
|
|
64
|
-
const mockEntities: TestEntity[] = [{ id: 1 }, { id: 2 }];
|
|
65
|
-
vi.mocked(mockRepository.findAndCount).mockResolvedValue([mockEntities, 10]);
|
|
66
|
-
|
|
67
|
-
const result = await paginateWithRepository(mockRepository, { page: 1, pageSize: 10 });
|
|
68
|
-
|
|
69
|
-
expect(result).toEqual({
|
|
70
|
-
items: mockEntities,
|
|
71
|
-
total: 10,
|
|
72
|
-
page: 1,
|
|
73
|
-
pageSize: 10,
|
|
74
|
-
totalPages: 1,
|
|
75
|
-
hasNextPage: false,
|
|
76
|
-
hasPreviousPage: false,
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('应正确使用自定义映射函数', async () => {
|
|
81
|
-
const mockRepository = createMockRepository();
|
|
82
|
-
|
|
83
|
-
const mockEntities: TestEntity[] = [{ id: 1 }, { id: 2 }];
|
|
84
|
-
vi.mocked(mockRepository.findAndCount).mockResolvedValue([mockEntities, 10]);
|
|
85
|
-
|
|
86
|
-
const mapper = (entity: TestEntity): MappedDto => ({ mapped: entity });
|
|
87
|
-
const result = await paginateWithRepository(
|
|
88
|
-
mockRepository,
|
|
89
|
-
{ page: 1, pageSize: 10 },
|
|
90
|
-
{
|
|
91
|
-
mapper,
|
|
92
|
-
},
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
expect(result.items).toEqual([{ mapped: { id: 1 } }, { mapped: { id: 2 } }]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('应正确处理分页参数', async () => {
|
|
99
|
-
const mockRepository = createMockRepository();
|
|
100
|
-
|
|
101
|
-
const mockEntities: TestEntity[] = [{ id: 1 }];
|
|
102
|
-
vi.mocked(mockRepository.findAndCount).mockResolvedValue([mockEntities, 100]);
|
|
103
|
-
|
|
104
|
-
const result = await paginateWithRepository(mockRepository, { page: 2, pageSize: 20 });
|
|
105
|
-
|
|
106
|
-
expect(result.page).toBe(2);
|
|
107
|
-
expect(result.pageSize).toBe(20);
|
|
108
|
-
expect(result.totalPages).toBe(5);
|
|
109
|
-
expect(result.hasNextPage).toBe(true);
|
|
110
|
-
expect(result.hasPreviousPage).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('应正确使用查询选项', async () => {
|
|
114
|
-
const mockRepository = createMockRepository();
|
|
115
|
-
|
|
116
|
-
const mockEntities: TestEntity[] = [{ id: 1 }];
|
|
117
|
-
vi.mocked(mockRepository.findAndCount).mockResolvedValue([mockEntities, 10]);
|
|
118
|
-
|
|
119
|
-
const options = {
|
|
120
|
-
where: { status: 'active' },
|
|
121
|
-
order: { createdAt: 'DESC' } as FindOptionsOrder<TestEntity>,
|
|
122
|
-
relations: ['user'],
|
|
123
|
-
select: ['id', 'name'],
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
await paginateWithRepository(mockRepository, { page: 1, pageSize: 10 }, options);
|
|
127
|
-
|
|
128
|
-
expect(mockRepository.findAndCount).toHaveBeenCalledWith(
|
|
129
|
-
expect.objectContaining({
|
|
130
|
-
skip: 0,
|
|
131
|
-
take: 10,
|
|
132
|
-
where: options.where,
|
|
133
|
-
order: options.order,
|
|
134
|
-
relations: options.relations,
|
|
135
|
-
select: options.select,
|
|
136
|
-
}),
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('paginateWithQueryBuilder', () => {
|
|
142
|
-
it('应正确执行分页查询', async () => {
|
|
143
|
-
const mockQueryBuilder = createMockQueryBuilder();
|
|
144
|
-
|
|
145
|
-
const mockEntities: TestEntity[] = [{ id: 1 }, { id: 2 }];
|
|
146
|
-
vi.mocked(mockQueryBuilder.getManyAndCount).mockResolvedValue([mockEntities, 10]);
|
|
147
|
-
|
|
148
|
-
const result = await paginateWithQueryBuilder(mockQueryBuilder, { page: 1, pageSize: 10 });
|
|
149
|
-
|
|
150
|
-
expect(result).toEqual({
|
|
151
|
-
items: mockEntities,
|
|
152
|
-
total: 10,
|
|
153
|
-
page: 1,
|
|
154
|
-
pageSize: 10,
|
|
155
|
-
totalPages: 1,
|
|
156
|
-
hasNextPage: false,
|
|
157
|
-
hasPreviousPage: false,
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('应正确使用自定义映射函数', async () => {
|
|
162
|
-
const mockQueryBuilder = createMockQueryBuilder();
|
|
163
|
-
|
|
164
|
-
const mockEntities: TestEntity[] = [{ id: 1 }, { id: 2 }];
|
|
165
|
-
vi.mocked(mockQueryBuilder.getManyAndCount).mockResolvedValue([mockEntities, 10]);
|
|
166
|
-
|
|
167
|
-
const mapper = (entity: TestEntity): MappedDto => ({ mapped: entity });
|
|
168
|
-
const result = await paginateWithQueryBuilder(
|
|
169
|
-
mockQueryBuilder,
|
|
170
|
-
{ page: 1, pageSize: 10 },
|
|
171
|
-
mapper,
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
expect(result.items).toEqual([{ mapped: { id: 1 } }, { mapped: { id: 2 } }]);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('应正确设置 skip 和 take', async () => {
|
|
178
|
-
const mockQueryBuilder = createMockQueryBuilder();
|
|
179
|
-
|
|
180
|
-
vi.mocked(mockQueryBuilder.getManyAndCount).mockResolvedValue([[], 0]);
|
|
181
|
-
|
|
182
|
-
await paginateWithQueryBuilder(mockQueryBuilder, { page: 2, pageSize: 20 });
|
|
183
|
-
|
|
184
|
-
expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
|
|
185
|
-
expect(mockQueryBuilder.take).toHaveBeenCalledWith(20);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe('createPaginationQueryBuilder', () => {
|
|
190
|
-
it('应正确创建 QueryBuilder', () => {
|
|
191
|
-
const mockQueryBuilder = createMockQueryBuilder();
|
|
192
|
-
|
|
193
|
-
const mockRepository = createMockRepository();
|
|
194
|
-
vi.mocked(mockRepository.createQueryBuilder).mockReturnValue(mockQueryBuilder);
|
|
195
|
-
|
|
196
|
-
const result = createPaginationQueryBuilder(mockRepository);
|
|
197
|
-
|
|
198
|
-
expect(result).toBe(mockQueryBuilder);
|
|
199
|
-
expect(mockRepository.createQueryBuilder).toHaveBeenCalled();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('应使用自定义别名', () => {
|
|
203
|
-
const mockQueryBuilder = createMockQueryBuilder();
|
|
204
|
-
|
|
205
|
-
const mockRepository = createMockRepository();
|
|
206
|
-
vi.mocked(mockRepository.createQueryBuilder).mockReturnValue(mockQueryBuilder);
|
|
207
|
-
|
|
208
|
-
createPaginationQueryBuilder(mockRepository, 'custom_alias');
|
|
209
|
-
|
|
210
|
-
expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('custom_alias');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 分页参数验证工具函数测试
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
isValidPage,
|
|
8
|
-
isValidPageSize,
|
|
9
|
-
isValidOffset,
|
|
10
|
-
isValidLimit,
|
|
11
|
-
isValidPagination,
|
|
12
|
-
isValidOffsetPagination,
|
|
13
|
-
} from '@/utils/validation.util';
|
|
14
|
-
import { PAGINATION_DEFAULTS } from '@/types';
|
|
15
|
-
|
|
16
|
-
describe('isValidPage', () => {
|
|
17
|
-
it('应返回 true 对于有效的页码', () => {
|
|
18
|
-
expect(isValidPage(1)).toBe(true);
|
|
19
|
-
expect(isValidPage(10)).toBe(true);
|
|
20
|
-
expect(isValidPage(1000)).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('应返回 false 对于无效的页码', () => {
|
|
24
|
-
expect(isValidPage(0)).toBe(false);
|
|
25
|
-
expect(isValidPage(-1)).toBe(false);
|
|
26
|
-
expect(isValidPage(1.5)).toBe(false);
|
|
27
|
-
expect(isValidPage('1')).toBe(false);
|
|
28
|
-
expect(isValidPage(null)).toBe(false);
|
|
29
|
-
expect(isValidPage(undefined)).toBe(false);
|
|
30
|
-
expect(isValidPage(NaN)).toBe(false);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('isValidPageSize', () => {
|
|
35
|
-
it('应返回 true 对于有效的页面大小', () => {
|
|
36
|
-
expect(isValidPageSize(1)).toBe(true);
|
|
37
|
-
expect(isValidPageSize(20)).toBe(true);
|
|
38
|
-
expect(isValidPageSize(PAGINATION_DEFAULTS.maxPageSize)).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('应返回 false 对于无效的页面大小', () => {
|
|
42
|
-
expect(isValidPageSize(0)).toBe(false);
|
|
43
|
-
expect(isValidPageSize(-1)).toBe(false);
|
|
44
|
-
expect(isValidPageSize(PAGINATION_DEFAULTS.maxPageSize + 1)).toBe(false);
|
|
45
|
-
expect(isValidPageSize(1.5)).toBe(false);
|
|
46
|
-
expect(isValidPageSize('20')).toBe(false);
|
|
47
|
-
expect(isValidPageSize(null)).toBe(false);
|
|
48
|
-
expect(isValidPageSize(undefined)).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('isValidOffset', () => {
|
|
53
|
-
it('应返回 true 对于有效的偏移量', () => {
|
|
54
|
-
expect(isValidOffset(0)).toBe(true);
|
|
55
|
-
expect(isValidOffset(10)).toBe(true);
|
|
56
|
-
expect(isValidOffset(1000)).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('应返回 false 对于无效的偏移量', () => {
|
|
60
|
-
expect(isValidOffset(-1)).toBe(false);
|
|
61
|
-
expect(isValidOffset(1.5)).toBe(false);
|
|
62
|
-
expect(isValidOffset('10')).toBe(false);
|
|
63
|
-
expect(isValidOffset(null)).toBe(false);
|
|
64
|
-
expect(isValidOffset(undefined)).toBe(false);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('isValidLimit', () => {
|
|
69
|
-
it('应返回 true 对于有效的限制数量', () => {
|
|
70
|
-
expect(isValidLimit(1)).toBe(true);
|
|
71
|
-
expect(isValidLimit(20)).toBe(true);
|
|
72
|
-
expect(isValidLimit(PAGINATION_DEFAULTS.maxPageSize)).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('应返回 false 对于无效的限制数量', () => {
|
|
76
|
-
expect(isValidLimit(0)).toBe(false);
|
|
77
|
-
expect(isValidLimit(-1)).toBe(false);
|
|
78
|
-
expect(isValidLimit(PAGINATION_DEFAULTS.maxPageSize + 1)).toBe(false);
|
|
79
|
-
expect(isValidLimit(1.5)).toBe(false);
|
|
80
|
-
expect(isValidLimit('20')).toBe(false);
|
|
81
|
-
expect(isValidLimit(null)).toBe(false);
|
|
82
|
-
expect(isValidLimit(undefined)).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('isValidPagination', () => {
|
|
87
|
-
it('应返回 true 对于有效的分页参数', () => {
|
|
88
|
-
expect(isValidPagination({ page: 1, pageSize: 20 })).toBe(true);
|
|
89
|
-
expect(isValidPagination({ page: 5 })).toBe(true);
|
|
90
|
-
expect(isValidPagination({ pageSize: 50 })).toBe(true);
|
|
91
|
-
expect(isValidPagination({})).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('应返回 false 对于无效的分页参数', () => {
|
|
95
|
-
expect(isValidPagination(null)).toBe(false);
|
|
96
|
-
expect(isValidPagination('string')).toBe(false);
|
|
97
|
-
expect(isValidPagination(123)).toBe(false);
|
|
98
|
-
expect(isValidPagination({ page: -1 })).toBe(false);
|
|
99
|
-
expect(isValidPagination({ pageSize: 0 })).toBe(false);
|
|
100
|
-
expect(isValidPagination({ page: 1.5 })).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe('isValidOffsetPagination', () => {
|
|
105
|
-
it('应返回 true 对于有效的 offset 分页参数', () => {
|
|
106
|
-
expect(isValidOffsetPagination({ limit: 20, offset: 0 })).toBe(true);
|
|
107
|
-
expect(isValidOffsetPagination({ limit: 50 })).toBe(true);
|
|
108
|
-
expect(isValidOffsetPagination({ offset: 100 })).toBe(true);
|
|
109
|
-
expect(isValidOffsetPagination({})).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('应返回 false 对于无效的 offset 分页参数', () => {
|
|
113
|
-
expect(isValidOffsetPagination(null)).toBe(false);
|
|
114
|
-
expect(isValidOffsetPagination('string')).toBe(false);
|
|
115
|
-
expect(isValidOffsetPagination(123)).toBe(false);
|
|
116
|
-
expect(isValidOffsetPagination({ offset: -1 })).toBe(false);
|
|
117
|
-
expect(isValidOffsetPagination({ limit: 0 })).toBe(false);
|
|
118
|
-
expect(isValidOffsetPagination({ limit: 1.5 })).toBe(false);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeORM 分页适配器实现
|
|
3
|
-
*
|
|
4
|
-
* 提供 TypeORM 分页查询的核心实现功能
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
Repository,
|
|
9
|
-
SelectQueryBuilder,
|
|
10
|
-
ObjectLiteral,
|
|
11
|
-
FindManyOptions,
|
|
12
|
-
FindOptionsOrder,
|
|
13
|
-
} from 'typeorm';
|
|
14
|
-
import type { PaginationParams, PaginatedResult } from '@/types';
|
|
15
|
-
import type {
|
|
16
|
-
EntityMapper,
|
|
17
|
-
TypeOrmPaginateOptions,
|
|
18
|
-
TypeOrmPaginateOptionsWithMapper,
|
|
19
|
-
TypeOrmPaginateOptionsNoMapper,
|
|
20
|
-
} from './types';
|
|
21
|
-
import {
|
|
22
|
-
normalizePagination,
|
|
23
|
-
calculateOffset,
|
|
24
|
-
calculateTotalPages,
|
|
25
|
-
hasNextPage,
|
|
26
|
-
hasPreviousPage,
|
|
27
|
-
} from '@/utils';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 类型守卫:检查选项是否包含映射器
|
|
31
|
-
*
|
|
32
|
-
* @typeParam TEntity - 实体类型
|
|
33
|
-
* @typeParam TDto - DTO 类型
|
|
34
|
-
* @param options - 查询选项
|
|
35
|
-
* @returns 是否包含映射器
|
|
36
|
-
*/
|
|
37
|
-
function hasMapper<TEntity extends ObjectLiteral, TDto>(
|
|
38
|
-
options:
|
|
39
|
-
| TypeOrmPaginateOptionsWithMapper<TEntity, TDto>
|
|
40
|
-
| TypeOrmPaginateOptionsNoMapper<TEntity>
|
|
41
|
-
| undefined,
|
|
42
|
-
): options is TypeOrmPaginateOptionsWithMapper<TEntity, TDto> {
|
|
43
|
-
return options !== undefined && 'mapper' in options;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 构建分页结果
|
|
48
|
-
*
|
|
49
|
-
* 内部辅助函数,构建分页结果对象
|
|
50
|
-
*
|
|
51
|
-
* @typeParam TDto - DTO 类型
|
|
52
|
-
* @param items - 数据项数组
|
|
53
|
-
* @param total - 总数
|
|
54
|
-
* @param page - 当前页
|
|
55
|
-
* @param pageSize - 每页大小
|
|
56
|
-
* @returns 分页结果
|
|
57
|
-
*/
|
|
58
|
-
function buildPaginatedResult<TDto>(
|
|
59
|
-
items: TDto[],
|
|
60
|
-
total: number,
|
|
61
|
-
page: number,
|
|
62
|
-
pageSize: number,
|
|
63
|
-
): PaginatedResult<TDto> {
|
|
64
|
-
/**
|
|
65
|
-
* 计算得到的总页数
|
|
66
|
-
*/
|
|
67
|
-
const totalPages = calculateTotalPages(total, pageSize);
|
|
68
|
-
return {
|
|
69
|
-
items,
|
|
70
|
-
total,
|
|
71
|
-
page,
|
|
72
|
-
pageSize,
|
|
73
|
-
totalPages,
|
|
74
|
-
hasNextPage: hasNextPage(page, totalPages),
|
|
75
|
-
hasPreviousPage: hasPreviousPage(page),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 使用 Repository 进行分页查询
|
|
81
|
-
*
|
|
82
|
-
* 通过 TypeORM Repository 进行分页查询,支持实体到 DTO 的映射
|
|
83
|
-
*
|
|
84
|
-
* 当提供 mapper 时返回映射后的 DTO,否则返回原始实体
|
|
85
|
-
*
|
|
86
|
-
* @typeParam TEntity - 实体类型
|
|
87
|
-
* @typeParam TDto - DTO 类型
|
|
88
|
-
* @param repository - TypeORM 仓库
|
|
89
|
-
* @param params - 分页参数
|
|
90
|
-
* @param options - 查询选项
|
|
91
|
-
* @returns 分页结果
|
|
92
|
-
*/
|
|
93
|
-
export async function paginateWithRepository<TEntity extends ObjectLiteral, TDto = TEntity>(
|
|
94
|
-
repository: Repository<TEntity>,
|
|
95
|
-
params: PaginationParams,
|
|
96
|
-
options?: TypeOrmPaginateOptions<TEntity, TDto>,
|
|
97
|
-
): Promise<PaginatedResult<TEntity | TDto>> {
|
|
98
|
-
/**
|
|
99
|
-
* 规范化后的分页参数
|
|
100
|
-
*/
|
|
101
|
-
const { page, pageSize } = normalizePagination(params);
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 计算得到的偏移量
|
|
105
|
-
*/
|
|
106
|
-
const offset = calculateOffset(page, pageSize);
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* 默认排序规则
|
|
110
|
-
*/
|
|
111
|
-
const defaultOrder: FindOptionsOrder<TEntity> = { createdAt: 'DESC' };
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* TypeORM 查询选项配置
|
|
115
|
-
*/
|
|
116
|
-
const findOptions: FindManyOptions<TEntity> = {
|
|
117
|
-
skip: offset,
|
|
118
|
-
take: pageSize,
|
|
119
|
-
where: options?.where,
|
|
120
|
-
relations: options?.relations,
|
|
121
|
-
select: options?.select,
|
|
122
|
-
order: options?.order ?? defaultOrder,
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 查询结果:实体数组和总数
|
|
127
|
-
*/
|
|
128
|
-
const [entities, total] = await repository.findAndCount(findOptions);
|
|
129
|
-
|
|
130
|
-
if (hasMapper(options)) {
|
|
131
|
-
/**
|
|
132
|
-
* 映射后的 DTO 数组
|
|
133
|
-
*/
|
|
134
|
-
const items = await Promise.all(entities.map(options.mapper));
|
|
135
|
-
return buildPaginatedResult(items, total, page, pageSize);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return buildPaginatedResult(entities, total, page, pageSize);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 使用 QueryBuilder 进行分页查询
|
|
143
|
-
*
|
|
144
|
-
* 通过 TypeORM QueryBuilder 进行分页查询,支持实体到 DTO 的映射
|
|
145
|
-
*
|
|
146
|
-
* 当提供 mapper 时返回映射后的 DTO,否则返回原始实体
|
|
147
|
-
*
|
|
148
|
-
* @typeParam TEntity - 实体类型
|
|
149
|
-
* @typeParam TDto - DTO 类型
|
|
150
|
-
* @param queryBuilder - TypeORM QueryBuilder
|
|
151
|
-
* @param params - 分页参数
|
|
152
|
-
* @param mapper - 实体到 DTO 的映射函数(可选)
|
|
153
|
-
* @returns 分页结果
|
|
154
|
-
*/
|
|
155
|
-
export async function paginateWithQueryBuilder<TEntity extends ObjectLiteral, TDto = TEntity>(
|
|
156
|
-
queryBuilder: SelectQueryBuilder<TEntity>,
|
|
157
|
-
params: PaginationParams,
|
|
158
|
-
mapper?: EntityMapper<TEntity, TDto>,
|
|
159
|
-
): Promise<PaginatedResult<TEntity | TDto>> {
|
|
160
|
-
/**
|
|
161
|
-
* 规范化后的分页参数
|
|
162
|
-
*/
|
|
163
|
-
const { page, pageSize } = normalizePagination(params);
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* 计算得到的偏移量
|
|
167
|
-
*/
|
|
168
|
-
const offset = calculateOffset(page, pageSize);
|
|
169
|
-
|
|
170
|
-
queryBuilder.skip(offset).take(pageSize);
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* 查询结果:实体数组和总数
|
|
174
|
-
*/
|
|
175
|
-
const [entities, total] = await queryBuilder.getManyAndCount();
|
|
176
|
-
|
|
177
|
-
if (mapper) {
|
|
178
|
-
/**
|
|
179
|
-
* 映射后的 DTO 数组
|
|
180
|
-
*/
|
|
181
|
-
const items = await Promise.all(entities.map(mapper));
|
|
182
|
-
return buildPaginatedResult(items, total, page, pageSize);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return buildPaginatedResult(entities, total, page, pageSize);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* 创建分页查询构建器
|
|
190
|
-
*
|
|
191
|
-
* 创建一个用于分页查询的 TypeORM QueryBuilder
|
|
192
|
-
*
|
|
193
|
-
* @typeParam TEntity - 实体类型
|
|
194
|
-
* @param repository - TypeORM 仓库
|
|
195
|
-
* @param alias - 查询别名
|
|
196
|
-
* @returns QueryBuilder
|
|
197
|
-
*/
|
|
198
|
-
export function createPaginationQueryBuilder<TEntity extends ObjectLiteral>(
|
|
199
|
-
repository: Repository<TEntity>,
|
|
200
|
-
alias?: string,
|
|
201
|
-
): SelectQueryBuilder<TEntity> {
|
|
202
|
-
return repository.createQueryBuilder(alias ?? repository.metadata.name.toLowerCase());
|
|
203
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeORM 适配器入口
|
|
3
|
-
*
|
|
4
|
-
* 提供 TypeORM 分页查询功能,需要安装 typeorm 作为 peerDependency。
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```typescript
|
|
8
|
-
* import { paginateWithRepository } from '@longzai-intelligence/pagination/typeorm';
|
|
9
|
-
*
|
|
10
|
-
* const result = await paginateWithRepository(userRepository, {
|
|
11
|
-
* page: 1,
|
|
12
|
-
* pageSize: 20,
|
|
13
|
-
* });
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export type { EntityMapper, TypeOrmPaginateOptions } from './types';
|
|
18
|
-
|
|
19
|
-
export {
|
|
20
|
-
paginateWithRepository,
|
|
21
|
-
paginateWithQueryBuilder,
|
|
22
|
-
createPaginationQueryBuilder,
|
|
23
|
-
} from './adapter';
|