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.
@@ -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
+ }