@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { DatabaseClient, DatabaseTransactionContext } from "../databaseClient/databaseClient.core.js";
|
|
2
|
+
import type { Migration, MigrationRecord, MigrationResult, MigrationRunnerOptions, MigrationStatus } from "./migration.types.js";
|
|
3
|
+
export * from "./migration.types.js";
|
|
4
|
+
export { DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK, normalizeMigrations, getCurrentVersion, getLatestVersion, validateMigration, } from "./migration.helpers.js";
|
|
5
|
+
/**
|
|
6
|
+
* Runs and tracks database migrations.
|
|
7
|
+
*
|
|
8
|
+
* Every entry point re-reads the applied history *inside* the advisory
|
|
9
|
+
* lock before deciding what to execute, so two runners started together
|
|
10
|
+
* never apply or revert the same migration twice.
|
|
11
|
+
*/
|
|
12
|
+
export declare class MigrationRunner {
|
|
13
|
+
private readonly client;
|
|
14
|
+
private readonly migrations;
|
|
15
|
+
private readonly tableName;
|
|
16
|
+
private readonly lockKey;
|
|
17
|
+
private readonly dialect;
|
|
18
|
+
private readonly transactionOptions;
|
|
19
|
+
private readonly perItemTransaction;
|
|
20
|
+
constructor(client: DatabaseClient, migrations: readonly Migration[], options?: MigrationRunnerOptions);
|
|
21
|
+
/**
|
|
22
|
+
* Returns the migration status without executing anything.
|
|
23
|
+
*/
|
|
24
|
+
status(): Promise<MigrationStatus>;
|
|
25
|
+
/**
|
|
26
|
+
* Applies every pending migration in version order.
|
|
27
|
+
*/
|
|
28
|
+
migrate(): Promise<MigrationResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Alias of {@link migrate}.
|
|
31
|
+
*/
|
|
32
|
+
run(): Promise<MigrationResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Reverts the most recently applied migration.
|
|
35
|
+
*
|
|
36
|
+
* With a `steps` argument, reverts up to that many migrations (newest
|
|
37
|
+
* first) and returns the reverted records in rollback order.
|
|
38
|
+
*/
|
|
39
|
+
rollback(): Promise<MigrationRecord | null>;
|
|
40
|
+
rollback(steps: number): Promise<readonly MigrationRecord[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Reverts every applied migration, newest first.
|
|
43
|
+
*/
|
|
44
|
+
rollbackAll(): Promise<readonly MigrationRecord[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Creates the tracking table if it does not exist.
|
|
47
|
+
*/
|
|
48
|
+
ensureMigrationTable(): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Returns the applied migrations ordered by version.
|
|
51
|
+
*
|
|
52
|
+
* Pass the transaction context to read inside a running transaction
|
|
53
|
+
* (for example after acquiring the advisory lock).
|
|
54
|
+
*/
|
|
55
|
+
getAppliedMigrations(transaction?: DatabaseTransactionContext): Promise<readonly MigrationRecord[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Alias of {@link getAppliedMigrations}.
|
|
58
|
+
*/
|
|
59
|
+
getHistory(): Promise<readonly MigrationRecord[]>;
|
|
60
|
+
private rollbackSteps;
|
|
61
|
+
private revertRecord;
|
|
62
|
+
private computePending;
|
|
63
|
+
private buildTransactionOptions;
|
|
64
|
+
private executor;
|
|
65
|
+
private executeMigration;
|
|
66
|
+
private recordMigration;
|
|
67
|
+
private deleteMigrationRecord;
|
|
68
|
+
private acquireMigrationLock;
|
|
69
|
+
private migrationError;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a migration runner.
|
|
73
|
+
*/
|
|
74
|
+
export declare function createMigrationRunner(client: DatabaseClient, migrations: readonly Migration[], options?: MigrationRunnerOptions): MigrationRunner;
|
|
75
|
+
//# sourceMappingURL=migration.runner.d.ts.map
|
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
import { DatabaseError } from "@zudojs/errors";
|
|
2
|
-
import {
|
|
3
|
-
import { DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK, normalizeMigrations, getCurrentVersion, getLatestVersion,
|
|
1
|
+
import { DatabaseError, DatabaseOperation, } from "@zudojs/errors";
|
|
2
|
+
import { isDatabaseErrorLike } from "../databaseClient/databaseClient.errors.js";
|
|
3
|
+
import { DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK, normalizeMigrations, getCurrentVersion, getLatestVersion, hashLockKey, validateIdentifier, validateLockKey, } from "./migration.helpers.js";
|
|
4
|
+
import { getSqlDialect } from "./migration.dialect.js";
|
|
4
5
|
export * from "./migration.types.js";
|
|
5
6
|
export { DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK, normalizeMigrations, getCurrentVersion, getLatestVersion, validateMigration, } from "./migration.helpers.js";
|
|
6
7
|
/**
|
|
7
8
|
* Runs and tracks database migrations.
|
|
9
|
+
*
|
|
10
|
+
* Every entry point re-reads the applied history *inside* the advisory
|
|
11
|
+
* lock before deciding what to execute, so two runners started together
|
|
12
|
+
* never apply or revert the same migration twice.
|
|
8
13
|
*/
|
|
9
14
|
export class MigrationRunner {
|
|
10
15
|
client;
|
|
11
16
|
migrations;
|
|
12
17
|
tableName;
|
|
13
18
|
lockKey;
|
|
19
|
+
dialect;
|
|
20
|
+
transactionOptions;
|
|
21
|
+
perItemTransaction;
|
|
14
22
|
constructor(client, migrations, options = {}) {
|
|
15
23
|
if (!client)
|
|
16
24
|
throw new TypeError("A database client is required.");
|
|
@@ -18,122 +26,120 @@ export class MigrationRunner {
|
|
|
18
26
|
this.migrations = normalizeMigrations(migrations);
|
|
19
27
|
this.tableName = options.tableName ?? DEFAULT_MIGRATION_TABLE;
|
|
20
28
|
this.lockKey = options.lockKey ?? DEFAULT_MIGRATION_LOCK;
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const validateIdentifier = (id, name) => {
|
|
25
|
-
if (typeof id !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) {
|
|
26
|
-
throw new TypeError(`Invalid ${name}: "${id}".`);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
+
this.dialect = getSqlDialect(options.dialect);
|
|
30
|
+
this.transactionOptions = { ...options.transaction };
|
|
31
|
+
this.perItemTransaction = options.perItemTransaction ?? true;
|
|
29
32
|
validateIdentifier(this.tableName, "migration table name");
|
|
33
|
+
validateLockKey(this.lockKey, "migration lock key");
|
|
30
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns the migration status without executing anything.
|
|
37
|
+
*/
|
|
31
38
|
async status() {
|
|
32
39
|
await this.ensureMigrationTable();
|
|
33
40
|
const applied = await this.getAppliedMigrations();
|
|
34
|
-
const appliedVersions = new Set(applied.map((m) => m.version));
|
|
35
|
-
const pending = this.migrations.filter((m) => !appliedVersions.has(m.version));
|
|
36
41
|
return {
|
|
37
42
|
currentVersion: getCurrentVersion(applied),
|
|
38
43
|
latestVersion: getLatestVersion(this.migrations),
|
|
39
|
-
pending,
|
|
44
|
+
pending: this.computePending(applied),
|
|
40
45
|
applied,
|
|
41
46
|
};
|
|
42
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Applies every pending migration in version order.
|
|
50
|
+
*/
|
|
43
51
|
async migrate() {
|
|
44
52
|
await this.ensureMigrationTable();
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const pending = this.migrations.filter((m) => !appliedVersions.has(m.version));
|
|
53
|
+
const before = await this.getAppliedMigrations();
|
|
54
|
+
const pending = this.computePending(before);
|
|
48
55
|
if (pending.length === 0)
|
|
49
|
-
return { applied: [], skipped:
|
|
56
|
+
return { applied: [], skipped: before };
|
|
50
57
|
const newlyApplied = [];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const skipped = [...before];
|
|
59
|
+
if (!this.perItemTransaction) {
|
|
60
|
+
await this.client.transaction(async (transaction) => {
|
|
61
|
+
await this.acquireMigrationLock(transaction);
|
|
62
|
+
const applied = await this.getAppliedMigrations(transaction);
|
|
63
|
+
skipped.push(...applied.filter((r) => !hasVersion(before, r.version)));
|
|
64
|
+
for (const migration of this.computePending(applied)) {
|
|
65
|
+
await this.executeMigration(transaction, migration);
|
|
66
|
+
newlyApplied.push(await this.recordMigration(transaction, migration));
|
|
67
|
+
}
|
|
68
|
+
}, this.buildTransactionOptions());
|
|
69
|
+
return { applied: newlyApplied, skipped };
|
|
70
|
+
}
|
|
71
|
+
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 };
|
|
54
78
|
await this.executeMigration(transaction, migration);
|
|
55
79
|
const record = await this.recordMigration(transaction, migration);
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
return { record, skipped: false };
|
|
81
|
+
}, this.buildTransactionOptions());
|
|
82
|
+
if (outcome.skipped)
|
|
83
|
+
skipped.push(outcome.record);
|
|
84
|
+
else
|
|
85
|
+
newlyApplied.push(outcome.record);
|
|
86
|
+
}
|
|
87
|
+
return { applied: newlyApplied, skipped };
|
|
60
88
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
throw new DatabaseError(`Migration "${latest.name}" is recorded as applied but is not registered.`, {
|
|
72
|
-
metadata: { version: latest.version },
|
|
73
|
-
});
|
|
89
|
+
/**
|
|
90
|
+
* Alias of {@link migrate}.
|
|
91
|
+
*/
|
|
92
|
+
run() {
|
|
93
|
+
return this.migrate();
|
|
94
|
+
}
|
|
95
|
+
async rollback(steps) {
|
|
96
|
+
if (steps === undefined) {
|
|
97
|
+
const [record] = await this.rollbackSteps(1);
|
|
98
|
+
return record ?? null;
|
|
74
99
|
}
|
|
75
|
-
if (!
|
|
76
|
-
throw new
|
|
77
|
-
metadata: { version: migration.version },
|
|
78
|
-
});
|
|
100
|
+
if (!Number.isInteger(steps) || steps <= 0) {
|
|
101
|
+
throw new TypeError("Rollback steps must be a positive integer.");
|
|
79
102
|
}
|
|
80
|
-
|
|
81
|
-
await this.acquireMigrationLock(transaction);
|
|
82
|
-
await migration.down(transaction);
|
|
83
|
-
await this.deleteMigrationRecord(transaction, migration.version);
|
|
84
|
-
});
|
|
85
|
-
return latest;
|
|
103
|
+
return this.rollbackSteps(steps);
|
|
86
104
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const rolledBack = [];
|
|
93
|
-
await this.client.transaction(async (transaction) => {
|
|
94
|
-
await this.acquireMigrationLock(transaction);
|
|
95
|
-
for (let index = applied.length - 1; index >= 0; index -= 1) {
|
|
96
|
-
const record = applied[index];
|
|
97
|
-
if (!record)
|
|
98
|
-
continue;
|
|
99
|
-
const migration = this.migrations.find((c) => c.version === record.version);
|
|
100
|
-
if (!migration) {
|
|
101
|
-
throw new DatabaseError(`Migration "${record.name}" is not registered.`, {
|
|
102
|
-
metadata: { version: record.version },
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
if (!migration.down) {
|
|
106
|
-
throw new DatabaseError(`Migration "${migration.name}" does not define a rollback operation.`, {
|
|
107
|
-
metadata: { version: migration.version },
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
await migration.down(transaction);
|
|
111
|
-
await this.deleteMigrationRecord(transaction, migration.version);
|
|
112
|
-
rolledBack.push(record);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
return rolledBack;
|
|
105
|
+
/**
|
|
106
|
+
* Reverts every applied migration, newest first.
|
|
107
|
+
*/
|
|
108
|
+
rollbackAll() {
|
|
109
|
+
return this.rollbackSteps(Number.POSITIVE_INFINITY);
|
|
116
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Creates the tracking table if it does not exist.
|
|
113
|
+
*/
|
|
117
114
|
async ensureMigrationTable() {
|
|
118
|
-
const
|
|
115
|
+
const q = (id) => this.dialect.quoteIdentifier(id);
|
|
116
|
+
const { bigint, varchar255, timestamp } = this.dialect.types;
|
|
117
|
+
const ddl = `CREATE TABLE IF NOT EXISTS ${q(this.tableName)} (` +
|
|
118
|
+
`${q("version")} ${bigint} PRIMARY KEY, ` +
|
|
119
|
+
`${q("name")} ${varchar255} NOT NULL, ` +
|
|
120
|
+
`${q("applied_at")} ${timestamp} NOT NULL DEFAULT ${this.dialect.currentTimestamp})`;
|
|
119
121
|
try {
|
|
120
|
-
await this.client.
|
|
121
|
-
"version" INTEGER PRIMARY KEY,
|
|
122
|
-
"name" VARCHAR(255) NOT NULL,
|
|
123
|
-
"applied_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
124
|
-
)`);
|
|
122
|
+
await this.client.executeRawUnsafe(ddl);
|
|
125
123
|
}
|
|
126
124
|
catch (error) {
|
|
127
|
-
throw
|
|
125
|
+
throw this.migrationError("Failed to initialize the migration table.", {
|
|
128
126
|
cause: error,
|
|
129
127
|
metadata: { tableName: this.tableName },
|
|
130
128
|
});
|
|
131
129
|
}
|
|
132
130
|
}
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Returns the applied migrations ordered by version.
|
|
133
|
+
*
|
|
134
|
+
* Pass the transaction context to read inside a running transaction
|
|
135
|
+
* (for example after acquiring the advisory lock).
|
|
136
|
+
*/
|
|
137
|
+
async getAppliedMigrations(transaction) {
|
|
138
|
+
const q = (id) => this.dialect.quoteIdentifier(id);
|
|
139
|
+
const sql = `SELECT ${q("version")}, ${q("name")}, ${q("applied_at")} ` +
|
|
140
|
+
`FROM ${q(this.tableName)} ORDER BY ${q("version")} ASC`;
|
|
135
141
|
try {
|
|
136
|
-
const rows = await this.
|
|
142
|
+
const rows = await this.executor(transaction).query(sql);
|
|
137
143
|
return rows.map((row) => ({
|
|
138
144
|
version: Number(row.version),
|
|
139
145
|
name: row.name,
|
|
@@ -141,27 +147,105 @@ export class MigrationRunner {
|
|
|
141
147
|
}));
|
|
142
148
|
}
|
|
143
149
|
catch (error) {
|
|
144
|
-
throw
|
|
150
|
+
throw this.migrationError("Failed to read migration history.", {
|
|
145
151
|
cause: error,
|
|
146
152
|
metadata: { tableName: this.tableName },
|
|
147
153
|
});
|
|
148
154
|
}
|
|
149
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Alias of {@link getAppliedMigrations}.
|
|
158
|
+
*/
|
|
159
|
+
getHistory() {
|
|
160
|
+
return this.getAppliedMigrations();
|
|
161
|
+
}
|
|
162
|
+
async rollbackSteps(limit) {
|
|
163
|
+
await this.ensureMigrationTable();
|
|
164
|
+
const rolledBack = [];
|
|
165
|
+
if (!this.perItemTransaction) {
|
|
166
|
+
await this.client.transaction(async (transaction) => {
|
|
167
|
+
await this.acquireMigrationLock(transaction);
|
|
168
|
+
const applied = [...(await this.getAppliedMigrations(transaction))];
|
|
169
|
+
while (applied.length > 0 && rolledBack.length < limit) {
|
|
170
|
+
const record = applied.pop();
|
|
171
|
+
await this.revertRecord(transaction, record);
|
|
172
|
+
rolledBack.push(record);
|
|
173
|
+
}
|
|
174
|
+
}, this.buildTransactionOptions());
|
|
175
|
+
return rolledBack;
|
|
176
|
+
}
|
|
177
|
+
while (rolledBack.length < limit) {
|
|
178
|
+
const record = await this.client.transaction(async (transaction) => {
|
|
179
|
+
await this.acquireMigrationLock(transaction);
|
|
180
|
+
const applied = await this.getAppliedMigrations(transaction);
|
|
181
|
+
const latest = applied[applied.length - 1];
|
|
182
|
+
if (!latest)
|
|
183
|
+
return null;
|
|
184
|
+
await this.revertRecord(transaction, latest);
|
|
185
|
+
return latest;
|
|
186
|
+
}, this.buildTransactionOptions());
|
|
187
|
+
if (!record)
|
|
188
|
+
break;
|
|
189
|
+
rolledBack.push(record);
|
|
190
|
+
}
|
|
191
|
+
return rolledBack;
|
|
192
|
+
}
|
|
193
|
+
async revertRecord(transaction, record) {
|
|
194
|
+
const migration = this.migrations.find((m) => m.version === record.version);
|
|
195
|
+
if (!migration) {
|
|
196
|
+
throw this.migrationError(`Migration "${record.name}" is recorded as applied but is not registered.`, { metadata: { version: record.version } });
|
|
197
|
+
}
|
|
198
|
+
if (!migration.down) {
|
|
199
|
+
throw this.migrationError(`Migration "${migration.name}" does not define a rollback operation.`, { metadata: { version: migration.version } });
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
await migration.down(transaction);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw this.migrationError(`Rollback of migration "${migration.name}" failed.`, {
|
|
206
|
+
cause: error,
|
|
207
|
+
metadata: { version: migration.version, name: migration.name },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
await this.deleteMigrationRecord(transaction, migration.version);
|
|
211
|
+
}
|
|
212
|
+
computePending(applied) {
|
|
213
|
+
const versions = new Set(applied.map((record) => record.version));
|
|
214
|
+
return this.migrations.filter((m) => !versions.has(m.version));
|
|
215
|
+
}
|
|
216
|
+
buildTransactionOptions() {
|
|
217
|
+
return { ...this.transactionOptions };
|
|
218
|
+
}
|
|
219
|
+
executor(transaction) {
|
|
220
|
+
if (transaction) {
|
|
221
|
+
return {
|
|
222
|
+
query: (sql, ...values) => transaction.$queryRawUnsafe(sql, ...values),
|
|
223
|
+
execute: (sql, ...values) => transaction.$executeRawUnsafe(sql, ...values),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
query: (sql, ...values) => this.client.queryRawUnsafe(sql, values),
|
|
228
|
+
execute: (sql, ...values) => this.client.executeRawUnsafe(sql, values),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
150
231
|
async executeMigration(transaction, migration) {
|
|
151
232
|
try {
|
|
152
233
|
await migration.up(transaction);
|
|
153
234
|
}
|
|
154
235
|
catch (error) {
|
|
155
|
-
throw
|
|
236
|
+
throw this.migrationError(`Migration "${migration.name}" failed.`, {
|
|
156
237
|
cause: error,
|
|
157
238
|
metadata: { version: migration.version, name: migration.name },
|
|
158
239
|
});
|
|
159
240
|
}
|
|
160
241
|
}
|
|
161
242
|
async recordMigration(transaction, migration) {
|
|
162
|
-
const
|
|
243
|
+
const q = (id) => this.dialect.quoteIdentifier(id);
|
|
244
|
+
const p = (index) => this.dialect.placeholder(index);
|
|
245
|
+
const sql = `INSERT INTO ${q(this.tableName)} (${q("version")}, ${q("name")}) ` +
|
|
246
|
+
`VALUES (${p(1)}, ${p(2)})`;
|
|
163
247
|
try {
|
|
164
|
-
await transaction
|
|
248
|
+
await this.executor(transaction).execute(sql, BigInt(migration.version), migration.name);
|
|
165
249
|
return {
|
|
166
250
|
version: migration.version,
|
|
167
251
|
name: migration.name,
|
|
@@ -169,36 +253,40 @@ export class MigrationRunner {
|
|
|
169
253
|
};
|
|
170
254
|
}
|
|
171
255
|
catch (error) {
|
|
172
|
-
throw
|
|
173
|
-
cause: error,
|
|
174
|
-
metadata: { version: migration.version },
|
|
175
|
-
});
|
|
256
|
+
throw this.migrationError(`Failed to record migration "${migration.name}".`, { cause: error, metadata: { version: migration.version } });
|
|
176
257
|
}
|
|
177
258
|
}
|
|
178
259
|
async deleteMigrationRecord(transaction, version) {
|
|
179
|
-
const
|
|
260
|
+
const q = (id) => this.dialect.quoteIdentifier(id);
|
|
261
|
+
const sql = `DELETE FROM ${q(this.tableName)} ` +
|
|
262
|
+
`WHERE ${q("version")} = ${this.dialect.placeholder(1)}`;
|
|
180
263
|
try {
|
|
181
|
-
await transaction
|
|
264
|
+
await this.executor(transaction).execute(sql, BigInt(version));
|
|
182
265
|
}
|
|
183
266
|
catch (error) {
|
|
184
|
-
throw
|
|
185
|
-
cause: error,
|
|
186
|
-
metadata: { version },
|
|
187
|
-
});
|
|
267
|
+
throw this.migrationError(`Failed to remove migration record for version ${version}.`, { cause: error, metadata: { version } });
|
|
188
268
|
}
|
|
189
269
|
}
|
|
190
270
|
async acquireMigrationLock(transaction) {
|
|
191
271
|
try {
|
|
192
|
-
|
|
193
|
-
await transaction.$executeRaw `SELECT pg_advisory_xact_lock(${lock})`;
|
|
272
|
+
await this.executor(transaction).execute(this.dialect.advisoryTransactionLock(), hashLockKey(this.lockKey));
|
|
194
273
|
}
|
|
195
274
|
catch (error) {
|
|
196
|
-
throw
|
|
197
|
-
cause: error,
|
|
198
|
-
metadata: { lockKey: this.lockKey },
|
|
199
|
-
});
|
|
275
|
+
throw this.migrationError("Failed to acquire the database migration lock.", { cause: error, metadata: { lockKey: this.lockKey } });
|
|
200
276
|
}
|
|
201
277
|
}
|
|
278
|
+
migrationError(message, options = {}) {
|
|
279
|
+
if (isDatabaseErrorLike(options.cause) && !options.metadata) {
|
|
280
|
+
return options.cause;
|
|
281
|
+
}
|
|
282
|
+
return new DatabaseError(message, {
|
|
283
|
+
...options,
|
|
284
|
+
operation: DatabaseOperation.MIGRATION,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function hasVersion(records, version) {
|
|
289
|
+
return records.some((record) => record.version === version);
|
|
202
290
|
}
|
|
203
291
|
/**
|
|
204
292
|
* Creates a migration runner.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DatabaseTransactionContext } from "../databaseClient/databaseClient.core.js";
|
|
2
|
+
import type { TransactionOptions } from "../databaseType/databaseType.type.js";
|
|
3
|
+
import type { SqlDialectName } from "./migration.dialect.js";
|
|
4
|
+
/**
|
|
5
|
+
* Migration definition.
|
|
6
|
+
*
|
|
7
|
+
* Each migration must have a unique, monotonically ordered version. The
|
|
8
|
+
* version is stored in a BIGINT column, so timestamp-style versions such as
|
|
9
|
+
* `20260908120000` are supported up to `Number.MAX_SAFE_INTEGER`.
|
|
10
|
+
*/
|
|
11
|
+
export interface Migration {
|
|
12
|
+
readonly version: number;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/**
|
|
15
|
+
* Applies the migration.
|
|
16
|
+
*/
|
|
17
|
+
readonly up: (database: DatabaseTransactionContext) => Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Reverts the migration.
|
|
20
|
+
*/
|
|
21
|
+
readonly down?: (database: DatabaseTransactionContext) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Persisted migration record.
|
|
25
|
+
*/
|
|
26
|
+
export interface MigrationRecord {
|
|
27
|
+
readonly version: number;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly appliedAt: Date;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Migration execution result.
|
|
33
|
+
*/
|
|
34
|
+
export interface MigrationResult {
|
|
35
|
+
readonly applied: readonly MigrationRecord[];
|
|
36
|
+
readonly skipped: readonly MigrationRecord[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Migration status.
|
|
40
|
+
*/
|
|
41
|
+
export interface MigrationStatus {
|
|
42
|
+
readonly currentVersion: number;
|
|
43
|
+
readonly latestVersion: number;
|
|
44
|
+
readonly pending: readonly Migration[];
|
|
45
|
+
readonly applied: readonly MigrationRecord[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Transaction options accepted by the runners.
|
|
49
|
+
*/
|
|
50
|
+
export type RunnerTransactionOptions = Pick<TransactionOptions, "timeoutMs" | "maxWaitMs" | "isolationLevel">;
|
|
51
|
+
/**
|
|
52
|
+
* Migration runner options.
|
|
53
|
+
*/
|
|
54
|
+
export interface MigrationRunnerOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Tracking table name. Must be a plain SQL identifier.
|
|
57
|
+
*/
|
|
58
|
+
readonly tableName?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Advisory lock key used to serialise concurrent runners.
|
|
61
|
+
*/
|
|
62
|
+
readonly lockKey?: string;
|
|
63
|
+
/**
|
|
64
|
+
* SQL dialect. Only `postgresql` is implemented.
|
|
65
|
+
*/
|
|
66
|
+
readonly dialect?: SqlDialectName;
|
|
67
|
+
/**
|
|
68
|
+
* Options forwarded to every transaction the runner opens
|
|
69
|
+
* (`timeoutMs`, `maxWaitMs`, `isolationLevel`).
|
|
70
|
+
*/
|
|
71
|
+
readonly transaction?: RunnerTransactionOptions;
|
|
72
|
+
/**
|
|
73
|
+
* When `true` (default) each migration runs in its own transaction, so a
|
|
74
|
+
* slow migration cannot roll back earlier ones and the Prisma transaction
|
|
75
|
+
* timeout applies per migration. When `false` the whole batch runs in one
|
|
76
|
+
* transaction and is all-or-nothing.
|
|
77
|
+
*
|
|
78
|
+
* The advisory lock is transaction-scoped: it is held for the whole run
|
|
79
|
+
* when `perItemTransaction` is `false`, and re-acquired for every
|
|
80
|
+
* migration otherwise. Applied history is always re-read under the lock,
|
|
81
|
+
* so concurrent runners never execute the same migration twice.
|
|
82
|
+
*/
|
|
83
|
+
readonly perItemTransaction?: boolean;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=migration.types.d.ts.map
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Offset and cursor pagination utilities.
|
|
5
5
|
*/
|
|
6
|
-
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, type NormalizedPagination, type CursorPaginationInput, type CursorPaginationMeta, type CursorPaginatedResult, } from "./pagination.core.js";
|
|
6
|
+
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, validateCursorPayload, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, type NormalizedPagination, type CursorPaginationInput, type CursorPaginationMeta, type CursorPaginatedResult, type CursorPayload, type EncodeCursorOptions, type DecodeCursorOptions, } from "./pagination.core.js";
|
|
7
|
+
export { decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, type KeysetPageOptions, type KeysetWhere, } from "./pagination.keyset.js";
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/pagination/index.js
CHANGED
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Offset and cursor pagination utilities.
|
|
5
5
|
*/
|
|
6
|
-
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, } from "./pagination.core.js";
|
|
6
|
+
export { normalizePagination, normalizePage, normalizeLimit, calculateOffset, calculateTotalPages, createPaginationMeta, createPaginatedResult, getNextPage, getPreviousPage, isValidPage, getItemRange, paginateCollection, encodeCursor, decodeCursor, validateCursorPayload, normalizeCursorPagination, createCursorPaginationMeta, createCursorPaginatedResult, DEFAULT_PAGE, DEFAULT_LIMIT, MAX_LIMIT, } from "./pagination.core.js";
|
|
7
|
+
export { decodeKeysetCursor, buildKeysetWhere, createKeysetCursor, createKeysetPage, } from "./pagination.keyset.js";
|
|
7
8
|
//# sourceMappingURL=index.js.map
|