@noego/proper 0.0.9 → 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 +1120 -119
- package/bin/cli.js.map +1 -1
- package/bin/cli.mjs +1166 -122
- package/bin/cli.mjs.map +1 -1
- package/bin/index.d.mts +284 -4
- package/bin/index.d.ts +284 -4
- package/bin/index.js +1100 -108
- package/bin/index.js.map +1 -1
- package/bin/index.mjs +1087 -108
- package/bin/index.mjs.map +1 -1
- package/lib/runner.js.map +1 -1
- package/lib/runner.mjs.map +1 -1
- package/package.json +6 -3
- package/readme.md +92 -0
package/bin/index.mjs
CHANGED
|
@@ -39,14 +39,10 @@ var __async = (__this, __arguments, generator) => {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
// framework/MigrationRunner.ts
|
|
42
|
-
import
|
|
43
|
-
import mysql from "mysql2/promise";
|
|
44
|
-
import * as sqlite from "sqlite";
|
|
45
|
-
import * as sqlite3 from "sqlite3";
|
|
42
|
+
import fs6 from "fs";
|
|
46
43
|
|
|
47
44
|
// framework/MigrationDirectoryReader.ts
|
|
48
|
-
import
|
|
49
|
-
import path from "path";
|
|
45
|
+
import fs2 from "fs";
|
|
50
46
|
|
|
51
47
|
// framework/errors.ts
|
|
52
48
|
var MigrationError = class _MigrationError extends Error {
|
|
@@ -161,6 +157,54 @@ ${originalError.message}`;
|
|
|
161
157
|
return new _MigrationExecutionError(message);
|
|
162
158
|
}
|
|
163
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
|
+
};
|
|
164
208
|
var CLIError = class _CLIError extends MigrationError {
|
|
165
209
|
constructor(message) {
|
|
166
210
|
super(`CLI Error: ${message}`);
|
|
@@ -323,6 +367,36 @@ var SqlMigrationBuilder = class {
|
|
|
323
367
|
}
|
|
324
368
|
};
|
|
325
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
|
+
|
|
326
400
|
// framework/MigrationDirectoryReader.ts
|
|
327
401
|
var MigrationDirectoryReader = class {
|
|
328
402
|
constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
|
|
@@ -334,27 +408,23 @@ var MigrationDirectoryReader = class {
|
|
|
334
408
|
/**
|
|
335
409
|
* Resolves the appropriate file for a migration based on dialect.
|
|
336
410
|
* Priority: dialect-specific file > generic file
|
|
411
|
+
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
337
412
|
*/
|
|
338
413
|
resolveFile(baseName, direction) {
|
|
339
|
-
|
|
340
|
-
const dialectFile = path.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
341
|
-
if (fs.existsSync(dialectFile)) return dialectFile;
|
|
342
|
-
const genericFile = path.join(this.directory, `${baseName}.${direction}.sql`);
|
|
343
|
-
if (fs.existsSync(genericFile)) return genericFile;
|
|
344
|
-
return null;
|
|
414
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
345
415
|
}
|
|
346
416
|
/**
|
|
347
417
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
348
418
|
*/
|
|
349
419
|
isDialectSpecific(filePath) {
|
|
350
|
-
return /\.(mysql|sqlite)\.(up|down)\.sql$/i.test(filePath);
|
|
420
|
+
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
351
421
|
}
|
|
352
422
|
loadMigrations(table, connection) {
|
|
353
|
-
|
|
354
|
-
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);
|
|
355
425
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
356
426
|
dir_content.forEach((file) => {
|
|
357
|
-
const key = file
|
|
427
|
+
const key = canonicalMigrationKey(file);
|
|
358
428
|
uniqueKeys.add(key);
|
|
359
429
|
});
|
|
360
430
|
const migration_sorter = {};
|
|
@@ -394,14 +464,14 @@ var MigrationDirectoryReader = class {
|
|
|
394
464
|
return builder;
|
|
395
465
|
}
|
|
396
466
|
sql_up(file) {
|
|
397
|
-
let content =
|
|
467
|
+
let content = fs2.readFileSync(file).toString();
|
|
398
468
|
if (!this.isDialectSpecific(file)) {
|
|
399
469
|
content = this.read_strategy(content);
|
|
400
470
|
}
|
|
401
471
|
return content.trim();
|
|
402
472
|
}
|
|
403
473
|
sql_down(file) {
|
|
404
|
-
let content =
|
|
474
|
+
let content = fs2.readFileSync(file).toString();
|
|
405
475
|
if (!this.isDialectSpecific(file)) {
|
|
406
476
|
content = this.read_strategy(content);
|
|
407
477
|
}
|
|
@@ -410,7 +480,23 @@ var MigrationDirectoryReader = class {
|
|
|
410
480
|
};
|
|
411
481
|
|
|
412
482
|
// framework/MigrationSetup.ts
|
|
413
|
-
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
|
|
414
500
|
var MigrationSetup = class {
|
|
415
501
|
constructor(sqlrunner, config) {
|
|
416
502
|
this.sqlrunner = sqlrunner;
|
|
@@ -418,7 +504,7 @@ var MigrationSetup = class {
|
|
|
418
504
|
}
|
|
419
505
|
setup() {
|
|
420
506
|
return __async(this, null, function* () {
|
|
421
|
-
|
|
507
|
+
fs3.existsSync(this.config.migration_folder) || fs3.mkdirSync(this.config.migration_folder);
|
|
422
508
|
const tableName = this.config.migration_table;
|
|
423
509
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
424
510
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -426,6 +512,12 @@ var MigrationSetup = class {
|
|
|
426
512
|
up TEXT,
|
|
427
513
|
down TEXT,
|
|
428
514
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
515
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
516
|
+
id SERIAL PRIMARY KEY,
|
|
517
|
+
migration_key TEXT,
|
|
518
|
+
up TEXT,
|
|
519
|
+
down TEXT,
|
|
520
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
429
521
|
)` : `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
430
522
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
431
523
|
migration_key VARCHAR(255),
|
|
@@ -434,11 +526,39 @@ var MigrationSetup = class {
|
|
|
434
526
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
435
527
|
)`;
|
|
436
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);
|
|
437
556
|
});
|
|
438
557
|
}
|
|
439
558
|
teardown() {
|
|
440
559
|
return __async(this, null, function* () {
|
|
441
560
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
561
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
442
562
|
});
|
|
443
563
|
}
|
|
444
564
|
};
|
|
@@ -485,31 +605,702 @@ function MySqlDialectParser(sql) {
|
|
|
485
605
|
}
|
|
486
606
|
return result;
|
|
487
607
|
}
|
|
488
|
-
function
|
|
489
|
-
const
|
|
490
|
-
let result = "";
|
|
491
|
-
let isInSqliteBlock = true;
|
|
492
|
-
const startSqliteRegex = /^\s*--\s*\[\s*sqlite\s*\]\s*$/i;
|
|
608
|
+
function markerDialectParser(names) {
|
|
609
|
+
const startRegex = new RegExp(`^\\s*--\\s*\\[\\s*(${names.join("|")})\\s*\\]\\s*$`, "i");
|
|
493
610
|
const anyDialectRegex = /^\s*--\s*\[\s*\w+\s*\]\s*$/i;
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
611
|
+
return function(sql) {
|
|
612
|
+
const lines = sql.split("\n");
|
|
613
|
+
let result = "";
|
|
614
|
+
let capturing = true;
|
|
615
|
+
for (const line of lines) {
|
|
616
|
+
const trimmed = line.trim();
|
|
617
|
+
if (startRegex.test(trimmed)) {
|
|
618
|
+
capturing = true;
|
|
619
|
+
result += line + "\n";
|
|
620
|
+
continue;
|
|
621
|
+
} else if (anyDialectRegex.test(trimmed)) {
|
|
622
|
+
capturing = false;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (!capturing && line.toLowerCase().includes("create index")) {
|
|
626
|
+
capturing = true;
|
|
627
|
+
}
|
|
628
|
+
if (capturing) {
|
|
629
|
+
result += line + "\n";
|
|
630
|
+
}
|
|
503
631
|
}
|
|
504
|
-
|
|
505
|
-
|
|
632
|
+
return result;
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
var SqliteDialectParser = markerDialectParser(["sqlite"]);
|
|
636
|
+
var PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
|
|
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
|
+
}
|
|
506
689
|
}
|
|
507
|
-
|
|
508
|
-
|
|
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);
|
|
509
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
|
+
});
|
|
510
817
|
}
|
|
511
|
-
return
|
|
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));
|
|
512
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
|
+
};
|
|
513
1304
|
|
|
514
1305
|
// framework/SQLRunner.ts
|
|
515
1306
|
var BaseSQLRunner = class {
|
|
@@ -725,12 +1516,122 @@ ${sql}
|
|
|
725
1516
|
});
|
|
726
1517
|
}
|
|
727
1518
|
};
|
|
1519
|
+
var PgRunner = class _PgRunner extends BaseSQLRunner {
|
|
1520
|
+
constructor(connection) {
|
|
1521
|
+
super();
|
|
1522
|
+
this.connection = connection;
|
|
1523
|
+
}
|
|
1524
|
+
/** `?` -> `$1`, `$2`, ... outside of string literals / comments. */
|
|
1525
|
+
static toPositional(sql) {
|
|
1526
|
+
let out = "";
|
|
1527
|
+
let n = 0;
|
|
1528
|
+
let inSingle = false;
|
|
1529
|
+
let inDouble = false;
|
|
1530
|
+
let inLineComment = false;
|
|
1531
|
+
let inBlockComment = false;
|
|
1532
|
+
for (let i = 0; i < sql.length; i++) {
|
|
1533
|
+
const ch = sql[i];
|
|
1534
|
+
const next = sql[i + 1];
|
|
1535
|
+
if (inLineComment) {
|
|
1536
|
+
out += ch;
|
|
1537
|
+
if (ch === "\n") inLineComment = false;
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (inBlockComment) {
|
|
1541
|
+
out += ch;
|
|
1542
|
+
if (ch === "*" && next === "/") {
|
|
1543
|
+
out += next;
|
|
1544
|
+
i++;
|
|
1545
|
+
inBlockComment = false;
|
|
1546
|
+
}
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (inSingle) {
|
|
1550
|
+
out += ch;
|
|
1551
|
+
if (ch === "'") inSingle = false;
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
if (inDouble) {
|
|
1555
|
+
out += ch;
|
|
1556
|
+
if (ch === '"') inDouble = false;
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (ch === "-" && next === "-") {
|
|
1560
|
+
out += ch;
|
|
1561
|
+
inLineComment = true;
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
if (ch === "/" && next === "*") {
|
|
1565
|
+
out += ch + next;
|
|
1566
|
+
i++;
|
|
1567
|
+
inBlockComment = true;
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
if (ch === "'") {
|
|
1571
|
+
out += ch;
|
|
1572
|
+
inSingle = true;
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
if (ch === '"') {
|
|
1576
|
+
out += ch;
|
|
1577
|
+
inDouble = true;
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
if (ch === "?") {
|
|
1581
|
+
out += `$${++n}`;
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
out += ch;
|
|
1585
|
+
}
|
|
1586
|
+
return out;
|
|
1587
|
+
}
|
|
1588
|
+
run(sql, params) {
|
|
1589
|
+
return __async(this, null, function* () {
|
|
1590
|
+
if (params.length > 0) {
|
|
1591
|
+
return yield this.connection.query(_PgRunner.toPositional(sql), params);
|
|
1592
|
+
}
|
|
1593
|
+
return yield this.connection.query(sql);
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
_query(_0) {
|
|
1597
|
+
return __async(this, arguments, function* (sql, params = []) {
|
|
1598
|
+
const result = yield this.run(sql, params);
|
|
1599
|
+
return [result.rows, result];
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
_execute(_0) {
|
|
1603
|
+
return __async(this, arguments, function* (sql, params = []) {
|
|
1604
|
+
var _a;
|
|
1605
|
+
const result = yield this.run(sql, params);
|
|
1606
|
+
return [{ changes: (_a = result.rowCount) != null ? _a : 0, lastID: void 0 }, result];
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
_end() {
|
|
1610
|
+
return __async(this, null, function* () {
|
|
1611
|
+
if (this.connection && typeof this.connection.end === "function") {
|
|
1612
|
+
yield this.connection.end();
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
728
1617
|
|
|
729
1618
|
// framework/MigrationRunner.ts
|
|
730
|
-
function
|
|
1619
|
+
function toError2(error) {
|
|
731
1620
|
if (error instanceof Error) return error;
|
|
732
1621
|
return new Error(String(error));
|
|
733
1622
|
}
|
|
1623
|
+
function makeRunner(database, conn) {
|
|
1624
|
+
switch (database) {
|
|
1625
|
+
case "sql":
|
|
1626
|
+
return new SQLRunner(conn);
|
|
1627
|
+
case "sqlite":
|
|
1628
|
+
return new SQLiteRunner(conn);
|
|
1629
|
+
case "pg":
|
|
1630
|
+
return new PgRunner(conn);
|
|
1631
|
+
default:
|
|
1632
|
+
throw ConfigurationError.unknownDatabaseType(database);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
734
1635
|
function loadMigrationConfig(configFile) {
|
|
735
1636
|
const reader = new FileMigrationConfigReader(configFile);
|
|
736
1637
|
return reader.loadFile();
|
|
@@ -743,14 +1644,17 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
743
1644
|
return __async(this, null, function* () {
|
|
744
1645
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
745
1646
|
const config = configReader.loadFile();
|
|
1647
|
+
let factoryOwnsConnection = false;
|
|
746
1648
|
if (!conn) {
|
|
747
1649
|
conn = yield this.createConnection(config);
|
|
1650
|
+
factoryOwnsConnection = true;
|
|
748
1651
|
}
|
|
749
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1652
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
750
1653
|
});
|
|
751
1654
|
}
|
|
752
1655
|
static createConnection(config) {
|
|
753
1656
|
return __async(this, null, function* () {
|
|
1657
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
754
1658
|
let conn = null;
|
|
755
1659
|
switch (config.database) {
|
|
756
1660
|
case "sql":
|
|
@@ -761,23 +1665,42 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
761
1665
|
password: process.env.SQL_PASSWORD
|
|
762
1666
|
}, config.sql);
|
|
763
1667
|
try {
|
|
764
|
-
|
|
1668
|
+
const mysql = yield import("mysql2/promise");
|
|
1669
|
+
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
765
1670
|
return conn;
|
|
766
1671
|
} catch (error) {
|
|
767
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1672
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
768
1673
|
}
|
|
769
1674
|
case "sqlite":
|
|
770
1675
|
if (!config.sqlite) {
|
|
771
1676
|
throw ConfigurationError.missingDatabaseConfiguration("sqlite");
|
|
772
1677
|
}
|
|
773
1678
|
try {
|
|
774
|
-
|
|
1679
|
+
const sqlite = yield import("sqlite");
|
|
1680
|
+
const sqlite3 = yield import("sqlite3");
|
|
1681
|
+
conn = yield ((_d = (_c = sqlite.default) == null ? void 0 : _c.open) != null ? _d : sqlite.open)({
|
|
775
1682
|
filename: config.sqlite.database,
|
|
776
|
-
driver: sqlite3.Database
|
|
1683
|
+
driver: (_f = (_e = sqlite3.default) == null ? void 0 : _e.Database) != null ? _f : sqlite3.Database
|
|
777
1684
|
});
|
|
778
1685
|
return conn;
|
|
779
1686
|
} catch (error) {
|
|
780
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1687
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
1688
|
+
}
|
|
1689
|
+
case "pg":
|
|
1690
|
+
if (!config.pg && !process.env.DATABASE_URL) {
|
|
1691
|
+
throw ConfigurationError.missingDatabaseConfiguration("pg");
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
const pgcfg = (_g = config.pg) != null ? _g : {};
|
|
1695
|
+
const connectionString = (_h = pgcfg.connectionString) != null ? _h : process.env.DATABASE_URL;
|
|
1696
|
+
const settings2 = connectionString ? { connectionString, ssl: pgcfg.ssl } : __spreadProps(__spreadValues({}, pgcfg), { password: (_i = pgcfg.password) != null ? _i : process.env.PG_PASSWORD });
|
|
1697
|
+
const pg = yield import("pg");
|
|
1698
|
+
const Client = (_k = (_j = pg.default) == null ? void 0 : _j.Client) != null ? _k : pg.Client;
|
|
1699
|
+
conn = new Client(settings2);
|
|
1700
|
+
yield conn.connect();
|
|
1701
|
+
return conn;
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
781
1704
|
}
|
|
782
1705
|
default:
|
|
783
1706
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -791,7 +1714,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
791
1714
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
792
1715
|
});
|
|
793
1716
|
}
|
|
794
|
-
create(config, conn) {
|
|
1717
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
795
1718
|
return __async(this, null, function* () {
|
|
796
1719
|
let sqlrunner;
|
|
797
1720
|
let driverConnection = conn;
|
|
@@ -799,42 +1722,34 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
799
1722
|
sqlrunner = conn;
|
|
800
1723
|
driverConnection = null;
|
|
801
1724
|
} else {
|
|
802
|
-
|
|
803
|
-
case "sql":
|
|
804
|
-
sqlrunner = new SQLRunner(conn);
|
|
805
|
-
break;
|
|
806
|
-
case "sqlite":
|
|
807
|
-
sqlrunner = new SQLiteRunner(conn);
|
|
808
|
-
break;
|
|
809
|
-
default:
|
|
810
|
-
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
811
|
-
}
|
|
1725
|
+
sqlrunner = makeRunner(config.database, conn);
|
|
812
1726
|
}
|
|
813
1727
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
814
1728
|
const read_strategy = this.getReadStategy(config);
|
|
815
1729
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
816
|
-
|
|
817
|
-
|
|
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;
|
|
818
1743
|
});
|
|
819
1744
|
}
|
|
820
1745
|
createEmpty(config) {
|
|
821
1746
|
return __async(this, null, function* () {
|
|
822
1747
|
const conn = null;
|
|
823
|
-
|
|
824
|
-
switch (config.database) {
|
|
825
|
-
case "sql":
|
|
826
|
-
sqlrunner = new SQLRunner(conn);
|
|
827
|
-
break;
|
|
828
|
-
case "sqlite":
|
|
829
|
-
sqlrunner = new SQLiteRunner(conn);
|
|
830
|
-
break;
|
|
831
|
-
default:
|
|
832
|
-
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
833
|
-
}
|
|
1748
|
+
const sqlrunner = makeRunner(config.database, conn);
|
|
834
1749
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
835
1750
|
const read_strategy = this.getReadStategy(config);
|
|
836
1751
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
837
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1752
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
838
1753
|
});
|
|
839
1754
|
}
|
|
840
1755
|
getReadStategy(config) {
|
|
@@ -843,39 +1758,85 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
843
1758
|
return MySqlDialectParser;
|
|
844
1759
|
case "sqlite":
|
|
845
1760
|
return SqliteDialectParser;
|
|
1761
|
+
case "pg":
|
|
1762
|
+
return PgDialectParser;
|
|
846
1763
|
default:
|
|
847
1764
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
848
1765
|
}
|
|
849
1766
|
}
|
|
850
1767
|
};
|
|
851
1768
|
var MySQLMigrationRunner = class {
|
|
852
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1769
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
853
1770
|
this.config = config;
|
|
854
1771
|
this.directory = directory;
|
|
855
1772
|
this.setupRunner = setupRunner;
|
|
856
1773
|
this.sqlrunner = sqlrunner;
|
|
857
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 = [];
|
|
858
1784
|
}
|
|
859
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() {
|
|
860
1800
|
return __async(this, null, function* () {
|
|
861
1801
|
try {
|
|
862
1802
|
yield this.setupRunner.setup();
|
|
863
1803
|
} catch (error) {
|
|
864
|
-
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));
|
|
865
1805
|
}
|
|
1806
|
+
const patchRunner = new PatchRunner(this.sqlrunner, this.config);
|
|
1807
|
+
this.lastPatchResults = yield patchRunner.applyPending();
|
|
866
1808
|
});
|
|
867
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;
|
|
1818
|
+
});
|
|
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
|
+
}
|
|
868
1828
|
terminate() {
|
|
869
1829
|
return __async(this, null, function* () {
|
|
870
1830
|
try {
|
|
871
1831
|
yield this.setupRunner.teardown();
|
|
872
1832
|
} catch (error) {
|
|
873
|
-
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));
|
|
874
1834
|
}
|
|
875
1835
|
});
|
|
876
1836
|
}
|
|
877
1837
|
getMigrationsHistory() {
|
|
878
1838
|
return __async(this, null, function* () {
|
|
1839
|
+
yield this.setup();
|
|
879
1840
|
try {
|
|
880
1841
|
const results = yield this.sqlrunner.query(`
|
|
881
1842
|
select *
|
|
@@ -883,16 +1844,17 @@ var MySQLMigrationRunner = class {
|
|
|
883
1844
|
`);
|
|
884
1845
|
return results[0];
|
|
885
1846
|
} catch (error) {
|
|
886
|
-
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));
|
|
887
1848
|
}
|
|
888
1849
|
});
|
|
889
1850
|
}
|
|
890
1851
|
getMigrations() {
|
|
891
1852
|
return __async(this, null, function* () {
|
|
1853
|
+
yield this.setup();
|
|
892
1854
|
try {
|
|
893
1855
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
894
1856
|
} catch (error) {
|
|
895
|
-
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));
|
|
896
1858
|
}
|
|
897
1859
|
});
|
|
898
1860
|
}
|
|
@@ -905,7 +1867,7 @@ var MySQLMigrationRunner = class {
|
|
|
905
1867
|
if (error instanceof MigrationExecutionError) {
|
|
906
1868
|
throw error;
|
|
907
1869
|
}
|
|
908
|
-
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));
|
|
909
1871
|
}
|
|
910
1872
|
});
|
|
911
1873
|
}
|
|
@@ -918,12 +1880,13 @@ var MySQLMigrationRunner = class {
|
|
|
918
1880
|
if (error instanceof MigrationExecutionError) {
|
|
919
1881
|
throw error;
|
|
920
1882
|
}
|
|
921
|
-
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));
|
|
922
1884
|
}
|
|
923
1885
|
});
|
|
924
1886
|
}
|
|
925
1887
|
migrate(migrationNodes, forward) {
|
|
926
1888
|
return __async(this, null, function* () {
|
|
1889
|
+
yield this.setup();
|
|
927
1890
|
for (let node of migrationNodes) {
|
|
928
1891
|
try {
|
|
929
1892
|
if (forward) {
|
|
@@ -936,7 +1899,7 @@ var MySQLMigrationRunner = class {
|
|
|
936
1899
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
937
1900
|
node.name || String(node),
|
|
938
1901
|
forward ? node.up_sql() : node.down_sql(),
|
|
939
|
-
|
|
1902
|
+
toError2(error)
|
|
940
1903
|
);
|
|
941
1904
|
}
|
|
942
1905
|
}
|
|
@@ -944,6 +1907,7 @@ var MySQLMigrationRunner = class {
|
|
|
944
1907
|
}
|
|
945
1908
|
reset() {
|
|
946
1909
|
return __async(this, null, function* () {
|
|
1910
|
+
yield this.setup();
|
|
947
1911
|
try {
|
|
948
1912
|
let migrations = yield this.getMigrations();
|
|
949
1913
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -955,7 +1919,7 @@ var MySQLMigrationRunner = class {
|
|
|
955
1919
|
if (error instanceof MigrationExecutionError) {
|
|
956
1920
|
throw error;
|
|
957
1921
|
}
|
|
958
|
-
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));
|
|
959
1923
|
}
|
|
960
1924
|
});
|
|
961
1925
|
}
|
|
@@ -964,7 +1928,7 @@ var MySQLMigrationRunner = class {
|
|
|
964
1928
|
const creator = new MigrationCreator(this.config);
|
|
965
1929
|
creator.create(name);
|
|
966
1930
|
} catch (error) {
|
|
967
|
-
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));
|
|
968
1932
|
}
|
|
969
1933
|
}
|
|
970
1934
|
close() {
|
|
@@ -973,7 +1937,7 @@ var MySQLMigrationRunner = class {
|
|
|
973
1937
|
try {
|
|
974
1938
|
yield this.sqlrunner.end();
|
|
975
1939
|
} catch (error) {
|
|
976
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
1940
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
977
1941
|
}
|
|
978
1942
|
}
|
|
979
1943
|
});
|
|
@@ -987,7 +1951,7 @@ var MySQLMigrationRunner = class {
|
|
|
987
1951
|
return __async(this, null, function* () {
|
|
988
1952
|
try {
|
|
989
1953
|
console.log(`Checking for ${config_file}`);
|
|
990
|
-
const config_exist =
|
|
1954
|
+
const config_exist = fs6.existsSync(config_file);
|
|
991
1955
|
if (!config_exist) {
|
|
992
1956
|
console.log(`Creating ${config_file}`);
|
|
993
1957
|
const default_config = {
|
|
@@ -1001,11 +1965,11 @@ var MySQLMigrationRunner = class {
|
|
|
1001
1965
|
"password": ""
|
|
1002
1966
|
}
|
|
1003
1967
|
};
|
|
1004
|
-
|
|
1968
|
+
fs6.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
|
|
1005
1969
|
console.log(`Created ${config_file}`);
|
|
1006
1970
|
}
|
|
1007
1971
|
} catch (error) {
|
|
1008
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
1972
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1009
1973
|
}
|
|
1010
1974
|
});
|
|
1011
1975
|
}
|
|
@@ -1016,7 +1980,7 @@ var FileMigrationConfigReader = class {
|
|
|
1016
1980
|
}
|
|
1017
1981
|
loadFile() {
|
|
1018
1982
|
try {
|
|
1019
|
-
const fileContent =
|
|
1983
|
+
const fileContent = fs6.readFileSync(this.configFile);
|
|
1020
1984
|
const config = JSON.parse(fileContent.toString());
|
|
1021
1985
|
if (!config.migration_folder) {
|
|
1022
1986
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1029,6 +1993,8 @@ var FileMigrationConfigReader = class {
|
|
|
1029
1993
|
config.database = "sql";
|
|
1030
1994
|
} else if (config.sqlite) {
|
|
1031
1995
|
config.database = "sqlite";
|
|
1996
|
+
} else if (config.pg) {
|
|
1997
|
+
config.database = "pg";
|
|
1032
1998
|
} else {
|
|
1033
1999
|
throw ConfigurationError.missingRequiredProperty("database");
|
|
1034
2000
|
}
|
|
@@ -1038,7 +2004,7 @@ var FileMigrationConfigReader = class {
|
|
|
1038
2004
|
if (error instanceof ConfigurationError) {
|
|
1039
2005
|
throw error;
|
|
1040
2006
|
}
|
|
1041
|
-
const err =
|
|
2007
|
+
const err = toError2(error);
|
|
1042
2008
|
if (err.message.includes("ENOENT")) {
|
|
1043
2009
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1044
2010
|
}
|
|
@@ -1055,16 +2021,16 @@ var MigrationCreator = class {
|
|
|
1055
2021
|
throw new CLIError("Migration name is required");
|
|
1056
2022
|
}
|
|
1057
2023
|
try {
|
|
1058
|
-
if (!
|
|
1059
|
-
|
|
2024
|
+
if (!fs6.existsSync(this.config.migration_folder)) {
|
|
2025
|
+
fs6.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1060
2026
|
}
|
|
1061
2027
|
const now_timestamp = Date.now();
|
|
1062
2028
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1063
2029
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1064
|
-
|
|
2030
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1065
2031
|
-- Write your up migration here
|
|
1066
2032
|
`.trim());
|
|
1067
|
-
|
|
2033
|
+
fs6.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1068
2034
|
-- Write your down migration here
|
|
1069
2035
|
`.trim());
|
|
1070
2036
|
console.log(`Created migration files:`);
|
|
@@ -1074,16 +2040,16 @@ var MigrationCreator = class {
|
|
|
1074
2040
|
if (error instanceof CLIError) {
|
|
1075
2041
|
throw error;
|
|
1076
2042
|
}
|
|
1077
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2043
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1078
2044
|
}
|
|
1079
2045
|
}
|
|
1080
2046
|
};
|
|
1081
2047
|
|
|
1082
2048
|
// framework/SeedRunner.ts
|
|
1083
|
-
import
|
|
1084
|
-
import
|
|
2049
|
+
import fs7 from "fs";
|
|
2050
|
+
import path5 from "path";
|
|
1085
2051
|
import { pathToFileURL } from "url";
|
|
1086
|
-
import
|
|
2052
|
+
import Ajv2 from "ajv";
|
|
1087
2053
|
import addFormats from "ajv-formats";
|
|
1088
2054
|
import { tsImport } from "tsx/esm/api";
|
|
1089
2055
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1092,10 +2058,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1092
2058
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1093
2059
|
}
|
|
1094
2060
|
function walkForFile(rootDir, fileName) {
|
|
1095
|
-
if (!
|
|
1096
|
-
const entries =
|
|
2061
|
+
if (!fs7.existsSync(rootDir)) return null;
|
|
2062
|
+
const entries = fs7.readdirSync(rootDir, { withFileTypes: true });
|
|
1097
2063
|
for (const entry of entries) {
|
|
1098
|
-
const full =
|
|
2064
|
+
const full = path5.join(rootDir, entry.name);
|
|
1099
2065
|
if (entry.isDirectory()) {
|
|
1100
2066
|
const found = walkForFile(full, fileName);
|
|
1101
2067
|
if (found) return found;
|
|
@@ -1189,39 +2155,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1189
2155
|
}
|
|
1190
2156
|
function loadJson(filePath) {
|
|
1191
2157
|
return __async(this, null, function* () {
|
|
1192
|
-
const content = yield
|
|
2158
|
+
const content = yield fs7.promises.readFile(filePath, "utf8");
|
|
1193
2159
|
return JSON.parse(content);
|
|
1194
2160
|
});
|
|
1195
2161
|
}
|
|
1196
2162
|
function createValidator() {
|
|
1197
|
-
const
|
|
1198
|
-
addFormats(
|
|
1199
|
-
return
|
|
2163
|
+
const ajv2 = new Ajv2({ allErrors: true, strict: false });
|
|
2164
|
+
addFormats(ajv2);
|
|
2165
|
+
return ajv2;
|
|
1200
2166
|
}
|
|
1201
2167
|
function validateData(schemaPath, data, validate, log) {
|
|
1202
2168
|
return __async(this, null, function* () {
|
|
1203
2169
|
if (!validate || !schemaPath) return;
|
|
1204
|
-
const content = yield
|
|
2170
|
+
const content = yield fs7.promises.readFile(schemaPath, "utf8");
|
|
1205
2171
|
const schema = JSON.parse(content);
|
|
1206
|
-
const
|
|
1207
|
-
const validateFn =
|
|
2172
|
+
const ajv2 = createValidator();
|
|
2173
|
+
const validateFn = ajv2.compile(schema);
|
|
1208
2174
|
const ok = validateFn(data);
|
|
1209
2175
|
if (!ok) {
|
|
1210
2176
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1211
|
-
throw new Error(`Seed data validation failed: ${
|
|
2177
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1212
2178
|
}
|
|
1213
2179
|
});
|
|
1214
2180
|
}
|
|
1215
2181
|
function runSqlSeed(runner, resolved, direction) {
|
|
1216
2182
|
return __async(this, null, function* () {
|
|
1217
2183
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1218
|
-
const sql = yield
|
|
2184
|
+
const sql = yield fs7.promises.readFile(sqlPath, "utf8");
|
|
1219
2185
|
yield runner.query(sql);
|
|
1220
2186
|
});
|
|
1221
2187
|
}
|
|
1222
2188
|
function loadSeedModule(modulePath) {
|
|
1223
2189
|
return __async(this, null, function* () {
|
|
1224
|
-
const resolved =
|
|
2190
|
+
const resolved = path5.resolve(modulePath);
|
|
1225
2191
|
if (resolved.endsWith(".ts")) {
|
|
1226
2192
|
const fileUrl = pathToFileURL(resolved).href;
|
|
1227
2193
|
return tsImport(fileUrl, fileUrl);
|
|
@@ -1341,10 +2307,23 @@ function createSeedFactory(options) {
|
|
|
1341
2307
|
};
|
|
1342
2308
|
}
|
|
1343
2309
|
export {
|
|
2310
|
+
MigrationError,
|
|
1344
2311
|
MySQLMigrationRunner as MigrationRunner,
|
|
1345
2312
|
MigrationRunnerFactory,
|
|
2313
|
+
PatchConflictError,
|
|
2314
|
+
PatchCreator,
|
|
2315
|
+
PatchError,
|
|
2316
|
+
PatchExecutionError,
|
|
2317
|
+
PatchIntegrityError,
|
|
2318
|
+
PatchRunner,
|
|
2319
|
+
PatchValidationError,
|
|
2320
|
+
PgRunner,
|
|
2321
|
+
SQLRunner,
|
|
2322
|
+
SQLiteRunner,
|
|
1346
2323
|
createSeedFactory,
|
|
1347
2324
|
loadMigrationConfig,
|
|
2325
|
+
resolvePatchFolder,
|
|
2326
|
+
resolvePatchTable,
|
|
1348
2327
|
runSeedsWithRunner
|
|
1349
2328
|
};
|
|
1350
2329
|
//# sourceMappingURL=index.mjs.map
|