@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
@@ -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 { Prisma } from "@prisma/client";
3
- import { DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK, normalizeMigrations, getCurrentVersion, getLatestVersion, quoteIdentifier, hashLockKey, } from "./migration.helpers.js";
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.validateTableName();
22
- }
23
- validateTableName() {
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 applied = await this.getAppliedMigrations();
46
- const appliedVersions = new Set(applied.map((m) => m.version));
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: applied };
56
+ return { applied: [], skipped: before };
50
57
  const newlyApplied = [];
51
- await this.client.transaction(async (transaction) => {
52
- await this.acquireMigrationLock(transaction);
53
- for (const migration of pending) {
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
- newlyApplied.push(record);
57
- }
58
- });
59
- return { applied: newlyApplied, skipped: applied };
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
- async rollback() {
62
- await this.ensureMigrationTable();
63
- const applied = await this.getAppliedMigrations();
64
- if (applied.length === 0)
65
- return null;
66
- const latest = applied[applied.length - 1];
67
- if (!latest)
68
- return null;
69
- const migration = this.migrations.find((c) => c.version === latest.version);
70
- if (!migration) {
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 (!migration.down) {
76
- throw new DatabaseError(`Migration "${migration.name}" does not define a rollback operation.`, {
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
- await this.client.transaction(async (transaction) => {
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
- async rollbackAll() {
88
- await this.ensureMigrationTable();
89
- const applied = await this.getAppliedMigrations();
90
- if (applied.length === 0)
91
- return [];
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 table = quoteIdentifier(this.tableName);
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.executeRaw(Prisma.sql `CREATE TABLE IF NOT EXISTS ${table} (
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 new DatabaseError("Failed to initialize the migration table.", {
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
- async getAppliedMigrations() {
134
- const table = quoteIdentifier(this.tableName);
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.client.queryRaw(Prisma.sql `SELECT "version", "name", "applied_at" FROM ${table} ORDER BY "version" ASC`);
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 new DatabaseError("Failed to read migration history.", {
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 new DatabaseError(`Migration "${migration.name}" failed.`, {
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 table = quoteIdentifier(this.tableName);
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.$executeRawUnsafe(`INSERT INTO ${table} ("version", "name") VALUES ($1, $2)`, migration.version, migration.name);
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 new DatabaseError(`Failed to record migration "${migration.name}".`, {
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 table = quoteIdentifier(this.tableName);
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.$executeRawUnsafe(`DELETE FROM ${table} WHERE "version" = $1`, version);
264
+ await this.executor(transaction).execute(sql, BigInt(version));
182
265
  }
183
266
  catch (error) {
184
- throw new DatabaseError(`Failed to remove migration record for version ${version}.`, {
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
- const lock = hashLockKey(this.lockKey);
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 new DatabaseError("Failed to acquire the database migration lock.", {
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
@@ -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