@dbx-app/node-core 0.4.3 → 0.4.5
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/dist/connections.d.ts +8 -0
- package/dist/connections.js +13 -0
- package/dist/database.d.ts +53 -0
- package/dist/database.js +421 -16
- package/dist/diagnostics.d.ts +4 -2
- package/dist/diagnostics.js +15 -2
- package/dist/entrypoint.d.ts +1 -0
- package/dist/entrypoint.js +20 -0
- package/dist/format.d.ts +2 -0
- package/dist/format.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/sql-safety.d.ts +2 -0
- package/dist/sql-safety.js +19 -4
- package/dist/web-backend.js +122 -0
- package/package.json +3 -1
package/dist/connections.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface ConnectionConfig {
|
|
|
17
17
|
proxy_username?: string;
|
|
18
18
|
proxy_password?: string;
|
|
19
19
|
ssl: boolean;
|
|
20
|
+
ca_cert_path?: string;
|
|
21
|
+
oracle_connection_type?: "service_name" | "sid";
|
|
22
|
+
redis_connection_mode?: "standalone" | "sentinel";
|
|
23
|
+
redis_sentinel_master?: string;
|
|
24
|
+
redis_sentinel_nodes?: string;
|
|
25
|
+
redis_sentinel_username?: string;
|
|
26
|
+
redis_sentinel_password?: string;
|
|
27
|
+
redis_sentinel_tls?: boolean;
|
|
20
28
|
}
|
|
21
29
|
export interface ConnectionStoreOptions {
|
|
22
30
|
path?: string;
|
package/dist/connections.js
CHANGED
|
@@ -53,6 +53,9 @@ export async function loadConnections(options = {}) {
|
|
|
53
53
|
config.password = getSecret(db, row.id, "password");
|
|
54
54
|
if (!config.proxy_password)
|
|
55
55
|
config.proxy_password = getSecret(db, row.id, "proxy_password");
|
|
56
|
+
if (!config.redis_sentinel_password) {
|
|
57
|
+
config.redis_sentinel_password = getSecret(db, row.id, "redis_sentinel_password");
|
|
58
|
+
}
|
|
56
59
|
configs.push(config);
|
|
57
60
|
}
|
|
58
61
|
return configs;
|
|
@@ -148,7 +151,14 @@ export async function addConnection(config) {
|
|
|
148
151
|
proxy_password: "",
|
|
149
152
|
ssl: normalized.ssl ?? false,
|
|
150
153
|
sysdba: false,
|
|
154
|
+
oracle_connection_type: normalized.oracle_connection_type ?? null,
|
|
151
155
|
connection_string: null,
|
|
156
|
+
redis_connection_mode: normalized.redis_connection_mode ?? "standalone",
|
|
157
|
+
redis_sentinel_master: normalized.redis_sentinel_master ?? "",
|
|
158
|
+
redis_sentinel_nodes: normalized.redis_sentinel_nodes ?? "",
|
|
159
|
+
redis_sentinel_username: normalized.redis_sentinel_username ?? "",
|
|
160
|
+
redis_sentinel_password: "",
|
|
161
|
+
redis_sentinel_tls: normalized.redis_sentinel_tls ?? false,
|
|
152
162
|
};
|
|
153
163
|
const configJson = JSON.stringify(full);
|
|
154
164
|
const insert = db.transaction(() => {
|
|
@@ -159,6 +169,9 @@ export async function addConnection(config) {
|
|
|
159
169
|
if (normalized.proxy_password) {
|
|
160
170
|
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "proxy_password", normalized.proxy_password);
|
|
161
171
|
}
|
|
172
|
+
if (normalized.redis_sentinel_password) {
|
|
173
|
+
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "redis_sentinel_password", normalized.redis_sentinel_password);
|
|
174
|
+
}
|
|
162
175
|
});
|
|
163
176
|
insert();
|
|
164
177
|
db.close();
|
package/dist/database.d.ts
CHANGED
|
@@ -23,3 +23,56 @@ export interface QueryOptions {
|
|
|
23
23
|
export declare function executeQuery(config: ConnectionConfig, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
|
24
24
|
export declare function listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]>;
|
|
25
25
|
export declare function describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]>;
|
|
26
|
+
export declare function mongoDocumentsToQueryResult(documents: unknown[], total: number): QueryResult;
|
|
27
|
+
export declare function inferMongoColumns(documents: unknown[]): ColumnInfo[];
|
|
28
|
+
interface MongoFindCommand {
|
|
29
|
+
collection: string;
|
|
30
|
+
filter: string;
|
|
31
|
+
skip: number;
|
|
32
|
+
limit: number;
|
|
33
|
+
sort?: string;
|
|
34
|
+
}
|
|
35
|
+
interface MongoCountDocumentsCommand {
|
|
36
|
+
collection: string;
|
|
37
|
+
filter: string;
|
|
38
|
+
}
|
|
39
|
+
interface MongoAggregateCommand {
|
|
40
|
+
collection: string;
|
|
41
|
+
pipeline: string;
|
|
42
|
+
}
|
|
43
|
+
export type MongoWriteCommand = {
|
|
44
|
+
kind: "insert";
|
|
45
|
+
collection: string;
|
|
46
|
+
docsJson: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "update";
|
|
49
|
+
collection: string;
|
|
50
|
+
filter: string;
|
|
51
|
+
update: string;
|
|
52
|
+
many: boolean;
|
|
53
|
+
} | {
|
|
54
|
+
kind: "delete";
|
|
55
|
+
collection: string;
|
|
56
|
+
filter: string;
|
|
57
|
+
many: boolean;
|
|
58
|
+
};
|
|
59
|
+
export declare function parseMongoFindCommand(input: string): MongoFindCommand | null;
|
|
60
|
+
export declare function parseMongoCountDocumentsCommand(input: string): MongoCountDocumentsCommand | null;
|
|
61
|
+
export declare function parseMongoAggregateCommand(input: string): MongoAggregateCommand | null;
|
|
62
|
+
export declare function mongoAggregateWriteStage(pipelineJson: string): "$out" | "$merge" | null;
|
|
63
|
+
export declare function parseMongoWriteCommand(input: string): MongoWriteCommand | null;
|
|
64
|
+
export declare function evaluateMongoWriteSafety(command: MongoWriteCommand, options: {
|
|
65
|
+
allowWrites?: boolean;
|
|
66
|
+
allowDangerous?: boolean;
|
|
67
|
+
}): {
|
|
68
|
+
allowed: boolean;
|
|
69
|
+
reason?: string;
|
|
70
|
+
};
|
|
71
|
+
export declare function evaluateMongoAggregateSafety(command: MongoAggregateCommand, options: {
|
|
72
|
+
allowWrites?: boolean;
|
|
73
|
+
allowDangerous?: boolean;
|
|
74
|
+
}): {
|
|
75
|
+
allowed: boolean;
|
|
76
|
+
reason?: string;
|
|
77
|
+
};
|
|
78
|
+
export {};
|
package/dist/database.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import Database from "better-sqlite3";
|
|
6
6
|
import { sqlSafetyFromEnv } from "./sql-safety.js";
|
|
7
|
+
import { isDirectQueryType } from "./diagnostics.js";
|
|
7
8
|
const MAX_ROWS = 100;
|
|
8
9
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
9
10
|
const QUERY_TIMEOUT_MS = 30_000;
|
|
@@ -194,19 +195,6 @@ function portBytes(port) {
|
|
|
194
195
|
function isMysqlType(dbType) {
|
|
195
196
|
return dbType === "mysql" || dbType === "doris" || dbType === "starrocks";
|
|
196
197
|
}
|
|
197
|
-
function isDirectType(dbType) {
|
|
198
|
-
switch (dbType) {
|
|
199
|
-
case "postgres":
|
|
200
|
-
case "redshift":
|
|
201
|
-
case "mysql":
|
|
202
|
-
case "doris":
|
|
203
|
-
case "starrocks":
|
|
204
|
-
case "sqlite":
|
|
205
|
-
return true;
|
|
206
|
-
default:
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
198
|
function bridgeAppDataDir() {
|
|
211
199
|
const home = homedir();
|
|
212
200
|
switch (platform()) {
|
|
@@ -337,7 +325,36 @@ function sqliteQuery(config, sql, options) {
|
|
|
337
325
|
}
|
|
338
326
|
}
|
|
339
327
|
export async function executeQuery(config, sql, options) {
|
|
340
|
-
if (
|
|
328
|
+
if (config.db_type === "mongodb") {
|
|
329
|
+
const find = parseMongoFindCommand(sql);
|
|
330
|
+
if (find) {
|
|
331
|
+
const result = await withTimeout(mongoFindDocuments(config, find.collection, find.skip, find.limit, find.filter, find.sort), resolveTimeoutMs(options));
|
|
332
|
+
return mongoDocumentsToQueryResult(result.documents.slice(0, resolveMaxRows(options)), result.total);
|
|
333
|
+
}
|
|
334
|
+
const count = parseMongoCountDocumentsCommand(sql);
|
|
335
|
+
if (count) {
|
|
336
|
+
const result = await withTimeout(mongoFindDocuments(config, count.collection, 0, 1, count.filter), resolveTimeoutMs(options));
|
|
337
|
+
return { columns: ["count"], rows: [{ count: result.total }], row_count: 1 };
|
|
338
|
+
}
|
|
339
|
+
const aggregate = parseMongoAggregateCommand(sql);
|
|
340
|
+
if (aggregate) {
|
|
341
|
+
const safety = evaluateMongoAggregateSafety(aggregate, sqlSafetyFromEnv());
|
|
342
|
+
if (!safety.allowed)
|
|
343
|
+
throw new Error(safety.reason);
|
|
344
|
+
const result = await withTimeout(mongoAggregateDocuments(config, aggregate.collection, aggregate.pipeline, resolveMaxRows(options)), resolveTimeoutMs(options));
|
|
345
|
+
return mongoDocumentsToQueryResult(result.documents.slice(0, resolveMaxRows(options)), result.total);
|
|
346
|
+
}
|
|
347
|
+
const write = parseMongoWriteCommand(sql);
|
|
348
|
+
if (write) {
|
|
349
|
+
const safety = evaluateMongoWriteSafety(write, sqlSafetyFromEnv());
|
|
350
|
+
if (!safety.allowed)
|
|
351
|
+
throw new Error(safety.reason);
|
|
352
|
+
const affected = await withTimeout(executeMongoWrite(config, write), resolveTimeoutMs(options));
|
|
353
|
+
return { columns: [], rows: [], row_count: affected };
|
|
354
|
+
}
|
|
355
|
+
throw new Error("Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.projects.countDocuments({}), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})");
|
|
356
|
+
}
|
|
357
|
+
if (isDirectQueryType(config.db_type)) {
|
|
341
358
|
return query(config, sql, undefined, options);
|
|
342
359
|
}
|
|
343
360
|
const result = await withTimeout(bridgeDataRequest("/data/execute-query", {
|
|
@@ -348,11 +365,19 @@ export async function executeQuery(config, sql, options) {
|
|
|
348
365
|
return convertBridgeQueryResult(result, options);
|
|
349
366
|
}
|
|
350
367
|
export async function listTables(config, schema) {
|
|
368
|
+
if (config.db_type === "mongodb") {
|
|
369
|
+
const collections = await bridgeDataRequest("/data/mongo/list-collections", {
|
|
370
|
+
connection_name: config.name,
|
|
371
|
+
database: config.database || "",
|
|
372
|
+
schema: schema || "",
|
|
373
|
+
});
|
|
374
|
+
return collections.map((name) => ({ name, type: "COLLECTION" }));
|
|
375
|
+
}
|
|
351
376
|
if (config.db_type === "sqlite") {
|
|
352
377
|
const result = await query(config, `SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
353
378
|
return result.rows.map((r) => ({ name: String(r.name || ""), type: String(r.type || "table") }));
|
|
354
379
|
}
|
|
355
|
-
if (!
|
|
380
|
+
if (!isDirectQueryType(config.db_type)) {
|
|
356
381
|
const tables = await bridgeDataRequest("/data/list-tables", {
|
|
357
382
|
connection_name: config.name,
|
|
358
383
|
database: config.database || "",
|
|
@@ -370,6 +395,10 @@ export async function listTables(config, schema) {
|
|
|
370
395
|
return result.rows.map((r) => ({ name: String(r.name || r.NAME), type: String(r.type || r.TYPE || "TABLE") }));
|
|
371
396
|
}
|
|
372
397
|
export async function describeTable(config, table, schema) {
|
|
398
|
+
if (config.db_type === "mongodb") {
|
|
399
|
+
const result = await mongoFindDocuments(config, table, 0, 20, "{}");
|
|
400
|
+
return inferMongoColumns(result.documents);
|
|
401
|
+
}
|
|
373
402
|
if (config.db_type === "sqlite") {
|
|
374
403
|
const result = await query(config, `PRAGMA table_info(${quoteSqliteIdentifier(table)})`);
|
|
375
404
|
return result.rows.map((r) => ({
|
|
@@ -381,7 +410,7 @@ export async function describeTable(config, table, schema) {
|
|
|
381
410
|
comment: null,
|
|
382
411
|
}));
|
|
383
412
|
}
|
|
384
|
-
if (!
|
|
413
|
+
if (!isDirectQueryType(config.db_type)) {
|
|
385
414
|
const columns = await bridgeDataRequest("/data/describe-table", {
|
|
386
415
|
connection_name: config.name,
|
|
387
416
|
database: config.database || "",
|
|
@@ -413,3 +442,379 @@ export async function describeTable(config, table, schema) {
|
|
|
413
442
|
comment: r.comment != null ? String(r.comment) : null,
|
|
414
443
|
}));
|
|
415
444
|
}
|
|
445
|
+
async function mongoFindDocuments(config, collection, skip, limit, filter, sort) {
|
|
446
|
+
return bridgeDataRequest("/data/mongo/find-documents", {
|
|
447
|
+
connection_name: config.name,
|
|
448
|
+
database: config.database || "",
|
|
449
|
+
collection,
|
|
450
|
+
skip,
|
|
451
|
+
limit,
|
|
452
|
+
filter,
|
|
453
|
+
sort,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async function executeMongoWrite(config, command) {
|
|
457
|
+
if (command.kind === "insert") {
|
|
458
|
+
const result = await bridgeDataRequest("/data/mongo/insert-documents", {
|
|
459
|
+
connection_name: config.name,
|
|
460
|
+
database: config.database || "",
|
|
461
|
+
collection: command.collection,
|
|
462
|
+
docs_json: command.docsJson,
|
|
463
|
+
});
|
|
464
|
+
return result.affected_rows;
|
|
465
|
+
}
|
|
466
|
+
if (command.kind === "update") {
|
|
467
|
+
const result = await bridgeDataRequest("/data/mongo/update-documents", {
|
|
468
|
+
connection_name: config.name,
|
|
469
|
+
database: config.database || "",
|
|
470
|
+
collection: command.collection,
|
|
471
|
+
filter_json: command.filter,
|
|
472
|
+
update_json: command.update,
|
|
473
|
+
many: command.many,
|
|
474
|
+
});
|
|
475
|
+
return result.affected_rows;
|
|
476
|
+
}
|
|
477
|
+
const result = await bridgeDataRequest("/data/mongo/delete-documents", {
|
|
478
|
+
connection_name: config.name,
|
|
479
|
+
database: config.database || "",
|
|
480
|
+
collection: command.collection,
|
|
481
|
+
filter_json: command.filter,
|
|
482
|
+
many: command.many,
|
|
483
|
+
});
|
|
484
|
+
return result.affected_rows;
|
|
485
|
+
}
|
|
486
|
+
async function mongoAggregateDocuments(config, collection, pipelineJson, maxRows) {
|
|
487
|
+
return bridgeDataRequest("/data/mongo/aggregate-documents", {
|
|
488
|
+
connection_name: config.name,
|
|
489
|
+
database: config.database || "",
|
|
490
|
+
collection,
|
|
491
|
+
pipeline_json: pipelineJson,
|
|
492
|
+
max_rows: maxRows,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
export function mongoDocumentsToQueryResult(documents, total) {
|
|
496
|
+
const columns = [];
|
|
497
|
+
for (const doc of documents) {
|
|
498
|
+
if (isRecord(doc)) {
|
|
499
|
+
for (const key of Object.keys(doc)) {
|
|
500
|
+
if (!columns.includes(key))
|
|
501
|
+
columns.push(key);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else if (!columns.includes("value")) {
|
|
505
|
+
columns.push("value");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const rows = documents.map((doc) => {
|
|
509
|
+
const row = {};
|
|
510
|
+
for (const column of columns) {
|
|
511
|
+
row[column] = isRecord(doc) ? toCellValue(doc[column]) : column === "value" ? toCellValue(doc) : null;
|
|
512
|
+
}
|
|
513
|
+
return row;
|
|
514
|
+
});
|
|
515
|
+
return { columns, rows, row_count: rows.length };
|
|
516
|
+
}
|
|
517
|
+
export function inferMongoColumns(documents) {
|
|
518
|
+
const columns = new Map();
|
|
519
|
+
for (const doc of documents) {
|
|
520
|
+
if (!isRecord(doc)) {
|
|
521
|
+
const entry = columns.get("value") ?? { types: new Set(), nullable: false };
|
|
522
|
+
entry.types.add(mongoTypeName(doc));
|
|
523
|
+
columns.set("value", entry);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
for (const [name, value] of Object.entries(doc)) {
|
|
527
|
+
const entry = columns.get(name) ?? { types: new Set(), nullable: false };
|
|
528
|
+
entry.types.add(mongoTypeName(value));
|
|
529
|
+
if (value === null || value === undefined)
|
|
530
|
+
entry.nullable = true;
|
|
531
|
+
columns.set(name, entry);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return Array.from(columns.entries()).map(([name, entry]) => ({
|
|
535
|
+
name,
|
|
536
|
+
data_type: Array.from(entry.types).sort().join(" | ") || "unknown",
|
|
537
|
+
is_nullable: entry.nullable,
|
|
538
|
+
column_default: null,
|
|
539
|
+
is_primary_key: name === "_id",
|
|
540
|
+
comment: null,
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
export function parseMongoFindCommand(input) {
|
|
544
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
545
|
+
const target = parseCollectionMethodTarget(source, "find");
|
|
546
|
+
if (!target)
|
|
547
|
+
return null;
|
|
548
|
+
const findOpenIndex = source.indexOf("(", target.methodCallIndex);
|
|
549
|
+
const findCloseIndex = findMatchingParen(source, findOpenIndex);
|
|
550
|
+
if (findCloseIndex < 0)
|
|
551
|
+
return null;
|
|
552
|
+
const findArgs = splitTopLevel(source.slice(findOpenIndex + 1, findCloseIndex));
|
|
553
|
+
const filter = normalizeJsonArgument(findArgs[0] || "{}");
|
|
554
|
+
if (!filter)
|
|
555
|
+
return null;
|
|
556
|
+
const chain = source.slice(findCloseIndex + 1).trim();
|
|
557
|
+
if (chain && !chain.startsWith("."))
|
|
558
|
+
return null;
|
|
559
|
+
const sortArg = readChainedCallArgument(chain, "sort");
|
|
560
|
+
let sort;
|
|
561
|
+
if (sortArg !== undefined) {
|
|
562
|
+
const parsedSort = normalizeJsonArgument(sortArg);
|
|
563
|
+
if (!parsedSort)
|
|
564
|
+
return null;
|
|
565
|
+
sort = parsedSort;
|
|
566
|
+
}
|
|
567
|
+
const skip = readChainedIntegerArgument(chain, "skip", 0);
|
|
568
|
+
const limit = readChainedIntegerArgument(chain, "limit", MAX_ROWS);
|
|
569
|
+
if (skip === null || limit === null)
|
|
570
|
+
return null;
|
|
571
|
+
return { collection: target.collection, filter, skip, limit, sort };
|
|
572
|
+
}
|
|
573
|
+
export function parseMongoCountDocumentsCommand(input) {
|
|
574
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
575
|
+
const target = parseCollectionMethodTarget(source, "countDocuments");
|
|
576
|
+
if (!target)
|
|
577
|
+
return null;
|
|
578
|
+
const openIndex = source.indexOf("(", target.methodCallIndex);
|
|
579
|
+
const closeIndex = findMatchingParen(source, openIndex);
|
|
580
|
+
if (closeIndex < 0 || source.slice(closeIndex + 1).trim())
|
|
581
|
+
return null;
|
|
582
|
+
const args = splitTopLevel(source.slice(openIndex + 1, closeIndex));
|
|
583
|
+
if (args.length > 1 && args.slice(1).some((arg) => arg.trim()))
|
|
584
|
+
return null;
|
|
585
|
+
const filter = normalizeJsonArgument(args[0] || "{}");
|
|
586
|
+
return filter ? { collection: target.collection, filter } : null;
|
|
587
|
+
}
|
|
588
|
+
export function parseMongoAggregateCommand(input) {
|
|
589
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
590
|
+
const target = parseCollectionMethodTarget(source, "aggregate");
|
|
591
|
+
if (!target)
|
|
592
|
+
return null;
|
|
593
|
+
const args = parseMethodArgs(source, target.methodCallIndex);
|
|
594
|
+
if (!args || args.length !== 1)
|
|
595
|
+
return null;
|
|
596
|
+
const pipeline = normalizeJsonArgument(args[0]);
|
|
597
|
+
if (!pipeline)
|
|
598
|
+
return null;
|
|
599
|
+
return Array.isArray(JSON.parse(pipeline)) ? { collection: target.collection, pipeline } : null;
|
|
600
|
+
}
|
|
601
|
+
export function mongoAggregateWriteStage(pipelineJson) {
|
|
602
|
+
try {
|
|
603
|
+
const pipeline = JSON.parse(pipelineJson);
|
|
604
|
+
if (!Array.isArray(pipeline))
|
|
605
|
+
return null;
|
|
606
|
+
for (const stage of pipeline) {
|
|
607
|
+
if (!isRecord(stage))
|
|
608
|
+
continue;
|
|
609
|
+
if (Object.prototype.hasOwnProperty.call(stage, "$out"))
|
|
610
|
+
return "$out";
|
|
611
|
+
if (Object.prototype.hasOwnProperty.call(stage, "$merge"))
|
|
612
|
+
return "$merge";
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
export function parseMongoWriteCommand(input) {
|
|
621
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
622
|
+
const insertOne = parseCollectionMethodTarget(source, "insertOne");
|
|
623
|
+
if (insertOne) {
|
|
624
|
+
const args = parseMethodArgs(source, insertOne.methodCallIndex);
|
|
625
|
+
if (!args || args.length !== 1)
|
|
626
|
+
return null;
|
|
627
|
+
const doc = normalizeJsonArgument(args[0]);
|
|
628
|
+
return doc ? { kind: "insert", collection: insertOne.collection, docsJson: doc } : null;
|
|
629
|
+
}
|
|
630
|
+
const insertMany = parseCollectionMethodTarget(source, "insertMany");
|
|
631
|
+
if (insertMany) {
|
|
632
|
+
const args = parseMethodArgs(source, insertMany.methodCallIndex);
|
|
633
|
+
if (!args || args.length !== 1)
|
|
634
|
+
return null;
|
|
635
|
+
const docs = normalizeJsonArgument(args[0]);
|
|
636
|
+
if (!docs)
|
|
637
|
+
return null;
|
|
638
|
+
return Array.isArray(JSON.parse(docs)) ? { kind: "insert", collection: insertMany.collection, docsJson: docs } : null;
|
|
639
|
+
}
|
|
640
|
+
for (const method of ["updateOne", "updateMany"]) {
|
|
641
|
+
const target = parseCollectionMethodTarget(source, method);
|
|
642
|
+
if (!target)
|
|
643
|
+
continue;
|
|
644
|
+
const args = parseMethodArgs(source, target.methodCallIndex);
|
|
645
|
+
if (!args || args.length !== 2)
|
|
646
|
+
return null;
|
|
647
|
+
const filter = normalizeJsonArgument(args[0]);
|
|
648
|
+
const update = normalizeJsonArgument(args[1]);
|
|
649
|
+
if (!filter || !update)
|
|
650
|
+
return null;
|
|
651
|
+
return { kind: "update", collection: target.collection, filter, update, many: method === "updateMany" };
|
|
652
|
+
}
|
|
653
|
+
for (const method of ["deleteOne", "deleteMany"]) {
|
|
654
|
+
const target = parseCollectionMethodTarget(source, method);
|
|
655
|
+
if (!target)
|
|
656
|
+
continue;
|
|
657
|
+
const args = parseMethodArgs(source, target.methodCallIndex);
|
|
658
|
+
if (!args || args.length !== 1)
|
|
659
|
+
return null;
|
|
660
|
+
const filter = normalizeJsonArgument(args[0]);
|
|
661
|
+
if (!filter)
|
|
662
|
+
return null;
|
|
663
|
+
return { kind: "delete", collection: target.collection, filter, many: method === "deleteMany" };
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
export function evaluateMongoWriteSafety(command, options) {
|
|
668
|
+
if (!options.allowWrites) {
|
|
669
|
+
return {
|
|
670
|
+
allowed: false,
|
|
671
|
+
reason: "MCP MongoDB execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.",
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (!options.allowDangerous && (command.kind === "update" || command.kind === "delete") && isEmptyJsonObject(command.filter)) {
|
|
675
|
+
return {
|
|
676
|
+
allowed: false,
|
|
677
|
+
reason: "MongoDB update/delete commands must include a non-empty filter unless DBX_MCP_ALLOW_DANGEROUS_SQL=1 is set.",
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return { allowed: true };
|
|
681
|
+
}
|
|
682
|
+
export function evaluateMongoAggregateSafety(command, options) {
|
|
683
|
+
const writeStage = mongoAggregateWriteStage(command.pipeline);
|
|
684
|
+
if (!writeStage)
|
|
685
|
+
return { allowed: true };
|
|
686
|
+
if (!options.allowWrites) {
|
|
687
|
+
return {
|
|
688
|
+
allowed: false,
|
|
689
|
+
reason: `MongoDB aggregate stage "${writeStage}" writes data. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.`,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (!options.allowDangerous) {
|
|
693
|
+
return {
|
|
694
|
+
allowed: false,
|
|
695
|
+
reason: `MongoDB aggregate stage "${writeStage}" is dangerous. Set DBX_MCP_ALLOW_DANGEROUS_SQL=1 to allow it.`,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
return { allowed: true };
|
|
699
|
+
}
|
|
700
|
+
function parseCollectionMethodTarget(source, method) {
|
|
701
|
+
const direct = new RegExp(`^db\\.([A-Za-z_$][\\w$]*)\\.${method}\\s*\\(`).exec(source);
|
|
702
|
+
if (direct)
|
|
703
|
+
return { collection: direct[1], methodCallIndex: source.indexOf(`.${method}`) };
|
|
704
|
+
const quoted = new RegExp(`^db\\.getCollection\\(\\s*(['"])([^'"]+)\\1\\s*\\)\\.${method}\\s*\\(`).exec(source);
|
|
705
|
+
if (quoted)
|
|
706
|
+
return { collection: quoted[2], methodCallIndex: source.indexOf(`.${method}`) };
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
function parseMethodArgs(source, methodCallIndex) {
|
|
710
|
+
const openIndex = source.indexOf("(", methodCallIndex);
|
|
711
|
+
const closeIndex = findMatchingParen(source, openIndex);
|
|
712
|
+
if (closeIndex < 0 || source.slice(closeIndex + 1).trim())
|
|
713
|
+
return null;
|
|
714
|
+
return splitTopLevel(source.slice(openIndex + 1, closeIndex));
|
|
715
|
+
}
|
|
716
|
+
function readChainedCallArgument(chain, method) {
|
|
717
|
+
const pattern = new RegExp(`\\.${method}\\s*\\(`, "g");
|
|
718
|
+
const match = pattern.exec(chain);
|
|
719
|
+
if (!match)
|
|
720
|
+
return undefined;
|
|
721
|
+
const openIndex = match.index + match[0].lastIndexOf("(");
|
|
722
|
+
const closeIndex = findMatchingParen(chain, openIndex);
|
|
723
|
+
return closeIndex < 0 ? undefined : chain.slice(openIndex + 1, closeIndex);
|
|
724
|
+
}
|
|
725
|
+
function readChainedIntegerArgument(chain, method, fallback) {
|
|
726
|
+
const arg = readChainedCallArgument(chain, method);
|
|
727
|
+
if (arg === undefined)
|
|
728
|
+
return fallback;
|
|
729
|
+
if (!/^\d+$/.test(arg.trim()))
|
|
730
|
+
return null;
|
|
731
|
+
return Number(arg.trim());
|
|
732
|
+
}
|
|
733
|
+
function normalizeJsonArgument(arg) {
|
|
734
|
+
const value = (arg.trim() || "{}").replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}');
|
|
735
|
+
try {
|
|
736
|
+
JSON.parse(value);
|
|
737
|
+
return value;
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function isEmptyJsonObject(json) {
|
|
744
|
+
try {
|
|
745
|
+
const parsed = JSON.parse(json);
|
|
746
|
+
return isRecord(parsed) && Object.keys(parsed).length === 0;
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function splitTopLevel(source) {
|
|
753
|
+
const parts = [];
|
|
754
|
+
let depth = 0;
|
|
755
|
+
let start = 0;
|
|
756
|
+
let quote = null;
|
|
757
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
758
|
+
const ch = source[i];
|
|
759
|
+
if (quote) {
|
|
760
|
+
if (ch === "\\" && i + 1 < source.length)
|
|
761
|
+
i += 1;
|
|
762
|
+
else if (ch === quote)
|
|
763
|
+
quote = null;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (ch === "'" || ch === '"')
|
|
767
|
+
quote = ch;
|
|
768
|
+
else if (ch === "{" || ch === "[" || ch === "(")
|
|
769
|
+
depth += 1;
|
|
770
|
+
else if (ch === "}" || ch === "]" || ch === ")")
|
|
771
|
+
depth -= 1;
|
|
772
|
+
else if (ch === "," && depth === 0) {
|
|
773
|
+
parts.push(source.slice(start, i));
|
|
774
|
+
start = i + 1;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
parts.push(source.slice(start));
|
|
778
|
+
return parts;
|
|
779
|
+
}
|
|
780
|
+
function findMatchingParen(source, openIndex) {
|
|
781
|
+
if (openIndex < 0 || source[openIndex] !== "(")
|
|
782
|
+
return -1;
|
|
783
|
+
let depth = 0;
|
|
784
|
+
let quote = null;
|
|
785
|
+
for (let i = openIndex; i < source.length; i += 1) {
|
|
786
|
+
const ch = source[i];
|
|
787
|
+
if (quote) {
|
|
788
|
+
if (ch === "\\" && i + 1 < source.length)
|
|
789
|
+
i += 1;
|
|
790
|
+
else if (ch === quote)
|
|
791
|
+
quote = null;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (ch === "'" || ch === '"')
|
|
795
|
+
quote = ch;
|
|
796
|
+
else if (ch === "(")
|
|
797
|
+
depth += 1;
|
|
798
|
+
else if (ch === ")") {
|
|
799
|
+
depth -= 1;
|
|
800
|
+
if (depth === 0)
|
|
801
|
+
return i;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return -1;
|
|
805
|
+
}
|
|
806
|
+
function isRecord(value) {
|
|
807
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
808
|
+
}
|
|
809
|
+
function mongoTypeName(value) {
|
|
810
|
+
if (value === null || value === undefined)
|
|
811
|
+
return "null";
|
|
812
|
+
if (Array.isArray(value))
|
|
813
|
+
return "array";
|
|
814
|
+
if (isRecord(value))
|
|
815
|
+
return "object";
|
|
816
|
+
return typeof value;
|
|
817
|
+
}
|
|
818
|
+
function toCellValue(value) {
|
|
819
|
+
return typeof value === "object" && value !== null ? JSON.stringify(value) : value;
|
|
820
|
+
}
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite"];
|
|
2
|
-
export
|
|
1
|
+
export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
|
|
2
|
+
export type DirectQueryType = (typeof DIRECT_QUERY_TYPES)[number];
|
|
3
|
+
export declare function isDirectQueryType(dbType: string): dbType is DirectQueryType;
|
|
4
|
+
export declare const BRIDGE_REQUIRED_TYPES: readonly ["redis", "mongodb", "duckdb", "clickhouse", "sqlserver", "oracle", "elasticsearch", "dameng", "kingbase", "highgo", "vastbase", "goldendb", "yashandb", "databricks", "saphana", "teradata", "vertica", "firebird", "exasol", "oceanbase-oracle", "gbase", "tdengine", "h2", "snowflake", "trino", "hive", "db2", "informix", "neo4j", "cassandra", "bigquery", "kylin", "sundb", "xugu", "jdbc", "access"];
|
|
3
5
|
export interface DbxDiagnostics {
|
|
4
6
|
appDataDir: string;
|
|
5
7
|
dbPath: string;
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
2
|
import { bridgePortFilePath, dbPath, appDataDir } from "./paths.js";
|
|
3
3
|
import { inspectConnectionStore } from "./connections.js";
|
|
4
|
-
export const DIRECT_QUERY_TYPES = ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite"];
|
|
4
|
+
export const DIRECT_QUERY_TYPES = ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
|
|
5
|
+
const DIRECT_QUERY_TYPE_SET = new Set(DIRECT_QUERY_TYPES);
|
|
6
|
+
export function isDirectQueryType(dbType) {
|
|
7
|
+
return DIRECT_QUERY_TYPE_SET.has(dbType);
|
|
8
|
+
}
|
|
5
9
|
export const BRIDGE_REQUIRED_TYPES = [
|
|
6
10
|
"redis",
|
|
7
11
|
"mongodb",
|
|
@@ -15,7 +19,15 @@ export const BRIDGE_REQUIRED_TYPES = [
|
|
|
15
19
|
"highgo",
|
|
16
20
|
"vastbase",
|
|
17
21
|
"goldendb",
|
|
18
|
-
"
|
|
22
|
+
"yashandb",
|
|
23
|
+
"databricks",
|
|
24
|
+
"saphana",
|
|
25
|
+
"teradata",
|
|
26
|
+
"vertica",
|
|
27
|
+
"firebird",
|
|
28
|
+
"exasol",
|
|
29
|
+
"oceanbase-oracle",
|
|
30
|
+
"gbase",
|
|
19
31
|
"tdengine",
|
|
20
32
|
"h2",
|
|
21
33
|
"snowflake",
|
|
@@ -28,6 +40,7 @@ export const BRIDGE_REQUIRED_TYPES = [
|
|
|
28
40
|
"bigquery",
|
|
29
41
|
"kylin",
|
|
30
42
|
"sundb",
|
|
43
|
+
"xugu",
|
|
31
44
|
"jdbc",
|
|
32
45
|
"access",
|
|
33
46
|
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isMainModule(moduleUrl: string, argvPath: string | undefined): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { normalize, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export function isMainModule(moduleUrl, argvPath) {
|
|
5
|
+
if (!argvPath)
|
|
6
|
+
return false;
|
|
7
|
+
return normalizeEntryPath(fileURLToPath(moduleUrl)) === normalizeEntryPath(argvPath);
|
|
8
|
+
}
|
|
9
|
+
function normalizeEntryPath(path) {
|
|
10
|
+
const normalized = normalize(realpathIfPossible(resolve(path)));
|
|
11
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
12
|
+
}
|
|
13
|
+
function realpathIfPossible(path) {
|
|
14
|
+
try {
|
|
15
|
+
return realpathSync.native(path);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/format.d.ts
ADDED
package/dist/format.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function mdTable(headers, rows) {
|
|
2
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length), 3));
|
|
3
|
+
const header = `| ${headers.map((h, i) => h.padEnd(widths[i])).join(" | ")} |`;
|
|
4
|
+
const sep = `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`;
|
|
5
|
+
const body = rows.map((r) => `| ${r.map((c, i) => (c || "").padEnd(widths[i])).join(" | ")} |`).join("\n");
|
|
6
|
+
return body ? `${header}\n${sep}\n${body}` : `${header}\n${sep}`;
|
|
7
|
+
}
|
|
8
|
+
export function formatCell(value) {
|
|
9
|
+
if (value === null || value === undefined)
|
|
10
|
+
return "NULL";
|
|
11
|
+
if (typeof value === "object")
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export * from "./bridge.js";
|
|
|
3
3
|
export * from "./connections.js";
|
|
4
4
|
export * from "./database.js";
|
|
5
5
|
export * from "./diagnostics.js";
|
|
6
|
+
export * from "./entrypoint.js";
|
|
7
|
+
export * from "./format.js";
|
|
6
8
|
export * from "./paths.js";
|
|
7
9
|
export * from "./schema-context.js";
|
|
8
10
|
export * from "./sql-safety.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export * from "./bridge.js";
|
|
|
3
3
|
export * from "./connections.js";
|
|
4
4
|
export * from "./database.js";
|
|
5
5
|
export * from "./diagnostics.js";
|
|
6
|
+
export * from "./entrypoint.js";
|
|
7
|
+
export * from "./format.js";
|
|
6
8
|
export * from "./paths.js";
|
|
7
9
|
export * from "./schema-context.js";
|
|
8
10
|
export * from "./sql-safety.js";
|
package/dist/sql-safety.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface SqlSafetyOptions {
|
|
2
2
|
allowWrites?: boolean;
|
|
3
3
|
allowDangerous?: boolean;
|
|
4
|
+
allowMultipleStatements?: boolean;
|
|
4
5
|
}
|
|
5
6
|
export interface SqlSafetyDecision {
|
|
6
7
|
allowed: boolean;
|
|
@@ -8,3 +9,4 @@ export interface SqlSafetyDecision {
|
|
|
8
9
|
}
|
|
9
10
|
export declare function evaluateSqlSafety(sql: string, options?: SqlSafetyOptions): SqlSafetyDecision;
|
|
10
11
|
export declare function sqlSafetyFromEnv(env?: NodeJS.ProcessEnv): SqlSafetyOptions;
|
|
12
|
+
export declare function splitSqlStatements(sql: string): string[];
|
package/dist/sql-safety.js
CHANGED
|
@@ -4,9 +4,24 @@ export function evaluateSqlSafety(sql, options = {}) {
|
|
|
4
4
|
const statements = splitSqlStatements(sql);
|
|
5
5
|
if (statements.length === 0)
|
|
6
6
|
return { allowed: false, reason: "SQL is empty." };
|
|
7
|
-
if (statements.length > 1)
|
|
8
|
-
return { allowed: false, reason: "Only one SQL statement is allowed per
|
|
9
|
-
|
|
7
|
+
if (statements.length > 1 && !options.allowMultipleStatements) {
|
|
8
|
+
return { allowed: false, reason: "Only one SQL statement is allowed per query." };
|
|
9
|
+
}
|
|
10
|
+
for (let i = 0; i < statements.length; i++) {
|
|
11
|
+
const decision = evaluateSingleSqlStatementSafety(statements[i], options);
|
|
12
|
+
if (!decision.allowed && statements.length > 1) {
|
|
13
|
+
return {
|
|
14
|
+
allowed: false,
|
|
15
|
+
reason: `Statement ${i + 1}: ${decision.reason ?? "SQL blocked."}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (!decision.allowed)
|
|
19
|
+
return decision;
|
|
20
|
+
}
|
|
21
|
+
return { allowed: true };
|
|
22
|
+
}
|
|
23
|
+
function evaluateSingleSqlStatementSafety(sql, options = {}) {
|
|
24
|
+
const normalized = stripSqlCommentsAndStrings(sql).trim();
|
|
10
25
|
const firstKeyword = normalized.match(/^[a-zA-Z_]+/)?.[0]?.toLowerCase();
|
|
11
26
|
if (!firstKeyword)
|
|
12
27
|
return { allowed: false, reason: "SQL statement is not recognized." };
|
|
@@ -37,7 +52,7 @@ export function sqlSafetyFromEnv(env = process.env) {
|
|
|
37
52
|
allowDangerous: env.DBX_MCP_ALLOW_DANGEROUS_SQL === "1" || env.DBX_MCP_ALLOW_DANGEROUS_SQL === "true",
|
|
38
53
|
};
|
|
39
54
|
}
|
|
40
|
-
function splitSqlStatements(sql) {
|
|
55
|
+
export function splitSqlStatements(sql) {
|
|
41
56
|
const statements = [];
|
|
42
57
|
let current = "";
|
|
43
58
|
let quote = null;
|
package/dist/web-backend.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { evaluateMongoAggregateSafety, evaluateMongoWriteSafety, inferMongoColumns, mongoDocumentsToQueryResult, parseMongoAggregateCommand, parseMongoCountDocumentsCommand, parseMongoFindCommand, parseMongoWriteCommand, } from "./database.js";
|
|
2
|
+
import { sqlSafetyFromEnv } from "./sql-safety.js";
|
|
1
3
|
const baseUrl = process.env.DBX_WEB_URL.replace(/\/+$/, "");
|
|
2
4
|
const password = process.env.DBX_WEB_PASSWORD || "";
|
|
3
5
|
let sessionCookie = null;
|
|
@@ -73,6 +75,14 @@ async function ensureConnected(config) {
|
|
|
73
75
|
}
|
|
74
76
|
export async function listTables(config, schema) {
|
|
75
77
|
await ensureConnected(config);
|
|
78
|
+
if (config.db_type === "mongodb") {
|
|
79
|
+
const res = await apiFetch("/api/mongo/list-collections", {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: JSON.stringify({ connectionId: config.id, database: config.database || "" }),
|
|
82
|
+
});
|
|
83
|
+
const collections = (await res.json());
|
|
84
|
+
return collections.map((name) => ({ name, type: "COLLECTION" }));
|
|
85
|
+
}
|
|
76
86
|
const params = new URLSearchParams({
|
|
77
87
|
connection_id: config.id,
|
|
78
88
|
database: config.database || "",
|
|
@@ -83,6 +93,14 @@ export async function listTables(config, schema) {
|
|
|
83
93
|
}
|
|
84
94
|
export async function describeTable(config, table, schema) {
|
|
85
95
|
await ensureConnected(config);
|
|
96
|
+
if (config.db_type === "mongodb") {
|
|
97
|
+
const res = await apiFetch("/api/mongo/find-documents", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
body: JSON.stringify({ connectionId: config.id, database: config.database || "", collection: table, skip: 0, limit: 20, filter: "{}" }),
|
|
100
|
+
});
|
|
101
|
+
const result = (await res.json());
|
|
102
|
+
return inferMongoColumns(result.documents);
|
|
103
|
+
}
|
|
86
104
|
const params = new URLSearchParams({
|
|
87
105
|
connection_id: config.id,
|
|
88
106
|
database: config.database || "",
|
|
@@ -94,6 +112,68 @@ export async function describeTable(config, table, schema) {
|
|
|
94
112
|
}
|
|
95
113
|
export async function executeQuery(config, sql, options) {
|
|
96
114
|
await ensureConnected(config);
|
|
115
|
+
if (config.db_type === "mongodb") {
|
|
116
|
+
const find = parseMongoFindCommand(sql);
|
|
117
|
+
if (find) {
|
|
118
|
+
const res = await apiFetch("/api/mongo/find-documents", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
connectionId: config.id,
|
|
122
|
+
database: config.database || "",
|
|
123
|
+
collection: find.collection,
|
|
124
|
+
skip: find.skip,
|
|
125
|
+
limit: find.limit,
|
|
126
|
+
filter: find.filter,
|
|
127
|
+
sort: find.sort,
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
const result = (await res.json());
|
|
131
|
+
return mongoDocumentsToQueryResult(result.documents.slice(0, options?.maxRows ?? result.documents.length), result.total);
|
|
132
|
+
}
|
|
133
|
+
const count = parseMongoCountDocumentsCommand(sql);
|
|
134
|
+
if (count) {
|
|
135
|
+
const res = await apiFetch("/api/mongo/find-documents", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
connectionId: config.id,
|
|
139
|
+
database: config.database || "",
|
|
140
|
+
collection: count.collection,
|
|
141
|
+
skip: 0,
|
|
142
|
+
limit: 1,
|
|
143
|
+
filter: count.filter,
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
const result = (await res.json());
|
|
147
|
+
return { columns: ["count"], rows: [{ count: result.total }], row_count: 1 };
|
|
148
|
+
}
|
|
149
|
+
const aggregate = parseMongoAggregateCommand(sql);
|
|
150
|
+
if (aggregate) {
|
|
151
|
+
const safety = evaluateMongoAggregateSafety(aggregate, sqlSafetyFromEnv());
|
|
152
|
+
if (!safety.allowed)
|
|
153
|
+
throw new Error(safety.reason);
|
|
154
|
+
const res = await apiFetch("/api/mongo/aggregate-documents", {
|
|
155
|
+
method: "POST",
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
connectionId: config.id,
|
|
158
|
+
database: config.database || "",
|
|
159
|
+
collection: aggregate.collection,
|
|
160
|
+
pipelineJson: aggregate.pipeline,
|
|
161
|
+
maxRows: options?.maxRows ?? 100,
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
const result = (await res.json());
|
|
165
|
+
return mongoDocumentsToQueryResult(result.documents.slice(0, options?.maxRows ?? result.documents.length), result.total);
|
|
166
|
+
}
|
|
167
|
+
const write = parseMongoWriteCommand(sql);
|
|
168
|
+
if (write) {
|
|
169
|
+
const safety = evaluateMongoWriteSafety(write, sqlSafetyFromEnv());
|
|
170
|
+
if (!safety.allowed)
|
|
171
|
+
throw new Error(safety.reason);
|
|
172
|
+
const affected = await executeMongoWrite(config, write);
|
|
173
|
+
return { columns: [], rows: [], row_count: affected };
|
|
174
|
+
}
|
|
175
|
+
throw new Error("Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.projects.countDocuments({}), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})");
|
|
176
|
+
}
|
|
97
177
|
const res = await apiFetch("/api/query/execute", {
|
|
98
178
|
method: "POST",
|
|
99
179
|
body: JSON.stringify({
|
|
@@ -113,3 +193,45 @@ export async function executeQuery(config, sql, options) {
|
|
|
113
193
|
const limitedRows = rows.slice(0, options?.maxRows ?? rows.length);
|
|
114
194
|
return { columns: data.columns, rows: limitedRows, row_count: limitedRows.length };
|
|
115
195
|
}
|
|
196
|
+
async function executeMongoWrite(config, command) {
|
|
197
|
+
if (command.kind === "insert") {
|
|
198
|
+
const res = await apiFetch("/api/mongo/insert-documents", {
|
|
199
|
+
method: "POST",
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
connectionId: config.id,
|
|
202
|
+
database: config.database || "",
|
|
203
|
+
collection: command.collection,
|
|
204
|
+
docsJson: command.docsJson,
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
const result = (await res.json());
|
|
208
|
+
return result.affected_rows;
|
|
209
|
+
}
|
|
210
|
+
if (command.kind === "update") {
|
|
211
|
+
const res = await apiFetch("/api/mongo/update-documents", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
connectionId: config.id,
|
|
215
|
+
database: config.database || "",
|
|
216
|
+
collection: command.collection,
|
|
217
|
+
filterJson: command.filter,
|
|
218
|
+
updateJson: command.update,
|
|
219
|
+
many: command.many,
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
const result = (await res.json());
|
|
223
|
+
return result.affected_rows;
|
|
224
|
+
}
|
|
225
|
+
const res = await apiFetch("/api/mongo/delete-documents", {
|
|
226
|
+
method: "POST",
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
connectionId: config.id,
|
|
229
|
+
database: config.database || "",
|
|
230
|
+
collection: command.collection,
|
|
231
|
+
filterJson: command.filter,
|
|
232
|
+
many: command.many,
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
const result = (await res.json());
|
|
236
|
+
return result.affected_rows;
|
|
237
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbx-app/node-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Shared Node.js database and DBX connection utilities for DBX CLI and MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"./connections": "./dist/connections.js",
|
|
17
17
|
"./database": "./dist/database.js",
|
|
18
18
|
"./diagnostics": "./dist/diagnostics.js",
|
|
19
|
+
"./entrypoint": "./dist/entrypoint.js",
|
|
20
|
+
"./format": "./dist/format.js",
|
|
19
21
|
"./paths": "./dist/paths.js",
|
|
20
22
|
"./schema-context": "./dist/schema-context.js",
|
|
21
23
|
"./sql-safety": "./dist/sql-safety.js"
|