@voltro/plugin-comments 0.84.0 → 0.86.0

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/index.js CHANGED
@@ -1,94 +1,142 @@
1
- import { CommentAccessRefused as e, CommentNotFound as t, CommentNotYours as n } from "./errors.js";
2
- import { a as r, c as i, d as a, f as o, i as s, l as c, m as l, n as u, o as d, p as f, r as p, s as m, t as h, u as g } from "./rpc-DlFs6xXs.js";
3
- import { Effect as _, Option as v } from "effect";
4
- import { and as y, contains as b, eq as x, generateId as S, id as C, inSet as w, json as T, neq as E, or as D, table as O, text as k, timestamp as A } from "@voltro/database";
5
- import { definePlugin as j, pluginInstanceName as M, publishReactivity as N } from "@voltro/protocol";
6
- import { NotificationService as P } from "@voltro/plugin-notifications";
1
+ import { CommentAccessRefused as e, CommentNotFound as t, CommentNotYours as n, CommentStatusRefused as r } from "./errors.js";
2
+ import { QUOTE_MAX_LENGTH as i, findQuote as a, textQuote as o } from "./quote.js";
3
+ import { BASE_NAME as s, COMMENT_THREAD_KINDS as c, commentsFeed as l, commentsRpcClientImports as u, createDescriptor as d, deleteDescriptor as f, editDescriptor as p, listDescriptor as m, markReadDescriptor as h, mentionSearchDescriptor as g, reactDescriptor as _, resolveDescriptor as v } from "./rpc.js";
4
+ import { Effect as y, Option as b } from "effect";
5
+ import { and as x, contains as S, eq as C, generateId as w, id as T, inSet as E, json as D, neq as O, or as k, table as A, text as j, timestamp as M } from "@voltro/database";
6
+ import { definePlugin as N, pluginInstanceName as P, publishReactivity as F } from "@voltro/protocol";
7
+ import { NotificationService as I } from "@voltro/plugin-notifications";
7
8
  //#region src/index.ts
8
- var F = O("_voltro_comment_threads", {
9
- id: C({ prefix: "cmtthr" }),
10
- anchor: k(),
11
- quote: T().nullable(),
12
- status: k().default("open"),
13
- createdBy: k(),
14
- createdAt: A().default("now"),
15
- tenantId: k().nullable()
16
- }).index("byThreadAnchor", ["anchor", "tenantId"]), I = O("_voltro_comments", {
17
- id: C({ prefix: "cmt" }),
18
- threadId: k(),
19
- body: k(),
20
- authorSubjectId: k(),
21
- mentions: T(),
22
- editedAt: A().nullable(),
23
- createdAt: A().default("now"),
24
- tenantId: k().nullable()
25
- }).index("byCommentThread", ["threadId"]), L = O("_voltro_comment_reactions", {
26
- id: C({ prefix: "cmtrx" }),
27
- commentId: k(),
28
- subjectId: k(),
29
- emoji: k()
9
+ var L = A("_voltro_comment_threads", {
10
+ id: T({ prefix: "cmtthr" }),
11
+ anchor: j(),
12
+ quote: D().nullable(),
13
+ kind: j().default("discussion"),
14
+ status: j().default("open"),
15
+ createdBy: j(),
16
+ createdAt: M().default("now"),
17
+ tenantId: j().nullable()
18
+ }).index("byThreadAnchor", ["anchor", "tenantId"]), R = A("_voltro_comments", {
19
+ id: T({ prefix: "cmt" }),
20
+ threadId: j(),
21
+ body: j(),
22
+ authorSubjectId: j(),
23
+ mentions: D(),
24
+ editedAt: M().nullable(),
25
+ createdAt: M().default("now"),
26
+ tenantId: j().nullable()
27
+ }).index("byCommentThread", ["threadId"]), z = A("_voltro_comment_thread_events", {
28
+ id: T({ prefix: "cmtevt" }),
29
+ threadId: j(),
30
+ fromStatus: j().nullable(),
31
+ toStatus: j(),
32
+ actor: j(),
33
+ reason: j().nullable(),
34
+ at: M().default("now"),
35
+ tenantId: j().nullable()
36
+ }).index("byThreadEvents", ["threadId"]), B = A("_voltro_comment_reactions", {
37
+ id: T({ prefix: "cmtrx" }),
38
+ commentId: j(),
39
+ subjectId: j(),
40
+ emoji: j()
30
41
  }).unique("byReactionIdentity", [
31
42
  "commentId",
32
43
  "subjectId",
33
44
  "emoji"
34
- ]).index("byReactionComment", ["commentId"]), R = O("_voltro_comment_reads", {
35
- id: C({ prefix: "cmtread" }),
36
- threadId: k(),
37
- subjectId: k(),
38
- lastReadAt: A()
39
- }).unique("byReadThreadSubject", ["threadId", "subjectId"]), z = h, B = (e) => {
45
+ ]).index("byReactionComment", ["commentId"]), V = A("_voltro_comment_reads", {
46
+ id: T({ prefix: "cmtread" }),
47
+ threadId: j(),
48
+ subjectId: j(),
49
+ lastReadAt: M()
50
+ }).unique("byReadThreadSubject", ["threadId", "subjectId"]), H = s, U = (e) => {
40
51
  let t = e.request.subject ?? {};
41
52
  return {
42
53
  id: t.id ?? null,
43
54
  tenantId: t.tenantId ?? null,
44
55
  scopes: t.scopes ?? []
45
56
  };
46
- }, V = (o = {}) => {
47
- let l = M({
48
- base: z,
49
- alias: o.alias,
50
- instance: o.name
51
- }), f, h, C = async (e, t, n) => f === void 0 ? [] : f.query({
57
+ }, W = (i = {}) => {
58
+ let a = P({
59
+ base: H,
60
+ alias: i.alias,
61
+ instance: i.name
62
+ }), o, s, T = async (e, t, n) => o === void 0 ? [] : o.query({
52
63
  table: e,
53
64
  predicate: t,
54
65
  order: [],
55
66
  take: n,
56
67
  skip: void 0,
57
68
  projection: void 0
58
- }), T = () => {
59
- N(h, u);
60
- }, O = (t, n) => _.gen(function* () {
61
- let r = B(n);
62
- return o.access?.viaEntity === void 0 ? o.access?.scope === void 0 ? yield* _.fail(new e({
69
+ }), D = async (e, t, n, r) => {
70
+ let i = [], a = [...new Set(n)];
71
+ for (let n = 0; n < a.length; n += 500) {
72
+ let o = E(t, a.slice(n, n + 500));
73
+ i.push(...await T(e, r === void 0 ? o : x(o, r)));
74
+ }
75
+ return i;
76
+ }, A = (e, t) => {
77
+ let n = /* @__PURE__ */ new Map();
78
+ for (let r of e) {
79
+ let e = String(r[t]), i = n.get(e);
80
+ i === void 0 ? n.set(e, [r]) : i.push(r);
81
+ }
82
+ return n;
83
+ }, j = () => {
84
+ F(s, l);
85
+ }, M = (t, n) => y.gen(function* () {
86
+ let r = U(n);
87
+ return i.access?.viaEntity === void 0 ? i.access?.scope === void 0 ? yield* y.fail(new e({
63
88
  anchor: t,
64
89
  reason: "no access rule declared — set commentsPlugin({ access: { viaEntity } }) or { access: { scope } }"
65
- })) : r.scopes.includes(o.access.scope) ? void 0 : yield* _.fail(new e({
90
+ })) : r.scopes.includes(i.access.scope) ? void 0 : yield* y.fail(new e({
66
91
  anchor: t,
67
- reason: `missing scope '${o.access.scope}'`
68
- })) : (yield* _.promise(async () => {
92
+ reason: `missing scope '${i.access.scope}'`
93
+ })) : (yield* y.promise(async () => {
69
94
  try {
70
- return await o.access.viaEntity({
95
+ return await i.access.viaEntity({
71
96
  anchor: t,
72
97
  subject: r,
73
- store: f
98
+ store: o
74
99
  });
75
100
  } catch {
76
101
  return !1;
77
102
  }
78
- })) ? void 0 : yield* _.fail(new e({
103
+ })) ? void 0 : yield* y.fail(new e({
79
104
  anchor: t,
80
105
  reason: "the anchored entity is not readable by this caller (or no longer available)"
81
106
  }));
82
- }), k = async (e) => (await C("_voltro_comment_threads", x("id", e), 1))[0], A = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", V = (e) => _.gen(function* () {
107
+ }), W = async (e) => (await T("_voltro_comment_threads", C("id", e), 1))[0], G = (e) => ({
108
+ id: String(e.id),
109
+ anchor: String(e.anchor),
110
+ kind: c.includes(String(e.kind)) ? e.kind : "discussion",
111
+ status: e.status === "resolved" ? "resolved" : "open",
112
+ createdBy: String(e.createdBy)
113
+ }), K = (e, t) => i.visibleTo === void 0 || i.visibleTo({
114
+ thread: G(e),
115
+ subject: U(t)
116
+ }), q = async (e, t) => {
117
+ let n = await W(e);
118
+ return n !== void 0 && K(n, t) ? n : void 0;
119
+ }, J = async (e, t) => {
120
+ await e.insert("_voltro_comment_thread_events", {
121
+ id: w({
122
+ kind: "typeid",
123
+ prefix: "cmtevt"
124
+ }, "_voltro_comment_thread_events"),
125
+ threadId: t.threadId,
126
+ fromStatus: t.from,
127
+ toStatus: t.to,
128
+ actor: t.actor,
129
+ reason: t.reason,
130
+ at: (/* @__PURE__ */ new Date()).toISOString(),
131
+ tenantId: t.tenantId
132
+ });
133
+ }, Y = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", X = (e) => y.gen(function* () {
83
134
  if (e.mentions.length === 0) return;
84
- let t = yield* _.serviceOption(P);
85
- if (v.isNone(t)) {
86
- yield* _.logInfo("comments: mention recorded without notification — @voltro/plugin-notifications is not configured");
87
- return;
88
- }
89
- for (let n of e.mentions) n !== e.author && (yield* _.promise(() => t.value.send({
135
+ let t = yield* y.serviceOption(I);
136
+ if (b.isNone(t)) yield* y.logInfo("comments: mention recorded without notification — @voltro/plugin-notifications is not configured");
137
+ else for (let n of e.mentions) n !== e.author && (yield* y.promise(() => t.value.send({
90
138
  to: n,
91
- category: o.mentionCategory ?? "mention",
139
+ category: i.mentionCategory ?? "mention",
92
140
  title: "You were mentioned",
93
141
  body: e.body.length > 200 ? `${e.body.slice(0, 200)}…` : e.body,
94
142
  data: {
@@ -97,191 +145,261 @@ var F = O("_voltro_comment_threads", {
97
145
  },
98
146
  tenantId: e.tenantId
99
147
  }).then(() => void 0).catch(() => void 0)));
100
- }), H = async (e, t) => {
101
- if (e.length === 0 || o.resolveMentions === void 0) return [];
148
+ }), Z = async (e, t) => {
149
+ if (e.length === 0 || i.resolveMentions === void 0) return [];
102
150
  let n = [];
103
151
  for (let r of [...new Set(e)].slice(0, 20)) {
104
- let e = (await o.resolveMentions({
152
+ let e = (await i.resolveMentions({
105
153
  query: r,
106
154
  subject: t
107
155
  }).catch(() => [])).find((e) => e.subjectId === r);
108
- e !== void 0 && (o.crossTenant === !0 || e.tenantId === void 0 || e.tenantId === null || e.tenantId === t.tenantId) && n.push(r);
156
+ e !== void 0 && (i.crossTenant === !0 || e.tenantId === void 0 || e.tenantId === null || e.tenantId === t.tenantId) && n.push(r);
109
157
  }
110
158
  return n;
111
- }, U = async (e, t, n) => {
112
- let r = await C("_voltro_comment_threads", y(x("anchor", e), x("tenantId", t))), i = [];
113
- for (let e of r) {
114
- let t = [...await C("_voltro_comments", x("threadId", String(e.id)))];
115
- t.sort((e, t) => A(e.createdAt) < A(t.createdAt) ? -1 : 1);
116
- let r = n === null ? [] : await C("_voltro_comment_reads", y(x("threadId", String(e.id)), x("subjectId", n)), 1), a = r[0] ? A(r[0].lastReadAt) : "", o = [];
159
+ }, Q = async (e, t) => {
160
+ let { tenantId: n, id: r } = U(t), i = (await T("_voltro_comment_threads", x(C("anchor", e), C("tenantId", n)))).filter((e) => K(e, t));
161
+ if (i.length === 0) return [];
162
+ let a = i.map((e) => String(e.id)), [o, s, c] = await Promise.all([
163
+ D("_voltro_comments", "threadId", a),
164
+ r === null ? Promise.resolve([]) : D("_voltro_comment_reads", "threadId", a, C("subjectId", r)),
165
+ D("_voltro_comment_thread_events", "threadId", a)
166
+ ]), l = A(o, "threadId"), u = A(s, "threadId"), d = A(c, "threadId"), f = A(await D("_voltro_comment_reactions", "commentId", o.map((e) => String(e.id))), "commentId"), p = [];
167
+ for (let e of i) {
168
+ let t = l.get(String(e.id)) ?? [];
169
+ t.sort((e, t) => Y(e.createdAt) < Y(t.createdAt) ? -1 : 1);
170
+ let n = u.get(String(e.id)) ?? [], i = n[0] ? Y(n[0].lastReadAt) : "", a = [];
117
171
  for (let e of t) {
118
- let t = await C("_voltro_comment_reactions", x("commentId", String(e.id))), r = /* @__PURE__ */ new Map();
172
+ let t = f.get(String(e.id)) ?? [], n = /* @__PURE__ */ new Map();
119
173
  for (let e of t) {
120
- let t = r.get(String(e.emoji)) ?? {
174
+ let t = n.get(String(e.emoji)) ?? {
121
175
  count: 0,
122
176
  mine: !1
123
177
  };
124
- t.count += 1, String(e.subjectId) === n && (t.mine = !0), r.set(String(e.emoji), t);
178
+ t.count += 1, String(e.subjectId) === r && (t.mine = !0), n.set(String(e.emoji), t);
125
179
  }
126
- o.push({
180
+ a.push({
127
181
  id: String(e.id),
128
182
  threadId: String(e.threadId),
129
183
  body: String(e.body),
130
184
  authorSubjectId: String(e.authorSubjectId),
131
185
  mentions: e.mentions ?? [],
132
- editedAt: e.editedAt == null ? null : A(e.editedAt),
133
- createdAt: A(e.createdAt),
134
- reactions: [...r.entries()].map(([e, t]) => ({
186
+ editedAt: e.editedAt == null ? null : Y(e.editedAt),
187
+ createdAt: Y(e.createdAt),
188
+ reactions: [...n.entries()].map(([e, t]) => ({
135
189
  emoji: e,
136
190
  count: t.count,
137
191
  mine: t.mine
138
192
  }))
139
193
  });
140
194
  }
141
- i.push({
142
- id: String(e.id),
143
- anchor: String(e.anchor),
195
+ let o = d.get(String(e.id)) ?? [];
196
+ o.sort((e, t) => Y(e.at) < Y(t.at) ? -1 : Y(e.at) > Y(t.at) ? 1 : String(e.id) < String(t.id) ? -1 : 1);
197
+ let s = G(e);
198
+ p.push({
199
+ id: s.id,
200
+ anchor: s.anchor,
201
+ kind: s.kind,
144
202
  quote: e.quote ?? null,
145
- status: e.status === "resolved" ? "resolved" : "open",
203
+ status: s.status,
146
204
  createdBy: String(e.createdBy),
147
- createdAt: A(e.createdAt),
148
- comments: o,
149
- unreadCount: o.filter((e) => String(e.authorSubjectId) !== n && String(e.createdAt) > a).length
205
+ createdAt: Y(e.createdAt),
206
+ comments: a,
207
+ unreadCount: a.filter((e) => String(e.authorSubjectId) !== r && String(e.createdAt) > i).length,
208
+ history: o.map((e) => ({
209
+ from: e.fromStatus === "open" || e.fromStatus === "resolved" ? e.fromStatus : null,
210
+ to: e.toStatus === "resolved" ? "resolved" : "open",
211
+ by: String(e.actor),
212
+ at: Y(e.at),
213
+ reason: e.reason == null ? null : String(e.reason)
214
+ }))
150
215
  });
151
216
  }
152
- return i.sort((e, t) => String(e.createdAt) < String(t.createdAt) ? -1 : 1), i;
153
- }, W = [
217
+ return p.sort((e, t) => String(e.createdAt) < String(t.createdAt) ? -1 : 1), p;
218
+ }, ee = [
154
219
  {
155
220
  ...m,
156
221
  description: "Threads + comments + reactions + unread for one anchor, live.",
157
- execute: (e, t) => _.gen(function* () {
222
+ execute: (e, t) => y.gen(function* () {
158
223
  let { anchor: n } = e;
159
- yield* O(n, t);
160
- let r = B(t);
161
- return yield* _.promise(() => U(n, r.tenantId, r.id));
224
+ return yield* M(n, t), yield* y.promise(() => Q(n, t));
162
225
  })
163
226
  },
164
227
  {
165
- ...s,
228
+ ...d,
166
229
  description: "Comment on an anchor — a reply into a thread, or a new thread.",
167
- execute: (e, n) => _.gen(function* () {
168
- let r = e;
169
- yield* O(r.anchor, n);
170
- let i = B(n), a = i.id ?? "anonymous", o = f;
171
- if (o === void 0) return yield* _.fail(new t({ id: r.anchor }));
172
- let s = r.threadId;
173
- if (s !== void 0) {
174
- let e = yield* _.promise(() => k(s));
175
- if (e === void 0 || String(e.anchor) !== r.anchor) return yield* _.fail(new t({ id: s }));
176
- } else s = S({
177
- kind: "typeid",
178
- prefix: "cmtthr"
179
- }, "_voltro_comment_threads"), yield* _.promise(() => o.insert("_voltro_comment_threads", {
180
- id: s,
181
- anchor: r.anchor,
182
- quote: r.quote ?? null,
183
- status: "open",
184
- createdBy: a,
185
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
186
- tenantId: i.tenantId
187
- }));
188
- let c = yield* _.promise(() => H(r.mentions ?? [], i)), l = S({
230
+ execute: (n, r) => y.gen(function* () {
231
+ let i = n;
232
+ yield* M(i.anchor, r);
233
+ let a = U(r), s = a.id ?? "anonymous", c = o;
234
+ if (c === void 0) return yield* y.fail(new t({ id: i.anchor }));
235
+ let l = i.threadId;
236
+ if (l !== void 0) {
237
+ let e = yield* y.promise(() => q(l, r));
238
+ if (e === void 0 || String(e.anchor) !== i.anchor) return yield* y.fail(new t({ id: l }));
239
+ } else {
240
+ let t = w({
241
+ kind: "typeid",
242
+ prefix: "cmtthr"
243
+ }, "_voltro_comment_threads");
244
+ l = t;
245
+ let n = i.kind ?? "discussion";
246
+ if (!K({
247
+ id: t,
248
+ anchor: i.anchor,
249
+ kind: n,
250
+ status: "open",
251
+ createdBy: s
252
+ }, r)) return yield* y.fail(new e({
253
+ anchor: i.anchor,
254
+ reason: `a ${n} thread here would not be visible to its author`
255
+ }));
256
+ yield* y.promise(async () => {
257
+ await c.insert("_voltro_comment_threads", {
258
+ id: t,
259
+ anchor: i.anchor,
260
+ quote: i.quote ?? null,
261
+ kind: n,
262
+ status: "open",
263
+ createdBy: s,
264
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
265
+ tenantId: a.tenantId
266
+ }), await J(c, {
267
+ threadId: t,
268
+ from: null,
269
+ to: "open",
270
+ actor: s,
271
+ reason: null,
272
+ tenantId: a.tenantId
273
+ });
274
+ });
275
+ }
276
+ let u = yield* y.promise(() => Z(i.mentions ?? [], a)), d = w({
189
277
  kind: "typeid",
190
278
  prefix: "cmt"
191
279
  }, "_voltro_comments");
192
- return yield* _.promise(() => o.insert("_voltro_comments", {
193
- id: l,
194
- threadId: s,
195
- body: r.body,
196
- authorSubjectId: a,
197
- mentions: c,
280
+ return yield* y.promise(() => c.insert("_voltro_comments", {
281
+ id: d,
282
+ threadId: l,
283
+ body: i.body,
284
+ authorSubjectId: s,
285
+ mentions: u,
198
286
  editedAt: null,
199
287
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
200
- tenantId: i.tenantId
201
- })), T(), yield* V({
202
- mentions: c,
203
- author: a,
204
- anchor: r.anchor,
205
- threadId: s,
206
- body: r.body,
207
- tenantId: i.tenantId
288
+ tenantId: a.tenantId
289
+ })), j(), yield* X({
290
+ mentions: u,
291
+ author: s,
292
+ anchor: i.anchor,
293
+ threadId: l,
294
+ body: i.body,
295
+ tenantId: a.tenantId
208
296
  }), {
209
- threadId: s,
210
- commentId: l
297
+ threadId: l,
298
+ commentId: d
211
299
  };
212
300
  })
213
301
  },
214
302
  {
215
- ...d,
303
+ ...p,
216
304
  description: "Edit your own comment.",
217
- execute: (e, r) => _.gen(function* () {
218
- let i = e, a = (yield* _.promise(() => C("_voltro_comments", x("id", i.commentId), 1)))[0];
219
- if (a === void 0) return yield* _.fail(new t({ id: i.commentId }));
220
- let o = yield* _.promise(() => k(String(a.threadId)));
221
- return o === void 0 ? yield* _.fail(new t({ id: i.commentId })) : (yield* O(String(o.anchor), r), String(a.authorSubjectId) === (B(r).id ?? "anonymous") ? (yield* _.promise(() => f.update("_voltro_comments", i.commentId, {
305
+ execute: (e, r) => y.gen(function* () {
306
+ let i = e, a = (yield* y.promise(() => T("_voltro_comments", C("id", i.commentId), 1)))[0];
307
+ if (a === void 0) return yield* y.fail(new t({ id: i.commentId }));
308
+ let s = yield* y.promise(() => q(String(a.threadId), r));
309
+ return s === void 0 ? yield* y.fail(new t({ id: i.commentId })) : (yield* M(String(s.anchor), r), String(a.authorSubjectId) === (U(r).id ?? "anonymous") ? (yield* y.promise(() => o.update("_voltro_comments", i.commentId, {
222
310
  body: i.body,
223
311
  editedAt: (/* @__PURE__ */ new Date()).toISOString()
224
- })), T(), { ok: !0 }) : yield* _.fail(new n({ id: i.commentId })));
312
+ })), j(), { ok: !0 }) : yield* y.fail(new n({ id: i.commentId })));
225
313
  })
226
314
  },
227
315
  {
228
- ...a,
316
+ ...v,
229
317
  description: "Resolve or reopen a thread.",
230
- execute: (e, n) => _.gen(function* () {
231
- let r = e, i = yield* _.promise(() => k(r.threadId));
232
- return i === void 0 ? yield* _.fail(new t({ id: r.threadId })) : (yield* O(String(i.anchor), n), yield* _.promise(() => f.update("_voltro_comment_threads", r.threadId, { status: r.resolved ? "resolved" : "open" })), T(), { ok: !0 });
318
+ execute: (e, n) => y.gen(function* () {
319
+ let a = e, s = yield* y.promise(() => q(a.threadId, n));
320
+ if (s === void 0) return yield* y.fail(new t({ id: a.threadId }));
321
+ yield* M(String(s.anchor), n);
322
+ let c = G(s), l = a.resolved ? "resolved" : "open";
323
+ if (c.status === l) return { ok: !0 };
324
+ let u = U(n);
325
+ if (i.statusPolicy !== void 0 && !(yield* y.promise(async () => {
326
+ try {
327
+ return await i.statusPolicy({
328
+ thread: c,
329
+ to: l,
330
+ subject: u,
331
+ store: o
332
+ });
333
+ } catch {
334
+ return !1;
335
+ }
336
+ }))) return yield* y.fail(new r({
337
+ threadId: a.threadId,
338
+ reason: `the status policy does not let this caller ${a.resolved ? "resolve" : "reopen"} this ${c.kind} thread`
339
+ }));
340
+ let d = a.reason?.trim() ? a.reason.trim() : null;
341
+ return yield* y.promise(async () => {
342
+ await o.update("_voltro_comment_threads", a.threadId, { status: l }), await J(o, {
343
+ threadId: a.threadId,
344
+ from: c.status,
345
+ to: l,
346
+ actor: u.id ?? "anonymous",
347
+ reason: d,
348
+ tenantId: u.tenantId
349
+ });
350
+ }), j(), { ok: !0 };
233
351
  })
234
352
  },
235
353
  {
236
- ...r,
354
+ ...f,
237
355
  description: "Delete your own comment (comments:moderate deletes any).",
238
- execute: (e, r) => _.gen(function* () {
239
- let i = e, a = (yield* _.promise(() => C("_voltro_comments", x("id", i.commentId), 1)))[0];
240
- if (a === void 0) return yield* _.fail(new t({ id: i.commentId }));
241
- let o = yield* _.promise(() => k(String(a.threadId)));
242
- if (o === void 0) return yield* _.fail(new t({ id: i.commentId }));
243
- yield* O(String(o.anchor), r);
244
- let s = B(r);
245
- return String(a.authorSubjectId) !== (s.id ?? "anonymous") && !s.scopes.includes("comments:moderate") ? yield* _.fail(new n({ id: i.commentId })) : (yield* _.promise(async () => {
246
- for (let e of await C("_voltro_comment_reactions", x("commentId", i.commentId))) await f.delete("_voltro_comment_reactions", String(e.id));
247
- await f.delete("_voltro_comments", i.commentId);
248
- }), T(), { ok: !0 });
356
+ execute: (e, r) => y.gen(function* () {
357
+ let i = e, a = (yield* y.promise(() => T("_voltro_comments", C("id", i.commentId), 1)))[0];
358
+ if (a === void 0) return yield* y.fail(new t({ id: i.commentId }));
359
+ let s = yield* y.promise(() => q(String(a.threadId), r));
360
+ if (s === void 0) return yield* y.fail(new t({ id: i.commentId }));
361
+ yield* M(String(s.anchor), r);
362
+ let c = U(r);
363
+ return String(a.authorSubjectId) !== (c.id ?? "anonymous") && !c.scopes.includes("comments:moderate") ? yield* y.fail(new n({ id: i.commentId })) : (yield* y.promise(async () => {
364
+ for (let e of await T("_voltro_comment_reactions", C("commentId", i.commentId))) await o.delete("_voltro_comment_reactions", String(e.id));
365
+ await o.delete("_voltro_comments", i.commentId);
366
+ }), j(), { ok: !0 });
249
367
  })
250
368
  },
251
369
  {
252
- ...g,
370
+ ..._,
253
371
  description: "Toggle an emoji reaction on a comment.",
254
- execute: (e, n) => _.gen(function* () {
255
- let r = e, i = (yield* _.promise(() => C("_voltro_comments", x("id", r.commentId), 1)))[0];
256
- if (i === void 0) return yield* _.fail(new t({ id: r.commentId }));
257
- let a = yield* _.promise(() => k(String(i.threadId)));
258
- if (a === void 0) return yield* _.fail(new t({ id: r.commentId }));
259
- yield* O(String(a.anchor), n);
260
- let o = B(n).id ?? "anonymous", s = (yield* _.promise(() => C("_voltro_comment_reactions", y(x("commentId", r.commentId), y(x("subjectId", o), x("emoji", r.emoji))), 1)))[0];
261
- if (s !== void 0) return yield* _.promise(() => f.delete("_voltro_comment_reactions", String(s.id))), T(), { reacted: !1 };
262
- let c = S({
372
+ execute: (e, n) => y.gen(function* () {
373
+ let r = e, i = (yield* y.promise(() => T("_voltro_comments", C("id", r.commentId), 1)))[0];
374
+ if (i === void 0) return yield* y.fail(new t({ id: r.commentId }));
375
+ let a = yield* y.promise(() => q(String(i.threadId), n));
376
+ if (a === void 0) return yield* y.fail(new t({ id: r.commentId }));
377
+ yield* M(String(a.anchor), n);
378
+ let s = U(n).id ?? "anonymous", c = (yield* y.promise(() => T("_voltro_comment_reactions", x(C("commentId", r.commentId), x(C("subjectId", s), C("emoji", r.emoji))), 1)))[0];
379
+ if (c !== void 0) return yield* y.promise(() => o.delete("_voltro_comment_reactions", String(c.id))), j(), { reacted: !1 };
380
+ let l = w({
263
381
  kind: "typeid",
264
382
  prefix: "cmtrx"
265
383
  }, "_voltro_comment_reactions");
266
- return yield* _.promise(() => f.insert("_voltro_comment_reactions", {
267
- id: c,
384
+ return yield* y.promise(() => o.insert("_voltro_comment_reactions", {
385
+ id: l,
268
386
  commentId: r.commentId,
269
- subjectId: o,
387
+ subjectId: s,
270
388
  emoji: r.emoji
271
- })), T(), { reacted: !0 };
389
+ })), j(), { reacted: !0 };
272
390
  })
273
391
  },
274
392
  {
275
- ...i,
393
+ ...h,
276
394
  description: "Stamp the caller's read marker for a thread.",
277
- execute: (e, n) => _.gen(function* () {
395
+ execute: (e, n) => y.gen(function* () {
278
396
  let r = e;
279
- if ((yield* _.promise(() => k(r.threadId))) === void 0) return yield* _.fail(new t({ id: r.threadId }));
280
- let i = B(n).id ?? "anonymous", a = S({
397
+ if ((yield* y.promise(() => W(r.threadId))) === void 0) return yield* y.fail(new t({ id: r.threadId }));
398
+ let i = U(n).id ?? "anonymous", a = w({
281
399
  kind: "typeid",
282
400
  prefix: "cmtread"
283
401
  }, "_voltro_comment_reads");
284
- return yield* _.promise(() => f.upsert("_voltro_comment_reads", {
402
+ return yield* y.promise(() => o.upsert("_voltro_comment_reads", {
285
403
  id: a,
286
404
  threadId: r.threadId,
287
405
  subjectId: i,
@@ -293,50 +411,51 @@ var F = O("_voltro_comment_threads", {
293
411
  })
294
412
  },
295
413
  {
296
- ...c,
414
+ ...g,
297
415
  description: "@-mention autocomplete over the app-declared directory, tenant-filtered.",
298
- execute: (e, t) => _.promise(async () => {
416
+ execute: (e, t) => y.promise(async () => {
299
417
  let { query: n } = e;
300
- if (o.resolveMentions === void 0) return [];
301
- let r = B(t);
302
- return (await o.resolveMentions({
418
+ if (i.resolveMentions === void 0) return [];
419
+ let r = U(t);
420
+ return (await i.resolveMentions({
303
421
  query: n,
304
422
  subject: {
305
423
  id: r.id,
306
424
  tenantId: r.tenantId
307
425
  }
308
- }).catch(() => [])).filter((e) => o.crossTenant === !0 || e.tenantId === void 0 || e.tenantId === null || e.tenantId === r.tenantId).map((e) => ({
426
+ }).catch(() => [])).filter((e) => i.crossTenant === !0 || e.tenantId === void 0 || e.tenantId === null || e.tenantId === r.tenantId).map((e) => ({
309
427
  subjectId: e.subjectId,
310
428
  label: e.label
311
429
  }));
312
430
  })
313
431
  }
314
- ], G = (e) => ({
432
+ ], $ = (e) => ({
315
433
  kind: "json",
316
434
  data: e
317
435
  });
318
- return j({
319
- name: l,
320
- baseName: z,
436
+ return N({
437
+ name: a,
438
+ baseName: H,
321
439
  description: "Comment threads on any app entity — replies, resolve/reopen, @-mentions with notifications, reactions, unread — live over the reactive engine.",
322
440
  permissions: ["inspect:read", "store:write"],
323
441
  extendSchema: { tables: [
324
- F,
325
- I,
326
442
  L,
327
- R
443
+ z,
444
+ R,
445
+ B,
446
+ V
328
447
  ] },
329
- routes: W,
330
- rpcClientDescriptors: p,
448
+ routes: ee,
449
+ rpcClientDescriptors: u,
331
450
  inspectEndpoints: [{
332
451
  method: "GET",
333
452
  path: "/threads",
334
453
  scope: "process",
335
454
  description: "Thread volume and searchable, paginated threads (operator surface).",
336
- handler: (e) => _.promise(async () => {
455
+ handler: (e) => y.promise(async () => {
337
456
  let t = new URL(e.url, "http://inspect.local").searchParams, n = t.get("threadId"), r = Number(t.get("offset") ?? 0), i = Number.isSafeInteger(r) && r >= 0 ? r : 0, a = (t.get("search") ?? "").trim().slice(0, 200);
338
457
  if (n) {
339
- let e = (await C("_voltro_comment_threads", x("id", n), 1))[0], t = x("threadId", n), [r, a] = e && f ? await Promise.all([f.query({
458
+ let e = (await T("_voltro_comment_threads", C("id", n), 1))[0], t = C("threadId", n), [r, a] = e && o ? await Promise.all([o.query({
340
459
  table: "_voltro_comments",
341
460
  predicate: t,
342
461
  order: [{
@@ -349,7 +468,7 @@ var F = O("_voltro_comment_threads", {
349
468
  take: 50,
350
469
  skip: i,
351
470
  projection: void 0
352
- }), f.query({
471
+ }), o.query({
353
472
  table: "_voltro_comments",
354
473
  predicate: t,
355
474
  order: [],
@@ -360,32 +479,32 @@ var F = O("_voltro_comment_threads", {
360
479
  op: "count",
361
480
  alias: "count"
362
481
  }]
363
- })]) : [[], []], o = Number(a[0]?.count ?? 0);
364
- return G({
482
+ })]) : [[], []], s = Number(a[0]?.count ?? 0);
483
+ return $({
365
484
  stats: {
366
485
  threads: +!!e,
367
486
  open: e && e.status !== "resolved" ? 1 : 0,
368
- comments: o
487
+ comments: s
369
488
  },
370
489
  recent: [],
371
490
  detail: {
372
491
  threadId: n,
373
492
  found: !!e,
374
493
  offset: i,
375
- total: o,
494
+ total: s,
376
495
  messages: r.map((e) => ({
377
496
  id: String(e.id),
378
497
  body: String(e.body),
379
498
  author: String(e.authorSubjectId),
380
- createdAt: A(e.createdAt),
381
- editedAt: e.editedAt == null ? null : A(e.editedAt)
499
+ createdAt: Y(e.createdAt),
500
+ editedAt: e.editedAt == null ? null : Y(e.editedAt)
382
501
  }))
383
502
  }
384
503
  });
385
504
  }
386
- if (!f) throw Error("Comments store is not available");
387
- let o = async (e, t) => {
388
- let n = await f.query({
505
+ if (!o) throw Error("Comments store is not available");
506
+ let s = async (e, t) => {
507
+ let n = await o.query({
389
508
  table: e,
390
509
  predicate: t,
391
510
  order: [],
@@ -398,10 +517,10 @@ var F = O("_voltro_comment_threads", {
398
517
  }]
399
518
  });
400
519
  return Number(n[0]?.count ?? 0);
401
- }, s = a ? D(b("anchor", a), b("id", a), b("createdBy", a)) : void 0, [c, l, u, d, p] = await Promise.all([
402
- f.query({
520
+ }, c = a ? k(S("anchor", a), S("id", a), S("createdBy", a)) : void 0, [l, u, d, f, p] = await Promise.all([
521
+ o.query({
403
522
  table: "_voltro_comment_threads",
404
- predicate: s,
523
+ predicate: c,
405
524
  order: [{
406
525
  column: "createdAt",
407
526
  direction: "desc"
@@ -413,13 +532,13 @@ var F = O("_voltro_comment_threads", {
413
532
  skip: i,
414
533
  projection: void 0
415
534
  }),
416
- o("_voltro_comment_threads"),
417
- o("_voltro_comment_threads", E("status", "resolved")),
418
- o("_voltro_comments"),
419
- o("_voltro_comment_threads", s)
420
- ]), m = c.length === 0 ? [] : await f.query({
535
+ s("_voltro_comment_threads"),
536
+ s("_voltro_comment_threads", O("status", "resolved")),
537
+ s("_voltro_comments"),
538
+ s("_voltro_comment_threads", c)
539
+ ]), m = l.length === 0 ? [] : await o.query({
421
540
  table: "_voltro_comments",
422
- predicate: w("threadId", c.map((e) => String(e.id))),
541
+ predicate: E("threadId", l.map((e) => String(e.id))),
423
542
  order: [],
424
543
  take: void 0,
425
544
  skip: void 0,
@@ -434,11 +553,11 @@ var F = O("_voltro_comment_threads", {
434
553
  }],
435
554
  groupBy: ["threadId"]
436
555
  }), h = new Map(m.map((e) => [String(e.threadId), Number(e.count)]));
437
- return G({
556
+ return $({
438
557
  stats: {
439
- threads: l,
440
- open: u,
441
- comments: d
558
+ threads: u,
559
+ open: d,
560
+ comments: f
442
561
  },
443
562
  page: {
444
563
  offset: i,
@@ -446,27 +565,71 @@ var F = O("_voltro_comment_threads", {
446
565
  total: p,
447
566
  pageSize: 50
448
567
  },
449
- recent: c.map((e) => ({
568
+ recent: l.map((e) => ({
450
569
  id: String(e.id),
451
570
  anchor: String(e.anchor),
452
571
  status: String(e.status),
453
572
  createdBy: String(e.createdBy),
454
- createdAt: A(e.createdAt),
573
+ createdAt: Y(e.createdAt),
455
574
  comments: h.get(String(e.id)) ?? 0
456
575
  }))
457
576
  });
458
577
  })
459
578
  }],
460
579
  bindDataStore: (e) => {
461
- f = e, h = e;
580
+ o = e, s = e;
462
581
  },
463
- onActivate: (e) => _.sync(() => {
464
- o.access?.viaEntity === void 0 && o.access?.scope === void 0 && e.logger.warn("comments: no access rule declared — EVERY read and write will be refused (fail-closed). Set commentsPlugin({ access: { viaEntity } }) or { access: { scope } }."), e.logger.info("comments active", { mentions: o.resolveMentions !== void 0 });
582
+ onActivate: (e) => y.sync(() => {
583
+ i.access?.viaEntity === void 0 && i.access?.scope === void 0 && e.logger.warn("comments: no access rule declared — EVERY read and write will be refused (fail-closed). Set commentsPlugin({ access: { viaEntity } }) or { access: { scope } }."), e.logger.info("comments active", { mentions: i.resolveMentions !== void 0 });
465
584
  }),
466
- onDeactivate: () => _.sync(() => {
467
- f = void 0, h = void 0;
585
+ onDeactivate: () => y.sync(() => {
586
+ o = void 0, s = void 0;
468
587
  })
469
588
  });
470
- }, H = () => "plugin-comments";
589
+ }, G = async (e, t, n = /* @__PURE__ */ new Date()) => {
590
+ let r = (t, n) => e.query({
591
+ table: t,
592
+ predicate: n,
593
+ order: [],
594
+ take: void 0,
595
+ skip: void 0,
596
+ projection: void 0
597
+ }), i = 0, a = 0;
598
+ for (let [o, s] of Object.entries(t)) {
599
+ if (s === void 0 || !(s >= 0)) continue;
600
+ let t = n.getTime() - s * 864e5;
601
+ for (let c of await r("_voltro_comment_threads", x(C("status", "resolved"), C("kind", o)))) {
602
+ let o = [...await r("_voltro_comment_thread_events", C("threadId", String(c.id)))], l = (e) => new Date(e.at instanceof Date ? e.at : String(e.at)).getTime();
603
+ o.sort((e, t) => l(e) - l(t));
604
+ let u = [...o].reverse().find((e) => e.toStatus === "resolved" && e.fromStatus === "open");
605
+ if (u === void 0 || l(u) > t) continue;
606
+ let d = await r("_voltro_comments", C("threadId", String(c.id)));
607
+ if (d.length !== 0) {
608
+ for (let t of d) {
609
+ for (let n of await r("_voltro_comment_reactions", C("commentId", String(t.id)))) await e.delete("_voltro_comment_reactions", String(n.id));
610
+ await e.delete("_voltro_comments", String(t.id));
611
+ }
612
+ for (let t of await r("_voltro_comment_reads", C("threadId", String(c.id)))) await e.delete("_voltro_comment_reads", String(t.id));
613
+ await e.insert("_voltro_comment_thread_events", {
614
+ id: w({
615
+ kind: "typeid",
616
+ prefix: "cmtevt"
617
+ }, "_voltro_comment_thread_events"),
618
+ threadId: String(c.id),
619
+ fromStatus: "resolved",
620
+ toStatus: "resolved",
621
+ actor: "retention",
622
+ reason: `${d.length} comment(s) removed ${s} day(s) after resolving`,
623
+ at: n.toISOString(),
624
+ tenantId: c.tenantId ?? null
625
+ }), i += 1, a += d.length;
626
+ }
627
+ }
628
+ }
629
+ return {
630
+ threads: i,
631
+ comments: a
632
+ };
633
+ }, K = () => "plugin-comments";
471
634
  //#endregion
472
- export { e as CommentAccessRefused, t as CommentNotFound, n as CommentNotYours, o as QUOTE_MAX_LENGTH, L as commentReactionsTable, R as commentReadsTable, F as commentThreadsTable, u as commentsFeed, V as commentsPlugin, I as commentsTable, f as findQuote, H as pluginComments, l as textQuote };
635
+ export { c as COMMENT_THREAD_KINDS, e as CommentAccessRefused, t as CommentNotFound, n as CommentNotYours, r as CommentStatusRefused, i as QUOTE_MAX_LENGTH, G as applyCommentRetention, B as commentReactionsTable, V as commentReadsTable, z as commentThreadEventsTable, L as commentThreadsTable, l as commentsFeed, W as commentsPlugin, R as commentsTable, a as findQuote, K as pluginComments, o as textQuote };