planche 0.8.14 → 0.9.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.
Files changed (141) hide show
  1. package/app/assets/1p4z4GHT.js +2 -0
  2. package/app/assets/{ClQzFEcL.js → 6NUleTnS.js} +1 -1
  3. package/app/assets/ANL7Od0T.js +1 -0
  4. package/app/assets/B7zF7RU4.js +2 -0
  5. package/app/assets/B8zpsP_u.js +959 -0
  6. package/app/assets/B9DHeSQH.js +1 -0
  7. package/app/assets/B9NCreMu.js +1 -0
  8. package/app/assets/{CjoKIEBz.js → B9dhM8vw.js} +1 -1
  9. package/app/assets/BFw3KCAr.js +1 -0
  10. package/app/assets/BGuDcBB_.js +1 -0
  11. package/app/assets/BHe42P9W.js +1 -0
  12. package/app/assets/BJFk0Zzq.js +2 -0
  13. package/app/assets/BMGo7X_N.js +2 -0
  14. package/app/assets/BRZhVXRC.js +1 -0
  15. package/app/assets/BVwlLPu6.js +1 -0
  16. package/app/assets/BYWnKgIY.js +3 -0
  17. package/app/assets/BYvM8E39.js +1 -0
  18. package/app/assets/B_1rFijd.js +1 -0
  19. package/app/assets/Bgu3teFB.js +11 -0
  20. package/app/assets/BgvMpmFj.js +1 -0
  21. package/app/assets/BhPOHTk7.js +7 -0
  22. package/app/assets/BiyRPyXV.js +1 -0
  23. package/app/assets/Bkq2aetC.js +1 -0
  24. package/app/assets/BnI4D_rY.js +1 -0
  25. package/app/assets/BniymiMs.js +2 -0
  26. package/app/assets/Br4335vw.js +1 -0
  27. package/app/assets/BsYZ-U7K.js +1 -0
  28. package/app/assets/BvEGx3Si.js +4 -0
  29. package/app/assets/Bvu_eiyU.js +3 -0
  30. package/app/assets/C1gqjEN3.js +1 -0
  31. package/app/assets/C2S2GXdP.js +1 -0
  32. package/app/assets/C3w9xAkk.js +1 -0
  33. package/app/assets/C4QyqQoa.js +1 -0
  34. package/app/assets/C5Yv1gpQ.js +1 -0
  35. package/app/assets/CCQ6oQgi.js +1 -0
  36. package/app/assets/{CoSpkBZh.js → CDfSdkjY.js} +1 -1
  37. package/app/assets/CIIDOG9Y.js +1 -0
  38. package/app/assets/CP2U-viH.js +1 -0
  39. package/app/assets/CVZ3Bw6o.js +1 -0
  40. package/app/assets/CWb3a6Xp.js +1 -0
  41. package/app/assets/CWota0kb.js +1 -0
  42. package/app/assets/{B1pia_Rg.js → CYz610vE.js} +1 -1
  43. package/app/assets/Cdop6jK1.js +2 -0
  44. package/app/assets/CgQ0qqq4.js +1 -0
  45. package/app/assets/CiL8zoYm.js +1 -0
  46. package/app/assets/CkIm3SJy.js +1 -0
  47. package/app/assets/ClytWHSf.js +1 -0
  48. package/app/assets/CnoXRrek.js +1 -0
  49. package/app/assets/CqTgU2vp.css +2 -0
  50. package/app/assets/Cu_sGzNJ.js +1 -0
  51. package/app/assets/CwsH45Wr.js +2 -0
  52. package/app/assets/D3_CBO8-.js +1 -0
  53. package/app/assets/D4l4mWtD.js +1 -0
  54. package/app/assets/D6oRJsd8.js +1 -0
  55. package/app/assets/D8_hwGjG.js +1 -0
  56. package/app/assets/{CEtGUZGE.js → D8dVzi6I.js} +1 -1
  57. package/app/assets/DDeeBXtX.js +1 -0
  58. package/app/assets/DDjCZLb0.js +1 -0
  59. package/app/assets/DFeHpd8Y.js +1 -0
  60. package/app/assets/DKf3DkR6.js +1 -0
  61. package/app/assets/DO0W14Ho.js +1 -0
  62. package/app/assets/DPnYgSl3.js +1 -0
  63. package/app/assets/DU-Eln5x.js +1 -0
  64. package/app/assets/DY1k-USH.js +1 -0
  65. package/app/assets/DeNEO8HN.js +1 -0
  66. package/app/assets/DkqeIq13.js +1 -0
  67. package/app/assets/DnRoHgQS.js +1 -0
  68. package/app/assets/Dy6-E_uL.js +1 -0
  69. package/app/assets/DyQ8cEic.js +1 -0
  70. package/app/assets/DzRyVlxz.js +41 -0
  71. package/app/assets/FAouNaq9.js +1 -0
  72. package/app/assets/Nro86Y63.js +1 -0
  73. package/app/assets/Q6DQerac.js +8 -0
  74. package/app/assets/S7HbowYz.js +373 -0
  75. package/app/assets/UX-csQNt.js +2 -0
  76. package/app/assets/Udlj1Cgf.js +1 -0
  77. package/app/assets/{C0bzfYFC.js → XCH_yrJ8.js} +1 -1
  78. package/app/assets/_zaonxnw.js +1 -0
  79. package/app/assets/f6MhVR01.js +27 -0
  80. package/app/assets/tQiLT0zv.js +2 -0
  81. package/app/index.html +35 -27
  82. package/app/landing.css +17 -0
  83. package/dist/{chunk-XAI7WLQN.js → chunk-SDRLD5PK.js} +995 -121
  84. package/dist/embed.js +1 -1
  85. package/dist/index.js +1 -1
  86. package/package.json +6 -2
  87. package/app/assets/8RmjiG1u.js +0 -1
  88. package/app/assets/B6E7IbMa.js +0 -1
  89. package/app/assets/BBLekwhn.js +0 -1
  90. package/app/assets/BCmzDvBt.js +0 -1
  91. package/app/assets/BHsXOlR9.js +0 -2
  92. package/app/assets/BLPGwQub.js +0 -1
  93. package/app/assets/BO_H_8D-.js +0 -1
  94. package/app/assets/BUcTqhSL.js +0 -1
  95. package/app/assets/BX4n4TJQ.js +0 -1
  96. package/app/assets/BZ6aghND.js +0 -959
  97. package/app/assets/BbFio3pA.js +0 -1
  98. package/app/assets/BbdU8Zop.js +0 -2
  99. package/app/assets/Bdp1DKAx.js +0 -1
  100. package/app/assets/BgkATqP8.js +0 -1
  101. package/app/assets/Bpq2Whsw.js +0 -3
  102. package/app/assets/BuhEpB12.js +0 -1
  103. package/app/assets/BxW20weH.js +0 -1
  104. package/app/assets/C-8gMuEc.js +0 -1
  105. package/app/assets/C29gYFqD.js +0 -2
  106. package/app/assets/C3PpGnxB.js +0 -1
  107. package/app/assets/CF6jyexg.js +0 -41
  108. package/app/assets/CNC8pjhK.js +0 -1
  109. package/app/assets/CS2mpWDL.js +0 -1
  110. package/app/assets/CSYxJn5_.js +0 -1
  111. package/app/assets/CVpzA8TC.js +0 -8
  112. package/app/assets/CZoj-eMm.js +0 -1
  113. package/app/assets/Cf8o-5U6.js +0 -2
  114. package/app/assets/CkPnfgzt.js +0 -2
  115. package/app/assets/ClIJYMXh.js +0 -1
  116. package/app/assets/CxGlndnu.js +0 -1
  117. package/app/assets/D0jMUNSI.js +0 -2
  118. package/app/assets/D3ZaTPbf.js +0 -1
  119. package/app/assets/D6kbK-ng.js +0 -1
  120. package/app/assets/DEEGN6UG.js +0 -6
  121. package/app/assets/DF5d9evm.js +0 -1
  122. package/app/assets/DGpfJHer.js +0 -1
  123. package/app/assets/DOU2MWdA.js +0 -1
  124. package/app/assets/DPdwSUSw.js +0 -1
  125. package/app/assets/DXtaGfxh.js +0 -1
  126. package/app/assets/DbqogdCK.js +0 -356
  127. package/app/assets/DdHg_zlz.js +0 -1
  128. package/app/assets/DiIO0MJM.js +0 -1
  129. package/app/assets/DnZlL3PP.js +0 -1
  130. package/app/assets/H8HkC925.js +0 -1
  131. package/app/assets/KuSp3gJs.css +0 -2
  132. package/app/assets/VZqLr_d1.js +0 -7
  133. package/app/assets/_OOlglTI.js +0 -1
  134. package/app/assets/bgQ-QVd7.js +0 -1
  135. package/app/assets/mA5FXiEh.js +0 -2
  136. package/app/assets/mZyfO6B4.js +0 -1
  137. package/app/assets/p4yYbMPY.js +0 -2
  138. package/app/assets/pjlgs2cl.js +0 -1
  139. package/app/assets/pneb1la9.js +0 -1
  140. package/app/assets/uVZVQeN3.js +0 -27
  141. package/app/assets/zspOy9D_.js +0 -2
@@ -11,6 +11,7 @@ var PRO_FEATURES = [
11
11
  "sync",
12
12
  "ai_explain"
13
13
  ];
14
+ var BATCH_LIMIT = 1e3;
14
15
 
15
16
  // src/ai-cli.ts
16
17
  import { execFile as execFileCb2, spawn } from "child_process";
@@ -728,17 +729,17 @@ var MySqlDriver = class {
728
729
  if (USE_STATEMENT.test(sql2)) this.currentDatabase = null;
729
730
  if (Array.isArray(results) && fields) {
730
731
  const fieldPackets = fields;
731
- const mappedFields = fieldPackets.map((field) => ({
732
- name: field.name,
733
- orgName: field.orgName ?? field.name,
734
- type: getTypeName(field.type ?? 253, field.characterSet),
735
- table: field.table ?? "",
736
- orgTable: field.orgTable ?? "",
737
- db: field.db ?? "",
738
- maxLength: field.length ?? 0
732
+ const mappedFields = fieldPackets.map((field3) => ({
733
+ name: field3.name,
734
+ orgName: field3.orgName ?? field3.name,
735
+ type: getTypeName(field3.type ?? 253, field3.characterSet),
736
+ table: field3.table ?? "",
737
+ orgTable: field3.orgTable ?? "",
738
+ db: field3.db ?? "",
739
+ maxLength: field3.length ?? 0
739
740
  }));
740
741
  const records = results.map(
741
- (row) => fieldPackets.map((field) => row[field.name])
742
+ (row) => fieldPackets.map((field3) => row[field3.name])
742
743
  );
743
744
  return { fields: mappedFields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
744
745
  }
@@ -942,16 +943,16 @@ var PostgresDriver = class {
942
943
  const last = [...results].reverse().find((r) => r.fields.length > 0) ?? results[results.length - 1];
943
944
  if (last.fields.length > 0) {
944
945
  await this.resolveRelations(last.fields.map((f) => f.tableID));
945
- const fields = last.fields.map((field) => {
946
- const rel = this.relations.get(field.tableID);
946
+ const fields = last.fields.map((field3) => {
947
+ const rel = this.relations.get(field3.tableID);
947
948
  return {
948
- name: field.name,
949
- orgName: rel?.columns.get(field.columnID) ?? field.name,
950
- type: this.typeNames[field.dataTypeID] ?? `oid:${field.dataTypeID}`,
949
+ name: field3.name,
950
+ orgName: rel?.columns.get(field3.columnID) ?? field3.name,
951
+ type: this.typeNames[field3.dataTypeID] ?? `oid:${field3.dataTypeID}`,
951
952
  table: rel?.table ?? "",
952
953
  orgTable: rel?.table ?? "",
953
954
  db: rel?.schema ?? "",
954
- maxLength: field.dataTypeSize > 0 ? field.dataTypeSize : 0
955
+ maxLength: field3.dataTypeSize > 0 ? field3.dataTypeSize : 0
955
956
  };
956
957
  });
957
958
  return { fields, records: last.rows, affectedRows: 0, insertId: 0, isResultQuery: true };
@@ -2305,6 +2306,799 @@ var doRemoteDriverFactory = {
2305
2306
  }
2306
2307
  };
2307
2308
 
2309
+ // src/drivers/redis.ts
2310
+ import { Redis } from "ioredis";
2311
+
2312
+ // ../shared/redis-command.ts
2313
+ var RedisSyntaxError = class extends Error {
2314
+ constructor(message) {
2315
+ super(message);
2316
+ this.name = "RedisSyntaxError";
2317
+ }
2318
+ };
2319
+ var SPACE = /\s/;
2320
+ var HEX = /^[0-9a-fA-F]{2}$/;
2321
+ var ESCAPES = { n: "\n", r: "\r", t: " ", b: "\b", a: "\x07" };
2322
+ function tokenizeRedisCommand(line) {
2323
+ const args = [];
2324
+ let i = 0;
2325
+ const n = line.length;
2326
+ while (true) {
2327
+ while (i < n && SPACE.test(line[i])) i++;
2328
+ if (i >= n) return args;
2329
+ let current = "";
2330
+ let inDouble = false;
2331
+ let inSingle = false;
2332
+ let done = false;
2333
+ while (!done) {
2334
+ if (inDouble) {
2335
+ if (i >= n) throw new RedisSyntaxError("Unbalanced quotes in command");
2336
+ const ch = line[i];
2337
+ if (ch === "\\" && line[i + 1] === "x" && HEX.test(line.slice(i + 2, i + 4))) {
2338
+ current += String.fromCharCode(parseInt(line.slice(i + 2, i + 4), 16));
2339
+ i += 4;
2340
+ continue;
2341
+ }
2342
+ if (ch === "\\" && i + 1 < n) {
2343
+ const next = line[i + 1];
2344
+ current += ESCAPES[next] ?? next;
2345
+ i += 2;
2346
+ continue;
2347
+ }
2348
+ if (ch === '"') {
2349
+ if (i + 1 < n && !SPACE.test(line[i + 1])) throw new RedisSyntaxError("Closing quote must be followed by a space");
2350
+ inDouble = false;
2351
+ done = true;
2352
+ i++;
2353
+ continue;
2354
+ }
2355
+ current += ch;
2356
+ i++;
2357
+ } else if (inSingle) {
2358
+ if (i >= n) throw new RedisSyntaxError("Unbalanced quotes in command");
2359
+ const ch = line[i];
2360
+ if (ch === "\\" && line[i + 1] === "'") {
2361
+ current += "'";
2362
+ i += 2;
2363
+ continue;
2364
+ }
2365
+ if (ch === "'") {
2366
+ if (i + 1 < n && !SPACE.test(line[i + 1])) throw new RedisSyntaxError("Closing quote must be followed by a space");
2367
+ inSingle = false;
2368
+ done = true;
2369
+ i++;
2370
+ continue;
2371
+ }
2372
+ current += ch;
2373
+ i++;
2374
+ } else {
2375
+ if (i >= n) {
2376
+ done = true;
2377
+ continue;
2378
+ }
2379
+ const ch = line[i];
2380
+ if (SPACE.test(ch)) {
2381
+ done = true;
2382
+ continue;
2383
+ }
2384
+ if (ch === '"') inDouble = true;
2385
+ else if (ch === "'") inSingle = true;
2386
+ else current += ch;
2387
+ i++;
2388
+ }
2389
+ }
2390
+ args.push(current);
2391
+ }
2392
+ }
2393
+ function redisCommandName(args) {
2394
+ const name = (args[0] ?? "").toUpperCase();
2395
+ if (CONTAINER_COMMANDS.has(name) && args.length > 1) return `${name} ${args[1].toUpperCase()}`;
2396
+ return name;
2397
+ }
2398
+ var CONTAINER_COMMANDS = /* @__PURE__ */ new Set([
2399
+ "ACL",
2400
+ "CLIENT",
2401
+ "CLUSTER",
2402
+ "COMMAND",
2403
+ "CONFIG",
2404
+ "FUNCTION",
2405
+ "LATENCY",
2406
+ "MEMORY",
2407
+ "MODULE",
2408
+ "OBJECT",
2409
+ "PUBSUB",
2410
+ "SCRIPT",
2411
+ "SLOWLOG",
2412
+ "XGROUP",
2413
+ "XINFO"
2414
+ ]);
2415
+ var READ_ONLY_EXTRA = /* @__PURE__ */ new Set([
2416
+ "PING",
2417
+ "ECHO",
2418
+ "INFO",
2419
+ "TIME",
2420
+ "SELECT",
2421
+ "LASTSAVE",
2422
+ "ROLE",
2423
+ "QUIT",
2424
+ "RESET",
2425
+ "COMMAND",
2426
+ "COMMAND INFO",
2427
+ "COMMAND COUNT",
2428
+ "COMMAND DOCS",
2429
+ "COMMAND LIST",
2430
+ "COMMAND GETKEYS",
2431
+ "CLIENT ID",
2432
+ "CLIENT INFO",
2433
+ "CLIENT GETNAME"
2434
+ ]);
2435
+ var SCRIPTED = /* @__PURE__ */ new Set(["EVAL", "EVALSHA", "FCALL", "MULTI", "EXEC"]);
2436
+ function isRedisReadOnlyCommand(args, flags) {
2437
+ const name = redisCommandName(args);
2438
+ if (READ_ONLY_EXTRA.has(name)) return true;
2439
+ if (SCRIPTED.has(name) || !flags) return false;
2440
+ if (flags.includes("write") || flags.includes("admin") || flags.includes("pubsub") || flags.includes("blocking")) return false;
2441
+ return flags.includes("readonly");
2442
+ }
2443
+
2444
+ // src/read-only.ts
2445
+ var ReadOnlyViolation = class extends Error {
2446
+ status = 403;
2447
+ constructor(message) {
2448
+ super(message);
2449
+ this.name = "ReadOnlyViolation";
2450
+ }
2451
+ };
2452
+ var READ_KEYWORDS = /* @__PURE__ */ new Set(["SELECT", "WITH", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "VALUES", "TABLE", "PRAGMA"]);
2453
+ function splitStatements(sql2) {
2454
+ const out = [];
2455
+ let cur = "";
2456
+ let i = 0;
2457
+ const n = sql2.length;
2458
+ while (i < n) {
2459
+ const ch = sql2[i];
2460
+ const next = sql2[i + 1];
2461
+ if (ch === "-" && next === "-") {
2462
+ while (i < n && sql2[i] !== "\n") i++;
2463
+ cur += " ";
2464
+ continue;
2465
+ }
2466
+ if (ch === "#") {
2467
+ while (i < n && sql2[i] !== "\n") i++;
2468
+ cur += " ";
2469
+ continue;
2470
+ }
2471
+ if (ch === "/" && next === "*") {
2472
+ i += 2;
2473
+ while (i < n && !(sql2[i] === "*" && sql2[i + 1] === "/")) i++;
2474
+ i += 2;
2475
+ cur += " ";
2476
+ continue;
2477
+ }
2478
+ if (ch === "'" || ch === '"' || ch === "`" || ch === "[") {
2479
+ const close = ch === "[" ? "]" : ch;
2480
+ i++;
2481
+ while (i < n) {
2482
+ if (sql2[i] === "\\" && close === "'") {
2483
+ i += 2;
2484
+ continue;
2485
+ }
2486
+ if (sql2[i] === close) {
2487
+ if (sql2[i + 1] === close && close !== "]") {
2488
+ i += 2;
2489
+ continue;
2490
+ }
2491
+ break;
2492
+ }
2493
+ i++;
2494
+ }
2495
+ i++;
2496
+ cur += ch === "[" ? " x " : " 'x' ";
2497
+ continue;
2498
+ }
2499
+ if (ch === "$" && next === "$") {
2500
+ const end = sql2.indexOf("$$", i + 2);
2501
+ i = end === -1 ? n : end + 2;
2502
+ cur += " 'x' ";
2503
+ continue;
2504
+ }
2505
+ if (ch === ";") {
2506
+ out.push(cur);
2507
+ cur = "";
2508
+ i++;
2509
+ continue;
2510
+ }
2511
+ cur += ch;
2512
+ i++;
2513
+ }
2514
+ out.push(cur);
2515
+ return out.map((s) => s.trim()).filter((s) => s.length > 0);
2516
+ }
2517
+ function assertReadOnlyStatement(sql2) {
2518
+ const statements = splitStatements(sql2);
2519
+ if (statements.length === 0) return;
2520
+ if (statements.length > 1) throw new ReadOnlyViolation("Read-only profile: one statement per request");
2521
+ const stmt = statements[0];
2522
+ const first = /^[A-Za-z]+/.exec(stmt)?.[0].toUpperCase() ?? "";
2523
+ if (!READ_KEYWORDS.has(first)) throw new ReadOnlyViolation(`Read-only profile: ${first || "this statement"} is not allowed`);
2524
+ if (first === "WITH") {
2525
+ const words = stmt.toUpperCase().match(/\b(INSERT|UPDATE|DELETE|MERGE|REPLACE)\b/);
2526
+ if (words) throw new ReadOnlyViolation(`Read-only profile: ${words[1]} inside WITH is not allowed`);
2527
+ }
2528
+ if (first === "PRAGMA" && /[=(]/.test(stmt)) throw new ReadOnlyViolation("Read-only profile: PRAGMA may only read");
2529
+ if (/\bINTO\s+(OUTFILE|DUMPFILE)\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: INTO OUTFILE is not allowed");
2530
+ if (/\bFOR\s+(UPDATE|SHARE|NO\s+KEY\s+UPDATE|KEY\s+SHARE)\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: locking reads are not allowed");
2531
+ if (/\bLOCK\s+IN\s+SHARE\s+MODE\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: locking reads are not allowed");
2532
+ if (first === "SELECT" && /\bINTO\b/i.test(stmt) && !/\bINTO\s+@/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: SELECT INTO is not allowed");
2533
+ }
2534
+
2535
+ // src/drivers/redis.ts
2536
+ function clusterError(err) {
2537
+ const message = err instanceof Error ? err.message : String(err);
2538
+ const redirect = /^(MOVED|ASK) \d+ (\S+)/.exec(message);
2539
+ if (redirect) {
2540
+ return new Error(`This key lives on another node of the Redis Cluster (${redirect[2]}). Planche connects to a single node and does not follow cluster redirects yet; connect a profile to ${redirect[2]} to work with it.`);
2541
+ }
2542
+ if (/^CLUSTERDOWN/.test(message)) return new Error(`The Redis Cluster is down: ${message}`);
2543
+ return null;
2544
+ }
2545
+ function redisDatabaseIndex(name) {
2546
+ const m = name ? /^db(\d{1,5})$/.exec(name) : null;
2547
+ return m ? Number(m[1]) : null;
2548
+ }
2549
+ function field(name, type = "string") {
2550
+ return { name, orgName: name, type, table: "", orgTable: "", db: "", maxLength: 0 };
2551
+ }
2552
+ function plainReply(reply) {
2553
+ if (reply === null || reply === void 0) return null;
2554
+ if (Buffer.isBuffer(reply)) return reply.toString("utf8");
2555
+ if (reply instanceof Error) return { error: reply.message };
2556
+ if (Array.isArray(reply)) return reply.map(plainReply);
2557
+ if (typeof reply === "object") {
2558
+ return Object.fromEntries(Object.entries(reply).map(([k, v]) => [k, plainReply(v)]));
2559
+ }
2560
+ return reply;
2561
+ }
2562
+ function cell(value) {
2563
+ if (value === null || typeof value === "string" || typeof value === "number") return value;
2564
+ return JSON.stringify(value);
2565
+ }
2566
+ var PAIR_REPLIES = {
2567
+ HGETALL: ["field", "value"],
2568
+ "CONFIG GET": ["parameter", "value"]
2569
+ };
2570
+ var WITHSCORES = /* @__PURE__ */ new Set(["ZRANGE", "ZREVRANGE", "ZRANGEBYSCORE", "ZREVRANGEBYSCORE", "ZRANDMEMBER", "ZDIFF", "ZINTER", "ZUNION"]);
2571
+ var SCAN_COLUMNS = {
2572
+ SCAN: ["key"],
2573
+ SSCAN: ["member"],
2574
+ HSCAN: ["field", "value"],
2575
+ ZSCAN: ["member", "score"]
2576
+ };
2577
+ function pairs(values, columns) {
2578
+ const rows = [];
2579
+ for (let i = 0; i < values.length; i += columns.length) rows.push(columns.map((_, j) => cell(values[i + j] ?? null)));
2580
+ return rows;
2581
+ }
2582
+ function replyToResult(args, raw) {
2583
+ const reply = plainReply(raw);
2584
+ const base = { affectedRows: 0, insertId: 0, isResultQuery: true, reply };
2585
+ const name = redisCommandName(args);
2586
+ const upperArgs = args.map((a) => a.toUpperCase());
2587
+ if (Array.isArray(reply)) {
2588
+ const scan = SCAN_COLUMNS[name];
2589
+ if (scan && reply.length === 2 && Array.isArray(reply[1])) {
2590
+ const columns = hscanNoValues(name, upperArgs) ? ["field"] : scan;
2591
+ return { ...base, fields: columns.map((c) => field(c)), records: pairs(reply[1], columns) };
2592
+ }
2593
+ const pair = PAIR_REPLIES[name] ?? (WITHSCORES.has(name) && upperArgs.includes("WITHSCORES") ? ["member", "score"] : void 0) ?? (name === "HRANDFIELD" && upperArgs.includes("WITHVALUES") ? ["field", "value"] : void 0);
2594
+ if (pair && reply.every((v) => !Array.isArray(v))) {
2595
+ return { ...base, fields: pair.map((c) => field(c)), records: pairs(reply, pair) };
2596
+ }
2597
+ return {
2598
+ ...base,
2599
+ fields: [field("#", "int"), field("value")],
2600
+ records: reply.map((v, i) => [i + 1, cell(v)])
2601
+ };
2602
+ }
2603
+ if (reply !== null && typeof reply === "object") {
2604
+ return {
2605
+ ...base,
2606
+ fields: [field("field"), field("value")],
2607
+ records: Object.entries(reply).map(([k, v]) => [k, cell(v)])
2608
+ };
2609
+ }
2610
+ return { ...base, fields: [field("value", typeof reply === "number" ? "int" : "string")], records: [[reply]] };
2611
+ }
2612
+ function hscanNoValues(name, upperArgs) {
2613
+ return name === "HSCAN" && upperArgs.includes("NOVALUES");
2614
+ }
2615
+ function toOptions(config) {
2616
+ return {
2617
+ host: config.host,
2618
+ port: config.port,
2619
+ username: config.user || void 0,
2620
+ password: config.password || void 0,
2621
+ db: config.db ?? 0,
2622
+ tls: config.ssl ? { rejectUnauthorized: config.sslVerify === true, servername: config.host } : void 0,
2623
+ // RESP2 works on every server (RESP3 needs Redis 6 and HELLO).
2624
+ protocol: 2,
2625
+ // Integers above 2^53 (INCR counters, IDs) must not lose digits.
2626
+ stringNumbers: true,
2627
+ lazyConnect: true,
2628
+ connectTimeout: 1e4,
2629
+ // The app reconnects from the profile when a connection is lost; a
2630
+ // silently reconnecting socket would forget the selected database.
2631
+ retryStrategy: () => null,
2632
+ maxRetriesPerRequest: 0,
2633
+ enableOfflineQueue: false,
2634
+ connectionName: "planche"
2635
+ };
2636
+ }
2637
+ async function connect(config) {
2638
+ const client2 = new Redis(toOptions(config));
2639
+ client2.on("error", (err) => console.error("[redis] Connection error:", err.message));
2640
+ try {
2641
+ await client2.connect();
2642
+ } catch (err) {
2643
+ client2.disconnect();
2644
+ throw err;
2645
+ }
2646
+ return client2;
2647
+ }
2648
+ var RedisDriver = class {
2649
+ constructor(client2, config, clientId, cluster) {
2650
+ this.client = client2;
2651
+ this.config = config;
2652
+ this.clientId = clientId;
2653
+ this.cluster = cluster;
2654
+ this.label = `${config.user ? `${config.user}@` : ""}${config.host}:${config.port}${cluster ? " (cluster node)" : ""}`;
2655
+ }
2656
+ client;
2657
+ config;
2658
+ clientId;
2659
+ cluster;
2660
+ type = "redis";
2661
+ label;
2662
+ /** Open feeds (Pub/Sub, MONITOR), each with the function that closes it. */
2663
+ feeds = /* @__PURE__ */ new Set();
2664
+ /** `COMMAND INFO` flags per command name, for read-only sessions. */
2665
+ flags = /* @__PURE__ */ new Map();
2666
+ /**
2667
+ * Run `sql`, in logical database `database` when one is given. The
2668
+ * connection is shared by every request of this profile (tree, console
2669
+ * tabs), so the `SELECT` and the command go out as one pipeline — one
2670
+ * socket write, with no other request able to land between them. Tracking
2671
+ * the current database instead would race: two requests for different
2672
+ * databases could each see the other's `SELECT`.
2673
+ */
2674
+ async query(sql2, database) {
2675
+ const args = parseCommand(sql2);
2676
+ const db = this.databaseFor(database);
2677
+ try {
2678
+ if (db === null) return replyToResult(args, await this.client.call(args[0], args.slice(1)));
2679
+ const replies = await this.client.pipeline().call("SELECT", String(db)).call(args[0], ...args.slice(1)).exec();
2680
+ const [selectReply, commandReply] = replies ?? [];
2681
+ if (selectReply?.[0]) throw selectReply[0];
2682
+ if (!commandReply) throw new Error("No reply from Redis");
2683
+ if (commandReply[0]) throw commandReply[0];
2684
+ return replyToResult(args, commandReply[1]);
2685
+ } catch (err) {
2686
+ throw clusterError(err) ?? err;
2687
+ }
2688
+ }
2689
+ /** The database index to select, or null for none; a cluster has only db0. */
2690
+ databaseFor(database) {
2691
+ const db = redisDatabaseIndex(database);
2692
+ if (this.cluster && db !== null && db > 0) throw new Error("A Redis Cluster has only db0.");
2693
+ return this.cluster ? null : db;
2694
+ }
2695
+ /**
2696
+ * All statements in one pipeline, behind the same `SELECT` — one socket
2697
+ * write, so another request can neither interleave nor switch the
2698
+ * database midway. A line that does not parse, or a command Redis refuses,
2699
+ * fills its own slot with the error.
2700
+ */
2701
+ async batch(statements, database) {
2702
+ const parsed = statements.map((sql2) => {
2703
+ try {
2704
+ return parseCommand(sql2);
2705
+ } catch (err) {
2706
+ return err instanceof Error ? err : new Error(String(err));
2707
+ }
2708
+ });
2709
+ const pipeline = this.client.pipeline();
2710
+ const db = this.databaseFor(database);
2711
+ if (db !== null) pipeline.call("SELECT", String(db));
2712
+ for (const args of parsed) if (!(args instanceof Error)) pipeline.call(args[0], ...args.slice(1));
2713
+ const replies = await pipeline.exec() ?? [];
2714
+ if (db !== null) {
2715
+ const selected = replies.shift();
2716
+ if (selected?.[0]) throw selected[0];
2717
+ }
2718
+ return parsed.map((args) => {
2719
+ if (args instanceof Error) return args;
2720
+ const [err, reply] = replies.shift() ?? [new Error("No reply from Redis"), null];
2721
+ return err ? clusterError(err) ?? (err instanceof Error ? err : new Error(String(err))) : replyToResult(args, reply);
2722
+ });
2723
+ }
2724
+ async assertReadOnly(sql2) {
2725
+ const args = parseCommand(sql2);
2726
+ const name = args[0].toLowerCase();
2727
+ if (!this.flags.has(name)) {
2728
+ const info = await this.client.call("COMMAND", ["INFO", name]).catch(() => null);
2729
+ const entry = Array.isArray(info) ? info[0] : null;
2730
+ this.flags.set(name, Array.isArray(entry) && Array.isArray(entry[2]) ? entry[2].map((f) => String(f).toLowerCase()) : void 0);
2731
+ }
2732
+ if (!isRedisReadOnlyCommand(args, this.flags.get(name))) {
2733
+ throw new ReadOnlyViolation(`Read-only profile: ${redisCommandName(args)} is not a read command`);
2734
+ }
2735
+ }
2736
+ /**
2737
+ * A live feed on a dedicated connection: Pub/Sub messages or MONITOR's
2738
+ * log. Each feed closes when its signal aborts or the driver closes.
2739
+ */
2740
+ async stream(request, emit, signal) {
2741
+ if (request.kind === "monitor") {
2742
+ const base = new Redis({ ...toOptions(this.config), lazyConnect: true });
2743
+ const monitor = await base.monitor();
2744
+ const close2 = () => {
2745
+ monitor.disconnect();
2746
+ base.disconnect();
2747
+ this.feeds.delete(close2);
2748
+ };
2749
+ this.feeds.add(close2);
2750
+ monitor.on("error", (err) => emit({ type: "error", message: err.message }));
2751
+ monitor.on("monitor", (time, args, source, database) => {
2752
+ emit({ type: "command", time: Math.round(Number(time) * 1e3), db: `db${database}`, client: source, args: args.map(String) });
2753
+ });
2754
+ signal.addEventListener("abort", close2, { once: true });
2755
+ return;
2756
+ }
2757
+ if (request.channels.length === 0 && request.patterns.length === 0) throw new Error("Name at least one channel or pattern");
2758
+ const subscriber = await connect(this.config);
2759
+ const close = () => {
2760
+ subscriber.disconnect();
2761
+ this.feeds.delete(close);
2762
+ };
2763
+ this.feeds.add(close);
2764
+ subscriber.on("message", (channel, message) => emit({ type: "message", time: Date.now(), channel, message }));
2765
+ subscriber.on("pmessage", (pattern, channel, message) => emit({ type: "message", time: Date.now(), channel, pattern, message }));
2766
+ try {
2767
+ if (request.channels.length) await subscriber.subscribe(...request.channels);
2768
+ if (request.patterns.length) await subscriber.psubscribe(...request.patterns);
2769
+ } catch (err) {
2770
+ close();
2771
+ throw err;
2772
+ }
2773
+ signal.addEventListener("abort", close, { once: true });
2774
+ }
2775
+ /**
2776
+ * A blocked command (BLPOP, XREAD BLOCK, WAIT) is released from a second
2777
+ * connection with `CLIENT UNBLOCK`; a long Lua script with `SCRIPT KILL`.
2778
+ */
2779
+ async cancel() {
2780
+ const killer = await connect(this.config);
2781
+ try {
2782
+ if (this.clientId !== null) await killer.call("CLIENT", ["UNBLOCK", String(this.clientId), "ERROR"]).catch(() => void 0);
2783
+ await killer.call("SCRIPT", ["KILL"]).catch(() => void 0);
2784
+ } finally {
2785
+ killer.disconnect();
2786
+ }
2787
+ }
2788
+ async close() {
2789
+ for (const close of [...this.feeds]) close();
2790
+ await this.client.quit().catch(() => this.client.disconnect());
2791
+ }
2792
+ };
2793
+ var FEED_COMMANDS = /* @__PURE__ */ new Set(["SUBSCRIBE", "PSUBSCRIBE", "SSUBSCRIBE", "MONITOR"]);
2794
+ function parseCommand(sql2) {
2795
+ if (/[\r\n]/.test(sql2.trim())) throw new Error("Send one Redis command per request");
2796
+ const args = tokenizeRedisCommand(sql2);
2797
+ if (args.length === 0) throw new Error("Empty command");
2798
+ if (FEED_COMMANDS.has(args[0].toUpperCase())) {
2799
+ throw new Error(`${args[0].toUpperCase()} holds its connection open; use the ${args[0].toUpperCase() === "MONITOR" ? "Monitor" : "Pub/Sub"} tool instead of the console`);
2800
+ }
2801
+ return args;
2802
+ }
2803
+ var redisDriverFactory = {
2804
+ async open(config) {
2805
+ const client2 = await connect(config);
2806
+ const id = await client2.call("CLIENT", ["ID"]).catch(() => null);
2807
+ const info = await client2.call("INFO", ["cluster"]).catch(() => "");
2808
+ return new RedisDriver(client2, config, id === null ? null : Number(id), /cluster_enabled:1/.test(String(info)));
2809
+ },
2810
+ async test(config) {
2811
+ const client2 = await connect(config);
2812
+ try {
2813
+ await client2.call("PING");
2814
+ } finally {
2815
+ client2.disconnect();
2816
+ }
2817
+ },
2818
+ parseConfig(body) {
2819
+ const host = typeof body.host === "string" ? body.host.trim() : "";
2820
+ if (!host) throw new Error("host is required");
2821
+ const named = typeof body.database === "string" ? redisDatabaseIndex(body.database) ?? Number(body.database) : void 0;
2822
+ const db = Number(body.db ?? named ?? 0);
2823
+ return {
2824
+ driver: "redis",
2825
+ host,
2826
+ port: Number(body.port) || 6379,
2827
+ user: typeof body.user === "string" && body.user ? body.user : void 0,
2828
+ password: typeof body.password === "string" ? body.password : "",
2829
+ db: Number.isInteger(db) && db >= 0 ? db : 0,
2830
+ ssl: body.ssl === true,
2831
+ sslVerify: body.sslVerify === true
2832
+ };
2833
+ }
2834
+ };
2835
+
2836
+ // src/drivers/elasticsearch.ts
2837
+ import http from "http";
2838
+ import https from "https";
2839
+
2840
+ // ../shared/es-request.ts
2841
+ var METHOD_LINE = /^(GET|POST|PUT|DELETE|HEAD)\s+(\S+)\s*$/i;
2842
+ var EsRequestError = class extends Error {
2843
+ constructor(message) {
2844
+ super(message);
2845
+ this.name = "EsRequestError";
2846
+ }
2847
+ };
2848
+ function parseEsRequest(text) {
2849
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
2850
+ const first = lines.findIndex((l) => l.trim() !== "" && !isComment(l));
2851
+ if (first === -1) throw new EsRequestError("Empty request");
2852
+ const m = METHOD_LINE.exec(lines[first].trim());
2853
+ if (!m) throw new EsRequestError("A request starts with a method and a path, e.g. GET /_cat/indices");
2854
+ const rawPath = m[2];
2855
+ return {
2856
+ method: m[1].toUpperCase(),
2857
+ path: rawPath.startsWith("/") ? rawPath : `/${rawPath}`,
2858
+ body: lines.slice(first + 1).join("\n").trim()
2859
+ };
2860
+ }
2861
+ function isComment(line) {
2862
+ const t = line.trimStart();
2863
+ return t.startsWith("#") || t.startsWith("//");
2864
+ }
2865
+ function pathSegments(path) {
2866
+ return path.split("?")[0].split("/").filter(Boolean).map((s) => decodeURIComponent(s));
2867
+ }
2868
+ var READ_POST = /* @__PURE__ */ new Set([
2869
+ "_search",
2870
+ "_msearch",
2871
+ "_count",
2872
+ "_mget",
2873
+ "_field_caps",
2874
+ "_explain",
2875
+ "_analyze",
2876
+ "_termvectors",
2877
+ "_mtermvectors",
2878
+ "_rank_eval"
2879
+ ]);
2880
+ var READ_POST_PAIRS = /* @__PURE__ */ new Set(["_validate/query", "_search/template", "_render/template", "_msearch/template"]);
2881
+ var SQL_READ = /^\s*(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)\b/i;
2882
+ function isSqlEndpoint(segments) {
2883
+ return segments[0] === "_sql" || segments[0] === "_plugins" && segments[1] === "_sql" || segments[0] === "_opendistro" && segments[1] === "_sql";
2884
+ }
2885
+ function sqlQuery(body) {
2886
+ try {
2887
+ const parsed = JSON.parse(body);
2888
+ return typeof parsed.query === "string" ? parsed.query : null;
2889
+ } catch {
2890
+ return null;
2891
+ }
2892
+ }
2893
+ function isEsReadOnlyRequest(request) {
2894
+ if (request.method === "GET" || request.method === "HEAD") return true;
2895
+ if (request.method !== "POST") return false;
2896
+ const segments = pathSegments(request.path);
2897
+ if (isSqlEndpoint(segments)) {
2898
+ const query = sqlQuery(request.body);
2899
+ return query !== null && SQL_READ.test(query);
2900
+ }
2901
+ const last = segments[segments.length - 1] ?? "";
2902
+ const lastTwo = segments.slice(-2).join("/");
2903
+ return READ_POST.has(last) || READ_POST_PAIRS.has(lastTwo);
2904
+ }
2905
+ function isNdjsonEndpoint(path) {
2906
+ const segments = pathSegments(path);
2907
+ const last = segments[segments.length - 1] ?? "";
2908
+ return last === "_bulk" || last === "_msearch" || segments.slice(-2).join("/") === "_msearch/template";
2909
+ }
2910
+
2911
+ // src/drivers/elasticsearch.ts
2912
+ var REQUEST_TIMEOUT_MS = 6e4;
2913
+ function field2(name, type = "string") {
2914
+ return { name, orgName: name, type, table: "", orgTable: "", db: "", maxLength: 0 };
2915
+ }
2916
+ function cell2(value) {
2917
+ if (value === null || value === void 0) return null;
2918
+ if (typeof value === "object") return JSON.stringify(value);
2919
+ return value;
2920
+ }
2921
+ function isRecord(value) {
2922
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2923
+ }
2924
+ function objectsToResult(objects, leading = []) {
2925
+ const names = [...leading];
2926
+ for (const obj of objects) for (const key of Object.keys(obj)) if (!names.includes(key)) names.push(key);
2927
+ return {
2928
+ fields: names.map((n) => field2(n)),
2929
+ records: objects.map((obj) => names.map((n) => cell2(obj[n])))
2930
+ };
2931
+ }
2932
+ function esReplyToResult(request, reply) {
2933
+ const base = { affectedRows: 0, insertId: 0, isResultQuery: true };
2934
+ if (request.method === "HEAD") {
2935
+ return { ...base, fields: [field2("status", "int")], records: [[reply.status]], reply: reply.status };
2936
+ }
2937
+ let body = reply.text;
2938
+ if (reply.contentType.includes("json") && reply.text.trim()) {
2939
+ try {
2940
+ body = JSON.parse(reply.text);
2941
+ } catch {
2942
+ body = reply.text;
2943
+ }
2944
+ }
2945
+ if (typeof body === "string") {
2946
+ const lines = body.split("\n").filter((l) => l.length > 0);
2947
+ return { ...base, fields: [field2("text")], records: lines.map((l) => [l]), reply: body };
2948
+ }
2949
+ const hits = isRecord(body) && isRecord(body.hits) && Array.isArray(body.hits.hits) ? body.hits.hits : null;
2950
+ if (hits) {
2951
+ const rows = hits.filter(isRecord).map((hit) => {
2952
+ const source = isRecord(hit._source) ? hit._source : {};
2953
+ return { _index: hit._index, _id: hit._id, _score: hit._score, ...source };
2954
+ });
2955
+ const table = objectsToResult(rows, ["_index", "_id", "_score"]);
2956
+ return { ...base, ...table, reply: body };
2957
+ }
2958
+ if (Array.isArray(body)) {
2959
+ if (body.every(isRecord)) return { ...base, ...objectsToResult(body), reply: body };
2960
+ return { ...base, fields: [field2("#", "int"), field2("value")], records: body.map((v, i) => [i + 1, cell2(v)]), reply: body };
2961
+ }
2962
+ if (isRecord(body)) {
2963
+ return { ...base, fields: [field2("field"), field2("value")], records: Object.entries(body).map(([k, v]) => [k, cell2(v)]), reply: body };
2964
+ }
2965
+ return { ...base, fields: [field2("value")], records: [[cell2(body)]], reply: body };
2966
+ }
2967
+ function esErrorMessage(status, text) {
2968
+ try {
2969
+ const parsed = JSON.parse(text);
2970
+ const error = parsed.error;
2971
+ if (isRecord(error)) {
2972
+ const rootCause = Array.isArray(error.root_cause) && isRecord(error.root_cause[0]) ? error.root_cause[0] : null;
2973
+ const reason = error.reason ?? rootCause?.reason;
2974
+ return `${String(error.type ?? status)}: ${String(reason ?? "request failed")}`;
2975
+ }
2976
+ if (typeof error === "string") return error;
2977
+ if (typeof parsed.message === "string") return parsed.message;
2978
+ } catch {
2979
+ }
2980
+ return text.trim() ? `HTTP ${status}: ${text.trim().slice(0, 500)}` : `HTTP ${status}`;
2981
+ }
2982
+ function authHeader(config) {
2983
+ if (!config.password) return void 0;
2984
+ if (config.user) return `Basic ${Buffer.from(`${config.user}:${config.password}`).toString("base64")}`;
2985
+ return `ApiKey ${config.password}`;
2986
+ }
2987
+ var ElasticsearchDriver = class {
2988
+ constructor(config) {
2989
+ this.config = config;
2990
+ this.label = `${config.ssl ? "https" : "http"}://${config.host}:${config.port}`;
2991
+ }
2992
+ config;
2993
+ type = "elasticsearch";
2994
+ cancelKind = "abandon";
2995
+ label;
2996
+ inFlight = /* @__PURE__ */ new Set();
2997
+ /** One HTTP exchange; rejects only for transport failures, not for error statuses. */
2998
+ send(request) {
2999
+ const { config } = this;
3000
+ const transport = config.ssl ? https : http;
3001
+ const ndjson = isNdjsonEndpoint(request.path);
3002
+ const body = request.body ? ndjson && !request.body.endsWith("\n") ? `${request.body}
3003
+ ` : request.body : void 0;
3004
+ const headers = { accept: "application/json" };
3005
+ const auth = authHeader(config);
3006
+ if (auth) headers.authorization = auth;
3007
+ if (body !== void 0) {
3008
+ headers["content-type"] = ndjson ? "application/x-ndjson" : "application/json";
3009
+ headers["content-length"] = String(Buffer.byteLength(body));
3010
+ }
3011
+ return new Promise((resolve3, reject) => {
3012
+ const req = transport.request({
3013
+ host: config.host,
3014
+ port: config.port,
3015
+ method: request.method,
3016
+ path: request.path,
3017
+ headers,
3018
+ timeout: REQUEST_TIMEOUT_MS,
3019
+ ...config.ssl ? { rejectUnauthorized: config.sslVerify === true, servername: config.host } : {}
3020
+ }, (res) => {
3021
+ const chunks = [];
3022
+ res.on("data", (chunk) => chunks.push(chunk));
3023
+ res.on("end", () => {
3024
+ this.inFlight.delete(req);
3025
+ resolve3({ status: res.statusCode ?? 0, contentType: String(res.headers["content-type"] ?? ""), text: Buffer.concat(chunks).toString("utf8") });
3026
+ });
3027
+ res.on("error", reject);
3028
+ });
3029
+ this.inFlight.add(req);
3030
+ req.on("timeout", () => req.destroy(new Error(`No answer within ${REQUEST_TIMEOUT_MS / 1e3}s`)));
3031
+ req.on("error", (err) => {
3032
+ this.inFlight.delete(req);
3033
+ reject(err);
3034
+ });
3035
+ if (body !== void 0) req.write(body);
3036
+ req.end();
3037
+ });
3038
+ }
3039
+ async query(sql2) {
3040
+ const request = parseEsRequest(sql2);
3041
+ const reply = await this.send(request);
3042
+ if (reply.status >= 400 && request.method !== "HEAD") {
3043
+ throw Object.assign(new Error(esErrorMessage(reply.status, reply.text)), { esStatus: reply.status });
3044
+ }
3045
+ return esReplyToResult(request, reply);
3046
+ }
3047
+ async assertReadOnly(sql2) {
3048
+ const request = parseEsRequest(sql2);
3049
+ if (!isEsReadOnlyRequest(request)) {
3050
+ throw new ReadOnlyViolation(`Read-only profile: ${request.method} ${request.path.split("?")[0]} is not a read request`);
3051
+ }
3052
+ }
3053
+ /** Requests are independent HTTP calls: cancelling drops the ones in flight. */
3054
+ async cancel() {
3055
+ for (const req of this.inFlight) req.destroy(new Error("Cancelled"));
3056
+ this.inFlight.clear();
3057
+ }
3058
+ async close() {
3059
+ await this.cancel();
3060
+ }
3061
+ };
3062
+ async function identify(driver) {
3063
+ const reply = await driver.send({ method: "GET", path: "/", body: "" });
3064
+ if (reply.status === 401 || reply.status === 403) {
3065
+ throw Object.assign(new Error(esErrorMessage(reply.status, reply.text)), { status: 401 });
3066
+ }
3067
+ if (reply.status >= 400) throw new Error(esErrorMessage(reply.status, reply.text));
3068
+ try {
3069
+ const info = JSON.parse(reply.text);
3070
+ const product = info.version?.distribution === "opensearch" ? "OpenSearch" : "Elasticsearch";
3071
+ return `${product} ${info.version?.number ?? ""}`.trim();
3072
+ } catch {
3073
+ throw new Error("The server did not answer like Elasticsearch or OpenSearch");
3074
+ }
3075
+ }
3076
+ var elasticsearchDriverFactory = {
3077
+ async open(config) {
3078
+ const driver = new ElasticsearchDriver(config);
3079
+ const product = await identify(driver);
3080
+ driver.label = `${product} at ${driver.label}`;
3081
+ return driver;
3082
+ },
3083
+ async test(config) {
3084
+ await identify(new ElasticsearchDriver(config));
3085
+ },
3086
+ parseConfig(body) {
3087
+ const host = typeof body.host === "string" ? body.host.trim() : "";
3088
+ if (!host) throw new Error("host is required");
3089
+ const ssl = body.ssl === true;
3090
+ return {
3091
+ driver: "elasticsearch",
3092
+ host,
3093
+ port: Number(body.port) || (ssl ? 443 : 9200),
3094
+ user: typeof body.user === "string" && body.user ? body.user : void 0,
3095
+ password: typeof body.password === "string" ? body.password : "",
3096
+ ssl,
3097
+ sslVerify: body.sslVerify === true
3098
+ };
3099
+ }
3100
+ };
3101
+
2308
3102
  // src/ssh-tunnel.ts
2309
3103
  import { createServer } from "net";
2310
3104
  import { mkdir as mkdir4, readFile as readFile6, appendFile } from "fs/promises";
@@ -2488,9 +3282,9 @@ function keyType(blob) {
2488
3282
  if (!/^[A-Za-z0-9@.-]+$/.test(type)) throw new TunnelError("Malformed host key", 400);
2489
3283
  return type;
2490
3284
  }
2491
- function knownHostsEntryMatches(field, patterns) {
2492
- if (field.startsWith("|1|")) {
2493
- const [, , salt, hash] = field.split("|");
3285
+ function knownHostsEntryMatches(field3, patterns) {
3286
+ if (field3.startsWith("|1|")) {
3287
+ const [, , salt, hash] = field3.split("|");
2494
3288
  if (!salt || !hash) return false;
2495
3289
  const key = Buffer.from(salt, "base64");
2496
3290
  const want = Buffer.from(hash, "base64");
@@ -2499,7 +3293,7 @@ function knownHostsEntryMatches(field, patterns) {
2499
3293
  return got.length === want.length && timingSafeEqual(got, want);
2500
3294
  });
2501
3295
  }
2502
- const entries = field.toLowerCase().split(",");
3296
+ const entries = field3.toLowerCase().split(",");
2503
3297
  return patterns.some((p) => hostPatternMatches(entries, p));
2504
3298
  }
2505
3299
  function hostPatterns(host, port) {
@@ -2720,8 +3514,11 @@ var factories = {
2720
3514
  oracle: oracleDriverFactory,
2721
3515
  d1: d1DriverFactory,
2722
3516
  do: doDriverFactory,
2723
- "do-remote": doRemoteDriverFactory
3517
+ "do-remote": doRemoteDriverFactory,
3518
+ redis: redisDriverFactory,
3519
+ elasticsearch: elasticsearchDriverFactory
2724
3520
  };
3521
+ var DRIVER_TYPES = Object.keys(factories);
2725
3522
  function isDriverType(value) {
2726
3523
  return typeof value === "string" && value in factories;
2727
3524
  }
@@ -2754,7 +3551,11 @@ function tunneled(driver, tunnel, label) {
2754
3551
  type: driver.type,
2755
3552
  label,
2756
3553
  query: (sql2, database) => driver.query(sql2, database),
3554
+ batch: driver.batch ? (statements, database) => driver.batch(statements, database) : void 0,
3555
+ stream: driver.stream ? (request, emit, signal) => driver.stream(request, emit, signal) : void 0,
2757
3556
  setReadOnly: driver.setReadOnly ? () => driver.setReadOnly() : void 0,
3557
+ // Forwarded, or a read-only gateway profile over SSH would fall back to the SQL filter.
3558
+ assertReadOnly: driver.assertReadOnly ? (sql2) => driver.assertReadOnly(sql2) : void 0,
2758
3559
  // Forwarded, or a tunnelled connection would report that its engine cannot
2759
3560
  // interrupt a statement when it is only this wrapper that cannot.
2760
3561
  cancel: driver.cancel ? () => driver.cancel() : void 0,
@@ -2771,7 +3572,7 @@ function openDriver(config) {
2771
3572
  return withTunnel(config, async (direct, tunnel) => {
2772
3573
  const driver = await openDirect(direct);
2773
3574
  if (!tunnel || isDirectDriver(config) || !config.ssh) return driver;
2774
- const label = `${config.user}@${config.host}:${config.port} via ssh ${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`;
3575
+ const label = `${config.user ? `${config.user}@` : ""}${config.host}:${config.port} via ssh ${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`;
2775
3576
  return tunneled(driver, tunnel, label);
2776
3577
  });
2777
3578
  }
@@ -2802,6 +3603,10 @@ function openDirect(config) {
2802
3603
  return factories.do.open(config);
2803
3604
  case "do-remote":
2804
3605
  return factories["do-remote"].open(config);
3606
+ case "redis":
3607
+ return factories.redis.open(config);
3608
+ case "elasticsearch":
3609
+ return factories.elasticsearch.open(config);
2805
3610
  }
2806
3611
  }
2807
3612
  function testDirect(config) {
@@ -2822,6 +3627,10 @@ function testDirect(config) {
2822
3627
  return factories.do.test(config);
2823
3628
  case "do-remote":
2824
3629
  return factories["do-remote"].test(config);
3630
+ case "redis":
3631
+ return factories.redis.test(config);
3632
+ case "elasticsearch":
3633
+ return factories.elasticsearch.test(config);
2825
3634
  }
2826
3635
  }
2827
3636
 
@@ -2970,6 +3779,7 @@ var LOST_CODES = /* @__PURE__ */ new Set([
2970
3779
  "PROTOCOL_ENQUEUE_AFTER_DESTROY",
2971
3780
  // node sockets (pg, mssql/tedious surface these directly)
2972
3781
  "ECONNRESET",
3782
+ "ECONNREFUSED",
2973
3783
  "EPIPE",
2974
3784
  "ETIMEDOUT",
2975
3785
  "ECONNABORTED",
@@ -2998,6 +3808,8 @@ var LOST_MESSAGES = [
2998
3808
  /connection is closed/i,
2999
3809
  // mssql
3000
3810
  /socket hang up/i,
3811
+ /stream isn't writeable/i,
3812
+ // ioredis with the offline queue off
3001
3813
  /ORA-0311[34]\b/,
3002
3814
  /ORA-01012\b/,
3003
3815
  /ORA-12541\b/,
@@ -3017,103 +3829,14 @@ function isConnectionLost(err) {
3017
3829
  return e.originalError !== void 0 && e.originalError !== err && isConnectionLost(e.originalError);
3018
3830
  }
3019
3831
 
3020
- // src/read-only.ts
3021
- var ReadOnlyViolation = class extends Error {
3022
- status = 403;
3023
- constructor(message) {
3024
- super(message);
3025
- this.name = "ReadOnlyViolation";
3026
- }
3027
- };
3028
- var READ_KEYWORDS = /* @__PURE__ */ new Set(["SELECT", "WITH", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "VALUES", "TABLE", "PRAGMA"]);
3029
- function splitStatements(sql2) {
3030
- const out = [];
3031
- let cur = "";
3032
- let i = 0;
3033
- const n = sql2.length;
3034
- while (i < n) {
3035
- const ch = sql2[i];
3036
- const next = sql2[i + 1];
3037
- if (ch === "-" && next === "-") {
3038
- while (i < n && sql2[i] !== "\n") i++;
3039
- cur += " ";
3040
- continue;
3041
- }
3042
- if (ch === "#") {
3043
- while (i < n && sql2[i] !== "\n") i++;
3044
- cur += " ";
3045
- continue;
3046
- }
3047
- if (ch === "/" && next === "*") {
3048
- i += 2;
3049
- while (i < n && !(sql2[i] === "*" && sql2[i + 1] === "/")) i++;
3050
- i += 2;
3051
- cur += " ";
3052
- continue;
3053
- }
3054
- if (ch === "'" || ch === '"' || ch === "`" || ch === "[") {
3055
- const close = ch === "[" ? "]" : ch;
3056
- i++;
3057
- while (i < n) {
3058
- if (sql2[i] === "\\" && close === "'") {
3059
- i += 2;
3060
- continue;
3061
- }
3062
- if (sql2[i] === close) {
3063
- if (sql2[i + 1] === close && close !== "]") {
3064
- i += 2;
3065
- continue;
3066
- }
3067
- break;
3068
- }
3069
- i++;
3070
- }
3071
- i++;
3072
- cur += ch === "[" ? " x " : " 'x' ";
3073
- continue;
3074
- }
3075
- if (ch === "$" && next === "$") {
3076
- const end = sql2.indexOf("$$", i + 2);
3077
- i = end === -1 ? n : end + 2;
3078
- cur += " 'x' ";
3079
- continue;
3080
- }
3081
- if (ch === ";") {
3082
- out.push(cur);
3083
- cur = "";
3084
- i++;
3085
- continue;
3086
- }
3087
- cur += ch;
3088
- i++;
3089
- }
3090
- out.push(cur);
3091
- return out.map((s) => s.trim()).filter((s) => s.length > 0);
3092
- }
3093
- function assertReadOnlyStatement(sql2) {
3094
- const statements = splitStatements(sql2);
3095
- if (statements.length === 0) return;
3096
- if (statements.length > 1) throw new ReadOnlyViolation("Read-only profile: one statement per request");
3097
- const stmt = statements[0];
3098
- const first = /^[A-Za-z]+/.exec(stmt)?.[0].toUpperCase() ?? "";
3099
- if (!READ_KEYWORDS.has(first)) throw new ReadOnlyViolation(`Read-only profile: ${first || "this statement"} is not allowed`);
3100
- if (first === "WITH") {
3101
- const words = stmt.toUpperCase().match(/\b(INSERT|UPDATE|DELETE|MERGE|REPLACE)\b/);
3102
- if (words) throw new ReadOnlyViolation(`Read-only profile: ${words[1]} inside WITH is not allowed`);
3103
- }
3104
- if (first === "PRAGMA" && /[=(]/.test(stmt)) throw new ReadOnlyViolation("Read-only profile: PRAGMA may only read");
3105
- if (/\bINTO\s+(OUTFILE|DUMPFILE)\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: INTO OUTFILE is not allowed");
3106
- if (/\bFOR\s+(UPDATE|SHARE|NO\s+KEY\s+UPDATE|KEY\s+SHARE)\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: locking reads are not allowed");
3107
- if (/\bLOCK\s+IN\s+SHARE\s+MODE\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: locking reads are not allowed");
3108
- if (first === "SELECT" && /\bINTO\b/i.test(stmt) && !/\bINTO\s+@/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: SELECT INTO is not allowed");
3109
- }
3110
-
3111
3832
  // src/tunneling.ts
3112
3833
  var IDLE_TIMEOUT_MS = Number(process.env.PLANCHE_IDLE_TIMEOUT_MS) || 30 * 60 * 1e3;
3113
3834
  var MAX_CONNECTIONS = Number(process.env.PLANCHE_MAX_CONNECTIONS) || 64;
3114
3835
  var ConnectionManager = class {
3115
3836
  connections = /* @__PURE__ */ new Map();
3116
3837
  lastUsed = /* @__PURE__ */ new Map();
3838
+ /** Live feeds open per connection. */
3839
+ feeds = /* @__PURE__ */ new Map();
3117
3840
  /** Connections opened for a read-only gateway profile: writes are refused before reaching the driver. */
3118
3841
  readOnly = /* @__PURE__ */ new Set();
3119
3842
  constructor() {
@@ -3124,7 +3847,7 @@ var ConnectionManager = class {
3124
3847
  async closeIdle() {
3125
3848
  const cutoff = Date.now() - IDLE_TIMEOUT_MS;
3126
3849
  for (const [id, used] of this.lastUsed) {
3127
- if (used < cutoff) {
3850
+ if (used < cutoff && !this.feeds.get(id)) {
3128
3851
  console.log(`[ConnectionManager] Closing idle connection: ${id}`);
3129
3852
  await this.disconnect(id).catch(() => void 0);
3130
3853
  }
@@ -3166,19 +3889,86 @@ var ConnectionManager = class {
3166
3889
  throw new Error(`Connection not found: ${connectionId}`);
3167
3890
  }
3168
3891
  this.lastUsed.set(connectionId, Date.now());
3169
- if (this.readOnly.has(connectionId)) assertReadOnlyStatement(sql2);
3892
+ if (this.readOnly.has(connectionId)) {
3893
+ if (driver.assertReadOnly) await driver.assertReadOnly(sql2);
3894
+ else assertReadOnlyStatement(sql2);
3895
+ }
3170
3896
  try {
3171
3897
  return await driver.query(sql2, database);
3172
3898
  } catch (err) {
3173
3899
  if (!isConnectionLost(err)) throw err;
3174
- console.log(`[ConnectionManager] Connection lost, evicting: ${connectionId}`);
3175
- this.connections.delete(connectionId);
3176
- this.lastUsed.delete(connectionId);
3177
- this.readOnly.delete(connectionId);
3178
- await driver.close().catch(() => void 0);
3179
- throw new ConnectionLostError(`Connection lost: ${err instanceof Error ? err.message : String(err)}`);
3900
+ return this.evictLost(connectionId, driver, err);
3180
3901
  }
3181
3902
  }
3903
+ /**
3904
+ * The socket died (network change, server timeout, sleep): forget the
3905
+ * connection so the client re-opens it from the profile instead of
3906
+ * hitting the same dead driver again.
3907
+ */
3908
+ async evictLost(connectionId, driver, err) {
3909
+ console.log(`[ConnectionManager] Connection lost, evicting: ${connectionId}`);
3910
+ this.connections.delete(connectionId);
3911
+ this.lastUsed.delete(connectionId);
3912
+ this.readOnly.delete(connectionId);
3913
+ await driver.close().catch(() => void 0);
3914
+ throw new ConnectionLostError(`Connection lost: ${err instanceof Error ? err.message : String(err)}`);
3915
+ }
3916
+ /**
3917
+ * Run several statements in order on one connection. On a read-only
3918
+ * session every statement is checked *before* any runs, and one write
3919
+ * refuses the whole batch — a batch must never be a way around the check.
3920
+ */
3921
+ async batch(connectionId, statements, database) {
3922
+ const driver = this.connections.get(connectionId);
3923
+ if (!driver) throw new Error(`Connection not found: ${connectionId}`);
3924
+ this.lastUsed.set(connectionId, Date.now());
3925
+ if (this.readOnly.has(connectionId)) {
3926
+ for (const sql2 of statements) {
3927
+ if (driver.assertReadOnly) await driver.assertReadOnly(sql2);
3928
+ else assertReadOnlyStatement(sql2);
3929
+ }
3930
+ }
3931
+ if (driver.batch) {
3932
+ try {
3933
+ return await driver.batch(statements, database);
3934
+ } catch (err) {
3935
+ if (!isConnectionLost(err)) throw err;
3936
+ return this.evictLost(connectionId, driver, err);
3937
+ }
3938
+ }
3939
+ const results = [];
3940
+ for (const sql2 of statements) {
3941
+ try {
3942
+ results.push(await this.query(connectionId, sql2, database));
3943
+ } catch (err) {
3944
+ if (err instanceof ConnectionLostError) throw err;
3945
+ results.push(err instanceof Error ? err : new Error(String(err)));
3946
+ }
3947
+ }
3948
+ return results;
3949
+ }
3950
+ /**
3951
+ * Start a live feed on a connection. On a read-only session only
3952
+ * subscriptions are allowed: MONITOR shows every client's commands, which
3953
+ * is an administrator's view, not a read of the data.
3954
+ */
3955
+ async stream(connectionId, request, emit, signal) {
3956
+ const driver = this.connections.get(connectionId);
3957
+ if (!driver) throw new Error(`Connection not found: ${connectionId}`);
3958
+ if (!driver.stream) throw Object.assign(new Error("This connection has no live feeds"), { status: 400 });
3959
+ if (request.kind === "monitor" && this.readOnly.has(connectionId)) {
3960
+ throw new ReadOnlyViolation("Read-only profile: MONITOR is not available");
3961
+ }
3962
+ this.lastUsed.set(connectionId, Date.now());
3963
+ await driver.stream(request, emit, signal);
3964
+ this.feeds.set(connectionId, (this.feeds.get(connectionId) ?? 0) + 1);
3965
+ signal.addEventListener("abort", () => {
3966
+ const open = (this.feeds.get(connectionId) ?? 1) - 1;
3967
+ if (open > 0) this.feeds.set(connectionId, open);
3968
+ else this.feeds.delete(connectionId);
3969
+ this.lastUsed.set(connectionId, Date.now());
3970
+ }, { once: true });
3971
+ }
3182
3972
  /**
3183
3973
  * Interrupt whatever is running on a connection. Best effort: an engine with
3184
3974
  * no way to interrupt (better-sqlite3 blocks the thread) reports false rather
@@ -3256,6 +4046,8 @@ function errorExtra(err) {
3256
4046
  }
3257
4047
  return {};
3258
4048
  }
4049
+ var STREAM_FLUSH_MS = 100;
4050
+ var STREAM_BATCH_CAP = 500;
3259
4051
  var errorMessage = (err) => err instanceof Error ? err.message : String(err);
3260
4052
  function secretEquals(given, expected) {
3261
4053
  const a = createHash2("sha256").update(given).digest();
@@ -3386,7 +4178,8 @@ function createAgent(options) {
3386
4178
  version: options.version,
3387
4179
  protocolVersion: PROTOCOL_VERSION,
3388
4180
  activeConnections: manager.activeConnections,
3389
- uptime: process.uptime()
4181
+ uptime: process.uptime(),
4182
+ drivers: DRIVER_TYPES
3390
4183
  };
3391
4184
  const ai = await aiConfigView();
3392
4185
  body.ai = { configured: ai.configured, model: ai.model, tools: true };
@@ -3468,6 +4261,86 @@ function createAgent(options) {
3468
4261
  fail(res, errorStatus(err), message, errorExtra(err));
3469
4262
  }
3470
4263
  });
4264
+ app.post("/batch", async (req, res) => {
4265
+ const { connectionId, statements, database } = req.body ?? {};
4266
+ if (typeof connectionId !== "string" || !connectionId || !Array.isArray(statements) || !statements.every((s) => typeof s === "string" && s)) {
4267
+ fail(res, 400, "connectionId and a list of statements are required");
4268
+ return;
4269
+ }
4270
+ if (statements.length > BATCH_LIMIT) {
4271
+ fail(res, 400, `At most ${BATCH_LIMIT} statements per batch`);
4272
+ return;
4273
+ }
4274
+ if (!manager.has(connectionId)) {
4275
+ fail(res, 404, `Connection not found: ${connectionId}`);
4276
+ return;
4277
+ }
4278
+ const start = Date.now();
4279
+ try {
4280
+ const outcomes = await manager.batch(connectionId, statements, typeof database === "string" ? database : void 0);
4281
+ const results = outcomes.map((o) => o instanceof Error ? { success: false, message: errorMessage(o) } : { success: true, ...o });
4282
+ const body = { success: true, results, execTime: Date.now() - start };
4283
+ res.json(body);
4284
+ } catch (err) {
4285
+ const message = errorMessage(err);
4286
+ if (err instanceof ConnectionLostError) {
4287
+ fail(res, 404, message);
4288
+ return;
4289
+ }
4290
+ const status = err?.status;
4291
+ fail(res, typeof status === "number" && status >= 400 && status < 600 ? status : 422, message);
4292
+ }
4293
+ });
4294
+ app.post("/stream", async (req, res) => {
4295
+ const { connectionId, kind, channels, patterns } = req.body ?? {};
4296
+ const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0).slice(0, 100) : [];
4297
+ if (typeof connectionId !== "string" || kind !== "subscribe" && kind !== "monitor") {
4298
+ fail(res, 400, "connectionId and kind (subscribe | monitor) are required");
4299
+ return;
4300
+ }
4301
+ if (!manager.has(connectionId)) {
4302
+ fail(res, 404, `Connection not found: ${connectionId}`);
4303
+ return;
4304
+ }
4305
+ const stop = new AbortController();
4306
+ let pending2 = [];
4307
+ let dropped = 0;
4308
+ const emit = (event) => {
4309
+ if (pending2.length < STREAM_BATCH_CAP) pending2.push(event);
4310
+ else dropped += 1;
4311
+ };
4312
+ try {
4313
+ await manager.stream(connectionId, { kind, channels: strings(channels), patterns: strings(patterns) }, emit, stop.signal);
4314
+ } catch (err) {
4315
+ stop.abort();
4316
+ const status = err?.status;
4317
+ fail(res, typeof status === "number" && status >= 400 && status < 600 ? status : 422, errorMessage(err));
4318
+ return;
4319
+ }
4320
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
4321
+ res.write(`data: ${JSON.stringify({ type: "ready" })}
4322
+
4323
+ `);
4324
+ const flush = setInterval(() => {
4325
+ const batch = pending2;
4326
+ pending2 = [];
4327
+ if (dropped > 0) {
4328
+ batch.push({ type: "dropped", count: dropped });
4329
+ dropped = 0;
4330
+ }
4331
+ for (const event of batch) res.write(`data: ${JSON.stringify(event)}
4332
+
4333
+ `);
4334
+ }, STREAM_FLUSH_MS);
4335
+ const keepAlive = setInterval(() => res.write(": ping\n\n"), 15e3);
4336
+ const end = () => {
4337
+ clearInterval(flush);
4338
+ clearInterval(keepAlive);
4339
+ stop.abort();
4340
+ };
4341
+ req.on("close", end);
4342
+ res.on("close", end);
4343
+ });
3471
4344
  app.post("/query", async (req, res) => {
3472
4345
  try {
3473
4346
  const { connectionId, sql: sql2, database } = req.body ?? {};
@@ -3489,6 +4362,7 @@ function createAgent(options) {
3489
4362
  affectedRows: result.affectedRows,
3490
4363
  insertId: result.insertId,
3491
4364
  isResultQuery: result.isResultQuery,
4365
+ ...result.reply !== void 0 ? { reply: result.reply } : {},
3492
4366
  execTime
3493
4367
  });
3494
4368
  } catch (err) {