@noego/proper 0.1.0 → 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 +947 -73
- package/bin/cli.js.map +1 -1
- package/bin/cli.mjs +989 -73
- package/bin/cli.mjs.map +1 -1
- package/bin/index.d.mts +181 -3
- package/bin/index.d.ts +181 -3
- package/bin/index.js +925 -62
- package/bin/index.js.map +1 -1
- package/bin/index.mjs +915 -62
- package/bin/index.mjs.map +1 -1
- package/package.json +3 -2
- 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;
|
|
@@ -432,12 +514,7 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
432
514
|
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
433
515
|
*/
|
|
434
516
|
resolveFile(baseName, direction) {
|
|
435
|
-
|
|
436
|
-
const dialectFile = path.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
437
|
-
if (fs.existsSync(dialectFile)) return dialectFile;
|
|
438
|
-
const genericFile = path.join(this.directory, `${baseName}.${direction}.sql`);
|
|
439
|
-
if (fs.existsSync(genericFile)) return genericFile;
|
|
440
|
-
return null;
|
|
517
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
441
518
|
}
|
|
442
519
|
/**
|
|
443
520
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
@@ -446,11 +523,11 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
446
523
|
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
447
524
|
}
|
|
448
525
|
loadMigrations(table, connection) {
|
|
449
|
-
|
|
450
|
-
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);
|
|
451
528
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
452
529
|
dir_content.forEach((file) => {
|
|
453
|
-
const key = file
|
|
530
|
+
const key = canonicalMigrationKey(file);
|
|
454
531
|
uniqueKeys.add(key);
|
|
455
532
|
});
|
|
456
533
|
const migration_sorter = {};
|
|
@@ -490,14 +567,14 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
490
567
|
return builder;
|
|
491
568
|
}
|
|
492
569
|
sql_up(file) {
|
|
493
|
-
let content =
|
|
570
|
+
let content = fs2.readFileSync(file).toString();
|
|
494
571
|
if (!this.isDialectSpecific(file)) {
|
|
495
572
|
content = this.read_strategy(content);
|
|
496
573
|
}
|
|
497
574
|
return content.trim();
|
|
498
575
|
}
|
|
499
576
|
sql_down(file) {
|
|
500
|
-
let content =
|
|
577
|
+
let content = fs2.readFileSync(file).toString();
|
|
501
578
|
if (!this.isDialectSpecific(file)) {
|
|
502
579
|
content = this.read_strategy(content);
|
|
503
580
|
}
|
|
@@ -507,11 +584,31 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
507
584
|
}
|
|
508
585
|
});
|
|
509
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
|
+
|
|
510
606
|
// framework/MigrationSetup.ts
|
|
511
|
-
import
|
|
607
|
+
import fs3 from "fs";
|
|
512
608
|
var MigrationSetup;
|
|
513
609
|
var init_MigrationSetup = __esm({
|
|
514
610
|
"framework/MigrationSetup.ts"() {
|
|
611
|
+
init_PatchTypes();
|
|
515
612
|
MigrationSetup = class {
|
|
516
613
|
constructor(sqlrunner, config) {
|
|
517
614
|
this.sqlrunner = sqlrunner;
|
|
@@ -519,7 +616,7 @@ var init_MigrationSetup = __esm({
|
|
|
519
616
|
}
|
|
520
617
|
setup() {
|
|
521
618
|
return __async(this, null, function* () {
|
|
522
|
-
|
|
619
|
+
fs3.existsSync(this.config.migration_folder) || fs3.mkdirSync(this.config.migration_folder);
|
|
523
620
|
const tableName = this.config.migration_table;
|
|
524
621
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
525
622
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -541,11 +638,39 @@ var init_MigrationSetup = __esm({
|
|
|
541
638
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
542
639
|
)`;
|
|
543
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);
|
|
544
668
|
});
|
|
545
669
|
}
|
|
546
670
|
teardown() {
|
|
547
671
|
return __async(this, null, function* () {
|
|
548
672
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
673
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
549
674
|
});
|
|
550
675
|
}
|
|
551
676
|
};
|
|
@@ -614,6 +739,703 @@ var init_MigrationDialectParser = __esm({
|
|
|
614
739
|
}
|
|
615
740
|
});
|
|
616
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);
|
|
769
|
+
}
|
|
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);
|
|
817
|
+
}
|
|
818
|
+
fail(`Schema validation failed: ${detail}`, fileName, patchKey);
|
|
819
|
+
}
|
|
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 };
|
|
851
|
+
}
|
|
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
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
|
|
617
1439
|
// framework/SQLRunner.ts
|
|
618
1440
|
function isPromiseLike(value) {
|
|
619
1441
|
return !!value && typeof value.then === "function";
|
|
@@ -933,8 +1755,8 @@ ${sql}
|
|
|
933
1755
|
});
|
|
934
1756
|
|
|
935
1757
|
// framework/MigrationRunner.ts
|
|
936
|
-
import
|
|
937
|
-
function
|
|
1758
|
+
import fs6 from "fs";
|
|
1759
|
+
function toError2(error) {
|
|
938
1760
|
if (error instanceof Error) return error;
|
|
939
1761
|
return new Error(String(error));
|
|
940
1762
|
}
|
|
@@ -957,6 +1779,9 @@ var init_MigrationRunner = __esm({
|
|
|
957
1779
|
init_MigrationSetup();
|
|
958
1780
|
init_MigrationFilter();
|
|
959
1781
|
init_MigrationDialectParser();
|
|
1782
|
+
init_PatchRunner();
|
|
1783
|
+
init_PatchCreator();
|
|
1784
|
+
init_PatchTypes();
|
|
960
1785
|
init_SQLRunner();
|
|
961
1786
|
init_errors();
|
|
962
1787
|
MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
@@ -967,10 +1792,12 @@ var init_MigrationRunner = __esm({
|
|
|
967
1792
|
return __async(this, null, function* () {
|
|
968
1793
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
969
1794
|
const config = configReader.loadFile();
|
|
1795
|
+
let factoryOwnsConnection = false;
|
|
970
1796
|
if (!conn) {
|
|
971
1797
|
conn = yield this.createConnection(config);
|
|
1798
|
+
factoryOwnsConnection = true;
|
|
972
1799
|
}
|
|
973
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1800
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
974
1801
|
});
|
|
975
1802
|
}
|
|
976
1803
|
static createConnection(config) {
|
|
@@ -990,7 +1817,7 @@ var init_MigrationRunner = __esm({
|
|
|
990
1817
|
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
991
1818
|
return conn;
|
|
992
1819
|
} catch (error) {
|
|
993
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1820
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
994
1821
|
}
|
|
995
1822
|
case "sqlite":
|
|
996
1823
|
if (!config.sqlite) {
|
|
@@ -1005,7 +1832,7 @@ var init_MigrationRunner = __esm({
|
|
|
1005
1832
|
});
|
|
1006
1833
|
return conn;
|
|
1007
1834
|
} catch (error) {
|
|
1008
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1835
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
1009
1836
|
}
|
|
1010
1837
|
case "pg":
|
|
1011
1838
|
if (!config.pg && !process.env.DATABASE_URL) {
|
|
@@ -1021,7 +1848,7 @@ var init_MigrationRunner = __esm({
|
|
|
1021
1848
|
yield conn.connect();
|
|
1022
1849
|
return conn;
|
|
1023
1850
|
} catch (error) {
|
|
1024
|
-
throw DatabaseConnectionError.connectionFailed("pg",
|
|
1851
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
1025
1852
|
}
|
|
1026
1853
|
default:
|
|
1027
1854
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -1035,7 +1862,7 @@ var init_MigrationRunner = __esm({
|
|
|
1035
1862
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
1036
1863
|
});
|
|
1037
1864
|
}
|
|
1038
|
-
create(config, conn) {
|
|
1865
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
1039
1866
|
return __async(this, null, function* () {
|
|
1040
1867
|
let sqlrunner;
|
|
1041
1868
|
let driverConnection = conn;
|
|
@@ -1048,8 +1875,19 @@ var init_MigrationRunner = __esm({
|
|
|
1048
1875
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
1049
1876
|
const read_strategy = this.getReadStategy(config);
|
|
1050
1877
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
1051
|
-
|
|
1052
|
-
|
|
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;
|
|
1053
1891
|
});
|
|
1054
1892
|
}
|
|
1055
1893
|
createEmpty(config) {
|
|
@@ -1059,7 +1897,7 @@ var init_MigrationRunner = __esm({
|
|
|
1059
1897
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
1060
1898
|
const read_strategy = this.getReadStategy(config);
|
|
1061
1899
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
1062
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1900
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
1063
1901
|
});
|
|
1064
1902
|
}
|
|
1065
1903
|
getReadStategy(config) {
|
|
@@ -1076,33 +1914,77 @@ var init_MigrationRunner = __esm({
|
|
|
1076
1914
|
}
|
|
1077
1915
|
};
|
|
1078
1916
|
MySQLMigrationRunner = class {
|
|
1079
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1917
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
1080
1918
|
this.config = config;
|
|
1081
1919
|
this.directory = directory;
|
|
1082
1920
|
this.setupRunner = setupRunner;
|
|
1083
1921
|
this.sqlrunner = sqlrunner;
|
|
1084
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 = [];
|
|
1085
1932
|
}
|
|
1086
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() {
|
|
1087
1948
|
return __async(this, null, function* () {
|
|
1088
1949
|
try {
|
|
1089
1950
|
yield this.setupRunner.setup();
|
|
1090
1951
|
} catch (error) {
|
|
1091
|
-
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));
|
|
1092
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;
|
|
1093
1966
|
});
|
|
1094
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
|
+
}
|
|
1095
1976
|
terminate() {
|
|
1096
1977
|
return __async(this, null, function* () {
|
|
1097
1978
|
try {
|
|
1098
1979
|
yield this.setupRunner.teardown();
|
|
1099
1980
|
} catch (error) {
|
|
1100
|
-
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));
|
|
1101
1982
|
}
|
|
1102
1983
|
});
|
|
1103
1984
|
}
|
|
1104
1985
|
getMigrationsHistory() {
|
|
1105
1986
|
return __async(this, null, function* () {
|
|
1987
|
+
yield this.setup();
|
|
1106
1988
|
try {
|
|
1107
1989
|
const results = yield this.sqlrunner.query(`
|
|
1108
1990
|
select *
|
|
@@ -1110,16 +1992,17 @@ var init_MigrationRunner = __esm({
|
|
|
1110
1992
|
`);
|
|
1111
1993
|
return results[0];
|
|
1112
1994
|
} catch (error) {
|
|
1113
|
-
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));
|
|
1114
1996
|
}
|
|
1115
1997
|
});
|
|
1116
1998
|
}
|
|
1117
1999
|
getMigrations() {
|
|
1118
2000
|
return __async(this, null, function* () {
|
|
2001
|
+
yield this.setup();
|
|
1119
2002
|
try {
|
|
1120
2003
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
1121
2004
|
} catch (error) {
|
|
1122
|
-
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));
|
|
1123
2006
|
}
|
|
1124
2007
|
});
|
|
1125
2008
|
}
|
|
@@ -1132,7 +2015,7 @@ var init_MigrationRunner = __esm({
|
|
|
1132
2015
|
if (error instanceof MigrationExecutionError) {
|
|
1133
2016
|
throw error;
|
|
1134
2017
|
}
|
|
1135
|
-
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));
|
|
1136
2019
|
}
|
|
1137
2020
|
});
|
|
1138
2021
|
}
|
|
@@ -1145,12 +2028,13 @@ var init_MigrationRunner = __esm({
|
|
|
1145
2028
|
if (error instanceof MigrationExecutionError) {
|
|
1146
2029
|
throw error;
|
|
1147
2030
|
}
|
|
1148
|
-
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));
|
|
1149
2032
|
}
|
|
1150
2033
|
});
|
|
1151
2034
|
}
|
|
1152
2035
|
migrate(migrationNodes, forward) {
|
|
1153
2036
|
return __async(this, null, function* () {
|
|
2037
|
+
yield this.setup();
|
|
1154
2038
|
for (let node of migrationNodes) {
|
|
1155
2039
|
try {
|
|
1156
2040
|
if (forward) {
|
|
@@ -1163,7 +2047,7 @@ var init_MigrationRunner = __esm({
|
|
|
1163
2047
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
1164
2048
|
node.name || String(node),
|
|
1165
2049
|
forward ? node.up_sql() : node.down_sql(),
|
|
1166
|
-
|
|
2050
|
+
toError2(error)
|
|
1167
2051
|
);
|
|
1168
2052
|
}
|
|
1169
2053
|
}
|
|
@@ -1171,6 +2055,7 @@ var init_MigrationRunner = __esm({
|
|
|
1171
2055
|
}
|
|
1172
2056
|
reset() {
|
|
1173
2057
|
return __async(this, null, function* () {
|
|
2058
|
+
yield this.setup();
|
|
1174
2059
|
try {
|
|
1175
2060
|
let migrations = yield this.getMigrations();
|
|
1176
2061
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -1182,7 +2067,7 @@ var init_MigrationRunner = __esm({
|
|
|
1182
2067
|
if (error instanceof MigrationExecutionError) {
|
|
1183
2068
|
throw error;
|
|
1184
2069
|
}
|
|
1185
|
-
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));
|
|
1186
2071
|
}
|
|
1187
2072
|
});
|
|
1188
2073
|
}
|
|
@@ -1191,7 +2076,7 @@ var init_MigrationRunner = __esm({
|
|
|
1191
2076
|
const creator = new MigrationCreator(this.config);
|
|
1192
2077
|
creator.create(name);
|
|
1193
2078
|
} catch (error) {
|
|
1194
|
-
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));
|
|
1195
2080
|
}
|
|
1196
2081
|
}
|
|
1197
2082
|
close() {
|
|
@@ -1200,7 +2085,7 @@ var init_MigrationRunner = __esm({
|
|
|
1200
2085
|
try {
|
|
1201
2086
|
yield this.sqlrunner.end();
|
|
1202
2087
|
} catch (error) {
|
|
1203
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
2088
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
1204
2089
|
}
|
|
1205
2090
|
}
|
|
1206
2091
|
});
|
|
@@ -1214,7 +2099,7 @@ var init_MigrationRunner = __esm({
|
|
|
1214
2099
|
return __async(this, null, function* () {
|
|
1215
2100
|
try {
|
|
1216
2101
|
console.log(`Checking for ${config_file}`);
|
|
1217
|
-
const config_exist =
|
|
2102
|
+
const config_exist = fs6.existsSync(config_file);
|
|
1218
2103
|
if (!config_exist) {
|
|
1219
2104
|
console.log(`Creating ${config_file}`);
|
|
1220
2105
|
const default_config = {
|
|
@@ -1228,11 +2113,11 @@ var init_MigrationRunner = __esm({
|
|
|
1228
2113
|
"password": ""
|
|
1229
2114
|
}
|
|
1230
2115
|
};
|
|
1231
|
-
|
|
2116
|
+
fs6.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
|
|
1232
2117
|
console.log(`Created ${config_file}`);
|
|
1233
2118
|
}
|
|
1234
2119
|
} catch (error) {
|
|
1235
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
2120
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1236
2121
|
}
|
|
1237
2122
|
});
|
|
1238
2123
|
}
|
|
@@ -1243,7 +2128,7 @@ var init_MigrationRunner = __esm({
|
|
|
1243
2128
|
}
|
|
1244
2129
|
loadFile() {
|
|
1245
2130
|
try {
|
|
1246
|
-
const fileContent =
|
|
2131
|
+
const fileContent = fs6.readFileSync(this.configFile);
|
|
1247
2132
|
const config = JSON.parse(fileContent.toString());
|
|
1248
2133
|
if (!config.migration_folder) {
|
|
1249
2134
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1267,7 +2152,7 @@ var init_MigrationRunner = __esm({
|
|
|
1267
2152
|
if (error instanceof ConfigurationError) {
|
|
1268
2153
|
throw error;
|
|
1269
2154
|
}
|
|
1270
|
-
const err =
|
|
2155
|
+
const err = toError2(error);
|
|
1271
2156
|
if (err.message.includes("ENOENT")) {
|
|
1272
2157
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1273
2158
|
}
|
|
@@ -1284,16 +2169,16 @@ var init_MigrationRunner = __esm({
|
|
|
1284
2169
|
throw new CLIError("Migration name is required");
|
|
1285
2170
|
}
|
|
1286
2171
|
try {
|
|
1287
|
-
if (!
|
|
1288
|
-
|
|
2172
|
+
if (!fs6.existsSync(this.config.migration_folder)) {
|
|
2173
|
+
fs6.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1289
2174
|
}
|
|
1290
2175
|
const now_timestamp = Date.now();
|
|
1291
2176
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1292
2177
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1293
|
-
|
|
2178
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1294
2179
|
-- Write your up migration here
|
|
1295
2180
|
`.trim());
|
|
1296
|
-
|
|
2181
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1297
2182
|
-- Write your down migration here
|
|
1298
2183
|
`.trim());
|
|
1299
2184
|
console.log(`Created migration files:`);
|
|
@@ -1303,7 +2188,7 @@ var init_MigrationRunner = __esm({
|
|
|
1303
2188
|
if (error instanceof CLIError) {
|
|
1304
2189
|
throw error;
|
|
1305
2190
|
}
|
|
1306
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2191
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1307
2192
|
}
|
|
1308
2193
|
}
|
|
1309
2194
|
};
|
|
@@ -1311,10 +2196,10 @@ var init_MigrationRunner = __esm({
|
|
|
1311
2196
|
});
|
|
1312
2197
|
|
|
1313
2198
|
// framework/SeedRunner.ts
|
|
1314
|
-
import
|
|
1315
|
-
import
|
|
2199
|
+
import fs7 from "fs";
|
|
2200
|
+
import path5 from "path";
|
|
1316
2201
|
import { pathToFileURL } from "url";
|
|
1317
|
-
import
|
|
2202
|
+
import Ajv2 from "ajv";
|
|
1318
2203
|
import addFormats from "ajv-formats";
|
|
1319
2204
|
import { tsImport } from "tsx/esm/api";
|
|
1320
2205
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1323,10 +2208,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1323
2208
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1324
2209
|
}
|
|
1325
2210
|
function walkForFile(rootDir, fileName) {
|
|
1326
|
-
if (!
|
|
1327
|
-
const entries =
|
|
2211
|
+
if (!fs7.existsSync(rootDir)) return null;
|
|
2212
|
+
const entries = fs7.readdirSync(rootDir, { withFileTypes: true });
|
|
1328
2213
|
for (const entry of entries) {
|
|
1329
|
-
const full =
|
|
2214
|
+
const full = path5.join(rootDir, entry.name);
|
|
1330
2215
|
if (entry.isDirectory()) {
|
|
1331
2216
|
const found = walkForFile(full, fileName);
|
|
1332
2217
|
if (found) return found;
|
|
@@ -1420,39 +2305,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1420
2305
|
}
|
|
1421
2306
|
function loadJson(filePath) {
|
|
1422
2307
|
return __async(this, null, function* () {
|
|
1423
|
-
const content = yield
|
|
2308
|
+
const content = yield fs7.promises.readFile(filePath, "utf8");
|
|
1424
2309
|
return JSON.parse(content);
|
|
1425
2310
|
});
|
|
1426
2311
|
}
|
|
1427
2312
|
function createValidator() {
|
|
1428
|
-
const
|
|
1429
|
-
addFormats(
|
|
1430
|
-
return
|
|
2313
|
+
const ajv2 = new Ajv2({ allErrors: true, strict: false });
|
|
2314
|
+
addFormats(ajv2);
|
|
2315
|
+
return ajv2;
|
|
1431
2316
|
}
|
|
1432
2317
|
function validateData(schemaPath, data, validate, log) {
|
|
1433
2318
|
return __async(this, null, function* () {
|
|
1434
2319
|
if (!validate || !schemaPath) return;
|
|
1435
|
-
const content = yield
|
|
2320
|
+
const content = yield fs7.promises.readFile(schemaPath, "utf8");
|
|
1436
2321
|
const schema = JSON.parse(content);
|
|
1437
|
-
const
|
|
1438
|
-
const validateFn =
|
|
2322
|
+
const ajv2 = createValidator();
|
|
2323
|
+
const validateFn = ajv2.compile(schema);
|
|
1439
2324
|
const ok = validateFn(data);
|
|
1440
2325
|
if (!ok) {
|
|
1441
2326
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1442
|
-
throw new Error(`Seed data validation failed: ${
|
|
2327
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1443
2328
|
}
|
|
1444
2329
|
});
|
|
1445
2330
|
}
|
|
1446
2331
|
function runSqlSeed(runner, resolved, direction) {
|
|
1447
2332
|
return __async(this, null, function* () {
|
|
1448
2333
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1449
|
-
const sql = yield
|
|
2334
|
+
const sql = yield fs7.promises.readFile(sqlPath, "utf8");
|
|
1450
2335
|
yield runner.query(sql);
|
|
1451
2336
|
});
|
|
1452
2337
|
}
|
|
1453
2338
|
function loadSeedModule(modulePath) {
|
|
1454
2339
|
return __async(this, null, function* () {
|
|
1455
|
-
const resolved =
|
|
2340
|
+
const resolved = path5.resolve(modulePath);
|
|
1456
2341
|
if (resolved.endsWith(".ts")) {
|
|
1457
2342
|
const fileUrl = pathToFileURL(resolved).href;
|
|
1458
2343
|
return tsImport(fileUrl, fileUrl);
|
|
@@ -1567,7 +2452,7 @@ var require_cli = __commonJS({
|
|
|
1567
2452
|
}
|
|
1568
2453
|
var commands = args.commands;
|
|
1569
2454
|
var command = commands[0];
|
|
1570
|
-
var load_database = !["init", "create", "help"].includes(command.toLowerCase());
|
|
2455
|
+
var load_database = !["init", "create", "help", "patch"].includes(command.toLowerCase());
|
|
1571
2456
|
var config_file = args.flags.config || "proper.json";
|
|
1572
2457
|
if (command.toLowerCase() === "help") {
|
|
1573
2458
|
printUsage();
|
|
@@ -1576,9 +2461,17 @@ var require_cli = __commonJS({
|
|
|
1576
2461
|
console.log(`Loading database: ${load_database}`);
|
|
1577
2462
|
var pending_runner = load_database ? MigrationRunnerFactory.create(config_file) : MigrationRunnerFactory.createEmpty(config_file);
|
|
1578
2463
|
pending_runner.then((runner) => __async(null, null, function* () {
|
|
2464
|
+
let failed = false;
|
|
1579
2465
|
try {
|
|
1580
2466
|
if (load_database) {
|
|
1581
|
-
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
|
+
}
|
|
1582
2475
|
}
|
|
1583
2476
|
switch (command.toLowerCase()) {
|
|
1584
2477
|
case "up":
|
|
@@ -1598,7 +2491,6 @@ var require_cli = __commonJS({
|
|
|
1598
2491
|
} else {
|
|
1599
2492
|
console.log("No pending migrations");
|
|
1600
2493
|
}
|
|
1601
|
-
yield runner.close();
|
|
1602
2494
|
break;
|
|
1603
2495
|
case "down":
|
|
1604
2496
|
const migrations_rollback = yield runner.getMigrations();
|
|
@@ -1619,13 +2511,11 @@ var require_cli = __commonJS({
|
|
|
1619
2511
|
} else {
|
|
1620
2512
|
console.log("No migrations to roll back");
|
|
1621
2513
|
}
|
|
1622
|
-
yield runner.close();
|
|
1623
2514
|
break;
|
|
1624
2515
|
case "reset":
|
|
1625
2516
|
console.log("Resetting all migrations...");
|
|
1626
2517
|
yield runner.reset();
|
|
1627
2518
|
console.log("Reset completed successfully");
|
|
1628
|
-
yield runner.close();
|
|
1629
2519
|
break;
|
|
1630
2520
|
case "create":
|
|
1631
2521
|
let filename = args.flags.name || commands[1];
|
|
@@ -1634,11 +2524,22 @@ var require_cli = __commonJS({
|
|
|
1634
2524
|
}
|
|
1635
2525
|
filename = filename.replace(/\s/g, "_");
|
|
1636
2526
|
runner.createMigration(filename);
|
|
1637
|
-
runner.close();
|
|
1638
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
|
+
}
|
|
1639
2541
|
case "init":
|
|
1640
2542
|
yield runner.init(config_file);
|
|
1641
|
-
runner.close();
|
|
1642
2543
|
break;
|
|
1643
2544
|
case "status":
|
|
1644
2545
|
const { printTable } = __require("console-table-printer");
|
|
@@ -1652,7 +2553,6 @@ var require_cli = __commonJS({
|
|
|
1652
2553
|
};
|
|
1653
2554
|
}));
|
|
1654
2555
|
printTable(yield Promise.all(table));
|
|
1655
|
-
runner.close();
|
|
1656
2556
|
break;
|
|
1657
2557
|
case "query":
|
|
1658
2558
|
const sql_query = args.flags.query || commands[1];
|
|
@@ -1696,7 +2596,6 @@ Returned ${results.length} row(s)`);
|
|
|
1696
2596
|
} catch (error) {
|
|
1697
2597
|
throw new CLIError(`Query execution failed: ${error.message}`);
|
|
1698
2598
|
}
|
|
1699
|
-
yield runner.close();
|
|
1700
2599
|
break;
|
|
1701
2600
|
case "seed": {
|
|
1702
2601
|
const subCommands = commands.slice(1);
|
|
@@ -1729,7 +2628,6 @@ Returned ${results.length} row(s)`);
|
|
|
1729
2628
|
const reader = new FileMigrationConfigReader(config_file);
|
|
1730
2629
|
const migrationConfig = reader.loadFile();
|
|
1731
2630
|
yield runSeedsWithRunner(runner, migrationConfig, action, seedOptions);
|
|
1732
|
-
yield runner.close();
|
|
1733
2631
|
break;
|
|
1734
2632
|
}
|
|
1735
2633
|
default:
|
|
@@ -1740,9 +2638,25 @@ Returned ${results.length} row(s)`);
|
|
|
1740
2638
|
if (error.stack && process.env.DEBUG) {
|
|
1741
2639
|
console.error(error.stack);
|
|
1742
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) {
|
|
1743
2651
|
process.exit(1);
|
|
1744
2652
|
}
|
|
1745
|
-
}))
|
|
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
|
+
});
|
|
1746
2660
|
function printUsage() {
|
|
1747
2661
|
console.log(`
|
|
1748
2662
|
SQL Proper - Database migration tool
|
|
@@ -1755,6 +2669,7 @@ Commands:
|
|
|
1755
2669
|
down Roll back completed migrations
|
|
1756
2670
|
reset Roll back all migrations and reapply them
|
|
1757
2671
|
create Create a new migration
|
|
2672
|
+
patch Create a new ledger patch file (repairs migration history; no database access)
|
|
1758
2673
|
init Initialize a new config file
|
|
1759
2674
|
status Show migration status
|
|
1760
2675
|
query Execute a SQL query and display results
|
|
@@ -1773,6 +2688,7 @@ Examples:
|
|
|
1773
2688
|
proper down --increment 3 Roll back the last 3 applied migrations
|
|
1774
2689
|
proper down --all Roll back all completed migrations
|
|
1775
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
|
|
1776
2692
|
proper init Create a new config file
|
|
1777
2693
|
proper status Show the status of all migrations
|
|
1778
2694
|
proper query "select * from users" Execute a SQL query
|