@noego/proper 0.1.0 → 0.2.1
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 +951 -73
- package/bin/cli.js.map +1 -1
- package/bin/cli.mjs +994 -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 +929 -62
- package/bin/index.js.map +1 -1
- package/bin/index.mjs +919 -62
- package/bin/index.mjs.map +1 -1
- package/package.json +3 -2
- package/readme.md +97 -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,52 @@ 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 isMigrationFile(fileName) {
|
|
467
|
+
return MIGRATION_FILE_REGEX.test(fileName);
|
|
468
|
+
}
|
|
469
|
+
function canonicalMigrationKey(value) {
|
|
470
|
+
return value.replace(MIGRATION_FILE_REGEX, "").toLowerCase();
|
|
471
|
+
}
|
|
472
|
+
function resolveMigrationFile(directory, baseName, direction, dialect) {
|
|
473
|
+
const dialectExt = dialect === "sql" ? "mysql" : dialect;
|
|
474
|
+
const dialectFile = path.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
475
|
+
if (fs.existsSync(dialectFile)) return dialectFile;
|
|
476
|
+
const genericFile = path.join(directory, `${baseName}.${direction}.sql`);
|
|
477
|
+
if (fs.existsSync(genericFile)) return genericFile;
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
function loadMigrationManifest(directory, dialect) {
|
|
481
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
482
|
+
if (!fs.existsSync(directory)) return manifest;
|
|
483
|
+
const files = fs.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name).filter(isMigrationFile);
|
|
484
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
485
|
+
files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
|
|
486
|
+
uniqueKeys.forEach((key) => {
|
|
487
|
+
manifest.set(key, {
|
|
488
|
+
key,
|
|
489
|
+
upFile: resolveMigrationFile(directory, key, "up", dialect),
|
|
490
|
+
downFile: resolveMigrationFile(directory, key, "down", dialect)
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
return manifest;
|
|
494
|
+
}
|
|
495
|
+
var MIGRATION_FILE_REGEX;
|
|
496
|
+
var init_MigrationManifest = __esm({
|
|
497
|
+
"framework/MigrationManifest.ts"() {
|
|
498
|
+
MIGRATION_FILE_REGEX = /(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// framework/MigrationDirectoryReader.ts
|
|
503
|
+
import fs2 from "fs";
|
|
418
504
|
var MigrationDirectoryReader;
|
|
419
505
|
var init_MigrationDirectoryReader = __esm({
|
|
420
506
|
"framework/MigrationDirectoryReader.ts"() {
|
|
421
507
|
init_SqlMigrationBuilder();
|
|
508
|
+
init_MigrationManifest();
|
|
422
509
|
MigrationDirectoryReader = class {
|
|
423
510
|
constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
|
|
424
511
|
this.directory = directory;
|
|
@@ -432,12 +519,7 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
432
519
|
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
433
520
|
*/
|
|
434
521
|
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;
|
|
522
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
441
523
|
}
|
|
442
524
|
/**
|
|
443
525
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
@@ -446,11 +528,11 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
446
528
|
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
447
529
|
}
|
|
448
530
|
loadMigrations(table, connection) {
|
|
449
|
-
|
|
450
|
-
const dir_content =
|
|
531
|
+
fs2.existsSync(this.directory) || fs2.mkdirSync(this.directory);
|
|
532
|
+
const dir_content = fs2.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name).filter(isMigrationFile);
|
|
451
533
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
452
534
|
dir_content.forEach((file) => {
|
|
453
|
-
const key = file
|
|
535
|
+
const key = canonicalMigrationKey(file);
|
|
454
536
|
uniqueKeys.add(key);
|
|
455
537
|
});
|
|
456
538
|
const migration_sorter = {};
|
|
@@ -490,14 +572,14 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
490
572
|
return builder;
|
|
491
573
|
}
|
|
492
574
|
sql_up(file) {
|
|
493
|
-
let content =
|
|
575
|
+
let content = fs2.readFileSync(file).toString();
|
|
494
576
|
if (!this.isDialectSpecific(file)) {
|
|
495
577
|
content = this.read_strategy(content);
|
|
496
578
|
}
|
|
497
579
|
return content.trim();
|
|
498
580
|
}
|
|
499
581
|
sql_down(file) {
|
|
500
|
-
let content =
|
|
582
|
+
let content = fs2.readFileSync(file).toString();
|
|
501
583
|
if (!this.isDialectSpecific(file)) {
|
|
502
584
|
content = this.read_strategy(content);
|
|
503
585
|
}
|
|
@@ -507,11 +589,31 @@ var init_MigrationDirectoryReader = __esm({
|
|
|
507
589
|
}
|
|
508
590
|
});
|
|
509
591
|
|
|
592
|
+
// framework/PatchTypes.ts
|
|
593
|
+
import path2 from "path";
|
|
594
|
+
function resolvePatchFolder(config) {
|
|
595
|
+
if (config.patch_folder) return config.patch_folder;
|
|
596
|
+
const dir = path2.dirname(config.migration_folder);
|
|
597
|
+
return dir === "." && !config.migration_folder.includes(path2.sep) && !config.migration_folder.includes("/") ? "patches" : path2.join(dir, "patches");
|
|
598
|
+
}
|
|
599
|
+
function resolvePatchTable(config) {
|
|
600
|
+
return config.patch_table || DEFAULT_PATCH_TABLE;
|
|
601
|
+
}
|
|
602
|
+
var PATCH_FORMAT_VERSION, DEFAULT_PATCH_TABLE, PATCH_FILENAME_REGEX;
|
|
603
|
+
var init_PatchTypes = __esm({
|
|
604
|
+
"framework/PatchTypes.ts"() {
|
|
605
|
+
PATCH_FORMAT_VERSION = 1;
|
|
606
|
+
DEFAULT_PATCH_TABLE = "proper_patches";
|
|
607
|
+
PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
510
611
|
// framework/MigrationSetup.ts
|
|
511
|
-
import
|
|
612
|
+
import fs3 from "fs";
|
|
512
613
|
var MigrationSetup;
|
|
513
614
|
var init_MigrationSetup = __esm({
|
|
514
615
|
"framework/MigrationSetup.ts"() {
|
|
616
|
+
init_PatchTypes();
|
|
515
617
|
MigrationSetup = class {
|
|
516
618
|
constructor(sqlrunner, config) {
|
|
517
619
|
this.sqlrunner = sqlrunner;
|
|
@@ -519,7 +621,7 @@ var init_MigrationSetup = __esm({
|
|
|
519
621
|
}
|
|
520
622
|
setup() {
|
|
521
623
|
return __async(this, null, function* () {
|
|
522
|
-
|
|
624
|
+
fs3.existsSync(this.config.migration_folder) || fs3.mkdirSync(this.config.migration_folder);
|
|
523
625
|
const tableName = this.config.migration_table;
|
|
524
626
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
525
627
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -541,11 +643,39 @@ var init_MigrationSetup = __esm({
|
|
|
541
643
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
542
644
|
)`;
|
|
543
645
|
yield this.sqlrunner.query(createTableSql);
|
|
646
|
+
const patchTable = resolvePatchTable(this.config);
|
|
647
|
+
const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
648
|
+
migration_table TEXT NOT NULL,
|
|
649
|
+
patch_key TEXT NOT NULL,
|
|
650
|
+
checksum TEXT NOT NULL,
|
|
651
|
+
format_version INTEGER NOT NULL,
|
|
652
|
+
description TEXT NOT NULL,
|
|
653
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
654
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
655
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
656
|
+
migration_table TEXT NOT NULL,
|
|
657
|
+
patch_key TEXT NOT NULL,
|
|
658
|
+
checksum TEXT NOT NULL,
|
|
659
|
+
format_version INTEGER NOT NULL,
|
|
660
|
+
description TEXT NOT NULL,
|
|
661
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
662
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
663
|
+
)` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
664
|
+
migration_table VARCHAR(255) NOT NULL,
|
|
665
|
+
patch_key VARCHAR(255) NOT NULL,
|
|
666
|
+
checksum CHAR(64) NOT NULL,
|
|
667
|
+
format_version INT NOT NULL,
|
|
668
|
+
description TEXT NOT NULL,
|
|
669
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
670
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
671
|
+
)`;
|
|
672
|
+
yield this.sqlrunner.query(createPatchTableSql);
|
|
544
673
|
});
|
|
545
674
|
}
|
|
546
675
|
teardown() {
|
|
547
676
|
return __async(this, null, function* () {
|
|
548
677
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
678
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
549
679
|
});
|
|
550
680
|
}
|
|
551
681
|
};
|
|
@@ -614,6 +744,703 @@ var init_MigrationDialectParser = __esm({
|
|
|
614
744
|
}
|
|
615
745
|
});
|
|
616
746
|
|
|
747
|
+
// framework/PatchValidator.ts
|
|
748
|
+
import Ajv from "ajv";
|
|
749
|
+
import { parseDocument } from "yaml";
|
|
750
|
+
function fail(message, file, patchKey) {
|
|
751
|
+
throw new PatchValidationError(message, file, patchKey);
|
|
752
|
+
}
|
|
753
|
+
function checkMigrationKey(value, context, file, patchKey) {
|
|
754
|
+
if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
|
|
755
|
+
if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
|
|
756
|
+
if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
|
|
757
|
+
if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
|
|
758
|
+
fail(`${context}: migration key length out of bounds`, file, patchKey);
|
|
759
|
+
}
|
|
760
|
+
return canonicalMigrationKey(value);
|
|
761
|
+
}
|
|
762
|
+
function assertStrictYaml(doc, file, patchKey) {
|
|
763
|
+
if (doc.errors.length > 0) {
|
|
764
|
+
fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
|
|
765
|
+
}
|
|
766
|
+
if (doc.warnings.length > 0) {
|
|
767
|
+
fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
|
|
768
|
+
}
|
|
769
|
+
const visit = (node) => {
|
|
770
|
+
var _a, _b;
|
|
771
|
+
if (node == null || typeof node !== "object") return;
|
|
772
|
+
if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
|
|
773
|
+
fail("YAML aliases are not permitted in patch files", file, patchKey);
|
|
774
|
+
}
|
|
775
|
+
if (node.anchor) {
|
|
776
|
+
fail("YAML anchors are not permitted in patch files", file, patchKey);
|
|
777
|
+
}
|
|
778
|
+
if (node.tag && ![
|
|
779
|
+
"tag:yaml.org,2002:str",
|
|
780
|
+
"tag:yaml.org,2002:int",
|
|
781
|
+
"tag:yaml.org,2002:bool",
|
|
782
|
+
"tag:yaml.org,2002:null",
|
|
783
|
+
"tag:yaml.org,2002:map",
|
|
784
|
+
"tag:yaml.org,2002:seq"
|
|
785
|
+
].includes(node.tag)) {
|
|
786
|
+
fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
|
|
787
|
+
}
|
|
788
|
+
if (Array.isArray(node.items)) {
|
|
789
|
+
for (const item of node.items) {
|
|
790
|
+
if (item && typeof item === "object" && "key" in item) {
|
|
791
|
+
const keyValue = (_b = item.key) == null ? void 0 : _b.value;
|
|
792
|
+
if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
|
|
793
|
+
visit(item.key);
|
|
794
|
+
visit(item.value);
|
|
795
|
+
} else {
|
|
796
|
+
visit(item);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
visit(doc.contents);
|
|
802
|
+
}
|
|
803
|
+
function parsePatchContent(content, fileName, patchKey) {
|
|
804
|
+
var _a;
|
|
805
|
+
const doc = parseDocument(content, {
|
|
806
|
+
uniqueKeys: true,
|
|
807
|
+
// duplicate mapping keys become errors
|
|
808
|
+
merge: false,
|
|
809
|
+
schema: "core",
|
|
810
|
+
version: "1.2"
|
|
811
|
+
});
|
|
812
|
+
assertStrictYaml(doc, fileName, patchKey);
|
|
813
|
+
const raw = doc.toJS({ mapAsMap: false });
|
|
814
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
815
|
+
fail("Patch document must be a YAML mapping", fileName, patchKey);
|
|
816
|
+
}
|
|
817
|
+
if (!validateSchema(raw)) {
|
|
818
|
+
const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
|
|
819
|
+
const anyRaw = raw;
|
|
820
|
+
if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
|
|
821
|
+
fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
822
|
+
}
|
|
823
|
+
fail(`Schema validation failed: ${detail}`, fileName, patchKey);
|
|
824
|
+
}
|
|
825
|
+
const parsed = raw;
|
|
826
|
+
if (parsed.version !== PATCH_FORMAT_VERSION) {
|
|
827
|
+
fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
828
|
+
}
|
|
829
|
+
const description = parsed.description.trim();
|
|
830
|
+
if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
|
|
831
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
832
|
+
fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
|
|
833
|
+
}
|
|
834
|
+
const operations = parsed.operations.map((op, index) => {
|
|
835
|
+
const verbs = Object.keys(op);
|
|
836
|
+
const verb = verbs[0];
|
|
837
|
+
const context = `operation ${index} (${verb})`;
|
|
838
|
+
switch (verb) {
|
|
839
|
+
case "rename_migration": {
|
|
840
|
+
const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
|
|
841
|
+
const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
|
|
842
|
+
if (from === to) {
|
|
843
|
+
fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
|
|
844
|
+
}
|
|
845
|
+
return { verb: "rename_migration", from, to };
|
|
846
|
+
}
|
|
847
|
+
case "mark_applied":
|
|
848
|
+
return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
|
|
849
|
+
case "unmark_applied":
|
|
850
|
+
return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
|
|
851
|
+
default:
|
|
852
|
+
fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
return { version: parsed.version, description, operations };
|
|
856
|
+
}
|
|
857
|
+
function validatePatchPlan(patches, manifest) {
|
|
858
|
+
const renames = [];
|
|
859
|
+
for (const patch of patches) {
|
|
860
|
+
patch.operations.forEach((op, index) => {
|
|
861
|
+
if (op.verb === "rename_migration") {
|
|
862
|
+
renames.push({ from: op.from, to: op.to, file: patch.fileName });
|
|
863
|
+
} else if (op.verb === "mark_applied") {
|
|
864
|
+
if (!manifest.has(op.key)) {
|
|
865
|
+
throw new PatchValidationError(
|
|
866
|
+
`operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
|
|
867
|
+
patch.fileName,
|
|
868
|
+
patch.patchKey
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
if (renames.length === 0) return;
|
|
875
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
876
|
+
renames.forEach((r) => {
|
|
877
|
+
mentioned.add(r.from);
|
|
878
|
+
mentioned.add(r.to);
|
|
879
|
+
});
|
|
880
|
+
const finalKey = (start) => {
|
|
881
|
+
let current = start;
|
|
882
|
+
for (const r of renames) {
|
|
883
|
+
if (current === r.from) current = r.to;
|
|
884
|
+
}
|
|
885
|
+
return current;
|
|
886
|
+
};
|
|
887
|
+
for (const key of mentioned) {
|
|
888
|
+
const finish = finalKey(key);
|
|
889
|
+
if (manifest.has(key)) {
|
|
890
|
+
if (finish !== key) {
|
|
891
|
+
throw new PatchValidationError(
|
|
892
|
+
`rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
if (!manifest.has(finish)) {
|
|
897
|
+
throw new PatchValidationError(
|
|
898
|
+
`rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
var MAX_DESCRIPTION_LENGTH, MAX_MIGRATION_KEY_LENGTH, migrationKeySchema, patchSchema, ajv, validateSchema;
|
|
905
|
+
var init_PatchValidator = __esm({
|
|
906
|
+
"framework/PatchValidator.ts"() {
|
|
907
|
+
init_errors();
|
|
908
|
+
init_MigrationManifest();
|
|
909
|
+
init_PatchTypes();
|
|
910
|
+
MAX_DESCRIPTION_LENGTH = 500;
|
|
911
|
+
MAX_MIGRATION_KEY_LENGTH = 255;
|
|
912
|
+
migrationKeySchema = {
|
|
913
|
+
type: "string",
|
|
914
|
+
minLength: 1,
|
|
915
|
+
maxLength: MAX_MIGRATION_KEY_LENGTH
|
|
916
|
+
};
|
|
917
|
+
patchSchema = {
|
|
918
|
+
type: "object",
|
|
919
|
+
additionalProperties: false,
|
|
920
|
+
required: ["version", "description", "operations"],
|
|
921
|
+
properties: {
|
|
922
|
+
version: { type: "integer" },
|
|
923
|
+
description: { type: "string" },
|
|
924
|
+
operations: {
|
|
925
|
+
type: "array",
|
|
926
|
+
minItems: 1,
|
|
927
|
+
items: {
|
|
928
|
+
type: "object",
|
|
929
|
+
additionalProperties: false,
|
|
930
|
+
minProperties: 1,
|
|
931
|
+
maxProperties: 1,
|
|
932
|
+
properties: {
|
|
933
|
+
rename_migration: {
|
|
934
|
+
type: "object",
|
|
935
|
+
additionalProperties: false,
|
|
936
|
+
required: ["from", "to"],
|
|
937
|
+
properties: { from: migrationKeySchema, to: migrationKeySchema }
|
|
938
|
+
},
|
|
939
|
+
mark_applied: {
|
|
940
|
+
type: "object",
|
|
941
|
+
additionalProperties: false,
|
|
942
|
+
required: ["key"],
|
|
943
|
+
properties: { key: migrationKeySchema }
|
|
944
|
+
},
|
|
945
|
+
unmark_applied: {
|
|
946
|
+
type: "object",
|
|
947
|
+
additionalProperties: false,
|
|
948
|
+
required: ["key"],
|
|
949
|
+
properties: { key: migrationKeySchema }
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
ajv = new Ajv({ allErrors: true, strict: true });
|
|
957
|
+
validateSchema = ajv.compile(patchSchema);
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// framework/PatchDirectoryReader.ts
|
|
962
|
+
import crypto from "crypto";
|
|
963
|
+
import fs4 from "fs";
|
|
964
|
+
import path3 from "path";
|
|
965
|
+
var PatchDirectoryReader;
|
|
966
|
+
var init_PatchDirectoryReader = __esm({
|
|
967
|
+
"framework/PatchDirectoryReader.ts"() {
|
|
968
|
+
init_errors();
|
|
969
|
+
init_PatchValidator();
|
|
970
|
+
init_PatchTypes();
|
|
971
|
+
PatchDirectoryReader = class {
|
|
972
|
+
constructor(directory) {
|
|
973
|
+
this.directory = directory;
|
|
974
|
+
}
|
|
975
|
+
loadPatches() {
|
|
976
|
+
if (!fs4.existsSync(this.directory)) return [];
|
|
977
|
+
const entries = fs4.readdirSync(this.directory, { withFileTypes: true });
|
|
978
|
+
const patchFiles = [];
|
|
979
|
+
for (const entry of entries) {
|
|
980
|
+
if (!entry.name.endsWith(".yaml")) continue;
|
|
981
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
982
|
+
if (entry.isSymbolicLink()) {
|
|
983
|
+
throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
|
|
984
|
+
}
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
patchFiles.push(entry.name);
|
|
988
|
+
}
|
|
989
|
+
patchFiles.sort((a, b) => {
|
|
990
|
+
const stampA = parseInt(a.slice(0, 13), 10);
|
|
991
|
+
const stampB = parseInt(b.slice(0, 13), 10);
|
|
992
|
+
if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
|
|
993
|
+
return stampA - stampB;
|
|
994
|
+
}
|
|
995
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
996
|
+
});
|
|
997
|
+
return patchFiles.map((fileName) => {
|
|
998
|
+
const match = PATCH_FILENAME_REGEX.exec(fileName);
|
|
999
|
+
if (!match) {
|
|
1000
|
+
throw new PatchValidationError(
|
|
1001
|
+
`invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
|
|
1002
|
+
fileName
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
const patchKey = fileName.slice(0, -".yaml".length);
|
|
1006
|
+
const filePath = path3.join(this.directory, fileName);
|
|
1007
|
+
const bytes = fs4.readFileSync(filePath);
|
|
1008
|
+
const checksum = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
1009
|
+
const content = bytes.toString("utf8");
|
|
1010
|
+
const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
|
|
1011
|
+
return { patchKey, fileName, filePath, checksum, version, description, operations };
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// framework/PatchRunner.ts
|
|
1019
|
+
function toError(error) {
|
|
1020
|
+
if (error instanceof Error) return error;
|
|
1021
|
+
return new Error(String(error));
|
|
1022
|
+
}
|
|
1023
|
+
function extractRows(result) {
|
|
1024
|
+
if (!Array.isArray(result)) return [];
|
|
1025
|
+
if (Array.isArray(result[0])) return result[0];
|
|
1026
|
+
if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
|
|
1027
|
+
return result;
|
|
1028
|
+
}
|
|
1029
|
+
if (result[0] == null) return [];
|
|
1030
|
+
return [result[0]];
|
|
1031
|
+
}
|
|
1032
|
+
var PatchRunner;
|
|
1033
|
+
var init_PatchRunner = __esm({
|
|
1034
|
+
"framework/PatchRunner.ts"() {
|
|
1035
|
+
init_PatchDirectoryReader();
|
|
1036
|
+
init_PatchValidator();
|
|
1037
|
+
init_MigrationManifest();
|
|
1038
|
+
init_PatchTypes();
|
|
1039
|
+
init_errors();
|
|
1040
|
+
PatchRunner = class {
|
|
1041
|
+
constructor(sqlrunner, config) {
|
|
1042
|
+
this.sqlrunner = sqlrunner;
|
|
1043
|
+
this.config = config;
|
|
1044
|
+
this.patchTable = resolvePatchTable(config);
|
|
1045
|
+
this.migrationTable = config.migration_table;
|
|
1046
|
+
this.dialect = config.database;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Discovers, validates, and applies every unapplied patch in order.
|
|
1050
|
+
* Each unapplied patch is its own transaction; earlier committed patches
|
|
1051
|
+
* remain committed if a later patch fails.
|
|
1052
|
+
*/
|
|
1053
|
+
applyPending() {
|
|
1054
|
+
return __async(this, null, function* () {
|
|
1055
|
+
const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
|
|
1056
|
+
const patches = reader.loadPatches();
|
|
1057
|
+
const history = yield this.loadHistory();
|
|
1058
|
+
if (patches.length === 0 && history.length === 0) {
|
|
1059
|
+
return [];
|
|
1060
|
+
}
|
|
1061
|
+
const byKey = new Map(patches.map((p) => [p.patchKey, p]));
|
|
1062
|
+
for (const row of history) {
|
|
1063
|
+
const file = byKey.get(row.patch_key);
|
|
1064
|
+
if (!file) {
|
|
1065
|
+
throw new PatchIntegrityError(
|
|
1066
|
+
`applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
|
|
1067
|
+
void 0,
|
|
1068
|
+
row.patch_key
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
if (file.checksum !== row.checksum) {
|
|
1072
|
+
throw new PatchIntegrityError(
|
|
1073
|
+
`applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
|
|
1074
|
+
file.fileName,
|
|
1075
|
+
row.patch_key,
|
|
1076
|
+
row.checksum,
|
|
1077
|
+
file.checksum
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
|
|
1082
|
+
validatePatchPlan(patches, manifest);
|
|
1083
|
+
const appliedKeys = new Set(history.map((r) => r.patch_key));
|
|
1084
|
+
const results = [];
|
|
1085
|
+
for (const patch of patches) {
|
|
1086
|
+
if (appliedKeys.has(patch.patchKey)) {
|
|
1087
|
+
results.push({
|
|
1088
|
+
patchKey: patch.patchKey,
|
|
1089
|
+
fileName: patch.fileName,
|
|
1090
|
+
status: "already_applied",
|
|
1091
|
+
operations: []
|
|
1092
|
+
});
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
results.push(yield this.applyOne(patch, manifest));
|
|
1096
|
+
}
|
|
1097
|
+
return results;
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
loadHistory() {
|
|
1101
|
+
return __async(this, null, function* () {
|
|
1102
|
+
try {
|
|
1103
|
+
const result = yield this.sqlrunner.query(
|
|
1104
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
|
|
1105
|
+
[this.migrationTable]
|
|
1106
|
+
);
|
|
1107
|
+
return extractRows(result);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
throw new PatchExecutionError(
|
|
1110
|
+
`failed to read patch history from '${this.patchTable}'`,
|
|
1111
|
+
void 0,
|
|
1112
|
+
void 0,
|
|
1113
|
+
void 0,
|
|
1114
|
+
void 0,
|
|
1115
|
+
toError(error)
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
beginSql() {
|
|
1121
|
+
switch (this.dialect) {
|
|
1122
|
+
case "sqlite":
|
|
1123
|
+
return "BEGIN IMMEDIATE";
|
|
1124
|
+
case "pg":
|
|
1125
|
+
return "BEGIN";
|
|
1126
|
+
default:
|
|
1127
|
+
return "START TRANSACTION";
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
begin(patch) {
|
|
1131
|
+
return __async(this, null, function* () {
|
|
1132
|
+
const deadline = Date.now() + 1e4;
|
|
1133
|
+
while (true) {
|
|
1134
|
+
try {
|
|
1135
|
+
yield this.sqlrunner.execute(this.beginSql());
|
|
1136
|
+
return;
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
const message = toError(error).message;
|
|
1139
|
+
if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
|
|
1140
|
+
yield new Promise((resolve) => setTimeout(resolve, 50));
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
throw new PatchExecutionError(
|
|
1144
|
+
"failed to start patch transaction",
|
|
1145
|
+
patch.fileName,
|
|
1146
|
+
patch.patchKey,
|
|
1147
|
+
void 0,
|
|
1148
|
+
void 0,
|
|
1149
|
+
toError(error)
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
rollbackQuietly() {
|
|
1156
|
+
return __async(this, null, function* () {
|
|
1157
|
+
try {
|
|
1158
|
+
yield this.sqlrunner.execute("ROLLBACK");
|
|
1159
|
+
} catch (e) {
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
applyOne(patch, manifest) {
|
|
1164
|
+
return __async(this, null, function* () {
|
|
1165
|
+
yield this.begin(patch);
|
|
1166
|
+
try {
|
|
1167
|
+
yield this.sqlrunner.execute(
|
|
1168
|
+
`INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
|
|
1169
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1170
|
+
[this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
|
|
1171
|
+
);
|
|
1172
|
+
} catch (claimError) {
|
|
1173
|
+
yield this.rollbackQuietly();
|
|
1174
|
+
const committed = yield this.findCommittedRow(patch.patchKey);
|
|
1175
|
+
if (committed) {
|
|
1176
|
+
if (committed.checksum === patch.checksum) {
|
|
1177
|
+
return {
|
|
1178
|
+
patchKey: patch.patchKey,
|
|
1179
|
+
fileName: patch.fileName,
|
|
1180
|
+
status: "already_applied",
|
|
1181
|
+
operations: []
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
throw new PatchIntegrityError(
|
|
1185
|
+
`patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
|
|
1186
|
+
patch.fileName,
|
|
1187
|
+
patch.patchKey,
|
|
1188
|
+
committed.checksum,
|
|
1189
|
+
patch.checksum
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
throw new PatchExecutionError(
|
|
1193
|
+
"failed to claim patch-history row",
|
|
1194
|
+
patch.fileName,
|
|
1195
|
+
patch.patchKey,
|
|
1196
|
+
void 0,
|
|
1197
|
+
void 0,
|
|
1198
|
+
toError(claimError)
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
const operationResults = [];
|
|
1202
|
+
try {
|
|
1203
|
+
for (let index = 0; index < patch.operations.length; index++) {
|
|
1204
|
+
operationResults.push(
|
|
1205
|
+
yield this.applyOperation(patch, patch.operations[index], index, manifest)
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
yield this.sqlrunner.execute("COMMIT");
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
yield this.rollbackQuietly();
|
|
1211
|
+
if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
|
|
1212
|
+
throw error;
|
|
1213
|
+
}
|
|
1214
|
+
throw new PatchExecutionError(
|
|
1215
|
+
"patch application failed",
|
|
1216
|
+
patch.fileName,
|
|
1217
|
+
patch.patchKey,
|
|
1218
|
+
void 0,
|
|
1219
|
+
void 0,
|
|
1220
|
+
toError(error)
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
patchKey: patch.patchKey,
|
|
1225
|
+
fileName: patch.fileName,
|
|
1226
|
+
status: "applied",
|
|
1227
|
+
operations: operationResults
|
|
1228
|
+
};
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
findCommittedRow(patchKey) {
|
|
1232
|
+
return __async(this, null, function* () {
|
|
1233
|
+
const result = yield this.sqlrunner.query(
|
|
1234
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
|
|
1235
|
+
[this.migrationTable, patchKey]
|
|
1236
|
+
);
|
|
1237
|
+
const list = extractRows(result);
|
|
1238
|
+
return list.length > 0 ? list[0] : null;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
countRows(key) {
|
|
1242
|
+
return __async(this, null, function* () {
|
|
1243
|
+
var _a, _b, _c;
|
|
1244
|
+
const result = yield this.sqlrunner.query(
|
|
1245
|
+
`SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1246
|
+
[key]
|
|
1247
|
+
);
|
|
1248
|
+
const rows = extractRows(result);
|
|
1249
|
+
const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
|
|
1250
|
+
return Number(value != null ? value : 0);
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
conflict(patch, index, verb, message, keys, counts) {
|
|
1254
|
+
throw new PatchConflictError(
|
|
1255
|
+
`operation ${index} (${verb}): ${message}`,
|
|
1256
|
+
patch.fileName,
|
|
1257
|
+
patch.patchKey,
|
|
1258
|
+
index,
|
|
1259
|
+
verb,
|
|
1260
|
+
keys,
|
|
1261
|
+
counts
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
applyOperation(patch, op, index, manifest) {
|
|
1265
|
+
return __async(this, null, function* () {
|
|
1266
|
+
var _a, _b, _c, _d;
|
|
1267
|
+
try {
|
|
1268
|
+
switch (op.verb) {
|
|
1269
|
+
case "rename_migration": {
|
|
1270
|
+
const fromCount = yield this.countRows(op.from);
|
|
1271
|
+
const toCount = yield this.countRows(op.to);
|
|
1272
|
+
const counts = { [op.from]: fromCount, [op.to]: toCount };
|
|
1273
|
+
if (fromCount > 1 || toCount > 1) {
|
|
1274
|
+
this.conflict(
|
|
1275
|
+
patch,
|
|
1276
|
+
index,
|
|
1277
|
+
op.verb,
|
|
1278
|
+
`ledger corruption: duplicate rows for a migration key`,
|
|
1279
|
+
[op.from, op.to],
|
|
1280
|
+
counts
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
if (fromCount === 1 && toCount === 1) {
|
|
1284
|
+
this.conflict(
|
|
1285
|
+
patch,
|
|
1286
|
+
index,
|
|
1287
|
+
op.verb,
|
|
1288
|
+
`both '${op.from}' and '${op.to}' exist in the ledger`,
|
|
1289
|
+
[op.from, op.to],
|
|
1290
|
+
counts
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
if (fromCount === 0) {
|
|
1294
|
+
return { verb: op.verb, changed: false };
|
|
1295
|
+
}
|
|
1296
|
+
const target = manifest.get(op.to);
|
|
1297
|
+
if (target) {
|
|
1298
|
+
yield this.sqlrunner.execute(
|
|
1299
|
+
`UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
|
|
1300
|
+
[op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
|
|
1301
|
+
);
|
|
1302
|
+
} else {
|
|
1303
|
+
yield this.sqlrunner.execute(
|
|
1304
|
+
`UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
|
|
1305
|
+
[op.to, op.from]
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
return { verb: op.verb, changed: true };
|
|
1309
|
+
}
|
|
1310
|
+
case "mark_applied": {
|
|
1311
|
+
const count = yield this.countRows(op.key);
|
|
1312
|
+
if (count > 1) {
|
|
1313
|
+
this.conflict(
|
|
1314
|
+
patch,
|
|
1315
|
+
index,
|
|
1316
|
+
op.verb,
|
|
1317
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1318
|
+
[op.key],
|
|
1319
|
+
{ [op.key]: count }
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
if (count === 1) {
|
|
1323
|
+
return { verb: op.verb, changed: false };
|
|
1324
|
+
}
|
|
1325
|
+
const entry = manifest.get(op.key);
|
|
1326
|
+
yield this.sqlrunner.execute(
|
|
1327
|
+
`INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
|
|
1328
|
+
[op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
|
|
1329
|
+
);
|
|
1330
|
+
return { verb: op.verb, changed: true };
|
|
1331
|
+
}
|
|
1332
|
+
case "unmark_applied": {
|
|
1333
|
+
const count = yield this.countRows(op.key);
|
|
1334
|
+
if (count > 1) {
|
|
1335
|
+
this.conflict(
|
|
1336
|
+
patch,
|
|
1337
|
+
index,
|
|
1338
|
+
op.verb,
|
|
1339
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1340
|
+
[op.key],
|
|
1341
|
+
{ [op.key]: count }
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
if (count === 0) {
|
|
1345
|
+
return { verb: op.verb, changed: false };
|
|
1346
|
+
}
|
|
1347
|
+
yield this.sqlrunner.execute(
|
|
1348
|
+
`DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1349
|
+
[op.key]
|
|
1350
|
+
);
|
|
1351
|
+
return { verb: op.verb, changed: true };
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
if (error instanceof PatchConflictError) throw error;
|
|
1356
|
+
throw new PatchExecutionError(
|
|
1357
|
+
`operation failed`,
|
|
1358
|
+
patch.fileName,
|
|
1359
|
+
patch.patchKey,
|
|
1360
|
+
index,
|
|
1361
|
+
op.verb,
|
|
1362
|
+
toError(error)
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
// framework/PatchCreator.ts
|
|
1372
|
+
import fs5 from "fs";
|
|
1373
|
+
import path4 from "path";
|
|
1374
|
+
var MAX_NAME_LENGTH, SCAFFOLD, PatchCreator;
|
|
1375
|
+
var init_PatchCreator = __esm({
|
|
1376
|
+
"framework/PatchCreator.ts"() {
|
|
1377
|
+
init_errors();
|
|
1378
|
+
MAX_NAME_LENGTH = 120;
|
|
1379
|
+
SCAFFOLD = `version: 1
|
|
1380
|
+
description: TODO
|
|
1381
|
+
operations: []
|
|
1382
|
+
`;
|
|
1383
|
+
PatchCreator = class _PatchCreator {
|
|
1384
|
+
constructor(patchFolder) {
|
|
1385
|
+
this.patchFolder = patchFolder;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
|
|
1389
|
+
* Rejects empty results, path separators, `..`, control characters,
|
|
1390
|
+
* characters outside [a-z0-9_-], and names longer than 120 characters.
|
|
1391
|
+
*/
|
|
1392
|
+
static normalizeName(name) {
|
|
1393
|
+
const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
|
|
1394
|
+
if (normalized.length === 0) {
|
|
1395
|
+
throw new CLIError("Patch name is required");
|
|
1396
|
+
}
|
|
1397
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
1398
|
+
throw new CLIError("Patch name must not contain path separators");
|
|
1399
|
+
}
|
|
1400
|
+
if (normalized.includes("..")) {
|
|
1401
|
+
throw new CLIError("Patch name must not contain '..'");
|
|
1402
|
+
}
|
|
1403
|
+
if (/[\x00-\x1f\x7f]/.test(normalized)) {
|
|
1404
|
+
throw new CLIError("Patch name must not contain control characters");
|
|
1405
|
+
}
|
|
1406
|
+
if (!/^[a-z0-9_-]+$/.test(normalized)) {
|
|
1407
|
+
throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
|
|
1408
|
+
}
|
|
1409
|
+
if (normalized.length > MAX_NAME_LENGTH) {
|
|
1410
|
+
throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
|
|
1411
|
+
}
|
|
1412
|
+
return normalized;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
|
|
1416
|
+
* file creation. On a millisecond-stamp collision, mints a later stamp
|
|
1417
|
+
* and retries. Returns the created path.
|
|
1418
|
+
*/
|
|
1419
|
+
create(name) {
|
|
1420
|
+
const normalized = _PatchCreator.normalizeName(name);
|
|
1421
|
+
if (!fs5.existsSync(this.patchFolder)) {
|
|
1422
|
+
fs5.mkdirSync(this.patchFolder, { recursive: true });
|
|
1423
|
+
}
|
|
1424
|
+
let stamp = Date.now();
|
|
1425
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
1426
|
+
const filePath = path4.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
|
|
1427
|
+
try {
|
|
1428
|
+
fs5.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
|
|
1429
|
+
return filePath;
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
if (error && error.code === "EEXIST") {
|
|
1432
|
+
stamp += 1;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
throw error;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
throw new CLIError("Unable to create patch file: too many filename collisions");
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
|
|
617
1444
|
// framework/SQLRunner.ts
|
|
618
1445
|
function isPromiseLike(value) {
|
|
619
1446
|
return !!value && typeof value.then === "function";
|
|
@@ -933,8 +1760,8 @@ ${sql}
|
|
|
933
1760
|
});
|
|
934
1761
|
|
|
935
1762
|
// framework/MigrationRunner.ts
|
|
936
|
-
import
|
|
937
|
-
function
|
|
1763
|
+
import fs6 from "fs";
|
|
1764
|
+
function toError2(error) {
|
|
938
1765
|
if (error instanceof Error) return error;
|
|
939
1766
|
return new Error(String(error));
|
|
940
1767
|
}
|
|
@@ -957,6 +1784,9 @@ var init_MigrationRunner = __esm({
|
|
|
957
1784
|
init_MigrationSetup();
|
|
958
1785
|
init_MigrationFilter();
|
|
959
1786
|
init_MigrationDialectParser();
|
|
1787
|
+
init_PatchRunner();
|
|
1788
|
+
init_PatchCreator();
|
|
1789
|
+
init_PatchTypes();
|
|
960
1790
|
init_SQLRunner();
|
|
961
1791
|
init_errors();
|
|
962
1792
|
MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
@@ -967,10 +1797,12 @@ var init_MigrationRunner = __esm({
|
|
|
967
1797
|
return __async(this, null, function* () {
|
|
968
1798
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
969
1799
|
const config = configReader.loadFile();
|
|
1800
|
+
let factoryOwnsConnection = false;
|
|
970
1801
|
if (!conn) {
|
|
971
1802
|
conn = yield this.createConnection(config);
|
|
1803
|
+
factoryOwnsConnection = true;
|
|
972
1804
|
}
|
|
973
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1805
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
974
1806
|
});
|
|
975
1807
|
}
|
|
976
1808
|
static createConnection(config) {
|
|
@@ -990,7 +1822,7 @@ var init_MigrationRunner = __esm({
|
|
|
990
1822
|
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
991
1823
|
return conn;
|
|
992
1824
|
} catch (error) {
|
|
993
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1825
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
994
1826
|
}
|
|
995
1827
|
case "sqlite":
|
|
996
1828
|
if (!config.sqlite) {
|
|
@@ -1005,7 +1837,7 @@ var init_MigrationRunner = __esm({
|
|
|
1005
1837
|
});
|
|
1006
1838
|
return conn;
|
|
1007
1839
|
} catch (error) {
|
|
1008
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1840
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
1009
1841
|
}
|
|
1010
1842
|
case "pg":
|
|
1011
1843
|
if (!config.pg && !process.env.DATABASE_URL) {
|
|
@@ -1021,7 +1853,7 @@ var init_MigrationRunner = __esm({
|
|
|
1021
1853
|
yield conn.connect();
|
|
1022
1854
|
return conn;
|
|
1023
1855
|
} catch (error) {
|
|
1024
|
-
throw DatabaseConnectionError.connectionFailed("pg",
|
|
1856
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
1025
1857
|
}
|
|
1026
1858
|
default:
|
|
1027
1859
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -1035,7 +1867,7 @@ var init_MigrationRunner = __esm({
|
|
|
1035
1867
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
1036
1868
|
});
|
|
1037
1869
|
}
|
|
1038
|
-
create(config, conn) {
|
|
1870
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
1039
1871
|
return __async(this, null, function* () {
|
|
1040
1872
|
let sqlrunner;
|
|
1041
1873
|
let driverConnection = conn;
|
|
@@ -1048,8 +1880,19 @@ var init_MigrationRunner = __esm({
|
|
|
1048
1880
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
1049
1881
|
const read_strategy = this.getReadStategy(config);
|
|
1050
1882
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
1051
|
-
|
|
1052
|
-
|
|
1883
|
+
const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
|
|
1884
|
+
try {
|
|
1885
|
+
yield runner.setup();
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
if (factoryOwnsConnection) {
|
|
1888
|
+
try {
|
|
1889
|
+
yield sqlrunner.end();
|
|
1890
|
+
} catch (e) {
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
throw error;
|
|
1894
|
+
}
|
|
1895
|
+
return runner;
|
|
1053
1896
|
});
|
|
1054
1897
|
}
|
|
1055
1898
|
createEmpty(config) {
|
|
@@ -1059,7 +1902,7 @@ var init_MigrationRunner = __esm({
|
|
|
1059
1902
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
1060
1903
|
const read_strategy = this.getReadStategy(config);
|
|
1061
1904
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
1062
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1905
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
1063
1906
|
});
|
|
1064
1907
|
}
|
|
1065
1908
|
getReadStategy(config) {
|
|
@@ -1076,33 +1919,77 @@ var init_MigrationRunner = __esm({
|
|
|
1076
1919
|
}
|
|
1077
1920
|
};
|
|
1078
1921
|
MySQLMigrationRunner = class {
|
|
1079
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1922
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
1080
1923
|
this.config = config;
|
|
1081
1924
|
this.directory = directory;
|
|
1082
1925
|
this.setupRunner = setupRunner;
|
|
1083
1926
|
this.sqlrunner = sqlrunner;
|
|
1084
1927
|
this.connection = connection;
|
|
1928
|
+
this.preflightEnabled = preflightEnabled;
|
|
1929
|
+
/**
|
|
1930
|
+
* Memoized in-flight preflight promise. Simultaneous or repeated calls
|
|
1931
|
+
* to setup() on one runner execute the preflight (migration table setup
|
|
1932
|
+
* + patch application) exactly once. Cleared after rejection so a caller
|
|
1933
|
+
* may retry after fixing the cause.
|
|
1934
|
+
*/
|
|
1935
|
+
this.preflightPromise = null;
|
|
1936
|
+
this.lastPatchResults = [];
|
|
1085
1937
|
}
|
|
1086
1938
|
setup() {
|
|
1939
|
+
return __async(this, null, function* () {
|
|
1940
|
+
if (!this.preflightEnabled) {
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (!this.preflightPromise) {
|
|
1944
|
+
this.preflightPromise = this.runPreflight();
|
|
1945
|
+
this.preflightPromise.catch(() => {
|
|
1946
|
+
this.preflightPromise = null;
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
return this.preflightPromise;
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
runPreflight() {
|
|
1087
1953
|
return __async(this, null, function* () {
|
|
1088
1954
|
try {
|
|
1089
1955
|
yield this.setupRunner.setup();
|
|
1090
1956
|
} catch (error) {
|
|
1091
|
-
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0,
|
|
1957
|
+
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
|
|
1092
1958
|
}
|
|
1959
|
+
const patchRunner = new PatchRunner(this.sqlrunner, this.config);
|
|
1960
|
+
this.lastPatchResults = yield patchRunner.applyPending();
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Delegates to the same idempotent preflight; returns the results of the
|
|
1965
|
+
* patch pass that ran (or is running) for this runner.
|
|
1966
|
+
*/
|
|
1967
|
+
applyPendingPatches() {
|
|
1968
|
+
return __async(this, null, function* () {
|
|
1969
|
+
yield this.setup();
|
|
1970
|
+
return this.lastPatchResults;
|
|
1093
1971
|
});
|
|
1094
1972
|
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Scaffolds a new ledger patch file and returns the created path.
|
|
1975
|
+
* Never connects to a database.
|
|
1976
|
+
*/
|
|
1977
|
+
createPatch(name) {
|
|
1978
|
+
const creator = new PatchCreator(resolvePatchFolder(this.config));
|
|
1979
|
+
return creator.create(name);
|
|
1980
|
+
}
|
|
1095
1981
|
terminate() {
|
|
1096
1982
|
return __async(this, null, function* () {
|
|
1097
1983
|
try {
|
|
1098
1984
|
yield this.setupRunner.teardown();
|
|
1099
1985
|
} catch (error) {
|
|
1100
|
-
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0,
|
|
1986
|
+
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
|
|
1101
1987
|
}
|
|
1102
1988
|
});
|
|
1103
1989
|
}
|
|
1104
1990
|
getMigrationsHistory() {
|
|
1105
1991
|
return __async(this, null, function* () {
|
|
1992
|
+
yield this.setup();
|
|
1106
1993
|
try {
|
|
1107
1994
|
const results = yield this.sqlrunner.query(`
|
|
1108
1995
|
select *
|
|
@@ -1110,16 +1997,17 @@ var init_MigrationRunner = __esm({
|
|
|
1110
1997
|
`);
|
|
1111
1998
|
return results[0];
|
|
1112
1999
|
} catch (error) {
|
|
1113
|
-
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0,
|
|
2000
|
+
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
|
|
1114
2001
|
}
|
|
1115
2002
|
});
|
|
1116
2003
|
}
|
|
1117
2004
|
getMigrations() {
|
|
1118
2005
|
return __async(this, null, function* () {
|
|
2006
|
+
yield this.setup();
|
|
1119
2007
|
try {
|
|
1120
2008
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
1121
2009
|
} catch (error) {
|
|
1122
|
-
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0,
|
|
2010
|
+
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
|
|
1123
2011
|
}
|
|
1124
2012
|
});
|
|
1125
2013
|
}
|
|
@@ -1132,7 +2020,7 @@ var init_MigrationRunner = __esm({
|
|
|
1132
2020
|
if (error instanceof MigrationExecutionError) {
|
|
1133
2021
|
throw error;
|
|
1134
2022
|
}
|
|
1135
|
-
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0,
|
|
2023
|
+
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
|
|
1136
2024
|
}
|
|
1137
2025
|
});
|
|
1138
2026
|
}
|
|
@@ -1145,12 +2033,13 @@ var init_MigrationRunner = __esm({
|
|
|
1145
2033
|
if (error instanceof MigrationExecutionError) {
|
|
1146
2034
|
throw error;
|
|
1147
2035
|
}
|
|
1148
|
-
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0,
|
|
2036
|
+
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
|
|
1149
2037
|
}
|
|
1150
2038
|
});
|
|
1151
2039
|
}
|
|
1152
2040
|
migrate(migrationNodes, forward) {
|
|
1153
2041
|
return __async(this, null, function* () {
|
|
2042
|
+
yield this.setup();
|
|
1154
2043
|
for (let node of migrationNodes) {
|
|
1155
2044
|
try {
|
|
1156
2045
|
if (forward) {
|
|
@@ -1163,7 +2052,7 @@ var init_MigrationRunner = __esm({
|
|
|
1163
2052
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
1164
2053
|
node.name || String(node),
|
|
1165
2054
|
forward ? node.up_sql() : node.down_sql(),
|
|
1166
|
-
|
|
2055
|
+
toError2(error)
|
|
1167
2056
|
);
|
|
1168
2057
|
}
|
|
1169
2058
|
}
|
|
@@ -1171,6 +2060,7 @@ var init_MigrationRunner = __esm({
|
|
|
1171
2060
|
}
|
|
1172
2061
|
reset() {
|
|
1173
2062
|
return __async(this, null, function* () {
|
|
2063
|
+
yield this.setup();
|
|
1174
2064
|
try {
|
|
1175
2065
|
let migrations = yield this.getMigrations();
|
|
1176
2066
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -1182,7 +2072,7 @@ var init_MigrationRunner = __esm({
|
|
|
1182
2072
|
if (error instanceof MigrationExecutionError) {
|
|
1183
2073
|
throw error;
|
|
1184
2074
|
}
|
|
1185
|
-
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0,
|
|
2075
|
+
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
|
|
1186
2076
|
}
|
|
1187
2077
|
});
|
|
1188
2078
|
}
|
|
@@ -1191,7 +2081,7 @@ var init_MigrationRunner = __esm({
|
|
|
1191
2081
|
const creator = new MigrationCreator(this.config);
|
|
1192
2082
|
creator.create(name);
|
|
1193
2083
|
} catch (error) {
|
|
1194
|
-
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0,
|
|
2084
|
+
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
|
|
1195
2085
|
}
|
|
1196
2086
|
}
|
|
1197
2087
|
close() {
|
|
@@ -1200,7 +2090,7 @@ var init_MigrationRunner = __esm({
|
|
|
1200
2090
|
try {
|
|
1201
2091
|
yield this.sqlrunner.end();
|
|
1202
2092
|
} catch (error) {
|
|
1203
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
2093
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
1204
2094
|
}
|
|
1205
2095
|
}
|
|
1206
2096
|
});
|
|
@@ -1214,7 +2104,7 @@ var init_MigrationRunner = __esm({
|
|
|
1214
2104
|
return __async(this, null, function* () {
|
|
1215
2105
|
try {
|
|
1216
2106
|
console.log(`Checking for ${config_file}`);
|
|
1217
|
-
const config_exist =
|
|
2107
|
+
const config_exist = fs6.existsSync(config_file);
|
|
1218
2108
|
if (!config_exist) {
|
|
1219
2109
|
console.log(`Creating ${config_file}`);
|
|
1220
2110
|
const default_config = {
|
|
@@ -1228,11 +2118,11 @@ var init_MigrationRunner = __esm({
|
|
|
1228
2118
|
"password": ""
|
|
1229
2119
|
}
|
|
1230
2120
|
};
|
|
1231
|
-
|
|
2121
|
+
fs6.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
|
|
1232
2122
|
console.log(`Created ${config_file}`);
|
|
1233
2123
|
}
|
|
1234
2124
|
} catch (error) {
|
|
1235
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
2125
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1236
2126
|
}
|
|
1237
2127
|
});
|
|
1238
2128
|
}
|
|
@@ -1243,7 +2133,7 @@ var init_MigrationRunner = __esm({
|
|
|
1243
2133
|
}
|
|
1244
2134
|
loadFile() {
|
|
1245
2135
|
try {
|
|
1246
|
-
const fileContent =
|
|
2136
|
+
const fileContent = fs6.readFileSync(this.configFile);
|
|
1247
2137
|
const config = JSON.parse(fileContent.toString());
|
|
1248
2138
|
if (!config.migration_folder) {
|
|
1249
2139
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1267,7 +2157,7 @@ var init_MigrationRunner = __esm({
|
|
|
1267
2157
|
if (error instanceof ConfigurationError) {
|
|
1268
2158
|
throw error;
|
|
1269
2159
|
}
|
|
1270
|
-
const err =
|
|
2160
|
+
const err = toError2(error);
|
|
1271
2161
|
if (err.message.includes("ENOENT")) {
|
|
1272
2162
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1273
2163
|
}
|
|
@@ -1284,16 +2174,16 @@ var init_MigrationRunner = __esm({
|
|
|
1284
2174
|
throw new CLIError("Migration name is required");
|
|
1285
2175
|
}
|
|
1286
2176
|
try {
|
|
1287
|
-
if (!
|
|
1288
|
-
|
|
2177
|
+
if (!fs6.existsSync(this.config.migration_folder)) {
|
|
2178
|
+
fs6.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1289
2179
|
}
|
|
1290
2180
|
const now_timestamp = Date.now();
|
|
1291
2181
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1292
2182
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1293
|
-
|
|
2183
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1294
2184
|
-- Write your up migration here
|
|
1295
2185
|
`.trim());
|
|
1296
|
-
|
|
2186
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1297
2187
|
-- Write your down migration here
|
|
1298
2188
|
`.trim());
|
|
1299
2189
|
console.log(`Created migration files:`);
|
|
@@ -1303,7 +2193,7 @@ var init_MigrationRunner = __esm({
|
|
|
1303
2193
|
if (error instanceof CLIError) {
|
|
1304
2194
|
throw error;
|
|
1305
2195
|
}
|
|
1306
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2196
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1307
2197
|
}
|
|
1308
2198
|
}
|
|
1309
2199
|
};
|
|
@@ -1311,10 +2201,10 @@ var init_MigrationRunner = __esm({
|
|
|
1311
2201
|
});
|
|
1312
2202
|
|
|
1313
2203
|
// framework/SeedRunner.ts
|
|
1314
|
-
import
|
|
1315
|
-
import
|
|
2204
|
+
import fs7 from "fs";
|
|
2205
|
+
import path5 from "path";
|
|
1316
2206
|
import { pathToFileURL } from "url";
|
|
1317
|
-
import
|
|
2207
|
+
import Ajv2 from "ajv";
|
|
1318
2208
|
import addFormats from "ajv-formats";
|
|
1319
2209
|
import { tsImport } from "tsx/esm/api";
|
|
1320
2210
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1323,10 +2213,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1323
2213
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1324
2214
|
}
|
|
1325
2215
|
function walkForFile(rootDir, fileName) {
|
|
1326
|
-
if (!
|
|
1327
|
-
const entries =
|
|
2216
|
+
if (!fs7.existsSync(rootDir)) return null;
|
|
2217
|
+
const entries = fs7.readdirSync(rootDir, { withFileTypes: true });
|
|
1328
2218
|
for (const entry of entries) {
|
|
1329
|
-
const full =
|
|
2219
|
+
const full = path5.join(rootDir, entry.name);
|
|
1330
2220
|
if (entry.isDirectory()) {
|
|
1331
2221
|
const found = walkForFile(full, fileName);
|
|
1332
2222
|
if (found) return found;
|
|
@@ -1420,39 +2310,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1420
2310
|
}
|
|
1421
2311
|
function loadJson(filePath) {
|
|
1422
2312
|
return __async(this, null, function* () {
|
|
1423
|
-
const content = yield
|
|
2313
|
+
const content = yield fs7.promises.readFile(filePath, "utf8");
|
|
1424
2314
|
return JSON.parse(content);
|
|
1425
2315
|
});
|
|
1426
2316
|
}
|
|
1427
2317
|
function createValidator() {
|
|
1428
|
-
const
|
|
1429
|
-
addFormats(
|
|
1430
|
-
return
|
|
2318
|
+
const ajv2 = new Ajv2({ allErrors: true, strict: false });
|
|
2319
|
+
addFormats(ajv2);
|
|
2320
|
+
return ajv2;
|
|
1431
2321
|
}
|
|
1432
2322
|
function validateData(schemaPath, data, validate, log) {
|
|
1433
2323
|
return __async(this, null, function* () {
|
|
1434
2324
|
if (!validate || !schemaPath) return;
|
|
1435
|
-
const content = yield
|
|
2325
|
+
const content = yield fs7.promises.readFile(schemaPath, "utf8");
|
|
1436
2326
|
const schema = JSON.parse(content);
|
|
1437
|
-
const
|
|
1438
|
-
const validateFn =
|
|
2327
|
+
const ajv2 = createValidator();
|
|
2328
|
+
const validateFn = ajv2.compile(schema);
|
|
1439
2329
|
const ok = validateFn(data);
|
|
1440
2330
|
if (!ok) {
|
|
1441
2331
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1442
|
-
throw new Error(`Seed data validation failed: ${
|
|
2332
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1443
2333
|
}
|
|
1444
2334
|
});
|
|
1445
2335
|
}
|
|
1446
2336
|
function runSqlSeed(runner, resolved, direction) {
|
|
1447
2337
|
return __async(this, null, function* () {
|
|
1448
2338
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1449
|
-
const sql = yield
|
|
2339
|
+
const sql = yield fs7.promises.readFile(sqlPath, "utf8");
|
|
1450
2340
|
yield runner.query(sql);
|
|
1451
2341
|
});
|
|
1452
2342
|
}
|
|
1453
2343
|
function loadSeedModule(modulePath) {
|
|
1454
2344
|
return __async(this, null, function* () {
|
|
1455
|
-
const resolved =
|
|
2345
|
+
const resolved = path5.resolve(modulePath);
|
|
1456
2346
|
if (resolved.endsWith(".ts")) {
|
|
1457
2347
|
const fileUrl = pathToFileURL(resolved).href;
|
|
1458
2348
|
return tsImport(fileUrl, fileUrl);
|
|
@@ -1567,7 +2457,7 @@ var require_cli = __commonJS({
|
|
|
1567
2457
|
}
|
|
1568
2458
|
var commands = args.commands;
|
|
1569
2459
|
var command = commands[0];
|
|
1570
|
-
var load_database = !["init", "create", "help"].includes(command.toLowerCase());
|
|
2460
|
+
var load_database = !["init", "create", "help", "patch"].includes(command.toLowerCase());
|
|
1571
2461
|
var config_file = args.flags.config || "proper.json";
|
|
1572
2462
|
if (command.toLowerCase() === "help") {
|
|
1573
2463
|
printUsage();
|
|
@@ -1576,9 +2466,17 @@ var require_cli = __commonJS({
|
|
|
1576
2466
|
console.log(`Loading database: ${load_database}`);
|
|
1577
2467
|
var pending_runner = load_database ? MigrationRunnerFactory.create(config_file) : MigrationRunnerFactory.createEmpty(config_file);
|
|
1578
2468
|
pending_runner.then((runner) => __async(null, null, function* () {
|
|
2469
|
+
let failed = false;
|
|
1579
2470
|
try {
|
|
1580
2471
|
if (load_database) {
|
|
1581
|
-
yield runner.
|
|
2472
|
+
const patch_results = yield runner.applyPendingPatches();
|
|
2473
|
+
for (const patch of patch_results) {
|
|
2474
|
+
if (patch.status === "applied") {
|
|
2475
|
+
const changed = patch.operations.filter((op) => op.changed).length;
|
|
2476
|
+
const noop = patch.operations.length - changed;
|
|
2477
|
+
console.log(`Applied patch ${patch.fileName}: ${changed} changed, ${noop} no-op operation(s)`);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
1582
2480
|
}
|
|
1583
2481
|
switch (command.toLowerCase()) {
|
|
1584
2482
|
case "up":
|
|
@@ -1598,7 +2496,6 @@ var require_cli = __commonJS({
|
|
|
1598
2496
|
} else {
|
|
1599
2497
|
console.log("No pending migrations");
|
|
1600
2498
|
}
|
|
1601
|
-
yield runner.close();
|
|
1602
2499
|
break;
|
|
1603
2500
|
case "down":
|
|
1604
2501
|
const migrations_rollback = yield runner.getMigrations();
|
|
@@ -1619,13 +2516,11 @@ var require_cli = __commonJS({
|
|
|
1619
2516
|
} else {
|
|
1620
2517
|
console.log("No migrations to roll back");
|
|
1621
2518
|
}
|
|
1622
|
-
yield runner.close();
|
|
1623
2519
|
break;
|
|
1624
2520
|
case "reset":
|
|
1625
2521
|
console.log("Resetting all migrations...");
|
|
1626
2522
|
yield runner.reset();
|
|
1627
2523
|
console.log("Reset completed successfully");
|
|
1628
|
-
yield runner.close();
|
|
1629
2524
|
break;
|
|
1630
2525
|
case "create":
|
|
1631
2526
|
let filename = args.flags.name || commands[1];
|
|
@@ -1634,11 +2529,22 @@ var require_cli = __commonJS({
|
|
|
1634
2529
|
}
|
|
1635
2530
|
filename = filename.replace(/\s/g, "_");
|
|
1636
2531
|
runner.createMigration(filename);
|
|
1637
|
-
runner.close();
|
|
1638
2532
|
break;
|
|
2533
|
+
case "patch": {
|
|
2534
|
+
const patch_name = args.flags.name || commands[1];
|
|
2535
|
+
if (!patch_name) {
|
|
2536
|
+
throw new CLIError("Patch name is required. Usage: proper patch <name>");
|
|
2537
|
+
}
|
|
2538
|
+
const created_path = runner.createPatch(patch_name);
|
|
2539
|
+
console.log(`Created patch file:`);
|
|
2540
|
+
console.log(` ${created_path}`);
|
|
2541
|
+
console.log("");
|
|
2542
|
+
console.log("Complete this file before running another database-backed");
|
|
2543
|
+
console.log("Proper command: 'operations: []' is intentionally not runnable.");
|
|
2544
|
+
break;
|
|
2545
|
+
}
|
|
1639
2546
|
case "init":
|
|
1640
2547
|
yield runner.init(config_file);
|
|
1641
|
-
runner.close();
|
|
1642
2548
|
break;
|
|
1643
2549
|
case "status":
|
|
1644
2550
|
const { printTable } = __require("console-table-printer");
|
|
@@ -1652,7 +2558,6 @@ var require_cli = __commonJS({
|
|
|
1652
2558
|
};
|
|
1653
2559
|
}));
|
|
1654
2560
|
printTable(yield Promise.all(table));
|
|
1655
|
-
runner.close();
|
|
1656
2561
|
break;
|
|
1657
2562
|
case "query":
|
|
1658
2563
|
const sql_query = args.flags.query || commands[1];
|
|
@@ -1696,7 +2601,6 @@ Returned ${results.length} row(s)`);
|
|
|
1696
2601
|
} catch (error) {
|
|
1697
2602
|
throw new CLIError(`Query execution failed: ${error.message}`);
|
|
1698
2603
|
}
|
|
1699
|
-
yield runner.close();
|
|
1700
2604
|
break;
|
|
1701
2605
|
case "seed": {
|
|
1702
2606
|
const subCommands = commands.slice(1);
|
|
@@ -1729,7 +2633,6 @@ Returned ${results.length} row(s)`);
|
|
|
1729
2633
|
const reader = new FileMigrationConfigReader(config_file);
|
|
1730
2634
|
const migrationConfig = reader.loadFile();
|
|
1731
2635
|
yield runSeedsWithRunner(runner, migrationConfig, action, seedOptions);
|
|
1732
|
-
yield runner.close();
|
|
1733
2636
|
break;
|
|
1734
2637
|
}
|
|
1735
2638
|
default:
|
|
@@ -1740,9 +2643,25 @@ Returned ${results.length} row(s)`);
|
|
|
1740
2643
|
if (error.stack && process.env.DEBUG) {
|
|
1741
2644
|
console.error(error.stack);
|
|
1742
2645
|
}
|
|
2646
|
+
failed = true;
|
|
2647
|
+
} finally {
|
|
2648
|
+
try {
|
|
2649
|
+
yield runner.close();
|
|
2650
|
+
} catch (closeError) {
|
|
2651
|
+
console.error(`Error closing connection: ${closeError.message}`);
|
|
2652
|
+
failed = true;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
if (failed) {
|
|
1743
2656
|
process.exit(1);
|
|
1744
2657
|
}
|
|
1745
|
-
}))
|
|
2658
|
+
})).catch((error) => {
|
|
2659
|
+
console.error(`Error: ${error.message}`);
|
|
2660
|
+
if (error.stack && process.env.DEBUG) {
|
|
2661
|
+
console.error(error.stack);
|
|
2662
|
+
}
|
|
2663
|
+
process.exit(1);
|
|
2664
|
+
});
|
|
1746
2665
|
function printUsage() {
|
|
1747
2666
|
console.log(`
|
|
1748
2667
|
SQL Proper - Database migration tool
|
|
@@ -1755,6 +2674,7 @@ Commands:
|
|
|
1755
2674
|
down Roll back completed migrations
|
|
1756
2675
|
reset Roll back all migrations and reapply them
|
|
1757
2676
|
create Create a new migration
|
|
2677
|
+
patch Create a new ledger patch file (repairs migration history; no database access)
|
|
1758
2678
|
init Initialize a new config file
|
|
1759
2679
|
status Show migration status
|
|
1760
2680
|
query Execute a SQL query and display results
|
|
@@ -1773,6 +2693,7 @@ Examples:
|
|
|
1773
2693
|
proper down --increment 3 Roll back the last 3 applied migrations
|
|
1774
2694
|
proper down --all Roll back all completed migrations
|
|
1775
2695
|
proper create my_migration Create a new migration named "my_migration"
|
|
2696
|
+
proper patch fix_renamed_keys Scaffold a ledger patch file in the patch folder
|
|
1776
2697
|
proper init Create a new config file
|
|
1777
2698
|
proper status Show the status of all migrations
|
|
1778
2699
|
proper query "select * from users" Execute a SQL query
|