@zudojs/database 0.0.1 → 1.0.0
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 +223 -17
- package/dist/cache/cache.memory.d.ts +167 -0
- package/dist/cache/cache.memory.js +186 -40
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -2
- package/dist/database/database.core.d.ts +91 -0
- package/dist/database/database.core.js +150 -0
- package/dist/databaseClient/databaseClient.core.d.ts +165 -14
- package/dist/databaseClient/databaseClient.core.js +304 -70
- package/dist/databaseClient/databaseClient.errors.d.ts +112 -0
- package/dist/databaseClient/databaseClient.errors.js +425 -0
- package/dist/databaseClient/index.d.ts +2 -1
- package/dist/databaseClient/index.js +2 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts +163 -0
- package/dist/databaseConnection/databaseConnection.manager.js +138 -76
- package/dist/databaseConnection/index.d.ts +1 -1
- package/dist/databaseType/databaseType.type.d.ts +224 -0
- package/dist/databaseType/databaseType.type.js +10 -0
- package/dist/health/health.check.d.ts +74 -0
- package/dist/health/health.check.js +78 -47
- package/dist/health/index.d.ts +1 -1
- package/dist/health/index.js +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.js +11 -11
- package/dist/locks/index.d.ts +2 -2
- package/dist/locks/index.js +2 -2
- package/dist/locks/locks.core.d.ts +128 -0
- package/dist/locks/locks.core.js +144 -85
- package/dist/migration/index.d.ts +3 -1
- package/dist/migration/index.js +2 -0
- package/dist/migration/migration.dialect.d.ts +65 -0
- package/dist/migration/migration.dialect.js +63 -0
- package/dist/migration/migration.helpers.d.ts +65 -0
- package/dist/migration/migration.helpers.js +141 -0
- package/dist/migration/migration.runner.d.ts +75 -0
- package/dist/migration/migration.runner.js +195 -107
- package/dist/migration/migration.types.d.ts +85 -0
- package/dist/migration/migration.types.js +2 -0
- package/dist/pagination/index.d.ts +2 -1
- package/dist/pagination/index.js +2 -1
- package/dist/pagination/pagination.core.d.ts +163 -0
- package/dist/pagination/pagination.core.js +279 -0
- package/dist/pagination/pagination.keyset.d.ts +55 -0
- package/dist/pagination/pagination.keyset.js +108 -0
- package/dist/queryBuilder/index.d.ts +4 -3
- package/dist/queryBuilder/index.js +3 -2
- package/dist/queryBuilder/queryBuilder.core.d.ts +178 -0
- package/dist/queryBuilder/queryBuilder.core.js +420 -0
- package/dist/queryBuilder/queryBuilder.factory.d.ts +6 -0
- package/dist/queryBuilder/queryBuilder.factory.js +8 -0
- package/dist/queryBuilder/queryBuilder.filter.d.ts +170 -0
- package/dist/queryBuilder/queryBuilder.filter.js +497 -0
- package/dist/queryBuilder/queryBuilder.prisma.d.ts +60 -0
- package/dist/queryBuilder/queryBuilder.prisma.js +233 -0
- package/dist/queryBuilder/queryBuilder.type.d.ts +48 -0
- package/dist/queryBuilder/queryBuilder.type.js +2 -0
- package/dist/relations/index.d.ts +1 -1
- package/dist/relations/index.js +1 -1
- package/dist/relations/relations.definition.d.ts +184 -0
- package/dist/relations/relations.definition.js +362 -0
- package/dist/repository/index.d.ts +2 -1
- package/dist/repository/index.js +1 -0
- package/dist/repository/repository.base.d.ts +270 -0
- package/dist/repository/repository.base.js +350 -84
- package/dist/repository/repository.errors.d.ts +60 -0
- package/dist/repository/repository.errors.js +235 -0
- package/dist/seed/index.d.ts +2 -2
- package/dist/seed/index.js +1 -1
- package/dist/seed/seed.runner.d.ts +160 -0
- package/dist/seed/seed.runner.js +194 -225
- package/dist/transaction/index.d.ts +1 -1
- package/dist/transaction/index.js +1 -1
- package/dist/transaction/transaction.core.d.ts +117 -0
- package/dist/transaction/transaction.core.js +83 -56
- package/dist/unitOfWork/unitOfWork.core.d.ts +44 -0
- package/dist/unitOfWork/unitOfWork.core.js +9 -20
- package/package.json +35 -19
- package/dist/cache/cache.memory.d.ts.map +0 -1
- package/dist/cache/cache.memory.js.map +0 -1
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/index.js.map +0 -1
- package/dist/database/database.core.d.ts.map +0 -1
- package/dist/database/index.d.ts.map +0 -1
- package/dist/database/index.js.map +0 -1
- package/dist/databaseClient/databaseClient.core.d.ts.map +0 -1
- package/dist/databaseClient/databaseClient.core.js.map +0 -1
- package/dist/databaseClient/databaseClient.logger.d.ts.map +0 -1
- package/dist/databaseClient/databaseClient.logger.js.map +0 -1
- package/dist/databaseClient/index.d.ts.map +0 -1
- package/dist/databaseClient/index.js.map +0 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts.map +0 -1
- package/dist/databaseConnection/databaseConnection.manager.js.map +0 -1
- package/dist/databaseConnection/index.d.ts.map +0 -1
- package/dist/databaseConnection/index.js.map +0 -1
- package/dist/databaseType/index.d.ts.map +0 -1
- package/dist/databaseType/index.js.map +0 -1
- package/dist/health/health.check.d.ts.map +0 -1
- package/dist/health/health.check.js.map +0 -1
- package/dist/health/index.d.ts.map +0 -1
- package/dist/health/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/locks/index.d.ts.map +0 -1
- package/dist/locks/index.js.map +0 -1
- package/dist/locks/locks.core.d.ts.map +0 -1
- package/dist/locks/locks.core.js.map +0 -1
- package/dist/migration/index.d.ts.map +0 -1
- package/dist/migration/index.js.map +0 -1
- package/dist/migration/migration.helpers.d.ts.map +0 -1
- package/dist/migration/migration.runner.d.ts.map +0 -1
- package/dist/migration/migration.runner.js.map +0 -1
- package/dist/migration/migration.types.d.ts.map +0 -1
- package/dist/pagination/index.d.ts.map +0 -1
- package/dist/pagination/index.js.map +0 -1
- package/dist/queryBuilder/index.d.ts.map +0 -1
- package/dist/queryBuilder/index.js.map +0 -1
- package/dist/relations/index.d.ts.map +0 -1
- package/dist/relations/index.js.map +0 -1
- package/dist/repository/index.d.ts.map +0 -1
- package/dist/repository/index.js.map +0 -1
- package/dist/repository/repository.base.d.ts.map +0 -1
- package/dist/repository/repository.base.js.map +0 -1
- package/dist/seed/index.d.ts.map +0 -1
- package/dist/seed/index.js.map +0 -1
- package/dist/seed/seed.runner.d.ts.map +0 -1
- package/dist/seed/seed.runner.js.map +0 -1
- package/dist/transaction/index.d.ts.map +0 -1
- package/dist/transaction/index.js.map +0 -1
- package/dist/transaction/transaction.core.d.ts.map +0 -1
- package/dist/transaction/transaction.core.js.map +0 -1
- package/dist/unitOfWork/index.d.ts.map +0 -1
- package/dist/unitOfWork/index.js.map +0 -1
- package/dist/unitOfWork/unitOfWork.core.d.ts.map +0 -1
- package/dist/unitOfWork/unitOfWork.core.js.map +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { PaginationInput, PaginationMeta, PaginatedResult } from "../databaseType/databaseType.type.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default pagination values.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_PAGE = 1;
|
|
6
|
+
export declare const DEFAULT_LIMIT = 20;
|
|
7
|
+
export declare const MAX_LIMIT = 100;
|
|
8
|
+
/**
|
|
9
|
+
* Normalized pagination configuration.
|
|
10
|
+
*/
|
|
11
|
+
export interface NormalizedPagination {
|
|
12
|
+
readonly page: number;
|
|
13
|
+
readonly limit: number;
|
|
14
|
+
readonly offset: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Cursor pagination request.
|
|
18
|
+
*/
|
|
19
|
+
export interface CursorPaginationInput {
|
|
20
|
+
readonly cursor?: string | null;
|
|
21
|
+
readonly limit?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Cursor pagination metadata.
|
|
25
|
+
*/
|
|
26
|
+
export interface CursorPaginationMeta {
|
|
27
|
+
readonly limit: number;
|
|
28
|
+
readonly hasNextPage: boolean;
|
|
29
|
+
readonly hasPreviousPage: boolean;
|
|
30
|
+
readonly nextCursor?: string;
|
|
31
|
+
readonly previousCursor?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Cursor paginated result.
|
|
35
|
+
*/
|
|
36
|
+
export interface CursorPaginatedResult<TEntity> {
|
|
37
|
+
readonly data: readonly TEntity[];
|
|
38
|
+
readonly meta: CursorPaginationMeta;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalizes page and limit values.
|
|
42
|
+
*/
|
|
43
|
+
export declare function normalizePagination(input?: PaginationInput): NormalizedPagination;
|
|
44
|
+
/**
|
|
45
|
+
* Normalizes a page number.
|
|
46
|
+
*/
|
|
47
|
+
export declare function normalizePage(page?: number): number;
|
|
48
|
+
/**
|
|
49
|
+
* Normalizes a page size.
|
|
50
|
+
*/
|
|
51
|
+
export declare function normalizeLimit(limit?: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* Calculates the database offset for a page.
|
|
54
|
+
*/
|
|
55
|
+
export declare function calculateOffset(page: number, limit: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* Calculates the total number of pages.
|
|
58
|
+
*/
|
|
59
|
+
export declare function calculateTotalPages(total: number, limit: number): number;
|
|
60
|
+
/**
|
|
61
|
+
* Creates pagination metadata.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createPaginationMeta(page: number, limit: number, total: number): PaginationMeta;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a paginated result.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createPaginatedResult<TEntity>(data: readonly TEntity[], page: number, limit: number, total: number): PaginatedResult<TEntity>;
|
|
68
|
+
/**
|
|
69
|
+
* Gets the next page number.
|
|
70
|
+
*/
|
|
71
|
+
export declare function getNextPage(meta: PaginationMeta): number | null;
|
|
72
|
+
/**
|
|
73
|
+
* Gets the previous page number.
|
|
74
|
+
*/
|
|
75
|
+
export declare function getPreviousPage(meta: PaginationMeta): number | null;
|
|
76
|
+
/**
|
|
77
|
+
* Checks whether a page number is valid for the result set.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isValidPage(page: number, totalPages: number): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Calculates the item range represented by a page.
|
|
82
|
+
*
|
|
83
|
+
* For example, page 2 with a limit of 20 and total of 55 returns
|
|
84
|
+
* `{ start: 21, end: 40 }`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getItemRange(page: number, limit: number, total: number): {
|
|
87
|
+
readonly start: number;
|
|
88
|
+
readonly end: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Applies offset pagination to an in-memory collection.
|
|
92
|
+
*
|
|
93
|
+
* This is useful for adapters and tests that need the same pagination
|
|
94
|
+
* semantics without querying the database directly.
|
|
95
|
+
*/
|
|
96
|
+
export declare function paginateCollection<TEntity>(items: readonly TEntity[], input?: PaginationInput): PaginatedResult<TEntity>;
|
|
97
|
+
/**
|
|
98
|
+
* Normalizes cursor pagination input.
|
|
99
|
+
*/
|
|
100
|
+
export declare function normalizeCursorPagination(input?: CursorPaginationInput): Required<Pick<CursorPaginationInput, "limit">> & Pick<CursorPaginationInput, "cursor">;
|
|
101
|
+
/**
|
|
102
|
+
* Creates cursor pagination metadata.
|
|
103
|
+
*/
|
|
104
|
+
export declare function createCursorPaginationMeta(limit: number, options: {
|
|
105
|
+
readonly hasNextPage: boolean;
|
|
106
|
+
readonly hasPreviousPage?: boolean;
|
|
107
|
+
readonly nextCursor?: string | null;
|
|
108
|
+
readonly previousCursor?: string | null;
|
|
109
|
+
}): CursorPaginationMeta;
|
|
110
|
+
/**
|
|
111
|
+
* Creates a cursor paginated result.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createCursorPaginatedResult<TEntity>(data: readonly TEntity[], limit: number, options: {
|
|
114
|
+
readonly hasNextPage: boolean;
|
|
115
|
+
readonly hasPreviousPage?: boolean;
|
|
116
|
+
readonly nextCursor?: string | null;
|
|
117
|
+
readonly previousCursor?: string | null;
|
|
118
|
+
}): CursorPaginatedResult<TEntity>;
|
|
119
|
+
/**
|
|
120
|
+
* Options for encoding a cursor.
|
|
121
|
+
*/
|
|
122
|
+
export interface EncodeCursorOptions {
|
|
123
|
+
/**
|
|
124
|
+
* When supplied the cursor is signed with HMAC-SHA256 so clients cannot
|
|
125
|
+
* forge or tamper with its payload.
|
|
126
|
+
*/
|
|
127
|
+
readonly secret?: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Options for decoding a cursor.
|
|
131
|
+
*/
|
|
132
|
+
export interface DecodeCursorOptions extends EncodeCursorOptions {
|
|
133
|
+
/**
|
|
134
|
+
* Keys the decoded payload may contain. Any other key is rejected.
|
|
135
|
+
*/
|
|
136
|
+
readonly allowedFields?: readonly string[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A validated cursor payload: a flat object of primitive values.
|
|
140
|
+
*/
|
|
141
|
+
export type CursorPayload = Readonly<Record<string, string | number | boolean | null>>;
|
|
142
|
+
/**
|
|
143
|
+
* Encodes a cursor value.
|
|
144
|
+
*
|
|
145
|
+
* Without a `secret` the cursor is plain base64url JSON and must be treated
|
|
146
|
+
* as client-controlled input; pass `allowedFields` to `decodeCursor` (or
|
|
147
|
+
* use a secret) before feeding it to a query.
|
|
148
|
+
*/
|
|
149
|
+
export declare function encodeCursor(value: unknown, options?: EncodeCursorOptions): string;
|
|
150
|
+
/**
|
|
151
|
+
* Decodes a cursor value.
|
|
152
|
+
*
|
|
153
|
+
* When `secret` is supplied the signature is verified; when
|
|
154
|
+
* `allowedFields` is supplied the payload must be a flat object whose keys
|
|
155
|
+
* are all allowed and whose values are primitives.
|
|
156
|
+
*/
|
|
157
|
+
export declare function decodeCursor<T = unknown>(cursor: string, options?: DecodeCursorOptions): T;
|
|
158
|
+
/**
|
|
159
|
+
* Validates that a decoded cursor is a flat object of primitive values
|
|
160
|
+
* restricted to the allowed fields.
|
|
161
|
+
*/
|
|
162
|
+
export declare function validateCursorPayload(value: unknown, allowedFields: readonly string[]): asserts value is CursorPayload;
|
|
163
|
+
//# sourceMappingURL=pagination.core.d.ts.map
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Default pagination values.
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_PAGE = 1;
|
|
6
|
+
export const DEFAULT_LIMIT = 20;
|
|
7
|
+
export const MAX_LIMIT = 100;
|
|
8
|
+
/**
|
|
9
|
+
* Normalizes page and limit values.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizePagination(input) {
|
|
12
|
+
const page = normalizePage(input?.page);
|
|
13
|
+
const limit = normalizeLimit(input?.limit);
|
|
14
|
+
return {
|
|
15
|
+
page,
|
|
16
|
+
limit,
|
|
17
|
+
offset: calculateOffset(page, limit),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalizes a page number.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizePage(page) {
|
|
24
|
+
if (page === undefined || !Number.isFinite(page)) {
|
|
25
|
+
return DEFAULT_PAGE;
|
|
26
|
+
}
|
|
27
|
+
return Math.max(1, Math.floor(page));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Normalizes a page size.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeLimit(limit) {
|
|
33
|
+
if (limit === undefined || !Number.isFinite(limit)) {
|
|
34
|
+
return DEFAULT_LIMIT;
|
|
35
|
+
}
|
|
36
|
+
return Math.min(MAX_LIMIT, Math.max(1, Math.floor(limit)));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Calculates the database offset for a page.
|
|
40
|
+
*/
|
|
41
|
+
export function calculateOffset(page, limit) {
|
|
42
|
+
return (normalizePage(page) - 1) * normalizeLimit(limit);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Calculates the total number of pages.
|
|
46
|
+
*/
|
|
47
|
+
export function calculateTotalPages(total, limit) {
|
|
48
|
+
const normalizedTotal = Math.max(0, Math.floor(total));
|
|
49
|
+
const normalizedLimit = normalizeLimit(limit);
|
|
50
|
+
if (normalizedTotal === 0) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
return Math.ceil(normalizedTotal / normalizedLimit);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates pagination metadata.
|
|
57
|
+
*/
|
|
58
|
+
export function createPaginationMeta(page, limit, total) {
|
|
59
|
+
const normalizedPage = normalizePage(page);
|
|
60
|
+
const normalizedLimit = normalizeLimit(limit);
|
|
61
|
+
const normalizedTotal = Math.max(0, Math.floor(total));
|
|
62
|
+
const totalPages = calculateTotalPages(normalizedTotal, normalizedLimit);
|
|
63
|
+
const hasNextPage = totalPages > 0 && normalizedPage < totalPages;
|
|
64
|
+
const hasPreviousPage = normalizedPage > 1 && totalPages > 0;
|
|
65
|
+
return {
|
|
66
|
+
page: normalizedPage,
|
|
67
|
+
limit: normalizedLimit,
|
|
68
|
+
total: normalizedTotal,
|
|
69
|
+
totalPages,
|
|
70
|
+
hasNextPage,
|
|
71
|
+
hasPreviousPage,
|
|
72
|
+
hasNext: hasNextPage,
|
|
73
|
+
hasPrev: hasPreviousPage,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Creates a paginated result.
|
|
78
|
+
*/
|
|
79
|
+
export function createPaginatedResult(data, page, limit, total) {
|
|
80
|
+
return {
|
|
81
|
+
data: [...data],
|
|
82
|
+
meta: createPaginationMeta(page, limit, total),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Gets the next page number.
|
|
87
|
+
*/
|
|
88
|
+
export function getNextPage(meta) {
|
|
89
|
+
if (!meta.hasNextPage) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return meta.page + 1;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Gets the previous page number.
|
|
96
|
+
*/
|
|
97
|
+
export function getPreviousPage(meta) {
|
|
98
|
+
if (!meta.hasPreviousPage) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return Math.max(1, meta.page - 1);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Checks whether a page number is valid for the result set.
|
|
105
|
+
*/
|
|
106
|
+
export function isValidPage(page, totalPages) {
|
|
107
|
+
if (!Number.isFinite(page) || !Number.isFinite(totalPages)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const normalizedPage = Math.floor(page);
|
|
111
|
+
const normalizedTotalPages = Math.max(0, Math.floor(totalPages));
|
|
112
|
+
if (normalizedTotalPages === 0) {
|
|
113
|
+
return normalizedPage === 1;
|
|
114
|
+
}
|
|
115
|
+
return normalizedPage >= 1 && normalizedPage <= normalizedTotalPages;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Calculates the item range represented by a page.
|
|
119
|
+
*
|
|
120
|
+
* For example, page 2 with a limit of 20 and total of 55 returns
|
|
121
|
+
* `{ start: 21, end: 40 }`.
|
|
122
|
+
*/
|
|
123
|
+
export function getItemRange(page, limit, total) {
|
|
124
|
+
const normalizedPage = normalizePage(page);
|
|
125
|
+
const normalizedLimit = normalizeLimit(limit);
|
|
126
|
+
const normalizedTotal = Math.max(0, Math.floor(total));
|
|
127
|
+
if (normalizedTotal === 0) {
|
|
128
|
+
return {
|
|
129
|
+
start: 0,
|
|
130
|
+
end: 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const start = calculateOffset(normalizedPage, normalizedLimit) + 1;
|
|
134
|
+
const end = Math.min(start + normalizedLimit - 1, normalizedTotal);
|
|
135
|
+
if (start > normalizedTotal) {
|
|
136
|
+
// The page lies past the end of the data set: there is no item range.
|
|
137
|
+
return {
|
|
138
|
+
start: 0,
|
|
139
|
+
end: 0,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
start,
|
|
144
|
+
end,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Applies offset pagination to an in-memory collection.
|
|
149
|
+
*
|
|
150
|
+
* This is useful for adapters and tests that need the same pagination
|
|
151
|
+
* semantics without querying the database directly.
|
|
152
|
+
*/
|
|
153
|
+
export function paginateCollection(items, input) {
|
|
154
|
+
const pagination = normalizePagination(input);
|
|
155
|
+
const total = items.length;
|
|
156
|
+
const data = items.slice(pagination.offset, pagination.offset + pagination.limit);
|
|
157
|
+
return createPaginatedResult(data, pagination.page, pagination.limit, total);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Normalizes cursor pagination input.
|
|
161
|
+
*/
|
|
162
|
+
export function normalizeCursorPagination(input) {
|
|
163
|
+
return {
|
|
164
|
+
cursor: input?.cursor ?? null,
|
|
165
|
+
limit: normalizeLimit(input?.limit),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Creates cursor pagination metadata.
|
|
170
|
+
*/
|
|
171
|
+
export function createCursorPaginationMeta(limit, options) {
|
|
172
|
+
return {
|
|
173
|
+
limit: normalizeLimit(limit),
|
|
174
|
+
hasNextPage: options.hasNextPage,
|
|
175
|
+
hasPreviousPage: options.hasPreviousPage ?? false,
|
|
176
|
+
nextCursor: options.nextCursor ?? undefined,
|
|
177
|
+
previousCursor: options.previousCursor ?? undefined,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Creates a cursor paginated result.
|
|
182
|
+
*/
|
|
183
|
+
export function createCursorPaginatedResult(data, limit, options) {
|
|
184
|
+
return {
|
|
185
|
+
data: [...data],
|
|
186
|
+
meta: createCursorPaginationMeta(limit, options),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const CURSOR_SIGNATURE_SEPARATOR = ".";
|
|
190
|
+
const FORBIDDEN_CURSOR_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
191
|
+
/**
|
|
192
|
+
* Encodes a cursor value.
|
|
193
|
+
*
|
|
194
|
+
* Without a `secret` the cursor is plain base64url JSON and must be treated
|
|
195
|
+
* as client-controlled input; pass `allowedFields` to `decodeCursor` (or
|
|
196
|
+
* use a secret) before feeding it to a query.
|
|
197
|
+
*/
|
|
198
|
+
export function encodeCursor(value, options = {}) {
|
|
199
|
+
const serialized = JSON.stringify(value);
|
|
200
|
+
if (typeof serialized !== "string") {
|
|
201
|
+
throw new TypeError("Cursor value could not be serialized.");
|
|
202
|
+
}
|
|
203
|
+
const payload = Buffer.from(serialized, "utf8").toString("base64url");
|
|
204
|
+
if (options.secret === undefined) {
|
|
205
|
+
return payload;
|
|
206
|
+
}
|
|
207
|
+
validateSecret(options.secret);
|
|
208
|
+
return `${payload}${CURSOR_SIGNATURE_SEPARATOR}${signCursor(payload, options.secret)}`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Decodes a cursor value.
|
|
212
|
+
*
|
|
213
|
+
* When `secret` is supplied the signature is verified; when
|
|
214
|
+
* `allowedFields` is supplied the payload must be a flat object whose keys
|
|
215
|
+
* are all allowed and whose values are primitives.
|
|
216
|
+
*/
|
|
217
|
+
export function decodeCursor(cursor, options = {}) {
|
|
218
|
+
if (typeof cursor !== "string" || cursor.trim().length === 0) {
|
|
219
|
+
throw new TypeError("A cursor value is required.");
|
|
220
|
+
}
|
|
221
|
+
let payload = cursor;
|
|
222
|
+
if (options.secret !== undefined) {
|
|
223
|
+
validateSecret(options.secret);
|
|
224
|
+
const separator = cursor.lastIndexOf(CURSOR_SIGNATURE_SEPARATOR);
|
|
225
|
+
if (separator <= 0) {
|
|
226
|
+
throw new TypeError("Invalid pagination cursor signature.");
|
|
227
|
+
}
|
|
228
|
+
payload = cursor.slice(0, separator);
|
|
229
|
+
const signature = cursor.slice(separator + 1);
|
|
230
|
+
const expected = signCursor(payload, options.secret);
|
|
231
|
+
if (signature.length !== expected.length ||
|
|
232
|
+
!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
233
|
+
throw new TypeError("Invalid pagination cursor signature.");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
let decoded;
|
|
237
|
+
try {
|
|
238
|
+
decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
throw new TypeError("Invalid pagination cursor.", {
|
|
242
|
+
cause: error,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (options.allowedFields !== undefined) {
|
|
246
|
+
validateCursorPayload(decoded, options.allowedFields);
|
|
247
|
+
}
|
|
248
|
+
return decoded;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Validates that a decoded cursor is a flat object of primitive values
|
|
252
|
+
* restricted to the allowed fields.
|
|
253
|
+
*/
|
|
254
|
+
export function validateCursorPayload(value, allowedFields) {
|
|
255
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
256
|
+
throw new TypeError("Pagination cursor payload must be an object.");
|
|
257
|
+
}
|
|
258
|
+
const allowed = new Set(allowedFields);
|
|
259
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
260
|
+
if (FORBIDDEN_CURSOR_KEYS.has(key) || !allowed.has(key)) {
|
|
261
|
+
throw new TypeError(`Pagination cursor contains an unexpected field "${key}".`);
|
|
262
|
+
}
|
|
263
|
+
if (entry !== null &&
|
|
264
|
+
typeof entry !== "string" &&
|
|
265
|
+
typeof entry !== "number" &&
|
|
266
|
+
typeof entry !== "boolean") {
|
|
267
|
+
throw new TypeError(`Pagination cursor field "${key}" must be a primitive value.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function signCursor(payload, secret) {
|
|
272
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
273
|
+
}
|
|
274
|
+
function validateSecret(secret) {
|
|
275
|
+
if (typeof secret !== "string" || secret.length === 0) {
|
|
276
|
+
throw new TypeError("A cursor secret must be a non-empty string.");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=pagination.core.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { SortInput } from "../databaseType/databaseType.type.js";
|
|
2
|
+
import { type CursorPaginatedResult, type CursorPayload } from "./pagination.core.js";
|
|
3
|
+
/**
|
|
4
|
+
* Options for building a keyset page.
|
|
5
|
+
*/
|
|
6
|
+
export interface KeysetPageOptions<TField extends string = string> {
|
|
7
|
+
/**
|
|
8
|
+
* The sort definition the rows were fetched with. The cursor is derived
|
|
9
|
+
* from these fields, in order.
|
|
10
|
+
*/
|
|
11
|
+
readonly sort: readonly SortInput<TField>[];
|
|
12
|
+
/**
|
|
13
|
+
* The requested page size. Rows beyond this size indicate a next page.
|
|
14
|
+
*/
|
|
15
|
+
readonly limit: number;
|
|
16
|
+
/**
|
|
17
|
+
* The cursor the page was requested with, if any.
|
|
18
|
+
*/
|
|
19
|
+
readonly cursor?: string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Secret used to sign cursors. Strongly recommended for public APIs.
|
|
22
|
+
*/
|
|
23
|
+
readonly secret?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A Prisma-style filter object produced by the keyset helpers.
|
|
27
|
+
*/
|
|
28
|
+
export type KeysetWhere = Readonly<Record<string, unknown>>;
|
|
29
|
+
/**
|
|
30
|
+
* Decodes and validates a keyset cursor against the sort definition.
|
|
31
|
+
*
|
|
32
|
+
* The payload may only contain the sort fields and primitive values.
|
|
33
|
+
*/
|
|
34
|
+
export declare function decodeKeysetCursor<TField extends string = string>(cursor: string, sort: readonly SortInput<TField>[], secret?: string): CursorPayload;
|
|
35
|
+
/**
|
|
36
|
+
* Builds a Prisma-compatible `where` fragment that selects the rows that
|
|
37
|
+
* come strictly after the cursor position in the given sort order.
|
|
38
|
+
*
|
|
39
|
+
* For a sort of `[a asc, b desc]` the result is
|
|
40
|
+
* `OR: [{ a: { gt: A } }, { AND: [{ a: A }, { b: { lt: B } }] }]`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildKeysetWhere<TField extends string = string>(cursor: CursorPayload, sort: readonly SortInput<TField>[]): KeysetWhere;
|
|
43
|
+
/**
|
|
44
|
+
* Derives the cursor payload for a row from the sort fields.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createKeysetCursor<TField extends string = string>(row: Readonly<Record<string, unknown>>, sort: readonly SortInput<TField>[], secret?: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Turns `limit + 1` fetched rows into a cursor-paginated result.
|
|
49
|
+
*
|
|
50
|
+
* Fetch `limit + 1` rows ordered by `sort`, then pass them here: the extra
|
|
51
|
+
* row signals a next page and is dropped, and `nextCursor` is derived from
|
|
52
|
+
* the last returned row.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createKeysetPage<TEntity extends Readonly<Record<string, unknown>>, TField extends string = string>(rows: readonly TEntity[], options: KeysetPageOptions<TField>): CursorPaginatedResult<TEntity>;
|
|
55
|
+
//# sourceMappingURL=pagination.keyset.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createCursorPaginatedResult, decodeCursor, encodeCursor, normalizeLimit, } from "./pagination.core.js";
|
|
2
|
+
/**
|
|
3
|
+
* Decodes and validates a keyset cursor against the sort definition.
|
|
4
|
+
*
|
|
5
|
+
* The payload may only contain the sort fields and primitive values.
|
|
6
|
+
*/
|
|
7
|
+
export function decodeKeysetCursor(cursor, sort, secret) {
|
|
8
|
+
validateSort(sort);
|
|
9
|
+
const payload = decodeCursor(cursor, {
|
|
10
|
+
secret,
|
|
11
|
+
allowedFields: sort.map((entry) => entry.field),
|
|
12
|
+
});
|
|
13
|
+
for (const entry of sort) {
|
|
14
|
+
if (!(entry.field in payload)) {
|
|
15
|
+
throw new TypeError(`Pagination cursor is missing sort field "${entry.field}".`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return payload;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Builds a Prisma-compatible `where` fragment that selects the rows that
|
|
22
|
+
* come strictly after the cursor position in the given sort order.
|
|
23
|
+
*
|
|
24
|
+
* For a sort of `[a asc, b desc]` the result is
|
|
25
|
+
* `OR: [{ a: { gt: A } }, { AND: [{ a: A }, { b: { lt: B } }] }]`.
|
|
26
|
+
*/
|
|
27
|
+
export function buildKeysetWhere(cursor, sort) {
|
|
28
|
+
validateSort(sort);
|
|
29
|
+
const branches = [];
|
|
30
|
+
sort.forEach((entry, index) => {
|
|
31
|
+
const comparison = entry.direction === "desc" ? "lt" : "gt";
|
|
32
|
+
const conditions = sort
|
|
33
|
+
.slice(0, index)
|
|
34
|
+
.map((previous) => ({
|
|
35
|
+
[previous.field]: { equals: cursor[previous.field] },
|
|
36
|
+
}));
|
|
37
|
+
conditions.push({
|
|
38
|
+
[entry.field]: { [comparison]: cursor[entry.field] },
|
|
39
|
+
});
|
|
40
|
+
branches.push(conditions.length === 1 ? conditions[0] : { AND: conditions });
|
|
41
|
+
});
|
|
42
|
+
return branches.length === 1 ? branches[0] : { OR: branches };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Derives the cursor payload for a row from the sort fields.
|
|
46
|
+
*/
|
|
47
|
+
export function createKeysetCursor(row, sort, secret) {
|
|
48
|
+
validateSort(sort);
|
|
49
|
+
const payload = {};
|
|
50
|
+
for (const entry of sort) {
|
|
51
|
+
payload[entry.field] = toCursorValue(row[entry.field], entry.field);
|
|
52
|
+
}
|
|
53
|
+
return encodeCursor(payload, { secret });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Turns `limit + 1` fetched rows into a cursor-paginated result.
|
|
57
|
+
*
|
|
58
|
+
* Fetch `limit + 1` rows ordered by `sort`, then pass them here: the extra
|
|
59
|
+
* row signals a next page and is dropped, and `nextCursor` is derived from
|
|
60
|
+
* the last returned row.
|
|
61
|
+
*/
|
|
62
|
+
export function createKeysetPage(rows, options) {
|
|
63
|
+
const limit = normalizeLimit(options.limit);
|
|
64
|
+
const hasNextPage = rows.length > limit;
|
|
65
|
+
const data = rows.slice(0, limit);
|
|
66
|
+
const last = data[data.length - 1];
|
|
67
|
+
return createCursorPaginatedResult(data, limit, {
|
|
68
|
+
hasNextPage,
|
|
69
|
+
hasPreviousPage: options.cursor !== undefined && options.cursor !== null,
|
|
70
|
+
nextCursor: hasNextPage && last
|
|
71
|
+
? createKeysetCursor(last, options.sort, options.secret)
|
|
72
|
+
: null,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function toCursorValue(value, field) {
|
|
76
|
+
if (value instanceof Date) {
|
|
77
|
+
return value.toISOString();
|
|
78
|
+
}
|
|
79
|
+
if (typeof value === "bigint") {
|
|
80
|
+
return value.toString();
|
|
81
|
+
}
|
|
82
|
+
if (value === null ||
|
|
83
|
+
typeof value === "string" ||
|
|
84
|
+
typeof value === "number" ||
|
|
85
|
+
typeof value === "boolean") {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
throw new TypeError(`Cannot build a cursor from field "${field}": unsupported value type.`);
|
|
89
|
+
}
|
|
90
|
+
function validateSort(sort) {
|
|
91
|
+
if (!Array.isArray(sort) || sort.length === 0) {
|
|
92
|
+
throw new TypeError("Keyset pagination requires at least one sort field.");
|
|
93
|
+
}
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const entry of sort) {
|
|
96
|
+
if (typeof entry.field !== "string" || entry.field.trim().length === 0) {
|
|
97
|
+
throw new TypeError("Keyset sort fields must be non-empty strings.");
|
|
98
|
+
}
|
|
99
|
+
if (entry.direction !== "asc" && entry.direction !== "desc") {
|
|
100
|
+
throw new TypeError(`Keyset sort field "${entry.field}" has an invalid direction.`);
|
|
101
|
+
}
|
|
102
|
+
if (seen.has(entry.field)) {
|
|
103
|
+
throw new TypeError(`Keyset sort field "${entry.field}" is duplicated.`);
|
|
104
|
+
}
|
|
105
|
+
seen.add(entry.field);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=pagination.keyset.js.map
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Database-neutral query construction and filter helpers.
|
|
5
5
|
*/
|
|
6
6
|
export { QueryBuilder } from "./queryBuilder.core.js";
|
|
7
|
-
export { createQueryBuilder
|
|
8
|
-
export
|
|
9
|
-
export {
|
|
7
|
+
export { createQueryBuilder } from "./queryBuilder.factory.js";
|
|
8
|
+
export { toPrismaWhere, toPrismaArgs, toPrismaOrderBy, toPrismaSelect, toPrismaSkipTake, type PrismaWhere, type PrismaQueryArgs, type ToPrismaArgsOptions, } from "./queryBuilder.prisma.js";
|
|
9
|
+
export type { QueryCondition, QueryFilter, QueryOperator, RelationOperator, QueryBuilderState, } from "./queryBuilder.type.js";
|
|
10
|
+
export { equals, notEquals, inList, notInList, lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual, contains, startsWith, endsWith, isNull, isNotNull, and, or, not, condition, allOf, anyOf, fromObject, dateRange, oneOf, noneOf, optionalEquals, optionalContains, hasConditions, flattenAnd, cloneFilter, between, matchesPattern, isEmpty, isNotEmpty, dateOnly, isBefore, isAfter, isBetween, notCondition, relational, } from "./queryBuilder.filter.js";
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Database-neutral query construction and filter helpers.
|
|
5
5
|
*/
|
|
6
6
|
export { QueryBuilder } from "./queryBuilder.core.js";
|
|
7
|
-
export { createQueryBuilder
|
|
8
|
-
export {
|
|
7
|
+
export { createQueryBuilder } from "./queryBuilder.factory.js";
|
|
8
|
+
export { toPrismaWhere, toPrismaArgs, toPrismaOrderBy, toPrismaSelect, toPrismaSkipTake, } from "./queryBuilder.prisma.js";
|
|
9
|
+
export { equals, notEquals, inList, notInList, lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual, contains, startsWith, endsWith, isNull, isNotNull, and, or, not, condition, allOf, anyOf, fromObject, dateRange, oneOf, noneOf, optionalEquals, optionalContains, hasConditions, flattenAnd, cloneFilter, between, matchesPattern, isEmpty, isNotEmpty, dateOnly, isBefore, isAfter, isBetween, notCondition, relational, } from "./queryBuilder.filter.js";
|
|
9
10
|
//# sourceMappingURL=index.js.map
|