latticesql 3.4.3 → 3.4.4
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 +794 -169
- package/dist/index.cjs +792 -168
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +793 -168
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -238,13 +238,14 @@ function readManifest(outputDir) {
|
|
|
238
238
|
function writeManifest(outputDir, manifest) {
|
|
239
239
|
atomicWrite(manifestPath(outputDir), JSON.stringify(manifest, null, 2));
|
|
240
240
|
}
|
|
241
|
-
var import_node_path2, import_node_fs2;
|
|
241
|
+
var import_node_path2, import_node_fs2, TEMPLATE_VERSION;
|
|
242
242
|
var init_manifest = __esm({
|
|
243
243
|
"src/lifecycle/manifest.ts"() {
|
|
244
244
|
"use strict";
|
|
245
245
|
import_node_path2 = require("path");
|
|
246
246
|
import_node_fs2 = require("fs");
|
|
247
247
|
init_writer();
|
|
248
|
+
TEMPLATE_VERSION = 1;
|
|
248
249
|
}
|
|
249
250
|
});
|
|
250
251
|
|
|
@@ -278,6 +279,126 @@ var init_adapter = __esm({
|
|
|
278
279
|
}
|
|
279
280
|
});
|
|
280
281
|
|
|
282
|
+
// src/lifecycle/render-cursor.ts
|
|
283
|
+
function markToString(v2) {
|
|
284
|
+
if (v2 == null) return null;
|
|
285
|
+
if (v2 instanceof Date) return v2.toISOString();
|
|
286
|
+
if (typeof v2 === "string") return v2;
|
|
287
|
+
if (typeof v2 === "number" || typeof v2 === "bigint" || typeof v2 === "boolean") return String(v2);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function padNumericMark(v2) {
|
|
291
|
+
const s2 = markToString(v2);
|
|
292
|
+
if (s2 == null) return null;
|
|
293
|
+
if (/^\d+$/.test(s2)) return s2.padStart(20, "0");
|
|
294
|
+
return s2;
|
|
295
|
+
}
|
|
296
|
+
async function changelogExists(adapter) {
|
|
297
|
+
if (adapter.dialect === "postgres") {
|
|
298
|
+
const row2 = await getAsyncOrSync(
|
|
299
|
+
adapter,
|
|
300
|
+
`SELECT to_regclass('__lattice_changelog') AS reg`
|
|
301
|
+
);
|
|
302
|
+
return !!row2 && row2.reg != null;
|
|
303
|
+
}
|
|
304
|
+
const row = await getAsyncOrSync(
|
|
305
|
+
adapter,
|
|
306
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='__lattice_changelog'`
|
|
307
|
+
);
|
|
308
|
+
return !!row;
|
|
309
|
+
}
|
|
310
|
+
async function changelogMark(adapter) {
|
|
311
|
+
try {
|
|
312
|
+
if (!await changelogExists(adapter)) return null;
|
|
313
|
+
const col = adapter.dialect === "postgres" ? "seq" : "rowid";
|
|
314
|
+
const row = await getAsyncOrSync(
|
|
315
|
+
adapter,
|
|
316
|
+
`SELECT MAX(${col}) AS m FROM __lattice_changelog`
|
|
317
|
+
);
|
|
318
|
+
return padNumericMark(row?.m);
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function sharingMarks(adapter) {
|
|
324
|
+
if (adapter.dialect !== "postgres") return { grants: null, owners: null };
|
|
325
|
+
try {
|
|
326
|
+
const reg = await getAsyncOrSync(
|
|
327
|
+
adapter,
|
|
328
|
+
`SELECT to_regclass('__lattice_changes') AS reg`
|
|
329
|
+
);
|
|
330
|
+
const hasFeed = !!reg && reg.reg != null;
|
|
331
|
+
if (hasFeed) {
|
|
332
|
+
const row = await getAsyncOrSync(
|
|
333
|
+
adapter,
|
|
334
|
+
`SELECT COUNT(*) AS n, MAX(seq) AS m FROM lattice_changes_since(0, 1000)`
|
|
335
|
+
);
|
|
336
|
+
const digest = digestOf(row?.n, row?.m);
|
|
337
|
+
return { grants: digest, owners: digest };
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
let owners = null;
|
|
342
|
+
let grants = null;
|
|
343
|
+
try {
|
|
344
|
+
const o3 = await getAsyncOrSync(
|
|
345
|
+
adapter,
|
|
346
|
+
`SELECT COUNT(*) AS n, MAX(updated_at) AS m FROM __lattice_owners`
|
|
347
|
+
);
|
|
348
|
+
owners = digestOf(o3?.n, o3?.m);
|
|
349
|
+
} catch {
|
|
350
|
+
owners = null;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const g6 = await getAsyncOrSync(
|
|
354
|
+
adapter,
|
|
355
|
+
`SELECT COUNT(*) AS n, MAX(granted_at) AS m FROM __lattice_row_grants`
|
|
356
|
+
);
|
|
357
|
+
grants = digestOf(g6?.n, g6?.m);
|
|
358
|
+
} catch {
|
|
359
|
+
grants = null;
|
|
360
|
+
}
|
|
361
|
+
return { grants, owners };
|
|
362
|
+
}
|
|
363
|
+
function digestOf(count, max) {
|
|
364
|
+
const n3 = padNumericMark(count);
|
|
365
|
+
if (n3 == null) return null;
|
|
366
|
+
const m4 = markToString(max) ?? "";
|
|
367
|
+
return `${n3}#${m4}`;
|
|
368
|
+
}
|
|
369
|
+
async function computeRenderCursor(adapter) {
|
|
370
|
+
try {
|
|
371
|
+
const [changelog, sharing] = await Promise.all([changelogMark(adapter), sharingMarks(adapter)]);
|
|
372
|
+
return { changelog, grants: sharing.grants, owners: sharing.owners };
|
|
373
|
+
} catch {
|
|
374
|
+
return { ...EMPTY_CURSOR };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function cursorIsFresh(recorded, live, templateVersion = TEMPLATE_VERSION) {
|
|
378
|
+
if (recorded == null) return false;
|
|
379
|
+
if (recorded.templateVersion !== templateVersion) return false;
|
|
380
|
+
const rc = recorded.cursor;
|
|
381
|
+
if (rc == null) return false;
|
|
382
|
+
if (!fieldFresh(rc.changelog, live.changelog, (r6, l4) => l4 <= r6)) return false;
|
|
383
|
+
if (!fieldFresh(rc.grants, live.grants, (r6, l4) => l4 === r6)) return false;
|
|
384
|
+
if (!fieldFresh(rc.owners, live.owners, (r6, l4) => l4 === r6)) return false;
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
function fieldFresh(recorded, live, ok) {
|
|
388
|
+
if (recorded == null && live == null) return true;
|
|
389
|
+
if (recorded == null || live == null) return false;
|
|
390
|
+
return ok(recorded, live);
|
|
391
|
+
}
|
|
392
|
+
var EMPTY_CURSOR;
|
|
393
|
+
var init_render_cursor = __esm({
|
|
394
|
+
"src/lifecycle/render-cursor.ts"() {
|
|
395
|
+
"use strict";
|
|
396
|
+
init_adapter();
|
|
397
|
+
init_manifest();
|
|
398
|
+
EMPTY_CURSOR = { changelog: null, grants: null, owners: null };
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
281
402
|
// src/db/sqlite.ts
|
|
282
403
|
var import_better_sqlite3, SQLiteAdapter;
|
|
283
404
|
var init_sqlite = __esm({
|
|
@@ -2320,7 +2441,18 @@ var init_concurrency = __esm({
|
|
|
2320
2441
|
});
|
|
2321
2442
|
|
|
2322
2443
|
// src/render/engine.ts
|
|
2323
|
-
|
|
2444
|
+
function entityContentChanged(fresh, prior) {
|
|
2445
|
+
const freshKeys = Object.keys(fresh);
|
|
2446
|
+
const priorKeys = Object.keys(prior);
|
|
2447
|
+
if (freshKeys.length !== priorKeys.length) return true;
|
|
2448
|
+
for (const k6 of freshKeys) {
|
|
2449
|
+
const p3 = prior[k6];
|
|
2450
|
+
if (p3 == null) return true;
|
|
2451
|
+
if (p3.hash === "" || p3.hash !== fresh[k6]?.hash) return true;
|
|
2452
|
+
}
|
|
2453
|
+
return false;
|
|
2454
|
+
}
|
|
2455
|
+
var import_node_path5, import_node_fs4, DeferredTableProgress, YIELD_EVERY_ENTITIES, RENDER_TABLE_CONCURRENCY, NOOP_RENDER, RenderEngine;
|
|
2324
2456
|
var init_engine = __esm({
|
|
2325
2457
|
"src/render/engine.ts"() {
|
|
2326
2458
|
"use strict";
|
|
@@ -2332,9 +2464,44 @@ var init_engine = __esm({
|
|
|
2332
2464
|
init_entity_query();
|
|
2333
2465
|
init_entity_templates();
|
|
2334
2466
|
init_manifest();
|
|
2467
|
+
init_render_cursor();
|
|
2335
2468
|
init_cleanup();
|
|
2336
2469
|
init_progress();
|
|
2337
2470
|
init_concurrency();
|
|
2471
|
+
DeferredTableProgress = class {
|
|
2472
|
+
constructor(throttle) {
|
|
2473
|
+
this.throttle = throttle;
|
|
2474
|
+
}
|
|
2475
|
+
changed = false;
|
|
2476
|
+
pendingStart = null;
|
|
2477
|
+
/** Buffer the `table-start` event; emitted only if/when the table changes. */
|
|
2478
|
+
start(event) {
|
|
2479
|
+
if (this.changed) {
|
|
2480
|
+
this.throttle.force(event);
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
this.pendingStart = event;
|
|
2484
|
+
}
|
|
2485
|
+
/** Mark that an entity's content changed — flush the held `table-start` once. */
|
|
2486
|
+
markChanged() {
|
|
2487
|
+
if (this.changed) return;
|
|
2488
|
+
this.changed = true;
|
|
2489
|
+
if (this.pendingStart) {
|
|
2490
|
+
this.throttle.force(this.pendingStart);
|
|
2491
|
+
this.pendingStart = null;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
/** Coalesced per-entity progress — dropped entirely until the table changed. */
|
|
2495
|
+
tick(event) {
|
|
2496
|
+
if (!this.changed) return;
|
|
2497
|
+
this.throttle.tick(event);
|
|
2498
|
+
}
|
|
2499
|
+
/** Lifecycle event (`table-done`) — emitted only if the table changed. */
|
|
2500
|
+
force(event) {
|
|
2501
|
+
if (!this.changed) return;
|
|
2502
|
+
this.throttle.force(event);
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2338
2505
|
YIELD_EVERY_ENTITIES = 200;
|
|
2339
2506
|
RENDER_TABLE_CONCURRENCY = 4;
|
|
2340
2507
|
NOOP_RENDER = () => "";
|
|
@@ -2451,20 +2618,23 @@ var init_engine = __esm({
|
|
|
2451
2618
|
}
|
|
2452
2619
|
const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
|
|
2453
2620
|
const filePath = (0, import_node_path5.join)(outputDir, def.outputFile);
|
|
2454
|
-
|
|
2621
|
+
const wrote = atomicWrite(filePath, content);
|
|
2622
|
+
if (wrote) {
|
|
2455
2623
|
filesWritten.push(filePath);
|
|
2456
2624
|
} else {
|
|
2457
2625
|
counters.skipped++;
|
|
2458
2626
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2627
|
+
if (wrote) {
|
|
2628
|
+
throttle.force({
|
|
2629
|
+
kind: "table-done",
|
|
2630
|
+
table: name,
|
|
2631
|
+
entitiesRendered: rows.length,
|
|
2632
|
+
entitiesTotal: rows.length,
|
|
2633
|
+
tableIndex: 0,
|
|
2634
|
+
tableCount: 0,
|
|
2635
|
+
pct: 100
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2468
2638
|
}
|
|
2469
2639
|
for (const [name, def] of this._schema.getMultis()) {
|
|
2470
2640
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
@@ -2478,32 +2648,38 @@ var init_engine = __esm({
|
|
|
2478
2648
|
tables[t8] = await this._schema.queryTable(this._adapter, t8, this._readRel);
|
|
2479
2649
|
}
|
|
2480
2650
|
}
|
|
2651
|
+
let wroteAny = false;
|
|
2481
2652
|
for (const key of keys) {
|
|
2482
2653
|
const content = def.render(key, tables);
|
|
2483
2654
|
const filePath = (0, import_node_path5.join)(outputDir, def.outputFile(key));
|
|
2484
2655
|
if (atomicWrite(filePath, content)) {
|
|
2485
2656
|
filesWritten.push(filePath);
|
|
2657
|
+
wroteAny = true;
|
|
2486
2658
|
} else {
|
|
2487
2659
|
counters.skipped++;
|
|
2488
2660
|
}
|
|
2489
2661
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2662
|
+
if (wroteAny) {
|
|
2663
|
+
throttle.force({
|
|
2664
|
+
kind: "table-done",
|
|
2665
|
+
table: name,
|
|
2666
|
+
entitiesRendered: keys.length,
|
|
2667
|
+
entitiesTotal: keys.length,
|
|
2668
|
+
tableIndex: 0,
|
|
2669
|
+
tableCount: 0,
|
|
2670
|
+
pct: 100
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2499
2673
|
}
|
|
2674
|
+
const priorManifest = readManifest(outputDir);
|
|
2500
2675
|
const entityContextManifest = await this._renderEntityContexts(
|
|
2501
2676
|
outputDir,
|
|
2502
2677
|
filesWritten,
|
|
2503
2678
|
counters,
|
|
2504
2679
|
throttle,
|
|
2505
2680
|
signal,
|
|
2506
|
-
opts.changedTables
|
|
2681
|
+
opts.changedTables,
|
|
2682
|
+
priorManifest
|
|
2507
2683
|
);
|
|
2508
2684
|
if (entityContextManifest === null) {
|
|
2509
2685
|
return this._abortedResult(filesWritten, counters, start);
|
|
@@ -2514,10 +2690,13 @@ var init_engine = __esm({
|
|
|
2514
2690
|
const prev = readManifest(outputDir);
|
|
2515
2691
|
entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
|
|
2516
2692
|
}
|
|
2693
|
+
const cursor = await computeRenderCursor(this._adapter);
|
|
2517
2694
|
writeManifest(outputDir, {
|
|
2518
2695
|
version: 2,
|
|
2519
2696
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2520
|
-
entityContexts
|
|
2697
|
+
entityContexts,
|
|
2698
|
+
templateVersion: TEMPLATE_VERSION,
|
|
2699
|
+
cursor
|
|
2521
2700
|
});
|
|
2522
2701
|
}
|
|
2523
2702
|
const result = {
|
|
@@ -2583,7 +2762,7 @@ var init_engine = __esm({
|
|
|
2583
2762
|
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
2584
2763
|
* via `signal`.
|
|
2585
2764
|
*/
|
|
2586
|
-
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
|
|
2765
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables, priorManifest) {
|
|
2587
2766
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
2588
2767
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
2589
2768
|
if (d6.protected) protectedTables.add(t8);
|
|
@@ -2602,8 +2781,10 @@ var init_engine = __esm({
|
|
|
2602
2781
|
const baseRows = await this._schema.queryTable(this._adapter, table, this._readRel);
|
|
2603
2782
|
const allRows = this._foldRows ? await this._foldRows(table, baseRows) : baseRows;
|
|
2604
2783
|
const directoryRoot = def.directoryRoot ?? table;
|
|
2784
|
+
const deferred = new DeferredTableProgress(throttle);
|
|
2785
|
+
const priorEntities = priorManifest?.entityContexts[table]?.entities ?? {};
|
|
2605
2786
|
const entitiesTotal = allRows.length;
|
|
2606
|
-
|
|
2787
|
+
deferred.start({
|
|
2607
2788
|
kind: "table-start",
|
|
2608
2789
|
table,
|
|
2609
2790
|
entitiesRendered: 0,
|
|
@@ -2612,6 +2793,7 @@ var init_engine = __esm({
|
|
|
2612
2793
|
tableCount,
|
|
2613
2794
|
pct: 0
|
|
2614
2795
|
});
|
|
2796
|
+
if (Object.keys(priorEntities).length !== entitiesTotal) deferred.markChanged();
|
|
2615
2797
|
const manifestEntry = {
|
|
2616
2798
|
directoryRoot,
|
|
2617
2799
|
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
@@ -2727,8 +2909,10 @@ var init_engine = __esm({
|
|
|
2727
2909
|
}
|
|
2728
2910
|
}
|
|
2729
2911
|
manifestEntry.entities[slug] = entityFileHashes;
|
|
2912
|
+
const priorHashes = normalizeEntityFiles(priorEntities[slug] ?? {});
|
|
2913
|
+
if (entityContentChanged(entityFileHashes, priorHashes)) deferred.markChanged();
|
|
2730
2914
|
const entitiesRendered = i6 + 1;
|
|
2731
|
-
|
|
2915
|
+
deferred.tick({
|
|
2732
2916
|
kind: "table-progress",
|
|
2733
2917
|
table,
|
|
2734
2918
|
entitiesRendered,
|
|
@@ -2738,7 +2922,7 @@ var init_engine = __esm({
|
|
|
2738
2922
|
pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
|
|
2739
2923
|
});
|
|
2740
2924
|
}
|
|
2741
|
-
|
|
2925
|
+
deferred.force({
|
|
2742
2926
|
kind: "table-done",
|
|
2743
2927
|
table,
|
|
2744
2928
|
entitiesRendered: entitiesTotal,
|
|
@@ -4161,6 +4345,22 @@ function deleteAssistantCredential(kind) {
|
|
|
4161
4345
|
void _removed;
|
|
4162
4346
|
saveAssistantCredentials(rest);
|
|
4163
4347
|
}
|
|
4348
|
+
function isAssistantCredentialCleared(kind) {
|
|
4349
|
+
return loadAssistantCredentials()[CLEARED_SENTINEL_PREFIX + kind] === "1";
|
|
4350
|
+
}
|
|
4351
|
+
function setAssistantCredentialCleared(kind) {
|
|
4352
|
+
const creds = loadAssistantCredentials();
|
|
4353
|
+
creds[CLEARED_SENTINEL_PREFIX + kind] = "1";
|
|
4354
|
+
saveAssistantCredentials(creds);
|
|
4355
|
+
}
|
|
4356
|
+
function clearAssistantCredentialCleared(kind) {
|
|
4357
|
+
const creds = loadAssistantCredentials();
|
|
4358
|
+
const sentinel = CLEARED_SENTINEL_PREFIX + kind;
|
|
4359
|
+
if (!(sentinel in creds)) return;
|
|
4360
|
+
const { [sentinel]: _removed, ...rest } = creds;
|
|
4361
|
+
void _removed;
|
|
4362
|
+
saveAssistantCredentials(rest);
|
|
4363
|
+
}
|
|
4164
4364
|
function ensureKeysDir() {
|
|
4165
4365
|
const dir = (0, import_node_path10.join)(ensureConfigDir(), KEYS_SUBDIR);
|
|
4166
4366
|
if (!(0, import_node_fs9.existsSync)(dir)) {
|
|
@@ -4205,7 +4405,7 @@ function deleteToken(label) {
|
|
|
4205
4405
|
const path2 = (0, import_node_path10.join)(ensureKeysDir(), label + TOKEN_EXT);
|
|
4206
4406
|
if ((0, import_node_fs9.existsSync)(path2)) (0, import_node_fs9.unlinkSync)(path2);
|
|
4207
4407
|
}
|
|
4208
|
-
var import_node_crypto5, import_node_fs9, import_node_os3, import_node_path10, import_yaml2, MASTER_KEY_FILENAME, IDENTITY_FILENAME, EMPTY_IDENTITY, PREFERENCES_FILENAME, DEFAULT_PREFERENCES, DB_CREDENTIALS_FILENAME, CRED_LOCK_FILENAME, LOCK_STALE_MS, LOCK_TIMEOUT_MS, lockDepthInProcess, S3_CONFIG_FILENAME, ASSISTANT_CREDENTIALS_FILENAME, KEYS_SUBDIR, TOKEN_EXT;
|
|
4408
|
+
var import_node_crypto5, import_node_fs9, import_node_os3, import_node_path10, import_yaml2, MASTER_KEY_FILENAME, IDENTITY_FILENAME, EMPTY_IDENTITY, PREFERENCES_FILENAME, DEFAULT_PREFERENCES, DB_CREDENTIALS_FILENAME, CRED_LOCK_FILENAME, LOCK_STALE_MS, LOCK_TIMEOUT_MS, lockDepthInProcess, S3_CONFIG_FILENAME, ASSISTANT_CREDENTIALS_FILENAME, CLEARED_SENTINEL_PREFIX, KEYS_SUBDIR, TOKEN_EXT;
|
|
4209
4409
|
var init_user_config = __esm({
|
|
4210
4410
|
"src/framework/user-config.ts"() {
|
|
4211
4411
|
"use strict";
|
|
@@ -4233,6 +4433,7 @@ var init_user_config = __esm({
|
|
|
4233
4433
|
lockDepthInProcess = 0;
|
|
4234
4434
|
S3_CONFIG_FILENAME = "s3-config.enc";
|
|
4235
4435
|
ASSISTANT_CREDENTIALS_FILENAME = "assistant-credentials.enc";
|
|
4436
|
+
CLEARED_SENTINEL_PREFIX = "__cleared__:";
|
|
4236
4437
|
KEYS_SUBDIR = "keys";
|
|
4237
4438
|
TOKEN_EXT = ".token";
|
|
4238
4439
|
}
|
|
@@ -5243,6 +5444,7 @@ var init_lattice = __esm({
|
|
|
5243
5444
|
init_shred();
|
|
5244
5445
|
init_encryption();
|
|
5245
5446
|
init_manifest();
|
|
5447
|
+
init_render_cursor();
|
|
5246
5448
|
import_node_fs12 = require("fs");
|
|
5247
5449
|
init_adapter();
|
|
5248
5450
|
init_sqlite();
|
|
@@ -5314,6 +5516,14 @@ var init_lattice = __esm({
|
|
|
5314
5516
|
_changelogTables = /* @__PURE__ */ new Set();
|
|
5315
5517
|
/** Current task context string for relevance filtering. */
|
|
5316
5518
|
_taskContext = "";
|
|
5519
|
+
/**
|
|
5520
|
+
* True when this connection opened against an already-provisioned cloud as a
|
|
5521
|
+
* SCOPED MEMBER (no role-management privilege → no CREATE/ALTER on the schema).
|
|
5522
|
+
* Set during init() by the same probe that decides introspect-only. Drives
|
|
5523
|
+
* {@link addColumn} to route DDL through the owner-side `lattice_member_add_column`
|
|
5524
|
+
* SECURITY DEFINER helper instead of issuing a raw ALTER the member can't run.
|
|
5525
|
+
*/
|
|
5526
|
+
_cloudMemberOpen = false;
|
|
5317
5527
|
_auditHandlers = [];
|
|
5318
5528
|
_renderHandlers = [];
|
|
5319
5529
|
_writebackHandlers = [];
|
|
@@ -5560,7 +5770,7 @@ var init_lattice = __esm({
|
|
|
5560
5770
|
/** Async tail of init(). See {@link init} for the sync-validation phase. */
|
|
5561
5771
|
async _initAsync(options) {
|
|
5562
5772
|
let introspectOnly = options.introspectOnly === true;
|
|
5563
|
-
if (
|
|
5773
|
+
if (this.getDialect() === "postgres") {
|
|
5564
5774
|
try {
|
|
5565
5775
|
const [marker, role] = await Promise.all([
|
|
5566
5776
|
getAsyncOrSync(this._adapter, `SELECT to_regclass('__lattice_owners') AS reg`),
|
|
@@ -5571,7 +5781,9 @@ var init_lattice = __esm({
|
|
|
5571
5781
|
]);
|
|
5572
5782
|
const provisioned = !!marker && marker.reg != null;
|
|
5573
5783
|
const canCreateRoles = !!role && role.rolcreaterole === true;
|
|
5574
|
-
|
|
5784
|
+
const memberOpen = provisioned && !canCreateRoles;
|
|
5785
|
+
introspectOnly = introspectOnly || memberOpen;
|
|
5786
|
+
this._cloudMemberOpen = memberOpen;
|
|
5575
5787
|
} catch {
|
|
5576
5788
|
}
|
|
5577
5789
|
}
|
|
@@ -5659,6 +5871,26 @@ var init_lattice = __esm({
|
|
|
5659
5871
|
getDialect() {
|
|
5660
5872
|
return this._adapter.dialect;
|
|
5661
5873
|
}
|
|
5874
|
+
/**
|
|
5875
|
+
* True when a table opts into the observation/changelog substrate
|
|
5876
|
+
* (`def.changelog`). Callers that want to bypass the high-level {@link delete}
|
|
5877
|
+
* with a transaction-scoped raw delete use this to know whether the table also
|
|
5878
|
+
* needs the changelog / write-hook / embedding side effects that only
|
|
5879
|
+
* `delete()` performs — so they can keep the high-level path for such tables.
|
|
5880
|
+
*/
|
|
5881
|
+
isChangelogTracked(table) {
|
|
5882
|
+
return this._changelogTables.has(table);
|
|
5883
|
+
}
|
|
5884
|
+
/**
|
|
5885
|
+
* True when this connection opened as a scoped cloud MEMBER (see
|
|
5886
|
+
* {@link _cloudMemberOpen}). Callers use it to route DDL-bearing work through
|
|
5887
|
+
* the owner-side SECURITY DEFINER helpers rather than issuing DDL the member's
|
|
5888
|
+
* role can't run (e.g. {@link addColumn} regenerates the masking view inside
|
|
5889
|
+
* `lattice_member_add_column`, so the caller must not also try to regenerate it).
|
|
5890
|
+
*/
|
|
5891
|
+
isCloudMemberOpen() {
|
|
5892
|
+
return this._cloudMemberOpen;
|
|
5893
|
+
}
|
|
5662
5894
|
/**
|
|
5663
5895
|
* Return the normalised primary-key column list for a registered
|
|
5664
5896
|
* table. Falls back to `['id']` for tables registered via raw DDL
|
|
@@ -5735,7 +5967,15 @@ var init_lattice = __esm({
|
|
|
5735
5967
|
assertSafeIdentifier(column, "column");
|
|
5736
5968
|
const existing = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5737
5969
|
if (!existing.includes(column)) {
|
|
5738
|
-
|
|
5970
|
+
if (this._cloudMemberOpen) {
|
|
5971
|
+
await runAsyncOrSync(this._adapter, `SELECT lattice_member_add_column(?, ?, ?)`, [
|
|
5972
|
+
table,
|
|
5973
|
+
column,
|
|
5974
|
+
typeSpec
|
|
5975
|
+
]);
|
|
5976
|
+
} else {
|
|
5977
|
+
await addColumnAsyncOrSync(this._adapter, table, column, typeSpec);
|
|
5978
|
+
}
|
|
5739
5979
|
}
|
|
5740
5980
|
const cols = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5741
5981
|
this._columnCache.set(table, new Set(cols));
|
|
@@ -6667,12 +6907,39 @@ var init_lattice = __esm({
|
|
|
6667
6907
|
async renderInBackground(outputDir, opts = {}) {
|
|
6668
6908
|
const notInit = this._notInitError();
|
|
6669
6909
|
if (notInit) return notInit;
|
|
6910
|
+
if (opts.gateOnOpen && !opts.changedTables) {
|
|
6911
|
+
const start = Date.now();
|
|
6912
|
+
const recorded = readManifest(outputDir);
|
|
6913
|
+
if (recorded != null) {
|
|
6914
|
+
const live = await computeRenderCursor(this._adapter);
|
|
6915
|
+
if (cursorIsFresh(recorded, live)) {
|
|
6916
|
+
opts.onProgress?.({
|
|
6917
|
+
kind: "done",
|
|
6918
|
+
table: null,
|
|
6919
|
+
entitiesRendered: 0,
|
|
6920
|
+
entitiesTotal: 0,
|
|
6921
|
+
tableIndex: 0,
|
|
6922
|
+
tableCount: 0,
|
|
6923
|
+
pct: 100,
|
|
6924
|
+
durationMs: Date.now() - start
|
|
6925
|
+
});
|
|
6926
|
+
const skipped = {
|
|
6927
|
+
filesWritten: [],
|
|
6928
|
+
filesSkipped: 0,
|
|
6929
|
+
durationMs: Date.now() - start
|
|
6930
|
+
};
|
|
6931
|
+
for (const h6 of this._renderHandlers) h6(skipped);
|
|
6932
|
+
return skipped;
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6670
6936
|
if (!opts.changedTables) {
|
|
6671
6937
|
this._pendingRenderAll = false;
|
|
6672
6938
|
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
6673
6939
|
this._autoRenderPending = false;
|
|
6674
6940
|
}
|
|
6675
|
-
|
|
6941
|
+
const { gateOnOpen: _gateOnOpen, ...engineOpts } = opts;
|
|
6942
|
+
return this._renderGuarded(outputDir, engineOpts);
|
|
6676
6943
|
}
|
|
6677
6944
|
/**
|
|
6678
6945
|
* Install a per-viewer read-relation resolver for ALL renders (initial,
|
|
@@ -47870,6 +48137,111 @@ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
|
47870
48137
|
AND g."pk" = ANY(p_pks)
|
|
47871
48138
|
AND o."owner_role" = session_user;
|
|
47872
48139
|
$fn$;
|
|
48140
|
+
|
|
48141
|
+
-- Add a column to a user table AS THE OWNER, on behalf of a scoped member. A
|
|
48142
|
+
-- member's role has no CREATE/ALTER on the schema (the bootstrap REVOKEs CREATE
|
|
48143
|
+
-- from PUBLIC), so a member's GUI "add a field" write (createRow/updateRow with a
|
|
48144
|
+
-- field the table lacks) cannot run ALTER TABLE itself. This SECURITY DEFINER
|
|
48145
|
+
-- helper performs that ALTER \u2014 and the masking-view regen \u2014 with the owner's
|
|
48146
|
+
-- rights, so member-added columns behave identically to owner-added ones.
|
|
48147
|
+
--
|
|
48148
|
+
-- Injection-safe + minimal: p_table must be an existing BASE table in the current
|
|
48149
|
+
-- schema (rejected otherwise); p_type is whitelisted against the exact set the
|
|
48150
|
+
-- library's addColumn emits for an auto-added column (TEXT / INTEGER / REAL, plus
|
|
48151
|
+
-- BOOLEAN) \u2014 never interpolated raw; both identifiers go through %I (quote_ident).
|
|
48152
|
+
-- Member-callable (granted EXECUTE to the member group), but it can only widen the
|
|
48153
|
+
-- schema, never read or alter another member's data.
|
|
48154
|
+
CREATE OR REPLACE FUNCTION lattice_member_add_column(p_table text, p_column text, p_type text)
|
|
48155
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
48156
|
+
DECLARE
|
|
48157
|
+
v_type text;
|
|
48158
|
+
v_view text := p_table || '_v';
|
|
48159
|
+
v_has_view boolean;
|
|
48160
|
+
v_pk_expr text;
|
|
48161
|
+
v_select text;
|
|
48162
|
+
BEGIN
|
|
48163
|
+
-- Never alter internal bookkeeping tables (names start with "_"). The GUI only
|
|
48164
|
+
-- ever calls this for a user entity table; rejecting the rest is defense-in-depth
|
|
48165
|
+
-- against a member invoking the function directly against ownership/audit/policy
|
|
48166
|
+
-- tables.
|
|
48167
|
+
IF left(p_table, 1) = '_' THEN
|
|
48168
|
+
RAISE EXCEPTION 'lattice: cannot add a column to internal table "%"', p_table;
|
|
48169
|
+
END IF;
|
|
48170
|
+
|
|
48171
|
+
-- p_table must be a real base table in THIS schema (search_path is pinned to the
|
|
48172
|
+
-- cloud schema by pinDefinerSearchPath, so to_regclass resolves there).
|
|
48173
|
+
IF NOT EXISTS (
|
|
48174
|
+
SELECT 1 FROM pg_class c
|
|
48175
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48176
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND c.relkind = 'r'
|
|
48177
|
+
) THEN
|
|
48178
|
+
RAISE EXCEPTION 'lattice: no such table "%"', p_table;
|
|
48179
|
+
END IF;
|
|
48180
|
+
|
|
48181
|
+
-- Whitelist the column type. These are exactly the specs addColumn's
|
|
48182
|
+
-- inferColumnType produces (TEXT / INTEGER / REAL); BOOLEAN is allowed too.
|
|
48183
|
+
-- Anything else is rejected \u2014 the type is spliced as %s (NOT %I), so it must be
|
|
48184
|
+
-- a known-safe literal and never caller-controlled SQL.
|
|
48185
|
+
v_type := upper(btrim(p_type));
|
|
48186
|
+
IF v_type NOT IN ('TEXT', 'INTEGER', 'REAL', 'BOOLEAN') THEN
|
|
48187
|
+
RAISE EXCEPTION 'lattice: unsupported column type "%"', p_type;
|
|
48188
|
+
END IF;
|
|
48189
|
+
|
|
48190
|
+
EXECUTE format('ALTER TABLE %I ADD COLUMN IF NOT EXISTS %I %s', p_table, p_column, v_type);
|
|
48191
|
+
|
|
48192
|
+
-- If the table is cell-masked (a "<table>_v" view exists, because some column has
|
|
48193
|
+
-- an audience), the view selects an explicit column list \u2014 so a new column is
|
|
48194
|
+
-- invisible to members until the view is regenerated. Rebuild it the same way the
|
|
48195
|
+
-- owner path (audienceViewSql / regenerateAudienceViewFromDb) does: pass every
|
|
48196
|
+
-- column through except those with an 'owner' audience in __lattice_column_policy
|
|
48197
|
+
-- (CASE WHEN lattice_is_owner(...) THEN col END), re-apply row visibility with
|
|
48198
|
+
-- WHERE lattice_row_visible(table, pk), and keep the member SELECT grant on the
|
|
48199
|
+
-- view. Unmasked tables need no regen \u2014 the member group's table-level base grant
|
|
48200
|
+
-- already covers the new column.
|
|
48201
|
+
SELECT EXISTS (
|
|
48202
|
+
SELECT 1 FROM pg_class c
|
|
48203
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48204
|
+
WHERE n.nspname = current_schema() AND c.relname = v_view AND c.relkind = 'v'
|
|
48205
|
+
) INTO v_has_view;
|
|
48206
|
+
|
|
48207
|
+
IF v_has_view THEN
|
|
48208
|
+
-- Canonical pk expression: CAST("col" AS TEXT) joined by TAB (chr(9)) \u2014 the
|
|
48209
|
+
-- same serialization the RLS policies + audienceViewSql use.
|
|
48210
|
+
SELECT string_agg(format('CAST(%I AS TEXT)', a.attname), ' || chr(9) || '
|
|
48211
|
+
ORDER BY array_position(i.indkey, a.attnum))
|
|
48212
|
+
INTO v_pk_expr
|
|
48213
|
+
FROM pg_index i
|
|
48214
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
48215
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48216
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
|
|
48217
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND i.indisprimary;
|
|
48218
|
+
IF v_pk_expr IS NULL THEN
|
|
48219
|
+
RAISE EXCEPTION 'lattice: cannot regenerate mask view for "%": no primary key', p_table;
|
|
48220
|
+
END IF;
|
|
48221
|
+
|
|
48222
|
+
-- Build the masked SELECT list in column order, applying the per-column policy.
|
|
48223
|
+
SELECT string_agg(
|
|
48224
|
+
CASE
|
|
48225
|
+
WHEN cp."audience" = 'owner'
|
|
48226
|
+
THEN format('CASE WHEN lattice_is_owner(%L, %s) THEN %I END AS %I',
|
|
48227
|
+
p_table, v_pk_expr, cols.column_name, cols.column_name)
|
|
48228
|
+
ELSE format('%I', cols.column_name)
|
|
48229
|
+
END,
|
|
48230
|
+
', ' ORDER BY cols.ordinal_position)
|
|
48231
|
+
INTO v_select
|
|
48232
|
+
FROM information_schema.columns cols
|
|
48233
|
+
LEFT JOIN "__lattice_column_policy" cp
|
|
48234
|
+
ON cp."table_name" = p_table AND cp."column_name" = cols.column_name
|
|
48235
|
+
AND cp."audience" NOT IN ('', 'everyone', 'row-audience')
|
|
48236
|
+
WHERE cols.table_schema = current_schema() AND cols.table_name = p_table;
|
|
48237
|
+
|
|
48238
|
+
EXECUTE format(
|
|
48239
|
+
'CREATE OR REPLACE VIEW %I AS SELECT %s FROM %I WHERE lattice_row_visible(%L, %s)',
|
|
48240
|
+
v_view, v_select, p_table, p_table, v_pk_expr);
|
|
48241
|
+
EXECUTE format('GRANT SELECT ON %I TO ${MEMBER_GROUP}', v_view);
|
|
48242
|
+
END IF;
|
|
48243
|
+
END $fn$;
|
|
48244
|
+
GRANT EXECUTE ON FUNCTION lattice_member_add_column(text, text, text) TO ${MEMBER_GROUP};
|
|
47873
48245
|
`;
|
|
47874
48246
|
}
|
|
47875
48247
|
});
|
|
@@ -47979,6 +48351,11 @@ async function revokeRow(db, table, pk, grantee) {
|
|
|
47979
48351
|
assertPg(db);
|
|
47980
48352
|
await runAsyncOrSync(db.adapter, `SELECT lattice_revoke_row(?, ?, ?)`, [table, pk, grantee]);
|
|
47981
48353
|
}
|
|
48354
|
+
async function batchRowGrants(db, table, pk, grant, revoke) {
|
|
48355
|
+
assertPg(db);
|
|
48356
|
+
for (const grantee of grant) await grantRow(db, table, pk, grantee);
|
|
48357
|
+
for (const grantee of revoke) await revokeRow(db, table, pk, grantee);
|
|
48358
|
+
}
|
|
47982
48359
|
async function revokeMemberRole(db, role) {
|
|
47983
48360
|
assertPg(db);
|
|
47984
48361
|
if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
|
|
@@ -49082,18 +49459,9 @@ function sessionUndoneFilters(undone, sessionId) {
|
|
|
49082
49459
|
if (sessionId) filters.push({ col: "session_id", op: "eq", val: sessionId });
|
|
49083
49460
|
return filters;
|
|
49084
49461
|
}
|
|
49085
|
-
|
|
49086
|
-
|
|
49087
|
-
filters: sessionUndoneFilters(1, sessionId)
|
|
49088
|
-
});
|
|
49089
|
-
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
49090
|
-
await db.insert("_lattice_gui_audit", {
|
|
49462
|
+
function buildAuditRow(table, rowId, op, before, after, sessionId, editTs) {
|
|
49463
|
+
return {
|
|
49091
49464
|
id: crypto.randomUUID(),
|
|
49092
|
-
// Set ts explicitly (don't rely on the column DEFAULT — it uses the
|
|
49093
|
-
// SQLite-only `strftime(...)`, which doesn't yield a parseable ISO string
|
|
49094
|
-
// on Postgres, so cloud history rendered "Invalid Date"). #4.6 — honor the
|
|
49095
|
-
// originating client's validated edit time when present (an offline edit
|
|
49096
|
-
// replayed later records when it was MADE, not when it synced), else now().
|
|
49097
49465
|
ts: sanitizeEditTs(editTs) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
49098
49466
|
table_name: table,
|
|
49099
49467
|
row_id: rowId,
|
|
@@ -49102,7 +49470,9 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
49102
49470
|
after_json: after ? JSON.stringify(after) : null,
|
|
49103
49471
|
undone: 0,
|
|
49104
49472
|
session_id: sessionId ?? null
|
|
49105
|
-
}
|
|
49473
|
+
};
|
|
49474
|
+
}
|
|
49475
|
+
function publishMutationFeed(feed, table, rowId, op, before, after, source) {
|
|
49106
49476
|
const labelRow = op === "delete" ? before : after;
|
|
49107
49477
|
feed.publish({
|
|
49108
49478
|
table,
|
|
@@ -49112,17 +49482,28 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
49112
49482
|
summary: feedSummary(op, table, labelRow)
|
|
49113
49483
|
});
|
|
49114
49484
|
}
|
|
49115
|
-
function
|
|
49116
|
-
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
49117
|
-
}
|
|
49118
|
-
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
49485
|
+
async function purgeRedoStack(db, sessionId) {
|
|
49119
49486
|
const undone = await db.query("_lattice_gui_audit", {
|
|
49120
49487
|
filters: sessionUndoneFilters(1, sessionId)
|
|
49121
49488
|
});
|
|
49122
49489
|
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
49490
|
+
}
|
|
49491
|
+
async function appendAudit(db, feed, table, rowId, op, before, after, source = "gui", sessionId, editTs) {
|
|
49492
|
+
await purgeRedoStack(db, sessionId);
|
|
49493
|
+
await db.insert(
|
|
49494
|
+
"_lattice_gui_audit",
|
|
49495
|
+
buildAuditRow(table, rowId, op, before, after, sessionId, editTs)
|
|
49496
|
+
);
|
|
49497
|
+
publishMutationFeed(feed, table, rowId, op, before, after, source);
|
|
49498
|
+
}
|
|
49499
|
+
function isSchemaOp(operation2) {
|
|
49500
|
+
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
49501
|
+
}
|
|
49502
|
+
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
49503
|
+
await purgeRedoStack(db, sessionId);
|
|
49123
49504
|
await db.insert("_lattice_gui_audit", {
|
|
49124
49505
|
id: crypto.randomUUID(),
|
|
49125
|
-
// Explicit ISO ts — see
|
|
49506
|
+
// Explicit ISO ts — see buildAuditRow (the SQLite-only strftime DEFAULT
|
|
49126
49507
|
// rendered "Invalid Date" on the Postgres/cloud path).
|
|
49127
49508
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49128
49509
|
table_name: table,
|
|
@@ -49157,7 +49538,7 @@ async function ensureColumns(db, table, values) {
|
|
|
49157
49538
|
const added = Object.keys(values).filter((k6) => !(k6 in existing));
|
|
49158
49539
|
if (added.length === 0) return [];
|
|
49159
49540
|
for (const col of added) await db.addColumn(table, col, inferColumnType(values[col]));
|
|
49160
|
-
if (db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
49541
|
+
if (!db.isCloudMemberOpen() && db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
49161
49542
|
const cols = db.getRegisteredColumns(table);
|
|
49162
49543
|
const pk = db.getPrimaryKey(table);
|
|
49163
49544
|
if (cols && pk.length > 0) await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
@@ -49279,7 +49660,14 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
49279
49660
|
ctx.clientTs
|
|
49280
49661
|
);
|
|
49281
49662
|
} else {
|
|
49282
|
-
await ctx
|
|
49663
|
+
await hardDelete(ctx, table, id, before);
|
|
49664
|
+
}
|
|
49665
|
+
}
|
|
49666
|
+
async function hardDelete(ctx, table, id, before) {
|
|
49667
|
+
const withClient = ctx.db.adapter.withClient?.bind(ctx.db.adapter);
|
|
49668
|
+
const pkCols = ctx.db.getPrimaryKey(table);
|
|
49669
|
+
const pkCol = pkCols.length === 1 ? pkCols[0] : void 0;
|
|
49670
|
+
if (!withClient || ctx.db.isChangelogTracked(table) || pkCol === void 0) {
|
|
49283
49671
|
await appendAudit(
|
|
49284
49672
|
ctx.db,
|
|
49285
49673
|
ctx.feed,
|
|
@@ -49292,10 +49680,30 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
49292
49680
|
ctx.sessionId,
|
|
49293
49681
|
ctx.clientTs
|
|
49294
49682
|
);
|
|
49683
|
+
await ctx.db.delete(table, id);
|
|
49684
|
+
return;
|
|
49295
49685
|
}
|
|
49686
|
+
const auditRow = buildAuditRow(table, id, "delete", before, null, ctx.sessionId, ctx.clientTs);
|
|
49687
|
+
await purgeRedoStack(ctx.db, ctx.sessionId);
|
|
49688
|
+
const auditCols = AUDIT_COLUMNS.map((c6) => `"${c6}"`).join(", ");
|
|
49689
|
+
const auditPlaceholders = AUDIT_COLUMNS.map(() => "?").join(", ");
|
|
49690
|
+
const auditValues = AUDIT_COLUMNS.map((c6) => auditRow[c6]);
|
|
49691
|
+
const pkColQuoted = pkCol.replace(/"/g, '""');
|
|
49692
|
+
await withClient(async (tx) => {
|
|
49693
|
+
await tx.run(
|
|
49694
|
+
`INSERT INTO "_lattice_gui_audit" (${auditCols}) VALUES (${auditPlaceholders})`,
|
|
49695
|
+
auditValues
|
|
49696
|
+
);
|
|
49697
|
+
await tx.run(`DELETE FROM "${table.replace(/"/g, '""')}" WHERE "${pkColQuoted}" = ?`, [id]);
|
|
49698
|
+
});
|
|
49699
|
+
publishMutationFeed(ctx.feed, table, id, "delete", before, null, ctx.source);
|
|
49296
49700
|
}
|
|
49297
|
-
async function linkRows(ctx, table, body) {
|
|
49298
|
-
|
|
49701
|
+
async function linkRows(ctx, table, body, forceVisibility) {
|
|
49702
|
+
if (forceVisibility !== void 0) {
|
|
49703
|
+
await ctx.db.insertForcingVisibility(table, body, forceVisibility);
|
|
49704
|
+
} else {
|
|
49705
|
+
await ctx.db.link(table, body);
|
|
49706
|
+
}
|
|
49299
49707
|
await appendAudit(ctx.db, ctx.feed, table, null, "link", null, body, ctx.source, ctx.sessionId);
|
|
49300
49708
|
}
|
|
49301
49709
|
async function unlinkRows(ctx, table, body) {
|
|
@@ -49433,13 +49841,24 @@ async function revertEntry(ctx, id) {
|
|
|
49433
49841
|
});
|
|
49434
49842
|
return { ok: true, entry };
|
|
49435
49843
|
}
|
|
49436
|
-
var import_node_crypto15, SCHEMA_OP_PREFIX;
|
|
49844
|
+
var import_node_crypto15, AUDIT_COLUMNS, SCHEMA_OP_PREFIX;
|
|
49437
49845
|
var init_mutations = __esm({
|
|
49438
49846
|
"src/gui/mutations.ts"() {
|
|
49439
49847
|
"use strict";
|
|
49440
49848
|
import_node_crypto15 = require("crypto");
|
|
49441
49849
|
init_cloud_connect();
|
|
49442
49850
|
init_audience();
|
|
49851
|
+
AUDIT_COLUMNS = [
|
|
49852
|
+
"id",
|
|
49853
|
+
"ts",
|
|
49854
|
+
"table_name",
|
|
49855
|
+
"row_id",
|
|
49856
|
+
"operation",
|
|
49857
|
+
"before_json",
|
|
49858
|
+
"after_json",
|
|
49859
|
+
"undone",
|
|
49860
|
+
"session_id"
|
|
49861
|
+
];
|
|
49443
49862
|
SCHEMA_OP_PREFIX = "schema.";
|
|
49444
49863
|
}
|
|
49445
49864
|
});
|
|
@@ -49727,6 +50146,10 @@ async function readMachineCredential(db, kind) {
|
|
|
49727
50146
|
}
|
|
49728
50147
|
return null;
|
|
49729
50148
|
}
|
|
50149
|
+
async function resolveAnthropicKey(db) {
|
|
50150
|
+
if (isAssistantCredentialCleared(CREDENTIALS.anthropic.kind)) return null;
|
|
50151
|
+
return await readMachineCredential(db, CREDENTIALS.anthropic.kind) ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
50152
|
+
}
|
|
49730
50153
|
function getAggressiveness() {
|
|
49731
50154
|
const n3 = readPreferences().aggressiveness;
|
|
49732
50155
|
if (!Number.isFinite(n3)) return DEFAULT_AGGRESSIVENESS;
|
|
@@ -49757,6 +50180,7 @@ async function getVoiceCredential(db) {
|
|
|
49757
50180
|
return null;
|
|
49758
50181
|
}
|
|
49759
50182
|
async function hasCredential(db, name, envVar) {
|
|
50183
|
+
if (isAssistantCredentialCleared(CREDENTIALS[name].kind)) return false;
|
|
49760
50184
|
return Boolean(await readMachineCredential(db, CREDENTIALS[name].kind)) || Boolean(process.env[envVar]);
|
|
49761
50185
|
}
|
|
49762
50186
|
async function resolveClaudeAuth(db) {
|
|
@@ -49779,7 +50203,7 @@ async function resolveClaudeAuth(db) {
|
|
|
49779
50203
|
} catch {
|
|
49780
50204
|
}
|
|
49781
50205
|
}
|
|
49782
|
-
const apiKey = await
|
|
50206
|
+
const apiKey = await resolveAnthropicKey(db);
|
|
49783
50207
|
return apiKey ? { apiKey } : null;
|
|
49784
50208
|
}
|
|
49785
50209
|
async function hasClaudeAuth(db) {
|
|
@@ -49876,6 +50300,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
49876
50300
|
}
|
|
49877
50301
|
const cred = CREDENTIALS[name];
|
|
49878
50302
|
setAssistantCredential(cred.kind, key);
|
|
50303
|
+
clearAssistantCredentialCleared(cred.kind);
|
|
49879
50304
|
if (db) {
|
|
49880
50305
|
for (const row of await liveSecretsOfKind(db, cred.kind)) {
|
|
49881
50306
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -49892,6 +50317,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
49892
50317
|
return true;
|
|
49893
50318
|
}
|
|
49894
50319
|
deleteAssistantCredential(CREDENTIALS[name].kind);
|
|
50320
|
+
setAssistantCredentialCleared(CREDENTIALS[name].kind);
|
|
49895
50321
|
if (db) {
|
|
49896
50322
|
for (const row of await liveSecretsOfKind(db, CREDENTIALS[name].kind)) {
|
|
49897
50323
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -52082,7 +52508,7 @@ function buildSchema(db) {
|
|
|
52082
52508
|
}
|
|
52083
52509
|
return out;
|
|
52084
52510
|
}
|
|
52085
|
-
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false) {
|
|
52511
|
+
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false, privateMode = false) {
|
|
52086
52512
|
if (!text.trim()) return [];
|
|
52087
52513
|
const auth = await resolveClaudeAuth(db);
|
|
52088
52514
|
if (!auth) {
|
|
@@ -52104,6 +52530,7 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52104
52530
|
});
|
|
52105
52531
|
return [];
|
|
52106
52532
|
}
|
|
52533
|
+
const forceVis = privateMode ? "private" : void 0;
|
|
52107
52534
|
const temperature = aggressivenessToTemperature(aggressiveness);
|
|
52108
52535
|
let description = "";
|
|
52109
52536
|
try {
|
|
@@ -52146,11 +52573,16 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52146
52573
|
}
|
|
52147
52574
|
if (jx) {
|
|
52148
52575
|
try {
|
|
52149
|
-
await linkRows(
|
|
52150
|
-
|
|
52151
|
-
|
|
52152
|
-
|
|
52153
|
-
|
|
52576
|
+
await linkRows(
|
|
52577
|
+
mctx,
|
|
52578
|
+
jx.junction,
|
|
52579
|
+
{
|
|
52580
|
+
id: crypto.randomUUID(),
|
|
52581
|
+
[jx.fileFk]: fileId,
|
|
52582
|
+
[jx.otherFk]: m4.id
|
|
52583
|
+
},
|
|
52584
|
+
forceVis
|
|
52585
|
+
);
|
|
52154
52586
|
linkedCount++;
|
|
52155
52587
|
if (created) {
|
|
52156
52588
|
mctx.feed.publish({
|
|
@@ -52209,16 +52641,21 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52209
52641
|
if ("name" in cols && row.name == null) row.name = obj2.label;
|
|
52210
52642
|
if ("title" in cols && row.title == null) row.title = obj2.label;
|
|
52211
52643
|
try {
|
|
52212
|
-
const { id: rowId } = await createRow(mctx, entity, row);
|
|
52644
|
+
const { id: rowId } = await createRow(mctx, entity, row, forceVis);
|
|
52213
52645
|
createdCount++;
|
|
52214
52646
|
const ent = entity;
|
|
52215
52647
|
const jx = junctions.find((j6) => j6.otherTable === ent) ?? (createJunction ? await createJunction(ent) : null);
|
|
52216
52648
|
if (jx) {
|
|
52217
|
-
await linkRows(
|
|
52218
|
-
|
|
52219
|
-
|
|
52220
|
-
|
|
52221
|
-
|
|
52649
|
+
await linkRows(
|
|
52650
|
+
mctx,
|
|
52651
|
+
jx.junction,
|
|
52652
|
+
{
|
|
52653
|
+
id: crypto.randomUUID(),
|
|
52654
|
+
[jx.fileFk]: fileId,
|
|
52655
|
+
[jx.otherFk]: rowId
|
|
52656
|
+
},
|
|
52657
|
+
forceVis
|
|
52658
|
+
);
|
|
52222
52659
|
}
|
|
52223
52660
|
} catch (e6) {
|
|
52224
52661
|
console.warn(`[ingest] create ${entity} from document failed:`, e6.message);
|
|
@@ -52232,12 +52669,17 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52232
52669
|
try {
|
|
52233
52670
|
const title = name.replace(/\.[^./\\]+$/, "").trim() || "Note";
|
|
52234
52671
|
const body = description.length > 0 ? description : text.slice(0, 2e3);
|
|
52235
|
-
const { id: noteId } = await createRow(
|
|
52236
|
-
|
|
52237
|
-
|
|
52238
|
-
|
|
52239
|
-
|
|
52240
|
-
|
|
52672
|
+
const { id: noteId } = await createRow(
|
|
52673
|
+
mctx,
|
|
52674
|
+
"notes",
|
|
52675
|
+
{
|
|
52676
|
+
id: crypto.randomUUID(),
|
|
52677
|
+
title,
|
|
52678
|
+
body,
|
|
52679
|
+
source_file_id: fileId
|
|
52680
|
+
},
|
|
52681
|
+
forceVis
|
|
52682
|
+
);
|
|
52241
52683
|
mctx.feed.publish({
|
|
52242
52684
|
table: "notes",
|
|
52243
52685
|
op: "insert",
|
|
@@ -52351,7 +52793,8 @@ async function ingestUrlAsFile(ctx, rawUrl, opts = {}) {
|
|
|
52351
52793
|
ctx.enrich.createJunction,
|
|
52352
52794
|
ctx.enrich.aggressiveness,
|
|
52353
52795
|
ctx.enrich.createEntity,
|
|
52354
|
-
true
|
|
52796
|
+
true,
|
|
52797
|
+
ctx.privateMode === true
|
|
52355
52798
|
);
|
|
52356
52799
|
}
|
|
52357
52800
|
return {
|
|
@@ -53229,13 +53672,22 @@ function loadSdk() {
|
|
|
53229
53672
|
throw new Error("Could not resolve the Anthropic constructor from '@anthropic-ai/sdk'");
|
|
53230
53673
|
return ctor;
|
|
53231
53674
|
}
|
|
53232
|
-
function
|
|
53233
|
-
const Anthropic = loadSdk();
|
|
53675
|
+
function buildAnthropicConfig(auth) {
|
|
53234
53676
|
const config = {};
|
|
53235
|
-
if (auth.authToken)
|
|
53236
|
-
|
|
53677
|
+
if (auth.authToken) {
|
|
53678
|
+
config.authToken = auth.authToken;
|
|
53679
|
+
config.apiKey = null;
|
|
53680
|
+
} else if (auth.apiKey) {
|
|
53681
|
+
config.apiKey = auth.apiKey;
|
|
53682
|
+
} else {
|
|
53683
|
+
config.apiKey = null;
|
|
53684
|
+
}
|
|
53237
53685
|
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
53238
|
-
|
|
53686
|
+
return config;
|
|
53687
|
+
}
|
|
53688
|
+
function createAnthropicClient(auth) {
|
|
53689
|
+
const Anthropic = loadSdk();
|
|
53690
|
+
const sdk = new Anthropic(buildAnthropicConfig(auth));
|
|
53239
53691
|
return {
|
|
53240
53692
|
async runTurn(params) {
|
|
53241
53693
|
const stream = sdk.messages.stream({
|
|
@@ -54670,8 +55122,14 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
54670
55122
|
},
|
|
54671
55123
|
{
|
|
54672
55124
|
name: "_lattice_gui_audit",
|
|
54673
|
-
|
|
54674
|
-
|
|
55125
|
+
// UPDATE + DELETE are needed by undo/redo/revert (flips an entry's `undone`)
|
|
55126
|
+
// and the redo-stack purge on a new mutation (deletes the session's undone
|
|
55127
|
+
// entries). Safe because enableGuiAuditRls installs per-op UPDATE and DELETE
|
|
55128
|
+
// policies whose USING is `row_id IS NULL OR lattice_row_visible(table_name,
|
|
55129
|
+
// row_id)` — so a member can only update/delete audit rows for entities it can
|
|
55130
|
+
// already see (or schema-level entries that carry no row data).
|
|
55131
|
+
privs: "SELECT, INSERT, UPDATE, DELETE",
|
|
55132
|
+
why: "GUI undo/redo/revert + redo-stack purge + version history; RLS (enableGuiAuditRls) scopes every op to entries whose underlying row the member can see"
|
|
54675
55133
|
},
|
|
54676
55134
|
{
|
|
54677
55135
|
name: "__lattice_user_identity",
|
|
@@ -55073,6 +55531,19 @@ async function normalizeImage(path2, maxBytes) {
|
|
|
55073
55531
|
function renderJpeg(sharp, path2, quality) {
|
|
55074
55532
|
return sharp(path2).rotate().resize({ width: MAX_DIM, height: MAX_DIM, fit: "inside", withoutEnlargement: true }).jpeg({ quality }).toBuffer();
|
|
55075
55533
|
}
|
|
55534
|
+
function buildVisionAnthropicConfig(auth) {
|
|
55535
|
+
const config = {};
|
|
55536
|
+
if (auth.authToken) {
|
|
55537
|
+
config.authToken = auth.authToken;
|
|
55538
|
+
config.apiKey = null;
|
|
55539
|
+
} else if (auth.apiKey) {
|
|
55540
|
+
config.apiKey = auth.apiKey;
|
|
55541
|
+
} else {
|
|
55542
|
+
config.apiKey = null;
|
|
55543
|
+
}
|
|
55544
|
+
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
55545
|
+
return config;
|
|
55546
|
+
}
|
|
55076
55547
|
function defaultSender(auth) {
|
|
55077
55548
|
return async (input) => {
|
|
55078
55549
|
const importMetaUrl = import_meta3.url;
|
|
@@ -55080,11 +55551,7 @@ function defaultSender(auth) {
|
|
|
55080
55551
|
const sdk = req("@anthropic-ai/sdk");
|
|
55081
55552
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
55082
55553
|
if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
|
|
55083
|
-
const
|
|
55084
|
-
if (auth.authToken) config.authToken = auth.authToken;
|
|
55085
|
-
else if (auth.apiKey) config.apiKey = auth.apiKey;
|
|
55086
|
-
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
55087
|
-
const client = new Anthropic(config);
|
|
55554
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
55088
55555
|
const res = await client.messages.create({
|
|
55089
55556
|
model: input.model,
|
|
55090
55557
|
max_tokens: 1024,
|
|
@@ -55111,11 +55578,7 @@ function defaultPdfSender(auth) {
|
|
|
55111
55578
|
const sdk = req("@anthropic-ai/sdk");
|
|
55112
55579
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
55113
55580
|
if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
|
|
55114
|
-
const
|
|
55115
|
-
if (auth.authToken) config.authToken = auth.authToken;
|
|
55116
|
-
else if (auth.apiKey) config.apiKey = auth.apiKey;
|
|
55117
|
-
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
55118
|
-
const client = new Anthropic(config);
|
|
55581
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
55119
55582
|
const res = await client.messages.create({
|
|
55120
55583
|
model: input.model,
|
|
55121
55584
|
max_tokens: 4096,
|
|
@@ -56055,6 +56518,8 @@ var css = `
|
|
|
56055
56518
|
.grants-panel .grants-title { font-weight: 600; margin-bottom: 6px; }
|
|
56056
56519
|
.grants-panel .grants-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
|
|
56057
56520
|
.grants-panel .grants-row input { accent-color: var(--accent); }
|
|
56521
|
+
.grants-panel .grants-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
|
|
56522
|
+
.grants-panel .grants-dirty { font-size: 12px; }
|
|
56058
56523
|
|
|
56059
56524
|
/* Inline create-row at the bottom of every table */
|
|
56060
56525
|
tr.create-row td { background: var(--surface-2); }
|
|
@@ -58521,6 +58986,15 @@ var appJs = `
|
|
|
58521
58986
|
// Per-table view state: 'live' (default) or 'trash' (soft-deleted rows).
|
|
58522
58987
|
var tableViewMode = {};
|
|
58523
58988
|
|
|
58989
|
+
// The (table, pk) of the per-row "Manage access" grants panel that is
|
|
58990
|
+
// currently open, or null when none is. A soft re-render (a concurrent edit
|
|
58991
|
+
// by another client fires pg_notify \u2192 realtime refresh \u2192 renderRoute({soft})
|
|
58992
|
+
// \u2192 renderDetail/renderFsItem repaint) would otherwise re-create the detail
|
|
58993
|
+
// view with the panel collapsed, dropping a staged multi-select mid-edit.
|
|
58994
|
+
// wireRowSharing reads this after each repaint and re-opens + re-populates the
|
|
58995
|
+
// panel WITHOUT any network call, so the staged selection survives.
|
|
58996
|
+
var openGrantsPanel = null;
|
|
58997
|
+
|
|
58524
58998
|
function renderTable(content, tableName) {
|
|
58525
58999
|
var myGen = renderGen;
|
|
58526
59000
|
clearUnseen(tableName);
|
|
@@ -58999,70 +59473,151 @@ var appJs = `
|
|
|
58999
59473
|
}).catch(function (e) { showToast('Visibility update failed: ' + e.message, {}); });
|
|
59000
59474
|
});
|
|
59001
59475
|
});
|
|
59002
|
-
var
|
|
59003
|
-
|
|
59476
|
+
var access = row._access || {};
|
|
59477
|
+
|
|
59478
|
+
// Render the staged member checklist + a single "Save sharing" / "Cancel"
|
|
59479
|
+
// into the panel. Checkbox toggles mutate ONLY the local desired map \u2014
|
|
59480
|
+
// NO network call per toggle (the old design auto-saved live, one POST per
|
|
59481
|
+
// checkbox, and each grant's pg_notify collapsed the panel). A single batch
|
|
59482
|
+
// request fires on Save. members is the already-fetched list; desired
|
|
59483
|
+
// seeds from the row's current grantees (or a caller-supplied staged map
|
|
59484
|
+
// when re-opening after a soft re-render).
|
|
59485
|
+
function populateGrantsPanel(panel, members, desired) {
|
|
59486
|
+
// Snapshot the CURRENT (committed) grantees so Save can diff desired-vs-
|
|
59487
|
+
// current into adds/removes. effectiveVisibility decides whether we're
|
|
59488
|
+
// actually switching INTO specific-people mode (custom-0 reads as private).
|
|
59489
|
+
var current = {};
|
|
59490
|
+
(access.grantees || []).forEach(function (g) { current[g] = true; });
|
|
59491
|
+
if (members.length === 0) {
|
|
59492
|
+
panel.innerHTML = '<div class="muted">No other members in this workspace yet.</div>';
|
|
59493
|
+
panel.hidden = false;
|
|
59494
|
+
return;
|
|
59495
|
+
}
|
|
59496
|
+
function dirtyCount() {
|
|
59497
|
+
var n = 0;
|
|
59498
|
+
members.forEach(function (m) {
|
|
59499
|
+
if (!!desired[m.role] !== !!current[m.role]) n++;
|
|
59500
|
+
});
|
|
59501
|
+
return n;
|
|
59502
|
+
}
|
|
59503
|
+
function render() {
|
|
59504
|
+
var changed = dirtyCount();
|
|
59505
|
+
panel.innerHTML = '<div class="grants-title">Who can see this</div>' +
|
|
59506
|
+
members.map(function (m) {
|
|
59507
|
+
var label = m.name || m.email || m.role;
|
|
59508
|
+
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
59509
|
+
(desired[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
59510
|
+
}).join('') +
|
|
59511
|
+
'<div class="grants-actions">' +
|
|
59512
|
+
'<button class="btn primary" id="grants-save"' + (changed ? '' : ' disabled') + '>Save sharing</button>' +
|
|
59513
|
+
'<button class="btn" id="grants-cancel">Cancel</button>' +
|
|
59514
|
+
'<span class="grants-dirty muted">' + (changed ? (changed === 1 ? '1 change' : changed + ' changes') : 'No changes') + '</span>' +
|
|
59515
|
+
'</div>';
|
|
59516
|
+
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
59517
|
+
cb.addEventListener('change', function () {
|
|
59518
|
+
var role = cb.getAttribute('data-grant-role');
|
|
59519
|
+
if (cb.checked) desired[role] = true; else delete desired[role];
|
|
59520
|
+
render(); // re-render to refresh the dirty indicator + Save state
|
|
59521
|
+
});
|
|
59522
|
+
});
|
|
59523
|
+
var cancelBtn = panel.querySelector('#grants-cancel');
|
|
59524
|
+
if (cancelBtn) cancelBtn.addEventListener('click', function () { closeGrantsPanel(panel); });
|
|
59525
|
+
var saveBtn = panel.querySelector('#grants-save');
|
|
59526
|
+
if (saveBtn) saveBtn.addEventListener('click', function () {
|
|
59527
|
+
var toAdd = [];
|
|
59528
|
+
var toRemove = [];
|
|
59529
|
+
members.forEach(function (m) {
|
|
59530
|
+
var want = !!desired[m.role];
|
|
59531
|
+
var have = !!current[m.role];
|
|
59532
|
+
if (want && !have) toAdd.push(m.role);
|
|
59533
|
+
if (!want && have) toRemove.push(m.role);
|
|
59534
|
+
});
|
|
59535
|
+
if (toAdd.length === 0 && toRemove.length === 0) { closeGrantsPanel(panel); return; }
|
|
59536
|
+
// Confirm the mode change ONCE, here \u2014 only when actually switching
|
|
59537
|
+
// INTO specific-people mode (effective vis isn't already custom AND we
|
|
59538
|
+
// are adding at least one grantee). Never per checkbox.
|
|
59539
|
+
if (effectiveVisibility(access) !== 'custom' && toAdd.length > 0) {
|
|
59540
|
+
if (!confirm('Sharing this with specific people switches it off "everyone"/"private". The chosen people will be able to see it. Continue?')) return;
|
|
59541
|
+
}
|
|
59542
|
+
withBusy(saveBtn, function () {
|
|
59543
|
+
return fetchJson('/api/cloud/row-grants', {
|
|
59544
|
+
method: 'POST',
|
|
59545
|
+
headers: { 'content-type': 'application/json' },
|
|
59546
|
+
body: JSON.stringify({ table: tableName, pk: id, grant: toAdd, revoke: toRemove }),
|
|
59547
|
+
}).then(function () {
|
|
59548
|
+
// Mirror the committed state locally so the re-render's indicator
|
|
59549
|
+
// is correct. The first grant flips the row to custom server-side;
|
|
59550
|
+
// revoking the last leaves custom-0, which effectiveVisibility
|
|
59551
|
+
// renders as private.
|
|
59552
|
+
var list = [];
|
|
59553
|
+
members.forEach(function (m) { if (desired[m.role]) list.push(m.role); });
|
|
59554
|
+
access.grantees = list;
|
|
59555
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
59556
|
+
openGrantsPanel = null; // a successful save closes the staging session
|
|
59557
|
+
invalidate(tableName);
|
|
59558
|
+
showToast('Sharing updated', {});
|
|
59559
|
+
reRender();
|
|
59560
|
+
}).catch(function (e) {
|
|
59561
|
+
// Surface loudly + leave the staged selection intact so the user
|
|
59562
|
+
// can retry; no silent partial-success.
|
|
59563
|
+
showToast('Sharing update failed: ' + e.message, {});
|
|
59564
|
+
});
|
|
59565
|
+
});
|
|
59566
|
+
});
|
|
59567
|
+
panel.hidden = false;
|
|
59568
|
+
}
|
|
59569
|
+
render();
|
|
59570
|
+
}
|
|
59571
|
+
|
|
59572
|
+
function closeGrantsPanel(panel) {
|
|
59573
|
+
if (panel) panel.hidden = true;
|
|
59574
|
+
openGrantsPanel = null;
|
|
59575
|
+
}
|
|
59576
|
+
|
|
59577
|
+
// Open (or toggle shut) the manage-access panel. Fetches the member list,
|
|
59578
|
+
// then stages from the row's current grantees. Opening must NOT pre-flip
|
|
59579
|
+
// the row to 'custom' \u2014 that left a never-shared row stuck at "custom (0)".
|
|
59580
|
+
function openManagePanel(triggerBtn) {
|
|
59004
59581
|
var panel = content.querySelector('#grants-panel');
|
|
59005
59582
|
if (!panel) return;
|
|
59006
|
-
if (!panel.hidden) { panel
|
|
59007
|
-
|
|
59008
|
-
|
|
59009
|
-
// row the user never actually shared stuck at "custom (0)". The first
|
|
59010
|
-
// grant flips it to custom server-side (lattice_grant_row); revoking the
|
|
59011
|
-
// last leaves it custom-with-0-grantees, which now reads as private. So
|
|
59012
|
-
// just load the member checklist.
|
|
59013
|
-
var ensure = Promise.resolve();
|
|
59014
|
-
withBusy(detailVisManage, function () {
|
|
59015
|
-
return ensure.then(function () {
|
|
59016
|
-
return fetchJson('/api/cloud/members');
|
|
59017
|
-
}).then(function (d) {
|
|
59583
|
+
if (!panel.hidden) { closeGrantsPanel(panel); return; }
|
|
59584
|
+
withBusy(triggerBtn, function () {
|
|
59585
|
+
return fetchJson('/api/cloud/members').then(function (d) {
|
|
59018
59586
|
// The grant target is a member ROLE: lattice_grant_row keys on the
|
|
59019
59587
|
// role, and _access.grantees holds role names. List every member
|
|
59020
59588
|
// except the owner (you don't grant the owner their own row).
|
|
59021
59589
|
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
59022
|
-
var
|
|
59023
|
-
(access.grantees || []).forEach(function (g) {
|
|
59024
|
-
|
|
59025
|
-
|
|
59026
|
-
} else {
|
|
59027
|
-
panel.innerHTML = '<div class="grants-title">Who can see this</div>' + members.map(function (m) {
|
|
59028
|
-
var label = m.name || m.email || m.role;
|
|
59029
|
-
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
59030
|
-
(granted[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
59031
|
-
}).join('');
|
|
59032
|
-
}
|
|
59033
|
-
panel.hidden = false;
|
|
59034
|
-
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
59035
|
-
cb.addEventListener('change', function () {
|
|
59036
|
-
var role = cb.getAttribute('data-grant-role');
|
|
59037
|
-
cb.disabled = true;
|
|
59038
|
-
fetchJson('/api/cloud/row-grant', {
|
|
59039
|
-
method: 'POST',
|
|
59040
|
-
headers: { 'content-type': 'application/json' },
|
|
59041
|
-
body: JSON.stringify({ table: tableName, pk: id, grantee: role, revoke: !cb.checked }),
|
|
59042
|
-
}).then(function () {
|
|
59043
|
-
var list = access.grantees || (access.grantees = []);
|
|
59044
|
-
var at = list.indexOf(role);
|
|
59045
|
-
if (cb.checked && at === -1) list.push(role);
|
|
59046
|
-
if (!cb.checked && at !== -1) list.splice(at, 1);
|
|
59047
|
-
// The first grant flips the row to custom server-side; mirror
|
|
59048
|
-
// that locally so the indicator updates. Revoking the last leaves
|
|
59049
|
-
// visibility 'custom' but effectiveVisibility renders custom-0 as
|
|
59050
|
-
// private, so the label flips back to "Private to you".
|
|
59051
|
-
if (list.length > 0) access.visibility = 'custom';
|
|
59052
|
-
var infoEl = content.querySelector('#detail-vis-info');
|
|
59053
|
-
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
59054
|
-
invalidate(tableName);
|
|
59055
|
-
}).catch(function (e) {
|
|
59056
|
-
cb.checked = !cb.checked; // revert the failed change
|
|
59057
|
-
showToast('Access update failed: ' + e.message, {});
|
|
59058
|
-
}).then(function () { cb.disabled = false; });
|
|
59059
|
-
});
|
|
59060
|
-
});
|
|
59061
|
-
var infoEl = content.querySelector('#detail-vis-info');
|
|
59062
|
-
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
59590
|
+
var desired = {};
|
|
59591
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
59592
|
+
openGrantsPanel = { table: tableName, pk: id };
|
|
59593
|
+
populateGrantsPanel(panel, members, desired);
|
|
59063
59594
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
59064
59595
|
});
|
|
59596
|
+
}
|
|
59597
|
+
|
|
59598
|
+
var detailVisManage = content.querySelector('#detail-vis-manage');
|
|
59599
|
+
if (detailVisManage) detailVisManage.addEventListener('click', function () {
|
|
59600
|
+
openManagePanel(detailVisManage);
|
|
59065
59601
|
});
|
|
59602
|
+
|
|
59603
|
+
// Preserve an open panel across a soft re-render: if the tracked panel
|
|
59604
|
+
// matches the row this view just repainted, re-open it and re-populate the
|
|
59605
|
+
// checklist from the freshly-fetched row._access WITHOUT any network call,
|
|
59606
|
+
// so a concurrent edit by another client doesn't lose a staged selection.
|
|
59607
|
+
if (openGrantsPanel && openGrantsPanel.table === tableName && openGrantsPanel.pk === id) {
|
|
59608
|
+
var rpanel = content.querySelector('#grants-panel');
|
|
59609
|
+
if (rpanel) {
|
|
59610
|
+
fetchJson('/api/cloud/members').then(function (d) {
|
|
59611
|
+
// Only re-populate if THIS panel is still the tracked-open one (a
|
|
59612
|
+
// newer navigation/save may have cleared it while members loaded).
|
|
59613
|
+
if (!openGrantsPanel || openGrantsPanel.table !== tableName || openGrantsPanel.pk !== id) return;
|
|
59614
|
+
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
59615
|
+
var desired = {};
|
|
59616
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
59617
|
+
populateGrantsPanel(rpanel, members, desired);
|
|
59618
|
+
}).catch(function () { /* best-effort restore; a click reopens it */ });
|
|
59619
|
+
}
|
|
59620
|
+
}
|
|
59066
59621
|
}
|
|
59067
59622
|
function renderDetail(content, tableName, id) {
|
|
59068
59623
|
var myGen = renderGen;
|
|
@@ -63846,13 +64401,21 @@ var appJs = `
|
|
|
63846
64401
|
}
|
|
63847
64402
|
function uploadFile(file) {
|
|
63848
64403
|
var done = pendingIngestItem(file.name || 'file');
|
|
64404
|
+
// Carry the composer's "Private mode" intent so an upload made while the
|
|
64405
|
+
// box is checked is stamped private at insert, instead of inheriting the
|
|
64406
|
+
// files-table default (which can be shared-to-everyone on a cloud). Read
|
|
64407
|
+
// the checkbox defensively \u2014 it may not be rendered. On a local workspace
|
|
64408
|
+
// the box is checked+disabled, so this is '1' there too; forced visibility
|
|
64409
|
+
// is a harmless no-op on the single-user SQLite path.
|
|
64410
|
+
var pv = document.getElementById('chat-private');
|
|
64411
|
+
var priv = pv && pv.checked ? '1' : '0';
|
|
63849
64412
|
return fetch('/api/ingest/upload', {
|
|
63850
64413
|
method: 'POST',
|
|
63851
64414
|
// Percent-encode the filename: HTTP header values must be ISO-8859-1,
|
|
63852
64415
|
// so a Unicode filename (emoji, smart quote, accent, em-dash) would
|
|
63853
64416
|
// otherwise make fetch() throw "String contains non ISO-8859-1 code
|
|
63854
64417
|
// point". The server decodeURIComponent()s it back.
|
|
63855
|
-
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file') },
|
|
64418
|
+
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file'), 'x-lattice-private': priv },
|
|
63856
64419
|
body: file,
|
|
63857
64420
|
})
|
|
63858
64421
|
.then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; }); })
|
|
@@ -64719,10 +65282,19 @@ function isUnderGlobalPrefix(packageRoot, execPath) {
|
|
|
64719
65282
|
}
|
|
64720
65283
|
function detectInstallContext(opts = {}) {
|
|
64721
65284
|
const pkgName = opts.pkgName ?? "latticesql";
|
|
64722
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
64723
65285
|
const env2 = opts.env ?? process.env;
|
|
64724
65286
|
const execPath = opts.execPath ?? process.execPath;
|
|
64725
|
-
const
|
|
65287
|
+
const rawCwd = opts.cwd ?? process.cwd();
|
|
65288
|
+
const rawModulePath = opts.modulePath ?? process.argv[1] ?? rawCwd;
|
|
65289
|
+
const resolveReal = (p3) => {
|
|
65290
|
+
try {
|
|
65291
|
+
return (0, import_node_fs27.realpathSync)(p3);
|
|
65292
|
+
} catch {
|
|
65293
|
+
return p3;
|
|
65294
|
+
}
|
|
65295
|
+
};
|
|
65296
|
+
const modulePath = resolveReal(rawModulePath);
|
|
65297
|
+
const cwd = resolveReal(rawCwd);
|
|
64726
65298
|
const packageRoot = findPackageRoot((0, import_node_path30.dirname)(modulePath), pkgName);
|
|
64727
65299
|
if (packageRoot && (0, import_node_fs27.existsSync)((0, import_node_path30.join)(packageRoot, ".git"))) {
|
|
64728
65300
|
return {
|
|
@@ -66508,6 +67080,27 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
66508
67080
|
});
|
|
66509
67081
|
return true;
|
|
66510
67082
|
}
|
|
67083
|
+
if (pathname === "/api/cloud/row-grants" && method === "POST") {
|
|
67084
|
+
await tryHandler(res, async () => {
|
|
67085
|
+
const body = await readJson(req);
|
|
67086
|
+
const table = typeof body.table === "string" ? body.table : "";
|
|
67087
|
+
const pk = typeof body.pk === "string" ? body.pk : "";
|
|
67088
|
+
const strList = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : [];
|
|
67089
|
+
const grant = strList(body.grant);
|
|
67090
|
+
const revoke = strList(body.revoke);
|
|
67091
|
+
if (!table || !pk) {
|
|
67092
|
+
sendJson(res, { error: "table and pk are required" }, 400);
|
|
67093
|
+
return;
|
|
67094
|
+
}
|
|
67095
|
+
if (ctx.db.getDialect() !== "postgres") {
|
|
67096
|
+
sendJson(res, { error: "Per-row sharing requires a cloud (Postgres) database" }, 400);
|
|
67097
|
+
return;
|
|
67098
|
+
}
|
|
67099
|
+
await batchRowGrants(ctx.db, table, pk, grant, revoke);
|
|
67100
|
+
sendJson(res, { ok: true, table, pk, granted: grant, revoked: revoke });
|
|
67101
|
+
});
|
|
67102
|
+
return true;
|
|
67103
|
+
}
|
|
66511
67104
|
if (pathname === "/api/cloud/s3-config" && method === "GET") {
|
|
66512
67105
|
await tryHandler(res, () => {
|
|
66513
67106
|
const label = activeWorkspaceLabel(ctx.configPath);
|
|
@@ -67304,7 +67897,7 @@ function enrichContext(ctx) {
|
|
|
67304
67897
|
...ctx.createEntity ? { createEntity: ctx.createEntity } : {}
|
|
67305
67898
|
};
|
|
67306
67899
|
}
|
|
67307
|
-
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
67900
|
+
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res, privateMode) {
|
|
67308
67901
|
try {
|
|
67309
67902
|
return await enrichWithLlm(
|
|
67310
67903
|
mctx,
|
|
@@ -67316,7 +67909,9 @@ async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
|
67316
67909
|
ctx.entityDescriptions,
|
|
67317
67910
|
ctx.createJunction,
|
|
67318
67911
|
ctx.aggressiveness,
|
|
67319
|
-
ctx.createEntity
|
|
67912
|
+
ctx.createEntity,
|
|
67913
|
+
false,
|
|
67914
|
+
privateMode
|
|
67320
67915
|
);
|
|
67321
67916
|
} catch (e6) {
|
|
67322
67917
|
const err = e6;
|
|
@@ -67395,7 +67990,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67395
67990
|
source: "ingest",
|
|
67396
67991
|
onColumnsAdded: columnDescriptionHook(ctx.db)
|
|
67397
67992
|
};
|
|
67993
|
+
const headerPrivate = req.headers["x-lattice-private"] === "1";
|
|
67398
67994
|
if (ctx.pathname === "/api/ingest/upload") {
|
|
67995
|
+
const forcePrivate2 = headerPrivate;
|
|
67399
67996
|
const rawName = typeof req.headers["x-filename"] === "string" && req.headers["x-filename"] || "";
|
|
67400
67997
|
let name2 = "upload";
|
|
67401
67998
|
if (rawName) {
|
|
@@ -67493,10 +68090,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67493
68090
|
...blob ? { blob_path: blob.blob_path } : {}
|
|
67494
68091
|
} : blob ? { ref_kind: "blob", blob_path: blob.blob_path } : {}
|
|
67495
68092
|
};
|
|
67496
|
-
const { id: id2 } = await createRow(
|
|
67497
|
-
|
|
67498
|
-
|
|
67499
|
-
|
|
68093
|
+
const { id: id2 } = await createRow(
|
|
68094
|
+
mctx,
|
|
68095
|
+
"files",
|
|
68096
|
+
{
|
|
68097
|
+
...await requiredFileDefaults(ctx.db, name2, fileId, uploadRow),
|
|
68098
|
+
...uploadRow
|
|
68099
|
+
},
|
|
68100
|
+
forcePrivate2 ? "private" : void 0
|
|
68101
|
+
);
|
|
67500
68102
|
try {
|
|
67501
68103
|
const dedupCtx = {
|
|
67502
68104
|
db: ctx.db,
|
|
@@ -67522,7 +68124,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67522
68124
|
}
|
|
67523
68125
|
let suggestedLinks = [];
|
|
67524
68126
|
if (!result.skip) {
|
|
67525
|
-
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res);
|
|
68127
|
+
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res, forcePrivate2);
|
|
67526
68128
|
if (links === null) return true;
|
|
67527
68129
|
suggestedLinks = links;
|
|
67528
68130
|
}
|
|
@@ -67549,6 +68151,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67549
68151
|
sendJson4(res, { error: e6.message }, 400);
|
|
67550
68152
|
return true;
|
|
67551
68153
|
}
|
|
68154
|
+
const forcePrivate = headerPrivate || body.private === true;
|
|
67552
68155
|
if (ctx.pathname === "/api/ingest/text") {
|
|
67553
68156
|
const rawText = typeof body.text === "string" ? body.text : "";
|
|
67554
68157
|
if (!rawText.trim()) {
|
|
@@ -67559,7 +68162,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67559
68162
|
if (sourceUrl) {
|
|
67560
68163
|
try {
|
|
67561
68164
|
const result = await ingestUrlAsFile(
|
|
67562
|
-
{ db: ctx.db, mctx, enrich: enrichContext(ctx) },
|
|
68165
|
+
{ db: ctx.db, mctx, enrich: enrichContext(ctx), privateMode: forcePrivate },
|
|
67563
68166
|
sourceUrl
|
|
67564
68167
|
);
|
|
67565
68168
|
sendJson4(
|
|
@@ -67588,11 +68191,25 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67588
68191
|
description: describe(content, mime2, title),
|
|
67589
68192
|
extraction_status: "extracted"
|
|
67590
68193
|
};
|
|
67591
|
-
const { id: id2 } = await createRow(
|
|
67592
|
-
|
|
67593
|
-
|
|
67594
|
-
|
|
67595
|
-
|
|
68194
|
+
const { id: id2 } = await createRow(
|
|
68195
|
+
mctx,
|
|
68196
|
+
"files",
|
|
68197
|
+
{
|
|
68198
|
+
...await requiredFileDefaults(ctx.db, title, textFileId, textRow),
|
|
68199
|
+
...textRow
|
|
68200
|
+
},
|
|
68201
|
+
forcePrivate ? "private" : void 0
|
|
68202
|
+
);
|
|
68203
|
+
const suggestedLinks = await enrichOrFail(
|
|
68204
|
+
mctx,
|
|
68205
|
+
ctx.db,
|
|
68206
|
+
id2,
|
|
68207
|
+
content,
|
|
68208
|
+
title,
|
|
68209
|
+
ctx,
|
|
68210
|
+
res,
|
|
68211
|
+
forcePrivate
|
|
68212
|
+
);
|
|
67596
68213
|
if (suggestedLinks === null) return true;
|
|
67597
68214
|
sendJson4(res, { id: id2, extraction_status: "extracted", suggestedLinks }, 201);
|
|
67598
68215
|
return true;
|
|
@@ -67631,10 +68248,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67631
68248
|
size_bytes: size,
|
|
67632
68249
|
extraction_status: "pending"
|
|
67633
68250
|
};
|
|
67634
|
-
const { id } = await createRow(
|
|
67635
|
-
|
|
67636
|
-
|
|
67637
|
-
|
|
68251
|
+
const { id } = await createRow(
|
|
68252
|
+
mctx,
|
|
68253
|
+
"files",
|
|
68254
|
+
{
|
|
68255
|
+
...await requiredFileDefaults(ctx.db, name, localFileId, localRow),
|
|
68256
|
+
...localRow
|
|
68257
|
+
},
|
|
68258
|
+
forcePrivate ? "private" : void 0
|
|
68259
|
+
);
|
|
67638
68260
|
try {
|
|
67639
68261
|
const result = await extractSource(ctx.db, abs, mime, name);
|
|
67640
68262
|
await updateRow(mctx, "files", id, {
|
|
@@ -67652,7 +68274,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67652
68274
|
ctx.entityDescriptions,
|
|
67653
68275
|
ctx.createJunction,
|
|
67654
68276
|
ctx.aggressiveness,
|
|
67655
|
-
ctx.createEntity
|
|
68277
|
+
ctx.createEntity,
|
|
68278
|
+
false,
|
|
68279
|
+
forcePrivate
|
|
67656
68280
|
);
|
|
67657
68281
|
sendJson4(
|
|
67658
68282
|
res,
|
|
@@ -68339,7 +68963,7 @@ function startBackgroundRender(active) {
|
|
|
68339
68963
|
}
|
|
68340
68964
|
bus.publish(e6);
|
|
68341
68965
|
};
|
|
68342
|
-
void db.renderInBackground(active.outputDir, { signal, onProgress }).then(
|
|
68966
|
+
void db.renderInBackground(active.outputDir, { signal, onProgress, gateOnOpen: true }).then(
|
|
68343
68967
|
() => {
|
|
68344
68968
|
},
|
|
68345
68969
|
(err) => {
|