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