@dbx-app/node-core 0.4.3 → 0.4.4
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 +417 -0
- package/dist/diagnostics.d.ts +2 -2
- package/dist/diagnostics.js +11 -2
- package/dist/format.d.ts +2 -0
- package/dist/format.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/web-backend.js +122 -0
- package/package.json +2 -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
|
@@ -337,6 +337,35 @@ function sqliteQuery(config, sql, options) {
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
export async function executeQuery(config, sql, options) {
|
|
340
|
+
if (config.db_type === "mongodb") {
|
|
341
|
+
const find = parseMongoFindCommand(sql);
|
|
342
|
+
if (find) {
|
|
343
|
+
const result = await withTimeout(mongoFindDocuments(config, find.collection, find.skip, find.limit, find.filter, find.sort), resolveTimeoutMs(options));
|
|
344
|
+
return mongoDocumentsToQueryResult(result.documents.slice(0, resolveMaxRows(options)), result.total);
|
|
345
|
+
}
|
|
346
|
+
const count = parseMongoCountDocumentsCommand(sql);
|
|
347
|
+
if (count) {
|
|
348
|
+
const result = await withTimeout(mongoFindDocuments(config, count.collection, 0, 1, count.filter), resolveTimeoutMs(options));
|
|
349
|
+
return { columns: ["count"], rows: [{ count: result.total }], row_count: 1 };
|
|
350
|
+
}
|
|
351
|
+
const aggregate = parseMongoAggregateCommand(sql);
|
|
352
|
+
if (aggregate) {
|
|
353
|
+
const safety = evaluateMongoAggregateSafety(aggregate, sqlSafetyFromEnv());
|
|
354
|
+
if (!safety.allowed)
|
|
355
|
+
throw new Error(safety.reason);
|
|
356
|
+
const result = await withTimeout(mongoAggregateDocuments(config, aggregate.collection, aggregate.pipeline, resolveMaxRows(options)), resolveTimeoutMs(options));
|
|
357
|
+
return mongoDocumentsToQueryResult(result.documents.slice(0, resolveMaxRows(options)), result.total);
|
|
358
|
+
}
|
|
359
|
+
const write = parseMongoWriteCommand(sql);
|
|
360
|
+
if (write) {
|
|
361
|
+
const safety = evaluateMongoWriteSafety(write, sqlSafetyFromEnv());
|
|
362
|
+
if (!safety.allowed)
|
|
363
|
+
throw new Error(safety.reason);
|
|
364
|
+
const affected = await withTimeout(executeMongoWrite(config, write), resolveTimeoutMs(options));
|
|
365
|
+
return { columns: [], rows: [], row_count: affected };
|
|
366
|
+
}
|
|
367
|
+
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({...})");
|
|
368
|
+
}
|
|
340
369
|
if (isDirectType(config.db_type)) {
|
|
341
370
|
return query(config, sql, undefined, options);
|
|
342
371
|
}
|
|
@@ -348,6 +377,14 @@ export async function executeQuery(config, sql, options) {
|
|
|
348
377
|
return convertBridgeQueryResult(result, options);
|
|
349
378
|
}
|
|
350
379
|
export async function listTables(config, schema) {
|
|
380
|
+
if (config.db_type === "mongodb") {
|
|
381
|
+
const collections = await bridgeDataRequest("/data/mongo/list-collections", {
|
|
382
|
+
connection_name: config.name,
|
|
383
|
+
database: config.database || "",
|
|
384
|
+
schema: schema || "",
|
|
385
|
+
});
|
|
386
|
+
return collections.map((name) => ({ name, type: "COLLECTION" }));
|
|
387
|
+
}
|
|
351
388
|
if (config.db_type === "sqlite") {
|
|
352
389
|
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
390
|
return result.rows.map((r) => ({ name: String(r.name || ""), type: String(r.type || "table") }));
|
|
@@ -370,6 +407,10 @@ export async function listTables(config, schema) {
|
|
|
370
407
|
return result.rows.map((r) => ({ name: String(r.name || r.NAME), type: String(r.type || r.TYPE || "TABLE") }));
|
|
371
408
|
}
|
|
372
409
|
export async function describeTable(config, table, schema) {
|
|
410
|
+
if (config.db_type === "mongodb") {
|
|
411
|
+
const result = await mongoFindDocuments(config, table, 0, 20, "{}");
|
|
412
|
+
return inferMongoColumns(result.documents);
|
|
413
|
+
}
|
|
373
414
|
if (config.db_type === "sqlite") {
|
|
374
415
|
const result = await query(config, `PRAGMA table_info(${quoteSqliteIdentifier(table)})`);
|
|
375
416
|
return result.rows.map((r) => ({
|
|
@@ -413,3 +454,379 @@ export async function describeTable(config, table, schema) {
|
|
|
413
454
|
comment: r.comment != null ? String(r.comment) : null,
|
|
414
455
|
}));
|
|
415
456
|
}
|
|
457
|
+
async function mongoFindDocuments(config, collection, skip, limit, filter, sort) {
|
|
458
|
+
return bridgeDataRequest("/data/mongo/find-documents", {
|
|
459
|
+
connection_name: config.name,
|
|
460
|
+
database: config.database || "",
|
|
461
|
+
collection,
|
|
462
|
+
skip,
|
|
463
|
+
limit,
|
|
464
|
+
filter,
|
|
465
|
+
sort,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async function executeMongoWrite(config, command) {
|
|
469
|
+
if (command.kind === "insert") {
|
|
470
|
+
const result = await bridgeDataRequest("/data/mongo/insert-documents", {
|
|
471
|
+
connection_name: config.name,
|
|
472
|
+
database: config.database || "",
|
|
473
|
+
collection: command.collection,
|
|
474
|
+
docs_json: command.docsJson,
|
|
475
|
+
});
|
|
476
|
+
return result.affected_rows;
|
|
477
|
+
}
|
|
478
|
+
if (command.kind === "update") {
|
|
479
|
+
const result = await bridgeDataRequest("/data/mongo/update-documents", {
|
|
480
|
+
connection_name: config.name,
|
|
481
|
+
database: config.database || "",
|
|
482
|
+
collection: command.collection,
|
|
483
|
+
filter_json: command.filter,
|
|
484
|
+
update_json: command.update,
|
|
485
|
+
many: command.many,
|
|
486
|
+
});
|
|
487
|
+
return result.affected_rows;
|
|
488
|
+
}
|
|
489
|
+
const result = await bridgeDataRequest("/data/mongo/delete-documents", {
|
|
490
|
+
connection_name: config.name,
|
|
491
|
+
database: config.database || "",
|
|
492
|
+
collection: command.collection,
|
|
493
|
+
filter_json: command.filter,
|
|
494
|
+
many: command.many,
|
|
495
|
+
});
|
|
496
|
+
return result.affected_rows;
|
|
497
|
+
}
|
|
498
|
+
async function mongoAggregateDocuments(config, collection, pipelineJson, maxRows) {
|
|
499
|
+
return bridgeDataRequest("/data/mongo/aggregate-documents", {
|
|
500
|
+
connection_name: config.name,
|
|
501
|
+
database: config.database || "",
|
|
502
|
+
collection,
|
|
503
|
+
pipeline_json: pipelineJson,
|
|
504
|
+
max_rows: maxRows,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
export function mongoDocumentsToQueryResult(documents, total) {
|
|
508
|
+
const columns = [];
|
|
509
|
+
for (const doc of documents) {
|
|
510
|
+
if (isRecord(doc)) {
|
|
511
|
+
for (const key of Object.keys(doc)) {
|
|
512
|
+
if (!columns.includes(key))
|
|
513
|
+
columns.push(key);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else if (!columns.includes("value")) {
|
|
517
|
+
columns.push("value");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const rows = documents.map((doc) => {
|
|
521
|
+
const row = {};
|
|
522
|
+
for (const column of columns) {
|
|
523
|
+
row[column] = isRecord(doc) ? toCellValue(doc[column]) : column === "value" ? toCellValue(doc) : null;
|
|
524
|
+
}
|
|
525
|
+
return row;
|
|
526
|
+
});
|
|
527
|
+
return { columns, rows, row_count: rows.length };
|
|
528
|
+
}
|
|
529
|
+
export function inferMongoColumns(documents) {
|
|
530
|
+
const columns = new Map();
|
|
531
|
+
for (const doc of documents) {
|
|
532
|
+
if (!isRecord(doc)) {
|
|
533
|
+
const entry = columns.get("value") ?? { types: new Set(), nullable: false };
|
|
534
|
+
entry.types.add(mongoTypeName(doc));
|
|
535
|
+
columns.set("value", entry);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
for (const [name, value] of Object.entries(doc)) {
|
|
539
|
+
const entry = columns.get(name) ?? { types: new Set(), nullable: false };
|
|
540
|
+
entry.types.add(mongoTypeName(value));
|
|
541
|
+
if (value === null || value === undefined)
|
|
542
|
+
entry.nullable = true;
|
|
543
|
+
columns.set(name, entry);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return Array.from(columns.entries()).map(([name, entry]) => ({
|
|
547
|
+
name,
|
|
548
|
+
data_type: Array.from(entry.types).sort().join(" | ") || "unknown",
|
|
549
|
+
is_nullable: entry.nullable,
|
|
550
|
+
column_default: null,
|
|
551
|
+
is_primary_key: name === "_id",
|
|
552
|
+
comment: null,
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
export function parseMongoFindCommand(input) {
|
|
556
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
557
|
+
const target = parseCollectionMethodTarget(source, "find");
|
|
558
|
+
if (!target)
|
|
559
|
+
return null;
|
|
560
|
+
const findOpenIndex = source.indexOf("(", target.methodCallIndex);
|
|
561
|
+
const findCloseIndex = findMatchingParen(source, findOpenIndex);
|
|
562
|
+
if (findCloseIndex < 0)
|
|
563
|
+
return null;
|
|
564
|
+
const findArgs = splitTopLevel(source.slice(findOpenIndex + 1, findCloseIndex));
|
|
565
|
+
const filter = normalizeJsonArgument(findArgs[0] || "{}");
|
|
566
|
+
if (!filter)
|
|
567
|
+
return null;
|
|
568
|
+
const chain = source.slice(findCloseIndex + 1).trim();
|
|
569
|
+
if (chain && !chain.startsWith("."))
|
|
570
|
+
return null;
|
|
571
|
+
const sortArg = readChainedCallArgument(chain, "sort");
|
|
572
|
+
let sort;
|
|
573
|
+
if (sortArg !== undefined) {
|
|
574
|
+
const parsedSort = normalizeJsonArgument(sortArg);
|
|
575
|
+
if (!parsedSort)
|
|
576
|
+
return null;
|
|
577
|
+
sort = parsedSort;
|
|
578
|
+
}
|
|
579
|
+
const skip = readChainedIntegerArgument(chain, "skip", 0);
|
|
580
|
+
const limit = readChainedIntegerArgument(chain, "limit", MAX_ROWS);
|
|
581
|
+
if (skip === null || limit === null)
|
|
582
|
+
return null;
|
|
583
|
+
return { collection: target.collection, filter, skip, limit, sort };
|
|
584
|
+
}
|
|
585
|
+
export function parseMongoCountDocumentsCommand(input) {
|
|
586
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
587
|
+
const target = parseCollectionMethodTarget(source, "countDocuments");
|
|
588
|
+
if (!target)
|
|
589
|
+
return null;
|
|
590
|
+
const openIndex = source.indexOf("(", target.methodCallIndex);
|
|
591
|
+
const closeIndex = findMatchingParen(source, openIndex);
|
|
592
|
+
if (closeIndex < 0 || source.slice(closeIndex + 1).trim())
|
|
593
|
+
return null;
|
|
594
|
+
const args = splitTopLevel(source.slice(openIndex + 1, closeIndex));
|
|
595
|
+
if (args.length > 1 && args.slice(1).some((arg) => arg.trim()))
|
|
596
|
+
return null;
|
|
597
|
+
const filter = normalizeJsonArgument(args[0] || "{}");
|
|
598
|
+
return filter ? { collection: target.collection, filter } : null;
|
|
599
|
+
}
|
|
600
|
+
export function parseMongoAggregateCommand(input) {
|
|
601
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
602
|
+
const target = parseCollectionMethodTarget(source, "aggregate");
|
|
603
|
+
if (!target)
|
|
604
|
+
return null;
|
|
605
|
+
const args = parseMethodArgs(source, target.methodCallIndex);
|
|
606
|
+
if (!args || args.length !== 1)
|
|
607
|
+
return null;
|
|
608
|
+
const pipeline = normalizeJsonArgument(args[0]);
|
|
609
|
+
if (!pipeline)
|
|
610
|
+
return null;
|
|
611
|
+
return Array.isArray(JSON.parse(pipeline)) ? { collection: target.collection, pipeline } : null;
|
|
612
|
+
}
|
|
613
|
+
export function mongoAggregateWriteStage(pipelineJson) {
|
|
614
|
+
try {
|
|
615
|
+
const pipeline = JSON.parse(pipelineJson);
|
|
616
|
+
if (!Array.isArray(pipeline))
|
|
617
|
+
return null;
|
|
618
|
+
for (const stage of pipeline) {
|
|
619
|
+
if (!isRecord(stage))
|
|
620
|
+
continue;
|
|
621
|
+
if (Object.prototype.hasOwnProperty.call(stage, "$out"))
|
|
622
|
+
return "$out";
|
|
623
|
+
if (Object.prototype.hasOwnProperty.call(stage, "$merge"))
|
|
624
|
+
return "$merge";
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
export function parseMongoWriteCommand(input) {
|
|
633
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
634
|
+
const insertOne = parseCollectionMethodTarget(source, "insertOne");
|
|
635
|
+
if (insertOne) {
|
|
636
|
+
const args = parseMethodArgs(source, insertOne.methodCallIndex);
|
|
637
|
+
if (!args || args.length !== 1)
|
|
638
|
+
return null;
|
|
639
|
+
const doc = normalizeJsonArgument(args[0]);
|
|
640
|
+
return doc ? { kind: "insert", collection: insertOne.collection, docsJson: doc } : null;
|
|
641
|
+
}
|
|
642
|
+
const insertMany = parseCollectionMethodTarget(source, "insertMany");
|
|
643
|
+
if (insertMany) {
|
|
644
|
+
const args = parseMethodArgs(source, insertMany.methodCallIndex);
|
|
645
|
+
if (!args || args.length !== 1)
|
|
646
|
+
return null;
|
|
647
|
+
const docs = normalizeJsonArgument(args[0]);
|
|
648
|
+
if (!docs)
|
|
649
|
+
return null;
|
|
650
|
+
return Array.isArray(JSON.parse(docs)) ? { kind: "insert", collection: insertMany.collection, docsJson: docs } : null;
|
|
651
|
+
}
|
|
652
|
+
for (const method of ["updateOne", "updateMany"]) {
|
|
653
|
+
const target = parseCollectionMethodTarget(source, method);
|
|
654
|
+
if (!target)
|
|
655
|
+
continue;
|
|
656
|
+
const args = parseMethodArgs(source, target.methodCallIndex);
|
|
657
|
+
if (!args || args.length !== 2)
|
|
658
|
+
return null;
|
|
659
|
+
const filter = normalizeJsonArgument(args[0]);
|
|
660
|
+
const update = normalizeJsonArgument(args[1]);
|
|
661
|
+
if (!filter || !update)
|
|
662
|
+
return null;
|
|
663
|
+
return { kind: "update", collection: target.collection, filter, update, many: method === "updateMany" };
|
|
664
|
+
}
|
|
665
|
+
for (const method of ["deleteOne", "deleteMany"]) {
|
|
666
|
+
const target = parseCollectionMethodTarget(source, method);
|
|
667
|
+
if (!target)
|
|
668
|
+
continue;
|
|
669
|
+
const args = parseMethodArgs(source, target.methodCallIndex);
|
|
670
|
+
if (!args || args.length !== 1)
|
|
671
|
+
return null;
|
|
672
|
+
const filter = normalizeJsonArgument(args[0]);
|
|
673
|
+
if (!filter)
|
|
674
|
+
return null;
|
|
675
|
+
return { kind: "delete", collection: target.collection, filter, many: method === "deleteMany" };
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
export function evaluateMongoWriteSafety(command, options) {
|
|
680
|
+
if (!options.allowWrites) {
|
|
681
|
+
return {
|
|
682
|
+
allowed: false,
|
|
683
|
+
reason: "MCP MongoDB execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.",
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (!options.allowDangerous && (command.kind === "update" || command.kind === "delete") && isEmptyJsonObject(command.filter)) {
|
|
687
|
+
return {
|
|
688
|
+
allowed: false,
|
|
689
|
+
reason: "MongoDB update/delete commands must include a non-empty filter unless DBX_MCP_ALLOW_DANGEROUS_SQL=1 is set.",
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return { allowed: true };
|
|
693
|
+
}
|
|
694
|
+
export function evaluateMongoAggregateSafety(command, options) {
|
|
695
|
+
const writeStage = mongoAggregateWriteStage(command.pipeline);
|
|
696
|
+
if (!writeStage)
|
|
697
|
+
return { allowed: true };
|
|
698
|
+
if (!options.allowWrites) {
|
|
699
|
+
return {
|
|
700
|
+
allowed: false,
|
|
701
|
+
reason: `MongoDB aggregate stage "${writeStage}" writes data. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.`,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
if (!options.allowDangerous) {
|
|
705
|
+
return {
|
|
706
|
+
allowed: false,
|
|
707
|
+
reason: `MongoDB aggregate stage "${writeStage}" is dangerous. Set DBX_MCP_ALLOW_DANGEROUS_SQL=1 to allow it.`,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
return { allowed: true };
|
|
711
|
+
}
|
|
712
|
+
function parseCollectionMethodTarget(source, method) {
|
|
713
|
+
const direct = new RegExp(`^db\\.([A-Za-z_$][\\w$]*)\\.${method}\\s*\\(`).exec(source);
|
|
714
|
+
if (direct)
|
|
715
|
+
return { collection: direct[1], methodCallIndex: source.indexOf(`.${method}`) };
|
|
716
|
+
const quoted = new RegExp(`^db\\.getCollection\\(\\s*(['"])([^'"]+)\\1\\s*\\)\\.${method}\\s*\\(`).exec(source);
|
|
717
|
+
if (quoted)
|
|
718
|
+
return { collection: quoted[2], methodCallIndex: source.indexOf(`.${method}`) };
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
function parseMethodArgs(source, methodCallIndex) {
|
|
722
|
+
const openIndex = source.indexOf("(", methodCallIndex);
|
|
723
|
+
const closeIndex = findMatchingParen(source, openIndex);
|
|
724
|
+
if (closeIndex < 0 || source.slice(closeIndex + 1).trim())
|
|
725
|
+
return null;
|
|
726
|
+
return splitTopLevel(source.slice(openIndex + 1, closeIndex));
|
|
727
|
+
}
|
|
728
|
+
function readChainedCallArgument(chain, method) {
|
|
729
|
+
const pattern = new RegExp(`\\.${method}\\s*\\(`, "g");
|
|
730
|
+
const match = pattern.exec(chain);
|
|
731
|
+
if (!match)
|
|
732
|
+
return undefined;
|
|
733
|
+
const openIndex = match.index + match[0].lastIndexOf("(");
|
|
734
|
+
const closeIndex = findMatchingParen(chain, openIndex);
|
|
735
|
+
return closeIndex < 0 ? undefined : chain.slice(openIndex + 1, closeIndex);
|
|
736
|
+
}
|
|
737
|
+
function readChainedIntegerArgument(chain, method, fallback) {
|
|
738
|
+
const arg = readChainedCallArgument(chain, method);
|
|
739
|
+
if (arg === undefined)
|
|
740
|
+
return fallback;
|
|
741
|
+
if (!/^\d+$/.test(arg.trim()))
|
|
742
|
+
return null;
|
|
743
|
+
return Number(arg.trim());
|
|
744
|
+
}
|
|
745
|
+
function normalizeJsonArgument(arg) {
|
|
746
|
+
const value = (arg.trim() || "{}").replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}');
|
|
747
|
+
try {
|
|
748
|
+
JSON.parse(value);
|
|
749
|
+
return value;
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function isEmptyJsonObject(json) {
|
|
756
|
+
try {
|
|
757
|
+
const parsed = JSON.parse(json);
|
|
758
|
+
return isRecord(parsed) && Object.keys(parsed).length === 0;
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function splitTopLevel(source) {
|
|
765
|
+
const parts = [];
|
|
766
|
+
let depth = 0;
|
|
767
|
+
let start = 0;
|
|
768
|
+
let quote = null;
|
|
769
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
770
|
+
const ch = source[i];
|
|
771
|
+
if (quote) {
|
|
772
|
+
if (ch === "\\" && i + 1 < source.length)
|
|
773
|
+
i += 1;
|
|
774
|
+
else if (ch === quote)
|
|
775
|
+
quote = null;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (ch === "'" || ch === '"')
|
|
779
|
+
quote = ch;
|
|
780
|
+
else if (ch === "{" || ch === "[" || ch === "(")
|
|
781
|
+
depth += 1;
|
|
782
|
+
else if (ch === "}" || ch === "]" || ch === ")")
|
|
783
|
+
depth -= 1;
|
|
784
|
+
else if (ch === "," && depth === 0) {
|
|
785
|
+
parts.push(source.slice(start, i));
|
|
786
|
+
start = i + 1;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
parts.push(source.slice(start));
|
|
790
|
+
return parts;
|
|
791
|
+
}
|
|
792
|
+
function findMatchingParen(source, openIndex) {
|
|
793
|
+
if (openIndex < 0 || source[openIndex] !== "(")
|
|
794
|
+
return -1;
|
|
795
|
+
let depth = 0;
|
|
796
|
+
let quote = null;
|
|
797
|
+
for (let i = openIndex; i < source.length; i += 1) {
|
|
798
|
+
const ch = source[i];
|
|
799
|
+
if (quote) {
|
|
800
|
+
if (ch === "\\" && i + 1 < source.length)
|
|
801
|
+
i += 1;
|
|
802
|
+
else if (ch === quote)
|
|
803
|
+
quote = null;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (ch === "'" || ch === '"')
|
|
807
|
+
quote = ch;
|
|
808
|
+
else if (ch === "(")
|
|
809
|
+
depth += 1;
|
|
810
|
+
else if (ch === ")") {
|
|
811
|
+
depth -= 1;
|
|
812
|
+
if (depth === 0)
|
|
813
|
+
return i;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return -1;
|
|
817
|
+
}
|
|
818
|
+
function isRecord(value) {
|
|
819
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
820
|
+
}
|
|
821
|
+
function mongoTypeName(value) {
|
|
822
|
+
if (value === null || value === undefined)
|
|
823
|
+
return "null";
|
|
824
|
+
if (Array.isArray(value))
|
|
825
|
+
return "array";
|
|
826
|
+
if (isRecord(value))
|
|
827
|
+
return "object";
|
|
828
|
+
return typeof value;
|
|
829
|
+
}
|
|
830
|
+
function toCellValue(value) {
|
|
831
|
+
return typeof value === "object" && value !== null ? JSON.stringify(value) : value;
|
|
832
|
+
}
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite"];
|
|
2
|
-
export declare const BRIDGE_REQUIRED_TYPES: readonly ["redis", "mongodb", "duckdb", "clickhouse", "sqlserver", "oracle", "elasticsearch", "dameng", "kingbase", "highgo", "vastbase", "goldendb", "
|
|
1
|
+
export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
|
|
2
|
+
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
3
|
export interface DbxDiagnostics {
|
|
4
4
|
appDataDir: string;
|
|
5
5
|
dbPath: string;
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
5
|
export const BRIDGE_REQUIRED_TYPES = [
|
|
6
6
|
"redis",
|
|
7
7
|
"mongodb",
|
|
@@ -15,7 +15,15 @@ export const BRIDGE_REQUIRED_TYPES = [
|
|
|
15
15
|
"highgo",
|
|
16
16
|
"vastbase",
|
|
17
17
|
"goldendb",
|
|
18
|
-
"
|
|
18
|
+
"yashandb",
|
|
19
|
+
"databricks",
|
|
20
|
+
"saphana",
|
|
21
|
+
"teradata",
|
|
22
|
+
"vertica",
|
|
23
|
+
"firebird",
|
|
24
|
+
"exasol",
|
|
25
|
+
"oceanbase-oracle",
|
|
26
|
+
"gbase",
|
|
19
27
|
"tdengine",
|
|
20
28
|
"h2",
|
|
21
29
|
"snowflake",
|
|
@@ -28,6 +36,7 @@ export const BRIDGE_REQUIRED_TYPES = [
|
|
|
28
36
|
"bigquery",
|
|
29
37
|
"kylin",
|
|
30
38
|
"sundb",
|
|
39
|
+
"xugu",
|
|
31
40
|
"jdbc",
|
|
32
41
|
"access",
|
|
33
42
|
];
|
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
package/dist/index.js
CHANGED
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.4",
|
|
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,7 @@
|
|
|
16
16
|
"./connections": "./dist/connections.js",
|
|
17
17
|
"./database": "./dist/database.js",
|
|
18
18
|
"./diagnostics": "./dist/diagnostics.js",
|
|
19
|
+
"./format": "./dist/format.js",
|
|
19
20
|
"./paths": "./dist/paths.js",
|
|
20
21
|
"./schema-context": "./dist/schema-context.js",
|
|
21
22
|
"./sql-safety": "./dist/sql-safety.js"
|