@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.
Files changed (98) hide show
  1. package/README.md +47 -0
  2. package/dist/cache/cache.memory.d.ts +0 -1
  3. package/dist/cache/cache.memory.js +0 -1
  4. package/dist/cache/index.d.ts +0 -1
  5. package/dist/cache/index.js +0 -1
  6. package/dist/database/database.core.d.ts +0 -1
  7. package/dist/database/database.core.js +0 -1
  8. package/dist/database/index.d.ts +0 -1
  9. package/dist/database/index.js +0 -1
  10. package/dist/databaseClient/databaseClient.core.d.ts +0 -1
  11. package/dist/databaseClient/databaseClient.core.js +0 -1
  12. package/dist/databaseClient/databaseClient.errors.d.ts +19 -2
  13. package/dist/databaseClient/databaseClient.errors.js +8 -1
  14. package/dist/databaseClient/databaseClient.logger.d.ts +0 -1
  15. package/dist/databaseClient/databaseClient.logger.js +0 -1
  16. package/dist/databaseClient/databaseClient.type.d.ts +0 -1
  17. package/dist/databaseClient/databaseClient.type.js +0 -1
  18. package/dist/databaseClient/index.d.ts +1 -2
  19. package/dist/databaseClient/index.js +1 -2
  20. package/dist/databaseConnection/databaseConnection.manager.d.ts +0 -1
  21. package/dist/databaseConnection/databaseConnection.manager.js +0 -1
  22. package/dist/databaseConnection/index.d.ts +0 -1
  23. package/dist/databaseConnection/index.js +0 -1
  24. package/dist/databaseType/databaseType.type.d.ts +6 -5
  25. package/dist/databaseType/databaseType.type.js +0 -1
  26. package/dist/databaseType/index.d.ts +0 -1
  27. package/dist/databaseType/index.js +0 -1
  28. package/dist/health/health.check.d.ts +0 -1
  29. package/dist/health/health.check.js +0 -1
  30. package/dist/health/index.d.ts +0 -1
  31. package/dist/health/index.js +0 -1
  32. package/dist/index.d.ts +3 -4
  33. package/dist/index.js +2 -3
  34. package/dist/locks/index.d.ts +0 -1
  35. package/dist/locks/index.js +0 -1
  36. package/dist/locks/locks.core.d.ts +0 -1
  37. package/dist/locks/locks.core.js +0 -1
  38. package/dist/migration/index.d.ts +0 -1
  39. package/dist/migration/index.js +0 -1
  40. package/dist/migration/migration.dialect.d.ts +0 -1
  41. package/dist/migration/migration.dialect.js +0 -1
  42. package/dist/migration/migration.helpers.d.ts +0 -1
  43. package/dist/migration/migration.helpers.js +4 -1
  44. package/dist/migration/migration.runner.d.ts +12 -1
  45. package/dist/migration/migration.runner.js +69 -17
  46. package/dist/migration/migration.types.d.ts +12 -1
  47. package/dist/migration/migration.types.js +0 -1
  48. package/dist/pagination/index.d.ts +1 -1
  49. package/dist/pagination/index.js +1 -1
  50. package/dist/pagination/pagination.core.d.ts +0 -1
  51. package/dist/pagination/pagination.core.js +0 -1
  52. package/dist/pagination/pagination.cursorError.d.ts +0 -1
  53. package/dist/pagination/pagination.cursorError.js +0 -1
  54. package/dist/pagination/pagination.keyset.d.ts +12 -1
  55. package/dist/pagination/pagination.keyset.js +15 -3
  56. package/dist/pagination/pagination.keysetDirection.d.ts +0 -1
  57. package/dist/pagination/pagination.keysetDirection.js +0 -1
  58. package/dist/pagination/pagination.keysetValue.d.ts +37 -0
  59. package/dist/pagination/pagination.keysetValue.js +55 -0
  60. package/dist/queryBuilder/index.d.ts +0 -1
  61. package/dist/queryBuilder/index.js +0 -1
  62. package/dist/queryBuilder/queryBuilder.core.d.ts +0 -1
  63. package/dist/queryBuilder/queryBuilder.core.js +0 -1
  64. package/dist/queryBuilder/queryBuilder.factory.d.ts +0 -1
  65. package/dist/queryBuilder/queryBuilder.factory.js +0 -1
  66. package/dist/queryBuilder/queryBuilder.filter.d.ts +0 -1
  67. package/dist/queryBuilder/queryBuilder.filter.js +0 -1
  68. package/dist/queryBuilder/queryBuilder.prisma.d.ts +0 -1
  69. package/dist/queryBuilder/queryBuilder.prisma.js +0 -1
  70. package/dist/queryBuilder/queryBuilder.type.d.ts +0 -1
  71. package/dist/queryBuilder/queryBuilder.type.js +0 -1
  72. package/dist/relations/index.d.ts +0 -1
  73. package/dist/relations/index.js +0 -1
  74. package/dist/relations/relations.definition.d.ts +0 -1
  75. package/dist/relations/relations.definition.js +0 -1
  76. package/dist/repository/index.d.ts +1 -2
  77. package/dist/repository/index.js +0 -1
  78. package/dist/repository/repository.base.d.ts +27 -1
  79. package/dist/repository/repository.base.js +13 -2
  80. package/dist/repository/repository.delegate.d.ts +0 -1
  81. package/dist/repository/repository.delegate.js +0 -1
  82. package/dist/repository/repository.errors.d.ts +3 -1
  83. package/dist/repository/repository.errors.js +17 -2
  84. package/dist/seed/index.d.ts +0 -1
  85. package/dist/seed/index.js +0 -1
  86. package/dist/seed/seed.runner.d.ts +0 -1
  87. package/dist/seed/seed.runner.js +0 -1
  88. package/dist/transaction/index.d.ts +0 -1
  89. package/dist/transaction/index.js +0 -1
  90. package/dist/transaction/transaction.backoff.d.ts +0 -1
  91. package/dist/transaction/transaction.backoff.js +0 -1
  92. package/dist/transaction/transaction.core.d.ts +16 -3
  93. package/dist/transaction/transaction.core.js +8 -2
  94. package/dist/unitOfWork/index.d.ts +0 -1
  95. package/dist/unitOfWork/index.js +0 -1
  96. package/dist/unitOfWork/unitOfWork.core.d.ts +0 -1
  97. package/dist/unitOfWork/unitOfWork.core.js +0 -1
  98. 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 = await this.client.transaction(async (transaction) => {
73
- await this.acquireMigrationLock(transaction);
74
- const applied = await this.getAppliedMigrations(transaction);
75
- const existing = applied.find((r) => r.version === migration.version);
76
- if (existing)
77
- return { record: existing, skipped: true };
78
- await this.executeMigration(transaction, migration);
79
- const record = await this.recordMigration(transaction, migration);
80
- return { record, skipped: false };
81
- }, this.buildTransactionOptions());
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 record = await this.client.transaction(async (transaction) => {
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
- await this.revertRecord(transaction, latest);
185
- return latest;
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 (!record)
224
+ if (!step)
188
225
  break;
189
- rolledBack.push(record);
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
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=migration.types.js.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
- //# sourceMappingURL=index.d.ts.map
10
+ export { isDateCursorValue, keysetStrictFilter, keysetTieFilter, nextMillisecond, } from "./pagination.keysetValue.js";
@@ -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
- //# sourceMappingURL=index.js.map
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
@@ -279,4 +279,3 @@ function validateSecret(secret) {
279
279
  throw new TypeError("A cursor secret must be a non-empty string.");
280
280
  }
281
281
  }
282
- //# sourceMappingURL=pagination.core.js.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
@@ -14,4 +14,3 @@ export function createInvalidCursorError(message, reason, cause) {
14
14
  ...(cause !== undefined ? { cause } : {}),
15
15
  });
16
16
  }
17
- //# sourceMappingURL=pagination.cursorError.js.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]: { equals: cursor[previous.field] },
52
+ [previous.field]: keysetTieFilter(cursor[previous.field]),
46
53
  }));
47
54
  conditions.push({
48
- [entry.field]: { [comparison]: cursor[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
@@ -27,4 +27,3 @@ export function reverseKeysetSort(sort) {
27
27
  export function keysetFetchSort(sort, direction) {
28
28
  return direction === "backward" ? reverseKeysetSort(sort) : sort;
29
29
  }
30
- //# sourceMappingURL=pagination.keysetDirection.js.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
@@ -175,4 +175,3 @@ export declare class QueryBuilder<TField extends string = string> {
175
175
  */
176
176
  private addCondition;
177
177
  }
178
- //# sourceMappingURL=queryBuilder.core.d.ts.map
@@ -417,4 +417,3 @@ function deepFreeze(value) {
417
417
  }
418
418
  return value;
419
419
  }
420
- //# sourceMappingURL=queryBuilder.core.js.map
@@ -3,4 +3,3 @@ import { QueryBuilder } from "./queryBuilder.core.js";
3
3
  * Creates a new query builder.
4
4
  */
5
5
  export declare function createQueryBuilder<TField extends string = string>(): QueryBuilder<TField>;
6
- //# sourceMappingURL=queryBuilder.factory.d.ts.map
@@ -5,4 +5,3 @@ import { QueryBuilder } from "./queryBuilder.core.js";
5
5
  export function createQueryBuilder() {
6
6
  return new QueryBuilder();
7
7
  }
8
- //# sourceMappingURL=queryBuilder.factory.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
@@ -494,4 +494,3 @@ function validateDate(value, name) {
494
494
  throw new TypeError(`Invalid ${name} date.`);
495
495
  }
496
496
  }
497
- //# sourceMappingURL=queryBuilder.filter.js.map
@@ -57,4 +57,3 @@ export declare function toPrismaSkipTake(pagination?: PaginationInput, offset?:
57
57
  readonly skip?: number;
58
58
  readonly take?: number;
59
59
  };
60
- //# sourceMappingURL=queryBuilder.prisma.d.ts.map
@@ -225,4 +225,3 @@ function validateFieldName(field) {
225
225
  throw new TypeError(`Invalid query field name "${String(field)}".`);
226
226
  }
227
227
  }
228
- //# sourceMappingURL=queryBuilder.prisma.js.map
@@ -45,4 +45,3 @@ export interface QueryBuilderState<TField extends string = string> {
45
45
  readonly select?: readonly TField[];
46
46
  readonly include?: readonly RelationInclude[];
47
47
  }
48
- //# sourceMappingURL=queryBuilder.type.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=queryBuilder.type.js.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
@@ -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
@@ -379,4 +379,3 @@ function validateFieldName(field) {
379
379
  throw new TypeError(`Invalid relation field name "${String(field)}".`);
380
380
  }
381
381
  }
382
- //# sourceMappingURL=relations.definition.js.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
@@ -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
@@ -9,4 +9,3 @@
9
9
  export function toDelegateOperations(delegate) {
10
10
  return delegate;
11
11
  }
12
- //# sourceMappingURL=repository.delegate.js.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