latticesql 3.4.0 → 3.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/dist/cli.js +225 -57
- package/dist/index.cjs +224 -56
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +224 -56
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -919,43 +919,57 @@ var init_postgres = __esm({
|
|
|
919
919
|
},
|
|
920
920
|
{
|
|
921
921
|
warn: "could not register json_extract polyfill:",
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
922
|
+
// Create ONLY if absent. `CREATE OR REPLACE` on an existing function requires
|
|
923
|
+
// ownership, but on a cloud the function is owned by whichever single role
|
|
924
|
+
// created it first — so every OTHER member's per-connect replace raised "must
|
|
925
|
+
// be owner of function" and (sharing the render transaction) aborted it,
|
|
926
|
+
// yielding an empty render. The IF-absent guard makes a present function a
|
|
927
|
+
// clean no-op for everyone, regardless of who owns it.
|
|
928
|
+
sql: `DO $do$ BEGIN
|
|
929
|
+
IF to_regprocedure('json_extract(text, text)') IS NULL THEN
|
|
930
|
+
CREATE FUNCTION json_extract(doc text, path text)
|
|
931
|
+
RETURNS text
|
|
932
|
+
LANGUAGE sql
|
|
933
|
+
IMMUTABLE
|
|
934
|
+
AS $fn$
|
|
935
|
+
SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
|
|
936
|
+
$fn$;
|
|
937
|
+
END IF;
|
|
938
|
+
END $do$;`
|
|
929
939
|
},
|
|
930
940
|
{
|
|
931
941
|
warn: "could not register strftime polyfill:",
|
|
932
|
-
sql: `
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
942
|
+
sql: `DO $do$ BEGIN
|
|
943
|
+
IF to_regprocedure('strftime(text, text)') IS NULL THEN
|
|
944
|
+
CREATE FUNCTION strftime(format text, modifier text)
|
|
945
|
+
RETURNS text
|
|
946
|
+
LANGUAGE plpgsql
|
|
947
|
+
IMMUTABLE
|
|
948
|
+
AS $fn$
|
|
949
|
+
DECLARE ts timestamptz;
|
|
950
|
+
BEGIN
|
|
951
|
+
IF modifier = 'now' THEN
|
|
952
|
+
ts := now();
|
|
953
|
+
ELSE
|
|
954
|
+
ts := modifier::timestamptz;
|
|
955
|
+
END IF;
|
|
956
|
+
RETURN to_char(
|
|
957
|
+
ts AT TIME ZONE 'UTC',
|
|
958
|
+
replace(replace(replace(replace(replace(replace(replace(replace(
|
|
959
|
+
format,
|
|
960
|
+
'%Y', 'YYYY'),
|
|
961
|
+
'%m', 'MM'),
|
|
962
|
+
'%d', 'DD'),
|
|
963
|
+
'%H', 'HH24'),
|
|
964
|
+
'%M', 'MI'),
|
|
965
|
+
'%S', 'SS'),
|
|
966
|
+
'%f', 'MS'),
|
|
967
|
+
'T', '"T"')
|
|
968
|
+
);
|
|
969
|
+
END;
|
|
970
|
+
$fn$;
|
|
971
|
+
END IF;
|
|
972
|
+
END $do$;`
|
|
959
973
|
}
|
|
960
974
|
];
|
|
961
975
|
}
|
|
@@ -2364,6 +2378,34 @@ var init_engine = __esm({
|
|
|
2364
2378
|
setRenderFold(fn) {
|
|
2365
2379
|
this._foldRows = fn;
|
|
2366
2380
|
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Incremental scope: is this entity-context table affected by a change to one
|
|
2383
|
+
* of `changed`? Affected when the table itself changed (its own rows / `self`
|
|
2384
|
+
* source / index) OR any of its files SOURCES from a changed table (a cross-
|
|
2385
|
+
* table dependent — e.g. an AGENT.md that lists the agent's tasks must re-render
|
|
2386
|
+
* when `tasks` changes). A `custom` source runs an arbitrary query, so we can't
|
|
2387
|
+
* prove independence — treat it as always-affected (conservative, never stale).
|
|
2388
|
+
*/
|
|
2389
|
+
_entityAffected(table, def, changed) {
|
|
2390
|
+
if (changed.has(table)) return true;
|
|
2391
|
+
for (const spec of Object.values(def.files)) {
|
|
2392
|
+
if (this._sourceTouches(spec.source, changed)) return true;
|
|
2393
|
+
}
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
_sourceTouches(source, changed) {
|
|
2397
|
+
if (source == null || typeof source !== "object") return false;
|
|
2398
|
+
const s2 = source;
|
|
2399
|
+
if (s2.type === "custom") return true;
|
|
2400
|
+
if (typeof s2.table === "string" && changed.has(s2.table)) return true;
|
|
2401
|
+
if (typeof s2.junctionTable === "string" && changed.has(s2.junctionTable)) return true;
|
|
2402
|
+
if (s2.sources != null && typeof s2.sources === "object") {
|
|
2403
|
+
for (const sub of Object.values(s2.sources)) {
|
|
2404
|
+
if (this._sourceTouches(sub, changed)) return true;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2367
2409
|
async render(outputDir, opts = {}) {
|
|
2368
2410
|
const start = Date.now();
|
|
2369
2411
|
const filesWritten = [];
|
|
@@ -2373,6 +2415,7 @@ var init_engine = __esm({
|
|
|
2373
2415
|
for (const [name, def] of this._schema.getTables()) {
|
|
2374
2416
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
2375
2417
|
if (this._skipEmpty && def.render === NOOP_RENDER) continue;
|
|
2418
|
+
if (opts.changedTables && !opts.changedTables.has(name)) continue;
|
|
2376
2419
|
let rows = await this._schema.queryTable(this._adapter, name, this._readRel);
|
|
2377
2420
|
if (def.relevanceFilter) {
|
|
2378
2421
|
const ctx = this._getTaskContext();
|
|
@@ -2425,6 +2468,9 @@ var init_engine = __esm({
|
|
|
2425
2468
|
}
|
|
2426
2469
|
for (const [name, def] of this._schema.getMultis()) {
|
|
2427
2470
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
2471
|
+
if (opts.changedTables && def.tables && !def.tables.some((t8) => opts.changedTables?.has(t8))) {
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2428
2474
|
const keys = await def.keys();
|
|
2429
2475
|
const tables = {};
|
|
2430
2476
|
if (def.tables) {
|
|
@@ -2456,16 +2502,22 @@ var init_engine = __esm({
|
|
|
2456
2502
|
filesWritten,
|
|
2457
2503
|
counters,
|
|
2458
2504
|
throttle,
|
|
2459
|
-
signal
|
|
2505
|
+
signal,
|
|
2506
|
+
opts.changedTables
|
|
2460
2507
|
);
|
|
2461
2508
|
if (entityContextManifest === null) {
|
|
2462
2509
|
return this._abortedResult(filesWritten, counters, start);
|
|
2463
2510
|
}
|
|
2464
2511
|
if (this._schema.getEntityContexts().size > 0) {
|
|
2512
|
+
let entityContexts = entityContextManifest;
|
|
2513
|
+
if (opts.changedTables) {
|
|
2514
|
+
const prev = readManifest(outputDir);
|
|
2515
|
+
entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
|
|
2516
|
+
}
|
|
2465
2517
|
writeManifest(outputDir, {
|
|
2466
2518
|
version: 2,
|
|
2467
2519
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2468
|
-
entityContexts
|
|
2520
|
+
entityContexts
|
|
2469
2521
|
});
|
|
2470
2522
|
}
|
|
2471
2523
|
const result = {
|
|
@@ -2531,12 +2583,14 @@ var init_engine = __esm({
|
|
|
2531
2583
|
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
2532
2584
|
* via `signal`.
|
|
2533
2585
|
*/
|
|
2534
|
-
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
|
|
2586
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
|
|
2535
2587
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
2536
2588
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
2537
2589
|
if (d6.protected) protectedTables.add(t8);
|
|
2538
2590
|
}
|
|
2539
|
-
const entityTables = [...this._schema.getEntityContexts()]
|
|
2591
|
+
const entityTables = [...this._schema.getEntityContexts()].filter(
|
|
2592
|
+
([table, def]) => !changedTables || this._entityAffected(table, def, changedTables)
|
|
2593
|
+
);
|
|
2540
2594
|
const tableCount = entityTables.length;
|
|
2541
2595
|
if (signal?.aborted) return null;
|
|
2542
2596
|
const renderedEntries = await mapWithConcurrency(
|
|
@@ -5236,6 +5290,16 @@ var init_lattice = __esm({
|
|
|
5236
5290
|
_autoRenderPending = false;
|
|
5237
5291
|
_autoRenderInFlight = false;
|
|
5238
5292
|
_autoRenderDebounceMs = 250;
|
|
5293
|
+
/**
|
|
5294
|
+
* Incremental auto-render scope, accumulated between debounced renders. A write
|
|
5295
|
+
* or a remote (cloud) change records the AFFECTED table here, so the next
|
|
5296
|
+
* auto-render re-renders only that entity (+ its cross-table dependents) instead
|
|
5297
|
+
* of the whole tree. `_pendingRenderAll` forces a full render (the initial
|
|
5298
|
+
* render, or a change with no known table). Captured + reset when a render
|
|
5299
|
+
* starts, so changes during a render re-accumulate and re-trigger.
|
|
5300
|
+
*/
|
|
5301
|
+
_pendingRenderTables = /* @__PURE__ */ new Set();
|
|
5302
|
+
_pendingRenderAll = true;
|
|
5239
5303
|
/** Cache of actual table columns (from PRAGMA), populated after init(). */
|
|
5240
5304
|
_columnCache = /* @__PURE__ */ new Map();
|
|
5241
5305
|
/** Derived encryption key (from options.encryptionKey via scrypt). */
|
|
@@ -6331,7 +6395,7 @@ var init_lattice = __esm({
|
|
|
6331
6395
|
`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
|
|
6332
6396
|
Object.values(filtered)
|
|
6333
6397
|
);
|
|
6334
|
-
this._scheduleAutoRender();
|
|
6398
|
+
this._scheduleAutoRender(junctionTable);
|
|
6335
6399
|
}
|
|
6336
6400
|
/**
|
|
6337
6401
|
* Delete rows from a junction table matching all given conditions.
|
|
@@ -6347,7 +6411,7 @@ var init_lattice = __esm({
|
|
|
6347
6411
|
`DELETE FROM "${junctionTable}" WHERE ${where}`,
|
|
6348
6412
|
entries.map(([, v2]) => v2)
|
|
6349
6413
|
);
|
|
6350
|
-
this._scheduleAutoRender();
|
|
6414
|
+
this._scheduleAutoRender(junctionTable);
|
|
6351
6415
|
}
|
|
6352
6416
|
// -------------------------------------------------------------------------
|
|
6353
6417
|
// Seeding DSL (v0.13+)
|
|
@@ -6603,6 +6667,11 @@ var init_lattice = __esm({
|
|
|
6603
6667
|
async renderInBackground(outputDir, opts = {}) {
|
|
6604
6668
|
const notInit = this._notInitError();
|
|
6605
6669
|
if (notInit) return notInit;
|
|
6670
|
+
if (!opts.changedTables) {
|
|
6671
|
+
this._pendingRenderAll = false;
|
|
6672
|
+
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
6673
|
+
this._autoRenderPending = false;
|
|
6674
|
+
}
|
|
6606
6675
|
return this._renderGuarded(outputDir, opts);
|
|
6607
6676
|
}
|
|
6608
6677
|
/**
|
|
@@ -6632,9 +6701,12 @@ var init_lattice = __esm({
|
|
|
6632
6701
|
* tree when a REMOTE change arrives — notably an owner re-sharing or un-sharing
|
|
6633
6702
|
* a row, after which the member's per-viewer projection must be recompiled. A
|
|
6634
6703
|
* no-op when auto-render isn't enabled.
|
|
6704
|
+
*
|
|
6705
|
+
* Pass the CHANGED table so only that entity (+ its cross-table dependents) is
|
|
6706
|
+
* re-rendered instead of the whole tree; omit it to force a full render.
|
|
6635
6707
|
*/
|
|
6636
|
-
requestRender() {
|
|
6637
|
-
this._scheduleAutoRender();
|
|
6708
|
+
requestRender(table) {
|
|
6709
|
+
this._scheduleAutoRender(table);
|
|
6638
6710
|
}
|
|
6639
6711
|
/**
|
|
6640
6712
|
* True while a render is actively writing the context tree + manifest (auto-
|
|
@@ -7035,7 +7107,7 @@ var init_lattice = __esm({
|
|
|
7035
7107
|
for (const h6 of this._errorHandlers) h6(err instanceof Error ? err : new Error(String(err)));
|
|
7036
7108
|
}
|
|
7037
7109
|
}
|
|
7038
|
-
this._scheduleAutoRender();
|
|
7110
|
+
this._scheduleAutoRender(table);
|
|
7039
7111
|
}
|
|
7040
7112
|
/**
|
|
7041
7113
|
* Turn on automatic rendering into `outputDir`. After this, every insert /
|
|
@@ -7059,10 +7131,18 @@ var init_lattice = __esm({
|
|
|
7059
7131
|
this._autoRenderPending = false;
|
|
7060
7132
|
return this;
|
|
7061
7133
|
}
|
|
7062
|
-
_scheduleAutoRender() {
|
|
7134
|
+
_scheduleAutoRender(table) {
|
|
7063
7135
|
if (!this._autoRenderDir) return;
|
|
7136
|
+
if (table === void 0) this._pendingRenderAll = true;
|
|
7137
|
+
else this._pendingRenderTables.add(table);
|
|
7064
7138
|
this._autoRenderPending = true;
|
|
7065
|
-
|
|
7139
|
+
this._armAutoRenderTimer();
|
|
7140
|
+
}
|
|
7141
|
+
/** Arm the debounce timer if not already armed. Does NOT change the render
|
|
7142
|
+
* scope — used both by `_scheduleAutoRender` and the post-render re-arm so a
|
|
7143
|
+
* re-arm never escalates a pending incremental render to a full one. */
|
|
7144
|
+
_armAutoRenderTimer() {
|
|
7145
|
+
if (!this._autoRenderDir || this._autoRenderTimer) return;
|
|
7066
7146
|
this._autoRenderTimer = setTimeout(() => {
|
|
7067
7147
|
this._autoRenderTimer = void 0;
|
|
7068
7148
|
void this._runAutoRender();
|
|
@@ -7102,10 +7182,15 @@ var init_lattice = __esm({
|
|
|
7102
7182
|
}
|
|
7103
7183
|
if (!this._autoRenderPending) return;
|
|
7104
7184
|
this._autoRenderPending = false;
|
|
7185
|
+
const renderAll = this._pendingRenderAll;
|
|
7186
|
+
const changed = this._pendingRenderTables;
|
|
7187
|
+
this._pendingRenderAll = false;
|
|
7188
|
+
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
7105
7189
|
this._autoRenderInFlight = true;
|
|
7106
7190
|
try {
|
|
7107
7191
|
const prevManifest = readManifest(dir);
|
|
7108
|
-
const
|
|
7192
|
+
const renderOpts = renderAll || changed.size === 0 ? {} : { changedTables: changed };
|
|
7193
|
+
const result = await this._render.render(dir, renderOpts);
|
|
7109
7194
|
for (const h6 of this._renderHandlers) h6(result);
|
|
7110
7195
|
const newManifest = readManifest(dir);
|
|
7111
7196
|
await this._render.cleanup(dir, prevManifest, {}, newManifest);
|
|
@@ -7117,7 +7202,7 @@ var init_lattice = __esm({
|
|
|
7117
7202
|
}
|
|
7118
7203
|
}
|
|
7119
7204
|
_rearmAutoRenderIfPending() {
|
|
7120
|
-
if (this._autoRenderPending) this.
|
|
7205
|
+
if (this._autoRenderPending) this._armAutoRenderTimer();
|
|
7121
7206
|
}
|
|
7122
7207
|
/**
|
|
7123
7208
|
* Update or remove the embedding for a row.
|
|
@@ -47282,6 +47367,17 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
|
|
|
47282
47367
|
FOR EACH ROW EXECUTE FUNCTION "${trg}"();
|
|
47283
47368
|
`;
|
|
47284
47369
|
}
|
|
47370
|
+
async function ownPolyfillsByGroup(db) {
|
|
47371
|
+
if (!isPg(db)) return;
|
|
47372
|
+
for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
|
|
47373
|
+
try {
|
|
47374
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
|
|
47375
|
+
if (reg?.reg == null) continue;
|
|
47376
|
+
await runAsyncOrSync(db.adapter, `ALTER FUNCTION ${sig} OWNER TO "${MEMBER_GROUP}"`);
|
|
47377
|
+
} catch {
|
|
47378
|
+
}
|
|
47379
|
+
}
|
|
47380
|
+
}
|
|
47285
47381
|
async function installCloudRls(db) {
|
|
47286
47382
|
if (!isPg(db)) return;
|
|
47287
47383
|
const schema = await cloudSchema(db);
|
|
@@ -47315,6 +47411,24 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
|
|
|
47315
47411
|
`
|
|
47316
47412
|
);
|
|
47317
47413
|
}
|
|
47414
|
+
async function enableChatPrivacyRls(db) {
|
|
47415
|
+
if (!isPg(db)) return;
|
|
47416
|
+
for (const t8 of ["chat_threads", "chat_messages"]) {
|
|
47417
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [t8]);
|
|
47418
|
+
if (reg?.reg == null) continue;
|
|
47419
|
+
const q3 = `"${t8}"`;
|
|
47420
|
+
await runCloudBootstrapSql(
|
|
47421
|
+
db,
|
|
47422
|
+
`
|
|
47423
|
+
ALTER TABLE ${q3} ENABLE ROW LEVEL SECURITY;
|
|
47424
|
+
ALTER TABLE ${q3} FORCE ROW LEVEL SECURITY;
|
|
47425
|
+
DROP POLICY IF EXISTS "lattice_chat_owner" ON ${q3};
|
|
47426
|
+
CREATE POLICY "lattice_chat_owner" ON ${q3} AS RESTRICTIVE FOR SELECT
|
|
47427
|
+
USING ("owner_user_id" IS NOT NULL AND "owner_user_id" = session_user);
|
|
47428
|
+
`
|
|
47429
|
+
);
|
|
47430
|
+
}
|
|
47431
|
+
}
|
|
47318
47432
|
async function enableRlsForTable(db, table, pkCols) {
|
|
47319
47433
|
if (!isPg(db)) return;
|
|
47320
47434
|
const schema = await cloudSchema(db);
|
|
@@ -54680,9 +54794,11 @@ async function secureCloud(db) {
|
|
|
54680
54794
|
if (db.getDialect() !== "postgres") return;
|
|
54681
54795
|
await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
|
|
54682
54796
|
await installCloudRls(db);
|
|
54797
|
+
await ownPolyfillsByGroup(db);
|
|
54683
54798
|
await installCloudSettings(db);
|
|
54684
54799
|
await db.ensureObservationSubstrate();
|
|
54685
54800
|
await enableChangelogRls(db);
|
|
54801
|
+
await enableChatPrivacyRls(db);
|
|
54686
54802
|
await convergeLegacyColumnAudience(db);
|
|
54687
54803
|
const registered = db.getRegisteredTableNames();
|
|
54688
54804
|
for (const table of registered) {
|
|
@@ -57728,12 +57844,17 @@ var appJs = `
|
|
|
57728
57844
|
fetchJson('/api/gui-meta/columns').catch(function () { return {}; }),
|
|
57729
57845
|
fetchJson('/api/system-tables').catch(function () { return { tables: [] }; }),
|
|
57730
57846
|
fetchJson('/api/workspaces').catch(function () { return null; }),
|
|
57847
|
+
fetchJson('/api/dbconfig').catch(function () { return {}; }),
|
|
57731
57848
|
]).then(function (results) {
|
|
57732
57849
|
state.entities = results[0];
|
|
57733
57850
|
state.iconOverrides = results[1] || {};
|
|
57734
57851
|
state.columnMeta = results[2] || {};
|
|
57735
57852
|
state.systemTables = (results[3] && results[3].tables) || [];
|
|
57736
57853
|
renderWsSwitcher(results[4]);
|
|
57854
|
+
// Re-point the header logo at the NEW workspace's mark \u2014 the switch path
|
|
57855
|
+
// must refresh branding the way boot does (the etag cache-busts the
|
|
57856
|
+
// <img>), else the previous workspace's logo stays until a hard refresh.
|
|
57857
|
+
applyWorkspaceLogo((results[5] || {}).logoEtag);
|
|
57737
57858
|
renderSidebar();
|
|
57738
57859
|
// renderWsSwitcher set cloudMode from the new workspace's kind; re-render
|
|
57739
57860
|
// the composer so the Private-mode toggle reflects local vs cloud (it is
|
|
@@ -66384,6 +66505,7 @@ function activeCloudCoords(configPath) {
|
|
|
66384
66505
|
init_assistant_routes();
|
|
66385
66506
|
|
|
66386
66507
|
// src/gui/chat-routes.ts
|
|
66508
|
+
init_adapter();
|
|
66387
66509
|
init_assistant_routes();
|
|
66388
66510
|
init_chat();
|
|
66389
66511
|
init_user_config();
|
|
@@ -66409,6 +66531,15 @@ function sendJson3(res, body, status = 200) {
|
|
|
66409
66531
|
function asStr(v2, fallback = "") {
|
|
66410
66532
|
return typeof v2 === "string" ? v2 : fallback;
|
|
66411
66533
|
}
|
|
66534
|
+
function isCloudChat(db) {
|
|
66535
|
+
return db.getDialect() === "postgres";
|
|
66536
|
+
}
|
|
66537
|
+
async function resolveChatOwnerId(db) {
|
|
66538
|
+
if (!isCloudChat(db)) return null;
|
|
66539
|
+
const row = await getAsyncOrSync(db.adapter, "SELECT session_user AS u");
|
|
66540
|
+
const u2 = row?.u;
|
|
66541
|
+
return typeof u2 === "string" && u2.length > 0 ? u2 : null;
|
|
66542
|
+
}
|
|
66412
66543
|
function readJson3(req) {
|
|
66413
66544
|
return new Promise((resolve12, reject) => {
|
|
66414
66545
|
let raw = "";
|
|
@@ -66601,12 +66732,20 @@ async function persistMessage(db, threadId, role, text, ownerUserId, turns, star
|
|
|
66601
66732
|
});
|
|
66602
66733
|
}
|
|
66603
66734
|
async function dispatchChatRoute(req, res, ctx) {
|
|
66735
|
+
const ownerUserId = await resolveChatOwnerId(ctx.db);
|
|
66736
|
+
const cloud = isCloudChat(ctx.db);
|
|
66737
|
+
const ownedByMe = (r6) => !cloud || r6.owner_user_id != null && r6.owner_user_id === ownerUserId;
|
|
66604
66738
|
if (ctx.method === "GET" && ctx.pathname === "/api/chat/threads") {
|
|
66739
|
+
if (cloud && ownerUserId == null) {
|
|
66740
|
+
sendJson3(res, { threads: [] });
|
|
66741
|
+
return true;
|
|
66742
|
+
}
|
|
66605
66743
|
const filters = [
|
|
66606
66744
|
{ col: "deleted_at", op: "isNull" }
|
|
66607
66745
|
];
|
|
66746
|
+
if (ownerUserId != null) filters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
66608
66747
|
const rows = await ctx.db.query("chat_threads", { filters, limit: 100 });
|
|
66609
|
-
const threads = rows.filter((r6) => !r6.deleted_at).map((r6) => ({
|
|
66748
|
+
const threads = rows.filter((r6) => !r6.deleted_at && ownedByMe(r6)).map((r6) => ({
|
|
66610
66749
|
id: asStr(r6.id),
|
|
66611
66750
|
title: asStr(r6.title, "Chat"),
|
|
66612
66751
|
created_at: asStr(r6.created_at)
|
|
@@ -66617,12 +66756,19 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66617
66756
|
const msgMatch = /^\/api\/chat\/threads\/([^/]+)\/messages$/.exec(ctx.pathname);
|
|
66618
66757
|
if (ctx.method === "GET" && msgMatch) {
|
|
66619
66758
|
const threadId2 = decodeURIComponent(msgMatch[1] ?? "");
|
|
66620
|
-
|
|
66759
|
+
if (cloud && ownerUserId == null) {
|
|
66760
|
+
sendJson3(res, { messages: [] });
|
|
66761
|
+
return true;
|
|
66762
|
+
}
|
|
66763
|
+
const msgFilters = [
|
|
66764
|
+
{ col: "thread_id", op: "eq", val: threadId2 }
|
|
66765
|
+
];
|
|
66766
|
+
if (ownerUserId != null) msgFilters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
66621
66767
|
const rows = await ctx.db.query("chat_messages", {
|
|
66622
66768
|
filters: msgFilters,
|
|
66623
66769
|
limit: 1e3
|
|
66624
66770
|
});
|
|
66625
|
-
const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at).map((r6) => {
|
|
66771
|
+
const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at && ownedByMe(r6)).map((r6) => {
|
|
66626
66772
|
let text = "";
|
|
66627
66773
|
let turns2;
|
|
66628
66774
|
let startedAt;
|
|
@@ -66675,12 +66821,21 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66675
66821
|
return true;
|
|
66676
66822
|
}
|
|
66677
66823
|
const requestedThread = typeof body.threadId === "string" ? body.threadId : null;
|
|
66824
|
+
if (cloud && ownerUserId == null) {
|
|
66825
|
+
sendJson3(res, { error: "Could not resolve your cloud identity; chat is disabled." }, 500);
|
|
66826
|
+
return true;
|
|
66827
|
+
}
|
|
66678
66828
|
const activeContext = parseActiveContext(body.activeContext, ctx.validTables);
|
|
66679
|
-
const history = await rehydrateHistory(
|
|
66829
|
+
const history = await rehydrateHistory(
|
|
66830
|
+
ctx.db,
|
|
66831
|
+
requestedThread,
|
|
66832
|
+
mapHistory(body.history),
|
|
66833
|
+
ownerUserId
|
|
66834
|
+
);
|
|
66680
66835
|
let threadId = "";
|
|
66681
66836
|
try {
|
|
66682
|
-
threadId = await ensureThread(ctx.db, requestedThread, message,
|
|
66683
|
-
await persistMessage(ctx.db, threadId, "user", message,
|
|
66837
|
+
threadId = await ensureThread(ctx.db, requestedThread, message, ownerUserId);
|
|
66838
|
+
await persistMessage(ctx.db, threadId, "user", message, ownerUserId);
|
|
66684
66839
|
} catch (e6) {
|
|
66685
66840
|
console.warn("[chat] persist user message failed:", e6.message);
|
|
66686
66841
|
}
|
|
@@ -66752,7 +66907,7 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66752
66907
|
threadId,
|
|
66753
66908
|
"assistant",
|
|
66754
66909
|
assistantText,
|
|
66755
|
-
|
|
66910
|
+
ownerUserId,
|
|
66756
66911
|
cleanTurns,
|
|
66757
66912
|
turnStartedAt,
|
|
66758
66913
|
assistantMsgId
|
|
@@ -67751,9 +67906,11 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
67751
67906
|
if (await cloudRlsInstalled(db) && await canManageRoles(db)) {
|
|
67752
67907
|
await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
|
|
67753
67908
|
await installCloudRls(db);
|
|
67909
|
+
await ownPolyfillsByGroup(db);
|
|
67754
67910
|
await installCloudSettings(db);
|
|
67755
67911
|
await db.ensureObservationSubstrate();
|
|
67756
67912
|
await enableChangelogRls(db);
|
|
67913
|
+
await enableChatPrivacyRls(db);
|
|
67757
67914
|
const access = await reconcileCloudMemberAccess(db);
|
|
67758
67915
|
convergeWarnings = access.skipped;
|
|
67759
67916
|
for (const s2 of convergeWarnings) {
|
|
@@ -67915,11 +68072,22 @@ function startBackgroundRender(active) {
|
|
|
67915
68072
|
active.eagerRenderWired = true;
|
|
67916
68073
|
let lastFire = 0;
|
|
67917
68074
|
let trailing;
|
|
68075
|
+
const pendingTables = /* @__PURE__ */ new Set();
|
|
68076
|
+
let pendingFull = false;
|
|
67918
68077
|
const fire = () => {
|
|
67919
68078
|
lastFire = Date.now();
|
|
67920
|
-
|
|
68079
|
+
if (pendingFull || pendingTables.size === 0) {
|
|
68080
|
+
pendingFull = false;
|
|
68081
|
+
pendingTables.clear();
|
|
68082
|
+
active.db.requestRender();
|
|
68083
|
+
return;
|
|
68084
|
+
}
|
|
68085
|
+
for (const t8 of pendingTables) active.db.requestRender(t8);
|
|
68086
|
+
pendingTables.clear();
|
|
67921
68087
|
};
|
|
67922
|
-
active.realtime.subscribePayload(() => {
|
|
68088
|
+
active.realtime.subscribePayload((payload) => {
|
|
68089
|
+
if (payload.table_name) pendingTables.add(payload.table_name);
|
|
68090
|
+
else pendingFull = true;
|
|
67923
68091
|
const since = Date.now() - lastFire;
|
|
67924
68092
|
if (since >= EAGER_RERENDER_MIN_INTERVAL_MS) {
|
|
67925
68093
|
fire();
|
package/dist/index.d.cts
CHANGED
|
@@ -1816,6 +1816,16 @@ type RenderProgressCallback = (event: RenderProgress) => void;
|
|
|
1816
1816
|
interface RenderOptions {
|
|
1817
1817
|
onProgress?: RenderProgressCallback;
|
|
1818
1818
|
signal?: AbortSignal;
|
|
1819
|
+
/**
|
|
1820
|
+
* Incremental render scope. When set, ONLY the entity-context tables affected
|
|
1821
|
+
* by a change to one of these tables are re-rendered — the changed table itself
|
|
1822
|
+
* plus any entity context that SOURCES from it (cross-table dependents) — and
|
|
1823
|
+
* every other table's manifest entry + rendered files are left untouched. Used
|
|
1824
|
+
* by the auto-render that fires on a single write or a single remote (cloud)
|
|
1825
|
+
* change, so a one-row edit re-renders one entity instead of the whole tree.
|
|
1826
|
+
* Omitted → a full render of everything (initial open, explicit `render()`).
|
|
1827
|
+
*/
|
|
1828
|
+
changedTables?: ReadonlySet<string>;
|
|
1819
1829
|
}
|
|
1820
1830
|
/**
|
|
1821
1831
|
* Coalesces high-frequency `table-progress` events down to ≤ ~5/sec per table,
|
|
@@ -1903,6 +1913,16 @@ declare class Lattice {
|
|
|
1903
1913
|
private _autoRenderPending;
|
|
1904
1914
|
private _autoRenderInFlight;
|
|
1905
1915
|
private _autoRenderDebounceMs;
|
|
1916
|
+
/**
|
|
1917
|
+
* Incremental auto-render scope, accumulated between debounced renders. A write
|
|
1918
|
+
* or a remote (cloud) change records the AFFECTED table here, so the next
|
|
1919
|
+
* auto-render re-renders only that entity (+ its cross-table dependents) instead
|
|
1920
|
+
* of the whole tree. `_pendingRenderAll` forces a full render (the initial
|
|
1921
|
+
* render, or a change with no known table). Captured + reset when a render
|
|
1922
|
+
* starts, so changes during a render re-accumulate and re-trigger.
|
|
1923
|
+
*/
|
|
1924
|
+
private _pendingRenderTables;
|
|
1925
|
+
private _pendingRenderAll;
|
|
1906
1926
|
/** Cache of actual table columns (from PRAGMA), populated after init(). */
|
|
1907
1927
|
private readonly _columnCache;
|
|
1908
1928
|
/** Derived encryption key (from options.encryptionKey via scrypt). */
|
|
@@ -2323,8 +2343,11 @@ declare class Lattice {
|
|
|
2323
2343
|
* tree when a REMOTE change arrives — notably an owner re-sharing or un-sharing
|
|
2324
2344
|
* a row, after which the member's per-viewer projection must be recompiled. A
|
|
2325
2345
|
* no-op when auto-render isn't enabled.
|
|
2346
|
+
*
|
|
2347
|
+
* Pass the CHANGED table so only that entity (+ its cross-table dependents) is
|
|
2348
|
+
* re-rendered instead of the whole tree; omit it to force a full render.
|
|
2326
2349
|
*/
|
|
2327
|
-
requestRender(): void;
|
|
2350
|
+
requestRender(table?: string): void;
|
|
2328
2351
|
/**
|
|
2329
2352
|
* True while a render is actively writing the context tree + manifest (auto-
|
|
2330
2353
|
* render OR a guarded background render). The file-loopback watcher checks this
|
|
@@ -2483,6 +2506,10 @@ declare class Lattice {
|
|
|
2483
2506
|
/** Turn off automatic rendering and cancel any pending render. */
|
|
2484
2507
|
disableAutoRender(): this;
|
|
2485
2508
|
private _scheduleAutoRender;
|
|
2509
|
+
/** Arm the debounce timer if not already armed. Does NOT change the render
|
|
2510
|
+
* scope — used both by `_scheduleAutoRender` and the post-render re-arm so a
|
|
2511
|
+
* re-arm never escalates a pending incremental render to a full one. */
|
|
2512
|
+
private _armAutoRenderTimer;
|
|
2486
2513
|
/**
|
|
2487
2514
|
* Shared single-flight render path used by {@link renderInBackground}.
|
|
2488
2515
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1816,6 +1816,16 @@ type RenderProgressCallback = (event: RenderProgress) => void;
|
|
|
1816
1816
|
interface RenderOptions {
|
|
1817
1817
|
onProgress?: RenderProgressCallback;
|
|
1818
1818
|
signal?: AbortSignal;
|
|
1819
|
+
/**
|
|
1820
|
+
* Incremental render scope. When set, ONLY the entity-context tables affected
|
|
1821
|
+
* by a change to one of these tables are re-rendered — the changed table itself
|
|
1822
|
+
* plus any entity context that SOURCES from it (cross-table dependents) — and
|
|
1823
|
+
* every other table's manifest entry + rendered files are left untouched. Used
|
|
1824
|
+
* by the auto-render that fires on a single write or a single remote (cloud)
|
|
1825
|
+
* change, so a one-row edit re-renders one entity instead of the whole tree.
|
|
1826
|
+
* Omitted → a full render of everything (initial open, explicit `render()`).
|
|
1827
|
+
*/
|
|
1828
|
+
changedTables?: ReadonlySet<string>;
|
|
1819
1829
|
}
|
|
1820
1830
|
/**
|
|
1821
1831
|
* Coalesces high-frequency `table-progress` events down to ≤ ~5/sec per table,
|
|
@@ -1903,6 +1913,16 @@ declare class Lattice {
|
|
|
1903
1913
|
private _autoRenderPending;
|
|
1904
1914
|
private _autoRenderInFlight;
|
|
1905
1915
|
private _autoRenderDebounceMs;
|
|
1916
|
+
/**
|
|
1917
|
+
* Incremental auto-render scope, accumulated between debounced renders. A write
|
|
1918
|
+
* or a remote (cloud) change records the AFFECTED table here, so the next
|
|
1919
|
+
* auto-render re-renders only that entity (+ its cross-table dependents) instead
|
|
1920
|
+
* of the whole tree. `_pendingRenderAll` forces a full render (the initial
|
|
1921
|
+
* render, or a change with no known table). Captured + reset when a render
|
|
1922
|
+
* starts, so changes during a render re-accumulate and re-trigger.
|
|
1923
|
+
*/
|
|
1924
|
+
private _pendingRenderTables;
|
|
1925
|
+
private _pendingRenderAll;
|
|
1906
1926
|
/** Cache of actual table columns (from PRAGMA), populated after init(). */
|
|
1907
1927
|
private readonly _columnCache;
|
|
1908
1928
|
/** Derived encryption key (from options.encryptionKey via scrypt). */
|
|
@@ -2323,8 +2343,11 @@ declare class Lattice {
|
|
|
2323
2343
|
* tree when a REMOTE change arrives — notably an owner re-sharing or un-sharing
|
|
2324
2344
|
* a row, after which the member's per-viewer projection must be recompiled. A
|
|
2325
2345
|
* no-op when auto-render isn't enabled.
|
|
2346
|
+
*
|
|
2347
|
+
* Pass the CHANGED table so only that entity (+ its cross-table dependents) is
|
|
2348
|
+
* re-rendered instead of the whole tree; omit it to force a full render.
|
|
2326
2349
|
*/
|
|
2327
|
-
requestRender(): void;
|
|
2350
|
+
requestRender(table?: string): void;
|
|
2328
2351
|
/**
|
|
2329
2352
|
* True while a render is actively writing the context tree + manifest (auto-
|
|
2330
2353
|
* render OR a guarded background render). The file-loopback watcher checks this
|
|
@@ -2483,6 +2506,10 @@ declare class Lattice {
|
|
|
2483
2506
|
/** Turn off automatic rendering and cancel any pending render. */
|
|
2484
2507
|
disableAutoRender(): this;
|
|
2485
2508
|
private _scheduleAutoRender;
|
|
2509
|
+
/** Arm the debounce timer if not already armed. Does NOT change the render
|
|
2510
|
+
* scope — used both by `_scheduleAutoRender` and the post-render re-arm so a
|
|
2511
|
+
* re-arm never escalates a pending incremental render to a full one. */
|
|
2512
|
+
private _armAutoRenderTimer;
|
|
2486
2513
|
/**
|
|
2487
2514
|
* Shared single-flight render path used by {@link renderInBackground}.
|
|
2488
2515
|
*
|