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.js
CHANGED
|
@@ -908,43 +908,57 @@ var init_postgres = __esm({
|
|
|
908
908
|
},
|
|
909
909
|
{
|
|
910
910
|
warn: "could not register json_extract polyfill:",
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
911
|
+
// Create ONLY if absent. `CREATE OR REPLACE` on an existing function requires
|
|
912
|
+
// ownership, but on a cloud the function is owned by whichever single role
|
|
913
|
+
// created it first — so every OTHER member's per-connect replace raised "must
|
|
914
|
+
// be owner of function" and (sharing the render transaction) aborted it,
|
|
915
|
+
// yielding an empty render. The IF-absent guard makes a present function a
|
|
916
|
+
// clean no-op for everyone, regardless of who owns it.
|
|
917
|
+
sql: `DO $do$ BEGIN
|
|
918
|
+
IF to_regprocedure('json_extract(text, text)') IS NULL THEN
|
|
919
|
+
CREATE FUNCTION json_extract(doc text, path text)
|
|
920
|
+
RETURNS text
|
|
921
|
+
LANGUAGE sql
|
|
922
|
+
IMMUTABLE
|
|
923
|
+
AS $fn$
|
|
924
|
+
SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
|
|
925
|
+
$fn$;
|
|
926
|
+
END IF;
|
|
927
|
+
END $do$;`
|
|
918
928
|
},
|
|
919
929
|
{
|
|
920
930
|
warn: "could not register strftime polyfill:",
|
|
921
|
-
sql: `
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
931
|
+
sql: `DO $do$ BEGIN
|
|
932
|
+
IF to_regprocedure('strftime(text, text)') IS NULL THEN
|
|
933
|
+
CREATE FUNCTION strftime(format text, modifier text)
|
|
934
|
+
RETURNS text
|
|
935
|
+
LANGUAGE plpgsql
|
|
936
|
+
IMMUTABLE
|
|
937
|
+
AS $fn$
|
|
938
|
+
DECLARE ts timestamptz;
|
|
939
|
+
BEGIN
|
|
940
|
+
IF modifier = 'now' THEN
|
|
941
|
+
ts := now();
|
|
942
|
+
ELSE
|
|
943
|
+
ts := modifier::timestamptz;
|
|
944
|
+
END IF;
|
|
945
|
+
RETURN to_char(
|
|
946
|
+
ts AT TIME ZONE 'UTC',
|
|
947
|
+
replace(replace(replace(replace(replace(replace(replace(replace(
|
|
948
|
+
format,
|
|
949
|
+
'%Y', 'YYYY'),
|
|
950
|
+
'%m', 'MM'),
|
|
951
|
+
'%d', 'DD'),
|
|
952
|
+
'%H', 'HH24'),
|
|
953
|
+
'%M', 'MI'),
|
|
954
|
+
'%S', 'SS'),
|
|
955
|
+
'%f', 'MS'),
|
|
956
|
+
'T', '"T"')
|
|
957
|
+
);
|
|
958
|
+
END;
|
|
959
|
+
$fn$;
|
|
960
|
+
END IF;
|
|
961
|
+
END $do$;`
|
|
948
962
|
}
|
|
949
963
|
];
|
|
950
964
|
}
|
|
@@ -2352,6 +2366,34 @@ var init_engine = __esm({
|
|
|
2352
2366
|
setRenderFold(fn) {
|
|
2353
2367
|
this._foldRows = fn;
|
|
2354
2368
|
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Incremental scope: is this entity-context table affected by a change to one
|
|
2371
|
+
* of `changed`? Affected when the table itself changed (its own rows / `self`
|
|
2372
|
+
* source / index) OR any of its files SOURCES from a changed table (a cross-
|
|
2373
|
+
* table dependent — e.g. an AGENT.md that lists the agent's tasks must re-render
|
|
2374
|
+
* when `tasks` changes). A `custom` source runs an arbitrary query, so we can't
|
|
2375
|
+
* prove independence — treat it as always-affected (conservative, never stale).
|
|
2376
|
+
*/
|
|
2377
|
+
_entityAffected(table, def, changed) {
|
|
2378
|
+
if (changed.has(table)) return true;
|
|
2379
|
+
for (const spec of Object.values(def.files)) {
|
|
2380
|
+
if (this._sourceTouches(spec.source, changed)) return true;
|
|
2381
|
+
}
|
|
2382
|
+
return false;
|
|
2383
|
+
}
|
|
2384
|
+
_sourceTouches(source, changed) {
|
|
2385
|
+
if (source == null || typeof source !== "object") return false;
|
|
2386
|
+
const s2 = source;
|
|
2387
|
+
if (s2.type === "custom") return true;
|
|
2388
|
+
if (typeof s2.table === "string" && changed.has(s2.table)) return true;
|
|
2389
|
+
if (typeof s2.junctionTable === "string" && changed.has(s2.junctionTable)) return true;
|
|
2390
|
+
if (s2.sources != null && typeof s2.sources === "object") {
|
|
2391
|
+
for (const sub of Object.values(s2.sources)) {
|
|
2392
|
+
if (this._sourceTouches(sub, changed)) return true;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2355
2397
|
async render(outputDir, opts = {}) {
|
|
2356
2398
|
const start = Date.now();
|
|
2357
2399
|
const filesWritten = [];
|
|
@@ -2361,6 +2403,7 @@ var init_engine = __esm({
|
|
|
2361
2403
|
for (const [name, def] of this._schema.getTables()) {
|
|
2362
2404
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
2363
2405
|
if (this._skipEmpty && def.render === NOOP_RENDER) continue;
|
|
2406
|
+
if (opts.changedTables && !opts.changedTables.has(name)) continue;
|
|
2364
2407
|
let rows = await this._schema.queryTable(this._adapter, name, this._readRel);
|
|
2365
2408
|
if (def.relevanceFilter) {
|
|
2366
2409
|
const ctx = this._getTaskContext();
|
|
@@ -2413,6 +2456,9 @@ var init_engine = __esm({
|
|
|
2413
2456
|
}
|
|
2414
2457
|
for (const [name, def] of this._schema.getMultis()) {
|
|
2415
2458
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
2459
|
+
if (opts.changedTables && def.tables && !def.tables.some((t8) => opts.changedTables?.has(t8))) {
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2416
2462
|
const keys = await def.keys();
|
|
2417
2463
|
const tables = {};
|
|
2418
2464
|
if (def.tables) {
|
|
@@ -2444,16 +2490,22 @@ var init_engine = __esm({
|
|
|
2444
2490
|
filesWritten,
|
|
2445
2491
|
counters,
|
|
2446
2492
|
throttle,
|
|
2447
|
-
signal
|
|
2493
|
+
signal,
|
|
2494
|
+
opts.changedTables
|
|
2448
2495
|
);
|
|
2449
2496
|
if (entityContextManifest === null) {
|
|
2450
2497
|
return this._abortedResult(filesWritten, counters, start);
|
|
2451
2498
|
}
|
|
2452
2499
|
if (this._schema.getEntityContexts().size > 0) {
|
|
2500
|
+
let entityContexts = entityContextManifest;
|
|
2501
|
+
if (opts.changedTables) {
|
|
2502
|
+
const prev = readManifest(outputDir);
|
|
2503
|
+
entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
|
|
2504
|
+
}
|
|
2453
2505
|
writeManifest(outputDir, {
|
|
2454
2506
|
version: 2,
|
|
2455
2507
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2456
|
-
entityContexts
|
|
2508
|
+
entityContexts
|
|
2457
2509
|
});
|
|
2458
2510
|
}
|
|
2459
2511
|
const result = {
|
|
@@ -2519,12 +2571,14 @@ var init_engine = __esm({
|
|
|
2519
2571
|
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
2520
2572
|
* via `signal`.
|
|
2521
2573
|
*/
|
|
2522
|
-
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
|
|
2574
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
|
|
2523
2575
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
2524
2576
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
2525
2577
|
if (d6.protected) protectedTables.add(t8);
|
|
2526
2578
|
}
|
|
2527
|
-
const entityTables = [...this._schema.getEntityContexts()]
|
|
2579
|
+
const entityTables = [...this._schema.getEntityContexts()].filter(
|
|
2580
|
+
([table, def]) => !changedTables || this._entityAffected(table, def, changedTables)
|
|
2581
|
+
);
|
|
2528
2582
|
const tableCount = entityTables.length;
|
|
2529
2583
|
if (signal?.aborted) return null;
|
|
2530
2584
|
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.
|
|
@@ -47275,6 +47360,17 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
|
|
|
47275
47360
|
FOR EACH ROW EXECUTE FUNCTION "${trg}"();
|
|
47276
47361
|
`;
|
|
47277
47362
|
}
|
|
47363
|
+
async function ownPolyfillsByGroup(db) {
|
|
47364
|
+
if (!isPg(db)) return;
|
|
47365
|
+
for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
|
|
47366
|
+
try {
|
|
47367
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
|
|
47368
|
+
if (reg?.reg == null) continue;
|
|
47369
|
+
await runAsyncOrSync(db.adapter, `ALTER FUNCTION ${sig} OWNER TO "${MEMBER_GROUP}"`);
|
|
47370
|
+
} catch {
|
|
47371
|
+
}
|
|
47372
|
+
}
|
|
47373
|
+
}
|
|
47278
47374
|
async function installCloudRls(db) {
|
|
47279
47375
|
if (!isPg(db)) return;
|
|
47280
47376
|
const schema = await cloudSchema(db);
|
|
@@ -47308,6 +47404,24 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
|
|
|
47308
47404
|
`
|
|
47309
47405
|
);
|
|
47310
47406
|
}
|
|
47407
|
+
async function enableChatPrivacyRls(db) {
|
|
47408
|
+
if (!isPg(db)) return;
|
|
47409
|
+
for (const t8 of ["chat_threads", "chat_messages"]) {
|
|
47410
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [t8]);
|
|
47411
|
+
if (reg?.reg == null) continue;
|
|
47412
|
+
const q3 = `"${t8}"`;
|
|
47413
|
+
await runCloudBootstrapSql(
|
|
47414
|
+
db,
|
|
47415
|
+
`
|
|
47416
|
+
ALTER TABLE ${q3} ENABLE ROW LEVEL SECURITY;
|
|
47417
|
+
ALTER TABLE ${q3} FORCE ROW LEVEL SECURITY;
|
|
47418
|
+
DROP POLICY IF EXISTS "lattice_chat_owner" ON ${q3};
|
|
47419
|
+
CREATE POLICY "lattice_chat_owner" ON ${q3} AS RESTRICTIVE FOR SELECT
|
|
47420
|
+
USING ("owner_user_id" IS NOT NULL AND "owner_user_id" = session_user);
|
|
47421
|
+
`
|
|
47422
|
+
);
|
|
47423
|
+
}
|
|
47424
|
+
}
|
|
47311
47425
|
async function enableRlsForTable(db, table, pkCols) {
|
|
47312
47426
|
if (!isPg(db)) return;
|
|
47313
47427
|
const schema = await cloudSchema(db);
|
|
@@ -54498,9 +54612,11 @@ async function secureCloud(db) {
|
|
|
54498
54612
|
if (db.getDialect() !== "postgres") return;
|
|
54499
54613
|
await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
|
|
54500
54614
|
await installCloudRls(db);
|
|
54615
|
+
await ownPolyfillsByGroup(db);
|
|
54501
54616
|
await installCloudSettings(db);
|
|
54502
54617
|
await db.ensureObservationSubstrate();
|
|
54503
54618
|
await enableChangelogRls(db);
|
|
54619
|
+
await enableChatPrivacyRls(db);
|
|
54504
54620
|
await convergeLegacyColumnAudience(db);
|
|
54505
54621
|
const registered = db.getRegisteredTableNames();
|
|
54506
54622
|
for (const table of registered) {
|
|
@@ -57553,12 +57669,17 @@ var appJs = `
|
|
|
57553
57669
|
fetchJson('/api/gui-meta/columns').catch(function () { return {}; }),
|
|
57554
57670
|
fetchJson('/api/system-tables').catch(function () { return { tables: [] }; }),
|
|
57555
57671
|
fetchJson('/api/workspaces').catch(function () { return null; }),
|
|
57672
|
+
fetchJson('/api/dbconfig').catch(function () { return {}; }),
|
|
57556
57673
|
]).then(function (results) {
|
|
57557
57674
|
state.entities = results[0];
|
|
57558
57675
|
state.iconOverrides = results[1] || {};
|
|
57559
57676
|
state.columnMeta = results[2] || {};
|
|
57560
57677
|
state.systemTables = (results[3] && results[3].tables) || [];
|
|
57561
57678
|
renderWsSwitcher(results[4]);
|
|
57679
|
+
// Re-point the header logo at the NEW workspace's mark \u2014 the switch path
|
|
57680
|
+
// must refresh branding the way boot does (the etag cache-busts the
|
|
57681
|
+
// <img>), else the previous workspace's logo stays until a hard refresh.
|
|
57682
|
+
applyWorkspaceLogo((results[5] || {}).logoEtag);
|
|
57562
57683
|
renderSidebar();
|
|
57563
57684
|
// renderWsSwitcher set cloudMode from the new workspace's kind; re-render
|
|
57564
57685
|
// the composer so the Private-mode toggle reflects local vs cloud (it is
|
|
@@ -66208,6 +66329,7 @@ function activeCloudCoords(configPath) {
|
|
|
66208
66329
|
init_assistant_routes();
|
|
66209
66330
|
|
|
66210
66331
|
// src/gui/chat-routes.ts
|
|
66332
|
+
init_adapter();
|
|
66211
66333
|
init_assistant_routes();
|
|
66212
66334
|
init_chat();
|
|
66213
66335
|
init_user_config();
|
|
@@ -66233,6 +66355,15 @@ function sendJson3(res, body, status = 200) {
|
|
|
66233
66355
|
function asStr(v2, fallback = "") {
|
|
66234
66356
|
return typeof v2 === "string" ? v2 : fallback;
|
|
66235
66357
|
}
|
|
66358
|
+
function isCloudChat(db) {
|
|
66359
|
+
return db.getDialect() === "postgres";
|
|
66360
|
+
}
|
|
66361
|
+
async function resolveChatOwnerId(db) {
|
|
66362
|
+
if (!isCloudChat(db)) return null;
|
|
66363
|
+
const row = await getAsyncOrSync(db.adapter, "SELECT session_user AS u");
|
|
66364
|
+
const u2 = row?.u;
|
|
66365
|
+
return typeof u2 === "string" && u2.length > 0 ? u2 : null;
|
|
66366
|
+
}
|
|
66236
66367
|
function readJson3(req) {
|
|
66237
66368
|
return new Promise((resolve12, reject) => {
|
|
66238
66369
|
let raw = "";
|
|
@@ -66425,12 +66556,20 @@ async function persistMessage(db, threadId, role, text, ownerUserId, turns, star
|
|
|
66425
66556
|
});
|
|
66426
66557
|
}
|
|
66427
66558
|
async function dispatchChatRoute(req, res, ctx) {
|
|
66559
|
+
const ownerUserId = await resolveChatOwnerId(ctx.db);
|
|
66560
|
+
const cloud = isCloudChat(ctx.db);
|
|
66561
|
+
const ownedByMe = (r6) => !cloud || r6.owner_user_id != null && r6.owner_user_id === ownerUserId;
|
|
66428
66562
|
if (ctx.method === "GET" && ctx.pathname === "/api/chat/threads") {
|
|
66563
|
+
if (cloud && ownerUserId == null) {
|
|
66564
|
+
sendJson3(res, { threads: [] });
|
|
66565
|
+
return true;
|
|
66566
|
+
}
|
|
66429
66567
|
const filters = [
|
|
66430
66568
|
{ col: "deleted_at", op: "isNull" }
|
|
66431
66569
|
];
|
|
66570
|
+
if (ownerUserId != null) filters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
66432
66571
|
const rows = await ctx.db.query("chat_threads", { filters, limit: 100 });
|
|
66433
|
-
const threads = rows.filter((r6) => !r6.deleted_at).map((r6) => ({
|
|
66572
|
+
const threads = rows.filter((r6) => !r6.deleted_at && ownedByMe(r6)).map((r6) => ({
|
|
66434
66573
|
id: asStr(r6.id),
|
|
66435
66574
|
title: asStr(r6.title, "Chat"),
|
|
66436
66575
|
created_at: asStr(r6.created_at)
|
|
@@ -66441,12 +66580,19 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66441
66580
|
const msgMatch = /^\/api\/chat\/threads\/([^/]+)\/messages$/.exec(ctx.pathname);
|
|
66442
66581
|
if (ctx.method === "GET" && msgMatch) {
|
|
66443
66582
|
const threadId2 = decodeURIComponent(msgMatch[1] ?? "");
|
|
66444
|
-
|
|
66583
|
+
if (cloud && ownerUserId == null) {
|
|
66584
|
+
sendJson3(res, { messages: [] });
|
|
66585
|
+
return true;
|
|
66586
|
+
}
|
|
66587
|
+
const msgFilters = [
|
|
66588
|
+
{ col: "thread_id", op: "eq", val: threadId2 }
|
|
66589
|
+
];
|
|
66590
|
+
if (ownerUserId != null) msgFilters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
66445
66591
|
const rows = await ctx.db.query("chat_messages", {
|
|
66446
66592
|
filters: msgFilters,
|
|
66447
66593
|
limit: 1e3
|
|
66448
66594
|
});
|
|
66449
|
-
const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at).map((r6) => {
|
|
66595
|
+
const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at && ownedByMe(r6)).map((r6) => {
|
|
66450
66596
|
let text = "";
|
|
66451
66597
|
let turns2;
|
|
66452
66598
|
let startedAt;
|
|
@@ -66499,12 +66645,21 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66499
66645
|
return true;
|
|
66500
66646
|
}
|
|
66501
66647
|
const requestedThread = typeof body.threadId === "string" ? body.threadId : null;
|
|
66648
|
+
if (cloud && ownerUserId == null) {
|
|
66649
|
+
sendJson3(res, { error: "Could not resolve your cloud identity; chat is disabled." }, 500);
|
|
66650
|
+
return true;
|
|
66651
|
+
}
|
|
66502
66652
|
const activeContext = parseActiveContext(body.activeContext, ctx.validTables);
|
|
66503
|
-
const history = await rehydrateHistory(
|
|
66653
|
+
const history = await rehydrateHistory(
|
|
66654
|
+
ctx.db,
|
|
66655
|
+
requestedThread,
|
|
66656
|
+
mapHistory(body.history),
|
|
66657
|
+
ownerUserId
|
|
66658
|
+
);
|
|
66504
66659
|
let threadId = "";
|
|
66505
66660
|
try {
|
|
66506
|
-
threadId = await ensureThread(ctx.db, requestedThread, message,
|
|
66507
|
-
await persistMessage(ctx.db, threadId, "user", message,
|
|
66661
|
+
threadId = await ensureThread(ctx.db, requestedThread, message, ownerUserId);
|
|
66662
|
+
await persistMessage(ctx.db, threadId, "user", message, ownerUserId);
|
|
66508
66663
|
} catch (e6) {
|
|
66509
66664
|
console.warn("[chat] persist user message failed:", e6.message);
|
|
66510
66665
|
}
|
|
@@ -66576,7 +66731,7 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66576
66731
|
threadId,
|
|
66577
66732
|
"assistant",
|
|
66578
66733
|
assistantText,
|
|
66579
|
-
|
|
66734
|
+
ownerUserId,
|
|
66580
66735
|
cleanTurns,
|
|
66581
66736
|
turnStartedAt,
|
|
66582
66737
|
assistantMsgId
|
|
@@ -67575,9 +67730,11 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
67575
67730
|
if (await cloudRlsInstalled(db) && await canManageRoles(db)) {
|
|
67576
67731
|
await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
|
|
67577
67732
|
await installCloudRls(db);
|
|
67733
|
+
await ownPolyfillsByGroup(db);
|
|
67578
67734
|
await installCloudSettings(db);
|
|
67579
67735
|
await db.ensureObservationSubstrate();
|
|
67580
67736
|
await enableChangelogRls(db);
|
|
67737
|
+
await enableChatPrivacyRls(db);
|
|
67581
67738
|
const access = await reconcileCloudMemberAccess(db);
|
|
67582
67739
|
convergeWarnings = access.skipped;
|
|
67583
67740
|
for (const s2 of convergeWarnings) {
|
|
@@ -67739,11 +67896,22 @@ function startBackgroundRender(active) {
|
|
|
67739
67896
|
active.eagerRenderWired = true;
|
|
67740
67897
|
let lastFire = 0;
|
|
67741
67898
|
let trailing;
|
|
67899
|
+
const pendingTables = /* @__PURE__ */ new Set();
|
|
67900
|
+
let pendingFull = false;
|
|
67742
67901
|
const fire = () => {
|
|
67743
67902
|
lastFire = Date.now();
|
|
67744
|
-
|
|
67903
|
+
if (pendingFull || pendingTables.size === 0) {
|
|
67904
|
+
pendingFull = false;
|
|
67905
|
+
pendingTables.clear();
|
|
67906
|
+
active.db.requestRender();
|
|
67907
|
+
return;
|
|
67908
|
+
}
|
|
67909
|
+
for (const t8 of pendingTables) active.db.requestRender(t8);
|
|
67910
|
+
pendingTables.clear();
|
|
67745
67911
|
};
|
|
67746
|
-
active.realtime.subscribePayload(() => {
|
|
67912
|
+
active.realtime.subscribePayload((payload) => {
|
|
67913
|
+
if (payload.table_name) pendingTables.add(payload.table_name);
|
|
67914
|
+
else pendingFull = true;
|
|
67747
67915
|
const since = Date.now() - lastFire;
|
|
67748
67916
|
if (since >= EAGER_RERENDER_MIN_INTERVAL_MS) {
|
|
67749
67917
|
fire();
|
package/package.json
CHANGED