@getstrata/core 2.2.18 → 2.2.21
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/CHANGELOG.md +12 -0
- package/dist/entries/database/migrations.js +33 -5
- package/dist/index.js +41 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.21
|
|
4
|
+
|
|
5
|
+
Make registered queue/outbox workers discoverable in CLI help and generated startup guidance. Announce polling after boot and when it stops without claiming dependency health, printing secrets, or starting workers automatically. Preserve explicit process ownership, delivery semantics and graceful lifecycle; cover idle, boot/runtime/cleanup failures and real SIGTERM draining.
|
|
6
|
+
|
|
7
|
+
## 2.2.20
|
|
8
|
+
|
|
9
|
+
Validate complete migration definitions before callbacks, locks or database effects; reject malformed exports and duplicate names with source diagnostics. Refuse fresh with missing applied definitions and rollback with incomplete latest batches; preserve partial forward migration and remove blanket history deletion. Cover physical schema/data/history preservation and reset recovery on real Postgres, MySQL and SQLite, plus loader and preflight regressions; document safe missing-file recovery and unchanged atomicity limits.
|
|
10
|
+
|
|
11
|
+
## 2.2.19
|
|
12
|
+
|
|
13
|
+
Fix explicit development fresh resets with unfinished outbox deliveries while preserving ordinary rollback guards. Record each completed reset down immediately so later refusal leaves recoverable migration progress; document partial-DDL/crash limits and old-history reconciliation. Add independent outbox-state and reset failure/migrate/retry regressions on real Postgres, MySQL and SQLite, history-write/reapplication fault injection, and a documented stateful regression review flow.
|
|
14
|
+
|
|
3
15
|
## 2.2.18
|
|
4
16
|
|
|
5
17
|
Add optional typed guardMigrationRollback for application-owned rollback history predicates; skip history reads only during explicit development fresh resets, propagate predicate failures, and reuse the contract for encryption-checkpoint archival protection. Document guard placement and unchanged reset/rollback safety; cover type, runner and real SQLite history regressions.
|
|
@@ -275,6 +275,28 @@ async function guardMigrationRollback(context, guard) {
|
|
|
275
275
|
|
|
276
276
|
// ../../src/core/database/migrations/runner.ts
|
|
277
277
|
var MIGRATIONS_TABLE = "framework_migrations";
|
|
278
|
+
function validateMigrations(migrations, sources) {
|
|
279
|
+
if (!Array.isArray(migrations))
|
|
280
|
+
throw new Error("Migrations must be an array of definitions.");
|
|
281
|
+
const names = new Map;
|
|
282
|
+
for (const [index, migration] of migrations.entries()) {
|
|
283
|
+
const source = sources?.[index] ?? `migration at index ${index}`;
|
|
284
|
+
if (!migration || typeof migration !== "object" || Array.isArray(migration) || typeof migration.name !== "string" || migration.name.trim().length === 0 || typeof migration.up !== "function" || typeof migration.down !== "function") {
|
|
285
|
+
throw new Error(`Invalid ${source}: expected a migration with a non-empty string name and up/down functions.`);
|
|
286
|
+
}
|
|
287
|
+
const previous = names.get(migration.name);
|
|
288
|
+
if (previous !== undefined)
|
|
289
|
+
throw new Error(`Duplicate migration name "${migration.name}" in ${previous} and ${source}.`);
|
|
290
|
+
names.set(migration.name, source);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function requireMigrationDefinitions(applied, migrations, operation) {
|
|
294
|
+
const names = new Set(migrations.map(({ name }) => name));
|
|
295
|
+
const missing = applied.filter(({ name }) => !names.has(name)).map(({ name }) => name);
|
|
296
|
+
if (missing.length > 0) {
|
|
297
|
+
throw new Error(`Cannot ${operation}: missing applied migration definitions: ${missing.join(", ")}. Restore the migration files before retrying; history has not been removed.`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
278
300
|
function migrationsNameColumn() {
|
|
279
301
|
return currentSqlDialect().driver === "mysql" ? "VARCHAR(255) PRIMARY KEY" : "TEXT PRIMARY KEY";
|
|
280
302
|
}
|
|
@@ -316,15 +338,17 @@ async function getAppliedMigrations(db) {
|
|
|
316
338
|
}
|
|
317
339
|
async function loadMigrationsFromDirectory(directory) {
|
|
318
340
|
const entries = await readdir(directory);
|
|
319
|
-
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry
|
|
341
|
+
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && !entry.endsWith(".d.ts") && !["types", "index", "runner", "advisoryLock", "migrationDatabase"].some((name) => entry === `${name}.ts` || entry === `${name}.js`)).sort();
|
|
320
342
|
const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
|
|
321
343
|
const moduleUrl = pathToFileURL(join(directory, fileName)).href;
|
|
322
344
|
const module = await import(moduleUrl);
|
|
323
345
|
return module.default;
|
|
324
346
|
}));
|
|
325
|
-
|
|
347
|
+
validateMigrations(loadedMigrations, migrationFiles.map((file) => join(directory, file)));
|
|
348
|
+
return loadedMigrations;
|
|
326
349
|
}
|
|
327
350
|
async function getMigrationStatus(db, migrations) {
|
|
351
|
+
validateMigrations(migrations);
|
|
328
352
|
const applied = await getAppliedMigrations(db);
|
|
329
353
|
const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
|
|
330
354
|
return migrations.map(({ name }) => ({
|
|
@@ -334,6 +358,7 @@ async function getMigrationStatus(db, migrations) {
|
|
|
334
358
|
}));
|
|
335
359
|
}
|
|
336
360
|
async function runPendingMigrations(db, migrations, options = {}) {
|
|
361
|
+
validateMigrations(migrations);
|
|
337
362
|
const applied = await getAppliedMigrations(db);
|
|
338
363
|
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
339
364
|
const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
|
|
@@ -349,6 +374,7 @@ async function runPendingMigrations(db, migrations, options = {}) {
|
|
|
349
374
|
return pendingMigrations.length;
|
|
350
375
|
}
|
|
351
376
|
async function migrateDatabase(db, migrations, options = {}) {
|
|
377
|
+
validateMigrations(migrations);
|
|
352
378
|
const { advisoryLock = false, onMigration } = options;
|
|
353
379
|
if (advisoryLock) {
|
|
354
380
|
return withMigrationLock(db, () => runPendingMigrations(db, migrations, { onMigration }));
|
|
@@ -356,11 +382,13 @@ async function migrateDatabase(db, migrations, options = {}) {
|
|
|
356
382
|
return runPendingMigrations(db, migrations, { onMigration });
|
|
357
383
|
}
|
|
358
384
|
async function rollbackDatabase(db, migrations, options = {}) {
|
|
385
|
+
validateMigrations(migrations);
|
|
359
386
|
const applied = await getAppliedMigrations(db);
|
|
360
387
|
if (applied.length === 0) {
|
|
361
388
|
return 0;
|
|
362
389
|
}
|
|
363
390
|
const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
|
|
391
|
+
requireMigrationDefinitions(applied.filter(({ batch }) => Number(batch) === lastBatch), migrations, "roll back latest batch");
|
|
364
392
|
const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
|
|
365
393
|
let rolledBack = 0;
|
|
366
394
|
for (const migration of [...migrations].reverse()) {
|
|
@@ -378,8 +406,10 @@ async function freshDatabase(db, migrations, options = {}) {
|
|
|
378
406
|
if (options.destructive && isProductionEnv()) {
|
|
379
407
|
throw new Error("Destructive migration reset is only available in development/test environments.");
|
|
380
408
|
}
|
|
409
|
+
validateMigrations(migrations);
|
|
381
410
|
const runFresh = async () => {
|
|
382
411
|
const applied = await getAppliedMigrations(db);
|
|
412
|
+
requireMigrationDefinitions(applied, migrations, "reset database");
|
|
383
413
|
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
384
414
|
const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
|
|
385
415
|
for (const migration of [...appliedMigrations].reverse()) {
|
|
@@ -388,9 +418,7 @@ async function freshDatabase(db, migrations, options = {}) {
|
|
|
388
418
|
await migration.down(db, { operation: "fresh" });
|
|
389
419
|
else
|
|
390
420
|
await migration.down(db);
|
|
391
|
-
|
|
392
|
-
if (appliedMigrations.length > 0) {
|
|
393
|
-
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
|
|
421
|
+
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = ${currentSqlDialect().placeholder(1)}`, [migration.name]);
|
|
394
422
|
}
|
|
395
423
|
await runPendingMigrations(db, migrations, options);
|
|
396
424
|
};
|
package/dist/index.js
CHANGED
|
@@ -7541,6 +7541,28 @@ import { join } from "path";
|
|
|
7541
7541
|
import { pathToFileURL } from "url";
|
|
7542
7542
|
init_dialect();
|
|
7543
7543
|
var MIGRATIONS_TABLE = "framework_migrations";
|
|
7544
|
+
function validateMigrations(migrations, sources) {
|
|
7545
|
+
if (!Array.isArray(migrations))
|
|
7546
|
+
throw new Error("Migrations must be an array of definitions.");
|
|
7547
|
+
const names = new Map;
|
|
7548
|
+
for (const [index, migration] of migrations.entries()) {
|
|
7549
|
+
const source = sources?.[index] ?? `migration at index ${index}`;
|
|
7550
|
+
if (!migration || typeof migration !== "object" || Array.isArray(migration) || typeof migration.name !== "string" || migration.name.trim().length === 0 || typeof migration.up !== "function" || typeof migration.down !== "function") {
|
|
7551
|
+
throw new Error(`Invalid ${source}: expected a migration with a non-empty string name and up/down functions.`);
|
|
7552
|
+
}
|
|
7553
|
+
const previous = names.get(migration.name);
|
|
7554
|
+
if (previous !== undefined)
|
|
7555
|
+
throw new Error(`Duplicate migration name "${migration.name}" in ${previous} and ${source}.`);
|
|
7556
|
+
names.set(migration.name, source);
|
|
7557
|
+
}
|
|
7558
|
+
}
|
|
7559
|
+
function requireMigrationDefinitions(applied, migrations, operation) {
|
|
7560
|
+
const names = new Set(migrations.map(({ name }) => name));
|
|
7561
|
+
const missing = applied.filter(({ name }) => !names.has(name)).map(({ name }) => name);
|
|
7562
|
+
if (missing.length > 0) {
|
|
7563
|
+
throw new Error(`Cannot ${operation}: missing applied migration definitions: ${missing.join(", ")}. Restore the migration files before retrying; history has not been removed.`);
|
|
7564
|
+
}
|
|
7565
|
+
}
|
|
7544
7566
|
function migrationsNameColumn() {
|
|
7545
7567
|
return currentSqlDialect().driver === "mysql" ? "VARCHAR(255) PRIMARY KEY" : "TEXT PRIMARY KEY";
|
|
7546
7568
|
}
|
|
@@ -7582,15 +7604,17 @@ async function getAppliedMigrations(db) {
|
|
|
7582
7604
|
}
|
|
7583
7605
|
async function loadMigrationsFromDirectory(directory) {
|
|
7584
7606
|
const entries = await readdir(directory);
|
|
7585
|
-
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry
|
|
7607
|
+
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && !entry.endsWith(".d.ts") && !["types", "index", "runner", "advisoryLock", "migrationDatabase"].some((name) => entry === `${name}.ts` || entry === `${name}.js`)).sort();
|
|
7586
7608
|
const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
|
|
7587
7609
|
const moduleUrl = pathToFileURL(join(directory, fileName)).href;
|
|
7588
7610
|
const module = await import(moduleUrl);
|
|
7589
7611
|
return module.default;
|
|
7590
7612
|
}));
|
|
7591
|
-
|
|
7613
|
+
validateMigrations(loadedMigrations, migrationFiles.map((file) => join(directory, file)));
|
|
7614
|
+
return loadedMigrations;
|
|
7592
7615
|
}
|
|
7593
7616
|
async function getMigrationStatus(db, migrations) {
|
|
7617
|
+
validateMigrations(migrations);
|
|
7594
7618
|
const applied = await getAppliedMigrations(db);
|
|
7595
7619
|
const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
|
|
7596
7620
|
return migrations.map(({ name }) => ({
|
|
@@ -7600,6 +7624,7 @@ async function getMigrationStatus(db, migrations) {
|
|
|
7600
7624
|
}));
|
|
7601
7625
|
}
|
|
7602
7626
|
async function runPendingMigrations(db, migrations, options = {}) {
|
|
7627
|
+
validateMigrations(migrations);
|
|
7603
7628
|
const applied = await getAppliedMigrations(db);
|
|
7604
7629
|
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
7605
7630
|
const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
|
|
@@ -7615,6 +7640,7 @@ async function runPendingMigrations(db, migrations, options = {}) {
|
|
|
7615
7640
|
return pendingMigrations.length;
|
|
7616
7641
|
}
|
|
7617
7642
|
async function migrateDatabase(db, migrations, options = {}) {
|
|
7643
|
+
validateMigrations(migrations);
|
|
7618
7644
|
const { advisoryLock = false, onMigration } = options;
|
|
7619
7645
|
if (advisoryLock) {
|
|
7620
7646
|
return withMigrationLock(db, () => runPendingMigrations(db, migrations, { onMigration }));
|
|
@@ -7622,11 +7648,13 @@ async function migrateDatabase(db, migrations, options = {}) {
|
|
|
7622
7648
|
return runPendingMigrations(db, migrations, { onMigration });
|
|
7623
7649
|
}
|
|
7624
7650
|
async function rollbackDatabase(db, migrations, options = {}) {
|
|
7651
|
+
validateMigrations(migrations);
|
|
7625
7652
|
const applied = await getAppliedMigrations(db);
|
|
7626
7653
|
if (applied.length === 0) {
|
|
7627
7654
|
return 0;
|
|
7628
7655
|
}
|
|
7629
7656
|
const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
|
|
7657
|
+
requireMigrationDefinitions(applied.filter(({ batch }) => Number(batch) === lastBatch), migrations, "roll back latest batch");
|
|
7630
7658
|
const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
|
|
7631
7659
|
let rolledBack = 0;
|
|
7632
7660
|
for (const migration of [...migrations].reverse()) {
|
|
@@ -7644,8 +7672,10 @@ async function freshDatabase(db, migrations, options = {}) {
|
|
|
7644
7672
|
if (options.destructive && isProductionEnv()) {
|
|
7645
7673
|
throw new Error("Destructive migration reset is only available in development/test environments.");
|
|
7646
7674
|
}
|
|
7675
|
+
validateMigrations(migrations);
|
|
7647
7676
|
const runFresh = async () => {
|
|
7648
7677
|
const applied = await getAppliedMigrations(db);
|
|
7678
|
+
requireMigrationDefinitions(applied, migrations, "reset database");
|
|
7649
7679
|
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
7650
7680
|
const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
|
|
7651
7681
|
for (const migration of [...appliedMigrations].reverse()) {
|
|
@@ -7654,9 +7684,7 @@ async function freshDatabase(db, migrations, options = {}) {
|
|
|
7654
7684
|
await migration.down(db, { operation: "fresh" });
|
|
7655
7685
|
else
|
|
7656
7686
|
await migration.down(db);
|
|
7657
|
-
|
|
7658
|
-
if (appliedMigrations.length > 0) {
|
|
7659
|
-
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
|
|
7687
|
+
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = ${currentSqlDialect().placeholder(1)}`, [migration.name]);
|
|
7660
7688
|
}
|
|
7661
7689
|
await runPendingMigrations(db, migrations, options);
|
|
7662
7690
|
};
|
|
@@ -10312,10 +10340,14 @@ function createOutboxMigration(name, driver, options = {}) {
|
|
|
10312
10340
|
SELECT 1 FROM strata_outbox_event WHERE id = strata_outbox_delivery.event_id))`);
|
|
10313
10341
|
}
|
|
10314
10342
|
},
|
|
10315
|
-
async down(db) {
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10343
|
+
async down(db, context) {
|
|
10344
|
+
await guardMigrationRollback(context, {
|
|
10345
|
+
async hasHistory() {
|
|
10346
|
+
const [row] = await db.unsafe("SELECT COUNT(*) AS pending FROM strata_outbox_delivery WHERE status <> 'completed'");
|
|
10347
|
+
return Number(row?.pending) > 0;
|
|
10348
|
+
},
|
|
10349
|
+
message: "Cannot drop outbox tables while undelivered events remain."
|
|
10350
|
+
});
|
|
10319
10351
|
await db.unsafe("DROP TABLE strata_outbox_delivery");
|
|
10320
10352
|
await db.unsafe("DROP TABLE strata_outbox_event");
|
|
10321
10353
|
}
|