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,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
+ }