@zudojs/database 1.2.1 → 1.3.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 +26 -1
- package/dist/databaseClient/databaseClient.core.d.ts +16 -1
- package/dist/databaseClient/databaseClient.core.js +15 -2
- package/dist/databaseClient/databaseClient.errors.d.ts +12 -1
- package/dist/databaseClient/databaseClient.errors.js +14 -1
- package/dist/databaseClient/index.d.ts +1 -1
- package/dist/databaseClient/index.js +1 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts +7 -2
- package/dist/databaseConnection/databaseConnection.manager.js +42 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/pagination/index.d.ts +2 -0
- package/dist/pagination/index.js +2 -0
- package/dist/pagination/pagination.core.d.ts +4 -0
- package/dist/pagination/pagination.core.js +12 -9
- package/dist/pagination/pagination.cursorError.d.ts +17 -0
- package/dist/pagination/pagination.cursorError.js +17 -0
- package/dist/pagination/pagination.keyset.d.ts +25 -8
- package/dist/pagination/pagination.keyset.js +44 -16
- package/dist/pagination/pagination.keysetDirection.d.ts +29 -0
- package/dist/pagination/pagination.keysetDirection.js +30 -0
- package/dist/repository/repository.base.d.ts +5 -0
- package/dist/repository/repository.base.js +10 -1
- package/dist/transaction/transaction.core.d.ts +13 -3
- package/dist/transaction/transaction.core.js +16 -4
- package/dist/unitOfWork/unitOfWork.core.js +3 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -116,8 +116,22 @@ const next = await users.paginateCursor(undefined, {
|
|
|
116
116
|
limit: 25,
|
|
117
117
|
sort: [{ field: "createdAt", direction: "desc" }],
|
|
118
118
|
});
|
|
119
|
+
// Page back: `previousCursor` is set on any non-empty page requested with a
|
|
120
|
+
// cursor. The rows come back in the requested sort order.
|
|
121
|
+
const back = await users.paginateCursor(undefined, {
|
|
122
|
+
cursor: next.meta.previousCursor,
|
|
123
|
+
limit: 25,
|
|
124
|
+
sort: [{ field: "createdAt", direction: "desc" }],
|
|
125
|
+
});
|
|
119
126
|
```
|
|
120
127
|
|
|
128
|
+
A missing, forged, tampered or malformed cursor rejects with a
|
|
129
|
+
`ValidationError` (400, exposable, `issues[0].code` such as
|
|
130
|
+
`"cursor_signature"`) before any query runs. The message never echoes the
|
|
131
|
+
cursor or its contents. Building keyset pages by hand? Fetch with
|
|
132
|
+
`keysetFetchSort(sort, getKeysetDirection(payload))` and pass that
|
|
133
|
+
`direction` to `createKeysetPage`.
|
|
134
|
+
|
|
121
135
|
## Migrations and seeds
|
|
122
136
|
|
|
123
137
|
```typescript
|
|
@@ -163,7 +177,14 @@ to size it for long-running steps.
|
|
|
163
177
|
|
|
164
178
|
## Errors
|
|
165
179
|
|
|
166
|
-
|
|
180
|
+
Errors your own transaction callback throws that are already `@zudojs/errors`
|
|
181
|
+
errors but not database errors (a `NotFoundError`, `DomainError`,
|
|
182
|
+
`ValidationError`, ...) roll the transaction back and propagate **unchanged**
|
|
183
|
+
from `transaction()`, `withTransaction()`, `TransactionManager` and units of
|
|
184
|
+
work: same instance, same status, and not logged as a database error.
|
|
185
|
+
Everything else, driver and database failures included, is normalised as below.
|
|
186
|
+
|
|
187
|
+
Every database failure surfaces as a `DatabaseError` from `@zudojs/errors`. Prisma codes
|
|
167
188
|
are mapped to `databaseCode`, an `ErrorCode` and an HTTP status (`P2002` /
|
|
168
189
|
`P2003` → 409, `P2025` → 404, `P2034` → retryable, `P1xxx` → 503 with a fixed
|
|
169
190
|
message that never includes the host name).
|
|
@@ -209,6 +230,10 @@ await locks.withRowLock("orders", orderId, async (tx) => {
|
|
|
209
230
|
await tx.$executeRawUnsafe('UPDATE "orders" SET "status" = $1 WHERE "id" = $2', "paid", orderId);
|
|
210
231
|
}, { mode: "for-no-key-update", skipLocked: true });
|
|
211
232
|
|
|
233
|
+
// The connection manager reconnects with exponential back-off after failed
|
|
234
|
+
// health checks. The back-off wait keeps the process alive until the reconnect
|
|
235
|
+
// finishes; `disconnect()` / `destroy()` cancel it immediately.
|
|
236
|
+
|
|
212
237
|
const health = await checkDatabaseHealth(client, { timeoutMs: 2_000 });
|
|
213
238
|
health.status; // "healthy" | "degraded" | "unhealthy"
|
|
214
239
|
await assertDatabaseHealth(client); // throws DatabaseUnhealthyError with the real cause
|
|
@@ -41,7 +41,16 @@ export interface PrismaTransactionOptions {
|
|
|
41
41
|
export interface PrismaClientLike {
|
|
42
42
|
$connect(): Promise<void>;
|
|
43
43
|
$disconnect(): Promise<void>;
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Declared loosely on purpose. Prisma 7 generates the client into the
|
|
46
|
+
* application (`prisma-client` generator), and its overloaded
|
|
47
|
+
* `$transaction` (batch array or interactive callback) cannot be assigned
|
|
48
|
+
* to a single generic signature, so a real client failed to type-check
|
|
49
|
+
* against this interface and needed a cast. Any `$transaction` is
|
|
50
|
+
* accepted here; `DatabaseClient` only ever calls the interactive form
|
|
51
|
+
* (see `InteractiveTransaction`).
|
|
52
|
+
*/
|
|
53
|
+
$transaction(...args: never[]): Promise<unknown>;
|
|
45
54
|
$queryRawUnsafe<TResult = unknown>(query: string, ...values: unknown[]): Promise<TResult>;
|
|
46
55
|
$executeRawUnsafe(query: string, ...values: unknown[]): Promise<number>;
|
|
47
56
|
$on?(event: "query", callback: (event: PrismaQueryEvent) => void): void;
|
|
@@ -135,6 +144,12 @@ export declare class DatabaseClient implements DatabaseClientContract<DatabaseTr
|
|
|
135
144
|
* is released at the same moment. Racing only the outer promise let the
|
|
136
145
|
* callback finish and the transaction commit after the caller had
|
|
137
146
|
* already been told it was aborted.
|
|
147
|
+
*
|
|
148
|
+
* A `@zudojs/errors` `BaseError` thrown by the callback that is not a
|
|
149
|
+
* `DatabaseError` (a `NotFoundError`, `DomainError`, `ValidationError`,
|
|
150
|
+
* ...) rolls the transaction back and is rethrown unchanged, without
|
|
151
|
+
* being logged as a database failure. Driver and database failures, and
|
|
152
|
+
* any other thrown value, are normalised to a `DatabaseError` and logged.
|
|
138
153
|
*/
|
|
139
154
|
transaction<TResult>(callback: TransactionCallback<DatabaseTransactionContext, TResult>, options?: TransactionOptions): Promise<TResult>;
|
|
140
155
|
/**
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import { DatabaseError, DatabaseOperation } from "@zudojs/errors";
|
|
16
16
|
import { createDefaultLogger } from "./databaseClient.logger.js";
|
|
17
|
-
import { normalizeDatabaseError } from "./databaseClient.errors.js";
|
|
17
|
+
import { isNonDatabaseBaseError, normalizeDatabaseError, } from "./databaseClient.errors.js";
|
|
18
18
|
/**
|
|
19
19
|
* Isolation levels accepted by Prisma's interactive transactions. The
|
|
20
20
|
* values are identical to the string names, so no namespace lookup is
|
|
@@ -180,6 +180,12 @@ export class DatabaseClient {
|
|
|
180
180
|
* is released at the same moment. Racing only the outer promise let the
|
|
181
181
|
* callback finish and the transaction commit after the caller had
|
|
182
182
|
* already been told it was aborted.
|
|
183
|
+
*
|
|
184
|
+
* A `@zudojs/errors` `BaseError` thrown by the callback that is not a
|
|
185
|
+
* `DatabaseError` (a `NotFoundError`, `DomainError`, `ValidationError`,
|
|
186
|
+
* ...) rolls the transaction back and is rethrown unchanged, without
|
|
187
|
+
* being logged as a database failure. Driver and database failures, and
|
|
188
|
+
* any other thrown value, are normalised to a `DatabaseError` and logged.
|
|
183
189
|
*/
|
|
184
190
|
async transaction(callback, options = {}) {
|
|
185
191
|
if (typeof callback !== "function") {
|
|
@@ -188,10 +194,17 @@ export class DatabaseClient {
|
|
|
188
194
|
throwIfAborted(options.signal);
|
|
189
195
|
await this.ensureConnected();
|
|
190
196
|
const transactionOptions = buildPrismaTransactionOptions(options);
|
|
197
|
+
const interactive = this.prisma.$transaction.bind(this.prisma);
|
|
191
198
|
try {
|
|
192
|
-
return await raceAbort(
|
|
199
|
+
return await raceAbort(interactive(async (transaction) => raceAbort(callback(transaction), options.signal), transactionOptions), options.signal);
|
|
193
200
|
}
|
|
194
201
|
catch (error) {
|
|
202
|
+
if (isNonDatabaseBaseError(error)) {
|
|
203
|
+
this.logger.debug("Database transaction rolled back by caller error.", {
|
|
204
|
+
code: error.code,
|
|
205
|
+
});
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
195
208
|
const normalized = normalizeDatabaseError(error, {
|
|
196
209
|
operation: DatabaseOperation.TRANSACTION,
|
|
197
210
|
fallbackMessage: "Database transaction failed.",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `clientVersion`), so the mapping works regardless of which copy of
|
|
12
12
|
* `@prisma/client` produced the error.
|
|
13
13
|
*/
|
|
14
|
-
import { DatabaseError, DatabaseOperation, type DatabaseErrorOptions } from "@zudojs/errors";
|
|
14
|
+
import { DatabaseError, DatabaseOperation, type BaseError, type DatabaseErrorOptions } from "@zudojs/errors";
|
|
15
15
|
import type { DatabaseErrorInfo } from "../databaseType/databaseType.type.js";
|
|
16
16
|
/**
|
|
17
17
|
* Structural `DatabaseError` guard.
|
|
@@ -23,6 +23,17 @@ import type { DatabaseErrorInfo } from "../databaseType/databaseType.type.js";
|
|
|
23
23
|
* (`category: "database"` plus a string `code`).
|
|
24
24
|
*/
|
|
25
25
|
export declare function isDatabaseErrorLike(value: unknown): value is DatabaseError;
|
|
26
|
+
/**
|
|
27
|
+
* Determines whether an error is a `@zudojs/errors` `BaseError` that is
|
|
28
|
+
* *not* a database failure: a domain, application, validation, not-found
|
|
29
|
+
* or other error raised by caller code.
|
|
30
|
+
*
|
|
31
|
+
* Transaction helpers (`DatabaseClient.transaction`, `withTransaction`,
|
|
32
|
+
* `TransactionManager`, units of work) roll back on such an error and
|
|
33
|
+
* rethrow the same instance unchanged, instead of wrapping it in a 500
|
|
34
|
+
* `DatabaseError` and logging it as a database failure.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isNonDatabaseBaseError(value: unknown): value is BaseError;
|
|
26
37
|
/**
|
|
27
38
|
* Semantic outcome of a database failure.
|
|
28
39
|
*/
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `clientVersion`), so the mapping works regardless of which copy of
|
|
12
12
|
* `@prisma/client` produced the error.
|
|
13
13
|
*/
|
|
14
|
-
import { DatabaseError, DatabaseOperation, ErrorCategory, isDatabaseError, ErrorCode, } from "@zudojs/errors";
|
|
14
|
+
import { DatabaseError, DatabaseOperation, ErrorCategory, isBaseError, isDatabaseError, ErrorCode, } from "@zudojs/errors";
|
|
15
15
|
/**
|
|
16
16
|
* Structural `DatabaseError` guard.
|
|
17
17
|
*
|
|
@@ -31,6 +31,19 @@ export function isDatabaseErrorLike(value) {
|
|
|
31
31
|
typeof candidate.code === "string" &&
|
|
32
32
|
typeof candidate.statusCode === "number");
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Determines whether an error is a `@zudojs/errors` `BaseError` that is
|
|
36
|
+
* *not* a database failure: a domain, application, validation, not-found
|
|
37
|
+
* or other error raised by caller code.
|
|
38
|
+
*
|
|
39
|
+
* Transaction helpers (`DatabaseClient.transaction`, `withTransaction`,
|
|
40
|
+
* `TransactionManager`, units of work) roll back on such an error and
|
|
41
|
+
* rethrow the same instance unchanged, instead of wrapping it in a 500
|
|
42
|
+
* `DatabaseError` and logging it as a database failure.
|
|
43
|
+
*/
|
|
44
|
+
export function isNonDatabaseBaseError(value) {
|
|
45
|
+
return isBaseError(value) && !isDatabaseErrorLike(value);
|
|
46
|
+
}
|
|
34
47
|
const CONNECTION_MESSAGE = "Database connection failed.";
|
|
35
48
|
const PRISMA_CODE_MAP = Object.freeze({
|
|
36
49
|
// Query engine errors
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Prisma-backed database client and connection infrastructure.
|
|
5
5
|
*/
|
|
6
6
|
export { DatabaseClient, DatabaseAbortError, createDatabaseClient, buildPrismaTransactionOptions, createAbortError, raceAbort, throwIfAborted, SUPPORTED_ISOLATION_LEVELS, type DatabaseClientOptions, type DatabaseTransactionContext, type PrismaClientLike, type PrismaDriverAdapterLike, type PrismaQueryEvent, type PrismaTransactionOptions, type RawQueryOptions, } from "./databaseClient.core.js";
|
|
7
|
-
export { normalizeDatabaseError, withDatabaseErrorMetadata, isPrismaError, isRetryableTransactionError, isConflictError, isNotFoundError, getDatabaseErrorCode, getDatabaseErrorKind, isDatabaseErrorLike, toDatabaseErrorInfo, RETRYABLE_DATABASE_CODES, type DatabaseErrorKind, type NormalizeDatabaseErrorOptions, type PrismaErrorLike, } from "./databaseClient.errors.js";
|
|
7
|
+
export { normalizeDatabaseError, withDatabaseErrorMetadata, isPrismaError, isRetryableTransactionError, isConflictError, isNotFoundError, getDatabaseErrorCode, getDatabaseErrorKind, isDatabaseErrorLike, isNonDatabaseBaseError, toDatabaseErrorInfo, RETRYABLE_DATABASE_CODES, type DatabaseErrorKind, type NormalizeDatabaseErrorOptions, type PrismaErrorLike, } from "./databaseClient.errors.js";
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Prisma-backed database client and connection infrastructure.
|
|
5
5
|
*/
|
|
6
6
|
export { DatabaseClient, DatabaseAbortError, createDatabaseClient, buildPrismaTransactionOptions, createAbortError, raceAbort, throwIfAborted, SUPPORTED_ISOLATION_LEVELS, } from "./databaseClient.core.js";
|
|
7
|
-
export { normalizeDatabaseError, withDatabaseErrorMetadata, isPrismaError, isRetryableTransactionError, isConflictError, isNotFoundError, getDatabaseErrorCode, getDatabaseErrorKind, isDatabaseErrorLike, toDatabaseErrorInfo, RETRYABLE_DATABASE_CODES, } from "./databaseClient.errors.js";
|
|
7
|
+
export { normalizeDatabaseError, withDatabaseErrorMetadata, isPrismaError, isRetryableTransactionError, isConflictError, isNotFoundError, getDatabaseErrorCode, getDatabaseErrorKind, isDatabaseErrorLike, isNonDatabaseBaseError, toDatabaseErrorInfo, RETRYABLE_DATABASE_CODES, } from "./databaseClient.errors.js";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
|
@@ -33,7 +33,9 @@ export interface DatabaseReconnectOptions {
|
|
|
33
33
|
readonly maxAttempts?: number;
|
|
34
34
|
/**
|
|
35
35
|
* Base delay between reconnect attempts; doubles each attempt up to
|
|
36
|
-
* `maxDelayMs`. Defaults to 500 ms.
|
|
36
|
+
* `maxDelayMs`. Defaults to 500 ms. The wait keeps the process alive
|
|
37
|
+
* (a reconnect is never abandoned because the event loop drained) and
|
|
38
|
+
* is cancelled by `disconnect()` / `destroy()`.
|
|
37
39
|
*/
|
|
38
40
|
readonly baseDelayMs?: number;
|
|
39
41
|
/**
|
|
@@ -84,6 +86,7 @@ export declare class DatabaseConnectionManager {
|
|
|
84
86
|
private healthCheckInFlight?;
|
|
85
87
|
private consecutiveFailures;
|
|
86
88
|
private reconnectPromise?;
|
|
89
|
+
private reconnectAbort?;
|
|
87
90
|
private lastHealth?;
|
|
88
91
|
private destroyed;
|
|
89
92
|
constructor(options?: DatabaseConnectionManagerOptions);
|
|
@@ -97,7 +100,8 @@ export declare class DatabaseConnectionManager {
|
|
|
97
100
|
*/
|
|
98
101
|
connect(): Promise<void>;
|
|
99
102
|
/**
|
|
100
|
-
* Closes the database connection.
|
|
103
|
+
* Closes the database connection. Cancels an in-progress reconnect,
|
|
104
|
+
* including its backoff wait, so no further attempt is made.
|
|
101
105
|
*/
|
|
102
106
|
disconnect(): Promise<void>;
|
|
103
107
|
/**
|
|
@@ -153,6 +157,7 @@ export declare class DatabaseConnectionManager {
|
|
|
153
157
|
destroy(): Promise<void>;
|
|
154
158
|
private performScheduledHealthCheck;
|
|
155
159
|
private reconnectWithBackoff;
|
|
160
|
+
private cancelReconnect;
|
|
156
161
|
private performReconnect;
|
|
157
162
|
private emit;
|
|
158
163
|
}
|
|
@@ -20,6 +20,7 @@ export class DatabaseConnectionManager {
|
|
|
20
20
|
healthCheckInFlight;
|
|
21
21
|
consecutiveFailures = 0;
|
|
22
22
|
reconnectPromise;
|
|
23
|
+
reconnectAbort;
|
|
23
24
|
lastHealth;
|
|
24
25
|
destroyed = false;
|
|
25
26
|
constructor(options = {}) {
|
|
@@ -78,10 +79,12 @@ export class DatabaseConnectionManager {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
|
-
* Closes the database connection.
|
|
82
|
+
* Closes the database connection. Cancels an in-progress reconnect,
|
|
83
|
+
* including its backoff wait, so no further attempt is made.
|
|
82
84
|
*/
|
|
83
85
|
async disconnect() {
|
|
84
86
|
this.stopHealthChecks();
|
|
87
|
+
this.cancelReconnect();
|
|
85
88
|
const status = this.client.getStatus();
|
|
86
89
|
if (status === "disconnected" || status === "disconnecting")
|
|
87
90
|
return;
|
|
@@ -219,33 +222,47 @@ export class DatabaseConnectionManager {
|
|
|
219
222
|
reconnectWithBackoff() {
|
|
220
223
|
if (this.reconnectPromise)
|
|
221
224
|
return this.reconnectPromise;
|
|
222
|
-
|
|
225
|
+
const abort = new AbortController();
|
|
226
|
+
this.reconnectAbort = abort;
|
|
227
|
+
this.reconnectPromise = this.performReconnect(abort.signal).finally(() => {
|
|
223
228
|
this.reconnectPromise = undefined;
|
|
229
|
+
if (this.reconnectAbort === abort)
|
|
230
|
+
this.reconnectAbort = undefined;
|
|
224
231
|
});
|
|
225
232
|
return this.reconnectPromise;
|
|
226
233
|
}
|
|
227
|
-
|
|
234
|
+
cancelReconnect() {
|
|
235
|
+
this.reconnectAbort?.abort();
|
|
236
|
+
this.reconnectAbort = undefined;
|
|
237
|
+
}
|
|
238
|
+
async performReconnect(signal) {
|
|
228
239
|
const policy = this.reconnect;
|
|
229
240
|
if (!policy)
|
|
230
241
|
return;
|
|
231
242
|
for (let attempt = 1; attempt <= policy.maxAttempts; attempt += 1) {
|
|
232
|
-
if (this.destroyed)
|
|
243
|
+
if (this.destroyed || signal.aborted)
|
|
233
244
|
return;
|
|
234
245
|
this.emit("reconnecting", undefined, attempt);
|
|
235
246
|
try {
|
|
236
247
|
await this.client.disconnect().catch(() => undefined);
|
|
248
|
+
if (signal.aborted)
|
|
249
|
+
return;
|
|
237
250
|
await this.client.connect();
|
|
251
|
+
if (signal.aborted)
|
|
252
|
+
return;
|
|
238
253
|
this.consecutiveFailures = 0;
|
|
239
254
|
this.emit("connected", undefined, attempt);
|
|
240
255
|
return;
|
|
241
256
|
}
|
|
242
257
|
catch (error) {
|
|
258
|
+
if (signal.aborted)
|
|
259
|
+
return;
|
|
243
260
|
this.emit("error", error, attempt);
|
|
244
261
|
if (attempt === policy.maxAttempts)
|
|
245
262
|
return;
|
|
246
263
|
const delay = Math.min(policy.maxDelayMs, policy.baseDelayMs * Math.pow(2, attempt - 1));
|
|
247
264
|
if (delay > 0)
|
|
248
|
-
await sleep(delay);
|
|
265
|
+
await sleep(delay, signal);
|
|
249
266
|
}
|
|
250
267
|
}
|
|
251
268
|
}
|
|
@@ -266,10 +283,27 @@ export class DatabaseConnectionManager {
|
|
|
266
283
|
}
|
|
267
284
|
}
|
|
268
285
|
}
|
|
269
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Backoff wait between reconnect attempts. The timer is deliberately *not*
|
|
288
|
+
* unref'd: a script whose only pending work is a reconnect must stay alive
|
|
289
|
+
* until the reconnect finishes. Aborting `signal` (from `disconnect()` or
|
|
290
|
+
* `destroy()`) clears the timer and resolves at once.
|
|
291
|
+
*/
|
|
292
|
+
function sleep(milliseconds, signal) {
|
|
270
293
|
return new Promise((resolve) => {
|
|
271
|
-
|
|
272
|
-
|
|
294
|
+
if (signal.aborted) {
|
|
295
|
+
resolve();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const onAbort = () => {
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
resolve();
|
|
301
|
+
};
|
|
302
|
+
const timer = setTimeout(() => {
|
|
303
|
+
signal.removeEventListener("abort", onAbort);
|
|
304
|
+
resolve();
|
|
305
|
+
}, milliseconds);
|
|
306
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
273
307
|
});
|
|
274
308
|
}
|
|
275
309
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export type { DatabaseOperationOptions, DatabaseStatus, TransactionIsolationLevel, DatabaseOperation, DatabaseConnectionOptions, DatabaseClientHealth, DatabaseHealth as DatabaseHealthInfo, TransactionOptions, TransactionCallback, Repository, SoftDeletableRepository, PaginationInput, PaginationMeta, PaginatedResult, SortDirection, SortInput, QueryOptions, DatabaseEntity, SoftDeletableEntity, AuditableEntity, DatabaseErrorInfo, DatabaseLogger, } from "./databaseType/index.js";
|
|
20
20
|
export { noopDatabaseLogger } from "./databaseType/index.js";
|
|
21
|
-
export { DatabaseClient, DatabaseAbortError, createDatabaseClient, buildPrismaTransactionOptions, createAbortError, raceAbort, throwIfAborted, SUPPORTED_ISOLATION_LEVELS, normalizeDatabaseError, withDatabaseErrorMetadata, isPrismaError, isRetryableTransactionError, isConflictError, isNotFoundError, getDatabaseErrorCode, getDatabaseErrorKind, isDatabaseErrorLike, toDatabaseErrorInfo, RETRYABLE_DATABASE_CODES, type DatabaseClientOptions, type DatabaseTransactionContext, type PrismaClientLike, type PrismaDriverAdapterLike, type PrismaQueryEvent, type PrismaTransactionOptions, type RawQueryOptions, type DatabaseErrorKind, type NormalizeDatabaseErrorOptions, type PrismaErrorLike, } from "./databaseClient/index.js";
|
|
21
|
+
export { DatabaseClient, DatabaseAbortError, createDatabaseClient, buildPrismaTransactionOptions, createAbortError, raceAbort, throwIfAborted, SUPPORTED_ISOLATION_LEVELS, normalizeDatabaseError, withDatabaseErrorMetadata, isPrismaError, isRetryableTransactionError, isConflictError, isNotFoundError, getDatabaseErrorCode, getDatabaseErrorKind, isDatabaseErrorLike, isNonDatabaseBaseError, toDatabaseErrorInfo, RETRYABLE_DATABASE_CODES, type DatabaseClientOptions, type DatabaseTransactionContext, type PrismaClientLike, type PrismaDriverAdapterLike, type PrismaQueryEvent, type PrismaTransactionOptions, type RawQueryOptions, type DatabaseErrorKind, type NormalizeDatabaseErrorOptions, type PrismaErrorLike, } from "./databaseClient/index.js";
|
|
22
22
|
export { DatabaseConnectionManager, createConnectionManager, type DatabaseConnectionEvent, type DatabaseConnectionListener, type DatabaseConnectionEventDetails, type DatabaseConnectionManagerOptions, type DatabaseReconnectOptions, } from "./databaseConnection/index.js";
|
|
23
23
|
export { Database, createDatabase, getDatabase, connectDatabase, disconnectDatabase, resetDatabase, } from "./database/index.js";
|
|
24
24
|
export { BaseRepository, mapRepositoryError, isPrismaErrorLike, toDatabaseOperation, type RepositoryDelegate, type BaseRepositoryOptions, type SoftDeleteOptions, type CursorQueryOptions, type TransactionClientLike, type RepositoryOperation, type RepositoryErrorContext, } from "./repository/index.js";
|
|
@@ -26,7 +26,7 @@ export { TransactionManager, createTransactionManager, withTransaction, withTran
|
|
|
26
26
|
export { DatabaseUnitOfWork, createUnitOfWork, executeUnitOfWork, type UnitOfWork, type UnitOfWorkOptions, } from "./unitOfWork/index.js";
|
|
27
27
|
export { QueryBuilder, createQueryBuilder, toPrismaWhere, toPrismaArgs, toPrismaOrderBy, toPrismaSelect, toPrismaSkipTake, type QueryCondition, type QueryFilter, type QueryOperator, type RelationOperator, type QueryBuilderState, type PrismaWhere, type PrismaQueryArgs, type ToPrismaArgsOptions, } from "./queryBuilder/index.js";
|
|
28
28
|
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/index.js";
|
|
29
|
-
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, validateCursorPayload, decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, type NormalizedPagination, type CursorPaginationInput, type CursorPaginationMeta, type CursorPaginatedResult, type CursorPayload, type EncodeCursorOptions, type DecodeCursorOptions, type KeysetPageOptions, type KeysetWhere, } from "./pagination/index.js";
|
|
29
|
+
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, validateCursorPayload, decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, KEYSET_BACKWARD_KEY, getKeysetDirection, reverseKeysetSort, keysetFetchSort, createInvalidCursorError, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, type NormalizedPagination, type CursorPaginationInput, type CursorPaginationMeta, type CursorPaginatedResult, type CursorPayload, type EncodeCursorOptions, type DecodeCursorOptions, type KeysetPageOptions, type KeysetWhere, type KeysetDirection, type InvalidCursorReason, } from "./pagination/index.js";
|
|
30
30
|
export { oneToOne, oneToMany, manyToOne, manyToMany, includeRelation, includeRelations, RelationRegistry, createRelationRegistry, validateRelation, validateInclude, toPrismaInclude, DEFAULT_INCLUDE_DEPTH, isRelationType, isCollectionRelation, isSingleRelation, type RelationDefinition, type RelationType, type RelationLoadOptions, type RelationInclude, type ToPrismaIncludeOptions, } from "./relations/index.js";
|
|
31
31
|
export { DatabaseLockManager, createLockManager, acquireAdvisoryLock, lockRow, buildLockClause, normalizeAdvisoryKey, normalizeAdvisoryKeyPair, resolveLockTransactionOptions, type DatabaseLockMode, type DatabaseLockOptions, type DatabaseLockResult, } from "./locks/index.js";
|
|
32
32
|
export { MemoryDatabaseCache, createDatabaseCache, createCacheKey, escapeCachePart, serializeCachePart, getOrSet, invalidateByPrefix, CACHE_KEY_SEPARATOR, type CacheEntry, type CacheOptions, type MemoryCacheOptions, type CacheStats, type DatabaseCache, } from "./cache/index.js";
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export { noopDatabaseLogger } from "./databaseType/index.js";
|
|
20
20
|
// Client
|
|
21
|
-
export { DatabaseClient, DatabaseAbortError, createDatabaseClient, buildPrismaTransactionOptions, createAbortError, raceAbort, throwIfAborted, SUPPORTED_ISOLATION_LEVELS, normalizeDatabaseError, withDatabaseErrorMetadata, isPrismaError, isRetryableTransactionError, isConflictError, isNotFoundError, getDatabaseErrorCode, getDatabaseErrorKind, isDatabaseErrorLike, toDatabaseErrorInfo, RETRYABLE_DATABASE_CODES, } from "./databaseClient/index.js";
|
|
21
|
+
export { DatabaseClient, DatabaseAbortError, createDatabaseClient, buildPrismaTransactionOptions, createAbortError, raceAbort, throwIfAborted, SUPPORTED_ISOLATION_LEVELS, normalizeDatabaseError, withDatabaseErrorMetadata, isPrismaError, isRetryableTransactionError, isConflictError, isNotFoundError, getDatabaseErrorCode, getDatabaseErrorKind, isDatabaseErrorLike, isNonDatabaseBaseError, toDatabaseErrorInfo, RETRYABLE_DATABASE_CODES, } from "./databaseClient/index.js";
|
|
22
22
|
// Connection
|
|
23
23
|
export { DatabaseConnectionManager, createConnectionManager, } from "./databaseConnection/index.js";
|
|
24
24
|
// Database facade
|
|
@@ -33,7 +33,7 @@ export { DatabaseUnitOfWork, createUnitOfWork, executeUnitOfWork, } from "./unit
|
|
|
33
33
|
export { QueryBuilder, createQueryBuilder, toPrismaWhere, toPrismaArgs, toPrismaOrderBy, toPrismaSelect, toPrismaSkipTake, } from "./queryBuilder/index.js";
|
|
34
34
|
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/index.js";
|
|
35
35
|
// Pagination
|
|
36
|
-
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, validateCursorPayload, decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, } from "./pagination/index.js";
|
|
36
|
+
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, validateCursorPayload, decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, KEYSET_BACKWARD_KEY, getKeysetDirection, reverseKeysetSort, keysetFetchSort, createInvalidCursorError, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, } from "./pagination/index.js";
|
|
37
37
|
// Relations
|
|
38
38
|
export { oneToOne, oneToMany, manyToOne, manyToMany, includeRelation, includeRelations, RelationRegistry, createRelationRegistry, validateRelation, validateInclude, toPrismaInclude, DEFAULT_INCLUDE_DEPTH, isRelationType, isCollectionRelation, isSingleRelation, } from "./relations/index.js";
|
|
39
39
|
// Locks
|
|
@@ -5,4 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, validateCursorPayload, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, type NormalizedPagination, type CursorPaginationInput, type CursorPaginationMeta, type CursorPaginatedResult, type CursorPayload, type EncodeCursorOptions, type DecodeCursorOptions, } from "./pagination.core.js";
|
|
7
7
|
export { decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, type KeysetPageOptions, type KeysetWhere, } from "./pagination.keyset.js";
|
|
8
|
+
export { KEYSET_BACKWARD_KEY, getKeysetDirection, reverseKeysetSort, keysetFetchSort, type KeysetDirection, } from "./pagination.keysetDirection.js";
|
|
9
|
+
export { createInvalidCursorError, type InvalidCursorReason, } from "./pagination.cursorError.js";
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/pagination/index.js
CHANGED
|
@@ -5,4 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, validateCursorPayload, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, } from "./pagination.core.js";
|
|
7
7
|
export { decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, } from "./pagination.keyset.js";
|
|
8
|
+
export { KEYSET_BACKWARD_KEY, getKeysetDirection, reverseKeysetSort, keysetFetchSort, } from "./pagination.keysetDirection.js";
|
|
9
|
+
export { createInvalidCursorError, } from "./pagination.cursorError.js";
|
|
8
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -153,6 +153,10 @@ export declare function encodeCursor(value: unknown, options?: EncodeCursorOptio
|
|
|
153
153
|
* When `secret` is supplied the signature is verified; when
|
|
154
154
|
* `allowedFields` is supplied the payload must be a flat object whose keys
|
|
155
155
|
* are all allowed and whose values are primitives.
|
|
156
|
+
*
|
|
157
|
+
* A missing, forged, tampered or malformed cursor throws a
|
|
158
|
+
* `ValidationError` (HTTP 400) whose message never echoes the cursor. An
|
|
159
|
+
* invalid `secret` is a programming error and still throws `TypeError`.
|
|
156
160
|
*/
|
|
157
161
|
export declare function decodeCursor<T = unknown>(cursor: string, options?: DecodeCursorOptions): T;
|
|
158
162
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createInvalidCursorError } from "./pagination.cursorError.js";
|
|
2
3
|
/**
|
|
3
4
|
* Default pagination values.
|
|
4
5
|
*/
|
|
@@ -213,24 +214,28 @@ export function encodeCursor(value, options = {}) {
|
|
|
213
214
|
* When `secret` is supplied the signature is verified; when
|
|
214
215
|
* `allowedFields` is supplied the payload must be a flat object whose keys
|
|
215
216
|
* are all allowed and whose values are primitives.
|
|
217
|
+
*
|
|
218
|
+
* A missing, forged, tampered or malformed cursor throws a
|
|
219
|
+
* `ValidationError` (HTTP 400) whose message never echoes the cursor. An
|
|
220
|
+
* invalid `secret` is a programming error and still throws `TypeError`.
|
|
216
221
|
*/
|
|
217
222
|
export function decodeCursor(cursor, options = {}) {
|
|
218
223
|
if (typeof cursor !== "string" || cursor.trim().length === 0) {
|
|
219
|
-
throw
|
|
224
|
+
throw createInvalidCursorError("A cursor value is required.", "cursor_required");
|
|
220
225
|
}
|
|
221
226
|
let payload = cursor;
|
|
222
227
|
if (options.secret !== undefined) {
|
|
223
228
|
validateSecret(options.secret);
|
|
224
229
|
const separator = cursor.lastIndexOf(CURSOR_SIGNATURE_SEPARATOR);
|
|
225
230
|
if (separator <= 0) {
|
|
226
|
-
throw
|
|
231
|
+
throw createInvalidCursorError("Invalid pagination cursor signature.", "cursor_signature");
|
|
227
232
|
}
|
|
228
233
|
payload = cursor.slice(0, separator);
|
|
229
234
|
const signature = cursor.slice(separator + 1);
|
|
230
235
|
const expected = signCursor(payload, options.secret);
|
|
231
236
|
if (signature.length !== expected.length ||
|
|
232
237
|
!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
233
|
-
throw
|
|
238
|
+
throw createInvalidCursorError("Invalid pagination cursor signature.", "cursor_signature");
|
|
234
239
|
}
|
|
235
240
|
}
|
|
236
241
|
let decoded;
|
|
@@ -238,9 +243,7 @@ export function decodeCursor(cursor, options = {}) {
|
|
|
238
243
|
decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
239
244
|
}
|
|
240
245
|
catch (error) {
|
|
241
|
-
throw
|
|
242
|
-
cause: error,
|
|
243
|
-
});
|
|
246
|
+
throw createInvalidCursorError("Invalid pagination cursor.", "cursor_malformed", error);
|
|
244
247
|
}
|
|
245
248
|
if (options.allowedFields !== undefined) {
|
|
246
249
|
validateCursorPayload(decoded, options.allowedFields);
|
|
@@ -253,18 +256,18 @@ export function decodeCursor(cursor, options = {}) {
|
|
|
253
256
|
*/
|
|
254
257
|
export function validateCursorPayload(value, allowedFields) {
|
|
255
258
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
256
|
-
throw
|
|
259
|
+
throw createInvalidCursorError("Pagination cursor payload must be an object.", "cursor_payload");
|
|
257
260
|
}
|
|
258
261
|
const allowed = new Set(allowedFields);
|
|
259
262
|
for (const [key, entry] of Object.entries(value)) {
|
|
260
263
|
if (FORBIDDEN_CURSOR_KEYS.has(key) || !allowed.has(key)) {
|
|
261
|
-
throw
|
|
264
|
+
throw createInvalidCursorError("Pagination cursor contains an unexpected field.", "cursor_field");
|
|
262
265
|
}
|
|
263
266
|
if (entry !== null &&
|
|
264
267
|
typeof entry !== "string" &&
|
|
265
268
|
typeof entry !== "number" &&
|
|
266
269
|
typeof entry !== "boolean") {
|
|
267
|
-
throw
|
|
270
|
+
throw createInvalidCursorError("Pagination cursor fields must be primitive values.", "cursor_payload");
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ValidationError } from "@zudojs/errors";
|
|
2
|
+
/**
|
|
3
|
+
* Why a client-supplied pagination cursor was rejected. Carried as the
|
|
4
|
+
* `code` of the single issue on the thrown `ValidationError`.
|
|
5
|
+
*/
|
|
6
|
+
export type InvalidCursorReason = "cursor_required" | "cursor_signature" | "cursor_malformed" | "cursor_payload" | "cursor_field";
|
|
7
|
+
/**
|
|
8
|
+
* Creates the error thrown for a missing, forged, tampered or malformed
|
|
9
|
+
* pagination cursor.
|
|
10
|
+
*
|
|
11
|
+
* A cursor is client input, so a bad one is a `ValidationError` (HTTP 400,
|
|
12
|
+
* exposable) rather than a `TypeError` that an HTTP layer would turn into a
|
|
13
|
+
* 500. Messages never echo the cursor or its contents, and the decoding
|
|
14
|
+
* failure is kept only as the non-exposed `cause`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createInvalidCursorError(message: string, reason: InvalidCursorReason, cause?: unknown): ValidationError;
|
|
17
|
+
//# sourceMappingURL=pagination.cursorError.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ValidationError } from "@zudojs/errors";
|
|
2
|
+
/**
|
|
3
|
+
* Creates the error thrown for a missing, forged, tampered or malformed
|
|
4
|
+
* pagination cursor.
|
|
5
|
+
*
|
|
6
|
+
* A cursor is client input, so a bad one is a `ValidationError` (HTTP 400,
|
|
7
|
+
* exposable) rather than a `TypeError` that an HTTP layer would turn into a
|
|
8
|
+
* 500. Messages never echo the cursor or its contents, and the decoding
|
|
9
|
+
* failure is kept only as the non-exposed `cause`.
|
|
10
|
+
*/
|
|
11
|
+
export function createInvalidCursorError(message, reason, cause) {
|
|
12
|
+
return new ValidationError(message, {
|
|
13
|
+
issues: [{ field: "cursor", path: ["cursor"], code: reason, message }],
|
|
14
|
+
...(cause !== undefined ? { cause } : {}),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=pagination.cursorError.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SortInput } from "../databaseType/databaseType.type.js";
|
|
2
2
|
import { type CursorPaginatedResult, type CursorPayload } from "./pagination.core.js";
|
|
3
|
+
import { type KeysetDirection } from "./pagination.keysetDirection.js";
|
|
3
4
|
/**
|
|
4
5
|
* Options for building a keyset page.
|
|
5
6
|
*/
|
|
@@ -21,6 +22,13 @@ export interface KeysetPageOptions<TField extends string = string> {
|
|
|
21
22
|
* Secret used to sign cursors. Strongly recommended for public APIs.
|
|
22
23
|
*/
|
|
23
24
|
readonly secret?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Direction the page was fetched in (default `"forward"`). Use
|
|
27
|
+
* `getKeysetDirection` on the decoded cursor. A backward page must be
|
|
28
|
+
* fetched with `keysetFetchSort(sort, "backward")`; its rows are put
|
|
29
|
+
* back into `sort` order here.
|
|
30
|
+
*/
|
|
31
|
+
readonly direction?: KeysetDirection;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* A Prisma-style filter object produced by the keyset helpers.
|
|
@@ -29,27 +37,36 @@ export type KeysetWhere = Readonly<Record<string, unknown>>;
|
|
|
29
37
|
/**
|
|
30
38
|
* Decodes and validates a keyset cursor against the sort definition.
|
|
31
39
|
*
|
|
32
|
-
* The payload may only contain the sort fields and primitive values
|
|
40
|
+
* The payload may only contain the sort fields and primitive values (plus
|
|
41
|
+
* the backward marker of a `previousCursor`). A forged, tampered or
|
|
42
|
+
* malformed cursor throws a `ValidationError` (HTTP 400).
|
|
33
43
|
*/
|
|
34
44
|
export declare function decodeKeysetCursor<TField extends string = string>(cursor: string, sort: readonly SortInput<TField>[], secret?: string): CursorPayload;
|
|
35
45
|
/**
|
|
36
46
|
* Builds a Prisma-compatible `where` fragment that selects the rows that
|
|
37
|
-
* come strictly after the cursor position in the given sort order
|
|
47
|
+
* come strictly after the cursor position in the given sort order, or
|
|
48
|
+
* strictly before it for a backward cursor (a `previousCursor`).
|
|
38
49
|
*
|
|
39
|
-
* For a sort of `[a asc, b desc]` the result is
|
|
50
|
+
* For a sort of `[a asc, b desc]` the forward result is
|
|
40
51
|
* `OR: [{ a: { gt: A } }, { AND: [{ a: A }, { b: { lt: B } }] }]`.
|
|
41
52
|
*/
|
|
42
53
|
export declare function buildKeysetWhere<TField extends string = string>(cursor: CursorPayload, sort: readonly SortInput<TField>[]): KeysetWhere;
|
|
43
54
|
/**
|
|
44
|
-
* Derives the cursor payload for a row from the sort fields.
|
|
55
|
+
* Derives the cursor payload for a row from the sort fields. A
|
|
56
|
+
* `"backward"` cursor selects the rows before `row` instead of after it.
|
|
45
57
|
*/
|
|
46
|
-
export declare function createKeysetCursor<TField extends string = string>(row: Readonly<Record<string, unknown>>, sort: readonly SortInput<TField>[], secret?: string): string;
|
|
58
|
+
export declare function createKeysetCursor<TField extends string = string>(row: Readonly<Record<string, unknown>>, sort: readonly SortInput<TField>[], secret?: string, direction?: KeysetDirection): string;
|
|
47
59
|
/**
|
|
48
60
|
* Turns `limit + 1` fetched rows into a cursor-paginated result.
|
|
49
61
|
*
|
|
50
|
-
* Fetch `limit + 1` rows ordered by `sort`,
|
|
51
|
-
*
|
|
52
|
-
* the last returned
|
|
62
|
+
* Fetch `limit + 1` rows ordered by `keysetFetchSort(sort, direction)`,
|
|
63
|
+
* then pass them here: the extra row signals another page in the fetch
|
|
64
|
+
* direction and is dropped. `nextCursor` is derived from the last returned
|
|
65
|
+
* row and `previousCursor` from the first, so a client can page both ways.
|
|
66
|
+
*
|
|
67
|
+
* Forward: `previousCursor` is set whenever the page was requested with a
|
|
68
|
+
* cursor and is not empty. Backward: `nextCursor` is always set for a
|
|
69
|
+
* non-empty page, and `previousCursor` only when more rows precede it.
|
|
53
70
|
*/
|
|
54
71
|
export declare function createKeysetPage<TEntity extends Readonly<Record<string, unknown>>, TField extends string = string>(rows: readonly TEntity[], options: KeysetPageOptions<TField>): CursorPaginatedResult<TEntity>;
|
|
55
72
|
//# sourceMappingURL=pagination.keyset.d.ts.map
|
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
import { createCursorPaginatedResult, decodeCursor, encodeCursor, normalizeLimit, } from "./pagination.core.js";
|
|
2
|
+
import { createInvalidCursorError } from "./pagination.cursorError.js";
|
|
3
|
+
import { KEYSET_BACKWARD_KEY, getKeysetDirection, reverseKeysetSort, } from "./pagination.keysetDirection.js";
|
|
2
4
|
/**
|
|
3
5
|
* Decodes and validates a keyset cursor against the sort definition.
|
|
4
6
|
*
|
|
5
|
-
* The payload may only contain the sort fields and primitive values
|
|
7
|
+
* The payload may only contain the sort fields and primitive values (plus
|
|
8
|
+
* the backward marker of a `previousCursor`). A forged, tampered or
|
|
9
|
+
* malformed cursor throws a `ValidationError` (HTTP 400).
|
|
6
10
|
*/
|
|
7
11
|
export function decodeKeysetCursor(cursor, sort, secret) {
|
|
8
12
|
validateSort(sort);
|
|
9
13
|
const payload = decodeCursor(cursor, {
|
|
10
14
|
secret,
|
|
11
|
-
allowedFields: sort.map((entry) => entry.field),
|
|
15
|
+
allowedFields: [...sort.map((entry) => entry.field), KEYSET_BACKWARD_KEY],
|
|
12
16
|
});
|
|
13
17
|
for (const entry of sort) {
|
|
14
|
-
if (!(entry.field
|
|
15
|
-
throw
|
|
18
|
+
if (!Object.hasOwn(payload, entry.field)) {
|
|
19
|
+
throw createInvalidCursorError(`Pagination cursor is missing sort field "${entry.field}".`, "cursor_field");
|
|
16
20
|
}
|
|
17
21
|
}
|
|
22
|
+
const marker = payload[KEYSET_BACKWARD_KEY];
|
|
23
|
+
if (marker !== undefined && marker !== true) {
|
|
24
|
+
throw createInvalidCursorError("Pagination cursor fields must be primitive values.", "cursor_payload");
|
|
25
|
+
}
|
|
18
26
|
return payload;
|
|
19
27
|
}
|
|
20
28
|
/**
|
|
21
29
|
* Builds a Prisma-compatible `where` fragment that selects the rows that
|
|
22
|
-
* come strictly after the cursor position in the given sort order
|
|
30
|
+
* come strictly after the cursor position in the given sort order, or
|
|
31
|
+
* strictly before it for a backward cursor (a `previousCursor`).
|
|
23
32
|
*
|
|
24
|
-
* For a sort of `[a asc, b desc]` the result is
|
|
33
|
+
* For a sort of `[a asc, b desc]` the forward result is
|
|
25
34
|
* `OR: [{ a: { gt: A } }, { AND: [{ a: A }, { b: { lt: B } }] }]`.
|
|
26
35
|
*/
|
|
27
36
|
export function buildKeysetWhere(cursor, sort) {
|
|
28
37
|
validateSort(sort);
|
|
29
38
|
const branches = [];
|
|
30
|
-
|
|
39
|
+
const effective = getKeysetDirection(cursor) === "backward" ? reverseKeysetSort(sort) : sort;
|
|
40
|
+
effective.forEach((entry, index) => {
|
|
31
41
|
const comparison = entry.direction === "desc" ? "lt" : "gt";
|
|
32
|
-
const conditions =
|
|
42
|
+
const conditions = effective
|
|
33
43
|
.slice(0, index)
|
|
34
44
|
.map((previous) => ({
|
|
35
45
|
[previous.field]: { equals: cursor[previous.field] },
|
|
@@ -42,34 +52,52 @@ export function buildKeysetWhere(cursor, sort) {
|
|
|
42
52
|
return branches.length === 1 ? branches[0] : { OR: branches };
|
|
43
53
|
}
|
|
44
54
|
/**
|
|
45
|
-
* Derives the cursor payload for a row from the sort fields.
|
|
55
|
+
* Derives the cursor payload for a row from the sort fields. A
|
|
56
|
+
* `"backward"` cursor selects the rows before `row` instead of after it.
|
|
46
57
|
*/
|
|
47
|
-
export function createKeysetCursor(row, sort, secret) {
|
|
58
|
+
export function createKeysetCursor(row, sort, secret, direction = "forward") {
|
|
48
59
|
validateSort(sort);
|
|
49
60
|
const payload = {};
|
|
50
61
|
for (const entry of sort) {
|
|
51
62
|
payload[entry.field] = toCursorValue(row[entry.field], entry.field);
|
|
52
63
|
}
|
|
64
|
+
if (direction === "backward") {
|
|
65
|
+
payload[KEYSET_BACKWARD_KEY] = true;
|
|
66
|
+
}
|
|
53
67
|
return encodeCursor(payload, { secret });
|
|
54
68
|
}
|
|
55
69
|
/**
|
|
56
70
|
* Turns `limit + 1` fetched rows into a cursor-paginated result.
|
|
57
71
|
*
|
|
58
|
-
* Fetch `limit + 1` rows ordered by `sort`,
|
|
59
|
-
*
|
|
60
|
-
* the last returned
|
|
72
|
+
* Fetch `limit + 1` rows ordered by `keysetFetchSort(sort, direction)`,
|
|
73
|
+
* then pass them here: the extra row signals another page in the fetch
|
|
74
|
+
* direction and is dropped. `nextCursor` is derived from the last returned
|
|
75
|
+
* row and `previousCursor` from the first, so a client can page both ways.
|
|
76
|
+
*
|
|
77
|
+
* Forward: `previousCursor` is set whenever the page was requested with a
|
|
78
|
+
* cursor and is not empty. Backward: `nextCursor` is always set for a
|
|
79
|
+
* non-empty page, and `previousCursor` only when more rows precede it.
|
|
61
80
|
*/
|
|
62
81
|
export function createKeysetPage(rows, options) {
|
|
63
82
|
const limit = normalizeLimit(options.limit);
|
|
64
|
-
const
|
|
65
|
-
const
|
|
83
|
+
const backward = options.direction === "backward";
|
|
84
|
+
const hasMore = rows.length > limit;
|
|
85
|
+
const page = rows.slice(0, limit);
|
|
86
|
+
const data = backward ? page.reverse() : page;
|
|
87
|
+
const first = data[0];
|
|
66
88
|
const last = data[data.length - 1];
|
|
89
|
+
const hasCursor = options.cursor !== undefined && options.cursor !== null;
|
|
90
|
+
const hasNextPage = backward ? hasCursor : hasMore;
|
|
91
|
+
const hasPreviousPage = backward ? hasMore : hasCursor;
|
|
67
92
|
return createCursorPaginatedResult(data, limit, {
|
|
68
93
|
hasNextPage,
|
|
69
|
-
hasPreviousPage
|
|
94
|
+
hasPreviousPage,
|
|
70
95
|
nextCursor: hasNextPage && last
|
|
71
96
|
? createKeysetCursor(last, options.sort, options.secret)
|
|
72
97
|
: null,
|
|
98
|
+
previousCursor: hasPreviousPage && first
|
|
99
|
+
? createKeysetCursor(first, options.sort, options.secret, "backward")
|
|
100
|
+
: null,
|
|
73
101
|
});
|
|
74
102
|
}
|
|
75
103
|
function toCursorValue(value, field) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SortInput } from "../databaseType/databaseType.type.js";
|
|
2
|
+
import type { CursorPayload } from "./pagination.core.js";
|
|
3
|
+
/**
|
|
4
|
+
* Direction a keyset cursor pages in. A `nextCursor` pages forward (rows
|
|
5
|
+
* after the cursor position); a `previousCursor` pages backward (rows
|
|
6
|
+
* before it).
|
|
7
|
+
*/
|
|
8
|
+
export type KeysetDirection = "forward" | "backward";
|
|
9
|
+
/**
|
|
10
|
+
* Reserved cursor payload key marking a backward cursor. It can never
|
|
11
|
+
* collide with a sort field, because sort fields must be identifiers.
|
|
12
|
+
*/
|
|
13
|
+
export declare const KEYSET_BACKWARD_KEY = "$before";
|
|
14
|
+
/**
|
|
15
|
+
* Returns the direction a decoded keyset cursor pages in.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getKeysetDirection(cursor: CursorPayload): KeysetDirection;
|
|
18
|
+
/**
|
|
19
|
+
* Flips every direction in a sort definition. A backward page is fetched
|
|
20
|
+
* with the reversed sort (so the rows nearest the cursor come first) and
|
|
21
|
+
* then put back into the requested order by `createKeysetPage`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function reverseKeysetSort<TField extends string>(sort: readonly SortInput<TField>[]): readonly SortInput<TField>[];
|
|
24
|
+
/**
|
|
25
|
+
* Returns the sort a page must be fetched with for the given direction:
|
|
26
|
+
* the requested sort going forward, the reversed sort going backward.
|
|
27
|
+
*/
|
|
28
|
+
export declare function keysetFetchSort<TField extends string>(sort: readonly SortInput<TField>[], direction: KeysetDirection): readonly SortInput<TField>[];
|
|
29
|
+
//# sourceMappingURL=pagination.keysetDirection.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved cursor payload key marking a backward cursor. It can never
|
|
3
|
+
* collide with a sort field, because sort fields must be identifiers.
|
|
4
|
+
*/
|
|
5
|
+
export const KEYSET_BACKWARD_KEY = "$before";
|
|
6
|
+
/**
|
|
7
|
+
* Returns the direction a decoded keyset cursor pages in.
|
|
8
|
+
*/
|
|
9
|
+
export function getKeysetDirection(cursor) {
|
|
10
|
+
return cursor[KEYSET_BACKWARD_KEY] === true ? "backward" : "forward";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Flips every direction in a sort definition. A backward page is fetched
|
|
14
|
+
* with the reversed sort (so the rows nearest the cursor come first) and
|
|
15
|
+
* then put back into the requested order by `createKeysetPage`.
|
|
16
|
+
*/
|
|
17
|
+
export function reverseKeysetSort(sort) {
|
|
18
|
+
return sort.map((entry) => ({
|
|
19
|
+
...entry,
|
|
20
|
+
direction: entry.direction === "desc" ? "asc" : "desc",
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns the sort a page must be fetched with for the given direction:
|
|
25
|
+
* the requested sort going forward, the reversed sort going backward.
|
|
26
|
+
*/
|
|
27
|
+
export function keysetFetchSort(sort, direction) {
|
|
28
|
+
return direction === "backward" ? reverseKeysetSort(sort) : sort;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=pagination.keysetDirection.js.map
|
|
@@ -177,6 +177,11 @@ export declare abstract class BaseRepository<TEntity, TId = string, TCreateInput
|
|
|
177
177
|
* tiebreaker), `limit + 1` rows are fetched and the extra row decides
|
|
178
178
|
* `hasNextPage`. Cursors are validated against the sort fields and, when
|
|
179
179
|
* `cursorSecret` is configured, signed.
|
|
180
|
+
*
|
|
181
|
+
* Pass `meta.nextCursor` to page forward and `meta.previousCursor` to
|
|
182
|
+
* page backward; both come back in `options.sort` order. A forged,
|
|
183
|
+
* tampered or malformed cursor rejects with a `ValidationError` (400)
|
|
184
|
+
* before any query runs.
|
|
180
185
|
*/
|
|
181
186
|
paginateCursor<TField extends string = string>(filter?: TWhereInput, options?: CursorQueryOptions<TField>): Promise<CursorPaginatedResult<TEntity>>;
|
|
182
187
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DatabaseError } from "@zudojs/errors";
|
|
2
2
|
import { createPaginationMeta, normalizeLimit, normalizePage, } from "../pagination/pagination.core.js";
|
|
3
3
|
import { buildKeysetWhere, createKeysetPage, decodeKeysetCursor, } from "../pagination/pagination.keyset.js";
|
|
4
|
+
import { getKeysetDirection, keysetFetchSort, } from "../pagination/pagination.keysetDirection.js";
|
|
4
5
|
import { toPrismaArgs, toPrismaOrderBy, } from "../queryBuilder/queryBuilder.prisma.js";
|
|
5
6
|
import { createAbortError, createTimeoutError, mapRepositoryError, } from "./repository.errors.js";
|
|
6
7
|
const DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
|
|
@@ -151,21 +152,28 @@ export class BaseRepository {
|
|
|
151
152
|
* tiebreaker), `limit + 1` rows are fetched and the extra row decides
|
|
152
153
|
* `hasNextPage`. Cursors are validated against the sort fields and, when
|
|
153
154
|
* `cursorSecret` is configured, signed.
|
|
155
|
+
*
|
|
156
|
+
* Pass `meta.nextCursor` to page forward and `meta.previousCursor` to
|
|
157
|
+
* page backward; both come back in `options.sort` order. A forged,
|
|
158
|
+
* tampered or malformed cursor rejects with a `ValidationError` (400)
|
|
159
|
+
* before any query runs.
|
|
154
160
|
*/
|
|
155
161
|
async paginateCursor(filter, options) {
|
|
156
162
|
if (filter !== undefined) {
|
|
157
163
|
this.validateFilter(filter);
|
|
158
164
|
}
|
|
159
165
|
const sort = this.buildCursorSort(options?.sort);
|
|
160
|
-
const orderBy = this.buildOrderBy(sort);
|
|
161
166
|
const limit = normalizeLimit(options?.limit);
|
|
162
167
|
const cursor = options?.cursor ?? null;
|
|
163
168
|
let where = this.scope(filter);
|
|
169
|
+
let direction = "forward";
|
|
164
170
|
if (cursor !== null) {
|
|
165
171
|
const payload = decodeKeysetCursor(cursor, sort, this.cursorSecret);
|
|
172
|
+
direction = getKeysetDirection(payload);
|
|
166
173
|
const keyset = buildKeysetWhere(payload, sort);
|
|
167
174
|
where = where === undefined ? keyset : { AND: [where, keyset] };
|
|
168
175
|
}
|
|
176
|
+
const orderBy = this.buildOrderBy(keysetFetchSort(sort, direction));
|
|
169
177
|
const rows = await this.execute("paginateCursor", () => this.delegate.findMany({
|
|
170
178
|
where: where,
|
|
171
179
|
take: limit + 1,
|
|
@@ -176,6 +184,7 @@ export class BaseRepository {
|
|
|
176
184
|
limit,
|
|
177
185
|
cursor,
|
|
178
186
|
secret: this.cursorSecret,
|
|
187
|
+
direction,
|
|
179
188
|
});
|
|
180
189
|
}
|
|
181
190
|
/**
|
|
@@ -77,9 +77,14 @@ export declare class TransactionManager {
|
|
|
77
77
|
* Executes a callback inside a managed transaction and returns the
|
|
78
78
|
* result together with the final context (`status: "committed"`).
|
|
79
79
|
*
|
|
80
|
-
* On failure the thrown `DatabaseError` carries
|
|
81
|
-
* `transactionStatus: "failed"` and the supplied
|
|
82
|
-
* {@link getTransactionContextFromError} to recover the
|
|
80
|
+
* On a database failure the thrown `DatabaseError` carries
|
|
81
|
+
* `transactionId`, `transactionStatus: "failed"` and the supplied
|
|
82
|
+
* metadata; use {@link getTransactionContextFromError} to recover the
|
|
83
|
+
* context.
|
|
84
|
+
*
|
|
85
|
+
* A non-database `BaseError` thrown by the callback (for example a
|
|
86
|
+
* `NotFoundError` or `DomainError`) rolls the transaction back and is
|
|
87
|
+
* rethrown as the same, unmodified instance.
|
|
83
88
|
*/
|
|
84
89
|
run<TResult>(callback: (transaction: DatabaseTransactionContext, context: TransactionContext) => Promise<TResult>, options?: ManagedTransactionOptions): Promise<TransactionOutcome<TResult>>;
|
|
85
90
|
/**
|
|
@@ -93,6 +98,11 @@ export declare class TransactionManager {
|
|
|
93
98
|
export declare function createTransactionManager(client: DatabaseClient): TransactionManager;
|
|
94
99
|
/**
|
|
95
100
|
* Executes a managed database transaction.
|
|
101
|
+
*
|
|
102
|
+
* The transaction rolls back when the callback throws. A `@zudojs/errors`
|
|
103
|
+
* `BaseError` that is not a `DatabaseError` (domain, application,
|
|
104
|
+
* validation, not-found, ...) propagates unchanged; driver and database
|
|
105
|
+
* failures and any other thrown value become a `DatabaseError`.
|
|
96
106
|
*/
|
|
97
107
|
export declare function withTransaction<TResult>(client: DatabaseClient, callback: (transaction: DatabaseTransactionContext, context: TransactionContext) => Promise<TResult>, options?: ManagedTransactionOptions): Promise<TResult>;
|
|
98
108
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseOperation } from "@zudojs/errors";
|
|
2
|
-
import { isDatabaseErrorLike, isRetryableTransactionError, normalizeDatabaseError, withDatabaseErrorMetadata, } from "../databaseClient/databaseClient.errors.js";
|
|
2
|
+
import { isDatabaseErrorLike, isNonDatabaseBaseError, isRetryableTransactionError, normalizeDatabaseError, withDatabaseErrorMetadata, } from "../databaseClient/databaseClient.errors.js";
|
|
3
3
|
import { computeRetryDelay } from "./transaction.backoff.js";
|
|
4
4
|
/**
|
|
5
5
|
* Generates a transaction identifier.
|
|
@@ -33,9 +33,14 @@ export class TransactionManager {
|
|
|
33
33
|
* Executes a callback inside a managed transaction and returns the
|
|
34
34
|
* result together with the final context (`status: "committed"`).
|
|
35
35
|
*
|
|
36
|
-
* On failure the thrown `DatabaseError` carries
|
|
37
|
-
* `transactionStatus: "failed"` and the supplied
|
|
38
|
-
* {@link getTransactionContextFromError} to recover the
|
|
36
|
+
* On a database failure the thrown `DatabaseError` carries
|
|
37
|
+
* `transactionId`, `transactionStatus: "failed"` and the supplied
|
|
38
|
+
* metadata; use {@link getTransactionContextFromError} to recover the
|
|
39
|
+
* context.
|
|
40
|
+
*
|
|
41
|
+
* A non-database `BaseError` thrown by the callback (for example a
|
|
42
|
+
* `NotFoundError` or `DomainError`) rolls the transaction back and is
|
|
43
|
+
* rethrown as the same, unmodified instance.
|
|
39
44
|
*/
|
|
40
45
|
async run(callback, options = {}) {
|
|
41
46
|
if (typeof callback !== "function") {
|
|
@@ -48,6 +53,8 @@ export class TransactionManager {
|
|
|
48
53
|
return { result, context: withStatus(base, "committed") };
|
|
49
54
|
}
|
|
50
55
|
catch (error) {
|
|
56
|
+
if (isNonDatabaseBaseError(error))
|
|
57
|
+
throw error;
|
|
51
58
|
const failed = withStatus(base, "failed");
|
|
52
59
|
throw attachTransactionContext(normalizeDatabaseError(error, {
|
|
53
60
|
operation: DatabaseOperation.TRANSACTION,
|
|
@@ -70,6 +77,11 @@ export function createTransactionManager(client) {
|
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
79
|
* Executes a managed database transaction.
|
|
80
|
+
*
|
|
81
|
+
* The transaction rolls back when the callback throws. A `@zudojs/errors`
|
|
82
|
+
* `BaseError` that is not a `DatabaseError` (domain, application,
|
|
83
|
+
* validation, not-found, ...) propagates unchanged; driver and database
|
|
84
|
+
* failures and any other thrown value become a `DatabaseError`.
|
|
73
85
|
*/
|
|
74
86
|
export async function withTransaction(client, callback, options) {
|
|
75
87
|
return createTransactionManager(client).execute(callback, options);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseOperation } from "@zudojs/errors";
|
|
2
|
-
import { normalizeDatabaseError } from "../databaseClient/databaseClient.errors.js";
|
|
2
|
+
import { isNonDatabaseBaseError, normalizeDatabaseError, } from "../databaseClient/databaseClient.errors.js";
|
|
3
3
|
/**
|
|
4
4
|
* Prisma-backed unit of work.
|
|
5
5
|
*/
|
|
@@ -23,6 +23,8 @@ export class DatabaseUnitOfWork {
|
|
|
23
23
|
return await callback(transaction);
|
|
24
24
|
}
|
|
25
25
|
catch (error) {
|
|
26
|
+
if (isNonDatabaseBaseError(error))
|
|
27
|
+
throw error;
|
|
26
28
|
throw normalizeDatabaseError(error, {
|
|
27
29
|
operation: DatabaseOperation.TRANSACTION,
|
|
28
30
|
fallbackMessage: "Unit of work execution failed.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/database",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Database abstraction layer with clients, repositories, transactions, and query building for Zudojs applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"!dist/.tsbuildinfo"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@zudojs/errors": "1.
|
|
45
|
-
"@zudojs/logger": "1.
|
|
46
|
-
"@zudojs/types": "1.
|
|
44
|
+
"@zudojs/errors": "1.3.0",
|
|
45
|
+
"@zudojs/logger": "1.4.0",
|
|
46
|
+
"@zudojs/types": "1.2.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@prisma/client": ">=7.0.0 <8"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@prisma/client": "7.10.0",
|
|
53
53
|
"prisma": "7.10.0",
|
|
54
54
|
"typescript": "7.0.2",
|
|
55
|
-
"vitest": "^
|
|
55
|
+
"vitest": "^5.0.1"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=24.0.0"
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"query",
|
|
68
68
|
"transactions"
|
|
69
69
|
],
|
|
70
|
-
"homepage": "https://
|
|
70
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-database",
|
|
71
71
|
"bugs": {
|
|
72
72
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
73
73
|
},
|