planche 0.3.0 → 0.4.1
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 +3 -2
- package/dist/index.js +181 -6
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# planche
|
|
2
2
|
|
|
3
3
|
Local agent for [Planche](https://planche.dev), a browser-based SQL client for
|
|
4
|
-
MySQL, MariaDB, PostgreSQL, SQLite, SQL Server and
|
|
4
|
+
MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle, Neon, Supabase and
|
|
5
|
+
Cloudflare D1.
|
|
5
6
|
|
|
6
7
|
Browsers cannot open TCP connections to a database, so Planche runs this small
|
|
7
8
|
agent on your machine. The app in your browser talks to the agent over
|
|
@@ -141,4 +142,4 @@ database connections open and forwards queries with the matching driver
|
|
|
141
142
|
|
|
142
143
|
## License
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
All rights reserved. Not yet released under an open-source license.
|
package/dist/index.js
CHANGED
|
@@ -209,7 +209,7 @@ function toClientConfig(config) {
|
|
|
209
209
|
user: config.user,
|
|
210
210
|
password: config.password,
|
|
211
211
|
database: config.database,
|
|
212
|
-
ssl: config.ssl ? { rejectUnauthorized:
|
|
212
|
+
ssl: config.ssl ? { rejectUnauthorized: config.sslVerify === true } : void 0,
|
|
213
213
|
types
|
|
214
214
|
};
|
|
215
215
|
}
|
|
@@ -332,7 +332,8 @@ var postgresDriverFactory = {
|
|
|
332
332
|
user,
|
|
333
333
|
password: typeof body.password === "string" ? body.password : "",
|
|
334
334
|
database: typeof body.database === "string" && body.database ? body.database : "postgres",
|
|
335
|
-
ssl: body.ssl === true
|
|
335
|
+
ssl: body.ssl === true,
|
|
336
|
+
sslVerify: body.sslVerify === true
|
|
336
337
|
};
|
|
337
338
|
}
|
|
338
339
|
};
|
|
@@ -751,6 +752,175 @@ var oracleDriverFactory = {
|
|
|
751
752
|
}
|
|
752
753
|
};
|
|
753
754
|
|
|
755
|
+
// src/drivers/d1.ts
|
|
756
|
+
var API = "https://api.cloudflare.com/client/v4";
|
|
757
|
+
var D1Error = class extends Error {
|
|
758
|
+
constructor(message, status) {
|
|
759
|
+
super(message);
|
|
760
|
+
this.status = status;
|
|
761
|
+
}
|
|
762
|
+
status;
|
|
763
|
+
};
|
|
764
|
+
var SCHEMA_PRAGMA = /^\s*PRAGMA\s+(?:"main"|main|'main'|\[main\])\s*\.\s*/i;
|
|
765
|
+
var DATABASE_LIST = /^\s*PRAGMA\s+database_list\s*;?\s*$/i;
|
|
766
|
+
var RESULT_KIND = /^\s*(SELECT|WITH|PRAGMA|EXPLAIN|VALUES)\b/i;
|
|
767
|
+
function singleSourceTable2(sql2) {
|
|
768
|
+
const m = /\bFROM\s+(?:"?main"?\s*\.\s*)?("[^"]+"|`[^`]+`|\[[^\]]+\]|[\w$]+)([\s\S]*)$/i.exec(sql2);
|
|
769
|
+
if (!m) return null;
|
|
770
|
+
const rest = m[2].replace(/\b(WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|LIMIT|OFFSET|UNION|EXCEPT|INTERSECT|WINDOW)\b[\s\S]*$/i, "");
|
|
771
|
+
if (/,|\bJOIN\b|\(/i.test(rest) || rest.trim().split(/\s+/).filter(Boolean).length > 2) return null;
|
|
772
|
+
const raw = m[1];
|
|
773
|
+
const first = raw[0];
|
|
774
|
+
if (first === '"') return raw.slice(1, -1).replaceAll('""', '"');
|
|
775
|
+
if (first === "`") return raw.slice(1, -1).replaceAll("``", "`");
|
|
776
|
+
if (first === "[") return raw.slice(1, -1);
|
|
777
|
+
return raw;
|
|
778
|
+
}
|
|
779
|
+
function normalizeDeclaredType2(declared) {
|
|
780
|
+
if (typeof declared !== "string") return null;
|
|
781
|
+
const base = declared.trim().toLowerCase().replace(/\(.*$/, "").trim();
|
|
782
|
+
return base || null;
|
|
783
|
+
}
|
|
784
|
+
function inferTypeFromValue2(value) {
|
|
785
|
+
if (value === null || value === void 0) return "null";
|
|
786
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
|
|
787
|
+
if (Array.isArray(value)) return "blob";
|
|
788
|
+
return "text";
|
|
789
|
+
}
|
|
790
|
+
function firstNonNull2(records, index) {
|
|
791
|
+
for (const row of records) {
|
|
792
|
+
const value = row[index];
|
|
793
|
+
if (value !== null && value !== void 0) return value;
|
|
794
|
+
}
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
var D1Driver = class {
|
|
798
|
+
type = "d1";
|
|
799
|
+
label;
|
|
800
|
+
url;
|
|
801
|
+
headers;
|
|
802
|
+
/** Declared column types per table, for result-source resolution. */
|
|
803
|
+
columns = /* @__PURE__ */ new Map();
|
|
804
|
+
constructor(config) {
|
|
805
|
+
this.label = `d1:${config.databaseId}`;
|
|
806
|
+
this.url = `${API}/accounts/${encodeURIComponent(config.accountId)}/d1/database/${encodeURIComponent(config.databaseId)}/raw`;
|
|
807
|
+
this.headers = { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "application/json" };
|
|
808
|
+
}
|
|
809
|
+
async raw(sql2) {
|
|
810
|
+
let res;
|
|
811
|
+
try {
|
|
812
|
+
res = await fetch(this.url, { method: "POST", headers: this.headers, body: JSON.stringify({ sql: sql2 }) });
|
|
813
|
+
} catch (err) {
|
|
814
|
+
throw new D1Error(`Cannot reach the Cloudflare API: ${err instanceof Error ? err.message : String(err)}`, 400);
|
|
815
|
+
}
|
|
816
|
+
let body = null;
|
|
817
|
+
try {
|
|
818
|
+
body = await res.json();
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
if (!res.ok || !body?.success) {
|
|
822
|
+
const message = body?.errors?.map((e) => e.message).join("; ") || `Cloudflare API responded ${res.status}`;
|
|
823
|
+
throw new D1Error(message, res.status === 401 || res.status === 403 ? 401 : res.ok ? void 0 : 400);
|
|
824
|
+
}
|
|
825
|
+
const result = body.result?.[0];
|
|
826
|
+
if (!result) throw new D1Error("Cloudflare API returned no result");
|
|
827
|
+
return result;
|
|
828
|
+
}
|
|
829
|
+
async tableColumns(table) {
|
|
830
|
+
const cached = this.columns.get(table);
|
|
831
|
+
if (cached !== void 0) return cached;
|
|
832
|
+
let out = null;
|
|
833
|
+
try {
|
|
834
|
+
const info = await this.raw(`PRAGMA table_info("${table.replaceAll('"', '""')}")`);
|
|
835
|
+
const cols = info.results?.columns ?? [];
|
|
836
|
+
const name = cols.indexOf("name");
|
|
837
|
+
const type = cols.indexOf("type");
|
|
838
|
+
const rows = info.results?.rows ?? [];
|
|
839
|
+
if (rows.length > 0 && name >= 0) {
|
|
840
|
+
out = new Map(rows.map((r) => [String(r[name]), normalizeDeclaredType2(type >= 0 ? r[type] : null)]));
|
|
841
|
+
}
|
|
842
|
+
} catch {
|
|
843
|
+
out = null;
|
|
844
|
+
}
|
|
845
|
+
if (this.columns.size > 200) this.columns.clear();
|
|
846
|
+
this.columns.set(table, out);
|
|
847
|
+
return out;
|
|
848
|
+
}
|
|
849
|
+
async query(sql2) {
|
|
850
|
+
if (DATABASE_LIST.test(sql2)) {
|
|
851
|
+
return {
|
|
852
|
+
fields: [
|
|
853
|
+
{ name: "seq", orgName: "seq", type: "integer", table: "", orgTable: "", db: "", maxLength: 0 },
|
|
854
|
+
{ name: "name", orgName: "name", type: "text", table: "", orgTable: "", db: "", maxLength: 0 },
|
|
855
|
+
{ name: "file", orgName: "file", type: "text", table: "", orgTable: "", db: "", maxLength: 0 }
|
|
856
|
+
],
|
|
857
|
+
records: [[0, "main", ""]],
|
|
858
|
+
affectedRows: 0,
|
|
859
|
+
insertId: 0,
|
|
860
|
+
isResultQuery: true
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const text = sql2.replace(SCHEMA_PRAGMA, "PRAGMA ");
|
|
864
|
+
if (/^\s*(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(text)) {
|
|
865
|
+
throw new D1Error("Cloudflare D1 does not support transaction statements over the API; every request auto-commits.");
|
|
866
|
+
}
|
|
867
|
+
if (this.isDdl(text)) this.columns.clear();
|
|
868
|
+
const result = await this.raw(text);
|
|
869
|
+
const columns = result.results?.columns ?? [];
|
|
870
|
+
const records = result.results?.rows ?? [];
|
|
871
|
+
if (columns.length === 0 && !RESULT_KIND.test(text)) {
|
|
872
|
+
return {
|
|
873
|
+
fields: [],
|
|
874
|
+
records: [],
|
|
875
|
+
affectedRows: result.meta?.changes ?? 0,
|
|
876
|
+
insertId: result.meta?.last_row_id ?? 0,
|
|
877
|
+
isResultQuery: false
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
const source = RESULT_KIND.test(text) ? singleSourceTable2(text) : null;
|
|
881
|
+
const known = source ? await this.tableColumns(source) : null;
|
|
882
|
+
const fields = columns.map((name, index) => {
|
|
883
|
+
const fromTable = known?.has(name) ?? false;
|
|
884
|
+
const type = (fromTable ? known?.get(name) : null) ?? inferTypeFromValue2(firstNonNull2(records, index));
|
|
885
|
+
return {
|
|
886
|
+
name,
|
|
887
|
+
orgName: name,
|
|
888
|
+
type,
|
|
889
|
+
table: fromTable && source ? source : "",
|
|
890
|
+
orgTable: fromTable && source ? source : "",
|
|
891
|
+
db: fromTable ? "main" : "",
|
|
892
|
+
maxLength: 0
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
|
|
896
|
+
}
|
|
897
|
+
isDdl(sql2) {
|
|
898
|
+
return /^\s*(CREATE|ALTER|DROP)\b/i.test(sql2);
|
|
899
|
+
}
|
|
900
|
+
async close() {
|
|
901
|
+
this.columns.clear();
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
var d1DriverFactory = {
|
|
905
|
+
async open(config) {
|
|
906
|
+
const driver = new D1Driver(config);
|
|
907
|
+
await driver.raw("SELECT 1");
|
|
908
|
+
return driver;
|
|
909
|
+
},
|
|
910
|
+
async test(config) {
|
|
911
|
+
await new D1Driver(config).raw("SELECT 1");
|
|
912
|
+
},
|
|
913
|
+
parseConfig(body) {
|
|
914
|
+
const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
|
|
915
|
+
const databaseId = typeof body.databaseId === "string" ? body.databaseId.trim() : "";
|
|
916
|
+
const apiToken = typeof body.apiToken === "string" ? body.apiToken.trim() : "";
|
|
917
|
+
if (!accountId || !databaseId || !apiToken) {
|
|
918
|
+
throw new Error("accountId, databaseId and apiToken are required");
|
|
919
|
+
}
|
|
920
|
+
return { driver: "d1", accountId, databaseId, apiToken };
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
754
924
|
// src/ssh-tunnel.ts
|
|
755
925
|
import { createServer } from "net";
|
|
756
926
|
import { mkdir, readFile as readFile2, appendFile } from "fs/promises";
|
|
@@ -1098,7 +1268,8 @@ var factories = {
|
|
|
1098
1268
|
postgres: postgresDriverFactory,
|
|
1099
1269
|
sqlite: sqliteDriverFactory,
|
|
1100
1270
|
mssql: mssqlDriverFactory,
|
|
1101
|
-
oracle: oracleDriverFactory
|
|
1271
|
+
oracle: oracleDriverFactory,
|
|
1272
|
+
d1: d1DriverFactory
|
|
1102
1273
|
};
|
|
1103
1274
|
function isDriverType(value) {
|
|
1104
1275
|
return typeof value === "string" && value in factories;
|
|
@@ -1109,12 +1280,12 @@ function parseDriverConfig(body) {
|
|
|
1109
1280
|
throw new Error(`Unsupported driver: ${String(driver)}`);
|
|
1110
1281
|
}
|
|
1111
1282
|
const config = factories[driver].parseConfig(body);
|
|
1112
|
-
if (config.driver === "sqlite") return config;
|
|
1283
|
+
if (config.driver === "sqlite" || config.driver === "d1") return config;
|
|
1113
1284
|
const ssh = parseSshConfig(body.ssh);
|
|
1114
1285
|
return ssh ? { ...config, ssh } : config;
|
|
1115
1286
|
}
|
|
1116
1287
|
async function withTunnel(config, run) {
|
|
1117
|
-
if (config.driver === "sqlite" || !config.ssh) return run(config, void 0);
|
|
1288
|
+
if (config.driver === "sqlite" || config.driver === "d1" || !config.ssh) return run(config, void 0);
|
|
1118
1289
|
const tunnel = await openSshTunnel(config.ssh, { host: config.host, port: config.port });
|
|
1119
1290
|
try {
|
|
1120
1291
|
return await run({ ...config, host: tunnel.host, port: tunnel.port, ssh: void 0 }, tunnel);
|
|
@@ -1140,7 +1311,7 @@ function tunneled(driver, tunnel, label) {
|
|
|
1140
1311
|
function openDriver(config) {
|
|
1141
1312
|
return withTunnel(config, async (direct, tunnel) => {
|
|
1142
1313
|
const driver = await openDirect(direct);
|
|
1143
|
-
if (!tunnel || config.driver === "sqlite" || !config.ssh) return driver;
|
|
1314
|
+
if (!tunnel || config.driver === "sqlite" || config.driver === "d1" || !config.ssh) return driver;
|
|
1144
1315
|
const label = `${config.user}@${config.host}:${config.port} via ssh ${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`;
|
|
1145
1316
|
return tunneled(driver, tunnel, label);
|
|
1146
1317
|
});
|
|
@@ -1166,6 +1337,8 @@ function openDirect(config) {
|
|
|
1166
1337
|
return factories.mssql.open(config);
|
|
1167
1338
|
case "oracle":
|
|
1168
1339
|
return factories.oracle.open(config);
|
|
1340
|
+
case "d1":
|
|
1341
|
+
return factories.d1.open(config);
|
|
1169
1342
|
}
|
|
1170
1343
|
}
|
|
1171
1344
|
function testDirect(config) {
|
|
@@ -1180,6 +1353,8 @@ function testDirect(config) {
|
|
|
1180
1353
|
return factories.mssql.test(config);
|
|
1181
1354
|
case "oracle":
|
|
1182
1355
|
return factories.oracle.test(config);
|
|
1356
|
+
case "d1":
|
|
1357
|
+
return factories.d1.test(config);
|
|
1183
1358
|
}
|
|
1184
1359
|
}
|
|
1185
1360
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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/SQL Server/Oracle databases from your machine.",
|
|
5
|
-
"license": "
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Local agent for Planche — lets the browser-based Planche SQL client reach MySQL/MariaDB/PostgreSQL/SQLite/SQL Server/Oracle/Neon/Supabase/Cloudflare D1 databases from your machine.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://planche.dev",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"mssql",
|
|
32
32
|
"sqlserver",
|
|
33
33
|
"oracle",
|
|
34
|
+
"neon",
|
|
35
|
+
"supabase",
|
|
36
|
+
"cloudflare-d1",
|
|
37
|
+
"d1",
|
|
34
38
|
"sql",
|
|
35
39
|
"client",
|
|
36
40
|
"gui"
|