@noego/proper 0.0.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +1120 -119
- package/bin/cli.js.map +1 -1
- package/bin/cli.mjs +1166 -122
- package/bin/cli.mjs.map +1 -1
- package/bin/index.d.mts +284 -4
- package/bin/index.d.ts +284 -4
- package/bin/index.js +1100 -108
- package/bin/index.js.map +1 -1
- package/bin/index.mjs +1087 -108
- package/bin/index.mjs.map +1 -1
- package/lib/runner.js.map +1 -1
- package/lib/runner.mjs.map +1 -1
- package/package.json +6 -3
- package/readme.md +92 -0
package/bin/index.js
CHANGED
|
@@ -66,23 +66,32 @@ var __async = (__this, __arguments, generator) => {
|
|
|
66
66
|
// index.ts
|
|
67
67
|
var index_exports = {};
|
|
68
68
|
__export(index_exports, {
|
|
69
|
+
MigrationError: () => MigrationError,
|
|
69
70
|
MigrationRunner: () => MySQLMigrationRunner,
|
|
70
71
|
MigrationRunnerFactory: () => MigrationRunnerFactory,
|
|
72
|
+
PatchConflictError: () => PatchConflictError,
|
|
73
|
+
PatchCreator: () => PatchCreator,
|
|
74
|
+
PatchError: () => PatchError,
|
|
75
|
+
PatchExecutionError: () => PatchExecutionError,
|
|
76
|
+
PatchIntegrityError: () => PatchIntegrityError,
|
|
77
|
+
PatchRunner: () => PatchRunner,
|
|
78
|
+
PatchValidationError: () => PatchValidationError,
|
|
79
|
+
PgRunner: () => PgRunner,
|
|
80
|
+
SQLRunner: () => SQLRunner,
|
|
81
|
+
SQLiteRunner: () => SQLiteRunner,
|
|
71
82
|
createSeedFactory: () => createSeedFactory,
|
|
72
83
|
loadMigrationConfig: () => loadMigrationConfig,
|
|
84
|
+
resolvePatchFolder: () => resolvePatchFolder,
|
|
85
|
+
resolvePatchTable: () => resolvePatchTable,
|
|
73
86
|
runSeedsWithRunner: () => runSeedsWithRunner
|
|
74
87
|
});
|
|
75
88
|
module.exports = __toCommonJS(index_exports);
|
|
76
89
|
|
|
77
90
|
// framework/MigrationRunner.ts
|
|
78
|
-
var
|
|
79
|
-
var import_promise = __toESM(require("mysql2/promise"));
|
|
80
|
-
var sqlite = __toESM(require("sqlite"));
|
|
81
|
-
var sqlite3 = __toESM(require("sqlite3"));
|
|
91
|
+
var import_fs6 = __toESM(require("fs"));
|
|
82
92
|
|
|
83
93
|
// framework/MigrationDirectoryReader.ts
|
|
84
|
-
var
|
|
85
|
-
var import_path = __toESM(require("path"));
|
|
94
|
+
var import_fs2 = __toESM(require("fs"));
|
|
86
95
|
|
|
87
96
|
// framework/errors.ts
|
|
88
97
|
var MigrationError = class _MigrationError extends Error {
|
|
@@ -197,6 +206,54 @@ ${originalError.message}`;
|
|
|
197
206
|
return new _MigrationExecutionError(message);
|
|
198
207
|
}
|
|
199
208
|
};
|
|
209
|
+
var PatchError = class _PatchError extends MigrationError {
|
|
210
|
+
constructor(message, patchFile, patchKey) {
|
|
211
|
+
super(`Patch Error: ${message}`);
|
|
212
|
+
this.patchFile = patchFile;
|
|
213
|
+
this.patchKey = patchKey;
|
|
214
|
+
this.name = "PatchError";
|
|
215
|
+
Object.setPrototypeOf(this, _PatchError.prototype);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var PatchValidationError = class _PatchValidationError extends PatchError {
|
|
219
|
+
constructor(message, patchFile, patchKey) {
|
|
220
|
+
super(message, patchFile, patchKey);
|
|
221
|
+
this.name = "PatchValidationError";
|
|
222
|
+
Object.setPrototypeOf(this, _PatchValidationError.prototype);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
var PatchIntegrityError = class _PatchIntegrityError extends PatchError {
|
|
226
|
+
constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
|
|
227
|
+
super(message, patchFile, patchKey);
|
|
228
|
+
this.expectedChecksum = expectedChecksum;
|
|
229
|
+
this.actualChecksum = actualChecksum;
|
|
230
|
+
this.name = "PatchIntegrityError";
|
|
231
|
+
Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
var PatchConflictError = class _PatchConflictError extends PatchError {
|
|
235
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
|
|
236
|
+
super(message, patchFile, patchKey);
|
|
237
|
+
this.operationIndex = operationIndex;
|
|
238
|
+
this.operationVerb = operationVerb;
|
|
239
|
+
this.migrationKeys = migrationKeys;
|
|
240
|
+
this.observedRowCounts = observedRowCounts;
|
|
241
|
+
this.name = "PatchConflictError";
|
|
242
|
+
Object.setPrototypeOf(this, _PatchConflictError.prototype);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var PatchExecutionError = class _PatchExecutionError extends PatchError {
|
|
246
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
|
|
247
|
+
super(originalError ? `${message}
|
|
248
|
+
Original Error:
|
|
249
|
+
${originalError.message}` : message, patchFile, patchKey);
|
|
250
|
+
this.operationIndex = operationIndex;
|
|
251
|
+
this.operationVerb = operationVerb;
|
|
252
|
+
this.originalError = originalError;
|
|
253
|
+
this.name = "PatchExecutionError";
|
|
254
|
+
Object.setPrototypeOf(this, _PatchExecutionError.prototype);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
200
257
|
var CLIError = class _CLIError extends MigrationError {
|
|
201
258
|
constructor(message) {
|
|
202
259
|
super(`CLI Error: ${message}`);
|
|
@@ -359,6 +416,36 @@ var SqlMigrationBuilder = class {
|
|
|
359
416
|
}
|
|
360
417
|
};
|
|
361
418
|
|
|
419
|
+
// framework/MigrationManifest.ts
|
|
420
|
+
var import_fs = __toESM(require("fs"));
|
|
421
|
+
var import_path = __toESM(require("path"));
|
|
422
|
+
function canonicalMigrationKey(value) {
|
|
423
|
+
return value.replace(/(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i, "").toLowerCase();
|
|
424
|
+
}
|
|
425
|
+
function resolveMigrationFile(directory, baseName, direction, dialect) {
|
|
426
|
+
const dialectExt = dialect === "sql" ? "mysql" : dialect;
|
|
427
|
+
const dialectFile = import_path.default.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
428
|
+
if (import_fs.default.existsSync(dialectFile)) return dialectFile;
|
|
429
|
+
const genericFile = import_path.default.join(directory, `${baseName}.${direction}.sql`);
|
|
430
|
+
if (import_fs.default.existsSync(genericFile)) return genericFile;
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
function loadMigrationManifest(directory, dialect) {
|
|
434
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
435
|
+
if (!import_fs.default.existsSync(directory)) return manifest;
|
|
436
|
+
const files = import_fs.default.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name);
|
|
437
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
438
|
+
files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
|
|
439
|
+
uniqueKeys.forEach((key) => {
|
|
440
|
+
manifest.set(key, {
|
|
441
|
+
key,
|
|
442
|
+
upFile: resolveMigrationFile(directory, key, "up", dialect),
|
|
443
|
+
downFile: resolveMigrationFile(directory, key, "down", dialect)
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
return manifest;
|
|
447
|
+
}
|
|
448
|
+
|
|
362
449
|
// framework/MigrationDirectoryReader.ts
|
|
363
450
|
var MigrationDirectoryReader = class {
|
|
364
451
|
constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
|
|
@@ -370,27 +457,23 @@ var MigrationDirectoryReader = class {
|
|
|
370
457
|
/**
|
|
371
458
|
* Resolves the appropriate file for a migration based on dialect.
|
|
372
459
|
* Priority: dialect-specific file > generic file
|
|
460
|
+
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
373
461
|
*/
|
|
374
462
|
resolveFile(baseName, direction) {
|
|
375
|
-
|
|
376
|
-
const dialectFile = import_path.default.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
377
|
-
if (import_fs.default.existsSync(dialectFile)) return dialectFile;
|
|
378
|
-
const genericFile = import_path.default.join(this.directory, `${baseName}.${direction}.sql`);
|
|
379
|
-
if (import_fs.default.existsSync(genericFile)) return genericFile;
|
|
380
|
-
return null;
|
|
463
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
381
464
|
}
|
|
382
465
|
/**
|
|
383
466
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
384
467
|
*/
|
|
385
468
|
isDialectSpecific(filePath) {
|
|
386
|
-
return /\.(mysql|sqlite)\.(up|down)\.sql$/i.test(filePath);
|
|
469
|
+
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
387
470
|
}
|
|
388
471
|
loadMigrations(table, connection) {
|
|
389
|
-
|
|
390
|
-
const dir_content =
|
|
472
|
+
import_fs2.default.existsSync(this.directory) || import_fs2.default.mkdirSync(this.directory);
|
|
473
|
+
const dir_content = import_fs2.default.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
|
|
391
474
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
392
475
|
dir_content.forEach((file) => {
|
|
393
|
-
const key = file
|
|
476
|
+
const key = canonicalMigrationKey(file);
|
|
394
477
|
uniqueKeys.add(key);
|
|
395
478
|
});
|
|
396
479
|
const migration_sorter = {};
|
|
@@ -430,14 +513,14 @@ var MigrationDirectoryReader = class {
|
|
|
430
513
|
return builder;
|
|
431
514
|
}
|
|
432
515
|
sql_up(file) {
|
|
433
|
-
let content =
|
|
516
|
+
let content = import_fs2.default.readFileSync(file).toString();
|
|
434
517
|
if (!this.isDialectSpecific(file)) {
|
|
435
518
|
content = this.read_strategy(content);
|
|
436
519
|
}
|
|
437
520
|
return content.trim();
|
|
438
521
|
}
|
|
439
522
|
sql_down(file) {
|
|
440
|
-
let content =
|
|
523
|
+
let content = import_fs2.default.readFileSync(file).toString();
|
|
441
524
|
if (!this.isDialectSpecific(file)) {
|
|
442
525
|
content = this.read_strategy(content);
|
|
443
526
|
}
|
|
@@ -446,7 +529,23 @@ var MigrationDirectoryReader = class {
|
|
|
446
529
|
};
|
|
447
530
|
|
|
448
531
|
// framework/MigrationSetup.ts
|
|
449
|
-
var
|
|
532
|
+
var import_fs3 = __toESM(require("fs"));
|
|
533
|
+
|
|
534
|
+
// framework/PatchTypes.ts
|
|
535
|
+
var import_path2 = __toESM(require("path"));
|
|
536
|
+
var PATCH_FORMAT_VERSION = 1;
|
|
537
|
+
var DEFAULT_PATCH_TABLE = "proper_patches";
|
|
538
|
+
var PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
|
|
539
|
+
function resolvePatchFolder(config) {
|
|
540
|
+
if (config.patch_folder) return config.patch_folder;
|
|
541
|
+
const dir = import_path2.default.dirname(config.migration_folder);
|
|
542
|
+
return dir === "." && !config.migration_folder.includes(import_path2.default.sep) && !config.migration_folder.includes("/") ? "patches" : import_path2.default.join(dir, "patches");
|
|
543
|
+
}
|
|
544
|
+
function resolvePatchTable(config) {
|
|
545
|
+
return config.patch_table || DEFAULT_PATCH_TABLE;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// framework/MigrationSetup.ts
|
|
450
549
|
var MigrationSetup = class {
|
|
451
550
|
constructor(sqlrunner, config) {
|
|
452
551
|
this.sqlrunner = sqlrunner;
|
|
@@ -454,7 +553,7 @@ var MigrationSetup = class {
|
|
|
454
553
|
}
|
|
455
554
|
setup() {
|
|
456
555
|
return __async(this, null, function* () {
|
|
457
|
-
|
|
556
|
+
import_fs3.default.existsSync(this.config.migration_folder) || import_fs3.default.mkdirSync(this.config.migration_folder);
|
|
458
557
|
const tableName = this.config.migration_table;
|
|
459
558
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
460
559
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -462,6 +561,12 @@ var MigrationSetup = class {
|
|
|
462
561
|
up TEXT,
|
|
463
562
|
down TEXT,
|
|
464
563
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
564
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
565
|
+
id SERIAL PRIMARY KEY,
|
|
566
|
+
migration_key TEXT,
|
|
567
|
+
up TEXT,
|
|
568
|
+
down TEXT,
|
|
569
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
465
570
|
)` : `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
466
571
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
467
572
|
migration_key VARCHAR(255),
|
|
@@ -470,11 +575,39 @@ var MigrationSetup = class {
|
|
|
470
575
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
471
576
|
)`;
|
|
472
577
|
yield this.sqlrunner.query(createTableSql);
|
|
578
|
+
const patchTable = resolvePatchTable(this.config);
|
|
579
|
+
const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
580
|
+
migration_table TEXT NOT NULL,
|
|
581
|
+
patch_key TEXT NOT NULL,
|
|
582
|
+
checksum TEXT NOT NULL,
|
|
583
|
+
format_version INTEGER NOT NULL,
|
|
584
|
+
description TEXT NOT NULL,
|
|
585
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
586
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
587
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
588
|
+
migration_table TEXT NOT NULL,
|
|
589
|
+
patch_key TEXT NOT NULL,
|
|
590
|
+
checksum TEXT NOT NULL,
|
|
591
|
+
format_version INTEGER NOT NULL,
|
|
592
|
+
description TEXT NOT NULL,
|
|
593
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
594
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
595
|
+
)` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
596
|
+
migration_table VARCHAR(255) NOT NULL,
|
|
597
|
+
patch_key VARCHAR(255) NOT NULL,
|
|
598
|
+
checksum CHAR(64) NOT NULL,
|
|
599
|
+
format_version INT NOT NULL,
|
|
600
|
+
description TEXT NOT NULL,
|
|
601
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
602
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
603
|
+
)`;
|
|
604
|
+
yield this.sqlrunner.query(createPatchTableSql);
|
|
473
605
|
});
|
|
474
606
|
}
|
|
475
607
|
teardown() {
|
|
476
608
|
return __async(this, null, function* () {
|
|
477
609
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
610
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
478
611
|
});
|
|
479
612
|
}
|
|
480
613
|
};
|
|
@@ -521,31 +654,702 @@ function MySqlDialectParser(sql) {
|
|
|
521
654
|
}
|
|
522
655
|
return result;
|
|
523
656
|
}
|
|
524
|
-
function
|
|
525
|
-
const
|
|
526
|
-
let result = "";
|
|
527
|
-
let isInSqliteBlock = true;
|
|
528
|
-
const startSqliteRegex = /^\s*--\s*\[\s*sqlite\s*\]\s*$/i;
|
|
657
|
+
function markerDialectParser(names) {
|
|
658
|
+
const startRegex = new RegExp(`^\\s*--\\s*\\[\\s*(${names.join("|")})\\s*\\]\\s*$`, "i");
|
|
529
659
|
const anyDialectRegex = /^\s*--\s*\[\s*\w+\s*\]\s*$/i;
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
660
|
+
return function(sql) {
|
|
661
|
+
const lines = sql.split("\n");
|
|
662
|
+
let result = "";
|
|
663
|
+
let capturing = true;
|
|
664
|
+
for (const line of lines) {
|
|
665
|
+
const trimmed = line.trim();
|
|
666
|
+
if (startRegex.test(trimmed)) {
|
|
667
|
+
capturing = true;
|
|
668
|
+
result += line + "\n";
|
|
669
|
+
continue;
|
|
670
|
+
} else if (anyDialectRegex.test(trimmed)) {
|
|
671
|
+
capturing = false;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (!capturing && line.toLowerCase().includes("create index")) {
|
|
675
|
+
capturing = true;
|
|
676
|
+
}
|
|
677
|
+
if (capturing) {
|
|
678
|
+
result += line + "\n";
|
|
679
|
+
}
|
|
539
680
|
}
|
|
540
|
-
|
|
541
|
-
|
|
681
|
+
return result;
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
var SqliteDialectParser = markerDialectParser(["sqlite"]);
|
|
685
|
+
var PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
|
|
686
|
+
|
|
687
|
+
// framework/PatchDirectoryReader.ts
|
|
688
|
+
var import_crypto = __toESM(require("crypto"));
|
|
689
|
+
var import_fs4 = __toESM(require("fs"));
|
|
690
|
+
var import_path3 = __toESM(require("path"));
|
|
691
|
+
|
|
692
|
+
// framework/PatchValidator.ts
|
|
693
|
+
var import_ajv = __toESM(require("ajv"));
|
|
694
|
+
var import_yaml = require("yaml");
|
|
695
|
+
var MAX_DESCRIPTION_LENGTH = 500;
|
|
696
|
+
var MAX_MIGRATION_KEY_LENGTH = 255;
|
|
697
|
+
var migrationKeySchema = {
|
|
698
|
+
type: "string",
|
|
699
|
+
minLength: 1,
|
|
700
|
+
maxLength: MAX_MIGRATION_KEY_LENGTH
|
|
701
|
+
};
|
|
702
|
+
var patchSchema = {
|
|
703
|
+
type: "object",
|
|
704
|
+
additionalProperties: false,
|
|
705
|
+
required: ["version", "description", "operations"],
|
|
706
|
+
properties: {
|
|
707
|
+
version: { type: "integer" },
|
|
708
|
+
description: { type: "string" },
|
|
709
|
+
operations: {
|
|
710
|
+
type: "array",
|
|
711
|
+
minItems: 1,
|
|
712
|
+
items: {
|
|
713
|
+
type: "object",
|
|
714
|
+
additionalProperties: false,
|
|
715
|
+
minProperties: 1,
|
|
716
|
+
maxProperties: 1,
|
|
717
|
+
properties: {
|
|
718
|
+
rename_migration: {
|
|
719
|
+
type: "object",
|
|
720
|
+
additionalProperties: false,
|
|
721
|
+
required: ["from", "to"],
|
|
722
|
+
properties: { from: migrationKeySchema, to: migrationKeySchema }
|
|
723
|
+
},
|
|
724
|
+
mark_applied: {
|
|
725
|
+
type: "object",
|
|
726
|
+
additionalProperties: false,
|
|
727
|
+
required: ["key"],
|
|
728
|
+
properties: { key: migrationKeySchema }
|
|
729
|
+
},
|
|
730
|
+
unmark_applied: {
|
|
731
|
+
type: "object",
|
|
732
|
+
additionalProperties: false,
|
|
733
|
+
required: ["key"],
|
|
734
|
+
properties: { key: migrationKeySchema }
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
542
738
|
}
|
|
543
|
-
|
|
544
|
-
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
var ajv = new import_ajv.default({ allErrors: true, strict: true });
|
|
742
|
+
var validateSchema = ajv.compile(patchSchema);
|
|
743
|
+
function fail(message, file, patchKey) {
|
|
744
|
+
throw new PatchValidationError(message, file, patchKey);
|
|
745
|
+
}
|
|
746
|
+
function checkMigrationKey(value, context, file, patchKey) {
|
|
747
|
+
if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
|
|
748
|
+
if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
|
|
749
|
+
if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
|
|
750
|
+
if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
|
|
751
|
+
fail(`${context}: migration key length out of bounds`, file, patchKey);
|
|
752
|
+
}
|
|
753
|
+
return canonicalMigrationKey(value);
|
|
754
|
+
}
|
|
755
|
+
function assertStrictYaml(doc, file, patchKey) {
|
|
756
|
+
if (doc.errors.length > 0) {
|
|
757
|
+
fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
|
|
758
|
+
}
|
|
759
|
+
if (doc.warnings.length > 0) {
|
|
760
|
+
fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
|
|
761
|
+
}
|
|
762
|
+
const visit = (node) => {
|
|
763
|
+
var _a, _b;
|
|
764
|
+
if (node == null || typeof node !== "object") return;
|
|
765
|
+
if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
|
|
766
|
+
fail("YAML aliases are not permitted in patch files", file, patchKey);
|
|
767
|
+
}
|
|
768
|
+
if (node.anchor) {
|
|
769
|
+
fail("YAML anchors are not permitted in patch files", file, patchKey);
|
|
770
|
+
}
|
|
771
|
+
if (node.tag && ![
|
|
772
|
+
"tag:yaml.org,2002:str",
|
|
773
|
+
"tag:yaml.org,2002:int",
|
|
774
|
+
"tag:yaml.org,2002:bool",
|
|
775
|
+
"tag:yaml.org,2002:null",
|
|
776
|
+
"tag:yaml.org,2002:map",
|
|
777
|
+
"tag:yaml.org,2002:seq"
|
|
778
|
+
].includes(node.tag)) {
|
|
779
|
+
fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
|
|
780
|
+
}
|
|
781
|
+
if (Array.isArray(node.items)) {
|
|
782
|
+
for (const item of node.items) {
|
|
783
|
+
if (item && typeof item === "object" && "key" in item) {
|
|
784
|
+
const keyValue = (_b = item.key) == null ? void 0 : _b.value;
|
|
785
|
+
if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
|
|
786
|
+
visit(item.key);
|
|
787
|
+
visit(item.value);
|
|
788
|
+
} else {
|
|
789
|
+
visit(item);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
visit(doc.contents);
|
|
795
|
+
}
|
|
796
|
+
function parsePatchContent(content, fileName, patchKey) {
|
|
797
|
+
var _a;
|
|
798
|
+
const doc = (0, import_yaml.parseDocument)(content, {
|
|
799
|
+
uniqueKeys: true,
|
|
800
|
+
// duplicate mapping keys become errors
|
|
801
|
+
merge: false,
|
|
802
|
+
schema: "core",
|
|
803
|
+
version: "1.2"
|
|
804
|
+
});
|
|
805
|
+
assertStrictYaml(doc, fileName, patchKey);
|
|
806
|
+
const raw = doc.toJS({ mapAsMap: false });
|
|
807
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
808
|
+
fail("Patch document must be a YAML mapping", fileName, patchKey);
|
|
809
|
+
}
|
|
810
|
+
if (!validateSchema(raw)) {
|
|
811
|
+
const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
|
|
812
|
+
const anyRaw = raw;
|
|
813
|
+
if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
|
|
814
|
+
fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
815
|
+
}
|
|
816
|
+
fail(`Schema validation failed: ${detail}`, fileName, patchKey);
|
|
817
|
+
}
|
|
818
|
+
const parsed = raw;
|
|
819
|
+
if (parsed.version !== PATCH_FORMAT_VERSION) {
|
|
820
|
+
fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
821
|
+
}
|
|
822
|
+
const description = parsed.description.trim();
|
|
823
|
+
if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
|
|
824
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
825
|
+
fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
|
|
826
|
+
}
|
|
827
|
+
const operations = parsed.operations.map((op, index) => {
|
|
828
|
+
const verbs = Object.keys(op);
|
|
829
|
+
const verb = verbs[0];
|
|
830
|
+
const context = `operation ${index} (${verb})`;
|
|
831
|
+
switch (verb) {
|
|
832
|
+
case "rename_migration": {
|
|
833
|
+
const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
|
|
834
|
+
const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
|
|
835
|
+
if (from === to) {
|
|
836
|
+
fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
|
|
837
|
+
}
|
|
838
|
+
return { verb: "rename_migration", from, to };
|
|
839
|
+
}
|
|
840
|
+
case "mark_applied":
|
|
841
|
+
return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
|
|
842
|
+
case "unmark_applied":
|
|
843
|
+
return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
|
|
844
|
+
default:
|
|
845
|
+
fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
|
|
545
846
|
}
|
|
847
|
+
});
|
|
848
|
+
return { version: parsed.version, description, operations };
|
|
849
|
+
}
|
|
850
|
+
function validatePatchPlan(patches, manifest) {
|
|
851
|
+
const renames = [];
|
|
852
|
+
for (const patch of patches) {
|
|
853
|
+
patch.operations.forEach((op, index) => {
|
|
854
|
+
if (op.verb === "rename_migration") {
|
|
855
|
+
renames.push({ from: op.from, to: op.to, file: patch.fileName });
|
|
856
|
+
} else if (op.verb === "mark_applied") {
|
|
857
|
+
if (!manifest.has(op.key)) {
|
|
858
|
+
throw new PatchValidationError(
|
|
859
|
+
`operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
|
|
860
|
+
patch.fileName,
|
|
861
|
+
patch.patchKey
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
});
|
|
546
866
|
}
|
|
547
|
-
return
|
|
867
|
+
if (renames.length === 0) return;
|
|
868
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
869
|
+
renames.forEach((r) => {
|
|
870
|
+
mentioned.add(r.from);
|
|
871
|
+
mentioned.add(r.to);
|
|
872
|
+
});
|
|
873
|
+
const finalKey = (start) => {
|
|
874
|
+
let current = start;
|
|
875
|
+
for (const r of renames) {
|
|
876
|
+
if (current === r.from) current = r.to;
|
|
877
|
+
}
|
|
878
|
+
return current;
|
|
879
|
+
};
|
|
880
|
+
for (const key of mentioned) {
|
|
881
|
+
const finish = finalKey(key);
|
|
882
|
+
if (manifest.has(key)) {
|
|
883
|
+
if (finish !== key) {
|
|
884
|
+
throw new PatchValidationError(
|
|
885
|
+
`rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
if (!manifest.has(finish)) {
|
|
890
|
+
throw new PatchValidationError(
|
|
891
|
+
`rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// framework/PatchDirectoryReader.ts
|
|
899
|
+
var PatchDirectoryReader = class {
|
|
900
|
+
constructor(directory) {
|
|
901
|
+
this.directory = directory;
|
|
902
|
+
}
|
|
903
|
+
loadPatches() {
|
|
904
|
+
if (!import_fs4.default.existsSync(this.directory)) return [];
|
|
905
|
+
const entries = import_fs4.default.readdirSync(this.directory, { withFileTypes: true });
|
|
906
|
+
const patchFiles = [];
|
|
907
|
+
for (const entry of entries) {
|
|
908
|
+
if (!entry.name.endsWith(".yaml")) continue;
|
|
909
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
910
|
+
if (entry.isSymbolicLink()) {
|
|
911
|
+
throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
|
|
912
|
+
}
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
patchFiles.push(entry.name);
|
|
916
|
+
}
|
|
917
|
+
patchFiles.sort((a, b) => {
|
|
918
|
+
const stampA = parseInt(a.slice(0, 13), 10);
|
|
919
|
+
const stampB = parseInt(b.slice(0, 13), 10);
|
|
920
|
+
if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
|
|
921
|
+
return stampA - stampB;
|
|
922
|
+
}
|
|
923
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
924
|
+
});
|
|
925
|
+
return patchFiles.map((fileName) => {
|
|
926
|
+
const match = PATCH_FILENAME_REGEX.exec(fileName);
|
|
927
|
+
if (!match) {
|
|
928
|
+
throw new PatchValidationError(
|
|
929
|
+
`invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
|
|
930
|
+
fileName
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
const patchKey = fileName.slice(0, -".yaml".length);
|
|
934
|
+
const filePath = import_path3.default.join(this.directory, fileName);
|
|
935
|
+
const bytes = import_fs4.default.readFileSync(filePath);
|
|
936
|
+
const checksum = import_crypto.default.createHash("sha256").update(bytes).digest("hex");
|
|
937
|
+
const content = bytes.toString("utf8");
|
|
938
|
+
const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
|
|
939
|
+
return { patchKey, fileName, filePath, checksum, version, description, operations };
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// framework/PatchRunner.ts
|
|
945
|
+
function toError(error) {
|
|
946
|
+
if (error instanceof Error) return error;
|
|
947
|
+
return new Error(String(error));
|
|
548
948
|
}
|
|
949
|
+
function extractRows(result) {
|
|
950
|
+
if (!Array.isArray(result)) return [];
|
|
951
|
+
if (Array.isArray(result[0])) return result[0];
|
|
952
|
+
if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
|
|
953
|
+
return result;
|
|
954
|
+
}
|
|
955
|
+
if (result[0] == null) return [];
|
|
956
|
+
return [result[0]];
|
|
957
|
+
}
|
|
958
|
+
var PatchRunner = class {
|
|
959
|
+
constructor(sqlrunner, config) {
|
|
960
|
+
this.sqlrunner = sqlrunner;
|
|
961
|
+
this.config = config;
|
|
962
|
+
this.patchTable = resolvePatchTable(config);
|
|
963
|
+
this.migrationTable = config.migration_table;
|
|
964
|
+
this.dialect = config.database;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Discovers, validates, and applies every unapplied patch in order.
|
|
968
|
+
* Each unapplied patch is its own transaction; earlier committed patches
|
|
969
|
+
* remain committed if a later patch fails.
|
|
970
|
+
*/
|
|
971
|
+
applyPending() {
|
|
972
|
+
return __async(this, null, function* () {
|
|
973
|
+
const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
|
|
974
|
+
const patches = reader.loadPatches();
|
|
975
|
+
const history = yield this.loadHistory();
|
|
976
|
+
if (patches.length === 0 && history.length === 0) {
|
|
977
|
+
return [];
|
|
978
|
+
}
|
|
979
|
+
const byKey = new Map(patches.map((p) => [p.patchKey, p]));
|
|
980
|
+
for (const row of history) {
|
|
981
|
+
const file = byKey.get(row.patch_key);
|
|
982
|
+
if (!file) {
|
|
983
|
+
throw new PatchIntegrityError(
|
|
984
|
+
`applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
|
|
985
|
+
void 0,
|
|
986
|
+
row.patch_key
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
if (file.checksum !== row.checksum) {
|
|
990
|
+
throw new PatchIntegrityError(
|
|
991
|
+
`applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
|
|
992
|
+
file.fileName,
|
|
993
|
+
row.patch_key,
|
|
994
|
+
row.checksum,
|
|
995
|
+
file.checksum
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
|
|
1000
|
+
validatePatchPlan(patches, manifest);
|
|
1001
|
+
const appliedKeys = new Set(history.map((r) => r.patch_key));
|
|
1002
|
+
const results = [];
|
|
1003
|
+
for (const patch of patches) {
|
|
1004
|
+
if (appliedKeys.has(patch.patchKey)) {
|
|
1005
|
+
results.push({
|
|
1006
|
+
patchKey: patch.patchKey,
|
|
1007
|
+
fileName: patch.fileName,
|
|
1008
|
+
status: "already_applied",
|
|
1009
|
+
operations: []
|
|
1010
|
+
});
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
results.push(yield this.applyOne(patch, manifest));
|
|
1014
|
+
}
|
|
1015
|
+
return results;
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
loadHistory() {
|
|
1019
|
+
return __async(this, null, function* () {
|
|
1020
|
+
try {
|
|
1021
|
+
const result = yield this.sqlrunner.query(
|
|
1022
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
|
|
1023
|
+
[this.migrationTable]
|
|
1024
|
+
);
|
|
1025
|
+
return extractRows(result);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
throw new PatchExecutionError(
|
|
1028
|
+
`failed to read patch history from '${this.patchTable}'`,
|
|
1029
|
+
void 0,
|
|
1030
|
+
void 0,
|
|
1031
|
+
void 0,
|
|
1032
|
+
void 0,
|
|
1033
|
+
toError(error)
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
beginSql() {
|
|
1039
|
+
switch (this.dialect) {
|
|
1040
|
+
case "sqlite":
|
|
1041
|
+
return "BEGIN IMMEDIATE";
|
|
1042
|
+
case "pg":
|
|
1043
|
+
return "BEGIN";
|
|
1044
|
+
default:
|
|
1045
|
+
return "START TRANSACTION";
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
begin(patch) {
|
|
1049
|
+
return __async(this, null, function* () {
|
|
1050
|
+
const deadline = Date.now() + 1e4;
|
|
1051
|
+
while (true) {
|
|
1052
|
+
try {
|
|
1053
|
+
yield this.sqlrunner.execute(this.beginSql());
|
|
1054
|
+
return;
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
const message = toError(error).message;
|
|
1057
|
+
if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
|
|
1058
|
+
yield new Promise((resolve) => setTimeout(resolve, 50));
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
throw new PatchExecutionError(
|
|
1062
|
+
"failed to start patch transaction",
|
|
1063
|
+
patch.fileName,
|
|
1064
|
+
patch.patchKey,
|
|
1065
|
+
void 0,
|
|
1066
|
+
void 0,
|
|
1067
|
+
toError(error)
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
rollbackQuietly() {
|
|
1074
|
+
return __async(this, null, function* () {
|
|
1075
|
+
try {
|
|
1076
|
+
yield this.sqlrunner.execute("ROLLBACK");
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
applyOne(patch, manifest) {
|
|
1082
|
+
return __async(this, null, function* () {
|
|
1083
|
+
yield this.begin(patch);
|
|
1084
|
+
try {
|
|
1085
|
+
yield this.sqlrunner.execute(
|
|
1086
|
+
`INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
|
|
1087
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1088
|
+
[this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
|
|
1089
|
+
);
|
|
1090
|
+
} catch (claimError) {
|
|
1091
|
+
yield this.rollbackQuietly();
|
|
1092
|
+
const committed = yield this.findCommittedRow(patch.patchKey);
|
|
1093
|
+
if (committed) {
|
|
1094
|
+
if (committed.checksum === patch.checksum) {
|
|
1095
|
+
return {
|
|
1096
|
+
patchKey: patch.patchKey,
|
|
1097
|
+
fileName: patch.fileName,
|
|
1098
|
+
status: "already_applied",
|
|
1099
|
+
operations: []
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
throw new PatchIntegrityError(
|
|
1103
|
+
`patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
|
|
1104
|
+
patch.fileName,
|
|
1105
|
+
patch.patchKey,
|
|
1106
|
+
committed.checksum,
|
|
1107
|
+
patch.checksum
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
throw new PatchExecutionError(
|
|
1111
|
+
"failed to claim patch-history row",
|
|
1112
|
+
patch.fileName,
|
|
1113
|
+
patch.patchKey,
|
|
1114
|
+
void 0,
|
|
1115
|
+
void 0,
|
|
1116
|
+
toError(claimError)
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
const operationResults = [];
|
|
1120
|
+
try {
|
|
1121
|
+
for (let index = 0; index < patch.operations.length; index++) {
|
|
1122
|
+
operationResults.push(
|
|
1123
|
+
yield this.applyOperation(patch, patch.operations[index], index, manifest)
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
yield this.sqlrunner.execute("COMMIT");
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
yield this.rollbackQuietly();
|
|
1129
|
+
if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
|
|
1130
|
+
throw error;
|
|
1131
|
+
}
|
|
1132
|
+
throw new PatchExecutionError(
|
|
1133
|
+
"patch application failed",
|
|
1134
|
+
patch.fileName,
|
|
1135
|
+
patch.patchKey,
|
|
1136
|
+
void 0,
|
|
1137
|
+
void 0,
|
|
1138
|
+
toError(error)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
patchKey: patch.patchKey,
|
|
1143
|
+
fileName: patch.fileName,
|
|
1144
|
+
status: "applied",
|
|
1145
|
+
operations: operationResults
|
|
1146
|
+
};
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
findCommittedRow(patchKey) {
|
|
1150
|
+
return __async(this, null, function* () {
|
|
1151
|
+
const result = yield this.sqlrunner.query(
|
|
1152
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
|
|
1153
|
+
[this.migrationTable, patchKey]
|
|
1154
|
+
);
|
|
1155
|
+
const list = extractRows(result);
|
|
1156
|
+
return list.length > 0 ? list[0] : null;
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
countRows(key) {
|
|
1160
|
+
return __async(this, null, function* () {
|
|
1161
|
+
var _a, _b, _c;
|
|
1162
|
+
const result = yield this.sqlrunner.query(
|
|
1163
|
+
`SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1164
|
+
[key]
|
|
1165
|
+
);
|
|
1166
|
+
const rows = extractRows(result);
|
|
1167
|
+
const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
|
|
1168
|
+
return Number(value != null ? value : 0);
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
conflict(patch, index, verb, message, keys, counts) {
|
|
1172
|
+
throw new PatchConflictError(
|
|
1173
|
+
`operation ${index} (${verb}): ${message}`,
|
|
1174
|
+
patch.fileName,
|
|
1175
|
+
patch.patchKey,
|
|
1176
|
+
index,
|
|
1177
|
+
verb,
|
|
1178
|
+
keys,
|
|
1179
|
+
counts
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
applyOperation(patch, op, index, manifest) {
|
|
1183
|
+
return __async(this, null, function* () {
|
|
1184
|
+
var _a, _b, _c, _d;
|
|
1185
|
+
try {
|
|
1186
|
+
switch (op.verb) {
|
|
1187
|
+
case "rename_migration": {
|
|
1188
|
+
const fromCount = yield this.countRows(op.from);
|
|
1189
|
+
const toCount = yield this.countRows(op.to);
|
|
1190
|
+
const counts = { [op.from]: fromCount, [op.to]: toCount };
|
|
1191
|
+
if (fromCount > 1 || toCount > 1) {
|
|
1192
|
+
this.conflict(
|
|
1193
|
+
patch,
|
|
1194
|
+
index,
|
|
1195
|
+
op.verb,
|
|
1196
|
+
`ledger corruption: duplicate rows for a migration key`,
|
|
1197
|
+
[op.from, op.to],
|
|
1198
|
+
counts
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
if (fromCount === 1 && toCount === 1) {
|
|
1202
|
+
this.conflict(
|
|
1203
|
+
patch,
|
|
1204
|
+
index,
|
|
1205
|
+
op.verb,
|
|
1206
|
+
`both '${op.from}' and '${op.to}' exist in the ledger`,
|
|
1207
|
+
[op.from, op.to],
|
|
1208
|
+
counts
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (fromCount === 0) {
|
|
1212
|
+
return { verb: op.verb, changed: false };
|
|
1213
|
+
}
|
|
1214
|
+
const target = manifest.get(op.to);
|
|
1215
|
+
if (target) {
|
|
1216
|
+
yield this.sqlrunner.execute(
|
|
1217
|
+
`UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
|
|
1218
|
+
[op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
|
|
1219
|
+
);
|
|
1220
|
+
} else {
|
|
1221
|
+
yield this.sqlrunner.execute(
|
|
1222
|
+
`UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
|
|
1223
|
+
[op.to, op.from]
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
return { verb: op.verb, changed: true };
|
|
1227
|
+
}
|
|
1228
|
+
case "mark_applied": {
|
|
1229
|
+
const count = yield this.countRows(op.key);
|
|
1230
|
+
if (count > 1) {
|
|
1231
|
+
this.conflict(
|
|
1232
|
+
patch,
|
|
1233
|
+
index,
|
|
1234
|
+
op.verb,
|
|
1235
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1236
|
+
[op.key],
|
|
1237
|
+
{ [op.key]: count }
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
if (count === 1) {
|
|
1241
|
+
return { verb: op.verb, changed: false };
|
|
1242
|
+
}
|
|
1243
|
+
const entry = manifest.get(op.key);
|
|
1244
|
+
yield this.sqlrunner.execute(
|
|
1245
|
+
`INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
|
|
1246
|
+
[op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
|
|
1247
|
+
);
|
|
1248
|
+
return { verb: op.verb, changed: true };
|
|
1249
|
+
}
|
|
1250
|
+
case "unmark_applied": {
|
|
1251
|
+
const count = yield this.countRows(op.key);
|
|
1252
|
+
if (count > 1) {
|
|
1253
|
+
this.conflict(
|
|
1254
|
+
patch,
|
|
1255
|
+
index,
|
|
1256
|
+
op.verb,
|
|
1257
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1258
|
+
[op.key],
|
|
1259
|
+
{ [op.key]: count }
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
if (count === 0) {
|
|
1263
|
+
return { verb: op.verb, changed: false };
|
|
1264
|
+
}
|
|
1265
|
+
yield this.sqlrunner.execute(
|
|
1266
|
+
`DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1267
|
+
[op.key]
|
|
1268
|
+
);
|
|
1269
|
+
return { verb: op.verb, changed: true };
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
if (error instanceof PatchConflictError) throw error;
|
|
1274
|
+
throw new PatchExecutionError(
|
|
1275
|
+
`operation failed`,
|
|
1276
|
+
patch.fileName,
|
|
1277
|
+
patch.patchKey,
|
|
1278
|
+
index,
|
|
1279
|
+
op.verb,
|
|
1280
|
+
toError(error)
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// framework/PatchCreator.ts
|
|
1288
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1289
|
+
var import_path4 = __toESM(require("path"));
|
|
1290
|
+
var MAX_NAME_LENGTH = 120;
|
|
1291
|
+
var SCAFFOLD = `version: 1
|
|
1292
|
+
description: TODO
|
|
1293
|
+
operations: []
|
|
1294
|
+
`;
|
|
1295
|
+
var PatchCreator = class _PatchCreator {
|
|
1296
|
+
constructor(patchFolder) {
|
|
1297
|
+
this.patchFolder = patchFolder;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
|
|
1301
|
+
* Rejects empty results, path separators, `..`, control characters,
|
|
1302
|
+
* characters outside [a-z0-9_-], and names longer than 120 characters.
|
|
1303
|
+
*/
|
|
1304
|
+
static normalizeName(name) {
|
|
1305
|
+
const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
|
|
1306
|
+
if (normalized.length === 0) {
|
|
1307
|
+
throw new CLIError("Patch name is required");
|
|
1308
|
+
}
|
|
1309
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
1310
|
+
throw new CLIError("Patch name must not contain path separators");
|
|
1311
|
+
}
|
|
1312
|
+
if (normalized.includes("..")) {
|
|
1313
|
+
throw new CLIError("Patch name must not contain '..'");
|
|
1314
|
+
}
|
|
1315
|
+
if (/[\x00-\x1f\x7f]/.test(normalized)) {
|
|
1316
|
+
throw new CLIError("Patch name must not contain control characters");
|
|
1317
|
+
}
|
|
1318
|
+
if (!/^[a-z0-9_-]+$/.test(normalized)) {
|
|
1319
|
+
throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
|
|
1320
|
+
}
|
|
1321
|
+
if (normalized.length > MAX_NAME_LENGTH) {
|
|
1322
|
+
throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
|
|
1323
|
+
}
|
|
1324
|
+
return normalized;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
|
|
1328
|
+
* file creation. On a millisecond-stamp collision, mints a later stamp
|
|
1329
|
+
* and retries. Returns the created path.
|
|
1330
|
+
*/
|
|
1331
|
+
create(name) {
|
|
1332
|
+
const normalized = _PatchCreator.normalizeName(name);
|
|
1333
|
+
if (!import_fs5.default.existsSync(this.patchFolder)) {
|
|
1334
|
+
import_fs5.default.mkdirSync(this.patchFolder, { recursive: true });
|
|
1335
|
+
}
|
|
1336
|
+
let stamp = Date.now();
|
|
1337
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
1338
|
+
const filePath = import_path4.default.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
|
|
1339
|
+
try {
|
|
1340
|
+
import_fs5.default.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
|
|
1341
|
+
return filePath;
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
if (error && error.code === "EEXIST") {
|
|
1344
|
+
stamp += 1;
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
throw error;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
throw new CLIError("Unable to create patch file: too many filename collisions");
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
549
1353
|
|
|
550
1354
|
// framework/SQLRunner.ts
|
|
551
1355
|
var BaseSQLRunner = class {
|
|
@@ -761,12 +1565,122 @@ ${sql}
|
|
|
761
1565
|
});
|
|
762
1566
|
}
|
|
763
1567
|
};
|
|
1568
|
+
var PgRunner = class _PgRunner extends BaseSQLRunner {
|
|
1569
|
+
constructor(connection) {
|
|
1570
|
+
super();
|
|
1571
|
+
this.connection = connection;
|
|
1572
|
+
}
|
|
1573
|
+
/** `?` -> `$1`, `$2`, ... outside of string literals / comments. */
|
|
1574
|
+
static toPositional(sql) {
|
|
1575
|
+
let out = "";
|
|
1576
|
+
let n = 0;
|
|
1577
|
+
let inSingle = false;
|
|
1578
|
+
let inDouble = false;
|
|
1579
|
+
let inLineComment = false;
|
|
1580
|
+
let inBlockComment = false;
|
|
1581
|
+
for (let i = 0; i < sql.length; i++) {
|
|
1582
|
+
const ch = sql[i];
|
|
1583
|
+
const next = sql[i + 1];
|
|
1584
|
+
if (inLineComment) {
|
|
1585
|
+
out += ch;
|
|
1586
|
+
if (ch === "\n") inLineComment = false;
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (inBlockComment) {
|
|
1590
|
+
out += ch;
|
|
1591
|
+
if (ch === "*" && next === "/") {
|
|
1592
|
+
out += next;
|
|
1593
|
+
i++;
|
|
1594
|
+
inBlockComment = false;
|
|
1595
|
+
}
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
if (inSingle) {
|
|
1599
|
+
out += ch;
|
|
1600
|
+
if (ch === "'") inSingle = false;
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (inDouble) {
|
|
1604
|
+
out += ch;
|
|
1605
|
+
if (ch === '"') inDouble = false;
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
if (ch === "-" && next === "-") {
|
|
1609
|
+
out += ch;
|
|
1610
|
+
inLineComment = true;
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (ch === "/" && next === "*") {
|
|
1614
|
+
out += ch + next;
|
|
1615
|
+
i++;
|
|
1616
|
+
inBlockComment = true;
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
if (ch === "'") {
|
|
1620
|
+
out += ch;
|
|
1621
|
+
inSingle = true;
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (ch === '"') {
|
|
1625
|
+
out += ch;
|
|
1626
|
+
inDouble = true;
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (ch === "?") {
|
|
1630
|
+
out += `$${++n}`;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
out += ch;
|
|
1634
|
+
}
|
|
1635
|
+
return out;
|
|
1636
|
+
}
|
|
1637
|
+
run(sql, params) {
|
|
1638
|
+
return __async(this, null, function* () {
|
|
1639
|
+
if (params.length > 0) {
|
|
1640
|
+
return yield this.connection.query(_PgRunner.toPositional(sql), params);
|
|
1641
|
+
}
|
|
1642
|
+
return yield this.connection.query(sql);
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
_query(_0) {
|
|
1646
|
+
return __async(this, arguments, function* (sql, params = []) {
|
|
1647
|
+
const result = yield this.run(sql, params);
|
|
1648
|
+
return [result.rows, result];
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
_execute(_0) {
|
|
1652
|
+
return __async(this, arguments, function* (sql, params = []) {
|
|
1653
|
+
var _a;
|
|
1654
|
+
const result = yield this.run(sql, params);
|
|
1655
|
+
return [{ changes: (_a = result.rowCount) != null ? _a : 0, lastID: void 0 }, result];
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
_end() {
|
|
1659
|
+
return __async(this, null, function* () {
|
|
1660
|
+
if (this.connection && typeof this.connection.end === "function") {
|
|
1661
|
+
yield this.connection.end();
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
764
1666
|
|
|
765
1667
|
// framework/MigrationRunner.ts
|
|
766
|
-
function
|
|
1668
|
+
function toError2(error) {
|
|
767
1669
|
if (error instanceof Error) return error;
|
|
768
1670
|
return new Error(String(error));
|
|
769
1671
|
}
|
|
1672
|
+
function makeRunner(database, conn) {
|
|
1673
|
+
switch (database) {
|
|
1674
|
+
case "sql":
|
|
1675
|
+
return new SQLRunner(conn);
|
|
1676
|
+
case "sqlite":
|
|
1677
|
+
return new SQLiteRunner(conn);
|
|
1678
|
+
case "pg":
|
|
1679
|
+
return new PgRunner(conn);
|
|
1680
|
+
default:
|
|
1681
|
+
throw ConfigurationError.unknownDatabaseType(database);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
770
1684
|
function loadMigrationConfig(configFile) {
|
|
771
1685
|
const reader = new FileMigrationConfigReader(configFile);
|
|
772
1686
|
return reader.loadFile();
|
|
@@ -779,14 +1693,17 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
779
1693
|
return __async(this, null, function* () {
|
|
780
1694
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
781
1695
|
const config = configReader.loadFile();
|
|
1696
|
+
let factoryOwnsConnection = false;
|
|
782
1697
|
if (!conn) {
|
|
783
1698
|
conn = yield this.createConnection(config);
|
|
1699
|
+
factoryOwnsConnection = true;
|
|
784
1700
|
}
|
|
785
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1701
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
786
1702
|
});
|
|
787
1703
|
}
|
|
788
1704
|
static createConnection(config) {
|
|
789
1705
|
return __async(this, null, function* () {
|
|
1706
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
790
1707
|
let conn = null;
|
|
791
1708
|
switch (config.database) {
|
|
792
1709
|
case "sql":
|
|
@@ -797,23 +1714,42 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
797
1714
|
password: process.env.SQL_PASSWORD
|
|
798
1715
|
}, config.sql);
|
|
799
1716
|
try {
|
|
800
|
-
|
|
1717
|
+
const mysql = yield import("mysql2/promise");
|
|
1718
|
+
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
801
1719
|
return conn;
|
|
802
1720
|
} catch (error) {
|
|
803
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1721
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
804
1722
|
}
|
|
805
1723
|
case "sqlite":
|
|
806
1724
|
if (!config.sqlite) {
|
|
807
1725
|
throw ConfigurationError.missingDatabaseConfiguration("sqlite");
|
|
808
1726
|
}
|
|
809
1727
|
try {
|
|
810
|
-
|
|
1728
|
+
const sqlite = yield import("sqlite");
|
|
1729
|
+
const sqlite3 = yield import("sqlite3");
|
|
1730
|
+
conn = yield ((_d = (_c = sqlite.default) == null ? void 0 : _c.open) != null ? _d : sqlite.open)({
|
|
811
1731
|
filename: config.sqlite.database,
|
|
812
|
-
driver: sqlite3.Database
|
|
1732
|
+
driver: (_f = (_e = sqlite3.default) == null ? void 0 : _e.Database) != null ? _f : sqlite3.Database
|
|
813
1733
|
});
|
|
814
1734
|
return conn;
|
|
815
1735
|
} catch (error) {
|
|
816
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1736
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
1737
|
+
}
|
|
1738
|
+
case "pg":
|
|
1739
|
+
if (!config.pg && !process.env.DATABASE_URL) {
|
|
1740
|
+
throw ConfigurationError.missingDatabaseConfiguration("pg");
|
|
1741
|
+
}
|
|
1742
|
+
try {
|
|
1743
|
+
const pgcfg = (_g = config.pg) != null ? _g : {};
|
|
1744
|
+
const connectionString = (_h = pgcfg.connectionString) != null ? _h : process.env.DATABASE_URL;
|
|
1745
|
+
const settings2 = connectionString ? { connectionString, ssl: pgcfg.ssl } : __spreadProps(__spreadValues({}, pgcfg), { password: (_i = pgcfg.password) != null ? _i : process.env.PG_PASSWORD });
|
|
1746
|
+
const pg = yield import("pg");
|
|
1747
|
+
const Client = (_k = (_j = pg.default) == null ? void 0 : _j.Client) != null ? _k : pg.Client;
|
|
1748
|
+
conn = new Client(settings2);
|
|
1749
|
+
yield conn.connect();
|
|
1750
|
+
return conn;
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
817
1753
|
}
|
|
818
1754
|
default:
|
|
819
1755
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -827,7 +1763,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
827
1763
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
828
1764
|
});
|
|
829
1765
|
}
|
|
830
|
-
create(config, conn) {
|
|
1766
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
831
1767
|
return __async(this, null, function* () {
|
|
832
1768
|
let sqlrunner;
|
|
833
1769
|
let driverConnection = conn;
|
|
@@ -835,42 +1771,34 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
835
1771
|
sqlrunner = conn;
|
|
836
1772
|
driverConnection = null;
|
|
837
1773
|
} else {
|
|
838
|
-
|
|
839
|
-
case "sql":
|
|
840
|
-
sqlrunner = new SQLRunner(conn);
|
|
841
|
-
break;
|
|
842
|
-
case "sqlite":
|
|
843
|
-
sqlrunner = new SQLiteRunner(conn);
|
|
844
|
-
break;
|
|
845
|
-
default:
|
|
846
|
-
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
847
|
-
}
|
|
1774
|
+
sqlrunner = makeRunner(config.database, conn);
|
|
848
1775
|
}
|
|
849
1776
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
850
1777
|
const read_strategy = this.getReadStategy(config);
|
|
851
1778
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
852
|
-
|
|
853
|
-
|
|
1779
|
+
const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
|
|
1780
|
+
try {
|
|
1781
|
+
yield runner.setup();
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
if (factoryOwnsConnection) {
|
|
1784
|
+
try {
|
|
1785
|
+
yield sqlrunner.end();
|
|
1786
|
+
} catch (e) {
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
throw error;
|
|
1790
|
+
}
|
|
1791
|
+
return runner;
|
|
854
1792
|
});
|
|
855
1793
|
}
|
|
856
1794
|
createEmpty(config) {
|
|
857
1795
|
return __async(this, null, function* () {
|
|
858
1796
|
const conn = null;
|
|
859
|
-
|
|
860
|
-
switch (config.database) {
|
|
861
|
-
case "sql":
|
|
862
|
-
sqlrunner = new SQLRunner(conn);
|
|
863
|
-
break;
|
|
864
|
-
case "sqlite":
|
|
865
|
-
sqlrunner = new SQLiteRunner(conn);
|
|
866
|
-
break;
|
|
867
|
-
default:
|
|
868
|
-
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
869
|
-
}
|
|
1797
|
+
const sqlrunner = makeRunner(config.database, conn);
|
|
870
1798
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
871
1799
|
const read_strategy = this.getReadStategy(config);
|
|
872
1800
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
873
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1801
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
874
1802
|
});
|
|
875
1803
|
}
|
|
876
1804
|
getReadStategy(config) {
|
|
@@ -879,39 +1807,85 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
879
1807
|
return MySqlDialectParser;
|
|
880
1808
|
case "sqlite":
|
|
881
1809
|
return SqliteDialectParser;
|
|
1810
|
+
case "pg":
|
|
1811
|
+
return PgDialectParser;
|
|
882
1812
|
default:
|
|
883
1813
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
884
1814
|
}
|
|
885
1815
|
}
|
|
886
1816
|
};
|
|
887
1817
|
var MySQLMigrationRunner = class {
|
|
888
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1818
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
889
1819
|
this.config = config;
|
|
890
1820
|
this.directory = directory;
|
|
891
1821
|
this.setupRunner = setupRunner;
|
|
892
1822
|
this.sqlrunner = sqlrunner;
|
|
893
1823
|
this.connection = connection;
|
|
1824
|
+
this.preflightEnabled = preflightEnabled;
|
|
1825
|
+
/**
|
|
1826
|
+
* Memoized in-flight preflight promise. Simultaneous or repeated calls
|
|
1827
|
+
* to setup() on one runner execute the preflight (migration table setup
|
|
1828
|
+
* + patch application) exactly once. Cleared after rejection so a caller
|
|
1829
|
+
* may retry after fixing the cause.
|
|
1830
|
+
*/
|
|
1831
|
+
this.preflightPromise = null;
|
|
1832
|
+
this.lastPatchResults = [];
|
|
894
1833
|
}
|
|
895
1834
|
setup() {
|
|
1835
|
+
return __async(this, null, function* () {
|
|
1836
|
+
if (!this.preflightEnabled) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
if (!this.preflightPromise) {
|
|
1840
|
+
this.preflightPromise = this.runPreflight();
|
|
1841
|
+
this.preflightPromise.catch(() => {
|
|
1842
|
+
this.preflightPromise = null;
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
return this.preflightPromise;
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
runPreflight() {
|
|
896
1849
|
return __async(this, null, function* () {
|
|
897
1850
|
try {
|
|
898
1851
|
yield this.setupRunner.setup();
|
|
899
1852
|
} catch (error) {
|
|
900
|
-
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0,
|
|
1853
|
+
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
|
|
901
1854
|
}
|
|
1855
|
+
const patchRunner = new PatchRunner(this.sqlrunner, this.config);
|
|
1856
|
+
this.lastPatchResults = yield patchRunner.applyPending();
|
|
902
1857
|
});
|
|
903
1858
|
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Delegates to the same idempotent preflight; returns the results of the
|
|
1861
|
+
* patch pass that ran (or is running) for this runner.
|
|
1862
|
+
*/
|
|
1863
|
+
applyPendingPatches() {
|
|
1864
|
+
return __async(this, null, function* () {
|
|
1865
|
+
yield this.setup();
|
|
1866
|
+
return this.lastPatchResults;
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Scaffolds a new ledger patch file and returns the created path.
|
|
1871
|
+
* Never connects to a database.
|
|
1872
|
+
*/
|
|
1873
|
+
createPatch(name) {
|
|
1874
|
+
const creator = new PatchCreator(resolvePatchFolder(this.config));
|
|
1875
|
+
return creator.create(name);
|
|
1876
|
+
}
|
|
904
1877
|
terminate() {
|
|
905
1878
|
return __async(this, null, function* () {
|
|
906
1879
|
try {
|
|
907
1880
|
yield this.setupRunner.teardown();
|
|
908
1881
|
} catch (error) {
|
|
909
|
-
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0,
|
|
1882
|
+
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
|
|
910
1883
|
}
|
|
911
1884
|
});
|
|
912
1885
|
}
|
|
913
1886
|
getMigrationsHistory() {
|
|
914
1887
|
return __async(this, null, function* () {
|
|
1888
|
+
yield this.setup();
|
|
915
1889
|
try {
|
|
916
1890
|
const results = yield this.sqlrunner.query(`
|
|
917
1891
|
select *
|
|
@@ -919,16 +1893,17 @@ var MySQLMigrationRunner = class {
|
|
|
919
1893
|
`);
|
|
920
1894
|
return results[0];
|
|
921
1895
|
} catch (error) {
|
|
922
|
-
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0,
|
|
1896
|
+
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
|
|
923
1897
|
}
|
|
924
1898
|
});
|
|
925
1899
|
}
|
|
926
1900
|
getMigrations() {
|
|
927
1901
|
return __async(this, null, function* () {
|
|
1902
|
+
yield this.setup();
|
|
928
1903
|
try {
|
|
929
1904
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
930
1905
|
} catch (error) {
|
|
931
|
-
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0,
|
|
1906
|
+
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
|
|
932
1907
|
}
|
|
933
1908
|
});
|
|
934
1909
|
}
|
|
@@ -941,7 +1916,7 @@ var MySQLMigrationRunner = class {
|
|
|
941
1916
|
if (error instanceof MigrationExecutionError) {
|
|
942
1917
|
throw error;
|
|
943
1918
|
}
|
|
944
|
-
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0,
|
|
1919
|
+
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
|
|
945
1920
|
}
|
|
946
1921
|
});
|
|
947
1922
|
}
|
|
@@ -954,12 +1929,13 @@ var MySQLMigrationRunner = class {
|
|
|
954
1929
|
if (error instanceof MigrationExecutionError) {
|
|
955
1930
|
throw error;
|
|
956
1931
|
}
|
|
957
|
-
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0,
|
|
1932
|
+
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
|
|
958
1933
|
}
|
|
959
1934
|
});
|
|
960
1935
|
}
|
|
961
1936
|
migrate(migrationNodes, forward) {
|
|
962
1937
|
return __async(this, null, function* () {
|
|
1938
|
+
yield this.setup();
|
|
963
1939
|
for (let node of migrationNodes) {
|
|
964
1940
|
try {
|
|
965
1941
|
if (forward) {
|
|
@@ -972,7 +1948,7 @@ var MySQLMigrationRunner = class {
|
|
|
972
1948
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
973
1949
|
node.name || String(node),
|
|
974
1950
|
forward ? node.up_sql() : node.down_sql(),
|
|
975
|
-
|
|
1951
|
+
toError2(error)
|
|
976
1952
|
);
|
|
977
1953
|
}
|
|
978
1954
|
}
|
|
@@ -980,6 +1956,7 @@ var MySQLMigrationRunner = class {
|
|
|
980
1956
|
}
|
|
981
1957
|
reset() {
|
|
982
1958
|
return __async(this, null, function* () {
|
|
1959
|
+
yield this.setup();
|
|
983
1960
|
try {
|
|
984
1961
|
let migrations = yield this.getMigrations();
|
|
985
1962
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -991,7 +1968,7 @@ var MySQLMigrationRunner = class {
|
|
|
991
1968
|
if (error instanceof MigrationExecutionError) {
|
|
992
1969
|
throw error;
|
|
993
1970
|
}
|
|
994
|
-
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0,
|
|
1971
|
+
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
|
|
995
1972
|
}
|
|
996
1973
|
});
|
|
997
1974
|
}
|
|
@@ -1000,7 +1977,7 @@ var MySQLMigrationRunner = class {
|
|
|
1000
1977
|
const creator = new MigrationCreator(this.config);
|
|
1001
1978
|
creator.create(name);
|
|
1002
1979
|
} catch (error) {
|
|
1003
|
-
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0,
|
|
1980
|
+
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
|
|
1004
1981
|
}
|
|
1005
1982
|
}
|
|
1006
1983
|
close() {
|
|
@@ -1009,7 +1986,7 @@ var MySQLMigrationRunner = class {
|
|
|
1009
1986
|
try {
|
|
1010
1987
|
yield this.sqlrunner.end();
|
|
1011
1988
|
} catch (error) {
|
|
1012
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
1989
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
1013
1990
|
}
|
|
1014
1991
|
}
|
|
1015
1992
|
});
|
|
@@ -1023,7 +2000,7 @@ var MySQLMigrationRunner = class {
|
|
|
1023
2000
|
return __async(this, null, function* () {
|
|
1024
2001
|
try {
|
|
1025
2002
|
console.log(`Checking for ${config_file}`);
|
|
1026
|
-
const config_exist =
|
|
2003
|
+
const config_exist = import_fs6.default.existsSync(config_file);
|
|
1027
2004
|
if (!config_exist) {
|
|
1028
2005
|
console.log(`Creating ${config_file}`);
|
|
1029
2006
|
const default_config = {
|
|
@@ -1037,11 +2014,11 @@ var MySQLMigrationRunner = class {
|
|
|
1037
2014
|
"password": ""
|
|
1038
2015
|
}
|
|
1039
2016
|
};
|
|
1040
|
-
|
|
2017
|
+
import_fs6.default.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
|
|
1041
2018
|
console.log(`Created ${config_file}`);
|
|
1042
2019
|
}
|
|
1043
2020
|
} catch (error) {
|
|
1044
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
2021
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1045
2022
|
}
|
|
1046
2023
|
});
|
|
1047
2024
|
}
|
|
@@ -1052,7 +2029,7 @@ var FileMigrationConfigReader = class {
|
|
|
1052
2029
|
}
|
|
1053
2030
|
loadFile() {
|
|
1054
2031
|
try {
|
|
1055
|
-
const fileContent =
|
|
2032
|
+
const fileContent = import_fs6.default.readFileSync(this.configFile);
|
|
1056
2033
|
const config = JSON.parse(fileContent.toString());
|
|
1057
2034
|
if (!config.migration_folder) {
|
|
1058
2035
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1065,6 +2042,8 @@ var FileMigrationConfigReader = class {
|
|
|
1065
2042
|
config.database = "sql";
|
|
1066
2043
|
} else if (config.sqlite) {
|
|
1067
2044
|
config.database = "sqlite";
|
|
2045
|
+
} else if (config.pg) {
|
|
2046
|
+
config.database = "pg";
|
|
1068
2047
|
} else {
|
|
1069
2048
|
throw ConfigurationError.missingRequiredProperty("database");
|
|
1070
2049
|
}
|
|
@@ -1074,7 +2053,7 @@ var FileMigrationConfigReader = class {
|
|
|
1074
2053
|
if (error instanceof ConfigurationError) {
|
|
1075
2054
|
throw error;
|
|
1076
2055
|
}
|
|
1077
|
-
const err =
|
|
2056
|
+
const err = toError2(error);
|
|
1078
2057
|
if (err.message.includes("ENOENT")) {
|
|
1079
2058
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1080
2059
|
}
|
|
@@ -1091,16 +2070,16 @@ var MigrationCreator = class {
|
|
|
1091
2070
|
throw new CLIError("Migration name is required");
|
|
1092
2071
|
}
|
|
1093
2072
|
try {
|
|
1094
|
-
if (!
|
|
1095
|
-
|
|
2073
|
+
if (!import_fs6.default.existsSync(this.config.migration_folder)) {
|
|
2074
|
+
import_fs6.default.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1096
2075
|
}
|
|
1097
2076
|
const now_timestamp = Date.now();
|
|
1098
2077
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1099
2078
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1100
|
-
|
|
2079
|
+
import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1101
2080
|
-- Write your up migration here
|
|
1102
2081
|
`.trim());
|
|
1103
|
-
|
|
2082
|
+
import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1104
2083
|
-- Write your down migration here
|
|
1105
2084
|
`.trim());
|
|
1106
2085
|
console.log(`Created migration files:`);
|
|
@@ -1110,16 +2089,16 @@ var MigrationCreator = class {
|
|
|
1110
2089
|
if (error instanceof CLIError) {
|
|
1111
2090
|
throw error;
|
|
1112
2091
|
}
|
|
1113
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2092
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1114
2093
|
}
|
|
1115
2094
|
}
|
|
1116
2095
|
};
|
|
1117
2096
|
|
|
1118
2097
|
// framework/SeedRunner.ts
|
|
1119
|
-
var
|
|
1120
|
-
var
|
|
2098
|
+
var import_fs7 = __toESM(require("fs"));
|
|
2099
|
+
var import_path5 = __toESM(require("path"));
|
|
1121
2100
|
var import_url = require("url");
|
|
1122
|
-
var
|
|
2101
|
+
var import_ajv2 = __toESM(require("ajv"));
|
|
1123
2102
|
var import_ajv_formats = __toESM(require("ajv-formats"));
|
|
1124
2103
|
var import_api = require("tsx/esm/api");
|
|
1125
2104
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1128,10 +2107,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1128
2107
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1129
2108
|
}
|
|
1130
2109
|
function walkForFile(rootDir, fileName) {
|
|
1131
|
-
if (!
|
|
1132
|
-
const entries =
|
|
2110
|
+
if (!import_fs7.default.existsSync(rootDir)) return null;
|
|
2111
|
+
const entries = import_fs7.default.readdirSync(rootDir, { withFileTypes: true });
|
|
1133
2112
|
for (const entry of entries) {
|
|
1134
|
-
const full =
|
|
2113
|
+
const full = import_path5.default.join(rootDir, entry.name);
|
|
1135
2114
|
if (entry.isDirectory()) {
|
|
1136
2115
|
const found = walkForFile(full, fileName);
|
|
1137
2116
|
if (found) return found;
|
|
@@ -1225,39 +2204,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1225
2204
|
}
|
|
1226
2205
|
function loadJson(filePath) {
|
|
1227
2206
|
return __async(this, null, function* () {
|
|
1228
|
-
const content = yield
|
|
2207
|
+
const content = yield import_fs7.default.promises.readFile(filePath, "utf8");
|
|
1229
2208
|
return JSON.parse(content);
|
|
1230
2209
|
});
|
|
1231
2210
|
}
|
|
1232
2211
|
function createValidator() {
|
|
1233
|
-
const
|
|
1234
|
-
(0, import_ajv_formats.default)(
|
|
1235
|
-
return
|
|
2212
|
+
const ajv2 = new import_ajv2.default({ allErrors: true, strict: false });
|
|
2213
|
+
(0, import_ajv_formats.default)(ajv2);
|
|
2214
|
+
return ajv2;
|
|
1236
2215
|
}
|
|
1237
2216
|
function validateData(schemaPath, data, validate, log) {
|
|
1238
2217
|
return __async(this, null, function* () {
|
|
1239
2218
|
if (!validate || !schemaPath) return;
|
|
1240
|
-
const content = yield
|
|
2219
|
+
const content = yield import_fs7.default.promises.readFile(schemaPath, "utf8");
|
|
1241
2220
|
const schema = JSON.parse(content);
|
|
1242
|
-
const
|
|
1243
|
-
const validateFn =
|
|
2221
|
+
const ajv2 = createValidator();
|
|
2222
|
+
const validateFn = ajv2.compile(schema);
|
|
1244
2223
|
const ok = validateFn(data);
|
|
1245
2224
|
if (!ok) {
|
|
1246
2225
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1247
|
-
throw new Error(`Seed data validation failed: ${
|
|
2226
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1248
2227
|
}
|
|
1249
2228
|
});
|
|
1250
2229
|
}
|
|
1251
2230
|
function runSqlSeed(runner, resolved, direction) {
|
|
1252
2231
|
return __async(this, null, function* () {
|
|
1253
2232
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1254
|
-
const sql = yield
|
|
2233
|
+
const sql = yield import_fs7.default.promises.readFile(sqlPath, "utf8");
|
|
1255
2234
|
yield runner.query(sql);
|
|
1256
2235
|
});
|
|
1257
2236
|
}
|
|
1258
2237
|
function loadSeedModule(modulePath) {
|
|
1259
2238
|
return __async(this, null, function* () {
|
|
1260
|
-
const resolved =
|
|
2239
|
+
const resolved = import_path5.default.resolve(modulePath);
|
|
1261
2240
|
if (resolved.endsWith(".ts")) {
|
|
1262
2241
|
const fileUrl = (0, import_url.pathToFileURL)(resolved).href;
|
|
1263
2242
|
return (0, import_api.tsImport)(fileUrl, fileUrl);
|
|
@@ -1378,10 +2357,23 @@ function createSeedFactory(options) {
|
|
|
1378
2357
|
}
|
|
1379
2358
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1380
2359
|
0 && (module.exports = {
|
|
2360
|
+
MigrationError,
|
|
1381
2361
|
MigrationRunner,
|
|
1382
2362
|
MigrationRunnerFactory,
|
|
2363
|
+
PatchConflictError,
|
|
2364
|
+
PatchCreator,
|
|
2365
|
+
PatchError,
|
|
2366
|
+
PatchExecutionError,
|
|
2367
|
+
PatchIntegrityError,
|
|
2368
|
+
PatchRunner,
|
|
2369
|
+
PatchValidationError,
|
|
2370
|
+
PgRunner,
|
|
2371
|
+
SQLRunner,
|
|
2372
|
+
SQLiteRunner,
|
|
1383
2373
|
createSeedFactory,
|
|
1384
2374
|
loadMigrationConfig,
|
|
2375
|
+
resolvePatchFolder,
|
|
2376
|
+
resolvePatchTable,
|
|
1385
2377
|
runSeedsWithRunner
|
|
1386
2378
|
});
|
|
1387
2379
|
//# sourceMappingURL=index.js.map
|