@zudojs/database 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -17
  3. package/dist/cache/cache.memory.d.ts +167 -0
  4. package/dist/cache/cache.memory.js +186 -40
  5. package/dist/cache/index.d.ts +2 -2
  6. package/dist/cache/index.js +2 -2
  7. package/dist/database/database.core.d.ts +91 -0
  8. package/dist/database/database.core.js +21 -4
  9. package/dist/databaseClient/databaseClient.core.d.ts +165 -14
  10. package/dist/databaseClient/databaseClient.core.js +304 -70
  11. package/dist/databaseClient/databaseClient.errors.d.ts +112 -0
  12. package/dist/databaseClient/databaseClient.errors.js +425 -0
  13. package/dist/databaseClient/index.d.ts +2 -1
  14. package/dist/databaseClient/index.js +2 -1
  15. package/dist/databaseConnection/databaseConnection.manager.d.ts +163 -0
  16. package/dist/databaseConnection/databaseConnection.manager.js +138 -76
  17. package/dist/databaseConnection/index.d.ts +1 -1
  18. package/dist/databaseType/databaseType.type.d.ts +224 -0
  19. package/dist/databaseType/databaseType.type.js +0 -35
  20. package/dist/health/health.check.d.ts +74 -0
  21. package/dist/health/health.check.js +78 -47
  22. package/dist/health/index.d.ts +1 -1
  23. package/dist/health/index.js +1 -1
  24. package/dist/index.d.ts +14 -14
  25. package/dist/index.js +11 -11
  26. package/dist/locks/index.d.ts +2 -2
  27. package/dist/locks/index.js +2 -2
  28. package/dist/locks/locks.core.d.ts +128 -0
  29. package/dist/locks/locks.core.js +144 -85
  30. package/dist/migration/index.d.ts +3 -1
  31. package/dist/migration/index.js +2 -0
  32. package/dist/migration/migration.dialect.d.ts +65 -0
  33. package/dist/migration/migration.dialect.js +63 -0
  34. package/dist/migration/migration.helpers.d.ts +65 -0
  35. package/dist/migration/migration.helpers.js +60 -18
  36. package/dist/migration/migration.runner.d.ts +75 -0
  37. package/dist/migration/migration.runner.js +195 -107
  38. package/dist/migration/migration.types.d.ts +85 -0
  39. package/dist/pagination/index.d.ts +2 -1
  40. package/dist/pagination/index.js +2 -1
  41. package/dist/pagination/pagination.core.d.ts +163 -0
  42. package/dist/pagination/pagination.core.js +78 -9
  43. package/dist/pagination/pagination.keyset.d.ts +55 -0
  44. package/dist/pagination/pagination.keyset.js +108 -0
  45. package/dist/queryBuilder/index.d.ts +4 -3
  46. package/dist/queryBuilder/index.js +3 -2
  47. package/dist/queryBuilder/queryBuilder.core.d.ts +178 -0
  48. package/dist/queryBuilder/queryBuilder.core.js +92 -14
  49. package/dist/queryBuilder/queryBuilder.factory.d.ts +6 -0
  50. package/dist/queryBuilder/queryBuilder.factory.js +0 -25
  51. package/dist/queryBuilder/queryBuilder.filter.d.ts +170 -0
  52. package/dist/queryBuilder/queryBuilder.filter.js +191 -30
  53. package/dist/queryBuilder/queryBuilder.prisma.d.ts +60 -0
  54. package/dist/queryBuilder/queryBuilder.prisma.js +233 -0
  55. package/dist/queryBuilder/queryBuilder.type.d.ts +48 -0
  56. package/dist/relations/index.d.ts +1 -1
  57. package/dist/relations/index.js +1 -1
  58. package/dist/relations/relations.definition.d.ts +184 -0
  59. package/dist/relations/relations.definition.js +186 -19
  60. package/dist/repository/index.d.ts +2 -1
  61. package/dist/repository/index.js +1 -0
  62. package/dist/repository/repository.base.d.ts +270 -0
  63. package/dist/repository/repository.base.js +350 -84
  64. package/dist/repository/repository.errors.d.ts +60 -0
  65. package/dist/repository/repository.errors.js +235 -0
  66. package/dist/seed/index.d.ts +2 -2
  67. package/dist/seed/index.js +1 -1
  68. package/dist/seed/seed.runner.d.ts +160 -0
  69. package/dist/seed/seed.runner.js +194 -225
  70. package/dist/transaction/index.d.ts +1 -1
  71. package/dist/transaction/index.js +1 -1
  72. package/dist/transaction/transaction.core.d.ts +117 -0
  73. package/dist/transaction/transaction.core.js +83 -56
  74. package/dist/unitOfWork/unitOfWork.core.d.ts +44 -0
  75. package/dist/unitOfWork/unitOfWork.core.js +9 -20
  76. package/package.json +40 -24
  77. package/dist/cache/cache.memory.d.ts.map +0 -1
  78. package/dist/cache/cache.memory.js.map +0 -1
  79. package/dist/cache/index.d.ts.map +0 -1
  80. package/dist/cache/index.js.map +0 -1
  81. package/dist/database/database.core.d.ts.map +0 -1
  82. package/dist/database/database.core.js.map +0 -1
  83. package/dist/database/index.d.ts.map +0 -1
  84. package/dist/database/index.js.map +0 -1
  85. package/dist/databaseClient/databaseClient.core.d.ts.map +0 -1
  86. package/dist/databaseClient/databaseClient.core.js.map +0 -1
  87. package/dist/databaseClient/databaseClient.logger.d.ts.map +0 -1
  88. package/dist/databaseClient/databaseClient.logger.js.map +0 -1
  89. package/dist/databaseClient/index.d.ts.map +0 -1
  90. package/dist/databaseClient/index.js.map +0 -1
  91. package/dist/databaseConnection/databaseConnection.manager.d.ts.map +0 -1
  92. package/dist/databaseConnection/databaseConnection.manager.js.map +0 -1
  93. package/dist/databaseConnection/index.d.ts.map +0 -1
  94. package/dist/databaseConnection/index.js.map +0 -1
  95. package/dist/databaseType/databaseType.type.d.ts.map +0 -1
  96. package/dist/databaseType/databaseType.type.js.map +0 -1
  97. package/dist/databaseType/index.d.ts.map +0 -1
  98. package/dist/databaseType/index.js.map +0 -1
  99. package/dist/health/health.check.d.ts.map +0 -1
  100. package/dist/health/health.check.js.map +0 -1
  101. package/dist/health/index.d.ts.map +0 -1
  102. package/dist/health/index.js.map +0 -1
  103. package/dist/index.d.ts.map +0 -1
  104. package/dist/index.js.map +0 -1
  105. package/dist/locks/index.d.ts.map +0 -1
  106. package/dist/locks/index.js.map +0 -1
  107. package/dist/locks/locks.core.d.ts.map +0 -1
  108. package/dist/locks/locks.core.js.map +0 -1
  109. package/dist/migration/index.d.ts.map +0 -1
  110. package/dist/migration/index.js.map +0 -1
  111. package/dist/migration/migration.helpers.d.ts.map +0 -1
  112. package/dist/migration/migration.helpers.js.map +0 -1
  113. package/dist/migration/migration.runner.d.ts.map +0 -1
  114. package/dist/migration/migration.runner.js.map +0 -1
  115. package/dist/migration/migration.types.d.ts.map +0 -1
  116. package/dist/migration/migration.types.js.map +0 -1
  117. package/dist/pagination/index.d.ts.map +0 -1
  118. package/dist/pagination/index.js.map +0 -1
  119. package/dist/pagination/pagination.core.d.ts.map +0 -1
  120. package/dist/pagination/pagination.core.js.map +0 -1
  121. package/dist/queryBuilder/index.d.ts.map +0 -1
  122. package/dist/queryBuilder/index.js.map +0 -1
  123. package/dist/queryBuilder/queryBuilder.core.d.ts.map +0 -1
  124. package/dist/queryBuilder/queryBuilder.core.js.map +0 -1
  125. package/dist/queryBuilder/queryBuilder.factory.d.ts.map +0 -1
  126. package/dist/queryBuilder/queryBuilder.factory.js.map +0 -1
  127. package/dist/queryBuilder/queryBuilder.filter.d.ts.map +0 -1
  128. package/dist/queryBuilder/queryBuilder.filter.js.map +0 -1
  129. package/dist/queryBuilder/queryBuilder.type.d.ts.map +0 -1
  130. package/dist/queryBuilder/queryBuilder.type.js.map +0 -1
  131. package/dist/relations/index.d.ts.map +0 -1
  132. package/dist/relations/index.js.map +0 -1
  133. package/dist/relations/relations.definition.d.ts.map +0 -1
  134. package/dist/relations/relations.definition.js.map +0 -1
  135. package/dist/repository/index.d.ts.map +0 -1
  136. package/dist/repository/index.js.map +0 -1
  137. package/dist/repository/repository.base.d.ts.map +0 -1
  138. package/dist/repository/repository.base.js.map +0 -1
  139. package/dist/seed/index.d.ts.map +0 -1
  140. package/dist/seed/index.js.map +0 -1
  141. package/dist/seed/seed.runner.d.ts.map +0 -1
  142. package/dist/seed/seed.runner.js.map +0 -1
  143. package/dist/transaction/index.d.ts.map +0 -1
  144. package/dist/transaction/index.js.map +0 -1
  145. package/dist/transaction/transaction.core.d.ts.map +0 -1
  146. package/dist/transaction/transaction.core.js.map +0 -1
  147. package/dist/unitOfWork/index.d.ts.map +0 -1
  148. package/dist/unitOfWork/index.js.map +0 -1
  149. package/dist/unitOfWork/unitOfWork.core.d.ts.map +0 -1
  150. package/dist/unitOfWork/unitOfWork.core.js.map +0 -1
@@ -1,10 +1,10 @@
1
- import { DatabaseError } from "@zudojs/errors";
1
+ import { DatabaseError, DatabaseOperation, ErrorCode } from "@zudojs/errors";
2
+ import { normalizeDatabaseError } from "../databaseClient/databaseClient.errors.js";
3
+ import { fnv1a64, hashLockKey, SQL_IDENTIFIER_PATTERN, } from "../migration/migration.helpers.js";
2
4
  /**
3
- * Application-level lock abstraction.
5
+ * Application-level lock abstraction for PostgreSQL.
4
6
  *
5
- * Database row locks are normally acquired inside a transaction.
6
- * This class provides safe SQL generation for PostgreSQL-compatible
7
- * databases without interpolating untrusted values into SQL.
7
+ * Locks are acquired inside a transaction and released when it ends.
8
8
  */
9
9
  export class DatabaseLockManager {
10
10
  client;
@@ -17,37 +17,37 @@ export class DatabaseLockManager {
17
17
  /**
18
18
  * Executes work inside a transaction after acquiring a PostgreSQL
19
19
  * advisory transaction lock.
20
- *
21
- * The lock is automatically released by PostgreSQL when the
22
- * transaction ends.
23
20
  */
24
21
  async withAdvisoryLock(lockKey, callback, options = {}) {
25
22
  validateLockKey(lockKey);
26
- if (typeof callback !== "function") {
27
- throw new DatabaseError("A lock callback is required.");
28
- }
23
+ validateCallback(callback);
29
24
  return this.client.transaction(async (transaction) => {
30
25
  await acquireAdvisoryLock(transaction, lockKey, options);
31
26
  return callback(transaction);
32
- });
27
+ }, resolveLockTransactionOptions(options));
33
28
  }
34
29
  /**
35
- * Acquires a row-level lock and executes work against the supplied
36
- * table and identifier.
30
+ * Acquires a row-level lock and executes work while holding it.
37
31
  *
38
- * The table name is validated as an SQL identifier before being
39
- * interpolated into the query.
32
+ * @throws {DatabaseError} when the row does not exist or was skipped
33
+ * because another transaction holds it (`skipLocked`).
40
34
  */
41
35
  async withRowLock(tableName, id, callback, options = {}) {
42
36
  validateIdentifier(tableName, "table name");
43
37
  validateId(id);
44
- if (typeof callback !== "function") {
45
- throw new DatabaseError("A lock callback is required.");
46
- }
38
+ validateCallback(callback);
47
39
  return this.client.transaction(async (transaction) => {
48
- await lockRow(transaction, tableName, id, options);
40
+ const lock = await lockRow(transaction, tableName, id, options);
41
+ if (!lock.acquired) {
42
+ throw new DatabaseError(`Row ${String(id)} in "${tableName}" is locked by another transaction.`, {
43
+ code: ErrorCode.CONFLICT,
44
+ statusCode: 409,
45
+ operation: DatabaseOperation.QUERY,
46
+ metadata: { tableName, id, mode: lock.mode ?? null, skipped: true },
47
+ });
48
+ }
49
49
  return callback(transaction);
50
- });
50
+ }, resolveLockTransactionOptions(options));
51
51
  }
52
52
  /**
53
53
  * Returns the underlying database client.
@@ -65,61 +65,75 @@ export function createLockManager(client) {
65
65
  /**
66
66
  * Acquires a PostgreSQL advisory transaction lock.
67
67
  *
68
- * The lock key is converted to a deterministic 64-bit advisory key.
68
+ * The lock key is hashed with FNV-1a to a signed 64-bit key (or, with a
69
+ * `namespace`, to a pair of signed 32-bit keys).
69
70
  */
70
71
  export async function acquireAdvisoryLock(transaction, lockKey, options = {}) {
71
72
  validateLockKey(lockKey);
72
- const key = normalizeAdvisoryKey(lockKey);
73
+ const useNamespace = options.namespace !== undefined;
74
+ if (useNamespace)
75
+ validateLockKey(options.namespace, "lock namespace");
76
+ const fn = options.noWait ? "pg_try_advisory_xact_lock" : "pg_advisory_xact_lock";
77
+ const sql = useNamespace
78
+ ? `SELECT ${fn}($1, $2) AS acquired`
79
+ : `SELECT ${fn}($1) AS acquired`;
80
+ const values = useNamespace
81
+ ? normalizeAdvisoryKeyPair(options.namespace, lockKey)
82
+ : [normalizeAdvisoryKey(lockKey)];
73
83
  try {
74
- if (options.noWait) {
75
- const acquired = await transaction.$queryRaw `
76
- SELECT pg_try_advisory_xact_lock(
77
- ${key}
78
- ) AS acquired
79
- `;
80
- if (!acquired[0]?.acquired) {
81
- throw new DatabaseError(`Database advisory lock "${lockKey}" is already held.`);
82
- }
83
- return;
84
+ await applyLockTimeout(transaction, options.timeoutMs);
85
+ const rows = await transaction.$queryRawUnsafe(sql, ...values);
86
+ if (options.noWait && rows[0]?.acquired !== true) {
87
+ throw new DatabaseError(`Database advisory lock "${lockKey}" is already held.`, {
88
+ code: ErrorCode.CONFLICT,
89
+ statusCode: 409,
90
+ operation: DatabaseOperation.QUERY,
91
+ metadata: { lockKey, namespace: options.namespace ?? null },
92
+ });
84
93
  }
85
- await transaction.$executeRaw `
86
- SELECT pg_advisory_xact_lock(
87
- ${key}
88
- )
89
- `;
94
+ return { acquired: true, lockKey };
90
95
  }
91
96
  catch (error) {
92
- if (error instanceof DatabaseError) {
93
- throw error;
94
- }
95
- throw new DatabaseError(`Failed to acquire database advisory lock "${lockKey}".`, {
96
- cause: error,
97
- metadata: {
98
- lockKey,
99
- mode: options.mode ?? "for-update",
100
- },
97
+ throw normalizeDatabaseError(error, {
98
+ operation: DatabaseOperation.QUERY,
99
+ fallbackMessage: `Failed to acquire database advisory lock "${lockKey}".`,
100
+ metadata: { lockKey, namespace: options.namespace ?? null },
101
101
  });
102
102
  }
103
103
  }
104
104
  /**
105
105
  * Acquires a row-level PostgreSQL lock.
106
+ *
107
+ * @throws {DatabaseError} when the row does not exist.
108
+ * @returns `acquired: false` only when `skipLocked` skipped a row held by
109
+ * another transaction.
106
110
  */
107
111
  export async function lockRow(transaction, tableName, id, options = {}) {
108
112
  validateIdentifier(tableName, "table name");
109
113
  validateId(id);
110
114
  const mode = options.mode ?? "for-update";
111
115
  const clause = buildLockClause(mode, options);
116
+ const lockKey = `${tableName}:${String(id)}`;
112
117
  try {
113
- await transaction.$executeRawUnsafe(`SELECT 1 FROM "${tableName}" WHERE "id" = $1 ${clause}`, id);
118
+ await applyLockTimeout(transaction, options.timeoutMs);
119
+ const rows = await transaction.$queryRawUnsafe(`SELECT 1 FROM "${tableName}" WHERE "id" = $1 ${clause}`, id);
120
+ if (rows.length > 0)
121
+ return { acquired: true, lockKey, mode };
122
+ if (options.skipLocked)
123
+ return { acquired: false, lockKey, mode };
124
+ throw new DatabaseError(`Row ${String(id)} in "${tableName}" was not found.`, {
125
+ code: ErrorCode.RESOURCE_NOT_FOUND,
126
+ statusCode: 404,
127
+ expose: true,
128
+ operation: DatabaseOperation.QUERY,
129
+ metadata: { tableName, id, mode },
130
+ });
114
131
  }
115
132
  catch (error) {
116
- throw new DatabaseError(`Failed to acquire row lock on "${tableName}".`, {
117
- cause: error,
118
- metadata: {
119
- tableName,
120
- id,
121
- mode,
122
- },
133
+ throw normalizeDatabaseError(error, {
134
+ operation: DatabaseOperation.QUERY,
135
+ fallbackMessage: `Failed to acquire row lock on "${tableName}".`,
136
+ metadata: { tableName, id, mode },
123
137
  });
124
138
  }
125
139
  }
@@ -129,17 +143,12 @@ export async function lockRow(transaction, tableName, id, options = {}) {
129
143
  export function buildLockClause(mode, options = {}) {
130
144
  const lockMode = getLockModeSql(mode);
131
145
  const modifiers = [];
132
- if (options.noWait) {
146
+ if (options.noWait)
133
147
  modifiers.push("NOWAIT");
134
- }
135
- else if (options.skipLocked) {
148
+ else if (options.skipLocked)
136
149
  modifiers.push("SKIP LOCKED");
137
- }
138
150
  return [lockMode, ...modifiers].join(" ");
139
151
  }
140
- /**
141
- * Maps the public lock mode to PostgreSQL SQL.
142
- */
143
152
  function getLockModeSql(mode) {
144
153
  switch (mode) {
145
154
  case "for-update":
@@ -155,44 +164,91 @@ function getLockModeSql(mode) {
155
164
  }
156
165
  }
157
166
  /**
158
- * Converts an application lock key into a deterministic signed
159
- * 64-bit integer represented as a bigint.
167
+ * Converts an application lock key into a deterministic signed 64-bit
168
+ * advisory key (FNV-1a 64).
160
169
  */
161
170
  export function normalizeAdvisoryKey(lockKey) {
162
171
  validateLockKey(lockKey);
163
- const bytes = new TextEncoder().encode(lockKey);
164
- let hash = 1469598103934665603n;
165
- for (const byte of bytes) {
166
- hash ^= BigInt(byte);
167
- hash = BigInt.asIntN(64, hash * 1099511628211n);
172
+ return hashLockKey(lockKey);
173
+ }
174
+ /**
175
+ * Converts a namespace and key into the two signed 32-bit integers used by
176
+ * the two-argument advisory lock functions.
177
+ */
178
+ export function normalizeAdvisoryKeyPair(namespace, lockKey) {
179
+ validateLockKey(namespace, "lock namespace");
180
+ validateLockKey(lockKey);
181
+ return [toInt32(fnv1a64(namespace)), toInt32(fnv1a64(lockKey))];
182
+ }
183
+ function toInt32(value) {
184
+ return Number(BigInt.asIntN(32, value ^ (value >> 32n)));
185
+ }
186
+ /**
187
+ * Prisma's default interactive-transaction timeout.
188
+ */
189
+ const DEFAULT_PRISMA_TRANSACTION_TIMEOUT_MS = 5_000;
190
+ /**
191
+ * Derives the transaction options for a lock so that `lock_timeout` is
192
+ * always shorter than the surrounding Prisma transaction timeout.
193
+ *
194
+ * Without this, a lock wait longer than Prisma's 5 s default would surface
195
+ * as a generic "transaction already closed" error instead of a lock
196
+ * timeout. When `transaction.timeoutMs` is not supplied it is raised to
197
+ * cover the lock wait plus the default budget for the callback.
198
+ *
199
+ * @throws {TypeError} when `transaction.timeoutMs` is explicitly shorter
200
+ * than `timeoutMs`.
201
+ */
202
+ export function resolveLockTransactionOptions(options = {}) {
203
+ const { timeoutMs, transaction } = options;
204
+ if (timeoutMs === undefined)
205
+ return transaction;
206
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
207
+ throw new TypeError("Lock timeoutMs must be a non-negative finite number.");
168
208
  }
169
- return BigInt.asIntN(64, hash);
209
+ const explicit = transaction?.timeoutMs;
210
+ if (explicit !== undefined) {
211
+ if (explicit <= timeoutMs) {
212
+ throw new TypeError(`Lock timeoutMs (${timeoutMs}) must be shorter than transaction.timeoutMs (${explicit}).`);
213
+ }
214
+ return transaction;
215
+ }
216
+ if (timeoutMs < DEFAULT_PRISMA_TRANSACTION_TIMEOUT_MS)
217
+ return transaction;
218
+ return {
219
+ ...transaction,
220
+ timeoutMs: Math.floor(timeoutMs) + DEFAULT_PRISMA_TRANSACTION_TIMEOUT_MS,
221
+ };
170
222
  }
171
223
  /**
172
- * Validates an advisory lock key.
224
+ * Applies `SET LOCAL lock_timeout` for the current transaction.
173
225
  */
174
- function validateLockKey(lockKey) {
226
+ async function applyLockTimeout(transaction, timeoutMs) {
227
+ if (timeoutMs === undefined)
228
+ return;
229
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
230
+ throw new TypeError("Lock timeoutMs must be a non-negative finite number.");
231
+ }
232
+ await transaction.$executeRawUnsafe(`SET LOCAL lock_timeout = ${Math.floor(timeoutMs)}`);
233
+ }
234
+ function validateCallback(callback) {
235
+ if (typeof callback !== "function") {
236
+ throw new TypeError("A lock callback is required.");
237
+ }
238
+ }
239
+ function validateLockKey(lockKey, name = "lock key") {
175
240
  if (typeof lockKey !== "string" || lockKey.trim().length === 0) {
176
- throw new TypeError("A non-empty database lock key is required.");
241
+ throw new TypeError(`A non-empty database ${name} is required.`);
177
242
  }
178
243
  if (lockKey.length > 255) {
179
- throw new TypeError("Database lock keys cannot exceed 255 characters.");
244
+ throw new TypeError(`Database ${name}s cannot exceed 255 characters.`);
180
245
  }
181
246
  }
182
- /**
183
- * Validates an SQL identifier.
184
- */
185
247
  function validateIdentifier(value, name) {
186
- if (typeof value !== "string" || value.length === 0) {
187
- throw new TypeError(`A valid ${name} is required.`);
188
- }
189
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
190
- throw new TypeError(`Invalid ${name}: "${value}".`);
248
+ if (typeof value !== "string" || !SQL_IDENTIFIER_PATTERN.test(value)) {
249
+ throw new TypeError(`Invalid ${name}: "${String(value)}".`);
191
250
  }
192
251
  }
193
- /**
194
- * Validates a database identifier value.
195
- */
196
252
  function validateId(id) {
197
253
  if (typeof id === "string" && id.trim().length === 0) {
198
254
  throw new TypeError("A non-empty database identifier is required.");
@@ -200,5 +256,8 @@ function validateId(id) {
200
256
  if (typeof id === "number" && !Number.isFinite(id)) {
201
257
  throw new TypeError("A finite database identifier is required.");
202
258
  }
259
+ if (typeof id !== "string" && typeof id !== "number") {
260
+ throw new TypeError("A database identifier must be a string or number.");
261
+ }
203
262
  }
204
263
  //# sourceMappingURL=locks.core.js.map
@@ -3,5 +3,7 @@
3
3
  *
4
4
  * Database migration runner with version tracking.
5
5
  */
6
- export { MigrationRunner, createMigrationRunner, normalizeMigrations, validateMigration, getLatestVersion, getCurrentVersion, DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK, type Migration, type MigrationRecord, type MigrationResult, type MigrationStatus, type MigrationRunnerOptions, } from "./migration.runner.js";
6
+ export { MigrationRunner, createMigrationRunner, normalizeMigrations, validateMigration, getLatestVersion, getCurrentVersion, DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK, type Migration, type MigrationRecord, type MigrationResult, type MigrationStatus, type MigrationRunnerOptions, type RunnerTransactionOptions, } from "./migration.runner.js";
7
+ export { SQL_IDENTIFIER_PATTERN, validateIdentifier, validateLockKey, quoteIdentifier, hashLockKey, fnv1a64, FNV1A_64_OFFSET_BASIS, FNV1A_64_PRIME, } from "./migration.helpers.js";
8
+ export { getSqlDialect, isSqlDialectName, UnsupportedDialectError, DEFAULT_SQL_DIALECT, type SqlDialect, type SqlDialectName, } from "./migration.dialect.js";
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,6 @@
4
4
  * Database migration runner with version tracking.
5
5
  */
6
6
  export { MigrationRunner, createMigrationRunner, normalizeMigrations, validateMigration, getLatestVersion, getCurrentVersion, DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK, } from "./migration.runner.js";
7
+ export { SQL_IDENTIFIER_PATTERN, validateIdentifier, validateLockKey, quoteIdentifier, hashLockKey, fnv1a64, FNV1A_64_OFFSET_BASIS, FNV1A_64_PRIME, } from "./migration.helpers.js";
8
+ export { getSqlDialect, isSqlDialectName, UnsupportedDialectError, DEFAULT_SQL_DIALECT, } from "./migration.dialect.js";
7
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,65 @@
1
+ import { DatabaseError } from "@zudojs/errors";
2
+ /**
3
+ * SQL dialects known to the migration and seed runners.
4
+ *
5
+ * Only `postgresql` is implemented today. Requesting any other dialect
6
+ * throws an {@link UnsupportedDialectError} so callers fail loudly instead
7
+ * of running PostgreSQL syntax against another engine.
8
+ */
9
+ export type SqlDialectName = "postgresql" | "mysql" | "sqlite";
10
+ /**
11
+ * Dialect strategy used to build the small amount of raw SQL the runners
12
+ * need (tracking tables, history queries, and advisory locks).
13
+ */
14
+ export interface SqlDialect {
15
+ readonly name: SqlDialectName;
16
+ /**
17
+ * Quotes a validated identifier (table or column name).
18
+ */
19
+ quoteIdentifier(identifier: string): string;
20
+ /**
21
+ * Returns the positional placeholder for the 1-based parameter index.
22
+ */
23
+ placeholder(index: number): string;
24
+ /**
25
+ * Column type names used by the tracking tables.
26
+ */
27
+ readonly types: {
28
+ readonly bigint: string;
29
+ readonly varchar255: string;
30
+ readonly timestamp: string;
31
+ };
32
+ /**
33
+ * Returns the SQL used to acquire a transaction-scoped advisory lock.
34
+ *
35
+ * The statement must accept exactly one 64-bit integer parameter.
36
+ */
37
+ advisoryTransactionLock(): string;
38
+ /**
39
+ * Default expression for the current timestamp.
40
+ */
41
+ readonly currentTimestamp: string;
42
+ }
43
+ /**
44
+ * Raised when a dialect is requested that the runners do not implement.
45
+ */
46
+ export declare class UnsupportedDialectError extends DatabaseError {
47
+ constructor(dialect: string);
48
+ }
49
+ /**
50
+ * Default dialect used by the runners.
51
+ */
52
+ export declare const DEFAULT_SQL_DIALECT: SqlDialectName;
53
+ /**
54
+ * Checks whether a value names a known dialect.
55
+ */
56
+ export declare function isSqlDialectName(value: unknown): value is SqlDialectName;
57
+ /**
58
+ * Resolves a dialect strategy by name.
59
+ *
60
+ * @throws {UnsupportedDialectError} for `mysql` and `sqlite`, which are
61
+ * recognised but not implemented.
62
+ * @throws {TypeError} for unknown dialect names.
63
+ */
64
+ export declare function getSqlDialect(name?: SqlDialectName): SqlDialect;
65
+ //# sourceMappingURL=migration.dialect.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { DatabaseError } from "@zudojs/errors";
2
+ import { validateIdentifier } from "./migration.helpers.js";
3
+ /**
4
+ * Raised when a dialect is requested that the runners do not implement.
5
+ */
6
+ export class UnsupportedDialectError extends DatabaseError {
7
+ constructor(dialect) {
8
+ super(`SQL dialect "${dialect}" is not supported by the migration and seed runners. Only "postgresql" is implemented.`, {
9
+ metadata: { dialect },
10
+ statusCode: 500,
11
+ });
12
+ this.name = "UnsupportedDialectError";
13
+ }
14
+ }
15
+ const POSTGRESQL_DIALECT = Object.freeze({
16
+ name: "postgresql",
17
+ quoteIdentifier(identifier) {
18
+ validateIdentifier(identifier, "identifier");
19
+ return `"${identifier}"`;
20
+ },
21
+ placeholder(index) {
22
+ if (!Number.isInteger(index) || index < 1) {
23
+ throw new TypeError("Placeholder index must be a positive integer.");
24
+ }
25
+ return `$${index}`;
26
+ },
27
+ types: Object.freeze({
28
+ bigint: "BIGINT",
29
+ varchar255: "VARCHAR(255)",
30
+ timestamp: "TIMESTAMP",
31
+ }),
32
+ advisoryTransactionLock() {
33
+ return "SELECT pg_advisory_xact_lock($1)";
34
+ },
35
+ currentTimestamp: "CURRENT_TIMESTAMP",
36
+ });
37
+ /**
38
+ * Default dialect used by the runners.
39
+ */
40
+ export const DEFAULT_SQL_DIALECT = "postgresql";
41
+ /**
42
+ * Checks whether a value names a known dialect.
43
+ */
44
+ export function isSqlDialectName(value) {
45
+ return value === "postgresql" || value === "mysql" || value === "sqlite";
46
+ }
47
+ /**
48
+ * Resolves a dialect strategy by name.
49
+ *
50
+ * @throws {UnsupportedDialectError} for `mysql` and `sqlite`, which are
51
+ * recognised but not implemented.
52
+ * @throws {TypeError} for unknown dialect names.
53
+ */
54
+ export function getSqlDialect(name = DEFAULT_SQL_DIALECT) {
55
+ if (name === "postgresql") {
56
+ return POSTGRESQL_DIALECT;
57
+ }
58
+ if (isSqlDialectName(name)) {
59
+ throw new UnsupportedDialectError(name);
60
+ }
61
+ throw new TypeError(`Unknown SQL dialect: "${String(name)}".`);
62
+ }
63
+ //# sourceMappingURL=migration.dialect.js.map
@@ -0,0 +1,65 @@
1
+ import type { Migration, MigrationRecord } from "./migration.types.js";
2
+ /**
3
+ * Default migration table.
4
+ */
5
+ export declare const DEFAULT_MIGRATION_TABLE = "_migrations";
6
+ /**
7
+ * Default migration advisory lock.
8
+ */
9
+ export declare const DEFAULT_MIGRATION_LOCK = "database:migrations";
10
+ /**
11
+ * Strict SQL identifier pattern shared by the runners.
12
+ */
13
+ export declare const SQL_IDENTIFIER_PATTERN: RegExp;
14
+ /**
15
+ * Normalizes and validates migrations.
16
+ */
17
+ export declare function normalizeMigrations(migrations: readonly Migration[]): readonly Migration[];
18
+ /**
19
+ * Validates one migration.
20
+ */
21
+ export declare function validateMigration(migration: Migration): void;
22
+ /**
23
+ * Returns the highest registered migration version.
24
+ */
25
+ export declare function getLatestVersion(migrations: readonly Migration[]): number;
26
+ /**
27
+ * Returns the highest applied migration version.
28
+ */
29
+ export declare function getCurrentVersion(migrations: readonly MigrationRecord[]): number;
30
+ /**
31
+ * Quotes a validated SQL identifier using double quotes.
32
+ *
33
+ * Prefer `SqlDialect.quoteIdentifier` when a dialect is available.
34
+ */
35
+ export declare function quoteIdentifier(identifier: string): string;
36
+ /**
37
+ * Validates an SQL identifier against {@link SQL_IDENTIFIER_PATTERN}.
38
+ */
39
+ export declare function validateIdentifier(identifier: string, name: string): void;
40
+ /**
41
+ * Validates an advisory lock key.
42
+ */
43
+ export declare function validateLockKey(lockKey: string, name?: string): void;
44
+ /**
45
+ * FNV-1a 64-bit offset basis (0xcbf29ce484222325).
46
+ */
47
+ export declare const FNV1A_64_OFFSET_BASIS = 14695981039346656037n;
48
+ /**
49
+ * FNV-1a 64-bit prime (0x100000001b3).
50
+ */
51
+ export declare const FNV1A_64_PRIME = 1099511628211n;
52
+ /**
53
+ * Computes the unsigned FNV-1a 64-bit hash of a string (UTF-8 bytes).
54
+ *
55
+ * Matches FNV-1a implementations in other languages, so lock keys can be
56
+ * shared with services outside this package.
57
+ */
58
+ export declare function fnv1a64(value: string): bigint;
59
+ /**
60
+ * Creates a deterministic signed 64-bit advisory lock key (FNV-1a).
61
+ *
62
+ * Suitable as the single `bigint` argument of `pg_advisory_xact_lock`.
63
+ */
64
+ export declare function hashLockKey(value: string): bigint;
65
+ //# sourceMappingURL=migration.helpers.d.ts.map
@@ -6,6 +6,10 @@ export const DEFAULT_MIGRATION_TABLE = "_migrations";
6
6
  * Default migration advisory lock.
7
7
  */
8
8
  export const DEFAULT_MIGRATION_LOCK = "database:migrations";
9
+ /**
10
+ * Strict SQL identifier pattern shared by the runners.
11
+ */
12
+ export const SQL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
13
  /**
10
14
  * Normalizes and validates migrations.
11
15
  */
@@ -38,6 +42,9 @@ export function validateMigration(migration) {
38
42
  if (!Number.isInteger(migration.version) || migration.version <= 0) {
39
43
  throw new TypeError("Migration version must be a positive integer.");
40
44
  }
45
+ if (migration.version > Number.MAX_SAFE_INTEGER) {
46
+ throw new TypeError(`Migration version ${migration.version} exceeds Number.MAX_SAFE_INTEGER.`);
47
+ }
41
48
  if (typeof migration.name !== "string" ||
42
49
  migration.name.trim().length === 0) {
43
50
  throw new TypeError("Migration name is required.");
@@ -53,47 +60,82 @@ export function validateMigration(migration) {
53
60
  }
54
61
  }
55
62
  /**
56
- * Returns the latest registered migration version.
63
+ * Returns the highest registered migration version.
57
64
  */
58
65
  export function getLatestVersion(migrations) {
59
- if (migrations.length === 0)
60
- return 0;
61
- return migrations[migrations.length - 1].version;
66
+ let latest = 0;
67
+ for (const migration of migrations) {
68
+ if (migration.version > latest)
69
+ latest = migration.version;
70
+ }
71
+ return latest;
62
72
  }
63
73
  /**
64
- * Returns the current applied migration version.
74
+ * Returns the highest applied migration version.
65
75
  */
66
76
  export function getCurrentVersion(migrations) {
67
- if (migrations.length === 0)
68
- return 0;
69
- return migrations[migrations.length - 1].version;
77
+ let current = 0;
78
+ for (const record of migrations) {
79
+ if (record.version > current)
80
+ current = record.version;
81
+ }
82
+ return current;
70
83
  }
71
84
  /**
72
- * Quotes a validated SQL identifier.
85
+ * Quotes a validated SQL identifier using double quotes.
86
+ *
87
+ * Prefer `SqlDialect.quoteIdentifier` when a dialect is available.
73
88
  */
74
89
  export function quoteIdentifier(identifier) {
75
90
  validateIdentifier(identifier, "identifier");
76
91
  return `"${identifier}"`;
77
92
  }
78
93
  /**
79
- * Validates an SQL identifier.
94
+ * Validates an SQL identifier against {@link SQL_IDENTIFIER_PATTERN}.
80
95
  */
81
96
  export function validateIdentifier(identifier, name) {
82
- if (typeof identifier !== "string" ||
83
- !/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
84
- throw new TypeError(`Invalid ${name}: "${identifier}".`);
97
+ if (typeof identifier !== "string" || !SQL_IDENTIFIER_PATTERN.test(identifier)) {
98
+ throw new TypeError(`Invalid ${name}: "${String(identifier)}".`);
85
99
  }
86
100
  }
87
101
  /**
88
- * Creates a deterministic signed 64-bit advisory lock key.
102
+ * Validates an advisory lock key.
89
103
  */
90
- export function hashLockKey(value) {
91
- let hash = 1469598103934665603n;
104
+ export function validateLockKey(lockKey, name = "lock key") {
105
+ if (typeof lockKey !== "string" || lockKey.trim().length === 0) {
106
+ throw new TypeError(`A database ${name} is required.`);
107
+ }
108
+ }
109
+ /**
110
+ * FNV-1a 64-bit offset basis (0xcbf29ce484222325).
111
+ */
112
+ export const FNV1A_64_OFFSET_BASIS = 14695981039346656037n;
113
+ /**
114
+ * FNV-1a 64-bit prime (0x100000001b3).
115
+ */
116
+ export const FNV1A_64_PRIME = 1099511628211n;
117
+ const UINT64_MASK = (1n << 64n) - 1n;
118
+ /**
119
+ * Computes the unsigned FNV-1a 64-bit hash of a string (UTF-8 bytes).
120
+ *
121
+ * Matches FNV-1a implementations in other languages, so lock keys can be
122
+ * shared with services outside this package.
123
+ */
124
+ export function fnv1a64(value) {
125
+ let hash = FNV1A_64_OFFSET_BASIS;
92
126
  const bytes = new TextEncoder().encode(value);
93
127
  for (const byte of bytes) {
94
128
  hash ^= BigInt(byte);
95
- hash = BigInt.asIntN(64, hash * 1099511628211n);
129
+ hash = (hash * FNV1A_64_PRIME) & UINT64_MASK;
96
130
  }
97
- return BigInt.asIntN(64, hash);
131
+ return hash;
132
+ }
133
+ /**
134
+ * Creates a deterministic signed 64-bit advisory lock key (FNV-1a).
135
+ *
136
+ * Suitable as the single `bigint` argument of `pg_advisory_xact_lock`.
137
+ */
138
+ export function hashLockKey(value) {
139
+ return BigInt.asIntN(64, fnv1a64(value));
98
140
  }
99
141
  //# sourceMappingURL=migration.helpers.js.map