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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +181 -6
  3. 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 Oracle.
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
- MIT
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: false } : void 0,
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.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
- "license": "MIT",
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"