@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.js
CHANGED
|
@@ -123,11 +123,10 @@ function migration_filter(_0) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// framework/MigrationRunner.ts
|
|
126
|
-
var
|
|
126
|
+
var import_fs6 = __toESM(require("fs"));
|
|
127
127
|
|
|
128
128
|
// framework/MigrationDirectoryReader.ts
|
|
129
|
-
var
|
|
130
|
-
var import_path = __toESM(require("path"));
|
|
129
|
+
var import_fs2 = __toESM(require("fs"));
|
|
131
130
|
|
|
132
131
|
// framework/errors.ts
|
|
133
132
|
var MigrationError = class _MigrationError extends Error {
|
|
@@ -242,6 +241,54 @@ ${originalError.message}`;
|
|
|
242
241
|
return new _MigrationExecutionError(message);
|
|
243
242
|
}
|
|
244
243
|
};
|
|
244
|
+
var PatchError = class _PatchError extends MigrationError {
|
|
245
|
+
constructor(message, patchFile, patchKey) {
|
|
246
|
+
super(`Patch Error: ${message}`);
|
|
247
|
+
this.patchFile = patchFile;
|
|
248
|
+
this.patchKey = patchKey;
|
|
249
|
+
this.name = "PatchError";
|
|
250
|
+
Object.setPrototypeOf(this, _PatchError.prototype);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var PatchValidationError = class _PatchValidationError extends PatchError {
|
|
254
|
+
constructor(message, patchFile, patchKey) {
|
|
255
|
+
super(message, patchFile, patchKey);
|
|
256
|
+
this.name = "PatchValidationError";
|
|
257
|
+
Object.setPrototypeOf(this, _PatchValidationError.prototype);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
var PatchIntegrityError = class _PatchIntegrityError extends PatchError {
|
|
261
|
+
constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
|
|
262
|
+
super(message, patchFile, patchKey);
|
|
263
|
+
this.expectedChecksum = expectedChecksum;
|
|
264
|
+
this.actualChecksum = actualChecksum;
|
|
265
|
+
this.name = "PatchIntegrityError";
|
|
266
|
+
Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var PatchConflictError = class _PatchConflictError extends PatchError {
|
|
270
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
|
|
271
|
+
super(message, patchFile, patchKey);
|
|
272
|
+
this.operationIndex = operationIndex;
|
|
273
|
+
this.operationVerb = operationVerb;
|
|
274
|
+
this.migrationKeys = migrationKeys;
|
|
275
|
+
this.observedRowCounts = observedRowCounts;
|
|
276
|
+
this.name = "PatchConflictError";
|
|
277
|
+
Object.setPrototypeOf(this, _PatchConflictError.prototype);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var PatchExecutionError = class _PatchExecutionError extends PatchError {
|
|
281
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
|
|
282
|
+
super(originalError ? `${message}
|
|
283
|
+
Original Error:
|
|
284
|
+
${originalError.message}` : message, patchFile, patchKey);
|
|
285
|
+
this.operationIndex = operationIndex;
|
|
286
|
+
this.operationVerb = operationVerb;
|
|
287
|
+
this.originalError = originalError;
|
|
288
|
+
this.name = "PatchExecutionError";
|
|
289
|
+
Object.setPrototypeOf(this, _PatchExecutionError.prototype);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
245
292
|
var CLIError = class _CLIError extends MigrationError {
|
|
246
293
|
constructor(message) {
|
|
247
294
|
super(`CLI Error: ${message}`);
|
|
@@ -404,6 +451,36 @@ var SqlMigrationBuilder = class {
|
|
|
404
451
|
}
|
|
405
452
|
};
|
|
406
453
|
|
|
454
|
+
// framework/MigrationManifest.ts
|
|
455
|
+
var import_fs = __toESM(require("fs"));
|
|
456
|
+
var import_path = __toESM(require("path"));
|
|
457
|
+
function canonicalMigrationKey(value) {
|
|
458
|
+
return value.replace(/(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i, "").toLowerCase();
|
|
459
|
+
}
|
|
460
|
+
function resolveMigrationFile(directory, baseName, direction, dialect) {
|
|
461
|
+
const dialectExt = dialect === "sql" ? "mysql" : dialect;
|
|
462
|
+
const dialectFile = import_path.default.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
463
|
+
if (import_fs.default.existsSync(dialectFile)) return dialectFile;
|
|
464
|
+
const genericFile = import_path.default.join(directory, `${baseName}.${direction}.sql`);
|
|
465
|
+
if (import_fs.default.existsSync(genericFile)) return genericFile;
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
function loadMigrationManifest(directory, dialect) {
|
|
469
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
470
|
+
if (!import_fs.default.existsSync(directory)) return manifest;
|
|
471
|
+
const files = import_fs.default.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name);
|
|
472
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
473
|
+
files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
|
|
474
|
+
uniqueKeys.forEach((key) => {
|
|
475
|
+
manifest.set(key, {
|
|
476
|
+
key,
|
|
477
|
+
upFile: resolveMigrationFile(directory, key, "up", dialect),
|
|
478
|
+
downFile: resolveMigrationFile(directory, key, "down", dialect)
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
return manifest;
|
|
482
|
+
}
|
|
483
|
+
|
|
407
484
|
// framework/MigrationDirectoryReader.ts
|
|
408
485
|
var MigrationDirectoryReader = class {
|
|
409
486
|
constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
|
|
@@ -418,12 +495,7 @@ var MigrationDirectoryReader = class {
|
|
|
418
495
|
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
419
496
|
*/
|
|
420
497
|
resolveFile(baseName, direction) {
|
|
421
|
-
|
|
422
|
-
const dialectFile = import_path.default.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
423
|
-
if (import_fs.default.existsSync(dialectFile)) return dialectFile;
|
|
424
|
-
const genericFile = import_path.default.join(this.directory, `${baseName}.${direction}.sql`);
|
|
425
|
-
if (import_fs.default.existsSync(genericFile)) return genericFile;
|
|
426
|
-
return null;
|
|
498
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
427
499
|
}
|
|
428
500
|
/**
|
|
429
501
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
@@ -432,11 +504,11 @@ var MigrationDirectoryReader = class {
|
|
|
432
504
|
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
433
505
|
}
|
|
434
506
|
loadMigrations(table, connection) {
|
|
435
|
-
|
|
436
|
-
const dir_content =
|
|
507
|
+
import_fs2.default.existsSync(this.directory) || import_fs2.default.mkdirSync(this.directory);
|
|
508
|
+
const dir_content = import_fs2.default.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
|
|
437
509
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
438
510
|
dir_content.forEach((file) => {
|
|
439
|
-
const key = file
|
|
511
|
+
const key = canonicalMigrationKey(file);
|
|
440
512
|
uniqueKeys.add(key);
|
|
441
513
|
});
|
|
442
514
|
const migration_sorter = {};
|
|
@@ -476,14 +548,14 @@ var MigrationDirectoryReader = class {
|
|
|
476
548
|
return builder;
|
|
477
549
|
}
|
|
478
550
|
sql_up(file) {
|
|
479
|
-
let content =
|
|
551
|
+
let content = import_fs2.default.readFileSync(file).toString();
|
|
480
552
|
if (!this.isDialectSpecific(file)) {
|
|
481
553
|
content = this.read_strategy(content);
|
|
482
554
|
}
|
|
483
555
|
return content.trim();
|
|
484
556
|
}
|
|
485
557
|
sql_down(file) {
|
|
486
|
-
let content =
|
|
558
|
+
let content = import_fs2.default.readFileSync(file).toString();
|
|
487
559
|
if (!this.isDialectSpecific(file)) {
|
|
488
560
|
content = this.read_strategy(content);
|
|
489
561
|
}
|
|
@@ -492,7 +564,23 @@ var MigrationDirectoryReader = class {
|
|
|
492
564
|
};
|
|
493
565
|
|
|
494
566
|
// framework/MigrationSetup.ts
|
|
495
|
-
var
|
|
567
|
+
var import_fs3 = __toESM(require("fs"));
|
|
568
|
+
|
|
569
|
+
// framework/PatchTypes.ts
|
|
570
|
+
var import_path2 = __toESM(require("path"));
|
|
571
|
+
var PATCH_FORMAT_VERSION = 1;
|
|
572
|
+
var DEFAULT_PATCH_TABLE = "proper_patches";
|
|
573
|
+
var PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
|
|
574
|
+
function resolvePatchFolder(config) {
|
|
575
|
+
if (config.patch_folder) return config.patch_folder;
|
|
576
|
+
const dir = import_path2.default.dirname(config.migration_folder);
|
|
577
|
+
return dir === "." && !config.migration_folder.includes(import_path2.default.sep) && !config.migration_folder.includes("/") ? "patches" : import_path2.default.join(dir, "patches");
|
|
578
|
+
}
|
|
579
|
+
function resolvePatchTable(config) {
|
|
580
|
+
return config.patch_table || DEFAULT_PATCH_TABLE;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// framework/MigrationSetup.ts
|
|
496
584
|
var MigrationSetup = class {
|
|
497
585
|
constructor(sqlrunner, config) {
|
|
498
586
|
this.sqlrunner = sqlrunner;
|
|
@@ -500,7 +588,7 @@ var MigrationSetup = class {
|
|
|
500
588
|
}
|
|
501
589
|
setup() {
|
|
502
590
|
return __async(this, null, function* () {
|
|
503
|
-
|
|
591
|
+
import_fs3.default.existsSync(this.config.migration_folder) || import_fs3.default.mkdirSync(this.config.migration_folder);
|
|
504
592
|
const tableName = this.config.migration_table;
|
|
505
593
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
506
594
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -522,11 +610,39 @@ var MigrationSetup = class {
|
|
|
522
610
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
523
611
|
)`;
|
|
524
612
|
yield this.sqlrunner.query(createTableSql);
|
|
613
|
+
const patchTable = resolvePatchTable(this.config);
|
|
614
|
+
const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
615
|
+
migration_table TEXT NOT NULL,
|
|
616
|
+
patch_key TEXT NOT NULL,
|
|
617
|
+
checksum TEXT NOT NULL,
|
|
618
|
+
format_version INTEGER NOT NULL,
|
|
619
|
+
description TEXT NOT NULL,
|
|
620
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
621
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
622
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
623
|
+
migration_table TEXT NOT NULL,
|
|
624
|
+
patch_key TEXT NOT NULL,
|
|
625
|
+
checksum TEXT NOT NULL,
|
|
626
|
+
format_version INTEGER NOT NULL,
|
|
627
|
+
description TEXT NOT NULL,
|
|
628
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
629
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
630
|
+
)` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
631
|
+
migration_table VARCHAR(255) NOT NULL,
|
|
632
|
+
patch_key VARCHAR(255) NOT NULL,
|
|
633
|
+
checksum CHAR(64) NOT NULL,
|
|
634
|
+
format_version INT NOT NULL,
|
|
635
|
+
description TEXT NOT NULL,
|
|
636
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
637
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
638
|
+
)`;
|
|
639
|
+
yield this.sqlrunner.query(createPatchTableSql);
|
|
525
640
|
});
|
|
526
641
|
}
|
|
527
642
|
teardown() {
|
|
528
643
|
return __async(this, null, function* () {
|
|
529
644
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
645
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
530
646
|
});
|
|
531
647
|
}
|
|
532
648
|
};
|
|
@@ -588,6 +704,673 @@ function markerDialectParser(names) {
|
|
|
588
704
|
var SqliteDialectParser = markerDialectParser(["sqlite"]);
|
|
589
705
|
var PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
|
|
590
706
|
|
|
707
|
+
// framework/PatchDirectoryReader.ts
|
|
708
|
+
var import_crypto = __toESM(require("crypto"));
|
|
709
|
+
var import_fs4 = __toESM(require("fs"));
|
|
710
|
+
var import_path3 = __toESM(require("path"));
|
|
711
|
+
|
|
712
|
+
// framework/PatchValidator.ts
|
|
713
|
+
var import_ajv = __toESM(require("ajv"));
|
|
714
|
+
var import_yaml = require("yaml");
|
|
715
|
+
var MAX_DESCRIPTION_LENGTH = 500;
|
|
716
|
+
var MAX_MIGRATION_KEY_LENGTH = 255;
|
|
717
|
+
var migrationKeySchema = {
|
|
718
|
+
type: "string",
|
|
719
|
+
minLength: 1,
|
|
720
|
+
maxLength: MAX_MIGRATION_KEY_LENGTH
|
|
721
|
+
};
|
|
722
|
+
var patchSchema = {
|
|
723
|
+
type: "object",
|
|
724
|
+
additionalProperties: false,
|
|
725
|
+
required: ["version", "description", "operations"],
|
|
726
|
+
properties: {
|
|
727
|
+
version: { type: "integer" },
|
|
728
|
+
description: { type: "string" },
|
|
729
|
+
operations: {
|
|
730
|
+
type: "array",
|
|
731
|
+
minItems: 1,
|
|
732
|
+
items: {
|
|
733
|
+
type: "object",
|
|
734
|
+
additionalProperties: false,
|
|
735
|
+
minProperties: 1,
|
|
736
|
+
maxProperties: 1,
|
|
737
|
+
properties: {
|
|
738
|
+
rename_migration: {
|
|
739
|
+
type: "object",
|
|
740
|
+
additionalProperties: false,
|
|
741
|
+
required: ["from", "to"],
|
|
742
|
+
properties: { from: migrationKeySchema, to: migrationKeySchema }
|
|
743
|
+
},
|
|
744
|
+
mark_applied: {
|
|
745
|
+
type: "object",
|
|
746
|
+
additionalProperties: false,
|
|
747
|
+
required: ["key"],
|
|
748
|
+
properties: { key: migrationKeySchema }
|
|
749
|
+
},
|
|
750
|
+
unmark_applied: {
|
|
751
|
+
type: "object",
|
|
752
|
+
additionalProperties: false,
|
|
753
|
+
required: ["key"],
|
|
754
|
+
properties: { key: migrationKeySchema }
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
var ajv = new import_ajv.default({ allErrors: true, strict: true });
|
|
762
|
+
var validateSchema = ajv.compile(patchSchema);
|
|
763
|
+
function fail(message, file, patchKey) {
|
|
764
|
+
throw new PatchValidationError(message, file, patchKey);
|
|
765
|
+
}
|
|
766
|
+
function checkMigrationKey(value, context, file, patchKey) {
|
|
767
|
+
if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
|
|
768
|
+
if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
|
|
769
|
+
if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
|
|
770
|
+
if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
|
|
771
|
+
fail(`${context}: migration key length out of bounds`, file, patchKey);
|
|
772
|
+
}
|
|
773
|
+
return canonicalMigrationKey(value);
|
|
774
|
+
}
|
|
775
|
+
function assertStrictYaml(doc, file, patchKey) {
|
|
776
|
+
if (doc.errors.length > 0) {
|
|
777
|
+
fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
|
|
778
|
+
}
|
|
779
|
+
if (doc.warnings.length > 0) {
|
|
780
|
+
fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
|
|
781
|
+
}
|
|
782
|
+
const visit = (node) => {
|
|
783
|
+
var _a, _b;
|
|
784
|
+
if (node == null || typeof node !== "object") return;
|
|
785
|
+
if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
|
|
786
|
+
fail("YAML aliases are not permitted in patch files", file, patchKey);
|
|
787
|
+
}
|
|
788
|
+
if (node.anchor) {
|
|
789
|
+
fail("YAML anchors are not permitted in patch files", file, patchKey);
|
|
790
|
+
}
|
|
791
|
+
if (node.tag && ![
|
|
792
|
+
"tag:yaml.org,2002:str",
|
|
793
|
+
"tag:yaml.org,2002:int",
|
|
794
|
+
"tag:yaml.org,2002:bool",
|
|
795
|
+
"tag:yaml.org,2002:null",
|
|
796
|
+
"tag:yaml.org,2002:map",
|
|
797
|
+
"tag:yaml.org,2002:seq"
|
|
798
|
+
].includes(node.tag)) {
|
|
799
|
+
fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
|
|
800
|
+
}
|
|
801
|
+
if (Array.isArray(node.items)) {
|
|
802
|
+
for (const item of node.items) {
|
|
803
|
+
if (item && typeof item === "object" && "key" in item) {
|
|
804
|
+
const keyValue = (_b = item.key) == null ? void 0 : _b.value;
|
|
805
|
+
if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
|
|
806
|
+
visit(item.key);
|
|
807
|
+
visit(item.value);
|
|
808
|
+
} else {
|
|
809
|
+
visit(item);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
visit(doc.contents);
|
|
815
|
+
}
|
|
816
|
+
function parsePatchContent(content, fileName, patchKey) {
|
|
817
|
+
var _a;
|
|
818
|
+
const doc = (0, import_yaml.parseDocument)(content, {
|
|
819
|
+
uniqueKeys: true,
|
|
820
|
+
// duplicate mapping keys become errors
|
|
821
|
+
merge: false,
|
|
822
|
+
schema: "core",
|
|
823
|
+
version: "1.2"
|
|
824
|
+
});
|
|
825
|
+
assertStrictYaml(doc, fileName, patchKey);
|
|
826
|
+
const raw = doc.toJS({ mapAsMap: false });
|
|
827
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
828
|
+
fail("Patch document must be a YAML mapping", fileName, patchKey);
|
|
829
|
+
}
|
|
830
|
+
if (!validateSchema(raw)) {
|
|
831
|
+
const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
|
|
832
|
+
const anyRaw = raw;
|
|
833
|
+
if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
|
|
834
|
+
fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
835
|
+
}
|
|
836
|
+
fail(`Schema validation failed: ${detail}`, fileName, patchKey);
|
|
837
|
+
}
|
|
838
|
+
const parsed = raw;
|
|
839
|
+
if (parsed.version !== PATCH_FORMAT_VERSION) {
|
|
840
|
+
fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
841
|
+
}
|
|
842
|
+
const description = parsed.description.trim();
|
|
843
|
+
if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
|
|
844
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
845
|
+
fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
|
|
846
|
+
}
|
|
847
|
+
const operations = parsed.operations.map((op, index) => {
|
|
848
|
+
const verbs = Object.keys(op);
|
|
849
|
+
const verb = verbs[0];
|
|
850
|
+
const context = `operation ${index} (${verb})`;
|
|
851
|
+
switch (verb) {
|
|
852
|
+
case "rename_migration": {
|
|
853
|
+
const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
|
|
854
|
+
const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
|
|
855
|
+
if (from === to) {
|
|
856
|
+
fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
|
|
857
|
+
}
|
|
858
|
+
return { verb: "rename_migration", from, to };
|
|
859
|
+
}
|
|
860
|
+
case "mark_applied":
|
|
861
|
+
return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
|
|
862
|
+
case "unmark_applied":
|
|
863
|
+
return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
|
|
864
|
+
default:
|
|
865
|
+
fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
return { version: parsed.version, description, operations };
|
|
869
|
+
}
|
|
870
|
+
function validatePatchPlan(patches, manifest) {
|
|
871
|
+
const renames = [];
|
|
872
|
+
for (const patch of patches) {
|
|
873
|
+
patch.operations.forEach((op, index) => {
|
|
874
|
+
if (op.verb === "rename_migration") {
|
|
875
|
+
renames.push({ from: op.from, to: op.to, file: patch.fileName });
|
|
876
|
+
} else if (op.verb === "mark_applied") {
|
|
877
|
+
if (!manifest.has(op.key)) {
|
|
878
|
+
throw new PatchValidationError(
|
|
879
|
+
`operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
|
|
880
|
+
patch.fileName,
|
|
881
|
+
patch.patchKey
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
if (renames.length === 0) return;
|
|
888
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
889
|
+
renames.forEach((r) => {
|
|
890
|
+
mentioned.add(r.from);
|
|
891
|
+
mentioned.add(r.to);
|
|
892
|
+
});
|
|
893
|
+
const finalKey = (start) => {
|
|
894
|
+
let current = start;
|
|
895
|
+
for (const r of renames) {
|
|
896
|
+
if (current === r.from) current = r.to;
|
|
897
|
+
}
|
|
898
|
+
return current;
|
|
899
|
+
};
|
|
900
|
+
for (const key of mentioned) {
|
|
901
|
+
const finish = finalKey(key);
|
|
902
|
+
if (manifest.has(key)) {
|
|
903
|
+
if (finish !== key) {
|
|
904
|
+
throw new PatchValidationError(
|
|
905
|
+
`rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
if (!manifest.has(finish)) {
|
|
910
|
+
throw new PatchValidationError(
|
|
911
|
+
`rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// framework/PatchDirectoryReader.ts
|
|
919
|
+
var PatchDirectoryReader = class {
|
|
920
|
+
constructor(directory) {
|
|
921
|
+
this.directory = directory;
|
|
922
|
+
}
|
|
923
|
+
loadPatches() {
|
|
924
|
+
if (!import_fs4.default.existsSync(this.directory)) return [];
|
|
925
|
+
const entries = import_fs4.default.readdirSync(this.directory, { withFileTypes: true });
|
|
926
|
+
const patchFiles = [];
|
|
927
|
+
for (const entry of entries) {
|
|
928
|
+
if (!entry.name.endsWith(".yaml")) continue;
|
|
929
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
930
|
+
if (entry.isSymbolicLink()) {
|
|
931
|
+
throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
|
|
932
|
+
}
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
patchFiles.push(entry.name);
|
|
936
|
+
}
|
|
937
|
+
patchFiles.sort((a, b) => {
|
|
938
|
+
const stampA = parseInt(a.slice(0, 13), 10);
|
|
939
|
+
const stampB = parseInt(b.slice(0, 13), 10);
|
|
940
|
+
if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
|
|
941
|
+
return stampA - stampB;
|
|
942
|
+
}
|
|
943
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
944
|
+
});
|
|
945
|
+
return patchFiles.map((fileName) => {
|
|
946
|
+
const match = PATCH_FILENAME_REGEX.exec(fileName);
|
|
947
|
+
if (!match) {
|
|
948
|
+
throw new PatchValidationError(
|
|
949
|
+
`invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
|
|
950
|
+
fileName
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const patchKey = fileName.slice(0, -".yaml".length);
|
|
954
|
+
const filePath = import_path3.default.join(this.directory, fileName);
|
|
955
|
+
const bytes = import_fs4.default.readFileSync(filePath);
|
|
956
|
+
const checksum = import_crypto.default.createHash("sha256").update(bytes).digest("hex");
|
|
957
|
+
const content = bytes.toString("utf8");
|
|
958
|
+
const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
|
|
959
|
+
return { patchKey, fileName, filePath, checksum, version, description, operations };
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// framework/PatchRunner.ts
|
|
965
|
+
function toError(error) {
|
|
966
|
+
if (error instanceof Error) return error;
|
|
967
|
+
return new Error(String(error));
|
|
968
|
+
}
|
|
969
|
+
function extractRows(result) {
|
|
970
|
+
if (!Array.isArray(result)) return [];
|
|
971
|
+
if (Array.isArray(result[0])) return result[0];
|
|
972
|
+
if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
|
|
973
|
+
return result;
|
|
974
|
+
}
|
|
975
|
+
if (result[0] == null) return [];
|
|
976
|
+
return [result[0]];
|
|
977
|
+
}
|
|
978
|
+
var PatchRunner = class {
|
|
979
|
+
constructor(sqlrunner, config) {
|
|
980
|
+
this.sqlrunner = sqlrunner;
|
|
981
|
+
this.config = config;
|
|
982
|
+
this.patchTable = resolvePatchTable(config);
|
|
983
|
+
this.migrationTable = config.migration_table;
|
|
984
|
+
this.dialect = config.database;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Discovers, validates, and applies every unapplied patch in order.
|
|
988
|
+
* Each unapplied patch is its own transaction; earlier committed patches
|
|
989
|
+
* remain committed if a later patch fails.
|
|
990
|
+
*/
|
|
991
|
+
applyPending() {
|
|
992
|
+
return __async(this, null, function* () {
|
|
993
|
+
const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
|
|
994
|
+
const patches = reader.loadPatches();
|
|
995
|
+
const history = yield this.loadHistory();
|
|
996
|
+
if (patches.length === 0 && history.length === 0) {
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
const byKey = new Map(patches.map((p) => [p.patchKey, p]));
|
|
1000
|
+
for (const row of history) {
|
|
1001
|
+
const file = byKey.get(row.patch_key);
|
|
1002
|
+
if (!file) {
|
|
1003
|
+
throw new PatchIntegrityError(
|
|
1004
|
+
`applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
|
|
1005
|
+
void 0,
|
|
1006
|
+
row.patch_key
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
if (file.checksum !== row.checksum) {
|
|
1010
|
+
throw new PatchIntegrityError(
|
|
1011
|
+
`applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
|
|
1012
|
+
file.fileName,
|
|
1013
|
+
row.patch_key,
|
|
1014
|
+
row.checksum,
|
|
1015
|
+
file.checksum
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
|
|
1020
|
+
validatePatchPlan(patches, manifest);
|
|
1021
|
+
const appliedKeys = new Set(history.map((r) => r.patch_key));
|
|
1022
|
+
const results = [];
|
|
1023
|
+
for (const patch of patches) {
|
|
1024
|
+
if (appliedKeys.has(patch.patchKey)) {
|
|
1025
|
+
results.push({
|
|
1026
|
+
patchKey: patch.patchKey,
|
|
1027
|
+
fileName: patch.fileName,
|
|
1028
|
+
status: "already_applied",
|
|
1029
|
+
operations: []
|
|
1030
|
+
});
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
results.push(yield this.applyOne(patch, manifest));
|
|
1034
|
+
}
|
|
1035
|
+
return results;
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
loadHistory() {
|
|
1039
|
+
return __async(this, null, function* () {
|
|
1040
|
+
try {
|
|
1041
|
+
const result = yield this.sqlrunner.query(
|
|
1042
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
|
|
1043
|
+
[this.migrationTable]
|
|
1044
|
+
);
|
|
1045
|
+
return extractRows(result);
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
throw new PatchExecutionError(
|
|
1048
|
+
`failed to read patch history from '${this.patchTable}'`,
|
|
1049
|
+
void 0,
|
|
1050
|
+
void 0,
|
|
1051
|
+
void 0,
|
|
1052
|
+
void 0,
|
|
1053
|
+
toError(error)
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
beginSql() {
|
|
1059
|
+
switch (this.dialect) {
|
|
1060
|
+
case "sqlite":
|
|
1061
|
+
return "BEGIN IMMEDIATE";
|
|
1062
|
+
case "pg":
|
|
1063
|
+
return "BEGIN";
|
|
1064
|
+
default:
|
|
1065
|
+
return "START TRANSACTION";
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
begin(patch) {
|
|
1069
|
+
return __async(this, null, function* () {
|
|
1070
|
+
const deadline = Date.now() + 1e4;
|
|
1071
|
+
while (true) {
|
|
1072
|
+
try {
|
|
1073
|
+
yield this.sqlrunner.execute(this.beginSql());
|
|
1074
|
+
return;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
const message = toError(error).message;
|
|
1077
|
+
if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
|
|
1078
|
+
yield new Promise((resolve) => setTimeout(resolve, 50));
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
throw new PatchExecutionError(
|
|
1082
|
+
"failed to start patch transaction",
|
|
1083
|
+
patch.fileName,
|
|
1084
|
+
patch.patchKey,
|
|
1085
|
+
void 0,
|
|
1086
|
+
void 0,
|
|
1087
|
+
toError(error)
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
rollbackQuietly() {
|
|
1094
|
+
return __async(this, null, function* () {
|
|
1095
|
+
try {
|
|
1096
|
+
yield this.sqlrunner.execute("ROLLBACK");
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
applyOne(patch, manifest) {
|
|
1102
|
+
return __async(this, null, function* () {
|
|
1103
|
+
yield this.begin(patch);
|
|
1104
|
+
try {
|
|
1105
|
+
yield this.sqlrunner.execute(
|
|
1106
|
+
`INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
|
|
1107
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1108
|
+
[this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
|
|
1109
|
+
);
|
|
1110
|
+
} catch (claimError) {
|
|
1111
|
+
yield this.rollbackQuietly();
|
|
1112
|
+
const committed = yield this.findCommittedRow(patch.patchKey);
|
|
1113
|
+
if (committed) {
|
|
1114
|
+
if (committed.checksum === patch.checksum) {
|
|
1115
|
+
return {
|
|
1116
|
+
patchKey: patch.patchKey,
|
|
1117
|
+
fileName: patch.fileName,
|
|
1118
|
+
status: "already_applied",
|
|
1119
|
+
operations: []
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
throw new PatchIntegrityError(
|
|
1123
|
+
`patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
|
|
1124
|
+
patch.fileName,
|
|
1125
|
+
patch.patchKey,
|
|
1126
|
+
committed.checksum,
|
|
1127
|
+
patch.checksum
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
throw new PatchExecutionError(
|
|
1131
|
+
"failed to claim patch-history row",
|
|
1132
|
+
patch.fileName,
|
|
1133
|
+
patch.patchKey,
|
|
1134
|
+
void 0,
|
|
1135
|
+
void 0,
|
|
1136
|
+
toError(claimError)
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const operationResults = [];
|
|
1140
|
+
try {
|
|
1141
|
+
for (let index = 0; index < patch.operations.length; index++) {
|
|
1142
|
+
operationResults.push(
|
|
1143
|
+
yield this.applyOperation(patch, patch.operations[index], index, manifest)
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
yield this.sqlrunner.execute("COMMIT");
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
yield this.rollbackQuietly();
|
|
1149
|
+
if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
1152
|
+
throw new PatchExecutionError(
|
|
1153
|
+
"patch application failed",
|
|
1154
|
+
patch.fileName,
|
|
1155
|
+
patch.patchKey,
|
|
1156
|
+
void 0,
|
|
1157
|
+
void 0,
|
|
1158
|
+
toError(error)
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
patchKey: patch.patchKey,
|
|
1163
|
+
fileName: patch.fileName,
|
|
1164
|
+
status: "applied",
|
|
1165
|
+
operations: operationResults
|
|
1166
|
+
};
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
findCommittedRow(patchKey) {
|
|
1170
|
+
return __async(this, null, function* () {
|
|
1171
|
+
const result = yield this.sqlrunner.query(
|
|
1172
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
|
|
1173
|
+
[this.migrationTable, patchKey]
|
|
1174
|
+
);
|
|
1175
|
+
const list = extractRows(result);
|
|
1176
|
+
return list.length > 0 ? list[0] : null;
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
countRows(key) {
|
|
1180
|
+
return __async(this, null, function* () {
|
|
1181
|
+
var _a, _b, _c;
|
|
1182
|
+
const result = yield this.sqlrunner.query(
|
|
1183
|
+
`SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1184
|
+
[key]
|
|
1185
|
+
);
|
|
1186
|
+
const rows = extractRows(result);
|
|
1187
|
+
const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
|
|
1188
|
+
return Number(value != null ? value : 0);
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
conflict(patch, index, verb, message, keys, counts) {
|
|
1192
|
+
throw new PatchConflictError(
|
|
1193
|
+
`operation ${index} (${verb}): ${message}`,
|
|
1194
|
+
patch.fileName,
|
|
1195
|
+
patch.patchKey,
|
|
1196
|
+
index,
|
|
1197
|
+
verb,
|
|
1198
|
+
keys,
|
|
1199
|
+
counts
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
applyOperation(patch, op, index, manifest) {
|
|
1203
|
+
return __async(this, null, function* () {
|
|
1204
|
+
var _a, _b, _c, _d;
|
|
1205
|
+
try {
|
|
1206
|
+
switch (op.verb) {
|
|
1207
|
+
case "rename_migration": {
|
|
1208
|
+
const fromCount = yield this.countRows(op.from);
|
|
1209
|
+
const toCount = yield this.countRows(op.to);
|
|
1210
|
+
const counts = { [op.from]: fromCount, [op.to]: toCount };
|
|
1211
|
+
if (fromCount > 1 || toCount > 1) {
|
|
1212
|
+
this.conflict(
|
|
1213
|
+
patch,
|
|
1214
|
+
index,
|
|
1215
|
+
op.verb,
|
|
1216
|
+
`ledger corruption: duplicate rows for a migration key`,
|
|
1217
|
+
[op.from, op.to],
|
|
1218
|
+
counts
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
if (fromCount === 1 && toCount === 1) {
|
|
1222
|
+
this.conflict(
|
|
1223
|
+
patch,
|
|
1224
|
+
index,
|
|
1225
|
+
op.verb,
|
|
1226
|
+
`both '${op.from}' and '${op.to}' exist in the ledger`,
|
|
1227
|
+
[op.from, op.to],
|
|
1228
|
+
counts
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
if (fromCount === 0) {
|
|
1232
|
+
return { verb: op.verb, changed: false };
|
|
1233
|
+
}
|
|
1234
|
+
const target = manifest.get(op.to);
|
|
1235
|
+
if (target) {
|
|
1236
|
+
yield this.sqlrunner.execute(
|
|
1237
|
+
`UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
|
|
1238
|
+
[op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
|
|
1239
|
+
);
|
|
1240
|
+
} else {
|
|
1241
|
+
yield this.sqlrunner.execute(
|
|
1242
|
+
`UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
|
|
1243
|
+
[op.to, op.from]
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
return { verb: op.verb, changed: true };
|
|
1247
|
+
}
|
|
1248
|
+
case "mark_applied": {
|
|
1249
|
+
const count = yield this.countRows(op.key);
|
|
1250
|
+
if (count > 1) {
|
|
1251
|
+
this.conflict(
|
|
1252
|
+
patch,
|
|
1253
|
+
index,
|
|
1254
|
+
op.verb,
|
|
1255
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1256
|
+
[op.key],
|
|
1257
|
+
{ [op.key]: count }
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (count === 1) {
|
|
1261
|
+
return { verb: op.verb, changed: false };
|
|
1262
|
+
}
|
|
1263
|
+
const entry = manifest.get(op.key);
|
|
1264
|
+
yield this.sqlrunner.execute(
|
|
1265
|
+
`INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
|
|
1266
|
+
[op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
|
|
1267
|
+
);
|
|
1268
|
+
return { verb: op.verb, changed: true };
|
|
1269
|
+
}
|
|
1270
|
+
case "unmark_applied": {
|
|
1271
|
+
const count = yield this.countRows(op.key);
|
|
1272
|
+
if (count > 1) {
|
|
1273
|
+
this.conflict(
|
|
1274
|
+
patch,
|
|
1275
|
+
index,
|
|
1276
|
+
op.verb,
|
|
1277
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1278
|
+
[op.key],
|
|
1279
|
+
{ [op.key]: count }
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
if (count === 0) {
|
|
1283
|
+
return { verb: op.verb, changed: false };
|
|
1284
|
+
}
|
|
1285
|
+
yield this.sqlrunner.execute(
|
|
1286
|
+
`DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1287
|
+
[op.key]
|
|
1288
|
+
);
|
|
1289
|
+
return { verb: op.verb, changed: true };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if (error instanceof PatchConflictError) throw error;
|
|
1294
|
+
throw new PatchExecutionError(
|
|
1295
|
+
`operation failed`,
|
|
1296
|
+
patch.fileName,
|
|
1297
|
+
patch.patchKey,
|
|
1298
|
+
index,
|
|
1299
|
+
op.verb,
|
|
1300
|
+
toError(error)
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// framework/PatchCreator.ts
|
|
1308
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1309
|
+
var import_path4 = __toESM(require("path"));
|
|
1310
|
+
var MAX_NAME_LENGTH = 120;
|
|
1311
|
+
var SCAFFOLD = `version: 1
|
|
1312
|
+
description: TODO
|
|
1313
|
+
operations: []
|
|
1314
|
+
`;
|
|
1315
|
+
var PatchCreator = class _PatchCreator {
|
|
1316
|
+
constructor(patchFolder) {
|
|
1317
|
+
this.patchFolder = patchFolder;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
|
|
1321
|
+
* Rejects empty results, path separators, `..`, control characters,
|
|
1322
|
+
* characters outside [a-z0-9_-], and names longer than 120 characters.
|
|
1323
|
+
*/
|
|
1324
|
+
static normalizeName(name) {
|
|
1325
|
+
const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
|
|
1326
|
+
if (normalized.length === 0) {
|
|
1327
|
+
throw new CLIError("Patch name is required");
|
|
1328
|
+
}
|
|
1329
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
1330
|
+
throw new CLIError("Patch name must not contain path separators");
|
|
1331
|
+
}
|
|
1332
|
+
if (normalized.includes("..")) {
|
|
1333
|
+
throw new CLIError("Patch name must not contain '..'");
|
|
1334
|
+
}
|
|
1335
|
+
if (/[\x00-\x1f\x7f]/.test(normalized)) {
|
|
1336
|
+
throw new CLIError("Patch name must not contain control characters");
|
|
1337
|
+
}
|
|
1338
|
+
if (!/^[a-z0-9_-]+$/.test(normalized)) {
|
|
1339
|
+
throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
|
|
1340
|
+
}
|
|
1341
|
+
if (normalized.length > MAX_NAME_LENGTH) {
|
|
1342
|
+
throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
|
|
1343
|
+
}
|
|
1344
|
+
return normalized;
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
|
|
1348
|
+
* file creation. On a millisecond-stamp collision, mints a later stamp
|
|
1349
|
+
* and retries. Returns the created path.
|
|
1350
|
+
*/
|
|
1351
|
+
create(name) {
|
|
1352
|
+
const normalized = _PatchCreator.normalizeName(name);
|
|
1353
|
+
if (!import_fs5.default.existsSync(this.patchFolder)) {
|
|
1354
|
+
import_fs5.default.mkdirSync(this.patchFolder, { recursive: true });
|
|
1355
|
+
}
|
|
1356
|
+
let stamp = Date.now();
|
|
1357
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
1358
|
+
const filePath = import_path4.default.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
|
|
1359
|
+
try {
|
|
1360
|
+
import_fs5.default.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
|
|
1361
|
+
return filePath;
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
if (error && error.code === "EEXIST") {
|
|
1364
|
+
stamp += 1;
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
throw error;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
throw new CLIError("Unable to create patch file: too many filename collisions");
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
591
1374
|
// framework/SQLRunner.ts
|
|
592
1375
|
var BaseSQLRunner = class {
|
|
593
1376
|
/**
|
|
@@ -902,7 +1685,7 @@ var PgRunner = class _PgRunner extends BaseSQLRunner {
|
|
|
902
1685
|
};
|
|
903
1686
|
|
|
904
1687
|
// framework/MigrationRunner.ts
|
|
905
|
-
function
|
|
1688
|
+
function toError2(error) {
|
|
906
1689
|
if (error instanceof Error) return error;
|
|
907
1690
|
return new Error(String(error));
|
|
908
1691
|
}
|
|
@@ -926,10 +1709,12 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
926
1709
|
return __async(this, null, function* () {
|
|
927
1710
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
928
1711
|
const config = configReader.loadFile();
|
|
1712
|
+
let factoryOwnsConnection = false;
|
|
929
1713
|
if (!conn) {
|
|
930
1714
|
conn = yield this.createConnection(config);
|
|
1715
|
+
factoryOwnsConnection = true;
|
|
931
1716
|
}
|
|
932
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1717
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
933
1718
|
});
|
|
934
1719
|
}
|
|
935
1720
|
static createConnection(config) {
|
|
@@ -949,7 +1734,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
949
1734
|
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
950
1735
|
return conn;
|
|
951
1736
|
} catch (error) {
|
|
952
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1737
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
953
1738
|
}
|
|
954
1739
|
case "sqlite":
|
|
955
1740
|
if (!config.sqlite) {
|
|
@@ -964,7 +1749,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
964
1749
|
});
|
|
965
1750
|
return conn;
|
|
966
1751
|
} catch (error) {
|
|
967
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1752
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
968
1753
|
}
|
|
969
1754
|
case "pg":
|
|
970
1755
|
if (!config.pg && !process.env.DATABASE_URL) {
|
|
@@ -980,7 +1765,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
980
1765
|
yield conn.connect();
|
|
981
1766
|
return conn;
|
|
982
1767
|
} catch (error) {
|
|
983
|
-
throw DatabaseConnectionError.connectionFailed("pg",
|
|
1768
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
984
1769
|
}
|
|
985
1770
|
default:
|
|
986
1771
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -994,7 +1779,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
994
1779
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
995
1780
|
});
|
|
996
1781
|
}
|
|
997
|
-
create(config, conn) {
|
|
1782
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
998
1783
|
return __async(this, null, function* () {
|
|
999
1784
|
let sqlrunner;
|
|
1000
1785
|
let driverConnection = conn;
|
|
@@ -1007,8 +1792,19 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
1007
1792
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
1008
1793
|
const read_strategy = this.getReadStategy(config);
|
|
1009
1794
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
1010
|
-
|
|
1011
|
-
|
|
1795
|
+
const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
|
|
1796
|
+
try {
|
|
1797
|
+
yield runner.setup();
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
if (factoryOwnsConnection) {
|
|
1800
|
+
try {
|
|
1801
|
+
yield sqlrunner.end();
|
|
1802
|
+
} catch (e) {
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
throw error;
|
|
1806
|
+
}
|
|
1807
|
+
return runner;
|
|
1012
1808
|
});
|
|
1013
1809
|
}
|
|
1014
1810
|
createEmpty(config) {
|
|
@@ -1018,7 +1814,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
1018
1814
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
1019
1815
|
const read_strategy = this.getReadStategy(config);
|
|
1020
1816
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
1021
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1817
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
1022
1818
|
});
|
|
1023
1819
|
}
|
|
1024
1820
|
getReadStategy(config) {
|
|
@@ -1035,33 +1831,77 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
1035
1831
|
}
|
|
1036
1832
|
};
|
|
1037
1833
|
var MySQLMigrationRunner = class {
|
|
1038
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1834
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
1039
1835
|
this.config = config;
|
|
1040
1836
|
this.directory = directory;
|
|
1041
1837
|
this.setupRunner = setupRunner;
|
|
1042
1838
|
this.sqlrunner = sqlrunner;
|
|
1043
1839
|
this.connection = connection;
|
|
1840
|
+
this.preflightEnabled = preflightEnabled;
|
|
1841
|
+
/**
|
|
1842
|
+
* Memoized in-flight preflight promise. Simultaneous or repeated calls
|
|
1843
|
+
* to setup() on one runner execute the preflight (migration table setup
|
|
1844
|
+
* + patch application) exactly once. Cleared after rejection so a caller
|
|
1845
|
+
* may retry after fixing the cause.
|
|
1846
|
+
*/
|
|
1847
|
+
this.preflightPromise = null;
|
|
1848
|
+
this.lastPatchResults = [];
|
|
1044
1849
|
}
|
|
1045
1850
|
setup() {
|
|
1851
|
+
return __async(this, null, function* () {
|
|
1852
|
+
if (!this.preflightEnabled) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (!this.preflightPromise) {
|
|
1856
|
+
this.preflightPromise = this.runPreflight();
|
|
1857
|
+
this.preflightPromise.catch(() => {
|
|
1858
|
+
this.preflightPromise = null;
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
return this.preflightPromise;
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
runPreflight() {
|
|
1046
1865
|
return __async(this, null, function* () {
|
|
1047
1866
|
try {
|
|
1048
1867
|
yield this.setupRunner.setup();
|
|
1049
1868
|
} catch (error) {
|
|
1050
|
-
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0,
|
|
1869
|
+
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
|
|
1051
1870
|
}
|
|
1871
|
+
const patchRunner = new PatchRunner(this.sqlrunner, this.config);
|
|
1872
|
+
this.lastPatchResults = yield patchRunner.applyPending();
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Delegates to the same idempotent preflight; returns the results of the
|
|
1877
|
+
* patch pass that ran (or is running) for this runner.
|
|
1878
|
+
*/
|
|
1879
|
+
applyPendingPatches() {
|
|
1880
|
+
return __async(this, null, function* () {
|
|
1881
|
+
yield this.setup();
|
|
1882
|
+
return this.lastPatchResults;
|
|
1052
1883
|
});
|
|
1053
1884
|
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Scaffolds a new ledger patch file and returns the created path.
|
|
1887
|
+
* Never connects to a database.
|
|
1888
|
+
*/
|
|
1889
|
+
createPatch(name) {
|
|
1890
|
+
const creator = new PatchCreator(resolvePatchFolder(this.config));
|
|
1891
|
+
return creator.create(name);
|
|
1892
|
+
}
|
|
1054
1893
|
terminate() {
|
|
1055
1894
|
return __async(this, null, function* () {
|
|
1056
1895
|
try {
|
|
1057
1896
|
yield this.setupRunner.teardown();
|
|
1058
1897
|
} catch (error) {
|
|
1059
|
-
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0,
|
|
1898
|
+
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
|
|
1060
1899
|
}
|
|
1061
1900
|
});
|
|
1062
1901
|
}
|
|
1063
1902
|
getMigrationsHistory() {
|
|
1064
1903
|
return __async(this, null, function* () {
|
|
1904
|
+
yield this.setup();
|
|
1065
1905
|
try {
|
|
1066
1906
|
const results = yield this.sqlrunner.query(`
|
|
1067
1907
|
select *
|
|
@@ -1069,16 +1909,17 @@ var MySQLMigrationRunner = class {
|
|
|
1069
1909
|
`);
|
|
1070
1910
|
return results[0];
|
|
1071
1911
|
} catch (error) {
|
|
1072
|
-
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0,
|
|
1912
|
+
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
|
|
1073
1913
|
}
|
|
1074
1914
|
});
|
|
1075
1915
|
}
|
|
1076
1916
|
getMigrations() {
|
|
1077
1917
|
return __async(this, null, function* () {
|
|
1918
|
+
yield this.setup();
|
|
1078
1919
|
try {
|
|
1079
1920
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
1080
1921
|
} catch (error) {
|
|
1081
|
-
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0,
|
|
1922
|
+
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
|
|
1082
1923
|
}
|
|
1083
1924
|
});
|
|
1084
1925
|
}
|
|
@@ -1091,7 +1932,7 @@ var MySQLMigrationRunner = class {
|
|
|
1091
1932
|
if (error instanceof MigrationExecutionError) {
|
|
1092
1933
|
throw error;
|
|
1093
1934
|
}
|
|
1094
|
-
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0,
|
|
1935
|
+
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
|
|
1095
1936
|
}
|
|
1096
1937
|
});
|
|
1097
1938
|
}
|
|
@@ -1104,12 +1945,13 @@ var MySQLMigrationRunner = class {
|
|
|
1104
1945
|
if (error instanceof MigrationExecutionError) {
|
|
1105
1946
|
throw error;
|
|
1106
1947
|
}
|
|
1107
|
-
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0,
|
|
1948
|
+
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
|
|
1108
1949
|
}
|
|
1109
1950
|
});
|
|
1110
1951
|
}
|
|
1111
1952
|
migrate(migrationNodes, forward) {
|
|
1112
1953
|
return __async(this, null, function* () {
|
|
1954
|
+
yield this.setup();
|
|
1113
1955
|
for (let node of migrationNodes) {
|
|
1114
1956
|
try {
|
|
1115
1957
|
if (forward) {
|
|
@@ -1122,7 +1964,7 @@ var MySQLMigrationRunner = class {
|
|
|
1122
1964
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
1123
1965
|
node.name || String(node),
|
|
1124
1966
|
forward ? node.up_sql() : node.down_sql(),
|
|
1125
|
-
|
|
1967
|
+
toError2(error)
|
|
1126
1968
|
);
|
|
1127
1969
|
}
|
|
1128
1970
|
}
|
|
@@ -1130,6 +1972,7 @@ var MySQLMigrationRunner = class {
|
|
|
1130
1972
|
}
|
|
1131
1973
|
reset() {
|
|
1132
1974
|
return __async(this, null, function* () {
|
|
1975
|
+
yield this.setup();
|
|
1133
1976
|
try {
|
|
1134
1977
|
let migrations = yield this.getMigrations();
|
|
1135
1978
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -1141,7 +1984,7 @@ var MySQLMigrationRunner = class {
|
|
|
1141
1984
|
if (error instanceof MigrationExecutionError) {
|
|
1142
1985
|
throw error;
|
|
1143
1986
|
}
|
|
1144
|
-
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0,
|
|
1987
|
+
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
|
|
1145
1988
|
}
|
|
1146
1989
|
});
|
|
1147
1990
|
}
|
|
@@ -1150,7 +1993,7 @@ var MySQLMigrationRunner = class {
|
|
|
1150
1993
|
const creator = new MigrationCreator(this.config);
|
|
1151
1994
|
creator.create(name);
|
|
1152
1995
|
} catch (error) {
|
|
1153
|
-
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0,
|
|
1996
|
+
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
|
|
1154
1997
|
}
|
|
1155
1998
|
}
|
|
1156
1999
|
close() {
|
|
@@ -1159,7 +2002,7 @@ var MySQLMigrationRunner = class {
|
|
|
1159
2002
|
try {
|
|
1160
2003
|
yield this.sqlrunner.end();
|
|
1161
2004
|
} catch (error) {
|
|
1162
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
2005
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
1163
2006
|
}
|
|
1164
2007
|
}
|
|
1165
2008
|
});
|
|
@@ -1173,7 +2016,7 @@ var MySQLMigrationRunner = class {
|
|
|
1173
2016
|
return __async(this, null, function* () {
|
|
1174
2017
|
try {
|
|
1175
2018
|
console.log(`Checking for ${config_file2}`);
|
|
1176
|
-
const config_exist =
|
|
2019
|
+
const config_exist = import_fs6.default.existsSync(config_file2);
|
|
1177
2020
|
if (!config_exist) {
|
|
1178
2021
|
console.log(`Creating ${config_file2}`);
|
|
1179
2022
|
const default_config = {
|
|
@@ -1187,11 +2030,11 @@ var MySQLMigrationRunner = class {
|
|
|
1187
2030
|
"password": ""
|
|
1188
2031
|
}
|
|
1189
2032
|
};
|
|
1190
|
-
|
|
2033
|
+
import_fs6.default.writeFileSync(config_file2, JSON.stringify(default_config, null, 2));
|
|
1191
2034
|
console.log(`Created ${config_file2}`);
|
|
1192
2035
|
}
|
|
1193
2036
|
} catch (error) {
|
|
1194
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
2037
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1195
2038
|
}
|
|
1196
2039
|
});
|
|
1197
2040
|
}
|
|
@@ -1202,7 +2045,7 @@ var FileMigrationConfigReader = class {
|
|
|
1202
2045
|
}
|
|
1203
2046
|
loadFile() {
|
|
1204
2047
|
try {
|
|
1205
|
-
const fileContent =
|
|
2048
|
+
const fileContent = import_fs6.default.readFileSync(this.configFile);
|
|
1206
2049
|
const config = JSON.parse(fileContent.toString());
|
|
1207
2050
|
if (!config.migration_folder) {
|
|
1208
2051
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1226,7 +2069,7 @@ var FileMigrationConfigReader = class {
|
|
|
1226
2069
|
if (error instanceof ConfigurationError) {
|
|
1227
2070
|
throw error;
|
|
1228
2071
|
}
|
|
1229
|
-
const err =
|
|
2072
|
+
const err = toError2(error);
|
|
1230
2073
|
if (err.message.includes("ENOENT")) {
|
|
1231
2074
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1232
2075
|
}
|
|
@@ -1243,16 +2086,16 @@ var MigrationCreator = class {
|
|
|
1243
2086
|
throw new CLIError("Migration name is required");
|
|
1244
2087
|
}
|
|
1245
2088
|
try {
|
|
1246
|
-
if (!
|
|
1247
|
-
|
|
2089
|
+
if (!import_fs6.default.existsSync(this.config.migration_folder)) {
|
|
2090
|
+
import_fs6.default.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1248
2091
|
}
|
|
1249
2092
|
const now_timestamp = Date.now();
|
|
1250
2093
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1251
2094
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1252
|
-
|
|
2095
|
+
import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1253
2096
|
-- Write your up migration here
|
|
1254
2097
|
`.trim());
|
|
1255
|
-
|
|
2098
|
+
import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1256
2099
|
-- Write your down migration here
|
|
1257
2100
|
`.trim());
|
|
1258
2101
|
console.log(`Created migration files:`);
|
|
@@ -1262,16 +2105,16 @@ var MigrationCreator = class {
|
|
|
1262
2105
|
if (error instanceof CLIError) {
|
|
1263
2106
|
throw error;
|
|
1264
2107
|
}
|
|
1265
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2108
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1266
2109
|
}
|
|
1267
2110
|
}
|
|
1268
2111
|
};
|
|
1269
2112
|
|
|
1270
2113
|
// framework/SeedRunner.ts
|
|
1271
|
-
var
|
|
1272
|
-
var
|
|
2114
|
+
var import_fs7 = __toESM(require("fs"));
|
|
2115
|
+
var import_path5 = __toESM(require("path"));
|
|
1273
2116
|
var import_url = require("url");
|
|
1274
|
-
var
|
|
2117
|
+
var import_ajv2 = __toESM(require("ajv"));
|
|
1275
2118
|
var import_ajv_formats = __toESM(require("ajv-formats"));
|
|
1276
2119
|
var import_api = require("tsx/esm/api");
|
|
1277
2120
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1280,10 +2123,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1280
2123
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1281
2124
|
}
|
|
1282
2125
|
function walkForFile(rootDir, fileName) {
|
|
1283
|
-
if (!
|
|
1284
|
-
const entries =
|
|
2126
|
+
if (!import_fs7.default.existsSync(rootDir)) return null;
|
|
2127
|
+
const entries = import_fs7.default.readdirSync(rootDir, { withFileTypes: true });
|
|
1285
2128
|
for (const entry of entries) {
|
|
1286
|
-
const full =
|
|
2129
|
+
const full = import_path5.default.join(rootDir, entry.name);
|
|
1287
2130
|
if (entry.isDirectory()) {
|
|
1288
2131
|
const found = walkForFile(full, fileName);
|
|
1289
2132
|
if (found) return found;
|
|
@@ -1377,39 +2220,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1377
2220
|
}
|
|
1378
2221
|
function loadJson(filePath) {
|
|
1379
2222
|
return __async(this, null, function* () {
|
|
1380
|
-
const content = yield
|
|
2223
|
+
const content = yield import_fs7.default.promises.readFile(filePath, "utf8");
|
|
1381
2224
|
return JSON.parse(content);
|
|
1382
2225
|
});
|
|
1383
2226
|
}
|
|
1384
2227
|
function createValidator() {
|
|
1385
|
-
const
|
|
1386
|
-
(0, import_ajv_formats.default)(
|
|
1387
|
-
return
|
|
2228
|
+
const ajv2 = new import_ajv2.default({ allErrors: true, strict: false });
|
|
2229
|
+
(0, import_ajv_formats.default)(ajv2);
|
|
2230
|
+
return ajv2;
|
|
1388
2231
|
}
|
|
1389
2232
|
function validateData(schemaPath, data, validate, log) {
|
|
1390
2233
|
return __async(this, null, function* () {
|
|
1391
2234
|
if (!validate || !schemaPath) return;
|
|
1392
|
-
const content = yield
|
|
2235
|
+
const content = yield import_fs7.default.promises.readFile(schemaPath, "utf8");
|
|
1393
2236
|
const schema = JSON.parse(content);
|
|
1394
|
-
const
|
|
1395
|
-
const validateFn =
|
|
2237
|
+
const ajv2 = createValidator();
|
|
2238
|
+
const validateFn = ajv2.compile(schema);
|
|
1396
2239
|
const ok = validateFn(data);
|
|
1397
2240
|
if (!ok) {
|
|
1398
2241
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1399
|
-
throw new Error(`Seed data validation failed: ${
|
|
2242
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1400
2243
|
}
|
|
1401
2244
|
});
|
|
1402
2245
|
}
|
|
1403
2246
|
function runSqlSeed(runner, resolved, direction) {
|
|
1404
2247
|
return __async(this, null, function* () {
|
|
1405
2248
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1406
|
-
const sql = yield
|
|
2249
|
+
const sql = yield import_fs7.default.promises.readFile(sqlPath, "utf8");
|
|
1407
2250
|
yield runner.query(sql);
|
|
1408
2251
|
});
|
|
1409
2252
|
}
|
|
1410
2253
|
function loadSeedModule(modulePath) {
|
|
1411
2254
|
return __async(this, null, function* () {
|
|
1412
|
-
const resolved =
|
|
2255
|
+
const resolved = import_path5.default.resolve(modulePath);
|
|
1413
2256
|
if (resolved.endsWith(".ts")) {
|
|
1414
2257
|
const fileUrl = (0, import_url.pathToFileURL)(resolved).href;
|
|
1415
2258
|
return (0, import_api.tsImport)(fileUrl, fileUrl);
|
|
@@ -1510,7 +2353,7 @@ if (!args.commands || args.commands.length === 0) {
|
|
|
1510
2353
|
}
|
|
1511
2354
|
var commands = args.commands;
|
|
1512
2355
|
var command = commands[0];
|
|
1513
|
-
var load_database = !["init", "create", "help"].includes(command.toLowerCase());
|
|
2356
|
+
var load_database = !["init", "create", "help", "patch"].includes(command.toLowerCase());
|
|
1514
2357
|
var config_file = args.flags.config || "proper.json";
|
|
1515
2358
|
if (command.toLowerCase() === "help") {
|
|
1516
2359
|
printUsage();
|
|
@@ -1519,9 +2362,17 @@ if (command.toLowerCase() === "help") {
|
|
|
1519
2362
|
console.log(`Loading database: ${load_database}`);
|
|
1520
2363
|
var pending_runner = load_database ? MigrationRunnerFactory.create(config_file) : MigrationRunnerFactory.createEmpty(config_file);
|
|
1521
2364
|
pending_runner.then((runner) => __async(null, null, function* () {
|
|
2365
|
+
let failed = false;
|
|
1522
2366
|
try {
|
|
1523
2367
|
if (load_database) {
|
|
1524
|
-
yield runner.
|
|
2368
|
+
const patch_results = yield runner.applyPendingPatches();
|
|
2369
|
+
for (const patch of patch_results) {
|
|
2370
|
+
if (patch.status === "applied") {
|
|
2371
|
+
const changed = patch.operations.filter((op) => op.changed).length;
|
|
2372
|
+
const noop = patch.operations.length - changed;
|
|
2373
|
+
console.log(`Applied patch ${patch.fileName}: ${changed} changed, ${noop} no-op operation(s)`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
1525
2376
|
}
|
|
1526
2377
|
switch (command.toLowerCase()) {
|
|
1527
2378
|
case "up":
|
|
@@ -1541,7 +2392,6 @@ pending_runner.then((runner) => __async(null, null, function* () {
|
|
|
1541
2392
|
} else {
|
|
1542
2393
|
console.log("No pending migrations");
|
|
1543
2394
|
}
|
|
1544
|
-
yield runner.close();
|
|
1545
2395
|
break;
|
|
1546
2396
|
case "down":
|
|
1547
2397
|
const migrations_rollback = yield runner.getMigrations();
|
|
@@ -1562,13 +2412,11 @@ pending_runner.then((runner) => __async(null, null, function* () {
|
|
|
1562
2412
|
} else {
|
|
1563
2413
|
console.log("No migrations to roll back");
|
|
1564
2414
|
}
|
|
1565
|
-
yield runner.close();
|
|
1566
2415
|
break;
|
|
1567
2416
|
case "reset":
|
|
1568
2417
|
console.log("Resetting all migrations...");
|
|
1569
2418
|
yield runner.reset();
|
|
1570
2419
|
console.log("Reset completed successfully");
|
|
1571
|
-
yield runner.close();
|
|
1572
2420
|
break;
|
|
1573
2421
|
case "create":
|
|
1574
2422
|
let filename = args.flags.name || commands[1];
|
|
@@ -1577,11 +2425,22 @@ pending_runner.then((runner) => __async(null, null, function* () {
|
|
|
1577
2425
|
}
|
|
1578
2426
|
filename = filename.replace(/\s/g, "_");
|
|
1579
2427
|
runner.createMigration(filename);
|
|
1580
|
-
runner.close();
|
|
1581
2428
|
break;
|
|
2429
|
+
case "patch": {
|
|
2430
|
+
const patch_name = args.flags.name || commands[1];
|
|
2431
|
+
if (!patch_name) {
|
|
2432
|
+
throw new CLIError("Patch name is required. Usage: proper patch <name>");
|
|
2433
|
+
}
|
|
2434
|
+
const created_path = runner.createPatch(patch_name);
|
|
2435
|
+
console.log(`Created patch file:`);
|
|
2436
|
+
console.log(` ${created_path}`);
|
|
2437
|
+
console.log("");
|
|
2438
|
+
console.log("Complete this file before running another database-backed");
|
|
2439
|
+
console.log("Proper command: 'operations: []' is intentionally not runnable.");
|
|
2440
|
+
break;
|
|
2441
|
+
}
|
|
1582
2442
|
case "init":
|
|
1583
2443
|
yield runner.init(config_file);
|
|
1584
|
-
runner.close();
|
|
1585
2444
|
break;
|
|
1586
2445
|
case "status":
|
|
1587
2446
|
const { printTable } = require("console-table-printer");
|
|
@@ -1595,7 +2454,6 @@ pending_runner.then((runner) => __async(null, null, function* () {
|
|
|
1595
2454
|
};
|
|
1596
2455
|
}));
|
|
1597
2456
|
printTable(yield Promise.all(table));
|
|
1598
|
-
runner.close();
|
|
1599
2457
|
break;
|
|
1600
2458
|
case "query":
|
|
1601
2459
|
const sql_query = args.flags.query || commands[1];
|
|
@@ -1639,7 +2497,6 @@ Returned ${results.length} row(s)`);
|
|
|
1639
2497
|
} catch (error) {
|
|
1640
2498
|
throw new CLIError(`Query execution failed: ${error.message}`);
|
|
1641
2499
|
}
|
|
1642
|
-
yield runner.close();
|
|
1643
2500
|
break;
|
|
1644
2501
|
case "seed": {
|
|
1645
2502
|
const subCommands = commands.slice(1);
|
|
@@ -1672,7 +2529,6 @@ Returned ${results.length} row(s)`);
|
|
|
1672
2529
|
const reader = new FileMigrationConfigReader(config_file);
|
|
1673
2530
|
const migrationConfig = reader.loadFile();
|
|
1674
2531
|
yield runSeedsWithRunner(runner, migrationConfig, action, seedOptions);
|
|
1675
|
-
yield runner.close();
|
|
1676
2532
|
break;
|
|
1677
2533
|
}
|
|
1678
2534
|
default:
|
|
@@ -1683,9 +2539,25 @@ Returned ${results.length} row(s)`);
|
|
|
1683
2539
|
if (error.stack && process.env.DEBUG) {
|
|
1684
2540
|
console.error(error.stack);
|
|
1685
2541
|
}
|
|
2542
|
+
failed = true;
|
|
2543
|
+
} finally {
|
|
2544
|
+
try {
|
|
2545
|
+
yield runner.close();
|
|
2546
|
+
} catch (closeError) {
|
|
2547
|
+
console.error(`Error closing connection: ${closeError.message}`);
|
|
2548
|
+
failed = true;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
if (failed) {
|
|
1686
2552
|
process.exit(1);
|
|
1687
2553
|
}
|
|
1688
|
-
}))
|
|
2554
|
+
})).catch((error) => {
|
|
2555
|
+
console.error(`Error: ${error.message}`);
|
|
2556
|
+
if (error.stack && process.env.DEBUG) {
|
|
2557
|
+
console.error(error.stack);
|
|
2558
|
+
}
|
|
2559
|
+
process.exit(1);
|
|
2560
|
+
});
|
|
1689
2561
|
function printUsage() {
|
|
1690
2562
|
console.log(`
|
|
1691
2563
|
SQL Proper - Database migration tool
|
|
@@ -1698,6 +2570,7 @@ Commands:
|
|
|
1698
2570
|
down Roll back completed migrations
|
|
1699
2571
|
reset Roll back all migrations and reapply them
|
|
1700
2572
|
create Create a new migration
|
|
2573
|
+
patch Create a new ledger patch file (repairs migration history; no database access)
|
|
1701
2574
|
init Initialize a new config file
|
|
1702
2575
|
status Show migration status
|
|
1703
2576
|
query Execute a SQL query and display results
|
|
@@ -1716,6 +2589,7 @@ Examples:
|
|
|
1716
2589
|
proper down --increment 3 Roll back the last 3 applied migrations
|
|
1717
2590
|
proper down --all Roll back all completed migrations
|
|
1718
2591
|
proper create my_migration Create a new migration named "my_migration"
|
|
2592
|
+
proper patch fix_renamed_keys Scaffold a ledger patch file in the patch folder
|
|
1719
2593
|
proper init Create a new config file
|
|
1720
2594
|
proper status Show the status of all migrations
|
|
1721
2595
|
proper query "select * from users" Execute a SQL query
|