updebe 0.1.1 → 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/dist/commands/init.js +29 -0
- package/dist/commands/upgradeHead.js +21 -0
- package/dist/config/database.js +74 -0
- package/dist/db/connection.js +54 -0
- package/dist/db/migrationState.js +48 -0
- package/dist/migrations/files.js +95 -0
- package/dist/migrations/runner.js +124 -0
- package/dist/updebe.js +17 -1
- package/package.json +8 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runInit = runInit;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
function getCurrentDateStamp() {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const year = now.getFullYear();
|
|
12
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
13
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
14
|
+
return `${year}${month}${day}`;
|
|
15
|
+
}
|
|
16
|
+
function runInit(baseDir = process.cwd()) {
|
|
17
|
+
const updebeDir = node_path_1.default.join(baseDir, "updebe");
|
|
18
|
+
const migrationsDir = node_path_1.default.join(updebeDir, "Migrations");
|
|
19
|
+
const dateStamp = getCurrentDateStamp();
|
|
20
|
+
const migrationFilePath = node_path_1.default.join(migrationsDir, `${dateStamp}_init.ts`);
|
|
21
|
+
const configFilePath = node_path_1.default.join(updebeDir, "config.udb");
|
|
22
|
+
node_fs_1.default.mkdirSync(migrationsDir, { recursive: true });
|
|
23
|
+
if (!node_fs_1.default.existsSync(configFilePath)) {
|
|
24
|
+
node_fs_1.default.writeFileSync(configFilePath, "", "utf8");
|
|
25
|
+
}
|
|
26
|
+
if (!node_fs_1.default.existsSync(migrationFilePath)) {
|
|
27
|
+
node_fs_1.default.writeFileSync(migrationFilePath, "export const revision = {\n _id: \"20260827_init\",\n revised_id: \"\"\n};\n\nexport async function upgrade(): Promise<void> {\n // Write upgrade SQL here\n}\n\nexport async function downgrade(): Promise<void> {\n // Write downgrade SQL here\n}\n", "utf8");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runUpgradeHead = runUpgradeHead;
|
|
4
|
+
const database_1 = require("../config/database");
|
|
5
|
+
const connection_1 = require("../db/connection");
|
|
6
|
+
const files_1 = require("../migrations/files");
|
|
7
|
+
const runner_1 = require("../migrations/runner");
|
|
8
|
+
async function runUpgradeHead() {
|
|
9
|
+
const migrations = (0, files_1.loadMigrationDefinitions)();
|
|
10
|
+
const dbConfig = (0, database_1.resolveDatabaseConfig)();
|
|
11
|
+
await (0, connection_1.withDatabaseClient)(dbConfig, async (client) => {
|
|
12
|
+
try {
|
|
13
|
+
await (0, runner_1.runUpgradeToHead)(client, dbConfig, migrations);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error((0, runner_1.formatMigrationError)(error));
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
console.log("Hey! I'm working");
|
|
21
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveDatabaseConfig = resolveDatabaseConfig;
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
dotenv_1.default.config();
|
|
9
|
+
const ANSI_GREEN = "\x1b[32m";
|
|
10
|
+
const ANSI_RED = "\x1b[31m";
|
|
11
|
+
const ANSI_RESET = "\x1b[0m";
|
|
12
|
+
function readEnv(name) {
|
|
13
|
+
const value = process.env[name];
|
|
14
|
+
if (!value) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
19
|
+
}
|
|
20
|
+
function parsePort(rawPort) {
|
|
21
|
+
const port = Number(rawPort);
|
|
22
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
23
|
+
throw new Error("Invalid environment variable: PGDB_PORT must be an integer between 1 and 65535");
|
|
24
|
+
}
|
|
25
|
+
return port;
|
|
26
|
+
}
|
|
27
|
+
function resolveDatabaseConfig() {
|
|
28
|
+
try {
|
|
29
|
+
const schema = readEnv("PGDB_SCHEMA") ?? "public";
|
|
30
|
+
const databaseUrl = readEnv("DATABASE_URL");
|
|
31
|
+
if (databaseUrl) {
|
|
32
|
+
try {
|
|
33
|
+
new URL(databaseUrl);
|
|
34
|
+
console.log(`${ANSI_GREEN}Database config resolved from DATABASE_URL (schema: ${schema})${ANSI_RESET}`);
|
|
35
|
+
return { databaseUrl, schema };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error("Invalid environment variable: DATABASE_URL must be a valid URL");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const user = readEnv("PGDB_USER");
|
|
42
|
+
const host = readEnv("PGDB_HOST");
|
|
43
|
+
const database = readEnv("PGDB_DATABASE");
|
|
44
|
+
const password = readEnv("PGDB_PASSWORD");
|
|
45
|
+
const rawPort = readEnv("PGDB_PORT");
|
|
46
|
+
const missingVars = [
|
|
47
|
+
["PGDB_USER", user],
|
|
48
|
+
["PGDB_HOST", host],
|
|
49
|
+
["PGDB_DATABASE", database],
|
|
50
|
+
["PGDB_PASSWORD", password],
|
|
51
|
+
["PGDB_PORT", rawPort]
|
|
52
|
+
]
|
|
53
|
+
.filter(([, value]) => !value)
|
|
54
|
+
.map(([name]) => name);
|
|
55
|
+
if (missingVars.length > 0) {
|
|
56
|
+
throw new Error(`Missing required environment variables: ${missingVars.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
const port = parsePort(rawPort);
|
|
59
|
+
const encodedUser = encodeURIComponent(user);
|
|
60
|
+
const encodedPassword = encodeURIComponent(password);
|
|
61
|
+
const encodedDatabase = encodeURIComponent(database);
|
|
62
|
+
const builtDatabaseUrl = `postgresql://${encodedUser}:${encodedPassword}@${host}:${port}/${encodedDatabase}`;
|
|
63
|
+
console.log(`${ANSI_GREEN}Database config resolved from PGDB_* variables (schema: ${schema})${ANSI_RESET}`);
|
|
64
|
+
return {
|
|
65
|
+
databaseUrl: builtDatabaseUrl,
|
|
66
|
+
schema
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
71
|
+
console.error(`${ANSI_RED}Database config error: ${message}${ANSI_RESET}`);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.testDatabaseConnection = testDatabaseConnection;
|
|
4
|
+
exports.withDatabaseClient = withDatabaseClient;
|
|
5
|
+
const pg_1 = require("pg");
|
|
6
|
+
const ANSI_GREEN = "\x1b[32m";
|
|
7
|
+
const ANSI_RED = "\x1b[31m";
|
|
8
|
+
const ANSI_RESET = "\x1b[0m";
|
|
9
|
+
function formatSafeError(error) {
|
|
10
|
+
if (!(error instanceof Error)) {
|
|
11
|
+
return "Unknown database error";
|
|
12
|
+
}
|
|
13
|
+
// Avoid leaking credentials if a driver error includes connection details.
|
|
14
|
+
return error.message.replace(/:\/\/[^@\s]+@/g, "://***:***@");
|
|
15
|
+
}
|
|
16
|
+
async function testDatabaseConnection(config) {
|
|
17
|
+
const client = new pg_1.Client({ connectionString: config.databaseUrl });
|
|
18
|
+
try {
|
|
19
|
+
await client.connect();
|
|
20
|
+
await client.query("SELECT 1");
|
|
21
|
+
console.log(`${ANSI_GREEN}Database connected successfully (schema: ${config.schema})${ANSI_RESET}`);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const message = formatSafeError(error);
|
|
25
|
+
console.error(`${ANSI_RED}Database connection error: ${message}${ANSI_RESET}`);
|
|
26
|
+
throw new Error("Database connection failed");
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await client.end().catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function withDatabaseClient(config, action) {
|
|
33
|
+
const client = new pg_1.Client({ connectionString: config.databaseUrl });
|
|
34
|
+
let connected = false;
|
|
35
|
+
try {
|
|
36
|
+
await client.connect();
|
|
37
|
+
connected = true;
|
|
38
|
+
console.log(`${ANSI_GREEN}Database connected successfully (schema: ${config.schema})${ANSI_RESET}`);
|
|
39
|
+
return await action(client);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (connected) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
const message = formatSafeError(error);
|
|
46
|
+
console.error(`${ANSI_RED}Database connection error: ${message}${ANSI_RESET}`);
|
|
47
|
+
throw new Error("Database connection failed");
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (connected) {
|
|
51
|
+
await client.end().catch(() => undefined);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureMigrationState = ensureMigrationState;
|
|
4
|
+
exports.getCurrentRevisionId = getCurrentRevisionId;
|
|
5
|
+
exports.setCurrentRevisionId = setCurrentRevisionId;
|
|
6
|
+
const ANSI_GREEN = "\x1b[32m";
|
|
7
|
+
const ANSI_RESET = "\x1b[0m";
|
|
8
|
+
const STATE_KEY = "current";
|
|
9
|
+
const TABLE_NAME = "updebe_migrations";
|
|
10
|
+
function quoteIdentifier(identifier) {
|
|
11
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
12
|
+
throw new Error(`Invalid schema name: ${identifier}`);
|
|
13
|
+
}
|
|
14
|
+
return `"${identifier}"`;
|
|
15
|
+
}
|
|
16
|
+
function getStateTableReference(schema) {
|
|
17
|
+
return `${quoteIdentifier(schema)}."${TABLE_NAME}"`;
|
|
18
|
+
}
|
|
19
|
+
async function ensureMigrationState(client, schema) {
|
|
20
|
+
const quotedSchema = quoteIdentifier(schema);
|
|
21
|
+
const tableRef = getStateTableReference(schema);
|
|
22
|
+
await client.query(`CREATE SCHEMA IF NOT EXISTS ${quotedSchema}`);
|
|
23
|
+
await client.query(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS ${tableRef} (
|
|
25
|
+
singleton_key TEXT PRIMARY KEY,
|
|
26
|
+
revision_id TEXT NOT NULL,
|
|
27
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
28
|
+
)
|
|
29
|
+
`);
|
|
30
|
+
console.log(`${ANSI_GREEN}Migration state ready at ${schema}.${TABLE_NAME}${ANSI_RESET}`);
|
|
31
|
+
}
|
|
32
|
+
async function getCurrentRevisionId(client, schema) {
|
|
33
|
+
const tableRef = getStateTableReference(schema);
|
|
34
|
+
const result = await client.query(`SELECT revision_id FROM ${tableRef} WHERE singleton_key = $1 LIMIT 1`, [STATE_KEY]);
|
|
35
|
+
if (result.rowCount === 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return result.rows[0].revision_id;
|
|
39
|
+
}
|
|
40
|
+
async function setCurrentRevisionId(client, schema, revisionId) {
|
|
41
|
+
const tableRef = getStateTableReference(schema);
|
|
42
|
+
await client.query(`
|
|
43
|
+
INSERT INTO ${tableRef} (singleton_key, revision_id, updated_at)
|
|
44
|
+
VALUES ($1, $2, NOW())
|
|
45
|
+
ON CONFLICT (singleton_key)
|
|
46
|
+
DO UPDATE SET revision_id = EXCLUDED.revision_id, updated_at = NOW()
|
|
47
|
+
`, [STATE_KEY, revisionId]);
|
|
48
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadMigrationDefinitions = loadMigrationDefinitions;
|
|
7
|
+
exports.loadMigrationModule = loadMigrationModule;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_module_1 = require("node:module");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_url_1 = require("node:url");
|
|
12
|
+
let tsNodeRegistered = false;
|
|
13
|
+
function extractRevision(content, filePath) {
|
|
14
|
+
const revisionBlockMatch = content.match(/export\s+const\s+revision\s*=\s*\{([\s\S]*?)\}/m);
|
|
15
|
+
if (!revisionBlockMatch) {
|
|
16
|
+
throw new Error(`${filePath}: missing required key revision`);
|
|
17
|
+
}
|
|
18
|
+
const revisionBlock = revisionBlockMatch[1];
|
|
19
|
+
const idMatch = revisionBlock.match(/\b_id\s*:\s*(["'`])(.*?)\1/m);
|
|
20
|
+
const revisedIdMatch = revisionBlock.match(/\brevised_id\s*:\s*(["'`])(.*?)\1/m);
|
|
21
|
+
if (!idMatch) {
|
|
22
|
+
throw new Error(`${filePath}: missing required key revision._id`);
|
|
23
|
+
}
|
|
24
|
+
if (!revisedIdMatch) {
|
|
25
|
+
throw new Error(`${filePath}: missing required key revision.revised_id`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
_id: idMatch[2],
|
|
29
|
+
revised_id: revisedIdMatch[2]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function validateMigrationFunctions(content, filePath) {
|
|
33
|
+
if (!/export\s+(?:async\s+)?function\s+upgrade\s*\(/m.test(content)) {
|
|
34
|
+
throw new Error(`${filePath}: missing required function upgrade`);
|
|
35
|
+
}
|
|
36
|
+
if (!/export\s+(?:async\s+)?function\s+downgrade\s*\(/m.test(content)) {
|
|
37
|
+
throw new Error(`${filePath}: missing required function downgrade`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function loadMigrationDefinitions(baseDir = process.cwd()) {
|
|
41
|
+
const migrationsDir = node_path_1.default.join(baseDir, "updebe", "Migrations");
|
|
42
|
+
if (!node_fs_1.default.existsSync(migrationsDir)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const fileNames = node_fs_1.default.readdirSync(migrationsDir)
|
|
46
|
+
.filter((fileName) => fileName.endsWith(".ts"))
|
|
47
|
+
.sort();
|
|
48
|
+
return fileNames.map((fileName) => {
|
|
49
|
+
const relativePath = `updebe/Migrations/${fileName}`;
|
|
50
|
+
const fullPath = node_path_1.default.join(migrationsDir, fileName);
|
|
51
|
+
let content;
|
|
52
|
+
try {
|
|
53
|
+
content = node_fs_1.default.readFileSync(fullPath, "utf8");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error(`${relativePath}: unable to read file`);
|
|
57
|
+
}
|
|
58
|
+
const revision = extractRevision(content, relativePath);
|
|
59
|
+
validateMigrationFunctions(content, relativePath);
|
|
60
|
+
return {
|
|
61
|
+
fullPath,
|
|
62
|
+
filePath: relativePath,
|
|
63
|
+
revision
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function ensureTsNodeRuntime() {
|
|
68
|
+
if (tsNodeRegistered) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const runtimeRequire = (0, node_module_1.createRequire)((0, node_url_1.pathToFileURL)(__filename).href);
|
|
72
|
+
runtimeRequire("ts-node/register/transpile-only");
|
|
73
|
+
tsNodeRegistered = true;
|
|
74
|
+
}
|
|
75
|
+
function loadCompiledModule(fullPath) {
|
|
76
|
+
ensureTsNodeRuntime();
|
|
77
|
+
const localRequire = (0, node_module_1.createRequire)((0, node_url_1.pathToFileURL)(fullPath).href);
|
|
78
|
+
const resolvedPath = localRequire.resolve(fullPath);
|
|
79
|
+
delete localRequire.cache[resolvedPath];
|
|
80
|
+
return localRequire(resolvedPath);
|
|
81
|
+
}
|
|
82
|
+
function loadMigrationModule(definition) {
|
|
83
|
+
const rawModule = loadCompiledModule(definition.fullPath);
|
|
84
|
+
if (typeof rawModule.upgrade !== "function") {
|
|
85
|
+
throw new Error(`${definition.filePath}: export upgrade must be a function`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof rawModule.downgrade !== "function") {
|
|
88
|
+
throw new Error(`${definition.filePath}: export downgrade must be a function`);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...definition,
|
|
92
|
+
upgrade: rawModule.upgrade,
|
|
93
|
+
downgrade: rawModule.downgrade
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runUpgradeToHead = runUpgradeToHead;
|
|
7
|
+
exports.formatMigrationError = formatMigrationError;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const migrationState_1 = require("../db/migrationState");
|
|
11
|
+
const files_1 = require("./files");
|
|
12
|
+
const ANSI_GREEN = "\x1b[32m";
|
|
13
|
+
const ANSI_YELLOW = "\x1b[33m";
|
|
14
|
+
const ANSI_RED = "\x1b[31m";
|
|
15
|
+
const ANSI_RESET = "\x1b[0m";
|
|
16
|
+
const INIT_DB_FILE = node_path_1.default.join("updebe", "_init_db.sql");
|
|
17
|
+
function buildPendingMigrations(migrations, currentRevisionId) {
|
|
18
|
+
const byPreviousRevision = new Map();
|
|
19
|
+
for (const migration of migrations) {
|
|
20
|
+
const key = migration.revision.revised_id;
|
|
21
|
+
const list = byPreviousRevision.get(key) ?? [];
|
|
22
|
+
list.push(migration);
|
|
23
|
+
byPreviousRevision.set(key, list);
|
|
24
|
+
}
|
|
25
|
+
for (const [key, list] of byPreviousRevision.entries()) {
|
|
26
|
+
if (list.length > 1) {
|
|
27
|
+
const files = list.map((item) => item.filePath).join(", ");
|
|
28
|
+
throw new Error(`Branching migrations detected after '${key}': ${files}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const pending = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
let pointer = currentRevisionId ?? "";
|
|
34
|
+
while (true) {
|
|
35
|
+
const next = byPreviousRevision.get(pointer)?.[0];
|
|
36
|
+
if (!next) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (seen.has(next.revision._id)) {
|
|
40
|
+
throw new Error(`Cycle detected at revision '${next.revision._id}'`);
|
|
41
|
+
}
|
|
42
|
+
pending.push(next);
|
|
43
|
+
seen.add(next.revision._id);
|
|
44
|
+
pointer = next.revision._id;
|
|
45
|
+
}
|
|
46
|
+
if (currentRevisionId && pending.length === 0) {
|
|
47
|
+
const isKnownRevision = migrations.some((migration) => migration.revision._id === currentRevisionId);
|
|
48
|
+
const canContinueFromCurrent = migrations.some((migration) => migration.revision.revised_id === currentRevisionId);
|
|
49
|
+
if (!isKnownRevision && !canContinueFromCurrent) {
|
|
50
|
+
throw new Error(`Current revision '${currentRevisionId}' is not present in migration chain`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return pending;
|
|
54
|
+
}
|
|
55
|
+
async function applyMigration(client, schema, migration) {
|
|
56
|
+
const loaded = (0, files_1.loadMigrationModule)(migration);
|
|
57
|
+
await client.query("BEGIN");
|
|
58
|
+
try {
|
|
59
|
+
await loaded.upgrade({
|
|
60
|
+
query: async (sql, params) => client.query(sql, params)
|
|
61
|
+
});
|
|
62
|
+
await (0, migrationState_1.setCurrentRevisionId)(client, schema, loaded.revision._id);
|
|
63
|
+
await client.query("COMMIT");
|
|
64
|
+
console.log(`${ANSI_GREEN}Applied migration ${loaded.revision._id}${ANSI_RESET}`);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await client.query("ROLLBACK");
|
|
68
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
69
|
+
throw new Error(`Failed migration ${loaded.filePath}: ${message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function sanitizeInitSql(rawSql) {
|
|
73
|
+
return rawSql
|
|
74
|
+
.split(/\r?\n/)
|
|
75
|
+
.filter((line) => !line.trimStart().startsWith("\\"))
|
|
76
|
+
.join("\n");
|
|
77
|
+
}
|
|
78
|
+
async function applyInitialSchemaIfPresent(client, baseDir = process.cwd()) {
|
|
79
|
+
const initFilePath = node_path_1.default.join(baseDir, INIT_DB_FILE);
|
|
80
|
+
if (!node_fs_1.default.existsSync(initFilePath)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const rawSql = node_fs_1.default.readFileSync(initFilePath, "utf8");
|
|
84
|
+
const sanitizedSql = sanitizeInitSql(rawSql).trim();
|
|
85
|
+
if (!sanitizedSql) {
|
|
86
|
+
throw new Error(`${INIT_DB_FILE}: file is empty or only contains unsupported psql commands`);
|
|
87
|
+
}
|
|
88
|
+
await client.query("BEGIN");
|
|
89
|
+
try {
|
|
90
|
+
await client.query(sanitizedSql);
|
|
91
|
+
await client.query("COMMIT");
|
|
92
|
+
console.log(`${ANSI_GREEN}Initial schema loaded from ${INIT_DB_FILE}${ANSI_RESET}`);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
await client.query("ROLLBACK");
|
|
96
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
97
|
+
throw new Error(`Failed to apply ${INIT_DB_FILE}: ${message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function runUpgradeToHead(client, config, migrations) {
|
|
101
|
+
await (0, migrationState_1.ensureMigrationState)(client, config.schema);
|
|
102
|
+
let currentRevisionId = await (0, migrationState_1.getCurrentRevisionId)(client, config.schema);
|
|
103
|
+
if (currentRevisionId === null) {
|
|
104
|
+
await applyInitialSchemaIfPresent(client);
|
|
105
|
+
currentRevisionId = await (0, migrationState_1.getCurrentRevisionId)(client, config.schema);
|
|
106
|
+
}
|
|
107
|
+
const pending = buildPendingMigrations(migrations, currentRevisionId);
|
|
108
|
+
if (pending.length === 0) {
|
|
109
|
+
console.log(`${ANSI_YELLOW}No pending migrations. Already at head.${ANSI_RESET}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(`${ANSI_YELLOW}Applying ${pending.length} migration(s)...${ANSI_RESET}`);
|
|
113
|
+
for (const migration of pending) {
|
|
114
|
+
await applyMigration(client, config.schema, migration);
|
|
115
|
+
}
|
|
116
|
+
const nextHead = pending[pending.length - 1].revision._id;
|
|
117
|
+
console.log(`${ANSI_GREEN}Upgrade completed. Current revision: ${nextHead}${ANSI_RESET}`);
|
|
118
|
+
}
|
|
119
|
+
function formatMigrationError(error) {
|
|
120
|
+
if (error instanceof Error) {
|
|
121
|
+
return `${ANSI_RED}${error.message}${ANSI_RESET}`;
|
|
122
|
+
}
|
|
123
|
+
return `${ANSI_RED}Unknown migration error${ANSI_RESET}`;
|
|
124
|
+
}
|
package/dist/updebe.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const init_1 = require("./commands/init");
|
|
5
|
+
const upgradeHead_1 = require("./commands/upgradeHead");
|
|
4
6
|
const command = process.argv[2];
|
|
7
|
+
if (command === "init") {
|
|
8
|
+
try {
|
|
9
|
+
(0, init_1.runInit)();
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
13
|
+
console.error(message);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
5
17
|
if (command === "upgrade:head") {
|
|
6
|
-
|
|
18
|
+
(0, upgradeHead_1.runUpgradeHead)().catch((error) => {
|
|
19
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
20
|
+
console.error(message);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
});
|
|
7
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "updebe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Schema migration tool",
|
|
6
6
|
"main": "dist/updebe.js",
|
|
@@ -16,8 +16,14 @@
|
|
|
16
16
|
"preupdebe": "npm run build",
|
|
17
17
|
"updebe": "node dist/updebe.js"
|
|
18
18
|
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"dotenv": "^17.4.2",
|
|
21
|
+
"pg": "^8.23.0",
|
|
22
|
+
"ts-node": "^10.9.2",
|
|
23
|
+
"typescript": "^7.0.2"
|
|
24
|
+
},
|
|
19
25
|
"devDependencies": {
|
|
20
26
|
"@types/node": "^24.13.3",
|
|
21
|
-
"
|
|
27
|
+
"@types/pg": "^8.23.1"
|
|
22
28
|
}
|
|
23
29
|
}
|