latticesql 4.3.1 → 4.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -917,7 +917,7 @@ async function registerPostgresPolyfills(run) {
917
917
  await run(sql);
918
918
  } catch (err) {
919
919
  const msg = err instanceof Error ? err.message : String(err);
920
- if (/permission denied/i.test(msg)) {
920
+ if (/permission denied/i.test(msg) || /must be owner of/i.test(msg)) {
921
921
  permissionDenied = true;
922
922
  } else {
923
923
  console.warn(`[PostgresAdapter] ${warn}`, msg);
@@ -1439,40 +1439,111 @@ var init_postgres = __esm({
1439
1439
  SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
1440
1440
  $fn$;
1441
1441
  END IF;
1442
+ END $do$;`
1443
+ },
1444
+ {
1445
+ warn: "could not register strftime format helper:",
1446
+ // Shared format translator (SQLite strftime tokens → to_char patterns), in UTC.
1447
+ // Factored out so the 2-arg and 3-arg strftime overloads share one definition.
1448
+ // Each polyfill below is CREATE OR REPLACE (so an existing cloud's prior copy is
1449
+ // UPGRADED when the owner opens), wrapped in a DO block whose EXCEPTION swallows a
1450
+ // scoped member's insufficient-privilege failure IN A SUBTRANSACTION — so a member
1451
+ // that runs registration inside a transaction is NOT aborted. ("must be owner of
1452
+ // function", raised when a non-owner CREATE OR REPLACEs another role's function, is
1453
+ // SQLSTATE 42501 / `insufficient_privilege`, same class as "permission denied".) The
1454
+ // owner already owns + replaced the function; the member simply uses the owner's
1455
+ // copy — and `ownPolyfillsByGroup` re-owns the whole set to the member group so any
1456
+ // member can replace them on a later upgrade.
1457
+ sql: `DO $do$ BEGIN
1458
+ CREATE OR REPLACE FUNCTION __lattice_strftime_fmt(ts timestamptz, format text)
1459
+ RETURNS text
1460
+ LANGUAGE sql
1461
+ IMMUTABLE
1462
+ AS $fn$
1463
+ SELECT to_char(
1464
+ ts AT TIME ZONE 'UTC',
1465
+ replace(replace(replace(replace(replace(replace(replace(replace(
1466
+ format,
1467
+ '%Y', 'YYYY'),
1468
+ '%m', 'MM'),
1469
+ '%d', 'DD'),
1470
+ '%H', 'HH24'),
1471
+ '%M', 'MI'),
1472
+ '%S', 'SS'),
1473
+ '%f', 'MS'),
1474
+ 'T', '"T"')
1475
+ );
1476
+ $fn$;
1477
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1442
1478
  END $do$;`
1443
1479
  },
1444
1480
  {
1445
1481
  warn: "could not register strftime polyfill:",
1482
+ // The pre-4.3.3 strftime cast the modifier straight to timestamptz and threw
1483
+ // `invalid input syntax for type timestamp with time zone: ""` on a legacy ''
1484
+ // value (3.x stored nullable TEXT timestamps as ''), bricking the whole workspace
1485
+ // open. Now an empty, whitespace, or unparseable time string returns NULL (SQLite's
1486
+ // strftime semantics) instead of aborting the query.
1446
1487
  sql: `DO $do$ BEGIN
1447
- IF to_regprocedure('strftime(text, text)') IS NULL THEN
1448
- CREATE FUNCTION strftime(format text, modifier text)
1449
- RETURNS text
1450
- LANGUAGE plpgsql
1451
- IMMUTABLE
1452
- AS $fn$
1453
- DECLARE ts timestamptz;
1454
- BEGIN
1455
- IF modifier = 'now' THEN
1456
- ts := now();
1457
- ELSE
1488
+ CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
1489
+ RETURNS text
1490
+ LANGUAGE plpgsql
1491
+ IMMUTABLE
1492
+ AS $fn$
1493
+ DECLARE ts timestamptz;
1494
+ BEGIN
1495
+ IF modifier = 'now' THEN
1496
+ ts := now();
1497
+ ELSIF NULLIF(btrim(modifier), '') IS NULL THEN
1498
+ RETURN NULL;
1499
+ ELSE
1500
+ BEGIN
1458
1501
  ts := modifier::timestamptz;
1459
- END IF;
1460
- RETURN to_char(
1461
- ts AT TIME ZONE 'UTC',
1462
- replace(replace(replace(replace(replace(replace(replace(replace(
1463
- format,
1464
- '%Y', 'YYYY'),
1465
- '%m', 'MM'),
1466
- '%d', 'DD'),
1467
- '%H', 'HH24'),
1468
- '%M', 'MI'),
1469
- '%S', 'SS'),
1470
- '%f', 'MS'),
1471
- 'T', '"T"')
1472
- );
1502
+ EXCEPTION WHEN others THEN
1503
+ RETURN NULL;
1504
+ END;
1505
+ END IF;
1506
+ RETURN __lattice_strftime_fmt(ts, format);
1507
+ END;
1508
+ $fn$;
1509
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1510
+ END $do$;`
1511
+ },
1512
+ {
1513
+ warn: "could not register 3-arg strftime polyfill:",
1514
+ // SQLite's 3-arg strftime(format, timestring, modifier) — used by the changelog
1515
+ // retention prune (e.g. strftime('%Y-...','now','-30 days')). Postgres had no
1516
+ // 3-arg overload, so that prune threw `function strftime(...) does not exist` on
1517
+ // every PG cloud with retention. Resolve the base time, apply the modifier as an
1518
+ // interval, then reuse the shared formatter. Empty/invalid → NULL, never throws.
1519
+ sql: `DO $do$ BEGIN
1520
+ CREATE OR REPLACE FUNCTION strftime(format text, timestring text, modifier text)
1521
+ RETURNS text
1522
+ LANGUAGE plpgsql
1523
+ IMMUTABLE
1524
+ AS $fn$
1525
+ DECLARE ts timestamptz;
1526
+ BEGIN
1527
+ IF timestring = 'now' THEN
1528
+ ts := now();
1529
+ ELSIF NULLIF(btrim(timestring), '') IS NULL THEN
1530
+ RETURN NULL;
1531
+ ELSE
1532
+ BEGIN
1533
+ ts := timestring::timestamptz;
1534
+ EXCEPTION WHEN others THEN
1535
+ RETURN NULL;
1536
+ END;
1537
+ END IF;
1538
+ BEGIN
1539
+ ts := ts + modifier::interval;
1540
+ EXCEPTION WHEN others THEN
1541
+ RETURN NULL;
1473
1542
  END;
1474
- $fn$;
1475
- END IF;
1543
+ RETURN __lattice_strftime_fmt(ts, format);
1544
+ END;
1545
+ $fn$;
1546
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1476
1547
  END $do$;`
1477
1548
  }
1478
1549
  ];
@@ -9391,7 +9462,12 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
9391
9462
  async function ownPolyfillsByGroup(db) {
9392
9463
  if (!isPg(db)) return;
9393
9464
  const group3 = await memberGroupFor(db);
9394
- for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
9465
+ for (const sig of [
9466
+ "json_extract(text, text)",
9467
+ "strftime(text, text)",
9468
+ "strftime(text, text, text)",
9469
+ "__lattice_strftime_fmt(timestamptz, text)"
9470
+ ]) {
9395
9471
  try {
9396
9472
  const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
9397
9473
  if (reg?.reg == null) continue;
@@ -23491,12 +23567,12 @@ var init_getNodeModulesParentDirs = __esm({
23491
23567
  "node_modules/@aws-sdk/core/dist-es/submodules/client/util-user-agent-node/getNodeModulesParentDirs.js"() {
23492
23568
  "use strict";
23493
23569
  import_node_path21 = require("path");
23494
- getNodeModulesParentDirs = (dirname18) => {
23570
+ getNodeModulesParentDirs = (dirname19) => {
23495
23571
  const cwd = process.cwd();
23496
- if (!dirname18) {
23572
+ if (!dirname19) {
23497
23573
  return [cwd];
23498
23574
  }
23499
- const normalizedPath = (0, import_node_path21.normalize)(dirname18);
23575
+ const normalizedPath = (0, import_node_path21.normalize)(dirname19);
23500
23576
  const parts = normalizedPath.split(import_node_path21.sep);
23501
23577
  const nodeModulesIndex = parts.indexOf("node_modules");
23502
23578
  const parentDir = nodeModulesIndex !== -1 ? parts.slice(0, nodeModulesIndex).join(import_node_path21.sep) : normalizedPath;
@@ -23574,8 +23650,8 @@ var init_getTypeScriptUserAgentPair = __esm({
23574
23650
  tscVersion = null;
23575
23651
  return void 0;
23576
23652
  }
23577
- const dirname18 = typeof __dirname !== "undefined" ? __dirname : void 0;
23578
- const nodeModulesParentDirs = getNodeModulesParentDirs(dirname18);
23653
+ const dirname19 = typeof __dirname !== "undefined" ? __dirname : void 0;
23654
+ const nodeModulesParentDirs = getNodeModulesParentDirs(dirname19);
23579
23655
  let versionFromApp;
23580
23656
  for (const nodeModulesParentDir of nodeModulesParentDirs) {
23581
23657
  try {
@@ -80292,20 +80368,49 @@ var MAX_LIST_ENTRIES = 2e3;
80292
80368
  var MAX_INGEST_FILES = 500;
80293
80369
  var MAX_INGEST_DEPTH = 8;
80294
80370
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", "__pycache__", ".venv", "venv"]);
80295
- function rootsFile() {
80296
- return (0, import_node_path44.join)(configDir(), "sources.json");
80371
+ function rootsFile(configPath) {
80372
+ return (0, import_node_path44.join)((0, import_node_path44.dirname)(configPath), "sources.json");
80373
+ }
80374
+ function migrateGlobalRootsIfNeeded(configPath) {
80375
+ const wsFile = rootsFile(configPath);
80376
+ if ((0, import_node_fs41.existsSync)(wsFile)) return;
80377
+ const globalFile = (0, import_node_path44.join)(configDir(), "sources.json");
80378
+ if (globalFile === wsFile || !(0, import_node_fs41.existsSync)(globalFile)) return;
80379
+ let roots;
80380
+ try {
80381
+ const parsed = JSON.parse((0, import_node_fs41.readFileSync)(globalFile, "utf8"));
80382
+ if (!Array.isArray(parsed.roots) || parsed.roots.length === 0) return;
80383
+ roots = parsed.roots;
80384
+ } catch {
80385
+ return;
80386
+ }
80387
+ try {
80388
+ (0, import_node_fs41.mkdirSync)((0, import_node_path44.dirname)(wsFile), { recursive: true });
80389
+ (0, import_node_fs41.writeFileSync)(wsFile, JSON.stringify({ roots }, null, 2), "utf8");
80390
+ } catch {
80391
+ return;
80392
+ }
80393
+ try {
80394
+ (0, import_node_fs41.rmSync)(globalFile, { force: true });
80395
+ } catch {
80396
+ try {
80397
+ (0, import_node_fs41.writeFileSync)(globalFile, JSON.stringify({ roots: [] }, null, 2), "utf8");
80398
+ } catch {
80399
+ }
80400
+ }
80297
80401
  }
80298
- function readRoots() {
80402
+ function readRoots(configPath) {
80403
+ migrateGlobalRootsIfNeeded(configPath);
80299
80404
  try {
80300
- const raw = (0, import_node_fs41.readFileSync)(rootsFile(), "utf8");
80405
+ const raw = (0, import_node_fs41.readFileSync)(rootsFile(configPath), "utf8");
80301
80406
  const parsed = JSON.parse(raw);
80302
80407
  return Array.isArray(parsed.roots) ? parsed.roots : [];
80303
80408
  } catch {
80304
80409
  return [];
80305
80410
  }
80306
80411
  }
80307
- function writeRoots(roots) {
80308
- (0, import_node_fs41.writeFileSync)(rootsFile(), JSON.stringify({ roots }, null, 2), "utf8");
80412
+ function writeRoots(configPath, roots) {
80413
+ (0, import_node_fs41.writeFileSync)(rootsFile(configPath), JSON.stringify({ roots }, null, 2), "utf8");
80309
80414
  }
80310
80415
  function safeResolveInside2(target, roots) {
80311
80416
  const abs = (0, import_node_path44.resolve)(target);
@@ -80395,14 +80500,14 @@ async function ingestFolder(abs, ingestFile) {
80395
80500
  return { ingested, skipped };
80396
80501
  }
80397
80502
  async function dispatchSourcesRoute(req, res, deps) {
80398
- const { pathname, method, ingestFile } = deps;
80503
+ const { pathname, method, ingestFile, configPath } = deps;
80399
80504
  if (!pathname.startsWith("/api/sources/")) return false;
80400
80505
  if (!localFileOpenEnabled()) {
80401
80506
  sendJson(res, { enabled: false });
80402
80507
  return true;
80403
80508
  }
80404
80509
  if (pathname === "/api/sources/roots" && method === "GET") {
80405
- sendJson(res, { enabled: true, roots: readRoots() });
80510
+ sendJson(res, { enabled: true, roots: readRoots(configPath) });
80406
80511
  return true;
80407
80512
  }
80408
80513
  if (pathname === "/api/sources/roots" && method === "POST") {
@@ -80428,12 +80533,12 @@ async function dispatchSourcesRoute(req, res, deps) {
80428
80533
  sendJson(res, { error: `path not found: ${abs}` }, 400);
80429
80534
  return true;
80430
80535
  }
80431
- const roots = readRoots();
80536
+ const roots = readRoots(configPath);
80432
80537
  let root6 = roots.find((r6) => (0, import_node_path44.resolve)(r6.path) === abs);
80433
80538
  if (!root6) {
80434
80539
  root6 = { id: (0, import_node_crypto24.randomUUID)(), path: abs, kind, name: (0, import_node_path44.basename)(abs) || abs };
80435
80540
  roots.push(root6);
80436
- writeRoots(roots);
80541
+ writeRoots(configPath, roots);
80437
80542
  }
80438
80543
  let result;
80439
80544
  if (kind === "folder") result = await ingestFolder(abs, ingestFile);
@@ -80444,8 +80549,8 @@ async function dispatchSourcesRoute(req, res, deps) {
80444
80549
  const delMatch = /^\/api\/sources\/roots\/([^/]+)$/.exec(pathname);
80445
80550
  if (delMatch && method === "DELETE") {
80446
80551
  const id = decodeURIComponent(delMatch[1] ?? "");
80447
- const roots = readRoots().filter((r6) => r6.id !== id);
80448
- writeRoots(roots);
80552
+ const roots = readRoots(configPath).filter((r6) => r6.id !== id);
80553
+ writeRoots(configPath, roots);
80449
80554
  sendJson(res, { ok: true });
80450
80555
  return true;
80451
80556
  }
@@ -80463,7 +80568,7 @@ async function dispatchSourcesRoute(req, res, deps) {
80463
80568
  sendJson(res, { error: "path is required" }, 400);
80464
80569
  return true;
80465
80570
  }
80466
- const abs = safeResolveInside2(target, readRoots());
80571
+ const abs = safeResolveInside2(target, readRoots(configPath));
80467
80572
  if (!abs) {
80468
80573
  sendJson(res, { error: "path is outside any registered source root" }, 403);
80469
80574
  return true;
@@ -80478,7 +80583,7 @@ async function dispatchSourcesRoute(req, res, deps) {
80478
80583
  if (pathname === "/api/sources/ingest-folder" && method === "POST") {
80479
80584
  const body = await readJson(req).catch(() => ({}));
80480
80585
  const target = typeof body.path === "string" ? body.path : "";
80481
- const abs = safeResolveInside2(target, readRoots());
80586
+ const abs = safeResolveInside2(target, readRoots(configPath));
80482
80587
  if (!abs) {
80483
80588
  sendJson(res, { error: "path is outside any registered source root" }, 403);
80484
80589
  return true;
@@ -83117,6 +83222,7 @@ async function startGuiServer(options) {
83117
83222
  return await dispatchSourcesRoute(req2, res2, {
83118
83223
  db: active.db,
83119
83224
  ingestFile: (p3) => ingestLocalFile(ingestCtx, mctx, p3, false),
83225
+ configPath: active.configPath,
83120
83226
  pathname,
83121
83227
  method
83122
83228
  });