@zudojs/database 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -17
- package/dist/cache/cache.memory.d.ts +167 -0
- package/dist/cache/cache.memory.js +186 -40
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -2
- package/dist/database/database.core.d.ts +91 -0
- package/dist/database/database.core.js +150 -0
- package/dist/databaseClient/databaseClient.core.d.ts +165 -14
- package/dist/databaseClient/databaseClient.core.js +304 -70
- package/dist/databaseClient/databaseClient.errors.d.ts +112 -0
- package/dist/databaseClient/databaseClient.errors.js +425 -0
- package/dist/databaseClient/index.d.ts +2 -1
- package/dist/databaseClient/index.js +2 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts +163 -0
- package/dist/databaseConnection/databaseConnection.manager.js +138 -76
- package/dist/databaseConnection/index.d.ts +1 -1
- package/dist/databaseType/databaseType.type.d.ts +224 -0
- package/dist/databaseType/databaseType.type.js +10 -0
- package/dist/health/health.check.d.ts +74 -0
- package/dist/health/health.check.js +78 -47
- package/dist/health/index.d.ts +1 -1
- package/dist/health/index.js +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.js +11 -11
- package/dist/locks/index.d.ts +2 -2
- package/dist/locks/index.js +2 -2
- package/dist/locks/locks.core.d.ts +128 -0
- package/dist/locks/locks.core.js +144 -85
- package/dist/migration/index.d.ts +3 -1
- package/dist/migration/index.js +2 -0
- package/dist/migration/migration.dialect.d.ts +65 -0
- package/dist/migration/migration.dialect.js +63 -0
- package/dist/migration/migration.helpers.d.ts +65 -0
- package/dist/migration/migration.helpers.js +141 -0
- package/dist/migration/migration.runner.d.ts +75 -0
- package/dist/migration/migration.runner.js +195 -107
- package/dist/migration/migration.types.d.ts +85 -0
- package/dist/migration/migration.types.js +2 -0
- package/dist/pagination/index.d.ts +2 -1
- package/dist/pagination/index.js +2 -1
- package/dist/pagination/pagination.core.d.ts +163 -0
- package/dist/pagination/pagination.core.js +279 -0
- package/dist/pagination/pagination.keyset.d.ts +55 -0
- package/dist/pagination/pagination.keyset.js +108 -0
- package/dist/queryBuilder/index.d.ts +4 -3
- package/dist/queryBuilder/index.js +3 -2
- package/dist/queryBuilder/queryBuilder.core.d.ts +178 -0
- package/dist/queryBuilder/queryBuilder.core.js +420 -0
- package/dist/queryBuilder/queryBuilder.factory.d.ts +6 -0
- package/dist/queryBuilder/queryBuilder.factory.js +8 -0
- package/dist/queryBuilder/queryBuilder.filter.d.ts +170 -0
- package/dist/queryBuilder/queryBuilder.filter.js +497 -0
- package/dist/queryBuilder/queryBuilder.prisma.d.ts +60 -0
- package/dist/queryBuilder/queryBuilder.prisma.js +233 -0
- package/dist/queryBuilder/queryBuilder.type.d.ts +48 -0
- package/dist/queryBuilder/queryBuilder.type.js +2 -0
- package/dist/relations/index.d.ts +1 -1
- package/dist/relations/index.js +1 -1
- package/dist/relations/relations.definition.d.ts +184 -0
- package/dist/relations/relations.definition.js +362 -0
- package/dist/repository/index.d.ts +2 -1
- package/dist/repository/index.js +1 -0
- package/dist/repository/repository.base.d.ts +270 -0
- package/dist/repository/repository.base.js +350 -84
- package/dist/repository/repository.errors.d.ts +60 -0
- package/dist/repository/repository.errors.js +235 -0
- package/dist/seed/index.d.ts +2 -2
- package/dist/seed/index.js +1 -1
- package/dist/seed/seed.runner.d.ts +160 -0
- package/dist/seed/seed.runner.js +194 -225
- package/dist/transaction/index.d.ts +1 -1
- package/dist/transaction/index.js +1 -1
- package/dist/transaction/transaction.core.d.ts +117 -0
- package/dist/transaction/transaction.core.js +83 -56
- package/dist/unitOfWork/unitOfWork.core.d.ts +44 -0
- package/dist/unitOfWork/unitOfWork.core.js +9 -20
- package/package.json +35 -19
- package/dist/cache/cache.memory.d.ts.map +0 -1
- package/dist/cache/cache.memory.js.map +0 -1
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/index.js.map +0 -1
- package/dist/database/database.core.d.ts.map +0 -1
- package/dist/database/index.d.ts.map +0 -1
- package/dist/database/index.js.map +0 -1
- package/dist/databaseClient/databaseClient.core.d.ts.map +0 -1
- package/dist/databaseClient/databaseClient.core.js.map +0 -1
- package/dist/databaseClient/databaseClient.logger.d.ts.map +0 -1
- package/dist/databaseClient/databaseClient.logger.js.map +0 -1
- package/dist/databaseClient/index.d.ts.map +0 -1
- package/dist/databaseClient/index.js.map +0 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts.map +0 -1
- package/dist/databaseConnection/databaseConnection.manager.js.map +0 -1
- package/dist/databaseConnection/index.d.ts.map +0 -1
- package/dist/databaseConnection/index.js.map +0 -1
- package/dist/databaseType/index.d.ts.map +0 -1
- package/dist/databaseType/index.js.map +0 -1
- package/dist/health/health.check.d.ts.map +0 -1
- package/dist/health/health.check.js.map +0 -1
- package/dist/health/index.d.ts.map +0 -1
- package/dist/health/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/locks/index.d.ts.map +0 -1
- package/dist/locks/index.js.map +0 -1
- package/dist/locks/locks.core.d.ts.map +0 -1
- package/dist/locks/locks.core.js.map +0 -1
- package/dist/migration/index.d.ts.map +0 -1
- package/dist/migration/index.js.map +0 -1
- package/dist/migration/migration.helpers.d.ts.map +0 -1
- package/dist/migration/migration.runner.d.ts.map +0 -1
- package/dist/migration/migration.runner.js.map +0 -1
- package/dist/migration/migration.types.d.ts.map +0 -1
- package/dist/pagination/index.d.ts.map +0 -1
- package/dist/pagination/index.js.map +0 -1
- package/dist/queryBuilder/index.d.ts.map +0 -1
- package/dist/queryBuilder/index.js.map +0 -1
- package/dist/relations/index.d.ts.map +0 -1
- package/dist/relations/index.js.map +0 -1
- package/dist/repository/index.d.ts.map +0 -1
- package/dist/repository/index.js.map +0 -1
- package/dist/repository/repository.base.d.ts.map +0 -1
- package/dist/repository/repository.base.js.map +0 -1
- package/dist/seed/index.d.ts.map +0 -1
- package/dist/seed/index.js.map +0 -1
- package/dist/seed/seed.runner.d.ts.map +0 -1
- package/dist/seed/seed.runner.js.map +0 -1
- package/dist/transaction/index.d.ts.map +0 -1
- package/dist/transaction/index.js.map +0 -1
- package/dist/transaction/transaction.core.d.ts.map +0 -1
- package/dist/transaction/transaction.core.js.map +0 -1
- package/dist/unitOfWork/index.d.ts.map +0 -1
- package/dist/unitOfWork/index.js.map +0 -1
- package/dist/unitOfWork/unitOfWork.core.d.ts.map +0 -1
- package/dist/unitOfWork/unitOfWork.core.js.map +0 -1
package/dist/locks/locks.core.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|
36
|
-
* table and identifier.
|
|
30
|
+
* Acquires a row-level lock and executes work while holding it.
|
|
37
31
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
${
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
SELECT pg_advisory_xact_lock(
|
|
87
|
-
${key}
|
|
88
|
-
)
|
|
89
|
-
`;
|
|
94
|
+
return { acquired: true, lockKey };
|
|
90
95
|
}
|
|
91
96
|
catch (error) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
224
|
+
* Applies `SET LOCAL lock_timeout` for the current transaction.
|
|
173
225
|
*/
|
|
174
|
-
function
|
|
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(
|
|
241
|
+
throw new TypeError(`A non-empty database ${name} is required.`);
|
|
177
242
|
}
|
|
178
243
|
if (lockKey.length > 255) {
|
|
179
|
-
throw new TypeError(
|
|
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
|
|
187
|
-
throw new TypeError(`
|
|
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
|
package/dist/migration/index.js
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default migration table.
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_MIGRATION_TABLE = "_migrations";
|
|
5
|
+
/**
|
|
6
|
+
* Default migration advisory lock.
|
|
7
|
+
*/
|
|
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_]*$/;
|
|
13
|
+
/**
|
|
14
|
+
* Normalizes and validates migrations.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeMigrations(migrations) {
|
|
17
|
+
if (!Array.isArray(migrations)) {
|
|
18
|
+
throw new TypeError("Migrations must be an array.");
|
|
19
|
+
}
|
|
20
|
+
const normalized = migrations
|
|
21
|
+
.map((migration) => {
|
|
22
|
+
validateMigration(migration);
|
|
23
|
+
return Object.freeze({ ...migration });
|
|
24
|
+
})
|
|
25
|
+
.sort((first, second) => first.version - second.version);
|
|
26
|
+
for (let index = 1; index < normalized.length; index += 1) {
|
|
27
|
+
const previous = normalized[index - 1];
|
|
28
|
+
const current = normalized[index];
|
|
29
|
+
if (previous && current && previous.version === current.version) {
|
|
30
|
+
throw new TypeError(`Duplicate migration version: ${current.version}.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze(normalized);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validates one migration.
|
|
37
|
+
*/
|
|
38
|
+
export function validateMigration(migration) {
|
|
39
|
+
if (!migration || typeof migration !== "object") {
|
|
40
|
+
throw new TypeError("A migration definition is required.");
|
|
41
|
+
}
|
|
42
|
+
if (!Number.isInteger(migration.version) || migration.version <= 0) {
|
|
43
|
+
throw new TypeError("Migration version must be a positive integer.");
|
|
44
|
+
}
|
|
45
|
+
if (migration.version > Number.MAX_SAFE_INTEGER) {
|
|
46
|
+
throw new TypeError(`Migration version ${migration.version} exceeds Number.MAX_SAFE_INTEGER.`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof migration.name !== "string" ||
|
|
49
|
+
migration.name.trim().length === 0) {
|
|
50
|
+
throw new TypeError("Migration name is required.");
|
|
51
|
+
}
|
|
52
|
+
if (migration.name.length > 255) {
|
|
53
|
+
throw new TypeError("Migration name cannot exceed 255 characters.");
|
|
54
|
+
}
|
|
55
|
+
if (typeof migration.up !== "function") {
|
|
56
|
+
throw new TypeError(`Migration "${migration.name}" requires an up function.`);
|
|
57
|
+
}
|
|
58
|
+
if (migration.down !== undefined && typeof migration.down !== "function") {
|
|
59
|
+
throw new TypeError(`Migration "${migration.name}" has an invalid down function.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns the highest registered migration version.
|
|
64
|
+
*/
|
|
65
|
+
export function getLatestVersion(migrations) {
|
|
66
|
+
let latest = 0;
|
|
67
|
+
for (const migration of migrations) {
|
|
68
|
+
if (migration.version > latest)
|
|
69
|
+
latest = migration.version;
|
|
70
|
+
}
|
|
71
|
+
return latest;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns the highest applied migration version.
|
|
75
|
+
*/
|
|
76
|
+
export function getCurrentVersion(migrations) {
|
|
77
|
+
let current = 0;
|
|
78
|
+
for (const record of migrations) {
|
|
79
|
+
if (record.version > current)
|
|
80
|
+
current = record.version;
|
|
81
|
+
}
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Quotes a validated SQL identifier using double quotes.
|
|
86
|
+
*
|
|
87
|
+
* Prefer `SqlDialect.quoteIdentifier` when a dialect is available.
|
|
88
|
+
*/
|
|
89
|
+
export function quoteIdentifier(identifier) {
|
|
90
|
+
validateIdentifier(identifier, "identifier");
|
|
91
|
+
return `"${identifier}"`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Validates an SQL identifier against {@link SQL_IDENTIFIER_PATTERN}.
|
|
95
|
+
*/
|
|
96
|
+
export function validateIdentifier(identifier, name) {
|
|
97
|
+
if (typeof identifier !== "string" || !SQL_IDENTIFIER_PATTERN.test(identifier)) {
|
|
98
|
+
throw new TypeError(`Invalid ${name}: "${String(identifier)}".`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Validates an advisory lock key.
|
|
103
|
+
*/
|
|
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;
|
|
126
|
+
const bytes = new TextEncoder().encode(value);
|
|
127
|
+
for (const byte of bytes) {
|
|
128
|
+
hash ^= BigInt(byte);
|
|
129
|
+
hash = (hash * FNV1A_64_PRIME) & UINT64_MASK;
|
|
130
|
+
}
|
|
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));
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=migration.helpers.js.map
|