planche 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/index.js +311 -16
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# planche
|
|
2
2
|
|
|
3
3
|
Local agent for [Planche](https://planche.dev), a browser-based SQL client for
|
|
4
|
-
MySQL, MariaDB, PostgreSQL and
|
|
4
|
+
MySQL, MariaDB, PostgreSQL, SQLite, SQL Server and Oracle.
|
|
5
5
|
|
|
6
6
|
Browsers cannot open TCP connections to a database, so Planche runs this small
|
|
7
7
|
agent on your machine. The app in your browser talks to the agent over
|
|
@@ -90,7 +90,14 @@ database connections open and forwards queries with the matching driver
|
|
|
90
90
|
- **SQLite** — opens a file on your disk by path. Set `PLANCHE_SQLITE_DIR` to
|
|
91
91
|
limit which files can be opened. (Small SQLite files can also be opened
|
|
92
92
|
directly in the browser without the agent.)
|
|
93
|
-
- **
|
|
93
|
+
- **SQL Server** — same model as PostgreSQL: the connection is bound to one
|
|
94
|
+
database and the app's "database" is a schema (`dbo`, ...). `USE` works in
|
|
95
|
+
the editor because every statement runs on the same session; scripts may
|
|
96
|
+
contain `GO` lines.
|
|
97
|
+
- **Oracle** — thin-mode driver, no Instant Client needed. Schemas (users) are
|
|
98
|
+
the app's "databases", switched with `ALTER SESSION SET CURRENT_SCHEMA`.
|
|
99
|
+
PL/SQL blocks end with a `/` line as in SQL*Plus.
|
|
100
|
+
- **SSH tunnel** — MySQL, PostgreSQL, SQL Server and Oracle profiles can go through an SSH jump
|
|
94
101
|
host (password, private key, or the local `ssh-agent`). The agent opens the
|
|
95
102
|
tunnel with `ssh2` on a loopback port and points the driver at it. The host
|
|
96
103
|
may be a `Host` alias from `~/.ssh/config`: `HostName`, `Port`, `User` and
|
package/dist/index.js
CHANGED
|
@@ -80,13 +80,13 @@ var MySqlDriver = class {
|
|
|
80
80
|
* dropping temporary tables and user variables.
|
|
81
81
|
*/
|
|
82
82
|
currentDatabase;
|
|
83
|
-
async query(
|
|
83
|
+
async query(sql2, database) {
|
|
84
84
|
if (database && database !== this.currentDatabase) {
|
|
85
85
|
await this.connection.query(`USE ${mysql.escapeId(database)}`);
|
|
86
86
|
this.currentDatabase = database;
|
|
87
87
|
}
|
|
88
|
-
const [results, fields] = await this.connection.query(
|
|
89
|
-
if (USE_STATEMENT.test(
|
|
88
|
+
const [results, fields] = await this.connection.query(sql2);
|
|
89
|
+
if (USE_STATEMENT.test(sql2)) this.currentDatabase = null;
|
|
90
90
|
if (Array.isArray(results) && fields) {
|
|
91
91
|
const fieldPackets = fields;
|
|
92
92
|
const mappedFields = fieldPackets.map((field) => ({
|
|
@@ -269,13 +269,13 @@ var PostgresDriver = class {
|
|
|
269
269
|
} catch {
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
-
async query(
|
|
272
|
+
async query(sql2, database) {
|
|
273
273
|
if (database && database !== this.currentSchema) {
|
|
274
274
|
await this.client.query(`SET search_path TO ${quoteIdent(database)}, public`);
|
|
275
275
|
this.currentSchema = database;
|
|
276
276
|
}
|
|
277
|
-
const result = await this.client.query({ text:
|
|
278
|
-
if (SEARCH_PATH_STATEMENT.test(
|
|
277
|
+
const result = await this.client.query({ text: sql2, rowMode: "array" });
|
|
278
|
+
if (SEARCH_PATH_STATEMENT.test(sql2)) this.currentSchema = null;
|
|
279
279
|
const results = Array.isArray(result) ? result : [result];
|
|
280
280
|
const last = [...results].reverse().find((r) => r.fields.length > 0) ?? results[results.length - 1];
|
|
281
281
|
if (last.fields.length > 0) {
|
|
@@ -385,13 +385,13 @@ var SqliteDriver = class {
|
|
|
385
385
|
db;
|
|
386
386
|
type = "sqlite";
|
|
387
387
|
label;
|
|
388
|
-
async query(
|
|
388
|
+
async query(sql2) {
|
|
389
389
|
let statement;
|
|
390
390
|
try {
|
|
391
|
-
statement = this.db.prepare(
|
|
391
|
+
statement = this.db.prepare(sql2);
|
|
392
392
|
} catch (err) {
|
|
393
393
|
if (err instanceof RangeError && /more than one statement/i.test(err.message)) {
|
|
394
|
-
this.db.exec(
|
|
394
|
+
this.db.exec(sql2);
|
|
395
395
|
return { fields: [], records: [], affectedRows: 0, insertId: 0, isResultQuery: false };
|
|
396
396
|
}
|
|
397
397
|
throw err;
|
|
@@ -466,6 +466,291 @@ var sqliteDriverFactory = {
|
|
|
466
466
|
}
|
|
467
467
|
};
|
|
468
468
|
|
|
469
|
+
// src/drivers/mssql.ts
|
|
470
|
+
import sql from "mssql";
|
|
471
|
+
function toPoolConfig(config) {
|
|
472
|
+
return {
|
|
473
|
+
server: config.host,
|
|
474
|
+
port: config.port,
|
|
475
|
+
user: config.user,
|
|
476
|
+
password: config.password,
|
|
477
|
+
database: config.database,
|
|
478
|
+
// One physical connection: manual transactions and USE need session affinity.
|
|
479
|
+
pool: { min: 1, max: 1, idleTimeoutMillis: 24 * 60 * 60 * 1e3 },
|
|
480
|
+
options: {
|
|
481
|
+
encrypt: config.ssl === true,
|
|
482
|
+
trustServerCertificate: true,
|
|
483
|
+
// Keep date/time values as sent by the server rather than shifting to UTC.
|
|
484
|
+
useUTC: false,
|
|
485
|
+
enableArithAbort: true
|
|
486
|
+
},
|
|
487
|
+
requestTimeout: 0
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
var pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
491
|
+
function formatDate(d) {
|
|
492
|
+
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
493
|
+
const ms = d.getMilliseconds();
|
|
494
|
+
return `${date} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${ms ? "." + pad(ms, 3) : ""}`;
|
|
495
|
+
}
|
|
496
|
+
function normalizeValue(value) {
|
|
497
|
+
if (value instanceof Date) return formatDate(value);
|
|
498
|
+
if (Buffer.isBuffer(value)) return value.toString("hex");
|
|
499
|
+
return value;
|
|
500
|
+
}
|
|
501
|
+
var RESULT_QUERY = /^\s*(?:WITH|SELECT|\()/i;
|
|
502
|
+
var MssqlDriver = class {
|
|
503
|
+
constructor(pool, config) {
|
|
504
|
+
this.pool = pool;
|
|
505
|
+
this.label = `${config.user}@${config.host}:${config.port}/${config.database}`;
|
|
506
|
+
}
|
|
507
|
+
pool;
|
|
508
|
+
type = "mssql";
|
|
509
|
+
label;
|
|
510
|
+
/** Source columns per SQL text; `sys.dm_exec_describe_first_result_set` is not free. */
|
|
511
|
+
sources = /* @__PURE__ */ new Map();
|
|
512
|
+
/**
|
|
513
|
+
* The `mssql` driver does not report which table a result column comes
|
|
514
|
+
* from; ask the server to describe the statement instead so the grid can
|
|
515
|
+
* decide whether it is editable and address rows by primary key.
|
|
516
|
+
*/
|
|
517
|
+
async describe(text) {
|
|
518
|
+
const cached = this.sources.get(text);
|
|
519
|
+
if (cached !== void 0) return cached;
|
|
520
|
+
let result = null;
|
|
521
|
+
try {
|
|
522
|
+
const req = this.pool.request();
|
|
523
|
+
req.input("sql", sql.NVarChar(sql.MAX), text);
|
|
524
|
+
const res = await req.query(
|
|
525
|
+
"SELECT name, source_schema, source_table, source_column, system_type_name FROM sys.dm_exec_describe_first_result_set(@sql, NULL, 1) WHERE is_hidden = 0 ORDER BY column_ordinal"
|
|
526
|
+
);
|
|
527
|
+
result = res.recordset.map((r) => ({
|
|
528
|
+
schema: r.source_schema ?? "",
|
|
529
|
+
table: r.source_table ?? "",
|
|
530
|
+
column: r.source_column ?? "",
|
|
531
|
+
type: (r.system_type_name ?? "").toLowerCase()
|
|
532
|
+
}));
|
|
533
|
+
} catch {
|
|
534
|
+
result = null;
|
|
535
|
+
}
|
|
536
|
+
if (this.sources.size > 200) this.sources.clear();
|
|
537
|
+
this.sources.set(text, result);
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
async query(text) {
|
|
541
|
+
const req = this.pool.request();
|
|
542
|
+
req.arrayRowMode = true;
|
|
543
|
+
const res = await req.query(text);
|
|
544
|
+
const sets = res.recordsets;
|
|
545
|
+
const last = sets.length > 0 ? sets[sets.length - 1] : void 0;
|
|
546
|
+
const columns = last?.columns;
|
|
547
|
+
if (last && columns) {
|
|
548
|
+
const meta = Object.values(columns).sort((a, b) => a.index - b.index);
|
|
549
|
+
const sources = RESULT_QUERY.test(text) ? await this.describe(text) : null;
|
|
550
|
+
const fields = meta.map((col, i) => {
|
|
551
|
+
const src = sources && sources.length === meta.length ? sources[i] : void 0;
|
|
552
|
+
const typeName = col.type?.declaration ?? "";
|
|
553
|
+
return {
|
|
554
|
+
name: col.name,
|
|
555
|
+
orgName: src?.column || col.name,
|
|
556
|
+
type: src?.type || typeName.toLowerCase(),
|
|
557
|
+
table: src?.table ?? "",
|
|
558
|
+
orgTable: src?.table ?? "",
|
|
559
|
+
db: src?.schema ?? "",
|
|
560
|
+
maxLength: typeof col.length === "number" ? col.length : 0
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
const records = last.map((row) => row.map(normalizeValue));
|
|
564
|
+
return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
fields: [],
|
|
568
|
+
records: [],
|
|
569
|
+
affectedRows: res.rowsAffected.reduce((sum, n) => sum + n, 0),
|
|
570
|
+
insertId: 0,
|
|
571
|
+
isResultQuery: false
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async close() {
|
|
575
|
+
await this.pool.close();
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
var mssqlDriverFactory = {
|
|
579
|
+
async open(config) {
|
|
580
|
+
const pool = new sql.ConnectionPool(toPoolConfig(config));
|
|
581
|
+
await pool.connect();
|
|
582
|
+
return new MssqlDriver(pool, config);
|
|
583
|
+
},
|
|
584
|
+
async test(config) {
|
|
585
|
+
const pool = new sql.ConnectionPool(toPoolConfig(config));
|
|
586
|
+
await pool.connect();
|
|
587
|
+
await pool.close();
|
|
588
|
+
},
|
|
589
|
+
parseConfig(body) {
|
|
590
|
+
const host = typeof body.host === "string" ? body.host.trim() : "";
|
|
591
|
+
const user = typeof body.user === "string" ? body.user : "";
|
|
592
|
+
if (!host || !user) {
|
|
593
|
+
throw new Error("host and user are required");
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
driver: "mssql",
|
|
597
|
+
host,
|
|
598
|
+
port: Number(body.port) || 1433,
|
|
599
|
+
user,
|
|
600
|
+
password: typeof body.password === "string" ? body.password : "",
|
|
601
|
+
database: typeof body.database === "string" && body.database ? body.database : "master",
|
|
602
|
+
ssl: body.ssl === true
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// src/drivers/oracle.ts
|
|
608
|
+
import oracledb from "oracledb";
|
|
609
|
+
oracledb.autoCommit = true;
|
|
610
|
+
oracledb.fetchAsString = [oracledb.CLOB];
|
|
611
|
+
var fetchTypeHandler = (meta) => meta.dbType === oracledb.DB_TYPE_NUMBER ? { type: oracledb.STRING } : void 0;
|
|
612
|
+
var pad2 = (n, width = 2) => String(n).padStart(width, "0");
|
|
613
|
+
function formatDate2(d) {
|
|
614
|
+
const ms = d.getMilliseconds();
|
|
615
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}${ms ? "." + pad2(ms, 3) : ""}`;
|
|
616
|
+
}
|
|
617
|
+
function normalizeValue2(value) {
|
|
618
|
+
if (value instanceof Date) return formatDate2(value);
|
|
619
|
+
if (Buffer.isBuffer(value)) return value.toString("hex");
|
|
620
|
+
return value;
|
|
621
|
+
}
|
|
622
|
+
var SESSION_FORMATS = [
|
|
623
|
+
"ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'",
|
|
624
|
+
"ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF3'",
|
|
625
|
+
"ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF3 TZH:TZM'"
|
|
626
|
+
];
|
|
627
|
+
var quoteIdent2 = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
628
|
+
var PLSQL = /^\s*(BEGIN|DECLARE|CREATE\s+(OR\s+REPLACE\s+)?((NON)?EDITIONABLE\s+)?(PROCEDURE|FUNCTION|TRIGGER|PACKAGE|TYPE)\b)/i;
|
|
629
|
+
function normalizeStatement(sql2) {
|
|
630
|
+
const trimmed = sql2.trim().replace(/\n\/\s*$/, "");
|
|
631
|
+
if (PLSQL.test(trimmed)) return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
|
|
632
|
+
return trimmed.replace(/;\s*$/, "");
|
|
633
|
+
}
|
|
634
|
+
var SET_SCHEMA = /^\s*ALTER\s+SESSION\s+SET\s+CURRENT_SCHEMA\b/i;
|
|
635
|
+
var START_TX = /^\s*SET\s+TRANSACTION\b/i;
|
|
636
|
+
var END_TX = /^\s*(COMMIT|ROLLBACK)\b/i;
|
|
637
|
+
function singleSourceTable(sql2) {
|
|
638
|
+
const m = /\bFROM\s+("?[\w$#]+"?)(?:\s*\.\s*("?[\w$#]+"?))?([\s\S]*)$/i.exec(sql2);
|
|
639
|
+
if (!m) return null;
|
|
640
|
+
const rest = m[3].replace(/\b(WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|FETCH|OFFSET|UNION|MINUS|INTERSECT|FOR\s+UPDATE)\b[\s\S]*$/i, "");
|
|
641
|
+
if (/,|\bJOIN\b|\(/i.test(rest) || rest.trim().split(/\s+/).filter(Boolean).length > 2) return null;
|
|
642
|
+
const unquote = (v) => v.startsWith('"') ? v.slice(1, -1).replaceAll('""', '"') : v.toUpperCase();
|
|
643
|
+
return m[2] ? { schema: unquote(m[1]), table: unquote(m[2]) } : { table: unquote(m[1]) };
|
|
644
|
+
}
|
|
645
|
+
var OracleDriver = class {
|
|
646
|
+
constructor(conn, config) {
|
|
647
|
+
this.conn = conn;
|
|
648
|
+
this.label = `${config.user}@${config.host}:${config.port}/${config.serviceName}`;
|
|
649
|
+
this.currentSchema = config.user.toUpperCase();
|
|
650
|
+
}
|
|
651
|
+
conn;
|
|
652
|
+
type = "oracle";
|
|
653
|
+
label;
|
|
654
|
+
currentSchema;
|
|
655
|
+
manualTx = false;
|
|
656
|
+
/** Column names per `OWNER.TABLE`, for result-source resolution. */
|
|
657
|
+
columns = /* @__PURE__ */ new Map();
|
|
658
|
+
async init() {
|
|
659
|
+
for (const stmt of SESSION_FORMATS) await this.conn.execute(stmt);
|
|
660
|
+
}
|
|
661
|
+
async tableColumns(schema, table) {
|
|
662
|
+
const key = `${schema}.${table}`;
|
|
663
|
+
const cached = this.columns.get(key);
|
|
664
|
+
if (cached !== void 0) return cached;
|
|
665
|
+
let out = null;
|
|
666
|
+
try {
|
|
667
|
+
const res = await this.conn.execute(
|
|
668
|
+
"SELECT column_name FROM all_tab_columns WHERE owner = :o AND table_name = :t",
|
|
669
|
+
{ o: schema, t: table },
|
|
670
|
+
{ outFormat: oracledb.OUT_FORMAT_ARRAY }
|
|
671
|
+
);
|
|
672
|
+
out = res.rows && res.rows.length > 0 ? new Set(res.rows.map((r) => r[0])) : null;
|
|
673
|
+
} catch {
|
|
674
|
+
out = null;
|
|
675
|
+
}
|
|
676
|
+
if (this.columns.size > 200) this.columns.clear();
|
|
677
|
+
this.columns.set(key, out);
|
|
678
|
+
return out;
|
|
679
|
+
}
|
|
680
|
+
async query(sql2, database) {
|
|
681
|
+
if (database && database !== this.currentSchema) {
|
|
682
|
+
await this.conn.execute(`ALTER SESSION SET CURRENT_SCHEMA = ${quoteIdent2(database)}`);
|
|
683
|
+
this.currentSchema = database;
|
|
684
|
+
}
|
|
685
|
+
const text = normalizeStatement(sql2);
|
|
686
|
+
if (START_TX.test(text)) this.manualTx = true;
|
|
687
|
+
const res = await this.conn.execute(text, [], {
|
|
688
|
+
outFormat: oracledb.OUT_FORMAT_ARRAY,
|
|
689
|
+
fetchTypeHandler,
|
|
690
|
+
autoCommit: !this.manualTx
|
|
691
|
+
});
|
|
692
|
+
if (SET_SCHEMA.test(text)) this.currentSchema = null;
|
|
693
|
+
if (END_TX.test(text)) this.manualTx = false;
|
|
694
|
+
if (res.metaData && res.metaData.length > 0) {
|
|
695
|
+
const source = singleSourceTable(text);
|
|
696
|
+
const schema = source?.schema ?? this.currentSchema ?? "";
|
|
697
|
+
const known = source && schema ? await this.tableColumns(schema, source.table) : null;
|
|
698
|
+
const fields = res.metaData.map((col) => {
|
|
699
|
+
const fromTable = known?.has(col.name) ?? false;
|
|
700
|
+
return {
|
|
701
|
+
name: col.name,
|
|
702
|
+
orgName: col.name,
|
|
703
|
+
type: (col.dbTypeName ?? "").toLowerCase(),
|
|
704
|
+
table: fromTable ? source.table : "",
|
|
705
|
+
orgTable: fromTable ? source.table : "",
|
|
706
|
+
db: fromTable ? schema : "",
|
|
707
|
+
maxLength: col.byteSize ?? 0
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
const records = (res.rows ?? []).map((row) => row.map(normalizeValue2));
|
|
711
|
+
return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
|
|
712
|
+
}
|
|
713
|
+
return { fields: [], records: [], affectedRows: res.rowsAffected ?? 0, insertId: 0, isResultQuery: false };
|
|
714
|
+
}
|
|
715
|
+
async close() {
|
|
716
|
+
await this.conn.close();
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
function connectionAttributes(config) {
|
|
720
|
+
return {
|
|
721
|
+
user: config.user,
|
|
722
|
+
password: config.password,
|
|
723
|
+
connectString: `${config.host}:${config.port}/${config.serviceName}`
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
var oracleDriverFactory = {
|
|
727
|
+
async open(config) {
|
|
728
|
+
const conn = await oracledb.getConnection(connectionAttributes(config));
|
|
729
|
+
const driver = new OracleDriver(conn, config);
|
|
730
|
+
await driver.init();
|
|
731
|
+
return driver;
|
|
732
|
+
},
|
|
733
|
+
async test(config) {
|
|
734
|
+
const conn = await oracledb.getConnection(connectionAttributes(config));
|
|
735
|
+
await conn.close();
|
|
736
|
+
},
|
|
737
|
+
parseConfig(body) {
|
|
738
|
+
const host = typeof body.host === "string" ? body.host.trim() : "";
|
|
739
|
+
const user = typeof body.user === "string" ? body.user : "";
|
|
740
|
+
if (!host || !user) {
|
|
741
|
+
throw new Error("host and user are required");
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
driver: "oracle",
|
|
745
|
+
host,
|
|
746
|
+
port: Number(body.port) || 1521,
|
|
747
|
+
user,
|
|
748
|
+
password: typeof body.password === "string" ? body.password : "",
|
|
749
|
+
serviceName: typeof body.serviceName === "string" && body.serviceName ? body.serviceName : "FREEPDB1"
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
469
754
|
// src/ssh-tunnel.ts
|
|
470
755
|
import { createServer } from "net";
|
|
471
756
|
import { mkdir, readFile as readFile2, appendFile } from "fs/promises";
|
|
@@ -811,7 +1096,9 @@ async function openSshTunnel(requested, target) {
|
|
|
811
1096
|
var factories = {
|
|
812
1097
|
mysql: mysqlDriverFactory,
|
|
813
1098
|
postgres: postgresDriverFactory,
|
|
814
|
-
sqlite: sqliteDriverFactory
|
|
1099
|
+
sqlite: sqliteDriverFactory,
|
|
1100
|
+
mssql: mssqlDriverFactory,
|
|
1101
|
+
oracle: oracleDriverFactory
|
|
815
1102
|
};
|
|
816
1103
|
function isDriverType(value) {
|
|
817
1104
|
return typeof value === "string" && value in factories;
|
|
@@ -840,7 +1127,7 @@ function tunneled(driver, tunnel, label) {
|
|
|
840
1127
|
return {
|
|
841
1128
|
type: driver.type,
|
|
842
1129
|
label,
|
|
843
|
-
query: (
|
|
1130
|
+
query: (sql2, database) => driver.query(sql2, database),
|
|
844
1131
|
async close() {
|
|
845
1132
|
try {
|
|
846
1133
|
await driver.close();
|
|
@@ -875,6 +1162,10 @@ function openDirect(config) {
|
|
|
875
1162
|
return factories.postgres.open(config);
|
|
876
1163
|
case "sqlite":
|
|
877
1164
|
return factories.sqlite.open(config);
|
|
1165
|
+
case "mssql":
|
|
1166
|
+
return factories.mssql.open(config);
|
|
1167
|
+
case "oracle":
|
|
1168
|
+
return factories.oracle.open(config);
|
|
878
1169
|
}
|
|
879
1170
|
}
|
|
880
1171
|
function testDirect(config) {
|
|
@@ -885,6 +1176,10 @@ function testDirect(config) {
|
|
|
885
1176
|
return factories.postgres.test(config);
|
|
886
1177
|
case "sqlite":
|
|
887
1178
|
return factories.sqlite.test(config);
|
|
1179
|
+
case "mssql":
|
|
1180
|
+
return factories.mssql.test(config);
|
|
1181
|
+
case "oracle":
|
|
1182
|
+
return factories.oracle.test(config);
|
|
888
1183
|
}
|
|
889
1184
|
}
|
|
890
1185
|
|
|
@@ -925,13 +1220,13 @@ var ConnectionManager = class {
|
|
|
925
1220
|
* Execute a SQL query on an existing connection.
|
|
926
1221
|
* Optionally switch database before executing (drivers that support it).
|
|
927
1222
|
*/
|
|
928
|
-
async query(connectionId,
|
|
1223
|
+
async query(connectionId, sql2, database) {
|
|
929
1224
|
const driver = this.connections.get(connectionId);
|
|
930
1225
|
if (!driver) {
|
|
931
1226
|
throw new Error(`Connection not found: ${connectionId}`);
|
|
932
1227
|
}
|
|
933
1228
|
this.lastUsed.set(connectionId, Date.now());
|
|
934
|
-
return driver.query(
|
|
1229
|
+
return driver.query(sql2, database);
|
|
935
1230
|
}
|
|
936
1231
|
/**
|
|
937
1232
|
* Close and remove a connection.
|
|
@@ -1073,8 +1368,8 @@ function createAgent(options) {
|
|
|
1073
1368
|
});
|
|
1074
1369
|
app.post("/query", async (req, res) => {
|
|
1075
1370
|
try {
|
|
1076
|
-
const { connectionId, sql, database } = req.body;
|
|
1077
|
-
if (typeof connectionId !== "string" || typeof
|
|
1371
|
+
const { connectionId, sql: sql2, database } = req.body;
|
|
1372
|
+
if (typeof connectionId !== "string" || typeof sql2 !== "string" || !connectionId || !sql2) {
|
|
1078
1373
|
fail(res, 400, "connectionId and sql are required");
|
|
1079
1374
|
return;
|
|
1080
1375
|
}
|
|
@@ -1083,7 +1378,7 @@ function createAgent(options) {
|
|
|
1083
1378
|
return;
|
|
1084
1379
|
}
|
|
1085
1380
|
const start = Date.now();
|
|
1086
|
-
const result = await manager.query(connectionId,
|
|
1381
|
+
const result = await manager.query(connectionId, sql2, database);
|
|
1087
1382
|
const execTime = Date.now() - start;
|
|
1088
1383
|
res.json({
|
|
1089
1384
|
success: true,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "planche",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Local agent for Planche — lets the browser-based Planche SQL client reach MySQL/MariaDB/PostgreSQL/SQLite databases from your machine.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Local agent for Planche — lets the browser-based Planche SQL client reach MySQL/MariaDB/PostgreSQL/SQLite/SQL Server/Oracle databases from your machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://planche.dev",
|
|
7
7
|
"repository": {
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"postgres",
|
|
29
29
|
"postgresql",
|
|
30
30
|
"sqlite",
|
|
31
|
+
"mssql",
|
|
32
|
+
"sqlserver",
|
|
33
|
+
"oracle",
|
|
31
34
|
"sql",
|
|
32
35
|
"client",
|
|
33
36
|
"gui"
|
|
@@ -42,14 +45,18 @@
|
|
|
42
45
|
"dependencies": {
|
|
43
46
|
"better-sqlite3": "^12.11.1",
|
|
44
47
|
"express": "^5.1.0",
|
|
48
|
+
"mssql": "^12.7.0",
|
|
45
49
|
"mysql2": "^3.14.1",
|
|
50
|
+
"oracledb": "^7.0.1",
|
|
46
51
|
"pg": "^8.23.0",
|
|
47
52
|
"ssh2": "^1.17.0"
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
50
55
|
"@types/better-sqlite3": "^9.6.0",
|
|
51
56
|
"@types/express": "^5.0.0",
|
|
57
|
+
"@types/mssql": "^12.3.0",
|
|
52
58
|
"@types/node": "^24.0.0",
|
|
59
|
+
"@types/oracledb": "^7.0.2",
|
|
53
60
|
"@types/pg": "^8.23.1",
|
|
54
61
|
"@types/ssh2": "^1.15.5",
|
|
55
62
|
"tsup": "^8.5.0",
|