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/cli.js
CHANGED
|
@@ -507,7 +507,23 @@ function deleteAssistantCredential(kind) {
|
|
|
507
507
|
void _removed;
|
|
508
508
|
saveAssistantCredentials(rest);
|
|
509
509
|
}
|
|
510
|
-
|
|
510
|
+
function isAssistantCredentialCleared(kind) {
|
|
511
|
+
return loadAssistantCredentials()[CLEARED_SENTINEL_PREFIX + kind] === "1";
|
|
512
|
+
}
|
|
513
|
+
function setAssistantCredentialCleared(kind) {
|
|
514
|
+
const creds = loadAssistantCredentials();
|
|
515
|
+
creds[CLEARED_SENTINEL_PREFIX + kind] = "1";
|
|
516
|
+
saveAssistantCredentials(creds);
|
|
517
|
+
}
|
|
518
|
+
function clearAssistantCredentialCleared(kind) {
|
|
519
|
+
const creds = loadAssistantCredentials();
|
|
520
|
+
const sentinel = CLEARED_SENTINEL_PREFIX + kind;
|
|
521
|
+
if (!(sentinel in creds)) return;
|
|
522
|
+
const { [sentinel]: _removed, ...rest } = creds;
|
|
523
|
+
void _removed;
|
|
524
|
+
saveAssistantCredentials(rest);
|
|
525
|
+
}
|
|
526
|
+
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;
|
|
511
527
|
var init_user_config = __esm({
|
|
512
528
|
"src/framework/user-config.ts"() {
|
|
513
529
|
"use strict";
|
|
@@ -530,6 +546,7 @@ var init_user_config = __esm({
|
|
|
530
546
|
lockDepthInProcess = 0;
|
|
531
547
|
S3_CONFIG_FILENAME = "s3-config.enc";
|
|
532
548
|
ASSISTANT_CREDENTIALS_FILENAME = "assistant-credentials.enc";
|
|
549
|
+
CLEARED_SENTINEL_PREFIX = "__cleared__:";
|
|
533
550
|
}
|
|
534
551
|
});
|
|
535
552
|
|
|
@@ -615,14 +632,6 @@ function resolveDbPath(raw, configDir2) {
|
|
|
615
632
|
}
|
|
616
633
|
return resolve(configDir2, raw);
|
|
617
634
|
}
|
|
618
|
-
function warnDeprecatedRef(entity, field, target) {
|
|
619
|
-
const key = `${entity}.${field}`;
|
|
620
|
-
if (warnedDeprecatedRefs.has(key)) return;
|
|
621
|
-
warnedDeprecatedRefs.add(key);
|
|
622
|
-
console.warn(
|
|
623
|
-
`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.`
|
|
624
|
-
);
|
|
625
|
-
}
|
|
626
635
|
function entityToTableDef(entityName, entity) {
|
|
627
636
|
const rawFields = entity.fields;
|
|
628
637
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -649,7 +658,6 @@ function entityToTableDef(entityName, entity) {
|
|
|
649
658
|
table: field.ref,
|
|
650
659
|
foreignKey: fieldName
|
|
651
660
|
};
|
|
652
|
-
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
653
661
|
}
|
|
654
662
|
}
|
|
655
663
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
@@ -806,12 +814,10 @@ function parseEntityContexts(entityContexts) {
|
|
|
806
814
|
}
|
|
807
815
|
return result;
|
|
808
816
|
}
|
|
809
|
-
var warnedDeprecatedRefs;
|
|
810
817
|
var init_parser = __esm({
|
|
811
818
|
"src/config/parser.ts"() {
|
|
812
819
|
"use strict";
|
|
813
820
|
init_user_config();
|
|
814
|
-
warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
815
821
|
}
|
|
816
822
|
});
|
|
817
823
|
|
|
@@ -960,10 +966,12 @@ function readManifest(outputDir) {
|
|
|
960
966
|
function writeManifest(outputDir, manifest) {
|
|
961
967
|
atomicWrite(manifestPath(outputDir), JSON.stringify(manifest, null, 2));
|
|
962
968
|
}
|
|
969
|
+
var TEMPLATE_VERSION;
|
|
963
970
|
var init_manifest = __esm({
|
|
964
971
|
"src/lifecycle/manifest.ts"() {
|
|
965
972
|
"use strict";
|
|
966
973
|
init_writer();
|
|
974
|
+
TEMPLATE_VERSION = 1;
|
|
967
975
|
}
|
|
968
976
|
});
|
|
969
977
|
|
|
@@ -997,6 +1005,126 @@ var init_adapter = __esm({
|
|
|
997
1005
|
}
|
|
998
1006
|
});
|
|
999
1007
|
|
|
1008
|
+
// src/lifecycle/render-cursor.ts
|
|
1009
|
+
function markToString(v2) {
|
|
1010
|
+
if (v2 == null) return null;
|
|
1011
|
+
if (v2 instanceof Date) return v2.toISOString();
|
|
1012
|
+
if (typeof v2 === "string") return v2;
|
|
1013
|
+
if (typeof v2 === "number" || typeof v2 === "bigint" || typeof v2 === "boolean") return String(v2);
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
function padNumericMark(v2) {
|
|
1017
|
+
const s2 = markToString(v2);
|
|
1018
|
+
if (s2 == null) return null;
|
|
1019
|
+
if (/^\d+$/.test(s2)) return s2.padStart(20, "0");
|
|
1020
|
+
return s2;
|
|
1021
|
+
}
|
|
1022
|
+
async function changelogExists(adapter) {
|
|
1023
|
+
if (adapter.dialect === "postgres") {
|
|
1024
|
+
const row2 = await getAsyncOrSync(
|
|
1025
|
+
adapter,
|
|
1026
|
+
`SELECT to_regclass('__lattice_changelog') AS reg`
|
|
1027
|
+
);
|
|
1028
|
+
return !!row2 && row2.reg != null;
|
|
1029
|
+
}
|
|
1030
|
+
const row = await getAsyncOrSync(
|
|
1031
|
+
adapter,
|
|
1032
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='__lattice_changelog'`
|
|
1033
|
+
);
|
|
1034
|
+
return !!row;
|
|
1035
|
+
}
|
|
1036
|
+
async function changelogMark(adapter) {
|
|
1037
|
+
try {
|
|
1038
|
+
if (!await changelogExists(adapter)) return null;
|
|
1039
|
+
const col = adapter.dialect === "postgres" ? "seq" : "rowid";
|
|
1040
|
+
const row = await getAsyncOrSync(
|
|
1041
|
+
adapter,
|
|
1042
|
+
`SELECT MAX(${col}) AS m FROM __lattice_changelog`
|
|
1043
|
+
);
|
|
1044
|
+
return padNumericMark(row?.m);
|
|
1045
|
+
} catch {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async function sharingMarks(adapter) {
|
|
1050
|
+
if (adapter.dialect !== "postgres") return { grants: null, owners: null };
|
|
1051
|
+
try {
|
|
1052
|
+
const reg = await getAsyncOrSync(
|
|
1053
|
+
adapter,
|
|
1054
|
+
`SELECT to_regclass('__lattice_changes') AS reg`
|
|
1055
|
+
);
|
|
1056
|
+
const hasFeed = !!reg && reg.reg != null;
|
|
1057
|
+
if (hasFeed) {
|
|
1058
|
+
const row = await getAsyncOrSync(
|
|
1059
|
+
adapter,
|
|
1060
|
+
`SELECT COUNT(*) AS n, MAX(seq) AS m FROM lattice_changes_since(0, 1000)`
|
|
1061
|
+
);
|
|
1062
|
+
const digest = digestOf(row?.n, row?.m);
|
|
1063
|
+
return { grants: digest, owners: digest };
|
|
1064
|
+
}
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
let owners = null;
|
|
1068
|
+
let grants = null;
|
|
1069
|
+
try {
|
|
1070
|
+
const o3 = await getAsyncOrSync(
|
|
1071
|
+
adapter,
|
|
1072
|
+
`SELECT COUNT(*) AS n, MAX(updated_at) AS m FROM __lattice_owners`
|
|
1073
|
+
);
|
|
1074
|
+
owners = digestOf(o3?.n, o3?.m);
|
|
1075
|
+
} catch {
|
|
1076
|
+
owners = null;
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
const g6 = await getAsyncOrSync(
|
|
1080
|
+
adapter,
|
|
1081
|
+
`SELECT COUNT(*) AS n, MAX(granted_at) AS m FROM __lattice_row_grants`
|
|
1082
|
+
);
|
|
1083
|
+
grants = digestOf(g6?.n, g6?.m);
|
|
1084
|
+
} catch {
|
|
1085
|
+
grants = null;
|
|
1086
|
+
}
|
|
1087
|
+
return { grants, owners };
|
|
1088
|
+
}
|
|
1089
|
+
function digestOf(count, max) {
|
|
1090
|
+
const n3 = padNumericMark(count);
|
|
1091
|
+
if (n3 == null) return null;
|
|
1092
|
+
const m4 = markToString(max) ?? "";
|
|
1093
|
+
return `${n3}#${m4}`;
|
|
1094
|
+
}
|
|
1095
|
+
async function computeRenderCursor(adapter) {
|
|
1096
|
+
try {
|
|
1097
|
+
const [changelog, sharing] = await Promise.all([changelogMark(adapter), sharingMarks(adapter)]);
|
|
1098
|
+
return { changelog, grants: sharing.grants, owners: sharing.owners };
|
|
1099
|
+
} catch {
|
|
1100
|
+
return { ...EMPTY_CURSOR };
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function cursorIsFresh(recorded, live, templateVersion = TEMPLATE_VERSION) {
|
|
1104
|
+
if (recorded == null) return false;
|
|
1105
|
+
if (recorded.templateVersion !== templateVersion) return false;
|
|
1106
|
+
const rc = recorded.cursor;
|
|
1107
|
+
if (rc == null) return false;
|
|
1108
|
+
if (!fieldFresh(rc.changelog, live.changelog, (r6, l4) => l4 <= r6)) return false;
|
|
1109
|
+
if (!fieldFresh(rc.grants, live.grants, (r6, l4) => l4 === r6)) return false;
|
|
1110
|
+
if (!fieldFresh(rc.owners, live.owners, (r6, l4) => l4 === r6)) return false;
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
function fieldFresh(recorded, live, ok) {
|
|
1114
|
+
if (recorded == null && live == null) return true;
|
|
1115
|
+
if (recorded == null || live == null) return false;
|
|
1116
|
+
return ok(recorded, live);
|
|
1117
|
+
}
|
|
1118
|
+
var EMPTY_CURSOR;
|
|
1119
|
+
var init_render_cursor = __esm({
|
|
1120
|
+
"src/lifecycle/render-cursor.ts"() {
|
|
1121
|
+
"use strict";
|
|
1122
|
+
init_adapter();
|
|
1123
|
+
init_manifest();
|
|
1124
|
+
EMPTY_CURSOR = { changelog: null, grants: null, owners: null };
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1000
1128
|
// src/db/sqlite.ts
|
|
1001
1129
|
import Database from "better-sqlite3";
|
|
1002
1130
|
var SQLiteAdapter;
|
|
@@ -3035,7 +3163,18 @@ var init_concurrency = __esm({
|
|
|
3035
3163
|
// src/render/engine.ts
|
|
3036
3164
|
import { join as join7, basename, isAbsolute, resolve as resolve3, sep } from "path";
|
|
3037
3165
|
import { mkdirSync as mkdirSync5, existsSync as existsSync7, copyFileSync as copyFileSync2 } from "fs";
|
|
3038
|
-
|
|
3166
|
+
function entityContentChanged(fresh, prior) {
|
|
3167
|
+
const freshKeys = Object.keys(fresh);
|
|
3168
|
+
const priorKeys = Object.keys(prior);
|
|
3169
|
+
if (freshKeys.length !== priorKeys.length) return true;
|
|
3170
|
+
for (const k6 of freshKeys) {
|
|
3171
|
+
const p3 = prior[k6];
|
|
3172
|
+
if (p3 == null) return true;
|
|
3173
|
+
if (p3.hash === "" || p3.hash !== fresh[k6]?.hash) return true;
|
|
3174
|
+
}
|
|
3175
|
+
return false;
|
|
3176
|
+
}
|
|
3177
|
+
var DeferredTableProgress, YIELD_EVERY_ENTITIES, RENDER_TABLE_CONCURRENCY, NOOP_RENDER, RenderEngine;
|
|
3039
3178
|
var init_engine = __esm({
|
|
3040
3179
|
"src/render/engine.ts"() {
|
|
3041
3180
|
"use strict";
|
|
@@ -3045,9 +3184,44 @@ var init_engine = __esm({
|
|
|
3045
3184
|
init_entity_query();
|
|
3046
3185
|
init_entity_templates();
|
|
3047
3186
|
init_manifest();
|
|
3187
|
+
init_render_cursor();
|
|
3048
3188
|
init_cleanup();
|
|
3049
3189
|
init_progress();
|
|
3050
3190
|
init_concurrency();
|
|
3191
|
+
DeferredTableProgress = class {
|
|
3192
|
+
constructor(throttle) {
|
|
3193
|
+
this.throttle = throttle;
|
|
3194
|
+
}
|
|
3195
|
+
changed = false;
|
|
3196
|
+
pendingStart = null;
|
|
3197
|
+
/** Buffer the `table-start` event; emitted only if/when the table changes. */
|
|
3198
|
+
start(event) {
|
|
3199
|
+
if (this.changed) {
|
|
3200
|
+
this.throttle.force(event);
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
this.pendingStart = event;
|
|
3204
|
+
}
|
|
3205
|
+
/** Mark that an entity's content changed — flush the held `table-start` once. */
|
|
3206
|
+
markChanged() {
|
|
3207
|
+
if (this.changed) return;
|
|
3208
|
+
this.changed = true;
|
|
3209
|
+
if (this.pendingStart) {
|
|
3210
|
+
this.throttle.force(this.pendingStart);
|
|
3211
|
+
this.pendingStart = null;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
/** Coalesced per-entity progress — dropped entirely until the table changed. */
|
|
3215
|
+
tick(event) {
|
|
3216
|
+
if (!this.changed) return;
|
|
3217
|
+
this.throttle.tick(event);
|
|
3218
|
+
}
|
|
3219
|
+
/** Lifecycle event (`table-done`) — emitted only if the table changed. */
|
|
3220
|
+
force(event) {
|
|
3221
|
+
if (!this.changed) return;
|
|
3222
|
+
this.throttle.force(event);
|
|
3223
|
+
}
|
|
3224
|
+
};
|
|
3051
3225
|
YIELD_EVERY_ENTITIES = 200;
|
|
3052
3226
|
RENDER_TABLE_CONCURRENCY = 4;
|
|
3053
3227
|
NOOP_RENDER = () => "";
|
|
@@ -3164,20 +3338,23 @@ var init_engine = __esm({
|
|
|
3164
3338
|
}
|
|
3165
3339
|
const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
|
|
3166
3340
|
const filePath = join7(outputDir, def.outputFile);
|
|
3167
|
-
|
|
3341
|
+
const wrote = atomicWrite(filePath, content);
|
|
3342
|
+
if (wrote) {
|
|
3168
3343
|
filesWritten.push(filePath);
|
|
3169
3344
|
} else {
|
|
3170
3345
|
counters.skipped++;
|
|
3171
3346
|
}
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3347
|
+
if (wrote) {
|
|
3348
|
+
throttle.force({
|
|
3349
|
+
kind: "table-done",
|
|
3350
|
+
table: name,
|
|
3351
|
+
entitiesRendered: rows.length,
|
|
3352
|
+
entitiesTotal: rows.length,
|
|
3353
|
+
tableIndex: 0,
|
|
3354
|
+
tableCount: 0,
|
|
3355
|
+
pct: 100
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3181
3358
|
}
|
|
3182
3359
|
for (const [name, def] of this._schema.getMultis()) {
|
|
3183
3360
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
@@ -3191,32 +3368,38 @@ var init_engine = __esm({
|
|
|
3191
3368
|
tables[t8] = await this._schema.queryTable(this._adapter, t8, this._readRel);
|
|
3192
3369
|
}
|
|
3193
3370
|
}
|
|
3371
|
+
let wroteAny = false;
|
|
3194
3372
|
for (const key of keys) {
|
|
3195
3373
|
const content = def.render(key, tables);
|
|
3196
3374
|
const filePath = join7(outputDir, def.outputFile(key));
|
|
3197
3375
|
if (atomicWrite(filePath, content)) {
|
|
3198
3376
|
filesWritten.push(filePath);
|
|
3377
|
+
wroteAny = true;
|
|
3199
3378
|
} else {
|
|
3200
3379
|
counters.skipped++;
|
|
3201
3380
|
}
|
|
3202
3381
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3382
|
+
if (wroteAny) {
|
|
3383
|
+
throttle.force({
|
|
3384
|
+
kind: "table-done",
|
|
3385
|
+
table: name,
|
|
3386
|
+
entitiesRendered: keys.length,
|
|
3387
|
+
entitiesTotal: keys.length,
|
|
3388
|
+
tableIndex: 0,
|
|
3389
|
+
tableCount: 0,
|
|
3390
|
+
pct: 100
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3212
3393
|
}
|
|
3394
|
+
const priorManifest = readManifest(outputDir);
|
|
3213
3395
|
const entityContextManifest = await this._renderEntityContexts(
|
|
3214
3396
|
outputDir,
|
|
3215
3397
|
filesWritten,
|
|
3216
3398
|
counters,
|
|
3217
3399
|
throttle,
|
|
3218
3400
|
signal,
|
|
3219
|
-
opts.changedTables
|
|
3401
|
+
opts.changedTables,
|
|
3402
|
+
priorManifest
|
|
3220
3403
|
);
|
|
3221
3404
|
if (entityContextManifest === null) {
|
|
3222
3405
|
return this._abortedResult(filesWritten, counters, start);
|
|
@@ -3227,10 +3410,13 @@ var init_engine = __esm({
|
|
|
3227
3410
|
const prev = readManifest(outputDir);
|
|
3228
3411
|
entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
|
|
3229
3412
|
}
|
|
3413
|
+
const cursor = await computeRenderCursor(this._adapter);
|
|
3230
3414
|
writeManifest(outputDir, {
|
|
3231
3415
|
version: 2,
|
|
3232
3416
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3233
|
-
entityContexts
|
|
3417
|
+
entityContexts,
|
|
3418
|
+
templateVersion: TEMPLATE_VERSION,
|
|
3419
|
+
cursor
|
|
3234
3420
|
});
|
|
3235
3421
|
}
|
|
3236
3422
|
const result = {
|
|
@@ -3296,7 +3482,7 @@ var init_engine = __esm({
|
|
|
3296
3482
|
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
3297
3483
|
* via `signal`.
|
|
3298
3484
|
*/
|
|
3299
|
-
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
|
|
3485
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables, priorManifest) {
|
|
3300
3486
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
3301
3487
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
3302
3488
|
if (d6.protected) protectedTables.add(t8);
|
|
@@ -3315,8 +3501,10 @@ var init_engine = __esm({
|
|
|
3315
3501
|
const baseRows = await this._schema.queryTable(this._adapter, table, this._readRel);
|
|
3316
3502
|
const allRows = this._foldRows ? await this._foldRows(table, baseRows) : baseRows;
|
|
3317
3503
|
const directoryRoot = def.directoryRoot ?? table;
|
|
3504
|
+
const deferred = new DeferredTableProgress(throttle);
|
|
3505
|
+
const priorEntities = priorManifest?.entityContexts[table]?.entities ?? {};
|
|
3318
3506
|
const entitiesTotal = allRows.length;
|
|
3319
|
-
|
|
3507
|
+
deferred.start({
|
|
3320
3508
|
kind: "table-start",
|
|
3321
3509
|
table,
|
|
3322
3510
|
entitiesRendered: 0,
|
|
@@ -3325,6 +3513,7 @@ var init_engine = __esm({
|
|
|
3325
3513
|
tableCount,
|
|
3326
3514
|
pct: 0
|
|
3327
3515
|
});
|
|
3516
|
+
if (Object.keys(priorEntities).length !== entitiesTotal) deferred.markChanged();
|
|
3328
3517
|
const manifestEntry = {
|
|
3329
3518
|
directoryRoot,
|
|
3330
3519
|
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
@@ -3440,8 +3629,10 @@ var init_engine = __esm({
|
|
|
3440
3629
|
}
|
|
3441
3630
|
}
|
|
3442
3631
|
manifestEntry.entities[slug] = entityFileHashes;
|
|
3632
|
+
const priorHashes = normalizeEntityFiles(priorEntities[slug] ?? {});
|
|
3633
|
+
if (entityContentChanged(entityFileHashes, priorHashes)) deferred.markChanged();
|
|
3443
3634
|
const entitiesRendered = i6 + 1;
|
|
3444
|
-
|
|
3635
|
+
deferred.tick({
|
|
3445
3636
|
kind: "table-progress",
|
|
3446
3637
|
table,
|
|
3447
3638
|
entitiesRendered,
|
|
@@ -3451,7 +3642,7 @@ var init_engine = __esm({
|
|
|
3451
3642
|
pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
|
|
3452
3643
|
});
|
|
3453
3644
|
}
|
|
3454
|
-
|
|
3645
|
+
deferred.force({
|
|
3455
3646
|
kind: "table-done",
|
|
3456
3647
|
table,
|
|
3457
3648
|
entitiesRendered: entitiesTotal,
|
|
@@ -5085,6 +5276,7 @@ var init_lattice = __esm({
|
|
|
5085
5276
|
init_shred();
|
|
5086
5277
|
init_encryption();
|
|
5087
5278
|
init_manifest();
|
|
5279
|
+
init_render_cursor();
|
|
5088
5280
|
init_adapter();
|
|
5089
5281
|
init_sqlite();
|
|
5090
5282
|
init_postgres();
|
|
@@ -5155,6 +5347,14 @@ var init_lattice = __esm({
|
|
|
5155
5347
|
_changelogTables = /* @__PURE__ */ new Set();
|
|
5156
5348
|
/** Current task context string for relevance filtering. */
|
|
5157
5349
|
_taskContext = "";
|
|
5350
|
+
/**
|
|
5351
|
+
* True when this connection opened against an already-provisioned cloud as a
|
|
5352
|
+
* SCOPED MEMBER (no role-management privilege → no CREATE/ALTER on the schema).
|
|
5353
|
+
* Set during init() by the same probe that decides introspect-only. Drives
|
|
5354
|
+
* {@link addColumn} to route DDL through the owner-side `lattice_member_add_column`
|
|
5355
|
+
* SECURITY DEFINER helper instead of issuing a raw ALTER the member can't run.
|
|
5356
|
+
*/
|
|
5357
|
+
_cloudMemberOpen = false;
|
|
5158
5358
|
_auditHandlers = [];
|
|
5159
5359
|
_renderHandlers = [];
|
|
5160
5360
|
_writebackHandlers = [];
|
|
@@ -5401,7 +5601,7 @@ var init_lattice = __esm({
|
|
|
5401
5601
|
/** Async tail of init(). See {@link init} for the sync-validation phase. */
|
|
5402
5602
|
async _initAsync(options) {
|
|
5403
5603
|
let introspectOnly = options.introspectOnly === true;
|
|
5404
|
-
if (
|
|
5604
|
+
if (this.getDialect() === "postgres") {
|
|
5405
5605
|
try {
|
|
5406
5606
|
const [marker, role] = await Promise.all([
|
|
5407
5607
|
getAsyncOrSync(this._adapter, `SELECT to_regclass('__lattice_owners') AS reg`),
|
|
@@ -5412,7 +5612,9 @@ var init_lattice = __esm({
|
|
|
5412
5612
|
]);
|
|
5413
5613
|
const provisioned = !!marker && marker.reg != null;
|
|
5414
5614
|
const canCreateRoles = !!role && role.rolcreaterole === true;
|
|
5415
|
-
|
|
5615
|
+
const memberOpen = provisioned && !canCreateRoles;
|
|
5616
|
+
introspectOnly = introspectOnly || memberOpen;
|
|
5617
|
+
this._cloudMemberOpen = memberOpen;
|
|
5416
5618
|
} catch {
|
|
5417
5619
|
}
|
|
5418
5620
|
}
|
|
@@ -5500,6 +5702,26 @@ var init_lattice = __esm({
|
|
|
5500
5702
|
getDialect() {
|
|
5501
5703
|
return this._adapter.dialect;
|
|
5502
5704
|
}
|
|
5705
|
+
/**
|
|
5706
|
+
* True when a table opts into the observation/changelog substrate
|
|
5707
|
+
* (`def.changelog`). Callers that want to bypass the high-level {@link delete}
|
|
5708
|
+
* with a transaction-scoped raw delete use this to know whether the table also
|
|
5709
|
+
* needs the changelog / write-hook / embedding side effects that only
|
|
5710
|
+
* `delete()` performs — so they can keep the high-level path for such tables.
|
|
5711
|
+
*/
|
|
5712
|
+
isChangelogTracked(table) {
|
|
5713
|
+
return this._changelogTables.has(table);
|
|
5714
|
+
}
|
|
5715
|
+
/**
|
|
5716
|
+
* True when this connection opened as a scoped cloud MEMBER (see
|
|
5717
|
+
* {@link _cloudMemberOpen}). Callers use it to route DDL-bearing work through
|
|
5718
|
+
* the owner-side SECURITY DEFINER helpers rather than issuing DDL the member's
|
|
5719
|
+
* role can't run (e.g. {@link addColumn} regenerates the masking view inside
|
|
5720
|
+
* `lattice_member_add_column`, so the caller must not also try to regenerate it).
|
|
5721
|
+
*/
|
|
5722
|
+
isCloudMemberOpen() {
|
|
5723
|
+
return this._cloudMemberOpen;
|
|
5724
|
+
}
|
|
5503
5725
|
/**
|
|
5504
5726
|
* Return the normalised primary-key column list for a registered
|
|
5505
5727
|
* table. Falls back to `['id']` for tables registered via raw DDL
|
|
@@ -5576,7 +5798,15 @@ var init_lattice = __esm({
|
|
|
5576
5798
|
assertSafeIdentifier(column, "column");
|
|
5577
5799
|
const existing = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5578
5800
|
if (!existing.includes(column)) {
|
|
5579
|
-
|
|
5801
|
+
if (this._cloudMemberOpen) {
|
|
5802
|
+
await runAsyncOrSync(this._adapter, `SELECT lattice_member_add_column(?, ?, ?)`, [
|
|
5803
|
+
table,
|
|
5804
|
+
column,
|
|
5805
|
+
typeSpec
|
|
5806
|
+
]);
|
|
5807
|
+
} else {
|
|
5808
|
+
await addColumnAsyncOrSync(this._adapter, table, column, typeSpec);
|
|
5809
|
+
}
|
|
5580
5810
|
}
|
|
5581
5811
|
const cols = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5582
5812
|
this._columnCache.set(table, new Set(cols));
|
|
@@ -6508,12 +6738,39 @@ var init_lattice = __esm({
|
|
|
6508
6738
|
async renderInBackground(outputDir, opts = {}) {
|
|
6509
6739
|
const notInit = this._notInitError();
|
|
6510
6740
|
if (notInit) return notInit;
|
|
6741
|
+
if (opts.gateOnOpen && !opts.changedTables) {
|
|
6742
|
+
const start = Date.now();
|
|
6743
|
+
const recorded = readManifest(outputDir);
|
|
6744
|
+
if (recorded != null) {
|
|
6745
|
+
const live = await computeRenderCursor(this._adapter);
|
|
6746
|
+
if (cursorIsFresh(recorded, live)) {
|
|
6747
|
+
opts.onProgress?.({
|
|
6748
|
+
kind: "done",
|
|
6749
|
+
table: null,
|
|
6750
|
+
entitiesRendered: 0,
|
|
6751
|
+
entitiesTotal: 0,
|
|
6752
|
+
tableIndex: 0,
|
|
6753
|
+
tableCount: 0,
|
|
6754
|
+
pct: 100,
|
|
6755
|
+
durationMs: Date.now() - start
|
|
6756
|
+
});
|
|
6757
|
+
const skipped = {
|
|
6758
|
+
filesWritten: [],
|
|
6759
|
+
filesSkipped: 0,
|
|
6760
|
+
durationMs: Date.now() - start
|
|
6761
|
+
};
|
|
6762
|
+
for (const h6 of this._renderHandlers) h6(skipped);
|
|
6763
|
+
return skipped;
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6511
6767
|
if (!opts.changedTables) {
|
|
6512
6768
|
this._pendingRenderAll = false;
|
|
6513
6769
|
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
6514
6770
|
this._autoRenderPending = false;
|
|
6515
6771
|
}
|
|
6516
|
-
|
|
6772
|
+
const { gateOnOpen: _gateOnOpen, ...engineOpts } = opts;
|
|
6773
|
+
return this._renderGuarded(outputDir, engineOpts);
|
|
6517
6774
|
}
|
|
6518
6775
|
/**
|
|
6519
6776
|
* Install a per-viewer read-relation resolver for ALL renders (initial,
|
|
@@ -8623,6 +8880,111 @@ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
|
8623
8880
|
AND g."pk" = ANY(p_pks)
|
|
8624
8881
|
AND o."owner_role" = session_user;
|
|
8625
8882
|
$fn$;
|
|
8883
|
+
|
|
8884
|
+
-- Add a column to a user table AS THE OWNER, on behalf of a scoped member. A
|
|
8885
|
+
-- member's role has no CREATE/ALTER on the schema (the bootstrap REVOKEs CREATE
|
|
8886
|
+
-- from PUBLIC), so a member's GUI "add a field" write (createRow/updateRow with a
|
|
8887
|
+
-- field the table lacks) cannot run ALTER TABLE itself. This SECURITY DEFINER
|
|
8888
|
+
-- helper performs that ALTER \u2014 and the masking-view regen \u2014 with the owner's
|
|
8889
|
+
-- rights, so member-added columns behave identically to owner-added ones.
|
|
8890
|
+
--
|
|
8891
|
+
-- Injection-safe + minimal: p_table must be an existing BASE table in the current
|
|
8892
|
+
-- schema (rejected otherwise); p_type is whitelisted against the exact set the
|
|
8893
|
+
-- library's addColumn emits for an auto-added column (TEXT / INTEGER / REAL, plus
|
|
8894
|
+
-- BOOLEAN) \u2014 never interpolated raw; both identifiers go through %I (quote_ident).
|
|
8895
|
+
-- Member-callable (granted EXECUTE to the member group), but it can only widen the
|
|
8896
|
+
-- schema, never read or alter another member's data.
|
|
8897
|
+
CREATE OR REPLACE FUNCTION lattice_member_add_column(p_table text, p_column text, p_type text)
|
|
8898
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
8899
|
+
DECLARE
|
|
8900
|
+
v_type text;
|
|
8901
|
+
v_view text := p_table || '_v';
|
|
8902
|
+
v_has_view boolean;
|
|
8903
|
+
v_pk_expr text;
|
|
8904
|
+
v_select text;
|
|
8905
|
+
BEGIN
|
|
8906
|
+
-- Never alter internal bookkeeping tables (names start with "_"). The GUI only
|
|
8907
|
+
-- ever calls this for a user entity table; rejecting the rest is defense-in-depth
|
|
8908
|
+
-- against a member invoking the function directly against ownership/audit/policy
|
|
8909
|
+
-- tables.
|
|
8910
|
+
IF left(p_table, 1) = '_' THEN
|
|
8911
|
+
RAISE EXCEPTION 'lattice: cannot add a column to internal table "%"', p_table;
|
|
8912
|
+
END IF;
|
|
8913
|
+
|
|
8914
|
+
-- p_table must be a real base table in THIS schema (search_path is pinned to the
|
|
8915
|
+
-- cloud schema by pinDefinerSearchPath, so to_regclass resolves there).
|
|
8916
|
+
IF NOT EXISTS (
|
|
8917
|
+
SELECT 1 FROM pg_class c
|
|
8918
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
8919
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND c.relkind = 'r'
|
|
8920
|
+
) THEN
|
|
8921
|
+
RAISE EXCEPTION 'lattice: no such table "%"', p_table;
|
|
8922
|
+
END IF;
|
|
8923
|
+
|
|
8924
|
+
-- Whitelist the column type. These are exactly the specs addColumn's
|
|
8925
|
+
-- inferColumnType produces (TEXT / INTEGER / REAL); BOOLEAN is allowed too.
|
|
8926
|
+
-- Anything else is rejected \u2014 the type is spliced as %s (NOT %I), so it must be
|
|
8927
|
+
-- a known-safe literal and never caller-controlled SQL.
|
|
8928
|
+
v_type := upper(btrim(p_type));
|
|
8929
|
+
IF v_type NOT IN ('TEXT', 'INTEGER', 'REAL', 'BOOLEAN') THEN
|
|
8930
|
+
RAISE EXCEPTION 'lattice: unsupported column type "%"', p_type;
|
|
8931
|
+
END IF;
|
|
8932
|
+
|
|
8933
|
+
EXECUTE format('ALTER TABLE %I ADD COLUMN IF NOT EXISTS %I %s', p_table, p_column, v_type);
|
|
8934
|
+
|
|
8935
|
+
-- If the table is cell-masked (a "<table>_v" view exists, because some column has
|
|
8936
|
+
-- an audience), the view selects an explicit column list \u2014 so a new column is
|
|
8937
|
+
-- invisible to members until the view is regenerated. Rebuild it the same way the
|
|
8938
|
+
-- owner path (audienceViewSql / regenerateAudienceViewFromDb) does: pass every
|
|
8939
|
+
-- column through except those with an 'owner' audience in __lattice_column_policy
|
|
8940
|
+
-- (CASE WHEN lattice_is_owner(...) THEN col END), re-apply row visibility with
|
|
8941
|
+
-- WHERE lattice_row_visible(table, pk), and keep the member SELECT grant on the
|
|
8942
|
+
-- view. Unmasked tables need no regen \u2014 the member group's table-level base grant
|
|
8943
|
+
-- already covers the new column.
|
|
8944
|
+
SELECT EXISTS (
|
|
8945
|
+
SELECT 1 FROM pg_class c
|
|
8946
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
8947
|
+
WHERE n.nspname = current_schema() AND c.relname = v_view AND c.relkind = 'v'
|
|
8948
|
+
) INTO v_has_view;
|
|
8949
|
+
|
|
8950
|
+
IF v_has_view THEN
|
|
8951
|
+
-- Canonical pk expression: CAST("col" AS TEXT) joined by TAB (chr(9)) \u2014 the
|
|
8952
|
+
-- same serialization the RLS policies + audienceViewSql use.
|
|
8953
|
+
SELECT string_agg(format('CAST(%I AS TEXT)', a.attname), ' || chr(9) || '
|
|
8954
|
+
ORDER BY array_position(i.indkey, a.attnum))
|
|
8955
|
+
INTO v_pk_expr
|
|
8956
|
+
FROM pg_index i
|
|
8957
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
8958
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
8959
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
|
|
8960
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND i.indisprimary;
|
|
8961
|
+
IF v_pk_expr IS NULL THEN
|
|
8962
|
+
RAISE EXCEPTION 'lattice: cannot regenerate mask view for "%": no primary key', p_table;
|
|
8963
|
+
END IF;
|
|
8964
|
+
|
|
8965
|
+
-- Build the masked SELECT list in column order, applying the per-column policy.
|
|
8966
|
+
SELECT string_agg(
|
|
8967
|
+
CASE
|
|
8968
|
+
WHEN cp."audience" = 'owner'
|
|
8969
|
+
THEN format('CASE WHEN lattice_is_owner(%L, %s) THEN %I END AS %I',
|
|
8970
|
+
p_table, v_pk_expr, cols.column_name, cols.column_name)
|
|
8971
|
+
ELSE format('%I', cols.column_name)
|
|
8972
|
+
END,
|
|
8973
|
+
', ' ORDER BY cols.ordinal_position)
|
|
8974
|
+
INTO v_select
|
|
8975
|
+
FROM information_schema.columns cols
|
|
8976
|
+
LEFT JOIN "__lattice_column_policy" cp
|
|
8977
|
+
ON cp."table_name" = p_table AND cp."column_name" = cols.column_name
|
|
8978
|
+
AND cp."audience" NOT IN ('', 'everyone', 'row-audience')
|
|
8979
|
+
WHERE cols.table_schema = current_schema() AND cols.table_name = p_table;
|
|
8980
|
+
|
|
8981
|
+
EXECUTE format(
|
|
8982
|
+
'CREATE OR REPLACE VIEW %I AS SELECT %s FROM %I WHERE lattice_row_visible(%L, %s)',
|
|
8983
|
+
v_view, v_select, p_table, p_table, v_pk_expr);
|
|
8984
|
+
EXECUTE format('GRANT SELECT ON %I TO ${MEMBER_GROUP}', v_view);
|
|
8985
|
+
END IF;
|
|
8986
|
+
END $fn$;
|
|
8987
|
+
GRANT EXECUTE ON FUNCTION lattice_member_add_column(text, text, text) TO ${MEMBER_GROUP};
|
|
8626
8988
|
`;
|
|
8627
8989
|
}
|
|
8628
8990
|
});
|
|
@@ -8796,18 +9158,9 @@ function sessionUndoneFilters(undone, sessionId) {
|
|
|
8796
9158
|
if (sessionId) filters.push({ col: "session_id", op: "eq", val: sessionId });
|
|
8797
9159
|
return filters;
|
|
8798
9160
|
}
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
filters: sessionUndoneFilters(1, sessionId)
|
|
8802
|
-
});
|
|
8803
|
-
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
8804
|
-
await db.insert("_lattice_gui_audit", {
|
|
9161
|
+
function buildAuditRow(table, rowId, op, before, after, sessionId, editTs) {
|
|
9162
|
+
return {
|
|
8805
9163
|
id: crypto.randomUUID(),
|
|
8806
|
-
// Set ts explicitly (don't rely on the column DEFAULT — it uses the
|
|
8807
|
-
// SQLite-only `strftime(...)`, which doesn't yield a parseable ISO string
|
|
8808
|
-
// on Postgres, so cloud history rendered "Invalid Date"). #4.6 — honor the
|
|
8809
|
-
// originating client's validated edit time when present (an offline edit
|
|
8810
|
-
// replayed later records when it was MADE, not when it synced), else now().
|
|
8811
9164
|
ts: sanitizeEditTs(editTs) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8812
9165
|
table_name: table,
|
|
8813
9166
|
row_id: rowId,
|
|
@@ -8816,7 +9169,9 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
8816
9169
|
after_json: after ? JSON.stringify(after) : null,
|
|
8817
9170
|
undone: 0,
|
|
8818
9171
|
session_id: sessionId ?? null
|
|
8819
|
-
}
|
|
9172
|
+
};
|
|
9173
|
+
}
|
|
9174
|
+
function publishMutationFeed(feed, table, rowId, op, before, after, source) {
|
|
8820
9175
|
const labelRow = op === "delete" ? before : after;
|
|
8821
9176
|
feed.publish({
|
|
8822
9177
|
table,
|
|
@@ -8826,17 +9181,28 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
8826
9181
|
summary: feedSummary(op, table, labelRow)
|
|
8827
9182
|
});
|
|
8828
9183
|
}
|
|
8829
|
-
function
|
|
8830
|
-
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
8831
|
-
}
|
|
8832
|
-
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
9184
|
+
async function purgeRedoStack(db, sessionId) {
|
|
8833
9185
|
const undone = await db.query("_lattice_gui_audit", {
|
|
8834
9186
|
filters: sessionUndoneFilters(1, sessionId)
|
|
8835
9187
|
});
|
|
8836
9188
|
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
9189
|
+
}
|
|
9190
|
+
async function appendAudit(db, feed, table, rowId, op, before, after, source = "gui", sessionId, editTs) {
|
|
9191
|
+
await purgeRedoStack(db, sessionId);
|
|
9192
|
+
await db.insert(
|
|
9193
|
+
"_lattice_gui_audit",
|
|
9194
|
+
buildAuditRow(table, rowId, op, before, after, sessionId, editTs)
|
|
9195
|
+
);
|
|
9196
|
+
publishMutationFeed(feed, table, rowId, op, before, after, source);
|
|
9197
|
+
}
|
|
9198
|
+
function isSchemaOp(operation2) {
|
|
9199
|
+
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
9200
|
+
}
|
|
9201
|
+
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
9202
|
+
await purgeRedoStack(db, sessionId);
|
|
8837
9203
|
await db.insert("_lattice_gui_audit", {
|
|
8838
9204
|
id: crypto.randomUUID(),
|
|
8839
|
-
// Explicit ISO ts — see
|
|
9205
|
+
// Explicit ISO ts — see buildAuditRow (the SQLite-only strftime DEFAULT
|
|
8840
9206
|
// rendered "Invalid Date" on the Postgres/cloud path).
|
|
8841
9207
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8842
9208
|
table_name: table,
|
|
@@ -8871,7 +9237,7 @@ async function ensureColumns(db, table, values) {
|
|
|
8871
9237
|
const added = Object.keys(values).filter((k6) => !(k6 in existing));
|
|
8872
9238
|
if (added.length === 0) return [];
|
|
8873
9239
|
for (const col of added) await db.addColumn(table, col, inferColumnType(values[col]));
|
|
8874
|
-
if (db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
9240
|
+
if (!db.isCloudMemberOpen() && db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
8875
9241
|
const cols = db.getRegisteredColumns(table);
|
|
8876
9242
|
const pk = db.getPrimaryKey(table);
|
|
8877
9243
|
if (cols && pk.length > 0) await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
@@ -8993,7 +9359,14 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
8993
9359
|
ctx.clientTs
|
|
8994
9360
|
);
|
|
8995
9361
|
} else {
|
|
8996
|
-
await ctx
|
|
9362
|
+
await hardDelete(ctx, table, id, before);
|
|
9363
|
+
}
|
|
9364
|
+
}
|
|
9365
|
+
async function hardDelete(ctx, table, id, before) {
|
|
9366
|
+
const withClient = ctx.db.adapter.withClient?.bind(ctx.db.adapter);
|
|
9367
|
+
const pkCols = ctx.db.getPrimaryKey(table);
|
|
9368
|
+
const pkCol = pkCols.length === 1 ? pkCols[0] : void 0;
|
|
9369
|
+
if (!withClient || ctx.db.isChangelogTracked(table) || pkCol === void 0) {
|
|
8997
9370
|
await appendAudit(
|
|
8998
9371
|
ctx.db,
|
|
8999
9372
|
ctx.feed,
|
|
@@ -9006,10 +9379,30 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
9006
9379
|
ctx.sessionId,
|
|
9007
9380
|
ctx.clientTs
|
|
9008
9381
|
);
|
|
9382
|
+
await ctx.db.delete(table, id);
|
|
9383
|
+
return;
|
|
9009
9384
|
}
|
|
9385
|
+
const auditRow = buildAuditRow(table, id, "delete", before, null, ctx.sessionId, ctx.clientTs);
|
|
9386
|
+
await purgeRedoStack(ctx.db, ctx.sessionId);
|
|
9387
|
+
const auditCols = AUDIT_COLUMNS.map((c6) => `"${c6}"`).join(", ");
|
|
9388
|
+
const auditPlaceholders = AUDIT_COLUMNS.map(() => "?").join(", ");
|
|
9389
|
+
const auditValues = AUDIT_COLUMNS.map((c6) => auditRow[c6]);
|
|
9390
|
+
const pkColQuoted = pkCol.replace(/"/g, '""');
|
|
9391
|
+
await withClient(async (tx) => {
|
|
9392
|
+
await tx.run(
|
|
9393
|
+
`INSERT INTO "_lattice_gui_audit" (${auditCols}) VALUES (${auditPlaceholders})`,
|
|
9394
|
+
auditValues
|
|
9395
|
+
);
|
|
9396
|
+
await tx.run(`DELETE FROM "${table.replace(/"/g, '""')}" WHERE "${pkColQuoted}" = ?`, [id]);
|
|
9397
|
+
});
|
|
9398
|
+
publishMutationFeed(ctx.feed, table, id, "delete", before, null, ctx.source);
|
|
9010
9399
|
}
|
|
9011
|
-
async function linkRows(ctx, table, body) {
|
|
9012
|
-
|
|
9400
|
+
async function linkRows(ctx, table, body, forceVisibility) {
|
|
9401
|
+
if (forceVisibility !== void 0) {
|
|
9402
|
+
await ctx.db.insertForcingVisibility(table, body, forceVisibility);
|
|
9403
|
+
} else {
|
|
9404
|
+
await ctx.db.link(table, body);
|
|
9405
|
+
}
|
|
9013
9406
|
await appendAudit(ctx.db, ctx.feed, table, null, "link", null, body, ctx.source, ctx.sessionId);
|
|
9014
9407
|
}
|
|
9015
9408
|
async function unlinkRows(ctx, table, body) {
|
|
@@ -9147,12 +9540,23 @@ async function revertEntry(ctx, id) {
|
|
|
9147
9540
|
});
|
|
9148
9541
|
return { ok: true, entry };
|
|
9149
9542
|
}
|
|
9150
|
-
var SCHEMA_OP_PREFIX;
|
|
9543
|
+
var AUDIT_COLUMNS, SCHEMA_OP_PREFIX;
|
|
9151
9544
|
var init_mutations = __esm({
|
|
9152
9545
|
"src/gui/mutations.ts"() {
|
|
9153
9546
|
"use strict";
|
|
9154
9547
|
init_cloud_connect();
|
|
9155
9548
|
init_audience();
|
|
9549
|
+
AUDIT_COLUMNS = [
|
|
9550
|
+
"id",
|
|
9551
|
+
"ts",
|
|
9552
|
+
"table_name",
|
|
9553
|
+
"row_id",
|
|
9554
|
+
"operation",
|
|
9555
|
+
"before_json",
|
|
9556
|
+
"after_json",
|
|
9557
|
+
"undone",
|
|
9558
|
+
"session_id"
|
|
9559
|
+
];
|
|
9156
9560
|
SCHEMA_OP_PREFIX = "schema.";
|
|
9157
9561
|
}
|
|
9158
9562
|
});
|
|
@@ -9439,6 +9843,10 @@ async function readMachineCredential(db, kind) {
|
|
|
9439
9843
|
}
|
|
9440
9844
|
return null;
|
|
9441
9845
|
}
|
|
9846
|
+
async function resolveAnthropicKey(db) {
|
|
9847
|
+
if (isAssistantCredentialCleared(CREDENTIALS.anthropic.kind)) return null;
|
|
9848
|
+
return await readMachineCredential(db, CREDENTIALS.anthropic.kind) ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
9849
|
+
}
|
|
9442
9850
|
function getAggressiveness() {
|
|
9443
9851
|
const n3 = readPreferences().aggressiveness;
|
|
9444
9852
|
if (!Number.isFinite(n3)) return DEFAULT_AGGRESSIVENESS;
|
|
@@ -9469,6 +9877,7 @@ async function getVoiceCredential(db) {
|
|
|
9469
9877
|
return null;
|
|
9470
9878
|
}
|
|
9471
9879
|
async function hasCredential(db, name, envVar) {
|
|
9880
|
+
if (isAssistantCredentialCleared(CREDENTIALS[name].kind)) return false;
|
|
9472
9881
|
return Boolean(await readMachineCredential(db, CREDENTIALS[name].kind)) || Boolean(process.env[envVar]);
|
|
9473
9882
|
}
|
|
9474
9883
|
async function resolveClaudeAuth(db) {
|
|
@@ -9491,7 +9900,7 @@ async function resolveClaudeAuth(db) {
|
|
|
9491
9900
|
} catch {
|
|
9492
9901
|
}
|
|
9493
9902
|
}
|
|
9494
|
-
const apiKey = await
|
|
9903
|
+
const apiKey = await resolveAnthropicKey(db);
|
|
9495
9904
|
return apiKey ? { apiKey } : null;
|
|
9496
9905
|
}
|
|
9497
9906
|
async function hasClaudeAuth(db) {
|
|
@@ -9588,6 +9997,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
9588
9997
|
}
|
|
9589
9998
|
const cred = CREDENTIALS[name];
|
|
9590
9999
|
setAssistantCredential(cred.kind, key);
|
|
10000
|
+
clearAssistantCredentialCleared(cred.kind);
|
|
9591
10001
|
if (db) {
|
|
9592
10002
|
for (const row of await liveSecretsOfKind(db, cred.kind)) {
|
|
9593
10003
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -9604,6 +10014,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
9604
10014
|
return true;
|
|
9605
10015
|
}
|
|
9606
10016
|
deleteAssistantCredential(CREDENTIALS[name].kind);
|
|
10017
|
+
setAssistantCredentialCleared(CREDENTIALS[name].kind);
|
|
9607
10018
|
if (db) {
|
|
9608
10019
|
for (const row of await liveSecretsOfKind(db, CREDENTIALS[name].kind)) {
|
|
9609
10020
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -10810,6 +11221,11 @@ async function revokeRow(db, table, pk, grantee) {
|
|
|
10810
11221
|
assertPg(db);
|
|
10811
11222
|
await runAsyncOrSync(db.adapter, `SELECT lattice_revoke_row(?, ?, ?)`, [table, pk, grantee]);
|
|
10812
11223
|
}
|
|
11224
|
+
async function batchRowGrants(db, table, pk, grant, revoke) {
|
|
11225
|
+
assertPg(db);
|
|
11226
|
+
for (const grantee of grant) await grantRow(db, table, pk, grantee);
|
|
11227
|
+
for (const grantee of revoke) await revokeRow(db, table, pk, grantee);
|
|
11228
|
+
}
|
|
10813
11229
|
async function revokeMemberRole(db, role) {
|
|
10814
11230
|
assertPg(db);
|
|
10815
11231
|
if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
|
|
@@ -12053,7 +12469,7 @@ function buildSchema(db) {
|
|
|
12053
12469
|
}
|
|
12054
12470
|
return out;
|
|
12055
12471
|
}
|
|
12056
|
-
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false) {
|
|
12472
|
+
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false, privateMode = false) {
|
|
12057
12473
|
if (!text.trim()) return [];
|
|
12058
12474
|
const auth = await resolveClaudeAuth(db);
|
|
12059
12475
|
if (!auth) {
|
|
@@ -12075,6 +12491,7 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
12075
12491
|
});
|
|
12076
12492
|
return [];
|
|
12077
12493
|
}
|
|
12494
|
+
const forceVis = privateMode ? "private" : void 0;
|
|
12078
12495
|
const temperature = aggressivenessToTemperature(aggressiveness);
|
|
12079
12496
|
let description = "";
|
|
12080
12497
|
try {
|
|
@@ -12117,11 +12534,16 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
12117
12534
|
}
|
|
12118
12535
|
if (jx) {
|
|
12119
12536
|
try {
|
|
12120
|
-
await linkRows(
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12537
|
+
await linkRows(
|
|
12538
|
+
mctx,
|
|
12539
|
+
jx.junction,
|
|
12540
|
+
{
|
|
12541
|
+
id: crypto.randomUUID(),
|
|
12542
|
+
[jx.fileFk]: fileId,
|
|
12543
|
+
[jx.otherFk]: m4.id
|
|
12544
|
+
},
|
|
12545
|
+
forceVis
|
|
12546
|
+
);
|
|
12125
12547
|
linkedCount++;
|
|
12126
12548
|
if (created) {
|
|
12127
12549
|
mctx.feed.publish({
|
|
@@ -12180,16 +12602,21 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
12180
12602
|
if ("name" in cols && row.name == null) row.name = obj2.label;
|
|
12181
12603
|
if ("title" in cols && row.title == null) row.title = obj2.label;
|
|
12182
12604
|
try {
|
|
12183
|
-
const { id: rowId } = await createRow(mctx, entity, row);
|
|
12605
|
+
const { id: rowId } = await createRow(mctx, entity, row, forceVis);
|
|
12184
12606
|
createdCount++;
|
|
12185
12607
|
const ent = entity;
|
|
12186
12608
|
const jx = junctions.find((j6) => j6.otherTable === ent) ?? (createJunction ? await createJunction(ent) : null);
|
|
12187
12609
|
if (jx) {
|
|
12188
|
-
await linkRows(
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12610
|
+
await linkRows(
|
|
12611
|
+
mctx,
|
|
12612
|
+
jx.junction,
|
|
12613
|
+
{
|
|
12614
|
+
id: crypto.randomUUID(),
|
|
12615
|
+
[jx.fileFk]: fileId,
|
|
12616
|
+
[jx.otherFk]: rowId
|
|
12617
|
+
},
|
|
12618
|
+
forceVis
|
|
12619
|
+
);
|
|
12193
12620
|
}
|
|
12194
12621
|
} catch (e6) {
|
|
12195
12622
|
console.warn(`[ingest] create ${entity} from document failed:`, e6.message);
|
|
@@ -12203,12 +12630,17 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
12203
12630
|
try {
|
|
12204
12631
|
const title = name.replace(/\.[^./\\]+$/, "").trim() || "Note";
|
|
12205
12632
|
const body = description.length > 0 ? description : text.slice(0, 2e3);
|
|
12206
|
-
const { id: noteId } = await createRow(
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12633
|
+
const { id: noteId } = await createRow(
|
|
12634
|
+
mctx,
|
|
12635
|
+
"notes",
|
|
12636
|
+
{
|
|
12637
|
+
id: crypto.randomUUID(),
|
|
12638
|
+
title,
|
|
12639
|
+
body,
|
|
12640
|
+
source_file_id: fileId
|
|
12641
|
+
},
|
|
12642
|
+
forceVis
|
|
12643
|
+
);
|
|
12212
12644
|
mctx.feed.publish({
|
|
12213
12645
|
table: "notes",
|
|
12214
12646
|
op: "insert",
|
|
@@ -12694,7 +13126,8 @@ async function ingestUrlAsFile(ctx, rawUrl, opts = {}) {
|
|
|
12694
13126
|
ctx.enrich.createJunction,
|
|
12695
13127
|
ctx.enrich.aggressiveness,
|
|
12696
13128
|
ctx.enrich.createEntity,
|
|
12697
|
-
true
|
|
13129
|
+
true,
|
|
13130
|
+
ctx.privateMode === true
|
|
12698
13131
|
);
|
|
12699
13132
|
}
|
|
12700
13133
|
return {
|
|
@@ -13573,13 +14006,22 @@ function loadSdk() {
|
|
|
13573
14006
|
throw new Error("Could not resolve the Anthropic constructor from '@anthropic-ai/sdk'");
|
|
13574
14007
|
return ctor;
|
|
13575
14008
|
}
|
|
13576
|
-
function
|
|
13577
|
-
const Anthropic = loadSdk();
|
|
14009
|
+
function buildAnthropicConfig(auth) {
|
|
13578
14010
|
const config = {};
|
|
13579
|
-
if (auth.authToken)
|
|
13580
|
-
|
|
14011
|
+
if (auth.authToken) {
|
|
14012
|
+
config.authToken = auth.authToken;
|
|
14013
|
+
config.apiKey = null;
|
|
14014
|
+
} else if (auth.apiKey) {
|
|
14015
|
+
config.apiKey = auth.apiKey;
|
|
14016
|
+
} else {
|
|
14017
|
+
config.apiKey = null;
|
|
14018
|
+
}
|
|
13581
14019
|
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
13582
|
-
|
|
14020
|
+
return config;
|
|
14021
|
+
}
|
|
14022
|
+
function createAnthropicClient(auth) {
|
|
14023
|
+
const Anthropic = loadSdk();
|
|
14024
|
+
const sdk = new Anthropic(buildAnthropicConfig(auth));
|
|
13583
14025
|
return {
|
|
13584
14026
|
async runTurn(params) {
|
|
13585
14027
|
const stream = sdk.messages.stream({
|
|
@@ -53145,7 +53587,7 @@ async function checkForUpdate(pkgName, currentVersion, opts = {}) {
|
|
|
53145
53587
|
// src/update-context.ts
|
|
53146
53588
|
init_user_config();
|
|
53147
53589
|
import { execFileSync } from "child_process";
|
|
53148
|
-
import { existsSync as existsSync14, lstatSync, readFileSync as readFileSync10 } from "fs";
|
|
53590
|
+
import { existsSync as existsSync14, lstatSync, readFileSync as readFileSync10, realpathSync } from "fs";
|
|
53149
53591
|
import { dirname as dirname7, join as join13, sep as sep2 } from "path";
|
|
53150
53592
|
var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
53151
53593
|
function isValidVersion(v2) {
|
|
@@ -53175,10 +53617,19 @@ function isUnderGlobalPrefix(packageRoot, execPath) {
|
|
|
53175
53617
|
}
|
|
53176
53618
|
function detectInstallContext(opts = {}) {
|
|
53177
53619
|
const pkgName = opts.pkgName ?? "latticesql";
|
|
53178
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
53179
53620
|
const env2 = opts.env ?? process.env;
|
|
53180
53621
|
const execPath = opts.execPath ?? process.execPath;
|
|
53181
|
-
const
|
|
53622
|
+
const rawCwd = opts.cwd ?? process.cwd();
|
|
53623
|
+
const rawModulePath = opts.modulePath ?? process.argv[1] ?? rawCwd;
|
|
53624
|
+
const resolveReal = (p3) => {
|
|
53625
|
+
try {
|
|
53626
|
+
return realpathSync(p3);
|
|
53627
|
+
} catch {
|
|
53628
|
+
return p3;
|
|
53629
|
+
}
|
|
53630
|
+
};
|
|
53631
|
+
const modulePath = resolveReal(rawModulePath);
|
|
53632
|
+
const cwd = resolveReal(rawCwd);
|
|
53182
53633
|
const packageRoot = findPackageRoot(dirname7(modulePath), pkgName);
|
|
53183
53634
|
if (packageRoot && existsSync14(join13(packageRoot, ".git"))) {
|
|
53184
53635
|
return {
|
|
@@ -53632,6 +54083,8 @@ var css = `
|
|
|
53632
54083
|
.app-version:empty { display: none; }
|
|
53633
54084
|
.app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
|
|
53634
54085
|
.app-update[hidden] { display: none; }
|
|
54086
|
+
#app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
54087
|
+
#app-update-link[hidden] { display: none; }
|
|
53635
54088
|
/* Unseen-change count next to a sidebar entity. */
|
|
53636
54089
|
.nav-badge {
|
|
53637
54090
|
display: inline-block; min-width: 16px; text-align: center;
|
|
@@ -54179,6 +54632,8 @@ var css = `
|
|
|
54179
54632
|
.grants-panel .grants-title { font-weight: 600; margin-bottom: 6px; }
|
|
54180
54633
|
.grants-panel .grants-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
|
|
54181
54634
|
.grants-panel .grants-row input { accent-color: var(--accent); }
|
|
54635
|
+
.grants-panel .grants-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
|
|
54636
|
+
.grants-panel .grants-dirty { font-size: 12px; }
|
|
54182
54637
|
|
|
54183
54638
|
/* Inline create-row at the bottom of every table */
|
|
54184
54639
|
tr.create-row td { background: var(--surface-2); }
|
|
@@ -55108,6 +55563,12 @@ var appJs = `
|
|
|
55108
55563
|
// drag handle once the app has booted.
|
|
55109
55564
|
var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
|
|
55110
55565
|
if (!isNaN(savedRail)) applyRailWidth(savedRail);
|
|
55566
|
+
// The version chip + manual-upgrade link live in the static shell (present
|
|
55567
|
+
// from first paint, in both the normal and virgin-state boots), so wire the
|
|
55568
|
+
// click handler and run the first availability check here \u2014 independent of
|
|
55569
|
+
// the async workspace bootstrap. checkServerVersion() refreshes it later.
|
|
55570
|
+
wireUpdateLink();
|
|
55571
|
+
checkUpdateAvailable();
|
|
55111
55572
|
// Failsafe: never leave the overlay up forever if a fetch hangs without
|
|
55112
55573
|
// rejecting, or a future early-return (e.g. the virgin-state screen)
|
|
55113
55574
|
// bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
|
|
@@ -55592,6 +56053,26 @@ var appJs = `
|
|
|
55592
56053
|
showUpdatePill(label || 'Updated \u2014 reloading\u2026');
|
|
55593
56054
|
setTimeout(function () { location.reload(); }, 600);
|
|
55594
56055
|
}
|
|
56056
|
+
// Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
|
|
56057
|
+
// the version chip only when the server reports a newer, installable version.
|
|
56058
|
+
// The auto-updater installs in the background on its own cadence; this lets
|
|
56059
|
+
// the user force it now. Best-effort; the link stays hidden on any failure.
|
|
56060
|
+
function checkUpdateAvailable() {
|
|
56061
|
+
var el = document.getElementById('app-update-link');
|
|
56062
|
+
if (!el) return;
|
|
56063
|
+
fetch('/api/update/status')
|
|
56064
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
56065
|
+
.then(function (s) {
|
|
56066
|
+
if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
|
|
56067
|
+
el.textContent = 'Update available \u2014 Upgrade';
|
|
56068
|
+
el.title = 'Install v' + s.latest + ' and restart';
|
|
56069
|
+
el.hidden = false;
|
|
56070
|
+
} else {
|
|
56071
|
+
el.hidden = true;
|
|
56072
|
+
}
|
|
56073
|
+
})
|
|
56074
|
+
.catch(function () { /* best-effort \u2014 keep the link hidden */ });
|
|
56075
|
+
}
|
|
55595
56076
|
// On every (re)connect, ask the server its version. A change vs BOOT_VERSION
|
|
55596
56077
|
// means a relaunch onto new code \u2192 reload. Best-effort; never throws.
|
|
55597
56078
|
function checkServerVersion() {
|
|
@@ -55605,6 +56086,31 @@ var appJs = `
|
|
|
55605
56086
|
else hideUpdatePill();
|
|
55606
56087
|
})
|
|
55607
56088
|
.catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
|
|
56089
|
+
// Refresh the manual-upgrade link alongside the reconnect version check.
|
|
56090
|
+
checkUpdateAvailable();
|
|
56091
|
+
}
|
|
56092
|
+
// Wire the manual-upgrade link's click: kick off the install (the server
|
|
56093
|
+
// installs the latest and restarts onto it) and surface the progress. On
|
|
56094
|
+
// success we do nothing else \u2014 the update-applied event + the reconnect
|
|
56095
|
+
// version check land the page on the new version (no manual reload). A
|
|
56096
|
+
// false ok means the install can't run (unsupervised) \u2014 toast why.
|
|
56097
|
+
function wireUpdateLink() {
|
|
56098
|
+
var el = document.getElementById('app-update-link');
|
|
56099
|
+
if (!el) return;
|
|
56100
|
+
el.addEventListener('click', function (e) {
|
|
56101
|
+
e.preventDefault();
|
|
56102
|
+
el.hidden = true;
|
|
56103
|
+
showUpdatePill('Updating\u2026');
|
|
56104
|
+
fetch('/api/update/apply', { method: 'POST' })
|
|
56105
|
+
.then(function (r) { return r.json(); })
|
|
56106
|
+
.then(function (d) {
|
|
56107
|
+
if (d && d.ok === false) {
|
|
56108
|
+
hideUpdatePill();
|
|
56109
|
+
showToast(d.error || 'Update unavailable', {});
|
|
56110
|
+
}
|
|
56111
|
+
})
|
|
56112
|
+
.catch(function () { /* server may already be restarting */ });
|
|
56113
|
+
});
|
|
55608
56114
|
}
|
|
55609
56115
|
function dispatchStreamMessage(type, data) {
|
|
55610
56116
|
if (type === 'realtime-state') {
|
|
@@ -56645,6 +57151,15 @@ var appJs = `
|
|
|
56645
57151
|
// Per-table view state: 'live' (default) or 'trash' (soft-deleted rows).
|
|
56646
57152
|
var tableViewMode = {};
|
|
56647
57153
|
|
|
57154
|
+
// The (table, pk) of the per-row "Manage access" grants panel that is
|
|
57155
|
+
// currently open, or null when none is. A soft re-render (a concurrent edit
|
|
57156
|
+
// by another client fires pg_notify \u2192 realtime refresh \u2192 renderRoute({soft})
|
|
57157
|
+
// \u2192 renderDetail/renderFsItem repaint) would otherwise re-create the detail
|
|
57158
|
+
// view with the panel collapsed, dropping a staged multi-select mid-edit.
|
|
57159
|
+
// wireRowSharing reads this after each repaint and re-opens + re-populates the
|
|
57160
|
+
// panel WITHOUT any network call, so the staged selection survives.
|
|
57161
|
+
var openGrantsPanel = null;
|
|
57162
|
+
|
|
56648
57163
|
function renderTable(content, tableName) {
|
|
56649
57164
|
var myGen = renderGen;
|
|
56650
57165
|
clearUnseen(tableName);
|
|
@@ -57123,70 +57638,151 @@ var appJs = `
|
|
|
57123
57638
|
}).catch(function (e) { showToast('Visibility update failed: ' + e.message, {}); });
|
|
57124
57639
|
});
|
|
57125
57640
|
});
|
|
57126
|
-
var
|
|
57127
|
-
|
|
57641
|
+
var access = row._access || {};
|
|
57642
|
+
|
|
57643
|
+
// Render the staged member checklist + a single "Save sharing" / "Cancel"
|
|
57644
|
+
// into the panel. Checkbox toggles mutate ONLY the local desired map \u2014
|
|
57645
|
+
// NO network call per toggle (the old design auto-saved live, one POST per
|
|
57646
|
+
// checkbox, and each grant's pg_notify collapsed the panel). A single batch
|
|
57647
|
+
// request fires on Save. members is the already-fetched list; desired
|
|
57648
|
+
// seeds from the row's current grantees (or a caller-supplied staged map
|
|
57649
|
+
// when re-opening after a soft re-render).
|
|
57650
|
+
function populateGrantsPanel(panel, members, desired) {
|
|
57651
|
+
// Snapshot the CURRENT (committed) grantees so Save can diff desired-vs-
|
|
57652
|
+
// current into adds/removes. effectiveVisibility decides whether we're
|
|
57653
|
+
// actually switching INTO specific-people mode (custom-0 reads as private).
|
|
57654
|
+
var current = {};
|
|
57655
|
+
(access.grantees || []).forEach(function (g) { current[g] = true; });
|
|
57656
|
+
if (members.length === 0) {
|
|
57657
|
+
panel.innerHTML = '<div class="muted">No other members in this workspace yet.</div>';
|
|
57658
|
+
panel.hidden = false;
|
|
57659
|
+
return;
|
|
57660
|
+
}
|
|
57661
|
+
function dirtyCount() {
|
|
57662
|
+
var n = 0;
|
|
57663
|
+
members.forEach(function (m) {
|
|
57664
|
+
if (!!desired[m.role] !== !!current[m.role]) n++;
|
|
57665
|
+
});
|
|
57666
|
+
return n;
|
|
57667
|
+
}
|
|
57668
|
+
function render() {
|
|
57669
|
+
var changed = dirtyCount();
|
|
57670
|
+
panel.innerHTML = '<div class="grants-title">Who can see this</div>' +
|
|
57671
|
+
members.map(function (m) {
|
|
57672
|
+
var label = m.name || m.email || m.role;
|
|
57673
|
+
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
57674
|
+
(desired[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
57675
|
+
}).join('') +
|
|
57676
|
+
'<div class="grants-actions">' +
|
|
57677
|
+
'<button class="btn primary" id="grants-save"' + (changed ? '' : ' disabled') + '>Save sharing</button>' +
|
|
57678
|
+
'<button class="btn" id="grants-cancel">Cancel</button>' +
|
|
57679
|
+
'<span class="grants-dirty muted">' + (changed ? (changed === 1 ? '1 change' : changed + ' changes') : 'No changes') + '</span>' +
|
|
57680
|
+
'</div>';
|
|
57681
|
+
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
57682
|
+
cb.addEventListener('change', function () {
|
|
57683
|
+
var role = cb.getAttribute('data-grant-role');
|
|
57684
|
+
if (cb.checked) desired[role] = true; else delete desired[role];
|
|
57685
|
+
render(); // re-render to refresh the dirty indicator + Save state
|
|
57686
|
+
});
|
|
57687
|
+
});
|
|
57688
|
+
var cancelBtn = panel.querySelector('#grants-cancel');
|
|
57689
|
+
if (cancelBtn) cancelBtn.addEventListener('click', function () { closeGrantsPanel(panel); });
|
|
57690
|
+
var saveBtn = panel.querySelector('#grants-save');
|
|
57691
|
+
if (saveBtn) saveBtn.addEventListener('click', function () {
|
|
57692
|
+
var toAdd = [];
|
|
57693
|
+
var toRemove = [];
|
|
57694
|
+
members.forEach(function (m) {
|
|
57695
|
+
var want = !!desired[m.role];
|
|
57696
|
+
var have = !!current[m.role];
|
|
57697
|
+
if (want && !have) toAdd.push(m.role);
|
|
57698
|
+
if (!want && have) toRemove.push(m.role);
|
|
57699
|
+
});
|
|
57700
|
+
if (toAdd.length === 0 && toRemove.length === 0) { closeGrantsPanel(panel); return; }
|
|
57701
|
+
// Confirm the mode change ONCE, here \u2014 only when actually switching
|
|
57702
|
+
// INTO specific-people mode (effective vis isn't already custom AND we
|
|
57703
|
+
// are adding at least one grantee). Never per checkbox.
|
|
57704
|
+
if (effectiveVisibility(access) !== 'custom' && toAdd.length > 0) {
|
|
57705
|
+
if (!confirm('Sharing this with specific people switches it off "everyone"/"private". The chosen people will be able to see it. Continue?')) return;
|
|
57706
|
+
}
|
|
57707
|
+
withBusy(saveBtn, function () {
|
|
57708
|
+
return fetchJson('/api/cloud/row-grants', {
|
|
57709
|
+
method: 'POST',
|
|
57710
|
+
headers: { 'content-type': 'application/json' },
|
|
57711
|
+
body: JSON.stringify({ table: tableName, pk: id, grant: toAdd, revoke: toRemove }),
|
|
57712
|
+
}).then(function () {
|
|
57713
|
+
// Mirror the committed state locally so the re-render's indicator
|
|
57714
|
+
// is correct. The first grant flips the row to custom server-side;
|
|
57715
|
+
// revoking the last leaves custom-0, which effectiveVisibility
|
|
57716
|
+
// renders as private.
|
|
57717
|
+
var list = [];
|
|
57718
|
+
members.forEach(function (m) { if (desired[m.role]) list.push(m.role); });
|
|
57719
|
+
access.grantees = list;
|
|
57720
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
57721
|
+
openGrantsPanel = null; // a successful save closes the staging session
|
|
57722
|
+
invalidate(tableName);
|
|
57723
|
+
showToast('Sharing updated', {});
|
|
57724
|
+
reRender();
|
|
57725
|
+
}).catch(function (e) {
|
|
57726
|
+
// Surface loudly + leave the staged selection intact so the user
|
|
57727
|
+
// can retry; no silent partial-success.
|
|
57728
|
+
showToast('Sharing update failed: ' + e.message, {});
|
|
57729
|
+
});
|
|
57730
|
+
});
|
|
57731
|
+
});
|
|
57732
|
+
panel.hidden = false;
|
|
57733
|
+
}
|
|
57734
|
+
render();
|
|
57735
|
+
}
|
|
57736
|
+
|
|
57737
|
+
function closeGrantsPanel(panel) {
|
|
57738
|
+
if (panel) panel.hidden = true;
|
|
57739
|
+
openGrantsPanel = null;
|
|
57740
|
+
}
|
|
57741
|
+
|
|
57742
|
+
// Open (or toggle shut) the manage-access panel. Fetches the member list,
|
|
57743
|
+
// then stages from the row's current grantees. Opening must NOT pre-flip
|
|
57744
|
+
// the row to 'custom' \u2014 that left a never-shared row stuck at "custom (0)".
|
|
57745
|
+
function openManagePanel(triggerBtn) {
|
|
57128
57746
|
var panel = content.querySelector('#grants-panel');
|
|
57129
57747
|
if (!panel) return;
|
|
57130
|
-
if (!panel.hidden) { panel
|
|
57131
|
-
|
|
57132
|
-
|
|
57133
|
-
// row the user never actually shared stuck at "custom (0)". The first
|
|
57134
|
-
// grant flips it to custom server-side (lattice_grant_row); revoking the
|
|
57135
|
-
// last leaves it custom-with-0-grantees, which now reads as private. So
|
|
57136
|
-
// just load the member checklist.
|
|
57137
|
-
var ensure = Promise.resolve();
|
|
57138
|
-
withBusy(detailVisManage, function () {
|
|
57139
|
-
return ensure.then(function () {
|
|
57140
|
-
return fetchJson('/api/cloud/members');
|
|
57141
|
-
}).then(function (d) {
|
|
57748
|
+
if (!panel.hidden) { closeGrantsPanel(panel); return; }
|
|
57749
|
+
withBusy(triggerBtn, function () {
|
|
57750
|
+
return fetchJson('/api/cloud/members').then(function (d) {
|
|
57142
57751
|
// The grant target is a member ROLE: lattice_grant_row keys on the
|
|
57143
57752
|
// role, and _access.grantees holds role names. List every member
|
|
57144
57753
|
// except the owner (you don't grant the owner their own row).
|
|
57145
57754
|
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
57146
|
-
var
|
|
57147
|
-
(access.grantees || []).forEach(function (g) {
|
|
57148
|
-
|
|
57149
|
-
|
|
57150
|
-
} else {
|
|
57151
|
-
panel.innerHTML = '<div class="grants-title">Who can see this</div>' + members.map(function (m) {
|
|
57152
|
-
var label = m.name || m.email || m.role;
|
|
57153
|
-
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
57154
|
-
(granted[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
57155
|
-
}).join('');
|
|
57156
|
-
}
|
|
57157
|
-
panel.hidden = false;
|
|
57158
|
-
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
57159
|
-
cb.addEventListener('change', function () {
|
|
57160
|
-
var role = cb.getAttribute('data-grant-role');
|
|
57161
|
-
cb.disabled = true;
|
|
57162
|
-
fetchJson('/api/cloud/row-grant', {
|
|
57163
|
-
method: 'POST',
|
|
57164
|
-
headers: { 'content-type': 'application/json' },
|
|
57165
|
-
body: JSON.stringify({ table: tableName, pk: id, grantee: role, revoke: !cb.checked }),
|
|
57166
|
-
}).then(function () {
|
|
57167
|
-
var list = access.grantees || (access.grantees = []);
|
|
57168
|
-
var at = list.indexOf(role);
|
|
57169
|
-
if (cb.checked && at === -1) list.push(role);
|
|
57170
|
-
if (!cb.checked && at !== -1) list.splice(at, 1);
|
|
57171
|
-
// The first grant flips the row to custom server-side; mirror
|
|
57172
|
-
// that locally so the indicator updates. Revoking the last leaves
|
|
57173
|
-
// visibility 'custom' but effectiveVisibility renders custom-0 as
|
|
57174
|
-
// private, so the label flips back to "Private to you".
|
|
57175
|
-
if (list.length > 0) access.visibility = 'custom';
|
|
57176
|
-
var infoEl = content.querySelector('#detail-vis-info');
|
|
57177
|
-
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
57178
|
-
invalidate(tableName);
|
|
57179
|
-
}).catch(function (e) {
|
|
57180
|
-
cb.checked = !cb.checked; // revert the failed change
|
|
57181
|
-
showToast('Access update failed: ' + e.message, {});
|
|
57182
|
-
}).then(function () { cb.disabled = false; });
|
|
57183
|
-
});
|
|
57184
|
-
});
|
|
57185
|
-
var infoEl = content.querySelector('#detail-vis-info');
|
|
57186
|
-
if (infoEl) infoEl.textContent = visInfoLabel(access);
|
|
57755
|
+
var desired = {};
|
|
57756
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
57757
|
+
openGrantsPanel = { table: tableName, pk: id };
|
|
57758
|
+
populateGrantsPanel(panel, members, desired);
|
|
57187
57759
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
57188
57760
|
});
|
|
57761
|
+
}
|
|
57762
|
+
|
|
57763
|
+
var detailVisManage = content.querySelector('#detail-vis-manage');
|
|
57764
|
+
if (detailVisManage) detailVisManage.addEventListener('click', function () {
|
|
57765
|
+
openManagePanel(detailVisManage);
|
|
57189
57766
|
});
|
|
57767
|
+
|
|
57768
|
+
// Preserve an open panel across a soft re-render: if the tracked panel
|
|
57769
|
+
// matches the row this view just repainted, re-open it and re-populate the
|
|
57770
|
+
// checklist from the freshly-fetched row._access WITHOUT any network call,
|
|
57771
|
+
// so a concurrent edit by another client doesn't lose a staged selection.
|
|
57772
|
+
if (openGrantsPanel && openGrantsPanel.table === tableName && openGrantsPanel.pk === id) {
|
|
57773
|
+
var rpanel = content.querySelector('#grants-panel');
|
|
57774
|
+
if (rpanel) {
|
|
57775
|
+
fetchJson('/api/cloud/members').then(function (d) {
|
|
57776
|
+
// Only re-populate if THIS panel is still the tracked-open one (a
|
|
57777
|
+
// newer navigation/save may have cleared it while members loaded).
|
|
57778
|
+
if (!openGrantsPanel || openGrantsPanel.table !== tableName || openGrantsPanel.pk !== id) return;
|
|
57779
|
+
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
57780
|
+
var desired = {};
|
|
57781
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
57782
|
+
populateGrantsPanel(rpanel, members, desired);
|
|
57783
|
+
}).catch(function () { /* best-effort restore; a click reopens it */ });
|
|
57784
|
+
}
|
|
57785
|
+
}
|
|
57190
57786
|
}
|
|
57191
57787
|
function renderDetail(content, tableName, id) {
|
|
57192
57788
|
var myGen = renderGen;
|
|
@@ -61970,13 +62566,21 @@ var appJs = `
|
|
|
61970
62566
|
}
|
|
61971
62567
|
function uploadFile(file) {
|
|
61972
62568
|
var done = pendingIngestItem(file.name || 'file');
|
|
62569
|
+
// Carry the composer's "Private mode" intent so an upload made while the
|
|
62570
|
+
// box is checked is stamped private at insert, instead of inheriting the
|
|
62571
|
+
// files-table default (which can be shared-to-everyone on a cloud). Read
|
|
62572
|
+
// the checkbox defensively \u2014 it may not be rendered. On a local workspace
|
|
62573
|
+
// the box is checked+disabled, so this is '1' there too; forced visibility
|
|
62574
|
+
// is a harmless no-op on the single-user SQLite path.
|
|
62575
|
+
var pv = document.getElementById('chat-private');
|
|
62576
|
+
var priv = pv && pv.checked ? '1' : '0';
|
|
61973
62577
|
return fetch('/api/ingest/upload', {
|
|
61974
62578
|
method: 'POST',
|
|
61975
62579
|
// Percent-encode the filename: HTTP header values must be ISO-8859-1,
|
|
61976
62580
|
// so a Unicode filename (emoji, smart quote, accent, em-dash) would
|
|
61977
62581
|
// otherwise make fetch() throw "String contains non ISO-8859-1 code
|
|
61978
62582
|
// point". The server decodeURIComponent()s it back.
|
|
61979
|
-
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file') },
|
|
62583
|
+
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file'), 'x-lattice-private': priv },
|
|
61980
62584
|
body: file,
|
|
61981
62585
|
})
|
|
61982
62586
|
.then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; }); })
|
|
@@ -62324,6 +62928,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
62324
62928
|
<span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
|
|
62325
62929
|
<span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
|
|
62326
62930
|
<span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
|
|
62931
|
+
<a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
|
|
62327
62932
|
<button id="settings-gear" title="Settings" aria-label="Open settings">
|
|
62328
62933
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
62329
62934
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -63072,8 +63677,14 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
63072
63677
|
},
|
|
63073
63678
|
{
|
|
63074
63679
|
name: "_lattice_gui_audit",
|
|
63075
|
-
|
|
63076
|
-
|
|
63680
|
+
// UPDATE + DELETE are needed by undo/redo/revert (flips an entry's `undone`)
|
|
63681
|
+
// and the redo-stack purge on a new mutation (deletes the session's undone
|
|
63682
|
+
// entries). Safe because enableGuiAuditRls installs per-op UPDATE and DELETE
|
|
63683
|
+
// policies whose USING is `row_id IS NULL OR lattice_row_visible(table_name,
|
|
63684
|
+
// row_id)` — so a member can only update/delete audit rows for entities it can
|
|
63685
|
+
// already see (or schema-level entries that carry no row data).
|
|
63686
|
+
privs: "SELECT, INSERT, UPDATE, DELETE",
|
|
63687
|
+
why: "GUI undo/redo/revert + redo-stack purge + version history; RLS (enableGuiAuditRls) scopes every op to entries whose underlying row the member can see"
|
|
63077
63688
|
},
|
|
63078
63689
|
{
|
|
63079
63690
|
name: "__lattice_user_identity",
|
|
@@ -65148,6 +65759,27 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
65148
65759
|
});
|
|
65149
65760
|
return true;
|
|
65150
65761
|
}
|
|
65762
|
+
if (pathname === "/api/cloud/row-grants" && method === "POST") {
|
|
65763
|
+
await tryHandler(res, async () => {
|
|
65764
|
+
const body = await readJson(req);
|
|
65765
|
+
const table = typeof body.table === "string" ? body.table : "";
|
|
65766
|
+
const pk = typeof body.pk === "string" ? body.pk : "";
|
|
65767
|
+
const strList = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : [];
|
|
65768
|
+
const grant = strList(body.grant);
|
|
65769
|
+
const revoke = strList(body.revoke);
|
|
65770
|
+
if (!table || !pk) {
|
|
65771
|
+
sendJson(res, { error: "table and pk are required" }, 400);
|
|
65772
|
+
return;
|
|
65773
|
+
}
|
|
65774
|
+
if (ctx.db.getDialect() !== "postgres") {
|
|
65775
|
+
sendJson(res, { error: "Per-row sharing requires a cloud (Postgres) database" }, 400);
|
|
65776
|
+
return;
|
|
65777
|
+
}
|
|
65778
|
+
await batchRowGrants(ctx.db, table, pk, grant, revoke);
|
|
65779
|
+
sendJson(res, { ok: true, table, pk, granted: grant, revoked: revoke });
|
|
65780
|
+
});
|
|
65781
|
+
return true;
|
|
65782
|
+
}
|
|
65151
65783
|
if (pathname === "/api/cloud/s3-config" && method === "GET") {
|
|
65152
65784
|
await tryHandler(res, () => {
|
|
65153
65785
|
const label = activeWorkspaceLabel(ctx.configPath);
|
|
@@ -65890,6 +66522,19 @@ async function normalizeImage(path2, maxBytes) {
|
|
|
65890
66522
|
function renderJpeg(sharp, path2, quality) {
|
|
65891
66523
|
return sharp(path2).rotate().resize({ width: MAX_DIM, height: MAX_DIM, fit: "inside", withoutEnlargement: true }).jpeg({ quality }).toBuffer();
|
|
65892
66524
|
}
|
|
66525
|
+
function buildVisionAnthropicConfig(auth) {
|
|
66526
|
+
const config = {};
|
|
66527
|
+
if (auth.authToken) {
|
|
66528
|
+
config.authToken = auth.authToken;
|
|
66529
|
+
config.apiKey = null;
|
|
66530
|
+
} else if (auth.apiKey) {
|
|
66531
|
+
config.apiKey = auth.apiKey;
|
|
66532
|
+
} else {
|
|
66533
|
+
config.apiKey = null;
|
|
66534
|
+
}
|
|
66535
|
+
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
66536
|
+
return config;
|
|
66537
|
+
}
|
|
65893
66538
|
function defaultSender(auth) {
|
|
65894
66539
|
return async (input) => {
|
|
65895
66540
|
const importMetaUrl = import.meta.url;
|
|
@@ -65897,11 +66542,7 @@ function defaultSender(auth) {
|
|
|
65897
66542
|
const sdk = req("@anthropic-ai/sdk");
|
|
65898
66543
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
65899
66544
|
if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
|
|
65900
|
-
const
|
|
65901
|
-
if (auth.authToken) config.authToken = auth.authToken;
|
|
65902
|
-
else if (auth.apiKey) config.apiKey = auth.apiKey;
|
|
65903
|
-
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
65904
|
-
const client = new Anthropic(config);
|
|
66545
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
65905
66546
|
const res = await client.messages.create({
|
|
65906
66547
|
model: input.model,
|
|
65907
66548
|
max_tokens: 1024,
|
|
@@ -65928,11 +66569,7 @@ function defaultPdfSender(auth) {
|
|
|
65928
66569
|
const sdk = req("@anthropic-ai/sdk");
|
|
65929
66570
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
65930
66571
|
if (!Anthropic) throw new Error("Could not resolve Anthropic from '@anthropic-ai/sdk'");
|
|
65931
|
-
const
|
|
65932
|
-
if (auth.authToken) config.authToken = auth.authToken;
|
|
65933
|
-
else if (auth.apiKey) config.apiKey = auth.apiKey;
|
|
65934
|
-
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
65935
|
-
const client = new Anthropic(config);
|
|
66572
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
65936
66573
|
const res = await client.messages.create({
|
|
65937
66574
|
model: input.model,
|
|
65938
66575
|
max_tokens: 4096,
|
|
@@ -66094,7 +66731,7 @@ function enrichContext(ctx) {
|
|
|
66094
66731
|
...ctx.createEntity ? { createEntity: ctx.createEntity } : {}
|
|
66095
66732
|
};
|
|
66096
66733
|
}
|
|
66097
|
-
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
66734
|
+
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res, privateMode) {
|
|
66098
66735
|
try {
|
|
66099
66736
|
return await enrichWithLlm(
|
|
66100
66737
|
mctx,
|
|
@@ -66106,7 +66743,9 @@ async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
|
66106
66743
|
ctx.entityDescriptions,
|
|
66107
66744
|
ctx.createJunction,
|
|
66108
66745
|
ctx.aggressiveness,
|
|
66109
|
-
ctx.createEntity
|
|
66746
|
+
ctx.createEntity,
|
|
66747
|
+
false,
|
|
66748
|
+
privateMode
|
|
66110
66749
|
);
|
|
66111
66750
|
} catch (e6) {
|
|
66112
66751
|
const err = e6;
|
|
@@ -66185,7 +66824,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66185
66824
|
source: "ingest",
|
|
66186
66825
|
onColumnsAdded: columnDescriptionHook(ctx.db)
|
|
66187
66826
|
};
|
|
66827
|
+
const headerPrivate = req.headers["x-lattice-private"] === "1";
|
|
66188
66828
|
if (ctx.pathname === "/api/ingest/upload") {
|
|
66829
|
+
const forcePrivate2 = headerPrivate;
|
|
66189
66830
|
const rawName = typeof req.headers["x-filename"] === "string" && req.headers["x-filename"] || "";
|
|
66190
66831
|
let name2 = "upload";
|
|
66191
66832
|
if (rawName) {
|
|
@@ -66283,10 +66924,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66283
66924
|
...blob ? { blob_path: blob.blob_path } : {}
|
|
66284
66925
|
} : blob ? { ref_kind: "blob", blob_path: blob.blob_path } : {}
|
|
66285
66926
|
};
|
|
66286
|
-
const { id: id2 } = await createRow(
|
|
66287
|
-
|
|
66288
|
-
|
|
66289
|
-
|
|
66927
|
+
const { id: id2 } = await createRow(
|
|
66928
|
+
mctx,
|
|
66929
|
+
"files",
|
|
66930
|
+
{
|
|
66931
|
+
...await requiredFileDefaults(ctx.db, name2, fileId, uploadRow),
|
|
66932
|
+
...uploadRow
|
|
66933
|
+
},
|
|
66934
|
+
forcePrivate2 ? "private" : void 0
|
|
66935
|
+
);
|
|
66290
66936
|
try {
|
|
66291
66937
|
const dedupCtx = {
|
|
66292
66938
|
db: ctx.db,
|
|
@@ -66312,7 +66958,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66312
66958
|
}
|
|
66313
66959
|
let suggestedLinks = [];
|
|
66314
66960
|
if (!result.skip) {
|
|
66315
|
-
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res);
|
|
66961
|
+
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res, forcePrivate2);
|
|
66316
66962
|
if (links === null) return true;
|
|
66317
66963
|
suggestedLinks = links;
|
|
66318
66964
|
}
|
|
@@ -66339,6 +66985,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66339
66985
|
sendJson4(res, { error: e6.message }, 400);
|
|
66340
66986
|
return true;
|
|
66341
66987
|
}
|
|
66988
|
+
const forcePrivate = headerPrivate || body.private === true;
|
|
66342
66989
|
if (ctx.pathname === "/api/ingest/text") {
|
|
66343
66990
|
const rawText = typeof body.text === "string" ? body.text : "";
|
|
66344
66991
|
if (!rawText.trim()) {
|
|
@@ -66349,7 +66996,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66349
66996
|
if (sourceUrl) {
|
|
66350
66997
|
try {
|
|
66351
66998
|
const result = await ingestUrlAsFile(
|
|
66352
|
-
{ db: ctx.db, mctx, enrich: enrichContext(ctx) },
|
|
66999
|
+
{ db: ctx.db, mctx, enrich: enrichContext(ctx), privateMode: forcePrivate },
|
|
66353
67000
|
sourceUrl
|
|
66354
67001
|
);
|
|
66355
67002
|
sendJson4(
|
|
@@ -66378,11 +67025,25 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66378
67025
|
description: describe(content, mime2, title),
|
|
66379
67026
|
extraction_status: "extracted"
|
|
66380
67027
|
};
|
|
66381
|
-
const { id: id2 } = await createRow(
|
|
66382
|
-
|
|
66383
|
-
|
|
66384
|
-
|
|
66385
|
-
|
|
67028
|
+
const { id: id2 } = await createRow(
|
|
67029
|
+
mctx,
|
|
67030
|
+
"files",
|
|
67031
|
+
{
|
|
67032
|
+
...await requiredFileDefaults(ctx.db, title, textFileId, textRow),
|
|
67033
|
+
...textRow
|
|
67034
|
+
},
|
|
67035
|
+
forcePrivate ? "private" : void 0
|
|
67036
|
+
);
|
|
67037
|
+
const suggestedLinks = await enrichOrFail(
|
|
67038
|
+
mctx,
|
|
67039
|
+
ctx.db,
|
|
67040
|
+
id2,
|
|
67041
|
+
content,
|
|
67042
|
+
title,
|
|
67043
|
+
ctx,
|
|
67044
|
+
res,
|
|
67045
|
+
forcePrivate
|
|
67046
|
+
);
|
|
66386
67047
|
if (suggestedLinks === null) return true;
|
|
66387
67048
|
sendJson4(res, { id: id2, extraction_status: "extracted", suggestedLinks }, 201);
|
|
66388
67049
|
return true;
|
|
@@ -66421,10 +67082,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66421
67082
|
size_bytes: size,
|
|
66422
67083
|
extraction_status: "pending"
|
|
66423
67084
|
};
|
|
66424
|
-
const { id } = await createRow(
|
|
66425
|
-
|
|
66426
|
-
|
|
66427
|
-
|
|
67085
|
+
const { id } = await createRow(
|
|
67086
|
+
mctx,
|
|
67087
|
+
"files",
|
|
67088
|
+
{
|
|
67089
|
+
...await requiredFileDefaults(ctx.db, name, localFileId, localRow),
|
|
67090
|
+
...localRow
|
|
67091
|
+
},
|
|
67092
|
+
forcePrivate ? "private" : void 0
|
|
67093
|
+
);
|
|
66428
67094
|
try {
|
|
66429
67095
|
const result = await extractSource(ctx.db, abs, mime, name);
|
|
66430
67096
|
await updateRow(mctx, "files", id, {
|
|
@@ -66442,7 +67108,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66442
67108
|
ctx.entityDescriptions,
|
|
66443
67109
|
ctx.createJunction,
|
|
66444
67110
|
ctx.aggressiveness,
|
|
66445
|
-
ctx.createEntity
|
|
67111
|
+
ctx.createEntity,
|
|
67112
|
+
false,
|
|
67113
|
+
forcePrivate
|
|
66446
67114
|
);
|
|
66447
67115
|
sendJson4(
|
|
66448
67116
|
res,
|
|
@@ -67129,7 +67797,7 @@ function startBackgroundRender(active) {
|
|
|
67129
67797
|
}
|
|
67130
67798
|
bus.publish(e6);
|
|
67131
67799
|
};
|
|
67132
|
-
void db.renderInBackground(active.outputDir, { signal, onProgress }).then(
|
|
67800
|
+
void db.renderInBackground(active.outputDir, { signal, onProgress, gateOnOpen: true }).then(
|
|
67133
67801
|
() => {
|
|
67134
67802
|
},
|
|
67135
67803
|
(err) => {
|
|
@@ -67471,6 +68139,28 @@ async function startGuiServer(options) {
|
|
|
67471
68139
|
setActive(next, created.id);
|
|
67472
68140
|
return created.id;
|
|
67473
68141
|
};
|
|
68142
|
+
const cleanupWorkspaceFiles = (root6, ws) => {
|
|
68143
|
+
if (!ws.configPath && ws.kind === "local") {
|
|
68144
|
+
rmSync(workspaceDir(root6, ws.dir), { recursive: true, force: true });
|
|
68145
|
+
} else if (ws.kind === "cloud") {
|
|
68146
|
+
if (ws.configPath && existsSync25(ws.configPath)) {
|
|
68147
|
+
rmSync(ws.configPath, { force: true });
|
|
68148
|
+
}
|
|
68149
|
+
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
68150
|
+
const label = labelMatch?.[1];
|
|
68151
|
+
if (label) {
|
|
68152
|
+
const stillUsed = listWorkspaces(root6).some(
|
|
68153
|
+
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
68154
|
+
);
|
|
68155
|
+
if (!stillUsed) {
|
|
68156
|
+
try {
|
|
68157
|
+
deleteDbCredential(label);
|
|
68158
|
+
} catch {
|
|
68159
|
+
}
|
|
68160
|
+
}
|
|
68161
|
+
}
|
|
68162
|
+
}
|
|
68163
|
+
};
|
|
67474
68164
|
const handleVirginRoute = async (req, res, pathname, method) => {
|
|
67475
68165
|
if (method === "GET" && pathname === "/") {
|
|
67476
68166
|
sendText(
|
|
@@ -67522,6 +68212,35 @@ async function startGuiServer(options) {
|
|
|
67522
68212
|
}
|
|
67523
68213
|
return true;
|
|
67524
68214
|
}
|
|
68215
|
+
if (method === "POST" && pathname === "/api/workspaces/delete") {
|
|
68216
|
+
if (!latticeRoot) {
|
|
68217
|
+
sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
|
|
68218
|
+
return true;
|
|
68219
|
+
}
|
|
68220
|
+
const body = await readJson(req);
|
|
68221
|
+
if (typeof body.id !== "string") {
|
|
68222
|
+
sendJson(res, { error: "id must be a string" }, 400);
|
|
68223
|
+
return true;
|
|
68224
|
+
}
|
|
68225
|
+
const ws = getWorkspace(latticeRoot, body.id);
|
|
68226
|
+
if (!ws) {
|
|
68227
|
+
sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
|
|
68228
|
+
return true;
|
|
68229
|
+
}
|
|
68230
|
+
removeWorkspace(latticeRoot, ws.id);
|
|
68231
|
+
try {
|
|
68232
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
68233
|
+
} catch (e6) {
|
|
68234
|
+
sendJson(
|
|
68235
|
+
res,
|
|
68236
|
+
{ error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
|
|
68237
|
+
500
|
|
68238
|
+
);
|
|
68239
|
+
return true;
|
|
68240
|
+
}
|
|
68241
|
+
sendJson(res, { ok: true, switchedTo: null });
|
|
68242
|
+
return true;
|
|
68243
|
+
}
|
|
67525
68244
|
if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
|
|
67526
68245
|
await redeemInvite(createCloudWorkspace, req, res);
|
|
67527
68246
|
return true;
|
|
@@ -67556,6 +68275,18 @@ async function startGuiServer(options) {
|
|
|
67556
68275
|
);
|
|
67557
68276
|
return;
|
|
67558
68277
|
}
|
|
68278
|
+
if (method === "POST" && pathname === "/api/update/apply") {
|
|
68279
|
+
if (updateService) {
|
|
68280
|
+
void updateService.checkNow(true);
|
|
68281
|
+
sendJson(res, { ok: true, status: updateService.status() });
|
|
68282
|
+
} else {
|
|
68283
|
+
sendJson(res, {
|
|
68284
|
+
ok: false,
|
|
68285
|
+
error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
|
|
68286
|
+
});
|
|
68287
|
+
}
|
|
68288
|
+
return;
|
|
68289
|
+
}
|
|
67559
68290
|
if (!activeRef) {
|
|
67560
68291
|
if (await handleVirginRoute(req, res, pathname, method)) return;
|
|
67561
68292
|
sendJson(res, { error: "No active workspace" }, 409);
|
|
@@ -68649,26 +69380,7 @@ async function startGuiServer(options) {
|
|
|
68649
69380
|
}
|
|
68650
69381
|
removeWorkspace(latticeRoot, ws.id);
|
|
68651
69382
|
try {
|
|
68652
|
-
|
|
68653
|
-
rmSync(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
|
|
68654
|
-
} else if (ws.kind === "cloud") {
|
|
68655
|
-
if (ws.configPath && existsSync25(ws.configPath)) {
|
|
68656
|
-
rmSync(ws.configPath, { force: true });
|
|
68657
|
-
}
|
|
68658
|
-
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
|
|
68659
|
-
const label = labelMatch?.[1];
|
|
68660
|
-
if (label) {
|
|
68661
|
-
const stillUsed = listWorkspaces(latticeRoot).some(
|
|
68662
|
-
(w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
|
|
68663
|
-
);
|
|
68664
|
-
if (!stillUsed) {
|
|
68665
|
-
try {
|
|
68666
|
-
deleteDbCredential(label);
|
|
68667
|
-
} catch {
|
|
68668
|
-
}
|
|
68669
|
-
}
|
|
68670
|
-
}
|
|
68671
|
-
}
|
|
69383
|
+
cleanupWorkspaceFiles(latticeRoot, ws);
|
|
68672
69384
|
} catch (e6) {
|
|
68673
69385
|
sendJson(
|
|
68674
69386
|
res,
|
|
@@ -69134,7 +69846,9 @@ ${e6.stack ?? ""}`
|
|
|
69134
69846
|
}
|
|
69135
69847
|
}
|
|
69136
69848
|
};
|
|
69137
|
-
if (options.
|
|
69849
|
+
if (options.updateServiceFactory) {
|
|
69850
|
+
updateService = options.updateServiceFactory(broadcast);
|
|
69851
|
+
} else if (options.selfUpdate && guiVersion) {
|
|
69138
69852
|
updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
|
|
69139
69853
|
}
|
|
69140
69854
|
const handleEventStream = (ws) => {
|
|
@@ -69507,7 +70221,7 @@ function printHelp() {
|
|
|
69507
70221
|
);
|
|
69508
70222
|
}
|
|
69509
70223
|
function getVersion() {
|
|
69510
|
-
if (true) return "3.4.
|
|
70224
|
+
if (true) return "3.4.5";
|
|
69511
70225
|
try {
|
|
69512
70226
|
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
69513
70227
|
const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
|