@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.mjs
CHANGED
|
@@ -39,11 +39,10 @@ var __async = (__this, __arguments, generator) => {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
// framework/MigrationRunner.ts
|
|
42
|
-
import
|
|
42
|
+
import fs6 from "fs";
|
|
43
43
|
|
|
44
44
|
// framework/MigrationDirectoryReader.ts
|
|
45
|
-
import
|
|
46
|
-
import path from "path";
|
|
45
|
+
import fs2 from "fs";
|
|
47
46
|
|
|
48
47
|
// framework/errors.ts
|
|
49
48
|
var MigrationError = class _MigrationError extends Error {
|
|
@@ -158,6 +157,54 @@ ${originalError.message}`;
|
|
|
158
157
|
return new _MigrationExecutionError(message);
|
|
159
158
|
}
|
|
160
159
|
};
|
|
160
|
+
var PatchError = class _PatchError extends MigrationError {
|
|
161
|
+
constructor(message, patchFile, patchKey) {
|
|
162
|
+
super(`Patch Error: ${message}`);
|
|
163
|
+
this.patchFile = patchFile;
|
|
164
|
+
this.patchKey = patchKey;
|
|
165
|
+
this.name = "PatchError";
|
|
166
|
+
Object.setPrototypeOf(this, _PatchError.prototype);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var PatchValidationError = class _PatchValidationError extends PatchError {
|
|
170
|
+
constructor(message, patchFile, patchKey) {
|
|
171
|
+
super(message, patchFile, patchKey);
|
|
172
|
+
this.name = "PatchValidationError";
|
|
173
|
+
Object.setPrototypeOf(this, _PatchValidationError.prototype);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var PatchIntegrityError = class _PatchIntegrityError extends PatchError {
|
|
177
|
+
constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
|
|
178
|
+
super(message, patchFile, patchKey);
|
|
179
|
+
this.expectedChecksum = expectedChecksum;
|
|
180
|
+
this.actualChecksum = actualChecksum;
|
|
181
|
+
this.name = "PatchIntegrityError";
|
|
182
|
+
Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var PatchConflictError = class _PatchConflictError extends PatchError {
|
|
186
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
|
|
187
|
+
super(message, patchFile, patchKey);
|
|
188
|
+
this.operationIndex = operationIndex;
|
|
189
|
+
this.operationVerb = operationVerb;
|
|
190
|
+
this.migrationKeys = migrationKeys;
|
|
191
|
+
this.observedRowCounts = observedRowCounts;
|
|
192
|
+
this.name = "PatchConflictError";
|
|
193
|
+
Object.setPrototypeOf(this, _PatchConflictError.prototype);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var PatchExecutionError = class _PatchExecutionError extends PatchError {
|
|
197
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
|
|
198
|
+
super(originalError ? `${message}
|
|
199
|
+
Original Error:
|
|
200
|
+
${originalError.message}` : message, patchFile, patchKey);
|
|
201
|
+
this.operationIndex = operationIndex;
|
|
202
|
+
this.operationVerb = operationVerb;
|
|
203
|
+
this.originalError = originalError;
|
|
204
|
+
this.name = "PatchExecutionError";
|
|
205
|
+
Object.setPrototypeOf(this, _PatchExecutionError.prototype);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
161
208
|
var CLIError = class _CLIError extends MigrationError {
|
|
162
209
|
constructor(message) {
|
|
163
210
|
super(`CLI Error: ${message}`);
|
|
@@ -320,6 +367,40 @@ var SqlMigrationBuilder = class {
|
|
|
320
367
|
}
|
|
321
368
|
};
|
|
322
369
|
|
|
370
|
+
// framework/MigrationManifest.ts
|
|
371
|
+
import fs from "fs";
|
|
372
|
+
import path from "path";
|
|
373
|
+
var MIGRATION_FILE_REGEX = /(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i;
|
|
374
|
+
function isMigrationFile(fileName) {
|
|
375
|
+
return MIGRATION_FILE_REGEX.test(fileName);
|
|
376
|
+
}
|
|
377
|
+
function canonicalMigrationKey(value) {
|
|
378
|
+
return value.replace(MIGRATION_FILE_REGEX, "").toLowerCase();
|
|
379
|
+
}
|
|
380
|
+
function resolveMigrationFile(directory, baseName, direction, dialect) {
|
|
381
|
+
const dialectExt = dialect === "sql" ? "mysql" : dialect;
|
|
382
|
+
const dialectFile = path.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
383
|
+
if (fs.existsSync(dialectFile)) return dialectFile;
|
|
384
|
+
const genericFile = path.join(directory, `${baseName}.${direction}.sql`);
|
|
385
|
+
if (fs.existsSync(genericFile)) return genericFile;
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
function loadMigrationManifest(directory, dialect) {
|
|
389
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
390
|
+
if (!fs.existsSync(directory)) return manifest;
|
|
391
|
+
const files = fs.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name).filter(isMigrationFile);
|
|
392
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
393
|
+
files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
|
|
394
|
+
uniqueKeys.forEach((key) => {
|
|
395
|
+
manifest.set(key, {
|
|
396
|
+
key,
|
|
397
|
+
upFile: resolveMigrationFile(directory, key, "up", dialect),
|
|
398
|
+
downFile: resolveMigrationFile(directory, key, "down", dialect)
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
return manifest;
|
|
402
|
+
}
|
|
403
|
+
|
|
323
404
|
// framework/MigrationDirectoryReader.ts
|
|
324
405
|
var MigrationDirectoryReader = class {
|
|
325
406
|
constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
|
|
@@ -334,12 +415,7 @@ var MigrationDirectoryReader = class {
|
|
|
334
415
|
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
335
416
|
*/
|
|
336
417
|
resolveFile(baseName, direction) {
|
|
337
|
-
|
|
338
|
-
const dialectFile = path.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
339
|
-
if (fs.existsSync(dialectFile)) return dialectFile;
|
|
340
|
-
const genericFile = path.join(this.directory, `${baseName}.${direction}.sql`);
|
|
341
|
-
if (fs.existsSync(genericFile)) return genericFile;
|
|
342
|
-
return null;
|
|
418
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
343
419
|
}
|
|
344
420
|
/**
|
|
345
421
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
@@ -348,11 +424,11 @@ var MigrationDirectoryReader = class {
|
|
|
348
424
|
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
349
425
|
}
|
|
350
426
|
loadMigrations(table, connection) {
|
|
351
|
-
|
|
352
|
-
const dir_content =
|
|
427
|
+
fs2.existsSync(this.directory) || fs2.mkdirSync(this.directory);
|
|
428
|
+
const dir_content = fs2.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name).filter(isMigrationFile);
|
|
353
429
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
354
430
|
dir_content.forEach((file) => {
|
|
355
|
-
const key = file
|
|
431
|
+
const key = canonicalMigrationKey(file);
|
|
356
432
|
uniqueKeys.add(key);
|
|
357
433
|
});
|
|
358
434
|
const migration_sorter = {};
|
|
@@ -392,14 +468,14 @@ var MigrationDirectoryReader = class {
|
|
|
392
468
|
return builder;
|
|
393
469
|
}
|
|
394
470
|
sql_up(file) {
|
|
395
|
-
let content =
|
|
471
|
+
let content = fs2.readFileSync(file).toString();
|
|
396
472
|
if (!this.isDialectSpecific(file)) {
|
|
397
473
|
content = this.read_strategy(content);
|
|
398
474
|
}
|
|
399
475
|
return content.trim();
|
|
400
476
|
}
|
|
401
477
|
sql_down(file) {
|
|
402
|
-
let content =
|
|
478
|
+
let content = fs2.readFileSync(file).toString();
|
|
403
479
|
if (!this.isDialectSpecific(file)) {
|
|
404
480
|
content = this.read_strategy(content);
|
|
405
481
|
}
|
|
@@ -408,7 +484,23 @@ var MigrationDirectoryReader = class {
|
|
|
408
484
|
};
|
|
409
485
|
|
|
410
486
|
// framework/MigrationSetup.ts
|
|
411
|
-
import
|
|
487
|
+
import fs3 from "fs";
|
|
488
|
+
|
|
489
|
+
// framework/PatchTypes.ts
|
|
490
|
+
import path2 from "path";
|
|
491
|
+
var PATCH_FORMAT_VERSION = 1;
|
|
492
|
+
var DEFAULT_PATCH_TABLE = "proper_patches";
|
|
493
|
+
var PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
|
|
494
|
+
function resolvePatchFolder(config) {
|
|
495
|
+
if (config.patch_folder) return config.patch_folder;
|
|
496
|
+
const dir = path2.dirname(config.migration_folder);
|
|
497
|
+
return dir === "." && !config.migration_folder.includes(path2.sep) && !config.migration_folder.includes("/") ? "patches" : path2.join(dir, "patches");
|
|
498
|
+
}
|
|
499
|
+
function resolvePatchTable(config) {
|
|
500
|
+
return config.patch_table || DEFAULT_PATCH_TABLE;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// framework/MigrationSetup.ts
|
|
412
504
|
var MigrationSetup = class {
|
|
413
505
|
constructor(sqlrunner, config) {
|
|
414
506
|
this.sqlrunner = sqlrunner;
|
|
@@ -416,7 +508,7 @@ var MigrationSetup = class {
|
|
|
416
508
|
}
|
|
417
509
|
setup() {
|
|
418
510
|
return __async(this, null, function* () {
|
|
419
|
-
|
|
511
|
+
fs3.existsSync(this.config.migration_folder) || fs3.mkdirSync(this.config.migration_folder);
|
|
420
512
|
const tableName = this.config.migration_table;
|
|
421
513
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
422
514
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -438,11 +530,39 @@ var MigrationSetup = class {
|
|
|
438
530
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
439
531
|
)`;
|
|
440
532
|
yield this.sqlrunner.query(createTableSql);
|
|
533
|
+
const patchTable = resolvePatchTable(this.config);
|
|
534
|
+
const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
535
|
+
migration_table TEXT NOT NULL,
|
|
536
|
+
patch_key TEXT NOT NULL,
|
|
537
|
+
checksum TEXT NOT NULL,
|
|
538
|
+
format_version INTEGER NOT NULL,
|
|
539
|
+
description TEXT NOT NULL,
|
|
540
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
541
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
542
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
543
|
+
migration_table TEXT NOT NULL,
|
|
544
|
+
patch_key TEXT NOT NULL,
|
|
545
|
+
checksum TEXT NOT NULL,
|
|
546
|
+
format_version INTEGER NOT NULL,
|
|
547
|
+
description TEXT NOT NULL,
|
|
548
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
549
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
550
|
+
)` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
551
|
+
migration_table VARCHAR(255) NOT NULL,
|
|
552
|
+
patch_key VARCHAR(255) NOT NULL,
|
|
553
|
+
checksum CHAR(64) NOT NULL,
|
|
554
|
+
format_version INT NOT NULL,
|
|
555
|
+
description TEXT NOT NULL,
|
|
556
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
557
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
558
|
+
)`;
|
|
559
|
+
yield this.sqlrunner.query(createPatchTableSql);
|
|
441
560
|
});
|
|
442
561
|
}
|
|
443
562
|
teardown() {
|
|
444
563
|
return __async(this, null, function* () {
|
|
445
564
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
565
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
446
566
|
});
|
|
447
567
|
}
|
|
448
568
|
};
|
|
@@ -519,6 +639,673 @@ function markerDialectParser(names) {
|
|
|
519
639
|
var SqliteDialectParser = markerDialectParser(["sqlite"]);
|
|
520
640
|
var PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
|
|
521
641
|
|
|
642
|
+
// framework/PatchDirectoryReader.ts
|
|
643
|
+
import crypto from "crypto";
|
|
644
|
+
import fs4 from "fs";
|
|
645
|
+
import path3 from "path";
|
|
646
|
+
|
|
647
|
+
// framework/PatchValidator.ts
|
|
648
|
+
import Ajv from "ajv";
|
|
649
|
+
import { parseDocument } from "yaml";
|
|
650
|
+
var MAX_DESCRIPTION_LENGTH = 500;
|
|
651
|
+
var MAX_MIGRATION_KEY_LENGTH = 255;
|
|
652
|
+
var migrationKeySchema = {
|
|
653
|
+
type: "string",
|
|
654
|
+
minLength: 1,
|
|
655
|
+
maxLength: MAX_MIGRATION_KEY_LENGTH
|
|
656
|
+
};
|
|
657
|
+
var patchSchema = {
|
|
658
|
+
type: "object",
|
|
659
|
+
additionalProperties: false,
|
|
660
|
+
required: ["version", "description", "operations"],
|
|
661
|
+
properties: {
|
|
662
|
+
version: { type: "integer" },
|
|
663
|
+
description: { type: "string" },
|
|
664
|
+
operations: {
|
|
665
|
+
type: "array",
|
|
666
|
+
minItems: 1,
|
|
667
|
+
items: {
|
|
668
|
+
type: "object",
|
|
669
|
+
additionalProperties: false,
|
|
670
|
+
minProperties: 1,
|
|
671
|
+
maxProperties: 1,
|
|
672
|
+
properties: {
|
|
673
|
+
rename_migration: {
|
|
674
|
+
type: "object",
|
|
675
|
+
additionalProperties: false,
|
|
676
|
+
required: ["from", "to"],
|
|
677
|
+
properties: { from: migrationKeySchema, to: migrationKeySchema }
|
|
678
|
+
},
|
|
679
|
+
mark_applied: {
|
|
680
|
+
type: "object",
|
|
681
|
+
additionalProperties: false,
|
|
682
|
+
required: ["key"],
|
|
683
|
+
properties: { key: migrationKeySchema }
|
|
684
|
+
},
|
|
685
|
+
unmark_applied: {
|
|
686
|
+
type: "object",
|
|
687
|
+
additionalProperties: false,
|
|
688
|
+
required: ["key"],
|
|
689
|
+
properties: { key: migrationKeySchema }
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
var ajv = new Ajv({ allErrors: true, strict: true });
|
|
697
|
+
var validateSchema = ajv.compile(patchSchema);
|
|
698
|
+
function fail(message, file, patchKey) {
|
|
699
|
+
throw new PatchValidationError(message, file, patchKey);
|
|
700
|
+
}
|
|
701
|
+
function checkMigrationKey(value, context, file, patchKey) {
|
|
702
|
+
if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
|
|
703
|
+
if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
|
|
704
|
+
if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
|
|
705
|
+
if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
|
|
706
|
+
fail(`${context}: migration key length out of bounds`, file, patchKey);
|
|
707
|
+
}
|
|
708
|
+
return canonicalMigrationKey(value);
|
|
709
|
+
}
|
|
710
|
+
function assertStrictYaml(doc, file, patchKey) {
|
|
711
|
+
if (doc.errors.length > 0) {
|
|
712
|
+
fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
|
|
713
|
+
}
|
|
714
|
+
if (doc.warnings.length > 0) {
|
|
715
|
+
fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
|
|
716
|
+
}
|
|
717
|
+
const visit = (node) => {
|
|
718
|
+
var _a, _b;
|
|
719
|
+
if (node == null || typeof node !== "object") return;
|
|
720
|
+
if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
|
|
721
|
+
fail("YAML aliases are not permitted in patch files", file, patchKey);
|
|
722
|
+
}
|
|
723
|
+
if (node.anchor) {
|
|
724
|
+
fail("YAML anchors are not permitted in patch files", file, patchKey);
|
|
725
|
+
}
|
|
726
|
+
if (node.tag && ![
|
|
727
|
+
"tag:yaml.org,2002:str",
|
|
728
|
+
"tag:yaml.org,2002:int",
|
|
729
|
+
"tag:yaml.org,2002:bool",
|
|
730
|
+
"tag:yaml.org,2002:null",
|
|
731
|
+
"tag:yaml.org,2002:map",
|
|
732
|
+
"tag:yaml.org,2002:seq"
|
|
733
|
+
].includes(node.tag)) {
|
|
734
|
+
fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
|
|
735
|
+
}
|
|
736
|
+
if (Array.isArray(node.items)) {
|
|
737
|
+
for (const item of node.items) {
|
|
738
|
+
if (item && typeof item === "object" && "key" in item) {
|
|
739
|
+
const keyValue = (_b = item.key) == null ? void 0 : _b.value;
|
|
740
|
+
if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
|
|
741
|
+
visit(item.key);
|
|
742
|
+
visit(item.value);
|
|
743
|
+
} else {
|
|
744
|
+
visit(item);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
visit(doc.contents);
|
|
750
|
+
}
|
|
751
|
+
function parsePatchContent(content, fileName, patchKey) {
|
|
752
|
+
var _a;
|
|
753
|
+
const doc = parseDocument(content, {
|
|
754
|
+
uniqueKeys: true,
|
|
755
|
+
// duplicate mapping keys become errors
|
|
756
|
+
merge: false,
|
|
757
|
+
schema: "core",
|
|
758
|
+
version: "1.2"
|
|
759
|
+
});
|
|
760
|
+
assertStrictYaml(doc, fileName, patchKey);
|
|
761
|
+
const raw = doc.toJS({ mapAsMap: false });
|
|
762
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
763
|
+
fail("Patch document must be a YAML mapping", fileName, patchKey);
|
|
764
|
+
}
|
|
765
|
+
if (!validateSchema(raw)) {
|
|
766
|
+
const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
|
|
767
|
+
const anyRaw = raw;
|
|
768
|
+
if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
|
|
769
|
+
fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
770
|
+
}
|
|
771
|
+
fail(`Schema validation failed: ${detail}`, fileName, patchKey);
|
|
772
|
+
}
|
|
773
|
+
const parsed = raw;
|
|
774
|
+
if (parsed.version !== PATCH_FORMAT_VERSION) {
|
|
775
|
+
fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
776
|
+
}
|
|
777
|
+
const description = parsed.description.trim();
|
|
778
|
+
if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
|
|
779
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
780
|
+
fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
|
|
781
|
+
}
|
|
782
|
+
const operations = parsed.operations.map((op, index) => {
|
|
783
|
+
const verbs = Object.keys(op);
|
|
784
|
+
const verb = verbs[0];
|
|
785
|
+
const context = `operation ${index} (${verb})`;
|
|
786
|
+
switch (verb) {
|
|
787
|
+
case "rename_migration": {
|
|
788
|
+
const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
|
|
789
|
+
const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
|
|
790
|
+
if (from === to) {
|
|
791
|
+
fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
|
|
792
|
+
}
|
|
793
|
+
return { verb: "rename_migration", from, to };
|
|
794
|
+
}
|
|
795
|
+
case "mark_applied":
|
|
796
|
+
return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
|
|
797
|
+
case "unmark_applied":
|
|
798
|
+
return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
|
|
799
|
+
default:
|
|
800
|
+
fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
return { version: parsed.version, description, operations };
|
|
804
|
+
}
|
|
805
|
+
function validatePatchPlan(patches, manifest) {
|
|
806
|
+
const renames = [];
|
|
807
|
+
for (const patch of patches) {
|
|
808
|
+
patch.operations.forEach((op, index) => {
|
|
809
|
+
if (op.verb === "rename_migration") {
|
|
810
|
+
renames.push({ from: op.from, to: op.to, file: patch.fileName });
|
|
811
|
+
} else if (op.verb === "mark_applied") {
|
|
812
|
+
if (!manifest.has(op.key)) {
|
|
813
|
+
throw new PatchValidationError(
|
|
814
|
+
`operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
|
|
815
|
+
patch.fileName,
|
|
816
|
+
patch.patchKey
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (renames.length === 0) return;
|
|
823
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
824
|
+
renames.forEach((r) => {
|
|
825
|
+
mentioned.add(r.from);
|
|
826
|
+
mentioned.add(r.to);
|
|
827
|
+
});
|
|
828
|
+
const finalKey = (start) => {
|
|
829
|
+
let current = start;
|
|
830
|
+
for (const r of renames) {
|
|
831
|
+
if (current === r.from) current = r.to;
|
|
832
|
+
}
|
|
833
|
+
return current;
|
|
834
|
+
};
|
|
835
|
+
for (const key of mentioned) {
|
|
836
|
+
const finish = finalKey(key);
|
|
837
|
+
if (manifest.has(key)) {
|
|
838
|
+
if (finish !== key) {
|
|
839
|
+
throw new PatchValidationError(
|
|
840
|
+
`rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
if (!manifest.has(finish)) {
|
|
845
|
+
throw new PatchValidationError(
|
|
846
|
+
`rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// framework/PatchDirectoryReader.ts
|
|
854
|
+
var PatchDirectoryReader = class {
|
|
855
|
+
constructor(directory) {
|
|
856
|
+
this.directory = directory;
|
|
857
|
+
}
|
|
858
|
+
loadPatches() {
|
|
859
|
+
if (!fs4.existsSync(this.directory)) return [];
|
|
860
|
+
const entries = fs4.readdirSync(this.directory, { withFileTypes: true });
|
|
861
|
+
const patchFiles = [];
|
|
862
|
+
for (const entry of entries) {
|
|
863
|
+
if (!entry.name.endsWith(".yaml")) continue;
|
|
864
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
865
|
+
if (entry.isSymbolicLink()) {
|
|
866
|
+
throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
|
|
867
|
+
}
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
patchFiles.push(entry.name);
|
|
871
|
+
}
|
|
872
|
+
patchFiles.sort((a, b) => {
|
|
873
|
+
const stampA = parseInt(a.slice(0, 13), 10);
|
|
874
|
+
const stampB = parseInt(b.slice(0, 13), 10);
|
|
875
|
+
if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
|
|
876
|
+
return stampA - stampB;
|
|
877
|
+
}
|
|
878
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
879
|
+
});
|
|
880
|
+
return patchFiles.map((fileName) => {
|
|
881
|
+
const match = PATCH_FILENAME_REGEX.exec(fileName);
|
|
882
|
+
if (!match) {
|
|
883
|
+
throw new PatchValidationError(
|
|
884
|
+
`invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
|
|
885
|
+
fileName
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const patchKey = fileName.slice(0, -".yaml".length);
|
|
889
|
+
const filePath = path3.join(this.directory, fileName);
|
|
890
|
+
const bytes = fs4.readFileSync(filePath);
|
|
891
|
+
const checksum = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
892
|
+
const content = bytes.toString("utf8");
|
|
893
|
+
const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
|
|
894
|
+
return { patchKey, fileName, filePath, checksum, version, description, operations };
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// framework/PatchRunner.ts
|
|
900
|
+
function toError(error) {
|
|
901
|
+
if (error instanceof Error) return error;
|
|
902
|
+
return new Error(String(error));
|
|
903
|
+
}
|
|
904
|
+
function extractRows(result) {
|
|
905
|
+
if (!Array.isArray(result)) return [];
|
|
906
|
+
if (Array.isArray(result[0])) return result[0];
|
|
907
|
+
if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
|
|
908
|
+
return result;
|
|
909
|
+
}
|
|
910
|
+
if (result[0] == null) return [];
|
|
911
|
+
return [result[0]];
|
|
912
|
+
}
|
|
913
|
+
var PatchRunner = class {
|
|
914
|
+
constructor(sqlrunner, config) {
|
|
915
|
+
this.sqlrunner = sqlrunner;
|
|
916
|
+
this.config = config;
|
|
917
|
+
this.patchTable = resolvePatchTable(config);
|
|
918
|
+
this.migrationTable = config.migration_table;
|
|
919
|
+
this.dialect = config.database;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Discovers, validates, and applies every unapplied patch in order.
|
|
923
|
+
* Each unapplied patch is its own transaction; earlier committed patches
|
|
924
|
+
* remain committed if a later patch fails.
|
|
925
|
+
*/
|
|
926
|
+
applyPending() {
|
|
927
|
+
return __async(this, null, function* () {
|
|
928
|
+
const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
|
|
929
|
+
const patches = reader.loadPatches();
|
|
930
|
+
const history = yield this.loadHistory();
|
|
931
|
+
if (patches.length === 0 && history.length === 0) {
|
|
932
|
+
return [];
|
|
933
|
+
}
|
|
934
|
+
const byKey = new Map(patches.map((p) => [p.patchKey, p]));
|
|
935
|
+
for (const row of history) {
|
|
936
|
+
const file = byKey.get(row.patch_key);
|
|
937
|
+
if (!file) {
|
|
938
|
+
throw new PatchIntegrityError(
|
|
939
|
+
`applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
|
|
940
|
+
void 0,
|
|
941
|
+
row.patch_key
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
if (file.checksum !== row.checksum) {
|
|
945
|
+
throw new PatchIntegrityError(
|
|
946
|
+
`applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
|
|
947
|
+
file.fileName,
|
|
948
|
+
row.patch_key,
|
|
949
|
+
row.checksum,
|
|
950
|
+
file.checksum
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
|
|
955
|
+
validatePatchPlan(patches, manifest);
|
|
956
|
+
const appliedKeys = new Set(history.map((r) => r.patch_key));
|
|
957
|
+
const results = [];
|
|
958
|
+
for (const patch of patches) {
|
|
959
|
+
if (appliedKeys.has(patch.patchKey)) {
|
|
960
|
+
results.push({
|
|
961
|
+
patchKey: patch.patchKey,
|
|
962
|
+
fileName: patch.fileName,
|
|
963
|
+
status: "already_applied",
|
|
964
|
+
operations: []
|
|
965
|
+
});
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
results.push(yield this.applyOne(patch, manifest));
|
|
969
|
+
}
|
|
970
|
+
return results;
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
loadHistory() {
|
|
974
|
+
return __async(this, null, function* () {
|
|
975
|
+
try {
|
|
976
|
+
const result = yield this.sqlrunner.query(
|
|
977
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
|
|
978
|
+
[this.migrationTable]
|
|
979
|
+
);
|
|
980
|
+
return extractRows(result);
|
|
981
|
+
} catch (error) {
|
|
982
|
+
throw new PatchExecutionError(
|
|
983
|
+
`failed to read patch history from '${this.patchTable}'`,
|
|
984
|
+
void 0,
|
|
985
|
+
void 0,
|
|
986
|
+
void 0,
|
|
987
|
+
void 0,
|
|
988
|
+
toError(error)
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
beginSql() {
|
|
994
|
+
switch (this.dialect) {
|
|
995
|
+
case "sqlite":
|
|
996
|
+
return "BEGIN IMMEDIATE";
|
|
997
|
+
case "pg":
|
|
998
|
+
return "BEGIN";
|
|
999
|
+
default:
|
|
1000
|
+
return "START TRANSACTION";
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
begin(patch) {
|
|
1004
|
+
return __async(this, null, function* () {
|
|
1005
|
+
const deadline = Date.now() + 1e4;
|
|
1006
|
+
while (true) {
|
|
1007
|
+
try {
|
|
1008
|
+
yield this.sqlrunner.execute(this.beginSql());
|
|
1009
|
+
return;
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
const message = toError(error).message;
|
|
1012
|
+
if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
|
|
1013
|
+
yield new Promise((resolve) => setTimeout(resolve, 50));
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
throw new PatchExecutionError(
|
|
1017
|
+
"failed to start patch transaction",
|
|
1018
|
+
patch.fileName,
|
|
1019
|
+
patch.patchKey,
|
|
1020
|
+
void 0,
|
|
1021
|
+
void 0,
|
|
1022
|
+
toError(error)
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
rollbackQuietly() {
|
|
1029
|
+
return __async(this, null, function* () {
|
|
1030
|
+
try {
|
|
1031
|
+
yield this.sqlrunner.execute("ROLLBACK");
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
applyOne(patch, manifest) {
|
|
1037
|
+
return __async(this, null, function* () {
|
|
1038
|
+
yield this.begin(patch);
|
|
1039
|
+
try {
|
|
1040
|
+
yield this.sqlrunner.execute(
|
|
1041
|
+
`INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
|
|
1042
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1043
|
+
[this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
|
|
1044
|
+
);
|
|
1045
|
+
} catch (claimError) {
|
|
1046
|
+
yield this.rollbackQuietly();
|
|
1047
|
+
const committed = yield this.findCommittedRow(patch.patchKey);
|
|
1048
|
+
if (committed) {
|
|
1049
|
+
if (committed.checksum === patch.checksum) {
|
|
1050
|
+
return {
|
|
1051
|
+
patchKey: patch.patchKey,
|
|
1052
|
+
fileName: patch.fileName,
|
|
1053
|
+
status: "already_applied",
|
|
1054
|
+
operations: []
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
throw new PatchIntegrityError(
|
|
1058
|
+
`patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
|
|
1059
|
+
patch.fileName,
|
|
1060
|
+
patch.patchKey,
|
|
1061
|
+
committed.checksum,
|
|
1062
|
+
patch.checksum
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
throw new PatchExecutionError(
|
|
1066
|
+
"failed to claim patch-history row",
|
|
1067
|
+
patch.fileName,
|
|
1068
|
+
patch.patchKey,
|
|
1069
|
+
void 0,
|
|
1070
|
+
void 0,
|
|
1071
|
+
toError(claimError)
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
const operationResults = [];
|
|
1075
|
+
try {
|
|
1076
|
+
for (let index = 0; index < patch.operations.length; index++) {
|
|
1077
|
+
operationResults.push(
|
|
1078
|
+
yield this.applyOperation(patch, patch.operations[index], index, manifest)
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
yield this.sqlrunner.execute("COMMIT");
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
yield this.rollbackQuietly();
|
|
1084
|
+
if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
throw new PatchExecutionError(
|
|
1088
|
+
"patch application failed",
|
|
1089
|
+
patch.fileName,
|
|
1090
|
+
patch.patchKey,
|
|
1091
|
+
void 0,
|
|
1092
|
+
void 0,
|
|
1093
|
+
toError(error)
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
patchKey: patch.patchKey,
|
|
1098
|
+
fileName: patch.fileName,
|
|
1099
|
+
status: "applied",
|
|
1100
|
+
operations: operationResults
|
|
1101
|
+
};
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
findCommittedRow(patchKey) {
|
|
1105
|
+
return __async(this, null, function* () {
|
|
1106
|
+
const result = yield this.sqlrunner.query(
|
|
1107
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
|
|
1108
|
+
[this.migrationTable, patchKey]
|
|
1109
|
+
);
|
|
1110
|
+
const list = extractRows(result);
|
|
1111
|
+
return list.length > 0 ? list[0] : null;
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
countRows(key) {
|
|
1115
|
+
return __async(this, null, function* () {
|
|
1116
|
+
var _a, _b, _c;
|
|
1117
|
+
const result = yield this.sqlrunner.query(
|
|
1118
|
+
`SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1119
|
+
[key]
|
|
1120
|
+
);
|
|
1121
|
+
const rows = extractRows(result);
|
|
1122
|
+
const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
|
|
1123
|
+
return Number(value != null ? value : 0);
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
conflict(patch, index, verb, message, keys, counts) {
|
|
1127
|
+
throw new PatchConflictError(
|
|
1128
|
+
`operation ${index} (${verb}): ${message}`,
|
|
1129
|
+
patch.fileName,
|
|
1130
|
+
patch.patchKey,
|
|
1131
|
+
index,
|
|
1132
|
+
verb,
|
|
1133
|
+
keys,
|
|
1134
|
+
counts
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
applyOperation(patch, op, index, manifest) {
|
|
1138
|
+
return __async(this, null, function* () {
|
|
1139
|
+
var _a, _b, _c, _d;
|
|
1140
|
+
try {
|
|
1141
|
+
switch (op.verb) {
|
|
1142
|
+
case "rename_migration": {
|
|
1143
|
+
const fromCount = yield this.countRows(op.from);
|
|
1144
|
+
const toCount = yield this.countRows(op.to);
|
|
1145
|
+
const counts = { [op.from]: fromCount, [op.to]: toCount };
|
|
1146
|
+
if (fromCount > 1 || toCount > 1) {
|
|
1147
|
+
this.conflict(
|
|
1148
|
+
patch,
|
|
1149
|
+
index,
|
|
1150
|
+
op.verb,
|
|
1151
|
+
`ledger corruption: duplicate rows for a migration key`,
|
|
1152
|
+
[op.from, op.to],
|
|
1153
|
+
counts
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
if (fromCount === 1 && toCount === 1) {
|
|
1157
|
+
this.conflict(
|
|
1158
|
+
patch,
|
|
1159
|
+
index,
|
|
1160
|
+
op.verb,
|
|
1161
|
+
`both '${op.from}' and '${op.to}' exist in the ledger`,
|
|
1162
|
+
[op.from, op.to],
|
|
1163
|
+
counts
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
if (fromCount === 0) {
|
|
1167
|
+
return { verb: op.verb, changed: false };
|
|
1168
|
+
}
|
|
1169
|
+
const target = manifest.get(op.to);
|
|
1170
|
+
if (target) {
|
|
1171
|
+
yield this.sqlrunner.execute(
|
|
1172
|
+
`UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
|
|
1173
|
+
[op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
|
|
1174
|
+
);
|
|
1175
|
+
} else {
|
|
1176
|
+
yield this.sqlrunner.execute(
|
|
1177
|
+
`UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
|
|
1178
|
+
[op.to, op.from]
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
return { verb: op.verb, changed: true };
|
|
1182
|
+
}
|
|
1183
|
+
case "mark_applied": {
|
|
1184
|
+
const count = yield this.countRows(op.key);
|
|
1185
|
+
if (count > 1) {
|
|
1186
|
+
this.conflict(
|
|
1187
|
+
patch,
|
|
1188
|
+
index,
|
|
1189
|
+
op.verb,
|
|
1190
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1191
|
+
[op.key],
|
|
1192
|
+
{ [op.key]: count }
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
if (count === 1) {
|
|
1196
|
+
return { verb: op.verb, changed: false };
|
|
1197
|
+
}
|
|
1198
|
+
const entry = manifest.get(op.key);
|
|
1199
|
+
yield this.sqlrunner.execute(
|
|
1200
|
+
`INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
|
|
1201
|
+
[op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
|
|
1202
|
+
);
|
|
1203
|
+
return { verb: op.verb, changed: true };
|
|
1204
|
+
}
|
|
1205
|
+
case "unmark_applied": {
|
|
1206
|
+
const count = yield this.countRows(op.key);
|
|
1207
|
+
if (count > 1) {
|
|
1208
|
+
this.conflict(
|
|
1209
|
+
patch,
|
|
1210
|
+
index,
|
|
1211
|
+
op.verb,
|
|
1212
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1213
|
+
[op.key],
|
|
1214
|
+
{ [op.key]: count }
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
if (count === 0) {
|
|
1218
|
+
return { verb: op.verb, changed: false };
|
|
1219
|
+
}
|
|
1220
|
+
yield this.sqlrunner.execute(
|
|
1221
|
+
`DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1222
|
+
[op.key]
|
|
1223
|
+
);
|
|
1224
|
+
return { verb: op.verb, changed: true };
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
if (error instanceof PatchConflictError) throw error;
|
|
1229
|
+
throw new PatchExecutionError(
|
|
1230
|
+
`operation failed`,
|
|
1231
|
+
patch.fileName,
|
|
1232
|
+
patch.patchKey,
|
|
1233
|
+
index,
|
|
1234
|
+
op.verb,
|
|
1235
|
+
toError(error)
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
// framework/PatchCreator.ts
|
|
1243
|
+
import fs5 from "fs";
|
|
1244
|
+
import path4 from "path";
|
|
1245
|
+
var MAX_NAME_LENGTH = 120;
|
|
1246
|
+
var SCAFFOLD = `version: 1
|
|
1247
|
+
description: TODO
|
|
1248
|
+
operations: []
|
|
1249
|
+
`;
|
|
1250
|
+
var PatchCreator = class _PatchCreator {
|
|
1251
|
+
constructor(patchFolder) {
|
|
1252
|
+
this.patchFolder = patchFolder;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
|
|
1256
|
+
* Rejects empty results, path separators, `..`, control characters,
|
|
1257
|
+
* characters outside [a-z0-9_-], and names longer than 120 characters.
|
|
1258
|
+
*/
|
|
1259
|
+
static normalizeName(name) {
|
|
1260
|
+
const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
|
|
1261
|
+
if (normalized.length === 0) {
|
|
1262
|
+
throw new CLIError("Patch name is required");
|
|
1263
|
+
}
|
|
1264
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
1265
|
+
throw new CLIError("Patch name must not contain path separators");
|
|
1266
|
+
}
|
|
1267
|
+
if (normalized.includes("..")) {
|
|
1268
|
+
throw new CLIError("Patch name must not contain '..'");
|
|
1269
|
+
}
|
|
1270
|
+
if (/[\x00-\x1f\x7f]/.test(normalized)) {
|
|
1271
|
+
throw new CLIError("Patch name must not contain control characters");
|
|
1272
|
+
}
|
|
1273
|
+
if (!/^[a-z0-9_-]+$/.test(normalized)) {
|
|
1274
|
+
throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
|
|
1275
|
+
}
|
|
1276
|
+
if (normalized.length > MAX_NAME_LENGTH) {
|
|
1277
|
+
throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
|
|
1278
|
+
}
|
|
1279
|
+
return normalized;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
|
|
1283
|
+
* file creation. On a millisecond-stamp collision, mints a later stamp
|
|
1284
|
+
* and retries. Returns the created path.
|
|
1285
|
+
*/
|
|
1286
|
+
create(name) {
|
|
1287
|
+
const normalized = _PatchCreator.normalizeName(name);
|
|
1288
|
+
if (!fs5.existsSync(this.patchFolder)) {
|
|
1289
|
+
fs5.mkdirSync(this.patchFolder, { recursive: true });
|
|
1290
|
+
}
|
|
1291
|
+
let stamp = Date.now();
|
|
1292
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
1293
|
+
const filePath = path4.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
|
|
1294
|
+
try {
|
|
1295
|
+
fs5.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
|
|
1296
|
+
return filePath;
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
if (error && error.code === "EEXIST") {
|
|
1299
|
+
stamp += 1;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
throw error;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
throw new CLIError("Unable to create patch file: too many filename collisions");
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
|
|
522
1309
|
// framework/SQLRunner.ts
|
|
523
1310
|
var BaseSQLRunner = class {
|
|
524
1311
|
/**
|
|
@@ -833,7 +1620,7 @@ var PgRunner = class _PgRunner extends BaseSQLRunner {
|
|
|
833
1620
|
};
|
|
834
1621
|
|
|
835
1622
|
// framework/MigrationRunner.ts
|
|
836
|
-
function
|
|
1623
|
+
function toError2(error) {
|
|
837
1624
|
if (error instanceof Error) return error;
|
|
838
1625
|
return new Error(String(error));
|
|
839
1626
|
}
|
|
@@ -861,10 +1648,12 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
861
1648
|
return __async(this, null, function* () {
|
|
862
1649
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
863
1650
|
const config = configReader.loadFile();
|
|
1651
|
+
let factoryOwnsConnection = false;
|
|
864
1652
|
if (!conn) {
|
|
865
1653
|
conn = yield this.createConnection(config);
|
|
1654
|
+
factoryOwnsConnection = true;
|
|
866
1655
|
}
|
|
867
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1656
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
868
1657
|
});
|
|
869
1658
|
}
|
|
870
1659
|
static createConnection(config) {
|
|
@@ -884,7 +1673,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
884
1673
|
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
885
1674
|
return conn;
|
|
886
1675
|
} catch (error) {
|
|
887
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1676
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
888
1677
|
}
|
|
889
1678
|
case "sqlite":
|
|
890
1679
|
if (!config.sqlite) {
|
|
@@ -899,7 +1688,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
899
1688
|
});
|
|
900
1689
|
return conn;
|
|
901
1690
|
} catch (error) {
|
|
902
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1691
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
903
1692
|
}
|
|
904
1693
|
case "pg":
|
|
905
1694
|
if (!config.pg && !process.env.DATABASE_URL) {
|
|
@@ -915,7 +1704,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
915
1704
|
yield conn.connect();
|
|
916
1705
|
return conn;
|
|
917
1706
|
} catch (error) {
|
|
918
|
-
throw DatabaseConnectionError.connectionFailed("pg",
|
|
1707
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
919
1708
|
}
|
|
920
1709
|
default:
|
|
921
1710
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -929,7 +1718,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
929
1718
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
930
1719
|
});
|
|
931
1720
|
}
|
|
932
|
-
create(config, conn) {
|
|
1721
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
933
1722
|
return __async(this, null, function* () {
|
|
934
1723
|
let sqlrunner;
|
|
935
1724
|
let driverConnection = conn;
|
|
@@ -942,8 +1731,19 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
942
1731
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
943
1732
|
const read_strategy = this.getReadStategy(config);
|
|
944
1733
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
945
|
-
|
|
946
|
-
|
|
1734
|
+
const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
|
|
1735
|
+
try {
|
|
1736
|
+
yield runner.setup();
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
if (factoryOwnsConnection) {
|
|
1739
|
+
try {
|
|
1740
|
+
yield sqlrunner.end();
|
|
1741
|
+
} catch (e) {
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
throw error;
|
|
1745
|
+
}
|
|
1746
|
+
return runner;
|
|
947
1747
|
});
|
|
948
1748
|
}
|
|
949
1749
|
createEmpty(config) {
|
|
@@ -953,7 +1753,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
953
1753
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
954
1754
|
const read_strategy = this.getReadStategy(config);
|
|
955
1755
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
956
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1756
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
957
1757
|
});
|
|
958
1758
|
}
|
|
959
1759
|
getReadStategy(config) {
|
|
@@ -970,33 +1770,77 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
970
1770
|
}
|
|
971
1771
|
};
|
|
972
1772
|
var MySQLMigrationRunner = class {
|
|
973
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1773
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
974
1774
|
this.config = config;
|
|
975
1775
|
this.directory = directory;
|
|
976
1776
|
this.setupRunner = setupRunner;
|
|
977
1777
|
this.sqlrunner = sqlrunner;
|
|
978
1778
|
this.connection = connection;
|
|
1779
|
+
this.preflightEnabled = preflightEnabled;
|
|
1780
|
+
/**
|
|
1781
|
+
* Memoized in-flight preflight promise. Simultaneous or repeated calls
|
|
1782
|
+
* to setup() on one runner execute the preflight (migration table setup
|
|
1783
|
+
* + patch application) exactly once. Cleared after rejection so a caller
|
|
1784
|
+
* may retry after fixing the cause.
|
|
1785
|
+
*/
|
|
1786
|
+
this.preflightPromise = null;
|
|
1787
|
+
this.lastPatchResults = [];
|
|
979
1788
|
}
|
|
980
1789
|
setup() {
|
|
1790
|
+
return __async(this, null, function* () {
|
|
1791
|
+
if (!this.preflightEnabled) {
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (!this.preflightPromise) {
|
|
1795
|
+
this.preflightPromise = this.runPreflight();
|
|
1796
|
+
this.preflightPromise.catch(() => {
|
|
1797
|
+
this.preflightPromise = null;
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
return this.preflightPromise;
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
runPreflight() {
|
|
981
1804
|
return __async(this, null, function* () {
|
|
982
1805
|
try {
|
|
983
1806
|
yield this.setupRunner.setup();
|
|
984
1807
|
} catch (error) {
|
|
985
|
-
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0,
|
|
1808
|
+
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
|
|
986
1809
|
}
|
|
1810
|
+
const patchRunner = new PatchRunner(this.sqlrunner, this.config);
|
|
1811
|
+
this.lastPatchResults = yield patchRunner.applyPending();
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Delegates to the same idempotent preflight; returns the results of the
|
|
1816
|
+
* patch pass that ran (or is running) for this runner.
|
|
1817
|
+
*/
|
|
1818
|
+
applyPendingPatches() {
|
|
1819
|
+
return __async(this, null, function* () {
|
|
1820
|
+
yield this.setup();
|
|
1821
|
+
return this.lastPatchResults;
|
|
987
1822
|
});
|
|
988
1823
|
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Scaffolds a new ledger patch file and returns the created path.
|
|
1826
|
+
* Never connects to a database.
|
|
1827
|
+
*/
|
|
1828
|
+
createPatch(name) {
|
|
1829
|
+
const creator = new PatchCreator(resolvePatchFolder(this.config));
|
|
1830
|
+
return creator.create(name);
|
|
1831
|
+
}
|
|
989
1832
|
terminate() {
|
|
990
1833
|
return __async(this, null, function* () {
|
|
991
1834
|
try {
|
|
992
1835
|
yield this.setupRunner.teardown();
|
|
993
1836
|
} catch (error) {
|
|
994
|
-
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0,
|
|
1837
|
+
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
|
|
995
1838
|
}
|
|
996
1839
|
});
|
|
997
1840
|
}
|
|
998
1841
|
getMigrationsHistory() {
|
|
999
1842
|
return __async(this, null, function* () {
|
|
1843
|
+
yield this.setup();
|
|
1000
1844
|
try {
|
|
1001
1845
|
const results = yield this.sqlrunner.query(`
|
|
1002
1846
|
select *
|
|
@@ -1004,16 +1848,17 @@ var MySQLMigrationRunner = class {
|
|
|
1004
1848
|
`);
|
|
1005
1849
|
return results[0];
|
|
1006
1850
|
} catch (error) {
|
|
1007
|
-
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0,
|
|
1851
|
+
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
|
|
1008
1852
|
}
|
|
1009
1853
|
});
|
|
1010
1854
|
}
|
|
1011
1855
|
getMigrations() {
|
|
1012
1856
|
return __async(this, null, function* () {
|
|
1857
|
+
yield this.setup();
|
|
1013
1858
|
try {
|
|
1014
1859
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
1015
1860
|
} catch (error) {
|
|
1016
|
-
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0,
|
|
1861
|
+
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
|
|
1017
1862
|
}
|
|
1018
1863
|
});
|
|
1019
1864
|
}
|
|
@@ -1026,7 +1871,7 @@ var MySQLMigrationRunner = class {
|
|
|
1026
1871
|
if (error instanceof MigrationExecutionError) {
|
|
1027
1872
|
throw error;
|
|
1028
1873
|
}
|
|
1029
|
-
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0,
|
|
1874
|
+
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
|
|
1030
1875
|
}
|
|
1031
1876
|
});
|
|
1032
1877
|
}
|
|
@@ -1039,12 +1884,13 @@ var MySQLMigrationRunner = class {
|
|
|
1039
1884
|
if (error instanceof MigrationExecutionError) {
|
|
1040
1885
|
throw error;
|
|
1041
1886
|
}
|
|
1042
|
-
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0,
|
|
1887
|
+
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
|
|
1043
1888
|
}
|
|
1044
1889
|
});
|
|
1045
1890
|
}
|
|
1046
1891
|
migrate(migrationNodes, forward) {
|
|
1047
1892
|
return __async(this, null, function* () {
|
|
1893
|
+
yield this.setup();
|
|
1048
1894
|
for (let node of migrationNodes) {
|
|
1049
1895
|
try {
|
|
1050
1896
|
if (forward) {
|
|
@@ -1057,7 +1903,7 @@ var MySQLMigrationRunner = class {
|
|
|
1057
1903
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
1058
1904
|
node.name || String(node),
|
|
1059
1905
|
forward ? node.up_sql() : node.down_sql(),
|
|
1060
|
-
|
|
1906
|
+
toError2(error)
|
|
1061
1907
|
);
|
|
1062
1908
|
}
|
|
1063
1909
|
}
|
|
@@ -1065,6 +1911,7 @@ var MySQLMigrationRunner = class {
|
|
|
1065
1911
|
}
|
|
1066
1912
|
reset() {
|
|
1067
1913
|
return __async(this, null, function* () {
|
|
1914
|
+
yield this.setup();
|
|
1068
1915
|
try {
|
|
1069
1916
|
let migrations = yield this.getMigrations();
|
|
1070
1917
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -1076,7 +1923,7 @@ var MySQLMigrationRunner = class {
|
|
|
1076
1923
|
if (error instanceof MigrationExecutionError) {
|
|
1077
1924
|
throw error;
|
|
1078
1925
|
}
|
|
1079
|
-
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0,
|
|
1926
|
+
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
|
|
1080
1927
|
}
|
|
1081
1928
|
});
|
|
1082
1929
|
}
|
|
@@ -1085,7 +1932,7 @@ var MySQLMigrationRunner = class {
|
|
|
1085
1932
|
const creator = new MigrationCreator(this.config);
|
|
1086
1933
|
creator.create(name);
|
|
1087
1934
|
} catch (error) {
|
|
1088
|
-
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0,
|
|
1935
|
+
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
|
|
1089
1936
|
}
|
|
1090
1937
|
}
|
|
1091
1938
|
close() {
|
|
@@ -1094,7 +1941,7 @@ var MySQLMigrationRunner = class {
|
|
|
1094
1941
|
try {
|
|
1095
1942
|
yield this.sqlrunner.end();
|
|
1096
1943
|
} catch (error) {
|
|
1097
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
1944
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
1098
1945
|
}
|
|
1099
1946
|
}
|
|
1100
1947
|
});
|
|
@@ -1108,7 +1955,7 @@ var MySQLMigrationRunner = class {
|
|
|
1108
1955
|
return __async(this, null, function* () {
|
|
1109
1956
|
try {
|
|
1110
1957
|
console.log(`Checking for ${config_file}`);
|
|
1111
|
-
const config_exist =
|
|
1958
|
+
const config_exist = fs6.existsSync(config_file);
|
|
1112
1959
|
if (!config_exist) {
|
|
1113
1960
|
console.log(`Creating ${config_file}`);
|
|
1114
1961
|
const default_config = {
|
|
@@ -1122,11 +1969,11 @@ var MySQLMigrationRunner = class {
|
|
|
1122
1969
|
"password": ""
|
|
1123
1970
|
}
|
|
1124
1971
|
};
|
|
1125
|
-
|
|
1972
|
+
fs6.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
|
|
1126
1973
|
console.log(`Created ${config_file}`);
|
|
1127
1974
|
}
|
|
1128
1975
|
} catch (error) {
|
|
1129
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
1976
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1130
1977
|
}
|
|
1131
1978
|
});
|
|
1132
1979
|
}
|
|
@@ -1137,7 +1984,7 @@ var FileMigrationConfigReader = class {
|
|
|
1137
1984
|
}
|
|
1138
1985
|
loadFile() {
|
|
1139
1986
|
try {
|
|
1140
|
-
const fileContent =
|
|
1987
|
+
const fileContent = fs6.readFileSync(this.configFile);
|
|
1141
1988
|
const config = JSON.parse(fileContent.toString());
|
|
1142
1989
|
if (!config.migration_folder) {
|
|
1143
1990
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1161,7 +2008,7 @@ var FileMigrationConfigReader = class {
|
|
|
1161
2008
|
if (error instanceof ConfigurationError) {
|
|
1162
2009
|
throw error;
|
|
1163
2010
|
}
|
|
1164
|
-
const err =
|
|
2011
|
+
const err = toError2(error);
|
|
1165
2012
|
if (err.message.includes("ENOENT")) {
|
|
1166
2013
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1167
2014
|
}
|
|
@@ -1178,16 +2025,16 @@ var MigrationCreator = class {
|
|
|
1178
2025
|
throw new CLIError("Migration name is required");
|
|
1179
2026
|
}
|
|
1180
2027
|
try {
|
|
1181
|
-
if (!
|
|
1182
|
-
|
|
2028
|
+
if (!fs6.existsSync(this.config.migration_folder)) {
|
|
2029
|
+
fs6.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1183
2030
|
}
|
|
1184
2031
|
const now_timestamp = Date.now();
|
|
1185
2032
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1186
2033
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1187
|
-
|
|
2034
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1188
2035
|
-- Write your up migration here
|
|
1189
2036
|
`.trim());
|
|
1190
|
-
|
|
2037
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1191
2038
|
-- Write your down migration here
|
|
1192
2039
|
`.trim());
|
|
1193
2040
|
console.log(`Created migration files:`);
|
|
@@ -1197,16 +2044,16 @@ var MigrationCreator = class {
|
|
|
1197
2044
|
if (error instanceof CLIError) {
|
|
1198
2045
|
throw error;
|
|
1199
2046
|
}
|
|
1200
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2047
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1201
2048
|
}
|
|
1202
2049
|
}
|
|
1203
2050
|
};
|
|
1204
2051
|
|
|
1205
2052
|
// framework/SeedRunner.ts
|
|
1206
|
-
import
|
|
1207
|
-
import
|
|
2053
|
+
import fs7 from "fs";
|
|
2054
|
+
import path5 from "path";
|
|
1208
2055
|
import { pathToFileURL } from "url";
|
|
1209
|
-
import
|
|
2056
|
+
import Ajv2 from "ajv";
|
|
1210
2057
|
import addFormats from "ajv-formats";
|
|
1211
2058
|
import { tsImport } from "tsx/esm/api";
|
|
1212
2059
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1215,10 +2062,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1215
2062
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1216
2063
|
}
|
|
1217
2064
|
function walkForFile(rootDir, fileName) {
|
|
1218
|
-
if (!
|
|
1219
|
-
const entries =
|
|
2065
|
+
if (!fs7.existsSync(rootDir)) return null;
|
|
2066
|
+
const entries = fs7.readdirSync(rootDir, { withFileTypes: true });
|
|
1220
2067
|
for (const entry of entries) {
|
|
1221
|
-
const full =
|
|
2068
|
+
const full = path5.join(rootDir, entry.name);
|
|
1222
2069
|
if (entry.isDirectory()) {
|
|
1223
2070
|
const found = walkForFile(full, fileName);
|
|
1224
2071
|
if (found) return found;
|
|
@@ -1312,39 +2159,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1312
2159
|
}
|
|
1313
2160
|
function loadJson(filePath) {
|
|
1314
2161
|
return __async(this, null, function* () {
|
|
1315
|
-
const content = yield
|
|
2162
|
+
const content = yield fs7.promises.readFile(filePath, "utf8");
|
|
1316
2163
|
return JSON.parse(content);
|
|
1317
2164
|
});
|
|
1318
2165
|
}
|
|
1319
2166
|
function createValidator() {
|
|
1320
|
-
const
|
|
1321
|
-
addFormats(
|
|
1322
|
-
return
|
|
2167
|
+
const ajv2 = new Ajv2({ allErrors: true, strict: false });
|
|
2168
|
+
addFormats(ajv2);
|
|
2169
|
+
return ajv2;
|
|
1323
2170
|
}
|
|
1324
2171
|
function validateData(schemaPath, data, validate, log) {
|
|
1325
2172
|
return __async(this, null, function* () {
|
|
1326
2173
|
if (!validate || !schemaPath) return;
|
|
1327
|
-
const content = yield
|
|
2174
|
+
const content = yield fs7.promises.readFile(schemaPath, "utf8");
|
|
1328
2175
|
const schema = JSON.parse(content);
|
|
1329
|
-
const
|
|
1330
|
-
const validateFn =
|
|
2176
|
+
const ajv2 = createValidator();
|
|
2177
|
+
const validateFn = ajv2.compile(schema);
|
|
1331
2178
|
const ok = validateFn(data);
|
|
1332
2179
|
if (!ok) {
|
|
1333
2180
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1334
|
-
throw new Error(`Seed data validation failed: ${
|
|
2181
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1335
2182
|
}
|
|
1336
2183
|
});
|
|
1337
2184
|
}
|
|
1338
2185
|
function runSqlSeed(runner, resolved, direction) {
|
|
1339
2186
|
return __async(this, null, function* () {
|
|
1340
2187
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1341
|
-
const sql = yield
|
|
2188
|
+
const sql = yield fs7.promises.readFile(sqlPath, "utf8");
|
|
1342
2189
|
yield runner.query(sql);
|
|
1343
2190
|
});
|
|
1344
2191
|
}
|
|
1345
2192
|
function loadSeedModule(modulePath) {
|
|
1346
2193
|
return __async(this, null, function* () {
|
|
1347
|
-
const resolved =
|
|
2194
|
+
const resolved = path5.resolve(modulePath);
|
|
1348
2195
|
if (resolved.endsWith(".ts")) {
|
|
1349
2196
|
const fileUrl = pathToFileURL(resolved).href;
|
|
1350
2197
|
return tsImport(fileUrl, fileUrl);
|
|
@@ -1464,13 +2311,23 @@ function createSeedFactory(options) {
|
|
|
1464
2311
|
};
|
|
1465
2312
|
}
|
|
1466
2313
|
export {
|
|
2314
|
+
MigrationError,
|
|
1467
2315
|
MySQLMigrationRunner as MigrationRunner,
|
|
1468
2316
|
MigrationRunnerFactory,
|
|
2317
|
+
PatchConflictError,
|
|
2318
|
+
PatchCreator,
|
|
2319
|
+
PatchError,
|
|
2320
|
+
PatchExecutionError,
|
|
2321
|
+
PatchIntegrityError,
|
|
2322
|
+
PatchRunner,
|
|
2323
|
+
PatchValidationError,
|
|
1469
2324
|
PgRunner,
|
|
1470
2325
|
SQLRunner,
|
|
1471
2326
|
SQLiteRunner,
|
|
1472
2327
|
createSeedFactory,
|
|
1473
2328
|
loadMigrationConfig,
|
|
2329
|
+
resolvePatchFolder,
|
|
2330
|
+
resolvePatchTable,
|
|
1474
2331
|
runSeedsWithRunner
|
|
1475
2332
|
};
|
|
1476
2333
|
//# sourceMappingURL=index.mjs.map
|