@spinajs/orm 2.0.486 → 2.0.487

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 (115) hide show
  1. package/lib/cjs/config/orm.d.ts +24 -0
  2. package/lib/cjs/config/orm.d.ts.map +1 -0
  3. package/lib/cjs/config/orm.js +26 -0
  4. package/lib/cjs/config/orm.js.map +1 -0
  5. package/lib/cjs/decorators.d.ts +3 -2
  6. package/lib/cjs/decorators.d.ts.map +1 -1
  7. package/lib/cjs/decorators.js +19 -2
  8. package/lib/cjs/decorators.js.map +1 -1
  9. package/lib/cjs/descriptor.d.ts +29 -1
  10. package/lib/cjs/descriptor.d.ts.map +1 -1
  11. package/lib/cjs/descriptor.js +37 -0
  12. package/lib/cjs/descriptor.js.map +1 -1
  13. package/lib/cjs/index.d.ts +6 -0
  14. package/lib/cjs/index.d.ts.map +1 -1
  15. package/lib/cjs/index.js +6 -0
  16. package/lib/cjs/index.js.map +1 -1
  17. package/lib/cjs/interfaces.d.ts +52 -2
  18. package/lib/cjs/interfaces.d.ts.map +1 -1
  19. package/lib/cjs/interfaces.js +5 -0
  20. package/lib/cjs/interfaces.js.map +1 -1
  21. package/lib/cjs/migration-environment.d.ts +70 -0
  22. package/lib/cjs/migration-environment.d.ts.map +1 -0
  23. package/lib/cjs/migration-environment.js +134 -0
  24. package/lib/cjs/migration-environment.js.map +1 -0
  25. package/lib/cjs/migration-runner.d.ts +108 -0
  26. package/lib/cjs/migration-runner.d.ts.map +1 -0
  27. package/lib/cjs/migration-runner.js +216 -0
  28. package/lib/cjs/migration-runner.js.map +1 -0
  29. package/lib/cjs/migration-service.d.ts +300 -0
  30. package/lib/cjs/migration-service.d.ts.map +1 -0
  31. package/lib/cjs/migration-service.js +829 -0
  32. package/lib/cjs/migration-service.js.map +1 -0
  33. package/lib/cjs/migration-sources.d.ts +85 -0
  34. package/lib/cjs/migration-sources.d.ts.map +1 -0
  35. package/lib/cjs/migration-sources.js +277 -0
  36. package/lib/cjs/migration-sources.js.map +1 -0
  37. package/lib/cjs/orm.d.ts +77 -15
  38. package/lib/cjs/orm.d.ts.map +1 -1
  39. package/lib/cjs/orm.js +156 -150
  40. package/lib/cjs/orm.js.map +1 -1
  41. package/lib/cjs/quoting.d.ts +33 -0
  42. package/lib/cjs/quoting.d.ts.map +1 -0
  43. package/lib/cjs/quoting.js +48 -0
  44. package/lib/cjs/quoting.js.map +1 -0
  45. package/lib/cjs/source-file.d.ts +10 -0
  46. package/lib/cjs/source-file.d.ts.map +1 -0
  47. package/lib/cjs/source-file.js +41 -0
  48. package/lib/cjs/source-file.js.map +1 -0
  49. package/lib/cjs/statements.d.ts +51 -1
  50. package/lib/cjs/statements.d.ts.map +1 -1
  51. package/lib/cjs/statements.js +73 -1
  52. package/lib/cjs/statements.js.map +1 -1
  53. package/lib/cjs/symbols.d.ts +11 -0
  54. package/lib/cjs/symbols.d.ts.map +1 -1
  55. package/lib/cjs/symbols.js +12 -1
  56. package/lib/cjs/symbols.js.map +1 -1
  57. package/lib/mjs/config/orm.d.ts +24 -0
  58. package/lib/mjs/config/orm.d.ts.map +1 -0
  59. package/lib/mjs/config/orm.js +24 -0
  60. package/lib/mjs/config/orm.js.map +1 -0
  61. package/lib/mjs/decorators.d.ts +3 -2
  62. package/lib/mjs/decorators.d.ts.map +1 -1
  63. package/lib/mjs/decorators.js +19 -2
  64. package/lib/mjs/decorators.js.map +1 -1
  65. package/lib/mjs/descriptor.d.ts +29 -1
  66. package/lib/mjs/descriptor.d.ts.map +1 -1
  67. package/lib/mjs/descriptor.js +36 -1
  68. package/lib/mjs/descriptor.js.map +1 -1
  69. package/lib/mjs/index.d.ts +6 -0
  70. package/lib/mjs/index.d.ts.map +1 -1
  71. package/lib/mjs/index.js +6 -0
  72. package/lib/mjs/index.js.map +1 -1
  73. package/lib/mjs/interfaces.d.ts +52 -2
  74. package/lib/mjs/interfaces.d.ts.map +1 -1
  75. package/lib/mjs/interfaces.js +5 -0
  76. package/lib/mjs/interfaces.js.map +1 -1
  77. package/lib/mjs/migration-environment.d.ts +70 -0
  78. package/lib/mjs/migration-environment.d.ts.map +1 -0
  79. package/lib/mjs/migration-environment.js +128 -0
  80. package/lib/mjs/migration-environment.js.map +1 -0
  81. package/lib/mjs/migration-runner.d.ts +108 -0
  82. package/lib/mjs/migration-runner.d.ts.map +1 -0
  83. package/lib/mjs/migration-runner.js +211 -0
  84. package/lib/mjs/migration-runner.js.map +1 -0
  85. package/lib/mjs/migration-service.d.ts +300 -0
  86. package/lib/mjs/migration-service.d.ts.map +1 -0
  87. package/lib/mjs/migration-service.js +824 -0
  88. package/lib/mjs/migration-service.js.map +1 -0
  89. package/lib/mjs/migration-sources.d.ts +85 -0
  90. package/lib/mjs/migration-sources.d.ts.map +1 -0
  91. package/lib/mjs/migration-sources.js +236 -0
  92. package/lib/mjs/migration-sources.js.map +1 -0
  93. package/lib/mjs/orm.d.ts +77 -15
  94. package/lib/mjs/orm.d.ts.map +1 -1
  95. package/lib/mjs/orm.js +158 -152
  96. package/lib/mjs/orm.js.map +1 -1
  97. package/lib/mjs/quoting.d.ts +33 -0
  98. package/lib/mjs/quoting.d.ts.map +1 -0
  99. package/lib/mjs/quoting.js +45 -0
  100. package/lib/mjs/quoting.js.map +1 -0
  101. package/lib/mjs/source-file.d.ts +10 -0
  102. package/lib/mjs/source-file.d.ts.map +1 -0
  103. package/lib/mjs/source-file.js +38 -0
  104. package/lib/mjs/source-file.js.map +1 -0
  105. package/lib/mjs/statements.d.ts +51 -1
  106. package/lib/mjs/statements.d.ts.map +1 -1
  107. package/lib/mjs/statements.js +71 -0
  108. package/lib/mjs/statements.js.map +1 -1
  109. package/lib/mjs/symbols.d.ts +11 -0
  110. package/lib/mjs/symbols.d.ts.map +1 -1
  111. package/lib/mjs/symbols.js +11 -0
  112. package/lib/mjs/symbols.js.map +1 -1
  113. package/lib/tsconfig.cjs.tsbuildinfo +1 -1
  114. package/lib/tsconfig.mjs.tsbuildinfo +1 -1
  115. package/package.json +7 -7
@@ -0,0 +1,829 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.DefaultMigrationService = exports.OrmMigrationService = exports.MIGRATION_LOCK_MAX_STEALS = exports.MIGRATION_LOCK_STALE_AFTER = exports.MIGRATION_LOCK_TIMEOUT = exports.MIGRATION_LOCK_POLL_INTERVAL = exports.MIGRATION_TABLE_NAME = void 0;
13
+ exports.migrationChecksum = migrationChecksum;
14
+ const di_1 = require("@spinajs/di");
15
+ const log_common_1 = require("@spinajs/log-common");
16
+ const node_crypto_1 = require("node:crypto");
17
+ const node_os_1 = require("node:os");
18
+ const driver_js_1 = require("./driver.js");
19
+ const interfaces_js_1 = require("./interfaces.js");
20
+ const exceptions_js_1 = require("./exceptions.js");
21
+ exports.MIGRATION_TABLE_NAME = 'spinajs_migration';
22
+ exports.MIGRATION_LOCK_POLL_INTERVAL = 500;
23
+ exports.MIGRATION_LOCK_TIMEOUT = 30_000;
24
+ exports.MIGRATION_LOCK_STALE_AFTER = 600_000;
25
+ /**
26
+ * How many times one `acquireLock()` call may remove a lock row it judged stale. A steal is
27
+ * not proof the row is gone - a DELETE can succeed and remove nothing - so without a cap the
28
+ * stale branch is free to warn and retry forever.
29
+ */
30
+ exports.MIGRATION_LOCK_MAX_STEALS = 3;
31
+ /**
32
+ * Fingerprint of a migration's source, used to detect a migration that was edited
33
+ * after it had already been applied.
34
+ */
35
+ function migrationChecksum(type) {
36
+ return (0, node_crypto_1.createHash)('sha256').update(type.toString()).digest('hex');
37
+ }
38
+ /**
39
+ * Per-connection migration execution contract. Configure an alternative
40
+ * implementation with db.Connections[n].Migration.Service (DI token).
41
+ */
42
+ let OrmMigrationService = class OrmMigrationService {
43
+ constructor(driver) {
44
+ this.driver = driver;
45
+ }
46
+ };
47
+ exports.OrmMigrationService = OrmMigrationService;
48
+ exports.OrmMigrationService = OrmMigrationService = __decorate([
49
+ (0, di_1.NewInstance)(),
50
+ __metadata("design:paramtypes", [driver_js_1.OrmDriver])
51
+ ], OrmMigrationService);
52
+ class DefaultMigrationService extends OrmMigrationService {
53
+ get table() {
54
+ return this.driver.Options.Migration?.Table ?? exports.MIGRATION_TABLE_NAME;
55
+ }
56
+ get lockTable() {
57
+ return `${this.table}_lock`;
58
+ }
59
+ /**
60
+ * Creates `name` unless it is already there, tolerating a second process that creates it in
61
+ * the window between the probe and the CREATE.
62
+ *
63
+ * That window cannot be closed with a lock: the lock table is one of the tables being created
64
+ * here, so it cannot guard its own creation. Two processes booting together therefore both see
65
+ * "absent" and both issue a CREATE, and the loser must not take the whole boot down with it.
66
+ * Only a table that really is present afterwards excuses the failure - anything else ( no
67
+ * permission, bad DDL, dead connection ) is a genuine error and is rethrown.
68
+ *
69
+ * Returns true when the table was *absent at probe time* - which is not the same as "this
70
+ * process created it", since the lost-race path returns true too. Callers use it to skip the
71
+ * legacy upgrade path: a table that appeared inside the race window was created by a peer
72
+ * running this same DDL, so it already carries the current shape.
73
+ */
74
+ async createTableIfAbsent(name, columns) {
75
+ // a builder executes at most once, so every statement needs a fresh SchemaQueryBuilder
76
+ const db = this.driver.Options.Database;
77
+ if (await this.driver.schema().tableExists(name, db)) {
78
+ return false;
79
+ }
80
+ try {
81
+ await this.driver.schema().createTable(name, columns);
82
+ }
83
+ catch (err) {
84
+ if (!(await this.driver.schema().tableExists(name, db))) {
85
+ throw new exceptions_js_1.OrmException(`Could not create migration table ${name} on connection ${this.driver.Options.Name}: ${err.message}`, undefined, undefined, undefined, err);
86
+ }
87
+ this.Log.trace(`Migration table ${name} on connection ${this.driver.Options.Name} was created concurrently by another process - continuing`);
88
+ }
89
+ return true;
90
+ }
91
+ async ensureStorage() {
92
+ // a builder executes at most once, so every statement needs a fresh SchemaQueryBuilder
93
+ const schema = () => this.driver.schema();
94
+ const db = this.driver.Options.Database;
95
+ // "was absent when we probed", not "we created it" - a lost race reports absent too
96
+ const wasAbsent = await this.createTableIfAbsent(this.table, (t) => {
97
+ t.string('Migration').unique().notNull();
98
+ t.dateTime('CreatedAt').notNull();
99
+ t.dateTime('StartedAt').notNull();
100
+ t.dateTime('FinishedAt');
101
+ t.dateTime('RolledBackAt');
102
+ t.text('Logs');
103
+ t.string('Checksum', 64);
104
+ t.int('Batch').notNull().default().value(1);
105
+ });
106
+ if (!wasAbsent) {
107
+ const cols = (await this.driver.tableInfo(this.table, db)) ?? [];
108
+ const has = (n) => cols.some((c) => c.Name === n);
109
+ if (!has('StartedAt') || !has('FinishedAt') || !has('RolledBackAt') || !has('Logs') || !has('Checksum') || !has('Batch')) {
110
+ await schema().alterTable(this.table, (t) => {
111
+ if (!has('StartedAt'))
112
+ t.dateTime('StartedAt').addColumn();
113
+ if (!has('FinishedAt'))
114
+ t.dateTime('FinishedAt').addColumn();
115
+ if (!has('RolledBackAt'))
116
+ t.dateTime('RolledBackAt').addColumn();
117
+ if (!has('Logs'))
118
+ t.text('Logs').addColumn();
119
+ if (!has('Checksum'))
120
+ t.string('Checksum', 64).addColumn();
121
+ if (!has('Batch')) {
122
+ // added nullable and backfilled below - a NOT NULL column cannot be bolted
123
+ // onto a table that already has rows
124
+ const batch = t.int('Batch');
125
+ batch.default().value(1);
126
+ batch.addColumn();
127
+ }
128
+ });
129
+ await this.backfillLegacyRows();
130
+ }
131
+ }
132
+ await this.createTableIfAbsent(this.lockTable, (t) => {
133
+ t.int('Id').unique().notNull();
134
+ t.dateTime('AcquiredAt').notNull();
135
+ t.string('Owner', 255).notNull();
136
+ });
137
+ }
138
+ async records() {
139
+ return ((await this.driver.select().from(this.table)) ?? []);
140
+ }
141
+ /**
142
+ * Fills the columns the upgrade above has just added. A row written before they existed carries
143
+ * nothing but `CreatedAt`, and a NULL `FinishedAt` reads as "never applied" - so without this
144
+ * every migration the deployment ran years ago would run again over a schema that already has
145
+ * it. `CreatedAt` is the only timestamp such a row has, so it is treated as both start and
146
+ * finish.
147
+ *
148
+ * Row by row through the update builder rather than as three set-based `UPDATE`s, and that is
149
+ * the point of the method: a set-based statement has to name the table itself, and the only
150
+ * way to do that here is raw SQL. `Migration.Table` is configuration - a name that needs
151
+ * quoting ( a reserved word, a dot, a space ) would then break this path alone, and only on a
152
+ * deployment that already has rows, which is the least reachable corner in the file. The
153
+ * builder quotes it exactly as every other statement in this class does. The cost is one UPDATE
154
+ * per legacy row, on the single boot that performs the upgrade and never again.
155
+ */
156
+ async backfillLegacyRows() {
157
+ for (const r of await this.records()) {
158
+ const patch = {};
159
+ // `== null` on purpose - the column is either absent from the row object ( undefined ) or
160
+ // present and NULL, depending on how the driver hydrates a freshly added column
161
+ if (r.StartedAt == null) {
162
+ patch.StartedAt = r.CreatedAt;
163
+ }
164
+ // `Logs` set with a NULL `FinishedAt` is the FAILED state. It cannot occur on a table this
165
+ // old - there was no Logs column to write - but stamping a finish over one would silently
166
+ // unblock a half-applied migration, so the guard is stated rather than assumed away.
167
+ if (r.FinishedAt == null && !r.Logs) {
168
+ patch.FinishedAt = r.CreatedAt;
169
+ }
170
+ if (r.Batch == null) {
171
+ patch.Batch = 1;
172
+ }
173
+ if (Object.keys(patch).length === 0) {
174
+ continue;
175
+ }
176
+ await this.driver.update().in(this.table).update(patch).where({ Migration: r.Migration });
177
+ }
178
+ }
179
+ /**
180
+ * Migrations that finished successfully and were not rolled back - the raw rows, unmerged with
181
+ * the registry.
182
+ *
183
+ * A convenience on this class rather than part of `OrmMigrationService`: nothing in the ORM,
184
+ * the runner or the CLI calls it, because they all need the registry merged in and go through
185
+ * `status()`. It is kept because a subclass, a script or a health check reaching for "what does
186
+ * this connection think it has applied?" should not have to reimplement the applied-gate, and
187
+ * getting that gate subtly wrong ( "a row exists" rather than the FinishedAt NOT NULL and
188
+ * RolledBackAt NULL pair ) is the classic way to re-run a migration.
189
+ */
190
+ async applied() {
191
+ return (await this.records()).filter((r) => r.FinishedAt !== null && r.FinishedAt !== undefined && !r.RolledBackAt);
192
+ }
193
+ /**
194
+ * Opens a migration's row: a fresh one, or a reset of whatever a previous failed or
195
+ * rolled-back attempt left behind.
196
+ */
197
+ async upsertStart(name, existing) {
198
+ const now = new Date();
199
+ if (existing) {
200
+ await this.driver.update().in(this.table).update({ StartedAt: now, FinishedAt: null, RolledBackAt: null, Logs: null }).where({ Migration: name });
201
+ }
202
+ else {
203
+ await this.driver.insert().into(this.table).values({ Migration: name, CreatedAt: now, StartedAt: now, FinishedAt: null, RolledBackAt: null, Logs: null, Checksum: null, Batch: 0 });
204
+ }
205
+ }
206
+ /**
207
+ * Closes a migration's row as applied. The batch number is stamped here rather than at
208
+ * insert time, so a row that never finishes carries no batch to be rolled back later.
209
+ */
210
+ async markFinished(name, batch, checksum) {
211
+ await this.driver.update().in(this.table).update({ FinishedAt: new Date(), Batch: batch, Checksum: checksum }).where({ Migration: name });
212
+ }
213
+ /**
214
+ * Records why a migration died. Failed state is `FinishedAt` NULL *and* `Logs` set - the pair
215
+ * `assertNoFailed` matches on - so this write establishes both rather than assuming the row
216
+ * already carries a NULL `FinishedAt`.
217
+ *
218
+ * It cannot assume it: a migration that was applied and later rolled back is pending again
219
+ * while still holding the old `FinishedAt`/`RolledBackAt` timestamps, and the reset
220
+ * `upsertStart` issued for the retry is inside the transaction that just unwound. Writing only
221
+ * `Logs` would leave `FinishedAt` set, and a half-applied migration would slip past the block.
222
+ */
223
+ async markFailed(name, err) {
224
+ await this.driver
225
+ .update()
226
+ .in(this.table)
227
+ .update({ Logs: `${err.message}\n${err.stack ?? ''}`, FinishedAt: null, RolledBackAt: null })
228
+ .where({ Migration: name });
229
+ }
230
+ /**
231
+ * A half-applied migration means the database is in a state nobody described. Refuse to
232
+ * pile more schema changes on top of it.
233
+ */
234
+ assertNoFailed(records) {
235
+ const failed = records.find((r) => !r.FinishedAt && r.Logs);
236
+ if (failed) {
237
+ throw new exceptions_js_1.OrmException(`Migration ${failed.Migration} on connection ${this.driver.Options.Name} failed previously and blocks migration runs. Inspect Logs column, fix the database manually, then run orm.Migration.resolve('${failed.Migration}', 'applied') or ('rolled-back'). That call reaches registered migrations only - if this one's class is gone, remove its row from ${this.table} by hand instead.`);
238
+ }
239
+ }
240
+ /**
241
+ * The shape of a row whose run never reached an outcome: `StartedAt`, written by `upsertStart`,
242
+ * and neither of the two writes that close it - `markFinished`'s `FinishedAt` or `markFailed`'s
243
+ * `Logs`. Nothing in this class produces it deliberately; a process killed between the start and
244
+ * the outcome does.
245
+ *
246
+ * `RolledBackAt` is excluded on purpose. `resolve('rolled-back')` also leaves `FinishedAt` and
247
+ * `Logs` NULL with `StartedAt` set, and that row is pending because somebody said so - not
248
+ * abandoned.
249
+ *
250
+ * The predicate says nothing about how much of the migration reached the database. It says only
251
+ * that nobody recorded the answer, which is exactly why it is worth surfacing.
252
+ */
253
+ isInterrupted(rec) {
254
+ return !!rec.StartedAt && !rec.FinishedAt && !rec.Logs && !rec.RolledBackAt;
255
+ }
256
+ /**
257
+ * Is a migration run in flight on this connection right now? Read, never acquired: the caller is
258
+ * `status()`, which must not block behind the run it is reporting on.
259
+ *
260
+ * The lock row is the only honest signal available, and it is judged exactly as `acquireLock`
261
+ * judges it - a row younger than `StaleAfter` means somebody is inside a run, an older one means
262
+ * the holder is presumed dead. Freshness rather than mere presence is what makes this usable
263
+ * here: a process killed mid-migration leaves BOTH its open tracking row and its lock row
264
+ * behind, so "a lock row exists" would hide every crash this is meant to surface, permanently.
265
+ *
266
+ * Two deliberate consequences. For `StaleAfter` after a crash the answer is "running" and the
267
+ * open row is not yet reported as interrupted - the same window in which `acquireLock` still
268
+ * waits for the holder, and with the same client-clock caveat documented there. And
269
+ * `Lock.Enabled: false` removes the signal altogether, so the answer is "not running": an open
270
+ * row then always reads as interrupted, which is right for the crash and wrong only for a report
271
+ * taken while a run is genuinely in progress.
272
+ */
273
+ async runInProgress() {
274
+ const { enabled, staleAfter } = this.lockOptions();
275
+ if (!enabled) {
276
+ return false;
277
+ }
278
+ const rows = ((await this.driver.select().from(this.lockTable).where({ Id: 1 })) ?? []);
279
+ const holder = rows[0];
280
+ if (!holder) {
281
+ return false;
282
+ }
283
+ const acquiredAt = holder.AcquiredAt instanceof Date ? holder.AcquiredAt : new Date(holder.AcquiredAt);
284
+ // NaN from an unreadable timestamp compares false, which reports "not running" - the same
285
+ // bias as everywhere else here: a warning an operator can dismiss beats a silence they cannot
286
+ return Date.now() - acquiredAt.getTime() <= staleAfter;
287
+ }
288
+ /**
289
+ * Warns about every migration this run is about to re-run whose row says a previous attempt was
290
+ * started and never closed. No lock check is needed here, unlike in `status()`: this runs inside
291
+ * `withLock`, so the only run in flight on this connection is this one.
292
+ *
293
+ * It warns rather than blocks, and that is a judgement call worth stating. The row records that
294
+ * a run STARTED, not that anything reached the database, so blocking would escalate "unknown" to
295
+ * "refuse to migrate" - and it would do so for the common, harmless shapes too: an idempotent
296
+ * `CREATE TABLE` that had not run yet, or any migration on a `PerMigration` / `PerRun`
297
+ * connection, whose transaction unwound the partial work when the process died. In those cases
298
+ * re-running from the top is exactly right, and a block would turn every OOM kill during a long
299
+ * migration into an operator ticket.
300
+ *
301
+ * The case that is genuinely dangerous is `Transaction.Mode: None` ( the default ) plus
302
+ * non-idempotent DML: half the INSERTs are already in, nothing recorded which half, and the
303
+ * re-run applies them again. Non-idempotent DDL is the recoverable version of the same thing -
304
+ * it fails, and the failed row then blocks properly. Neither is detectable from here, so the
305
+ * warning describes them and leaves the decision with the operator, who is also the only party
306
+ * that can look at the data.
307
+ */
308
+ warnOnInterrupted(records, pending) {
309
+ for (const u of pending) {
310
+ const rec = records.find((r) => r.Migration === u.name);
311
+ if (!rec || !this.isInterrupted(rec)) {
312
+ continue;
313
+ }
314
+ // rendered without `new Date(...)`: a dialect that hands timestamps back as strings would
315
+ // make that an Invalid Date on some formats, and `toISOString()` on one THROWS - out of a
316
+ // warning, which would replace the diagnosis with a crash
317
+ const started = rec.StartedAt instanceof Date ? rec.StartedAt.toISOString() : String(rec.StartedAt);
318
+ this.Log.warn(`Migration ${u.name} on connection ${this.driver.Options.Name} was STARTED and never finished - its tracking row carries StartedAt with neither FinishedAt nor Logs, so the process running it was killed mid-migration ( started ${started} ). It is being RE-RUN from the top and nothing recorded how far the first attempt got. Under Migration.Transaction.Mode None ( the default ) whatever it had already applied is still in the database: non-idempotent DDL will fail and land in the failed state, non-idempotent data changes will be applied a second time and nothing will say so. Check what the first attempt left behind, or record the truth with orm.Migration.resolve('${u.name}', 'applied') or ('rolled-back') before running again.`);
319
+ }
320
+ }
321
+ transactionMode() {
322
+ return this.driver.Options.Migration?.Transaction?.Mode ?? interfaces_js_1.MigrationTransactionMode.None;
323
+ }
324
+ /**
325
+ * True when this migration must run outside any wrapping transaction ( TypeORM parity:
326
+ * `public transaction = false` on the migration class - needed for DDL that cannot be
327
+ * transacted, such as MySQL index rebuilds ).
328
+ *
329
+ * That declaration is an *instance* field, assigned in the constructor, so it never reaches
330
+ * the prototype - the resolved instance is the only place it can be read from. A prototype
331
+ * getter or a static property is honoured too, so a migration may also opt out without
332
+ * being constructed.
333
+ */
334
+ optedOutOfTransaction(u, instance) {
335
+ return instance?.transaction === false || u.type.prototype?.transaction === false || u.type?.transaction === false;
336
+ }
337
+ /**
338
+ * Advisory only: transpilation differences move the checksum as readily as an edit does,
339
+ * so this warns and never blocks.
340
+ */
341
+ warnOnChecksumDrift(u, records) {
342
+ const rec = records.find((r) => r.Migration === u.name);
343
+ if (rec?.Checksum && rec.Checksum !== migrationChecksum(u.type)) {
344
+ this.Log.warn(`Migration ${u.name} source changed since it was applied (checksum mismatch). This is advisory - transpilation differences also change the checksum.`);
345
+ }
346
+ }
347
+ lockOptions() {
348
+ const cfg = this.driver.Options.Migration?.Lock;
349
+ return {
350
+ enabled: cfg?.Enabled ?? true,
351
+ timeout: cfg?.Timeout ?? exports.MIGRATION_LOCK_TIMEOUT,
352
+ staleAfter: cfg?.StaleAfter ?? exports.MIGRATION_LOCK_STALE_AFTER,
353
+ };
354
+ }
355
+ /**
356
+ * Identity written into the lock row. It exists to answer "who is holding this?" when a run
357
+ * blocks, so it has to survive being read on another machine.
358
+ */
359
+ lockOwner() {
360
+ return `${(0, node_os_1.hostname)()}:${process.pid}`;
361
+ }
362
+ /**
363
+ * Takes the single row of the lock table, waiting for whoever has it.
364
+ *
365
+ * The row is claimed by INSERT rather than by "SELECT then INSERT": `Id` is unique, so the
366
+ * database decides the winner in one statement and two processes racing here cannot both
367
+ * succeed. A refused insert is therefore read as "somebody else holds it" - which is also why
368
+ * the holder is re-read afterwards rather than guessed at.
369
+ *
370
+ * Staleness is judged against the *client* clock: `AcquiredAt` is written here as
371
+ * `new Date()` and compared to this host's `Date.now()`. That is sound for the case this
372
+ * lock is built for - one process migrating, crashing, and restarting to find its own
373
+ * abandoned row - but on hosts whose clocks disagree the window is off by the skew, which
374
+ * shows up as stealing too early or waiting too long. Stamping `AcquiredAt` from the
375
+ * database ( a driver-level `CURRENT_TIMESTAMP` default and a server-side comparison ) would
376
+ * remove the assumption; it needs dialect support that does not exist here yet.
377
+ */
378
+ async acquireLock() {
379
+ const { timeout, staleAfter } = this.lockOptions();
380
+ const owner = this.lockOwner();
381
+ const start = Date.now();
382
+ const expired = () => Date.now() - start > timeout;
383
+ let steals = 0;
384
+ for (;;) {
385
+ try {
386
+ await this.driver.insert().into(this.lockTable).values({ Id: 1, AcquiredAt: new Date(), Owner: owner });
387
+ return;
388
+ }
389
+ catch (err) {
390
+ // deliberately unguarded: a select that dies here means the connection is gone, and that
391
+ // has to surface as itself rather than as a lock timeout thirty seconds later
392
+ const rows = ((await this.driver.select().from(this.lockTable).where({ Id: 1 })) ?? []);
393
+ const holder = rows[0];
394
+ if (holder) {
395
+ const acquiredAt = holder.AcquiredAt instanceof Date ? holder.AcquiredAt : new Date(holder.AcquiredAt);
396
+ const stale = Date.now() - acquiredAt.getTime() > staleAfter;
397
+ // A process that died mid-run leaves its row behind and nothing else will ever clear
398
+ // it. The attempts are capped because a steal is not proof the row is gone - a DELETE
399
+ // can succeed and remove nothing, and the read that follows then finds the same row.
400
+ // Uncapped, that is a sleepless loop warning once per turn and never timing out.
401
+ if (stale && steals < exports.MIGRATION_LOCK_MAX_STEALS) {
402
+ steals++;
403
+ // Loud, because the alternative reading is a live run that outlasted StaleAfter - in
404
+ // which case two migration runs are now in flight and somebody has to know
405
+ this.Log.warn(`Stealing stale migration lock on connection ${this.driver.Options.Name}, held by ${holder.Owner} since ${acquiredAt.toISOString()} ( older than ${staleAfter}ms ). If that process is still alive, two migration runs are now in flight - raise Migration.Lock.StaleAfter above the longest run.`);
406
+ await this.driver.del().from(this.lockTable).where({ Id: 1 });
407
+ // straight back to the INSERT: the row should be free now, and sleeping a poll
408
+ // interval for a lock nobody holds is pure boot latency. Only when the deadline has
409
+ // already passed does this fall through to the throw below
410
+ if (!expired()) {
411
+ continue;
412
+ }
413
+ }
414
+ if (expired()) {
415
+ throw new exceptions_js_1.OrmException(`Could not acquire migration lock on connection ${this.driver.Options.Name} within ${timeout}ms - held by ${holder.Owner} since ${acquiredAt.toISOString()}${steals > 0 ? `, and ${steals} attempt(s) to remove it as stale left it in place - delete the row from ${this.lockTable} by hand` : ''}`);
416
+ }
417
+ }
418
+ else if (expired()) {
419
+ // no row to blame, so the insert is failing for its own reasons ( missing table, lost
420
+ // connection ). Carrying that message is the only thing that makes this diagnosable
421
+ throw new exceptions_js_1.OrmException(`Could not acquire migration lock on connection ${this.driver.Options.Name} within ${timeout}ms - no lock row is present, the last insert failed with: ${err.message}`, undefined, undefined, undefined, err);
422
+ }
423
+ await new Promise((r) => setTimeout(r, exports.MIGRATION_LOCK_POLL_INTERVAL));
424
+ }
425
+ }
426
+ }
427
+ /**
428
+ * Drops the lock row unconditionally rather than only the row this process wrote. A run whose
429
+ * lock was stolen as stale would otherwise have nothing to release, and the alternative -
430
+ * deleting only `Owner = ours` - leaves the table holding a row nobody will clear if the owner
431
+ * string ever changes underneath a run. Losing a stolen lock is the lesser harm: the thief
432
+ * already assumed the run was dead.
433
+ */
434
+ async releaseLock() {
435
+ await this.driver.del().from(this.lockTable).where({ Id: 1 });
436
+ }
437
+ /**
438
+ * Concurrency guard around a whole run: one migration run per connection at a time, across
439
+ * processes. Note the release is `finally` - a run that throws must not leave the connection
440
+ * locked until the staleness window expires.
441
+ */
442
+ async withLock(fn) {
443
+ if (!this.lockOptions().enabled) {
444
+ return fn();
445
+ }
446
+ await this.acquireLock();
447
+ try {
448
+ return await fn();
449
+ }
450
+ finally {
451
+ try {
452
+ await this.releaseLock();
453
+ }
454
+ catch (err) {
455
+ // a throw out of `finally` replaces whatever the run was already throwing, and the
456
+ // migration error is the one the operator actually needs - the likeliest reason the
457
+ // release died is the same dead connection that killed the run. The lock is left behind
458
+ // instead: it goes stale and the next run steals it, which is what StaleAfter is for.
459
+ this.Log.error(`Could not release the migration lock on connection ${this.driver.Options.Name}: ${err.message}. It will block further runs until it goes stale ( Migration.Lock.StaleAfter ) or its row is deleted from ${this.lockTable} by hand.`);
460
+ }
461
+ }
462
+ }
463
+ async up(units, options) {
464
+ await this.ensureStorage();
465
+ return await this.withLock(async () => {
466
+ const records = await this.records();
467
+ this.assertNoFailed(records);
468
+ const isApplied = (n) => records.some((r) => r.Migration === n && r.FinishedAt && !r.RolledBackAt);
469
+ const pending = units.filter((u) => !isApplied(u.name));
470
+ if (pending.length === 0) {
471
+ return [];
472
+ }
473
+ // before the fake branch as well as the real one: `fake: true` on a row nobody closed
474
+ // records "applied" over a migration whose actual effect is unknown, which is the outcome
475
+ // most worth being told about
476
+ this.warnOnInterrupted(records, pending);
477
+ // Resolved up front, once each. `transaction = false` only exists on a constructed
478
+ // migration, so the segmenting below cannot be decided without instances - and resolving
479
+ // a second time at execution would build every migration twice.
480
+ const instances = new Map();
481
+ for (const u of pending) {
482
+ instances.set(u.name, await this.driver.Container.resolve(u.type, [this.driver]));
483
+ }
484
+ const instanceOf = (u) => instances.get(u.name);
485
+ const optedOut = (u) => this.optedOutOfTransaction(u, instances.get(u.name));
486
+ const batch = Math.max(0, ...records.filter((r) => r.FinishedAt && !r.RolledBackAt).map((r) => r.Batch ?? 0)) + 1;
487
+ const executed = [];
488
+ if (options?.fake) {
489
+ const stamp = new Date();
490
+ for (const u of pending) {
491
+ const existing = records.find((r) => r.Migration === u.name);
492
+ if (existing) {
493
+ await this.driver
494
+ .update()
495
+ .in(this.table)
496
+ .update({ StartedAt: stamp, FinishedAt: stamp, RolledBackAt: null, Logs: null, Batch: batch, Checksum: migrationChecksum(u.type) })
497
+ .where({ Migration: u.name });
498
+ }
499
+ else {
500
+ await this.driver
501
+ .insert()
502
+ .into(this.table)
503
+ .values({ Migration: u.name, CreatedAt: stamp, StartedAt: stamp, FinishedAt: stamp, RolledBackAt: null, Logs: null, Checksum: migrationChecksum(u.type), Batch: batch });
504
+ }
505
+ executed.push(instanceOf(u));
506
+ this.Log.info(`Migration ${u.name}: faked (recorded without executing)`);
507
+ }
508
+ return executed;
509
+ }
510
+ const startAndRun = async (u) => {
511
+ const migration = instanceOf(u);
512
+ // the snapshot taken at the top of the run is still accurate here: a unit appears at
513
+ // most once in `pending`, and nothing writes its row before this point
514
+ const existing = records.find((r) => r.Migration === u.name);
515
+ await this.upsertStart(u.name, existing);
516
+ this.warnOnChecksumDrift(u, records);
517
+ await migration.up(this.driver);
518
+ await this.markFinished(u.name, batch, migrationChecksum(u.type));
519
+ executed.push(migration);
520
+ this.Log.info(`Migration ${u.name}:up() success !`);
521
+ };
522
+ /**
523
+ * Only ever called once the transaction that was running the migration has unwound - a
524
+ * failure row written inside it would be rolled back with everything else, leaving no
525
+ * trace of what broke.
526
+ */
527
+ const recordFailure = async (name, err) => {
528
+ try {
529
+ // deliberately re-read rather than reuse the run's snapshot: this has to observe
530
+ // post-rollback state, which may have lost the row `upsertStart` inserted
531
+ const fresh = (await this.records()).find((r) => r.Migration === name);
532
+ if (!fresh) {
533
+ await this.upsertStart(name, undefined);
534
+ }
535
+ await this.markFailed(name, err);
536
+ }
537
+ catch (bookkeeping) {
538
+ // the likeliest reason up() died is a connection that is now gone, which kills this
539
+ // write too. The migration error is what the caller needs; losing it to a secondary
540
+ // failure would hide both the migration name and the root cause.
541
+ this.Log.error(`Migration ${name} on connection ${this.driver.Options.Name} failed, and the failure row could not be written: ${bookkeeping.message}. The tracking table may not reflect that this migration is half-applied - verify it by hand.`);
542
+ }
543
+ };
544
+ const failure = (name, err) => new exceptions_js_1.OrmException(`Migration ${name} failed on connection ${this.driver.Options.Name}: ${err.message}`, undefined, undefined, undefined, err);
545
+ const execute = async (u, wrap) => {
546
+ try {
547
+ if (wrap) {
548
+ await this.driver.transaction(async () => {
549
+ await startAndRun(u);
550
+ });
551
+ }
552
+ else {
553
+ await startAndRun(u);
554
+ }
555
+ }
556
+ catch (err) {
557
+ await recordFailure(u.name, err);
558
+ throw failure(u.name, err);
559
+ }
560
+ };
561
+ const mode = this.transactionMode();
562
+ if (mode === interfaces_js_1.MigrationTransactionMode.PerRun) {
563
+ // One transaction per run of consecutive non-opted-out migrations. An opted-out one
564
+ // splits the run rather than being silently dragged into the shared transaction.
565
+ const queue = [...pending];
566
+ while (queue.length > 0) {
567
+ const head = queue.shift();
568
+ if (optedOut(head)) {
569
+ await execute(head, false);
570
+ continue;
571
+ }
572
+ const segment = [head];
573
+ while (queue.length > 0 && !optedOut(queue[0])) {
574
+ segment.push(queue.shift());
575
+ }
576
+ let current = head;
577
+ try {
578
+ await this.driver.transaction(async () => {
579
+ for (const u of segment) {
580
+ current = u;
581
+ await startAndRun(u);
582
+ }
583
+ });
584
+ }
585
+ catch (err) {
586
+ await recordFailure(current.name, err);
587
+ throw failure(current.name, err);
588
+ }
589
+ }
590
+ }
591
+ else if (mode === interfaces_js_1.MigrationTransactionMode.PerMigration) {
592
+ for (const u of pending) {
593
+ await execute(u, !optedOut(u));
594
+ }
595
+ }
596
+ else {
597
+ for (const u of pending) {
598
+ await execute(u, false);
599
+ }
600
+ }
601
+ return executed;
602
+ });
603
+ }
604
+ async down(units, options) {
605
+ await this.ensureStorage();
606
+ return await this.withLock(async () => {
607
+ const records = await this.records();
608
+ const appliedRows = records.filter((r) => r.FinishedAt && !r.RolledBackAt);
609
+ // a failed row is not applied, so it is simply absent from the set below. Deliberate -
610
+ // down() is a recovery path and blocking it would leave operators with only resolve() -
611
+ // but silently stepping around it hides that the connection stays blocked afterwards
612
+ const failedRows = records.filter((r) => !r.FinishedAt && r.Logs);
613
+ if (failedRows.length > 0) {
614
+ // every one of them has to be cleared: assertNoFailed blocks on the first failed row it
615
+ // finds, so resolving one of two leaves the connection just as blocked as before
616
+ this.Log.warn(`Migration(s) ${failedRows.map((r) => r.Migration).join(', ')} on connection ${this.driver.Options.Name} are in failed state and are skipped by this rollback - the schema may end up reverted while every later up() stays blocked. Clear each of them with orm.Migration.resolve('${failedRows[0].Migration}', 'applied') or ('rolled-back').`);
617
+ }
618
+ if (appliedRows.length === 0) {
619
+ return [];
620
+ }
621
+ // default is the last batch alone: one `up` run is one unit of work, so one `down`
622
+ // undoes exactly that run rather than the whole history
623
+ let target = appliedRows;
624
+ if (!options?.all) {
625
+ const lastBatch = Math.max(...appliedRows.map((r) => r.Batch ?? 0));
626
+ target = appliedRows.filter((r) => (r.Batch ?? 0) === lastBatch);
627
+ }
628
+ // a row with no registered unit can never be rolled back - its class is gone, so there is
629
+ // no down() to run and dropping the row alone would lie about the schema. Nothing can be
630
+ // done about it here, but returning [] without a word leaves it applied forever
631
+ const orphans = target.filter((r) => !units.some((u) => u.name === r.Migration));
632
+ if (orphans.length > 0) {
633
+ this.Log.warn(`Migration(s) ${orphans.map((r) => r.Migration).join(', ')} on connection ${this.driver.Options.Name} are recorded as applied but no registered migration matches them (file deleted or renamed). They cannot be rolled back and stay applied - restore the migration file, or remove the row by hand once the schema is undone.`);
634
+ }
635
+ // newest first - a migration has to be undone before the one it was built on top of.
636
+ // Same-timestamp migrations fall back to reverse name, mirroring the forward order
637
+ const toRun = units.filter((u) => target.some((r) => r.Migration === u.name)).sort((a, b) => (a.created < b.created ? 1 : a.created > b.created ? -1 : b.name.localeCompare(a.name)));
638
+ if (toRun.length === 0) {
639
+ return [];
640
+ }
641
+ // resolved up front, once each - `transaction = false` is an instance field, so the
642
+ // segmentation below cannot be decided without instances, and resolving again at
643
+ // execution time would construct every migration twice
644
+ const instances = new Map();
645
+ for (const u of toRun) {
646
+ instances.set(u.name, await this.driver.Container.resolve(u.type, [this.driver]));
647
+ }
648
+ const optedOut = (u) => this.optedOutOfTransaction(u, instances.get(u.name));
649
+ const executed = [];
650
+ const runOne = async (u) => {
651
+ const migration = instances.get(u.name);
652
+ if (!options?.fake) {
653
+ await migration.down(this.driver);
654
+ }
655
+ try {
656
+ // the row is dropped rather than stamped RolledBackAt: a deleted row and a
657
+ // rolled-back one are both "pending" to `up()`, and dropping keeps the table
658
+ // holding only migrations that are actually present in the database
659
+ await this.driver.del().from(this.table).where({ Migration: u.name });
660
+ }
661
+ catch (err) {
662
+ // down() already succeeded, so outside a transaction the schema is reverted while
663
+ // the row still claims the migration is applied - the one state nothing downstream
664
+ // can detect. Inside a transaction the wrapper unwinds both, so it is not a lie there
665
+ if (!options?.fake && this.driver.CurrentTransaction === undefined) {
666
+ this.Log.error(`Migration ${u.name} on connection ${this.driver.Options.Name}: down() completed but its tracking row could not be removed (${err.message}). The schema is reverted while the table still reports this migration as applied - delete the row from ${this.table} by hand, or every later up() will skip it.`);
667
+ }
668
+ throw err;
669
+ }
670
+ executed.push(migration);
671
+ this.Log.info(`Migration ${u.name}: ${options?.fake ? 'faked (record removed without executing)' : 'down() success !'}`);
672
+ };
673
+ /**
674
+ * No `Logs` row is written on a failed rollback, unlike `up()`: `Logs` set with a NULL
675
+ * `FinishedAt` is the state `assertNoFailed` blocks on, and stamping it here would take a
676
+ * connection whose only remaining recovery path is `down()` and lock that path shut too.
677
+ * The name and connection are carried on the exception instead.
678
+ */
679
+ const failure = (name, err) => new exceptions_js_1.OrmException(`Migration ${name} failed to roll back on connection ${this.driver.Options.Name}: ${err.message}`, undefined, undefined, undefined, err);
680
+ const execute = async (u, wrap) => {
681
+ try {
682
+ if (wrap) {
683
+ await this.driver.transaction(async () => {
684
+ await runOne(u);
685
+ });
686
+ }
687
+ else {
688
+ await runOne(u);
689
+ }
690
+ }
691
+ catch (err) {
692
+ throw failure(u.name, err);
693
+ }
694
+ };
695
+ const mode = this.transactionMode();
696
+ if (options?.fake) {
697
+ // nothing is executed, so there is no schema change worth wrapping
698
+ for (const u of toRun) {
699
+ await execute(u, false);
700
+ }
701
+ }
702
+ else if (mode === interfaces_js_1.MigrationTransactionMode.PerRun) {
703
+ // one transaction per stretch of consecutive non-opted-out migrations, exactly as `up()`
704
+ // segments. Refusing the whole rollback instead would leave a PerRun connection holding
705
+ // one `transaction = false` migration able to up() but never able to down() as a whole
706
+ const queue = [...toRun];
707
+ while (queue.length > 0) {
708
+ const head = queue.shift();
709
+ if (optedOut(head)) {
710
+ await execute(head, false);
711
+ continue;
712
+ }
713
+ const segment = [head];
714
+ while (queue.length > 0 && !optedOut(queue[0])) {
715
+ segment.push(queue.shift());
716
+ }
717
+ let current = head;
718
+ try {
719
+ await this.driver.transaction(async () => {
720
+ for (const u of segment) {
721
+ current = u;
722
+ await runOne(u);
723
+ }
724
+ });
725
+ }
726
+ catch (err) {
727
+ throw failure(current.name, err);
728
+ }
729
+ }
730
+ }
731
+ else if (mode === interfaces_js_1.MigrationTransactionMode.PerMigration) {
732
+ for (const u of toRun) {
733
+ await execute(u, !optedOut(u));
734
+ }
735
+ }
736
+ else {
737
+ for (const u of toRun) {
738
+ await execute(u, false);
739
+ }
740
+ }
741
+ return executed;
742
+ });
743
+ }
744
+ async status(units) {
745
+ await this.ensureStorage();
746
+ // deliberately unlocked, unlike up()/down(): this is a read-only report and taking the lock
747
+ // would make it block for the whole Timeout behind any running migration - exactly when
748
+ // somebody is asking what is going on
749
+ const records = await this.records();
750
+ // read once for the whole report rather than per entry: every row on this connection is
751
+ // judged against the same "is anything running right now?", and asking N times would let the
752
+ // answer change halfway down a single report
753
+ const running = await this.runInProgress();
754
+ return units.map((u) => {
755
+ const rec = records.find((r) => r.Migration === u.name);
756
+ const applied = !!(rec?.FinishedAt && !rec.RolledBackAt);
757
+ const failed = !!(rec && !rec.FinishedAt && rec.Logs);
758
+ return {
759
+ name: u.name,
760
+ connection: this.driver.Options.Name,
761
+ applied,
762
+ failed,
763
+ rolledBack: !!rec?.RolledBackAt,
764
+ pending: !applied && !failed,
765
+ // a live run legitimately holds an open row, so it is only "interrupted" once nobody is
766
+ // there to close it
767
+ interrupted: !!rec && !running && this.isInterrupted(rec),
768
+ batch: rec?.Batch ?? null,
769
+ startedAt: rec?.StartedAt ?? null,
770
+ finishedAt: rec?.FinishedAt ?? null,
771
+ checksumMismatch: !!(rec?.Checksum && rec.Checksum !== migrationChecksum(u.type)),
772
+ };
773
+ });
774
+ }
775
+ async resolve(name, action, unit) {
776
+ await this.ensureStorage();
777
+ // deliberately unlocked, unlike up()/down(): this is the recovery path for a run that died,
778
+ // quite possibly while holding the lock. Blocking it behind that lock would shut the only
779
+ // door left open
780
+ const records = await this.records();
781
+ const rec = records.find((r) => r.Migration === name);
782
+ // Two shapes may be forced, and they are the two whose real outcome nobody recorded: FAILED
783
+ // ( FinishedAt NULL, Logs set ) and INTERRUPTED ( StartedAt set, neither FinishedAt nor Logs -
784
+ // a run killed before it could write either ). Anything else is healthy, rolled back or
785
+ // absent, and rewriting it would destroy state nobody asked to lose.
786
+ //
787
+ // Deliberately without a lock check, exactly as the method comment above says: this is the
788
+ // recovery path for a run that died, quite possibly still holding the lock. The cost is that
789
+ // an operator who forces a row while a run is genuinely in progress overwrites it - in a
790
+ // deployment running one migration process at a time, which is the supported shape, the run
791
+ // in progress is the operator's own.
792
+ if (!rec || (!(!rec.FinishedAt && rec.Logs) && !this.isInterrupted(rec))) {
793
+ throw new exceptions_js_1.OrmException(`Migration ${name} is not in failed state on connection ${this.driver.Options.Name}, and was not interrupted either - nothing to resolve`);
794
+ }
795
+ if (action === 'applied') {
796
+ // A batch has to be stamped here, exactly as `markFinished` would have: `upsertStart`
797
+ // inserts `Batch: 0` and a first-time failure never reached `markFinished`, so a row
798
+ // resolved without this stays at 0. `down()`'s default target is max(Batch) among applied
799
+ // rows, so a batch-0 row is silently excluded from every default rollback the moment any
800
+ // other row carries a real batch - reachable only via `{ all: true }`. Same rule as `up()`:
801
+ // one past the highest applied batch, which also puts it in the next default rollback.
802
+ const batch = Math.max(0, ...records.filter((r) => r.FinishedAt && !r.RolledBackAt).map((r) => r.Batch ?? 0)) + 1;
803
+ const patch = { FinishedAt: new Date(), Batch: batch };
804
+ // The checksum can only come from the migration class, which this call does not have
805
+ // unless the caller supplies it. Without it the column stays NULL and drift can never be
806
+ // reported for this migration - a NULL is preferable to inventing a fingerprint for
807
+ // source that was never verified against the database.
808
+ if (unit) {
809
+ patch.Checksum = migrationChecksum(unit.type);
810
+ }
811
+ await this.driver.update().in(this.table).update(patch).where({ Migration: name });
812
+ this.Log.info(`Migration ${name} resolved as applied (batch ${batch})${unit ? '' : ' - Checksum left NULL, drift cannot be detected for it'}`);
813
+ }
814
+ else {
815
+ // Logs is cleared, not merely annotated with RolledBackAt. Failed state is the pair
816
+ // `FinishedAt` NULL *and* `Logs` set, and a rolled-back resolution leaves FinishedAt
817
+ // NULL - so a row that kept its Logs would still trip `assertNoFailed` and block every
818
+ // later run, which is the exact thing this call exists to undo.
819
+ await this.driver.update().in(this.table).update({ RolledBackAt: new Date(), Logs: null }).where({ Migration: name });
820
+ this.Log.info(`Migration ${name} resolved as rolled-back (pending again)`);
821
+ }
822
+ }
823
+ }
824
+ exports.DefaultMigrationService = DefaultMigrationService;
825
+ __decorate([
826
+ (0, log_common_1.Logger)('ORM'),
827
+ __metadata("design:type", log_common_1.Log)
828
+ ], DefaultMigrationService.prototype, "Log", void 0);
829
+ //# sourceMappingURL=migration-service.js.map