@dogulabs/sql-migrator 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -2
- package/src/config/config.ts +75 -0
- package/src/index.ts +22 -204
- package/src/module/ch/ch.ts +66 -0
- package/src/module/ch/migrate-down.ch.ts +44 -0
- package/src/module/ch/migrate-reset.ch.ts +63 -0
- package/src/module/ch/migrate-up.ch.ts +45 -0
- package/src/module/common/generate-migration.common.ts +28 -0
- package/src/module/pg/migrate-down.pg.ts +34 -0
- package/src/module/pg/migrate-reset.pg.ts +56 -0
- package/src/module/pg/migrate-up.pg.ts +37 -0
- package/src/module/pg/pg.ts +69 -0
- package/tsconfig.json +39 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogulabs/sql-migrator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
"url": "git+https://github.com/moons-id/ts-node-sql-migrator.git"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
|
-
"migration"
|
|
19
|
+
"migration",
|
|
20
|
+
"postgresql",
|
|
21
|
+
"postgres",
|
|
22
|
+
"sql",
|
|
23
|
+
"database",
|
|
24
|
+
"migrator",
|
|
25
|
+
"clickhouse"
|
|
20
26
|
],
|
|
21
27
|
"author": "rizkynovando@gmail.com",
|
|
22
28
|
"license": "GPL-3.0-or-later",
|
|
@@ -26,12 +32,15 @@
|
|
|
26
32
|
"homepage": "https://github.com/moons-id/node-sql-migrator#readme",
|
|
27
33
|
"description": "",
|
|
28
34
|
"dependencies": {
|
|
35
|
+
"@clickhouse/client": "^1.17.0",
|
|
29
36
|
"@types/pg": "^8.15.5",
|
|
30
37
|
"dotenv": "^17.2.2",
|
|
38
|
+
"node-vault-client": "^1.0.2",
|
|
31
39
|
"pg": "^8.16.3"
|
|
32
40
|
},
|
|
33
41
|
"devDependencies": {
|
|
34
42
|
"@types/node": "^20.0.0",
|
|
43
|
+
"@types/node-vault-client": "^1.0.1",
|
|
35
44
|
"ts-node": "^10.9.2",
|
|
36
45
|
"typescript": "^5.6.0"
|
|
37
46
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import vaultClient from 'node-vault-client';
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
type EnvConfig = {
|
|
7
|
+
pg: PgConfig;
|
|
8
|
+
clickHouse: ClickHouseConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type PgConfig = {
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
user: string;
|
|
15
|
+
password: string;
|
|
16
|
+
database: string;
|
|
17
|
+
queryTimeout: number;
|
|
18
|
+
connectionTimeout: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ClickHouseConfig = {
|
|
22
|
+
port: number;
|
|
23
|
+
host: string;
|
|
24
|
+
database: string;
|
|
25
|
+
pass: string;
|
|
26
|
+
user: string;
|
|
27
|
+
ssl: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pgConfig: PgConfig = {
|
|
31
|
+
host: process.env.PG_HOST || 'localhost',
|
|
32
|
+
port: Number(process.env.PG_PORT) || 5432,
|
|
33
|
+
user: process.env.PG_USER || '',
|
|
34
|
+
password: process.env.PG_PASS || '',
|
|
35
|
+
database: process.env.PG_DB || '',
|
|
36
|
+
queryTimeout: Number(process.env.PG_QUERY_TIMEOUT) || 30000,
|
|
37
|
+
connectionTimeout: Number(process.env.PG_CONNECTION_TIMEOUT) || 5000,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const clickHouseConfig: ClickHouseConfig = {
|
|
41
|
+
port: Number(process.env.CH_PORT) || 0,
|
|
42
|
+
host: process.env.CH_HOST || 'localhost',
|
|
43
|
+
database: process.env.CH_DB || '',
|
|
44
|
+
pass: process.env.CH_PASS || '',
|
|
45
|
+
user: process.env.CH_USER || '',
|
|
46
|
+
ssl: process.env.CH_SSL === 'true' || process.env.CH_SSL === '1',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const envConfig: EnvConfig = {
|
|
50
|
+
pg: pgConfig,
|
|
51
|
+
clickHouse: clickHouseConfig,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
export async function loadVaultConfig() {
|
|
56
|
+
try {
|
|
57
|
+
const vault = vaultClient.boot('main', {
|
|
58
|
+
api: {
|
|
59
|
+
url: process.env.VAULT_URL ?? '',
|
|
60
|
+
apiVersion: 'v1',
|
|
61
|
+
},
|
|
62
|
+
auth: {
|
|
63
|
+
type: 'token',
|
|
64
|
+
config: { token: process.env.VAULT_TOKEN ?? ''},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const kv = await vault.read(process.env.VAULT_PATH ?? '');
|
|
68
|
+
|
|
69
|
+
Object.assign(envConfig, kv.getData())
|
|
70
|
+
console.log(`Vault loaded from ${process.env.VAULT_URL}, with path ${process.env.VAULT_PATH}`);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.log(`Vault failed to load: ${(e as Error).message}`);
|
|
73
|
+
console.log('using .env')
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import {join} from 'path';
|
|
5
|
-
import {Pool, PoolClient, PoolConfig} from "pg";
|
|
6
3
|
import 'dotenv/config'
|
|
4
|
+
import { pg } from "./module/pg/pg.js";
|
|
5
|
+
import { ch } from "./module/ch/ch.js";
|
|
6
|
+
import { loadVaultConfig } from "./config/config.js";
|
|
7
|
+
|
|
7
8
|
|
|
8
9
|
function getArg(key: string, defaultValue = "") {
|
|
9
10
|
const arg = process.argv.find((a) => a.startsWith(`--${key}=`));
|
|
@@ -11,208 +12,25 @@ function getArg(key: string, defaultValue = "") {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
async function runMigrations() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
host: process.env.PG_HOST,
|
|
33
|
-
port: Number(process.env.PG_PORT),
|
|
34
|
-
password: process.env.PG_PASS,
|
|
35
|
-
user: process.env.PG_USER,
|
|
36
|
-
database: process.env.PG_DB,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (process.env.PG_CA?.length) {
|
|
40
|
-
pgConfig.ssl = { ca: process.env.PG_CA }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
pg = new Pool(pgConfig)
|
|
44
|
-
await pg.query(`CREATE TABLE IF NOT EXISTS node_migrator_${type}s (
|
|
45
|
-
id BIGSERIAL PRIMARY KEY,
|
|
46
|
-
version VARCHAR(255) NOT NULL,
|
|
47
|
-
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
48
|
-
)`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// @ts-ignore
|
|
52
|
-
const scriptsDir = join(process.cwd(), 'db', driver, type === 'migration' ? 'migration' : 'seed');
|
|
53
|
-
switch (action) {
|
|
54
|
-
case 'up':
|
|
55
|
-
await migrateUp(pg, scriptsDir, type);
|
|
56
|
-
break;
|
|
57
|
-
case 'down':
|
|
58
|
-
await migrateDown(pg, scriptsDir, type);
|
|
59
|
-
break;
|
|
60
|
-
case 'reset':
|
|
61
|
-
await migrateReset(pg, scriptsDir, type);
|
|
62
|
-
break;
|
|
63
|
-
case 'new':
|
|
64
|
-
generateMigration(scriptsDir, name, type);
|
|
65
|
-
break;
|
|
66
|
-
default:
|
|
67
|
-
console.log('Invalid action. Please use "up", "down", or "reset".');
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
} catch (e) {
|
|
71
|
-
console.error('Migration failed: ', (e as Error).message);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
} finally {
|
|
74
|
-
process.exit(0);
|
|
15
|
+
await loadVaultConfig()
|
|
16
|
+
|
|
17
|
+
const action = getArg("action", "up") ?? "up";
|
|
18
|
+
const driver = getArg("driver", "postgres") ?? "";
|
|
19
|
+
const type = getArg("type", "migration") ?? "migration";
|
|
20
|
+
const name = getArg("name", "") ?? "";
|
|
21
|
+
|
|
22
|
+
switch (driver) {
|
|
23
|
+
case "clickhouse":
|
|
24
|
+
await ch(type, action, name);
|
|
25
|
+
break;
|
|
26
|
+
case "postgres":
|
|
27
|
+
await pg(type, action, name);
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
await ch(type, action, name);
|
|
31
|
+
await pg(type, action, name);
|
|
32
|
+
break;
|
|
75
33
|
}
|
|
76
34
|
}
|
|
77
35
|
|
|
78
|
-
async function migrateUp(db: Pool|null, scriptsDir: string, type: string) {
|
|
79
|
-
if (!db) throw Error('Database connection is required')
|
|
80
|
-
try {
|
|
81
|
-
console.log(`Pushing database ${type}s...`);
|
|
82
|
-
const res = await db.query(`SELECT version FROM node_migrator_${type}s`)
|
|
83
|
-
const versions = res.rows.map((row: {version: string}) => row.version)
|
|
84
|
-
|
|
85
|
-
const files = fs.readdirSync(scriptsDir)
|
|
86
|
-
.filter((file) => {
|
|
87
|
-
const isVersion = versions.includes(file.split('_')[0])
|
|
88
|
-
return !isVersion && file.endsWith('.sql')
|
|
89
|
-
}).sort()
|
|
90
|
-
|
|
91
|
-
for (const file of files) {
|
|
92
|
-
const migrationPath = join(scriptsDir, file);
|
|
93
|
-
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
94
|
-
|
|
95
|
-
const sqls = migrationScripts.split('-- +migrator UP')[1] || ''
|
|
96
|
-
const sql = sqls?.split('-- +migrator DOWN')[0] || ''
|
|
97
|
-
await db.query(sql);
|
|
98
|
-
|
|
99
|
-
const version = file.split('_')[0]
|
|
100
|
-
await db.query(`INSERT INTO node_migrator_${type}s (version) VALUES ($1)`, [version])
|
|
101
|
-
console.log(`✓ Migration pushed: ${file}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
console.log(`All ${type}s pushed successfully!`);
|
|
105
|
-
} catch (e) {
|
|
106
|
-
console.error('Migration UP failed: ', (e as Error).message);
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function migrateDown(db: Pool|null, scriptsDir: string, type: string) {
|
|
112
|
-
if (type === 'seed') {
|
|
113
|
-
throw new Error("Seeder cannot be reverted. use 'reset' instead.")
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!db) throw Error('Database connection is required')
|
|
117
|
-
try {
|
|
118
|
-
console.log(`Rolling back database ${type}s...`);
|
|
119
|
-
const res = await db.query(`SELECT version FROM node_migrator_${type}s ORDER BY created_at DESC LIMIT 1`)
|
|
120
|
-
const version = res.rows[0].version
|
|
121
|
-
|
|
122
|
-
const file = fs.readdirSync(scriptsDir)
|
|
123
|
-
.find((file) => file.startsWith(`${version}_`) && file.endsWith('.sql'))
|
|
124
|
-
|
|
125
|
-
if (!file) throw Error(`Migration script version ${version} not found`)
|
|
126
|
-
const migrationPath = join(scriptsDir, file);
|
|
127
|
-
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
128
|
-
|
|
129
|
-
const sqls = migrationScripts.split('-- +migrator DOWN')[1] || ''
|
|
130
|
-
const sql = sqls?.split('-- +migrator UP')[0] || ''
|
|
131
|
-
await db.query(sql);
|
|
132
|
-
|
|
133
|
-
await db.query(`DELETE FROM node_migrator_${type}s WHERE version = $1`, [version])
|
|
134
|
-
console.log(`✓ Migration reverted: ${file}`);
|
|
135
|
-
} catch (e) {
|
|
136
|
-
console.error('Migration DOWN failed: ', (e as Error).message);
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function migrateReset(db: Pool|null, scriptsDir: string, type: string) {
|
|
142
|
-
if (!db) throw Error('Database connection is required')
|
|
143
|
-
try {
|
|
144
|
-
console.log(`Reverting database ${type}s...`);
|
|
145
|
-
if (type === 'seed') {
|
|
146
|
-
await db.query("DROP TABLE IF EXISTS node_migrator_seeds");
|
|
147
|
-
console.log("✓ Seed cleaned");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const res = await db.query(`SELECT version FROM node_migrator_${type}s`)
|
|
151
|
-
const versions = res.rows.map((row: {version: string}) => row.version)
|
|
152
|
-
const scripts: string[] = []
|
|
153
|
-
const availableVersions: string[] = []
|
|
154
|
-
|
|
155
|
-
const files = fs.readdirSync(scriptsDir)
|
|
156
|
-
.filter((file) => {
|
|
157
|
-
const isVersion = versions.includes(file.split('_')[0])
|
|
158
|
-
return isVersion && file.endsWith('.sql')
|
|
159
|
-
}).sort().reverse()
|
|
160
|
-
|
|
161
|
-
for (const file of files) {
|
|
162
|
-
if (files.includes(file)) {
|
|
163
|
-
scripts.push(file)
|
|
164
|
-
availableVersions.push(file.split('_')[0] as string)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (scripts.length !== res.rowCount) {
|
|
169
|
-
const missingVersions = versions.filter((v: string) => !availableVersions.includes(v))
|
|
170
|
-
throw Error(`Missing version: ${missingVersions}`)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (const file of scripts) {
|
|
174
|
-
const migrationPath = join(scriptsDir, file);
|
|
175
|
-
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
176
|
-
|
|
177
|
-
const sqls = migrationScripts.split('-- +migrator DOWN')[1] || ''
|
|
178
|
-
const sql = sqls?.split('-- +migrator UP')[0] || ''
|
|
179
|
-
await db.query(sql);
|
|
180
|
-
|
|
181
|
-
console.log(`✓ Migration ${file} reverted`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
await db.query(`DELETE FROM node_migrator_${type}s`)
|
|
185
|
-
await db.query("DROP TABLE IF EXISTS node_migrator_seeds");
|
|
186
|
-
console.log("Migrations rolled back");
|
|
187
|
-
} catch (e) {
|
|
188
|
-
console.error('Migration DOWN failed: ', (e as Error).message);
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function generateMigration(scriptsDir: string, name: string, type: string) {
|
|
194
|
-
if (name.length < 1) throw Error("name argument required")
|
|
195
|
-
const timestamp = new Date().toISOString().replace(/\..+/, "").replace(/[^0-9]/g, "")
|
|
196
|
-
const migrationPath = join(scriptsDir, `${timestamp}_${name}.sql`);
|
|
197
|
-
|
|
198
|
-
if (!fs.existsSync(scriptsDir)) {
|
|
199
|
-
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const migrationScripts = `-- +migrator UP
|
|
203
|
-
-- +migrator statement BEGIN
|
|
204
|
-
|
|
205
|
-
-- +migrator statement END
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
-- +migrator DOWN
|
|
209
|
-
-- +migrator statement BEGIN
|
|
210
|
-
|
|
211
|
-
-- +migrator statement END
|
|
212
|
-
`
|
|
213
|
-
fs.writeFileSync(migrationPath, migrationScripts);
|
|
214
|
-
|
|
215
|
-
console.log(`✓ ${type} created: ${timestamp}_${name}.sql`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
36
|
await runMigrations();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { migrateUpCh } from "./migrate-up.ch.js";
|
|
2
|
+
import { migrateDownCh } from "./migrate-down.ch.js";
|
|
3
|
+
import { migrateResetCh } from "./migrate-reset.ch.js";
|
|
4
|
+
import { generateMigrationCommon } from "../common/generate-migration.common.js";
|
|
5
|
+
import { createClient } from "@clickhouse/client";
|
|
6
|
+
import { NodeClickHouseClient } from "@clickhouse/client/dist/client.js";
|
|
7
|
+
import { envConfig } from "../../config/config.js";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export async function ch(type: string, action: string, name: string) {
|
|
12
|
+
if (!['migration', 'seed'].includes(type)) {
|
|
13
|
+
console.log('Invalid type. Please use "migration" or "seed".');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
if (action === 'new' && type === 'seed' && name === '') {
|
|
17
|
+
console.log('Invalid name. Please use "new seed" following by name.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const client = createClient({
|
|
23
|
+
url: `${envConfig.clickHouse.ssl ? "https" : "http"}://${envConfig.clickHouse.host}:${envConfig.clickHouse.port}`,
|
|
24
|
+
username: envConfig.clickHouse.user,
|
|
25
|
+
password: envConfig.clickHouse.pass,
|
|
26
|
+
database: envConfig.clickHouse.database,
|
|
27
|
+
request_timeout: 10_000,
|
|
28
|
+
});
|
|
29
|
+
if (action !== 'new') {
|
|
30
|
+
await client.query({
|
|
31
|
+
query: `
|
|
32
|
+
CREATE TABLE IF NOT EXISTS node_migrator_${type}s (
|
|
33
|
+
id Int64 PRIMARY KEY,
|
|
34
|
+
version String NOT NULL,
|
|
35
|
+
created_at DateTime('UTC') NOT NULL DEFAULT now()
|
|
36
|
+
)`,
|
|
37
|
+
format: 'JSONEachRow',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
const scriptsDir = join(process.cwd(), 'db', 'clickhouse', type === 'migration' ? 'migration' : 'seed');
|
|
43
|
+
switch (action) {
|
|
44
|
+
case 'up':
|
|
45
|
+
await migrateUpCh(client, scriptsDir, type);
|
|
46
|
+
break;
|
|
47
|
+
case 'down':
|
|
48
|
+
await migrateDownCh(client, scriptsDir, type);
|
|
49
|
+
break;
|
|
50
|
+
case 'reset':
|
|
51
|
+
await migrateResetCh(client, scriptsDir, type);
|
|
52
|
+
break;
|
|
53
|
+
case 'new':
|
|
54
|
+
generateMigrationCommon(scriptsDir, name, type);
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
console.log('Invalid action. Please use "up", "down", or "reset".');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error('Migration failed: ', (e as Error).message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
} finally {
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { NodeClickHouseClient } from "@clickhouse/client/dist/client";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function migrateDownCh(db: NodeClickHouseClient|null, scriptsDir: string, type: string) {
|
|
7
|
+
if (type === 'seed') {
|
|
8
|
+
throw new Error("Seeder cannot be reverted. use 'reset' instead.")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!db) throw Error('Database connection is required')
|
|
12
|
+
try {
|
|
13
|
+
console.log(`Rolling back database ${type}s...`);
|
|
14
|
+
|
|
15
|
+
const res = await db.query({
|
|
16
|
+
query: `SELECT version FROM node_migrator_${type}s`,
|
|
17
|
+
format: 'JSONEachRow',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const data = await res.json() as any;
|
|
21
|
+
const version = data[0].version;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const file = fs.readdirSync(scriptsDir)
|
|
25
|
+
.find((file) => file.startsWith(`${version}_`) && file.endsWith('.sql'))
|
|
26
|
+
|
|
27
|
+
if (!file) throw Error(`Migration script version ${version} not found`)
|
|
28
|
+
const migrationPath = join(scriptsDir, file);
|
|
29
|
+
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
30
|
+
|
|
31
|
+
const sqls = migrationScripts.split('-- +migrator DOWN')[1] || ''
|
|
32
|
+
const sql = sqls?.split('-- +migrator UP')[0] || ''
|
|
33
|
+
await db.exec({ query: sql });
|
|
34
|
+
|
|
35
|
+
await db.exec({
|
|
36
|
+
query: `DELETE FROM node_migrator_${type}s WHERE version = {val1: String}`,
|
|
37
|
+
query_params: { val1: version },
|
|
38
|
+
})
|
|
39
|
+
console.log(`✓ Migration reverted: ${file}`);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('Migration DOWN failed: ', (e as Error).message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { NodeClickHouseClient } from "@clickhouse/client/dist/client";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function migrateResetCh(db: NodeClickHouseClient|null, scriptsDir: string, type: string) {
|
|
7
|
+
if (!db) throw Error('Database connection is required')
|
|
8
|
+
try {
|
|
9
|
+
console.log(`Reverting database ${type}s...`);
|
|
10
|
+
if (type === 'seed') {
|
|
11
|
+
await db.exec({ query: "DROP TABLE IF EXISTS node_migrator_seeds" });
|
|
12
|
+
console.log("✓ Seed cleaned");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const res = await db.query({
|
|
17
|
+
query: `SELECT version FROM node_migrator_${type}s`,
|
|
18
|
+
format: 'JSONEachRow',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const data = await res.json() as any;
|
|
22
|
+
const versions = data.map((row: {version: string}) => row.version)
|
|
23
|
+
|
|
24
|
+
const scripts: string[] = []
|
|
25
|
+
const availableVersions: string[] = []
|
|
26
|
+
|
|
27
|
+
const files = fs.readdirSync(scriptsDir)
|
|
28
|
+
.filter((file) => {
|
|
29
|
+
const isVersion = versions.includes(file.split('_')[0])
|
|
30
|
+
return isVersion && file.endsWith('.sql')
|
|
31
|
+
}).sort().reverse()
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (files.includes(file)) {
|
|
35
|
+
scripts.push(file)
|
|
36
|
+
availableVersions.push(file.split('_')[0] as string)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (scripts.length !== data.length) {
|
|
41
|
+
const missingVersions = versions.filter((v: string) => !availableVersions.includes(v))
|
|
42
|
+
throw Error(`Missing version: ${missingVersions}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const file of scripts) {
|
|
46
|
+
const migrationPath = join(scriptsDir, file);
|
|
47
|
+
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
48
|
+
|
|
49
|
+
const sqls = migrationScripts.split('-- +migrator DOWN')[1] || ''
|
|
50
|
+
const sql = sqls?.split('-- +migrator UP')[0] || ''
|
|
51
|
+
await db.exec({ query: sql });
|
|
52
|
+
|
|
53
|
+
console.log(`✓ Migration ${file} reverted`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await db.exec({ query: `DELETE FROM node_migrator_${type}s WHERE 1=1` })
|
|
57
|
+
await db.exec({ query: "DROP TABLE IF EXISTS node_migrator_seeds" });
|
|
58
|
+
console.log("Migrations rolled back");
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error('Migration RESET failed: ', (e as Error).message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NodeClickHouseClient } from "@clickhouse/client/dist/client";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function migrateUpCh(db: NodeClickHouseClient|null, scriptsDir: string, type: string) {
|
|
7
|
+
if (!db) throw Error('Database connection is required')
|
|
8
|
+
try {
|
|
9
|
+
console.log(`Pushing database ${type}s...`);
|
|
10
|
+
const res = await db.query({
|
|
11
|
+
query: `SELECT version FROM node_migrator_${type}s`,
|
|
12
|
+
format: 'JSONEachRow',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const data = await res.json() as any;
|
|
16
|
+
const versions = data.map((row: {version: string}) => row.version)
|
|
17
|
+
|
|
18
|
+
const files = fs.readdirSync(scriptsDir)
|
|
19
|
+
.filter((file) => {
|
|
20
|
+
const isVersion = versions.includes(file.split('_')[0])
|
|
21
|
+
return !isVersion && file.endsWith('.sql')
|
|
22
|
+
}).sort()
|
|
23
|
+
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const migrationPath = join(scriptsDir, file);
|
|
26
|
+
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
27
|
+
|
|
28
|
+
const sqls = migrationScripts.split('-- +migrator UP')[1] || ''
|
|
29
|
+
const sql = sqls?.split('-- +migrator DOWN')[0] || ''
|
|
30
|
+
await db.exec({ query: sql });
|
|
31
|
+
|
|
32
|
+
const version = file.split('_')[0]
|
|
33
|
+
await db.exec({
|
|
34
|
+
query: `INSERT INTO node_migrator_${type}s (version) VALUES ({val: String})`,
|
|
35
|
+
query_params: { val: version },
|
|
36
|
+
})
|
|
37
|
+
console.log(`✓ Migration pushed: ${file}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`All ${type}s pushed successfully!`);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error('Migration UP failed: ', (e as Error).message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export function generateMigrationCommon(scriptsDir: string, name: string, type: string) {
|
|
6
|
+
if (name.length < 1) throw Error("name argument required")
|
|
7
|
+
const timestamp = new Date().toISOString().replace(/\..+/, "").replace(/[^0-9]/g, "")
|
|
8
|
+
const migrationPath = join(scriptsDir, `${timestamp}_${name}.sql`);
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
11
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const migrationScripts = `-- +migrator UP
|
|
15
|
+
-- +migrator statement BEGIN
|
|
16
|
+
|
|
17
|
+
-- +migrator statement END
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
-- +migrator DOWN
|
|
21
|
+
-- +migrator statement BEGIN
|
|
22
|
+
|
|
23
|
+
-- +migrator statement END
|
|
24
|
+
`
|
|
25
|
+
fs.writeFileSync(migrationPath, migrationScripts);
|
|
26
|
+
|
|
27
|
+
console.log(`✓ ${type} created: ${timestamp}_${name}.sql`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function migrateDownPg(db: Pool|null, scriptsDir: string, type: string) {
|
|
7
|
+
if (type === 'seed') {
|
|
8
|
+
throw new Error("Seeder cannot be reverted. use 'reset' instead.")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!db) throw Error('Database connection is required')
|
|
12
|
+
try {
|
|
13
|
+
console.log(`Rolling back database ${type}s...`);
|
|
14
|
+
const res = await db.query(`SELECT version FROM node_migrator_${type}s ORDER BY created_at DESC LIMIT 1`)
|
|
15
|
+
const version = res.rows[0].version
|
|
16
|
+
|
|
17
|
+
const file = fs.readdirSync(scriptsDir)
|
|
18
|
+
.find((file) => file.startsWith(`${version}_`) && file.endsWith('.sql'))
|
|
19
|
+
|
|
20
|
+
if (!file) throw Error(`Migration script version ${version} not found`)
|
|
21
|
+
const migrationPath = join(scriptsDir, file);
|
|
22
|
+
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
23
|
+
|
|
24
|
+
const sqls = migrationScripts.split('-- +migrator DOWN')[1] || ''
|
|
25
|
+
const sql = sqls?.split('-- +migrator UP')[0] || ''
|
|
26
|
+
await db.query(sql);
|
|
27
|
+
|
|
28
|
+
await db.query(`DELETE FROM node_migrator_${type}s WHERE version = $1`, [version])
|
|
29
|
+
console.log(`✓ Migration reverted: ${file}`);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error('Migration DOWN failed: ', (e as Error).message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function migrateResetPg(db: Pool|null, scriptsDir: string, type: string) {
|
|
7
|
+
if (!db) throw Error('Database connection is required')
|
|
8
|
+
try {
|
|
9
|
+
console.log(`Reverting database ${type}s...`);
|
|
10
|
+
if (type === 'seed') {
|
|
11
|
+
await db.query("DROP TABLE IF EXISTS node_migrator_seeds");
|
|
12
|
+
console.log("✓ Seed cleaned");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const res = await db.query(`SELECT version FROM node_migrator_${type}s`)
|
|
16
|
+
const versions = res.rows.map((row: {version: string}) => row.version)
|
|
17
|
+
const scripts: string[] = []
|
|
18
|
+
const availableVersions: string[] = []
|
|
19
|
+
|
|
20
|
+
const files = fs.readdirSync(scriptsDir)
|
|
21
|
+
.filter((file) => {
|
|
22
|
+
const isVersion = versions.includes(file.split('_')[0] ?? '')
|
|
23
|
+
return isVersion && file.endsWith('.sql')
|
|
24
|
+
}).sort().reverse()
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (files.includes(file)) {
|
|
28
|
+
scripts.push(file)
|
|
29
|
+
availableVersions.push(file.split('_')[0] as string)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (scripts.length !== res.rowCount) {
|
|
34
|
+
const missingVersions = versions.filter((v: string) => !availableVersions.includes(v))
|
|
35
|
+
throw Error(`Missing version: ${missingVersions}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const file of scripts) {
|
|
39
|
+
const migrationPath = join(scriptsDir, file);
|
|
40
|
+
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
41
|
+
|
|
42
|
+
const sqls = migrationScripts.split('-- +migrator DOWN')[1] || ''
|
|
43
|
+
const sql = sqls?.split('-- +migrator UP')[0] || ''
|
|
44
|
+
await db.query(sql);
|
|
45
|
+
|
|
46
|
+
console.log(`✓ Migration ${file} reverted`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await db.query(`DELETE FROM node_migrator_${type}s`)
|
|
50
|
+
await db.query("DROP TABLE IF EXISTS node_migrator_seeds");
|
|
51
|
+
console.log("Migrations rolled back");
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error('Migration RESET failed: ', (e as Error).message);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function migrateUpPg(db: Pool|null, scriptsDir: string, type: string) {
|
|
7
|
+
if (!db) throw Error('Database connection is required')
|
|
8
|
+
try {
|
|
9
|
+
console.log(`Pushing database ${type}s...`);
|
|
10
|
+
const res = await db.query(`SELECT version FROM node_migrator_${type}s`)
|
|
11
|
+
const versions = res.rows.map((row: {version: string}) => row.version)
|
|
12
|
+
|
|
13
|
+
const files = fs.readdirSync(scriptsDir)
|
|
14
|
+
.filter((file) => {
|
|
15
|
+
const isVersion = versions.includes(file.split('_')[0] ?? '')
|
|
16
|
+
return !isVersion && file.endsWith('.sql')
|
|
17
|
+
}).sort()
|
|
18
|
+
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const migrationPath = join(scriptsDir, file);
|
|
21
|
+
const migrationScripts = fs.readFileSync(migrationPath, 'utf8');
|
|
22
|
+
|
|
23
|
+
const sqls = migrationScripts.split('-- +migrator UP')[1] || ''
|
|
24
|
+
const sql = sqls?.split('-- +migrator DOWN')[0] || ''
|
|
25
|
+
await db.query(sql);
|
|
26
|
+
|
|
27
|
+
const version = file.split('_')[0]
|
|
28
|
+
await db.query(`INSERT INTO node_migrator_${type}s (version) VALUES ($1)`, [version])
|
|
29
|
+
console.log(`✓ Migration pushed: ${file}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`All ${type}s pushed successfully!`);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error('Migration UP failed: ', (e as Error).message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { PoolConfig } from "pg";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
import { migrateUpPg } from "./migrate-up.pg.js";
|
|
4
|
+
import { migrateDownPg } from "./migrate-down.pg.js";
|
|
5
|
+
import { migrateResetPg } from "./migrate-reset.pg.js";
|
|
6
|
+
import { generateMigrationCommon } from "../common/generate-migration.common.js";
|
|
7
|
+
import { envConfig } from "../../config/config.js";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export async function pg(type: string, action: string, name: string) {
|
|
12
|
+
if (!['migration', 'seed'].includes(type)) {
|
|
13
|
+
console.log('Invalid type. Please use "migration" or "seed".');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
if (action === 'new' && type === 'seed' && name === '') {
|
|
17
|
+
console.log('Invalid name. Please use "new seed" following by name.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
let pg: Pool | null = null;
|
|
23
|
+
if (action !== 'new') {
|
|
24
|
+
const pgConfig: PoolConfig = {
|
|
25
|
+
host: envConfig.pg.host,
|
|
26
|
+
port: envConfig.pg.port,
|
|
27
|
+
password: envConfig.pg.password,
|
|
28
|
+
user: envConfig.pg.user,
|
|
29
|
+
database: envConfig.pg.database,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.env.PG_CA?.length) {
|
|
33
|
+
pgConfig.ssl = { ca: process.env.PG_CA }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pg = new Pool(pgConfig)
|
|
37
|
+
await pg.query(`CREATE TABLE IF NOT EXISTS node_migrator_${type}s (
|
|
38
|
+
id BIGSERIAL PRIMARY KEY,
|
|
39
|
+
version VARCHAR(255) NOT NULL,
|
|
40
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
41
|
+
)`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
const scriptsDir = join(process.cwd(), 'db', 'postgres', type === 'migration' ? 'migration' : 'seed');
|
|
46
|
+
switch (action) {
|
|
47
|
+
case 'up':
|
|
48
|
+
await migrateUpPg(pg, scriptsDir, type);
|
|
49
|
+
break;
|
|
50
|
+
case 'down':
|
|
51
|
+
await migrateDownPg(pg, scriptsDir, type);
|
|
52
|
+
break;
|
|
53
|
+
case 'reset':
|
|
54
|
+
await migrateResetPg(pg, scriptsDir, type);
|
|
55
|
+
break;
|
|
56
|
+
case 'new':
|
|
57
|
+
generateMigrationCommon(scriptsDir, name, type);
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
console.log('Invalid action. Please use "up", "down", or "reset".');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('Migration failed: ', (e as Error).message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
} finally {
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
3
|
+
// File Layout
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
|
|
7
|
+
// Environment Settings
|
|
8
|
+
// See also https://aka.ms/tsconfig/module
|
|
9
|
+
"module": "ES2022",
|
|
10
|
+
"moduleResolution": "node",
|
|
11
|
+
"target": "esnext",
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
// For nodejs:
|
|
14
|
+
// "lib": ["esnext"],
|
|
15
|
+
// "types": ["node"],
|
|
16
|
+
// and npm install -D @types/node
|
|
17
|
+
|
|
18
|
+
// Other Outputs
|
|
19
|
+
"sourceMap": true,
|
|
20
|
+
"declaration": true,
|
|
21
|
+
"declarationMap": true,
|
|
22
|
+
|
|
23
|
+
// Stricter Typechecking Options
|
|
24
|
+
"noUncheckedIndexedAccess": true,
|
|
25
|
+
"exactOptionalPropertyTypes": true,
|
|
26
|
+
|
|
27
|
+
// Style Options
|
|
28
|
+
// "noImplicitReturns": true,
|
|
29
|
+
// "noImplicitOverride": true,
|
|
30
|
+
// "noUnusedLocals": true,
|
|
31
|
+
// "noUnusedParameters": true,
|
|
32
|
+
// "noFallthroughCasesInSwitch": true,
|
|
33
|
+
// "noPropertyAccessFromIndexSignature": true,
|
|
34
|
+
|
|
35
|
+
// Recommended Options
|
|
11
36
|
"strict": true,
|
|
37
|
+
"jsx": "react-jsx",
|
|
38
|
+
"verbatimModuleSyntax": true,
|
|
39
|
+
"isolatedModules": true,
|
|
40
|
+
"noUncheckedSideEffectImports": true,
|
|
41
|
+
"moduleDetection": "force",
|
|
12
42
|
"skipLibCheck": true,
|
|
43
|
+
"allowSyntheticDefaultImports": true
|
|
13
44
|
},
|
|
14
45
|
"include": ["src"],
|
|
15
46
|
"exclude": ["node_modules", "dist"]
|