latticesql 3.4.2 → 3.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +878 -170
- package/dist/index.cjs +876 -169
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +877 -169
- 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
|
}
|
|
@@ -5244,6 +5446,7 @@ var init_lattice = __esm({
|
|
|
5244
5446
|
init_shred();
|
|
5245
5447
|
init_encryption();
|
|
5246
5448
|
init_manifest();
|
|
5449
|
+
init_render_cursor();
|
|
5247
5450
|
init_adapter();
|
|
5248
5451
|
init_sqlite();
|
|
5249
5452
|
init_postgres();
|
|
@@ -5314,6 +5517,14 @@ var init_lattice = __esm({
|
|
|
5314
5517
|
_changelogTables = /* @__PURE__ */ new Set();
|
|
5315
5518
|
/** Current task context string for relevance filtering. */
|
|
5316
5519
|
_taskContext = "";
|
|
5520
|
+
/**
|
|
5521
|
+
* True when this connection opened against an already-provisioned cloud as a
|
|
5522
|
+
* SCOPED MEMBER (no role-management privilege → no CREATE/ALTER on the schema).
|
|
5523
|
+
* Set during init() by the same probe that decides introspect-only. Drives
|
|
5524
|
+
* {@link addColumn} to route DDL through the owner-side `lattice_member_add_column`
|
|
5525
|
+
* SECURITY DEFINER helper instead of issuing a raw ALTER the member can't run.
|
|
5526
|
+
*/
|
|
5527
|
+
_cloudMemberOpen = false;
|
|
5317
5528
|
_auditHandlers = [];
|
|
5318
5529
|
_renderHandlers = [];
|
|
5319
5530
|
_writebackHandlers = [];
|
|
@@ -5560,7 +5771,7 @@ var init_lattice = __esm({
|
|
|
5560
5771
|
/** Async tail of init(). See {@link init} for the sync-validation phase. */
|
|
5561
5772
|
async _initAsync(options) {
|
|
5562
5773
|
let introspectOnly = options.introspectOnly === true;
|
|
5563
|
-
if (
|
|
5774
|
+
if (this.getDialect() === "postgres") {
|
|
5564
5775
|
try {
|
|
5565
5776
|
const [marker, role] = await Promise.all([
|
|
5566
5777
|
getAsyncOrSync(this._adapter, `SELECT to_regclass('__lattice_owners') AS reg`),
|
|
@@ -5571,7 +5782,9 @@ var init_lattice = __esm({
|
|
|
5571
5782
|
]);
|
|
5572
5783
|
const provisioned = !!marker && marker.reg != null;
|
|
5573
5784
|
const canCreateRoles = !!role && role.rolcreaterole === true;
|
|
5574
|
-
|
|
5785
|
+
const memberOpen = provisioned && !canCreateRoles;
|
|
5786
|
+
introspectOnly = introspectOnly || memberOpen;
|
|
5787
|
+
this._cloudMemberOpen = memberOpen;
|
|
5575
5788
|
} catch {
|
|
5576
5789
|
}
|
|
5577
5790
|
}
|
|
@@ -5659,6 +5872,26 @@ var init_lattice = __esm({
|
|
|
5659
5872
|
getDialect() {
|
|
5660
5873
|
return this._adapter.dialect;
|
|
5661
5874
|
}
|
|
5875
|
+
/**
|
|
5876
|
+
* True when a table opts into the observation/changelog substrate
|
|
5877
|
+
* (`def.changelog`). Callers that want to bypass the high-level {@link delete}
|
|
5878
|
+
* with a transaction-scoped raw delete use this to know whether the table also
|
|
5879
|
+
* needs the changelog / write-hook / embedding side effects that only
|
|
5880
|
+
* `delete()` performs — so they can keep the high-level path for such tables.
|
|
5881
|
+
*/
|
|
5882
|
+
isChangelogTracked(table) {
|
|
5883
|
+
return this._changelogTables.has(table);
|
|
5884
|
+
}
|
|
5885
|
+
/**
|
|
5886
|
+
* True when this connection opened as a scoped cloud MEMBER (see
|
|
5887
|
+
* {@link _cloudMemberOpen}). Callers use it to route DDL-bearing work through
|
|
5888
|
+
* the owner-side SECURITY DEFINER helpers rather than issuing DDL the member's
|
|
5889
|
+
* role can't run (e.g. {@link addColumn} regenerates the masking view inside
|
|
5890
|
+
* `lattice_member_add_column`, so the caller must not also try to regenerate it).
|
|
5891
|
+
*/
|
|
5892
|
+
isCloudMemberOpen() {
|
|
5893
|
+
return this._cloudMemberOpen;
|
|
5894
|
+
}
|
|
5662
5895
|
/**
|
|
5663
5896
|
* Return the normalised primary-key column list for a registered
|
|
5664
5897
|
* table. Falls back to `['id']` for tables registered via raw DDL
|
|
@@ -5735,7 +5968,15 @@ var init_lattice = __esm({
|
|
|
5735
5968
|
assertSafeIdentifier(column, "column");
|
|
5736
5969
|
const existing = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5737
5970
|
if (!existing.includes(column)) {
|
|
5738
|
-
|
|
5971
|
+
if (this._cloudMemberOpen) {
|
|
5972
|
+
await runAsyncOrSync(this._adapter, `SELECT lattice_member_add_column(?, ?, ?)`, [
|
|
5973
|
+
table,
|
|
5974
|
+
column,
|
|
5975
|
+
typeSpec
|
|
5976
|
+
]);
|
|
5977
|
+
} else {
|
|
5978
|
+
await addColumnAsyncOrSync(this._adapter, table, column, typeSpec);
|
|
5979
|
+
}
|
|
5739
5980
|
}
|
|
5740
5981
|
const cols = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5741
5982
|
this._columnCache.set(table, new Set(cols));
|
|
@@ -6667,12 +6908,39 @@ var init_lattice = __esm({
|
|
|
6667
6908
|
async renderInBackground(outputDir, opts = {}) {
|
|
6668
6909
|
const notInit = this._notInitError();
|
|
6669
6910
|
if (notInit) return notInit;
|
|
6911
|
+
if (opts.gateOnOpen && !opts.changedTables) {
|
|
6912
|
+
const start = Date.now();
|
|
6913
|
+
const recorded = readManifest(outputDir);
|
|
6914
|
+
if (recorded != null) {
|
|
6915
|
+
const live = await computeRenderCursor(this._adapter);
|
|
6916
|
+
if (cursorIsFresh(recorded, live)) {
|
|
6917
|
+
opts.onProgress?.({
|
|
6918
|
+
kind: "done",
|
|
6919
|
+
table: null,
|
|
6920
|
+
entitiesRendered: 0,
|
|
6921
|
+
entitiesTotal: 0,
|
|
6922
|
+
tableIndex: 0,
|
|
6923
|
+
tableCount: 0,
|
|
6924
|
+
pct: 100,
|
|
6925
|
+
durationMs: Date.now() - start
|
|
6926
|
+
});
|
|
6927
|
+
const skipped = {
|
|
6928
|
+
filesWritten: [],
|
|
6929
|
+
filesSkipped: 0,
|
|
6930
|
+
durationMs: Date.now() - start
|
|
6931
|
+
};
|
|
6932
|
+
for (const h6 of this._renderHandlers) h6(skipped);
|
|
6933
|
+
return skipped;
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6670
6937
|
if (!opts.changedTables) {
|
|
6671
6938
|
this._pendingRenderAll = false;
|
|
6672
6939
|
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
6673
6940
|
this._autoRenderPending = false;
|
|
6674
6941
|
}
|
|
6675
|
-
|
|
6942
|
+
const { gateOnOpen: _gateOnOpen, ...engineOpts } = opts;
|
|
6943
|
+
return this._renderGuarded(outputDir, engineOpts);
|
|
6676
6944
|
}
|
|
6677
6945
|
/**
|
|
6678
6946
|
* Install a per-viewer read-relation resolver for ALL renders (initial,
|
|
@@ -47863,6 +48131,111 @@ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
|
47863
48131
|
AND g."pk" = ANY(p_pks)
|
|
47864
48132
|
AND o."owner_role" = session_user;
|
|
47865
48133
|
$fn$;
|
|
48134
|
+
|
|
48135
|
+
-- Add a column to a user table AS THE OWNER, on behalf of a scoped member. A
|
|
48136
|
+
-- member's role has no CREATE/ALTER on the schema (the bootstrap REVOKEs CREATE
|
|
48137
|
+
-- from PUBLIC), so a member's GUI "add a field" write (createRow/updateRow with a
|
|
48138
|
+
-- field the table lacks) cannot run ALTER TABLE itself. This SECURITY DEFINER
|
|
48139
|
+
-- helper performs that ALTER \u2014 and the masking-view regen \u2014 with the owner's
|
|
48140
|
+
-- rights, so member-added columns behave identically to owner-added ones.
|
|
48141
|
+
--
|
|
48142
|
+
-- Injection-safe + minimal: p_table must be an existing BASE table in the current
|
|
48143
|
+
-- schema (rejected otherwise); p_type is whitelisted against the exact set the
|
|
48144
|
+
-- library's addColumn emits for an auto-added column (TEXT / INTEGER / REAL, plus
|
|
48145
|
+
-- BOOLEAN) \u2014 never interpolated raw; both identifiers go through %I (quote_ident).
|
|
48146
|
+
-- Member-callable (granted EXECUTE to the member group), but it can only widen the
|
|
48147
|
+
-- schema, never read or alter another member's data.
|
|
48148
|
+
CREATE OR REPLACE FUNCTION lattice_member_add_column(p_table text, p_column text, p_type text)
|
|
48149
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
48150
|
+
DECLARE
|
|
48151
|
+
v_type text;
|
|
48152
|
+
v_view text := p_table || '_v';
|
|
48153
|
+
v_has_view boolean;
|
|
48154
|
+
v_pk_expr text;
|
|
48155
|
+
v_select text;
|
|
48156
|
+
BEGIN
|
|
48157
|
+
-- Never alter internal bookkeeping tables (names start with "_"). The GUI only
|
|
48158
|
+
-- ever calls this for a user entity table; rejecting the rest is defense-in-depth
|
|
48159
|
+
-- against a member invoking the function directly against ownership/audit/policy
|
|
48160
|
+
-- tables.
|
|
48161
|
+
IF left(p_table, 1) = '_' THEN
|
|
48162
|
+
RAISE EXCEPTION 'lattice: cannot add a column to internal table "%"', p_table;
|
|
48163
|
+
END IF;
|
|
48164
|
+
|
|
48165
|
+
-- p_table must be a real base table in THIS schema (search_path is pinned to the
|
|
48166
|
+
-- cloud schema by pinDefinerSearchPath, so to_regclass resolves there).
|
|
48167
|
+
IF NOT EXISTS (
|
|
48168
|
+
SELECT 1 FROM pg_class c
|
|
48169
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48170
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND c.relkind = 'r'
|
|
48171
|
+
) THEN
|
|
48172
|
+
RAISE EXCEPTION 'lattice: no such table "%"', p_table;
|
|
48173
|
+
END IF;
|
|
48174
|
+
|
|
48175
|
+
-- Whitelist the column type. These are exactly the specs addColumn's
|
|
48176
|
+
-- inferColumnType produces (TEXT / INTEGER / REAL); BOOLEAN is allowed too.
|
|
48177
|
+
-- Anything else is rejected \u2014 the type is spliced as %s (NOT %I), so it must be
|
|
48178
|
+
-- a known-safe literal and never caller-controlled SQL.
|
|
48179
|
+
v_type := upper(btrim(p_type));
|
|
48180
|
+
IF v_type NOT IN ('TEXT', 'INTEGER', 'REAL', 'BOOLEAN') THEN
|
|
48181
|
+
RAISE EXCEPTION 'lattice: unsupported column type "%"', p_type;
|
|
48182
|
+
END IF;
|
|
48183
|
+
|
|
48184
|
+
EXECUTE format('ALTER TABLE %I ADD COLUMN IF NOT EXISTS %I %s', p_table, p_column, v_type);
|
|
48185
|
+
|
|
48186
|
+
-- If the table is cell-masked (a "<table>_v" view exists, because some column has
|
|
48187
|
+
-- an audience), the view selects an explicit column list \u2014 so a new column is
|
|
48188
|
+
-- invisible to members until the view is regenerated. Rebuild it the same way the
|
|
48189
|
+
-- owner path (audienceViewSql / regenerateAudienceViewFromDb) does: pass every
|
|
48190
|
+
-- column through except those with an 'owner' audience in __lattice_column_policy
|
|
48191
|
+
-- (CASE WHEN lattice_is_owner(...) THEN col END), re-apply row visibility with
|
|
48192
|
+
-- WHERE lattice_row_visible(table, pk), and keep the member SELECT grant on the
|
|
48193
|
+
-- view. Unmasked tables need no regen \u2014 the member group's table-level base grant
|
|
48194
|
+
-- already covers the new column.
|
|
48195
|
+
SELECT EXISTS (
|
|
48196
|
+
SELECT 1 FROM pg_class c
|
|
48197
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48198
|
+
WHERE n.nspname = current_schema() AND c.relname = v_view AND c.relkind = 'v'
|
|
48199
|
+
) INTO v_has_view;
|
|
48200
|
+
|
|
48201
|
+
IF v_has_view THEN
|
|
48202
|
+
-- Canonical pk expression: CAST("col" AS TEXT) joined by TAB (chr(9)) \u2014 the
|
|
48203
|
+
-- same serialization the RLS policies + audienceViewSql use.
|
|
48204
|
+
SELECT string_agg(format('CAST(%I AS TEXT)', a.attname), ' || chr(9) || '
|
|
48205
|
+
ORDER BY array_position(i.indkey, a.attnum))
|
|
48206
|
+
INTO v_pk_expr
|
|
48207
|
+
FROM pg_index i
|
|
48208
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
48209
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48210
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
|
|
48211
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND i.indisprimary;
|
|
48212
|
+
IF v_pk_expr IS NULL THEN
|
|
48213
|
+
RAISE EXCEPTION 'lattice: cannot regenerate mask view for "%": no primary key', p_table;
|
|
48214
|
+
END IF;
|
|
48215
|
+
|
|
48216
|
+
-- Build the masked SELECT list in column order, applying the per-column policy.
|
|
48217
|
+
SELECT string_agg(
|
|
48218
|
+
CASE
|
|
48219
|
+
WHEN cp."audience" = 'owner'
|
|
48220
|
+
THEN format('CASE WHEN lattice_is_owner(%L, %s) THEN %I END AS %I',
|
|
48221
|
+
p_table, v_pk_expr, cols.column_name, cols.column_name)
|
|
48222
|
+
ELSE format('%I', cols.column_name)
|
|
48223
|
+
END,
|
|
48224
|
+
', ' ORDER BY cols.ordinal_position)
|
|
48225
|
+
INTO v_select
|
|
48226
|
+
FROM information_schema.columns cols
|
|
48227
|
+
LEFT JOIN "__lattice_column_policy" cp
|
|
48228
|
+
ON cp."table_name" = p_table AND cp."column_name" = cols.column_name
|
|
48229
|
+
AND cp."audience" NOT IN ('', 'everyone', 'row-audience')
|
|
48230
|
+
WHERE cols.table_schema = current_schema() AND cols.table_name = p_table;
|
|
48231
|
+
|
|
48232
|
+
EXECUTE format(
|
|
48233
|
+
'CREATE OR REPLACE VIEW %I AS SELECT %s FROM %I WHERE lattice_row_visible(%L, %s)',
|
|
48234
|
+
v_view, v_select, p_table, p_table, v_pk_expr);
|
|
48235
|
+
EXECUTE format('GRANT SELECT ON %I TO ${MEMBER_GROUP}', v_view);
|
|
48236
|
+
END IF;
|
|
48237
|
+
END $fn$;
|
|
48238
|
+
GRANT EXECUTE ON FUNCTION lattice_member_add_column(text, text, text) TO ${MEMBER_GROUP};
|
|
47866
48239
|
`;
|
|
47867
48240
|
}
|
|
47868
48241
|
});
|
|
@@ -47973,6 +48346,11 @@ async function revokeRow(db, table, pk, grantee) {
|
|
|
47973
48346
|
assertPg(db);
|
|
47974
48347
|
await runAsyncOrSync(db.adapter, `SELECT lattice_revoke_row(?, ?, ?)`, [table, pk, grantee]);
|
|
47975
48348
|
}
|
|
48349
|
+
async function batchRowGrants(db, table, pk, grant, revoke) {
|
|
48350
|
+
assertPg(db);
|
|
48351
|
+
for (const grantee of grant) await grantRow(db, table, pk, grantee);
|
|
48352
|
+
for (const grantee of revoke) await revokeRow(db, table, pk, grantee);
|
|
48353
|
+
}
|
|
47976
48354
|
async function revokeMemberRole(db, role) {
|
|
47977
48355
|
assertPg(db);
|
|
47978
48356
|
if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
|
|
@@ -49075,18 +49453,9 @@ function sessionUndoneFilters(undone, sessionId) {
|
|
|
49075
49453
|
if (sessionId) filters.push({ col: "session_id", op: "eq", val: sessionId });
|
|
49076
49454
|
return filters;
|
|
49077
49455
|
}
|
|
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", {
|
|
49456
|
+
function buildAuditRow(table, rowId, op, before, after, sessionId, editTs) {
|
|
49457
|
+
return {
|
|
49084
49458
|
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
49459
|
ts: sanitizeEditTs(editTs) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
49091
49460
|
table_name: table,
|
|
49092
49461
|
row_id: rowId,
|
|
@@ -49095,7 +49464,9 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
49095
49464
|
after_json: after ? JSON.stringify(after) : null,
|
|
49096
49465
|
undone: 0,
|
|
49097
49466
|
session_id: sessionId ?? null
|
|
49098
|
-
}
|
|
49467
|
+
};
|
|
49468
|
+
}
|
|
49469
|
+
function publishMutationFeed(feed, table, rowId, op, before, after, source) {
|
|
49099
49470
|
const labelRow = op === "delete" ? before : after;
|
|
49100
49471
|
feed.publish({
|
|
49101
49472
|
table,
|
|
@@ -49105,17 +49476,28 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
49105
49476
|
summary: feedSummary(op, table, labelRow)
|
|
49106
49477
|
});
|
|
49107
49478
|
}
|
|
49108
|
-
function
|
|
49109
|
-
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
49110
|
-
}
|
|
49111
|
-
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
49479
|
+
async function purgeRedoStack(db, sessionId) {
|
|
49112
49480
|
const undone = await db.query("_lattice_gui_audit", {
|
|
49113
49481
|
filters: sessionUndoneFilters(1, sessionId)
|
|
49114
49482
|
});
|
|
49115
49483
|
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
49484
|
+
}
|
|
49485
|
+
async function appendAudit(db, feed, table, rowId, op, before, after, source = "gui", sessionId, editTs) {
|
|
49486
|
+
await purgeRedoStack(db, sessionId);
|
|
49487
|
+
await db.insert(
|
|
49488
|
+
"_lattice_gui_audit",
|
|
49489
|
+
buildAuditRow(table, rowId, op, before, after, sessionId, editTs)
|
|
49490
|
+
);
|
|
49491
|
+
publishMutationFeed(feed, table, rowId, op, before, after, source);
|
|
49492
|
+
}
|
|
49493
|
+
function isSchemaOp(operation2) {
|
|
49494
|
+
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
49495
|
+
}
|
|
49496
|
+
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
49497
|
+
await purgeRedoStack(db, sessionId);
|
|
49116
49498
|
await db.insert("_lattice_gui_audit", {
|
|
49117
49499
|
id: crypto.randomUUID(),
|
|
49118
|
-
// Explicit ISO ts — see
|
|
49500
|
+
// Explicit ISO ts — see buildAuditRow (the SQLite-only strftime DEFAULT
|
|
49119
49501
|
// rendered "Invalid Date" on the Postgres/cloud path).
|
|
49120
49502
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49121
49503
|
table_name: table,
|
|
@@ -49150,7 +49532,7 @@ async function ensureColumns(db, table, values) {
|
|
|
49150
49532
|
const added = Object.keys(values).filter((k6) => !(k6 in existing));
|
|
49151
49533
|
if (added.length === 0) return [];
|
|
49152
49534
|
for (const col of added) await db.addColumn(table, col, inferColumnType(values[col]));
|
|
49153
|
-
if (db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
49535
|
+
if (!db.isCloudMemberOpen() && db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
49154
49536
|
const cols = db.getRegisteredColumns(table);
|
|
49155
49537
|
const pk = db.getPrimaryKey(table);
|
|
49156
49538
|
if (cols && pk.length > 0) await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
@@ -49272,7 +49654,14 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
49272
49654
|
ctx.clientTs
|
|
49273
49655
|
);
|
|
49274
49656
|
} else {
|
|
49275
|
-
await ctx
|
|
49657
|
+
await hardDelete(ctx, table, id, before);
|
|
49658
|
+
}
|
|
49659
|
+
}
|
|
49660
|
+
async function hardDelete(ctx, table, id, before) {
|
|
49661
|
+
const withClient = ctx.db.adapter.withClient?.bind(ctx.db.adapter);
|
|
49662
|
+
const pkCols = ctx.db.getPrimaryKey(table);
|
|
49663
|
+
const pkCol = pkCols.length === 1 ? pkCols[0] : void 0;
|
|
49664
|
+
if (!withClient || ctx.db.isChangelogTracked(table) || pkCol === void 0) {
|
|
49276
49665
|
await appendAudit(
|
|
49277
49666
|
ctx.db,
|
|
49278
49667
|
ctx.feed,
|
|
@@ -49285,10 +49674,30 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
49285
49674
|
ctx.sessionId,
|
|
49286
49675
|
ctx.clientTs
|
|
49287
49676
|
);
|
|
49677
|
+
await ctx.db.delete(table, id);
|
|
49678
|
+
return;
|
|
49288
49679
|
}
|
|
49680
|
+
const auditRow = buildAuditRow(table, id, "delete", before, null, ctx.sessionId, ctx.clientTs);
|
|
49681
|
+
await purgeRedoStack(ctx.db, ctx.sessionId);
|
|
49682
|
+
const auditCols = AUDIT_COLUMNS.map((c6) => `"${c6}"`).join(", ");
|
|
49683
|
+
const auditPlaceholders = AUDIT_COLUMNS.map(() => "?").join(", ");
|
|
49684
|
+
const auditValues = AUDIT_COLUMNS.map((c6) => auditRow[c6]);
|
|
49685
|
+
const pkColQuoted = pkCol.replace(/"/g, '""');
|
|
49686
|
+
await withClient(async (tx) => {
|
|
49687
|
+
await tx.run(
|
|
49688
|
+
`INSERT INTO "_lattice_gui_audit" (${auditCols}) VALUES (${auditPlaceholders})`,
|
|
49689
|
+
auditValues
|
|
49690
|
+
);
|
|
49691
|
+
await tx.run(`DELETE FROM "${table.replace(/"/g, '""')}" WHERE "${pkColQuoted}" = ?`, [id]);
|
|
49692
|
+
});
|
|
49693
|
+
publishMutationFeed(ctx.feed, table, id, "delete", before, null, ctx.source);
|
|
49289
49694
|
}
|
|
49290
|
-
async function linkRows(ctx, table, body) {
|
|
49291
|
-
|
|
49695
|
+
async function linkRows(ctx, table, body, forceVisibility) {
|
|
49696
|
+
if (forceVisibility !== void 0) {
|
|
49697
|
+
await ctx.db.insertForcingVisibility(table, body, forceVisibility);
|
|
49698
|
+
} else {
|
|
49699
|
+
await ctx.db.link(table, body);
|
|
49700
|
+
}
|
|
49292
49701
|
await appendAudit(ctx.db, ctx.feed, table, null, "link", null, body, ctx.source, ctx.sessionId);
|
|
49293
49702
|
}
|
|
49294
49703
|
async function unlinkRows(ctx, table, body) {
|
|
@@ -49426,12 +49835,23 @@ async function revertEntry(ctx, id) {
|
|
|
49426
49835
|
});
|
|
49427
49836
|
return { ok: true, entry };
|
|
49428
49837
|
}
|
|
49429
|
-
var SCHEMA_OP_PREFIX;
|
|
49838
|
+
var AUDIT_COLUMNS, SCHEMA_OP_PREFIX;
|
|
49430
49839
|
var init_mutations = __esm({
|
|
49431
49840
|
"src/gui/mutations.ts"() {
|
|
49432
49841
|
"use strict";
|
|
49433
49842
|
init_cloud_connect();
|
|
49434
49843
|
init_audience();
|
|
49844
|
+
AUDIT_COLUMNS = [
|
|
49845
|
+
"id",
|
|
49846
|
+
"ts",
|
|
49847
|
+
"table_name",
|
|
49848
|
+
"row_id",
|
|
49849
|
+
"operation",
|
|
49850
|
+
"before_json",
|
|
49851
|
+
"after_json",
|
|
49852
|
+
"undone",
|
|
49853
|
+
"session_id"
|
|
49854
|
+
];
|
|
49435
49855
|
SCHEMA_OP_PREFIX = "schema.";
|
|
49436
49856
|
}
|
|
49437
49857
|
});
|
|
@@ -49718,6 +50138,10 @@ async function readMachineCredential(db, kind) {
|
|
|
49718
50138
|
}
|
|
49719
50139
|
return null;
|
|
49720
50140
|
}
|
|
50141
|
+
async function resolveAnthropicKey(db) {
|
|
50142
|
+
if (isAssistantCredentialCleared(CREDENTIALS.anthropic.kind)) return null;
|
|
50143
|
+
return await readMachineCredential(db, CREDENTIALS.anthropic.kind) ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
50144
|
+
}
|
|
49721
50145
|
function getAggressiveness() {
|
|
49722
50146
|
const n3 = readPreferences().aggressiveness;
|
|
49723
50147
|
if (!Number.isFinite(n3)) return DEFAULT_AGGRESSIVENESS;
|
|
@@ -49748,6 +50172,7 @@ async function getVoiceCredential(db) {
|
|
|
49748
50172
|
return null;
|
|
49749
50173
|
}
|
|
49750
50174
|
async function hasCredential(db, name, envVar) {
|
|
50175
|
+
if (isAssistantCredentialCleared(CREDENTIALS[name].kind)) return false;
|
|
49751
50176
|
return Boolean(await readMachineCredential(db, CREDENTIALS[name].kind)) || Boolean(process.env[envVar]);
|
|
49752
50177
|
}
|
|
49753
50178
|
async function resolveClaudeAuth(db) {
|
|
@@ -49770,7 +50195,7 @@ async function resolveClaudeAuth(db) {
|
|
|
49770
50195
|
} catch {
|
|
49771
50196
|
}
|
|
49772
50197
|
}
|
|
49773
|
-
const apiKey = await
|
|
50198
|
+
const apiKey = await resolveAnthropicKey(db);
|
|
49774
50199
|
return apiKey ? { apiKey } : null;
|
|
49775
50200
|
}
|
|
49776
50201
|
async function hasClaudeAuth(db) {
|
|
@@ -49867,6 +50292,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
49867
50292
|
}
|
|
49868
50293
|
const cred = CREDENTIALS[name];
|
|
49869
50294
|
setAssistantCredential(cred.kind, key);
|
|
50295
|
+
clearAssistantCredentialCleared(cred.kind);
|
|
49870
50296
|
if (db) {
|
|
49871
50297
|
for (const row of await liveSecretsOfKind(db, cred.kind)) {
|
|
49872
50298
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -49883,6 +50309,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
49883
50309
|
return true;
|
|
49884
50310
|
}
|
|
49885
50311
|
deleteAssistantCredential(CREDENTIALS[name].kind);
|
|
50312
|
+
setAssistantCredentialCleared(CREDENTIALS[name].kind);
|
|
49886
50313
|
if (db) {
|
|
49887
50314
|
for (const row of await liveSecretsOfKind(db, CREDENTIALS[name].kind)) {
|
|
49888
50315
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -52072,7 +52499,7 @@ function buildSchema(db) {
|
|
|
52072
52499
|
}
|
|
52073
52500
|
return out;
|
|
52074
52501
|
}
|
|
52075
|
-
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false) {
|
|
52502
|
+
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false, privateMode = false) {
|
|
52076
52503
|
if (!text.trim()) return [];
|
|
52077
52504
|
const auth = await resolveClaudeAuth(db);
|
|
52078
52505
|
if (!auth) {
|
|
@@ -52094,6 +52521,7 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52094
52521
|
});
|
|
52095
52522
|
return [];
|
|
52096
52523
|
}
|
|
52524
|
+
const forceVis = privateMode ? "private" : void 0;
|
|
52097
52525
|
const temperature = aggressivenessToTemperature(aggressiveness);
|
|
52098
52526
|
let description = "";
|
|
52099
52527
|
try {
|
|
@@ -52136,11 +52564,16 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52136
52564
|
}
|
|
52137
52565
|
if (jx) {
|
|
52138
52566
|
try {
|
|
52139
|
-
await linkRows(
|
|
52140
|
-
|
|
52141
|
-
|
|
52142
|
-
|
|
52143
|
-
|
|
52567
|
+
await linkRows(
|
|
52568
|
+
mctx,
|
|
52569
|
+
jx.junction,
|
|
52570
|
+
{
|
|
52571
|
+
id: crypto.randomUUID(),
|
|
52572
|
+
[jx.fileFk]: fileId,
|
|
52573
|
+
[jx.otherFk]: m4.id
|
|
52574
|
+
},
|
|
52575
|
+
forceVis
|
|
52576
|
+
);
|
|
52144
52577
|
linkedCount++;
|
|
52145
52578
|
if (created) {
|
|
52146
52579
|
mctx.feed.publish({
|
|
@@ -52199,16 +52632,21 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52199
52632
|
if ("name" in cols && row.name == null) row.name = obj2.label;
|
|
52200
52633
|
if ("title" in cols && row.title == null) row.title = obj2.label;
|
|
52201
52634
|
try {
|
|
52202
|
-
const { id: rowId } = await createRow(mctx, entity, row);
|
|
52635
|
+
const { id: rowId } = await createRow(mctx, entity, row, forceVis);
|
|
52203
52636
|
createdCount++;
|
|
52204
52637
|
const ent = entity;
|
|
52205
52638
|
const jx = junctions.find((j6) => j6.otherTable === ent) ?? (createJunction ? await createJunction(ent) : null);
|
|
52206
52639
|
if (jx) {
|
|
52207
|
-
await linkRows(
|
|
52208
|
-
|
|
52209
|
-
|
|
52210
|
-
|
|
52211
|
-
|
|
52640
|
+
await linkRows(
|
|
52641
|
+
mctx,
|
|
52642
|
+
jx.junction,
|
|
52643
|
+
{
|
|
52644
|
+
id: crypto.randomUUID(),
|
|
52645
|
+
[jx.fileFk]: fileId,
|
|
52646
|
+
[jx.otherFk]: rowId
|
|
52647
|
+
},
|
|
52648
|
+
forceVis
|
|
52649
|
+
);
|
|
52212
52650
|
}
|
|
52213
52651
|
} catch (e6) {
|
|
52214
52652
|
console.warn(`[ingest] create ${entity} from document failed:`, e6.message);
|
|
@@ -52222,12 +52660,17 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
52222
52660
|
try {
|
|
52223
52661
|
const title = name.replace(/\.[^./\\]+$/, "").trim() || "Note";
|
|
52224
52662
|
const body = description.length > 0 ? description : text.slice(0, 2e3);
|
|
52225
|
-
const { id: noteId } = await createRow(
|
|
52226
|
-
|
|
52227
|
-
|
|
52228
|
-
|
|
52229
|
-
|
|
52230
|
-
|
|
52663
|
+
const { id: noteId } = await createRow(
|
|
52664
|
+
mctx,
|
|
52665
|
+
"notes",
|
|
52666
|
+
{
|
|
52667
|
+
id: crypto.randomUUID(),
|
|
52668
|
+
title,
|
|
52669
|
+
body,
|
|
52670
|
+
source_file_id: fileId
|
|
52671
|
+
},
|
|
52672
|
+
forceVis
|
|
52673
|
+
);
|
|
52231
52674
|
mctx.feed.publish({
|
|
52232
52675
|
table: "notes",
|
|
52233
52676
|
op: "insert",
|
|
@@ -52341,7 +52784,8 @@ async function ingestUrlAsFile(ctx, rawUrl, opts = {}) {
|
|
|
52341
52784
|
ctx.enrich.createJunction,
|
|
52342
52785
|
ctx.enrich.aggressiveness,
|
|
52343
52786
|
ctx.enrich.createEntity,
|
|
52344
|
-
true
|
|
52787
|
+
true,
|
|
52788
|
+
ctx.privateMode === true
|
|
52345
52789
|
);
|
|
52346
52790
|
}
|
|
52347
52791
|
return {
|
|
@@ -53220,13 +53664,22 @@ function loadSdk() {
|
|
|
53220
53664
|
throw new Error("Could not resolve the Anthropic constructor from '@anthropic-ai/sdk'");
|
|
53221
53665
|
return ctor;
|
|
53222
53666
|
}
|
|
53223
|
-
function
|
|
53224
|
-
const Anthropic = loadSdk();
|
|
53667
|
+
function buildAnthropicConfig(auth) {
|
|
53225
53668
|
const config = {};
|
|
53226
|
-
if (auth.authToken)
|
|
53227
|
-
|
|
53669
|
+
if (auth.authToken) {
|
|
53670
|
+
config.authToken = auth.authToken;
|
|
53671
|
+
config.apiKey = null;
|
|
53672
|
+
} else if (auth.apiKey) {
|
|
53673
|
+
config.apiKey = auth.apiKey;
|
|
53674
|
+
} else {
|
|
53675
|
+
config.apiKey = null;
|
|
53676
|
+
}
|
|
53228
53677
|
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
53229
|
-
|
|
53678
|
+
return config;
|
|
53679
|
+
}
|
|
53680
|
+
function createAnthropicClient(auth) {
|
|
53681
|
+
const Anthropic = loadSdk();
|
|
53682
|
+
const sdk = new Anthropic(buildAnthropicConfig(auth));
|
|
53230
53683
|
return {
|
|
53231
53684
|
async runTurn(params) {
|
|
53232
53685
|
const stream = sdk.messages.stream({
|
|
@@ -54488,8 +54941,14 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
54488
54941
|
},
|
|
54489
54942
|
{
|
|
54490
54943
|
name: "_lattice_gui_audit",
|
|
54491
|
-
|
|
54492
|
-
|
|
54944
|
+
// UPDATE + DELETE are needed by undo/redo/revert (flips an entry's `undone`)
|
|
54945
|
+
// and the redo-stack purge on a new mutation (deletes the session's undone
|
|
54946
|
+
// entries). Safe because enableGuiAuditRls installs per-op UPDATE and DELETE
|
|
54947
|
+
// policies whose USING is `row_id IS NULL OR lattice_row_visible(table_name,
|
|
54948
|
+
// row_id)` — so a member can only update/delete audit rows for entities it can
|
|
54949
|
+
// already see (or schema-level entries that carry no row data).
|
|
54950
|
+
privs: "SELECT, INSERT, UPDATE, DELETE",
|
|
54951
|
+
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
54952
|
},
|
|
54494
54953
|
{
|
|
54495
54954
|
name: "__lattice_user_identity",
|
|
@@ -54890,6 +55349,19 @@ async function normalizeImage(path2, maxBytes) {
|
|
|
54890
55349
|
function renderJpeg(sharp, path2, quality) {
|
|
54891
55350
|
return sharp(path2).rotate().resize({ width: MAX_DIM, height: MAX_DIM, fit: "inside", withoutEnlargement: true }).jpeg({ quality }).toBuffer();
|
|
54892
55351
|
}
|
|
55352
|
+
function buildVisionAnthropicConfig(auth) {
|
|
55353
|
+
const config = {};
|
|
55354
|
+
if (auth.authToken) {
|
|
55355
|
+
config.authToken = auth.authToken;
|
|
55356
|
+
config.apiKey = null;
|
|
55357
|
+
} else if (auth.apiKey) {
|
|
55358
|
+
config.apiKey = auth.apiKey;
|
|
55359
|
+
} else {
|
|
55360
|
+
config.apiKey = null;
|
|
55361
|
+
}
|
|
55362
|
+
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
55363
|
+
return config;
|
|
55364
|
+
}
|
|
54893
55365
|
function defaultSender(auth) {
|
|
54894
55366
|
return async (input) => {
|
|
54895
55367
|
const importMetaUrl = import.meta.url;
|
|
@@ -54897,11 +55369,7 @@ function defaultSender(auth) {
|
|
|
54897
55369
|
const sdk = req("@anthropic-ai/sdk");
|
|
54898
55370
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
54899
55371
|
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);
|
|
55372
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
54905
55373
|
const res = await client.messages.create({
|
|
54906
55374
|
model: input.model,
|
|
54907
55375
|
max_tokens: 1024,
|
|
@@ -54928,11 +55396,7 @@ function defaultPdfSender(auth) {
|
|
|
54928
55396
|
const sdk = req("@anthropic-ai/sdk");
|
|
54929
55397
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
54930
55398
|
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);
|
|
55399
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
54936
55400
|
const res = await client.messages.create({
|
|
54937
55401
|
model: input.model,
|
|
54938
55402
|
max_tokens: 4096,
|
|
@@ -55880,6 +56344,8 @@ var css = `
|
|
|
55880
56344
|
.grants-panel .grants-title { font-weight: 600; margin-bottom: 6px; }
|
|
55881
56345
|
.grants-panel .grants-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
|
|
55882
56346
|
.grants-panel .grants-row input { accent-color: var(--accent); }
|
|
56347
|
+
.grants-panel .grants-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
|
|
56348
|
+
.grants-panel .grants-dirty { font-size: 12px; }
|
|
55883
56349
|
|
|
55884
56350
|
/* Inline create-row at the bottom of every table */
|
|
55885
56351
|
tr.create-row td { background: var(--surface-2); }
|
|
@@ -56224,6 +56690,25 @@ var css = `
|
|
|
56224
56690
|
animation: feedSpin 0.7s linear infinite; vertical-align: middle;
|
|
56225
56691
|
}
|
|
56226
56692
|
@keyframes feedSpin { to { transform: rotate(360deg); } }
|
|
56693
|
+
/* Batch-upload progress bar \u2014 pinned to the top of the feed while a
|
|
56694
|
+
multi-file drop drains through the bounded-concurrency queue. */
|
|
56695
|
+
.ingest-progress {
|
|
56696
|
+
position: sticky; top: 0; z-index: 3;
|
|
56697
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
56698
|
+
padding: 8px 10px; border-radius: 8px;
|
|
56699
|
+
background: var(--surface); border: 1px solid rgba(190, 242, 100, 0.22);
|
|
56700
|
+
box-shadow: var(--shadow-1), var(--glow-accent-soft);
|
|
56701
|
+
}
|
|
56702
|
+
.ingest-progress-label { font-size: 12px; font-weight: 500; color: var(--text); }
|
|
56703
|
+
.ingest-progress-track {
|
|
56704
|
+
height: 6px; border-radius: 999px; overflow: hidden; background: var(--border-strong);
|
|
56705
|
+
}
|
|
56706
|
+
.ingest-progress-fill {
|
|
56707
|
+
height: 100%; width: 0%; border-radius: 999px;
|
|
56708
|
+
background: linear-gradient(90deg, var(--accent-deep), var(--accent));
|
|
56709
|
+
box-shadow: 0 0 8px rgba(190, 242, 100, 0.5);
|
|
56710
|
+
transition: width 0.3s ease;
|
|
56711
|
+
}
|
|
56227
56712
|
.assistant-rail {
|
|
56228
56713
|
position: relative;
|
|
56229
56714
|
background:
|
|
@@ -58327,6 +58812,15 @@ var appJs = `
|
|
|
58327
58812
|
// Per-table view state: 'live' (default) or 'trash' (soft-deleted rows).
|
|
58328
58813
|
var tableViewMode = {};
|
|
58329
58814
|
|
|
58815
|
+
// The (table, pk) of the per-row "Manage access" grants panel that is
|
|
58816
|
+
// currently open, or null when none is. A soft re-render (a concurrent edit
|
|
58817
|
+
// by another client fires pg_notify \u2192 realtime refresh \u2192 renderRoute({soft})
|
|
58818
|
+
// \u2192 renderDetail/renderFsItem repaint) would otherwise re-create the detail
|
|
58819
|
+
// view with the panel collapsed, dropping a staged multi-select mid-edit.
|
|
58820
|
+
// wireRowSharing reads this after each repaint and re-opens + re-populates the
|
|
58821
|
+
// panel WITHOUT any network call, so the staged selection survives.
|
|
58822
|
+
var openGrantsPanel = null;
|
|
58823
|
+
|
|
58330
58824
|
function renderTable(content, tableName) {
|
|
58331
58825
|
var myGen = renderGen;
|
|
58332
58826
|
clearUnseen(tableName);
|
|
@@ -58805,70 +59299,151 @@ var appJs = `
|
|
|
58805
59299
|
}).catch(function (e) { showToast('Visibility update failed: ' + e.message, {}); });
|
|
58806
59300
|
});
|
|
58807
59301
|
});
|
|
58808
|
-
var
|
|
58809
|
-
|
|
59302
|
+
var access = row._access || {};
|
|
59303
|
+
|
|
59304
|
+
// Render the staged member checklist + a single "Save sharing" / "Cancel"
|
|
59305
|
+
// into the panel. Checkbox toggles mutate ONLY the local desired map \u2014
|
|
59306
|
+
// NO network call per toggle (the old design auto-saved live, one POST per
|
|
59307
|
+
// checkbox, and each grant's pg_notify collapsed the panel). A single batch
|
|
59308
|
+
// request fires on Save. members is the already-fetched list; desired
|
|
59309
|
+
// seeds from the row's current grantees (or a caller-supplied staged map
|
|
59310
|
+
// when re-opening after a soft re-render).
|
|
59311
|
+
function populateGrantsPanel(panel, members, desired) {
|
|
59312
|
+
// Snapshot the CURRENT (committed) grantees so Save can diff desired-vs-
|
|
59313
|
+
// current into adds/removes. effectiveVisibility decides whether we're
|
|
59314
|
+
// actually switching INTO specific-people mode (custom-0 reads as private).
|
|
59315
|
+
var current = {};
|
|
59316
|
+
(access.grantees || []).forEach(function (g) { current[g] = true; });
|
|
59317
|
+
if (members.length === 0) {
|
|
59318
|
+
panel.innerHTML = '<div class="muted">No other members in this workspace yet.</div>';
|
|
59319
|
+
panel.hidden = false;
|
|
59320
|
+
return;
|
|
59321
|
+
}
|
|
59322
|
+
function dirtyCount() {
|
|
59323
|
+
var n = 0;
|
|
59324
|
+
members.forEach(function (m) {
|
|
59325
|
+
if (!!desired[m.role] !== !!current[m.role]) n++;
|
|
59326
|
+
});
|
|
59327
|
+
return n;
|
|
59328
|
+
}
|
|
59329
|
+
function render() {
|
|
59330
|
+
var changed = dirtyCount();
|
|
59331
|
+
panel.innerHTML = '<div class="grants-title">Who can see this</div>' +
|
|
59332
|
+
members.map(function (m) {
|
|
59333
|
+
var label = m.name || m.email || m.role;
|
|
59334
|
+
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
59335
|
+
(desired[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
59336
|
+
}).join('') +
|
|
59337
|
+
'<div class="grants-actions">' +
|
|
59338
|
+
'<button class="btn primary" id="grants-save"' + (changed ? '' : ' disabled') + '>Save sharing</button>' +
|
|
59339
|
+
'<button class="btn" id="grants-cancel">Cancel</button>' +
|
|
59340
|
+
'<span class="grants-dirty muted">' + (changed ? (changed === 1 ? '1 change' : changed + ' changes') : 'No changes') + '</span>' +
|
|
59341
|
+
'</div>';
|
|
59342
|
+
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
59343
|
+
cb.addEventListener('change', function () {
|
|
59344
|
+
var role = cb.getAttribute('data-grant-role');
|
|
59345
|
+
if (cb.checked) desired[role] = true; else delete desired[role];
|
|
59346
|
+
render(); // re-render to refresh the dirty indicator + Save state
|
|
59347
|
+
});
|
|
59348
|
+
});
|
|
59349
|
+
var cancelBtn = panel.querySelector('#grants-cancel');
|
|
59350
|
+
if (cancelBtn) cancelBtn.addEventListener('click', function () { closeGrantsPanel(panel); });
|
|
59351
|
+
var saveBtn = panel.querySelector('#grants-save');
|
|
59352
|
+
if (saveBtn) saveBtn.addEventListener('click', function () {
|
|
59353
|
+
var toAdd = [];
|
|
59354
|
+
var toRemove = [];
|
|
59355
|
+
members.forEach(function (m) {
|
|
59356
|
+
var want = !!desired[m.role];
|
|
59357
|
+
var have = !!current[m.role];
|
|
59358
|
+
if (want && !have) toAdd.push(m.role);
|
|
59359
|
+
if (!want && have) toRemove.push(m.role);
|
|
59360
|
+
});
|
|
59361
|
+
if (toAdd.length === 0 && toRemove.length === 0) { closeGrantsPanel(panel); return; }
|
|
59362
|
+
// Confirm the mode change ONCE, here \u2014 only when actually switching
|
|
59363
|
+
// INTO specific-people mode (effective vis isn't already custom AND we
|
|
59364
|
+
// are adding at least one grantee). Never per checkbox.
|
|
59365
|
+
if (effectiveVisibility(access) !== 'custom' && toAdd.length > 0) {
|
|
59366
|
+
if (!confirm('Sharing this with specific people switches it off "everyone"/"private". The chosen people will be able to see it. Continue?')) return;
|
|
59367
|
+
}
|
|
59368
|
+
withBusy(saveBtn, function () {
|
|
59369
|
+
return fetchJson('/api/cloud/row-grants', {
|
|
59370
|
+
method: 'POST',
|
|
59371
|
+
headers: { 'content-type': 'application/json' },
|
|
59372
|
+
body: JSON.stringify({ table: tableName, pk: id, grant: toAdd, revoke: toRemove }),
|
|
59373
|
+
}).then(function () {
|
|
59374
|
+
// Mirror the committed state locally so the re-render's indicator
|
|
59375
|
+
// is correct. The first grant flips the row to custom server-side;
|
|
59376
|
+
// revoking the last leaves custom-0, which effectiveVisibility
|
|
59377
|
+
// renders as private.
|
|
59378
|
+
var list = [];
|
|
59379
|
+
members.forEach(function (m) { if (desired[m.role]) list.push(m.role); });
|
|
59380
|
+
access.grantees = list;
|
|
59381
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
59382
|
+
openGrantsPanel = null; // a successful save closes the staging session
|
|
59383
|
+
invalidate(tableName);
|
|
59384
|
+
showToast('Sharing updated', {});
|
|
59385
|
+
reRender();
|
|
59386
|
+
}).catch(function (e) {
|
|
59387
|
+
// Surface loudly + leave the staged selection intact so the user
|
|
59388
|
+
// can retry; no silent partial-success.
|
|
59389
|
+
showToast('Sharing update failed: ' + e.message, {});
|
|
59390
|
+
});
|
|
59391
|
+
});
|
|
59392
|
+
});
|
|
59393
|
+
panel.hidden = false;
|
|
59394
|
+
}
|
|
59395
|
+
render();
|
|
59396
|
+
}
|
|
59397
|
+
|
|
59398
|
+
function closeGrantsPanel(panel) {
|
|
59399
|
+
if (panel) panel.hidden = true;
|
|
59400
|
+
openGrantsPanel = null;
|
|
59401
|
+
}
|
|
59402
|
+
|
|
59403
|
+
// Open (or toggle shut) the manage-access panel. Fetches the member list,
|
|
59404
|
+
// then stages from the row's current grantees. Opening must NOT pre-flip
|
|
59405
|
+
// the row to 'custom' \u2014 that left a never-shared row stuck at "custom (0)".
|
|
59406
|
+
function openManagePanel(triggerBtn) {
|
|
58810
59407
|
var panel = content.querySelector('#grants-panel');
|
|
58811
59408
|
if (!panel) return;
|
|
58812
|
-
if (!panel.hidden) { panel
|
|
58813
|
-
|
|
58814
|
-
|
|
58815
|
-
// row the user never actually shared stuck at "custom (0)". The first
|
|
58816
|
-
// grant flips it to custom server-side (lattice_grant_row); revoking the
|
|
58817
|
-
// last leaves it custom-with-0-grantees, which now reads as private. So
|
|
58818
|
-
// just load the member checklist.
|
|
58819
|
-
var ensure = Promise.resolve();
|
|
58820
|
-
withBusy(detailVisManage, function () {
|
|
58821
|
-
return ensure.then(function () {
|
|
58822
|
-
return fetchJson('/api/cloud/members');
|
|
58823
|
-
}).then(function (d) {
|
|
59409
|
+
if (!panel.hidden) { closeGrantsPanel(panel); return; }
|
|
59410
|
+
withBusy(triggerBtn, function () {
|
|
59411
|
+
return fetchJson('/api/cloud/members').then(function (d) {
|
|
58824
59412
|
// The grant target is a member ROLE: lattice_grant_row keys on the
|
|
58825
59413
|
// role, and _access.grantees holds role names. List every member
|
|
58826
59414
|
// except the owner (you don't grant the owner their own row).
|
|
58827
59415
|
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
58828
|
-
var
|
|
58829
|
-
(access.grantees || []).forEach(function (g) {
|
|
58830
|
-
|
|
58831
|
-
|
|
58832
|
-
} else {
|
|
58833
|
-
panel.innerHTML = '<div class="grants-title">Who can see this</div>' + members.map(function (m) {
|
|
58834
|
-
var label = m.name || m.email || m.role;
|
|
58835
|
-
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
58836
|
-
(granted[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
58837
|
-
}).join('');
|
|
58838
|
-
}
|
|
58839
|
-
panel.hidden = false;
|
|
58840
|
-
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
58841
|
-
cb.addEventListener('change', function () {
|
|
58842
|
-
var role = cb.getAttribute('data-grant-role');
|
|
58843
|
-
cb.disabled = true;
|
|
58844
|
-
fetchJson('/api/cloud/row-grant', {
|
|
58845
|
-
method: 'POST',
|
|
58846
|
-
headers: { 'content-type': 'application/json' },
|
|
58847
|
-
body: JSON.stringify({ table: tableName, pk: id, grantee: role, revoke: !cb.checked }),
|
|
58848
|
-
}).then(function () {
|
|
58849
|
-
var list = access.grantees || (access.grantees = []);
|
|
58850
|
-
var at = list.indexOf(role);
|
|
58851
|
-
if (cb.checked && at === -1) list.push(role);
|
|
58852
|
-
if (!cb.checked && at !== -1) list.splice(at, 1);
|
|
58853
|
-
// The first grant flips the row to custom server-side; mirror
|
|
58854
|
-
// that locally so the indicator updates. Revoking the last leaves
|
|
58855
|
-
// visibility 'custom' but effectiveVisibility renders custom-0 as
|
|
58856
|
-
// private, so the label flips back to "Private to you".
|
|
58857
|
-
if (list.length > 0) access.visibility = 'custom';
|
|
58858
|
-
var infoEl = content.querySelector('#detail-vis-info');
|
|
58859
|
-
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
58860
|
-
invalidate(tableName);
|
|
58861
|
-
}).catch(function (e) {
|
|
58862
|
-
cb.checked = !cb.checked; // revert the failed change
|
|
58863
|
-
showToast('Access update failed: ' + e.message, {});
|
|
58864
|
-
}).then(function () { cb.disabled = false; });
|
|
58865
|
-
});
|
|
58866
|
-
});
|
|
58867
|
-
var infoEl = content.querySelector('#detail-vis-info');
|
|
58868
|
-
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
59416
|
+
var desired = {};
|
|
59417
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
59418
|
+
openGrantsPanel = { table: tableName, pk: id };
|
|
59419
|
+
populateGrantsPanel(panel, members, desired);
|
|
58869
59420
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
58870
59421
|
});
|
|
59422
|
+
}
|
|
59423
|
+
|
|
59424
|
+
var detailVisManage = content.querySelector('#detail-vis-manage');
|
|
59425
|
+
if (detailVisManage) detailVisManage.addEventListener('click', function () {
|
|
59426
|
+
openManagePanel(detailVisManage);
|
|
58871
59427
|
});
|
|
59428
|
+
|
|
59429
|
+
// Preserve an open panel across a soft re-render: if the tracked panel
|
|
59430
|
+
// matches the row this view just repainted, re-open it and re-populate the
|
|
59431
|
+
// checklist from the freshly-fetched row._access WITHOUT any network call,
|
|
59432
|
+
// so a concurrent edit by another client doesn't lose a staged selection.
|
|
59433
|
+
if (openGrantsPanel && openGrantsPanel.table === tableName && openGrantsPanel.pk === id) {
|
|
59434
|
+
var rpanel = content.querySelector('#grants-panel');
|
|
59435
|
+
if (rpanel) {
|
|
59436
|
+
fetchJson('/api/cloud/members').then(function (d) {
|
|
59437
|
+
// Only re-populate if THIS panel is still the tracked-open one (a
|
|
59438
|
+
// newer navigation/save may have cleared it while members loaded).
|
|
59439
|
+
if (!openGrantsPanel || openGrantsPanel.table !== tableName || openGrantsPanel.pk !== id) return;
|
|
59440
|
+
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
59441
|
+
var desired = {};
|
|
59442
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
59443
|
+
populateGrantsPanel(rpanel, members, desired);
|
|
59444
|
+
}).catch(function () { /* best-effort restore; a click reopens it */ });
|
|
59445
|
+
}
|
|
59446
|
+
}
|
|
58872
59447
|
}
|
|
58873
59448
|
function renderDetail(content, tableName, id) {
|
|
58874
59449
|
var myGen = renderGen;
|
|
@@ -63560,6 +64135,63 @@ var appJs = `
|
|
|
63560
64135
|
// Browsers can't expose the local path, so we POST the bytes; the
|
|
63561
64136
|
// server extracts + summarizes, then discards them (path stays null).
|
|
63562
64137
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
64138
|
+
// Cap how many uploads are in flight at once. A browser allows only ~6
|
|
64139
|
+
// HTTP/1.1 connections per host, so a bulk drop of N files would fire N
|
|
64140
|
+
// upload POSTs in parallel and saturate that budget \u2014 every other data
|
|
64141
|
+
// request (entities, rows, navigation) then queues for minutes behind the
|
|
64142
|
+
// multi-minute ingests and the GUI looks frozen. Holding uploads to a few
|
|
64143
|
+
// at a time leaves connections free for the rest of the app (and eases the
|
|
64144
|
+
// AI rate limit each ingest hits server-side). The realtime/feed streams are
|
|
64145
|
+
// already off this budget \u2014 they share one WebSocket \u2014 so this is the last
|
|
64146
|
+
// place a big batch could starve the connection pool.
|
|
64147
|
+
var INGEST_MAX_CONCURRENCY = 3;
|
|
64148
|
+
// Run a batch of upload thunks with at most \`limit\` in flight, calling
|
|
64149
|
+
// onProgress(done, total) as each settles. One failure never stalls the
|
|
64150
|
+
// batch \u2014 uploadFile surfaces its own error and resolves.
|
|
64151
|
+
function runIngestBatch(thunks, limit, onProgress) {
|
|
64152
|
+
return new Promise(function (resolve) {
|
|
64153
|
+
var total = thunks.length, idx = 0, done = 0;
|
|
64154
|
+
function startNext() {
|
|
64155
|
+
if (idx >= total) return;
|
|
64156
|
+
var thunk = thunks[idx++];
|
|
64157
|
+
Promise.resolve().then(thunk).catch(function () { /* already surfaced */ }).then(function () {
|
|
64158
|
+
done++;
|
|
64159
|
+
if (onProgress) onProgress(done, total);
|
|
64160
|
+
if (done === total) resolve(); else startNext();
|
|
64161
|
+
});
|
|
64162
|
+
}
|
|
64163
|
+
for (var i = 0; i < Math.min(limit, total); i++) startNext();
|
|
64164
|
+
});
|
|
64165
|
+
}
|
|
64166
|
+
// A batch-upload progress bar pinned to the top of the rail feed
|
|
64167
|
+
// ("Analyzing N of M\u2026"). The per-file "Analyzing <name>\u2026" cards still
|
|
64168
|
+
// appear, but only INGEST_MAX_CONCURRENCY at a time; this gives the
|
|
64169
|
+
// whole-batch view that the individual cards can't. Returns
|
|
64170
|
+
// { update(done, total), done() }.
|
|
64171
|
+
function ingestProgress(total) {
|
|
64172
|
+
var feedEl = document.getElementById('rail-feed');
|
|
64173
|
+
if (!feedEl) return { update: function () {}, done: function () {} };
|
|
64174
|
+
railEmptyGone();
|
|
64175
|
+
var wrap = document.createElement('div');
|
|
64176
|
+
wrap.className = 'ingest-progress';
|
|
64177
|
+
wrap.innerHTML =
|
|
64178
|
+
'<div class="ingest-progress-label">Analyzing 0 of ' + total + '\u2026</div>' +
|
|
64179
|
+
'<div class="ingest-progress-track"><div class="ingest-progress-fill"></div></div>';
|
|
64180
|
+
feedEl.insertBefore(wrap, feedEl.firstChild);
|
|
64181
|
+
var label = wrap.querySelector('.ingest-progress-label');
|
|
64182
|
+
var fill = wrap.querySelector('.ingest-progress-fill');
|
|
64183
|
+
return {
|
|
64184
|
+
update: function (n, t) {
|
|
64185
|
+
if (label) label.textContent = 'Analyzing ' + n + ' of ' + t + '\u2026';
|
|
64186
|
+
if (fill) fill.style.width = Math.round((n / t) * 100) + '%';
|
|
64187
|
+
},
|
|
64188
|
+
done: function () {
|
|
64189
|
+
if (fill) fill.style.width = '100%';
|
|
64190
|
+
if (label) label.textContent = 'Analyzed ' + total + ' file' + (total === 1 ? '' : 's');
|
|
64191
|
+
setTimeout(function () { if (wrap.parentNode) wrap.parentNode.removeChild(wrap); }, 2500);
|
|
64192
|
+
},
|
|
64193
|
+
};
|
|
64194
|
+
}
|
|
63563
64195
|
// Append a transient "Analyzing <file>\u2026" row to the feed so the user sees
|
|
63564
64196
|
// the ingest is processing in the background; returns a disposer. The real
|
|
63565
64197
|
// create/link feed events stream in over SSE as the server materializes them.
|
|
@@ -63595,13 +64227,21 @@ var appJs = `
|
|
|
63595
64227
|
}
|
|
63596
64228
|
function uploadFile(file) {
|
|
63597
64229
|
var done = pendingIngestItem(file.name || 'file');
|
|
64230
|
+
// Carry the composer's "Private mode" intent so an upload made while the
|
|
64231
|
+
// box is checked is stamped private at insert, instead of inheriting the
|
|
64232
|
+
// files-table default (which can be shared-to-everyone on a cloud). Read
|
|
64233
|
+
// the checkbox defensively \u2014 it may not be rendered. On a local workspace
|
|
64234
|
+
// the box is checked+disabled, so this is '1' there too; forced visibility
|
|
64235
|
+
// is a harmless no-op on the single-user SQLite path.
|
|
64236
|
+
var pv = document.getElementById('chat-private');
|
|
64237
|
+
var priv = pv && pv.checked ? '1' : '0';
|
|
63598
64238
|
return fetch('/api/ingest/upload', {
|
|
63599
64239
|
method: 'POST',
|
|
63600
64240
|
// Percent-encode the filename: HTTP header values must be ISO-8859-1,
|
|
63601
64241
|
// so a Unicode filename (emoji, smart quote, accent, em-dash) would
|
|
63602
64242
|
// otherwise make fetch() throw "String contains non ISO-8859-1 code
|
|
63603
64243
|
// point". The server decodeURIComponent()s it back.
|
|
63604
|
-
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file') },
|
|
64244
|
+
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file'), 'x-lattice-private': priv },
|
|
63605
64245
|
body: file,
|
|
63606
64246
|
})
|
|
63607
64247
|
.then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; }); })
|
|
@@ -63619,7 +64259,14 @@ var appJs = `
|
|
|
63619
64259
|
});
|
|
63620
64260
|
return;
|
|
63621
64261
|
}
|
|
63622
|
-
|
|
64262
|
+
// Multi-file: drain through the bounded-concurrency queue (so a big drop
|
|
64263
|
+
// can't saturate the connection budget) with a batch progress bar.
|
|
64264
|
+
var bar = ingestProgress(files.length);
|
|
64265
|
+
var thunks = [];
|
|
64266
|
+
for (var i = 0; i < files.length; i++) {
|
|
64267
|
+
(function (f) { thunks.push(function () { return uploadFile(f); }); })(files[i]);
|
|
64268
|
+
}
|
|
64269
|
+
runIngestBatch(thunks, INGEST_MAX_CONCURRENCY, bar.update).then(bar.done);
|
|
63623
64270
|
}
|
|
63624
64271
|
// Mobile: tapping the handle expands/collapses the bottom drawer.
|
|
63625
64272
|
function initRailDrawer() {
|
|
@@ -64430,7 +65077,7 @@ async function checkForUpdate(pkgName, currentVersion, opts = {}) {
|
|
|
64430
65077
|
// src/update-context.ts
|
|
64431
65078
|
init_user_config();
|
|
64432
65079
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
64433
|
-
import { existsSync as existsSync19, lstatSync as lstatSync2, readFileSync as readFileSync15 } from "fs";
|
|
65080
|
+
import { existsSync as existsSync19, lstatSync as lstatSync2, readFileSync as readFileSync15, realpathSync } from "fs";
|
|
64434
65081
|
import { dirname as dirname7, join as join24, sep as sep6 } from "path";
|
|
64435
65082
|
var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
64436
65083
|
function isValidVersion(v2) {
|
|
@@ -64460,10 +65107,19 @@ function isUnderGlobalPrefix(packageRoot, execPath) {
|
|
|
64460
65107
|
}
|
|
64461
65108
|
function detectInstallContext(opts = {}) {
|
|
64462
65109
|
const pkgName = opts.pkgName ?? "latticesql";
|
|
64463
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
64464
65110
|
const env2 = opts.env ?? process.env;
|
|
64465
65111
|
const execPath = opts.execPath ?? process.execPath;
|
|
64466
|
-
const
|
|
65112
|
+
const rawCwd = opts.cwd ?? process.cwd();
|
|
65113
|
+
const rawModulePath = opts.modulePath ?? process.argv[1] ?? rawCwd;
|
|
65114
|
+
const resolveReal = (p3) => {
|
|
65115
|
+
try {
|
|
65116
|
+
return realpathSync(p3);
|
|
65117
|
+
} catch {
|
|
65118
|
+
return p3;
|
|
65119
|
+
}
|
|
65120
|
+
};
|
|
65121
|
+
const modulePath = resolveReal(rawModulePath);
|
|
65122
|
+
const cwd = resolveReal(rawCwd);
|
|
64467
65123
|
const packageRoot = findPackageRoot(dirname7(modulePath), pkgName);
|
|
64468
65124
|
if (packageRoot && existsSync19(join24(packageRoot, ".git"))) {
|
|
64469
65125
|
return {
|
|
@@ -66249,6 +66905,27 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
66249
66905
|
});
|
|
66250
66906
|
return true;
|
|
66251
66907
|
}
|
|
66908
|
+
if (pathname === "/api/cloud/row-grants" && method === "POST") {
|
|
66909
|
+
await tryHandler(res, async () => {
|
|
66910
|
+
const body = await readJson(req);
|
|
66911
|
+
const table = typeof body.table === "string" ? body.table : "";
|
|
66912
|
+
const pk = typeof body.pk === "string" ? body.pk : "";
|
|
66913
|
+
const strList = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : [];
|
|
66914
|
+
const grant = strList(body.grant);
|
|
66915
|
+
const revoke = strList(body.revoke);
|
|
66916
|
+
if (!table || !pk) {
|
|
66917
|
+
sendJson(res, { error: "table and pk are required" }, 400);
|
|
66918
|
+
return;
|
|
66919
|
+
}
|
|
66920
|
+
if (ctx.db.getDialect() !== "postgres") {
|
|
66921
|
+
sendJson(res, { error: "Per-row sharing requires a cloud (Postgres) database" }, 400);
|
|
66922
|
+
return;
|
|
66923
|
+
}
|
|
66924
|
+
await batchRowGrants(ctx.db, table, pk, grant, revoke);
|
|
66925
|
+
sendJson(res, { ok: true, table, pk, granted: grant, revoked: revoke });
|
|
66926
|
+
});
|
|
66927
|
+
return true;
|
|
66928
|
+
}
|
|
66252
66929
|
if (pathname === "/api/cloud/s3-config" && method === "GET") {
|
|
66253
66930
|
await tryHandler(res, () => {
|
|
66254
66931
|
const label = activeWorkspaceLabel(ctx.configPath);
|
|
@@ -67045,7 +67722,7 @@ function enrichContext(ctx) {
|
|
|
67045
67722
|
...ctx.createEntity ? { createEntity: ctx.createEntity } : {}
|
|
67046
67723
|
};
|
|
67047
67724
|
}
|
|
67048
|
-
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
67725
|
+
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res, privateMode) {
|
|
67049
67726
|
try {
|
|
67050
67727
|
return await enrichWithLlm(
|
|
67051
67728
|
mctx,
|
|
@@ -67057,7 +67734,9 @@ async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
|
67057
67734
|
ctx.entityDescriptions,
|
|
67058
67735
|
ctx.createJunction,
|
|
67059
67736
|
ctx.aggressiveness,
|
|
67060
|
-
ctx.createEntity
|
|
67737
|
+
ctx.createEntity,
|
|
67738
|
+
false,
|
|
67739
|
+
privateMode
|
|
67061
67740
|
);
|
|
67062
67741
|
} catch (e6) {
|
|
67063
67742
|
const err = e6;
|
|
@@ -67136,7 +67815,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67136
67815
|
source: "ingest",
|
|
67137
67816
|
onColumnsAdded: columnDescriptionHook(ctx.db)
|
|
67138
67817
|
};
|
|
67818
|
+
const headerPrivate = req.headers["x-lattice-private"] === "1";
|
|
67139
67819
|
if (ctx.pathname === "/api/ingest/upload") {
|
|
67820
|
+
const forcePrivate2 = headerPrivate;
|
|
67140
67821
|
const rawName = typeof req.headers["x-filename"] === "string" && req.headers["x-filename"] || "";
|
|
67141
67822
|
let name2 = "upload";
|
|
67142
67823
|
if (rawName) {
|
|
@@ -67234,10 +67915,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67234
67915
|
...blob ? { blob_path: blob.blob_path } : {}
|
|
67235
67916
|
} : blob ? { ref_kind: "blob", blob_path: blob.blob_path } : {}
|
|
67236
67917
|
};
|
|
67237
|
-
const { id: id2 } = await createRow(
|
|
67238
|
-
|
|
67239
|
-
|
|
67240
|
-
|
|
67918
|
+
const { id: id2 } = await createRow(
|
|
67919
|
+
mctx,
|
|
67920
|
+
"files",
|
|
67921
|
+
{
|
|
67922
|
+
...await requiredFileDefaults(ctx.db, name2, fileId, uploadRow),
|
|
67923
|
+
...uploadRow
|
|
67924
|
+
},
|
|
67925
|
+
forcePrivate2 ? "private" : void 0
|
|
67926
|
+
);
|
|
67241
67927
|
try {
|
|
67242
67928
|
const dedupCtx = {
|
|
67243
67929
|
db: ctx.db,
|
|
@@ -67263,7 +67949,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67263
67949
|
}
|
|
67264
67950
|
let suggestedLinks = [];
|
|
67265
67951
|
if (!result.skip) {
|
|
67266
|
-
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res);
|
|
67952
|
+
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res, forcePrivate2);
|
|
67267
67953
|
if (links === null) return true;
|
|
67268
67954
|
suggestedLinks = links;
|
|
67269
67955
|
}
|
|
@@ -67290,6 +67976,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67290
67976
|
sendJson4(res, { error: e6.message }, 400);
|
|
67291
67977
|
return true;
|
|
67292
67978
|
}
|
|
67979
|
+
const forcePrivate = headerPrivate || body.private === true;
|
|
67293
67980
|
if (ctx.pathname === "/api/ingest/text") {
|
|
67294
67981
|
const rawText = typeof body.text === "string" ? body.text : "";
|
|
67295
67982
|
if (!rawText.trim()) {
|
|
@@ -67300,7 +67987,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67300
67987
|
if (sourceUrl) {
|
|
67301
67988
|
try {
|
|
67302
67989
|
const result = await ingestUrlAsFile(
|
|
67303
|
-
{ db: ctx.db, mctx, enrich: enrichContext(ctx) },
|
|
67990
|
+
{ db: ctx.db, mctx, enrich: enrichContext(ctx), privateMode: forcePrivate },
|
|
67304
67991
|
sourceUrl
|
|
67305
67992
|
);
|
|
67306
67993
|
sendJson4(
|
|
@@ -67329,11 +68016,25 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67329
68016
|
description: describe(content, mime2, title),
|
|
67330
68017
|
extraction_status: "extracted"
|
|
67331
68018
|
};
|
|
67332
|
-
const { id: id2 } = await createRow(
|
|
67333
|
-
|
|
67334
|
-
|
|
67335
|
-
|
|
67336
|
-
|
|
68019
|
+
const { id: id2 } = await createRow(
|
|
68020
|
+
mctx,
|
|
68021
|
+
"files",
|
|
68022
|
+
{
|
|
68023
|
+
...await requiredFileDefaults(ctx.db, title, textFileId, textRow),
|
|
68024
|
+
...textRow
|
|
68025
|
+
},
|
|
68026
|
+
forcePrivate ? "private" : void 0
|
|
68027
|
+
);
|
|
68028
|
+
const suggestedLinks = await enrichOrFail(
|
|
68029
|
+
mctx,
|
|
68030
|
+
ctx.db,
|
|
68031
|
+
id2,
|
|
68032
|
+
content,
|
|
68033
|
+
title,
|
|
68034
|
+
ctx,
|
|
68035
|
+
res,
|
|
68036
|
+
forcePrivate
|
|
68037
|
+
);
|
|
67337
68038
|
if (suggestedLinks === null) return true;
|
|
67338
68039
|
sendJson4(res, { id: id2, extraction_status: "extracted", suggestedLinks }, 201);
|
|
67339
68040
|
return true;
|
|
@@ -67372,10 +68073,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67372
68073
|
size_bytes: size,
|
|
67373
68074
|
extraction_status: "pending"
|
|
67374
68075
|
};
|
|
67375
|
-
const { id } = await createRow(
|
|
67376
|
-
|
|
67377
|
-
|
|
67378
|
-
|
|
68076
|
+
const { id } = await createRow(
|
|
68077
|
+
mctx,
|
|
68078
|
+
"files",
|
|
68079
|
+
{
|
|
68080
|
+
...await requiredFileDefaults(ctx.db, name, localFileId, localRow),
|
|
68081
|
+
...localRow
|
|
68082
|
+
},
|
|
68083
|
+
forcePrivate ? "private" : void 0
|
|
68084
|
+
);
|
|
67379
68085
|
try {
|
|
67380
68086
|
const result = await extractSource(ctx.db, abs, mime, name);
|
|
67381
68087
|
await updateRow(mctx, "files", id, {
|
|
@@ -67393,7 +68099,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
67393
68099
|
ctx.entityDescriptions,
|
|
67394
68100
|
ctx.createJunction,
|
|
67395
68101
|
ctx.aggressiveness,
|
|
67396
|
-
ctx.createEntity
|
|
68102
|
+
ctx.createEntity,
|
|
68103
|
+
false,
|
|
68104
|
+
forcePrivate
|
|
67397
68105
|
);
|
|
67398
68106
|
sendJson4(
|
|
67399
68107
|
res,
|
|
@@ -68080,7 +68788,7 @@ function startBackgroundRender(active) {
|
|
|
68080
68788
|
}
|
|
68081
68789
|
bus.publish(e6);
|
|
68082
68790
|
};
|
|
68083
|
-
void db.renderInBackground(active.outputDir, { signal, onProgress }).then(
|
|
68791
|
+
void db.renderInBackground(active.outputDir, { signal, onProgress, gateOnOpen: true }).then(
|
|
68084
68792
|
() => {
|
|
68085
68793
|
},
|
|
68086
68794
|
(err) => {
|