mpx-db 1.0.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/LICENSE +21 -0
- package/README.md +347 -0
- package/SUMMARY.md +182 -0
- package/bin/mpx-db.js +3 -0
- package/package.json +62 -0
- package/src/cli.js +141 -0
- package/src/commands/connections.js +79 -0
- package/src/commands/data.js +79 -0
- package/src/commands/migrate.js +318 -0
- package/src/commands/query.js +93 -0
- package/src/commands/schema.js +181 -0
- package/src/db/base-adapter.js +101 -0
- package/src/db/connection.js +46 -0
- package/src/db/mysql-adapter.js +144 -0
- package/src/db/postgres-adapter.js +150 -0
- package/src/db/sqlite-adapter.js +141 -0
- package/src/index.js +15 -0
- package/src/utils/config.js +109 -0
- package/src/utils/crypto.js +67 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import { createConnection } from '../db/connection.js';
|
|
4
|
+
import { resolveConnection } from './query.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Show database info
|
|
8
|
+
*/
|
|
9
|
+
export async function showInfo(target) {
|
|
10
|
+
let db;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const connectionString = await resolveConnection(target);
|
|
14
|
+
db = await createConnection(connectionString);
|
|
15
|
+
|
|
16
|
+
const info = await db.getInfo();
|
|
17
|
+
|
|
18
|
+
console.log(chalk.bold('\nDatabase Information'));
|
|
19
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
20
|
+
console.log(`${chalk.cyan('Type:')} ${info.type}`);
|
|
21
|
+
if (info.database) {
|
|
22
|
+
console.log(`${chalk.cyan('Database:')} ${info.database}`);
|
|
23
|
+
}
|
|
24
|
+
if (info.path) {
|
|
25
|
+
console.log(`${chalk.cyan('Path:')} ${info.path}`);
|
|
26
|
+
}
|
|
27
|
+
console.log(`${chalk.cyan('Size:')} ${info.sizeFormatted}`);
|
|
28
|
+
console.log(`${chalk.cyan('Tables:')} ${info.tables}`);
|
|
29
|
+
console.log(`${chalk.cyan('Total Rows:')} ${info.totalRows.toLocaleString()}`);
|
|
30
|
+
console.log();
|
|
31
|
+
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
} finally {
|
|
36
|
+
if (db) {
|
|
37
|
+
await db.disconnect();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List all tables
|
|
44
|
+
*/
|
|
45
|
+
export async function listTables(target) {
|
|
46
|
+
let db;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const connectionString = await resolveConnection(target);
|
|
50
|
+
db = await createConnection(connectionString);
|
|
51
|
+
|
|
52
|
+
const tables = await db.getTables();
|
|
53
|
+
|
|
54
|
+
if (tables.length === 0) {
|
|
55
|
+
console.log(chalk.yellow('No tables found'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const table = new Table({
|
|
60
|
+
head: ['Table', 'Type', 'Rows'].map(h => chalk.cyan(h)),
|
|
61
|
+
style: { head: [], border: ['gray'] },
|
|
62
|
+
colAligns: ['left', 'left', 'right']
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
for (const t of tables) {
|
|
66
|
+
table.push([
|
|
67
|
+
chalk.white(t.name),
|
|
68
|
+
chalk.gray(t.type),
|
|
69
|
+
chalk.yellow(t.rows.toLocaleString())
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(table.toString());
|
|
74
|
+
console.log(chalk.gray(`\n${tables.length} table(s)`));
|
|
75
|
+
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
} finally {
|
|
80
|
+
if (db) {
|
|
81
|
+
await db.disconnect();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Describe table schema
|
|
88
|
+
*/
|
|
89
|
+
export async function describeTable(target, tableName) {
|
|
90
|
+
let db;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const connectionString = await resolveConnection(target);
|
|
94
|
+
db = await createConnection(connectionString);
|
|
95
|
+
|
|
96
|
+
const schema = await db.getTableSchema(tableName);
|
|
97
|
+
|
|
98
|
+
if (schema.length === 0) {
|
|
99
|
+
console.log(chalk.yellow(`Table "${tableName}" not found or has no columns`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(chalk.bold(`\nTable: ${tableName}`));
|
|
104
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
105
|
+
|
|
106
|
+
const table = new Table({
|
|
107
|
+
head: ['Column', 'Type', 'Nullable', 'Default', 'Key'].map(h => chalk.cyan(h)),
|
|
108
|
+
style: { head: [], border: ['gray'] }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
for (const col of schema) {
|
|
112
|
+
table.push([
|
|
113
|
+
chalk.white(col.name),
|
|
114
|
+
chalk.gray(col.type),
|
|
115
|
+
col.nullable ? chalk.green('YES') : chalk.red('NO'),
|
|
116
|
+
col.default ? chalk.yellow(col.default) : chalk.gray('-'),
|
|
117
|
+
col.primaryKey ? chalk.magenta('PRI') : chalk.gray('-')
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(table.toString());
|
|
122
|
+
console.log();
|
|
123
|
+
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
} finally {
|
|
128
|
+
if (db) {
|
|
129
|
+
await db.disconnect();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Dump database schema
|
|
136
|
+
*/
|
|
137
|
+
export async function dumpSchema(target) {
|
|
138
|
+
let db;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const connectionString = await resolveConnection(target);
|
|
142
|
+
db = await createConnection(connectionString);
|
|
143
|
+
|
|
144
|
+
const info = await db.getInfo();
|
|
145
|
+
const tables = await db.getTables();
|
|
146
|
+
|
|
147
|
+
console.log(chalk.gray(`-- Database: ${info.database || info.path}`));
|
|
148
|
+
console.log(chalk.gray(`-- Type: ${info.type}`));
|
|
149
|
+
console.log(chalk.gray(`-- Generated: ${new Date().toISOString()}`));
|
|
150
|
+
console.log();
|
|
151
|
+
|
|
152
|
+
for (const table of tables) {
|
|
153
|
+
if (table.type !== 'table') continue;
|
|
154
|
+
|
|
155
|
+
const schema = await db.getTableSchema(table.name);
|
|
156
|
+
|
|
157
|
+
console.log(chalk.cyan(`-- Table: ${table.name}`));
|
|
158
|
+
console.log(chalk.gray(`-- Rows: ${table.rows}`));
|
|
159
|
+
|
|
160
|
+
// This is a simplified dump - real implementations would generate proper DDL
|
|
161
|
+
const cols = schema.map(c => {
|
|
162
|
+
let def = ` ${c.name} ${c.type}`;
|
|
163
|
+
if (!c.nullable) def += ' NOT NULL';
|
|
164
|
+
if (c.default) def += ` DEFAULT ${c.default}`;
|
|
165
|
+
return def;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
console.log(`CREATE TABLE ${table.name} (`);
|
|
169
|
+
console.log(cols.join(',\n'));
|
|
170
|
+
console.log(');\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
} finally {
|
|
177
|
+
if (db) {
|
|
178
|
+
await db.disconnect();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base database adapter
|
|
3
|
+
* All adapters must implement these methods
|
|
4
|
+
*/
|
|
5
|
+
export class BaseAdapter {
|
|
6
|
+
constructor(connectionString) {
|
|
7
|
+
this.connectionString = connectionString;
|
|
8
|
+
this.connection = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Connect to database
|
|
13
|
+
*/
|
|
14
|
+
async connect() {
|
|
15
|
+
throw new Error('connect() must be implemented');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Disconnect from database
|
|
20
|
+
*/
|
|
21
|
+
async disconnect() {
|
|
22
|
+
throw new Error('disconnect() must be implemented');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute a query
|
|
27
|
+
* @returns {Array} rows
|
|
28
|
+
*/
|
|
29
|
+
async query(sql, params = []) {
|
|
30
|
+
throw new Error('query() must be implemented');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute a statement (INSERT, UPDATE, DELETE)
|
|
35
|
+
* @returns {Object} { affectedRows, insertId }
|
|
36
|
+
*/
|
|
37
|
+
async execute(sql, params = []) {
|
|
38
|
+
throw new Error('execute() must be implemented');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get list of tables
|
|
43
|
+
*/
|
|
44
|
+
async getTables() {
|
|
45
|
+
throw new Error('getTables() must be implemented');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get table schema
|
|
50
|
+
*/
|
|
51
|
+
async getTableSchema(tableName) {
|
|
52
|
+
throw new Error('getTableSchema() must be implemented');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get database info (size, table count, etc.)
|
|
57
|
+
*/
|
|
58
|
+
async getInfo() {
|
|
59
|
+
throw new Error('getInfo() must be implemented');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get table row count
|
|
64
|
+
*/
|
|
65
|
+
async getRowCount(tableName) {
|
|
66
|
+
const result = await this.query(`SELECT COUNT(*) as count FROM ${tableName}`);
|
|
67
|
+
return result[0].count;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create migrations table if not exists
|
|
72
|
+
*/
|
|
73
|
+
async ensureMigrationsTable() {
|
|
74
|
+
throw new Error('ensureMigrationsTable() must be implemented');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get applied migrations
|
|
79
|
+
*/
|
|
80
|
+
async getAppliedMigrations() {
|
|
81
|
+
const rows = await this.query('SELECT * FROM mpx_migrations ORDER BY id ASC');
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Record migration as applied
|
|
87
|
+
*/
|
|
88
|
+
async recordMigration(name) {
|
|
89
|
+
await this.execute(
|
|
90
|
+
'INSERT INTO mpx_migrations (name, applied_at) VALUES (?, ?)',
|
|
91
|
+
[name, new Date().toISOString()]
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove migration record (for rollback)
|
|
97
|
+
*/
|
|
98
|
+
async removeMigration(name) {
|
|
99
|
+
await this.execute('DELETE FROM mpx_migrations WHERE name = ?', [name]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SQLiteAdapter } from './sqlite-adapter.js';
|
|
2
|
+
import { PostgresAdapter } from './postgres-adapter.js';
|
|
3
|
+
import { MySQLAdapter } from './mysql-adapter.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create database adapter from connection string
|
|
7
|
+
*/
|
|
8
|
+
export async function createConnection(connectionString) {
|
|
9
|
+
if (!connectionString) {
|
|
10
|
+
throw new Error('Connection string is required');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let adapter;
|
|
14
|
+
|
|
15
|
+
// Determine database type from connection string
|
|
16
|
+
if (connectionString.startsWith('sqlite://') || connectionString.startsWith('sqlite3://')) {
|
|
17
|
+
adapter = new SQLiteAdapter(connectionString);
|
|
18
|
+
} else if (connectionString.startsWith('postgres://') || connectionString.startsWith('postgresql://')) {
|
|
19
|
+
adapter = new PostgresAdapter(connectionString);
|
|
20
|
+
} else if (connectionString.startsWith('mysql://')) {
|
|
21
|
+
adapter = new MySQLAdapter(connectionString);
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Unsupported database type. Connection string must start with:\n` +
|
|
25
|
+
` - sqlite:// or sqlite3://\n` +
|
|
26
|
+
` - postgres:// or postgresql://\n` +
|
|
27
|
+
` - mysql://`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await adapter.connect();
|
|
32
|
+
return adapter;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Test connection
|
|
37
|
+
*/
|
|
38
|
+
export async function testConnection(connectionString) {
|
|
39
|
+
try {
|
|
40
|
+
const adapter = await createConnection(connectionString);
|
|
41
|
+
await adapter.disconnect();
|
|
42
|
+
return true;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { BaseAdapter } from './base-adapter.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MySQL adapter using mysql2
|
|
5
|
+
*/
|
|
6
|
+
export class MySQLAdapter extends BaseAdapter {
|
|
7
|
+
async connect() {
|
|
8
|
+
try {
|
|
9
|
+
const mysql = await import('mysql2/promise');
|
|
10
|
+
|
|
11
|
+
this.connection = await mysql.default.createConnection(this.connectionString);
|
|
12
|
+
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'MySQL driver not found. Install it with:\n npm install mysql2'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async disconnect() {
|
|
24
|
+
if (this.connection) {
|
|
25
|
+
await this.connection.end();
|
|
26
|
+
this.connection = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async query(sql, params = []) {
|
|
31
|
+
if (!this.connection) {
|
|
32
|
+
throw new Error('Not connected to database');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const [rows] = await this.connection.execute(sql, params);
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async execute(sql, params = []) {
|
|
40
|
+
if (!this.connection) {
|
|
41
|
+
throw new Error('Not connected to database');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const [result] = await this.connection.execute(sql, params);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
affectedRows: result.affectedRows,
|
|
48
|
+
insertId: result.insertId
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getTables() {
|
|
53
|
+
const rows = await this.query(`
|
|
54
|
+
SELECT
|
|
55
|
+
TABLE_NAME as name,
|
|
56
|
+
TABLE_TYPE as type
|
|
57
|
+
FROM information_schema.TABLES
|
|
58
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
59
|
+
ORDER BY TABLE_NAME
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
const tables = [];
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const countResult = await this.query(
|
|
65
|
+
`SELECT COUNT(*) as count FROM \`${row.name}\``
|
|
66
|
+
);
|
|
67
|
+
tables.push({
|
|
68
|
+
name: row.name,
|
|
69
|
+
type: row.type === 'BASE TABLE' ? 'table' : 'view',
|
|
70
|
+
rows: parseInt(countResult[0].count)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return tables;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getTableSchema(tableName) {
|
|
78
|
+
const rows = await this.query(`
|
|
79
|
+
SELECT
|
|
80
|
+
COLUMN_NAME as name,
|
|
81
|
+
COLUMN_TYPE as type,
|
|
82
|
+
IS_NULLABLE as nullable,
|
|
83
|
+
COLUMN_DEFAULT as \`default\`,
|
|
84
|
+
COLUMN_KEY as key_type
|
|
85
|
+
FROM information_schema.COLUMNS
|
|
86
|
+
WHERE TABLE_NAME = ?
|
|
87
|
+
AND TABLE_SCHEMA = DATABASE()
|
|
88
|
+
ORDER BY ORDINAL_POSITION
|
|
89
|
+
`, [tableName]);
|
|
90
|
+
|
|
91
|
+
return rows.map(row => ({
|
|
92
|
+
name: row.name,
|
|
93
|
+
type: row.type,
|
|
94
|
+
nullable: row.nullable === 'YES',
|
|
95
|
+
default: row.default,
|
|
96
|
+
primaryKey: row.key_type === 'PRI'
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async getInfo() {
|
|
101
|
+
const tables = await this.getTables();
|
|
102
|
+
const totalRows = tables.reduce((sum, t) => sum + t.rows, 0);
|
|
103
|
+
|
|
104
|
+
const sizeResult = await this.query(`
|
|
105
|
+
SELECT
|
|
106
|
+
SUM(data_length + index_length) as size
|
|
107
|
+
FROM information_schema.TABLES
|
|
108
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
109
|
+
`);
|
|
110
|
+
const size = parseInt(sizeResult[0].size || 0);
|
|
111
|
+
|
|
112
|
+
const dbResult = await this.query('SELECT DATABASE() as name');
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
type: 'MySQL',
|
|
116
|
+
database: dbResult[0].name,
|
|
117
|
+
size: size,
|
|
118
|
+
sizeFormatted: formatBytes(size),
|
|
119
|
+
tables: tables.length,
|
|
120
|
+
totalRows
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async ensureMigrationsTable() {
|
|
125
|
+
await this.execute(`
|
|
126
|
+
CREATE TABLE IF NOT EXISTS mpx_migrations (
|
|
127
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
128
|
+
name VARCHAR(255) NOT NULL UNIQUE,
|
|
129
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
130
|
+
)
|
|
131
|
+
`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format bytes to human readable
|
|
137
|
+
*/
|
|
138
|
+
function formatBytes(bytes) {
|
|
139
|
+
if (bytes === 0) return '0 Bytes';
|
|
140
|
+
const k = 1024;
|
|
141
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
142
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
143
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
144
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { BaseAdapter } from './base-adapter.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostgreSQL adapter using pg
|
|
5
|
+
*/
|
|
6
|
+
export class PostgresAdapter extends BaseAdapter {
|
|
7
|
+
async connect() {
|
|
8
|
+
try {
|
|
9
|
+
const pkg = await import('pg');
|
|
10
|
+
const { Client } = pkg.default || pkg;
|
|
11
|
+
|
|
12
|
+
this.connection = new Client({ connectionString: this.connectionString });
|
|
13
|
+
await this.connection.connect();
|
|
14
|
+
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'PostgreSQL driver not found. Install it with:\n npm install pg'
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async disconnect() {
|
|
26
|
+
if (this.connection) {
|
|
27
|
+
await this.connection.end();
|
|
28
|
+
this.connection = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async query(sql, params = []) {
|
|
33
|
+
if (!this.connection) {
|
|
34
|
+
throw new Error('Not connected to database');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await this.connection.query(sql, params);
|
|
38
|
+
return result.rows;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async execute(sql, params = []) {
|
|
42
|
+
if (!this.connection) {
|
|
43
|
+
throw new Error('Not connected to database');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await this.connection.query(sql, params);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
affectedRows: result.rowCount,
|
|
50
|
+
insertId: result.rows[0]?.id || null
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getTables() {
|
|
55
|
+
const rows = await this.query(`
|
|
56
|
+
SELECT
|
|
57
|
+
table_name as name,
|
|
58
|
+
table_type as type
|
|
59
|
+
FROM information_schema.tables
|
|
60
|
+
WHERE table_schema = 'public'
|
|
61
|
+
ORDER BY table_name
|
|
62
|
+
`);
|
|
63
|
+
|
|
64
|
+
const tables = [];
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
const countResult = await this.query(
|
|
67
|
+
`SELECT COUNT(*) as count FROM ${row.name}`
|
|
68
|
+
);
|
|
69
|
+
tables.push({
|
|
70
|
+
name: row.name,
|
|
71
|
+
type: row.type === 'BASE TABLE' ? 'table' : 'view',
|
|
72
|
+
rows: parseInt(countResult[0].count)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return tables;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getTableSchema(tableName) {
|
|
80
|
+
const rows = await this.query(`
|
|
81
|
+
SELECT
|
|
82
|
+
column_name as name,
|
|
83
|
+
data_type as type,
|
|
84
|
+
is_nullable as nullable,
|
|
85
|
+
column_default as "default",
|
|
86
|
+
CASE WHEN c.column_name IN (
|
|
87
|
+
SELECT kcu.column_name
|
|
88
|
+
FROM information_schema.table_constraints tc
|
|
89
|
+
JOIN information_schema.key_column_usage kcu
|
|
90
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
91
|
+
WHERE tc.table_name = $1
|
|
92
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
93
|
+
) THEN true ELSE false END as primary_key
|
|
94
|
+
FROM information_schema.columns c
|
|
95
|
+
WHERE table_name = $1
|
|
96
|
+
AND table_schema = 'public'
|
|
97
|
+
ORDER BY ordinal_position
|
|
98
|
+
`, [tableName]);
|
|
99
|
+
|
|
100
|
+
return rows.map(row => ({
|
|
101
|
+
name: row.name,
|
|
102
|
+
type: row.type,
|
|
103
|
+
nullable: row.nullable === 'YES',
|
|
104
|
+
default: row.default,
|
|
105
|
+
primaryKey: row.primary_key
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getInfo() {
|
|
110
|
+
const tables = await this.getTables();
|
|
111
|
+
const totalRows = tables.reduce((sum, t) => sum + t.rows, 0);
|
|
112
|
+
|
|
113
|
+
const sizeResult = await this.query(`
|
|
114
|
+
SELECT pg_database_size(current_database()) as size
|
|
115
|
+
`);
|
|
116
|
+
const size = parseInt(sizeResult[0].size);
|
|
117
|
+
|
|
118
|
+
const dbResult = await this.query('SELECT current_database() as name');
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
type: 'PostgreSQL',
|
|
122
|
+
database: dbResult[0].name,
|
|
123
|
+
size: size,
|
|
124
|
+
sizeFormatted: formatBytes(size),
|
|
125
|
+
tables: tables.length,
|
|
126
|
+
totalRows
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async ensureMigrationsTable() {
|
|
131
|
+
await this.execute(`
|
|
132
|
+
CREATE TABLE IF NOT EXISTS mpx_migrations (
|
|
133
|
+
id SERIAL PRIMARY KEY,
|
|
134
|
+
name VARCHAR(255) NOT NULL UNIQUE,
|
|
135
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
136
|
+
)
|
|
137
|
+
`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Format bytes to human readable
|
|
143
|
+
*/
|
|
144
|
+
function formatBytes(bytes) {
|
|
145
|
+
if (bytes === 0) return '0 Bytes';
|
|
146
|
+
const k = 1024;
|
|
147
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
148
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
149
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
150
|
+
}
|