@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/cli.mjs
CHANGED
|
@@ -121,7 +121,7 @@ var init_MigrationFilter = __esm({
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// framework/errors.ts
|
|
124
|
-
var MigrationError, ConfigurationError, DatabaseConnectionError, MigrationExecutionError, CLIError;
|
|
124
|
+
var MigrationError, ConfigurationError, DatabaseConnectionError, MigrationExecutionError, PatchError, PatchValidationError, PatchIntegrityError, PatchConflictError, PatchExecutionError, CLIError;
|
|
125
125
|
var init_errors = __esm({
|
|
126
126
|
"framework/errors.ts"() {
|
|
127
127
|
MigrationError = class _MigrationError extends Error {
|
|
@@ -236,6 +236,54 @@ ${originalError.message}`;
|
|
|
236
236
|
return new _MigrationExecutionError(message);
|
|
237
237
|
}
|
|
238
238
|
};
|
|
239
|
+
PatchError = class _PatchError extends MigrationError {
|
|
240
|
+
constructor(message, patchFile, patchKey) {
|
|
241
|
+
super(`Patch Error: ${message}`);
|
|
242
|
+
this.patchFile = patchFile;
|
|
243
|
+
this.patchKey = patchKey;
|
|
244
|
+
this.name = "PatchError";
|
|
245
|
+
Object.setPrototypeOf(this, _PatchError.prototype);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
PatchValidationError = class _PatchValidationError extends PatchError {
|
|
249
|
+
constructor(message, patchFile, patchKey) {
|
|
250
|
+
super(message, patchFile, patchKey);
|
|
251
|
+
this.name = "PatchValidationError";
|
|
252
|
+
Object.setPrototypeOf(this, _PatchValidationError.prototype);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
PatchIntegrityError = class _PatchIntegrityError extends PatchError {
|
|
256
|
+
constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
|
|
257
|
+
super(message, patchFile, patchKey);
|
|
258
|
+
this.expectedChecksum = expectedChecksum;
|
|
259
|
+
this.actualChecksum = actualChecksum;
|
|
260
|
+
this.name = "PatchIntegrityError";
|
|
261
|
+
Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
PatchConflictError = class _PatchConflictError extends PatchError {
|
|
265
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
|
|
266
|
+
super(message, patchFile, patchKey);
|
|
267
|
+
this.operationIndex = operationIndex;
|
|
268
|
+
this.operationVerb = operationVerb;
|
|
269
|
+
this.migrationKeys = migrationKeys;
|
|
270
|
+
this.observedRowCounts = observedRowCounts;
|
|
271
|
+
this.name = "PatchConflictError";
|
|
272
|
+
Object.setPrototypeOf(this, _PatchConflictError.prototype);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
PatchExecutionError = class _PatchExecutionError extends PatchError {
|
|
276
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
|
|
277
|
+
super(originalError ? `${message}
|
|
278
|
+
Original Error:
|
|
279
|
+
${originalError.message}` : message, patchFile, patchKey);
|
|
280
|
+
this.operationIndex = operationIndex;
|
|
281
|
+
this.operationVerb = operationVerb;
|
|
282
|
+
this.originalError = originalError;
|
|
283
|
+
this.name = "PatchExecutionError";
|
|
284
|
+
Object.setPrototypeOf(this, _PatchExecutionError.prototype);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
239
287
|
CLIError = class _CLIError extends MigrationError {
|
|
240
288
|
constructor(message) {
|
|
241
289
|
super(`CLI Error: ${message}`);
|
|
@@ -412,13 +460,47 @@ var init_SqlMigrationBuilder = __esm({
|
|
|
412
460
|
}
|
|
413
461
|
});
|
|
414
462
|
|
|
415
|
-
// framework/
|
|
463
|
+
// framework/MigrationManifest.ts
|
|
416
464
|
import fs from "fs";
|
|
417
465
|
import path from "path";
|
|
466
|
+
function canonicalMigrationKey(value) {
|
|
467
|
+
return value.replace(/(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i, "").toLowerCase();
|
|
468
|
+
}
|
|
469
|
+
function resolveMigrationFile(directory, baseName, direction, dialect) {
|
|
470
|
+
const dialectExt = dialect === "sql" ? "mysql" : dialect;
|
|
471
|
+
const dialectFile = path.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
472
|
+
if (fs.existsSync(dialectFile)) return dialectFile;
|
|
473
|
+
const genericFile = path.join(directory, `${baseName}.${direction}.sql`);
|
|
474
|
+
if (fs.existsSync(genericFile)) return genericFile;
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
function loadMigrationManifest(directory, dialect) {
|
|
478
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
479
|
+
if (!fs.existsSync(directory)) return manifest;
|
|
480
|
+
const files = fs.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name);
|
|
481
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
482
|
+
files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
|
|
483
|
+
uniqueKeys.forEach((key) => {
|
|
484
|
+
manifest.set(key, {
|
|
485
|
+
key,
|
|
486
|
+
upFile: resolveMigrationFile(directory, key, "up", dialect),
|
|
487
|
+
downFile: resolveMigrationFile(directory, key, "down", dialect)
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
return manifest;
|
|
491
|
+
}
|
|
492
|
+
var init_MigrationManifest = __esm({
|
|
493
|
+
"framework/MigrationManifest.ts"() {
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// framework/MigrationDirectoryReader.ts
|
|
498
|
+
import fs2 from "fs";
|
|
418
499
|
var MigrationDirectoryReader;
|
|
419
500
|
var init_MigrationDirectoryReader = __esm({
|
|
420
501
|
"framework/MigrationDirectoryReader.ts"() {
|
|
421
502
|
init_SqlMigrationBuilder();
|
|
503
|
+
init_MigrationManifest();
|
|
422
504
|
MigrationDirectoryReader = class {
|
|
423
505
|
constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
|
|
424
506
|
this.directory = directory;
|
|
@@ -429,27 +511,23 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
429
511
|
/**
|
|
430
512
|
* Resolves the appropriate file for a migration based on dialect.
|
|
431
513
|
* Priority: dialect-specific file > generic file
|
|
514
|
+
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
432
515
|
*/
|
|
433
516
|
resolveFile(baseName, direction) {
|
|
434
|
-
|
|
435
|
-
const dialectFile = path.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
436
|
-
if (fs.existsSync(dialectFile)) return dialectFile;
|
|
437
|
-
const genericFile = path.join(this.directory, `${baseName}.${direction}.sql`);
|
|
438
|
-
if (fs.existsSync(genericFile)) return genericFile;
|
|
439
|
-
return null;
|
|
517
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
440
518
|
}
|
|
441
519
|
/**
|
|
442
520
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
443
521
|
*/
|
|
444
522
|
isDialectSpecific(filePath) {
|
|
445
|
-
return /\.(mysql|sqlite)\.(up|down)\.sql$/i.test(filePath);
|
|
523
|
+
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
446
524
|
}
|
|
447
525
|
loadMigrations(table, connection) {
|
|
448
|
-
|
|
449
|
-
const dir_content =
|
|
526
|
+
fs2.existsSync(this.directory) || fs2.mkdirSync(this.directory);
|
|
527
|
+
const dir_content = fs2.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
|
|
450
528
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
451
529
|
dir_content.forEach((file) => {
|
|
452
|
-
const key = file
|
|
530
|
+
const key = canonicalMigrationKey(file);
|
|
453
531
|
uniqueKeys.add(key);
|
|
454
532
|
});
|
|
455
533
|
const migration_sorter = {};
|
|
@@ -489,14 +567,14 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
489
567
|
return builder;
|
|
490
568
|
}
|
|
491
569
|
sql_up(file) {
|
|
492
|
-
let content =
|
|
570
|
+
let content = fs2.readFileSync(file).toString();
|
|
493
571
|
if (!this.isDialectSpecific(file)) {
|
|
494
572
|
content = this.read_strategy(content);
|
|
495
573
|
}
|
|
496
574
|
return content.trim();
|
|
497
575
|
}
|
|
498
576
|
sql_down(file) {
|
|
499
|
-
let content =
|
|
577
|
+
let content = fs2.readFileSync(file).toString();
|
|
500
578
|
if (!this.isDialectSpecific(file)) {
|
|
501
579
|
content = this.read_strategy(content);
|
|
502
580
|
}
|
|
@@ -506,11 +584,31 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
506
584
|
}
|
|
507
585
|
});
|
|
508
586
|
|
|
587
|
+
// framework/PatchTypes.ts
|
|
588
|
+
import path2 from "path";
|
|
589
|
+
function resolvePatchFolder(config) {
|
|
590
|
+
if (config.patch_folder) return config.patch_folder;
|
|
591
|
+
const dir = path2.dirname(config.migration_folder);
|
|
592
|
+
return dir === "." && !config.migration_folder.includes(path2.sep) && !config.migration_folder.includes("/") ? "patches" : path2.join(dir, "patches");
|
|
593
|
+
}
|
|
594
|
+
function resolvePatchTable(config) {
|
|
595
|
+
return config.patch_table || DEFAULT_PATCH_TABLE;
|
|
596
|
+
}
|
|
597
|
+
var PATCH_FORMAT_VERSION, DEFAULT_PATCH_TABLE, PATCH_FILENAME_REGEX;
|
|
598
|
+
var init_PatchTypes = __esm({
|
|
599
|
+
"framework/PatchTypes.ts"() {
|
|
600
|
+
PATCH_FORMAT_VERSION = 1;
|
|
601
|
+
DEFAULT_PATCH_TABLE = "proper_patches";
|
|
602
|
+
PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
509
606
|
// framework/MigrationSetup.ts
|
|
510
|
-
import
|
|
607
|
+
import fs3 from "fs";
|
|
511
608
|
var MigrationSetup;
|
|
512
609
|
var init_MigrationSetup = __esm({
|
|
513
610
|
"framework/MigrationSetup.ts"() {
|
|
611
|
+
init_PatchTypes();
|
|
514
612
|
MigrationSetup = class {
|
|
515
613
|
constructor(sqlrunner, config) {
|
|
516
614
|
this.sqlrunner = sqlrunner;
|
|
@@ -518,7 +616,7 @@ var init_MigrationSetup = __esm({
|
|
|
518
616
|
}
|
|
519
617
|
setup() {
|
|
520
618
|
return __async(this, null, function* () {
|
|
521
|
-
|
|
619
|
+
fs3.existsSync(this.config.migration_folder) || fs3.mkdirSync(this.config.migration_folder);
|
|
522
620
|
const tableName = this.config.migration_table;
|
|
523
621
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
524
622
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -526,6 +624,12 @@ var init_MigrationSetup = __esm({
|
|
|
526
624
|
up TEXT,
|
|
527
625
|
down TEXT,
|
|
528
626
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
627
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
628
|
+
id SERIAL PRIMARY KEY,
|
|
629
|
+
migration_key TEXT,
|
|
630
|
+
up TEXT,
|
|
631
|
+
down TEXT,
|
|
632
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
529
633
|
)` : `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
530
634
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
531
635
|
migration_key VARCHAR(255),
|
|
@@ -534,11 +638,39 @@ var init_MigrationSetup = __esm({
|
|
|
534
638
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
535
639
|
)`;
|
|
536
640
|
yield this.sqlrunner.query(createTableSql);
|
|
641
|
+
const patchTable = resolvePatchTable(this.config);
|
|
642
|
+
const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
643
|
+
migration_table TEXT NOT NULL,
|
|
644
|
+
patch_key TEXT NOT NULL,
|
|
645
|
+
checksum TEXT NOT NULL,
|
|
646
|
+
format_version INTEGER NOT NULL,
|
|
647
|
+
description TEXT NOT NULL,
|
|
648
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
649
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
650
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
651
|
+
migration_table TEXT NOT NULL,
|
|
652
|
+
patch_key TEXT NOT NULL,
|
|
653
|
+
checksum TEXT NOT NULL,
|
|
654
|
+
format_version INTEGER NOT NULL,
|
|
655
|
+
description TEXT NOT NULL,
|
|
656
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
657
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
658
|
+
)` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
659
|
+
migration_table VARCHAR(255) NOT NULL,
|
|
660
|
+
patch_key VARCHAR(255) NOT NULL,
|
|
661
|
+
checksum CHAR(64) NOT NULL,
|
|
662
|
+
format_version INT NOT NULL,
|
|
663
|
+
description TEXT NOT NULL,
|
|
664
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
665
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
666
|
+
)`;
|
|
667
|
+
yield this.sqlrunner.query(createPatchTableSql);
|
|
537
668
|
});
|
|
538
669
|
}
|
|
539
670
|
teardown() {
|
|
540
671
|
return __async(this, null, function* () {
|
|
541
672
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
673
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
542
674
|
});
|
|
543
675
|
}
|
|
544
676
|
};
|
|
@@ -572,33 +704,735 @@ function MySqlDialectParser(sql) {
|
|
|
572
704
|
}
|
|
573
705
|
return result;
|
|
574
706
|
}
|
|
575
|
-
function
|
|
576
|
-
const
|
|
577
|
-
let result = "";
|
|
578
|
-
let isInSqliteBlock = true;
|
|
579
|
-
const startSqliteRegex = /^\s*--\s*\[\s*sqlite\s*\]\s*$/i;
|
|
707
|
+
function markerDialectParser(names) {
|
|
708
|
+
const startRegex = new RegExp(`^\\s*--\\s*\\[\\s*(${names.join("|")})\\s*\\]\\s*$`, "i");
|
|
580
709
|
const anyDialectRegex = /^\s*--\s*\[\s*\w+\s*\]\s*$/i;
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
710
|
+
return function(sql) {
|
|
711
|
+
const lines = sql.split("\n");
|
|
712
|
+
let result = "";
|
|
713
|
+
let capturing = true;
|
|
714
|
+
for (const line of lines) {
|
|
715
|
+
const trimmed = line.trim();
|
|
716
|
+
if (startRegex.test(trimmed)) {
|
|
717
|
+
capturing = true;
|
|
718
|
+
result += line + "\n";
|
|
719
|
+
continue;
|
|
720
|
+
} else if (anyDialectRegex.test(trimmed)) {
|
|
721
|
+
capturing = false;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (!capturing && line.toLowerCase().includes("create index")) {
|
|
725
|
+
capturing = true;
|
|
726
|
+
}
|
|
727
|
+
if (capturing) {
|
|
728
|
+
result += line + "\n";
|
|
729
|
+
}
|
|
590
730
|
}
|
|
591
|
-
|
|
592
|
-
|
|
731
|
+
return result;
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
var SqliteDialectParser, PgDialectParser;
|
|
735
|
+
var init_MigrationDialectParser = __esm({
|
|
736
|
+
"framework/MigrationDialectParser.ts"() {
|
|
737
|
+
SqliteDialectParser = markerDialectParser(["sqlite"]);
|
|
738
|
+
PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// framework/PatchValidator.ts
|
|
743
|
+
import Ajv from "ajv";
|
|
744
|
+
import { parseDocument } from "yaml";
|
|
745
|
+
function fail(message, file, patchKey) {
|
|
746
|
+
throw new PatchValidationError(message, file, patchKey);
|
|
747
|
+
}
|
|
748
|
+
function checkMigrationKey(value, context, file, patchKey) {
|
|
749
|
+
if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
|
|
750
|
+
if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
|
|
751
|
+
if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
|
|
752
|
+
if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
|
|
753
|
+
fail(`${context}: migration key length out of bounds`, file, patchKey);
|
|
754
|
+
}
|
|
755
|
+
return canonicalMigrationKey(value);
|
|
756
|
+
}
|
|
757
|
+
function assertStrictYaml(doc, file, patchKey) {
|
|
758
|
+
if (doc.errors.length > 0) {
|
|
759
|
+
fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
|
|
760
|
+
}
|
|
761
|
+
if (doc.warnings.length > 0) {
|
|
762
|
+
fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
|
|
763
|
+
}
|
|
764
|
+
const visit = (node) => {
|
|
765
|
+
var _a, _b;
|
|
766
|
+
if (node == null || typeof node !== "object") return;
|
|
767
|
+
if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
|
|
768
|
+
fail("YAML aliases are not permitted in patch files", file, patchKey);
|
|
593
769
|
}
|
|
594
|
-
if (
|
|
595
|
-
|
|
770
|
+
if (node.anchor) {
|
|
771
|
+
fail("YAML anchors are not permitted in patch files", file, patchKey);
|
|
772
|
+
}
|
|
773
|
+
if (node.tag && ![
|
|
774
|
+
"tag:yaml.org,2002:str",
|
|
775
|
+
"tag:yaml.org,2002:int",
|
|
776
|
+
"tag:yaml.org,2002:bool",
|
|
777
|
+
"tag:yaml.org,2002:null",
|
|
778
|
+
"tag:yaml.org,2002:map",
|
|
779
|
+
"tag:yaml.org,2002:seq"
|
|
780
|
+
].includes(node.tag)) {
|
|
781
|
+
fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
|
|
782
|
+
}
|
|
783
|
+
if (Array.isArray(node.items)) {
|
|
784
|
+
for (const item of node.items) {
|
|
785
|
+
if (item && typeof item === "object" && "key" in item) {
|
|
786
|
+
const keyValue = (_b = item.key) == null ? void 0 : _b.value;
|
|
787
|
+
if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
|
|
788
|
+
visit(item.key);
|
|
789
|
+
visit(item.value);
|
|
790
|
+
} else {
|
|
791
|
+
visit(item);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
visit(doc.contents);
|
|
797
|
+
}
|
|
798
|
+
function parsePatchContent(content, fileName, patchKey) {
|
|
799
|
+
var _a;
|
|
800
|
+
const doc = parseDocument(content, {
|
|
801
|
+
uniqueKeys: true,
|
|
802
|
+
// duplicate mapping keys become errors
|
|
803
|
+
merge: false,
|
|
804
|
+
schema: "core",
|
|
805
|
+
version: "1.2"
|
|
806
|
+
});
|
|
807
|
+
assertStrictYaml(doc, fileName, patchKey);
|
|
808
|
+
const raw = doc.toJS({ mapAsMap: false });
|
|
809
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
810
|
+
fail("Patch document must be a YAML mapping", fileName, patchKey);
|
|
811
|
+
}
|
|
812
|
+
if (!validateSchema(raw)) {
|
|
813
|
+
const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
|
|
814
|
+
const anyRaw = raw;
|
|
815
|
+
if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
|
|
816
|
+
fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
596
817
|
}
|
|
818
|
+
fail(`Schema validation failed: ${detail}`, fileName, patchKey);
|
|
597
819
|
}
|
|
598
|
-
|
|
820
|
+
const parsed = raw;
|
|
821
|
+
if (parsed.version !== PATCH_FORMAT_VERSION) {
|
|
822
|
+
fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
823
|
+
}
|
|
824
|
+
const description = parsed.description.trim();
|
|
825
|
+
if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
|
|
826
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
827
|
+
fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
|
|
828
|
+
}
|
|
829
|
+
const operations = parsed.operations.map((op, index) => {
|
|
830
|
+
const verbs = Object.keys(op);
|
|
831
|
+
const verb = verbs[0];
|
|
832
|
+
const context = `operation ${index} (${verb})`;
|
|
833
|
+
switch (verb) {
|
|
834
|
+
case "rename_migration": {
|
|
835
|
+
const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
|
|
836
|
+
const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
|
|
837
|
+
if (from === to) {
|
|
838
|
+
fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
|
|
839
|
+
}
|
|
840
|
+
return { verb: "rename_migration", from, to };
|
|
841
|
+
}
|
|
842
|
+
case "mark_applied":
|
|
843
|
+
return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
|
|
844
|
+
case "unmark_applied":
|
|
845
|
+
return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
|
|
846
|
+
default:
|
|
847
|
+
fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
return { version: parsed.version, description, operations };
|
|
599
851
|
}
|
|
600
|
-
|
|
601
|
-
|
|
852
|
+
function validatePatchPlan(patches, manifest) {
|
|
853
|
+
const renames = [];
|
|
854
|
+
for (const patch of patches) {
|
|
855
|
+
patch.operations.forEach((op, index) => {
|
|
856
|
+
if (op.verb === "rename_migration") {
|
|
857
|
+
renames.push({ from: op.from, to: op.to, file: patch.fileName });
|
|
858
|
+
} else if (op.verb === "mark_applied") {
|
|
859
|
+
if (!manifest.has(op.key)) {
|
|
860
|
+
throw new PatchValidationError(
|
|
861
|
+
`operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
|
|
862
|
+
patch.fileName,
|
|
863
|
+
patch.patchKey
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (renames.length === 0) return;
|
|
870
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
871
|
+
renames.forEach((r) => {
|
|
872
|
+
mentioned.add(r.from);
|
|
873
|
+
mentioned.add(r.to);
|
|
874
|
+
});
|
|
875
|
+
const finalKey = (start) => {
|
|
876
|
+
let current = start;
|
|
877
|
+
for (const r of renames) {
|
|
878
|
+
if (current === r.from) current = r.to;
|
|
879
|
+
}
|
|
880
|
+
return current;
|
|
881
|
+
};
|
|
882
|
+
for (const key of mentioned) {
|
|
883
|
+
const finish = finalKey(key);
|
|
884
|
+
if (manifest.has(key)) {
|
|
885
|
+
if (finish !== key) {
|
|
886
|
+
throw new PatchValidationError(
|
|
887
|
+
`rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
} else {
|
|
891
|
+
if (!manifest.has(finish)) {
|
|
892
|
+
throw new PatchValidationError(
|
|
893
|
+
`rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
var MAX_DESCRIPTION_LENGTH, MAX_MIGRATION_KEY_LENGTH, migrationKeySchema, patchSchema, ajv, validateSchema;
|
|
900
|
+
var init_PatchValidator = __esm({
|
|
901
|
+
"framework/PatchValidator.ts"() {
|
|
902
|
+
init_errors();
|
|
903
|
+
init_MigrationManifest();
|
|
904
|
+
init_PatchTypes();
|
|
905
|
+
MAX_DESCRIPTION_LENGTH = 500;
|
|
906
|
+
MAX_MIGRATION_KEY_LENGTH = 255;
|
|
907
|
+
migrationKeySchema = {
|
|
908
|
+
type: "string",
|
|
909
|
+
minLength: 1,
|
|
910
|
+
maxLength: MAX_MIGRATION_KEY_LENGTH
|
|
911
|
+
};
|
|
912
|
+
patchSchema = {
|
|
913
|
+
type: "object",
|
|
914
|
+
additionalProperties: false,
|
|
915
|
+
required: ["version", "description", "operations"],
|
|
916
|
+
properties: {
|
|
917
|
+
version: { type: "integer" },
|
|
918
|
+
description: { type: "string" },
|
|
919
|
+
operations: {
|
|
920
|
+
type: "array",
|
|
921
|
+
minItems: 1,
|
|
922
|
+
items: {
|
|
923
|
+
type: "object",
|
|
924
|
+
additionalProperties: false,
|
|
925
|
+
minProperties: 1,
|
|
926
|
+
maxProperties: 1,
|
|
927
|
+
properties: {
|
|
928
|
+
rename_migration: {
|
|
929
|
+
type: "object",
|
|
930
|
+
additionalProperties: false,
|
|
931
|
+
required: ["from", "to"],
|
|
932
|
+
properties: { from: migrationKeySchema, to: migrationKeySchema }
|
|
933
|
+
},
|
|
934
|
+
mark_applied: {
|
|
935
|
+
type: "object",
|
|
936
|
+
additionalProperties: false,
|
|
937
|
+
required: ["key"],
|
|
938
|
+
properties: { key: migrationKeySchema }
|
|
939
|
+
},
|
|
940
|
+
unmark_applied: {
|
|
941
|
+
type: "object",
|
|
942
|
+
additionalProperties: false,
|
|
943
|
+
required: ["key"],
|
|
944
|
+
properties: { key: migrationKeySchema }
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
ajv = new Ajv({ allErrors: true, strict: true });
|
|
952
|
+
validateSchema = ajv.compile(patchSchema);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// framework/PatchDirectoryReader.ts
|
|
957
|
+
import crypto from "crypto";
|
|
958
|
+
import fs4 from "fs";
|
|
959
|
+
import path3 from "path";
|
|
960
|
+
var PatchDirectoryReader;
|
|
961
|
+
var init_PatchDirectoryReader = __esm({
|
|
962
|
+
"framework/PatchDirectoryReader.ts"() {
|
|
963
|
+
init_errors();
|
|
964
|
+
init_PatchValidator();
|
|
965
|
+
init_PatchTypes();
|
|
966
|
+
PatchDirectoryReader = class {
|
|
967
|
+
constructor(directory) {
|
|
968
|
+
this.directory = directory;
|
|
969
|
+
}
|
|
970
|
+
loadPatches() {
|
|
971
|
+
if (!fs4.existsSync(this.directory)) return [];
|
|
972
|
+
const entries = fs4.readdirSync(this.directory, { withFileTypes: true });
|
|
973
|
+
const patchFiles = [];
|
|
974
|
+
for (const entry of entries) {
|
|
975
|
+
if (!entry.name.endsWith(".yaml")) continue;
|
|
976
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
977
|
+
if (entry.isSymbolicLink()) {
|
|
978
|
+
throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
|
|
979
|
+
}
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
patchFiles.push(entry.name);
|
|
983
|
+
}
|
|
984
|
+
patchFiles.sort((a, b) => {
|
|
985
|
+
const stampA = parseInt(a.slice(0, 13), 10);
|
|
986
|
+
const stampB = parseInt(b.slice(0, 13), 10);
|
|
987
|
+
if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
|
|
988
|
+
return stampA - stampB;
|
|
989
|
+
}
|
|
990
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
991
|
+
});
|
|
992
|
+
return patchFiles.map((fileName) => {
|
|
993
|
+
const match = PATCH_FILENAME_REGEX.exec(fileName);
|
|
994
|
+
if (!match) {
|
|
995
|
+
throw new PatchValidationError(
|
|
996
|
+
`invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
|
|
997
|
+
fileName
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const patchKey = fileName.slice(0, -".yaml".length);
|
|
1001
|
+
const filePath = path3.join(this.directory, fileName);
|
|
1002
|
+
const bytes = fs4.readFileSync(filePath);
|
|
1003
|
+
const checksum = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
1004
|
+
const content = bytes.toString("utf8");
|
|
1005
|
+
const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
|
|
1006
|
+
return { patchKey, fileName, filePath, checksum, version, description, operations };
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// framework/PatchRunner.ts
|
|
1014
|
+
function toError(error) {
|
|
1015
|
+
if (error instanceof Error) return error;
|
|
1016
|
+
return new Error(String(error));
|
|
1017
|
+
}
|
|
1018
|
+
function extractRows(result) {
|
|
1019
|
+
if (!Array.isArray(result)) return [];
|
|
1020
|
+
if (Array.isArray(result[0])) return result[0];
|
|
1021
|
+
if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
|
|
1022
|
+
return result;
|
|
1023
|
+
}
|
|
1024
|
+
if (result[0] == null) return [];
|
|
1025
|
+
return [result[0]];
|
|
1026
|
+
}
|
|
1027
|
+
var PatchRunner;
|
|
1028
|
+
var init_PatchRunner = __esm({
|
|
1029
|
+
"framework/PatchRunner.ts"() {
|
|
1030
|
+
init_PatchDirectoryReader();
|
|
1031
|
+
init_PatchValidator();
|
|
1032
|
+
init_MigrationManifest();
|
|
1033
|
+
init_PatchTypes();
|
|
1034
|
+
init_errors();
|
|
1035
|
+
PatchRunner = class {
|
|
1036
|
+
constructor(sqlrunner, config) {
|
|
1037
|
+
this.sqlrunner = sqlrunner;
|
|
1038
|
+
this.config = config;
|
|
1039
|
+
this.patchTable = resolvePatchTable(config);
|
|
1040
|
+
this.migrationTable = config.migration_table;
|
|
1041
|
+
this.dialect = config.database;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Discovers, validates, and applies every unapplied patch in order.
|
|
1045
|
+
* Each unapplied patch is its own transaction; earlier committed patches
|
|
1046
|
+
* remain committed if a later patch fails.
|
|
1047
|
+
*/
|
|
1048
|
+
applyPending() {
|
|
1049
|
+
return __async(this, null, function* () {
|
|
1050
|
+
const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
|
|
1051
|
+
const patches = reader.loadPatches();
|
|
1052
|
+
const history = yield this.loadHistory();
|
|
1053
|
+
if (patches.length === 0 && history.length === 0) {
|
|
1054
|
+
return [];
|
|
1055
|
+
}
|
|
1056
|
+
const byKey = new Map(patches.map((p) => [p.patchKey, p]));
|
|
1057
|
+
for (const row of history) {
|
|
1058
|
+
const file = byKey.get(row.patch_key);
|
|
1059
|
+
if (!file) {
|
|
1060
|
+
throw new PatchIntegrityError(
|
|
1061
|
+
`applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
|
|
1062
|
+
void 0,
|
|
1063
|
+
row.patch_key
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
if (file.checksum !== row.checksum) {
|
|
1067
|
+
throw new PatchIntegrityError(
|
|
1068
|
+
`applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
|
|
1069
|
+
file.fileName,
|
|
1070
|
+
row.patch_key,
|
|
1071
|
+
row.checksum,
|
|
1072
|
+
file.checksum
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
|
|
1077
|
+
validatePatchPlan(patches, manifest);
|
|
1078
|
+
const appliedKeys = new Set(history.map((r) => r.patch_key));
|
|
1079
|
+
const results = [];
|
|
1080
|
+
for (const patch of patches) {
|
|
1081
|
+
if (appliedKeys.has(patch.patchKey)) {
|
|
1082
|
+
results.push({
|
|
1083
|
+
patchKey: patch.patchKey,
|
|
1084
|
+
fileName: patch.fileName,
|
|
1085
|
+
status: "already_applied",
|
|
1086
|
+
operations: []
|
|
1087
|
+
});
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
results.push(yield this.applyOne(patch, manifest));
|
|
1091
|
+
}
|
|
1092
|
+
return results;
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
loadHistory() {
|
|
1096
|
+
return __async(this, null, function* () {
|
|
1097
|
+
try {
|
|
1098
|
+
const result = yield this.sqlrunner.query(
|
|
1099
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
|
|
1100
|
+
[this.migrationTable]
|
|
1101
|
+
);
|
|
1102
|
+
return extractRows(result);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
throw new PatchExecutionError(
|
|
1105
|
+
`failed to read patch history from '${this.patchTable}'`,
|
|
1106
|
+
void 0,
|
|
1107
|
+
void 0,
|
|
1108
|
+
void 0,
|
|
1109
|
+
void 0,
|
|
1110
|
+
toError(error)
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
beginSql() {
|
|
1116
|
+
switch (this.dialect) {
|
|
1117
|
+
case "sqlite":
|
|
1118
|
+
return "BEGIN IMMEDIATE";
|
|
1119
|
+
case "pg":
|
|
1120
|
+
return "BEGIN";
|
|
1121
|
+
default:
|
|
1122
|
+
return "START TRANSACTION";
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
begin(patch) {
|
|
1126
|
+
return __async(this, null, function* () {
|
|
1127
|
+
const deadline = Date.now() + 1e4;
|
|
1128
|
+
while (true) {
|
|
1129
|
+
try {
|
|
1130
|
+
yield this.sqlrunner.execute(this.beginSql());
|
|
1131
|
+
return;
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
const message = toError(error).message;
|
|
1134
|
+
if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
|
|
1135
|
+
yield new Promise((resolve) => setTimeout(resolve, 50));
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
throw new PatchExecutionError(
|
|
1139
|
+
"failed to start patch transaction",
|
|
1140
|
+
patch.fileName,
|
|
1141
|
+
patch.patchKey,
|
|
1142
|
+
void 0,
|
|
1143
|
+
void 0,
|
|
1144
|
+
toError(error)
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
rollbackQuietly() {
|
|
1151
|
+
return __async(this, null, function* () {
|
|
1152
|
+
try {
|
|
1153
|
+
yield this.sqlrunner.execute("ROLLBACK");
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
applyOne(patch, manifest) {
|
|
1159
|
+
return __async(this, null, function* () {
|
|
1160
|
+
yield this.begin(patch);
|
|
1161
|
+
try {
|
|
1162
|
+
yield this.sqlrunner.execute(
|
|
1163
|
+
`INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
|
|
1164
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1165
|
+
[this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
|
|
1166
|
+
);
|
|
1167
|
+
} catch (claimError) {
|
|
1168
|
+
yield this.rollbackQuietly();
|
|
1169
|
+
const committed = yield this.findCommittedRow(patch.patchKey);
|
|
1170
|
+
if (committed) {
|
|
1171
|
+
if (committed.checksum === patch.checksum) {
|
|
1172
|
+
return {
|
|
1173
|
+
patchKey: patch.patchKey,
|
|
1174
|
+
fileName: patch.fileName,
|
|
1175
|
+
status: "already_applied",
|
|
1176
|
+
operations: []
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
throw new PatchIntegrityError(
|
|
1180
|
+
`patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
|
|
1181
|
+
patch.fileName,
|
|
1182
|
+
patch.patchKey,
|
|
1183
|
+
committed.checksum,
|
|
1184
|
+
patch.checksum
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
throw new PatchExecutionError(
|
|
1188
|
+
"failed to claim patch-history row",
|
|
1189
|
+
patch.fileName,
|
|
1190
|
+
patch.patchKey,
|
|
1191
|
+
void 0,
|
|
1192
|
+
void 0,
|
|
1193
|
+
toError(claimError)
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
const operationResults = [];
|
|
1197
|
+
try {
|
|
1198
|
+
for (let index = 0; index < patch.operations.length; index++) {
|
|
1199
|
+
operationResults.push(
|
|
1200
|
+
yield this.applyOperation(patch, patch.operations[index], index, manifest)
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
yield this.sqlrunner.execute("COMMIT");
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
yield this.rollbackQuietly();
|
|
1206
|
+
if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
1209
|
+
throw new PatchExecutionError(
|
|
1210
|
+
"patch application failed",
|
|
1211
|
+
patch.fileName,
|
|
1212
|
+
patch.patchKey,
|
|
1213
|
+
void 0,
|
|
1214
|
+
void 0,
|
|
1215
|
+
toError(error)
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
patchKey: patch.patchKey,
|
|
1220
|
+
fileName: patch.fileName,
|
|
1221
|
+
status: "applied",
|
|
1222
|
+
operations: operationResults
|
|
1223
|
+
};
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
findCommittedRow(patchKey) {
|
|
1227
|
+
return __async(this, null, function* () {
|
|
1228
|
+
const result = yield this.sqlrunner.query(
|
|
1229
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
|
|
1230
|
+
[this.migrationTable, patchKey]
|
|
1231
|
+
);
|
|
1232
|
+
const list = extractRows(result);
|
|
1233
|
+
return list.length > 0 ? list[0] : null;
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
countRows(key) {
|
|
1237
|
+
return __async(this, null, function* () {
|
|
1238
|
+
var _a, _b, _c;
|
|
1239
|
+
const result = yield this.sqlrunner.query(
|
|
1240
|
+
`SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1241
|
+
[key]
|
|
1242
|
+
);
|
|
1243
|
+
const rows = extractRows(result);
|
|
1244
|
+
const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
|
|
1245
|
+
return Number(value != null ? value : 0);
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
conflict(patch, index, verb, message, keys, counts) {
|
|
1249
|
+
throw new PatchConflictError(
|
|
1250
|
+
`operation ${index} (${verb}): ${message}`,
|
|
1251
|
+
patch.fileName,
|
|
1252
|
+
patch.patchKey,
|
|
1253
|
+
index,
|
|
1254
|
+
verb,
|
|
1255
|
+
keys,
|
|
1256
|
+
counts
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
applyOperation(patch, op, index, manifest) {
|
|
1260
|
+
return __async(this, null, function* () {
|
|
1261
|
+
var _a, _b, _c, _d;
|
|
1262
|
+
try {
|
|
1263
|
+
switch (op.verb) {
|
|
1264
|
+
case "rename_migration": {
|
|
1265
|
+
const fromCount = yield this.countRows(op.from);
|
|
1266
|
+
const toCount = yield this.countRows(op.to);
|
|
1267
|
+
const counts = { [op.from]: fromCount, [op.to]: toCount };
|
|
1268
|
+
if (fromCount > 1 || toCount > 1) {
|
|
1269
|
+
this.conflict(
|
|
1270
|
+
patch,
|
|
1271
|
+
index,
|
|
1272
|
+
op.verb,
|
|
1273
|
+
`ledger corruption: duplicate rows for a migration key`,
|
|
1274
|
+
[op.from, op.to],
|
|
1275
|
+
counts
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (fromCount === 1 && toCount === 1) {
|
|
1279
|
+
this.conflict(
|
|
1280
|
+
patch,
|
|
1281
|
+
index,
|
|
1282
|
+
op.verb,
|
|
1283
|
+
`both '${op.from}' and '${op.to}' exist in the ledger`,
|
|
1284
|
+
[op.from, op.to],
|
|
1285
|
+
counts
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
if (fromCount === 0) {
|
|
1289
|
+
return { verb: op.verb, changed: false };
|
|
1290
|
+
}
|
|
1291
|
+
const target = manifest.get(op.to);
|
|
1292
|
+
if (target) {
|
|
1293
|
+
yield this.sqlrunner.execute(
|
|
1294
|
+
`UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
|
|
1295
|
+
[op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
|
|
1296
|
+
);
|
|
1297
|
+
} else {
|
|
1298
|
+
yield this.sqlrunner.execute(
|
|
1299
|
+
`UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
|
|
1300
|
+
[op.to, op.from]
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
return { verb: op.verb, changed: true };
|
|
1304
|
+
}
|
|
1305
|
+
case "mark_applied": {
|
|
1306
|
+
const count = yield this.countRows(op.key);
|
|
1307
|
+
if (count > 1) {
|
|
1308
|
+
this.conflict(
|
|
1309
|
+
patch,
|
|
1310
|
+
index,
|
|
1311
|
+
op.verb,
|
|
1312
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1313
|
+
[op.key],
|
|
1314
|
+
{ [op.key]: count }
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (count === 1) {
|
|
1318
|
+
return { verb: op.verb, changed: false };
|
|
1319
|
+
}
|
|
1320
|
+
const entry = manifest.get(op.key);
|
|
1321
|
+
yield this.sqlrunner.execute(
|
|
1322
|
+
`INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
|
|
1323
|
+
[op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
|
|
1324
|
+
);
|
|
1325
|
+
return { verb: op.verb, changed: true };
|
|
1326
|
+
}
|
|
1327
|
+
case "unmark_applied": {
|
|
1328
|
+
const count = yield this.countRows(op.key);
|
|
1329
|
+
if (count > 1) {
|
|
1330
|
+
this.conflict(
|
|
1331
|
+
patch,
|
|
1332
|
+
index,
|
|
1333
|
+
op.verb,
|
|
1334
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1335
|
+
[op.key],
|
|
1336
|
+
{ [op.key]: count }
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
if (count === 0) {
|
|
1340
|
+
return { verb: op.verb, changed: false };
|
|
1341
|
+
}
|
|
1342
|
+
yield this.sqlrunner.execute(
|
|
1343
|
+
`DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1344
|
+
[op.key]
|
|
1345
|
+
);
|
|
1346
|
+
return { verb: op.verb, changed: true };
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
if (error instanceof PatchConflictError) throw error;
|
|
1351
|
+
throw new PatchExecutionError(
|
|
1352
|
+
`operation failed`,
|
|
1353
|
+
patch.fileName,
|
|
1354
|
+
patch.patchKey,
|
|
1355
|
+
index,
|
|
1356
|
+
op.verb,
|
|
1357
|
+
toError(error)
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
// framework/PatchCreator.ts
|
|
1367
|
+
import fs5 from "fs";
|
|
1368
|
+
import path4 from "path";
|
|
1369
|
+
var MAX_NAME_LENGTH, SCAFFOLD, PatchCreator;
|
|
1370
|
+
var init_PatchCreator = __esm({
|
|
1371
|
+
"framework/PatchCreator.ts"() {
|
|
1372
|
+
init_errors();
|
|
1373
|
+
MAX_NAME_LENGTH = 120;
|
|
1374
|
+
SCAFFOLD = `version: 1
|
|
1375
|
+
description: TODO
|
|
1376
|
+
operations: []
|
|
1377
|
+
`;
|
|
1378
|
+
PatchCreator = class _PatchCreator {
|
|
1379
|
+
constructor(patchFolder) {
|
|
1380
|
+
this.patchFolder = patchFolder;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
|
|
1384
|
+
* Rejects empty results, path separators, `..`, control characters,
|
|
1385
|
+
* characters outside [a-z0-9_-], and names longer than 120 characters.
|
|
1386
|
+
*/
|
|
1387
|
+
static normalizeName(name) {
|
|
1388
|
+
const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
|
|
1389
|
+
if (normalized.length === 0) {
|
|
1390
|
+
throw new CLIError("Patch name is required");
|
|
1391
|
+
}
|
|
1392
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
1393
|
+
throw new CLIError("Patch name must not contain path separators");
|
|
1394
|
+
}
|
|
1395
|
+
if (normalized.includes("..")) {
|
|
1396
|
+
throw new CLIError("Patch name must not contain '..'");
|
|
1397
|
+
}
|
|
1398
|
+
if (/[\x00-\x1f\x7f]/.test(normalized)) {
|
|
1399
|
+
throw new CLIError("Patch name must not contain control characters");
|
|
1400
|
+
}
|
|
1401
|
+
if (!/^[a-z0-9_-]+$/.test(normalized)) {
|
|
1402
|
+
throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
|
|
1403
|
+
}
|
|
1404
|
+
if (normalized.length > MAX_NAME_LENGTH) {
|
|
1405
|
+
throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
|
|
1406
|
+
}
|
|
1407
|
+
return normalized;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
|
|
1411
|
+
* file creation. On a millisecond-stamp collision, mints a later stamp
|
|
1412
|
+
* and retries. Returns the created path.
|
|
1413
|
+
*/
|
|
1414
|
+
create(name) {
|
|
1415
|
+
const normalized = _PatchCreator.normalizeName(name);
|
|
1416
|
+
if (!fs5.existsSync(this.patchFolder)) {
|
|
1417
|
+
fs5.mkdirSync(this.patchFolder, { recursive: true });
|
|
1418
|
+
}
|
|
1419
|
+
let stamp = Date.now();
|
|
1420
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
1421
|
+
const filePath = path4.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
|
|
1422
|
+
try {
|
|
1423
|
+
fs5.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
|
|
1424
|
+
return filePath;
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
if (error && error.code === "EEXIST") {
|
|
1427
|
+
stamp += 1;
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
throw error;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
throw new CLIError("Unable to create patch file: too many filename collisions");
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
602
1436
|
}
|
|
603
1437
|
});
|
|
604
1438
|
|
|
@@ -606,7 +1440,7 @@ var init_MigrationDialectParser = __esm({
|
|
|
606
1440
|
function isPromiseLike(value) {
|
|
607
1441
|
return !!value && typeof value.then === "function";
|
|
608
1442
|
}
|
|
609
|
-
var BaseSQLRunner, SQLRunner, SQLiteRunner;
|
|
1443
|
+
var BaseSQLRunner, SQLRunner, SQLiteRunner, PgRunner;
|
|
610
1444
|
var init_SQLRunner = __esm({
|
|
611
1445
|
"framework/SQLRunner.ts"() {
|
|
612
1446
|
BaseSQLRunner = class {
|
|
@@ -819,18 +1653,125 @@ ${sql}
|
|
|
819
1653
|
});
|
|
820
1654
|
}
|
|
821
1655
|
};
|
|
1656
|
+
PgRunner = class _PgRunner extends BaseSQLRunner {
|
|
1657
|
+
constructor(connection) {
|
|
1658
|
+
super();
|
|
1659
|
+
this.connection = connection;
|
|
1660
|
+
}
|
|
1661
|
+
/** `?` -> `$1`, `$2`, ... outside of string literals / comments. */
|
|
1662
|
+
static toPositional(sql) {
|
|
1663
|
+
let out = "";
|
|
1664
|
+
let n = 0;
|
|
1665
|
+
let inSingle = false;
|
|
1666
|
+
let inDouble = false;
|
|
1667
|
+
let inLineComment = false;
|
|
1668
|
+
let inBlockComment = false;
|
|
1669
|
+
for (let i = 0; i < sql.length; i++) {
|
|
1670
|
+
const ch = sql[i];
|
|
1671
|
+
const next = sql[i + 1];
|
|
1672
|
+
if (inLineComment) {
|
|
1673
|
+
out += ch;
|
|
1674
|
+
if (ch === "\n") inLineComment = false;
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
if (inBlockComment) {
|
|
1678
|
+
out += ch;
|
|
1679
|
+
if (ch === "*" && next === "/") {
|
|
1680
|
+
out += next;
|
|
1681
|
+
i++;
|
|
1682
|
+
inBlockComment = false;
|
|
1683
|
+
}
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
if (inSingle) {
|
|
1687
|
+
out += ch;
|
|
1688
|
+
if (ch === "'") inSingle = false;
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
if (inDouble) {
|
|
1692
|
+
out += ch;
|
|
1693
|
+
if (ch === '"') inDouble = false;
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
if (ch === "-" && next === "-") {
|
|
1697
|
+
out += ch;
|
|
1698
|
+
inLineComment = true;
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
if (ch === "/" && next === "*") {
|
|
1702
|
+
out += ch + next;
|
|
1703
|
+
i++;
|
|
1704
|
+
inBlockComment = true;
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
if (ch === "'") {
|
|
1708
|
+
out += ch;
|
|
1709
|
+
inSingle = true;
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
if (ch === '"') {
|
|
1713
|
+
out += ch;
|
|
1714
|
+
inDouble = true;
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
if (ch === "?") {
|
|
1718
|
+
out += `$${++n}`;
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
out += ch;
|
|
1722
|
+
}
|
|
1723
|
+
return out;
|
|
1724
|
+
}
|
|
1725
|
+
run(sql, params) {
|
|
1726
|
+
return __async(this, null, function* () {
|
|
1727
|
+
if (params.length > 0) {
|
|
1728
|
+
return yield this.connection.query(_PgRunner.toPositional(sql), params);
|
|
1729
|
+
}
|
|
1730
|
+
return yield this.connection.query(sql);
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
_query(_0) {
|
|
1734
|
+
return __async(this, arguments, function* (sql, params = []) {
|
|
1735
|
+
const result = yield this.run(sql, params);
|
|
1736
|
+
return [result.rows, result];
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
_execute(_0) {
|
|
1740
|
+
return __async(this, arguments, function* (sql, params = []) {
|
|
1741
|
+
var _a;
|
|
1742
|
+
const result = yield this.run(sql, params);
|
|
1743
|
+
return [{ changes: (_a = result.rowCount) != null ? _a : 0, lastID: void 0 }, result];
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
_end() {
|
|
1747
|
+
return __async(this, null, function* () {
|
|
1748
|
+
if (this.connection && typeof this.connection.end === "function") {
|
|
1749
|
+
yield this.connection.end();
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
822
1754
|
}
|
|
823
1755
|
});
|
|
824
1756
|
|
|
825
1757
|
// framework/MigrationRunner.ts
|
|
826
|
-
import
|
|
827
|
-
|
|
828
|
-
import * as sqlite from "sqlite";
|
|
829
|
-
import * as sqlite3 from "sqlite3";
|
|
830
|
-
function toError(error) {
|
|
1758
|
+
import fs6 from "fs";
|
|
1759
|
+
function toError2(error) {
|
|
831
1760
|
if (error instanceof Error) return error;
|
|
832
1761
|
return new Error(String(error));
|
|
833
1762
|
}
|
|
1763
|
+
function makeRunner(database, conn) {
|
|
1764
|
+
switch (database) {
|
|
1765
|
+
case "sql":
|
|
1766
|
+
return new SQLRunner(conn);
|
|
1767
|
+
case "sqlite":
|
|
1768
|
+
return new SQLiteRunner(conn);
|
|
1769
|
+
case "pg":
|
|
1770
|
+
return new PgRunner(conn);
|
|
1771
|
+
default:
|
|
1772
|
+
throw ConfigurationError.unknownDatabaseType(database);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
834
1775
|
var MigrationRunnerFactory, MySQLMigrationRunner, FileMigrationConfigReader, MigrationCreator;
|
|
835
1776
|
var init_MigrationRunner = __esm({
|
|
836
1777
|
"framework/MigrationRunner.ts"() {
|
|
@@ -838,6 +1779,9 @@ var init_MigrationRunner = __esm({
|
|
|
838
1779
|
init_MigrationSetup();
|
|
839
1780
|
init_MigrationFilter();
|
|
840
1781
|
init_MigrationDialectParser();
|
|
1782
|
+
init_PatchRunner();
|
|
1783
|
+
init_PatchCreator();
|
|
1784
|
+
init_PatchTypes();
|
|
841
1785
|
init_SQLRunner();
|
|
842
1786
|
init_errors();
|
|
843
1787
|
MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
@@ -848,14 +1792,17 @@ var init_MigrationRunner = __esm({
|
|
|
848
1792
|
return __async(this, null, function* () {
|
|
849
1793
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
850
1794
|
const config = configReader.loadFile();
|
|
1795
|
+
let factoryOwnsConnection = false;
|
|
851
1796
|
if (!conn) {
|
|
852
1797
|
conn = yield this.createConnection(config);
|
|
1798
|
+
factoryOwnsConnection = true;
|
|
853
1799
|
}
|
|
854
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1800
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
855
1801
|
});
|
|
856
1802
|
}
|
|
857
1803
|
static createConnection(config) {
|
|
858
1804
|
return __async(this, null, function* () {
|
|
1805
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
859
1806
|
let conn = null;
|
|
860
1807
|
switch (config.database) {
|
|
861
1808
|
case "sql":
|
|
@@ -866,23 +1813,42 @@ var init_MigrationRunner = __esm({
|
|
|
866
1813
|
password: process.env.SQL_PASSWORD
|
|
867
1814
|
}, config.sql);
|
|
868
1815
|
try {
|
|
869
|
-
|
|
1816
|
+
const mysql = yield import("mysql2/promise");
|
|
1817
|
+
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
870
1818
|
return conn;
|
|
871
1819
|
} catch (error) {
|
|
872
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1820
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
873
1821
|
}
|
|
874
1822
|
case "sqlite":
|
|
875
1823
|
if (!config.sqlite) {
|
|
876
1824
|
throw ConfigurationError.missingDatabaseConfiguration("sqlite");
|
|
877
1825
|
}
|
|
878
1826
|
try {
|
|
879
|
-
|
|
1827
|
+
const sqlite = yield import("sqlite");
|
|
1828
|
+
const sqlite3 = yield import("sqlite3");
|
|
1829
|
+
conn = yield ((_d = (_c = sqlite.default) == null ? void 0 : _c.open) != null ? _d : sqlite.open)({
|
|
880
1830
|
filename: config.sqlite.database,
|
|
881
|
-
driver: sqlite3.Database
|
|
1831
|
+
driver: (_f = (_e = sqlite3.default) == null ? void 0 : _e.Database) != null ? _f : sqlite3.Database
|
|
882
1832
|
});
|
|
883
1833
|
return conn;
|
|
884
1834
|
} catch (error) {
|
|
885
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1835
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
1836
|
+
}
|
|
1837
|
+
case "pg":
|
|
1838
|
+
if (!config.pg && !process.env.DATABASE_URL) {
|
|
1839
|
+
throw ConfigurationError.missingDatabaseConfiguration("pg");
|
|
1840
|
+
}
|
|
1841
|
+
try {
|
|
1842
|
+
const pgcfg = (_g = config.pg) != null ? _g : {};
|
|
1843
|
+
const connectionString = (_h = pgcfg.connectionString) != null ? _h : process.env.DATABASE_URL;
|
|
1844
|
+
const settings2 = connectionString ? { connectionString, ssl: pgcfg.ssl } : __spreadProps(__spreadValues({}, pgcfg), { password: (_i = pgcfg.password) != null ? _i : process.env.PG_PASSWORD });
|
|
1845
|
+
const pg = yield import("pg");
|
|
1846
|
+
const Client = (_k = (_j = pg.default) == null ? void 0 : _j.Client) != null ? _k : pg.Client;
|
|
1847
|
+
conn = new Client(settings2);
|
|
1848
|
+
yield conn.connect();
|
|
1849
|
+
return conn;
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
886
1852
|
}
|
|
887
1853
|
default:
|
|
888
1854
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -896,7 +1862,7 @@ var init_MigrationRunner = __esm({
|
|
|
896
1862
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
897
1863
|
});
|
|
898
1864
|
}
|
|
899
|
-
create(config, conn) {
|
|
1865
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
900
1866
|
return __async(this, null, function* () {
|
|
901
1867
|
let sqlrunner;
|
|
902
1868
|
let driverConnection = conn;
|
|
@@ -904,42 +1870,34 @@ var init_MigrationRunner = __esm({
|
|
|
904
1870
|
sqlrunner = conn;
|
|
905
1871
|
driverConnection = null;
|
|
906
1872
|
} else {
|
|
907
|
-
|
|
908
|
-
case "sql":
|
|
909
|
-
sqlrunner = new SQLRunner(conn);
|
|
910
|
-
break;
|
|
911
|
-
case "sqlite":
|
|
912
|
-
sqlrunner = new SQLiteRunner(conn);
|
|
913
|
-
break;
|
|
914
|
-
default:
|
|
915
|
-
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
916
|
-
}
|
|
1873
|
+
sqlrunner = makeRunner(config.database, conn);
|
|
917
1874
|
}
|
|
918
1875
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
919
1876
|
const read_strategy = this.getReadStategy(config);
|
|
920
1877
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
921
|
-
|
|
922
|
-
|
|
1878
|
+
const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
|
|
1879
|
+
try {
|
|
1880
|
+
yield runner.setup();
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
if (factoryOwnsConnection) {
|
|
1883
|
+
try {
|
|
1884
|
+
yield sqlrunner.end();
|
|
1885
|
+
} catch (e) {
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
throw error;
|
|
1889
|
+
}
|
|
1890
|
+
return runner;
|
|
923
1891
|
});
|
|
924
1892
|
}
|
|
925
1893
|
createEmpty(config) {
|
|
926
1894
|
return __async(this, null, function* () {
|
|
927
1895
|
const conn = null;
|
|
928
|
-
|
|
929
|
-
switch (config.database) {
|
|
930
|
-
case "sql":
|
|
931
|
-
sqlrunner = new SQLRunner(conn);
|
|
932
|
-
break;
|
|
933
|
-
case "sqlite":
|
|
934
|
-
sqlrunner = new SQLiteRunner(conn);
|
|
935
|
-
break;
|
|
936
|
-
default:
|
|
937
|
-
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
938
|
-
}
|
|
1896
|
+
const sqlrunner = makeRunner(config.database, conn);
|
|
939
1897
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
940
1898
|
const read_strategy = this.getReadStategy(config);
|
|
941
1899
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
942
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1900
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
943
1901
|
});
|
|
944
1902
|
}
|
|
945
1903
|
getReadStategy(config) {
|
|
@@ -948,39 +1906,85 @@ var init_MigrationRunner = __esm({
|
|
|
948
1906
|
return MySqlDialectParser;
|
|
949
1907
|
case "sqlite":
|
|
950
1908
|
return SqliteDialectParser;
|
|
1909
|
+
case "pg":
|
|
1910
|
+
return PgDialectParser;
|
|
951
1911
|
default:
|
|
952
1912
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
953
1913
|
}
|
|
954
1914
|
}
|
|
955
1915
|
};
|
|
956
1916
|
MySQLMigrationRunner = class {
|
|
957
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1917
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
958
1918
|
this.config = config;
|
|
959
1919
|
this.directory = directory;
|
|
960
1920
|
this.setupRunner = setupRunner;
|
|
961
1921
|
this.sqlrunner = sqlrunner;
|
|
962
1922
|
this.connection = connection;
|
|
1923
|
+
this.preflightEnabled = preflightEnabled;
|
|
1924
|
+
/**
|
|
1925
|
+
* Memoized in-flight preflight promise. Simultaneous or repeated calls
|
|
1926
|
+
* to setup() on one runner execute the preflight (migration table setup
|
|
1927
|
+
* + patch application) exactly once. Cleared after rejection so a caller
|
|
1928
|
+
* may retry after fixing the cause.
|
|
1929
|
+
*/
|
|
1930
|
+
this.preflightPromise = null;
|
|
1931
|
+
this.lastPatchResults = [];
|
|
963
1932
|
}
|
|
964
1933
|
setup() {
|
|
1934
|
+
return __async(this, null, function* () {
|
|
1935
|
+
if (!this.preflightEnabled) {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
if (!this.preflightPromise) {
|
|
1939
|
+
this.preflightPromise = this.runPreflight();
|
|
1940
|
+
this.preflightPromise.catch(() => {
|
|
1941
|
+
this.preflightPromise = null;
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
return this.preflightPromise;
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
runPreflight() {
|
|
965
1948
|
return __async(this, null, function* () {
|
|
966
1949
|
try {
|
|
967
1950
|
yield this.setupRunner.setup();
|
|
968
1951
|
} catch (error) {
|
|
969
|
-
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0,
|
|
1952
|
+
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
|
|
970
1953
|
}
|
|
1954
|
+
const patchRunner = new PatchRunner(this.sqlrunner, this.config);
|
|
1955
|
+
this.lastPatchResults = yield patchRunner.applyPending();
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Delegates to the same idempotent preflight; returns the results of the
|
|
1960
|
+
* patch pass that ran (or is running) for this runner.
|
|
1961
|
+
*/
|
|
1962
|
+
applyPendingPatches() {
|
|
1963
|
+
return __async(this, null, function* () {
|
|
1964
|
+
yield this.setup();
|
|
1965
|
+
return this.lastPatchResults;
|
|
971
1966
|
});
|
|
972
1967
|
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Scaffolds a new ledger patch file and returns the created path.
|
|
1970
|
+
* Never connects to a database.
|
|
1971
|
+
*/
|
|
1972
|
+
createPatch(name) {
|
|
1973
|
+
const creator = new PatchCreator(resolvePatchFolder(this.config));
|
|
1974
|
+
return creator.create(name);
|
|
1975
|
+
}
|
|
973
1976
|
terminate() {
|
|
974
1977
|
return __async(this, null, function* () {
|
|
975
1978
|
try {
|
|
976
1979
|
yield this.setupRunner.teardown();
|
|
977
1980
|
} catch (error) {
|
|
978
|
-
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0,
|
|
1981
|
+
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
|
|
979
1982
|
}
|
|
980
1983
|
});
|
|
981
1984
|
}
|
|
982
1985
|
getMigrationsHistory() {
|
|
983
1986
|
return __async(this, null, function* () {
|
|
1987
|
+
yield this.setup();
|
|
984
1988
|
try {
|
|
985
1989
|
const results = yield this.sqlrunner.query(`
|
|
986
1990
|
select *
|
|
@@ -988,16 +1992,17 @@ var init_MigrationRunner = __esm({
|
|
|
988
1992
|
`);
|
|
989
1993
|
return results[0];
|
|
990
1994
|
} catch (error) {
|
|
991
|
-
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0,
|
|
1995
|
+
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
|
|
992
1996
|
}
|
|
993
1997
|
});
|
|
994
1998
|
}
|
|
995
1999
|
getMigrations() {
|
|
996
2000
|
return __async(this, null, function* () {
|
|
2001
|
+
yield this.setup();
|
|
997
2002
|
try {
|
|
998
2003
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
999
2004
|
} catch (error) {
|
|
1000
|
-
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0,
|
|
2005
|
+
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
|
|
1001
2006
|
}
|
|
1002
2007
|
});
|
|
1003
2008
|
}
|
|
@@ -1010,7 +2015,7 @@ var init_MigrationRunner = __esm({
|
|
|
1010
2015
|
if (error instanceof MigrationExecutionError) {
|
|
1011
2016
|
throw error;
|
|
1012
2017
|
}
|
|
1013
|
-
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0,
|
|
2018
|
+
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
|
|
1014
2019
|
}
|
|
1015
2020
|
});
|
|
1016
2021
|
}
|
|
@@ -1023,12 +2028,13 @@ var init_MigrationRunner = __esm({
|
|
|
1023
2028
|
if (error instanceof MigrationExecutionError) {
|
|
1024
2029
|
throw error;
|
|
1025
2030
|
}
|
|
1026
|
-
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0,
|
|
2031
|
+
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
|
|
1027
2032
|
}
|
|
1028
2033
|
});
|
|
1029
2034
|
}
|
|
1030
2035
|
migrate(migrationNodes, forward) {
|
|
1031
2036
|
return __async(this, null, function* () {
|
|
2037
|
+
yield this.setup();
|
|
1032
2038
|
for (let node of migrationNodes) {
|
|
1033
2039
|
try {
|
|
1034
2040
|
if (forward) {
|
|
@@ -1041,7 +2047,7 @@ var init_MigrationRunner = __esm({
|
|
|
1041
2047
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
1042
2048
|
node.name || String(node),
|
|
1043
2049
|
forward ? node.up_sql() : node.down_sql(),
|
|
1044
|
-
|
|
2050
|
+
toError2(error)
|
|
1045
2051
|
);
|
|
1046
2052
|
}
|
|
1047
2053
|
}
|
|
@@ -1049,6 +2055,7 @@ var init_MigrationRunner = __esm({
|
|
|
1049
2055
|
}
|
|
1050
2056
|
reset() {
|
|
1051
2057
|
return __async(this, null, function* () {
|
|
2058
|
+
yield this.setup();
|
|
1052
2059
|
try {
|
|
1053
2060
|
let migrations = yield this.getMigrations();
|
|
1054
2061
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -1060,7 +2067,7 @@ var init_MigrationRunner = __esm({
|
|
|
1060
2067
|
if (error instanceof MigrationExecutionError) {
|
|
1061
2068
|
throw error;
|
|
1062
2069
|
}
|
|
1063
|
-
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0,
|
|
2070
|
+
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
|
|
1064
2071
|
}
|
|
1065
2072
|
});
|
|
1066
2073
|
}
|
|
@@ -1069,7 +2076,7 @@ var init_MigrationRunner = __esm({
|
|
|
1069
2076
|
const creator = new MigrationCreator(this.config);
|
|
1070
2077
|
creator.create(name);
|
|
1071
2078
|
} catch (error) {
|
|
1072
|
-
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0,
|
|
2079
|
+
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
|
|
1073
2080
|
}
|
|
1074
2081
|
}
|
|
1075
2082
|
close() {
|
|
@@ -1078,7 +2085,7 @@ var init_MigrationRunner = __esm({
|
|
|
1078
2085
|
try {
|
|
1079
2086
|
yield this.sqlrunner.end();
|
|
1080
2087
|
} catch (error) {
|
|
1081
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
2088
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
1082
2089
|
}
|
|
1083
2090
|
}
|
|
1084
2091
|
});
|
|
@@ -1092,7 +2099,7 @@ var init_MigrationRunner = __esm({
|
|
|
1092
2099
|
return __async(this, null, function* () {
|
|
1093
2100
|
try {
|
|
1094
2101
|
console.log(`Checking for ${config_file}`);
|
|
1095
|
-
const config_exist =
|
|
2102
|
+
const config_exist = fs6.existsSync(config_file);
|
|
1096
2103
|
if (!config_exist) {
|
|
1097
2104
|
console.log(`Creating ${config_file}`);
|
|
1098
2105
|
const default_config = {
|
|
@@ -1106,11 +2113,11 @@ var init_MigrationRunner = __esm({
|
|
|
1106
2113
|
"password": ""
|
|
1107
2114
|
}
|
|
1108
2115
|
};
|
|
1109
|
-
|
|
2116
|
+
fs6.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
|
|
1110
2117
|
console.log(`Created ${config_file}`);
|
|
1111
2118
|
}
|
|
1112
2119
|
} catch (error) {
|
|
1113
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
2120
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1114
2121
|
}
|
|
1115
2122
|
});
|
|
1116
2123
|
}
|
|
@@ -1121,7 +2128,7 @@ var init_MigrationRunner = __esm({
|
|
|
1121
2128
|
}
|
|
1122
2129
|
loadFile() {
|
|
1123
2130
|
try {
|
|
1124
|
-
const fileContent =
|
|
2131
|
+
const fileContent = fs6.readFileSync(this.configFile);
|
|
1125
2132
|
const config = JSON.parse(fileContent.toString());
|
|
1126
2133
|
if (!config.migration_folder) {
|
|
1127
2134
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1134,6 +2141,8 @@ var init_MigrationRunner = __esm({
|
|
|
1134
2141
|
config.database = "sql";
|
|
1135
2142
|
} else if (config.sqlite) {
|
|
1136
2143
|
config.database = "sqlite";
|
|
2144
|
+
} else if (config.pg) {
|
|
2145
|
+
config.database = "pg";
|
|
1137
2146
|
} else {
|
|
1138
2147
|
throw ConfigurationError.missingRequiredProperty("database");
|
|
1139
2148
|
}
|
|
@@ -1143,7 +2152,7 @@ var init_MigrationRunner = __esm({
|
|
|
1143
2152
|
if (error instanceof ConfigurationError) {
|
|
1144
2153
|
throw error;
|
|
1145
2154
|
}
|
|
1146
|
-
const err =
|
|
2155
|
+
const err = toError2(error);
|
|
1147
2156
|
if (err.message.includes("ENOENT")) {
|
|
1148
2157
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1149
2158
|
}
|
|
@@ -1160,16 +2169,16 @@ var init_MigrationRunner = __esm({
|
|
|
1160
2169
|
throw new CLIError("Migration name is required");
|
|
1161
2170
|
}
|
|
1162
2171
|
try {
|
|
1163
|
-
if (!
|
|
1164
|
-
|
|
2172
|
+
if (!fs6.existsSync(this.config.migration_folder)) {
|
|
2173
|
+
fs6.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1165
2174
|
}
|
|
1166
2175
|
const now_timestamp = Date.now();
|
|
1167
2176
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1168
2177
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1169
|
-
|
|
2178
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1170
2179
|
-- Write your up migration here
|
|
1171
2180
|
`.trim());
|
|
1172
|
-
|
|
2181
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1173
2182
|
-- Write your down migration here
|
|
1174
2183
|
`.trim());
|
|
1175
2184
|
console.log(`Created migration files:`);
|
|
@@ -1179,7 +2188,7 @@ var init_MigrationRunner = __esm({
|
|
|
1179
2188
|
if (error instanceof CLIError) {
|
|
1180
2189
|
throw error;
|
|
1181
2190
|
}
|
|
1182
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2191
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1183
2192
|
}
|
|
1184
2193
|
}
|
|
1185
2194
|
};
|
|
@@ -1187,10 +2196,10 @@ var init_MigrationRunner = __esm({
|
|
|
1187
2196
|
});
|
|
1188
2197
|
|
|
1189
2198
|
// framework/SeedRunner.ts
|
|
1190
|
-
import
|
|
1191
|
-
import
|
|
2199
|
+
import fs7 from "fs";
|
|
2200
|
+
import path5 from "path";
|
|
1192
2201
|
import { pathToFileURL } from "url";
|
|
1193
|
-
import
|
|
2202
|
+
import Ajv2 from "ajv";
|
|
1194
2203
|
import addFormats from "ajv-formats";
|
|
1195
2204
|
import { tsImport } from "tsx/esm/api";
|
|
1196
2205
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1199,10 +2208,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1199
2208
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1200
2209
|
}
|
|
1201
2210
|
function walkForFile(rootDir, fileName) {
|
|
1202
|
-
if (!
|
|
1203
|
-
const entries =
|
|
2211
|
+
if (!fs7.existsSync(rootDir)) return null;
|
|
2212
|
+
const entries = fs7.readdirSync(rootDir, { withFileTypes: true });
|
|
1204
2213
|
for (const entry of entries) {
|
|
1205
|
-
const full =
|
|
2214
|
+
const full = path5.join(rootDir, entry.name);
|
|
1206
2215
|
if (entry.isDirectory()) {
|
|
1207
2216
|
const found = walkForFile(full, fileName);
|
|
1208
2217
|
if (found) return found;
|
|
@@ -1296,39 +2305,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1296
2305
|
}
|
|
1297
2306
|
function loadJson(filePath) {
|
|
1298
2307
|
return __async(this, null, function* () {
|
|
1299
|
-
const content = yield
|
|
2308
|
+
const content = yield fs7.promises.readFile(filePath, "utf8");
|
|
1300
2309
|
return JSON.parse(content);
|
|
1301
2310
|
});
|
|
1302
2311
|
}
|
|
1303
2312
|
function createValidator() {
|
|
1304
|
-
const
|
|
1305
|
-
addFormats(
|
|
1306
|
-
return
|
|
2313
|
+
const ajv2 = new Ajv2({ allErrors: true, strict: false });
|
|
2314
|
+
addFormats(ajv2);
|
|
2315
|
+
return ajv2;
|
|
1307
2316
|
}
|
|
1308
2317
|
function validateData(schemaPath, data, validate, log) {
|
|
1309
2318
|
return __async(this, null, function* () {
|
|
1310
2319
|
if (!validate || !schemaPath) return;
|
|
1311
|
-
const content = yield
|
|
2320
|
+
const content = yield fs7.promises.readFile(schemaPath, "utf8");
|
|
1312
2321
|
const schema = JSON.parse(content);
|
|
1313
|
-
const
|
|
1314
|
-
const validateFn =
|
|
2322
|
+
const ajv2 = createValidator();
|
|
2323
|
+
const validateFn = ajv2.compile(schema);
|
|
1315
2324
|
const ok = validateFn(data);
|
|
1316
2325
|
if (!ok) {
|
|
1317
2326
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1318
|
-
throw new Error(`Seed data validation failed: ${
|
|
2327
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1319
2328
|
}
|
|
1320
2329
|
});
|
|
1321
2330
|
}
|
|
1322
2331
|
function runSqlSeed(runner, resolved, direction) {
|
|
1323
2332
|
return __async(this, null, function* () {
|
|
1324
2333
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1325
|
-
const sql = yield
|
|
2334
|
+
const sql = yield fs7.promises.readFile(sqlPath, "utf8");
|
|
1326
2335
|
yield runner.query(sql);
|
|
1327
2336
|
});
|
|
1328
2337
|
}
|
|
1329
2338
|
function loadSeedModule(modulePath) {
|
|
1330
2339
|
return __async(this, null, function* () {
|
|
1331
|
-
const resolved =
|
|
2340
|
+
const resolved = path5.resolve(modulePath);
|
|
1332
2341
|
if (resolved.endsWith(".ts")) {
|
|
1333
2342
|
const fileUrl = pathToFileURL(resolved).href;
|
|
1334
2343
|
return tsImport(fileUrl, fileUrl);
|
|
@@ -1432,6 +2441,10 @@ var require_cli = __commonJS({
|
|
|
1432
2441
|
init_errors();
|
|
1433
2442
|
init_SeedRunner();
|
|
1434
2443
|
var args = MigrationCLIFactory.setup(process.argv);
|
|
2444
|
+
if (args.flags.help) {
|
|
2445
|
+
printUsage();
|
|
2446
|
+
process.exit(0);
|
|
2447
|
+
}
|
|
1435
2448
|
if (!args.commands || args.commands.length === 0) {
|
|
1436
2449
|
console.error("Error: No command specified");
|
|
1437
2450
|
printUsage();
|
|
@@ -1439,7 +2452,7 @@ var require_cli = __commonJS({
|
|
|
1439
2452
|
}
|
|
1440
2453
|
var commands = args.commands;
|
|
1441
2454
|
var command = commands[0];
|
|
1442
|
-
var load_database = !["init", "create", "help"].includes(command.toLowerCase());
|
|
2455
|
+
var load_database = !["init", "create", "help", "patch"].includes(command.toLowerCase());
|
|
1443
2456
|
var config_file = args.flags.config || "proper.json";
|
|
1444
2457
|
if (command.toLowerCase() === "help") {
|
|
1445
2458
|
printUsage();
|
|
@@ -1448,9 +2461,17 @@ var require_cli = __commonJS({
|
|
|
1448
2461
|
console.log(`Loading database: ${load_database}`);
|
|
1449
2462
|
var pending_runner = load_database ? MigrationRunnerFactory.create(config_file) : MigrationRunnerFactory.createEmpty(config_file);
|
|
1450
2463
|
pending_runner.then((runner) => __async(null, null, function* () {
|
|
2464
|
+
let failed = false;
|
|
1451
2465
|
try {
|
|
1452
2466
|
if (load_database) {
|
|
1453
|
-
yield runner.
|
|
2467
|
+
const patch_results = yield runner.applyPendingPatches();
|
|
2468
|
+
for (const patch of patch_results) {
|
|
2469
|
+
if (patch.status === "applied") {
|
|
2470
|
+
const changed = patch.operations.filter((op) => op.changed).length;
|
|
2471
|
+
const noop = patch.operations.length - changed;
|
|
2472
|
+
console.log(`Applied patch ${patch.fileName}: ${changed} changed, ${noop} no-op operation(s)`);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
1454
2475
|
}
|
|
1455
2476
|
switch (command.toLowerCase()) {
|
|
1456
2477
|
case "up":
|
|
@@ -1470,7 +2491,6 @@ var require_cli = __commonJS({
|
|
|
1470
2491
|
} else {
|
|
1471
2492
|
console.log("No pending migrations");
|
|
1472
2493
|
}
|
|
1473
|
-
yield runner.close();
|
|
1474
2494
|
break;
|
|
1475
2495
|
case "down":
|
|
1476
2496
|
const migrations_rollback = yield runner.getMigrations();
|
|
@@ -1491,13 +2511,11 @@ var require_cli = __commonJS({
|
|
|
1491
2511
|
} else {
|
|
1492
2512
|
console.log("No migrations to roll back");
|
|
1493
2513
|
}
|
|
1494
|
-
yield runner.close();
|
|
1495
2514
|
break;
|
|
1496
2515
|
case "reset":
|
|
1497
2516
|
console.log("Resetting all migrations...");
|
|
1498
2517
|
yield runner.reset();
|
|
1499
2518
|
console.log("Reset completed successfully");
|
|
1500
|
-
yield runner.close();
|
|
1501
2519
|
break;
|
|
1502
2520
|
case "create":
|
|
1503
2521
|
let filename = args.flags.name || commands[1];
|
|
@@ -1506,11 +2524,22 @@ var require_cli = __commonJS({
|
|
|
1506
2524
|
}
|
|
1507
2525
|
filename = filename.replace(/\s/g, "_");
|
|
1508
2526
|
runner.createMigration(filename);
|
|
1509
|
-
runner.close();
|
|
1510
2527
|
break;
|
|
2528
|
+
case "patch": {
|
|
2529
|
+
const patch_name = args.flags.name || commands[1];
|
|
2530
|
+
if (!patch_name) {
|
|
2531
|
+
throw new CLIError("Patch name is required. Usage: proper patch <name>");
|
|
2532
|
+
}
|
|
2533
|
+
const created_path = runner.createPatch(patch_name);
|
|
2534
|
+
console.log(`Created patch file:`);
|
|
2535
|
+
console.log(` ${created_path}`);
|
|
2536
|
+
console.log("");
|
|
2537
|
+
console.log("Complete this file before running another database-backed");
|
|
2538
|
+
console.log("Proper command: 'operations: []' is intentionally not runnable.");
|
|
2539
|
+
break;
|
|
2540
|
+
}
|
|
1511
2541
|
case "init":
|
|
1512
2542
|
yield runner.init(config_file);
|
|
1513
|
-
runner.close();
|
|
1514
2543
|
break;
|
|
1515
2544
|
case "status":
|
|
1516
2545
|
const { printTable } = __require("console-table-printer");
|
|
@@ -1524,7 +2553,6 @@ var require_cli = __commonJS({
|
|
|
1524
2553
|
};
|
|
1525
2554
|
}));
|
|
1526
2555
|
printTable(yield Promise.all(table));
|
|
1527
|
-
runner.close();
|
|
1528
2556
|
break;
|
|
1529
2557
|
case "query":
|
|
1530
2558
|
const sql_query = args.flags.query || commands[1];
|
|
@@ -1568,7 +2596,6 @@ Returned ${results.length} row(s)`);
|
|
|
1568
2596
|
} catch (error) {
|
|
1569
2597
|
throw new CLIError(`Query execution failed: ${error.message}`);
|
|
1570
2598
|
}
|
|
1571
|
-
yield runner.close();
|
|
1572
2599
|
break;
|
|
1573
2600
|
case "seed": {
|
|
1574
2601
|
const subCommands = commands.slice(1);
|
|
@@ -1601,7 +2628,6 @@ Returned ${results.length} row(s)`);
|
|
|
1601
2628
|
const reader = new FileMigrationConfigReader(config_file);
|
|
1602
2629
|
const migrationConfig = reader.loadFile();
|
|
1603
2630
|
yield runSeedsWithRunner(runner, migrationConfig, action, seedOptions);
|
|
1604
|
-
yield runner.close();
|
|
1605
2631
|
break;
|
|
1606
2632
|
}
|
|
1607
2633
|
default:
|
|
@@ -1612,9 +2638,25 @@ Returned ${results.length} row(s)`);
|
|
|
1612
2638
|
if (error.stack && process.env.DEBUG) {
|
|
1613
2639
|
console.error(error.stack);
|
|
1614
2640
|
}
|
|
2641
|
+
failed = true;
|
|
2642
|
+
} finally {
|
|
2643
|
+
try {
|
|
2644
|
+
yield runner.close();
|
|
2645
|
+
} catch (closeError) {
|
|
2646
|
+
console.error(`Error closing connection: ${closeError.message}`);
|
|
2647
|
+
failed = true;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (failed) {
|
|
1615
2651
|
process.exit(1);
|
|
1616
2652
|
}
|
|
1617
|
-
}))
|
|
2653
|
+
})).catch((error) => {
|
|
2654
|
+
console.error(`Error: ${error.message}`);
|
|
2655
|
+
if (error.stack && process.env.DEBUG) {
|
|
2656
|
+
console.error(error.stack);
|
|
2657
|
+
}
|
|
2658
|
+
process.exit(1);
|
|
2659
|
+
});
|
|
1618
2660
|
function printUsage() {
|
|
1619
2661
|
console.log(`
|
|
1620
2662
|
SQL Proper - Database migration tool
|
|
@@ -1627,6 +2669,7 @@ Commands:
|
|
|
1627
2669
|
down Roll back completed migrations
|
|
1628
2670
|
reset Roll back all migrations and reapply them
|
|
1629
2671
|
create Create a new migration
|
|
2672
|
+
patch Create a new ledger patch file (repairs migration history; no database access)
|
|
1630
2673
|
init Initialize a new config file
|
|
1631
2674
|
status Show migration status
|
|
1632
2675
|
query Execute a SQL query and display results
|
|
@@ -1645,6 +2688,7 @@ Examples:
|
|
|
1645
2688
|
proper down --increment 3 Roll back the last 3 applied migrations
|
|
1646
2689
|
proper down --all Roll back all completed migrations
|
|
1647
2690
|
proper create my_migration Create a new migration named "my_migration"
|
|
2691
|
+
proper patch fix_renamed_keys Scaffold a ledger patch file in the patch folder
|
|
1648
2692
|
proper init Create a new config file
|
|
1649
2693
|
proper status Show the status of all migrations
|
|
1650
2694
|
proper query "select * from users" Execute a SQL query
|