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