@zudojs/database 1.4.0 → 1.5.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 +47 -0
- package/dist/cache/cache.memory.d.ts +0 -1
- package/dist/cache/cache.memory.js +0 -1
- package/dist/cache/index.d.ts +0 -1
- package/dist/cache/index.js +0 -1
- package/dist/database/database.core.d.ts +0 -1
- package/dist/database/database.core.js +0 -1
- package/dist/database/index.d.ts +0 -1
- package/dist/database/index.js +0 -1
- package/dist/databaseClient/databaseClient.core.d.ts +0 -1
- package/dist/databaseClient/databaseClient.core.js +0 -1
- package/dist/databaseClient/databaseClient.errors.d.ts +19 -2
- package/dist/databaseClient/databaseClient.errors.js +8 -1
- package/dist/databaseClient/databaseClient.logger.d.ts +0 -1
- package/dist/databaseClient/databaseClient.logger.js +0 -1
- package/dist/databaseClient/databaseClient.type.d.ts +0 -1
- package/dist/databaseClient/databaseClient.type.js +0 -1
- package/dist/databaseClient/index.d.ts +1 -2
- package/dist/databaseClient/index.js +1 -2
- package/dist/databaseConnection/databaseConnection.manager.d.ts +0 -1
- package/dist/databaseConnection/databaseConnection.manager.js +0 -1
- package/dist/databaseConnection/index.d.ts +0 -1
- package/dist/databaseConnection/index.js +0 -1
- package/dist/databaseType/databaseType.type.d.ts +6 -5
- package/dist/databaseType/databaseType.type.js +0 -1
- package/dist/databaseType/index.d.ts +0 -1
- package/dist/databaseType/index.js +0 -1
- package/dist/health/health.check.d.ts +0 -1
- package/dist/health/health.check.js +0 -1
- package/dist/health/index.d.ts +0 -1
- package/dist/health/index.js +0 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -3
- package/dist/locks/index.d.ts +0 -1
- package/dist/locks/index.js +0 -1
- package/dist/locks/locks.core.d.ts +0 -1
- package/dist/locks/locks.core.js +0 -1
- package/dist/migration/index.d.ts +0 -1
- package/dist/migration/index.js +0 -1
- package/dist/migration/migration.dialect.d.ts +0 -1
- package/dist/migration/migration.dialect.js +0 -1
- package/dist/migration/migration.helpers.d.ts +0 -1
- package/dist/migration/migration.helpers.js +4 -1
- package/dist/migration/migration.runner.d.ts +12 -1
- package/dist/migration/migration.runner.js +69 -17
- package/dist/migration/migration.types.d.ts +12 -1
- package/dist/migration/migration.types.js +0 -1
- package/dist/pagination/index.d.ts +1 -1
- package/dist/pagination/index.js +1 -1
- package/dist/pagination/pagination.core.d.ts +0 -1
- package/dist/pagination/pagination.core.js +0 -1
- package/dist/pagination/pagination.cursorError.d.ts +0 -1
- package/dist/pagination/pagination.cursorError.js +0 -1
- package/dist/pagination/pagination.keyset.d.ts +12 -1
- package/dist/pagination/pagination.keyset.js +15 -3
- package/dist/pagination/pagination.keysetDirection.d.ts +0 -1
- package/dist/pagination/pagination.keysetDirection.js +0 -1
- package/dist/pagination/pagination.keysetValue.d.ts +37 -0
- package/dist/pagination/pagination.keysetValue.js +55 -0
- package/dist/queryBuilder/index.d.ts +0 -1
- package/dist/queryBuilder/index.js +0 -1
- package/dist/queryBuilder/queryBuilder.core.d.ts +0 -1
- package/dist/queryBuilder/queryBuilder.core.js +0 -1
- package/dist/queryBuilder/queryBuilder.factory.d.ts +0 -1
- package/dist/queryBuilder/queryBuilder.factory.js +0 -1
- package/dist/queryBuilder/queryBuilder.filter.d.ts +0 -1
- package/dist/queryBuilder/queryBuilder.filter.js +0 -1
- package/dist/queryBuilder/queryBuilder.prisma.d.ts +0 -1
- package/dist/queryBuilder/queryBuilder.prisma.js +0 -1
- package/dist/queryBuilder/queryBuilder.type.d.ts +0 -1
- package/dist/queryBuilder/queryBuilder.type.js +0 -1
- package/dist/relations/index.d.ts +0 -1
- package/dist/relations/index.js +0 -1
- package/dist/relations/relations.definition.d.ts +0 -1
- package/dist/relations/relations.definition.js +0 -1
- package/dist/repository/index.d.ts +1 -2
- package/dist/repository/index.js +0 -1
- package/dist/repository/repository.base.d.ts +27 -1
- package/dist/repository/repository.base.js +13 -2
- package/dist/repository/repository.delegate.d.ts +0 -1
- package/dist/repository/repository.delegate.js +0 -1
- package/dist/repository/repository.errors.d.ts +3 -1
- package/dist/repository/repository.errors.js +17 -2
- package/dist/seed/index.d.ts +0 -1
- package/dist/seed/index.js +0 -1
- package/dist/seed/seed.runner.d.ts +0 -1
- package/dist/seed/seed.runner.js +0 -1
- package/dist/transaction/index.d.ts +0 -1
- package/dist/transaction/index.js +0 -1
- package/dist/transaction/transaction.backoff.d.ts +0 -1
- package/dist/transaction/transaction.backoff.js +0 -1
- package/dist/transaction/transaction.core.d.ts +16 -3
- package/dist/transaction/transaction.core.js +8 -2
- package/dist/unitOfWork/index.d.ts +0 -1
- package/dist/unitOfWork/index.js +0 -1
- package/dist/unitOfWork/unitOfWork.core.d.ts +0 -1
- package/dist/unitOfWork/unitOfWork.core.js +0 -1
- package/package.json +4 -4
|
@@ -31,6 +31,10 @@ export class MigrationRunner {
|
|
|
31
31
|
this.perItemTransaction = options.perItemTransaction ?? true;
|
|
32
32
|
validateIdentifier(this.tableName, "migration table name");
|
|
33
33
|
validateLockKey(this.lockKey, "migration lock key");
|
|
34
|
+
const detached = this.migrations.find((m) => m.transaction === false);
|
|
35
|
+
if (detached && !this.perItemTransaction) {
|
|
36
|
+
throw this.migrationError(`Migration "${detached.name}" sets transaction: false, which requires perItemTransaction: true.`, { metadata: { version: detached.version, name: detached.name } });
|
|
37
|
+
}
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
36
40
|
* Returns the migration status without executing anything.
|
|
@@ -69,16 +73,18 @@ export class MigrationRunner {
|
|
|
69
73
|
return { applied: newlyApplied, skipped };
|
|
70
74
|
}
|
|
71
75
|
for (const migration of pending) {
|
|
72
|
-
const outcome =
|
|
73
|
-
await this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
const outcome = migration.transaction === false
|
|
77
|
+
? await this.applyDetached(migration)
|
|
78
|
+
: await this.client.transaction(async (transaction) => {
|
|
79
|
+
await this.acquireMigrationLock(transaction);
|
|
80
|
+
const applied = await this.getAppliedMigrations(transaction);
|
|
81
|
+
const existing = applied.find((r) => r.version === migration.version);
|
|
82
|
+
if (existing)
|
|
83
|
+
return { record: existing, skipped: true };
|
|
84
|
+
await this.executeMigration(transaction, migration);
|
|
85
|
+
const record = await this.recordMigration(transaction, migration);
|
|
86
|
+
return { record, skipped: false };
|
|
87
|
+
}, this.buildTransactionOptions());
|
|
82
88
|
if (outcome.skipped)
|
|
83
89
|
skipped.push(outcome.record);
|
|
84
90
|
else
|
|
@@ -86,6 +92,32 @@ export class MigrationRunner {
|
|
|
86
92
|
}
|
|
87
93
|
return { applied: newlyApplied, skipped };
|
|
88
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Applies a `transaction: false` migration: the applied check and the
|
|
97
|
+
* history record run in short transactions under the advisory lock, the
|
|
98
|
+
* body runs on the root client outside any transaction.
|
|
99
|
+
*/
|
|
100
|
+
async applyDetached(migration) {
|
|
101
|
+
const existing = await this.client.transaction(async (transaction) => {
|
|
102
|
+
await this.acquireMigrationLock(transaction);
|
|
103
|
+
const applied = await this.getAppliedMigrations(transaction);
|
|
104
|
+
return applied.find((r) => r.version === migration.version);
|
|
105
|
+
}, this.buildTransactionOptions());
|
|
106
|
+
if (existing)
|
|
107
|
+
return { record: existing, skipped: true };
|
|
108
|
+
await this.executeMigration(this.rootClient(), migration);
|
|
109
|
+
const record = await this.client.transaction(async (transaction) => {
|
|
110
|
+
await this.acquireMigrationLock(transaction);
|
|
111
|
+
const applied = await this.getAppliedMigrations(transaction);
|
|
112
|
+
return (applied.find((r) => r.version === migration.version) ??
|
|
113
|
+
(await this.recordMigration(transaction, migration)));
|
|
114
|
+
}, this.buildTransactionOptions());
|
|
115
|
+
return { record, skipped: false };
|
|
116
|
+
}
|
|
117
|
+
/** The root Prisma client, handed to migrations that opt out of a transaction. */
|
|
118
|
+
rootClient() {
|
|
119
|
+
return this.client.getPrisma();
|
|
120
|
+
}
|
|
89
121
|
/**
|
|
90
122
|
* Alias of {@link migrate}.
|
|
91
123
|
*/
|
|
@@ -175,22 +207,41 @@ export class MigrationRunner {
|
|
|
175
207
|
return rolledBack;
|
|
176
208
|
}
|
|
177
209
|
while (rolledBack.length < limit) {
|
|
178
|
-
const
|
|
210
|
+
const step = await this.client.transaction(async (transaction) => {
|
|
179
211
|
await this.acquireMigrationLock(transaction);
|
|
180
212
|
const applied = await this.getAppliedMigrations(transaction);
|
|
181
213
|
const latest = applied[applied.length - 1];
|
|
182
214
|
if (!latest)
|
|
183
215
|
return null;
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
const migration = this.requireRollback(latest);
|
|
217
|
+
if (migration.transaction === false) {
|
|
218
|
+
return { record: latest, detached: true };
|
|
219
|
+
}
|
|
220
|
+
await this.runDown(transaction, migration);
|
|
221
|
+
await this.deleteMigrationRecord(transaction, latest.version);
|
|
222
|
+
return { record: latest, detached: false };
|
|
186
223
|
}, this.buildTransactionOptions());
|
|
187
|
-
if (!
|
|
224
|
+
if (!step)
|
|
188
225
|
break;
|
|
189
|
-
|
|
226
|
+
if (step.detached)
|
|
227
|
+
await this.revertDetached(step.record);
|
|
228
|
+
rolledBack.push(step.record);
|
|
190
229
|
}
|
|
191
230
|
return rolledBack;
|
|
192
231
|
}
|
|
232
|
+
/** Reverts a `transaction: false` migration; see {@link applyDetached}. */
|
|
233
|
+
async revertDetached(record) {
|
|
234
|
+
await this.runDown(this.rootClient(), this.requireRollback(record));
|
|
235
|
+
await this.client.transaction(async (transaction) => {
|
|
236
|
+
await this.acquireMigrationLock(transaction);
|
|
237
|
+
await this.deleteMigrationRecord(transaction, record.version);
|
|
238
|
+
}, this.buildTransactionOptions());
|
|
239
|
+
}
|
|
193
240
|
async revertRecord(transaction, record) {
|
|
241
|
+
await this.runDown(transaction, this.requireRollback(record));
|
|
242
|
+
await this.deleteMigrationRecord(transaction, record.version);
|
|
243
|
+
}
|
|
244
|
+
requireRollback(record) {
|
|
194
245
|
const migration = this.migrations.find((m) => m.version === record.version);
|
|
195
246
|
if (!migration) {
|
|
196
247
|
throw this.migrationError(`Migration "${record.name}" is recorded as applied but is not registered.`, { metadata: { version: record.version } });
|
|
@@ -198,6 +249,9 @@ export class MigrationRunner {
|
|
|
198
249
|
if (!migration.down) {
|
|
199
250
|
throw this.migrationError(`Migration "${migration.name}" does not define a rollback operation.`, { metadata: { version: migration.version } });
|
|
200
251
|
}
|
|
252
|
+
return migration;
|
|
253
|
+
}
|
|
254
|
+
async runDown(transaction, migration) {
|
|
201
255
|
try {
|
|
202
256
|
await migration.down(transaction);
|
|
203
257
|
}
|
|
@@ -207,7 +261,6 @@ export class MigrationRunner {
|
|
|
207
261
|
metadata: { version: migration.version, name: migration.name },
|
|
208
262
|
});
|
|
209
263
|
}
|
|
210
|
-
await this.deleteMigrationRecord(transaction, migration.version);
|
|
211
264
|
}
|
|
212
265
|
computePending(applied) {
|
|
213
266
|
const versions = new Set(applied.map((record) => record.version));
|
|
@@ -294,4 +347,3 @@ function hasVersion(records, version) {
|
|
|
294
347
|
export function createMigrationRunner(client, migrations, options) {
|
|
295
348
|
return new MigrationRunner(client, migrations, options);
|
|
296
349
|
}
|
|
297
|
-
//# sourceMappingURL=migration.runner.js.map
|
|
@@ -22,6 +22,18 @@ export interface Migration<TTransaction extends DatabaseTransactionContext = Dat
|
|
|
22
22
|
* Reverts the migration.
|
|
23
23
|
*/
|
|
24
24
|
readonly down?: (database: TTransaction) => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* When `false`, `up` and `down` run outside a transaction, on the root
|
|
27
|
+
* client, which is what statements such as `CREATE INDEX CONCURRENTLY`
|
|
28
|
+
* or `ALTER TYPE ... ADD VALUE` require. Defaults to `true`.
|
|
29
|
+
*
|
|
30
|
+
* The applied check and the history record still happen inside short
|
|
31
|
+
* transactions under the advisory lock, but the lock is released while
|
|
32
|
+
* the body runs, so make such a migration idempotent (`IF NOT EXISTS`)
|
|
33
|
+
* to tolerate a second runner started at the same moment. Requires
|
|
34
|
+
* `perItemTransaction` (the default); a batch runner refuses it.
|
|
35
|
+
*/
|
|
36
|
+
readonly transaction?: boolean;
|
|
25
37
|
}
|
|
26
38
|
/**
|
|
27
39
|
* Persisted migration record.
|
|
@@ -85,4 +97,3 @@ export interface MigrationRunnerOptions {
|
|
|
85
97
|
*/
|
|
86
98
|
readonly perItemTransaction?: boolean;
|
|
87
99
|
}
|
|
88
|
-
//# sourceMappingURL=migration.types.d.ts.map
|
|
@@ -7,4 +7,4 @@ export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, ca
|
|
|
7
7
|
export { decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, type KeysetPageOptions, type KeysetWhere, } from "./pagination.keyset.js";
|
|
8
8
|
export { KEYSET_BACKWARD_KEY, getKeysetDirection, reverseKeysetSort, keysetFetchSort, type KeysetDirection, } from "./pagination.keysetDirection.js";
|
|
9
9
|
export { createInvalidCursorError, type InvalidCursorReason, } from "./pagination.cursorError.js";
|
|
10
|
-
|
|
10
|
+
export { isDateCursorValue, keysetStrictFilter, keysetTieFilter, nextMillisecond, } from "./pagination.keysetValue.js";
|
package/dist/pagination/index.js
CHANGED
|
@@ -7,4 +7,4 @@ export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, ca
|
|
|
7
7
|
export { decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, } from "./pagination.keyset.js";
|
|
8
8
|
export { KEYSET_BACKWARD_KEY, getKeysetDirection, reverseKeysetSort, keysetFetchSort, } from "./pagination.keysetDirection.js";
|
|
9
9
|
export { createInvalidCursorError, } from "./pagination.cursorError.js";
|
|
10
|
-
|
|
10
|
+
export { isDateCursorValue, keysetStrictFilter, keysetTieFilter, nextMillisecond, } from "./pagination.keysetValue.js";
|
|
@@ -164,4 +164,3 @@ export declare function decodeCursor<T = unknown>(cursor: string, options?: Deco
|
|
|
164
164
|
* restricted to the allowed fields.
|
|
165
165
|
*/
|
|
166
166
|
export declare function validateCursorPayload(value: unknown, allowedFields: readonly string[]): asserts value is CursorPayload;
|
|
167
|
-
//# sourceMappingURL=pagination.core.d.ts.map
|
|
@@ -14,4 +14,3 @@ export type InvalidCursorReason = "cursor_required" | "cursor_signature" | "curs
|
|
|
14
14
|
* failure is kept only as the non-exposed `cause`.
|
|
15
15
|
*/
|
|
16
16
|
export declare function createInvalidCursorError(message: string, reason: InvalidCursorReason, cause?: unknown): ValidationError;
|
|
17
|
-
//# sourceMappingURL=pagination.cursorError.d.ts.map
|
|
@@ -49,11 +49,23 @@ export declare function decodeKeysetCursor<TField extends string = string>(curso
|
|
|
49
49
|
*
|
|
50
50
|
* For a sort of `[a asc, b desc]` the forward result is
|
|
51
51
|
* `OR: [{ a: { gt: A } }, { AND: [{ a: A }, { b: { lt: B } }] }]`.
|
|
52
|
+
*
|
|
53
|
+
* A value encoded from a `Date` is compared as its millisecond bucket
|
|
54
|
+
* (`gte C, lt C + 1ms` for a tie; `lt C` / `gte C + 1ms` for the strict
|
|
55
|
+
* part), so rows created in the same millisecond as the cursor row on a
|
|
56
|
+
* microsecond-precision column fall through to the next sort field
|
|
57
|
+
* instead of being skipped. See `pagination.keysetValue.ts`.
|
|
52
58
|
*/
|
|
53
59
|
export declare function buildKeysetWhere<TField extends string = string>(cursor: CursorPayload, sort: readonly SortInput<TField>[]): KeysetWhere;
|
|
54
60
|
/**
|
|
55
61
|
* Derives the cursor payload for a row from the sort fields. A
|
|
56
62
|
* `"backward"` cursor selects the rows before `row` instead of after it.
|
|
63
|
+
*
|
|
64
|
+
* `sort` must be the exact sort the page is fetched and decoded with.
|
|
65
|
+
* `BaseRepository#paginateCursor` appends the id field as a tiebreaker, so
|
|
66
|
+
* a cursor for a repository page must include it: use
|
|
67
|
+
* `repository.createCursor(row, { sort })`, which does, rather than this
|
|
68
|
+
* helper with the bare sort.
|
|
57
69
|
*/
|
|
58
70
|
export declare function createKeysetCursor<TField extends string = string>(row: Readonly<Record<string, unknown>>, sort: readonly SortInput<TField>[], secret?: string, direction?: KeysetDirection): string;
|
|
59
71
|
/**
|
|
@@ -69,4 +81,3 @@ export declare function createKeysetCursor<TField extends string = string>(row:
|
|
|
69
81
|
* non-empty page, and `previousCursor` only when more rows precede it.
|
|
70
82
|
*/
|
|
71
83
|
export declare function createKeysetPage<TEntity extends Readonly<Record<string, unknown>>, TField extends string = string>(rows: readonly TEntity[], options: KeysetPageOptions<TField>): CursorPaginatedResult<TEntity>;
|
|
72
|
-
//# sourceMappingURL=pagination.keyset.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createCursorPaginatedResult, decodeCursor, encodeCursor, normalizeLimit, } from "./pagination.core.js";
|
|
2
2
|
import { createInvalidCursorError } from "./pagination.cursorError.js";
|
|
3
3
|
import { KEYSET_BACKWARD_KEY, getKeysetDirection, reverseKeysetSort, } from "./pagination.keysetDirection.js";
|
|
4
|
+
import { keysetStrictFilter, keysetTieFilter } from "./pagination.keysetValue.js";
|
|
4
5
|
/**
|
|
5
6
|
* Decodes and validates a keyset cursor against the sort definition.
|
|
6
7
|
*
|
|
@@ -32,6 +33,12 @@ export function decodeKeysetCursor(cursor, sort, secret) {
|
|
|
32
33
|
*
|
|
33
34
|
* For a sort of `[a asc, b desc]` the forward result is
|
|
34
35
|
* `OR: [{ a: { gt: A } }, { AND: [{ a: A }, { b: { lt: B } }] }]`.
|
|
36
|
+
*
|
|
37
|
+
* A value encoded from a `Date` is compared as its millisecond bucket
|
|
38
|
+
* (`gte C, lt C + 1ms` for a tie; `lt C` / `gte C + 1ms` for the strict
|
|
39
|
+
* part), so rows created in the same millisecond as the cursor row on a
|
|
40
|
+
* microsecond-precision column fall through to the next sort field
|
|
41
|
+
* instead of being skipped. See `pagination.keysetValue.ts`.
|
|
35
42
|
*/
|
|
36
43
|
export function buildKeysetWhere(cursor, sort) {
|
|
37
44
|
validateSort(sort);
|
|
@@ -42,10 +49,10 @@ export function buildKeysetWhere(cursor, sort) {
|
|
|
42
49
|
const conditions = effective
|
|
43
50
|
.slice(0, index)
|
|
44
51
|
.map((previous) => ({
|
|
45
|
-
[previous.field]:
|
|
52
|
+
[previous.field]: keysetTieFilter(cursor[previous.field]),
|
|
46
53
|
}));
|
|
47
54
|
conditions.push({
|
|
48
|
-
[entry.field]:
|
|
55
|
+
[entry.field]: keysetStrictFilter(cursor[entry.field], comparison),
|
|
49
56
|
});
|
|
50
57
|
branches.push(conditions.length === 1 ? conditions[0] : { AND: conditions });
|
|
51
58
|
});
|
|
@@ -54,6 +61,12 @@ export function buildKeysetWhere(cursor, sort) {
|
|
|
54
61
|
/**
|
|
55
62
|
* Derives the cursor payload for a row from the sort fields. A
|
|
56
63
|
* `"backward"` cursor selects the rows before `row` instead of after it.
|
|
64
|
+
*
|
|
65
|
+
* `sort` must be the exact sort the page is fetched and decoded with.
|
|
66
|
+
* `BaseRepository#paginateCursor` appends the id field as a tiebreaker, so
|
|
67
|
+
* a cursor for a repository page must include it: use
|
|
68
|
+
* `repository.createCursor(row, { sort })`, which does, rather than this
|
|
69
|
+
* helper with the bare sort.
|
|
57
70
|
*/
|
|
58
71
|
export function createKeysetCursor(row, sort, secret, direction = "forward") {
|
|
59
72
|
validateSort(sort);
|
|
@@ -133,4 +146,3 @@ function validateSort(sort) {
|
|
|
133
146
|
seen.add(entry.field);
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
|
-
//# sourceMappingURL=pagination.keyset.js.map
|
|
@@ -26,4 +26,3 @@ export declare function reverseKeysetSort<TField extends string>(sort: readonly
|
|
|
26
26
|
* the requested sort going forward, the reversed sort going backward.
|
|
27
27
|
*/
|
|
28
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,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor value comparisons for keyset pagination.
|
|
3
|
+
*
|
|
4
|
+
* A `Date` is encoded into a cursor as its ISO string, which carries
|
|
5
|
+
* millisecond precision. PostgreSQL `timestamp`/`timestamptz` columns keep
|
|
6
|
+
* microseconds unless declared with precision `(3)`, and Prisma hands rows
|
|
7
|
+
* back as JavaScript `Date`s, so the exact stored value is not available
|
|
8
|
+
* to the cursor. Comparing `field = C` against such a column matched
|
|
9
|
+
* nothing and `field < C` skipped every row created in the same
|
|
10
|
+
* millisecond as the cursor row.
|
|
11
|
+
*
|
|
12
|
+
* A date cursor value is therefore compared as its millisecond bucket
|
|
13
|
+
* `[C, C + 1ms)`: rows outside the bucket are ordered by the strict
|
|
14
|
+
* comparison, and rows inside it fall through to the next sort field (the
|
|
15
|
+
* id tiebreaker `paginateCursor` appends). On millisecond-precision
|
|
16
|
+
* columns this is exactly the old `=`/`<`/`>` comparison.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Whether a cursor value was encoded from a `Date` by `createKeysetCursor`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isDateCursorValue(value: unknown): value is string;
|
|
22
|
+
/**
|
|
23
|
+
* The ISO string one millisecond after a date cursor value: the exclusive
|
|
24
|
+
* upper bound of its millisecond bucket.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nextMillisecond(value: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* The Prisma filter selecting rows that tie with a cursor value on a sort
|
|
29
|
+
* field: `{ equals }` for ordinary values, the millisecond bucket for dates.
|
|
30
|
+
*/
|
|
31
|
+
export declare function keysetTieFilter(value: unknown): Readonly<Record<string, unknown>>;
|
|
32
|
+
/**
|
|
33
|
+
* The Prisma filter selecting rows strictly after (`"gt"`) or before
|
|
34
|
+
* (`"lt"`) a cursor value on a sort field. For a date the boundary is the
|
|
35
|
+
* whole millisecond bucket: `lt C` below it, `gte C + 1ms` above it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function keysetStrictFilter(value: unknown, comparison: "gt" | "lt"): Readonly<Record<string, unknown>>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor value comparisons for keyset pagination.
|
|
3
|
+
*
|
|
4
|
+
* A `Date` is encoded into a cursor as its ISO string, which carries
|
|
5
|
+
* millisecond precision. PostgreSQL `timestamp`/`timestamptz` columns keep
|
|
6
|
+
* microseconds unless declared with precision `(3)`, and Prisma hands rows
|
|
7
|
+
* back as JavaScript `Date`s, so the exact stored value is not available
|
|
8
|
+
* to the cursor. Comparing `field = C` against such a column matched
|
|
9
|
+
* nothing and `field < C` skipped every row created in the same
|
|
10
|
+
* millisecond as the cursor row.
|
|
11
|
+
*
|
|
12
|
+
* A date cursor value is therefore compared as its millisecond bucket
|
|
13
|
+
* `[C, C + 1ms)`: rows outside the bucket are ordered by the strict
|
|
14
|
+
* comparison, and rows inside it fall through to the next sort field (the
|
|
15
|
+
* id tiebreaker `paginateCursor` appends). On millisecond-precision
|
|
16
|
+
* columns this is exactly the old `=`/`<`/`>` comparison.
|
|
17
|
+
*/
|
|
18
|
+
/** Exactly the shape `Date.prototype.toISOString` emits. */
|
|
19
|
+
const ISO_MILLISECOND_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a cursor value was encoded from a `Date` by `createKeysetCursor`.
|
|
22
|
+
*/
|
|
23
|
+
export function isDateCursorValue(value) {
|
|
24
|
+
return (typeof value === "string" &&
|
|
25
|
+
ISO_MILLISECOND_PATTERN.test(value) &&
|
|
26
|
+
!Number.isNaN(Date.parse(value)));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The ISO string one millisecond after a date cursor value: the exclusive
|
|
30
|
+
* upper bound of its millisecond bucket.
|
|
31
|
+
*/
|
|
32
|
+
export function nextMillisecond(value) {
|
|
33
|
+
return new Date(Date.parse(value) + 1).toISOString();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The Prisma filter selecting rows that tie with a cursor value on a sort
|
|
37
|
+
* field: `{ equals }` for ordinary values, the millisecond bucket for dates.
|
|
38
|
+
*/
|
|
39
|
+
export function keysetTieFilter(value) {
|
|
40
|
+
if (isDateCursorValue(value)) {
|
|
41
|
+
return { gte: value, lt: nextMillisecond(value) };
|
|
42
|
+
}
|
|
43
|
+
return { equals: value };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The Prisma filter selecting rows strictly after (`"gt"`) or before
|
|
47
|
+
* (`"lt"`) a cursor value on a sort field. For a date the boundary is the
|
|
48
|
+
* whole millisecond bucket: `lt C` below it, `gte C + 1ms` above it.
|
|
49
|
+
*/
|
|
50
|
+
export function keysetStrictFilter(value, comparison) {
|
|
51
|
+
if (isDateCursorValue(value) && comparison === "gt") {
|
|
52
|
+
return { gte: nextMillisecond(value) };
|
|
53
|
+
}
|
|
54
|
+
return { [comparison]: value };
|
|
55
|
+
}
|
|
@@ -8,4 +8,3 @@ export { createQueryBuilder } from "./queryBuilder.factory.js";
|
|
|
8
8
|
export { toPrismaWhere, toPrismaArgs, toPrismaOrderBy, toPrismaSelect, toPrismaSkipTake, type PrismaWhere, type PrismaQueryArgs, type ToPrismaArgsOptions, } from "./queryBuilder.prisma.js";
|
|
9
9
|
export type { QueryCondition, QueryFilter, QueryOperator, RelationOperator, QueryBuilderState, } from "./queryBuilder.type.js";
|
|
10
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";
|
|
11
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -7,4 +7,3 @@ export { QueryBuilder } from "./queryBuilder.core.js";
|
|
|
7
7
|
export { createQueryBuilder } from "./queryBuilder.factory.js";
|
|
8
8
|
export { toPrismaWhere, toPrismaArgs, toPrismaOrderBy, toPrismaSelect, toPrismaSkipTake, } from "./queryBuilder.prisma.js";
|
|
9
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";
|
|
10
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -167,4 +167,3 @@ export declare function flattenAnd(filter: QueryFilter): QueryCondition[];
|
|
|
167
167
|
*/
|
|
168
168
|
export declare function cloneFilter(filter: QueryFilter): QueryFilter;
|
|
169
169
|
export declare function cloneFilter(filter?: QueryFilter): QueryFilter | undefined;
|
|
170
|
-
//# sourceMappingURL=queryBuilder.filter.d.ts.map
|
|
@@ -4,4 +4,3 @@
|
|
|
4
4
|
* Entity relation definitions and registry.
|
|
5
5
|
*/
|
|
6
6
|
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.definition.js";
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/relations/index.js
CHANGED
|
@@ -4,4 +4,3 @@
|
|
|
4
4
|
* Entity relation definitions and registry.
|
|
5
5
|
*/
|
|
6
6
|
export { oneToOne, oneToMany, manyToOne, manyToMany, includeRelation, includeRelations, RelationRegistry, createRelationRegistry, validateRelation, validateInclude, toPrismaInclude, DEFAULT_INCLUDE_DEPTH, isRelationType, isCollectionRelation, isSingleRelation, } from "./relations.definition.js";
|
|
7
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -181,4 +181,3 @@ export declare function isCollectionRelation(relation: RelationDefinition): bool
|
|
|
181
181
|
* Returns whether the relation represents a single entity.
|
|
182
182
|
*/
|
|
183
183
|
export declare function isSingleRelation(relation: RelationDefinition): boolean;
|
|
184
|
-
//# sourceMappingURL=relations.definition.d.ts.map
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generic repository pattern with Prisma delegate support.
|
|
5
5
|
*/
|
|
6
|
-
export { BaseRepository, type BaseRepositoryOptions, type SoftDeleteOptions, type CursorQueryOptions, type TransactionClientLike, } from "./repository.base.js";
|
|
6
|
+
export { BaseRepository, type BaseRepositoryOptions, type SoftDeleteOptions, type CreateCursorOptions, type CursorQueryOptions, type TransactionClientLike, } from "./repository.base.js";
|
|
7
7
|
export { type RepositoryDelegate, type RepositoryDelegateOperations, } from "./repository.delegate.js";
|
|
8
8
|
export { mapRepositoryError, isPrismaErrorLike, toDatabaseOperation, toErrorMetadata, createAbortError, createTimeoutError, type RepositoryOperation, type RepositoryErrorContext, type PrismaErrorLike, } from "./repository.errors.js";
|
|
9
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/repository/index.js
CHANGED
|
@@ -6,4 +6,3 @@
|
|
|
6
6
|
export { BaseRepository, } from "./repository.base.js";
|
|
7
7
|
export {} from "./repository.delegate.js";
|
|
8
8
|
export { mapRepositoryError, isPrismaErrorLike, toDatabaseOperation, toErrorMetadata, createAbortError, createTimeoutError, } from "./repository.errors.js";
|
|
9
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DatabaseOperationOptions, PaginatedResult, QueryOptions, Repository, SoftDeletableRepository, SortInput } from "../databaseType/databaseType.type.js";
|
|
2
2
|
import { type CursorPaginatedResult } from "../pagination/pagination.core.js";
|
|
3
|
+
import { type KeysetDirection } from "../pagination/pagination.keysetDirection.js";
|
|
3
4
|
import type { QueryBuilder } from "../queryBuilder/queryBuilder.core.js";
|
|
4
5
|
import type { QueryBuilderState } from "../queryBuilder/queryBuilder.type.js";
|
|
5
6
|
import type { RelationLoadOptions, RelationRegistry } from "../relations/relations.definition.js";
|
|
@@ -61,6 +62,22 @@ export interface CursorQueryOptions<TField extends string = string> extends Data
|
|
|
61
62
|
*/
|
|
62
63
|
readonly sort?: readonly SortInput<TField>[];
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Options for {@link BaseRepository.createCursor}.
|
|
67
|
+
*/
|
|
68
|
+
export interface CreateCursorOptions<TField extends string = string> {
|
|
69
|
+
/**
|
|
70
|
+
* The sort the page is fetched with, as passed to `paginateCursor`. The
|
|
71
|
+
* id field is appended as a tiebreaker when absent, exactly as
|
|
72
|
+
* `paginateCursor` does.
|
|
73
|
+
*/
|
|
74
|
+
readonly sort?: readonly SortInput<TField>[];
|
|
75
|
+
/**
|
|
76
|
+
* `"forward"` (default) selects the rows after `row`; `"backward"` the
|
|
77
|
+
* rows before it (a `previousCursor`).
|
|
78
|
+
*/
|
|
79
|
+
readonly direction?: KeysetDirection;
|
|
80
|
+
}
|
|
64
81
|
/**
|
|
65
82
|
* A Prisma transaction client (or any object exposing model delegates).
|
|
66
83
|
*/
|
|
@@ -133,6 +150,16 @@ export declare abstract class BaseRepository<TEntity, TId = string, TCreateInput
|
|
|
133
150
|
* before any query runs.
|
|
134
151
|
*/
|
|
135
152
|
paginateCursor<TField extends string = string>(filter?: TWhereInput, options?: CursorQueryOptions<TField>): Promise<CursorPaginatedResult<TEntity>>;
|
|
153
|
+
/**
|
|
154
|
+
* Builds a cursor positioned at `row` that `paginateCursor` accepts for
|
|
155
|
+
* the same `sort`: the id tiebreaker is appended and the cursor is
|
|
156
|
+
* signed with `cursorSecret` when configured.
|
|
157
|
+
*
|
|
158
|
+
* `createKeysetCursor(row, sort)` with the bare sort produces a cursor
|
|
159
|
+
* without the tiebreaker, which `paginateCursor` rejects as missing the
|
|
160
|
+
* id field.
|
|
161
|
+
*/
|
|
162
|
+
createCursor<TField extends string = string>(row: TEntity, options?: CreateCursorOptions<TField>): string;
|
|
136
163
|
/**
|
|
137
164
|
* Finds entities from a query builder (or its built state), applying the
|
|
138
165
|
* filter, sort, select, include and pagination it carries.
|
|
@@ -234,4 +261,3 @@ export declare abstract class BaseRepository<TEntity, TId = string, TCreateInput
|
|
|
234
261
|
private buildCursorSort;
|
|
235
262
|
private rebind;
|
|
236
263
|
}
|
|
237
|
-
//# sourceMappingURL=repository.base.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DatabaseError } from "@zudojs/errors";
|
|
2
2
|
import { createPaginationMeta, normalizeLimit, normalizePage, } from "../pagination/pagination.core.js";
|
|
3
|
-
import { buildKeysetWhere, createKeysetPage, decodeKeysetCursor, } from "../pagination/pagination.keyset.js";
|
|
3
|
+
import { buildKeysetWhere, createKeysetCursor, createKeysetPage, decodeKeysetCursor, } from "../pagination/pagination.keyset.js";
|
|
4
4
|
import { getKeysetDirection, keysetFetchSort, } from "../pagination/pagination.keysetDirection.js";
|
|
5
5
|
import { toPrismaArgs, toPrismaOrderBy, } from "../queryBuilder/queryBuilder.prisma.js";
|
|
6
6
|
import { toDelegateOperations, } from "./repository.delegate.js";
|
|
@@ -188,6 +188,18 @@ export class BaseRepository {
|
|
|
188
188
|
direction,
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Builds a cursor positioned at `row` that `paginateCursor` accepts for
|
|
193
|
+
* the same `sort`: the id tiebreaker is appended and the cursor is
|
|
194
|
+
* signed with `cursorSecret` when configured.
|
|
195
|
+
*
|
|
196
|
+
* `createKeysetCursor(row, sort)` with the bare sort produces a cursor
|
|
197
|
+
* without the tiebreaker, which `paginateCursor` rejects as missing the
|
|
198
|
+
* id field.
|
|
199
|
+
*/
|
|
200
|
+
createCursor(row, options) {
|
|
201
|
+
return createKeysetCursor(row, this.buildCursorSort(options?.sort), this.cursorSecret, options?.direction ?? "forward");
|
|
202
|
+
}
|
|
191
203
|
/**
|
|
192
204
|
* Finds entities from a query builder (or its built state), applying the
|
|
193
205
|
* filter, sort, select, include and pagination it carries.
|
|
@@ -536,4 +548,3 @@ function validateFieldName(field, name) {
|
|
|
536
548
|
function lowerFirst(value) {
|
|
537
549
|
return value.length === 0 ? value : value[0].toLowerCase() + value.slice(1);
|
|
538
550
|
}
|
|
539
|
-
//# sourceMappingURL=repository.base.js.map
|
|
@@ -90,4 +90,3 @@ export interface RepositoryDelegateOperations<TEntity, TId = string, TCreateInpu
|
|
|
90
90
|
* model delegate accepts at runtime.
|
|
91
91
|
*/
|
|
92
92
|
export declare function toDelegateOperations<TEntity, TId, TCreateInput, TUpdateInput, TWhereInput>(delegate: RepositoryDelegate<TEntity, TId, TCreateInput, TUpdateInput, TWhereInput>): RepositoryDelegateOperations<TEntity, TId, TCreateInput, TUpdateInput, TWhereInput>;
|
|
93
|
-
//# sourceMappingURL=repository.delegate.d.ts.map
|
|
@@ -40,6 +40,9 @@ export declare function toDatabaseOperation(operation: RepositoryOperation | str
|
|
|
40
40
|
* - `P2034` serialization failure → `ERR_DATABASE_TRANSACTION` / 409 (retryable)
|
|
41
41
|
* - `P2024` pool timeout → `ERR_DATABASE_TIMEOUT` / 503
|
|
42
42
|
* - `P1xxx` connection failures → `ERR_DATABASE_CONNECTION` / 503
|
|
43
|
+
* - every other code `normalizeDatabaseError` knows (`P2000` value too
|
|
44
|
+
* long / 400, `P2004` constraint / 409, `P2011` null / 400, `P2014`,
|
|
45
|
+
* `P2015`, `P2018`, `P2028`) → the same status, code and exposure
|
|
43
46
|
*
|
|
44
47
|
* Existing `DatabaseError`s are returned unchanged.
|
|
45
48
|
*/
|
|
@@ -57,4 +60,3 @@ export declare function createTimeoutError(context: Omit<RepositoryErrorContext,
|
|
|
57
60
|
* package accepts.
|
|
58
61
|
*/
|
|
59
62
|
export declare function toErrorMetadata(value?: Readonly<Record<string, unknown>>): ErrorMetadata;
|
|
60
|
-
//# sourceMappingURL=repository.errors.d.ts.map
|