@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/index.js
CHANGED
|
@@ -66,23 +66,32 @@ var __async = (__this, __arguments, generator) => {
|
|
|
66
66
|
// index.ts
|
|
67
67
|
var index_exports = {};
|
|
68
68
|
__export(index_exports, {
|
|
69
|
+
MigrationError: () => MigrationError,
|
|
69
70
|
MigrationRunner: () => MySQLMigrationRunner,
|
|
70
71
|
MigrationRunnerFactory: () => MigrationRunnerFactory,
|
|
72
|
+
PatchConflictError: () => PatchConflictError,
|
|
73
|
+
PatchCreator: () => PatchCreator,
|
|
74
|
+
PatchError: () => PatchError,
|
|
75
|
+
PatchExecutionError: () => PatchExecutionError,
|
|
76
|
+
PatchIntegrityError: () => PatchIntegrityError,
|
|
77
|
+
PatchRunner: () => PatchRunner,
|
|
78
|
+
PatchValidationError: () => PatchValidationError,
|
|
71
79
|
PgRunner: () => PgRunner,
|
|
72
80
|
SQLRunner: () => SQLRunner,
|
|
73
81
|
SQLiteRunner: () => SQLiteRunner,
|
|
74
82
|
createSeedFactory: () => createSeedFactory,
|
|
75
83
|
loadMigrationConfig: () => loadMigrationConfig,
|
|
84
|
+
resolvePatchFolder: () => resolvePatchFolder,
|
|
85
|
+
resolvePatchTable: () => resolvePatchTable,
|
|
76
86
|
runSeedsWithRunner: () => runSeedsWithRunner
|
|
77
87
|
});
|
|
78
88
|
module.exports = __toCommonJS(index_exports);
|
|
79
89
|
|
|
80
90
|
// framework/MigrationRunner.ts
|
|
81
|
-
var
|
|
91
|
+
var import_fs6 = __toESM(require("fs"));
|
|
82
92
|
|
|
83
93
|
// framework/MigrationDirectoryReader.ts
|
|
84
|
-
var
|
|
85
|
-
var import_path = __toESM(require("path"));
|
|
94
|
+
var import_fs2 = __toESM(require("fs"));
|
|
86
95
|
|
|
87
96
|
// framework/errors.ts
|
|
88
97
|
var MigrationError = class _MigrationError extends Error {
|
|
@@ -197,6 +206,54 @@ ${originalError.message}`;
|
|
|
197
206
|
return new _MigrationExecutionError(message);
|
|
198
207
|
}
|
|
199
208
|
};
|
|
209
|
+
var PatchError = class _PatchError extends MigrationError {
|
|
210
|
+
constructor(message, patchFile, patchKey) {
|
|
211
|
+
super(`Patch Error: ${message}`);
|
|
212
|
+
this.patchFile = patchFile;
|
|
213
|
+
this.patchKey = patchKey;
|
|
214
|
+
this.name = "PatchError";
|
|
215
|
+
Object.setPrototypeOf(this, _PatchError.prototype);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var PatchValidationError = class _PatchValidationError extends PatchError {
|
|
219
|
+
constructor(message, patchFile, patchKey) {
|
|
220
|
+
super(message, patchFile, patchKey);
|
|
221
|
+
this.name = "PatchValidationError";
|
|
222
|
+
Object.setPrototypeOf(this, _PatchValidationError.prototype);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
var PatchIntegrityError = class _PatchIntegrityError extends PatchError {
|
|
226
|
+
constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
|
|
227
|
+
super(message, patchFile, patchKey);
|
|
228
|
+
this.expectedChecksum = expectedChecksum;
|
|
229
|
+
this.actualChecksum = actualChecksum;
|
|
230
|
+
this.name = "PatchIntegrityError";
|
|
231
|
+
Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
var PatchConflictError = class _PatchConflictError extends PatchError {
|
|
235
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
|
|
236
|
+
super(message, patchFile, patchKey);
|
|
237
|
+
this.operationIndex = operationIndex;
|
|
238
|
+
this.operationVerb = operationVerb;
|
|
239
|
+
this.migrationKeys = migrationKeys;
|
|
240
|
+
this.observedRowCounts = observedRowCounts;
|
|
241
|
+
this.name = "PatchConflictError";
|
|
242
|
+
Object.setPrototypeOf(this, _PatchConflictError.prototype);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var PatchExecutionError = class _PatchExecutionError extends PatchError {
|
|
246
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
|
|
247
|
+
super(originalError ? `${message}
|
|
248
|
+
Original Error:
|
|
249
|
+
${originalError.message}` : message, patchFile, patchKey);
|
|
250
|
+
this.operationIndex = operationIndex;
|
|
251
|
+
this.operationVerb = operationVerb;
|
|
252
|
+
this.originalError = originalError;
|
|
253
|
+
this.name = "PatchExecutionError";
|
|
254
|
+
Object.setPrototypeOf(this, _PatchExecutionError.prototype);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
200
257
|
var CLIError = class _CLIError extends MigrationError {
|
|
201
258
|
constructor(message) {
|
|
202
259
|
super(`CLI Error: ${message}`);
|
|
@@ -359,6 +416,40 @@ var SqlMigrationBuilder = class {
|
|
|
359
416
|
}
|
|
360
417
|
};
|
|
361
418
|
|
|
419
|
+
// framework/MigrationManifest.ts
|
|
420
|
+
var import_fs = __toESM(require("fs"));
|
|
421
|
+
var import_path = __toESM(require("path"));
|
|
422
|
+
var MIGRATION_FILE_REGEX = /(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i;
|
|
423
|
+
function isMigrationFile(fileName) {
|
|
424
|
+
return MIGRATION_FILE_REGEX.test(fileName);
|
|
425
|
+
}
|
|
426
|
+
function canonicalMigrationKey(value) {
|
|
427
|
+
return value.replace(MIGRATION_FILE_REGEX, "").toLowerCase();
|
|
428
|
+
}
|
|
429
|
+
function resolveMigrationFile(directory, baseName, direction, dialect) {
|
|
430
|
+
const dialectExt = dialect === "sql" ? "mysql" : dialect;
|
|
431
|
+
const dialectFile = import_path.default.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
432
|
+
if (import_fs.default.existsSync(dialectFile)) return dialectFile;
|
|
433
|
+
const genericFile = import_path.default.join(directory, `${baseName}.${direction}.sql`);
|
|
434
|
+
if (import_fs.default.existsSync(genericFile)) return genericFile;
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
function loadMigrationManifest(directory, dialect) {
|
|
438
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
439
|
+
if (!import_fs.default.existsSync(directory)) return manifest;
|
|
440
|
+
const files = import_fs.default.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name).filter(isMigrationFile);
|
|
441
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
442
|
+
files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
|
|
443
|
+
uniqueKeys.forEach((key) => {
|
|
444
|
+
manifest.set(key, {
|
|
445
|
+
key,
|
|
446
|
+
upFile: resolveMigrationFile(directory, key, "up", dialect),
|
|
447
|
+
downFile: resolveMigrationFile(directory, key, "down", dialect)
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
return manifest;
|
|
451
|
+
}
|
|
452
|
+
|
|
362
453
|
// framework/MigrationDirectoryReader.ts
|
|
363
454
|
var MigrationDirectoryReader = class {
|
|
364
455
|
constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
|
|
@@ -373,12 +464,7 @@ var MigrationDirectoryReader = class {
|
|
|
373
464
|
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
374
465
|
*/
|
|
375
466
|
resolveFile(baseName, direction) {
|
|
376
|
-
|
|
377
|
-
const dialectFile = import_path.default.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
378
|
-
if (import_fs.default.existsSync(dialectFile)) return dialectFile;
|
|
379
|
-
const genericFile = import_path.default.join(this.directory, `${baseName}.${direction}.sql`);
|
|
380
|
-
if (import_fs.default.existsSync(genericFile)) return genericFile;
|
|
381
|
-
return null;
|
|
467
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
382
468
|
}
|
|
383
469
|
/**
|
|
384
470
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
@@ -387,11 +473,11 @@ var MigrationDirectoryReader = class {
|
|
|
387
473
|
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
388
474
|
}
|
|
389
475
|
loadMigrations(table, connection) {
|
|
390
|
-
|
|
391
|
-
const dir_content =
|
|
476
|
+
import_fs2.default.existsSync(this.directory) || import_fs2.default.mkdirSync(this.directory);
|
|
477
|
+
const dir_content = import_fs2.default.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name).filter(isMigrationFile);
|
|
392
478
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
393
479
|
dir_content.forEach((file) => {
|
|
394
|
-
const key = file
|
|
480
|
+
const key = canonicalMigrationKey(file);
|
|
395
481
|
uniqueKeys.add(key);
|
|
396
482
|
});
|
|
397
483
|
const migration_sorter = {};
|
|
@@ -431,14 +517,14 @@ var MigrationDirectoryReader = class {
|
|
|
431
517
|
return builder;
|
|
432
518
|
}
|
|
433
519
|
sql_up(file) {
|
|
434
|
-
let content =
|
|
520
|
+
let content = import_fs2.default.readFileSync(file).toString();
|
|
435
521
|
if (!this.isDialectSpecific(file)) {
|
|
436
522
|
content = this.read_strategy(content);
|
|
437
523
|
}
|
|
438
524
|
return content.trim();
|
|
439
525
|
}
|
|
440
526
|
sql_down(file) {
|
|
441
|
-
let content =
|
|
527
|
+
let content = import_fs2.default.readFileSync(file).toString();
|
|
442
528
|
if (!this.isDialectSpecific(file)) {
|
|
443
529
|
content = this.read_strategy(content);
|
|
444
530
|
}
|
|
@@ -447,7 +533,23 @@ var MigrationDirectoryReader = class {
|
|
|
447
533
|
};
|
|
448
534
|
|
|
449
535
|
// framework/MigrationSetup.ts
|
|
450
|
-
var
|
|
536
|
+
var import_fs3 = __toESM(require("fs"));
|
|
537
|
+
|
|
538
|
+
// framework/PatchTypes.ts
|
|
539
|
+
var import_path2 = __toESM(require("path"));
|
|
540
|
+
var PATCH_FORMAT_VERSION = 1;
|
|
541
|
+
var DEFAULT_PATCH_TABLE = "proper_patches";
|
|
542
|
+
var PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
|
|
543
|
+
function resolvePatchFolder(config) {
|
|
544
|
+
if (config.patch_folder) return config.patch_folder;
|
|
545
|
+
const dir = import_path2.default.dirname(config.migration_folder);
|
|
546
|
+
return dir === "." && !config.migration_folder.includes(import_path2.default.sep) && !config.migration_folder.includes("/") ? "patches" : import_path2.default.join(dir, "patches");
|
|
547
|
+
}
|
|
548
|
+
function resolvePatchTable(config) {
|
|
549
|
+
return config.patch_table || DEFAULT_PATCH_TABLE;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// framework/MigrationSetup.ts
|
|
451
553
|
var MigrationSetup = class {
|
|
452
554
|
constructor(sqlrunner, config) {
|
|
453
555
|
this.sqlrunner = sqlrunner;
|
|
@@ -455,7 +557,7 @@ var MigrationSetup = class {
|
|
|
455
557
|
}
|
|
456
558
|
setup() {
|
|
457
559
|
return __async(this, null, function* () {
|
|
458
|
-
|
|
560
|
+
import_fs3.default.existsSync(this.config.migration_folder) || import_fs3.default.mkdirSync(this.config.migration_folder);
|
|
459
561
|
const tableName = this.config.migration_table;
|
|
460
562
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
461
563
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -477,11 +579,39 @@ var MigrationSetup = class {
|
|
|
477
579
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
478
580
|
)`;
|
|
479
581
|
yield this.sqlrunner.query(createTableSql);
|
|
582
|
+
const patchTable = resolvePatchTable(this.config);
|
|
583
|
+
const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
584
|
+
migration_table TEXT NOT NULL,
|
|
585
|
+
patch_key TEXT NOT NULL,
|
|
586
|
+
checksum TEXT NOT NULL,
|
|
587
|
+
format_version INTEGER NOT NULL,
|
|
588
|
+
description TEXT NOT NULL,
|
|
589
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
590
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
591
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
592
|
+
migration_table TEXT NOT NULL,
|
|
593
|
+
patch_key TEXT NOT NULL,
|
|
594
|
+
checksum TEXT NOT NULL,
|
|
595
|
+
format_version INTEGER NOT NULL,
|
|
596
|
+
description TEXT NOT NULL,
|
|
597
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
598
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
599
|
+
)` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
600
|
+
migration_table VARCHAR(255) NOT NULL,
|
|
601
|
+
patch_key VARCHAR(255) NOT NULL,
|
|
602
|
+
checksum CHAR(64) NOT NULL,
|
|
603
|
+
format_version INT NOT NULL,
|
|
604
|
+
description TEXT NOT NULL,
|
|
605
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
606
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
607
|
+
)`;
|
|
608
|
+
yield this.sqlrunner.query(createPatchTableSql);
|
|
480
609
|
});
|
|
481
610
|
}
|
|
482
611
|
teardown() {
|
|
483
612
|
return __async(this, null, function* () {
|
|
484
613
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
614
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
485
615
|
});
|
|
486
616
|
}
|
|
487
617
|
};
|
|
@@ -558,6 +688,673 @@ function markerDialectParser(names) {
|
|
|
558
688
|
var SqliteDialectParser = markerDialectParser(["sqlite"]);
|
|
559
689
|
var PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
|
|
560
690
|
|
|
691
|
+
// framework/PatchDirectoryReader.ts
|
|
692
|
+
var import_crypto = __toESM(require("crypto"));
|
|
693
|
+
var import_fs4 = __toESM(require("fs"));
|
|
694
|
+
var import_path3 = __toESM(require("path"));
|
|
695
|
+
|
|
696
|
+
// framework/PatchValidator.ts
|
|
697
|
+
var import_ajv = __toESM(require("ajv"));
|
|
698
|
+
var import_yaml = require("yaml");
|
|
699
|
+
var MAX_DESCRIPTION_LENGTH = 500;
|
|
700
|
+
var MAX_MIGRATION_KEY_LENGTH = 255;
|
|
701
|
+
var migrationKeySchema = {
|
|
702
|
+
type: "string",
|
|
703
|
+
minLength: 1,
|
|
704
|
+
maxLength: MAX_MIGRATION_KEY_LENGTH
|
|
705
|
+
};
|
|
706
|
+
var patchSchema = {
|
|
707
|
+
type: "object",
|
|
708
|
+
additionalProperties: false,
|
|
709
|
+
required: ["version", "description", "operations"],
|
|
710
|
+
properties: {
|
|
711
|
+
version: { type: "integer" },
|
|
712
|
+
description: { type: "string" },
|
|
713
|
+
operations: {
|
|
714
|
+
type: "array",
|
|
715
|
+
minItems: 1,
|
|
716
|
+
items: {
|
|
717
|
+
type: "object",
|
|
718
|
+
additionalProperties: false,
|
|
719
|
+
minProperties: 1,
|
|
720
|
+
maxProperties: 1,
|
|
721
|
+
properties: {
|
|
722
|
+
rename_migration: {
|
|
723
|
+
type: "object",
|
|
724
|
+
additionalProperties: false,
|
|
725
|
+
required: ["from", "to"],
|
|
726
|
+
properties: { from: migrationKeySchema, to: migrationKeySchema }
|
|
727
|
+
},
|
|
728
|
+
mark_applied: {
|
|
729
|
+
type: "object",
|
|
730
|
+
additionalProperties: false,
|
|
731
|
+
required: ["key"],
|
|
732
|
+
properties: { key: migrationKeySchema }
|
|
733
|
+
},
|
|
734
|
+
unmark_applied: {
|
|
735
|
+
type: "object",
|
|
736
|
+
additionalProperties: false,
|
|
737
|
+
required: ["key"],
|
|
738
|
+
properties: { key: migrationKeySchema }
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
var ajv = new import_ajv.default({ allErrors: true, strict: true });
|
|
746
|
+
var validateSchema = ajv.compile(patchSchema);
|
|
747
|
+
function fail(message, file, patchKey) {
|
|
748
|
+
throw new PatchValidationError(message, file, patchKey);
|
|
749
|
+
}
|
|
750
|
+
function checkMigrationKey(value, context, file, patchKey) {
|
|
751
|
+
if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
|
|
752
|
+
if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
|
|
753
|
+
if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
|
|
754
|
+
if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
|
|
755
|
+
fail(`${context}: migration key length out of bounds`, file, patchKey);
|
|
756
|
+
}
|
|
757
|
+
return canonicalMigrationKey(value);
|
|
758
|
+
}
|
|
759
|
+
function assertStrictYaml(doc, file, patchKey) {
|
|
760
|
+
if (doc.errors.length > 0) {
|
|
761
|
+
fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
|
|
762
|
+
}
|
|
763
|
+
if (doc.warnings.length > 0) {
|
|
764
|
+
fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
|
|
765
|
+
}
|
|
766
|
+
const visit = (node) => {
|
|
767
|
+
var _a, _b;
|
|
768
|
+
if (node == null || typeof node !== "object") return;
|
|
769
|
+
if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
|
|
770
|
+
fail("YAML aliases are not permitted in patch files", file, patchKey);
|
|
771
|
+
}
|
|
772
|
+
if (node.anchor) {
|
|
773
|
+
fail("YAML anchors are not permitted in patch files", file, patchKey);
|
|
774
|
+
}
|
|
775
|
+
if (node.tag && ![
|
|
776
|
+
"tag:yaml.org,2002:str",
|
|
777
|
+
"tag:yaml.org,2002:int",
|
|
778
|
+
"tag:yaml.org,2002:bool",
|
|
779
|
+
"tag:yaml.org,2002:null",
|
|
780
|
+
"tag:yaml.org,2002:map",
|
|
781
|
+
"tag:yaml.org,2002:seq"
|
|
782
|
+
].includes(node.tag)) {
|
|
783
|
+
fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
|
|
784
|
+
}
|
|
785
|
+
if (Array.isArray(node.items)) {
|
|
786
|
+
for (const item of node.items) {
|
|
787
|
+
if (item && typeof item === "object" && "key" in item) {
|
|
788
|
+
const keyValue = (_b = item.key) == null ? void 0 : _b.value;
|
|
789
|
+
if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
|
|
790
|
+
visit(item.key);
|
|
791
|
+
visit(item.value);
|
|
792
|
+
} else {
|
|
793
|
+
visit(item);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
visit(doc.contents);
|
|
799
|
+
}
|
|
800
|
+
function parsePatchContent(content, fileName, patchKey) {
|
|
801
|
+
var _a;
|
|
802
|
+
const doc = (0, import_yaml.parseDocument)(content, {
|
|
803
|
+
uniqueKeys: true,
|
|
804
|
+
// duplicate mapping keys become errors
|
|
805
|
+
merge: false,
|
|
806
|
+
schema: "core",
|
|
807
|
+
version: "1.2"
|
|
808
|
+
});
|
|
809
|
+
assertStrictYaml(doc, fileName, patchKey);
|
|
810
|
+
const raw = doc.toJS({ mapAsMap: false });
|
|
811
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
812
|
+
fail("Patch document must be a YAML mapping", fileName, patchKey);
|
|
813
|
+
}
|
|
814
|
+
if (!validateSchema(raw)) {
|
|
815
|
+
const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
|
|
816
|
+
const anyRaw = raw;
|
|
817
|
+
if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
|
|
818
|
+
fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
819
|
+
}
|
|
820
|
+
fail(`Schema validation failed: ${detail}`, fileName, patchKey);
|
|
821
|
+
}
|
|
822
|
+
const parsed = raw;
|
|
823
|
+
if (parsed.version !== PATCH_FORMAT_VERSION) {
|
|
824
|
+
fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
825
|
+
}
|
|
826
|
+
const description = parsed.description.trim();
|
|
827
|
+
if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
|
|
828
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
829
|
+
fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
|
|
830
|
+
}
|
|
831
|
+
const operations = parsed.operations.map((op, index) => {
|
|
832
|
+
const verbs = Object.keys(op);
|
|
833
|
+
const verb = verbs[0];
|
|
834
|
+
const context = `operation ${index} (${verb})`;
|
|
835
|
+
switch (verb) {
|
|
836
|
+
case "rename_migration": {
|
|
837
|
+
const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
|
|
838
|
+
const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
|
|
839
|
+
if (from === to) {
|
|
840
|
+
fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
|
|
841
|
+
}
|
|
842
|
+
return { verb: "rename_migration", from, to };
|
|
843
|
+
}
|
|
844
|
+
case "mark_applied":
|
|
845
|
+
return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
|
|
846
|
+
case "unmark_applied":
|
|
847
|
+
return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
|
|
848
|
+
default:
|
|
849
|
+
fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
return { version: parsed.version, description, operations };
|
|
853
|
+
}
|
|
854
|
+
function validatePatchPlan(patches, manifest) {
|
|
855
|
+
const renames = [];
|
|
856
|
+
for (const patch of patches) {
|
|
857
|
+
patch.operations.forEach((op, index) => {
|
|
858
|
+
if (op.verb === "rename_migration") {
|
|
859
|
+
renames.push({ from: op.from, to: op.to, file: patch.fileName });
|
|
860
|
+
} else if (op.verb === "mark_applied") {
|
|
861
|
+
if (!manifest.has(op.key)) {
|
|
862
|
+
throw new PatchValidationError(
|
|
863
|
+
`operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
|
|
864
|
+
patch.fileName,
|
|
865
|
+
patch.patchKey
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
if (renames.length === 0) return;
|
|
872
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
873
|
+
renames.forEach((r) => {
|
|
874
|
+
mentioned.add(r.from);
|
|
875
|
+
mentioned.add(r.to);
|
|
876
|
+
});
|
|
877
|
+
const finalKey = (start) => {
|
|
878
|
+
let current = start;
|
|
879
|
+
for (const r of renames) {
|
|
880
|
+
if (current === r.from) current = r.to;
|
|
881
|
+
}
|
|
882
|
+
return current;
|
|
883
|
+
};
|
|
884
|
+
for (const key of mentioned) {
|
|
885
|
+
const finish = finalKey(key);
|
|
886
|
+
if (manifest.has(key)) {
|
|
887
|
+
if (finish !== key) {
|
|
888
|
+
throw new PatchValidationError(
|
|
889
|
+
`rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
} else {
|
|
893
|
+
if (!manifest.has(finish)) {
|
|
894
|
+
throw new PatchValidationError(
|
|
895
|
+
`rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// framework/PatchDirectoryReader.ts
|
|
903
|
+
var PatchDirectoryReader = class {
|
|
904
|
+
constructor(directory) {
|
|
905
|
+
this.directory = directory;
|
|
906
|
+
}
|
|
907
|
+
loadPatches() {
|
|
908
|
+
if (!import_fs4.default.existsSync(this.directory)) return [];
|
|
909
|
+
const entries = import_fs4.default.readdirSync(this.directory, { withFileTypes: true });
|
|
910
|
+
const patchFiles = [];
|
|
911
|
+
for (const entry of entries) {
|
|
912
|
+
if (!entry.name.endsWith(".yaml")) continue;
|
|
913
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
914
|
+
if (entry.isSymbolicLink()) {
|
|
915
|
+
throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
|
|
916
|
+
}
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
patchFiles.push(entry.name);
|
|
920
|
+
}
|
|
921
|
+
patchFiles.sort((a, b) => {
|
|
922
|
+
const stampA = parseInt(a.slice(0, 13), 10);
|
|
923
|
+
const stampB = parseInt(b.slice(0, 13), 10);
|
|
924
|
+
if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
|
|
925
|
+
return stampA - stampB;
|
|
926
|
+
}
|
|
927
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
928
|
+
});
|
|
929
|
+
return patchFiles.map((fileName) => {
|
|
930
|
+
const match = PATCH_FILENAME_REGEX.exec(fileName);
|
|
931
|
+
if (!match) {
|
|
932
|
+
throw new PatchValidationError(
|
|
933
|
+
`invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
|
|
934
|
+
fileName
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const patchKey = fileName.slice(0, -".yaml".length);
|
|
938
|
+
const filePath = import_path3.default.join(this.directory, fileName);
|
|
939
|
+
const bytes = import_fs4.default.readFileSync(filePath);
|
|
940
|
+
const checksum = import_crypto.default.createHash("sha256").update(bytes).digest("hex");
|
|
941
|
+
const content = bytes.toString("utf8");
|
|
942
|
+
const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
|
|
943
|
+
return { patchKey, fileName, filePath, checksum, version, description, operations };
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// framework/PatchRunner.ts
|
|
949
|
+
function toError(error) {
|
|
950
|
+
if (error instanceof Error) return error;
|
|
951
|
+
return new Error(String(error));
|
|
952
|
+
}
|
|
953
|
+
function extractRows(result) {
|
|
954
|
+
if (!Array.isArray(result)) return [];
|
|
955
|
+
if (Array.isArray(result[0])) return result[0];
|
|
956
|
+
if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
|
|
957
|
+
return result;
|
|
958
|
+
}
|
|
959
|
+
if (result[0] == null) return [];
|
|
960
|
+
return [result[0]];
|
|
961
|
+
}
|
|
962
|
+
var PatchRunner = class {
|
|
963
|
+
constructor(sqlrunner, config) {
|
|
964
|
+
this.sqlrunner = sqlrunner;
|
|
965
|
+
this.config = config;
|
|
966
|
+
this.patchTable = resolvePatchTable(config);
|
|
967
|
+
this.migrationTable = config.migration_table;
|
|
968
|
+
this.dialect = config.database;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Discovers, validates, and applies every unapplied patch in order.
|
|
972
|
+
* Each unapplied patch is its own transaction; earlier committed patches
|
|
973
|
+
* remain committed if a later patch fails.
|
|
974
|
+
*/
|
|
975
|
+
applyPending() {
|
|
976
|
+
return __async(this, null, function* () {
|
|
977
|
+
const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
|
|
978
|
+
const patches = reader.loadPatches();
|
|
979
|
+
const history = yield this.loadHistory();
|
|
980
|
+
if (patches.length === 0 && history.length === 0) {
|
|
981
|
+
return [];
|
|
982
|
+
}
|
|
983
|
+
const byKey = new Map(patches.map((p) => [p.patchKey, p]));
|
|
984
|
+
for (const row of history) {
|
|
985
|
+
const file = byKey.get(row.patch_key);
|
|
986
|
+
if (!file) {
|
|
987
|
+
throw new PatchIntegrityError(
|
|
988
|
+
`applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
|
|
989
|
+
void 0,
|
|
990
|
+
row.patch_key
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
if (file.checksum !== row.checksum) {
|
|
994
|
+
throw new PatchIntegrityError(
|
|
995
|
+
`applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
|
|
996
|
+
file.fileName,
|
|
997
|
+
row.patch_key,
|
|
998
|
+
row.checksum,
|
|
999
|
+
file.checksum
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
|
|
1004
|
+
validatePatchPlan(patches, manifest);
|
|
1005
|
+
const appliedKeys = new Set(history.map((r) => r.patch_key));
|
|
1006
|
+
const results = [];
|
|
1007
|
+
for (const patch of patches) {
|
|
1008
|
+
if (appliedKeys.has(patch.patchKey)) {
|
|
1009
|
+
results.push({
|
|
1010
|
+
patchKey: patch.patchKey,
|
|
1011
|
+
fileName: patch.fileName,
|
|
1012
|
+
status: "already_applied",
|
|
1013
|
+
operations: []
|
|
1014
|
+
});
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
results.push(yield this.applyOne(patch, manifest));
|
|
1018
|
+
}
|
|
1019
|
+
return results;
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
loadHistory() {
|
|
1023
|
+
return __async(this, null, function* () {
|
|
1024
|
+
try {
|
|
1025
|
+
const result = yield this.sqlrunner.query(
|
|
1026
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
|
|
1027
|
+
[this.migrationTable]
|
|
1028
|
+
);
|
|
1029
|
+
return extractRows(result);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
throw new PatchExecutionError(
|
|
1032
|
+
`failed to read patch history from '${this.patchTable}'`,
|
|
1033
|
+
void 0,
|
|
1034
|
+
void 0,
|
|
1035
|
+
void 0,
|
|
1036
|
+
void 0,
|
|
1037
|
+
toError(error)
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
beginSql() {
|
|
1043
|
+
switch (this.dialect) {
|
|
1044
|
+
case "sqlite":
|
|
1045
|
+
return "BEGIN IMMEDIATE";
|
|
1046
|
+
case "pg":
|
|
1047
|
+
return "BEGIN";
|
|
1048
|
+
default:
|
|
1049
|
+
return "START TRANSACTION";
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
begin(patch) {
|
|
1053
|
+
return __async(this, null, function* () {
|
|
1054
|
+
const deadline = Date.now() + 1e4;
|
|
1055
|
+
while (true) {
|
|
1056
|
+
try {
|
|
1057
|
+
yield this.sqlrunner.execute(this.beginSql());
|
|
1058
|
+
return;
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
const message = toError(error).message;
|
|
1061
|
+
if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
|
|
1062
|
+
yield new Promise((resolve) => setTimeout(resolve, 50));
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
throw new PatchExecutionError(
|
|
1066
|
+
"failed to start patch transaction",
|
|
1067
|
+
patch.fileName,
|
|
1068
|
+
patch.patchKey,
|
|
1069
|
+
void 0,
|
|
1070
|
+
void 0,
|
|
1071
|
+
toError(error)
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
rollbackQuietly() {
|
|
1078
|
+
return __async(this, null, function* () {
|
|
1079
|
+
try {
|
|
1080
|
+
yield this.sqlrunner.execute("ROLLBACK");
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
applyOne(patch, manifest) {
|
|
1086
|
+
return __async(this, null, function* () {
|
|
1087
|
+
yield this.begin(patch);
|
|
1088
|
+
try {
|
|
1089
|
+
yield this.sqlrunner.execute(
|
|
1090
|
+
`INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
|
|
1091
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1092
|
+
[this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
|
|
1093
|
+
);
|
|
1094
|
+
} catch (claimError) {
|
|
1095
|
+
yield this.rollbackQuietly();
|
|
1096
|
+
const committed = yield this.findCommittedRow(patch.patchKey);
|
|
1097
|
+
if (committed) {
|
|
1098
|
+
if (committed.checksum === patch.checksum) {
|
|
1099
|
+
return {
|
|
1100
|
+
patchKey: patch.patchKey,
|
|
1101
|
+
fileName: patch.fileName,
|
|
1102
|
+
status: "already_applied",
|
|
1103
|
+
operations: []
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
throw new PatchIntegrityError(
|
|
1107
|
+
`patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
|
|
1108
|
+
patch.fileName,
|
|
1109
|
+
patch.patchKey,
|
|
1110
|
+
committed.checksum,
|
|
1111
|
+
patch.checksum
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
throw new PatchExecutionError(
|
|
1115
|
+
"failed to claim patch-history row",
|
|
1116
|
+
patch.fileName,
|
|
1117
|
+
patch.patchKey,
|
|
1118
|
+
void 0,
|
|
1119
|
+
void 0,
|
|
1120
|
+
toError(claimError)
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
const operationResults = [];
|
|
1124
|
+
try {
|
|
1125
|
+
for (let index = 0; index < patch.operations.length; index++) {
|
|
1126
|
+
operationResults.push(
|
|
1127
|
+
yield this.applyOperation(patch, patch.operations[index], index, manifest)
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
yield this.sqlrunner.execute("COMMIT");
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
yield this.rollbackQuietly();
|
|
1133
|
+
if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
throw new PatchExecutionError(
|
|
1137
|
+
"patch application failed",
|
|
1138
|
+
patch.fileName,
|
|
1139
|
+
patch.patchKey,
|
|
1140
|
+
void 0,
|
|
1141
|
+
void 0,
|
|
1142
|
+
toError(error)
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
patchKey: patch.patchKey,
|
|
1147
|
+
fileName: patch.fileName,
|
|
1148
|
+
status: "applied",
|
|
1149
|
+
operations: operationResults
|
|
1150
|
+
};
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
findCommittedRow(patchKey) {
|
|
1154
|
+
return __async(this, null, function* () {
|
|
1155
|
+
const result = yield this.sqlrunner.query(
|
|
1156
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
|
|
1157
|
+
[this.migrationTable, patchKey]
|
|
1158
|
+
);
|
|
1159
|
+
const list = extractRows(result);
|
|
1160
|
+
return list.length > 0 ? list[0] : null;
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
countRows(key) {
|
|
1164
|
+
return __async(this, null, function* () {
|
|
1165
|
+
var _a, _b, _c;
|
|
1166
|
+
const result = yield this.sqlrunner.query(
|
|
1167
|
+
`SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1168
|
+
[key]
|
|
1169
|
+
);
|
|
1170
|
+
const rows = extractRows(result);
|
|
1171
|
+
const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
|
|
1172
|
+
return Number(value != null ? value : 0);
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
conflict(patch, index, verb, message, keys, counts) {
|
|
1176
|
+
throw new PatchConflictError(
|
|
1177
|
+
`operation ${index} (${verb}): ${message}`,
|
|
1178
|
+
patch.fileName,
|
|
1179
|
+
patch.patchKey,
|
|
1180
|
+
index,
|
|
1181
|
+
verb,
|
|
1182
|
+
keys,
|
|
1183
|
+
counts
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
applyOperation(patch, op, index, manifest) {
|
|
1187
|
+
return __async(this, null, function* () {
|
|
1188
|
+
var _a, _b, _c, _d;
|
|
1189
|
+
try {
|
|
1190
|
+
switch (op.verb) {
|
|
1191
|
+
case "rename_migration": {
|
|
1192
|
+
const fromCount = yield this.countRows(op.from);
|
|
1193
|
+
const toCount = yield this.countRows(op.to);
|
|
1194
|
+
const counts = { [op.from]: fromCount, [op.to]: toCount };
|
|
1195
|
+
if (fromCount > 1 || toCount > 1) {
|
|
1196
|
+
this.conflict(
|
|
1197
|
+
patch,
|
|
1198
|
+
index,
|
|
1199
|
+
op.verb,
|
|
1200
|
+
`ledger corruption: duplicate rows for a migration key`,
|
|
1201
|
+
[op.from, op.to],
|
|
1202
|
+
counts
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
if (fromCount === 1 && toCount === 1) {
|
|
1206
|
+
this.conflict(
|
|
1207
|
+
patch,
|
|
1208
|
+
index,
|
|
1209
|
+
op.verb,
|
|
1210
|
+
`both '${op.from}' and '${op.to}' exist in the ledger`,
|
|
1211
|
+
[op.from, op.to],
|
|
1212
|
+
counts
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
if (fromCount === 0) {
|
|
1216
|
+
return { verb: op.verb, changed: false };
|
|
1217
|
+
}
|
|
1218
|
+
const target = manifest.get(op.to);
|
|
1219
|
+
if (target) {
|
|
1220
|
+
yield this.sqlrunner.execute(
|
|
1221
|
+
`UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
|
|
1222
|
+
[op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
|
|
1223
|
+
);
|
|
1224
|
+
} else {
|
|
1225
|
+
yield this.sqlrunner.execute(
|
|
1226
|
+
`UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
|
|
1227
|
+
[op.to, op.from]
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
return { verb: op.verb, changed: true };
|
|
1231
|
+
}
|
|
1232
|
+
case "mark_applied": {
|
|
1233
|
+
const count = yield this.countRows(op.key);
|
|
1234
|
+
if (count > 1) {
|
|
1235
|
+
this.conflict(
|
|
1236
|
+
patch,
|
|
1237
|
+
index,
|
|
1238
|
+
op.verb,
|
|
1239
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1240
|
+
[op.key],
|
|
1241
|
+
{ [op.key]: count }
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (count === 1) {
|
|
1245
|
+
return { verb: op.verb, changed: false };
|
|
1246
|
+
}
|
|
1247
|
+
const entry = manifest.get(op.key);
|
|
1248
|
+
yield this.sqlrunner.execute(
|
|
1249
|
+
`INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
|
|
1250
|
+
[op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
|
|
1251
|
+
);
|
|
1252
|
+
return { verb: op.verb, changed: true };
|
|
1253
|
+
}
|
|
1254
|
+
case "unmark_applied": {
|
|
1255
|
+
const count = yield this.countRows(op.key);
|
|
1256
|
+
if (count > 1) {
|
|
1257
|
+
this.conflict(
|
|
1258
|
+
patch,
|
|
1259
|
+
index,
|
|
1260
|
+
op.verb,
|
|
1261
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1262
|
+
[op.key],
|
|
1263
|
+
{ [op.key]: count }
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
if (count === 0) {
|
|
1267
|
+
return { verb: op.verb, changed: false };
|
|
1268
|
+
}
|
|
1269
|
+
yield this.sqlrunner.execute(
|
|
1270
|
+
`DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1271
|
+
[op.key]
|
|
1272
|
+
);
|
|
1273
|
+
return { verb: op.verb, changed: true };
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
if (error instanceof PatchConflictError) throw error;
|
|
1278
|
+
throw new PatchExecutionError(
|
|
1279
|
+
`operation failed`,
|
|
1280
|
+
patch.fileName,
|
|
1281
|
+
patch.patchKey,
|
|
1282
|
+
index,
|
|
1283
|
+
op.verb,
|
|
1284
|
+
toError(error)
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// framework/PatchCreator.ts
|
|
1292
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1293
|
+
var import_path4 = __toESM(require("path"));
|
|
1294
|
+
var MAX_NAME_LENGTH = 120;
|
|
1295
|
+
var SCAFFOLD = `version: 1
|
|
1296
|
+
description: TODO
|
|
1297
|
+
operations: []
|
|
1298
|
+
`;
|
|
1299
|
+
var PatchCreator = class _PatchCreator {
|
|
1300
|
+
constructor(patchFolder) {
|
|
1301
|
+
this.patchFolder = patchFolder;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
|
|
1305
|
+
* Rejects empty results, path separators, `..`, control characters,
|
|
1306
|
+
* characters outside [a-z0-9_-], and names longer than 120 characters.
|
|
1307
|
+
*/
|
|
1308
|
+
static normalizeName(name) {
|
|
1309
|
+
const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
|
|
1310
|
+
if (normalized.length === 0) {
|
|
1311
|
+
throw new CLIError("Patch name is required");
|
|
1312
|
+
}
|
|
1313
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
1314
|
+
throw new CLIError("Patch name must not contain path separators");
|
|
1315
|
+
}
|
|
1316
|
+
if (normalized.includes("..")) {
|
|
1317
|
+
throw new CLIError("Patch name must not contain '..'");
|
|
1318
|
+
}
|
|
1319
|
+
if (/[\x00-\x1f\x7f]/.test(normalized)) {
|
|
1320
|
+
throw new CLIError("Patch name must not contain control characters");
|
|
1321
|
+
}
|
|
1322
|
+
if (!/^[a-z0-9_-]+$/.test(normalized)) {
|
|
1323
|
+
throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
|
|
1324
|
+
}
|
|
1325
|
+
if (normalized.length > MAX_NAME_LENGTH) {
|
|
1326
|
+
throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
|
|
1327
|
+
}
|
|
1328
|
+
return normalized;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
|
|
1332
|
+
* file creation. On a millisecond-stamp collision, mints a later stamp
|
|
1333
|
+
* and retries. Returns the created path.
|
|
1334
|
+
*/
|
|
1335
|
+
create(name) {
|
|
1336
|
+
const normalized = _PatchCreator.normalizeName(name);
|
|
1337
|
+
if (!import_fs5.default.existsSync(this.patchFolder)) {
|
|
1338
|
+
import_fs5.default.mkdirSync(this.patchFolder, { recursive: true });
|
|
1339
|
+
}
|
|
1340
|
+
let stamp = Date.now();
|
|
1341
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
1342
|
+
const filePath = import_path4.default.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
|
|
1343
|
+
try {
|
|
1344
|
+
import_fs5.default.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
|
|
1345
|
+
return filePath;
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (error && error.code === "EEXIST") {
|
|
1348
|
+
stamp += 1;
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
throw error;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
throw new CLIError("Unable to create patch file: too many filename collisions");
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
|
|
561
1358
|
// framework/SQLRunner.ts
|
|
562
1359
|
var BaseSQLRunner = class {
|
|
563
1360
|
/**
|
|
@@ -872,7 +1669,7 @@ var PgRunner = class _PgRunner extends BaseSQLRunner {
|
|
|
872
1669
|
};
|
|
873
1670
|
|
|
874
1671
|
// framework/MigrationRunner.ts
|
|
875
|
-
function
|
|
1672
|
+
function toError2(error) {
|
|
876
1673
|
if (error instanceof Error) return error;
|
|
877
1674
|
return new Error(String(error));
|
|
878
1675
|
}
|
|
@@ -900,10 +1697,12 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
900
1697
|
return __async(this, null, function* () {
|
|
901
1698
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
902
1699
|
const config = configReader.loadFile();
|
|
1700
|
+
let factoryOwnsConnection = false;
|
|
903
1701
|
if (!conn) {
|
|
904
1702
|
conn = yield this.createConnection(config);
|
|
1703
|
+
factoryOwnsConnection = true;
|
|
905
1704
|
}
|
|
906
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1705
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
907
1706
|
});
|
|
908
1707
|
}
|
|
909
1708
|
static createConnection(config) {
|
|
@@ -923,7 +1722,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
923
1722
|
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
924
1723
|
return conn;
|
|
925
1724
|
} catch (error) {
|
|
926
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1725
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
927
1726
|
}
|
|
928
1727
|
case "sqlite":
|
|
929
1728
|
if (!config.sqlite) {
|
|
@@ -938,7 +1737,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
938
1737
|
});
|
|
939
1738
|
return conn;
|
|
940
1739
|
} catch (error) {
|
|
941
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1740
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
942
1741
|
}
|
|
943
1742
|
case "pg":
|
|
944
1743
|
if (!config.pg && !process.env.DATABASE_URL) {
|
|
@@ -954,7 +1753,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
954
1753
|
yield conn.connect();
|
|
955
1754
|
return conn;
|
|
956
1755
|
} catch (error) {
|
|
957
|
-
throw DatabaseConnectionError.connectionFailed("pg",
|
|
1756
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
958
1757
|
}
|
|
959
1758
|
default:
|
|
960
1759
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -968,7 +1767,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
968
1767
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
969
1768
|
});
|
|
970
1769
|
}
|
|
971
|
-
create(config, conn) {
|
|
1770
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
972
1771
|
return __async(this, null, function* () {
|
|
973
1772
|
let sqlrunner;
|
|
974
1773
|
let driverConnection = conn;
|
|
@@ -981,8 +1780,19 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
981
1780
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
982
1781
|
const read_strategy = this.getReadStategy(config);
|
|
983
1782
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
984
|
-
|
|
985
|
-
|
|
1783
|
+
const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
|
|
1784
|
+
try {
|
|
1785
|
+
yield runner.setup();
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
if (factoryOwnsConnection) {
|
|
1788
|
+
try {
|
|
1789
|
+
yield sqlrunner.end();
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
throw error;
|
|
1794
|
+
}
|
|
1795
|
+
return runner;
|
|
986
1796
|
});
|
|
987
1797
|
}
|
|
988
1798
|
createEmpty(config) {
|
|
@@ -992,7 +1802,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
992
1802
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
993
1803
|
const read_strategy = this.getReadStategy(config);
|
|
994
1804
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
995
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1805
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
996
1806
|
});
|
|
997
1807
|
}
|
|
998
1808
|
getReadStategy(config) {
|
|
@@ -1009,33 +1819,77 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
1009
1819
|
}
|
|
1010
1820
|
};
|
|
1011
1821
|
var MySQLMigrationRunner = class {
|
|
1012
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1822
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
1013
1823
|
this.config = config;
|
|
1014
1824
|
this.directory = directory;
|
|
1015
1825
|
this.setupRunner = setupRunner;
|
|
1016
1826
|
this.sqlrunner = sqlrunner;
|
|
1017
1827
|
this.connection = connection;
|
|
1828
|
+
this.preflightEnabled = preflightEnabled;
|
|
1829
|
+
/**
|
|
1830
|
+
* Memoized in-flight preflight promise. Simultaneous or repeated calls
|
|
1831
|
+
* to setup() on one runner execute the preflight (migration table setup
|
|
1832
|
+
* + patch application) exactly once. Cleared after rejection so a caller
|
|
1833
|
+
* may retry after fixing the cause.
|
|
1834
|
+
*/
|
|
1835
|
+
this.preflightPromise = null;
|
|
1836
|
+
this.lastPatchResults = [];
|
|
1018
1837
|
}
|
|
1019
1838
|
setup() {
|
|
1839
|
+
return __async(this, null, function* () {
|
|
1840
|
+
if (!this.preflightEnabled) {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (!this.preflightPromise) {
|
|
1844
|
+
this.preflightPromise = this.runPreflight();
|
|
1845
|
+
this.preflightPromise.catch(() => {
|
|
1846
|
+
this.preflightPromise = null;
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
return this.preflightPromise;
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
runPreflight() {
|
|
1020
1853
|
return __async(this, null, function* () {
|
|
1021
1854
|
try {
|
|
1022
1855
|
yield this.setupRunner.setup();
|
|
1023
1856
|
} catch (error) {
|
|
1024
|
-
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0,
|
|
1857
|
+
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
|
|
1025
1858
|
}
|
|
1859
|
+
const patchRunner = new PatchRunner(this.sqlrunner, this.config);
|
|
1860
|
+
this.lastPatchResults = yield patchRunner.applyPending();
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Delegates to the same idempotent preflight; returns the results of the
|
|
1865
|
+
* patch pass that ran (or is running) for this runner.
|
|
1866
|
+
*/
|
|
1867
|
+
applyPendingPatches() {
|
|
1868
|
+
return __async(this, null, function* () {
|
|
1869
|
+
yield this.setup();
|
|
1870
|
+
return this.lastPatchResults;
|
|
1026
1871
|
});
|
|
1027
1872
|
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Scaffolds a new ledger patch file and returns the created path.
|
|
1875
|
+
* Never connects to a database.
|
|
1876
|
+
*/
|
|
1877
|
+
createPatch(name) {
|
|
1878
|
+
const creator = new PatchCreator(resolvePatchFolder(this.config));
|
|
1879
|
+
return creator.create(name);
|
|
1880
|
+
}
|
|
1028
1881
|
terminate() {
|
|
1029
1882
|
return __async(this, null, function* () {
|
|
1030
1883
|
try {
|
|
1031
1884
|
yield this.setupRunner.teardown();
|
|
1032
1885
|
} catch (error) {
|
|
1033
|
-
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0,
|
|
1886
|
+
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
|
|
1034
1887
|
}
|
|
1035
1888
|
});
|
|
1036
1889
|
}
|
|
1037
1890
|
getMigrationsHistory() {
|
|
1038
1891
|
return __async(this, null, function* () {
|
|
1892
|
+
yield this.setup();
|
|
1039
1893
|
try {
|
|
1040
1894
|
const results = yield this.sqlrunner.query(`
|
|
1041
1895
|
select *
|
|
@@ -1043,16 +1897,17 @@ var MySQLMigrationRunner = class {
|
|
|
1043
1897
|
`);
|
|
1044
1898
|
return results[0];
|
|
1045
1899
|
} catch (error) {
|
|
1046
|
-
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0,
|
|
1900
|
+
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
|
|
1047
1901
|
}
|
|
1048
1902
|
});
|
|
1049
1903
|
}
|
|
1050
1904
|
getMigrations() {
|
|
1051
1905
|
return __async(this, null, function* () {
|
|
1906
|
+
yield this.setup();
|
|
1052
1907
|
try {
|
|
1053
1908
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
1054
1909
|
} catch (error) {
|
|
1055
|
-
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0,
|
|
1910
|
+
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
|
|
1056
1911
|
}
|
|
1057
1912
|
});
|
|
1058
1913
|
}
|
|
@@ -1065,7 +1920,7 @@ var MySQLMigrationRunner = class {
|
|
|
1065
1920
|
if (error instanceof MigrationExecutionError) {
|
|
1066
1921
|
throw error;
|
|
1067
1922
|
}
|
|
1068
|
-
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0,
|
|
1923
|
+
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
|
|
1069
1924
|
}
|
|
1070
1925
|
});
|
|
1071
1926
|
}
|
|
@@ -1078,12 +1933,13 @@ var MySQLMigrationRunner = class {
|
|
|
1078
1933
|
if (error instanceof MigrationExecutionError) {
|
|
1079
1934
|
throw error;
|
|
1080
1935
|
}
|
|
1081
|
-
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0,
|
|
1936
|
+
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
|
|
1082
1937
|
}
|
|
1083
1938
|
});
|
|
1084
1939
|
}
|
|
1085
1940
|
migrate(migrationNodes, forward) {
|
|
1086
1941
|
return __async(this, null, function* () {
|
|
1942
|
+
yield this.setup();
|
|
1087
1943
|
for (let node of migrationNodes) {
|
|
1088
1944
|
try {
|
|
1089
1945
|
if (forward) {
|
|
@@ -1096,7 +1952,7 @@ var MySQLMigrationRunner = class {
|
|
|
1096
1952
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
1097
1953
|
node.name || String(node),
|
|
1098
1954
|
forward ? node.up_sql() : node.down_sql(),
|
|
1099
|
-
|
|
1955
|
+
toError2(error)
|
|
1100
1956
|
);
|
|
1101
1957
|
}
|
|
1102
1958
|
}
|
|
@@ -1104,6 +1960,7 @@ var MySQLMigrationRunner = class {
|
|
|
1104
1960
|
}
|
|
1105
1961
|
reset() {
|
|
1106
1962
|
return __async(this, null, function* () {
|
|
1963
|
+
yield this.setup();
|
|
1107
1964
|
try {
|
|
1108
1965
|
let migrations = yield this.getMigrations();
|
|
1109
1966
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -1115,7 +1972,7 @@ var MySQLMigrationRunner = class {
|
|
|
1115
1972
|
if (error instanceof MigrationExecutionError) {
|
|
1116
1973
|
throw error;
|
|
1117
1974
|
}
|
|
1118
|
-
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0,
|
|
1975
|
+
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
|
|
1119
1976
|
}
|
|
1120
1977
|
});
|
|
1121
1978
|
}
|
|
@@ -1124,7 +1981,7 @@ var MySQLMigrationRunner = class {
|
|
|
1124
1981
|
const creator = new MigrationCreator(this.config);
|
|
1125
1982
|
creator.create(name);
|
|
1126
1983
|
} catch (error) {
|
|
1127
|
-
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0,
|
|
1984
|
+
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
|
|
1128
1985
|
}
|
|
1129
1986
|
}
|
|
1130
1987
|
close() {
|
|
@@ -1133,7 +1990,7 @@ var MySQLMigrationRunner = class {
|
|
|
1133
1990
|
try {
|
|
1134
1991
|
yield this.sqlrunner.end();
|
|
1135
1992
|
} catch (error) {
|
|
1136
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
1993
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
1137
1994
|
}
|
|
1138
1995
|
}
|
|
1139
1996
|
});
|
|
@@ -1147,7 +2004,7 @@ var MySQLMigrationRunner = class {
|
|
|
1147
2004
|
return __async(this, null, function* () {
|
|
1148
2005
|
try {
|
|
1149
2006
|
console.log(`Checking for ${config_file}`);
|
|
1150
|
-
const config_exist =
|
|
2007
|
+
const config_exist = import_fs6.default.existsSync(config_file);
|
|
1151
2008
|
if (!config_exist) {
|
|
1152
2009
|
console.log(`Creating ${config_file}`);
|
|
1153
2010
|
const default_config = {
|
|
@@ -1161,11 +2018,11 @@ var MySQLMigrationRunner = class {
|
|
|
1161
2018
|
"password": ""
|
|
1162
2019
|
}
|
|
1163
2020
|
};
|
|
1164
|
-
|
|
2021
|
+
import_fs6.default.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
|
|
1165
2022
|
console.log(`Created ${config_file}`);
|
|
1166
2023
|
}
|
|
1167
2024
|
} catch (error) {
|
|
1168
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
2025
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1169
2026
|
}
|
|
1170
2027
|
});
|
|
1171
2028
|
}
|
|
@@ -1176,7 +2033,7 @@ var FileMigrationConfigReader = class {
|
|
|
1176
2033
|
}
|
|
1177
2034
|
loadFile() {
|
|
1178
2035
|
try {
|
|
1179
|
-
const fileContent =
|
|
2036
|
+
const fileContent = import_fs6.default.readFileSync(this.configFile);
|
|
1180
2037
|
const config = JSON.parse(fileContent.toString());
|
|
1181
2038
|
if (!config.migration_folder) {
|
|
1182
2039
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1200,7 +2057,7 @@ var FileMigrationConfigReader = class {
|
|
|
1200
2057
|
if (error instanceof ConfigurationError) {
|
|
1201
2058
|
throw error;
|
|
1202
2059
|
}
|
|
1203
|
-
const err =
|
|
2060
|
+
const err = toError2(error);
|
|
1204
2061
|
if (err.message.includes("ENOENT")) {
|
|
1205
2062
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1206
2063
|
}
|
|
@@ -1217,16 +2074,16 @@ var MigrationCreator = class {
|
|
|
1217
2074
|
throw new CLIError("Migration name is required");
|
|
1218
2075
|
}
|
|
1219
2076
|
try {
|
|
1220
|
-
if (!
|
|
1221
|
-
|
|
2077
|
+
if (!import_fs6.default.existsSync(this.config.migration_folder)) {
|
|
2078
|
+
import_fs6.default.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1222
2079
|
}
|
|
1223
2080
|
const now_timestamp = Date.now();
|
|
1224
2081
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1225
2082
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1226
|
-
|
|
2083
|
+
import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1227
2084
|
-- Write your up migration here
|
|
1228
2085
|
`.trim());
|
|
1229
|
-
|
|
2086
|
+
import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1230
2087
|
-- Write your down migration here
|
|
1231
2088
|
`.trim());
|
|
1232
2089
|
console.log(`Created migration files:`);
|
|
@@ -1236,16 +2093,16 @@ var MigrationCreator = class {
|
|
|
1236
2093
|
if (error instanceof CLIError) {
|
|
1237
2094
|
throw error;
|
|
1238
2095
|
}
|
|
1239
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2096
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1240
2097
|
}
|
|
1241
2098
|
}
|
|
1242
2099
|
};
|
|
1243
2100
|
|
|
1244
2101
|
// framework/SeedRunner.ts
|
|
1245
|
-
var
|
|
1246
|
-
var
|
|
2102
|
+
var import_fs7 = __toESM(require("fs"));
|
|
2103
|
+
var import_path5 = __toESM(require("path"));
|
|
1247
2104
|
var import_url = require("url");
|
|
1248
|
-
var
|
|
2105
|
+
var import_ajv2 = __toESM(require("ajv"));
|
|
1249
2106
|
var import_ajv_formats = __toESM(require("ajv-formats"));
|
|
1250
2107
|
var import_api = require("tsx/esm/api");
|
|
1251
2108
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1254,10 +2111,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1254
2111
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1255
2112
|
}
|
|
1256
2113
|
function walkForFile(rootDir, fileName) {
|
|
1257
|
-
if (!
|
|
1258
|
-
const entries =
|
|
2114
|
+
if (!import_fs7.default.existsSync(rootDir)) return null;
|
|
2115
|
+
const entries = import_fs7.default.readdirSync(rootDir, { withFileTypes: true });
|
|
1259
2116
|
for (const entry of entries) {
|
|
1260
|
-
const full =
|
|
2117
|
+
const full = import_path5.default.join(rootDir, entry.name);
|
|
1261
2118
|
if (entry.isDirectory()) {
|
|
1262
2119
|
const found = walkForFile(full, fileName);
|
|
1263
2120
|
if (found) return found;
|
|
@@ -1351,39 +2208,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1351
2208
|
}
|
|
1352
2209
|
function loadJson(filePath) {
|
|
1353
2210
|
return __async(this, null, function* () {
|
|
1354
|
-
const content = yield
|
|
2211
|
+
const content = yield import_fs7.default.promises.readFile(filePath, "utf8");
|
|
1355
2212
|
return JSON.parse(content);
|
|
1356
2213
|
});
|
|
1357
2214
|
}
|
|
1358
2215
|
function createValidator() {
|
|
1359
|
-
const
|
|
1360
|
-
(0, import_ajv_formats.default)(
|
|
1361
|
-
return
|
|
2216
|
+
const ajv2 = new import_ajv2.default({ allErrors: true, strict: false });
|
|
2217
|
+
(0, import_ajv_formats.default)(ajv2);
|
|
2218
|
+
return ajv2;
|
|
1362
2219
|
}
|
|
1363
2220
|
function validateData(schemaPath, data, validate, log) {
|
|
1364
2221
|
return __async(this, null, function* () {
|
|
1365
2222
|
if (!validate || !schemaPath) return;
|
|
1366
|
-
const content = yield
|
|
2223
|
+
const content = yield import_fs7.default.promises.readFile(schemaPath, "utf8");
|
|
1367
2224
|
const schema = JSON.parse(content);
|
|
1368
|
-
const
|
|
1369
|
-
const validateFn =
|
|
2225
|
+
const ajv2 = createValidator();
|
|
2226
|
+
const validateFn = ajv2.compile(schema);
|
|
1370
2227
|
const ok = validateFn(data);
|
|
1371
2228
|
if (!ok) {
|
|
1372
2229
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1373
|
-
throw new Error(`Seed data validation failed: ${
|
|
2230
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1374
2231
|
}
|
|
1375
2232
|
});
|
|
1376
2233
|
}
|
|
1377
2234
|
function runSqlSeed(runner, resolved, direction) {
|
|
1378
2235
|
return __async(this, null, function* () {
|
|
1379
2236
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1380
|
-
const sql = yield
|
|
2237
|
+
const sql = yield import_fs7.default.promises.readFile(sqlPath, "utf8");
|
|
1381
2238
|
yield runner.query(sql);
|
|
1382
2239
|
});
|
|
1383
2240
|
}
|
|
1384
2241
|
function loadSeedModule(modulePath) {
|
|
1385
2242
|
return __async(this, null, function* () {
|
|
1386
|
-
const resolved =
|
|
2243
|
+
const resolved = import_path5.default.resolve(modulePath);
|
|
1387
2244
|
if (resolved.endsWith(".ts")) {
|
|
1388
2245
|
const fileUrl = (0, import_url.pathToFileURL)(resolved).href;
|
|
1389
2246
|
return (0, import_api.tsImport)(fileUrl, fileUrl);
|
|
@@ -1504,13 +2361,23 @@ function createSeedFactory(options) {
|
|
|
1504
2361
|
}
|
|
1505
2362
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1506
2363
|
0 && (module.exports = {
|
|
2364
|
+
MigrationError,
|
|
1507
2365
|
MigrationRunner,
|
|
1508
2366
|
MigrationRunnerFactory,
|
|
2367
|
+
PatchConflictError,
|
|
2368
|
+
PatchCreator,
|
|
2369
|
+
PatchError,
|
|
2370
|
+
PatchExecutionError,
|
|
2371
|
+
PatchIntegrityError,
|
|
2372
|
+
PatchRunner,
|
|
2373
|
+
PatchValidationError,
|
|
1509
2374
|
PgRunner,
|
|
1510
2375
|
SQLRunner,
|
|
1511
2376
|
SQLiteRunner,
|
|
1512
2377
|
createSeedFactory,
|
|
1513
2378
|
loadMigrationConfig,
|
|
2379
|
+
resolvePatchFolder,
|
|
2380
|
+
resolvePatchTable,
|
|
1514
2381
|
runSeedsWithRunner
|
|
1515
2382
|
});
|
|
1516
2383
|
//# sourceMappingURL=index.js.map
|