latticesql 2.2.0 → 2.2.1
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 +147 -38
- package/dist/index.cjs +95 -23
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +95 -23
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4935,9 +4935,7 @@ var Lattice = class _Lattice {
|
|
|
4935
4935
|
`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
|
|
4936
4936
|
values
|
|
4937
4937
|
);
|
|
4938
|
-
const
|
|
4939
|
-
const rawPk = rowWithPk[pkCol];
|
|
4940
|
-
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
4938
|
+
const pkValue = this._serializeRowPk(table, rowWithPk);
|
|
4941
4939
|
await this._appendChangelog(table, pkValue, "insert", rowWithPk, null);
|
|
4942
4940
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
4943
4941
|
await this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
@@ -4980,9 +4978,7 @@ var Lattice = class _Lattice {
|
|
|
4980
4978
|
`INSERT INTO "${table}" (${cols}) VALUES (${placeholders}) ON CONFLICT(${conflictCols}) DO UPDATE SET ${updateCols}`,
|
|
4981
4979
|
values
|
|
4982
4980
|
);
|
|
4983
|
-
const
|
|
4984
|
-
const rawPk = rowWithPk[pkCol];
|
|
4985
|
-
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
4981
|
+
const pkValue = this._serializeRowPk(table, rowWithPk);
|
|
4986
4982
|
this._sanitizer.emitAudit(table, "update", pkValue);
|
|
4987
4983
|
this._scheduleAutoRender();
|
|
4988
4984
|
return pkValue;
|
|
@@ -5028,7 +5024,7 @@ var Lattice = class _Lattice {
|
|
|
5028
5024
|
}
|
|
5029
5025
|
const values = [...Object.values(encrypted), ...pkParams];
|
|
5030
5026
|
await runAsyncOrSync(this._adapter, `UPDATE "${table}" SET ${setCols} WHERE ${clause}`, values);
|
|
5031
|
-
const auditId =
|
|
5027
|
+
const auditId = this._serializePkLookup(table, id);
|
|
5032
5028
|
await this._appendChangelog(table, auditId, "update", sanitized, previousValues);
|
|
5033
5029
|
this._sanitizer.emitAudit(table, "update", auditId);
|
|
5034
5030
|
await this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
@@ -5063,7 +5059,7 @@ var Lattice = class _Lattice {
|
|
|
5063
5059
|
previousRow = await getAsyncOrSync(this._adapter, `SELECT * FROM "${table}" WHERE ${clause}`, params) ?? null;
|
|
5064
5060
|
}
|
|
5065
5061
|
await runAsyncOrSync(this._adapter, `DELETE FROM "${table}" WHERE ${clause}`, params);
|
|
5066
|
-
const auditId =
|
|
5062
|
+
const auditId = this._serializePkLookup(table, id);
|
|
5067
5063
|
await this._appendChangelog(
|
|
5068
5064
|
table,
|
|
5069
5065
|
auditId,
|
|
@@ -5501,19 +5497,24 @@ var Lattice = class _Lattice {
|
|
|
5501
5497
|
this._assertIdent(table);
|
|
5502
5498
|
if (opts.orderBy) this._assertIdent(table, opts.orderBy);
|
|
5503
5499
|
const cols = this._ensureColumnCache(table);
|
|
5504
|
-
const
|
|
5500
|
+
const pkExpr = this._pkSqlExpr(this._resolvedPkCols(table));
|
|
5505
5501
|
let softDelete = "";
|
|
5506
5502
|
if (cols.has("deleted_at") && opts.deleted !== "any") {
|
|
5507
5503
|
softDelete = opts.deleted === "only" ? `t."deleted_at" IS NOT NULL AND ` : `t."deleted_at" IS NULL AND `;
|
|
5508
5504
|
}
|
|
5509
|
-
const
|
|
5510
|
-
let
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5505
|
+
const params = [];
|
|
5506
|
+
let visClause;
|
|
5507
|
+
if (pkExpr) {
|
|
5508
|
+
visClause = rowAclVisibleExists("?", "?", pkExpr);
|
|
5509
|
+
params.push(opts.teamId, table, opts.userId, opts.userId);
|
|
5510
|
+
if (opts.noAclVisible) {
|
|
5511
|
+
visClause += ` OR ${rowAclAbsent("?", "?", pkExpr)}`;
|
|
5512
|
+
params.push(opts.teamId, table);
|
|
5513
|
+
}
|
|
5514
|
+
} else {
|
|
5515
|
+
visClause = opts.noAclVisible ? "1=1" : "1=0";
|
|
5515
5516
|
}
|
|
5516
|
-
sql
|
|
5517
|
+
let sql = `SELECT t.* FROM "${table}" t WHERE ${softDelete}(${visClause})`;
|
|
5517
5518
|
if (opts.orderBy && cols.has(opts.orderBy)) {
|
|
5518
5519
|
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
5519
5520
|
sql += ` ORDER BY t."${opts.orderBy}" ${dir}`;
|
|
@@ -5559,14 +5560,18 @@ var Lattice = class _Lattice {
|
|
|
5559
5560
|
for (const [i, spec] of bounded.entries()) {
|
|
5560
5561
|
this._assertIdent(spec.table);
|
|
5561
5562
|
const cols = this._ensureColumnCache(spec.table);
|
|
5562
|
-
const
|
|
5563
|
+
const pkExpr = this._pkSqlExpr(this._resolvedPkCols(spec.table));
|
|
5563
5564
|
const softDelete = cols.has("deleted_at") ? `t."deleted_at" IS NULL AND ` : "";
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5565
|
+
let predicate;
|
|
5566
|
+
if (pkExpr) {
|
|
5567
|
+
predicate = rowAclVisibleExists("?", "?", pkExpr);
|
|
5568
|
+
params.push(opts.teamId, spec.table, opts.userId, opts.userId);
|
|
5569
|
+
if (spec.noAclVisible) {
|
|
5570
|
+
predicate += ` OR ${rowAclAbsent("?", "?", pkExpr)}`;
|
|
5571
|
+
params.push(opts.teamId, spec.table);
|
|
5572
|
+
}
|
|
5573
|
+
} else {
|
|
5574
|
+
predicate = spec.noAclVisible ? "1=1" : "1=0";
|
|
5570
5575
|
}
|
|
5571
5576
|
selects.push(
|
|
5572
5577
|
`(SELECT COUNT(*) FROM "${spec.table}" t WHERE ${softDelete}(${predicate})) AS c${String(i)}`
|
|
@@ -5822,6 +5827,62 @@ var Lattice = class _Lattice {
|
|
|
5822
5827
|
const params = pkCols.map((col) => id[col]);
|
|
5823
5828
|
return { clause: clauses.join(" AND "), params };
|
|
5824
5829
|
}
|
|
5830
|
+
// ── Composite-key serialization for the row-level-permission layer ───────
|
|
5831
|
+
// The row ACL (`__lattice_row_acl`/`__lattice_row_grants`) and the
|
|
5832
|
+
// change-log key each row by a single TEXT `pk`. For a table whose primary
|
|
5833
|
+
// key spans several columns (e.g. a junction table `(project_id,
|
|
5834
|
+
// meeting_id)` with no `id`), that key must encode EVERY pk column, and the
|
|
5835
|
+
// write side (what we store) must match the read side (the SQL that
|
|
5836
|
+
// reconstructs it from row columns). These three helpers are the single
|
|
5837
|
+
// source of truth for both. A single-column key serializes to the bare
|
|
5838
|
+
// value (so all pre-2.2.1 single-`id` ACL data stays valid).
|
|
5839
|
+
static _PK_SEP = " ";
|
|
5840
|
+
/**
|
|
5841
|
+
* The primary-key columns of `table` that PHYSICALLY exist, in declared
|
|
5842
|
+
* order. Empty when the table has no Lattice-addressable key — e.g. a table
|
|
5843
|
+
* reached via raw SQL whose PK metadata defaulted to `['id']` but that has
|
|
5844
|
+
* no `id` column. Callers treat an empty result as "unkeyable" (no per-row
|
|
5845
|
+
* ACL is possible, so the row-perm SQL must not reference a pk column).
|
|
5846
|
+
*/
|
|
5847
|
+
_resolvedPkCols(table) {
|
|
5848
|
+
const cols = this._ensureColumnCache(table);
|
|
5849
|
+
return this._schema.getPrimaryKey(table).filter((c) => cols.has(c));
|
|
5850
|
+
}
|
|
5851
|
+
/** Canonical ACL / change-log `pk` string for a row. Matches {@link _pkSqlExpr}. */
|
|
5852
|
+
_serializeRowPk(table, row) {
|
|
5853
|
+
const pkCols = this._resolvedPkCols(table);
|
|
5854
|
+
const cols = pkCols.length > 0 ? pkCols : ["id"];
|
|
5855
|
+
return cols.map((c) => {
|
|
5856
|
+
const v = row[c];
|
|
5857
|
+
return v != null ? String(v) : "";
|
|
5858
|
+
}).join(_Lattice._PK_SEP);
|
|
5859
|
+
}
|
|
5860
|
+
/**
|
|
5861
|
+
* Canonical `pk` string for a {@link PkLookup} used by update/delete, so a
|
|
5862
|
+
* row addressed by lookup keys its change-log entry identically to the way
|
|
5863
|
+
* {@link _serializeRowPk} keyed it at insert time.
|
|
5864
|
+
*/
|
|
5865
|
+
_serializePkLookup(table, id) {
|
|
5866
|
+
if (typeof id === "string") return id;
|
|
5867
|
+
const pkCols = this._resolvedPkCols(table);
|
|
5868
|
+
if (pkCols.length === 0) return JSON.stringify(id);
|
|
5869
|
+
return pkCols.map((c) => {
|
|
5870
|
+
const v = id[c];
|
|
5871
|
+
return v != null ? String(v) : "";
|
|
5872
|
+
}).join(_Lattice._PK_SEP);
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* SQL expression reconstructing {@link _serializeRowPk} from a row aliased
|
|
5876
|
+
* `t`. Returns null when the table is unkeyable (no pk columns present) — the
|
|
5877
|
+
* caller must then avoid referencing a pk column at all. Dialect-aware tab
|
|
5878
|
+
* separator: SQLite `char(9)`, Postgres `chr(9)` (both = U+0009), matching
|
|
5879
|
+
* {@link _PK_SEP}.
|
|
5880
|
+
*/
|
|
5881
|
+
_pkSqlExpr(pkCols) {
|
|
5882
|
+
if (pkCols.length === 0) return null;
|
|
5883
|
+
const sep5 = this.getDialect() === "postgres" ? "chr(9)" : "char(9)";
|
|
5884
|
+
return pkCols.map((c) => `CAST(t."${c}" AS TEXT)`).join(` || ${sep5} || `);
|
|
5885
|
+
}
|
|
5825
5886
|
/**
|
|
5826
5887
|
* Convert Filter objects into SQL clause strings and bound params.
|
|
5827
5888
|
* An `in` filter with an empty array is silently ignored (produces no clause).
|
|
@@ -6359,6 +6420,13 @@ var NATIVE_ENTITY_DEFS = {
|
|
|
6359
6420
|
columns: {
|
|
6360
6421
|
id: "TEXT PRIMARY KEY",
|
|
6361
6422
|
title: "TEXT",
|
|
6423
|
+
// Cloud user id of the member who started the thread (the operator's
|
|
6424
|
+
// `teamContext.myUserId`). A chat is PRIVATE to its author — on a team
|
|
6425
|
+
// cloud the chat routes only ever return threads whose owner matches the
|
|
6426
|
+
// requesting member. NULL on local single-user databases (no team
|
|
6427
|
+
// context) and on pre-2.2.1 threads, which the routes treat as the local
|
|
6428
|
+
// operator's own (visible only when there is no team context).
|
|
6429
|
+
owner_user_id: "TEXT",
|
|
6362
6430
|
created_at: "TEXT NOT NULL DEFAULT (datetime('now'))",
|
|
6363
6431
|
updated_at: "TEXT NOT NULL DEFAULT (datetime('now'))",
|
|
6364
6432
|
deleted_at: "TEXT"
|
|
@@ -6373,6 +6441,10 @@ var NATIVE_ENTITY_DEFS = {
|
|
|
6373
6441
|
// Soft reference to chat_threads.id. Kept as a plain column (no FK)
|
|
6374
6442
|
// to match the generic, dialect-agnostic native-entity style.
|
|
6375
6443
|
thread_id: "TEXT",
|
|
6444
|
+
// Cloud user id of the member the message belongs to — mirrors the
|
|
6445
|
+
// owning thread's owner_user_id so a message read can be filtered
|
|
6446
|
+
// independently of the thread join. NULL on local DBs / pre-2.2.1 rows.
|
|
6447
|
+
owner_user_id: "TEXT",
|
|
6376
6448
|
// user | assistant | tool | feed | system
|
|
6377
6449
|
role: "TEXT NOT NULL DEFAULT 'user'",
|
|
6378
6450
|
// JSON payload: text, tool_use / tool_result blocks, attachments, or
|
|
@@ -21168,15 +21240,19 @@ function collapseSameRole(msgs) {
|
|
|
21168
21240
|
}
|
|
21169
21241
|
return out;
|
|
21170
21242
|
}
|
|
21171
|
-
async function rehydrateHistory(db, threadId, clientHistory) {
|
|
21243
|
+
async function rehydrateHistory(db, threadId, clientHistory, ownerUserId) {
|
|
21172
21244
|
if (!threadId || !rehydrateEnabled()) return clientHistory;
|
|
21173
21245
|
let rows;
|
|
21174
21246
|
try {
|
|
21247
|
+
const filters = [
|
|
21248
|
+
{ col: "thread_id", op: "eq", val: threadId },
|
|
21249
|
+
{ col: "deleted_at", op: "isNull" }
|
|
21250
|
+
];
|
|
21251
|
+
if (ownerUserId != null) {
|
|
21252
|
+
filters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
21253
|
+
}
|
|
21175
21254
|
rows = await db.query("chat_messages", {
|
|
21176
|
-
filters
|
|
21177
|
-
{ col: "thread_id", op: "eq", val: threadId },
|
|
21178
|
-
{ col: "deleted_at", op: "isNull" }
|
|
21179
|
-
],
|
|
21255
|
+
filters,
|
|
21180
21256
|
limit: 1e3
|
|
21181
21257
|
});
|
|
21182
21258
|
} catch {
|
|
@@ -21248,13 +21324,18 @@ async function rehydrateHistory(db, threadId, clientHistory) {
|
|
|
21248
21324
|
}
|
|
21249
21325
|
return merged;
|
|
21250
21326
|
}
|
|
21251
|
-
async function ensureThread(db, threadId, title) {
|
|
21327
|
+
async function ensureThread(db, threadId, title, ownerUserId) {
|
|
21252
21328
|
if (threadId) {
|
|
21253
21329
|
const existing = await db.get("chat_threads", threadId);
|
|
21254
|
-
|
|
21330
|
+
const ownsIt = ownerUserId == null || (existing?.owner_user_id ?? null) === ownerUserId;
|
|
21331
|
+
if (existing && !existing.deleted_at && ownsIt) return threadId;
|
|
21255
21332
|
}
|
|
21256
21333
|
const id = crypto.randomUUID();
|
|
21257
|
-
await db.insert("chat_threads", {
|
|
21334
|
+
await db.insert("chat_threads", {
|
|
21335
|
+
id,
|
|
21336
|
+
title: title.slice(0, 60) || "Chat",
|
|
21337
|
+
owner_user_id: ownerUserId
|
|
21338
|
+
});
|
|
21258
21339
|
return id;
|
|
21259
21340
|
}
|
|
21260
21341
|
var REHYDRATE_MAX_TURNS = 6;
|
|
@@ -21262,20 +21343,28 @@ var REHYDRATE_MAX_BYTES = 24e3;
|
|
|
21262
21343
|
function rehydrateEnabled() {
|
|
21263
21344
|
return process.env.LATTICE_CHAT_REHYDRATE !== "false";
|
|
21264
21345
|
}
|
|
21265
|
-
async function persistMessage(db, threadId, role, text, turns, startedAt) {
|
|
21346
|
+
async function persistMessage(db, threadId, role, text, ownerUserId, turns, startedAt) {
|
|
21266
21347
|
const payload = turns && turns.length > 0 ? { text, turns } : { text };
|
|
21267
21348
|
if (startedAt) payload.startedAt = startedAt;
|
|
21268
21349
|
await db.insert("chat_messages", {
|
|
21269
21350
|
id: crypto.randomUUID(),
|
|
21270
21351
|
thread_id: threadId,
|
|
21352
|
+
// Mirror the owning member onto each message so a message read can be
|
|
21353
|
+
// filtered independently of the thread join. NULL on local DBs.
|
|
21354
|
+
owner_user_id: ownerUserId,
|
|
21271
21355
|
role,
|
|
21272
21356
|
content_json: JSON.stringify(payload),
|
|
21273
21357
|
source: role === "user" ? "gui" : "ai"
|
|
21274
21358
|
});
|
|
21275
21359
|
}
|
|
21276
21360
|
async function dispatchChatRoute(req, res, ctx) {
|
|
21361
|
+
const owner = ctx.team ? ctx.team.myUserId : null;
|
|
21277
21362
|
if (ctx.method === "GET" && ctx.pathname === "/api/chat/threads") {
|
|
21278
|
-
const
|
|
21363
|
+
const filters = [
|
|
21364
|
+
{ col: "deleted_at", op: "isNull" }
|
|
21365
|
+
];
|
|
21366
|
+
if (owner != null) filters.push({ col: "owner_user_id", op: "eq", val: owner });
|
|
21367
|
+
const rows = await ctx.db.query("chat_threads", { filters, limit: 100 });
|
|
21279
21368
|
const threads = rows.filter((r) => !r.deleted_at).map((r) => ({
|
|
21280
21369
|
id: asStr(r.id),
|
|
21281
21370
|
title: asStr(r.title, "Chat"),
|
|
@@ -21287,7 +21376,19 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
21287
21376
|
const msgMatch = /^\/api\/chat\/threads\/([^/]+)\/messages$/.exec(ctx.pathname);
|
|
21288
21377
|
if (ctx.method === "GET" && msgMatch) {
|
|
21289
21378
|
const threadId2 = decodeURIComponent(msgMatch[1] ?? "");
|
|
21290
|
-
|
|
21379
|
+
if (owner != null) {
|
|
21380
|
+
const thread = await ctx.db.get("chat_threads", threadId2);
|
|
21381
|
+
if (!thread || thread.deleted_at || (thread.owner_user_id ?? null) !== owner) {
|
|
21382
|
+
sendJson4(res, { messages: [] });
|
|
21383
|
+
return true;
|
|
21384
|
+
}
|
|
21385
|
+
}
|
|
21386
|
+
const msgFilters = [{ col: "thread_id", op: "eq", val: threadId2 }];
|
|
21387
|
+
if (owner != null) msgFilters.push({ col: "owner_user_id", op: "eq", val: owner });
|
|
21388
|
+
const rows = await ctx.db.query("chat_messages", {
|
|
21389
|
+
filters: msgFilters,
|
|
21390
|
+
limit: 1e3
|
|
21391
|
+
});
|
|
21291
21392
|
const messages = rows.filter((r) => r.thread_id === threadId2 && !r.deleted_at).map((r) => {
|
|
21292
21393
|
let text = "";
|
|
21293
21394
|
let turns2;
|
|
@@ -21341,11 +21442,11 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
21341
21442
|
return true;
|
|
21342
21443
|
}
|
|
21343
21444
|
const requestedThread = typeof body.threadId === "string" ? body.threadId : null;
|
|
21344
|
-
const history = await rehydrateHistory(ctx.db, requestedThread, mapHistory(body.history));
|
|
21445
|
+
const history = await rehydrateHistory(ctx.db, requestedThread, mapHistory(body.history), owner);
|
|
21345
21446
|
let threadId = "";
|
|
21346
21447
|
try {
|
|
21347
|
-
threadId = await ensureThread(ctx.db, requestedThread, message);
|
|
21348
|
-
await persistMessage(ctx.db, threadId, "user", message);
|
|
21448
|
+
threadId = await ensureThread(ctx.db, requestedThread, message, owner);
|
|
21449
|
+
await persistMessage(ctx.db, threadId, "user", message, owner);
|
|
21349
21450
|
} catch (e) {
|
|
21350
21451
|
console.warn("[chat] persist user message failed:", e.message);
|
|
21351
21452
|
}
|
|
@@ -21432,7 +21533,15 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
21432
21533
|
...t.toolCalls.length > 0 ? { toolCalls: t.toolCalls } : {}
|
|
21433
21534
|
})).filter((t) => t.text.length > 0 || t.tools.length > 0 || (t.events?.length ?? 0) > 0);
|
|
21434
21535
|
try {
|
|
21435
|
-
await persistMessage(
|
|
21536
|
+
await persistMessage(
|
|
21537
|
+
ctx.db,
|
|
21538
|
+
threadId,
|
|
21539
|
+
"assistant",
|
|
21540
|
+
assistantText,
|
|
21541
|
+
owner,
|
|
21542
|
+
cleanTurns,
|
|
21543
|
+
turnStartedAt
|
|
21544
|
+
);
|
|
21436
21545
|
} catch (e) {
|
|
21437
21546
|
console.warn("[chat] persist assistant message failed:", e.message);
|
|
21438
21547
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -4935,9 +4935,7 @@ var Lattice = class _Lattice {
|
|
|
4935
4935
|
`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
|
|
4936
4936
|
values
|
|
4937
4937
|
);
|
|
4938
|
-
const
|
|
4939
|
-
const rawPk = rowWithPk[pkCol];
|
|
4940
|
-
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
4938
|
+
const pkValue = this._serializeRowPk(table, rowWithPk);
|
|
4941
4939
|
await this._appendChangelog(table, pkValue, "insert", rowWithPk, null);
|
|
4942
4940
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
4943
4941
|
await this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
@@ -4980,9 +4978,7 @@ var Lattice = class _Lattice {
|
|
|
4980
4978
|
`INSERT INTO "${table}" (${cols}) VALUES (${placeholders}) ON CONFLICT(${conflictCols}) DO UPDATE SET ${updateCols}`,
|
|
4981
4979
|
values
|
|
4982
4980
|
);
|
|
4983
|
-
const
|
|
4984
|
-
const rawPk = rowWithPk[pkCol];
|
|
4985
|
-
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
4981
|
+
const pkValue = this._serializeRowPk(table, rowWithPk);
|
|
4986
4982
|
this._sanitizer.emitAudit(table, "update", pkValue);
|
|
4987
4983
|
this._scheduleAutoRender();
|
|
4988
4984
|
return pkValue;
|
|
@@ -5028,7 +5024,7 @@ var Lattice = class _Lattice {
|
|
|
5028
5024
|
}
|
|
5029
5025
|
const values = [...Object.values(encrypted), ...pkParams];
|
|
5030
5026
|
await runAsyncOrSync(this._adapter, `UPDATE "${table}" SET ${setCols} WHERE ${clause}`, values);
|
|
5031
|
-
const auditId =
|
|
5027
|
+
const auditId = this._serializePkLookup(table, id);
|
|
5032
5028
|
await this._appendChangelog(table, auditId, "update", sanitized, previousValues);
|
|
5033
5029
|
this._sanitizer.emitAudit(table, "update", auditId);
|
|
5034
5030
|
await this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
@@ -5063,7 +5059,7 @@ var Lattice = class _Lattice {
|
|
|
5063
5059
|
previousRow = await getAsyncOrSync(this._adapter, `SELECT * FROM "${table}" WHERE ${clause}`, params) ?? null;
|
|
5064
5060
|
}
|
|
5065
5061
|
await runAsyncOrSync(this._adapter, `DELETE FROM "${table}" WHERE ${clause}`, params);
|
|
5066
|
-
const auditId =
|
|
5062
|
+
const auditId = this._serializePkLookup(table, id);
|
|
5067
5063
|
await this._appendChangelog(
|
|
5068
5064
|
table,
|
|
5069
5065
|
auditId,
|
|
@@ -5501,19 +5497,24 @@ var Lattice = class _Lattice {
|
|
|
5501
5497
|
this._assertIdent(table);
|
|
5502
5498
|
if (opts.orderBy) this._assertIdent(table, opts.orderBy);
|
|
5503
5499
|
const cols = this._ensureColumnCache(table);
|
|
5504
|
-
const
|
|
5500
|
+
const pkExpr = this._pkSqlExpr(this._resolvedPkCols(table));
|
|
5505
5501
|
let softDelete = "";
|
|
5506
5502
|
if (cols.has("deleted_at") && opts.deleted !== "any") {
|
|
5507
5503
|
softDelete = opts.deleted === "only" ? `t."deleted_at" IS NOT NULL AND ` : `t."deleted_at" IS NULL AND `;
|
|
5508
5504
|
}
|
|
5509
|
-
const
|
|
5510
|
-
let
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5505
|
+
const params = [];
|
|
5506
|
+
let visClause;
|
|
5507
|
+
if (pkExpr) {
|
|
5508
|
+
visClause = rowAclVisibleExists("?", "?", pkExpr);
|
|
5509
|
+
params.push(opts.teamId, table, opts.userId, opts.userId);
|
|
5510
|
+
if (opts.noAclVisible) {
|
|
5511
|
+
visClause += ` OR ${rowAclAbsent("?", "?", pkExpr)}`;
|
|
5512
|
+
params.push(opts.teamId, table);
|
|
5513
|
+
}
|
|
5514
|
+
} else {
|
|
5515
|
+
visClause = opts.noAclVisible ? "1=1" : "1=0";
|
|
5515
5516
|
}
|
|
5516
|
-
sql
|
|
5517
|
+
let sql = `SELECT t.* FROM "${table}" t WHERE ${softDelete}(${visClause})`;
|
|
5517
5518
|
if (opts.orderBy && cols.has(opts.orderBy)) {
|
|
5518
5519
|
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
5519
5520
|
sql += ` ORDER BY t."${opts.orderBy}" ${dir}`;
|
|
@@ -5559,14 +5560,18 @@ var Lattice = class _Lattice {
|
|
|
5559
5560
|
for (const [i, spec] of bounded.entries()) {
|
|
5560
5561
|
this._assertIdent(spec.table);
|
|
5561
5562
|
const cols = this._ensureColumnCache(spec.table);
|
|
5562
|
-
const
|
|
5563
|
+
const pkExpr = this._pkSqlExpr(this._resolvedPkCols(spec.table));
|
|
5563
5564
|
const softDelete = cols.has("deleted_at") ? `t."deleted_at" IS NULL AND ` : "";
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5565
|
+
let predicate;
|
|
5566
|
+
if (pkExpr) {
|
|
5567
|
+
predicate = rowAclVisibleExists("?", "?", pkExpr);
|
|
5568
|
+
params.push(opts.teamId, spec.table, opts.userId, opts.userId);
|
|
5569
|
+
if (spec.noAclVisible) {
|
|
5570
|
+
predicate += ` OR ${rowAclAbsent("?", "?", pkExpr)}`;
|
|
5571
|
+
params.push(opts.teamId, spec.table);
|
|
5572
|
+
}
|
|
5573
|
+
} else {
|
|
5574
|
+
predicate = spec.noAclVisible ? "1=1" : "1=0";
|
|
5570
5575
|
}
|
|
5571
5576
|
selects.push(
|
|
5572
5577
|
`(SELECT COUNT(*) FROM "${spec.table}" t WHERE ${softDelete}(${predicate})) AS c${String(i)}`
|
|
@@ -5822,6 +5827,62 @@ var Lattice = class _Lattice {
|
|
|
5822
5827
|
const params = pkCols.map((col) => id[col]);
|
|
5823
5828
|
return { clause: clauses.join(" AND "), params };
|
|
5824
5829
|
}
|
|
5830
|
+
// ── Composite-key serialization for the row-level-permission layer ───────
|
|
5831
|
+
// The row ACL (`__lattice_row_acl`/`__lattice_row_grants`) and the
|
|
5832
|
+
// change-log key each row by a single TEXT `pk`. For a table whose primary
|
|
5833
|
+
// key spans several columns (e.g. a junction table `(project_id,
|
|
5834
|
+
// meeting_id)` with no `id`), that key must encode EVERY pk column, and the
|
|
5835
|
+
// write side (what we store) must match the read side (the SQL that
|
|
5836
|
+
// reconstructs it from row columns). These three helpers are the single
|
|
5837
|
+
// source of truth for both. A single-column key serializes to the bare
|
|
5838
|
+
// value (so all pre-2.2.1 single-`id` ACL data stays valid).
|
|
5839
|
+
static _PK_SEP = " ";
|
|
5840
|
+
/**
|
|
5841
|
+
* The primary-key columns of `table` that PHYSICALLY exist, in declared
|
|
5842
|
+
* order. Empty when the table has no Lattice-addressable key — e.g. a table
|
|
5843
|
+
* reached via raw SQL whose PK metadata defaulted to `['id']` but that has
|
|
5844
|
+
* no `id` column. Callers treat an empty result as "unkeyable" (no per-row
|
|
5845
|
+
* ACL is possible, so the row-perm SQL must not reference a pk column).
|
|
5846
|
+
*/
|
|
5847
|
+
_resolvedPkCols(table) {
|
|
5848
|
+
const cols = this._ensureColumnCache(table);
|
|
5849
|
+
return this._schema.getPrimaryKey(table).filter((c) => cols.has(c));
|
|
5850
|
+
}
|
|
5851
|
+
/** Canonical ACL / change-log `pk` string for a row. Matches {@link _pkSqlExpr}. */
|
|
5852
|
+
_serializeRowPk(table, row) {
|
|
5853
|
+
const pkCols = this._resolvedPkCols(table);
|
|
5854
|
+
const cols = pkCols.length > 0 ? pkCols : ["id"];
|
|
5855
|
+
return cols.map((c) => {
|
|
5856
|
+
const v = row[c];
|
|
5857
|
+
return v != null ? String(v) : "";
|
|
5858
|
+
}).join(_Lattice._PK_SEP);
|
|
5859
|
+
}
|
|
5860
|
+
/**
|
|
5861
|
+
* Canonical `pk` string for a {@link PkLookup} used by update/delete, so a
|
|
5862
|
+
* row addressed by lookup keys its change-log entry identically to the way
|
|
5863
|
+
* {@link _serializeRowPk} keyed it at insert time.
|
|
5864
|
+
*/
|
|
5865
|
+
_serializePkLookup(table, id) {
|
|
5866
|
+
if (typeof id === "string") return id;
|
|
5867
|
+
const pkCols = this._resolvedPkCols(table);
|
|
5868
|
+
if (pkCols.length === 0) return JSON.stringify(id);
|
|
5869
|
+
return pkCols.map((c) => {
|
|
5870
|
+
const v = id[c];
|
|
5871
|
+
return v != null ? String(v) : "";
|
|
5872
|
+
}).join(_Lattice._PK_SEP);
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* SQL expression reconstructing {@link _serializeRowPk} from a row aliased
|
|
5876
|
+
* `t`. Returns null when the table is unkeyable (no pk columns present) — the
|
|
5877
|
+
* caller must then avoid referencing a pk column at all. Dialect-aware tab
|
|
5878
|
+
* separator: SQLite `char(9)`, Postgres `chr(9)` (both = U+0009), matching
|
|
5879
|
+
* {@link _PK_SEP}.
|
|
5880
|
+
*/
|
|
5881
|
+
_pkSqlExpr(pkCols) {
|
|
5882
|
+
if (pkCols.length === 0) return null;
|
|
5883
|
+
const sep2 = this.getDialect() === "postgres" ? "chr(9)" : "char(9)";
|
|
5884
|
+
return pkCols.map((c) => `CAST(t."${c}" AS TEXT)`).join(` || ${sep2} || `);
|
|
5885
|
+
}
|
|
5825
5886
|
/**
|
|
5826
5887
|
* Convert Filter objects into SQL clause strings and bound params.
|
|
5827
5888
|
* An `in` filter with an empty array is silently ignored (produces no clause).
|
|
@@ -6762,6 +6823,13 @@ var NATIVE_ENTITY_DEFS = {
|
|
|
6762
6823
|
columns: {
|
|
6763
6824
|
id: "TEXT PRIMARY KEY",
|
|
6764
6825
|
title: "TEXT",
|
|
6826
|
+
// Cloud user id of the member who started the thread (the operator's
|
|
6827
|
+
// `teamContext.myUserId`). A chat is PRIVATE to its author — on a team
|
|
6828
|
+
// cloud the chat routes only ever return threads whose owner matches the
|
|
6829
|
+
// requesting member. NULL on local single-user databases (no team
|
|
6830
|
+
// context) and on pre-2.2.1 threads, which the routes treat as the local
|
|
6831
|
+
// operator's own (visible only when there is no team context).
|
|
6832
|
+
owner_user_id: "TEXT",
|
|
6765
6833
|
created_at: "TEXT NOT NULL DEFAULT (datetime('now'))",
|
|
6766
6834
|
updated_at: "TEXT NOT NULL DEFAULT (datetime('now'))",
|
|
6767
6835
|
deleted_at: "TEXT"
|
|
@@ -6776,6 +6844,10 @@ var NATIVE_ENTITY_DEFS = {
|
|
|
6776
6844
|
// Soft reference to chat_threads.id. Kept as a plain column (no FK)
|
|
6777
6845
|
// to match the generic, dialect-agnostic native-entity style.
|
|
6778
6846
|
thread_id: "TEXT",
|
|
6847
|
+
// Cloud user id of the member the message belongs to — mirrors the
|
|
6848
|
+
// owning thread's owner_user_id so a message read can be filtered
|
|
6849
|
+
// independently of the thread join. NULL on local DBs / pre-2.2.1 rows.
|
|
6850
|
+
owner_user_id: "TEXT",
|
|
6779
6851
|
// user | assistant | tool | feed | system
|
|
6780
6852
|
role: "TEXT NOT NULL DEFAULT 'user'",
|
|
6781
6853
|
// JSON payload: text, tool_use / tool_result blocks, attachments, or
|
package/dist/index.d.cts
CHANGED
|
@@ -2111,6 +2111,31 @@ declare class Lattice {
|
|
|
2111
2111
|
* - `Record` → matches every PK column; all must be present in the object.
|
|
2112
2112
|
*/
|
|
2113
2113
|
private _pkWhere;
|
|
2114
|
+
private static readonly _PK_SEP;
|
|
2115
|
+
/**
|
|
2116
|
+
* The primary-key columns of `table` that PHYSICALLY exist, in declared
|
|
2117
|
+
* order. Empty when the table has no Lattice-addressable key — e.g. a table
|
|
2118
|
+
* reached via raw SQL whose PK metadata defaulted to `['id']` but that has
|
|
2119
|
+
* no `id` column. Callers treat an empty result as "unkeyable" (no per-row
|
|
2120
|
+
* ACL is possible, so the row-perm SQL must not reference a pk column).
|
|
2121
|
+
*/
|
|
2122
|
+
private _resolvedPkCols;
|
|
2123
|
+
/** Canonical ACL / change-log `pk` string for a row. Matches {@link _pkSqlExpr}. */
|
|
2124
|
+
private _serializeRowPk;
|
|
2125
|
+
/**
|
|
2126
|
+
* Canonical `pk` string for a {@link PkLookup} used by update/delete, so a
|
|
2127
|
+
* row addressed by lookup keys its change-log entry identically to the way
|
|
2128
|
+
* {@link _serializeRowPk} keyed it at insert time.
|
|
2129
|
+
*/
|
|
2130
|
+
private _serializePkLookup;
|
|
2131
|
+
/**
|
|
2132
|
+
* SQL expression reconstructing {@link _serializeRowPk} from a row aliased
|
|
2133
|
+
* `t`. Returns null when the table is unkeyable (no pk columns present) — the
|
|
2134
|
+
* caller must then avoid referencing a pk column at all. Dialect-aware tab
|
|
2135
|
+
* separator: SQLite `char(9)`, Postgres `chr(9)` (both = U+0009), matching
|
|
2136
|
+
* {@link _PK_SEP}.
|
|
2137
|
+
*/
|
|
2138
|
+
private _pkSqlExpr;
|
|
2114
2139
|
/**
|
|
2115
2140
|
* Convert Filter objects into SQL clause strings and bound params.
|
|
2116
2141
|
* An `in` filter with an empty array is silently ignored (produces no clause).
|
package/dist/index.d.ts
CHANGED
|
@@ -2111,6 +2111,31 @@ declare class Lattice {
|
|
|
2111
2111
|
* - `Record` → matches every PK column; all must be present in the object.
|
|
2112
2112
|
*/
|
|
2113
2113
|
private _pkWhere;
|
|
2114
|
+
private static readonly _PK_SEP;
|
|
2115
|
+
/**
|
|
2116
|
+
* The primary-key columns of `table` that PHYSICALLY exist, in declared
|
|
2117
|
+
* order. Empty when the table has no Lattice-addressable key — e.g. a table
|
|
2118
|
+
* reached via raw SQL whose PK metadata defaulted to `['id']` but that has
|
|
2119
|
+
* no `id` column. Callers treat an empty result as "unkeyable" (no per-row
|
|
2120
|
+
* ACL is possible, so the row-perm SQL must not reference a pk column).
|
|
2121
|
+
*/
|
|
2122
|
+
private _resolvedPkCols;
|
|
2123
|
+
/** Canonical ACL / change-log `pk` string for a row. Matches {@link _pkSqlExpr}. */
|
|
2124
|
+
private _serializeRowPk;
|
|
2125
|
+
/**
|
|
2126
|
+
* Canonical `pk` string for a {@link PkLookup} used by update/delete, so a
|
|
2127
|
+
* row addressed by lookup keys its change-log entry identically to the way
|
|
2128
|
+
* {@link _serializeRowPk} keyed it at insert time.
|
|
2129
|
+
*/
|
|
2130
|
+
private _serializePkLookup;
|
|
2131
|
+
/**
|
|
2132
|
+
* SQL expression reconstructing {@link _serializeRowPk} from a row aliased
|
|
2133
|
+
* `t`. Returns null when the table is unkeyable (no pk columns present) — the
|
|
2134
|
+
* caller must then avoid referencing a pk column at all. Dialect-aware tab
|
|
2135
|
+
* separator: SQLite `char(9)`, Postgres `chr(9)` (both = U+0009), matching
|
|
2136
|
+
* {@link _PK_SEP}.
|
|
2137
|
+
*/
|
|
2138
|
+
private _pkSqlExpr;
|
|
2114
2139
|
/**
|
|
2115
2140
|
* Convert Filter objects into SQL clause strings and bound params.
|
|
2116
2141
|
* An `in` filter with an empty array is silently ignored (produces no clause).
|
package/dist/index.js
CHANGED
|
@@ -4801,9 +4801,7 @@ var Lattice = class _Lattice {
|
|
|
4801
4801
|
`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`,
|
|
4802
4802
|
values
|
|
4803
4803
|
);
|
|
4804
|
-
const
|
|
4805
|
-
const rawPk = rowWithPk[pkCol];
|
|
4806
|
-
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
4804
|
+
const pkValue = this._serializeRowPk(table, rowWithPk);
|
|
4807
4805
|
await this._appendChangelog(table, pkValue, "insert", rowWithPk, null);
|
|
4808
4806
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
4809
4807
|
await this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
@@ -4846,9 +4844,7 @@ var Lattice = class _Lattice {
|
|
|
4846
4844
|
`INSERT INTO "${table}" (${cols}) VALUES (${placeholders}) ON CONFLICT(${conflictCols}) DO UPDATE SET ${updateCols}`,
|
|
4847
4845
|
values
|
|
4848
4846
|
);
|
|
4849
|
-
const
|
|
4850
|
-
const rawPk = rowWithPk[pkCol];
|
|
4851
|
-
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
4847
|
+
const pkValue = this._serializeRowPk(table, rowWithPk);
|
|
4852
4848
|
this._sanitizer.emitAudit(table, "update", pkValue);
|
|
4853
4849
|
this._scheduleAutoRender();
|
|
4854
4850
|
return pkValue;
|
|
@@ -4894,7 +4890,7 @@ var Lattice = class _Lattice {
|
|
|
4894
4890
|
}
|
|
4895
4891
|
const values = [...Object.values(encrypted), ...pkParams];
|
|
4896
4892
|
await runAsyncOrSync(this._adapter, `UPDATE "${table}" SET ${setCols} WHERE ${clause}`, values);
|
|
4897
|
-
const auditId =
|
|
4893
|
+
const auditId = this._serializePkLookup(table, id);
|
|
4898
4894
|
await this._appendChangelog(table, auditId, "update", sanitized, previousValues);
|
|
4899
4895
|
this._sanitizer.emitAudit(table, "update", auditId);
|
|
4900
4896
|
await this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
@@ -4929,7 +4925,7 @@ var Lattice = class _Lattice {
|
|
|
4929
4925
|
previousRow = await getAsyncOrSync(this._adapter, `SELECT * FROM "${table}" WHERE ${clause}`, params) ?? null;
|
|
4930
4926
|
}
|
|
4931
4927
|
await runAsyncOrSync(this._adapter, `DELETE FROM "${table}" WHERE ${clause}`, params);
|
|
4932
|
-
const auditId =
|
|
4928
|
+
const auditId = this._serializePkLookup(table, id);
|
|
4933
4929
|
await this._appendChangelog(
|
|
4934
4930
|
table,
|
|
4935
4931
|
auditId,
|
|
@@ -5367,19 +5363,24 @@ var Lattice = class _Lattice {
|
|
|
5367
5363
|
this._assertIdent(table);
|
|
5368
5364
|
if (opts.orderBy) this._assertIdent(table, opts.orderBy);
|
|
5369
5365
|
const cols = this._ensureColumnCache(table);
|
|
5370
|
-
const
|
|
5366
|
+
const pkExpr = this._pkSqlExpr(this._resolvedPkCols(table));
|
|
5371
5367
|
let softDelete = "";
|
|
5372
5368
|
if (cols.has("deleted_at") && opts.deleted !== "any") {
|
|
5373
5369
|
softDelete = opts.deleted === "only" ? `t."deleted_at" IS NOT NULL AND ` : `t."deleted_at" IS NULL AND `;
|
|
5374
5370
|
}
|
|
5375
|
-
const
|
|
5376
|
-
let
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5371
|
+
const params = [];
|
|
5372
|
+
let visClause;
|
|
5373
|
+
if (pkExpr) {
|
|
5374
|
+
visClause = rowAclVisibleExists("?", "?", pkExpr);
|
|
5375
|
+
params.push(opts.teamId, table, opts.userId, opts.userId);
|
|
5376
|
+
if (opts.noAclVisible) {
|
|
5377
|
+
visClause += ` OR ${rowAclAbsent("?", "?", pkExpr)}`;
|
|
5378
|
+
params.push(opts.teamId, table);
|
|
5379
|
+
}
|
|
5380
|
+
} else {
|
|
5381
|
+
visClause = opts.noAclVisible ? "1=1" : "1=0";
|
|
5381
5382
|
}
|
|
5382
|
-
sql
|
|
5383
|
+
let sql = `SELECT t.* FROM "${table}" t WHERE ${softDelete}(${visClause})`;
|
|
5383
5384
|
if (opts.orderBy && cols.has(opts.orderBy)) {
|
|
5384
5385
|
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
5385
5386
|
sql += ` ORDER BY t."${opts.orderBy}" ${dir}`;
|
|
@@ -5425,14 +5426,18 @@ var Lattice = class _Lattice {
|
|
|
5425
5426
|
for (const [i, spec] of bounded.entries()) {
|
|
5426
5427
|
this._assertIdent(spec.table);
|
|
5427
5428
|
const cols = this._ensureColumnCache(spec.table);
|
|
5428
|
-
const
|
|
5429
|
+
const pkExpr = this._pkSqlExpr(this._resolvedPkCols(spec.table));
|
|
5429
5430
|
const softDelete = cols.has("deleted_at") ? `t."deleted_at" IS NULL AND ` : "";
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5431
|
+
let predicate;
|
|
5432
|
+
if (pkExpr) {
|
|
5433
|
+
predicate = rowAclVisibleExists("?", "?", pkExpr);
|
|
5434
|
+
params.push(opts.teamId, spec.table, opts.userId, opts.userId);
|
|
5435
|
+
if (spec.noAclVisible) {
|
|
5436
|
+
predicate += ` OR ${rowAclAbsent("?", "?", pkExpr)}`;
|
|
5437
|
+
params.push(opts.teamId, spec.table);
|
|
5438
|
+
}
|
|
5439
|
+
} else {
|
|
5440
|
+
predicate = spec.noAclVisible ? "1=1" : "1=0";
|
|
5436
5441
|
}
|
|
5437
5442
|
selects.push(
|
|
5438
5443
|
`(SELECT COUNT(*) FROM "${spec.table}" t WHERE ${softDelete}(${predicate})) AS c${String(i)}`
|
|
@@ -5688,6 +5693,62 @@ var Lattice = class _Lattice {
|
|
|
5688
5693
|
const params = pkCols.map((col) => id[col]);
|
|
5689
5694
|
return { clause: clauses.join(" AND "), params };
|
|
5690
5695
|
}
|
|
5696
|
+
// ── Composite-key serialization for the row-level-permission layer ───────
|
|
5697
|
+
// The row ACL (`__lattice_row_acl`/`__lattice_row_grants`) and the
|
|
5698
|
+
// change-log key each row by a single TEXT `pk`. For a table whose primary
|
|
5699
|
+
// key spans several columns (e.g. a junction table `(project_id,
|
|
5700
|
+
// meeting_id)` with no `id`), that key must encode EVERY pk column, and the
|
|
5701
|
+
// write side (what we store) must match the read side (the SQL that
|
|
5702
|
+
// reconstructs it from row columns). These three helpers are the single
|
|
5703
|
+
// source of truth for both. A single-column key serializes to the bare
|
|
5704
|
+
// value (so all pre-2.2.1 single-`id` ACL data stays valid).
|
|
5705
|
+
static _PK_SEP = " ";
|
|
5706
|
+
/**
|
|
5707
|
+
* The primary-key columns of `table` that PHYSICALLY exist, in declared
|
|
5708
|
+
* order. Empty when the table has no Lattice-addressable key — e.g. a table
|
|
5709
|
+
* reached via raw SQL whose PK metadata defaulted to `['id']` but that has
|
|
5710
|
+
* no `id` column. Callers treat an empty result as "unkeyable" (no per-row
|
|
5711
|
+
* ACL is possible, so the row-perm SQL must not reference a pk column).
|
|
5712
|
+
*/
|
|
5713
|
+
_resolvedPkCols(table) {
|
|
5714
|
+
const cols = this._ensureColumnCache(table);
|
|
5715
|
+
return this._schema.getPrimaryKey(table).filter((c) => cols.has(c));
|
|
5716
|
+
}
|
|
5717
|
+
/** Canonical ACL / change-log `pk` string for a row. Matches {@link _pkSqlExpr}. */
|
|
5718
|
+
_serializeRowPk(table, row) {
|
|
5719
|
+
const pkCols = this._resolvedPkCols(table);
|
|
5720
|
+
const cols = pkCols.length > 0 ? pkCols : ["id"];
|
|
5721
|
+
return cols.map((c) => {
|
|
5722
|
+
const v = row[c];
|
|
5723
|
+
return v != null ? String(v) : "";
|
|
5724
|
+
}).join(_Lattice._PK_SEP);
|
|
5725
|
+
}
|
|
5726
|
+
/**
|
|
5727
|
+
* Canonical `pk` string for a {@link PkLookup} used by update/delete, so a
|
|
5728
|
+
* row addressed by lookup keys its change-log entry identically to the way
|
|
5729
|
+
* {@link _serializeRowPk} keyed it at insert time.
|
|
5730
|
+
*/
|
|
5731
|
+
_serializePkLookup(table, id) {
|
|
5732
|
+
if (typeof id === "string") return id;
|
|
5733
|
+
const pkCols = this._resolvedPkCols(table);
|
|
5734
|
+
if (pkCols.length === 0) return JSON.stringify(id);
|
|
5735
|
+
return pkCols.map((c) => {
|
|
5736
|
+
const v = id[c];
|
|
5737
|
+
return v != null ? String(v) : "";
|
|
5738
|
+
}).join(_Lattice._PK_SEP);
|
|
5739
|
+
}
|
|
5740
|
+
/**
|
|
5741
|
+
* SQL expression reconstructing {@link _serializeRowPk} from a row aliased
|
|
5742
|
+
* `t`. Returns null when the table is unkeyable (no pk columns present) — the
|
|
5743
|
+
* caller must then avoid referencing a pk column at all. Dialect-aware tab
|
|
5744
|
+
* separator: SQLite `char(9)`, Postgres `chr(9)` (both = U+0009), matching
|
|
5745
|
+
* {@link _PK_SEP}.
|
|
5746
|
+
*/
|
|
5747
|
+
_pkSqlExpr(pkCols) {
|
|
5748
|
+
if (pkCols.length === 0) return null;
|
|
5749
|
+
const sep2 = this.getDialect() === "postgres" ? "chr(9)" : "char(9)";
|
|
5750
|
+
return pkCols.map((c) => `CAST(t."${c}" AS TEXT)`).join(` || ${sep2} || `);
|
|
5751
|
+
}
|
|
5691
5752
|
/**
|
|
5692
5753
|
* Convert Filter objects into SQL clause strings and bound params.
|
|
5693
5754
|
* An `in` filter with an empty array is silently ignored (produces no clause).
|
|
@@ -6628,6 +6689,13 @@ var NATIVE_ENTITY_DEFS = {
|
|
|
6628
6689
|
columns: {
|
|
6629
6690
|
id: "TEXT PRIMARY KEY",
|
|
6630
6691
|
title: "TEXT",
|
|
6692
|
+
// Cloud user id of the member who started the thread (the operator's
|
|
6693
|
+
// `teamContext.myUserId`). A chat is PRIVATE to its author — on a team
|
|
6694
|
+
// cloud the chat routes only ever return threads whose owner matches the
|
|
6695
|
+
// requesting member. NULL on local single-user databases (no team
|
|
6696
|
+
// context) and on pre-2.2.1 threads, which the routes treat as the local
|
|
6697
|
+
// operator's own (visible only when there is no team context).
|
|
6698
|
+
owner_user_id: "TEXT",
|
|
6631
6699
|
created_at: "TEXT NOT NULL DEFAULT (datetime('now'))",
|
|
6632
6700
|
updated_at: "TEXT NOT NULL DEFAULT (datetime('now'))",
|
|
6633
6701
|
deleted_at: "TEXT"
|
|
@@ -6642,6 +6710,10 @@ var NATIVE_ENTITY_DEFS = {
|
|
|
6642
6710
|
// Soft reference to chat_threads.id. Kept as a plain column (no FK)
|
|
6643
6711
|
// to match the generic, dialect-agnostic native-entity style.
|
|
6644
6712
|
thread_id: "TEXT",
|
|
6713
|
+
// Cloud user id of the member the message belongs to — mirrors the
|
|
6714
|
+
// owning thread's owner_user_id so a message read can be filtered
|
|
6715
|
+
// independently of the thread join. NULL on local DBs / pre-2.2.1 rows.
|
|
6716
|
+
owner_user_id: "TEXT",
|
|
6645
6717
|
// user | assistant | tool | feed | system
|
|
6646
6718
|
role: "TEXT NOT NULL DEFAULT 'user'",
|
|
6647
6719
|
// JSON payload: text, tool_use / tool_result blocks, attachments, or
|
package/package.json
CHANGED