mpx-db 1.1.2 → 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 +19 -9
- package/package.json +4 -6
- package/src/commands/connections.js +2 -0
- package/src/commands/data.js +4 -2
- package/src/commands/migrate.js +42 -10
- package/src/commands/query.js +1 -4
- package/src/commands/schema.js +1 -1
- package/src/db/base-adapter.js +17 -1
- package/src/db/mysql-adapter.js +6 -1
- package/src/db/postgres-adapter.js +12 -1
- package/src/db/sqlite-adapter.js +5 -5
- package/src/mcp.js +1 -1
package/LICENSE
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
Mesaplex Dual License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Mesaplex
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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.
|
|
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
|
|
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": "
|
|
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",
|
|
@@ -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
|
}
|
package/src/commands/data.js
CHANGED
|
@@ -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 (
|
|
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;
|
package/src/commands/migrate.js
CHANGED
|
@@ -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
|
|
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
|
|
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);
|
package/src/commands/query.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/schema.js
CHANGED
|
@@ -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
|
}
|
package/src/db/base-adapter.js
CHANGED
|
@@ -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
|
|
package/src/db/mysql-adapter.js
CHANGED
|
@@ -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
|
|
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 (
|
package/src/db/sqlite-adapter.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 {
|