latticesql 3.4.3 → 3.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +794 -169
- package/dist/index.cjs +792 -168
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +793 -168
- package/package.json +1 -1
package/dist/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
|
|
|
@@ -960,10 +977,12 @@ function readManifest(outputDir) {
|
|
|
960
977
|
function writeManifest(outputDir, manifest) {
|
|
961
978
|
atomicWrite(manifestPath(outputDir), JSON.stringify(manifest, null, 2));
|
|
962
979
|
}
|
|
980
|
+
var TEMPLATE_VERSION;
|
|
963
981
|
var init_manifest = __esm({
|
|
964
982
|
"src/lifecycle/manifest.ts"() {
|
|
965
983
|
"use strict";
|
|
966
984
|
init_writer();
|
|
985
|
+
TEMPLATE_VERSION = 1;
|
|
967
986
|
}
|
|
968
987
|
});
|
|
969
988
|
|
|
@@ -997,6 +1016,126 @@ var init_adapter = __esm({
|
|
|
997
1016
|
}
|
|
998
1017
|
});
|
|
999
1018
|
|
|
1019
|
+
// src/lifecycle/render-cursor.ts
|
|
1020
|
+
function markToString(v2) {
|
|
1021
|
+
if (v2 == null) return null;
|
|
1022
|
+
if (v2 instanceof Date) return v2.toISOString();
|
|
1023
|
+
if (typeof v2 === "string") return v2;
|
|
1024
|
+
if (typeof v2 === "number" || typeof v2 === "bigint" || typeof v2 === "boolean") return String(v2);
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
function padNumericMark(v2) {
|
|
1028
|
+
const s2 = markToString(v2);
|
|
1029
|
+
if (s2 == null) return null;
|
|
1030
|
+
if (/^\d+$/.test(s2)) return s2.padStart(20, "0");
|
|
1031
|
+
return s2;
|
|
1032
|
+
}
|
|
1033
|
+
async function changelogExists(adapter) {
|
|
1034
|
+
if (adapter.dialect === "postgres") {
|
|
1035
|
+
const row2 = await getAsyncOrSync(
|
|
1036
|
+
adapter,
|
|
1037
|
+
`SELECT to_regclass('__lattice_changelog') AS reg`
|
|
1038
|
+
);
|
|
1039
|
+
return !!row2 && row2.reg != null;
|
|
1040
|
+
}
|
|
1041
|
+
const row = await getAsyncOrSync(
|
|
1042
|
+
adapter,
|
|
1043
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='__lattice_changelog'`
|
|
1044
|
+
);
|
|
1045
|
+
return !!row;
|
|
1046
|
+
}
|
|
1047
|
+
async function changelogMark(adapter) {
|
|
1048
|
+
try {
|
|
1049
|
+
if (!await changelogExists(adapter)) return null;
|
|
1050
|
+
const col = adapter.dialect === "postgres" ? "seq" : "rowid";
|
|
1051
|
+
const row = await getAsyncOrSync(
|
|
1052
|
+
adapter,
|
|
1053
|
+
`SELECT MAX(${col}) AS m FROM __lattice_changelog`
|
|
1054
|
+
);
|
|
1055
|
+
return padNumericMark(row?.m);
|
|
1056
|
+
} catch {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function sharingMarks(adapter) {
|
|
1061
|
+
if (adapter.dialect !== "postgres") return { grants: null, owners: null };
|
|
1062
|
+
try {
|
|
1063
|
+
const reg = await getAsyncOrSync(
|
|
1064
|
+
adapter,
|
|
1065
|
+
`SELECT to_regclass('__lattice_changes') AS reg`
|
|
1066
|
+
);
|
|
1067
|
+
const hasFeed = !!reg && reg.reg != null;
|
|
1068
|
+
if (hasFeed) {
|
|
1069
|
+
const row = await getAsyncOrSync(
|
|
1070
|
+
adapter,
|
|
1071
|
+
`SELECT COUNT(*) AS n, MAX(seq) AS m FROM lattice_changes_since(0, 1000)`
|
|
1072
|
+
);
|
|
1073
|
+
const digest = digestOf(row?.n, row?.m);
|
|
1074
|
+
return { grants: digest, owners: digest };
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
let owners = null;
|
|
1079
|
+
let grants = null;
|
|
1080
|
+
try {
|
|
1081
|
+
const o3 = await getAsyncOrSync(
|
|
1082
|
+
adapter,
|
|
1083
|
+
`SELECT COUNT(*) AS n, MAX(updated_at) AS m FROM __lattice_owners`
|
|
1084
|
+
);
|
|
1085
|
+
owners = digestOf(o3?.n, o3?.m);
|
|
1086
|
+
} catch {
|
|
1087
|
+
owners = null;
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
const g6 = await getAsyncOrSync(
|
|
1091
|
+
adapter,
|
|
1092
|
+
`SELECT COUNT(*) AS n, MAX(granted_at) AS m FROM __lattice_row_grants`
|
|
1093
|
+
);
|
|
1094
|
+
grants = digestOf(g6?.n, g6?.m);
|
|
1095
|
+
} catch {
|
|
1096
|
+
grants = null;
|
|
1097
|
+
}
|
|
1098
|
+
return { grants, owners };
|
|
1099
|
+
}
|
|
1100
|
+
function digestOf(count, max) {
|
|
1101
|
+
const n3 = padNumericMark(count);
|
|
1102
|
+
if (n3 == null) return null;
|
|
1103
|
+
const m4 = markToString(max) ?? "";
|
|
1104
|
+
return `${n3}#${m4}`;
|
|
1105
|
+
}
|
|
1106
|
+
async function computeRenderCursor(adapter) {
|
|
1107
|
+
try {
|
|
1108
|
+
const [changelog, sharing] = await Promise.all([changelogMark(adapter), sharingMarks(adapter)]);
|
|
1109
|
+
return { changelog, grants: sharing.grants, owners: sharing.owners };
|
|
1110
|
+
} catch {
|
|
1111
|
+
return { ...EMPTY_CURSOR };
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
function cursorIsFresh(recorded, live, templateVersion = TEMPLATE_VERSION) {
|
|
1115
|
+
if (recorded == null) return false;
|
|
1116
|
+
if (recorded.templateVersion !== templateVersion) return false;
|
|
1117
|
+
const rc = recorded.cursor;
|
|
1118
|
+
if (rc == null) return false;
|
|
1119
|
+
if (!fieldFresh(rc.changelog, live.changelog, (r6, l4) => l4 <= r6)) return false;
|
|
1120
|
+
if (!fieldFresh(rc.grants, live.grants, (r6, l4) => l4 === r6)) return false;
|
|
1121
|
+
if (!fieldFresh(rc.owners, live.owners, (r6, l4) => l4 === r6)) return false;
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
function fieldFresh(recorded, live, ok) {
|
|
1125
|
+
if (recorded == null && live == null) return true;
|
|
1126
|
+
if (recorded == null || live == null) return false;
|
|
1127
|
+
return ok(recorded, live);
|
|
1128
|
+
}
|
|
1129
|
+
var EMPTY_CURSOR;
|
|
1130
|
+
var init_render_cursor = __esm({
|
|
1131
|
+
"src/lifecycle/render-cursor.ts"() {
|
|
1132
|
+
"use strict";
|
|
1133
|
+
init_adapter();
|
|
1134
|
+
init_manifest();
|
|
1135
|
+
EMPTY_CURSOR = { changelog: null, grants: null, owners: null };
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1000
1139
|
// src/db/sqlite.ts
|
|
1001
1140
|
import Database from "better-sqlite3";
|
|
1002
1141
|
var SQLiteAdapter;
|
|
@@ -3035,7 +3174,18 @@ var init_concurrency = __esm({
|
|
|
3035
3174
|
// src/render/engine.ts
|
|
3036
3175
|
import { join as join7, basename, isAbsolute, resolve as resolve3, sep } from "path";
|
|
3037
3176
|
import { mkdirSync as mkdirSync5, existsSync as existsSync7, copyFileSync as copyFileSync2 } from "fs";
|
|
3038
|
-
|
|
3177
|
+
function entityContentChanged(fresh, prior) {
|
|
3178
|
+
const freshKeys = Object.keys(fresh);
|
|
3179
|
+
const priorKeys = Object.keys(prior);
|
|
3180
|
+
if (freshKeys.length !== priorKeys.length) return true;
|
|
3181
|
+
for (const k6 of freshKeys) {
|
|
3182
|
+
const p3 = prior[k6];
|
|
3183
|
+
if (p3 == null) return true;
|
|
3184
|
+
if (p3.hash === "" || p3.hash !== fresh[k6]?.hash) return true;
|
|
3185
|
+
}
|
|
3186
|
+
return false;
|
|
3187
|
+
}
|
|
3188
|
+
var DeferredTableProgress, YIELD_EVERY_ENTITIES, RENDER_TABLE_CONCURRENCY, NOOP_RENDER, RenderEngine;
|
|
3039
3189
|
var init_engine = __esm({
|
|
3040
3190
|
"src/render/engine.ts"() {
|
|
3041
3191
|
"use strict";
|
|
@@ -3045,9 +3195,44 @@ var init_engine = __esm({
|
|
|
3045
3195
|
init_entity_query();
|
|
3046
3196
|
init_entity_templates();
|
|
3047
3197
|
init_manifest();
|
|
3198
|
+
init_render_cursor();
|
|
3048
3199
|
init_cleanup();
|
|
3049
3200
|
init_progress();
|
|
3050
3201
|
init_concurrency();
|
|
3202
|
+
DeferredTableProgress = class {
|
|
3203
|
+
constructor(throttle) {
|
|
3204
|
+
this.throttle = throttle;
|
|
3205
|
+
}
|
|
3206
|
+
changed = false;
|
|
3207
|
+
pendingStart = null;
|
|
3208
|
+
/** Buffer the `table-start` event; emitted only if/when the table changes. */
|
|
3209
|
+
start(event) {
|
|
3210
|
+
if (this.changed) {
|
|
3211
|
+
this.throttle.force(event);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
this.pendingStart = event;
|
|
3215
|
+
}
|
|
3216
|
+
/** Mark that an entity's content changed — flush the held `table-start` once. */
|
|
3217
|
+
markChanged() {
|
|
3218
|
+
if (this.changed) return;
|
|
3219
|
+
this.changed = true;
|
|
3220
|
+
if (this.pendingStart) {
|
|
3221
|
+
this.throttle.force(this.pendingStart);
|
|
3222
|
+
this.pendingStart = null;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
/** Coalesced per-entity progress — dropped entirely until the table changed. */
|
|
3226
|
+
tick(event) {
|
|
3227
|
+
if (!this.changed) return;
|
|
3228
|
+
this.throttle.tick(event);
|
|
3229
|
+
}
|
|
3230
|
+
/** Lifecycle event (`table-done`) — emitted only if the table changed. */
|
|
3231
|
+
force(event) {
|
|
3232
|
+
if (!this.changed) return;
|
|
3233
|
+
this.throttle.force(event);
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3051
3236
|
YIELD_EVERY_ENTITIES = 200;
|
|
3052
3237
|
RENDER_TABLE_CONCURRENCY = 4;
|
|
3053
3238
|
NOOP_RENDER = () => "";
|
|
@@ -3164,20 +3349,23 @@ var init_engine = __esm({
|
|
|
3164
3349
|
}
|
|
3165
3350
|
const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
|
|
3166
3351
|
const filePath = join7(outputDir, def.outputFile);
|
|
3167
|
-
|
|
3352
|
+
const wrote = atomicWrite(filePath, content);
|
|
3353
|
+
if (wrote) {
|
|
3168
3354
|
filesWritten.push(filePath);
|
|
3169
3355
|
} else {
|
|
3170
3356
|
counters.skipped++;
|
|
3171
3357
|
}
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3358
|
+
if (wrote) {
|
|
3359
|
+
throttle.force({
|
|
3360
|
+
kind: "table-done",
|
|
3361
|
+
table: name,
|
|
3362
|
+
entitiesRendered: rows.length,
|
|
3363
|
+
entitiesTotal: rows.length,
|
|
3364
|
+
tableIndex: 0,
|
|
3365
|
+
tableCount: 0,
|
|
3366
|
+
pct: 100
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3181
3369
|
}
|
|
3182
3370
|
for (const [name, def] of this._schema.getMultis()) {
|
|
3183
3371
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
@@ -3191,32 +3379,38 @@ var init_engine = __esm({
|
|
|
3191
3379
|
tables[t8] = await this._schema.queryTable(this._adapter, t8, this._readRel);
|
|
3192
3380
|
}
|
|
3193
3381
|
}
|
|
3382
|
+
let wroteAny = false;
|
|
3194
3383
|
for (const key of keys) {
|
|
3195
3384
|
const content = def.render(key, tables);
|
|
3196
3385
|
const filePath = join7(outputDir, def.outputFile(key));
|
|
3197
3386
|
if (atomicWrite(filePath, content)) {
|
|
3198
3387
|
filesWritten.push(filePath);
|
|
3388
|
+
wroteAny = true;
|
|
3199
3389
|
} else {
|
|
3200
3390
|
counters.skipped++;
|
|
3201
3391
|
}
|
|
3202
3392
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3393
|
+
if (wroteAny) {
|
|
3394
|
+
throttle.force({
|
|
3395
|
+
kind: "table-done",
|
|
3396
|
+
table: name,
|
|
3397
|
+
entitiesRendered: keys.length,
|
|
3398
|
+
entitiesTotal: keys.length,
|
|
3399
|
+
tableIndex: 0,
|
|
3400
|
+
tableCount: 0,
|
|
3401
|
+
pct: 100
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3212
3404
|
}
|
|
3405
|
+
const priorManifest = readManifest(outputDir);
|
|
3213
3406
|
const entityContextManifest = await this._renderEntityContexts(
|
|
3214
3407
|
outputDir,
|
|
3215
3408
|
filesWritten,
|
|
3216
3409
|
counters,
|
|
3217
3410
|
throttle,
|
|
3218
3411
|
signal,
|
|
3219
|
-
opts.changedTables
|
|
3412
|
+
opts.changedTables,
|
|
3413
|
+
priorManifest
|
|
3220
3414
|
);
|
|
3221
3415
|
if (entityContextManifest === null) {
|
|
3222
3416
|
return this._abortedResult(filesWritten, counters, start);
|
|
@@ -3227,10 +3421,13 @@ var init_engine = __esm({
|
|
|
3227
3421
|
const prev = readManifest(outputDir);
|
|
3228
3422
|
entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
|
|
3229
3423
|
}
|
|
3424
|
+
const cursor = await computeRenderCursor(this._adapter);
|
|
3230
3425
|
writeManifest(outputDir, {
|
|
3231
3426
|
version: 2,
|
|
3232
3427
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3233
|
-
entityContexts
|
|
3428
|
+
entityContexts,
|
|
3429
|
+
templateVersion: TEMPLATE_VERSION,
|
|
3430
|
+
cursor
|
|
3234
3431
|
});
|
|
3235
3432
|
}
|
|
3236
3433
|
const result = {
|
|
@@ -3296,7 +3493,7 @@ var init_engine = __esm({
|
|
|
3296
3493
|
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
3297
3494
|
* via `signal`.
|
|
3298
3495
|
*/
|
|
3299
|
-
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
|
|
3496
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables, priorManifest) {
|
|
3300
3497
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
3301
3498
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
3302
3499
|
if (d6.protected) protectedTables.add(t8);
|
|
@@ -3315,8 +3512,10 @@ var init_engine = __esm({
|
|
|
3315
3512
|
const baseRows = await this._schema.queryTable(this._adapter, table, this._readRel);
|
|
3316
3513
|
const allRows = this._foldRows ? await this._foldRows(table, baseRows) : baseRows;
|
|
3317
3514
|
const directoryRoot = def.directoryRoot ?? table;
|
|
3515
|
+
const deferred = new DeferredTableProgress(throttle);
|
|
3516
|
+
const priorEntities = priorManifest?.entityContexts[table]?.entities ?? {};
|
|
3318
3517
|
const entitiesTotal = allRows.length;
|
|
3319
|
-
|
|
3518
|
+
deferred.start({
|
|
3320
3519
|
kind: "table-start",
|
|
3321
3520
|
table,
|
|
3322
3521
|
entitiesRendered: 0,
|
|
@@ -3325,6 +3524,7 @@ var init_engine = __esm({
|
|
|
3325
3524
|
tableCount,
|
|
3326
3525
|
pct: 0
|
|
3327
3526
|
});
|
|
3527
|
+
if (Object.keys(priorEntities).length !== entitiesTotal) deferred.markChanged();
|
|
3328
3528
|
const manifestEntry = {
|
|
3329
3529
|
directoryRoot,
|
|
3330
3530
|
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
@@ -3440,8 +3640,10 @@ var init_engine = __esm({
|
|
|
3440
3640
|
}
|
|
3441
3641
|
}
|
|
3442
3642
|
manifestEntry.entities[slug] = entityFileHashes;
|
|
3643
|
+
const priorHashes = normalizeEntityFiles(priorEntities[slug] ?? {});
|
|
3644
|
+
if (entityContentChanged(entityFileHashes, priorHashes)) deferred.markChanged();
|
|
3443
3645
|
const entitiesRendered = i6 + 1;
|
|
3444
|
-
|
|
3646
|
+
deferred.tick({
|
|
3445
3647
|
kind: "table-progress",
|
|
3446
3648
|
table,
|
|
3447
3649
|
entitiesRendered,
|
|
@@ -3451,7 +3653,7 @@ var init_engine = __esm({
|
|
|
3451
3653
|
pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
|
|
3452
3654
|
});
|
|
3453
3655
|
}
|
|
3454
|
-
|
|
3656
|
+
deferred.force({
|
|
3455
3657
|
kind: "table-done",
|
|
3456
3658
|
table,
|
|
3457
3659
|
entitiesRendered: entitiesTotal,
|
|
@@ -5085,6 +5287,7 @@ var init_lattice = __esm({
|
|
|
5085
5287
|
init_shred();
|
|
5086
5288
|
init_encryption();
|
|
5087
5289
|
init_manifest();
|
|
5290
|
+
init_render_cursor();
|
|
5088
5291
|
init_adapter();
|
|
5089
5292
|
init_sqlite();
|
|
5090
5293
|
init_postgres();
|
|
@@ -5155,6 +5358,14 @@ var init_lattice = __esm({
|
|
|
5155
5358
|
_changelogTables = /* @__PURE__ */ new Set();
|
|
5156
5359
|
/** Current task context string for relevance filtering. */
|
|
5157
5360
|
_taskContext = "";
|
|
5361
|
+
/**
|
|
5362
|
+
* True when this connection opened against an already-provisioned cloud as a
|
|
5363
|
+
* SCOPED MEMBER (no role-management privilege → no CREATE/ALTER on the schema).
|
|
5364
|
+
* Set during init() by the same probe that decides introspect-only. Drives
|
|
5365
|
+
* {@link addColumn} to route DDL through the owner-side `lattice_member_add_column`
|
|
5366
|
+
* SECURITY DEFINER helper instead of issuing a raw ALTER the member can't run.
|
|
5367
|
+
*/
|
|
5368
|
+
_cloudMemberOpen = false;
|
|
5158
5369
|
_auditHandlers = [];
|
|
5159
5370
|
_renderHandlers = [];
|
|
5160
5371
|
_writebackHandlers = [];
|
|
@@ -5401,7 +5612,7 @@ var init_lattice = __esm({
|
|
|
5401
5612
|
/** Async tail of init(). See {@link init} for the sync-validation phase. */
|
|
5402
5613
|
async _initAsync(options) {
|
|
5403
5614
|
let introspectOnly = options.introspectOnly === true;
|
|
5404
|
-
if (
|
|
5615
|
+
if (this.getDialect() === "postgres") {
|
|
5405
5616
|
try {
|
|
5406
5617
|
const [marker, role] = await Promise.all([
|
|
5407
5618
|
getAsyncOrSync(this._adapter, `SELECT to_regclass('__lattice_owners') AS reg`),
|
|
@@ -5412,7 +5623,9 @@ var init_lattice = __esm({
|
|
|
5412
5623
|
]);
|
|
5413
5624
|
const provisioned = !!marker && marker.reg != null;
|
|
5414
5625
|
const canCreateRoles = !!role && role.rolcreaterole === true;
|
|
5415
|
-
|
|
5626
|
+
const memberOpen = provisioned && !canCreateRoles;
|
|
5627
|
+
introspectOnly = introspectOnly || memberOpen;
|
|
5628
|
+
this._cloudMemberOpen = memberOpen;
|
|
5416
5629
|
} catch {
|
|
5417
5630
|
}
|
|
5418
5631
|
}
|
|
@@ -5500,6 +5713,26 @@ var init_lattice = __esm({
|
|
|
5500
5713
|
getDialect() {
|
|
5501
5714
|
return this._adapter.dialect;
|
|
5502
5715
|
}
|
|
5716
|
+
/**
|
|
5717
|
+
* True when a table opts into the observation/changelog substrate
|
|
5718
|
+
* (`def.changelog`). Callers that want to bypass the high-level {@link delete}
|
|
5719
|
+
* with a transaction-scoped raw delete use this to know whether the table also
|
|
5720
|
+
* needs the changelog / write-hook / embedding side effects that only
|
|
5721
|
+
* `delete()` performs — so they can keep the high-level path for such tables.
|
|
5722
|
+
*/
|
|
5723
|
+
isChangelogTracked(table) {
|
|
5724
|
+
return this._changelogTables.has(table);
|
|
5725
|
+
}
|
|
5726
|
+
/**
|
|
5727
|
+
* True when this connection opened as a scoped cloud MEMBER (see
|
|
5728
|
+
* {@link _cloudMemberOpen}). Callers use it to route DDL-bearing work through
|
|
5729
|
+
* the owner-side SECURITY DEFINER helpers rather than issuing DDL the member's
|
|
5730
|
+
* role can't run (e.g. {@link addColumn} regenerates the masking view inside
|
|
5731
|
+
* `lattice_member_add_column`, so the caller must not also try to regenerate it).
|
|
5732
|
+
*/
|
|
5733
|
+
isCloudMemberOpen() {
|
|
5734
|
+
return this._cloudMemberOpen;
|
|
5735
|
+
}
|
|
5503
5736
|
/**
|
|
5504
5737
|
* Return the normalised primary-key column list for a registered
|
|
5505
5738
|
* table. Falls back to `['id']` for tables registered via raw DDL
|
|
@@ -5576,7 +5809,15 @@ var init_lattice = __esm({
|
|
|
5576
5809
|
assertSafeIdentifier(column, "column");
|
|
5577
5810
|
const existing = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5578
5811
|
if (!existing.includes(column)) {
|
|
5579
|
-
|
|
5812
|
+
if (this._cloudMemberOpen) {
|
|
5813
|
+
await runAsyncOrSync(this._adapter, `SELECT lattice_member_add_column(?, ?, ?)`, [
|
|
5814
|
+
table,
|
|
5815
|
+
column,
|
|
5816
|
+
typeSpec
|
|
5817
|
+
]);
|
|
5818
|
+
} else {
|
|
5819
|
+
await addColumnAsyncOrSync(this._adapter, table, column, typeSpec);
|
|
5820
|
+
}
|
|
5580
5821
|
}
|
|
5581
5822
|
const cols = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
5582
5823
|
this._columnCache.set(table, new Set(cols));
|
|
@@ -6508,12 +6749,39 @@ var init_lattice = __esm({
|
|
|
6508
6749
|
async renderInBackground(outputDir, opts = {}) {
|
|
6509
6750
|
const notInit = this._notInitError();
|
|
6510
6751
|
if (notInit) return notInit;
|
|
6752
|
+
if (opts.gateOnOpen && !opts.changedTables) {
|
|
6753
|
+
const start = Date.now();
|
|
6754
|
+
const recorded = readManifest(outputDir);
|
|
6755
|
+
if (recorded != null) {
|
|
6756
|
+
const live = await computeRenderCursor(this._adapter);
|
|
6757
|
+
if (cursorIsFresh(recorded, live)) {
|
|
6758
|
+
opts.onProgress?.({
|
|
6759
|
+
kind: "done",
|
|
6760
|
+
table: null,
|
|
6761
|
+
entitiesRendered: 0,
|
|
6762
|
+
entitiesTotal: 0,
|
|
6763
|
+
tableIndex: 0,
|
|
6764
|
+
tableCount: 0,
|
|
6765
|
+
pct: 100,
|
|
6766
|
+
durationMs: Date.now() - start
|
|
6767
|
+
});
|
|
6768
|
+
const skipped = {
|
|
6769
|
+
filesWritten: [],
|
|
6770
|
+
filesSkipped: 0,
|
|
6771
|
+
durationMs: Date.now() - start
|
|
6772
|
+
};
|
|
6773
|
+
for (const h6 of this._renderHandlers) h6(skipped);
|
|
6774
|
+
return skipped;
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6511
6778
|
if (!opts.changedTables) {
|
|
6512
6779
|
this._pendingRenderAll = false;
|
|
6513
6780
|
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
6514
6781
|
this._autoRenderPending = false;
|
|
6515
6782
|
}
|
|
6516
|
-
|
|
6783
|
+
const { gateOnOpen: _gateOnOpen, ...engineOpts } = opts;
|
|
6784
|
+
return this._renderGuarded(outputDir, engineOpts);
|
|
6517
6785
|
}
|
|
6518
6786
|
/**
|
|
6519
6787
|
* Install a per-viewer read-relation resolver for ALL renders (initial,
|
|
@@ -8623,6 +8891,111 @@ LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
|
8623
8891
|
AND g."pk" = ANY(p_pks)
|
|
8624
8892
|
AND o."owner_role" = session_user;
|
|
8625
8893
|
$fn$;
|
|
8894
|
+
|
|
8895
|
+
-- Add a column to a user table AS THE OWNER, on behalf of a scoped member. A
|
|
8896
|
+
-- member's role has no CREATE/ALTER on the schema (the bootstrap REVOKEs CREATE
|
|
8897
|
+
-- from PUBLIC), so a member's GUI "add a field" write (createRow/updateRow with a
|
|
8898
|
+
-- field the table lacks) cannot run ALTER TABLE itself. This SECURITY DEFINER
|
|
8899
|
+
-- helper performs that ALTER \u2014 and the masking-view regen \u2014 with the owner's
|
|
8900
|
+
-- rights, so member-added columns behave identically to owner-added ones.
|
|
8901
|
+
--
|
|
8902
|
+
-- Injection-safe + minimal: p_table must be an existing BASE table in the current
|
|
8903
|
+
-- schema (rejected otherwise); p_type is whitelisted against the exact set the
|
|
8904
|
+
-- library's addColumn emits for an auto-added column (TEXT / INTEGER / REAL, plus
|
|
8905
|
+
-- BOOLEAN) \u2014 never interpolated raw; both identifiers go through %I (quote_ident).
|
|
8906
|
+
-- Member-callable (granted EXECUTE to the member group), but it can only widen the
|
|
8907
|
+
-- schema, never read or alter another member's data.
|
|
8908
|
+
CREATE OR REPLACE FUNCTION lattice_member_add_column(p_table text, p_column text, p_type text)
|
|
8909
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
8910
|
+
DECLARE
|
|
8911
|
+
v_type text;
|
|
8912
|
+
v_view text := p_table || '_v';
|
|
8913
|
+
v_has_view boolean;
|
|
8914
|
+
v_pk_expr text;
|
|
8915
|
+
v_select text;
|
|
8916
|
+
BEGIN
|
|
8917
|
+
-- Never alter internal bookkeeping tables (names start with "_"). The GUI only
|
|
8918
|
+
-- ever calls this for a user entity table; rejecting the rest is defense-in-depth
|
|
8919
|
+
-- against a member invoking the function directly against ownership/audit/policy
|
|
8920
|
+
-- tables.
|
|
8921
|
+
IF left(p_table, 1) = '_' THEN
|
|
8922
|
+
RAISE EXCEPTION 'lattice: cannot add a column to internal table "%"', p_table;
|
|
8923
|
+
END IF;
|
|
8924
|
+
|
|
8925
|
+
-- p_table must be a real base table in THIS schema (search_path is pinned to the
|
|
8926
|
+
-- cloud schema by pinDefinerSearchPath, so to_regclass resolves there).
|
|
8927
|
+
IF NOT EXISTS (
|
|
8928
|
+
SELECT 1 FROM pg_class c
|
|
8929
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
8930
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND c.relkind = 'r'
|
|
8931
|
+
) THEN
|
|
8932
|
+
RAISE EXCEPTION 'lattice: no such table "%"', p_table;
|
|
8933
|
+
END IF;
|
|
8934
|
+
|
|
8935
|
+
-- Whitelist the column type. These are exactly the specs addColumn's
|
|
8936
|
+
-- inferColumnType produces (TEXT / INTEGER / REAL); BOOLEAN is allowed too.
|
|
8937
|
+
-- Anything else is rejected \u2014 the type is spliced as %s (NOT %I), so it must be
|
|
8938
|
+
-- a known-safe literal and never caller-controlled SQL.
|
|
8939
|
+
v_type := upper(btrim(p_type));
|
|
8940
|
+
IF v_type NOT IN ('TEXT', 'INTEGER', 'REAL', 'BOOLEAN') THEN
|
|
8941
|
+
RAISE EXCEPTION 'lattice: unsupported column type "%"', p_type;
|
|
8942
|
+
END IF;
|
|
8943
|
+
|
|
8944
|
+
EXECUTE format('ALTER TABLE %I ADD COLUMN IF NOT EXISTS %I %s', p_table, p_column, v_type);
|
|
8945
|
+
|
|
8946
|
+
-- If the table is cell-masked (a "<table>_v" view exists, because some column has
|
|
8947
|
+
-- an audience), the view selects an explicit column list \u2014 so a new column is
|
|
8948
|
+
-- invisible to members until the view is regenerated. Rebuild it the same way the
|
|
8949
|
+
-- owner path (audienceViewSql / regenerateAudienceViewFromDb) does: pass every
|
|
8950
|
+
-- column through except those with an 'owner' audience in __lattice_column_policy
|
|
8951
|
+
-- (CASE WHEN lattice_is_owner(...) THEN col END), re-apply row visibility with
|
|
8952
|
+
-- WHERE lattice_row_visible(table, pk), and keep the member SELECT grant on the
|
|
8953
|
+
-- view. Unmasked tables need no regen \u2014 the member group's table-level base grant
|
|
8954
|
+
-- already covers the new column.
|
|
8955
|
+
SELECT EXISTS (
|
|
8956
|
+
SELECT 1 FROM pg_class c
|
|
8957
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
8958
|
+
WHERE n.nspname = current_schema() AND c.relname = v_view AND c.relkind = 'v'
|
|
8959
|
+
) INTO v_has_view;
|
|
8960
|
+
|
|
8961
|
+
IF v_has_view THEN
|
|
8962
|
+
-- Canonical pk expression: CAST("col" AS TEXT) joined by TAB (chr(9)) \u2014 the
|
|
8963
|
+
-- same serialization the RLS policies + audienceViewSql use.
|
|
8964
|
+
SELECT string_agg(format('CAST(%I AS TEXT)', a.attname), ' || chr(9) || '
|
|
8965
|
+
ORDER BY array_position(i.indkey, a.attnum))
|
|
8966
|
+
INTO v_pk_expr
|
|
8967
|
+
FROM pg_index i
|
|
8968
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
8969
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
8970
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
|
|
8971
|
+
WHERE n.nspname = current_schema() AND c.relname = p_table AND i.indisprimary;
|
|
8972
|
+
IF v_pk_expr IS NULL THEN
|
|
8973
|
+
RAISE EXCEPTION 'lattice: cannot regenerate mask view for "%": no primary key', p_table;
|
|
8974
|
+
END IF;
|
|
8975
|
+
|
|
8976
|
+
-- Build the masked SELECT list in column order, applying the per-column policy.
|
|
8977
|
+
SELECT string_agg(
|
|
8978
|
+
CASE
|
|
8979
|
+
WHEN cp."audience" = 'owner'
|
|
8980
|
+
THEN format('CASE WHEN lattice_is_owner(%L, %s) THEN %I END AS %I',
|
|
8981
|
+
p_table, v_pk_expr, cols.column_name, cols.column_name)
|
|
8982
|
+
ELSE format('%I', cols.column_name)
|
|
8983
|
+
END,
|
|
8984
|
+
', ' ORDER BY cols.ordinal_position)
|
|
8985
|
+
INTO v_select
|
|
8986
|
+
FROM information_schema.columns cols
|
|
8987
|
+
LEFT JOIN "__lattice_column_policy" cp
|
|
8988
|
+
ON cp."table_name" = p_table AND cp."column_name" = cols.column_name
|
|
8989
|
+
AND cp."audience" NOT IN ('', 'everyone', 'row-audience')
|
|
8990
|
+
WHERE cols.table_schema = current_schema() AND cols.table_name = p_table;
|
|
8991
|
+
|
|
8992
|
+
EXECUTE format(
|
|
8993
|
+
'CREATE OR REPLACE VIEW %I AS SELECT %s FROM %I WHERE lattice_row_visible(%L, %s)',
|
|
8994
|
+
v_view, v_select, p_table, p_table, v_pk_expr);
|
|
8995
|
+
EXECUTE format('GRANT SELECT ON %I TO ${MEMBER_GROUP}', v_view);
|
|
8996
|
+
END IF;
|
|
8997
|
+
END $fn$;
|
|
8998
|
+
GRANT EXECUTE ON FUNCTION lattice_member_add_column(text, text, text) TO ${MEMBER_GROUP};
|
|
8626
8999
|
`;
|
|
8627
9000
|
}
|
|
8628
9001
|
});
|
|
@@ -8796,18 +9169,9 @@ function sessionUndoneFilters(undone, sessionId) {
|
|
|
8796
9169
|
if (sessionId) filters.push({ col: "session_id", op: "eq", val: sessionId });
|
|
8797
9170
|
return filters;
|
|
8798
9171
|
}
|
|
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", {
|
|
9172
|
+
function buildAuditRow(table, rowId, op, before, after, sessionId, editTs) {
|
|
9173
|
+
return {
|
|
8805
9174
|
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
9175
|
ts: sanitizeEditTs(editTs) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8812
9176
|
table_name: table,
|
|
8813
9177
|
row_id: rowId,
|
|
@@ -8816,7 +9180,9 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
8816
9180
|
after_json: after ? JSON.stringify(after) : null,
|
|
8817
9181
|
undone: 0,
|
|
8818
9182
|
session_id: sessionId ?? null
|
|
8819
|
-
}
|
|
9183
|
+
};
|
|
9184
|
+
}
|
|
9185
|
+
function publishMutationFeed(feed, table, rowId, op, before, after, source) {
|
|
8820
9186
|
const labelRow = op === "delete" ? before : after;
|
|
8821
9187
|
feed.publish({
|
|
8822
9188
|
table,
|
|
@@ -8826,17 +9192,28 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
8826
9192
|
summary: feedSummary(op, table, labelRow)
|
|
8827
9193
|
});
|
|
8828
9194
|
}
|
|
8829
|
-
function
|
|
8830
|
-
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
8831
|
-
}
|
|
8832
|
-
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
9195
|
+
async function purgeRedoStack(db, sessionId) {
|
|
8833
9196
|
const undone = await db.query("_lattice_gui_audit", {
|
|
8834
9197
|
filters: sessionUndoneFilters(1, sessionId)
|
|
8835
9198
|
});
|
|
8836
9199
|
for (const r6 of undone) await db.delete("_lattice_gui_audit", r6.id);
|
|
9200
|
+
}
|
|
9201
|
+
async function appendAudit(db, feed, table, rowId, op, before, after, source = "gui", sessionId, editTs) {
|
|
9202
|
+
await purgeRedoStack(db, sessionId);
|
|
9203
|
+
await db.insert(
|
|
9204
|
+
"_lattice_gui_audit",
|
|
9205
|
+
buildAuditRow(table, rowId, op, before, after, sessionId, editTs)
|
|
9206
|
+
);
|
|
9207
|
+
publishMutationFeed(feed, table, rowId, op, before, after, source);
|
|
9208
|
+
}
|
|
9209
|
+
function isSchemaOp(operation2) {
|
|
9210
|
+
return operation2.startsWith(SCHEMA_OP_PREFIX);
|
|
9211
|
+
}
|
|
9212
|
+
async function recordSchemaAudit(db, feed, table, operation2, before, after, summary, source = "gui", sessionId) {
|
|
9213
|
+
await purgeRedoStack(db, sessionId);
|
|
8837
9214
|
await db.insert("_lattice_gui_audit", {
|
|
8838
9215
|
id: crypto.randomUUID(),
|
|
8839
|
-
// Explicit ISO ts — see
|
|
9216
|
+
// Explicit ISO ts — see buildAuditRow (the SQLite-only strftime DEFAULT
|
|
8840
9217
|
// rendered "Invalid Date" on the Postgres/cloud path).
|
|
8841
9218
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8842
9219
|
table_name: table,
|
|
@@ -8871,7 +9248,7 @@ async function ensureColumns(db, table, values) {
|
|
|
8871
9248
|
const added = Object.keys(values).filter((k6) => !(k6 in existing));
|
|
8872
9249
|
if (added.length === 0) return [];
|
|
8873
9250
|
for (const col of added) await db.addColumn(table, col, inferColumnType(values[col]));
|
|
8874
|
-
if (db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
9251
|
+
if (!db.isCloudMemberOpen() && db.getDialect() === "postgres" && await cloudRlsInstalled(db)) {
|
|
8875
9252
|
const cols = db.getRegisteredColumns(table);
|
|
8876
9253
|
const pk = db.getPrimaryKey(table);
|
|
8877
9254
|
if (cols && pk.length > 0) await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
@@ -8993,7 +9370,14 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
8993
9370
|
ctx.clientTs
|
|
8994
9371
|
);
|
|
8995
9372
|
} else {
|
|
8996
|
-
await ctx
|
|
9373
|
+
await hardDelete(ctx, table, id, before);
|
|
9374
|
+
}
|
|
9375
|
+
}
|
|
9376
|
+
async function hardDelete(ctx, table, id, before) {
|
|
9377
|
+
const withClient = ctx.db.adapter.withClient?.bind(ctx.db.adapter);
|
|
9378
|
+
const pkCols = ctx.db.getPrimaryKey(table);
|
|
9379
|
+
const pkCol = pkCols.length === 1 ? pkCols[0] : void 0;
|
|
9380
|
+
if (!withClient || ctx.db.isChangelogTracked(table) || pkCol === void 0) {
|
|
8997
9381
|
await appendAudit(
|
|
8998
9382
|
ctx.db,
|
|
8999
9383
|
ctx.feed,
|
|
@@ -9006,10 +9390,30 @@ async function deleteRow(ctx, table, id, hard) {
|
|
|
9006
9390
|
ctx.sessionId,
|
|
9007
9391
|
ctx.clientTs
|
|
9008
9392
|
);
|
|
9393
|
+
await ctx.db.delete(table, id);
|
|
9394
|
+
return;
|
|
9009
9395
|
}
|
|
9396
|
+
const auditRow = buildAuditRow(table, id, "delete", before, null, ctx.sessionId, ctx.clientTs);
|
|
9397
|
+
await purgeRedoStack(ctx.db, ctx.sessionId);
|
|
9398
|
+
const auditCols = AUDIT_COLUMNS.map((c6) => `"${c6}"`).join(", ");
|
|
9399
|
+
const auditPlaceholders = AUDIT_COLUMNS.map(() => "?").join(", ");
|
|
9400
|
+
const auditValues = AUDIT_COLUMNS.map((c6) => auditRow[c6]);
|
|
9401
|
+
const pkColQuoted = pkCol.replace(/"/g, '""');
|
|
9402
|
+
await withClient(async (tx) => {
|
|
9403
|
+
await tx.run(
|
|
9404
|
+
`INSERT INTO "_lattice_gui_audit" (${auditCols}) VALUES (${auditPlaceholders})`,
|
|
9405
|
+
auditValues
|
|
9406
|
+
);
|
|
9407
|
+
await tx.run(`DELETE FROM "${table.replace(/"/g, '""')}" WHERE "${pkColQuoted}" = ?`, [id]);
|
|
9408
|
+
});
|
|
9409
|
+
publishMutationFeed(ctx.feed, table, id, "delete", before, null, ctx.source);
|
|
9010
9410
|
}
|
|
9011
|
-
async function linkRows(ctx, table, body) {
|
|
9012
|
-
|
|
9411
|
+
async function linkRows(ctx, table, body, forceVisibility) {
|
|
9412
|
+
if (forceVisibility !== void 0) {
|
|
9413
|
+
await ctx.db.insertForcingVisibility(table, body, forceVisibility);
|
|
9414
|
+
} else {
|
|
9415
|
+
await ctx.db.link(table, body);
|
|
9416
|
+
}
|
|
9013
9417
|
await appendAudit(ctx.db, ctx.feed, table, null, "link", null, body, ctx.source, ctx.sessionId);
|
|
9014
9418
|
}
|
|
9015
9419
|
async function unlinkRows(ctx, table, body) {
|
|
@@ -9147,12 +9551,23 @@ async function revertEntry(ctx, id) {
|
|
|
9147
9551
|
});
|
|
9148
9552
|
return { ok: true, entry };
|
|
9149
9553
|
}
|
|
9150
|
-
var SCHEMA_OP_PREFIX;
|
|
9554
|
+
var AUDIT_COLUMNS, SCHEMA_OP_PREFIX;
|
|
9151
9555
|
var init_mutations = __esm({
|
|
9152
9556
|
"src/gui/mutations.ts"() {
|
|
9153
9557
|
"use strict";
|
|
9154
9558
|
init_cloud_connect();
|
|
9155
9559
|
init_audience();
|
|
9560
|
+
AUDIT_COLUMNS = [
|
|
9561
|
+
"id",
|
|
9562
|
+
"ts",
|
|
9563
|
+
"table_name",
|
|
9564
|
+
"row_id",
|
|
9565
|
+
"operation",
|
|
9566
|
+
"before_json",
|
|
9567
|
+
"after_json",
|
|
9568
|
+
"undone",
|
|
9569
|
+
"session_id"
|
|
9570
|
+
];
|
|
9156
9571
|
SCHEMA_OP_PREFIX = "schema.";
|
|
9157
9572
|
}
|
|
9158
9573
|
});
|
|
@@ -9439,6 +9854,10 @@ async function readMachineCredential(db, kind) {
|
|
|
9439
9854
|
}
|
|
9440
9855
|
return null;
|
|
9441
9856
|
}
|
|
9857
|
+
async function resolveAnthropicKey(db) {
|
|
9858
|
+
if (isAssistantCredentialCleared(CREDENTIALS.anthropic.kind)) return null;
|
|
9859
|
+
return await readMachineCredential(db, CREDENTIALS.anthropic.kind) ?? process.env.ANTHROPIC_API_KEY ?? null;
|
|
9860
|
+
}
|
|
9442
9861
|
function getAggressiveness() {
|
|
9443
9862
|
const n3 = readPreferences().aggressiveness;
|
|
9444
9863
|
if (!Number.isFinite(n3)) return DEFAULT_AGGRESSIVENESS;
|
|
@@ -9469,6 +9888,7 @@ async function getVoiceCredential(db) {
|
|
|
9469
9888
|
return null;
|
|
9470
9889
|
}
|
|
9471
9890
|
async function hasCredential(db, name, envVar) {
|
|
9891
|
+
if (isAssistantCredentialCleared(CREDENTIALS[name].kind)) return false;
|
|
9472
9892
|
return Boolean(await readMachineCredential(db, CREDENTIALS[name].kind)) || Boolean(process.env[envVar]);
|
|
9473
9893
|
}
|
|
9474
9894
|
async function resolveClaudeAuth(db) {
|
|
@@ -9491,7 +9911,7 @@ async function resolveClaudeAuth(db) {
|
|
|
9491
9911
|
} catch {
|
|
9492
9912
|
}
|
|
9493
9913
|
}
|
|
9494
|
-
const apiKey = await
|
|
9914
|
+
const apiKey = await resolveAnthropicKey(db);
|
|
9495
9915
|
return apiKey ? { apiKey } : null;
|
|
9496
9916
|
}
|
|
9497
9917
|
async function hasClaudeAuth(db) {
|
|
@@ -9588,6 +10008,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
9588
10008
|
}
|
|
9589
10009
|
const cred = CREDENTIALS[name];
|
|
9590
10010
|
setAssistantCredential(cred.kind, key);
|
|
10011
|
+
clearAssistantCredentialCleared(cred.kind);
|
|
9591
10012
|
if (db) {
|
|
9592
10013
|
for (const row of await liveSecretsOfKind(db, cred.kind)) {
|
|
9593
10014
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -9604,6 +10025,7 @@ async function dispatchAssistantRoute(req, res, ctx) {
|
|
|
9604
10025
|
return true;
|
|
9605
10026
|
}
|
|
9606
10027
|
deleteAssistantCredential(CREDENTIALS[name].kind);
|
|
10028
|
+
setAssistantCredentialCleared(CREDENTIALS[name].kind);
|
|
9607
10029
|
if (db) {
|
|
9608
10030
|
for (const row of await liveSecretsOfKind(db, CREDENTIALS[name].kind)) {
|
|
9609
10031
|
await db.update("secrets", row.id, { deleted_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -10810,6 +11232,11 @@ async function revokeRow(db, table, pk, grantee) {
|
|
|
10810
11232
|
assertPg(db);
|
|
10811
11233
|
await runAsyncOrSync(db.adapter, `SELECT lattice_revoke_row(?, ?, ?)`, [table, pk, grantee]);
|
|
10812
11234
|
}
|
|
11235
|
+
async function batchRowGrants(db, table, pk, grant, revoke) {
|
|
11236
|
+
assertPg(db);
|
|
11237
|
+
for (const grantee of grant) await grantRow(db, table, pk, grantee);
|
|
11238
|
+
for (const grantee of revoke) await revokeRow(db, table, pk, grantee);
|
|
11239
|
+
}
|
|
10813
11240
|
async function revokeMemberRole(db, role) {
|
|
10814
11241
|
assertPg(db);
|
|
10815
11242
|
if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
|
|
@@ -12053,7 +12480,7 @@ function buildSchema(db) {
|
|
|
12053
12480
|
}
|
|
12054
12481
|
return out;
|
|
12055
12482
|
}
|
|
12056
|
-
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false) {
|
|
12483
|
+
async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptions, createJunction, aggressiveness = DEFAULT_AGGRESSIVENESS, createEntity, untrusted = false, privateMode = false) {
|
|
12057
12484
|
if (!text.trim()) return [];
|
|
12058
12485
|
const auth = await resolveClaudeAuth(db);
|
|
12059
12486
|
if (!auth) {
|
|
@@ -12075,6 +12502,7 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
12075
12502
|
});
|
|
12076
12503
|
return [];
|
|
12077
12504
|
}
|
|
12505
|
+
const forceVis = privateMode ? "private" : void 0;
|
|
12078
12506
|
const temperature = aggressivenessToTemperature(aggressiveness);
|
|
12079
12507
|
let description = "";
|
|
12080
12508
|
try {
|
|
@@ -12117,11 +12545,16 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
12117
12545
|
}
|
|
12118
12546
|
if (jx) {
|
|
12119
12547
|
try {
|
|
12120
|
-
await linkRows(
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12548
|
+
await linkRows(
|
|
12549
|
+
mctx,
|
|
12550
|
+
jx.junction,
|
|
12551
|
+
{
|
|
12552
|
+
id: crypto.randomUUID(),
|
|
12553
|
+
[jx.fileFk]: fileId,
|
|
12554
|
+
[jx.otherFk]: m4.id
|
|
12555
|
+
},
|
|
12556
|
+
forceVis
|
|
12557
|
+
);
|
|
12125
12558
|
linkedCount++;
|
|
12126
12559
|
if (created) {
|
|
12127
12560
|
mctx.feed.publish({
|
|
@@ -12180,16 +12613,21 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
12180
12613
|
if ("name" in cols && row.name == null) row.name = obj2.label;
|
|
12181
12614
|
if ("title" in cols && row.title == null) row.title = obj2.label;
|
|
12182
12615
|
try {
|
|
12183
|
-
const { id: rowId } = await createRow(mctx, entity, row);
|
|
12616
|
+
const { id: rowId } = await createRow(mctx, entity, row, forceVis);
|
|
12184
12617
|
createdCount++;
|
|
12185
12618
|
const ent = entity;
|
|
12186
12619
|
const jx = junctions.find((j6) => j6.otherTable === ent) ?? (createJunction ? await createJunction(ent) : null);
|
|
12187
12620
|
if (jx) {
|
|
12188
|
-
await linkRows(
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12621
|
+
await linkRows(
|
|
12622
|
+
mctx,
|
|
12623
|
+
jx.junction,
|
|
12624
|
+
{
|
|
12625
|
+
id: crypto.randomUUID(),
|
|
12626
|
+
[jx.fileFk]: fileId,
|
|
12627
|
+
[jx.otherFk]: rowId
|
|
12628
|
+
},
|
|
12629
|
+
forceVis
|
|
12630
|
+
);
|
|
12193
12631
|
}
|
|
12194
12632
|
} catch (e6) {
|
|
12195
12633
|
console.warn(`[ingest] create ${entity} from document failed:`, e6.message);
|
|
@@ -12203,12 +12641,17 @@ async function enrichWithLlm(mctx, db, fileId, text, name, junctions, descriptio
|
|
|
12203
12641
|
try {
|
|
12204
12642
|
const title = name.replace(/\.[^./\\]+$/, "").trim() || "Note";
|
|
12205
12643
|
const body = description.length > 0 ? description : text.slice(0, 2e3);
|
|
12206
|
-
const { id: noteId } = await createRow(
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12644
|
+
const { id: noteId } = await createRow(
|
|
12645
|
+
mctx,
|
|
12646
|
+
"notes",
|
|
12647
|
+
{
|
|
12648
|
+
id: crypto.randomUUID(),
|
|
12649
|
+
title,
|
|
12650
|
+
body,
|
|
12651
|
+
source_file_id: fileId
|
|
12652
|
+
},
|
|
12653
|
+
forceVis
|
|
12654
|
+
);
|
|
12212
12655
|
mctx.feed.publish({
|
|
12213
12656
|
table: "notes",
|
|
12214
12657
|
op: "insert",
|
|
@@ -12694,7 +13137,8 @@ async function ingestUrlAsFile(ctx, rawUrl, opts = {}) {
|
|
|
12694
13137
|
ctx.enrich.createJunction,
|
|
12695
13138
|
ctx.enrich.aggressiveness,
|
|
12696
13139
|
ctx.enrich.createEntity,
|
|
12697
|
-
true
|
|
13140
|
+
true,
|
|
13141
|
+
ctx.privateMode === true
|
|
12698
13142
|
);
|
|
12699
13143
|
}
|
|
12700
13144
|
return {
|
|
@@ -13573,13 +14017,22 @@ function loadSdk() {
|
|
|
13573
14017
|
throw new Error("Could not resolve the Anthropic constructor from '@anthropic-ai/sdk'");
|
|
13574
14018
|
return ctor;
|
|
13575
14019
|
}
|
|
13576
|
-
function
|
|
13577
|
-
const Anthropic = loadSdk();
|
|
14020
|
+
function buildAnthropicConfig(auth) {
|
|
13578
14021
|
const config = {};
|
|
13579
|
-
if (auth.authToken)
|
|
13580
|
-
|
|
14022
|
+
if (auth.authToken) {
|
|
14023
|
+
config.authToken = auth.authToken;
|
|
14024
|
+
config.apiKey = null;
|
|
14025
|
+
} else if (auth.apiKey) {
|
|
14026
|
+
config.apiKey = auth.apiKey;
|
|
14027
|
+
} else {
|
|
14028
|
+
config.apiKey = null;
|
|
14029
|
+
}
|
|
13581
14030
|
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
13582
|
-
|
|
14031
|
+
return config;
|
|
14032
|
+
}
|
|
14033
|
+
function createAnthropicClient(auth) {
|
|
14034
|
+
const Anthropic = loadSdk();
|
|
14035
|
+
const sdk = new Anthropic(buildAnthropicConfig(auth));
|
|
13583
14036
|
return {
|
|
13584
14037
|
async runTurn(params) {
|
|
13585
14038
|
const stream = sdk.messages.stream({
|
|
@@ -53145,7 +53598,7 @@ async function checkForUpdate(pkgName, currentVersion, opts = {}) {
|
|
|
53145
53598
|
// src/update-context.ts
|
|
53146
53599
|
init_user_config();
|
|
53147
53600
|
import { execFileSync } from "child_process";
|
|
53148
|
-
import { existsSync as existsSync14, lstatSync, readFileSync as readFileSync10 } from "fs";
|
|
53601
|
+
import { existsSync as existsSync14, lstatSync, readFileSync as readFileSync10, realpathSync } from "fs";
|
|
53149
53602
|
import { dirname as dirname7, join as join13, sep as sep2 } from "path";
|
|
53150
53603
|
var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
53151
53604
|
function isValidVersion(v2) {
|
|
@@ -53175,10 +53628,19 @@ function isUnderGlobalPrefix(packageRoot, execPath) {
|
|
|
53175
53628
|
}
|
|
53176
53629
|
function detectInstallContext(opts = {}) {
|
|
53177
53630
|
const pkgName = opts.pkgName ?? "latticesql";
|
|
53178
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
53179
53631
|
const env2 = opts.env ?? process.env;
|
|
53180
53632
|
const execPath = opts.execPath ?? process.execPath;
|
|
53181
|
-
const
|
|
53633
|
+
const rawCwd = opts.cwd ?? process.cwd();
|
|
53634
|
+
const rawModulePath = opts.modulePath ?? process.argv[1] ?? rawCwd;
|
|
53635
|
+
const resolveReal = (p3) => {
|
|
53636
|
+
try {
|
|
53637
|
+
return realpathSync(p3);
|
|
53638
|
+
} catch {
|
|
53639
|
+
return p3;
|
|
53640
|
+
}
|
|
53641
|
+
};
|
|
53642
|
+
const modulePath = resolveReal(rawModulePath);
|
|
53643
|
+
const cwd = resolveReal(rawCwd);
|
|
53182
53644
|
const packageRoot = findPackageRoot(dirname7(modulePath), pkgName);
|
|
53183
53645
|
if (packageRoot && existsSync14(join13(packageRoot, ".git"))) {
|
|
53184
53646
|
return {
|
|
@@ -54179,6 +54641,8 @@ var css = `
|
|
|
54179
54641
|
.grants-panel .grants-title { font-weight: 600; margin-bottom: 6px; }
|
|
54180
54642
|
.grants-panel .grants-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
|
|
54181
54643
|
.grants-panel .grants-row input { accent-color: var(--accent); }
|
|
54644
|
+
.grants-panel .grants-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
|
|
54645
|
+
.grants-panel .grants-dirty { font-size: 12px; }
|
|
54182
54646
|
|
|
54183
54647
|
/* Inline create-row at the bottom of every table */
|
|
54184
54648
|
tr.create-row td { background: var(--surface-2); }
|
|
@@ -56645,6 +57109,15 @@ var appJs = `
|
|
|
56645
57109
|
// Per-table view state: 'live' (default) or 'trash' (soft-deleted rows).
|
|
56646
57110
|
var tableViewMode = {};
|
|
56647
57111
|
|
|
57112
|
+
// The (table, pk) of the per-row "Manage access" grants panel that is
|
|
57113
|
+
// currently open, or null when none is. A soft re-render (a concurrent edit
|
|
57114
|
+
// by another client fires pg_notify \u2192 realtime refresh \u2192 renderRoute({soft})
|
|
57115
|
+
// \u2192 renderDetail/renderFsItem repaint) would otherwise re-create the detail
|
|
57116
|
+
// view with the panel collapsed, dropping a staged multi-select mid-edit.
|
|
57117
|
+
// wireRowSharing reads this after each repaint and re-opens + re-populates the
|
|
57118
|
+
// panel WITHOUT any network call, so the staged selection survives.
|
|
57119
|
+
var openGrantsPanel = null;
|
|
57120
|
+
|
|
56648
57121
|
function renderTable(content, tableName) {
|
|
56649
57122
|
var myGen = renderGen;
|
|
56650
57123
|
clearUnseen(tableName);
|
|
@@ -57123,70 +57596,151 @@ var appJs = `
|
|
|
57123
57596
|
}).catch(function (e) { showToast('Visibility update failed: ' + e.message, {}); });
|
|
57124
57597
|
});
|
|
57125
57598
|
});
|
|
57126
|
-
var
|
|
57127
|
-
|
|
57599
|
+
var access = row._access || {};
|
|
57600
|
+
|
|
57601
|
+
// Render the staged member checklist + a single "Save sharing" / "Cancel"
|
|
57602
|
+
// into the panel. Checkbox toggles mutate ONLY the local desired map \u2014
|
|
57603
|
+
// NO network call per toggle (the old design auto-saved live, one POST per
|
|
57604
|
+
// checkbox, and each grant's pg_notify collapsed the panel). A single batch
|
|
57605
|
+
// request fires on Save. members is the already-fetched list; desired
|
|
57606
|
+
// seeds from the row's current grantees (or a caller-supplied staged map
|
|
57607
|
+
// when re-opening after a soft re-render).
|
|
57608
|
+
function populateGrantsPanel(panel, members, desired) {
|
|
57609
|
+
// Snapshot the CURRENT (committed) grantees so Save can diff desired-vs-
|
|
57610
|
+
// current into adds/removes. effectiveVisibility decides whether we're
|
|
57611
|
+
// actually switching INTO specific-people mode (custom-0 reads as private).
|
|
57612
|
+
var current = {};
|
|
57613
|
+
(access.grantees || []).forEach(function (g) { current[g] = true; });
|
|
57614
|
+
if (members.length === 0) {
|
|
57615
|
+
panel.innerHTML = '<div class="muted">No other members in this workspace yet.</div>';
|
|
57616
|
+
panel.hidden = false;
|
|
57617
|
+
return;
|
|
57618
|
+
}
|
|
57619
|
+
function dirtyCount() {
|
|
57620
|
+
var n = 0;
|
|
57621
|
+
members.forEach(function (m) {
|
|
57622
|
+
if (!!desired[m.role] !== !!current[m.role]) n++;
|
|
57623
|
+
});
|
|
57624
|
+
return n;
|
|
57625
|
+
}
|
|
57626
|
+
function render() {
|
|
57627
|
+
var changed = dirtyCount();
|
|
57628
|
+
panel.innerHTML = '<div class="grants-title">Who can see this</div>' +
|
|
57629
|
+
members.map(function (m) {
|
|
57630
|
+
var label = m.name || m.email || m.role;
|
|
57631
|
+
return '<label class="grants-row"><input type="checkbox" data-grant-role="' + escapeHtml(m.role) + '"' +
|
|
57632
|
+
(desired[m.role] ? ' checked' : '') + '> ' + escapeHtml(label) + '</label>';
|
|
57633
|
+
}).join('') +
|
|
57634
|
+
'<div class="grants-actions">' +
|
|
57635
|
+
'<button class="btn primary" id="grants-save"' + (changed ? '' : ' disabled') + '>Save sharing</button>' +
|
|
57636
|
+
'<button class="btn" id="grants-cancel">Cancel</button>' +
|
|
57637
|
+
'<span class="grants-dirty muted">' + (changed ? (changed === 1 ? '1 change' : changed + ' changes') : 'No changes') + '</span>' +
|
|
57638
|
+
'</div>';
|
|
57639
|
+
panel.querySelectorAll('[data-grant-role]').forEach(function (cb) {
|
|
57640
|
+
cb.addEventListener('change', function () {
|
|
57641
|
+
var role = cb.getAttribute('data-grant-role');
|
|
57642
|
+
if (cb.checked) desired[role] = true; else delete desired[role];
|
|
57643
|
+
render(); // re-render to refresh the dirty indicator + Save state
|
|
57644
|
+
});
|
|
57645
|
+
});
|
|
57646
|
+
var cancelBtn = panel.querySelector('#grants-cancel');
|
|
57647
|
+
if (cancelBtn) cancelBtn.addEventListener('click', function () { closeGrantsPanel(panel); });
|
|
57648
|
+
var saveBtn = panel.querySelector('#grants-save');
|
|
57649
|
+
if (saveBtn) saveBtn.addEventListener('click', function () {
|
|
57650
|
+
var toAdd = [];
|
|
57651
|
+
var toRemove = [];
|
|
57652
|
+
members.forEach(function (m) {
|
|
57653
|
+
var want = !!desired[m.role];
|
|
57654
|
+
var have = !!current[m.role];
|
|
57655
|
+
if (want && !have) toAdd.push(m.role);
|
|
57656
|
+
if (!want && have) toRemove.push(m.role);
|
|
57657
|
+
});
|
|
57658
|
+
if (toAdd.length === 0 && toRemove.length === 0) { closeGrantsPanel(panel); return; }
|
|
57659
|
+
// Confirm the mode change ONCE, here \u2014 only when actually switching
|
|
57660
|
+
// INTO specific-people mode (effective vis isn't already custom AND we
|
|
57661
|
+
// are adding at least one grantee). Never per checkbox.
|
|
57662
|
+
if (effectiveVisibility(access) !== 'custom' && toAdd.length > 0) {
|
|
57663
|
+
if (!confirm('Sharing this with specific people switches it off "everyone"/"private". The chosen people will be able to see it. Continue?')) return;
|
|
57664
|
+
}
|
|
57665
|
+
withBusy(saveBtn, function () {
|
|
57666
|
+
return fetchJson('/api/cloud/row-grants', {
|
|
57667
|
+
method: 'POST',
|
|
57668
|
+
headers: { 'content-type': 'application/json' },
|
|
57669
|
+
body: JSON.stringify({ table: tableName, pk: id, grant: toAdd, revoke: toRemove }),
|
|
57670
|
+
}).then(function () {
|
|
57671
|
+
// Mirror the committed state locally so the re-render's indicator
|
|
57672
|
+
// is correct. The first grant flips the row to custom server-side;
|
|
57673
|
+
// revoking the last leaves custom-0, which effectiveVisibility
|
|
57674
|
+
// renders as private.
|
|
57675
|
+
var list = [];
|
|
57676
|
+
members.forEach(function (m) { if (desired[m.role]) list.push(m.role); });
|
|
57677
|
+
access.grantees = list;
|
|
57678
|
+
if (list.length > 0) access.visibility = 'custom';
|
|
57679
|
+
openGrantsPanel = null; // a successful save closes the staging session
|
|
57680
|
+
invalidate(tableName);
|
|
57681
|
+
showToast('Sharing updated', {});
|
|
57682
|
+
reRender();
|
|
57683
|
+
}).catch(function (e) {
|
|
57684
|
+
// Surface loudly + leave the staged selection intact so the user
|
|
57685
|
+
// can retry; no silent partial-success.
|
|
57686
|
+
showToast('Sharing update failed: ' + e.message, {});
|
|
57687
|
+
});
|
|
57688
|
+
});
|
|
57689
|
+
});
|
|
57690
|
+
panel.hidden = false;
|
|
57691
|
+
}
|
|
57692
|
+
render();
|
|
57693
|
+
}
|
|
57694
|
+
|
|
57695
|
+
function closeGrantsPanel(panel) {
|
|
57696
|
+
if (panel) panel.hidden = true;
|
|
57697
|
+
openGrantsPanel = null;
|
|
57698
|
+
}
|
|
57699
|
+
|
|
57700
|
+
// Open (or toggle shut) the manage-access panel. Fetches the member list,
|
|
57701
|
+
// then stages from the row's current grantees. Opening must NOT pre-flip
|
|
57702
|
+
// the row to 'custom' \u2014 that left a never-shared row stuck at "custom (0)".
|
|
57703
|
+
function openManagePanel(triggerBtn) {
|
|
57128
57704
|
var panel = content.querySelector('#grants-panel');
|
|
57129
57705
|
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) {
|
|
57706
|
+
if (!panel.hidden) { closeGrantsPanel(panel); return; }
|
|
57707
|
+
withBusy(triggerBtn, function () {
|
|
57708
|
+
return fetchJson('/api/cloud/members').then(function (d) {
|
|
57142
57709
|
// The grant target is a member ROLE: lattice_grant_row keys on the
|
|
57143
57710
|
// role, and _access.grantees holds role names. List every member
|
|
57144
57711
|
// except the owner (you don't grant the owner their own row).
|
|
57145
57712
|
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);
|
|
57713
|
+
var desired = {};
|
|
57714
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
57715
|
+
openGrantsPanel = { table: tableName, pk: id };
|
|
57716
|
+
populateGrantsPanel(panel, members, desired);
|
|
57187
57717
|
}).catch(function (e) { showToast('Could not load members: ' + e.message, {}); });
|
|
57188
57718
|
});
|
|
57719
|
+
}
|
|
57720
|
+
|
|
57721
|
+
var detailVisManage = content.querySelector('#detail-vis-manage');
|
|
57722
|
+
if (detailVisManage) detailVisManage.addEventListener('click', function () {
|
|
57723
|
+
openManagePanel(detailVisManage);
|
|
57189
57724
|
});
|
|
57725
|
+
|
|
57726
|
+
// Preserve an open panel across a soft re-render: if the tracked panel
|
|
57727
|
+
// matches the row this view just repainted, re-open it and re-populate the
|
|
57728
|
+
// checklist from the freshly-fetched row._access WITHOUT any network call,
|
|
57729
|
+
// so a concurrent edit by another client doesn't lose a staged selection.
|
|
57730
|
+
if (openGrantsPanel && openGrantsPanel.table === tableName && openGrantsPanel.pk === id) {
|
|
57731
|
+
var rpanel = content.querySelector('#grants-panel');
|
|
57732
|
+
if (rpanel) {
|
|
57733
|
+
fetchJson('/api/cloud/members').then(function (d) {
|
|
57734
|
+
// Only re-populate if THIS panel is still the tracked-open one (a
|
|
57735
|
+
// newer navigation/save may have cleared it while members loaded).
|
|
57736
|
+
if (!openGrantsPanel || openGrantsPanel.table !== tableName || openGrantsPanel.pk !== id) return;
|
|
57737
|
+
var members = ((d && d.members) || []).filter(function (m) { return !m.isYou && m.status !== 'owner'; });
|
|
57738
|
+
var desired = {};
|
|
57739
|
+
(access.grantees || []).forEach(function (g) { desired[g] = true; });
|
|
57740
|
+
populateGrantsPanel(rpanel, members, desired);
|
|
57741
|
+
}).catch(function () { /* best-effort restore; a click reopens it */ });
|
|
57742
|
+
}
|
|
57743
|
+
}
|
|
57190
57744
|
}
|
|
57191
57745
|
function renderDetail(content, tableName, id) {
|
|
57192
57746
|
var myGen = renderGen;
|
|
@@ -61970,13 +62524,21 @@ var appJs = `
|
|
|
61970
62524
|
}
|
|
61971
62525
|
function uploadFile(file) {
|
|
61972
62526
|
var done = pendingIngestItem(file.name || 'file');
|
|
62527
|
+
// Carry the composer's "Private mode" intent so an upload made while the
|
|
62528
|
+
// box is checked is stamped private at insert, instead of inheriting the
|
|
62529
|
+
// files-table default (which can be shared-to-everyone on a cloud). Read
|
|
62530
|
+
// the checkbox defensively \u2014 it may not be rendered. On a local workspace
|
|
62531
|
+
// the box is checked+disabled, so this is '1' there too; forced visibility
|
|
62532
|
+
// is a harmless no-op on the single-user SQLite path.
|
|
62533
|
+
var pv = document.getElementById('chat-private');
|
|
62534
|
+
var priv = pv && pv.checked ? '1' : '0';
|
|
61973
62535
|
return fetch('/api/ingest/upload', {
|
|
61974
62536
|
method: 'POST',
|
|
61975
62537
|
// Percent-encode the filename: HTTP header values must be ISO-8859-1,
|
|
61976
62538
|
// so a Unicode filename (emoji, smart quote, accent, em-dash) would
|
|
61977
62539
|
// otherwise make fetch() throw "String contains non ISO-8859-1 code
|
|
61978
62540
|
// point". The server decodeURIComponent()s it back.
|
|
61979
|
-
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file') },
|
|
62541
|
+
headers: { 'content-type': file.type || 'application/octet-stream', 'x-filename': encodeURIComponent(file.name || 'file'), 'x-lattice-private': priv },
|
|
61980
62542
|
body: file,
|
|
61981
62543
|
})
|
|
61982
62544
|
.then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; }); })
|
|
@@ -63072,8 +63634,14 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
63072
63634
|
},
|
|
63073
63635
|
{
|
|
63074
63636
|
name: "_lattice_gui_audit",
|
|
63075
|
-
|
|
63076
|
-
|
|
63637
|
+
// UPDATE + DELETE are needed by undo/redo/revert (flips an entry's `undone`)
|
|
63638
|
+
// and the redo-stack purge on a new mutation (deletes the session's undone
|
|
63639
|
+
// entries). Safe because enableGuiAuditRls installs per-op UPDATE and DELETE
|
|
63640
|
+
// policies whose USING is `row_id IS NULL OR lattice_row_visible(table_name,
|
|
63641
|
+
// row_id)` — so a member can only update/delete audit rows for entities it can
|
|
63642
|
+
// already see (or schema-level entries that carry no row data).
|
|
63643
|
+
privs: "SELECT, INSERT, UPDATE, DELETE",
|
|
63644
|
+
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
63645
|
},
|
|
63078
63646
|
{
|
|
63079
63647
|
name: "__lattice_user_identity",
|
|
@@ -65148,6 +65716,27 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
65148
65716
|
});
|
|
65149
65717
|
return true;
|
|
65150
65718
|
}
|
|
65719
|
+
if (pathname === "/api/cloud/row-grants" && method === "POST") {
|
|
65720
|
+
await tryHandler(res, async () => {
|
|
65721
|
+
const body = await readJson(req);
|
|
65722
|
+
const table = typeof body.table === "string" ? body.table : "";
|
|
65723
|
+
const pk = typeof body.pk === "string" ? body.pk : "";
|
|
65724
|
+
const strList = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : [];
|
|
65725
|
+
const grant = strList(body.grant);
|
|
65726
|
+
const revoke = strList(body.revoke);
|
|
65727
|
+
if (!table || !pk) {
|
|
65728
|
+
sendJson(res, { error: "table and pk are required" }, 400);
|
|
65729
|
+
return;
|
|
65730
|
+
}
|
|
65731
|
+
if (ctx.db.getDialect() !== "postgres") {
|
|
65732
|
+
sendJson(res, { error: "Per-row sharing requires a cloud (Postgres) database" }, 400);
|
|
65733
|
+
return;
|
|
65734
|
+
}
|
|
65735
|
+
await batchRowGrants(ctx.db, table, pk, grant, revoke);
|
|
65736
|
+
sendJson(res, { ok: true, table, pk, granted: grant, revoked: revoke });
|
|
65737
|
+
});
|
|
65738
|
+
return true;
|
|
65739
|
+
}
|
|
65151
65740
|
if (pathname === "/api/cloud/s3-config" && method === "GET") {
|
|
65152
65741
|
await tryHandler(res, () => {
|
|
65153
65742
|
const label = activeWorkspaceLabel(ctx.configPath);
|
|
@@ -65890,6 +66479,19 @@ async function normalizeImage(path2, maxBytes) {
|
|
|
65890
66479
|
function renderJpeg(sharp, path2, quality) {
|
|
65891
66480
|
return sharp(path2).rotate().resize({ width: MAX_DIM, height: MAX_DIM, fit: "inside", withoutEnlargement: true }).jpeg({ quality }).toBuffer();
|
|
65892
66481
|
}
|
|
66482
|
+
function buildVisionAnthropicConfig(auth) {
|
|
66483
|
+
const config = {};
|
|
66484
|
+
if (auth.authToken) {
|
|
66485
|
+
config.authToken = auth.authToken;
|
|
66486
|
+
config.apiKey = null;
|
|
66487
|
+
} else if (auth.apiKey) {
|
|
66488
|
+
config.apiKey = auth.apiKey;
|
|
66489
|
+
} else {
|
|
66490
|
+
config.apiKey = null;
|
|
66491
|
+
}
|
|
66492
|
+
if (auth.betaHeader) config.defaultHeaders = { "anthropic-beta": auth.betaHeader };
|
|
66493
|
+
return config;
|
|
66494
|
+
}
|
|
65893
66495
|
function defaultSender(auth) {
|
|
65894
66496
|
return async (input) => {
|
|
65895
66497
|
const importMetaUrl = import.meta.url;
|
|
@@ -65897,11 +66499,7 @@ function defaultSender(auth) {
|
|
|
65897
66499
|
const sdk = req("@anthropic-ai/sdk");
|
|
65898
66500
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
65899
66501
|
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);
|
|
66502
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
65905
66503
|
const res = await client.messages.create({
|
|
65906
66504
|
model: input.model,
|
|
65907
66505
|
max_tokens: 1024,
|
|
@@ -65928,11 +66526,7 @@ function defaultPdfSender(auth) {
|
|
|
65928
66526
|
const sdk = req("@anthropic-ai/sdk");
|
|
65929
66527
|
const Anthropic = sdk.Anthropic ?? sdk.default;
|
|
65930
66528
|
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);
|
|
66529
|
+
const client = new Anthropic(buildVisionAnthropicConfig(auth));
|
|
65936
66530
|
const res = await client.messages.create({
|
|
65937
66531
|
model: input.model,
|
|
65938
66532
|
max_tokens: 4096,
|
|
@@ -66094,7 +66688,7 @@ function enrichContext(ctx) {
|
|
|
66094
66688
|
...ctx.createEntity ? { createEntity: ctx.createEntity } : {}
|
|
66095
66689
|
};
|
|
66096
66690
|
}
|
|
66097
|
-
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
66691
|
+
async function enrichOrFail(mctx, db, fileId, text, name, ctx, res, privateMode) {
|
|
66098
66692
|
try {
|
|
66099
66693
|
return await enrichWithLlm(
|
|
66100
66694
|
mctx,
|
|
@@ -66106,7 +66700,9 @@ async function enrichOrFail(mctx, db, fileId, text, name, ctx, res) {
|
|
|
66106
66700
|
ctx.entityDescriptions,
|
|
66107
66701
|
ctx.createJunction,
|
|
66108
66702
|
ctx.aggressiveness,
|
|
66109
|
-
ctx.createEntity
|
|
66703
|
+
ctx.createEntity,
|
|
66704
|
+
false,
|
|
66705
|
+
privateMode
|
|
66110
66706
|
);
|
|
66111
66707
|
} catch (e6) {
|
|
66112
66708
|
const err = e6;
|
|
@@ -66185,7 +66781,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66185
66781
|
source: "ingest",
|
|
66186
66782
|
onColumnsAdded: columnDescriptionHook(ctx.db)
|
|
66187
66783
|
};
|
|
66784
|
+
const headerPrivate = req.headers["x-lattice-private"] === "1";
|
|
66188
66785
|
if (ctx.pathname === "/api/ingest/upload") {
|
|
66786
|
+
const forcePrivate2 = headerPrivate;
|
|
66189
66787
|
const rawName = typeof req.headers["x-filename"] === "string" && req.headers["x-filename"] || "";
|
|
66190
66788
|
let name2 = "upload";
|
|
66191
66789
|
if (rawName) {
|
|
@@ -66283,10 +66881,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66283
66881
|
...blob ? { blob_path: blob.blob_path } : {}
|
|
66284
66882
|
} : blob ? { ref_kind: "blob", blob_path: blob.blob_path } : {}
|
|
66285
66883
|
};
|
|
66286
|
-
const { id: id2 } = await createRow(
|
|
66287
|
-
|
|
66288
|
-
|
|
66289
|
-
|
|
66884
|
+
const { id: id2 } = await createRow(
|
|
66885
|
+
mctx,
|
|
66886
|
+
"files",
|
|
66887
|
+
{
|
|
66888
|
+
...await requiredFileDefaults(ctx.db, name2, fileId, uploadRow),
|
|
66889
|
+
...uploadRow
|
|
66890
|
+
},
|
|
66891
|
+
forcePrivate2 ? "private" : void 0
|
|
66892
|
+
);
|
|
66290
66893
|
try {
|
|
66291
66894
|
const dedupCtx = {
|
|
66292
66895
|
db: ctx.db,
|
|
@@ -66312,7 +66915,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66312
66915
|
}
|
|
66313
66916
|
let suggestedLinks = [];
|
|
66314
66917
|
if (!result.skip) {
|
|
66315
|
-
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res);
|
|
66918
|
+
const links = await enrichOrFail(mctx, ctx.db, id2, result.text, name2, ctx, res, forcePrivate2);
|
|
66316
66919
|
if (links === null) return true;
|
|
66317
66920
|
suggestedLinks = links;
|
|
66318
66921
|
}
|
|
@@ -66339,6 +66942,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66339
66942
|
sendJson4(res, { error: e6.message }, 400);
|
|
66340
66943
|
return true;
|
|
66341
66944
|
}
|
|
66945
|
+
const forcePrivate = headerPrivate || body.private === true;
|
|
66342
66946
|
if (ctx.pathname === "/api/ingest/text") {
|
|
66343
66947
|
const rawText = typeof body.text === "string" ? body.text : "";
|
|
66344
66948
|
if (!rawText.trim()) {
|
|
@@ -66349,7 +66953,7 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66349
66953
|
if (sourceUrl) {
|
|
66350
66954
|
try {
|
|
66351
66955
|
const result = await ingestUrlAsFile(
|
|
66352
|
-
{ db: ctx.db, mctx, enrich: enrichContext(ctx) },
|
|
66956
|
+
{ db: ctx.db, mctx, enrich: enrichContext(ctx), privateMode: forcePrivate },
|
|
66353
66957
|
sourceUrl
|
|
66354
66958
|
);
|
|
66355
66959
|
sendJson4(
|
|
@@ -66378,11 +66982,25 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66378
66982
|
description: describe(content, mime2, title),
|
|
66379
66983
|
extraction_status: "extracted"
|
|
66380
66984
|
};
|
|
66381
|
-
const { id: id2 } = await createRow(
|
|
66382
|
-
|
|
66383
|
-
|
|
66384
|
-
|
|
66385
|
-
|
|
66985
|
+
const { id: id2 } = await createRow(
|
|
66986
|
+
mctx,
|
|
66987
|
+
"files",
|
|
66988
|
+
{
|
|
66989
|
+
...await requiredFileDefaults(ctx.db, title, textFileId, textRow),
|
|
66990
|
+
...textRow
|
|
66991
|
+
},
|
|
66992
|
+
forcePrivate ? "private" : void 0
|
|
66993
|
+
);
|
|
66994
|
+
const suggestedLinks = await enrichOrFail(
|
|
66995
|
+
mctx,
|
|
66996
|
+
ctx.db,
|
|
66997
|
+
id2,
|
|
66998
|
+
content,
|
|
66999
|
+
title,
|
|
67000
|
+
ctx,
|
|
67001
|
+
res,
|
|
67002
|
+
forcePrivate
|
|
67003
|
+
);
|
|
66386
67004
|
if (suggestedLinks === null) return true;
|
|
66387
67005
|
sendJson4(res, { id: id2, extraction_status: "extracted", suggestedLinks }, 201);
|
|
66388
67006
|
return true;
|
|
@@ -66421,10 +67039,15 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66421
67039
|
size_bytes: size,
|
|
66422
67040
|
extraction_status: "pending"
|
|
66423
67041
|
};
|
|
66424
|
-
const { id } = await createRow(
|
|
66425
|
-
|
|
66426
|
-
|
|
66427
|
-
|
|
67042
|
+
const { id } = await createRow(
|
|
67043
|
+
mctx,
|
|
67044
|
+
"files",
|
|
67045
|
+
{
|
|
67046
|
+
...await requiredFileDefaults(ctx.db, name, localFileId, localRow),
|
|
67047
|
+
...localRow
|
|
67048
|
+
},
|
|
67049
|
+
forcePrivate ? "private" : void 0
|
|
67050
|
+
);
|
|
66428
67051
|
try {
|
|
66429
67052
|
const result = await extractSource(ctx.db, abs, mime, name);
|
|
66430
67053
|
await updateRow(mctx, "files", id, {
|
|
@@ -66442,7 +67065,9 @@ async function dispatchIngestRoute(req, res, ctx) {
|
|
|
66442
67065
|
ctx.entityDescriptions,
|
|
66443
67066
|
ctx.createJunction,
|
|
66444
67067
|
ctx.aggressiveness,
|
|
66445
|
-
ctx.createEntity
|
|
67068
|
+
ctx.createEntity,
|
|
67069
|
+
false,
|
|
67070
|
+
forcePrivate
|
|
66446
67071
|
);
|
|
66447
67072
|
sendJson4(
|
|
66448
67073
|
res,
|
|
@@ -67129,7 +67754,7 @@ function startBackgroundRender(active) {
|
|
|
67129
67754
|
}
|
|
67130
67755
|
bus.publish(e6);
|
|
67131
67756
|
};
|
|
67132
|
-
void db.renderInBackground(active.outputDir, { signal, onProgress }).then(
|
|
67757
|
+
void db.renderInBackground(active.outputDir, { signal, onProgress, gateOnOpen: true }).then(
|
|
67133
67758
|
() => {
|
|
67134
67759
|
},
|
|
67135
67760
|
(err) => {
|
|
@@ -69507,7 +70132,7 @@ function printHelp() {
|
|
|
69507
70132
|
);
|
|
69508
70133
|
}
|
|
69509
70134
|
function getVersion() {
|
|
69510
|
-
if (true) return "3.4.
|
|
70135
|
+
if (true) return "3.4.4";
|
|
69511
70136
|
try {
|
|
69512
70137
|
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
69513
70138
|
const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
|