mpx-db 1.1.0 → 1.1.3

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 CHANGED
@@ -1,16 +1,26 @@
1
- MIT License
1
+ Mesaplex Dual License
2
2
 
3
3
  Copyright (c) 2026 Mesaplex
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ This software is available under two licensing options:
11
6
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
7
+ 1. FREE TIER (Personal Use)
8
+ - Limited daily usage
9
+ - Basic features only
10
+ - Personal and non-commercial use only
11
+
12
+ Permission is granted to use this software for personal, non-commercial
13
+ purposes subject to the usage limits described in the documentation.
14
+
15
+ 2. PRO LICENSE (Commercial Use)
16
+ - Unlimited usage
17
+ - All features unlocked
18
+ - JSON/CSV export
19
+ - CI/CD integration
20
+ - Commercial use allowed
21
+ - Priority support
22
+
23
+ To obtain a Pro license, visit: https://mesaplex.com
14
24
 
15
25
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
26
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpx-db",
3
- "version": "1.1.0",
3
+ "version": "1.1.3",
4
4
  "description": "Database management CLI - Connect, query, migrate, and manage databases from the terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "scripts": {
11
- "test": "node --test test/**/*.test.js",
11
+ "test": "node --test test/*.test.js",
12
12
  "test:watch": "node --test --watch test/**/*.test.js",
13
13
  "dev": "node bin/mpx-db.js"
14
14
  },
@@ -28,7 +28,7 @@
28
28
  "json-output"
29
29
  ],
30
30
  "author": "Mesaplex <support@mesaplex.com>",
31
- "license": "MIT",
31
+ "license": "SEE LICENSE IN LICENSE",
32
32
  "repository": {
33
33
  "type": "git",
34
34
  "url": "git+https://github.com/mesaplexdev/mpx-db.git"
@@ -43,9 +43,7 @@
43
43
  "better-sqlite3": "^12.6.2",
44
44
  "chalk": "^5.3.0",
45
45
  "cli-table3": "^0.6.5",
46
- "commander": "^12.1.0",
47
- "inquirer": "^10.0.0",
48
- "yaml": "^2.6.1"
46
+ "commander": "^12.1.0"
49
47
  },
50
48
  "peerDependencies": {
51
49
  "mysql2": "^3.11.5",
package/src/cli.js CHANGED
@@ -153,6 +153,78 @@ program
153
153
  .option('-o, --output <file>', 'Output file path')
154
154
  .action((target, table, options) => exportData(target, table, { ...globalOptions, ...options }));
155
155
 
156
+ // Update subcommand
157
+ program
158
+ .command('update')
159
+ .description('Check for updates and optionally install the latest version')
160
+ .option('--check', 'Only check for updates (do not install)')
161
+ .action(async (options) => {
162
+ const { checkForUpdate, performUpdate } = await import('./update.js');
163
+ const jsonMode = globalOptions.json;
164
+
165
+ try {
166
+ const info = checkForUpdate();
167
+
168
+ if (jsonMode) {
169
+ const output = {
170
+ current: info.current,
171
+ latest: info.latest,
172
+ updateAvailable: info.updateAvailable,
173
+ isGlobal: info.isGlobal
174
+ };
175
+
176
+ if (!options.check && info.updateAvailable) {
177
+ try {
178
+ const result = performUpdate(info.isGlobal);
179
+ output.updated = true;
180
+ output.newVersion = result.version;
181
+ } catch (err) {
182
+ output.updated = false;
183
+ output.error = err.message;
184
+ }
185
+ }
186
+
187
+ console.log(JSON.stringify(output, null, 2));
188
+ process.exit(0);
189
+ return;
190
+ }
191
+
192
+ // Human-readable output
193
+ if (!info.updateAvailable) {
194
+ console.log('');
195
+ console.log(chalk.green.bold(`✓ mpx-db v${info.current} is up to date`));
196
+ console.log('');
197
+ process.exit(0);
198
+ return;
199
+ }
200
+
201
+ console.log('');
202
+ console.log(chalk.yellow.bold(`⬆ Update available: v${info.current} → v${info.latest}`));
203
+
204
+ if (options.check) {
205
+ console.log(chalk.gray(`Run ${chalk.cyan('mpx-db update')} to install`));
206
+ console.log('');
207
+ process.exit(0);
208
+ return;
209
+ }
210
+
211
+ console.log(chalk.gray(`Installing v${info.latest}${info.isGlobal ? ' (global)' : ''}...`));
212
+
213
+ const result = performUpdate(info.isGlobal);
214
+ console.log(chalk.green.bold(`✓ Updated to v${result.version}`));
215
+ console.log('');
216
+ process.exit(0);
217
+ } catch (err) {
218
+ if (jsonMode) {
219
+ console.log(JSON.stringify({ error: err.message, code: 'ERR_UPDATE' }, null, 2));
220
+ } else {
221
+ console.error(chalk.red.bold('\n❌ Update check failed:'), err.message);
222
+ console.error('');
223
+ }
224
+ process.exit(1);
225
+ }
226
+ });
227
+
156
228
  // MCP subcommand
157
229
  program
158
230
  .command('mcp')
@@ -120,6 +120,7 @@ export async function removeConnection(name, options = {}) {
120
120
  name,
121
121
  message: deleted ? 'Connection deleted' : 'Connection not found'
122
122
  }, null, 2));
123
+ if (!deleted) process.exit(1);
123
124
  return;
124
125
  }
125
126
 
@@ -128,5 +129,6 @@ export async function removeConnection(name, options = {}) {
128
129
  console.log(chalk.green(`✓ Deleted connection "${name}"`));
129
130
  } else {
130
131
  console.log(chalk.yellow(`Connection "${name}" not found`));
132
+ process.exit(1);
131
133
  }
132
134
  }
@@ -14,10 +14,12 @@ export async function exportData(target, tableName, options = {}) {
14
14
  db = await createConnection(connectionString);
15
15
 
16
16
  // Query all data
17
- const rows = await db.query(`SELECT * FROM ${tableName}`);
17
+ const rows = await db.query(`SELECT * FROM ${db.quoteIdentifier(tableName)}`);
18
18
 
19
19
  if (rows.length === 0) {
20
- if (!options.quiet) {
20
+ if (options.json) {
21
+ console.log(JSON.stringify({ success: true, rows: [], rowCount: 0 }, null, 2));
22
+ } else if (!options.quiet) {
21
23
  console.log(chalk.yellow('No data to export'));
22
24
  }
23
25
  return;
@@ -7,6 +7,44 @@ import { resolveConnection } from './query.js';
7
7
 
8
8
  const MIGRATIONS_DIR = './migrations';
9
9
 
10
+ /**
11
+ * Split SQL into statements on semicolons, respecting quoted strings.
12
+ */
13
+ function splitStatements(sql) {
14
+ const statements = [];
15
+ let current = '';
16
+ let inSingle = false;
17
+ let inDouble = false;
18
+
19
+ for (let i = 0; i < sql.length; i++) {
20
+ const ch = sql[i];
21
+ const prev = i > 0 ? sql[i - 1] : '';
22
+
23
+ if (ch === "'" && !inDouble && prev !== '\\') {
24
+ inSingle = !inSingle;
25
+ } else if (ch === '"' && !inSingle && prev !== '\\') {
26
+ inDouble = !inDouble;
27
+ }
28
+
29
+ if (ch === ';' && !inSingle && !inDouble) {
30
+ const trimmed = current.trim();
31
+ if (trimmed.length > 0) {
32
+ statements.push(trimmed);
33
+ }
34
+ current = '';
35
+ } else {
36
+ current += ch;
37
+ }
38
+ }
39
+
40
+ const trimmed = current.trim();
41
+ if (trimmed.length > 0) {
42
+ statements.push(trimmed);
43
+ }
44
+
45
+ return statements;
46
+ }
47
+
10
48
  /**
11
49
  * Initialize migrations directory
12
50
  */
@@ -287,17 +325,14 @@ export async function runMigrations(target, options = {}) {
287
325
  console.log(chalk.gray(`→ ${name}`));
288
326
  }
289
327
 
290
- // Execute migration (split by semicolon for multiple statements)
328
+ // Execute migration (split by semicolon, respecting quoted strings)
291
329
  // Remove comment lines first, then split
292
330
  const cleanedSQL = migration.up
293
331
  .split('\n')
294
332
  .filter(line => !line.trim().startsWith('--'))
295
333
  .join('\n');
296
334
 
297
- const statements = cleanedSQL
298
- .split(';')
299
- .map(s => s.trim())
300
- .filter(s => s.length > 0);
335
+ const statements = splitStatements(cleanedSQL);
301
336
 
302
337
  for (const statement of statements) {
303
338
  await db.execute(statement);
@@ -385,17 +420,14 @@ export async function rollbackMigration(target, options = {}) {
385
420
  console.log(chalk.cyan(`Rolling back: ${last.name}`));
386
421
  }
387
422
 
388
- // Execute rollback (split by semicolon for multiple statements)
423
+ // Execute rollback (split by semicolon, respecting quoted strings)
389
424
  // Remove comment lines first, then split
390
425
  const cleanedSQL = migration.down
391
426
  .split('\n')
392
427
  .filter(line => !line.trim().startsWith('--'))
393
428
  .join('\n');
394
429
 
395
- const statements = cleanedSQL
396
- .split(';')
397
- .map(s => s.trim())
398
- .filter(s => s.length > 0);
430
+ const statements = splitStatements(cleanedSQL);
399
431
 
400
432
  for (const statement of statements) {
401
433
  await db.execute(statement);
@@ -36,15 +36,12 @@ export async function handleQuery(target, sql, options = {}) {
36
36
  rowCount: rows.length,
37
37
  duration
38
38
  }, null, 2));
39
- } else {
39
+ } else if (!options.quiet) {
40
40
  // Display results
41
41
  if (rows.length === 0) {
42
42
  console.log(chalk.yellow('No rows returned'));
43
43
  } else {
44
44
  displayTable(rows);
45
- }
46
-
47
- if (!options.quiet) {
48
45
  console.log(chalk.gray(`\n${rows.length} row(s) in ${duration}ms`));
49
46
  }
50
47
  }
@@ -224,7 +224,7 @@ export async function dumpSchema(target, options = {}) {
224
224
  return def;
225
225
  });
226
226
 
227
- sqlOutput += `CREATE TABLE ${table.name} (\n`;
227
+ sqlOutput += `CREATE TABLE "${table.name}" (\n`;
228
228
  sqlOutput += cols.join(',\n');
229
229
  sqlOutput += '\n);\n\n';
230
230
  }
@@ -8,6 +8,22 @@ export class BaseAdapter {
8
8
  this.connection = null;
9
9
  }
10
10
 
11
+ /**
12
+ * Validate and quote a table/identifier name to prevent SQL injection.
13
+ * Rejects names with dangerous characters. Override quoteChar in subclasses.
14
+ */
15
+ quoteIdentifier(name) {
16
+ if (!name || typeof name !== 'string') {
17
+ throw new Error('Invalid identifier: must be a non-empty string');
18
+ }
19
+ // Allow alphanumeric, underscores, dots (schema.table), hyphens
20
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.\\-]*$/.test(name)) {
21
+ throw new Error(`Invalid identifier: "${name}" contains disallowed characters`);
22
+ }
23
+ const q = this._identifierQuote || '"';
24
+ return `${q}${name.replace(new RegExp(`\\${q}`, 'g'), q + q)}${q}`;
25
+ }
26
+
11
27
  /**
12
28
  * Connect to database
13
29
  */
@@ -63,7 +79,7 @@ export class BaseAdapter {
63
79
  * Get table row count
64
80
  */
65
81
  async getRowCount(tableName) {
66
- const result = await this.query(`SELECT COUNT(*) as count FROM ${tableName}`);
82
+ const result = await this.query(`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(tableName)}`);
67
83
  return result[0].count;
68
84
  }
69
85
 
@@ -4,6 +4,11 @@ import { BaseAdapter } from './base-adapter.js';
4
4
  * MySQL adapter using mysql2
5
5
  */
6
6
  export class MySQLAdapter extends BaseAdapter {
7
+ constructor(connectionString) {
8
+ super(connectionString);
9
+ this._identifierQuote = '`';
10
+ }
11
+
7
12
  async connect() {
8
13
  try {
9
14
  const mysql = await import('mysql2/promise');
@@ -62,7 +67,7 @@ export class MySQLAdapter extends BaseAdapter {
62
67
  const tables = [];
63
68
  for (const row of rows) {
64
69
  const countResult = await this.query(
65
- `SELECT COUNT(*) as count FROM \`${row.name}\``
70
+ `SELECT COUNT(*) as count FROM ${this.quoteIdentifier(row.name)}`
66
71
  );
67
72
  tables.push({
68
73
  name: row.name,
@@ -64,7 +64,7 @@ export class PostgresAdapter extends BaseAdapter {
64
64
  const tables = [];
65
65
  for (const row of rows) {
66
66
  const countResult = await this.query(
67
- `SELECT COUNT(*) as count FROM ${row.name}`
67
+ `SELECT COUNT(*) as count FROM ${this.quoteIdentifier(row.name)}`
68
68
  );
69
69
  tables.push({
70
70
  name: row.name,
@@ -127,6 +127,17 @@ export class PostgresAdapter extends BaseAdapter {
127
127
  };
128
128
  }
129
129
 
130
+ async recordMigration(name) {
131
+ await this.execute(
132
+ 'INSERT INTO mpx_migrations (name, applied_at) VALUES ($1, $2)',
133
+ [name, new Date().toISOString()]
134
+ );
135
+ }
136
+
137
+ async removeMigration(name) {
138
+ await this.execute('DELETE FROM mpx_migrations WHERE name = $1', [name]);
139
+ }
140
+
130
141
  async ensureMigrationsTable() {
131
142
  await this.execute(`
132
143
  CREATE TABLE IF NOT EXISTS mpx_migrations (
@@ -10,9 +10,9 @@ export class SQLiteAdapter extends BaseAdapter {
10
10
  super(connectionString);
11
11
 
12
12
  // Extract file path from sqlite:// or sqlite3://
13
- this.dbPath = connectionString
14
- .replace(/^sqlite3?:\/\//, '')
15
- .replace(/^\//, ''); // Remove leading slash if absolute path
13
+ // sqlite:///tmp/foo.db /tmp/foo.db (absolute)
14
+ // sqlite://mydb.db → mydb.db (relative)
15
+ this.dbPath = connectionString.replace(/^sqlite3?:\/\//, '');
16
16
  }
17
17
 
18
18
  async connect() {
@@ -76,7 +76,7 @@ export class SQLiteAdapter extends BaseAdapter {
76
76
 
77
77
  const tables = [];
78
78
  for (const row of rows) {
79
- const countResult = await this.query(`SELECT COUNT(*) as count FROM ${row.name}`);
79
+ const countResult = await this.query(`SELECT COUNT(*) as count FROM ${this.quoteIdentifier(row.name)}`);
80
80
  tables.push({
81
81
  name: row.name,
82
82
  type: row.type,
@@ -88,7 +88,7 @@ export class SQLiteAdapter extends BaseAdapter {
88
88
  }
89
89
 
90
90
  async getTableSchema(tableName) {
91
- const rows = await this.query(`PRAGMA table_info(${tableName})`);
91
+ const rows = await this.query(`PRAGMA table_info(${this.quoteIdentifier(tableName)})`);
92
92
 
93
93
  return rows.map(row => ({
94
94
  name: row.name,
package/src/mcp.js CHANGED
@@ -261,7 +261,7 @@ export async function startMCPServer() {
261
261
  const db = await createConnection(connectionString);
262
262
 
263
263
  try {
264
- const rows = await db.query(`SELECT * FROM ${args.table}`);
264
+ const rows = await db.query(`SELECT * FROM ${db.quoteIdentifier(args.table)}`);
265
265
  await db.disconnect();
266
266
 
267
267
  return {
package/src/schema.js CHANGED
@@ -560,6 +560,19 @@ export function getSchema() {
560
560
  examples: [
561
561
  { command: 'mpx-db mcp', description: 'Start MCP stdio server' }
562
562
  ]
563
+ },
564
+ update: {
565
+ description: 'Check for updates and optionally install the latest version',
566
+ usage: 'mpx-db update [--check] [--json]',
567
+ flags: {
568
+ '--check': { description: 'Only check for updates (do not install)', default: false },
569
+ '--json': { description: 'Machine-readable JSON output', default: false }
570
+ },
571
+ examples: [
572
+ { command: 'mpx-db update', description: 'Check and install updates' },
573
+ { command: 'mpx-db update --check', description: 'Just check for updates' },
574
+ { command: 'mpx-db update --check --json', description: 'Check for updates (JSON output)' }
575
+ ]
563
576
  }
564
577
  },
565
578
  mcpConfig: {
package/src/update.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Update Command
3
+ *
4
+ * Checks npm for the latest version of mpx-db and offers to update.
5
+ */
6
+
7
+ import { execSync } from 'child_process';
8
+ import { readFileSync } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
15
+
16
+ /**
17
+ * Check npm registry for latest version
18
+ * @returns {object} { current, latest, updateAvailable, isGlobal }
19
+ */
20
+ export function checkForUpdate() {
21
+ const current = pkg.version;
22
+
23
+ let latest;
24
+ try {
25
+ latest = execSync('npm view mpx-db version', { encoding: 'utf8', timeout: 10000 }).trim();
26
+ } catch (err) {
27
+ throw new Error('Failed to check npm registry: ' + (err.message || 'unknown error'));
28
+ }
29
+
30
+ const updateAvailable = latest !== current && compareVersions(latest, current) > 0;
31
+
32
+ // Detect if installed globally
33
+ let isGlobal = false;
34
+ try {
35
+ const globalDir = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
36
+ isGlobal = __dirname.startsWith(globalDir) || process.argv[1]?.includes('node_modules/.bin');
37
+ } catch {
38
+ // Can't determine, assume local
39
+ }
40
+
41
+ return { current, latest, updateAvailable, isGlobal };
42
+ }
43
+
44
+ /**
45
+ * Perform the update
46
+ * @param {boolean} isGlobal - Install globally
47
+ * @returns {object} { success, version }
48
+ */
49
+ export function performUpdate(isGlobal) {
50
+ const cmd = isGlobal ? 'npm install -g mpx-db@latest' : 'npm install mpx-db@latest';
51
+ try {
52
+ execSync(cmd, { encoding: 'utf8', timeout: 60000, stdio: 'pipe' });
53
+ // Verify
54
+ const newVersion = execSync('npm view mpx-db version', { encoding: 'utf8', timeout: 10000 }).trim();
55
+ return { success: true, version: newVersion };
56
+ } catch (err) {
57
+ throw new Error('Update failed: ' + (err.message || 'unknown error'));
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Compare semver strings. Returns >0 if a > b, <0 if a < b, 0 if equal.
63
+ */
64
+ export function compareVersions(a, b) {
65
+ const pa = a.split('.').map(Number);
66
+ const pb = b.split('.').map(Number);
67
+ for (let i = 0; i < 3; i++) {
68
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
69
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
70
+ }
71
+ return 0;
72
+ }