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,141 @@
|
|
|
1
|
+
import { BaseAdapter } from './base-adapter.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SQLite adapter using better-sqlite3
|
|
7
|
+
*/
|
|
8
|
+
export class SQLiteAdapter extends BaseAdapter {
|
|
9
|
+
constructor(connectionString) {
|
|
10
|
+
super(connectionString);
|
|
11
|
+
|
|
12
|
+
// Extract file path from sqlite:// or sqlite3://
|
|
13
|
+
this.dbPath = connectionString
|
|
14
|
+
.replace(/^sqlite3?:\/\//, '')
|
|
15
|
+
.replace(/^\//, ''); // Remove leading slash if absolute path
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async connect() {
|
|
19
|
+
try {
|
|
20
|
+
// Try to import better-sqlite3
|
|
21
|
+
const Database = (await import('better-sqlite3')).default;
|
|
22
|
+
|
|
23
|
+
// Ensure directory exists
|
|
24
|
+
const dir = path.dirname(this.dbPath);
|
|
25
|
+
if (dir && dir !== '.' && !fs.existsSync(dir)) {
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.connection = new Database(this.dbPath);
|
|
30
|
+
this.connection.pragma('journal_mode = WAL'); // Better concurrency
|
|
31
|
+
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'SQLite driver not found. Install it with:\n npm install better-sqlite3'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async disconnect() {
|
|
43
|
+
if (this.connection) {
|
|
44
|
+
this.connection.close();
|
|
45
|
+
this.connection = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async query(sql, params = []) {
|
|
50
|
+
if (!this.connection) {
|
|
51
|
+
throw new Error('Not connected to database');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stmt = this.connection.prepare(sql);
|
|
55
|
+
return stmt.all(...params);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async execute(sql, params = []) {
|
|
59
|
+
if (!this.connection) {
|
|
60
|
+
throw new Error('Not connected to database');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const stmt = this.connection.prepare(sql);
|
|
64
|
+
const result = stmt.run(...params);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
affectedRows: result.changes,
|
|
68
|
+
insertId: result.lastInsertRowid
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getTables() {
|
|
73
|
+
const rows = await this.query(
|
|
74
|
+
"SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const tables = [];
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const countResult = await this.query(`SELECT COUNT(*) as count FROM ${row.name}`);
|
|
80
|
+
tables.push({
|
|
81
|
+
name: row.name,
|
|
82
|
+
type: row.type,
|
|
83
|
+
rows: countResult[0].count
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return tables;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getTableSchema(tableName) {
|
|
91
|
+
const rows = await this.query(`PRAGMA table_info(${tableName})`);
|
|
92
|
+
|
|
93
|
+
return rows.map(row => ({
|
|
94
|
+
name: row.name,
|
|
95
|
+
type: row.type,
|
|
96
|
+
nullable: row.notnull === 0,
|
|
97
|
+
default: row.dflt_value,
|
|
98
|
+
primaryKey: row.pk === 1
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getInfo() {
|
|
103
|
+
const tables = await this.getTables();
|
|
104
|
+
const totalRows = tables.reduce((sum, t) => sum + t.rows, 0);
|
|
105
|
+
|
|
106
|
+
let size = 0;
|
|
107
|
+
if (fs.existsSync(this.dbPath)) {
|
|
108
|
+
size = fs.statSync(this.dbPath).size;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
type: 'SQLite',
|
|
113
|
+
path: this.dbPath,
|
|
114
|
+
size: size,
|
|
115
|
+
sizeFormatted: formatBytes(size),
|
|
116
|
+
tables: tables.length,
|
|
117
|
+
totalRows
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async ensureMigrationsTable() {
|
|
122
|
+
await this.execute(`
|
|
123
|
+
CREATE TABLE IF NOT EXISTS mpx_migrations (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
name TEXT NOT NULL UNIQUE,
|
|
126
|
+
applied_at TEXT NOT NULL
|
|
127
|
+
)
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format bytes to human readable
|
|
134
|
+
*/
|
|
135
|
+
function formatBytes(bytes) {
|
|
136
|
+
if (bytes === 0) return '0 Bytes';
|
|
137
|
+
const k = 1024;
|
|
138
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
139
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
140
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
141
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mpx-db - Database management CLI
|
|
3
|
+
* Main exports for programmatic usage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { createConnection, testConnection } from './db/connection.js';
|
|
7
|
+
export { SQLiteAdapter } from './db/sqlite-adapter.js';
|
|
8
|
+
export { PostgresAdapter } from './db/postgres-adapter.js';
|
|
9
|
+
export { MySQLAdapter } from './db/mysql-adapter.js';
|
|
10
|
+
export {
|
|
11
|
+
saveConnection,
|
|
12
|
+
loadConnections,
|
|
13
|
+
deleteConnection,
|
|
14
|
+
getConnection
|
|
15
|
+
} from './utils/config.js';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { encrypt, decrypt } from './crypto.js';
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = path.join(os.homedir(), '.mpx-db');
|
|
7
|
+
const CONNECTIONS_FILE = path.join(CONFIG_DIR, 'connections.json');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure config directory exists
|
|
11
|
+
*/
|
|
12
|
+
function ensureConfigDir() {
|
|
13
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
14
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load all saved connections
|
|
20
|
+
*/
|
|
21
|
+
export function loadConnections() {
|
|
22
|
+
ensureConfigDir();
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(CONNECTIONS_FILE)) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const data = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8'));
|
|
29
|
+
|
|
30
|
+
// Decrypt passwords
|
|
31
|
+
const connections = {};
|
|
32
|
+
for (const [name, conn] of Object.entries(data)) {
|
|
33
|
+
connections[name] = {
|
|
34
|
+
...conn,
|
|
35
|
+
url: conn.encrypted ? decrypt(conn.url) : conn.url
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return connections;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Save a connection
|
|
44
|
+
*/
|
|
45
|
+
export function saveConnection(name, url) {
|
|
46
|
+
ensureConfigDir();
|
|
47
|
+
|
|
48
|
+
const connections = {};
|
|
49
|
+
if (fs.existsSync(CONNECTIONS_FILE)) {
|
|
50
|
+
const data = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8'));
|
|
51
|
+
Object.assign(connections, data);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Parse URL to extract type
|
|
55
|
+
const type = url.split(':')[0];
|
|
56
|
+
|
|
57
|
+
connections[name] = {
|
|
58
|
+
type,
|
|
59
|
+
url: encrypt(url),
|
|
60
|
+
encrypted: true,
|
|
61
|
+
createdAt: new Date().toISOString()
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(CONNECTIONS_FILE, JSON.stringify(connections, null, 2), { mode: 0o600 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Delete a connection
|
|
69
|
+
*/
|
|
70
|
+
export function deleteConnection(name) {
|
|
71
|
+
ensureConfigDir();
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(CONNECTIONS_FILE)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const connections = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8'));
|
|
78
|
+
|
|
79
|
+
if (!connections[name]) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
delete connections[name];
|
|
84
|
+
fs.writeFileSync(CONNECTIONS_FILE, JSON.stringify(connections, null, 2), { mode: 0o600 });
|
|
85
|
+
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get a specific connection
|
|
91
|
+
*/
|
|
92
|
+
export function getConnection(name) {
|
|
93
|
+
const connections = loadConnections();
|
|
94
|
+
return connections[name] || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load project config (.mpx-db.yaml)
|
|
99
|
+
*/
|
|
100
|
+
export function loadProjectConfig() {
|
|
101
|
+
const configPath = path.join(process.cwd(), '.mpx-db.yaml');
|
|
102
|
+
|
|
103
|
+
if (!fs.existsSync(configPath)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// We'll parse YAML when needed, for now just check existence
|
|
108
|
+
return configPath;
|
|
109
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
7
|
+
const KEY_LENGTH = 32;
|
|
8
|
+
const IV_LENGTH = 16;
|
|
9
|
+
const TAG_LENGTH = 16;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get or create encryption key
|
|
13
|
+
*/
|
|
14
|
+
function getEncryptionKey() {
|
|
15
|
+
const configDir = path.join(os.homedir(), '.mpx-db');
|
|
16
|
+
const keyFile = path.join(configDir, '.key');
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(configDir)) {
|
|
19
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(keyFile)) {
|
|
23
|
+
return fs.readFileSync(keyFile);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Generate new key
|
|
27
|
+
const key = crypto.randomBytes(KEY_LENGTH);
|
|
28
|
+
fs.writeFileSync(keyFile, key, { mode: 0o600 });
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Encrypt a string
|
|
34
|
+
*/
|
|
35
|
+
export function encrypt(text) {
|
|
36
|
+
const key = getEncryptionKey();
|
|
37
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
38
|
+
|
|
39
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
40
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
41
|
+
encrypted += cipher.final('hex');
|
|
42
|
+
|
|
43
|
+
const authTag = cipher.getAuthTag();
|
|
44
|
+
|
|
45
|
+
// Return iv + authTag + encrypted
|
|
46
|
+
return iv.toString('hex') + authTag.toString('hex') + encrypted;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Decrypt a string
|
|
51
|
+
*/
|
|
52
|
+
export function decrypt(encrypted) {
|
|
53
|
+
const key = getEncryptionKey();
|
|
54
|
+
|
|
55
|
+
// Extract iv, authTag, and encrypted data
|
|
56
|
+
const iv = Buffer.from(encrypted.slice(0, IV_LENGTH * 2), 'hex');
|
|
57
|
+
const authTag = Buffer.from(encrypted.slice(IV_LENGTH * 2, (IV_LENGTH + TAG_LENGTH) * 2), 'hex');
|
|
58
|
+
const encryptedData = encrypted.slice((IV_LENGTH + TAG_LENGTH) * 2);
|
|
59
|
+
|
|
60
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
61
|
+
decipher.setAuthTag(authTag);
|
|
62
|
+
|
|
63
|
+
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
64
|
+
decrypted += decipher.final('utf8');
|
|
65
|
+
|
|
66
|
+
return decrypted;
|
|
67
|
+
}
|