@spinabot/brigade 1.0.1 → 1.0.2
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/convex/_generated/api.d.ts +85 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/admin.d.ts +57 -0
- package/convex/admin.ts +315 -0
- package/convex/auth.d.ts +159 -0
- package/convex/auth.ts +217 -0
- package/convex/blobs.d.ts +38 -0
- package/convex/blobs.ts +115 -0
- package/convex/channels.d.ts +150 -0
- package/convex/channels.ts +455 -0
- package/convex/config.d.ts +67 -0
- package/convex/config.ts +168 -0
- package/convex/cron.d.ts +237 -0
- package/convex/cron.ts +199 -0
- package/convex/execApprovals.d.ts +31 -0
- package/convex/execApprovals.ts +58 -0
- package/convex/extensions.d.ts +30 -0
- package/convex/extensions.ts +51 -0
- package/convex/health.d.ts +18 -0
- package/convex/health.ts +69 -0
- package/convex/instance.d.ts +34 -0
- package/convex/instance.ts +82 -0
- package/convex/logs.d.ts +178 -0
- package/convex/logs.ts +253 -0
- package/convex/memory.d.ts +354 -0
- package/convex/memory.ts +536 -0
- package/convex/messages.d.ts +124 -0
- package/convex/messages.ts +347 -0
- package/convex/org.d.ts +75 -0
- package/convex/org.ts +99 -0
- package/convex/schema.d.ts +1130 -0
- package/convex/schema.ts +847 -0
- package/convex/sessions.d.ts +100 -0
- package/convex/sessions.ts +105 -0
- package/convex/skills.d.ts +73 -0
- package/convex/skills.ts +102 -0
- package/convex/subagents.d.ts +214 -0
- package/convex/subagents.ts +99 -0
- package/convex/tsconfig.json +23 -0
- package/convex/whatsappAuth.d.ts +52 -0
- package/convex/whatsappAuth.ts +151 -0
- package/convex/workspace.d.ts +49 -0
- package/convex/workspace.ts +106 -0
- package/dist/buildstamp.json +1 -1
- package/package.json +7 -1
- package/scripts/convex-dev.mjs +321 -0
- package/scripts/convex-push.mjs +69 -0
- package/scripts/install-convex.mjs +123 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// convex/messages.ts — sessionTranscriptRecords + sessionInboxEvents
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import type { Doc } from "./_generated/dataModel.js";
|
|
4
|
+
import { mutation, query, type MutationCtx } from "./_generated/server.js";
|
|
5
|
+
|
|
6
|
+
// Max sealed bytes per ROW. Convex caps a single DOCUMENT at 1 MiB; a record
|
|
7
|
+
// whose sealed payload exceeds this is split across several consecutive rows
|
|
8
|
+
// (chunkIndex / chunkCount) so no row approaches the cap. 768 KiB leaves
|
|
9
|
+
// generous headroom for the row's other fields + index overhead AND stays
|
|
10
|
+
// below Convex's ~1 MB "large document" WARN threshold. The reader
|
|
11
|
+
// concatenates the slices in seq order before decrypting.
|
|
12
|
+
const MAX_CHUNK_BYTES = 768 * 1024;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Insert one logical transcript record as 1+ chunk rows starting at
|
|
16
|
+
* `startSeq`; returns the next free seq. A record at or under the per-row
|
|
17
|
+
* cap is a single row (chunk fields unset — byte-identical to the legacy
|
|
18
|
+
* shape). A larger record is sliced into `ceil(size / MAX_CHUNK_BYTES)`
|
|
19
|
+
* rows that share `chunkCount` and carry sequential `chunkIndex`. All slices
|
|
20
|
+
* of one record are inserted inside the SAME mutation, so the group is
|
|
21
|
+
* atomic — a crash can never leave it torn.
|
|
22
|
+
*/
|
|
23
|
+
async function insertRecordChunked(
|
|
24
|
+
ctx: MutationCtx,
|
|
25
|
+
agentId: string,
|
|
26
|
+
sessionId: string,
|
|
27
|
+
startSeq: number,
|
|
28
|
+
now: number,
|
|
29
|
+
rec: { type: string; customType?: string; payload: ArrayBuffer },
|
|
30
|
+
): Promise<number> {
|
|
31
|
+
const bytes = rec.payload;
|
|
32
|
+
const total = bytes.byteLength;
|
|
33
|
+
const customType = rec.customType !== undefined ? { customType: rec.customType } : {};
|
|
34
|
+
if (total <= MAX_CHUNK_BYTES) {
|
|
35
|
+
await ctx.db.insert("sessionTranscriptRecords", {
|
|
36
|
+
agentId,
|
|
37
|
+
sessionId,
|
|
38
|
+
seq: startSeq,
|
|
39
|
+
type: rec.type,
|
|
40
|
+
...customType,
|
|
41
|
+
payload: bytes,
|
|
42
|
+
createdAt: now,
|
|
43
|
+
});
|
|
44
|
+
return startSeq + 1;
|
|
45
|
+
}
|
|
46
|
+
const chunkCount = Math.ceil(total / MAX_CHUNK_BYTES);
|
|
47
|
+
let seq = startSeq;
|
|
48
|
+
for (let i = 0; i < chunkCount; i += 1) {
|
|
49
|
+
const slice = bytes.slice(i * MAX_CHUNK_BYTES, Math.min((i + 1) * MAX_CHUNK_BYTES, total));
|
|
50
|
+
await ctx.db.insert("sessionTranscriptRecords", {
|
|
51
|
+
agentId,
|
|
52
|
+
sessionId,
|
|
53
|
+
seq,
|
|
54
|
+
type: rec.type,
|
|
55
|
+
...customType,
|
|
56
|
+
payload: slice,
|
|
57
|
+
chunkIndex: i,
|
|
58
|
+
chunkCount,
|
|
59
|
+
createdAt: now,
|
|
60
|
+
});
|
|
61
|
+
seq += 1;
|
|
62
|
+
}
|
|
63
|
+
return seq;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const appendRecord = mutation({
|
|
67
|
+
args: {
|
|
68
|
+
agentId: v.string(),
|
|
69
|
+
sessionId: v.string(),
|
|
70
|
+
type: v.string(),
|
|
71
|
+
customType: v.optional(v.string()),
|
|
72
|
+
payload: v.bytes(),
|
|
73
|
+
},
|
|
74
|
+
handler: async (ctx, args) => {
|
|
75
|
+
// Compute next seq under the agent+session lane. Convex serialises
|
|
76
|
+
// mutations on the same row keys, so this is race-safe.
|
|
77
|
+
const tail = await ctx.db
|
|
78
|
+
.query("sessionTranscriptRecords")
|
|
79
|
+
.withIndex("by_session_seq", (q) =>
|
|
80
|
+
q.eq("agentId", args.agentId).eq("sessionId", args.sessionId),
|
|
81
|
+
)
|
|
82
|
+
.order("desc")
|
|
83
|
+
.first();
|
|
84
|
+
const startSeq = (tail?.seq ?? 0) + 1;
|
|
85
|
+
await insertRecordChunked(ctx, args.agentId, args.sessionId, startSeq, Date.now(), {
|
|
86
|
+
type: args.type,
|
|
87
|
+
...(args.customType !== undefined ? { customType: args.customType } : {}),
|
|
88
|
+
payload: args.payload,
|
|
89
|
+
});
|
|
90
|
+
return { seq: startSeq };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Ordered batch append — the convex-mode SessionManager write-behind queue
|
|
95
|
+
* flushes whole batches in one transaction so a mid-batch crash can't leave
|
|
96
|
+
* a torn parent-id chain. */
|
|
97
|
+
export const appendRecordsBatch = mutation({
|
|
98
|
+
args: {
|
|
99
|
+
agentId: v.string(),
|
|
100
|
+
sessionId: v.string(),
|
|
101
|
+
records: v.array(
|
|
102
|
+
v.object({
|
|
103
|
+
type: v.string(),
|
|
104
|
+
customType: v.optional(v.string()),
|
|
105
|
+
payload: v.bytes(),
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
handler: async (ctx, args) => {
|
|
110
|
+
const tail = await ctx.db
|
|
111
|
+
.query("sessionTranscriptRecords")
|
|
112
|
+
.withIndex("by_session_seq", (q) =>
|
|
113
|
+
q.eq("agentId", args.agentId).eq("sessionId", args.sessionId),
|
|
114
|
+
)
|
|
115
|
+
.order("desc")
|
|
116
|
+
.first();
|
|
117
|
+
let seq = (tail?.seq ?? 0) + 1;
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
for (const r of args.records) {
|
|
120
|
+
seq = await insertRecordChunked(ctx, args.agentId, args.sessionId, seq, now, r);
|
|
121
|
+
}
|
|
122
|
+
return { lastSeq: seq - 1 };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** Wholesale transcript replace — realises Pi's `_rewriteFile` (v1→v3
|
|
127
|
+
* migration, branch extraction) as one transaction. */
|
|
128
|
+
export const replaceTranscript = mutation({
|
|
129
|
+
args: {
|
|
130
|
+
agentId: v.string(),
|
|
131
|
+
sessionId: v.string(),
|
|
132
|
+
records: v.array(
|
|
133
|
+
v.object({
|
|
134
|
+
type: v.string(),
|
|
135
|
+
customType: v.optional(v.string()),
|
|
136
|
+
payload: v.bytes(),
|
|
137
|
+
}),
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
handler: async (ctx, args) => {
|
|
141
|
+
const existing = await ctx.db
|
|
142
|
+
.query("sessionTranscriptRecords")
|
|
143
|
+
.withIndex("by_session_seq", (q) =>
|
|
144
|
+
q.eq("agentId", args.agentId).eq("sessionId", args.sessionId),
|
|
145
|
+
)
|
|
146
|
+
.collect();
|
|
147
|
+
for (const r of existing) await ctx.db.delete(r._id);
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
let seq = 1;
|
|
150
|
+
for (const r of args.records) {
|
|
151
|
+
seq = await insertRecordChunked(ctx, args.agentId, args.sessionId, seq, now, r);
|
|
152
|
+
}
|
|
153
|
+
return { count: args.records.length };
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export const readTranscript = query({
|
|
158
|
+
args: {
|
|
159
|
+
agentId: v.string(),
|
|
160
|
+
sessionId: v.string(),
|
|
161
|
+
limit: v.optional(v.number()),
|
|
162
|
+
// Cursor for pagination: return only records with seq > afterSeq. The
|
|
163
|
+
// client loops with the last page's max seq so a transcript larger than
|
|
164
|
+
// Convex's per-query read cap (~16k docs / 8MB) is read across calls
|
|
165
|
+
// instead of silently truncating at `take(limit)`.
|
|
166
|
+
afterSeq: v.optional(v.number()),
|
|
167
|
+
},
|
|
168
|
+
handler: async (ctx, args) => {
|
|
169
|
+
// Page cap by BOTH count AND bytes. `take(limit)` reads up to `limit`
|
|
170
|
+
// whole documents — but with chunked transcript records (each up to
|
|
171
|
+
// ~768 KiB) even ~20 rows blow Convex's 16 MiB per-EXECUTION read limit
|
|
172
|
+
// before the count cap is hit (the "Too many bytes read" crash at this
|
|
173
|
+
// handler). So iterate lazily and stop at a byte budget well under
|
|
174
|
+
// 16 MiB; the client pages with afterSeq until it receives an EMPTY page.
|
|
175
|
+
const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 4000) : 1000;
|
|
176
|
+
const after = args.afterSeq;
|
|
177
|
+
const BYTE_BUDGET = 8 * 1024 * 1024; // 8 MiB — half the 16 MiB exec read cap
|
|
178
|
+
const cursor = ctx.db
|
|
179
|
+
.query("sessionTranscriptRecords")
|
|
180
|
+
.withIndex("by_session_seq", (q) =>
|
|
181
|
+
after !== undefined
|
|
182
|
+
? q.eq("agentId", args.agentId).eq("sessionId", args.sessionId).gt("seq", after)
|
|
183
|
+
: q.eq("agentId", args.agentId).eq("sessionId", args.sessionId),
|
|
184
|
+
)
|
|
185
|
+
.order("asc");
|
|
186
|
+
const rows: Doc<"sessionTranscriptRecords">[] = [];
|
|
187
|
+
let bytes = 0;
|
|
188
|
+
for await (const row of cursor) {
|
|
189
|
+
const sz = row.payload?.byteLength ?? 0;
|
|
190
|
+
// Stop BEFORE exceeding the budget, but always return at least one
|
|
191
|
+
// row so the client makes forward progress even on an oversized one.
|
|
192
|
+
if (rows.length > 0 && bytes + sz > BYTE_BUDGET) break;
|
|
193
|
+
rows.push(row);
|
|
194
|
+
bytes += sz;
|
|
195
|
+
if (rows.length >= limit) break;
|
|
196
|
+
}
|
|
197
|
+
return rows;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/** Newest-first tail of (type, customType) only — for the bootstrap-delivery
|
|
202
|
+
* check, which must honour compaction-invalidation (a compaction newer than
|
|
203
|
+
* the marker means the bootstrap context was compacted out → re-deliver).
|
|
204
|
+
* Returns just the two fields the walk needs, not the sealed payloads. */
|
|
205
|
+
export const readMarkerTail = query({
|
|
206
|
+
args: { agentId: v.string(), sessionId: v.string(), limit: v.optional(v.number()) },
|
|
207
|
+
handler: async (ctx, args) => {
|
|
208
|
+
// Walk newest-first to find the most recent bootstrap-marker / compaction.
|
|
209
|
+
// `take(limit)` reads FULL documents (payloads included) just to look at
|
|
210
|
+
// type/customType — on a media-heavy session even a few hundred rows blow
|
|
211
|
+
// Convex's 16 MiB per-execution read cap. Iterate lazily, cap by bytes,
|
|
212
|
+
// and stop as soon as a `compaction` row is seen (the caller decides on
|
|
213
|
+
// the first marker/compaction anyway). Returns only {type, customType} —
|
|
214
|
+
// never the payload.
|
|
215
|
+
const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 1000) : 500;
|
|
216
|
+
const BYTE_BUDGET = 8 * 1024 * 1024; // 8 MiB — half the 16 MiB exec read cap
|
|
217
|
+
const out: Array<{ type: string; customType?: string }> = [];
|
|
218
|
+
let bytes = 0;
|
|
219
|
+
for await (const r of ctx.db
|
|
220
|
+
.query("sessionTranscriptRecords")
|
|
221
|
+
.withIndex("by_session_seq", (q) =>
|
|
222
|
+
q.eq("agentId", args.agentId).eq("sessionId", args.sessionId),
|
|
223
|
+
)
|
|
224
|
+
.order("desc")) {
|
|
225
|
+
out.push({ type: r.type, customType: r.customType });
|
|
226
|
+
// A compaction row is the caller's stopping point (it invalidates any
|
|
227
|
+
// older bootstrap marker) — no need to read further back.
|
|
228
|
+
if (r.type === "compaction") break;
|
|
229
|
+
bytes += r.payload?.byteLength ?? 0;
|
|
230
|
+
if (out.length >= limit || bytes >= BYTE_BUDGET) break;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
export const deleteTranscript = mutation({
|
|
237
|
+
args: { agentId: v.string(), sessionId: v.string() },
|
|
238
|
+
handler: async (ctx, args) => {
|
|
239
|
+
// Delete ONE bounded page and return how many rows were removed; the
|
|
240
|
+
// client loops until this returns 0. `.collect()` + delete-all would blow
|
|
241
|
+
// BOTH the 16 MiB per-execution READ cap (chunked rows are up to 768 KiB
|
|
242
|
+
// each) AND the ~8k-delete-per-mutation cap on a long/media-heavy session.
|
|
243
|
+
// Lossless — every row is eventually deleted across the client's calls.
|
|
244
|
+
const BYTE_BUDGET = 8 * 1024 * 1024; // half the 16 MiB read cap
|
|
245
|
+
const MAX_DELETE = 2000; // stay well under the per-mutation delete cap
|
|
246
|
+
const ids: Array<Doc<"sessionTranscriptRecords">["_id"]> = [];
|
|
247
|
+
let bytes = 0;
|
|
248
|
+
for await (const r of ctx.db
|
|
249
|
+
.query("sessionTranscriptRecords")
|
|
250
|
+
.withIndex("by_session_seq", (q) =>
|
|
251
|
+
q.eq("agentId", args.agentId).eq("sessionId", args.sessionId),
|
|
252
|
+
)
|
|
253
|
+
.order("asc")) {
|
|
254
|
+
const sz = r.payload?.byteLength ?? 0;
|
|
255
|
+
if (ids.length > 0 && bytes + sz > BYTE_BUDGET) break;
|
|
256
|
+
ids.push(r._id);
|
|
257
|
+
bytes += sz;
|
|
258
|
+
if (ids.length >= MAX_DELETE) break;
|
|
259
|
+
}
|
|
260
|
+
for (const id of ids) await ctx.db.delete(id);
|
|
261
|
+
return ids.length;
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Inbox (sessionInboxEvents)
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
export const inboxEnqueue = mutation({
|
|
270
|
+
args: {
|
|
271
|
+
sessionKey: v.string(),
|
|
272
|
+
text: v.bytes(),
|
|
273
|
+
// Client-supplied ts — areSystemEventsEqual (session-inbox.ts) does
|
|
274
|
+
// ts-equality during prefix matching, so we MUST preserve the
|
|
275
|
+
// producer's timestamp rather than stamping our own.
|
|
276
|
+
ts: v.optional(v.number()),
|
|
277
|
+
contextKey: v.optional(v.string()),
|
|
278
|
+
deliveryContext: v.optional(v.any()),
|
|
279
|
+
trusted: v.boolean(),
|
|
280
|
+
},
|
|
281
|
+
handler: async (ctx, args) => {
|
|
282
|
+
const tail = await ctx.db
|
|
283
|
+
.query("sessionInboxEvents")
|
|
284
|
+
.withIndex("by_session_seq", (q) => q.eq("sessionKey", args.sessionKey))
|
|
285
|
+
.order("desc")
|
|
286
|
+
.first();
|
|
287
|
+
const seq = (tail?.seq ?? 0) + 1;
|
|
288
|
+
await ctx.db.insert("sessionInboxEvents", {
|
|
289
|
+
sessionKey: args.sessionKey,
|
|
290
|
+
seq,
|
|
291
|
+
text: args.text,
|
|
292
|
+
ts: args.ts ?? Date.now(),
|
|
293
|
+
...(args.contextKey !== undefined ? { contextKey: args.contextKey } : {}),
|
|
294
|
+
...(args.deliveryContext !== undefined ? { deliveryContext: args.deliveryContext } : {}),
|
|
295
|
+
trusted: args.trusted,
|
|
296
|
+
});
|
|
297
|
+
return { seq };
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
export const inboxPeek = query({
|
|
302
|
+
args: { sessionKey: v.string() },
|
|
303
|
+
handler: async (ctx, args) => {
|
|
304
|
+
return ctx.db
|
|
305
|
+
.query("sessionInboxEvents")
|
|
306
|
+
.withIndex("by_session_seq", (q) => q.eq("sessionKey", args.sessionKey))
|
|
307
|
+
.order("asc")
|
|
308
|
+
.collect();
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
export const inboxDrain = mutation({
|
|
313
|
+
args: { sessionKey: v.string() },
|
|
314
|
+
handler: async (ctx, args) => {
|
|
315
|
+
const rows = await ctx.db
|
|
316
|
+
.query("sessionInboxEvents")
|
|
317
|
+
.withIndex("by_session_seq", (q) => q.eq("sessionKey", args.sessionKey))
|
|
318
|
+
.order("asc")
|
|
319
|
+
.collect();
|
|
320
|
+
for (const r of rows) await ctx.db.delete(r._id);
|
|
321
|
+
return rows;
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
export const inboxConsumePrefix = mutation({
|
|
326
|
+
args: { sessionKey: v.string(), prefixLength: v.number() },
|
|
327
|
+
handler: async (ctx, args) => {
|
|
328
|
+
const rows = await ctx.db
|
|
329
|
+
.query("sessionInboxEvents")
|
|
330
|
+
.withIndex("by_session_seq", (q) => q.eq("sessionKey", args.sessionKey))
|
|
331
|
+
.order("asc")
|
|
332
|
+
.take(args.prefixLength);
|
|
333
|
+
for (const r of rows) await ctx.db.delete(r._id);
|
|
334
|
+
return rows;
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
export const inboxHasEvents = query({
|
|
339
|
+
args: { sessionKey: v.string() },
|
|
340
|
+
handler: async (ctx, args) => {
|
|
341
|
+
const tail = await ctx.db
|
|
342
|
+
.query("sessionInboxEvents")
|
|
343
|
+
.withIndex("by_session_seq", (q) => q.eq("sessionKey", args.sessionKey))
|
|
344
|
+
.first();
|
|
345
|
+
return tail !== null;
|
|
346
|
+
},
|
|
347
|
+
});
|
package/convex/org.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export declare const appendDeriveAudit: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
mode: "open" | "explicit" | "derived";
|
|
3
|
+
ts: string;
|
|
4
|
+
topOrder: string;
|
|
5
|
+
ownerId: string;
|
|
6
|
+
edgeCount: number;
|
|
7
|
+
memberCount: number;
|
|
8
|
+
extraAllowCount: number;
|
|
9
|
+
extraDenyCount: number;
|
|
10
|
+
warnings: number;
|
|
11
|
+
}, Promise<void>>;
|
|
12
|
+
export declare const listDeriveAudit: import("convex/server").RegisteredQuery<"public", {
|
|
13
|
+
limit?: number | undefined;
|
|
14
|
+
ownerId: string;
|
|
15
|
+
}, Promise<{
|
|
16
|
+
_id: import("convex/values").GenericId<"orgDeriveAudit">;
|
|
17
|
+
_creationTime: number;
|
|
18
|
+
mode: "open" | "explicit" | "derived";
|
|
19
|
+
ts: string;
|
|
20
|
+
topOrder: string;
|
|
21
|
+
ownerId: string;
|
|
22
|
+
edgeCount: number;
|
|
23
|
+
memberCount: number;
|
|
24
|
+
extraAllowCount: number;
|
|
25
|
+
extraDenyCount: number;
|
|
26
|
+
warnings: number;
|
|
27
|
+
}[]>>;
|
|
28
|
+
export declare const getChart: import("convex/server").RegisteredQuery<"public", {
|
|
29
|
+
hash: string;
|
|
30
|
+
ownerId: string;
|
|
31
|
+
}, Promise<{
|
|
32
|
+
_id: import("convex/values").GenericId<"orgChartCache">;
|
|
33
|
+
_creationTime: number;
|
|
34
|
+
transient: boolean;
|
|
35
|
+
mtimeMs: number;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
themeId: string;
|
|
39
|
+
themeName: string;
|
|
40
|
+
mimeType: "image/png";
|
|
41
|
+
hash: string;
|
|
42
|
+
ownerId: string;
|
|
43
|
+
pngBytes: ArrayBuffer;
|
|
44
|
+
} | null>>;
|
|
45
|
+
export declare const putChart: import("convex/server").RegisteredMutation<"public", {
|
|
46
|
+
transient?: boolean | undefined;
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
themeId: string;
|
|
50
|
+
themeName: string;
|
|
51
|
+
hash: string;
|
|
52
|
+
ownerId: string;
|
|
53
|
+
pngBytes: ArrayBuffer;
|
|
54
|
+
}, Promise<void>>;
|
|
55
|
+
export declare const deleteChart: import("convex/server").RegisteredMutation<"public", {
|
|
56
|
+
hash: string;
|
|
57
|
+
ownerId: string;
|
|
58
|
+
}, Promise<void>>;
|
|
59
|
+
export declare const listCharts: import("convex/server").RegisteredQuery<"public", {
|
|
60
|
+
ownerId: string;
|
|
61
|
+
}, Promise<{
|
|
62
|
+
_id: import("convex/values").GenericId<"orgChartCache">;
|
|
63
|
+
_creationTime: number;
|
|
64
|
+
transient: boolean;
|
|
65
|
+
mtimeMs: number;
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
themeId: string;
|
|
69
|
+
themeName: string;
|
|
70
|
+
mimeType: "image/png";
|
|
71
|
+
hash: string;
|
|
72
|
+
ownerId: string;
|
|
73
|
+
pngBytes: ArrayBuffer;
|
|
74
|
+
}[]>>;
|
|
75
|
+
//# sourceMappingURL=org.d.ts.map
|
package/convex/org.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// convex/org.ts — orgDeriveAudit + orgChartCache
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import { mutation, query } from "./_generated/server.js";
|
|
4
|
+
|
|
5
|
+
const Mode = v.union(v.literal("derived"), v.literal("explicit"), v.literal("open"));
|
|
6
|
+
|
|
7
|
+
export const appendDeriveAudit = mutation({
|
|
8
|
+
args: {
|
|
9
|
+
ownerId: v.string(),
|
|
10
|
+
ts: v.string(),
|
|
11
|
+
topOrder: v.string(),
|
|
12
|
+
mode: Mode,
|
|
13
|
+
edgeCount: v.number(),
|
|
14
|
+
memberCount: v.number(),
|
|
15
|
+
extraAllowCount: v.number(),
|
|
16
|
+
extraDenyCount: v.number(),
|
|
17
|
+
warnings: v.number(),
|
|
18
|
+
},
|
|
19
|
+
handler: async (ctx, args) => {
|
|
20
|
+
await ctx.db.insert("orgDeriveAudit", args);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const listDeriveAudit = query({
|
|
25
|
+
args: { ownerId: v.string(), limit: v.optional(v.number()) },
|
|
26
|
+
handler: async (ctx, args) => {
|
|
27
|
+
const limit = args.limit && args.limit > 0 ? args.limit : 50;
|
|
28
|
+
return ctx.db
|
|
29
|
+
.query("orgDeriveAudit")
|
|
30
|
+
.withIndex("by_owner_ts", (q) => q.eq("ownerId", args.ownerId))
|
|
31
|
+
.order("desc")
|
|
32
|
+
.take(limit);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const getChart = query({
|
|
37
|
+
args: { ownerId: v.string(), hash: v.string() },
|
|
38
|
+
handler: async (ctx, args) => {
|
|
39
|
+
return ctx.db
|
|
40
|
+
.query("orgChartCache")
|
|
41
|
+
.withIndex("by_owner_hash", (q) => q.eq("ownerId", args.ownerId).eq("hash", args.hash))
|
|
42
|
+
.first();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const putChart = mutation({
|
|
47
|
+
args: {
|
|
48
|
+
ownerId: v.string(),
|
|
49
|
+
hash: v.string(),
|
|
50
|
+
pngBytes: v.bytes(),
|
|
51
|
+
width: v.number(),
|
|
52
|
+
height: v.number(),
|
|
53
|
+
themeId: v.string(),
|
|
54
|
+
themeName: v.string(),
|
|
55
|
+
transient: v.optional(v.boolean()),
|
|
56
|
+
},
|
|
57
|
+
handler: async (ctx, args) => {
|
|
58
|
+
const existing = await ctx.db
|
|
59
|
+
.query("orgChartCache")
|
|
60
|
+
.withIndex("by_owner_hash", (q) => q.eq("ownerId", args.ownerId).eq("hash", args.hash))
|
|
61
|
+
.first();
|
|
62
|
+
const payload = {
|
|
63
|
+
ownerId: args.ownerId,
|
|
64
|
+
hash: args.hash,
|
|
65
|
+
pngBytes: args.pngBytes,
|
|
66
|
+
width: args.width,
|
|
67
|
+
height: args.height,
|
|
68
|
+
themeId: args.themeId,
|
|
69
|
+
themeName: args.themeName,
|
|
70
|
+
mimeType: "image/png" as const,
|
|
71
|
+
mtimeMs: Date.now(),
|
|
72
|
+
transient: args.transient ?? false,
|
|
73
|
+
};
|
|
74
|
+
if (existing) await ctx.db.replace(existing._id, payload);
|
|
75
|
+
else await ctx.db.insert("orgChartCache", payload);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const deleteChart = mutation({
|
|
80
|
+
args: { ownerId: v.string(), hash: v.string() },
|
|
81
|
+
handler: async (ctx, args) => {
|
|
82
|
+
const existing = await ctx.db
|
|
83
|
+
.query("orgChartCache")
|
|
84
|
+
.withIndex("by_owner_hash", (q) => q.eq("ownerId", args.ownerId).eq("hash", args.hash))
|
|
85
|
+
.first();
|
|
86
|
+
if (existing) await ctx.db.delete(existing._id);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const listCharts = query({
|
|
91
|
+
args: { ownerId: v.string() },
|
|
92
|
+
handler: async (ctx, args) => {
|
|
93
|
+
return ctx.db
|
|
94
|
+
.query("orgChartCache")
|
|
95
|
+
.withIndex("by_owner_mtime", (q) => q.eq("ownerId", args.ownerId))
|
|
96
|
+
.order("desc")
|
|
97
|
+
.collect();
|
|
98
|
+
},
|
|
99
|
+
});
|