latticesql 3.4.3 → 3.4.5
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 +915 -201
- package/dist/index.cjs +914 -200
- package/dist/index.d.cts +114 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +914 -200
- 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
|
}
|
|
@@ -4326,14 +4527,6 @@ function resolveDbPath(raw, configDir2) {
|
|
|
4326
4527
|
}
|
|
4327
4528
|
return (0, import_node_path11.resolve)(configDir2, raw);
|
|
4328
4529
|
}
|
|
4329
|
-
function warnDeprecatedRef(entity, field, target) {
|
|
4330
|
-
const key = `${entity}.${field}`;
|
|
4331
|
-
if (warnedDeprecatedRefs.has(key)) return;
|
|
4332
|
-
warnedDeprecatedRefs.add(key);
|
|
4333
|
-
console.warn(
|
|
4334
|
-
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
4335
|
-
);
|
|
4336
|
-
}
|
|
4337
4530
|
function entityToTableDef(entityName, entity) {
|
|
4338
4531
|
const rawFields = entity.fields;
|
|
4339
4532
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -4360,7 +4553,6 @@ function entityToTableDef(entityName, entity) {
|
|
|
4360
4553
|
table: field.ref,
|
|
4361
4554
|
foreignKey: fieldName
|
|
4362
4555
|
};
|
|
4363
|
-
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
4364
4556
|
}
|
|
4365
4557
|
}
|
|
4366
4558
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
@@ -4517,7 +4709,7 @@ function parseEntityContexts(entityContexts) {
|
|
|
4517
4709
|
}
|
|
4518
4710
|
return result;
|
|
4519
4711
|
}
|
|
4520
|
-
var import_node_fs10, import_node_path11, import_yaml3
|
|
4712
|
+
var import_node_fs10, import_node_path11, import_yaml3;
|
|
4521
4713
|
var init_parser = __esm({
|
|
4522
4714
|
"src/config/parser.ts"() {
|
|
4523
4715
|
"use strict";
|
|
@@ -4525,7 +4717,6 @@ var init_parser = __esm({
|
|
|
4525
4717
|
import_node_path11 = require("path");
|
|
4526
4718
|
import_yaml3 = require("yaml");
|
|
4527
4719
|
init_user_config();
|
|
4528
|
-
warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
4529
4720
|
}
|
|
4530
4721
|
});
|
|
4531
4722
|
|
|
@@ -5243,6 +5434,7 @@ var init_lattice = __esm({
|
|
|
5243
5434
|
init_shred();
|
|
5244
5435
|
init_encryption();
|
|
5245
5436
|
init_manifest();
|
|
5437
|
+
init_render_cursor();
|
|
5246
5438
|
import_node_fs12 = require("fs");
|
|
5247
5439
|
init_adapter();
|
|
5248
5440
|
init_sqlite();
|
|
@@ -5314,6 +5506,14 @@ var init_lattice = __esm({
|
|
|
5314
5506
|
_changelogTables = /* @__PURE__ */ new Set();
|
|
5315
5507
|
/** Current task context string for relevance filtering. */
|
|
5316
5508
|
_taskContext = "";
|
|
5509
|
+
/**
|
|
5510
|
+
* True when this connection opened against an already-provisioned cloud as a
|
|
5511
|
+
* SCOPED MEMBER (no role-management privilege → no CREATE/ALTER on the schema).
|
|
5512
|
+
* Set during init() by the same probe that decides introspect-only. Drives
|
|
5513
|
+
* {@link addColumn} to route DDL through the owner-side `lattice_member_add_column`
|
|
5514
|
+
* SECURITY DEFINER helper instead of issuing a raw ALTER the member can't run.
|
|
5515
|
+
*/
|
|
5516
|
+
_cloudMemberOpen = false;
|
|
5317
5517
|
_auditHandlers = [];
|
|
5318
5518
|
_renderHandlers = [];
|
|
5319
5519
|
_writebackHandlers = [];
|
|
@@ -5560,7 +5760,7 @@ var init_lattice = __esm({
|
|
|
5560
5760
|
/** Async tail of init(). See {@link init} for the sync-validation phase. */
|
|
5561
5761
|
async _initAsync(options) {
|
|
5562
5762
|
let introspectOnly = options.introspectOnly === true;
|
|
5563
|
-
if (
|
|
5763
|
+
if (this.getDialect() === "postgres") {
|
|
5564
5764
|
try {
|
|
5565
5765
|
const [marker, role] = await Promise.all([
|
|
5566
5766
|
getAsyncOrSync(this._adapter, `SELECT to_regclass('__lattice_owners') AS reg`),
|
|
@@ -5571,7 +5771,9 @@ var init_lattice = __esm({
|
|
|
5571
5771
|
]);
|
|
5572
5772
|
const provisioned = !!marker && marker.reg != null;
|
|
5573
5773
|
const canCreateRoles = !!role && role.rolcreaterole === true;
|
|
5574
|
-
|
|
5774
|
+
const memberOpen = provisioned && !canCreateRoles;
|
|
5775
|
+
introspectOnly = introspectOnly || memberOpen;
|
|
5776
|
+
this._cloudMemberOpen = memberOpen;
|
|
5575
5777
|
} catch {
|
|
5576
5778
|
}
|
|
5577
5779
|
}
|
|
@@ -5659,6 +5861,26 @@ var init_lattice = __esm({
|
|
|
5659
5861
|
getDialect() {
|
|
5660
5862
|
return this._adapter.dialect;
|
|
5661
5863
|
}
|
|
5864
|
+
/**
|
|
5865
|
+
* True when a table opts into the observation/changelog substrate
|
|
5866
|
+
* (`def.changelog`). Callers that want to bypass the high-level {@link delete}
|
|
5867
|
+
* with a transaction-scoped raw delete use this to know whether the table also
|
|
5868
|
+
* needs the changelog / write-hook / embedding side effects that only
|
|
5869
|
+
* `delete()` performs — so they can keep the high-level path for such tables.
|
|
5870
|
+
*/
|
|
5871
|
+
isChangelogTracked(table) {
|
|
5872
|
+
return this._changelogTables.has(table);
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* True when this connection opened as a scoped cloud MEMBER (see
|
|
5876
|
+
* {@link _cloudMemberOpen}). Callers use it to route DDL-bearing work through
|
|
5877
|
+
* the owner-side SECURITY DEFINER helpers rather than issuing DDL the member's
|
|
5878
|
+
* role can't run (e.g. {@link addColumn} regenerates the masking view inside
|
|
5879
|
+
* `lattice_member_add_column`, so the caller must not also try to regenerate it).
|
|
5880
|
+
*/
|
|
5881
|
+
isCloudMemberOpen() {
|
|
5882
|
+
return this._cloudMemberOpen;
|
|
5883
|
+
}
|
|
5662
5884
|
/**
|
|
5663
5885
|
* Return the normalised primary-key column list for a registered
|
|
5664
5886
|
* table. Falls back to `['id']` for tables registered via raw DDL
|
|
@@ -5735,7 +5957,15 @@ var init_lattice = __esm({
|
|
|
5735
5957
|
assertSafeIdentifier(column, "column");
|
|
5736
5958
|
const existing = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5737
5959
|
if (!existing.includes(column)) {
|
|
5738
|
-
|
|
5960
|
+
if (this._cloudMemberOpen) {
|
|
5961
|
+
await runAsyncOrSync(this._adapter, `SELECT lattice_member_add_column(?, ?, ?)`, [
|
|
5962
|
+
table,
|
|
5963
|
+
column,
|
|
5964
|
+
typeSpec
|
|
5965
|
+
]);
|
|
5966
|
+
} else {
|
|
5967
|
+
await addColumnAsyncOrSync(this._adapter, table, column, typeSpec);
|
|
5968
|
+
}
|
|
5739
5969
|
}
|
|
5740
5970
|
const cols = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5741
5971
|
this._columnCache.set(table, new Set(cols));
|
|
@@ -6667,12 +6897,39 @@ var init_lattice = __esm({
|
|
|
6667
6897
|
async renderInBackground(outputDir, opts = {}) {
|
|
6668
6898
|
const notInit = this._notInitError();
|
|
6669
6899
|
if (notInit) return notInit;
|
|
6900
|
+
if (opts.gateOnOpen && !opts.changedTables) {
|
|
6901
|
+
const start = Date.now();
|
|
6902
|
+
const recorded = readManifest(outputDir);
|
|
6903
|
+
if (recorded != null) {
|
|
6904
|
+
const live = await computeRenderCursor(this._adapter);
|
|
6905
|
+
if (cursorIsFresh(recorded, live)) {
|
|
6906
|
+
opts.onProgress?.({
|
|
6907
|
+
kind: "done",
|
|
6908
|
+
table: null,
|
|
6909
|
+
entitiesRendered: 0,
|
|
6910
|
+
entitiesTotal: 0,
|
|
6911
|
+
tableIndex: 0,
|
|
6912
|
+
tableCount: 0,
|
|
6913
|
+
pct: 100,
|
|
6914
|
+
durationMs: Date.now() - start
|
|
6915
|
+
});
|
|
6916
|
+
const skipped = {
|
|
6917
|
+
filesWritten: [],
|
|
6918
|
+
filesSkipped: 0,
|
|
6919
|
+
durationMs: Date.now() - start
|
|
6920
|
+
};
|
|
6921
|
+
for (const h6 of this._renderHandlers) h6(skipped);
|
|
6922
|
+
return skipped;
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6670
6926
|
if (!opts.changedTables) {
|
|
6671
6927
|
this._pendingRenderAll = false;
|
|
6672
6928
|
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
6673
6929
|
this._autoRenderPending = false;
|
|
6674
6930
|
}
|
|
6675
|
-
|
|
6931
|
+
const { gateOnOpen: _gateOnOpen, ...engineOpts } = opts;
|
|
6932
|
+
return this._renderGuarded(outputDir, engineOpts);
|
|
6676
6933
|
}
|
|
6677
6934
|
/**
|
|
6678
6935
|
* Install a per-viewer read-relation resolver for ALL renders (initial,
|
|
@@ -47870,6 +48127,111 @@ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
|
47870
48127
|
AND g."pk" = ANY(p_pks)
|
|
47871
48128
|
AND o."owner_role" = session_user;
|
|
47872
48129
|
$fn$;
|
|
48130
|
+
|
|
48131
|
+
-- Add a column to a user table AS THE OWNER, on behalf of a scoped member. A
|
|
48132
|
+
-- member's role has no CREATE/ALTER on the schema (the bootstrap REVOKEs CREATE
|
|
48133
|
+
-- from PUBLIC), so a member's GUI "add a field" write (createRow/updateRow with a
|
|
48134
|
+
-- field the table lacks) cannot run ALTER TABLE itself. This SECURITY DEFINER
|
|
48135
|
+
-- helper performs that ALTER \u2014 and the masking-view regen \u2014 with the owner's
|
|
48136
|
+
-- rights, so member-added columns behave identically to owner-added ones.
|
|
48137
|
+
--
|
|
48138
|
+
-- Injection-safe + minimal: p_table must be an existing BASE table in the current
|
|
48139
|
+
-- schema (rejected otherwise); p_type is whitelisted against the exact set the
|
|
48140
|
+
-- library's addColumn emits for an auto-added column (TEXT / INTEGER / REAL, plus
|
|
48141
|
+
-- BOOLEAN) \u2014 never interpolated raw; both identifiers go through %I (quote_ident).
|
|
48142
|
+
-- Member-callable (granted EXECUTE to the member group), but it can only widen the
|
|
48143
|
+
-- schema, never read or alter another member's data.
|
|
48144
|
+
CREATE OR REPLACE FUNCTION lattice_member_add_column(p_table text, p_column text, p_type text)
|
|
48145
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
48146
|
+
DECLARE
|
|
48147
|
+
v_type text;
|
|
48148
|
+
v_view text := p_table || '_v';
|
|
48149
|
+
v_has_view boolean;
|
|
48150
|
+
v_pk_expr text;
|
|
48151
|
+
v_select text;
|
|
48152
|
+
BEGIN
|
|
48153
|
+
-- Never alter internal bookkeeping tables (names start with "_"). The GUI only
|
|
48154
|
+
-- ever calls this for a user entity table; rejecting the rest is defense-in-depth
|
|
48155
|
+
-- against a member invoking the function directly against ownership/audit/policy
|
|
48156
|
+
-- tables.
|
|
48157
|
+
IF left(p_table, 1) = '_' THEN
|
|
48158
|
+
RAISE EXCEPTION 'lattice: cannot add a column to internal table "%"', p_table;
|
|
48159
|
+
END IF;
|
|
48160
|
+
|
|
48161
|
+
-- p_table must be a real base table in THIS schema (search_path is pinned to the
|
|
48162
|
+
-- cloud schema by pinDefinerSearchPath, so to_regclass resolves there).
|
|
48163
|
+
IF NOT EXISTS (
|
|
48164
|
+
SELECT 1 FROM pg_class c
|
|
48165
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48166
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND c.relkind = 'r'
|
|
48167
|
+
) THEN
|
|
48168
|
+
RAISE EXCEPTION 'lattice: no such table "%"', p_table;
|
|
48169
|
+
END IF;
|
|
48170
|
+
|
|
48171
|
+
-- Whitelist the column type. These are exactly the specs addColumn's
|
|
48172
|
+
-- inferColumnType produces (TEXT / INTEGER / REAL); BOOLEAN is allowed too.
|
|
48173
|
+
-- Anything else is rejected \u2014 the type is spliced as %s (NOT %I), so it must be
|
|
48174
|
+
-- a known-safe literal and never caller-controlled SQL.
|
|
48175
|
+
v_type := upper(btrim(p_type));
|
|
48176
|
+
IF v_type NOT IN ('TEXT', 'INTEGER', 'REAL', 'BOOLEAN') THEN
|
|
48177
|
+
RAISE EXCEPTION 'lattice: unsupported column type "%"', p_type;
|
|
48178
|
+
END IF;
|
|
48179
|
+
|
|
48180
|
+
EXECUTE format('ALTER TABLE %I ADD COLUMN IF NOT EXISTS %I %s', p_table, p_column, v_type);
|
|
48181
|
+
|
|
48182
|
+
-- If the table is cell-masked (a "<table>_v" view exists, because some column has
|
|
48183
|
+
-- an audience), the view selects an explicit column list \u2014 so a new column is
|
|
48184
|
+
-- invisible to members until the view is regenerated. Rebuild it the same way the
|
|
48185
|
+
-- owner path (audienceViewSql / regenerateAudienceViewFromDb) does: pass every
|
|
48186
|
+
-- column through except those with an 'owner' audience in __lattice_column_policy
|
|
48187
|
+
-- (CASE WHEN lattice_is_owner(...) THEN col END), re-apply row visibility with
|
|
48188
|
+
-- WHERE lattice_row_visible(table, pk), and keep the member SELECT grant on the
|
|
48189
|
+
-- view. Unmasked tables need no regen \u2014 the member group's table-level base grant
|
|
48190
|
+
-- already covers the new column.
|
|
48191
|
+
SELECT EXISTS (
|
|
48192
|
+
SELECT 1 FROM pg_class c
|
|
48193
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48194
|
+
WHERE n.nspname = current_schema() AND c.relname = v_view AND c.relkind = 'v'
|
|
48195
|
+
) INTO v_has_view;
|
|
48196
|
+
|
|
48197
|
+
IF v_has_view THEN
|
|
48198
|
+
-- Canonical pk expression: CAST("col" AS TEXT) joined by TAB (chr(9)) \u2014 the
|
|
48199
|
+
-- same serialization the RLS policies + audienceViewSql use.
|
|
48200
|
+
SELECT string_agg(format('CAST(%I AS TEXT)', a.attname), ' || chr(9) || '
|
|
48201
|
+
ORDER BY array_position(i.indkey, a.attnum))
|
|
48202
|
+
INTO v_pk_expr
|
|
48203
|
+
FROM pg_index i
|
|
48204
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
48205
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48206
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
|
|
48207
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND i.indisprimary;
|
|
48208
|
+
IF v_pk_expr IS NULL THEN
|
|
48209
|
+
RAISE EXCEPTION 'lattice: cannot regenerate mask view for "%": no primary key', p_table;
|
|
48210
|
+
END IF;
|
|
48211
|
+
|
|
48212
|
+
-- Build the masked SELECT list in column order, applying the per-column policy.
|
|
48213
|
+
SELECT string_agg(
|
|
48214
|
+
CASE
|
|
48215
|
+
WHEN cp."audience" = 'owner'
|
|
48216
|
+
THEN format('CASE WHEN lattice_is_owner(%L, %s) THEN %I END AS %I',
|
|
48217
|
+
p_table, v_pk_expr, cols.column_name, cols.column_name)
|
|
48218
|
+
ELSE format('%I', cols.column_name)
|
|
48219
|
+
END,
|
|
48220
|
+
', ' ORDER BY cols.ordinal_position)
|
|
48221
|
+
INTO v_select
|
|
48222
|
+
FROM information_schema.columns cols
|
|
48223
|
+
LEFT JOIN "__lattice_column_policy" cp
|
|
48224
|
+
ON cp."table_name" = p_table AND cp."column_name" = cols.column_name
|
|
48225
|
+
AND cp."audience" NOT IN ('', 'everyone', 'row-audience')
|
|
48226
|
+
WHERE cols.table_schema = current_schema() AND cols.table_name = p_table;
|
|
48227
|
+
|
|
48228
|
+
EXECUTE format(
|
|
48229
|
+
'CREATE OR REPLACE VIEW %I AS SELECT %s FROM %I WHERE lattice_row_visible(%L, %s)',
|
|
48230
|
+
v_view, v_select, p_table, p_table, v_pk_expr);
|
|
48231
|
+
EXECUTE format('GRANT SELECT ON %I TO ${MEMBER_GROUP}', v_view);
|
|
48232
|
+
END IF;
|
|
48233
|
+
END $fn$;
|
|
48234
|
+
GRANT EXECUTE ON FUNCTION lattice_member_add_column(text, text, text) TO ${MEMBER_GROUP};
|
|
47873
48235
|
`;
|
|
47874
48236
|
}
|
|
47875
48237
|
});
|
|
@@ -47979,6 +48341,11 @@ async function revokeRow(db, table, pk, grantee) {
|
|
|
47979
48341
|
assertPg(db);
|
|
47980
48342
|
await runAsyncOrSync(db.adapter, `SELECT lattice_revoke_row(?, ?, ?)`, [table, pk, grantee]);
|
|
47981
48343
|
}
|
|
48344
|
+
async function batchRowGrants(db, table, pk, grant, revoke) {
|
|
48345
|
+
assertPg(db);
|
|
48346
|
+
for (const grantee of grant) await grantRow(db, table, pk, grantee);
|
|
48347
|
+
for (const grantee of revoke) await revokeRow(db, table, pk, grantee);
|
|
48348
|
+
}
|
|
47982
48349
|
async function revokeMemberRole(db, role) {
|
|
47983
48350
|
assertPg(db);
|
|
47984
48351
|
if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
|
|
@@ -49082,18 +49449,9 @@ function sessionUndoneFilters(undone, sessionId) {
|
|
|
49082
49449
|
if (sessionId) filters.push({ col: "session_id", op: "eq", val: sessionId });
|
|
49083
49450
|
return filters;
|
|
49084
49451
|
}
|
|
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", {
|
|
49452
|
+
function buildAuditRow(table, rowId, op, before, after, sessionId, editTs) {
|
|
49453
|
+
return {
|
|
49091
49454
|
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
49455
|
ts: sanitizeEditTs(editTs) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
49098
49456
|
table_name: table,
|
|
49099
49457
|
row_id: rowId,
|
|
@@ -49102,7 +49460,9 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
49102
49460
|
after_json: after ? JSON.stringify(after) : null,
|
|
49103
49461
|
undone: 0,
|
|
49104
49462
|
session_id: sessionId ?? null
|
|
49105
|
-
}
|
|
49463
|
+
};
|
|
49464
|
+
}
|
|
49465
|
+
function publishMutationFeed(feed, table, rowId, op, before, after, source) {
|
|
49106
49466
|
const labelRow = op === "delete" ? before : after;
|
|
49107
49467
|
feed.publish({
|
|
49108
49468
|
table,
|
|
@@ -49112,17 +49472,28 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
49112
49472
|
summary: feedSummary(op, table, labelRow)
|
|
49113
49473
|
});
|
|
49114
49474
|
}
|
|
49115
|
-
function
|
|
49116
|
-
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
49117
|
-
}
|
|
49118
|
-
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
49475
|
+
async function purgeRedoStack(db, sessionId) {
|
|
49119
49476
|
const undone = await db.query("_lattice_gui_audit", {
|
|
49120
49477
|
filters: sessionUndoneFilters(1, sessionId)
|
|
49121
49478
|
});
|
|
49122
49479
|
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
49480
|
+
}
|
|
49481
|
+
async function appendAudit(db, feed, table, rowId, op, before, after, source = "gui", sessionId, editTs) {
|
|
49482
|
+
await purgeRedoStack(db, sessionId);
|
|
49483
|
+
await db.insert(
|
|
49484
|
+
"_lattice_gui_audit",
|
|
49485
|
+
buildAuditRow(table, rowId, op, before, after, sessionId, editTs)
|
|
49486
|
+
);
|
|
49487
|
+
publishMutationFeed(feed, table, rowId, op, before, after, source);
|
|
49488
|
+
}
|
|
49489
|
+
function isSchemaOp(operation2) {
|
|
49490
|
+
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
49491
|
+
}
|
|
49492
|
+
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
49493
|
+
await purgeRedoStack(db, sessionId);
|
|
49123
49494
|
await db.insert("_lattice_gui_audit", {
|
|
49124
49495
|
id: crypto.randomUUID(),
|
|
49125
|
-
// Explicit ISO ts — see
|
|
49496
|
+
// Explicit ISO ts — see buildAuditRow (the SQLite-only strftime DEFAULT
|
|
49126
49497
|
// rendered "Invalid Date" on the Postgres/cloud path).
|
|
49127
49498
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49128
49499
|
table_name: table,
|
|
@@ -49157,7 +49528,7 @@ async function ensureColumns(db, table, values) {
|
|
|
49157
49528
|
const added = Object.keys(values).filter((k6) => !(k6 in existing));
|
|
49158
49529
|
if (added.length === 0) return [];
|
|
49159
49530
|
for (const col of added) await db.addColumn(table, col, inferColumnType(values[col]));
|
|
49160
|
-
if (db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
49531
|
+
if (!db.isCloudMemberOpen() && db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
49161
49532
|
const cols = db.getRegisteredColumns(table);
|
|
49162
49533
|
const pk = db.getPrimaryKey(table);
|
|
49163
49534
|
if (cols && pk.length > 0) await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
@@ -49279,7 +49650,14 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
49279
49650
|
ctx.clientTs
|
|
49280
49651
|
);
|
|
49281
49652
|
} else {
|
|
49282
|
-
await ctx
|
|
49653
|
+
await hardDelete(ctx, table, id, before);
|
|
49654
|
+
}
|
|
49655
|
+
}
|
|
49656
|
+
async function hardDelete(ctx, table, id, before) {
|
|
49657
|
+
const withClient = ctx.db.adapter.withClient?.bind(ctx.db.adapter);
|
|
49658
|
+
const pkCols = ctx.db.getPrimaryKey(table);
|
|
49659
|
+
const pkCol = pkCols.length === 1 ? pkCols[0] : void 0;
|
|
49660
|
+
if (!withClient || ctx.db.isChangelogTracked(table) || pkCol === void 0) {
|
|
49283
49661
|
await appendAudit(
|
|
49284
49662
|
ctx.db,
|
|
49285
49663
|
ctx.feed,
|
|
@@ -49292,10 +49670,30 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
49292
49670
|
ctx.sessionId,
|
|
49293
49671
|
ctx.clientTs
|
|
49294
49672
|
);
|
|
49673
|
+
await ctx.db.delete(table, id);
|
|
49674
|
+
return;
|
|
49295
49675
|
}
|
|
49676
|
+
const auditRow = buildAuditRow(table, id, "delete", before, null, ctx.sessionId, ctx.clientTs);
|
|
49677
|
+
await purgeRedoStack(ctx.db, ctx.sessionId);
|
|
49678
|
+
const auditCols = AUDIT_COLUMNS.map((c6) => `"${c6}"`).join(", ");
|
|
49679
|
+
const auditPlaceholders = AUDIT_COLUMNS.map(() => "?").join(", ");
|
|
49680
|
+
const auditValues = AUDIT_COLUMNS.map((c6) => auditRow[c6]);
|
|
49681
|
+
const pkColQuoted = pkCol.replace(/"/g, '""');
|
|
49682
|
+
await withClient(async (tx) => {
|
|
49683
|
+
await tx.run(
|
|
49684
|
+
`INSERT INTO "_lattice_gui_audit" (${auditCols}) VALUES (${auditPlaceholders})`,
|
|
49685
|
+
auditValues
|
|
49686
|
+
);
|
|
49687
|
+
await tx.run(`DELETE FROM "${table.replace(/"/g, '""')}" WHERE "${pkColQuoted}" = ?`, [id]);
|
|
49688
|
+
});
|
|
49689
|
+
publishMutationFeed(ctx.feed, table, id, "delete", before, null, ctx.source);
|
|
49296
49690
|
}
|
|
49297
|
-
async function linkRows(ctx, table, body) {
|
|
49298
|
-
|
|
49691
|
+
async function linkRows(ctx, table, body, forceVisibility) {
|
|
49692
|
+
if (forceVisibility !== void 0) {
|
|
49693
|
+
await ctx.db.insertForcingVisibility(table, body, forceVisibility);
|
|
49694
|
+
} else {
|
|
49695
|
+
await ctx.db.link(table, body);
|
|
49696
|
+
}
|
|
49299
49697
|
await appendAudit(ctx.db, ctx.feed, table, null, "link", null, body, ctx.source, ctx.sessionId);
|
|
49300
49698
|
}
|
|
49301
49699
|
async function unlinkRows(ctx, table, body) {
|
|
@@ -49433,13 +49831,24 @@ async function revertEntry(ctx, id) {
|
|
|
49433
49831
|
});
|
|
49434
49832
|
return { ok: true, entry };
|
|
49435
49833
|
}
|
|
49436
|
-
var import_node_crypto15, SCHEMA_OP_PREFIX;
|
|
49834
|
+
var import_node_crypto15, AUDIT_COLUMNS, SCHEMA_OP_PREFIX;
|
|
49437
49835
|
var init_mutations = __esm({
|
|
49438
49836
|
"src/gui/mutations.ts"() {
|
|
49439
49837
|
"use strict";
|
|
49440
49838
|
import_node_crypto15 = require("crypto");
|
|
49441
49839
|
init_cloud_connect();
|
|
49442
49840
|
init_audience();
|
|
49841
|
+
AUDIT_COLUMNS = [
|
|
49842
|
+
"id",
|
|
49843
|
+
"ts",
|
|
49844
|
+
"table_name",
|
|
49845
|
+
"row_id",
|
|
49846
|
+
"operation",
|
|
49847
|
+
"before_json",
|
|
49848
|
+
"after_json",
|
|
49849
|
+
"undone",
|
|
49850
|
+
"session_id"
|
|
49851
|
+
];
|
|
49443
49852
|
SCHEMA_OP_PREFIX = "schema.";
|
|
49444
49853
|
}
|
|
49445
49854
|
});
|
|
@@ -49727,6 +50136,10 @@ async function readMachineCredential(db, kind) {
|
|
|
49727
50136
|
}
|
|
49728
50137
|
return null;
|
|
49729
50138
|
}
|
|
50139
|
+
async function resolveAnthropicKey(db) {
|
|
50140
|
+
if (isAssistantCredentialCleared(CREDENTIALS.anthropic.kind)) return null;
|
|
50141
|
+
return await readMachineCredential(db, CREDENTIALS.anthropic.kind) ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
50142
|
+
}
|
|
49730
50143
|
function getAggressiveness() {
|
|
49731
50144
|
const n3 = readPreferences().aggressiveness;
|
|
49732
50145
|
if (!Number.isFinite(n3)) return DEFAULT_AGGRESSIVENESS;
|
|
@@ -49757,6 +50170,7 @@ async function getVoiceCredential(db) {
|
|
|
49757
50170
|
return null;
|
|
49758
50171
|
}
|
|
49759
50172
|
async function hasCredential(db, name, envVar) {
|
|
50173
|
+
if (isAssistantCredentialCleared(CREDENTIALS[name].kind)) return false;
|
|
49760
50174
|
return Boolean(await readMachineCredential(db, CREDENTIALS[name].kind)) || Boolean(process.env[envVar]);
|
|
49761
50175
|
}
|
|
49762
50176
|
async function resolveClaudeAuth(db) {
|
|
@@ -49779,7 +50193,7 @@ async function resolveClaudeAuth(db) {
|
|
|
49779
50193
|
} catch {
|
|
49780
50194
|
}
|
|
49781
50195
|
}
|
|
49782
|
-
const apiKey = await
|
|
50196
|
+
const apiKey = await resolveAnthropicKey(db);
|
|
49783
50197
|
return apiKey ? { apiKey } : null;
|
|
49784
50198
|
}
|
|
49785
50199
|
async function hasClaudeAuth(db) {
|
|
@@ -49876,6 +50290,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
49876
50290
|
}
|
|
49877
50291
|
const cred = CREDENTIALS[name];
|
|
49878
50292
|
setAssistantCredential(cred.kind, key);
|
|
50293
|
+
clearAssistantCredentialCleared(cred.kind);
|
|
49879
50294
|
if (db) {
|
|
49880
50295
|
for (const row of await liveSecretsOfKind(db, cred.kind)) {
|
|
49881
50296
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -49892,6 +50307,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
49892
50307
|
return true;
|
|
49893
50308
|
}
|
|
49894
50309
|
deleteAssistantCredential(CREDENTIALS[name].kind);
|
|
50310
|
+
setAssistantCredentialCleared(CREDENTIALS[name].kind);
|
|
49895
50311
|
if (db) {
|
|
49896
50312
|
for (const row of await liveSecretsOfKind(db, CREDENTIALS[name].kind)) {
|
|
49897
50313
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -52082,7 +52498,7 @@ function buildSchema(db) {
|
|
|
52082
52498
|
}
|
|
52083
52499
|
return out;
|
|
52084
52500
|
}
|
|
52085
|
-
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false) {
|
|
52501
|
+
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false, privateMode = false) {
|
|
52086
52502
|
if (!text.trim()) return [];
|
|
52087
52503
|
const auth = await resolveClaudeAuth(db);
|
|
52088
52504
|
if (!auth) {
|
|
@@ -52104,6 +52520,7 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52104
52520
|
});
|
|
52105
52521
|
return [];
|
|
52106
52522
|
}
|
|
52523
|
+
const forceVis = privateMode ? "private" : void 0;
|
|
52107
52524
|
const temperature = aggressivenessToTemperature(aggressiveness);
|
|
52108
52525
|
let description = "";
|
|
52109
52526
|
try {
|
|
@@ -52146,11 +52563,16 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52146
52563
|
}
|
|
52147
52564
|
if (jx) {
|
|
52148
52565
|
try {
|
|
52149
|
-
await linkRows(
|
|
52150
|
-
|
|
52151
|
-
|
|
52152
|
-
|
|
52153
|
-
|
|
52566
|
+
await linkRows(
|
|
52567
|
+
mctx,
|
|
52568
|
+
jx.junction,
|
|
52569
|
+
{
|
|
52570
|
+
id: crypto.randomUUID(),
|
|
52571
|
+
[jx.fileFk]: fileId,
|
|
52572
|
+
[jx.otherFk]: m4.id
|
|
52573
|
+
},
|
|
52574
|
+
forceVis
|
|
52575
|
+
);
|
|
52154
52576
|
linkedCount++;
|
|
52155
52577
|
if (created) {
|
|
52156
52578
|
mctx.feed.publish({
|
|
@@ -52209,16 +52631,21 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52209
52631
|
if ("name" in cols && row.name == null) row.name = obj2.label;
|
|
52210
52632
|
if ("title" in cols && row.title == null) row.title = obj2.label;
|
|
52211
52633
|
try {
|
|
52212
|
-
const { id: rowId } = await createRow(mctx, entity, row);
|
|
52634
|
+
const { id: rowId } = await createRow(mctx, entity, row, forceVis);
|
|
52213
52635
|
createdCount++;
|
|
52214
52636
|
const ent = entity;
|
|
52215
52637
|
const jx = junctions.find((j6) => j6.otherTable === ent) ?? (createJunction ? await createJunction(ent) : null);
|
|
52216
52638
|
if (jx) {
|
|
52217
|
-
await linkRows(
|
|
52218
|
-
|
|
52219
|
-
|
|
52220
|
-
|
|
52221
|
-
|
|
52639
|
+
await linkRows(
|
|
52640
|
+
mctx,
|
|
52641
|
+
jx.junction,
|
|
52642
|
+
{
|
|
52643
|
+
id: crypto.randomUUID(),
|
|
52644
|
+
[jx.fileFk]: fileId,
|
|
52645
|
+
[jx.otherFk]: rowId
|
|
52646
|
+
},
|
|
52647
|
+
forceVis
|
|
52648
|
+
);
|
|
52222
52649
|
}
|
|
52223
52650
|
} catch (e6) {
|
|
52224
52651
|
console.warn(`[ingest] create ${entity} from document failed:`, e6.message);
|
|
@@ -52232,12 +52659,17 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52232
52659
|
try {
|
|
52233
52660
|
const title = name.replace(/\.[^./\\]+$/, "").trim() || "Note";
|
|
52234
52661
|
const body = description.length > 0 ? description : text.slice(0, 2e3);
|
|
52235
|
-
const { id: noteId } = await createRow(
|
|
52236
|
-
|
|
52237
|
-
|
|
52238
|
-
|
|
52239
|
-
|
|
52240
|
-
|
|
52662
|
+
const { id: noteId } = await createRow(
|
|
52663
|
+
mctx,
|
|
52664
|
+
"notes",
|
|
52665
|
+
{
|
|
52666
|
+
id: crypto.randomUUID(),
|
|
52667
|
+
title,
|
|
52668
|
+
body,
|
|
52669
|
+
source_file_id: fileId
|
|
52670
|
+
},
|
|
52671
|
+
forceVis
|
|
52672
|
+
);
|
|
52241
52673
|
mctx.feed.publish({
|
|
52242
52674
|
table: "notes",
|
|
52243
52675
|
op: "insert",
|
|
@@ -52351,7 +52783,8 @@ async function ingestUrlAsFile(ctx, rawUrl, opts = {}) {
|
|
|
52351
52783
|
ctx.enrich.createJunction,
|
|
52352
52784
|
ctx.enrich.aggressiveness,
|
|
52353
52785
|
ctx.enrich.createEntity,
|
|
52354
|
-
true
|
|
52786
|
+
true,
|
|
52787
|
+
ctx.privateMode === true
|
|
52355
52788
|
);
|
|
52356
52789
|
}
|
|
52357
52790
|
return {
|
|
@@ -53229,13 +53662,22 @@ function loadSdk() {
|
|
|
53229
53662
|
throw new Error("Could not resolve the Anthropic constructor from '@anthropic-ai/sdk'");
|
|
53230
53663
|
return ctor;
|
|
53231
53664
|
}
|
|
53232
|
-
function
|
|
53233
|
-
const Anthropic = loadSdk();
|
|
53665
|
+
function buildAnthropicConfig(auth) {
|
|
53234
53666
|
const config = {};
|
|
53235
|
-
if (auth.authToken)
|
|
53236
|
-
|
|
53667
|
+
if (auth.authToken) {
|
|
53668
|
+
config.authToken = auth.authToken;
|
|
53669
|
+
config.apiKey = null;
|
|
53670
|
+
} else if (auth.apiKey) {
|
|
53671
|
+
config.apiKey = auth.apiKey;
|
|
53672
|
+
} else {
|
|
53673
|
+
config.apiKey = null;
|
|
53674
|
+
}
|
|
53237
53675
|
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
53238
|
-
|
|
53676
|
+
return config;
|
|
53677
|
+
}
|
|
53678
|
+
function createAnthropicClient(auth) {
|
|
53679
|
+
const Anthropic = loadSdk();
|
|
53680
|
+
const sdk = new Anthropic(buildAnthropicConfig(auth));
|
|
53239
53681
|
return {
|
|
53240
53682
|
async runTurn(params) {
|
|
53241
53683
|
const stream = sdk.messages.stream({
|
|
@@ -54670,8 +55112,14 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
54670
55112
|
},
|
|
54671
55113
|
{
|
|
54672
55114
|
name: "_lattice_gui_audit",
|
|
54673
|
-
|
|
54674
|
-
|
|
55115
|
+
// UPDATE + DELETE are needed by undo/redo/revert (flips an entry's `undone`)
|
|
55116
|
+
// and the redo-stack purge on a new mutation (deletes the session's undone
|
|
55117
|
+
// entries). Safe because enableGuiAuditRls installs per-op UPDATE and DELETE
|
|
55118
|
+
// policies whose USING is `row_id IS NULL OR lattice_row_visible(table_name,
|
|
55119
|
+
// row_id)` — so a member can only update/delete audit rows for entities it can
|
|
55120
|
+
// already see (or schema-level entries that carry no row data).
|
|
55121
|
+
privs: "SELECT, INSERT, UPDATE, DELETE",
|
|
55122
|
+
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
55123
|
},
|
|
54676
55124
|
{
|
|
54677
55125
|
name: "__lattice_user_identity",
|
|
@@ -55073,6 +55521,19 @@ async function normalizeImage(path2, maxBytes) {
|
|
|
55073
55521
|
function renderJpeg(sharp, path2, quality) {
|
|
55074
55522
|
return sharp(path2).rotate().resize({ width: MAX_DIM, height: MAX_DIM, fit: "inside", withoutEnlargement: true }).jpeg({ quality }).toBuffer();
|
|
55075
55523
|
}
|
|
55524
|
+
function buildVisionAnthropicConfig(auth) {
|
|
55525
|
+
const config = {};
|
|
55526
|
+
if (auth.authToken) {
|
|
55527
|
+
config.authToken = auth.authToken;
|
|
55528
|
+
config.apiKey = null;
|
|
55529
|
+
} else if (auth.apiKey) {
|
|
55530
|
+
config.apiKey = auth.apiKey;
|
|
55531
|
+
} else {
|
|
55532
|
+
config.apiKey = null;
|
|
55533
|
+
}
|
|
55534
|
+
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
55535
|
+
return config;
|
|
55536
|
+
}
|
|
55076
55537
|
function defaultSender(auth) {
|
|
55077
55538
|
return async (input) => {
|
|
55078
55539
|
const importMetaUrl = import_meta3.url;
|
|
@@ -55080,11 +55541,7 @@ function defaultSender(auth) {
|
|
|
55080
55541
|
const sdk = req("@anthropic-ai/sdk");
|
|
55081
55542
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
55082
55543
|
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);
|
|
55544
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
55088
55545
|
const res = await client.messages.create({
|
|
55089
55546
|
model: input.model,
|
|
55090
55547
|
max_tokens: 1024,
|
|
@@ -55111,11 +55568,7 @@ function defaultPdfSender(auth) {
|
|
|
55111
55568
|
const sdk = req("@anthropic-ai/sdk");
|
|
55112
55569
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
55113
55570
|
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);
|
|
55571
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
55119
55572
|
const res = await client.messages.create({
|
|
55120
55573
|
model: input.model,
|
|
55121
55574
|
max_tokens: 4096,
|
|
@@ -55508,6 +55961,8 @@ var css = `
|
|
|
55508
55961
|
.app-version:empty { display: none; }
|
|
55509
55962
|
.app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
|
|
55510
55963
|
.app-update[hidden] { display: none; }
|
|
55964
|
+
#app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
55965
|
+
#app-update-link[hidden] { display: none; }
|
|
55511
55966
|
/* Unseen-change count next to a sidebar entity. */
|
|
55512
55967
|
.nav-badge {
|
|
55513
55968
|
display: inline-block; min-width: 16px; text-align: center;
|
|
@@ -56055,6 +56510,8 @@ var css = `
|
|
|
56055
56510
|
.grants-panel .grants-title { font-weight: 600; margin-bottom: 6px; }
|
|
56056
56511
|
.grants-panel .grants-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
|
|
56057
56512
|
.grants-panel .grants-row input { accent-color: var(--accent); }
|
|
56513
|
+
.grants-panel .grants-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
|
|
56514
|
+
.grants-panel .grants-dirty { font-size: 12px; }
|
|
56058
56515
|
|
|
56059
56516
|
/* Inline create-row at the bottom of every table */
|
|
56060
56517
|
tr.create-row td { background: var(--surface-2); }
|
|
@@ -56984,6 +57441,12 @@ var appJs = `
|
|
|
56984
57441
|
// drag handle once the app has booted.
|
|
56985
57442
|
var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
|
|
56986
57443
|
if (!isNaN(savedRail)) applyRailWidth(savedRail);
|
|
57444
|
+
// The version chip + manual-upgrade link live in the static shell (present
|
|
57445
|
+
// from first paint, in both the normal and virgin-state boots), so wire the
|
|
57446
|
+
// click handler and run the first availability check here \u2014 independent of
|
|
57447
|
+
// the async workspace bootstrap. checkServerVersion() refreshes it later.
|
|
57448
|
+
wireUpdateLink();
|
|
57449
|
+
checkUpdateAvailable();
|
|
56987
57450
|
// Failsafe: never leave the overlay up forever if a fetch hangs without
|
|
56988
57451
|
// rejecting, or a future early-return (e.g. the virgin-state screen)
|
|
56989
57452
|
// bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
|
|
@@ -57468,6 +57931,26 @@ var appJs = `
|
|
|
57468
57931
|
showUpdatePill(label || 'Updated \u2014 reloading\u2026');
|
|
57469
57932
|
setTimeout(function () { location.reload(); }, 600);
|
|
57470
57933
|
}
|
|
57934
|
+
// Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
|
|
57935
|
+
// the version chip only when the server reports a newer, installable version.
|
|
57936
|
+
// The auto-updater installs in the background on its own cadence; this lets
|
|
57937
|
+
// the user force it now. Best-effort; the link stays hidden on any failure.
|
|
57938
|
+
function checkUpdateAvailable() {
|
|
57939
|
+
var el = document.getElementById('app-update-link');
|
|
57940
|
+
if (!el) return;
|
|
57941
|
+
fetch('/api/update/status')
|
|
57942
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
57943
|
+
.then(function (s) {
|
|
57944
|
+
if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
|
|
57945
|
+
el.textContent = 'Update available \u2014 Upgrade';
|
|
57946
|
+
el.title = 'Install v' + s.latest + ' and restart';
|
|
57947
|
+
el.hidden = false;
|
|
57948
|
+
} else {
|
|
57949
|
+
el.hidden = true;
|
|
57950
|
+
}
|
|
57951
|
+
})
|
|
57952
|
+
.catch(function () { /* best-effort \u2014 keep the link hidden */ });
|
|
57953
|
+
}
|
|
57471
57954
|
// On every (re)connect, ask the server its version. A change vs BOOT_VERSION
|
|
57472
57955
|
// means a relaunch onto new code \u2192 reload. Best-effort; never throws.
|
|
57473
57956
|
function checkServerVersion() {
|
|
@@ -57481,6 +57964,31 @@ var appJs = `
|
|
|
57481
57964
|
else hideUpdatePill();
|
|
57482
57965
|
})
|
|
57483
57966
|
.catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
|
|
57967
|
+
// Refresh the manual-upgrade link alongside the reconnect version check.
|
|
57968
|
+
checkUpdateAvailable();
|
|
57969
|
+
}
|
|
57970
|
+
// Wire the manual-upgrade link's click: kick off the install (the server
|
|
57971
|
+
// installs the latest and restarts onto it) and surface the progress. On
|
|
57972
|
+
// success we do nothing else \u2014 the update-applied event + the reconnect
|
|
57973
|
+
// version check land the page on the new version (no manual reload). A
|
|
57974
|
+
// false ok means the install can't run (unsupervised) \u2014 toast why.
|
|
57975
|
+
function wireUpdateLink() {
|
|
57976
|
+
var el = document.getElementById('app-update-link');
|
|
57977
|
+
if (!el) return;
|
|
57978
|
+
el.addEventListener('click', function (e) {
|
|
57979
|
+
e.preventDefault();
|
|
57980
|
+
el.hidden = true;
|
|
57981
|
+
showUpdatePill('Updating\u2026');
|
|
57982
|
+
fetch('/api/update/apply', { method: 'POST' })
|
|
57983
|
+
.then(function (r) { return r.json(); })
|
|
57984
|
+
.then(function (d) {
|
|
57985
|
+
if (d && d.ok === false) {
|
|
57986
|
+
hideUpdatePill();
|
|
57987
|
+
showToast(d.error || 'Update unavailable', {});
|
|
57988
|
+
}
|
|
57989
|
+
})
|
|
57990
|
+
.catch(function () { /* server may already be restarting */ });
|
|
57991
|
+
});
|
|
57484
57992
|
}
|
|
57485
57993
|
function dispatchStreamMessage(type, data) {
|
|
57486
57994
|
if (type === 'realtime-state') {
|
|
@@ -58521,6 +59029,15 @@ var appJs = `
|
|
|
58521
59029
|
// Per-table view state: 'live' (default) or 'trash' (soft-deleted rows).
|
|
58522
59030
|
var tableViewMode = {};
|
|
58523
59031
|
|
|
59032
|
+
// The (table, pk) of the per-row "Manage access" grants panel that is
|
|
59033
|
+
// currently open, or null when none is. A soft re-render (a concurrent edit
|
|
59034
|
+
// by another client fires pg_notify \u2192 realtime refresh \u2192 renderRoute({soft})
|
|
59035
|
+
// \u2192 renderDetail/renderFsItem repaint) would otherwise re-create the detail
|
|
59036
|
+
// view with the panel collapsed, dropping a staged multi-select mid-edit.
|
|
59037
|
+
// wireRowSharing reads this after each repaint and re-opens + re-populates the
|
|
59038
|
+
// panel WITHOUT any network call, so the staged selection survives.
|
|
59039
|
+
var openGrantsPanel = null;
|
|
59040
|
+
|
|
58524
59041
|
function renderTable(content, tableName) {
|
|
58525
59042
|
var myGen = renderGen;
|
|
58526
59043
|
clearUnseen(tableName);
|
|
@@ -58999,70 +59516,151 @@ var appJs = `
|
|
|
58999
59516
|
}).catch(function (e) { showToast('Visibility update failed: ' + e.message, {}); });
|
|
59000
59517
|
});
|
|
59001
59518
|
});
|
|
59002
|
-
var
|
|
59003
|
-
|
|
59519
|
+
var access = row._access || {};
|
|
59520
|
+
|
|
59521
|
+
// Render the staged member checklist + a single "Save sharing" / "Cancel"
|
|
59522
|
+
// into the panel. Checkbox toggles mutate ONLY the local desired map \u2014
|
|
59523
|
+
// NO network call per toggle (the old design auto-saved live, one POST per
|
|
59524
|
+
// checkbox, and each grant's pg_notify collapsed the panel). A single batch
|
|
59525
|
+
// request fires on Save. members is the already-fetched list; desired
|
|
59526
|
+
// seeds from the row's current grantees (or a caller-supplied staged map
|
|
59527
|
+
// when re-opening after a soft re-render).
|
|
59528
|
+
function populateGrantsPanel(panel, members, desired) {
|
|
59529
|
+
// Snapshot the CURRENT (committed) grantees so Save can diff desired-vs-
|
|
59530
|
+
// current into adds/removes. effectiveVisibility decides whether we're
|
|
59531
|
+
// actually switching INTO specific-people mode (custom-0 reads as private).
|
|
59532
|
+
var current = {};
|
|
59533
|
+
(access.grantees || []).forEach(function (g) { current[g] = true; });
|
|
59534
|
+
if (members.length === 0) {
|
|
59535
|
+
panel.innerHTML = '<div class="muted">No other members in this workspace yet.</div>';
|
|
59536
|
+
panel.hidden = false;
|
|
59537
|
+
return;
|
|
59538
|
+
}
|
|
59539
|
+
function dirtyCount() {
|
|
59540
|
+
var n = 0;
|
|
59541
|
+
members.forEach(function (m) {
|
|
59542
|
+
if (!!desired[m.role] !== !!current[m.role]) n++;
|
|
59543
|
+
});
|
|
59544
|
+
return n;
|
|
59545
|
+
}
|
|
59546
|
+
function render() {
|
|
59547
|
+
var changed = dirtyCount();
|
|
59548
|
+
panel.innerHTML = '<div class="grants-title">Who can see this</div>' +
|
|
59549
|
+
members.map(function (m) {
|
|
59550
|
+
var label = m.name || m.email || m.role;
|
|
59551
|
+
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
59552
|
+
(desired[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
59553
|
+
}).join('') +
|
|
59554
|
+
'<div class="grants-actions">' +
|
|
59555
|
+
'<button class="btn primary" id="grants-save"' + (changed ? '' : ' disabled') + '>Save sharing</button>' +
|
|
59556
|
+
'<button class="btn" id="grants-cancel">Cancel</button>' +
|
|
59557
|
+
'<span class="grants-dirty muted">' + (changed ? (changed === 1 ? '1 change' : changed + ' changes') : 'No changes') + '</span>' +
|
|
59558
|
+
'</div>';
|
|
59559
|
+
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
59560
|
+
cb.addEventListener('change', function () {
|
|
59561
|
+
var role = cb.getAttribute('data-grant-role');
|
|
59562
|
+
if (cb.checked) desired[role] = true; else delete desired[role];
|
|
59563
|
+
render(); // re-render to refresh the dirty indicator + Save state
|
|
59564
|
+
});
|
|
59565
|
+
});
|
|
59566
|
+
var cancelBtn = panel.querySelector('#grants-cancel');
|
|
59567
|
+
if (cancelBtn) cancelBtn.addEventListener('click', function () { closeGrantsPanel(panel); });
|
|
59568
|
+
var saveBtn = panel.querySelector('#grants-save');
|
|
59569
|
+
if (saveBtn) saveBtn.addEventListener('click', function () {
|
|
59570
|
+
var toAdd = [];
|
|
59571
|
+
var toRemove = [];
|
|
59572
|
+
members.forEach(function (m) {
|
|
59573
|
+
var want = !!desired[m.role];
|
|
59574
|
+
var have = !!current[m.role];
|
|
59575
|
+
if (want && !have) toAdd.push(m.role);
|
|
59576
|
+
if (!want && have) toRemove.push(m.role);
|
|
59577
|
+
});
|
|
59578
|
+
if (toAdd.length === 0 && toRemove.length === 0) { closeGrantsPanel(panel); return; }
|
|
59579
|
+
// Confirm the mode change ONCE, here \u2014 only when actually switching
|
|
59580
|
+
// INTO specific-people mode (effective vis isn't already custom AND we
|
|
59581
|
+
// are adding at least one grantee). Never per checkbox.
|
|
59582
|
+
if (effectiveVisibility(access) !== 'custom' && toAdd.length > 0) {
|
|
59583
|
+
if (!confirm('Sharing this with specific people switches it off "everyone"/"private". The chosen people will be able to see it. Continue?')) return;
|
|
59584
|
+
}
|
|
59585
|
+
withBusy(saveBtn, function () {
|
|
59586
|
+
return fetchJson('/api/cloud/row-grants', {
|
|
59587
|
+
method: 'POST',
|
|
59588
|
+
headers: { 'content-type': 'application/json' },
|
|
59589
|
+
body: JSON.stringify({ table: tableName, pk: id, grant: toAdd, revoke: toRemove }),
|
|
59590
|
+
}).then(function () {
|
|
59591
|
+
// Mirror the committed state locally so the re-render's indicator
|
|
59592
|
+
// is correct. The first grant flips the row to custom server-side;
|
|
59593
|
+
// revoking the last leaves custom-0, which effectiveVisibility
|
|
59594
|
+
// renders as private.
|
|
59595
|
+
var list = [];
|
|
59596
|
+
members.forEach(function (m) { if (desired[m.role]) list.push(m.role); });
|
|
59597
|
+
access.grantees = list;
|
|
59598
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
59599
|
+
openGrantsPanel = null; // a successful save closes the staging session
|
|
59600
|
+
invalidate(tableName);
|
|
59601
|
+
showToast('Sharing updated', {});
|
|
59602
|
+
reRender();
|
|
59603
|
+
}).catch(function (e) {
|
|
59604
|
+
// Surface loudly + leave the staged selection intact so the user
|
|
59605
|
+
// can retry; no silent partial-success.
|
|
59606
|
+
showToast('Sharing update failed: ' + e.message, {});
|
|
59607
|
+
});
|
|
59608
|
+
});
|
|
59609
|
+
});
|
|
59610
|
+
panel.hidden = false;
|
|
59611
|
+
}
|
|
59612
|
+
render();
|
|
59613
|
+
}
|
|
59614
|
+
|
|
59615
|
+
function closeGrantsPanel(panel) {
|
|
59616
|
+
if (panel) panel.hidden = true;
|
|
59617
|
+
openGrantsPanel = null;
|
|
59618
|
+
}
|
|
59619
|
+
|
|
59620
|
+
// Open (or toggle shut) the manage-access panel. Fetches the member list,
|
|
59621
|
+
// then stages from the row's current grantees. Opening must NOT pre-flip
|
|
59622
|
+
// the row to 'custom' \u2014 that left a never-shared row stuck at "custom (0)".
|
|
59623
|
+
function openManagePanel(triggerBtn) {
|
|
59004
59624
|
var panel = content.querySelector('#grants-panel');
|
|
59005
59625
|
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) {
|
|
59626
|
+
if (!panel.hidden) { closeGrantsPanel(panel); return; }
|
|
59627
|
+
withBusy(triggerBtn, function () {
|
|
59628
|
+
return fetchJson('/api/cloud/members').then(function (d) {
|
|
59018
59629
|
// The grant target is a member ROLE: lattice_grant_row keys on the
|
|
59019
59630
|
// role, and _access.grantees holds role names. List every member
|
|
59020
59631
|
// except the owner (you don't grant the owner their own row).
|
|
59021
59632
|
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);
|
|
59633
|
+
var desired = {};
|
|
59634
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
59635
|
+
openGrantsPanel = { table: tableName, pk: id };
|
|
59636
|
+
populateGrantsPanel(panel, members, desired);
|
|
59063
59637
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
59064
59638
|
});
|
|
59639
|
+
}
|
|
59640
|
+
|
|
59641
|
+
var detailVisManage = content.querySelector('#detail-vis-manage');
|
|
59642
|
+
if (detailVisManage) detailVisManage.addEventListener('click', function () {
|
|
59643
|
+
openManagePanel(detailVisManage);
|
|
59065
59644
|
});
|
|
59645
|
+
|
|
59646
|
+
// Preserve an open panel across a soft re-render: if the tracked panel
|
|
59647
|
+
// matches the row this view just repainted, re-open it and re-populate the
|
|
59648
|
+
// checklist from the freshly-fetched row._access WITHOUT any network call,
|
|
59649
|
+
// so a concurrent edit by another client doesn't lose a staged selection.
|
|
59650
|
+
if (openGrantsPanel && openGrantsPanel.table === tableName && openGrantsPanel.pk === id) {
|
|
59651
|
+
var rpanel = content.querySelector('#grants-panel');
|
|
59652
|
+
if (rpanel) {
|
|
59653
|
+
fetchJson('/api/cloud/members').then(function (d) {
|
|
59654
|
+
// Only re-populate if THIS panel is still the tracked-open one (a
|
|
59655
|
+
// newer navigation/save may have cleared it while members loaded).
|
|
59656
|
+
if (!openGrantsPanel || openGrantsPanel.table !== tableName || openGrantsPanel.pk !== id) return;
|
|
59657
|
+
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
59658
|
+
var desired = {};
|
|
59659
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
59660
|
+
populateGrantsPanel(rpanel, members, desired);
|
|
59661
|
+
}).catch(function () { /* best-effort restore; a click reopens it */ });
|
|
59662
|
+
}
|
|
59663
|
+
}
|
|
59066
59664
|
}
|
|
59067
59665
|
function renderDetail(content, tableName, id) {
|
|
59068
59666
|
var myGen = renderGen;
|
|
@@ -63846,13 +64444,21 @@ var appJs = `
|
|
|
63846
64444
|
}
|
|
63847
64445
|
function uploadFile(file) {
|
|
63848
64446
|
var done = pendingIngestItem(file.name || 'file');
|
|
64447
|
+
// Carry the composer's "Private mode" intent so an upload made while the
|
|
64448
|
+
// box is checked is stamped private at insert, instead of inheriting the
|
|
64449
|
+
// files-table default (which can be shared-to-everyone on a cloud). Read
|
|
64450
|
+
// the checkbox defensively \u2014 it may not be rendered. On a local workspace
|
|
64451
|
+
// the box is checked+disabled, so this is '1' there too; forced visibility
|
|
64452
|
+
// is a harmless no-op on the single-user SQLite path.
|
|
64453
|
+
var pv = document.getElementById('chat-private');
|
|
64454
|
+
var priv = pv && pv.checked ? '1' : '0';
|
|
63849
64455
|
return fetch('/api/ingest/upload', {
|
|
63850
64456
|
method: 'POST',
|
|
63851
64457
|
// Percent-encode the filename: HTTP header values must be ISO-8859-1,
|
|
63852
64458
|
// so a Unicode filename (emoji, smart quote, accent, em-dash) would
|
|
63853
64459
|
// otherwise make fetch() throw "String contains non ISO-8859-1 code
|
|
63854
64460
|
// point". The server decodeURIComponent()s it back.
|
|
63855
|
-
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file') },
|
|
64461
|
+
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file'), 'x-lattice-private': priv },
|
|
63856
64462
|
body: file,
|
|
63857
64463
|
})
|
|
63858
64464
|
.then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; }); })
|
|
@@ -64200,6 +64806,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
64200
64806
|
<span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
|
|
64201
64807
|
<span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
|
|
64202
64808
|
<span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
|
|
64809
|
+
<a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
|
|
64203
64810
|
<button id="settings-gear" title="Settings" aria-label="Open settings">
|
|
64204
64811
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
64205
64812
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -64719,10 +65326,19 @@ function isUnderGlobalPrefix(packageRoot, execPath) {
|
|
|
64719
65326
|
}
|
|
64720
65327
|
function detectInstallContext(opts = {}) {
|
|
64721
65328
|
const pkgName = opts.pkgName ?? "latticesql";
|
|
64722
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
64723
65329
|
const env2 = opts.env ?? process.env;
|
|
64724
65330
|
const execPath = opts.execPath ?? process.execPath;
|
|
64725
|
-
const
|
|
65331
|
+
const rawCwd = opts.cwd ?? process.cwd();
|
|
65332
|
+
const rawModulePath = opts.modulePath ?? process.argv[1] ?? rawCwd;
|
|
65333
|
+
const resolveReal = (p3) => {
|
|
65334
|
+
try {
|
|
65335
|
+
return (0, import_node_fs27.realpathSync)(p3);
|
|
65336
|
+
} catch {
|
|
65337
|
+
return p3;
|
|
65338
|
+
}
|
|
65339
|
+
};
|
|
65340
|
+
const modulePath = resolveReal(rawModulePath);
|
|
65341
|
+
const cwd = resolveReal(rawCwd);
|
|
64726
65342
|
const packageRoot = findPackageRoot((0, import_node_path30.dirname)(modulePath), pkgName);
|
|
64727
65343
|
if (packageRoot && (0, import_node_fs27.existsSync)((0, import_node_path30.join)(packageRoot, ".git"))) {
|
|
64728
65344
|
return {
|
|
@@ -66508,6 +67124,27 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
66508
67124
|
});
|
|
66509
67125
|
return true;
|
|
66510
67126
|
}
|
|
67127
|
+
if (pathname === "/api/cloud/row-grants" && method === "POST") {
|
|
67128
|
+
await tryHandler(res, async () => {
|
|
67129
|
+
const body = await readJson(req);
|
|
67130
|
+
const table = typeof body.table === "string" ? body.table : "";
|
|
67131
|
+
const pk = typeof body.pk === "string" ? body.pk : "";
|
|
67132
|
+
const strList = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : [];
|
|
67133
|
+
const grant = strList(body.grant);
|
|
67134
|
+
const revoke = strList(body.revoke);
|
|
67135
|
+
if (!table || !pk) {
|
|
67136
|
+
sendJson(res, { error: "table and pk are required" }, 400);
|
|
67137
|
+
return;
|
|
67138
|
+
}
|
|
67139
|
+
if (ctx.db.getDialect() !== "postgres") {
|
|
67140
|
+
sendJson(res, { error: "Per-row sharing requires a cloud (Postgres) database" }, 400);
|
|
67141
|
+
return;
|
|
67142
|
+
}
|
|
67143
|
+
await batchRowGrants(ctx.db, table, pk, grant, revoke);
|
|
67144
|
+
sendJson(res, { ok: true, table, pk, granted: grant, revoked: revoke });
|
|
67145
|
+
});
|
|
67146
|
+
return true;
|
|
67147
|
+
}
|
|
66511
67148
|
if (pathname === "/api/cloud/s3-config" && method === "GET") {
|
|
66512
67149
|
await tryHandler(res, () => {
|
|
66513
67150
|
const label = activeWorkspaceLabel(ctx.configPath);
|
|
@@ -67304,7 +67941,7 @@ function enrichContext(ctx) {
|
|
|
67304
67941
|
...ctx.createEntity ? { createEntity: ctx.createEntity } : {}
|
|
67305
67942
|
};
|
|
67306
67943
|
}
|
|
67307
|
-
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
67944
|
+
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res, privateMode) {
|
|
67308
67945
|
try {
|
|
67309
67946
|
return await enrichWithLlm(
|
|
67310
67947
|
mctx,
|
|
@@ -67316,7 +67953,9 @@ async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
|
67316
67953
|
ctx.entityDescriptions,
|
|
67317
67954
|
ctx.createJunction,
|
|
67318
67955
|
ctx.aggressiveness,
|
|
67319
|
-
ctx.createEntity
|
|
67956
|
+
ctx.createEntity,
|
|
67957
|
+
false,
|
|
67958
|
+
privateMode
|
|
67320
67959
|
);
|
|
67321
67960
|
} catch (e6) {
|
|
67322
67961
|
const err = e6;
|
|
@@ -67395,7 +68034,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67395
68034
|
source: "ingest",
|
|
67396
68035
|
onColumnsAdded: columnDescriptionHook(ctx.db)
|
|
67397
68036
|
};
|
|
68037
|
+
const headerPrivate = req.headers["x-lattice-private"] === "1";
|
|
67398
68038
|
if (ctx.pathname === "/api/ingest/upload") {
|
|
68039
|
+
const forcePrivate2 = headerPrivate;
|
|
67399
68040
|
const rawName = typeof req.headers["x-filename"] === "string" && req.headers["x-filename"] || "";
|
|
67400
68041
|
let name2 = "upload";
|
|
67401
68042
|
if (rawName) {
|
|
@@ -67493,10 +68134,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67493
68134
|
...blob ? { blob_path: blob.blob_path } : {}
|
|
67494
68135
|
} : blob ? { ref_kind: "blob", blob_path: blob.blob_path } : {}
|
|
67495
68136
|
};
|
|
67496
|
-
const { id: id2 } = await createRow(
|
|
67497
|
-
|
|
67498
|
-
|
|
67499
|
-
|
|
68137
|
+
const { id: id2 } = await createRow(
|
|
68138
|
+
mctx,
|
|
68139
|
+
"files",
|
|
68140
|
+
{
|
|
68141
|
+
...await requiredFileDefaults(ctx.db, name2, fileId, uploadRow),
|
|
68142
|
+
...uploadRow
|
|
68143
|
+
},
|
|
68144
|
+
forcePrivate2 ? "private" : void 0
|
|
68145
|
+
);
|
|
67500
68146
|
try {
|
|
67501
68147
|
const dedupCtx = {
|
|
67502
68148
|
db: ctx.db,
|
|
@@ -67522,7 +68168,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67522
68168
|
}
|
|
67523
68169
|
let suggestedLinks = [];
|
|
67524
68170
|
if (!result.skip) {
|
|
67525
|
-
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res);
|
|
68171
|
+
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res, forcePrivate2);
|
|
67526
68172
|
if (links === null) return true;
|
|
67527
68173
|
suggestedLinks = links;
|
|
67528
68174
|
}
|
|
@@ -67549,6 +68195,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67549
68195
|
sendJson4(res, { error: e6.message }, 400);
|
|
67550
68196
|
return true;
|
|
67551
68197
|
}
|
|
68198
|
+
const forcePrivate = headerPrivate || body.private === true;
|
|
67552
68199
|
if (ctx.pathname === "/api/ingest/text") {
|
|
67553
68200
|
const rawText = typeof body.text === "string" ? body.text : "";
|
|
67554
68201
|
if (!rawText.trim()) {
|
|
@@ -67559,7 +68206,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67559
68206
|
if (sourceUrl) {
|
|
67560
68207
|
try {
|
|
67561
68208
|
const result = await ingestUrlAsFile(
|
|
67562
|
-
{ db: ctx.db, mctx, enrich: enrichContext(ctx) },
|
|
68209
|
+
{ db: ctx.db, mctx, enrich: enrichContext(ctx), privateMode: forcePrivate },
|
|
67563
68210
|
sourceUrl
|
|
67564
68211
|
);
|
|
67565
68212
|
sendJson4(
|
|
@@ -67588,11 +68235,25 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67588
68235
|
description: describe(content, mime2, title),
|
|
67589
68236
|
extraction_status: "extracted"
|
|
67590
68237
|
};
|
|
67591
|
-
const { id: id2 } = await createRow(
|
|
67592
|
-
|
|
67593
|
-
|
|
67594
|
-
|
|
67595
|
-
|
|
68238
|
+
const { id: id2 } = await createRow(
|
|
68239
|
+
mctx,
|
|
68240
|
+
"files",
|
|
68241
|
+
{
|
|
68242
|
+
...await requiredFileDefaults(ctx.db, title, textFileId, textRow),
|
|
68243
|
+
...textRow
|
|
68244
|
+
},
|
|
68245
|
+
forcePrivate ? "private" : void 0
|
|
68246
|
+
);
|
|
68247
|
+
const suggestedLinks = await enrichOrFail(
|
|
68248
|
+
mctx,
|
|
68249
|
+
ctx.db,
|
|
68250
|
+
id2,
|
|
68251
|
+
content,
|
|
68252
|
+
title,
|
|
68253
|
+
ctx,
|
|
68254
|
+
res,
|
|
68255
|
+
forcePrivate
|
|
68256
|
+
);
|
|
67596
68257
|
if (suggestedLinks === null) return true;
|
|
67597
68258
|
sendJson4(res, { id: id2, extraction_status: "extracted", suggestedLinks }, 201);
|
|
67598
68259
|
return true;
|
|
@@ -67631,10 +68292,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67631
68292
|
size_bytes: size,
|
|
67632
68293
|
extraction_status: "pending"
|
|
67633
68294
|
};
|
|
67634
|
-
const { id } = await createRow(
|
|
67635
|
-
|
|
67636
|
-
|
|
67637
|
-
|
|
68295
|
+
const { id } = await createRow(
|
|
68296
|
+
mctx,
|
|
68297
|
+
"files",
|
|
68298
|
+
{
|
|
68299
|
+
...await requiredFileDefaults(ctx.db, name, localFileId, localRow),
|
|
68300
|
+
...localRow
|
|
68301
|
+
},
|
|
68302
|
+
forcePrivate ? "private" : void 0
|
|
68303
|
+
);
|
|
67638
68304
|
try {
|
|
67639
68305
|
const result = await extractSource(ctx.db, abs, mime, name);
|
|
67640
68306
|
await updateRow(mctx, "files", id, {
|
|
@@ -67652,7 +68318,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67652
68318
|
ctx.entityDescriptions,
|
|
67653
68319
|
ctx.createJunction,
|
|
67654
68320
|
ctx.aggressiveness,
|
|
67655
|
-
ctx.createEntity
|
|
68321
|
+
ctx.createEntity,
|
|
68322
|
+
false,
|
|
68323
|
+
forcePrivate
|
|
67656
68324
|
);
|
|
67657
68325
|
sendJson4(
|
|
67658
68326
|
res,
|
|
@@ -68339,7 +69007,7 @@ function startBackgroundRender(active) {
|
|
|
68339
69007
|
}
|
|
68340
69008
|
bus.publish(e6);
|
|
68341
69009
|
};
|
|
68342
|
-
void db.renderInBackground(active.outputDir, { signal, onProgress }).then(
|
|
69010
|
+
void db.renderInBackground(active.outputDir, { signal, onProgress, gateOnOpen: true }).then(
|
|
68343
69011
|
() => {
|
|
68344
69012
|
},
|
|
68345
69013
|
(err) => {
|
|
@@ -68681,6 +69349,28 @@ async function startGuiServer(options) {
|
|
|
68681
69349
|
setActive(next, created.id);
|
|
68682
69350
|
return created.id;
|
|
68683
69351
|
};
|
|
69352
|
+
const cleanupWorkspaceFiles = (root6, ws) => {
|
|
69353
|
+
if (!ws.configPath && ws.kind === "local") {
|
|
69354
|
+
(0, import_node_fs35.rmSync)(workspaceDir(root6, ws.dir), { recursive: true, force: true });
|
|
69355
|
+
} else if (ws.kind === "cloud") {
|
|
69356
|
+
if (ws.configPath && (0, import_node_fs35.existsSync)(ws.configPath)) {
|
|
69357
|
+
(0, import_node_fs35.rmSync)(ws.configPath, { force: true });
|
|
69358
|
+
}
|
|
69359
|
+
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
69360
|
+
const label = labelMatch?.[1];
|
|
69361
|
+
if (label) {
|
|
69362
|
+
const stillUsed = listWorkspaces(root6).some(
|
|
69363
|
+
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
69364
|
+
);
|
|
69365
|
+
if (!stillUsed) {
|
|
69366
|
+
try {
|
|
69367
|
+
deleteDbCredential(label);
|
|
69368
|
+
} catch {
|
|
69369
|
+
}
|
|
69370
|
+
}
|
|
69371
|
+
}
|
|
69372
|
+
}
|
|
69373
|
+
};
|
|
68684
69374
|
const handleVirginRoute = async (req, res, pathname, method) => {
|
|
68685
69375
|
if (method === "GET" && pathname === "/") {
|
|
68686
69376
|
sendText(
|
|
@@ -68732,6 +69422,35 @@ async function startGuiServer(options) {
|
|
|
68732
69422
|
}
|
|
68733
69423
|
return true;
|
|
68734
69424
|
}
|
|
69425
|
+
if (method === "POST" && pathname === "/api/workspaces/delete") {
|
|
69426
|
+
if (!latticeRoot) {
|
|
69427
|
+
sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
|
|
69428
|
+
return true;
|
|
69429
|
+
}
|
|
69430
|
+
const body = await readJson(req);
|
|
69431
|
+
if (typeof body.id !== "string") {
|
|
69432
|
+
sendJson(res, { error: "id must be a string" }, 400);
|
|
69433
|
+
return true;
|
|
69434
|
+
}
|
|
69435
|
+
const ws = getWorkspace(latticeRoot, body.id);
|
|
69436
|
+
if (!ws) {
|
|
69437
|
+
sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
|
|
69438
|
+
return true;
|
|
69439
|
+
}
|
|
69440
|
+
removeWorkspace(latticeRoot, ws.id);
|
|
69441
|
+
try {
|
|
69442
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
69443
|
+
} catch (e6) {
|
|
69444
|
+
sendJson(
|
|
69445
|
+
res,
|
|
69446
|
+
{ error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
|
|
69447
|
+
500
|
|
69448
|
+
);
|
|
69449
|
+
return true;
|
|
69450
|
+
}
|
|
69451
|
+
sendJson(res, { ok: true, switchedTo: null });
|
|
69452
|
+
return true;
|
|
69453
|
+
}
|
|
68735
69454
|
if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
|
|
68736
69455
|
await redeemInvite(createCloudWorkspace, req, res);
|
|
68737
69456
|
return true;
|
|
@@ -68766,6 +69485,18 @@ async function startGuiServer(options) {
|
|
|
68766
69485
|
);
|
|
68767
69486
|
return;
|
|
68768
69487
|
}
|
|
69488
|
+
if (method === "POST" && pathname === "/api/update/apply") {
|
|
69489
|
+
if (updateService) {
|
|
69490
|
+
void updateService.checkNow(true);
|
|
69491
|
+
sendJson(res, { ok: true, status: updateService.status() });
|
|
69492
|
+
} else {
|
|
69493
|
+
sendJson(res, {
|
|
69494
|
+
ok: false,
|
|
69495
|
+
error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
|
|
69496
|
+
});
|
|
69497
|
+
}
|
|
69498
|
+
return;
|
|
69499
|
+
}
|
|
68769
69500
|
if (!activeRef) {
|
|
68770
69501
|
if (await handleVirginRoute(req, res, pathname, method)) return;
|
|
68771
69502
|
sendJson(res, { error: "No active workspace" }, 409);
|
|
@@ -69859,26 +70590,7 @@ async function startGuiServer(options) {
|
|
|
69859
70590
|
}
|
|
69860
70591
|
removeWorkspace(latticeRoot, ws.id);
|
|
69861
70592
|
try {
|
|
69862
|
-
|
|
69863
|
-
(0, import_node_fs35.rmSync)(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
|
|
69864
|
-
} else if (ws.kind === "cloud") {
|
|
69865
|
-
if (ws.configPath && (0, import_node_fs35.existsSync)(ws.configPath)) {
|
|
69866
|
-
(0, import_node_fs35.rmSync)(ws.configPath, { force: true });
|
|
69867
|
-
}
|
|
69868
|
-
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
69869
|
-
const label = labelMatch?.[1];
|
|
69870
|
-
if (label) {
|
|
69871
|
-
const stillUsed = listWorkspaces(latticeRoot).some(
|
|
69872
|
-
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
69873
|
-
);
|
|
69874
|
-
if (!stillUsed) {
|
|
69875
|
-
try {
|
|
69876
|
-
deleteDbCredential(label);
|
|
69877
|
-
} catch {
|
|
69878
|
-
}
|
|
69879
|
-
}
|
|
69880
|
-
}
|
|
69881
|
-
}
|
|
70593
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
69882
70594
|
} catch (e6) {
|
|
69883
70595
|
sendJson(
|
|
69884
70596
|
res,
|
|
@@ -70344,7 +71056,9 @@ ${e6.stack ?? ""}`
|
|
|
70344
71056
|
}
|
|
70345
71057
|
}
|
|
70346
71058
|
};
|
|
70347
|
-
if (options.
|
|
71059
|
+
if (options.updateServiceFactory) {
|
|
71060
|
+
updateService = options.updateServiceFactory(broadcast);
|
|
71061
|
+
} else if (options.selfUpdate && guiVersion) {
|
|
70348
71062
|
updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
|
|
70349
71063
|
}
|
|
70350
71064
|
const handleEventStream = (ws) => {
|