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.js
CHANGED
|
@@ -231,10 +231,12 @@ function readManifest(outputDir) {
|
|
|
231
231
|
function writeManifest(outputDir, manifest) {
|
|
232
232
|
atomicWrite(manifestPath(outputDir), JSON.stringify(manifest, null, 2));
|
|
233
233
|
}
|
|
234
|
+
var TEMPLATE_VERSION;
|
|
234
235
|
var init_manifest = __esm({
|
|
235
236
|
"src/lifecycle/manifest.ts"() {
|
|
236
237
|
"use strict";
|
|
237
238
|
init_writer();
|
|
239
|
+
TEMPLATE_VERSION = 1;
|
|
238
240
|
}
|
|
239
241
|
});
|
|
240
242
|
|
|
@@ -268,6 +270,126 @@ var init_adapter = __esm({
|
|
|
268
270
|
}
|
|
269
271
|
});
|
|
270
272
|
|
|
273
|
+
// src/lifecycle/render-cursor.ts
|
|
274
|
+
function markToString(v2) {
|
|
275
|
+
if (v2 == null) return null;
|
|
276
|
+
if (v2 instanceof Date) return v2.toISOString();
|
|
277
|
+
if (typeof v2 === "string") return v2;
|
|
278
|
+
if (typeof v2 === "number" || typeof v2 === "bigint" || typeof v2 === "boolean") return String(v2);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function padNumericMark(v2) {
|
|
282
|
+
const s2 = markToString(v2);
|
|
283
|
+
if (s2 == null) return null;
|
|
284
|
+
if (/^\d+$/.test(s2)) return s2.padStart(20, "0");
|
|
285
|
+
return s2;
|
|
286
|
+
}
|
|
287
|
+
async function changelogExists(adapter) {
|
|
288
|
+
if (adapter.dialect === "postgres") {
|
|
289
|
+
const row2 = await getAsyncOrSync(
|
|
290
|
+
adapter,
|
|
291
|
+
`SELECT to_regclass('__lattice_changelog') AS reg`
|
|
292
|
+
);
|
|
293
|
+
return !!row2 && row2.reg != null;
|
|
294
|
+
}
|
|
295
|
+
const row = await getAsyncOrSync(
|
|
296
|
+
adapter,
|
|
297
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='__lattice_changelog'`
|
|
298
|
+
);
|
|
299
|
+
return !!row;
|
|
300
|
+
}
|
|
301
|
+
async function changelogMark(adapter) {
|
|
302
|
+
try {
|
|
303
|
+
if (!await changelogExists(adapter)) return null;
|
|
304
|
+
const col = adapter.dialect === "postgres" ? "seq" : "rowid";
|
|
305
|
+
const row = await getAsyncOrSync(
|
|
306
|
+
adapter,
|
|
307
|
+
`SELECT MAX(${col}) AS m FROM __lattice_changelog`
|
|
308
|
+
);
|
|
309
|
+
return padNumericMark(row?.m);
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function sharingMarks(adapter) {
|
|
315
|
+
if (adapter.dialect !== "postgres") return { grants: null, owners: null };
|
|
316
|
+
try {
|
|
317
|
+
const reg = await getAsyncOrSync(
|
|
318
|
+
adapter,
|
|
319
|
+
`SELECT to_regclass('__lattice_changes') AS reg`
|
|
320
|
+
);
|
|
321
|
+
const hasFeed = !!reg && reg.reg != null;
|
|
322
|
+
if (hasFeed) {
|
|
323
|
+
const row = await getAsyncOrSync(
|
|
324
|
+
adapter,
|
|
325
|
+
`SELECT COUNT(*) AS n, MAX(seq) AS m FROM lattice_changes_since(0, 1000)`
|
|
326
|
+
);
|
|
327
|
+
const digest = digestOf(row?.n, row?.m);
|
|
328
|
+
return { grants: digest, owners: digest };
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
let owners = null;
|
|
333
|
+
let grants = null;
|
|
334
|
+
try {
|
|
335
|
+
const o3 = await getAsyncOrSync(
|
|
336
|
+
adapter,
|
|
337
|
+
`SELECT COUNT(*) AS n, MAX(updated_at) AS m FROM __lattice_owners`
|
|
338
|
+
);
|
|
339
|
+
owners = digestOf(o3?.n, o3?.m);
|
|
340
|
+
} catch {
|
|
341
|
+
owners = null;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const g6 = await getAsyncOrSync(
|
|
345
|
+
adapter,
|
|
346
|
+
`SELECT COUNT(*) AS n, MAX(granted_at) AS m FROM __lattice_row_grants`
|
|
347
|
+
);
|
|
348
|
+
grants = digestOf(g6?.n, g6?.m);
|
|
349
|
+
} catch {
|
|
350
|
+
grants = null;
|
|
351
|
+
}
|
|
352
|
+
return { grants, owners };
|
|
353
|
+
}
|
|
354
|
+
function digestOf(count, max) {
|
|
355
|
+
const n3 = padNumericMark(count);
|
|
356
|
+
if (n3 == null) return null;
|
|
357
|
+
const m4 = markToString(max) ?? "";
|
|
358
|
+
return `${n3}#${m4}`;
|
|
359
|
+
}
|
|
360
|
+
async function computeRenderCursor(adapter) {
|
|
361
|
+
try {
|
|
362
|
+
const [changelog, sharing] = await Promise.all([changelogMark(adapter), sharingMarks(adapter)]);
|
|
363
|
+
return { changelog, grants: sharing.grants, owners: sharing.owners };
|
|
364
|
+
} catch {
|
|
365
|
+
return { ...EMPTY_CURSOR };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function cursorIsFresh(recorded, live, templateVersion = TEMPLATE_VERSION) {
|
|
369
|
+
if (recorded == null) return false;
|
|
370
|
+
if (recorded.templateVersion !== templateVersion) return false;
|
|
371
|
+
const rc = recorded.cursor;
|
|
372
|
+
if (rc == null) return false;
|
|
373
|
+
if (!fieldFresh(rc.changelog, live.changelog, (r6, l4) => l4 <= r6)) return false;
|
|
374
|
+
if (!fieldFresh(rc.grants, live.grants, (r6, l4) => l4 === r6)) return false;
|
|
375
|
+
if (!fieldFresh(rc.owners, live.owners, (r6, l4) => l4 === r6)) return false;
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
function fieldFresh(recorded, live, ok) {
|
|
379
|
+
if (recorded == null && live == null) return true;
|
|
380
|
+
if (recorded == null || live == null) return false;
|
|
381
|
+
return ok(recorded, live);
|
|
382
|
+
}
|
|
383
|
+
var EMPTY_CURSOR;
|
|
384
|
+
var init_render_cursor = __esm({
|
|
385
|
+
"src/lifecycle/render-cursor.ts"() {
|
|
386
|
+
"use strict";
|
|
387
|
+
init_adapter();
|
|
388
|
+
init_manifest();
|
|
389
|
+
EMPTY_CURSOR = { changelog: null, grants: null, owners: null };
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
271
393
|
// src/db/sqlite.ts
|
|
272
394
|
import Database from "better-sqlite3";
|
|
273
395
|
var SQLiteAdapter;
|
|
@@ -2310,7 +2432,18 @@ var init_concurrency = __esm({
|
|
|
2310
2432
|
// src/render/engine.ts
|
|
2311
2433
|
import { join as join4, basename, isAbsolute, resolve, sep } from "path";
|
|
2312
2434
|
import { mkdirSync as mkdirSync2, existsSync as existsSync4, copyFileSync as copyFileSync2 } from "fs";
|
|
2313
|
-
|
|
2435
|
+
function entityContentChanged(fresh, prior) {
|
|
2436
|
+
const freshKeys = Object.keys(fresh);
|
|
2437
|
+
const priorKeys = Object.keys(prior);
|
|
2438
|
+
if (freshKeys.length !== priorKeys.length) return true;
|
|
2439
|
+
for (const k6 of freshKeys) {
|
|
2440
|
+
const p3 = prior[k6];
|
|
2441
|
+
if (p3 == null) return true;
|
|
2442
|
+
if (p3.hash === "" || p3.hash !== fresh[k6]?.hash) return true;
|
|
2443
|
+
}
|
|
2444
|
+
return false;
|
|
2445
|
+
}
|
|
2446
|
+
var DeferredTableProgress, YIELD_EVERY_ENTITIES, RENDER_TABLE_CONCURRENCY, NOOP_RENDER, RenderEngine;
|
|
2314
2447
|
var init_engine = __esm({
|
|
2315
2448
|
"src/render/engine.ts"() {
|
|
2316
2449
|
"use strict";
|
|
@@ -2320,9 +2453,44 @@ var init_engine = __esm({
|
|
|
2320
2453
|
init_entity_query();
|
|
2321
2454
|
init_entity_templates();
|
|
2322
2455
|
init_manifest();
|
|
2456
|
+
init_render_cursor();
|
|
2323
2457
|
init_cleanup();
|
|
2324
2458
|
init_progress();
|
|
2325
2459
|
init_concurrency();
|
|
2460
|
+
DeferredTableProgress = class {
|
|
2461
|
+
constructor(throttle) {
|
|
2462
|
+
this.throttle = throttle;
|
|
2463
|
+
}
|
|
2464
|
+
changed = false;
|
|
2465
|
+
pendingStart = null;
|
|
2466
|
+
/** Buffer the `table-start` event; emitted only if/when the table changes. */
|
|
2467
|
+
start(event) {
|
|
2468
|
+
if (this.changed) {
|
|
2469
|
+
this.throttle.force(event);
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
this.pendingStart = event;
|
|
2473
|
+
}
|
|
2474
|
+
/** Mark that an entity's content changed — flush the held `table-start` once. */
|
|
2475
|
+
markChanged() {
|
|
2476
|
+
if (this.changed) return;
|
|
2477
|
+
this.changed = true;
|
|
2478
|
+
if (this.pendingStart) {
|
|
2479
|
+
this.throttle.force(this.pendingStart);
|
|
2480
|
+
this.pendingStart = null;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
/** Coalesced per-entity progress — dropped entirely until the table changed. */
|
|
2484
|
+
tick(event) {
|
|
2485
|
+
if (!this.changed) return;
|
|
2486
|
+
this.throttle.tick(event);
|
|
2487
|
+
}
|
|
2488
|
+
/** Lifecycle event (`table-done`) — emitted only if the table changed. */
|
|
2489
|
+
force(event) {
|
|
2490
|
+
if (!this.changed) return;
|
|
2491
|
+
this.throttle.force(event);
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2326
2494
|
YIELD_EVERY_ENTITIES = 200;
|
|
2327
2495
|
RENDER_TABLE_CONCURRENCY = 4;
|
|
2328
2496
|
NOOP_RENDER = () => "";
|
|
@@ -2439,20 +2607,23 @@ var init_engine = __esm({
|
|
|
2439
2607
|
}
|
|
2440
2608
|
const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
|
|
2441
2609
|
const filePath = join4(outputDir, def.outputFile);
|
|
2442
|
-
|
|
2610
|
+
const wrote = atomicWrite(filePath, content);
|
|
2611
|
+
if (wrote) {
|
|
2443
2612
|
filesWritten.push(filePath);
|
|
2444
2613
|
} else {
|
|
2445
2614
|
counters.skipped++;
|
|
2446
2615
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2616
|
+
if (wrote) {
|
|
2617
|
+
throttle.force({
|
|
2618
|
+
kind: "table-done",
|
|
2619
|
+
table: name,
|
|
2620
|
+
entitiesRendered: rows.length,
|
|
2621
|
+
entitiesTotal: rows.length,
|
|
2622
|
+
tableIndex: 0,
|
|
2623
|
+
tableCount: 0,
|
|
2624
|
+
pct: 100
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2456
2627
|
}
|
|
2457
2628
|
for (const [name, def] of this._schema.getMultis()) {
|
|
2458
2629
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
@@ -2466,32 +2637,38 @@ var init_engine = __esm({
|
|
|
2466
2637
|
tables[t8] = await this._schema.queryTable(this._adapter, t8, this._readRel);
|
|
2467
2638
|
}
|
|
2468
2639
|
}
|
|
2640
|
+
let wroteAny = false;
|
|
2469
2641
|
for (const key of keys) {
|
|
2470
2642
|
const content = def.render(key, tables);
|
|
2471
2643
|
const filePath = join4(outputDir, def.outputFile(key));
|
|
2472
2644
|
if (atomicWrite(filePath, content)) {
|
|
2473
2645
|
filesWritten.push(filePath);
|
|
2646
|
+
wroteAny = true;
|
|
2474
2647
|
} else {
|
|
2475
2648
|
counters.skipped++;
|
|
2476
2649
|
}
|
|
2477
2650
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2651
|
+
if (wroteAny) {
|
|
2652
|
+
throttle.force({
|
|
2653
|
+
kind: "table-done",
|
|
2654
|
+
table: name,
|
|
2655
|
+
entitiesRendered: keys.length,
|
|
2656
|
+
entitiesTotal: keys.length,
|
|
2657
|
+
tableIndex: 0,
|
|
2658
|
+
tableCount: 0,
|
|
2659
|
+
pct: 100
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2487
2662
|
}
|
|
2663
|
+
const priorManifest = readManifest(outputDir);
|
|
2488
2664
|
const entityContextManifest = await this._renderEntityContexts(
|
|
2489
2665
|
outputDir,
|
|
2490
2666
|
filesWritten,
|
|
2491
2667
|
counters,
|
|
2492
2668
|
throttle,
|
|
2493
2669
|
signal,
|
|
2494
|
-
opts.changedTables
|
|
2670
|
+
opts.changedTables,
|
|
2671
|
+
priorManifest
|
|
2495
2672
|
);
|
|
2496
2673
|
if (entityContextManifest === null) {
|
|
2497
2674
|
return this._abortedResult(filesWritten, counters, start);
|
|
@@ -2502,10 +2679,13 @@ var init_engine = __esm({
|
|
|
2502
2679
|
const prev = readManifest(outputDir);
|
|
2503
2680
|
entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
|
|
2504
2681
|
}
|
|
2682
|
+
const cursor = await computeRenderCursor(this._adapter);
|
|
2505
2683
|
writeManifest(outputDir, {
|
|
2506
2684
|
version: 2,
|
|
2507
2685
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2508
|
-
entityContexts
|
|
2686
|
+
entityContexts,
|
|
2687
|
+
templateVersion: TEMPLATE_VERSION,
|
|
2688
|
+
cursor
|
|
2509
2689
|
});
|
|
2510
2690
|
}
|
|
2511
2691
|
const result = {
|
|
@@ -2571,7 +2751,7 @@ var init_engine = __esm({
|
|
|
2571
2751
|
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
2572
2752
|
* via `signal`.
|
|
2573
2753
|
*/
|
|
2574
|
-
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
|
|
2754
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables, priorManifest) {
|
|
2575
2755
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
2576
2756
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
2577
2757
|
if (d6.protected) protectedTables.add(t8);
|
|
@@ -2590,8 +2770,10 @@ var init_engine = __esm({
|
|
|
2590
2770
|
const baseRows = await this._schema.queryTable(this._adapter, table, this._readRel);
|
|
2591
2771
|
const allRows = this._foldRows ? await this._foldRows(table, baseRows) : baseRows;
|
|
2592
2772
|
const directoryRoot = def.directoryRoot ?? table;
|
|
2773
|
+
const deferred = new DeferredTableProgress(throttle);
|
|
2774
|
+
const priorEntities = priorManifest?.entityContexts[table]?.entities ?? {};
|
|
2593
2775
|
const entitiesTotal = allRows.length;
|
|
2594
|
-
|
|
2776
|
+
deferred.start({
|
|
2595
2777
|
kind: "table-start",
|
|
2596
2778
|
table,
|
|
2597
2779
|
entitiesRendered: 0,
|
|
@@ -2600,6 +2782,7 @@ var init_engine = __esm({
|
|
|
2600
2782
|
tableCount,
|
|
2601
2783
|
pct: 0
|
|
2602
2784
|
});
|
|
2785
|
+
if (Object.keys(priorEntities).length !== entitiesTotal) deferred.markChanged();
|
|
2603
2786
|
const manifestEntry = {
|
|
2604
2787
|
directoryRoot,
|
|
2605
2788
|
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
@@ -2715,8 +2898,10 @@ var init_engine = __esm({
|
|
|
2715
2898
|
}
|
|
2716
2899
|
}
|
|
2717
2900
|
manifestEntry.entities[slug] = entityFileHashes;
|
|
2901
|
+
const priorHashes = normalizeEntityFiles(priorEntities[slug] ?? {});
|
|
2902
|
+
if (entityContentChanged(entityFileHashes, priorHashes)) deferred.markChanged();
|
|
2718
2903
|
const entitiesRendered = i6 + 1;
|
|
2719
|
-
|
|
2904
|
+
deferred.tick({
|
|
2720
2905
|
kind: "table-progress",
|
|
2721
2906
|
table,
|
|
2722
2907
|
entitiesRendered,
|
|
@@ -2726,7 +2911,7 @@ var init_engine = __esm({
|
|
|
2726
2911
|
pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
|
|
2727
2912
|
});
|
|
2728
2913
|
}
|
|
2729
|
-
|
|
2914
|
+
deferred.force({
|
|
2730
2915
|
kind: "table-done",
|
|
2731
2916
|
table,
|
|
2732
2917
|
entitiesRendered: entitiesTotal,
|
|
@@ -4166,6 +4351,22 @@ function deleteAssistantCredential(kind) {
|
|
|
4166
4351
|
void _removed;
|
|
4167
4352
|
saveAssistantCredentials(rest);
|
|
4168
4353
|
}
|
|
4354
|
+
function isAssistantCredentialCleared(kind) {
|
|
4355
|
+
return loadAssistantCredentials()[CLEARED_SENTINEL_PREFIX + kind] === "1";
|
|
4356
|
+
}
|
|
4357
|
+
function setAssistantCredentialCleared(kind) {
|
|
4358
|
+
const creds = loadAssistantCredentials();
|
|
4359
|
+
creds[CLEARED_SENTINEL_PREFIX + kind] = "1";
|
|
4360
|
+
saveAssistantCredentials(creds);
|
|
4361
|
+
}
|
|
4362
|
+
function clearAssistantCredentialCleared(kind) {
|
|
4363
|
+
const creds = loadAssistantCredentials();
|
|
4364
|
+
const sentinel = CLEARED_SENTINEL_PREFIX + kind;
|
|
4365
|
+
if (!(sentinel in creds)) return;
|
|
4366
|
+
const { [sentinel]: _removed, ...rest } = creds;
|
|
4367
|
+
void _removed;
|
|
4368
|
+
saveAssistantCredentials(rest);
|
|
4369
|
+
}
|
|
4169
4370
|
function ensureKeysDir() {
|
|
4170
4371
|
const dir = join9(ensureConfigDir(), KEYS_SUBDIR);
|
|
4171
4372
|
if (!existsSync9(dir)) {
|
|
@@ -4210,7 +4411,7 @@ function deleteToken(label) {
|
|
|
4210
4411
|
const path2 = join9(ensureKeysDir(), label + TOKEN_EXT);
|
|
4211
4412
|
if (existsSync9(path2)) unlinkSync3(path2);
|
|
4212
4413
|
}
|
|
4213
|
-
var 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;
|
|
4414
|
+
var 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;
|
|
4214
4415
|
var init_user_config = __esm({
|
|
4215
4416
|
"src/framework/user-config.ts"() {
|
|
4216
4417
|
"use strict";
|
|
@@ -4233,6 +4434,7 @@ var init_user_config = __esm({
|
|
|
4233
4434
|
lockDepthInProcess = 0;
|
|
4234
4435
|
S3_CONFIG_FILENAME = "s3-config.enc";
|
|
4235
4436
|
ASSISTANT_CREDENTIALS_FILENAME = "assistant-credentials.enc";
|
|
4437
|
+
CLEARED_SENTINEL_PREFIX = "__cleared__:";
|
|
4236
4438
|
KEYS_SUBDIR = "keys";
|
|
4237
4439
|
TOKEN_EXT = ".token";
|
|
4238
4440
|
}
|
|
@@ -4329,14 +4531,6 @@ function resolveDbPath(raw, configDir2) {
|
|
|
4329
4531
|
}
|
|
4330
4532
|
return resolve2(configDir2, raw);
|
|
4331
4533
|
}
|
|
4332
|
-
function warnDeprecatedRef(entity, field, target) {
|
|
4333
|
-
const key = `${entity}.${field}`;
|
|
4334
|
-
if (warnedDeprecatedRefs.has(key)) return;
|
|
4335
|
-
warnedDeprecatedRefs.add(key);
|
|
4336
|
-
console.warn(
|
|
4337
|
-
`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.`
|
|
4338
|
-
);
|
|
4339
|
-
}
|
|
4340
4534
|
function entityToTableDef(entityName, entity) {
|
|
4341
4535
|
const rawFields = entity.fields;
|
|
4342
4536
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -4363,7 +4557,6 @@ function entityToTableDef(entityName, entity) {
|
|
|
4363
4557
|
table: field.ref,
|
|
4364
4558
|
foreignKey: fieldName
|
|
4365
4559
|
};
|
|
4366
|
-
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
4367
4560
|
}
|
|
4368
4561
|
}
|
|
4369
4562
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
@@ -4520,12 +4713,10 @@ function parseEntityContexts(entityContexts) {
|
|
|
4520
4713
|
}
|
|
4521
4714
|
return result;
|
|
4522
4715
|
}
|
|
4523
|
-
var warnedDeprecatedRefs;
|
|
4524
4716
|
var init_parser = __esm({
|
|
4525
4717
|
"src/config/parser.ts"() {
|
|
4526
4718
|
"use strict";
|
|
4527
4719
|
init_user_config();
|
|
4528
|
-
warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
4529
4720
|
}
|
|
4530
4721
|
});
|
|
4531
4722
|
|
|
@@ -5244,6 +5435,7 @@ var init_lattice = __esm({
|
|
|
5244
5435
|
init_shred();
|
|
5245
5436
|
init_encryption();
|
|
5246
5437
|
init_manifest();
|
|
5438
|
+
init_render_cursor();
|
|
5247
5439
|
init_adapter();
|
|
5248
5440
|
init_sqlite();
|
|
5249
5441
|
init_postgres();
|
|
@@ -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,
|
|
@@ -47863,6 +48120,111 @@ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
|
47863
48120
|
AND g."pk" = ANY(p_pks)
|
|
47864
48121
|
AND o."owner_role" = session_user;
|
|
47865
48122
|
$fn$;
|
|
48123
|
+
|
|
48124
|
+
-- Add a column to a user table AS THE OWNER, on behalf of a scoped member. A
|
|
48125
|
+
-- member's role has no CREATE/ALTER on the schema (the bootstrap REVOKEs CREATE
|
|
48126
|
+
-- from PUBLIC), so a member's GUI "add a field" write (createRow/updateRow with a
|
|
48127
|
+
-- field the table lacks) cannot run ALTER TABLE itself. This SECURITY DEFINER
|
|
48128
|
+
-- helper performs that ALTER \u2014 and the masking-view regen \u2014 with the owner's
|
|
48129
|
+
-- rights, so member-added columns behave identically to owner-added ones.
|
|
48130
|
+
--
|
|
48131
|
+
-- Injection-safe + minimal: p_table must be an existing BASE table in the current
|
|
48132
|
+
-- schema (rejected otherwise); p_type is whitelisted against the exact set the
|
|
48133
|
+
-- library's addColumn emits for an auto-added column (TEXT / INTEGER / REAL, plus
|
|
48134
|
+
-- BOOLEAN) \u2014 never interpolated raw; both identifiers go through %I (quote_ident).
|
|
48135
|
+
-- Member-callable (granted EXECUTE to the member group), but it can only widen the
|
|
48136
|
+
-- schema, never read or alter another member's data.
|
|
48137
|
+
CREATE OR REPLACE FUNCTION lattice_member_add_column(p_table text, p_column text, p_type text)
|
|
48138
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
48139
|
+
DECLARE
|
|
48140
|
+
v_type text;
|
|
48141
|
+
v_view text := p_table || '_v';
|
|
48142
|
+
v_has_view boolean;
|
|
48143
|
+
v_pk_expr text;
|
|
48144
|
+
v_select text;
|
|
48145
|
+
BEGIN
|
|
48146
|
+
-- Never alter internal bookkeeping tables (names start with "_"). The GUI only
|
|
48147
|
+
-- ever calls this for a user entity table; rejecting the rest is defense-in-depth
|
|
48148
|
+
-- against a member invoking the function directly against ownership/audit/policy
|
|
48149
|
+
-- tables.
|
|
48150
|
+
IF left(p_table, 1) = '_' THEN
|
|
48151
|
+
RAISE EXCEPTION 'lattice: cannot add a column to internal table "%"', p_table;
|
|
48152
|
+
END IF;
|
|
48153
|
+
|
|
48154
|
+
-- p_table must be a real base table in THIS schema (search_path is pinned to the
|
|
48155
|
+
-- cloud schema by pinDefinerSearchPath, so to_regclass resolves there).
|
|
48156
|
+
IF NOT EXISTS (
|
|
48157
|
+
SELECT 1 FROM pg_class c
|
|
48158
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48159
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND c.relkind = 'r'
|
|
48160
|
+
) THEN
|
|
48161
|
+
RAISE EXCEPTION 'lattice: no such table "%"', p_table;
|
|
48162
|
+
END IF;
|
|
48163
|
+
|
|
48164
|
+
-- Whitelist the column type. These are exactly the specs addColumn's
|
|
48165
|
+
-- inferColumnType produces (TEXT / INTEGER / REAL); BOOLEAN is allowed too.
|
|
48166
|
+
-- Anything else is rejected \u2014 the type is spliced as %s (NOT %I), so it must be
|
|
48167
|
+
-- a known-safe literal and never caller-controlled SQL.
|
|
48168
|
+
v_type := upper(btrim(p_type));
|
|
48169
|
+
IF v_type NOT IN ('TEXT', 'INTEGER', 'REAL', 'BOOLEAN') THEN
|
|
48170
|
+
RAISE EXCEPTION 'lattice: unsupported column type "%"', p_type;
|
|
48171
|
+
END IF;
|
|
48172
|
+
|
|
48173
|
+
EXECUTE format('ALTER TABLE %I ADD COLUMN IF NOT EXISTS %I %s', p_table, p_column, v_type);
|
|
48174
|
+
|
|
48175
|
+
-- If the table is cell-masked (a "<table>_v" view exists, because some column has
|
|
48176
|
+
-- an audience), the view selects an explicit column list \u2014 so a new column is
|
|
48177
|
+
-- invisible to members until the view is regenerated. Rebuild it the same way the
|
|
48178
|
+
-- owner path (audienceViewSql / regenerateAudienceViewFromDb) does: pass every
|
|
48179
|
+
-- column through except those with an 'owner' audience in __lattice_column_policy
|
|
48180
|
+
-- (CASE WHEN lattice_is_owner(...) THEN col END), re-apply row visibility with
|
|
48181
|
+
-- WHERE lattice_row_visible(table, pk), and keep the member SELECT grant on the
|
|
48182
|
+
-- view. Unmasked tables need no regen \u2014 the member group's table-level base grant
|
|
48183
|
+
-- already covers the new column.
|
|
48184
|
+
SELECT EXISTS (
|
|
48185
|
+
SELECT 1 FROM pg_class c
|
|
48186
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48187
|
+
WHERE n.nspname = current_schema() AND c.relname = v_view AND c.relkind = 'v'
|
|
48188
|
+
) INTO v_has_view;
|
|
48189
|
+
|
|
48190
|
+
IF v_has_view THEN
|
|
48191
|
+
-- Canonical pk expression: CAST("col" AS TEXT) joined by TAB (chr(9)) \u2014 the
|
|
48192
|
+
-- same serialization the RLS policies + audienceViewSql use.
|
|
48193
|
+
SELECT string_agg(format('CAST(%I AS TEXT)', a.attname), ' || chr(9) || '
|
|
48194
|
+
ORDER BY array_position(i.indkey, a.attnum))
|
|
48195
|
+
INTO v_pk_expr
|
|
48196
|
+
FROM pg_index i
|
|
48197
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
48198
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48199
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
|
|
48200
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND i.indisprimary;
|
|
48201
|
+
IF v_pk_expr IS NULL THEN
|
|
48202
|
+
RAISE EXCEPTION 'lattice: cannot regenerate mask view for "%": no primary key', p_table;
|
|
48203
|
+
END IF;
|
|
48204
|
+
|
|
48205
|
+
-- Build the masked SELECT list in column order, applying the per-column policy.
|
|
48206
|
+
SELECT string_agg(
|
|
48207
|
+
CASE
|
|
48208
|
+
WHEN cp."audience" = 'owner'
|
|
48209
|
+
THEN format('CASE WHEN lattice_is_owner(%L, %s) THEN %I END AS %I',
|
|
48210
|
+
p_table, v_pk_expr, cols.column_name, cols.column_name)
|
|
48211
|
+
ELSE format('%I', cols.column_name)
|
|
48212
|
+
END,
|
|
48213
|
+
', ' ORDER BY cols.ordinal_position)
|
|
48214
|
+
INTO v_select
|
|
48215
|
+
FROM information_schema.columns cols
|
|
48216
|
+
LEFT JOIN "__lattice_column_policy" cp
|
|
48217
|
+
ON cp."table_name" = p_table AND cp."column_name" = cols.column_name
|
|
48218
|
+
AND cp."audience" NOT IN ('', 'everyone', 'row-audience')
|
|
48219
|
+
WHERE cols.table_schema = current_schema() AND cols.table_name = p_table;
|
|
48220
|
+
|
|
48221
|
+
EXECUTE format(
|
|
48222
|
+
'CREATE OR REPLACE VIEW %I AS SELECT %s FROM %I WHERE lattice_row_visible(%L, %s)',
|
|
48223
|
+
v_view, v_select, p_table, p_table, v_pk_expr);
|
|
48224
|
+
EXECUTE format('GRANT SELECT ON %I TO ${MEMBER_GROUP}', v_view);
|
|
48225
|
+
END IF;
|
|
48226
|
+
END $fn$;
|
|
48227
|
+
GRANT EXECUTE ON FUNCTION lattice_member_add_column(text, text, text) TO ${MEMBER_GROUP};
|
|
47866
48228
|
`;
|
|
47867
48229
|
}
|
|
47868
48230
|
});
|
|
@@ -47973,6 +48335,11 @@ async function revokeRow(db, table, pk, grantee) {
|
|
|
47973
48335
|
assertPg(db);
|
|
47974
48336
|
await runAsyncOrSync(db.adapter, `SELECT lattice_revoke_row(?, ?, ?)`, [table, pk, grantee]);
|
|
47975
48337
|
}
|
|
48338
|
+
async function batchRowGrants(db, table, pk, grant, revoke) {
|
|
48339
|
+
assertPg(db);
|
|
48340
|
+
for (const grantee of grant) await grantRow(db, table, pk, grantee);
|
|
48341
|
+
for (const grantee of revoke) await revokeRow(db, table, pk, grantee);
|
|
48342
|
+
}
|
|
47976
48343
|
async function revokeMemberRole(db, role) {
|
|
47977
48344
|
assertPg(db);
|
|
47978
48345
|
if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
|
|
@@ -49075,18 +49442,9 @@ function sessionUndoneFilters(undone, sessionId) {
|
|
|
49075
49442
|
if (sessionId) filters.push({ col: "session_id", op: "eq", val: sessionId });
|
|
49076
49443
|
return filters;
|
|
49077
49444
|
}
|
|
49078
|
-
|
|
49079
|
-
|
|
49080
|
-
filters: sessionUndoneFilters(1, sessionId)
|
|
49081
|
-
});
|
|
49082
|
-
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
49083
|
-
await db.insert("_lattice_gui_audit", {
|
|
49445
|
+
function buildAuditRow(table, rowId, op, before, after, sessionId, editTs) {
|
|
49446
|
+
return {
|
|
49084
49447
|
id: crypto.randomUUID(),
|
|
49085
|
-
// Set ts explicitly (don't rely on the column DEFAULT — it uses the
|
|
49086
|
-
// SQLite-only `strftime(...)`, which doesn't yield a parseable ISO string
|
|
49087
|
-
// on Postgres, so cloud history rendered "Invalid Date"). #4.6 — honor the
|
|
49088
|
-
// originating client's validated edit time when present (an offline edit
|
|
49089
|
-
// replayed later records when it was MADE, not when it synced), else now().
|
|
49090
49448
|
ts: sanitizeEditTs(editTs) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
49091
49449
|
table_name: table,
|
|
49092
49450
|
row_id: rowId,
|
|
@@ -49095,7 +49453,9 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
49095
49453
|
after_json: after ? JSON.stringify(after) : null,
|
|
49096
49454
|
undone: 0,
|
|
49097
49455
|
session_id: sessionId ?? null
|
|
49098
|
-
}
|
|
49456
|
+
};
|
|
49457
|
+
}
|
|
49458
|
+
function publishMutationFeed(feed, table, rowId, op, before, after, source) {
|
|
49099
49459
|
const labelRow = op === "delete" ? before : after;
|
|
49100
49460
|
feed.publish({
|
|
49101
49461
|
table,
|
|
@@ -49105,17 +49465,28 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
49105
49465
|
summary: feedSummary(op, table, labelRow)
|
|
49106
49466
|
});
|
|
49107
49467
|
}
|
|
49108
|
-
function
|
|
49109
|
-
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
49110
|
-
}
|
|
49111
|
-
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
49468
|
+
async function purgeRedoStack(db, sessionId) {
|
|
49112
49469
|
const undone = await db.query("_lattice_gui_audit", {
|
|
49113
49470
|
filters: sessionUndoneFilters(1, sessionId)
|
|
49114
49471
|
});
|
|
49115
49472
|
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
49473
|
+
}
|
|
49474
|
+
async function appendAudit(db, feed, table, rowId, op, before, after, source = "gui", sessionId, editTs) {
|
|
49475
|
+
await purgeRedoStack(db, sessionId);
|
|
49476
|
+
await db.insert(
|
|
49477
|
+
"_lattice_gui_audit",
|
|
49478
|
+
buildAuditRow(table, rowId, op, before, after, sessionId, editTs)
|
|
49479
|
+
);
|
|
49480
|
+
publishMutationFeed(feed, table, rowId, op, before, after, source);
|
|
49481
|
+
}
|
|
49482
|
+
function isSchemaOp(operation2) {
|
|
49483
|
+
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
49484
|
+
}
|
|
49485
|
+
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
49486
|
+
await purgeRedoStack(db, sessionId);
|
|
49116
49487
|
await db.insert("_lattice_gui_audit", {
|
|
49117
49488
|
id: crypto.randomUUID(),
|
|
49118
|
-
// Explicit ISO ts — see
|
|
49489
|
+
// Explicit ISO ts — see buildAuditRow (the SQLite-only strftime DEFAULT
|
|
49119
49490
|
// rendered "Invalid Date" on the Postgres/cloud path).
|
|
49120
49491
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49121
49492
|
table_name: table,
|
|
@@ -49150,7 +49521,7 @@ async function ensureColumns(db, table, values) {
|
|
|
49150
49521
|
const added = Object.keys(values).filter((k6) => !(k6 in existing));
|
|
49151
49522
|
if (added.length === 0) return [];
|
|
49152
49523
|
for (const col of added) await db.addColumn(table, col, inferColumnType(values[col]));
|
|
49153
|
-
if (db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
49524
|
+
if (!db.isCloudMemberOpen() && db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
49154
49525
|
const cols = db.getRegisteredColumns(table);
|
|
49155
49526
|
const pk = db.getPrimaryKey(table);
|
|
49156
49527
|
if (cols && pk.length > 0) await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
@@ -49272,7 +49643,14 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
49272
49643
|
ctx.clientTs
|
|
49273
49644
|
);
|
|
49274
49645
|
} else {
|
|
49275
|
-
await ctx
|
|
49646
|
+
await hardDelete(ctx, table, id, before);
|
|
49647
|
+
}
|
|
49648
|
+
}
|
|
49649
|
+
async function hardDelete(ctx, table, id, before) {
|
|
49650
|
+
const withClient = ctx.db.adapter.withClient?.bind(ctx.db.adapter);
|
|
49651
|
+
const pkCols = ctx.db.getPrimaryKey(table);
|
|
49652
|
+
const pkCol = pkCols.length === 1 ? pkCols[0] : void 0;
|
|
49653
|
+
if (!withClient || ctx.db.isChangelogTracked(table) || pkCol === void 0) {
|
|
49276
49654
|
await appendAudit(
|
|
49277
49655
|
ctx.db,
|
|
49278
49656
|
ctx.feed,
|
|
@@ -49285,10 +49663,30 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
49285
49663
|
ctx.sessionId,
|
|
49286
49664
|
ctx.clientTs
|
|
49287
49665
|
);
|
|
49666
|
+
await ctx.db.delete(table, id);
|
|
49667
|
+
return;
|
|
49288
49668
|
}
|
|
49669
|
+
const auditRow = buildAuditRow(table, id, "delete", before, null, ctx.sessionId, ctx.clientTs);
|
|
49670
|
+
await purgeRedoStack(ctx.db, ctx.sessionId);
|
|
49671
|
+
const auditCols = AUDIT_COLUMNS.map((c6) => `"${c6}"`).join(", ");
|
|
49672
|
+
const auditPlaceholders = AUDIT_COLUMNS.map(() => "?").join(", ");
|
|
49673
|
+
const auditValues = AUDIT_COLUMNS.map((c6) => auditRow[c6]);
|
|
49674
|
+
const pkColQuoted = pkCol.replace(/"/g, '""');
|
|
49675
|
+
await withClient(async (tx) => {
|
|
49676
|
+
await tx.run(
|
|
49677
|
+
`INSERT INTO "_lattice_gui_audit" (${auditCols}) VALUES (${auditPlaceholders})`,
|
|
49678
|
+
auditValues
|
|
49679
|
+
);
|
|
49680
|
+
await tx.run(`DELETE FROM "${table.replace(/"/g, '""')}" WHERE "${pkColQuoted}" = ?`, [id]);
|
|
49681
|
+
});
|
|
49682
|
+
publishMutationFeed(ctx.feed, table, id, "delete", before, null, ctx.source);
|
|
49289
49683
|
}
|
|
49290
|
-
async function linkRows(ctx, table, body) {
|
|
49291
|
-
|
|
49684
|
+
async function linkRows(ctx, table, body, forceVisibility) {
|
|
49685
|
+
if (forceVisibility !== void 0) {
|
|
49686
|
+
await ctx.db.insertForcingVisibility(table, body, forceVisibility);
|
|
49687
|
+
} else {
|
|
49688
|
+
await ctx.db.link(table, body);
|
|
49689
|
+
}
|
|
49292
49690
|
await appendAudit(ctx.db, ctx.feed, table, null, "link", null, body, ctx.source, ctx.sessionId);
|
|
49293
49691
|
}
|
|
49294
49692
|
async function unlinkRows(ctx, table, body) {
|
|
@@ -49426,12 +49824,23 @@ async function revertEntry(ctx, id) {
|
|
|
49426
49824
|
});
|
|
49427
49825
|
return { ok: true, entry };
|
|
49428
49826
|
}
|
|
49429
|
-
var SCHEMA_OP_PREFIX;
|
|
49827
|
+
var AUDIT_COLUMNS, SCHEMA_OP_PREFIX;
|
|
49430
49828
|
var init_mutations = __esm({
|
|
49431
49829
|
"src/gui/mutations.ts"() {
|
|
49432
49830
|
"use strict";
|
|
49433
49831
|
init_cloud_connect();
|
|
49434
49832
|
init_audience();
|
|
49833
|
+
AUDIT_COLUMNS = [
|
|
49834
|
+
"id",
|
|
49835
|
+
"ts",
|
|
49836
|
+
"table_name",
|
|
49837
|
+
"row_id",
|
|
49838
|
+
"operation",
|
|
49839
|
+
"before_json",
|
|
49840
|
+
"after_json",
|
|
49841
|
+
"undone",
|
|
49842
|
+
"session_id"
|
|
49843
|
+
];
|
|
49435
49844
|
SCHEMA_OP_PREFIX = "schema.";
|
|
49436
49845
|
}
|
|
49437
49846
|
});
|
|
@@ -49718,6 +50127,10 @@ async function readMachineCredential(db, kind) {
|
|
|
49718
50127
|
}
|
|
49719
50128
|
return null;
|
|
49720
50129
|
}
|
|
50130
|
+
async function resolveAnthropicKey(db) {
|
|
50131
|
+
if (isAssistantCredentialCleared(CREDENTIALS.anthropic.kind)) return null;
|
|
50132
|
+
return await readMachineCredential(db, CREDENTIALS.anthropic.kind) ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
50133
|
+
}
|
|
49721
50134
|
function getAggressiveness() {
|
|
49722
50135
|
const n3 = readPreferences().aggressiveness;
|
|
49723
50136
|
if (!Number.isFinite(n3)) return DEFAULT_AGGRESSIVENESS;
|
|
@@ -49748,6 +50161,7 @@ async function getVoiceCredential(db) {
|
|
|
49748
50161
|
return null;
|
|
49749
50162
|
}
|
|
49750
50163
|
async function hasCredential(db, name, envVar) {
|
|
50164
|
+
if (isAssistantCredentialCleared(CREDENTIALS[name].kind)) return false;
|
|
49751
50165
|
return Boolean(await readMachineCredential(db, CREDENTIALS[name].kind)) || Boolean(process.env[envVar]);
|
|
49752
50166
|
}
|
|
49753
50167
|
async function resolveClaudeAuth(db) {
|
|
@@ -49770,7 +50184,7 @@ async function resolveClaudeAuth(db) {
|
|
|
49770
50184
|
} catch {
|
|
49771
50185
|
}
|
|
49772
50186
|
}
|
|
49773
|
-
const apiKey = await
|
|
50187
|
+
const apiKey = await resolveAnthropicKey(db);
|
|
49774
50188
|
return apiKey ? { apiKey } : null;
|
|
49775
50189
|
}
|
|
49776
50190
|
async function hasClaudeAuth(db) {
|
|
@@ -49867,6 +50281,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
49867
50281
|
}
|
|
49868
50282
|
const cred = CREDENTIALS[name];
|
|
49869
50283
|
setAssistantCredential(cred.kind, key);
|
|
50284
|
+
clearAssistantCredentialCleared(cred.kind);
|
|
49870
50285
|
if (db) {
|
|
49871
50286
|
for (const row of await liveSecretsOfKind(db, cred.kind)) {
|
|
49872
50287
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -49883,6 +50298,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
49883
50298
|
return true;
|
|
49884
50299
|
}
|
|
49885
50300
|
deleteAssistantCredential(CREDENTIALS[name].kind);
|
|
50301
|
+
setAssistantCredentialCleared(CREDENTIALS[name].kind);
|
|
49886
50302
|
if (db) {
|
|
49887
50303
|
for (const row of await liveSecretsOfKind(db, CREDENTIALS[name].kind)) {
|
|
49888
50304
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -52072,7 +52488,7 @@ function buildSchema(db) {
|
|
|
52072
52488
|
}
|
|
52073
52489
|
return out;
|
|
52074
52490
|
}
|
|
52075
|
-
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false) {
|
|
52491
|
+
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false, privateMode = false) {
|
|
52076
52492
|
if (!text.trim()) return [];
|
|
52077
52493
|
const auth = await resolveClaudeAuth(db);
|
|
52078
52494
|
if (!auth) {
|
|
@@ -52094,6 +52510,7 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52094
52510
|
});
|
|
52095
52511
|
return [];
|
|
52096
52512
|
}
|
|
52513
|
+
const forceVis = privateMode ? "private" : void 0;
|
|
52097
52514
|
const temperature = aggressivenessToTemperature(aggressiveness);
|
|
52098
52515
|
let description = "";
|
|
52099
52516
|
try {
|
|
@@ -52136,11 +52553,16 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52136
52553
|
}
|
|
52137
52554
|
if (jx) {
|
|
52138
52555
|
try {
|
|
52139
|
-
await linkRows(
|
|
52140
|
-
|
|
52141
|
-
|
|
52142
|
-
|
|
52143
|
-
|
|
52556
|
+
await linkRows(
|
|
52557
|
+
mctx,
|
|
52558
|
+
jx.junction,
|
|
52559
|
+
{
|
|
52560
|
+
id: crypto.randomUUID(),
|
|
52561
|
+
[jx.fileFk]: fileId,
|
|
52562
|
+
[jx.otherFk]: m4.id
|
|
52563
|
+
},
|
|
52564
|
+
forceVis
|
|
52565
|
+
);
|
|
52144
52566
|
linkedCount++;
|
|
52145
52567
|
if (created) {
|
|
52146
52568
|
mctx.feed.publish({
|
|
@@ -52199,16 +52621,21 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52199
52621
|
if ("name" in cols && row.name == null) row.name = obj2.label;
|
|
52200
52622
|
if ("title" in cols && row.title == null) row.title = obj2.label;
|
|
52201
52623
|
try {
|
|
52202
|
-
const { id: rowId } = await createRow(mctx, entity, row);
|
|
52624
|
+
const { id: rowId } = await createRow(mctx, entity, row, forceVis);
|
|
52203
52625
|
createdCount++;
|
|
52204
52626
|
const ent = entity;
|
|
52205
52627
|
const jx = junctions.find((j6) => j6.otherTable === ent) ?? (createJunction ? await createJunction(ent) : null);
|
|
52206
52628
|
if (jx) {
|
|
52207
|
-
await linkRows(
|
|
52208
|
-
|
|
52209
|
-
|
|
52210
|
-
|
|
52211
|
-
|
|
52629
|
+
await linkRows(
|
|
52630
|
+
mctx,
|
|
52631
|
+
jx.junction,
|
|
52632
|
+
{
|
|
52633
|
+
id: crypto.randomUUID(),
|
|
52634
|
+
[jx.fileFk]: fileId,
|
|
52635
|
+
[jx.otherFk]: rowId
|
|
52636
|
+
},
|
|
52637
|
+
forceVis
|
|
52638
|
+
);
|
|
52212
52639
|
}
|
|
52213
52640
|
} catch (e6) {
|
|
52214
52641
|
console.warn(`[ingest] create ${entity} from document failed:`, e6.message);
|
|
@@ -52222,12 +52649,17 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52222
52649
|
try {
|
|
52223
52650
|
const title = name.replace(/\.[^./\\]+$/, "").trim() || "Note";
|
|
52224
52651
|
const body = description.length > 0 ? description : text.slice(0, 2e3);
|
|
52225
|
-
const { id: noteId } = await createRow(
|
|
52226
|
-
|
|
52227
|
-
|
|
52228
|
-
|
|
52229
|
-
|
|
52230
|
-
|
|
52652
|
+
const { id: noteId } = await createRow(
|
|
52653
|
+
mctx,
|
|
52654
|
+
"notes",
|
|
52655
|
+
{
|
|
52656
|
+
id: crypto.randomUUID(),
|
|
52657
|
+
title,
|
|
52658
|
+
body,
|
|
52659
|
+
source_file_id: fileId
|
|
52660
|
+
},
|
|
52661
|
+
forceVis
|
|
52662
|
+
);
|
|
52231
52663
|
mctx.feed.publish({
|
|
52232
52664
|
table: "notes",
|
|
52233
52665
|
op: "insert",
|
|
@@ -52341,7 +52773,8 @@ async function ingestUrlAsFile(ctx, rawUrl, opts = {}) {
|
|
|
52341
52773
|
ctx.enrich.createJunction,
|
|
52342
52774
|
ctx.enrich.aggressiveness,
|
|
52343
52775
|
ctx.enrich.createEntity,
|
|
52344
|
-
true
|
|
52776
|
+
true,
|
|
52777
|
+
ctx.privateMode === true
|
|
52345
52778
|
);
|
|
52346
52779
|
}
|
|
52347
52780
|
return {
|
|
@@ -53220,13 +53653,22 @@ function loadSdk() {
|
|
|
53220
53653
|
throw new Error("Could not resolve the Anthropic constructor from '@anthropic-ai/sdk'");
|
|
53221
53654
|
return ctor;
|
|
53222
53655
|
}
|
|
53223
|
-
function
|
|
53224
|
-
const Anthropic = loadSdk();
|
|
53656
|
+
function buildAnthropicConfig(auth) {
|
|
53225
53657
|
const config = {};
|
|
53226
|
-
if (auth.authToken)
|
|
53227
|
-
|
|
53658
|
+
if (auth.authToken) {
|
|
53659
|
+
config.authToken = auth.authToken;
|
|
53660
|
+
config.apiKey = null;
|
|
53661
|
+
} else if (auth.apiKey) {
|
|
53662
|
+
config.apiKey = auth.apiKey;
|
|
53663
|
+
} else {
|
|
53664
|
+
config.apiKey = null;
|
|
53665
|
+
}
|
|
53228
53666
|
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
53229
|
-
|
|
53667
|
+
return config;
|
|
53668
|
+
}
|
|
53669
|
+
function createAnthropicClient(auth) {
|
|
53670
|
+
const Anthropic = loadSdk();
|
|
53671
|
+
const sdk = new Anthropic(buildAnthropicConfig(auth));
|
|
53230
53672
|
return {
|
|
53231
53673
|
async runTurn(params) {
|
|
53232
53674
|
const stream = sdk.messages.stream({
|
|
@@ -54488,8 +54930,14 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
54488
54930
|
},
|
|
54489
54931
|
{
|
|
54490
54932
|
name: "_lattice_gui_audit",
|
|
54491
|
-
|
|
54492
|
-
|
|
54933
|
+
// UPDATE + DELETE are needed by undo/redo/revert (flips an entry's `undone`)
|
|
54934
|
+
// and the redo-stack purge on a new mutation (deletes the session's undone
|
|
54935
|
+
// entries). Safe because enableGuiAuditRls installs per-op UPDATE and DELETE
|
|
54936
|
+
// policies whose USING is `row_id IS NULL OR lattice_row_visible(table_name,
|
|
54937
|
+
// row_id)` — so a member can only update/delete audit rows for entities it can
|
|
54938
|
+
// already see (or schema-level entries that carry no row data).
|
|
54939
|
+
privs: "SELECT, INSERT, UPDATE, DELETE",
|
|
54940
|
+
why: "GUI undo/redo/revert + redo-stack purge + version history; RLS (enableGuiAuditRls) scopes every op to entries whose underlying row the member can see"
|
|
54493
54941
|
},
|
|
54494
54942
|
{
|
|
54495
54943
|
name: "__lattice_user_identity",
|
|
@@ -54890,6 +55338,19 @@ async function normalizeImage(path2, maxBytes) {
|
|
|
54890
55338
|
function renderJpeg(sharp, path2, quality) {
|
|
54891
55339
|
return sharp(path2).rotate().resize({ width: MAX_DIM, height: MAX_DIM, fit: "inside", withoutEnlargement: true }).jpeg({ quality }).toBuffer();
|
|
54892
55340
|
}
|
|
55341
|
+
function buildVisionAnthropicConfig(auth) {
|
|
55342
|
+
const config = {};
|
|
55343
|
+
if (auth.authToken) {
|
|
55344
|
+
config.authToken = auth.authToken;
|
|
55345
|
+
config.apiKey = null;
|
|
55346
|
+
} else if (auth.apiKey) {
|
|
55347
|
+
config.apiKey = auth.apiKey;
|
|
55348
|
+
} else {
|
|
55349
|
+
config.apiKey = null;
|
|
55350
|
+
}
|
|
55351
|
+
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
55352
|
+
return config;
|
|
55353
|
+
}
|
|
54893
55354
|
function defaultSender(auth) {
|
|
54894
55355
|
return async (input) => {
|
|
54895
55356
|
const importMetaUrl = import.meta.url;
|
|
@@ -54897,11 +55358,7 @@ function defaultSender(auth) {
|
|
|
54897
55358
|
const sdk = req("@anthropic-ai/sdk");
|
|
54898
55359
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
54899
55360
|
if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
|
|
54900
|
-
const
|
|
54901
|
-
if (auth.authToken) config.authToken = auth.authToken;
|
|
54902
|
-
else if (auth.apiKey) config.apiKey = auth.apiKey;
|
|
54903
|
-
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
54904
|
-
const client = new Anthropic(config);
|
|
55361
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
54905
55362
|
const res = await client.messages.create({
|
|
54906
55363
|
model: input.model,
|
|
54907
55364
|
max_tokens: 1024,
|
|
@@ -54928,11 +55385,7 @@ function defaultPdfSender(auth) {
|
|
|
54928
55385
|
const sdk = req("@anthropic-ai/sdk");
|
|
54929
55386
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
54930
55387
|
if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
|
|
54931
|
-
const
|
|
54932
|
-
if (auth.authToken) config.authToken = auth.authToken;
|
|
54933
|
-
else if (auth.apiKey) config.apiKey = auth.apiKey;
|
|
54934
|
-
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
54935
|
-
const client = new Anthropic(config);
|
|
55388
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
54936
55389
|
const res = await client.messages.create({
|
|
54937
55390
|
model: input.model,
|
|
54938
55391
|
max_tokens: 4096,
|
|
@@ -55333,6 +55786,8 @@ var css = `
|
|
|
55333
55786
|
.app-version:empty { display: none; }
|
|
55334
55787
|
.app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
|
|
55335
55788
|
.app-update[hidden] { display: none; }
|
|
55789
|
+
#app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
55790
|
+
#app-update-link[hidden] { display: none; }
|
|
55336
55791
|
/* Unseen-change count next to a sidebar entity. */
|
|
55337
55792
|
.nav-badge {
|
|
55338
55793
|
display: inline-block; min-width: 16px; text-align: center;
|
|
@@ -55880,6 +56335,8 @@ var css = `
|
|
|
55880
56335
|
.grants-panel .grants-title { font-weight: 600; margin-bottom: 6px; }
|
|
55881
56336
|
.grants-panel .grants-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
|
|
55882
56337
|
.grants-panel .grants-row input { accent-color: var(--accent); }
|
|
56338
|
+
.grants-panel .grants-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
|
|
56339
|
+
.grants-panel .grants-dirty { font-size: 12px; }
|
|
55883
56340
|
|
|
55884
56341
|
/* Inline create-row at the bottom of every table */
|
|
55885
56342
|
tr.create-row td { background: var(--surface-2); }
|
|
@@ -56809,6 +57266,12 @@ var appJs = `
|
|
|
56809
57266
|
// drag handle once the app has booted.
|
|
56810
57267
|
var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
|
|
56811
57268
|
if (!isNaN(savedRail)) applyRailWidth(savedRail);
|
|
57269
|
+
// The version chip + manual-upgrade link live in the static shell (present
|
|
57270
|
+
// from first paint, in both the normal and virgin-state boots), so wire the
|
|
57271
|
+
// click handler and run the first availability check here \u2014 independent of
|
|
57272
|
+
// the async workspace bootstrap. checkServerVersion() refreshes it later.
|
|
57273
|
+
wireUpdateLink();
|
|
57274
|
+
checkUpdateAvailable();
|
|
56812
57275
|
// Failsafe: never leave the overlay up forever if a fetch hangs without
|
|
56813
57276
|
// rejecting, or a future early-return (e.g. the virgin-state screen)
|
|
56814
57277
|
// bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
|
|
@@ -57293,6 +57756,26 @@ var appJs = `
|
|
|
57293
57756
|
showUpdatePill(label || 'Updated \u2014 reloading\u2026');
|
|
57294
57757
|
setTimeout(function () { location.reload(); }, 600);
|
|
57295
57758
|
}
|
|
57759
|
+
// Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
|
|
57760
|
+
// the version chip only when the server reports a newer, installable version.
|
|
57761
|
+
// The auto-updater installs in the background on its own cadence; this lets
|
|
57762
|
+
// the user force it now. Best-effort; the link stays hidden on any failure.
|
|
57763
|
+
function checkUpdateAvailable() {
|
|
57764
|
+
var el = document.getElementById('app-update-link');
|
|
57765
|
+
if (!el) return;
|
|
57766
|
+
fetch('/api/update/status')
|
|
57767
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
57768
|
+
.then(function (s) {
|
|
57769
|
+
if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
|
|
57770
|
+
el.textContent = 'Update available \u2014 Upgrade';
|
|
57771
|
+
el.title = 'Install v' + s.latest + ' and restart';
|
|
57772
|
+
el.hidden = false;
|
|
57773
|
+
} else {
|
|
57774
|
+
el.hidden = true;
|
|
57775
|
+
}
|
|
57776
|
+
})
|
|
57777
|
+
.catch(function () { /* best-effort \u2014 keep the link hidden */ });
|
|
57778
|
+
}
|
|
57296
57779
|
// On every (re)connect, ask the server its version. A change vs BOOT_VERSION
|
|
57297
57780
|
// means a relaunch onto new code \u2192 reload. Best-effort; never throws.
|
|
57298
57781
|
function checkServerVersion() {
|
|
@@ -57306,6 +57789,31 @@ var appJs = `
|
|
|
57306
57789
|
else hideUpdatePill();
|
|
57307
57790
|
})
|
|
57308
57791
|
.catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
|
|
57792
|
+
// Refresh the manual-upgrade link alongside the reconnect version check.
|
|
57793
|
+
checkUpdateAvailable();
|
|
57794
|
+
}
|
|
57795
|
+
// Wire the manual-upgrade link's click: kick off the install (the server
|
|
57796
|
+
// installs the latest and restarts onto it) and surface the progress. On
|
|
57797
|
+
// success we do nothing else \u2014 the update-applied event + the reconnect
|
|
57798
|
+
// version check land the page on the new version (no manual reload). A
|
|
57799
|
+
// false ok means the install can't run (unsupervised) \u2014 toast why.
|
|
57800
|
+
function wireUpdateLink() {
|
|
57801
|
+
var el = document.getElementById('app-update-link');
|
|
57802
|
+
if (!el) return;
|
|
57803
|
+
el.addEventListener('click', function (e) {
|
|
57804
|
+
e.preventDefault();
|
|
57805
|
+
el.hidden = true;
|
|
57806
|
+
showUpdatePill('Updating\u2026');
|
|
57807
|
+
fetch('/api/update/apply', { method: 'POST' })
|
|
57808
|
+
.then(function (r) { return r.json(); })
|
|
57809
|
+
.then(function (d) {
|
|
57810
|
+
if (d && d.ok === false) {
|
|
57811
|
+
hideUpdatePill();
|
|
57812
|
+
showToast(d.error || 'Update unavailable', {});
|
|
57813
|
+
}
|
|
57814
|
+
})
|
|
57815
|
+
.catch(function () { /* server may already be restarting */ });
|
|
57816
|
+
});
|
|
57309
57817
|
}
|
|
57310
57818
|
function dispatchStreamMessage(type, data) {
|
|
57311
57819
|
if (type === 'realtime-state') {
|
|
@@ -58346,6 +58854,15 @@ var appJs = `
|
|
|
58346
58854
|
// Per-table view state: 'live' (default) or 'trash' (soft-deleted rows).
|
|
58347
58855
|
var tableViewMode = {};
|
|
58348
58856
|
|
|
58857
|
+
// The (table, pk) of the per-row "Manage access" grants panel that is
|
|
58858
|
+
// currently open, or null when none is. A soft re-render (a concurrent edit
|
|
58859
|
+
// by another client fires pg_notify \u2192 realtime refresh \u2192 renderRoute({soft})
|
|
58860
|
+
// \u2192 renderDetail/renderFsItem repaint) would otherwise re-create the detail
|
|
58861
|
+
// view with the panel collapsed, dropping a staged multi-select mid-edit.
|
|
58862
|
+
// wireRowSharing reads this after each repaint and re-opens + re-populates the
|
|
58863
|
+
// panel WITHOUT any network call, so the staged selection survives.
|
|
58864
|
+
var openGrantsPanel = null;
|
|
58865
|
+
|
|
58349
58866
|
function renderTable(content, tableName) {
|
|
58350
58867
|
var myGen = renderGen;
|
|
58351
58868
|
clearUnseen(tableName);
|
|
@@ -58824,70 +59341,151 @@ var appJs = `
|
|
|
58824
59341
|
}).catch(function (e) { showToast('Visibility update failed: ' + e.message, {}); });
|
|
58825
59342
|
});
|
|
58826
59343
|
});
|
|
58827
|
-
var
|
|
58828
|
-
|
|
59344
|
+
var access = row._access || {};
|
|
59345
|
+
|
|
59346
|
+
// Render the staged member checklist + a single "Save sharing" / "Cancel"
|
|
59347
|
+
// into the panel. Checkbox toggles mutate ONLY the local desired map \u2014
|
|
59348
|
+
// NO network call per toggle (the old design auto-saved live, one POST per
|
|
59349
|
+
// checkbox, and each grant's pg_notify collapsed the panel). A single batch
|
|
59350
|
+
// request fires on Save. members is the already-fetched list; desired
|
|
59351
|
+
// seeds from the row's current grantees (or a caller-supplied staged map
|
|
59352
|
+
// when re-opening after a soft re-render).
|
|
59353
|
+
function populateGrantsPanel(panel, members, desired) {
|
|
59354
|
+
// Snapshot the CURRENT (committed) grantees so Save can diff desired-vs-
|
|
59355
|
+
// current into adds/removes. effectiveVisibility decides whether we're
|
|
59356
|
+
// actually switching INTO specific-people mode (custom-0 reads as private).
|
|
59357
|
+
var current = {};
|
|
59358
|
+
(access.grantees || []).forEach(function (g) { current[g] = true; });
|
|
59359
|
+
if (members.length === 0) {
|
|
59360
|
+
panel.innerHTML = '<div class="muted">No other members in this workspace yet.</div>';
|
|
59361
|
+
panel.hidden = false;
|
|
59362
|
+
return;
|
|
59363
|
+
}
|
|
59364
|
+
function dirtyCount() {
|
|
59365
|
+
var n = 0;
|
|
59366
|
+
members.forEach(function (m) {
|
|
59367
|
+
if (!!desired[m.role] !== !!current[m.role]) n++;
|
|
59368
|
+
});
|
|
59369
|
+
return n;
|
|
59370
|
+
}
|
|
59371
|
+
function render() {
|
|
59372
|
+
var changed = dirtyCount();
|
|
59373
|
+
panel.innerHTML = '<div class="grants-title">Who can see this</div>' +
|
|
59374
|
+
members.map(function (m) {
|
|
59375
|
+
var label = m.name || m.email || m.role;
|
|
59376
|
+
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
59377
|
+
(desired[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
59378
|
+
}).join('') +
|
|
59379
|
+
'<div class="grants-actions">' +
|
|
59380
|
+
'<button class="btn primary" id="grants-save"' + (changed ? '' : ' disabled') + '>Save sharing</button>' +
|
|
59381
|
+
'<button class="btn" id="grants-cancel">Cancel</button>' +
|
|
59382
|
+
'<span class="grants-dirty muted">' + (changed ? (changed === 1 ? '1 change' : changed + ' changes') : 'No changes') + '</span>' +
|
|
59383
|
+
'</div>';
|
|
59384
|
+
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
59385
|
+
cb.addEventListener('change', function () {
|
|
59386
|
+
var role = cb.getAttribute('data-grant-role');
|
|
59387
|
+
if (cb.checked) desired[role] = true; else delete desired[role];
|
|
59388
|
+
render(); // re-render to refresh the dirty indicator + Save state
|
|
59389
|
+
});
|
|
59390
|
+
});
|
|
59391
|
+
var cancelBtn = panel.querySelector('#grants-cancel');
|
|
59392
|
+
if (cancelBtn) cancelBtn.addEventListener('click', function () { closeGrantsPanel(panel); });
|
|
59393
|
+
var saveBtn = panel.querySelector('#grants-save');
|
|
59394
|
+
if (saveBtn) saveBtn.addEventListener('click', function () {
|
|
59395
|
+
var toAdd = [];
|
|
59396
|
+
var toRemove = [];
|
|
59397
|
+
members.forEach(function (m) {
|
|
59398
|
+
var want = !!desired[m.role];
|
|
59399
|
+
var have = !!current[m.role];
|
|
59400
|
+
if (want && !have) toAdd.push(m.role);
|
|
59401
|
+
if (!want && have) toRemove.push(m.role);
|
|
59402
|
+
});
|
|
59403
|
+
if (toAdd.length === 0 && toRemove.length === 0) { closeGrantsPanel(panel); return; }
|
|
59404
|
+
// Confirm the mode change ONCE, here \u2014 only when actually switching
|
|
59405
|
+
// INTO specific-people mode (effective vis isn't already custom AND we
|
|
59406
|
+
// are adding at least one grantee). Never per checkbox.
|
|
59407
|
+
if (effectiveVisibility(access) !== 'custom' && toAdd.length > 0) {
|
|
59408
|
+
if (!confirm('Sharing this with specific people switches it off "everyone"/"private". The chosen people will be able to see it. Continue?')) return;
|
|
59409
|
+
}
|
|
59410
|
+
withBusy(saveBtn, function () {
|
|
59411
|
+
return fetchJson('/api/cloud/row-grants', {
|
|
59412
|
+
method: 'POST',
|
|
59413
|
+
headers: { 'content-type': 'application/json' },
|
|
59414
|
+
body: JSON.stringify({ table: tableName, pk: id, grant: toAdd, revoke: toRemove }),
|
|
59415
|
+
}).then(function () {
|
|
59416
|
+
// Mirror the committed state locally so the re-render's indicator
|
|
59417
|
+
// is correct. The first grant flips the row to custom server-side;
|
|
59418
|
+
// revoking the last leaves custom-0, which effectiveVisibility
|
|
59419
|
+
// renders as private.
|
|
59420
|
+
var list = [];
|
|
59421
|
+
members.forEach(function (m) { if (desired[m.role]) list.push(m.role); });
|
|
59422
|
+
access.grantees = list;
|
|
59423
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
59424
|
+
openGrantsPanel = null; // a successful save closes the staging session
|
|
59425
|
+
invalidate(tableName);
|
|
59426
|
+
showToast('Sharing updated', {});
|
|
59427
|
+
reRender();
|
|
59428
|
+
}).catch(function (e) {
|
|
59429
|
+
// Surface loudly + leave the staged selection intact so the user
|
|
59430
|
+
// can retry; no silent partial-success.
|
|
59431
|
+
showToast('Sharing update failed: ' + e.message, {});
|
|
59432
|
+
});
|
|
59433
|
+
});
|
|
59434
|
+
});
|
|
59435
|
+
panel.hidden = false;
|
|
59436
|
+
}
|
|
59437
|
+
render();
|
|
59438
|
+
}
|
|
59439
|
+
|
|
59440
|
+
function closeGrantsPanel(panel) {
|
|
59441
|
+
if (panel) panel.hidden = true;
|
|
59442
|
+
openGrantsPanel = null;
|
|
59443
|
+
}
|
|
59444
|
+
|
|
59445
|
+
// Open (or toggle shut) the manage-access panel. Fetches the member list,
|
|
59446
|
+
// then stages from the row's current grantees. Opening must NOT pre-flip
|
|
59447
|
+
// the row to 'custom' \u2014 that left a never-shared row stuck at "custom (0)".
|
|
59448
|
+
function openManagePanel(triggerBtn) {
|
|
58829
59449
|
var panel = content.querySelector('#grants-panel');
|
|
58830
59450
|
if (!panel) return;
|
|
58831
|
-
if (!panel.hidden) { panel
|
|
58832
|
-
|
|
58833
|
-
|
|
58834
|
-
// row the user never actually shared stuck at "custom (0)". The first
|
|
58835
|
-
// grant flips it to custom server-side (lattice_grant_row); revoking the
|
|
58836
|
-
// last leaves it custom-with-0-grantees, which now reads as private. So
|
|
58837
|
-
// just load the member checklist.
|
|
58838
|
-
var ensure = Promise.resolve();
|
|
58839
|
-
withBusy(detailVisManage, function () {
|
|
58840
|
-
return ensure.then(function () {
|
|
58841
|
-
return fetchJson('/api/cloud/members');
|
|
58842
|
-
}).then(function (d) {
|
|
59451
|
+
if (!panel.hidden) { closeGrantsPanel(panel); return; }
|
|
59452
|
+
withBusy(triggerBtn, function () {
|
|
59453
|
+
return fetchJson('/api/cloud/members').then(function (d) {
|
|
58843
59454
|
// The grant target is a member ROLE: lattice_grant_row keys on the
|
|
58844
59455
|
// role, and _access.grantees holds role names. List every member
|
|
58845
59456
|
// except the owner (you don't grant the owner their own row).
|
|
58846
59457
|
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
58847
|
-
var
|
|
58848
|
-
(access.grantees || []).forEach(function (g) {
|
|
58849
|
-
|
|
58850
|
-
|
|
58851
|
-
} else {
|
|
58852
|
-
panel.innerHTML = '<div class="grants-title">Who can see this</div>' + members.map(function (m) {
|
|
58853
|
-
var label = m.name || m.email || m.role;
|
|
58854
|
-
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
58855
|
-
(granted[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
58856
|
-
}).join('');
|
|
58857
|
-
}
|
|
58858
|
-
panel.hidden = false;
|
|
58859
|
-
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
58860
|
-
cb.addEventListener('change', function () {
|
|
58861
|
-
var role = cb.getAttribute('data-grant-role');
|
|
58862
|
-
cb.disabled = true;
|
|
58863
|
-
fetchJson('/api/cloud/row-grant', {
|
|
58864
|
-
method: 'POST',
|
|
58865
|
-
headers: { 'content-type': 'application/json' },
|
|
58866
|
-
body: JSON.stringify({ table: tableName, pk: id, grantee: role, revoke: !cb.checked }),
|
|
58867
|
-
}).then(function () {
|
|
58868
|
-
var list = access.grantees || (access.grantees = []);
|
|
58869
|
-
var at = list.indexOf(role);
|
|
58870
|
-
if (cb.checked && at === -1) list.push(role);
|
|
58871
|
-
if (!cb.checked && at !== -1) list.splice(at, 1);
|
|
58872
|
-
// The first grant flips the row to custom server-side; mirror
|
|
58873
|
-
// that locally so the indicator updates. Revoking the last leaves
|
|
58874
|
-
// visibility 'custom' but effectiveVisibility renders custom-0 as
|
|
58875
|
-
// private, so the label flips back to "Private to you".
|
|
58876
|
-
if (list.length > 0) access.visibility = 'custom';
|
|
58877
|
-
var infoEl = content.querySelector('#detail-vis-info');
|
|
58878
|
-
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
58879
|
-
invalidate(tableName);
|
|
58880
|
-
}).catch(function (e) {
|
|
58881
|
-
cb.checked = !cb.checked; // revert the failed change
|
|
58882
|
-
showToast('Access update failed: ' + e.message, {});
|
|
58883
|
-
}).then(function () { cb.disabled = false; });
|
|
58884
|
-
});
|
|
58885
|
-
});
|
|
58886
|
-
var infoEl = content.querySelector('#detail-vis-info');
|
|
58887
|
-
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
59458
|
+
var desired = {};
|
|
59459
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
59460
|
+
openGrantsPanel = { table: tableName, pk: id };
|
|
59461
|
+
populateGrantsPanel(panel, members, desired);
|
|
58888
59462
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
58889
59463
|
});
|
|
59464
|
+
}
|
|
59465
|
+
|
|
59466
|
+
var detailVisManage = content.querySelector('#detail-vis-manage');
|
|
59467
|
+
if (detailVisManage) detailVisManage.addEventListener('click', function () {
|
|
59468
|
+
openManagePanel(detailVisManage);
|
|
58890
59469
|
});
|
|
59470
|
+
|
|
59471
|
+
// Preserve an open panel across a soft re-render: if the tracked panel
|
|
59472
|
+
// matches the row this view just repainted, re-open it and re-populate the
|
|
59473
|
+
// checklist from the freshly-fetched row._access WITHOUT any network call,
|
|
59474
|
+
// so a concurrent edit by another client doesn't lose a staged selection.
|
|
59475
|
+
if (openGrantsPanel && openGrantsPanel.table === tableName && openGrantsPanel.pk === id) {
|
|
59476
|
+
var rpanel = content.querySelector('#grants-panel');
|
|
59477
|
+
if (rpanel) {
|
|
59478
|
+
fetchJson('/api/cloud/members').then(function (d) {
|
|
59479
|
+
// Only re-populate if THIS panel is still the tracked-open one (a
|
|
59480
|
+
// newer navigation/save may have cleared it while members loaded).
|
|
59481
|
+
if (!openGrantsPanel || openGrantsPanel.table !== tableName || openGrantsPanel.pk !== id) return;
|
|
59482
|
+
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
59483
|
+
var desired = {};
|
|
59484
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
59485
|
+
populateGrantsPanel(rpanel, members, desired);
|
|
59486
|
+
}).catch(function () { /* best-effort restore; a click reopens it */ });
|
|
59487
|
+
}
|
|
59488
|
+
}
|
|
58891
59489
|
}
|
|
58892
59490
|
function renderDetail(content, tableName, id) {
|
|
58893
59491
|
var myGen = renderGen;
|
|
@@ -63671,13 +64269,21 @@ var appJs = `
|
|
|
63671
64269
|
}
|
|
63672
64270
|
function uploadFile(file) {
|
|
63673
64271
|
var done = pendingIngestItem(file.name || 'file');
|
|
64272
|
+
// Carry the composer's "Private mode" intent so an upload made while the
|
|
64273
|
+
// box is checked is stamped private at insert, instead of inheriting the
|
|
64274
|
+
// files-table default (which can be shared-to-everyone on a cloud). Read
|
|
64275
|
+
// the checkbox defensively \u2014 it may not be rendered. On a local workspace
|
|
64276
|
+
// the box is checked+disabled, so this is '1' there too; forced visibility
|
|
64277
|
+
// is a harmless no-op on the single-user SQLite path.
|
|
64278
|
+
var pv = document.getElementById('chat-private');
|
|
64279
|
+
var priv = pv && pv.checked ? '1' : '0';
|
|
63674
64280
|
return fetch('/api/ingest/upload', {
|
|
63675
64281
|
method: 'POST',
|
|
63676
64282
|
// Percent-encode the filename: HTTP header values must be ISO-8859-1,
|
|
63677
64283
|
// so a Unicode filename (emoji, smart quote, accent, em-dash) would
|
|
63678
64284
|
// otherwise make fetch() throw "String contains non ISO-8859-1 code
|
|
63679
64285
|
// point". The server decodeURIComponent()s it back.
|
|
63680
|
-
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file') },
|
|
64286
|
+
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file'), 'x-lattice-private': priv },
|
|
63681
64287
|
body: file,
|
|
63682
64288
|
})
|
|
63683
64289
|
.then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; }); })
|
|
@@ -64025,6 +64631,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
64025
64631
|
<span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
|
|
64026
64632
|
<span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
|
|
64027
64633
|
<span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
|
|
64634
|
+
<a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
|
|
64028
64635
|
<button id="settings-gear" title="Settings" aria-label="Open settings">
|
|
64029
64636
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
64030
64637
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -64513,7 +65120,7 @@ async function checkForUpdate(pkgName, currentVersion, opts = {}) {
|
|
|
64513
65120
|
// src/update-context.ts
|
|
64514
65121
|
init_user_config();
|
|
64515
65122
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
64516
|
-
import { existsSync as existsSync19, lstatSync as lstatSync2, readFileSync as readFileSync15 } from "fs";
|
|
65123
|
+
import { existsSync as existsSync19, lstatSync as lstatSync2, readFileSync as readFileSync15, realpathSync } from "fs";
|
|
64517
65124
|
import { dirname as dirname7, join as join24, sep as sep6 } from "path";
|
|
64518
65125
|
var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
64519
65126
|
function isValidVersion(v2) {
|
|
@@ -64543,10 +65150,19 @@ function isUnderGlobalPrefix(packageRoot, execPath) {
|
|
|
64543
65150
|
}
|
|
64544
65151
|
function detectInstallContext(opts = {}) {
|
|
64545
65152
|
const pkgName = opts.pkgName ?? "latticesql";
|
|
64546
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
64547
65153
|
const env2 = opts.env ?? process.env;
|
|
64548
65154
|
const execPath = opts.execPath ?? process.execPath;
|
|
64549
|
-
const
|
|
65155
|
+
const rawCwd = opts.cwd ?? process.cwd();
|
|
65156
|
+
const rawModulePath = opts.modulePath ?? process.argv[1] ?? rawCwd;
|
|
65157
|
+
const resolveReal = (p3) => {
|
|
65158
|
+
try {
|
|
65159
|
+
return realpathSync(p3);
|
|
65160
|
+
} catch {
|
|
65161
|
+
return p3;
|
|
65162
|
+
}
|
|
65163
|
+
};
|
|
65164
|
+
const modulePath = resolveReal(rawModulePath);
|
|
65165
|
+
const cwd = resolveReal(rawCwd);
|
|
64550
65166
|
const packageRoot = findPackageRoot(dirname7(modulePath), pkgName);
|
|
64551
65167
|
if (packageRoot && existsSync19(join24(packageRoot, ".git"))) {
|
|
64552
65168
|
return {
|
|
@@ -66332,6 +66948,27 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
66332
66948
|
});
|
|
66333
66949
|
return true;
|
|
66334
66950
|
}
|
|
66951
|
+
if (pathname === "/api/cloud/row-grants" && method === "POST") {
|
|
66952
|
+
await tryHandler(res, async () => {
|
|
66953
|
+
const body = await readJson(req);
|
|
66954
|
+
const table = typeof body.table === "string" ? body.table : "";
|
|
66955
|
+
const pk = typeof body.pk === "string" ? body.pk : "";
|
|
66956
|
+
const strList = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : [];
|
|
66957
|
+
const grant = strList(body.grant);
|
|
66958
|
+
const revoke = strList(body.revoke);
|
|
66959
|
+
if (!table || !pk) {
|
|
66960
|
+
sendJson(res, { error: "table and pk are required" }, 400);
|
|
66961
|
+
return;
|
|
66962
|
+
}
|
|
66963
|
+
if (ctx.db.getDialect() !== "postgres") {
|
|
66964
|
+
sendJson(res, { error: "Per-row sharing requires a cloud (Postgres) database" }, 400);
|
|
66965
|
+
return;
|
|
66966
|
+
}
|
|
66967
|
+
await batchRowGrants(ctx.db, table, pk, grant, revoke);
|
|
66968
|
+
sendJson(res, { ok: true, table, pk, granted: grant, revoked: revoke });
|
|
66969
|
+
});
|
|
66970
|
+
return true;
|
|
66971
|
+
}
|
|
66335
66972
|
if (pathname === "/api/cloud/s3-config" && method === "GET") {
|
|
66336
66973
|
await tryHandler(res, () => {
|
|
66337
66974
|
const label = activeWorkspaceLabel(ctx.configPath);
|
|
@@ -67128,7 +67765,7 @@ function enrichContext(ctx) {
|
|
|
67128
67765
|
...ctx.createEntity ? { createEntity: ctx.createEntity } : {}
|
|
67129
67766
|
};
|
|
67130
67767
|
}
|
|
67131
|
-
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
67768
|
+
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res, privateMode) {
|
|
67132
67769
|
try {
|
|
67133
67770
|
return await enrichWithLlm(
|
|
67134
67771
|
mctx,
|
|
@@ -67140,7 +67777,9 @@ async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
|
67140
67777
|
ctx.entityDescriptions,
|
|
67141
67778
|
ctx.createJunction,
|
|
67142
67779
|
ctx.aggressiveness,
|
|
67143
|
-
ctx.createEntity
|
|
67780
|
+
ctx.createEntity,
|
|
67781
|
+
false,
|
|
67782
|
+
privateMode
|
|
67144
67783
|
);
|
|
67145
67784
|
} catch (e6) {
|
|
67146
67785
|
const err = e6;
|
|
@@ -67219,7 +67858,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67219
67858
|
source: "ingest",
|
|
67220
67859
|
onColumnsAdded: columnDescriptionHook(ctx.db)
|
|
67221
67860
|
};
|
|
67861
|
+
const headerPrivate = req.headers["x-lattice-private"] === "1";
|
|
67222
67862
|
if (ctx.pathname === "/api/ingest/upload") {
|
|
67863
|
+
const forcePrivate2 = headerPrivate;
|
|
67223
67864
|
const rawName = typeof req.headers["x-filename"] === "string" && req.headers["x-filename"] || "";
|
|
67224
67865
|
let name2 = "upload";
|
|
67225
67866
|
if (rawName) {
|
|
@@ -67317,10 +67958,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67317
67958
|
...blob ? { blob_path: blob.blob_path } : {}
|
|
67318
67959
|
} : blob ? { ref_kind: "blob", blob_path: blob.blob_path } : {}
|
|
67319
67960
|
};
|
|
67320
|
-
const { id: id2 } = await createRow(
|
|
67321
|
-
|
|
67322
|
-
|
|
67323
|
-
|
|
67961
|
+
const { id: id2 } = await createRow(
|
|
67962
|
+
mctx,
|
|
67963
|
+
"files",
|
|
67964
|
+
{
|
|
67965
|
+
...await requiredFileDefaults(ctx.db, name2, fileId, uploadRow),
|
|
67966
|
+
...uploadRow
|
|
67967
|
+
},
|
|
67968
|
+
forcePrivate2 ? "private" : void 0
|
|
67969
|
+
);
|
|
67324
67970
|
try {
|
|
67325
67971
|
const dedupCtx = {
|
|
67326
67972
|
db: ctx.db,
|
|
@@ -67346,7 +67992,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67346
67992
|
}
|
|
67347
67993
|
let suggestedLinks = [];
|
|
67348
67994
|
if (!result.skip) {
|
|
67349
|
-
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res);
|
|
67995
|
+
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res, forcePrivate2);
|
|
67350
67996
|
if (links === null) return true;
|
|
67351
67997
|
suggestedLinks = links;
|
|
67352
67998
|
}
|
|
@@ -67373,6 +68019,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67373
68019
|
sendJson4(res, { error: e6.message }, 400);
|
|
67374
68020
|
return true;
|
|
67375
68021
|
}
|
|
68022
|
+
const forcePrivate = headerPrivate || body.private === true;
|
|
67376
68023
|
if (ctx.pathname === "/api/ingest/text") {
|
|
67377
68024
|
const rawText = typeof body.text === "string" ? body.text : "";
|
|
67378
68025
|
if (!rawText.trim()) {
|
|
@@ -67383,7 +68030,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67383
68030
|
if (sourceUrl) {
|
|
67384
68031
|
try {
|
|
67385
68032
|
const result = await ingestUrlAsFile(
|
|
67386
|
-
{ db: ctx.db, mctx, enrich: enrichContext(ctx) },
|
|
68033
|
+
{ db: ctx.db, mctx, enrich: enrichContext(ctx), privateMode: forcePrivate },
|
|
67387
68034
|
sourceUrl
|
|
67388
68035
|
);
|
|
67389
68036
|
sendJson4(
|
|
@@ -67412,11 +68059,25 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67412
68059
|
description: describe(content, mime2, title),
|
|
67413
68060
|
extraction_status: "extracted"
|
|
67414
68061
|
};
|
|
67415
|
-
const { id: id2 } = await createRow(
|
|
67416
|
-
|
|
67417
|
-
|
|
67418
|
-
|
|
67419
|
-
|
|
68062
|
+
const { id: id2 } = await createRow(
|
|
68063
|
+
mctx,
|
|
68064
|
+
"files",
|
|
68065
|
+
{
|
|
68066
|
+
...await requiredFileDefaults(ctx.db, title, textFileId, textRow),
|
|
68067
|
+
...textRow
|
|
68068
|
+
},
|
|
68069
|
+
forcePrivate ? "private" : void 0
|
|
68070
|
+
);
|
|
68071
|
+
const suggestedLinks = await enrichOrFail(
|
|
68072
|
+
mctx,
|
|
68073
|
+
ctx.db,
|
|
68074
|
+
id2,
|
|
68075
|
+
content,
|
|
68076
|
+
title,
|
|
68077
|
+
ctx,
|
|
68078
|
+
res,
|
|
68079
|
+
forcePrivate
|
|
68080
|
+
);
|
|
67420
68081
|
if (suggestedLinks === null) return true;
|
|
67421
68082
|
sendJson4(res, { id: id2, extraction_status: "extracted", suggestedLinks }, 201);
|
|
67422
68083
|
return true;
|
|
@@ -67455,10 +68116,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67455
68116
|
size_bytes: size,
|
|
67456
68117
|
extraction_status: "pending"
|
|
67457
68118
|
};
|
|
67458
|
-
const { id } = await createRow(
|
|
67459
|
-
|
|
67460
|
-
|
|
67461
|
-
|
|
68119
|
+
const { id } = await createRow(
|
|
68120
|
+
mctx,
|
|
68121
|
+
"files",
|
|
68122
|
+
{
|
|
68123
|
+
...await requiredFileDefaults(ctx.db, name, localFileId, localRow),
|
|
68124
|
+
...localRow
|
|
68125
|
+
},
|
|
68126
|
+
forcePrivate ? "private" : void 0
|
|
68127
|
+
);
|
|
67462
68128
|
try {
|
|
67463
68129
|
const result = await extractSource(ctx.db, abs, mime, name);
|
|
67464
68130
|
await updateRow(mctx, "files", id, {
|
|
@@ -67476,7 +68142,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67476
68142
|
ctx.entityDescriptions,
|
|
67477
68143
|
ctx.createJunction,
|
|
67478
68144
|
ctx.aggressiveness,
|
|
67479
|
-
ctx.createEntity
|
|
68145
|
+
ctx.createEntity,
|
|
68146
|
+
false,
|
|
68147
|
+
forcePrivate
|
|
67480
68148
|
);
|
|
67481
68149
|
sendJson4(
|
|
67482
68150
|
res,
|
|
@@ -68163,7 +68831,7 @@ function startBackgroundRender(active) {
|
|
|
68163
68831
|
}
|
|
68164
68832
|
bus.publish(e6);
|
|
68165
68833
|
};
|
|
68166
|
-
void db.renderInBackground(active.outputDir, { signal, onProgress }).then(
|
|
68834
|
+
void db.renderInBackground(active.outputDir, { signal, onProgress, gateOnOpen: true }).then(
|
|
68167
68835
|
() => {
|
|
68168
68836
|
},
|
|
68169
68837
|
(err) => {
|
|
@@ -68505,6 +69173,28 @@ async function startGuiServer(options) {
|
|
|
68505
69173
|
setActive(next, created.id);
|
|
68506
69174
|
return created.id;
|
|
68507
69175
|
};
|
|
69176
|
+
const cleanupWorkspaceFiles = (root6, ws) => {
|
|
69177
|
+
if (!ws.configPath && ws.kind === "local") {
|
|
69178
|
+
rmSync(workspaceDir(root6, ws.dir), { recursive: true, force: true });
|
|
69179
|
+
} else if (ws.kind === "cloud") {
|
|
69180
|
+
if (ws.configPath && existsSync24(ws.configPath)) {
|
|
69181
|
+
rmSync(ws.configPath, { force: true });
|
|
69182
|
+
}
|
|
69183
|
+
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
69184
|
+
const label = labelMatch?.[1];
|
|
69185
|
+
if (label) {
|
|
69186
|
+
const stillUsed = listWorkspaces(root6).some(
|
|
69187
|
+
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
69188
|
+
);
|
|
69189
|
+
if (!stillUsed) {
|
|
69190
|
+
try {
|
|
69191
|
+
deleteDbCredential(label);
|
|
69192
|
+
} catch {
|
|
69193
|
+
}
|
|
69194
|
+
}
|
|
69195
|
+
}
|
|
69196
|
+
}
|
|
69197
|
+
};
|
|
68508
69198
|
const handleVirginRoute = async (req, res, pathname, method) => {
|
|
68509
69199
|
if (method === "GET" && pathname === "/") {
|
|
68510
69200
|
sendText(
|
|
@@ -68556,6 +69246,35 @@ async function startGuiServer(options) {
|
|
|
68556
69246
|
}
|
|
68557
69247
|
return true;
|
|
68558
69248
|
}
|
|
69249
|
+
if (method === "POST" && pathname === "/api/workspaces/delete") {
|
|
69250
|
+
if (!latticeRoot) {
|
|
69251
|
+
sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
|
|
69252
|
+
return true;
|
|
69253
|
+
}
|
|
69254
|
+
const body = await readJson(req);
|
|
69255
|
+
if (typeof body.id !== "string") {
|
|
69256
|
+
sendJson(res, { error: "id must be a string" }, 400);
|
|
69257
|
+
return true;
|
|
69258
|
+
}
|
|
69259
|
+
const ws = getWorkspace(latticeRoot, body.id);
|
|
69260
|
+
if (!ws) {
|
|
69261
|
+
sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
|
|
69262
|
+
return true;
|
|
69263
|
+
}
|
|
69264
|
+
removeWorkspace(latticeRoot, ws.id);
|
|
69265
|
+
try {
|
|
69266
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
69267
|
+
} catch (e6) {
|
|
69268
|
+
sendJson(
|
|
69269
|
+
res,
|
|
69270
|
+
{ error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
|
|
69271
|
+
500
|
|
69272
|
+
);
|
|
69273
|
+
return true;
|
|
69274
|
+
}
|
|
69275
|
+
sendJson(res, { ok: true, switchedTo: null });
|
|
69276
|
+
return true;
|
|
69277
|
+
}
|
|
68559
69278
|
if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
|
|
68560
69279
|
await redeemInvite(createCloudWorkspace, req, res);
|
|
68561
69280
|
return true;
|
|
@@ -68590,6 +69309,18 @@ async function startGuiServer(options) {
|
|
|
68590
69309
|
);
|
|
68591
69310
|
return;
|
|
68592
69311
|
}
|
|
69312
|
+
if (method === "POST" && pathname === "/api/update/apply") {
|
|
69313
|
+
if (updateService) {
|
|
69314
|
+
void updateService.checkNow(true);
|
|
69315
|
+
sendJson(res, { ok: true, status: updateService.status() });
|
|
69316
|
+
} else {
|
|
69317
|
+
sendJson(res, {
|
|
69318
|
+
ok: false,
|
|
69319
|
+
error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
|
|
69320
|
+
});
|
|
69321
|
+
}
|
|
69322
|
+
return;
|
|
69323
|
+
}
|
|
68593
69324
|
if (!activeRef) {
|
|
68594
69325
|
if (await handleVirginRoute(req, res, pathname, method)) return;
|
|
68595
69326
|
sendJson(res, { error: "No active workspace" }, 409);
|
|
@@ -69683,26 +70414,7 @@ async function startGuiServer(options) {
|
|
|
69683
70414
|
}
|
|
69684
70415
|
removeWorkspace(latticeRoot, ws.id);
|
|
69685
70416
|
try {
|
|
69686
|
-
|
|
69687
|
-
rmSync(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
|
|
69688
|
-
} else if (ws.kind === "cloud") {
|
|
69689
|
-
if (ws.configPath && existsSync24(ws.configPath)) {
|
|
69690
|
-
rmSync(ws.configPath, { force: true });
|
|
69691
|
-
}
|
|
69692
|
-
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
69693
|
-
const label = labelMatch?.[1];
|
|
69694
|
-
if (label) {
|
|
69695
|
-
const stillUsed = listWorkspaces(latticeRoot).some(
|
|
69696
|
-
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
69697
|
-
);
|
|
69698
|
-
if (!stillUsed) {
|
|
69699
|
-
try {
|
|
69700
|
-
deleteDbCredential(label);
|
|
69701
|
-
} catch {
|
|
69702
|
-
}
|
|
69703
|
-
}
|
|
69704
|
-
}
|
|
69705
|
-
}
|
|
70417
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
69706
70418
|
} catch (e6) {
|
|
69707
70419
|
sendJson(
|
|
69708
70420
|
res,
|
|
@@ -70168,7 +70880,9 @@ ${e6.stack ?? ""}`
|
|
|
70168
70880
|
}
|
|
70169
70881
|
}
|
|
70170
70882
|
};
|
|
70171
|
-
if (options.
|
|
70883
|
+
if (options.updateServiceFactory) {
|
|
70884
|
+
updateService = options.updateServiceFactory(broadcast);
|
|
70885
|
+
} else if (options.selfUpdate && guiVersion) {
|
|
70172
70886
|
updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
|
|
70173
70887
|
}
|
|
70174
70888
|
const handleEventStream = (ws) => {
|