@voltro/plugin-comments 0.54.0 → 0.56.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/CHANGELOG.md +639 -2
- package/dist/index.js +135 -135
- package/dist/rpc.d.ts +12 -0
- package/dist/rpc.js +15 -15
- package/dist/web.js +22 -20
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
1
|
import { CommentAccessRefused as e, CommentNotFound as t, CommentNotYours as n } from "./errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { Effect as
|
|
4
|
-
import { and as
|
|
5
|
-
import { definePlugin as
|
|
6
|
-
import { NotificationService as
|
|
2
|
+
import { BASE_NAME as r, commentsFeed as i, commentsRpcClientImports as a, createDescriptor as o, deleteDescriptor as s, editDescriptor as c, listDescriptor as l, markReadDescriptor as u, mentionSearchDescriptor as d, reactDescriptor as f, resolveDescriptor as p } from "./rpc.js";
|
|
3
|
+
import { Effect as m, Option as h } from "effect";
|
|
4
|
+
import { and as g, eq as _, generateId as v, id as y, json as b, table as x, text as S, timestamp as C } from "@voltro/database";
|
|
5
|
+
import { definePlugin as w, pluginInstanceName as T, publishReactivity as E } from "@voltro/protocol";
|
|
6
|
+
import { NotificationService as D } from "@voltro/plugin-notifications";
|
|
7
7
|
//#region src/index.ts
|
|
8
|
-
var
|
|
9
|
-
id:
|
|
10
|
-
anchor:
|
|
11
|
-
status:
|
|
12
|
-
createdBy:
|
|
13
|
-
createdAt:
|
|
14
|
-
tenantId:
|
|
15
|
-
}).index("byThreadAnchor", ["anchor", "tenantId"]),
|
|
16
|
-
id:
|
|
17
|
-
threadId:
|
|
18
|
-
body:
|
|
19
|
-
authorSubjectId:
|
|
20
|
-
mentions:
|
|
21
|
-
editedAt:
|
|
22
|
-
createdAt:
|
|
23
|
-
tenantId:
|
|
24
|
-
}).index("byCommentThread", ["threadId"]),
|
|
25
|
-
id:
|
|
26
|
-
commentId:
|
|
27
|
-
subjectId:
|
|
28
|
-
emoji:
|
|
8
|
+
var O = x("_voltro_comment_threads", {
|
|
9
|
+
id: y({ prefix: "cmtthr" }),
|
|
10
|
+
anchor: S(),
|
|
11
|
+
status: S().default("open"),
|
|
12
|
+
createdBy: S(),
|
|
13
|
+
createdAt: C().default("now"),
|
|
14
|
+
tenantId: S().nullable()
|
|
15
|
+
}).index("byThreadAnchor", ["anchor", "tenantId"]), k = x("_voltro_comments", {
|
|
16
|
+
id: y({ prefix: "cmt" }),
|
|
17
|
+
threadId: S(),
|
|
18
|
+
body: S(),
|
|
19
|
+
authorSubjectId: S(),
|
|
20
|
+
mentions: b(),
|
|
21
|
+
editedAt: C().nullable(),
|
|
22
|
+
createdAt: C().default("now"),
|
|
23
|
+
tenantId: S().nullable()
|
|
24
|
+
}).index("byCommentThread", ["threadId"]), A = x("_voltro_comment_reactions", {
|
|
25
|
+
id: y({ prefix: "cmtrx" }),
|
|
26
|
+
commentId: S(),
|
|
27
|
+
subjectId: S(),
|
|
28
|
+
emoji: S()
|
|
29
29
|
}).unique("byReactionIdentity", [
|
|
30
30
|
"commentId",
|
|
31
31
|
"subjectId",
|
|
32
32
|
"emoji"
|
|
33
|
-
]).index("byReactionComment", ["commentId"]),
|
|
34
|
-
id:
|
|
35
|
-
threadId:
|
|
36
|
-
subjectId:
|
|
37
|
-
lastReadAt:
|
|
38
|
-
}).unique("byReadThreadSubject", ["threadId", "subjectId"]),
|
|
33
|
+
]).index("byReactionComment", ["commentId"]), j = x("_voltro_comment_reads", {
|
|
34
|
+
id: y({ prefix: "cmtread" }),
|
|
35
|
+
threadId: S(),
|
|
36
|
+
subjectId: S(),
|
|
37
|
+
lastReadAt: C()
|
|
38
|
+
}).unique("byReadThreadSubject", ["threadId", "subjectId"]), M = r, N = (e) => {
|
|
39
39
|
let t = e.request.subject ?? {};
|
|
40
40
|
return {
|
|
41
41
|
id: t.id ?? null,
|
|
42
42
|
tenantId: t.tenantId ?? null,
|
|
43
43
|
scopes: t.scopes ?? []
|
|
44
44
|
};
|
|
45
|
-
},
|
|
46
|
-
let y =
|
|
47
|
-
base:
|
|
48
|
-
alias:
|
|
49
|
-
instance:
|
|
45
|
+
}, P = (r = {}) => {
|
|
46
|
+
let y = T({
|
|
47
|
+
base: M,
|
|
48
|
+
alias: r.alias,
|
|
49
|
+
instance: r.name
|
|
50
50
|
}), b, x, S = async (e, t, n) => b === void 0 ? [] : b.query({
|
|
51
51
|
table: e,
|
|
52
52
|
predicate: t,
|
|
@@ -54,40 +54,40 @@ var D = b("_voltro_comment_threads", {
|
|
|
54
54
|
take: n,
|
|
55
55
|
skip: void 0,
|
|
56
56
|
projection: void 0
|
|
57
|
-
}),
|
|
58
|
-
|
|
59
|
-
}, P = (t, n) =>
|
|
60
|
-
let
|
|
61
|
-
return
|
|
57
|
+
}), C = () => {
|
|
58
|
+
E(x, i);
|
|
59
|
+
}, P = (t, n) => m.gen(function* () {
|
|
60
|
+
let i = N(n);
|
|
61
|
+
return r.access?.viaEntity === void 0 ? r.access?.scope === void 0 ? yield* m.fail(new e({
|
|
62
62
|
anchor: t,
|
|
63
63
|
reason: "no access rule declared — set commentsPlugin({ access: { viaEntity } }) or { access: { scope } }"
|
|
64
|
-
})) :
|
|
64
|
+
})) : i.scopes.includes(r.access.scope) ? void 0 : yield* m.fail(new e({
|
|
65
65
|
anchor: t,
|
|
66
|
-
reason: `missing scope '${
|
|
67
|
-
})) : (yield*
|
|
66
|
+
reason: `missing scope '${r.access.scope}'`
|
|
67
|
+
})) : (yield* m.promise(async () => {
|
|
68
68
|
try {
|
|
69
|
-
return await
|
|
69
|
+
return await r.access.viaEntity({
|
|
70
70
|
anchor: t,
|
|
71
|
-
subject:
|
|
71
|
+
subject: i,
|
|
72
72
|
store: b
|
|
73
73
|
});
|
|
74
74
|
} catch {
|
|
75
75
|
return !1;
|
|
76
76
|
}
|
|
77
|
-
})) ? void 0 : yield*
|
|
77
|
+
})) ? void 0 : yield* m.fail(new e({
|
|
78
78
|
anchor: t,
|
|
79
79
|
reason: "the anchored entity is not readable by this caller (or no longer available)"
|
|
80
80
|
}));
|
|
81
|
-
}), F = async (e) => (await S("_voltro_comment_threads",
|
|
81
|
+
}), F = async (e) => (await S("_voltro_comment_threads", _("id", e), 1))[0], I = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", L = (e) => m.gen(function* () {
|
|
82
82
|
if (e.mentions.length === 0) return;
|
|
83
|
-
let t = yield*
|
|
84
|
-
if (
|
|
85
|
-
yield*
|
|
83
|
+
let t = yield* m.serviceOption(D);
|
|
84
|
+
if (h.isNone(t)) {
|
|
85
|
+
yield* m.logInfo("comments: mention recorded without notification — @voltro/plugin-notifications is not configured");
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
-
for (let n of e.mentions) n !== e.author && (yield*
|
|
88
|
+
for (let n of e.mentions) n !== e.author && (yield* m.promise(() => t.value.send({
|
|
89
89
|
to: n,
|
|
90
|
-
category:
|
|
90
|
+
category: r.mentionCategory ?? "mention",
|
|
91
91
|
title: "You were mentioned",
|
|
92
92
|
body: e.body.length > 200 ? `${e.body.slice(0, 200)}…` : e.body,
|
|
93
93
|
data: {
|
|
@@ -97,24 +97,24 @@ var D = b("_voltro_comment_threads", {
|
|
|
97
97
|
tenantId: e.tenantId
|
|
98
98
|
}).then(() => void 0).catch(() => void 0)));
|
|
99
99
|
}), R = async (e, t) => {
|
|
100
|
-
if (e.length === 0 ||
|
|
100
|
+
if (e.length === 0 || r.resolveMentions === void 0) return [];
|
|
101
101
|
let n = [];
|
|
102
|
-
for (let
|
|
103
|
-
let e = (await
|
|
104
|
-
query:
|
|
102
|
+
for (let i of [...new Set(e)].slice(0, 20)) {
|
|
103
|
+
let e = (await r.resolveMentions({
|
|
104
|
+
query: i,
|
|
105
105
|
subject: t
|
|
106
|
-
}).catch(() => [])).find((e) => e.subjectId ===
|
|
107
|
-
e !== void 0 && (
|
|
106
|
+
}).catch(() => [])).find((e) => e.subjectId === i);
|
|
107
|
+
e !== void 0 && (r.crossTenant === !0 || e.tenantId === void 0 || e.tenantId === null || e.tenantId === t.tenantId) && n.push(i);
|
|
108
108
|
}
|
|
109
109
|
return n;
|
|
110
110
|
}, z = async (e, t, n) => {
|
|
111
|
-
let r = await S("_voltro_comment_threads",
|
|
111
|
+
let r = await S("_voltro_comment_threads", g(_("anchor", e), _("tenantId", t))), i = [];
|
|
112
112
|
for (let e of r) {
|
|
113
|
-
let t = [...await S("_voltro_comments",
|
|
113
|
+
let t = [...await S("_voltro_comments", _("threadId", String(e.id)))];
|
|
114
114
|
t.sort((e, t) => I(e.createdAt) < I(t.createdAt) ? -1 : 1);
|
|
115
|
-
let r = n === null ? [] : await S("_voltro_comment_reads",
|
|
115
|
+
let r = n === null ? [] : await S("_voltro_comment_reads", g(_("threadId", String(e.id)), _("subjectId", n)), 1), a = r[0] ? I(r[0].lastReadAt) : "", o = [];
|
|
116
116
|
for (let e of t) {
|
|
117
|
-
let t = await S("_voltro_comment_reactions",
|
|
117
|
+
let t = await S("_voltro_comment_reactions", _("commentId", String(e.id))), r = /* @__PURE__ */ new Map();
|
|
118
118
|
for (let e of t) {
|
|
119
119
|
let t = r.get(String(e.emoji)) ?? {
|
|
120
120
|
count: 0,
|
|
@@ -150,31 +150,31 @@ var D = b("_voltro_comment_threads", {
|
|
|
150
150
|
return i.sort((e, t) => String(e.createdAt) < String(t.createdAt) ? -1 : 1), i;
|
|
151
151
|
}, B = "comments:moderate", V = [
|
|
152
152
|
{
|
|
153
|
-
...
|
|
153
|
+
...l,
|
|
154
154
|
description: "Threads + comments + reactions + unread for one anchor, live.",
|
|
155
|
-
execute: (e, t) =>
|
|
155
|
+
execute: (e, t) => m.gen(function* () {
|
|
156
156
|
let { anchor: n } = e;
|
|
157
157
|
yield* P(n, t);
|
|
158
|
-
let r =
|
|
159
|
-
return yield*
|
|
158
|
+
let r = N(t);
|
|
159
|
+
return yield* m.promise(() => z(n, r.tenantId, r.id));
|
|
160
160
|
})
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
|
-
...
|
|
163
|
+
...o,
|
|
164
164
|
description: "Comment on an anchor — a reply into a thread, or a new thread.",
|
|
165
|
-
execute: (e, n) =>
|
|
165
|
+
execute: (e, n) => m.gen(function* () {
|
|
166
166
|
let r = e;
|
|
167
167
|
yield* P(r.anchor, n);
|
|
168
|
-
let i =
|
|
169
|
-
if (o === void 0) return yield*
|
|
168
|
+
let i = N(n), a = i.id ?? "anonymous", o = b;
|
|
169
|
+
if (o === void 0) return yield* m.fail(new t({ id: r.anchor }));
|
|
170
170
|
let s = r.threadId;
|
|
171
171
|
if (s !== void 0) {
|
|
172
|
-
let e = yield*
|
|
173
|
-
if (e === void 0 || String(e.anchor) !== r.anchor) return yield*
|
|
174
|
-
} else s =
|
|
172
|
+
let e = yield* m.promise(() => F(s));
|
|
173
|
+
if (e === void 0 || String(e.anchor) !== r.anchor) return yield* m.fail(new t({ id: s }));
|
|
174
|
+
} else s = v({
|
|
175
175
|
kind: "typeid",
|
|
176
176
|
prefix: "cmtthr"
|
|
177
|
-
}, "_voltro_comment_threads"), yield*
|
|
177
|
+
}, "_voltro_comment_threads"), yield* m.promise(() => o.insert("_voltro_comment_threads", {
|
|
178
178
|
id: s,
|
|
179
179
|
anchor: r.anchor,
|
|
180
180
|
status: "open",
|
|
@@ -182,11 +182,11 @@ var D = b("_voltro_comment_threads", {
|
|
|
182
182
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
183
183
|
tenantId: i.tenantId
|
|
184
184
|
}));
|
|
185
|
-
let c = yield*
|
|
185
|
+
let c = yield* m.promise(() => R(r.mentions ?? [], i)), l = v({
|
|
186
186
|
kind: "typeid",
|
|
187
187
|
prefix: "cmt"
|
|
188
188
|
}, "_voltro_comments");
|
|
189
|
-
return yield*
|
|
189
|
+
return yield* m.promise(() => o.insert("_voltro_comments", {
|
|
190
190
|
id: l,
|
|
191
191
|
threadId: s,
|
|
192
192
|
body: r.body,
|
|
@@ -195,7 +195,7 @@ var D = b("_voltro_comment_threads", {
|
|
|
195
195
|
editedAt: null,
|
|
196
196
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
197
197
|
tenantId: i.tenantId
|
|
198
|
-
})),
|
|
198
|
+
})), C(), yield* L({
|
|
199
199
|
mentions: c,
|
|
200
200
|
author: a,
|
|
201
201
|
anchor: r.anchor,
|
|
@@ -209,76 +209,76 @@ var D = b("_voltro_comment_threads", {
|
|
|
209
209
|
})
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
|
-
...
|
|
212
|
+
...c,
|
|
213
213
|
description: "Edit your own comment.",
|
|
214
|
-
execute: (e, r) =>
|
|
215
|
-
let i = e, a = (yield*
|
|
216
|
-
if (a === void 0) return yield*
|
|
217
|
-
let o = yield*
|
|
218
|
-
return o === void 0 ? yield*
|
|
214
|
+
execute: (e, r) => m.gen(function* () {
|
|
215
|
+
let i = e, a = (yield* m.promise(() => S("_voltro_comments", _("id", i.commentId), 1)))[0];
|
|
216
|
+
if (a === void 0) return yield* m.fail(new t({ id: i.commentId }));
|
|
217
|
+
let o = yield* m.promise(() => F(String(a.threadId)));
|
|
218
|
+
return o === void 0 ? yield* m.fail(new t({ id: i.commentId })) : (yield* P(String(o.anchor), r), String(a.authorSubjectId) === (N(r).id ?? "anonymous") ? (yield* m.promise(() => b.update("_voltro_comments", i.commentId, {
|
|
219
219
|
body: i.body,
|
|
220
220
|
editedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
221
|
-
})),
|
|
221
|
+
})), C(), { ok: !0 }) : yield* m.fail(new n({ id: i.commentId })));
|
|
222
222
|
})
|
|
223
223
|
},
|
|
224
224
|
{
|
|
225
|
-
...
|
|
225
|
+
...p,
|
|
226
226
|
description: "Resolve or reopen a thread.",
|
|
227
|
-
execute: (e, n) =>
|
|
228
|
-
let r = e, i = yield*
|
|
229
|
-
return i === void 0 ? yield*
|
|
227
|
+
execute: (e, n) => m.gen(function* () {
|
|
228
|
+
let r = e, i = yield* m.promise(() => F(r.threadId));
|
|
229
|
+
return i === void 0 ? yield* m.fail(new t({ id: r.threadId })) : (yield* P(String(i.anchor), n), yield* m.promise(() => b.update("_voltro_comment_threads", r.threadId, { status: r.resolved ? "resolved" : "open" })), C(), { ok: !0 });
|
|
230
230
|
})
|
|
231
231
|
},
|
|
232
232
|
{
|
|
233
|
-
...
|
|
233
|
+
...s,
|
|
234
234
|
description: "Delete your own comment (comments:moderate deletes any).",
|
|
235
|
-
execute: (e, r) =>
|
|
236
|
-
let i = e, a = (yield*
|
|
237
|
-
if (a === void 0) return yield*
|
|
238
|
-
let o = yield*
|
|
239
|
-
if (o === void 0) return yield*
|
|
235
|
+
execute: (e, r) => m.gen(function* () {
|
|
236
|
+
let i = e, a = (yield* m.promise(() => S("_voltro_comments", _("id", i.commentId), 1)))[0];
|
|
237
|
+
if (a === void 0) return yield* m.fail(new t({ id: i.commentId }));
|
|
238
|
+
let o = yield* m.promise(() => F(String(a.threadId)));
|
|
239
|
+
if (o === void 0) return yield* m.fail(new t({ id: i.commentId }));
|
|
240
240
|
yield* P(String(o.anchor), r);
|
|
241
|
-
let s =
|
|
242
|
-
return String(a.authorSubjectId) !== (s.id ?? "anonymous") && !s.scopes.includes(B) ? yield*
|
|
243
|
-
for (let e of await S("_voltro_comment_reactions",
|
|
241
|
+
let s = N(r);
|
|
242
|
+
return String(a.authorSubjectId) !== (s.id ?? "anonymous") && !s.scopes.includes(B) ? yield* m.fail(new n({ id: i.commentId })) : (yield* m.promise(async () => {
|
|
243
|
+
for (let e of await S("_voltro_comment_reactions", _("commentId", i.commentId))) await b.delete("_voltro_comment_reactions", String(e.id));
|
|
244
244
|
await b.delete("_voltro_comments", i.commentId);
|
|
245
|
-
}),
|
|
245
|
+
}), C(), { ok: !0 });
|
|
246
246
|
})
|
|
247
247
|
},
|
|
248
248
|
{
|
|
249
|
-
...
|
|
249
|
+
...f,
|
|
250
250
|
description: "Toggle an emoji reaction on a comment.",
|
|
251
|
-
execute: (e, n) =>
|
|
252
|
-
let r = e, i = (yield*
|
|
253
|
-
if (i === void 0) return yield*
|
|
254
|
-
let a = yield*
|
|
255
|
-
if (a === void 0) return yield*
|
|
251
|
+
execute: (e, n) => m.gen(function* () {
|
|
252
|
+
let r = e, i = (yield* m.promise(() => S("_voltro_comments", _("id", r.commentId), 1)))[0];
|
|
253
|
+
if (i === void 0) return yield* m.fail(new t({ id: r.commentId }));
|
|
254
|
+
let a = yield* m.promise(() => F(String(i.threadId)));
|
|
255
|
+
if (a === void 0) return yield* m.fail(new t({ id: r.commentId }));
|
|
256
256
|
yield* P(String(a.anchor), n);
|
|
257
|
-
let o =
|
|
258
|
-
if (s !== void 0) return yield*
|
|
259
|
-
let c =
|
|
257
|
+
let o = N(n).id ?? "anonymous", s = (yield* m.promise(() => S("_voltro_comment_reactions", g(_("commentId", r.commentId), g(_("subjectId", o), _("emoji", r.emoji))), 1)))[0];
|
|
258
|
+
if (s !== void 0) return yield* m.promise(() => b.delete("_voltro_comment_reactions", String(s.id))), C(), { reacted: !1 };
|
|
259
|
+
let c = v({
|
|
260
260
|
kind: "typeid",
|
|
261
261
|
prefix: "cmtrx"
|
|
262
262
|
}, "_voltro_comment_reactions");
|
|
263
|
-
return yield*
|
|
263
|
+
return yield* m.promise(() => b.insert("_voltro_comment_reactions", {
|
|
264
264
|
id: c,
|
|
265
265
|
commentId: r.commentId,
|
|
266
266
|
subjectId: o,
|
|
267
267
|
emoji: r.emoji
|
|
268
|
-
})),
|
|
268
|
+
})), C(), { reacted: !0 };
|
|
269
269
|
})
|
|
270
270
|
},
|
|
271
271
|
{
|
|
272
|
-
...
|
|
272
|
+
...u,
|
|
273
273
|
description: "Stamp the caller's read marker for a thread.",
|
|
274
|
-
execute: (e, n) =>
|
|
274
|
+
execute: (e, n) => m.gen(function* () {
|
|
275
275
|
let r = e;
|
|
276
|
-
if ((yield*
|
|
277
|
-
let i =
|
|
276
|
+
if ((yield* m.promise(() => F(r.threadId))) === void 0) return yield* m.fail(new t({ id: r.threadId }));
|
|
277
|
+
let i = N(n).id ?? "anonymous", a = v({
|
|
278
278
|
kind: "typeid",
|
|
279
279
|
prefix: "cmtread"
|
|
280
280
|
}, "_voltro_comment_reads");
|
|
281
|
-
return yield*
|
|
281
|
+
return yield* m.promise(() => b.upsert("_voltro_comment_reads", {
|
|
282
282
|
id: a,
|
|
283
283
|
threadId: r.threadId,
|
|
284
284
|
subjectId: i,
|
|
@@ -290,19 +290,19 @@ var D = b("_voltro_comment_threads", {
|
|
|
290
290
|
})
|
|
291
291
|
},
|
|
292
292
|
{
|
|
293
|
-
...
|
|
293
|
+
...d,
|
|
294
294
|
description: "@-mention autocomplete over the app-declared directory, tenant-filtered.",
|
|
295
|
-
execute: (e, t) =>
|
|
295
|
+
execute: (e, t) => m.promise(async () => {
|
|
296
296
|
let { query: n } = e;
|
|
297
|
-
if (
|
|
298
|
-
let
|
|
299
|
-
return (await
|
|
297
|
+
if (r.resolveMentions === void 0) return [];
|
|
298
|
+
let i = N(t);
|
|
299
|
+
return (await r.resolveMentions({
|
|
300
300
|
query: n,
|
|
301
301
|
subject: {
|
|
302
|
-
id:
|
|
303
|
-
tenantId:
|
|
302
|
+
id: i.id,
|
|
303
|
+
tenantId: i.tenantId
|
|
304
304
|
}
|
|
305
|
-
}).catch(() => [])).filter((e) =>
|
|
305
|
+
}).catch(() => [])).filter((e) => r.crossTenant === !0 || e.tenantId === void 0 || e.tenantId === null || e.tenantId === i.tenantId).map((e) => ({
|
|
306
306
|
subjectId: e.subjectId,
|
|
307
307
|
label: e.label
|
|
308
308
|
}));
|
|
@@ -312,25 +312,25 @@ var D = b("_voltro_comment_threads", {
|
|
|
312
312
|
kind: "json",
|
|
313
313
|
data: e
|
|
314
314
|
});
|
|
315
|
-
return
|
|
315
|
+
return w({
|
|
316
316
|
name: y,
|
|
317
|
-
baseName:
|
|
317
|
+
baseName: M,
|
|
318
318
|
description: "Comment threads on any app entity — replies, resolve/reopen, @-mentions with notifications, reactions, unread — live over the reactive engine.",
|
|
319
319
|
permissions: ["inspect:read", "store:write"],
|
|
320
320
|
declaredScopes: [B],
|
|
321
321
|
extendSchema: { tables: [
|
|
322
|
-
D,
|
|
323
322
|
O,
|
|
324
323
|
k,
|
|
325
|
-
A
|
|
324
|
+
A,
|
|
325
|
+
j
|
|
326
326
|
] },
|
|
327
327
|
routes: V,
|
|
328
|
-
rpcClientDescriptors:
|
|
328
|
+
rpcClientDescriptors: a,
|
|
329
329
|
inspectEndpoints: [{
|
|
330
330
|
method: "GET",
|
|
331
331
|
path: "/threads",
|
|
332
332
|
description: "Thread volume + the most recent threads (operator surface).",
|
|
333
|
-
handler: () =>
|
|
333
|
+
handler: () => m.promise(async () => {
|
|
334
334
|
let e = await S("_voltro_comment_threads", void 0), t = await S("_voltro_comments", void 0), n = [...e].sort((e, t) => I(e.createdAt) > I(t.createdAt) ? -1 : 1).slice(0, 50).map((e) => ({
|
|
335
335
|
id: String(e.id),
|
|
336
336
|
anchor: String(e.anchor),
|
|
@@ -352,13 +352,13 @@ var D = b("_voltro_comment_threads", {
|
|
|
352
352
|
bindDataStore: (e) => {
|
|
353
353
|
b = e, x = e;
|
|
354
354
|
},
|
|
355
|
-
onActivate: (e) =>
|
|
356
|
-
|
|
355
|
+
onActivate: (e) => m.sync(() => {
|
|
356
|
+
r.access?.viaEntity === void 0 && r.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: r.resolveMentions !== void 0 });
|
|
357
357
|
}),
|
|
358
|
-
onDeactivate: () =>
|
|
358
|
+
onDeactivate: () => m.sync(() => {
|
|
359
359
|
b = void 0, x = void 0;
|
|
360
360
|
})
|
|
361
361
|
});
|
|
362
|
-
},
|
|
362
|
+
}, F = () => "plugin-comments";
|
|
363
363
|
//#endregion
|
|
364
|
-
export { e as CommentAccessRefused, t as CommentNotFound, n as CommentNotYours,
|
|
364
|
+
export { e as CommentAccessRefused, t as CommentNotFound, n as CommentNotYours, A as commentReactionsTable, j as commentReadsTable, O as commentThreadsTable, i as commentsFeed, P as commentsPlugin, k as commentsTable, F as pluginComments };
|
package/dist/rpc.d.ts
CHANGED
|
@@ -4,6 +4,18 @@ import { QueryProcedureDescriptor } from '@voltro/protocol';
|
|
|
4
4
|
import { ReactivityChannel } from '@voltro/protocol';
|
|
5
5
|
import { Schema } from 'effect';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The plugin's canonical package name — the key an app's `alias` is recorded
|
|
9
|
+
* under, and what `./web`'s hooks resolve their tags through.
|
|
10
|
+
*
|
|
11
|
+
* It lives in this BROWSER-SAFE module rather than in `index.ts` because both
|
|
12
|
+
* halves need it and only one of them may reach the server graph: `index.ts`
|
|
13
|
+
* passes it to `pluginInstanceName` / `baseName`, and `web.ts` passes it to
|
|
14
|
+
* `pluginTag`. A second copy in `web.ts` would be a second definition of the
|
|
15
|
+
* plugin's identity with nothing comparing them.
|
|
16
|
+
*/
|
|
17
|
+
export declare const BASE_NAME = "@voltro/plugin-comments";
|
|
18
|
+
|
|
7
19
|
/** The anchor's access check refused the caller — or no access rule is
|
|
8
20
|
* declared at all (fail-closed: undeclared access is a refusal, not a pass).
|
|
9
21
|
* Also the answer for a thread whose anchor row no longer resolves (a
|
package/dist/rpc.js
CHANGED
|
@@ -2,7 +2,7 @@ import { CommentAccessRefused as e, CommentNotFound as t, CommentNotYours as n }
|
|
|
2
2
|
import { Schema as r } from "effect";
|
|
3
3
|
import { defineMutation as i, defineQuery as a, reactivityChannel as o } from "@voltro/protocol";
|
|
4
4
|
//#region src/rpc.ts
|
|
5
|
-
var s = o("comments"),
|
|
5
|
+
var s = "@voltro/plugin-comments", c = o("comments"), l = r.Struct({
|
|
6
6
|
id: r.String,
|
|
7
7
|
threadId: r.String,
|
|
8
8
|
body: r.String,
|
|
@@ -15,22 +15,22 @@ var s = o("comments"), c = r.Struct({
|
|
|
15
15
|
count: r.Number,
|
|
16
16
|
mine: r.Boolean
|
|
17
17
|
}))
|
|
18
|
-
}),
|
|
18
|
+
}), u = r.Struct({
|
|
19
19
|
id: r.String,
|
|
20
20
|
anchor: r.String,
|
|
21
21
|
status: r.Literal("open", "resolved"),
|
|
22
22
|
createdBy: r.String,
|
|
23
23
|
createdAt: r.String,
|
|
24
|
-
comments: r.Array(
|
|
24
|
+
comments: r.Array(l),
|
|
25
25
|
unreadCount: r.Number
|
|
26
|
-
}),
|
|
26
|
+
}), d = a({
|
|
27
27
|
name: "comments.list",
|
|
28
|
-
source:
|
|
28
|
+
source: c,
|
|
29
29
|
input: r.Struct({ anchor: r.String }),
|
|
30
|
-
output: r.Array(
|
|
30
|
+
output: r.Array(u),
|
|
31
31
|
error: e,
|
|
32
32
|
openAccess: "delegated: the executor runs the app-declared anchor access rule (viaEntity guard or scope) and is fail-closed — an undeclared rule refuses every read"
|
|
33
|
-
}),
|
|
33
|
+
}), f = i({
|
|
34
34
|
name: "comments.create",
|
|
35
35
|
input: r.Struct({
|
|
36
36
|
anchor: r.String,
|
|
@@ -44,7 +44,7 @@ var s = o("comments"), c = r.Struct({
|
|
|
44
44
|
}),
|
|
45
45
|
error: r.Union(e, t),
|
|
46
46
|
openAccess: "delegated: the executor runs the app-declared anchor access rule before any write; the author is always the calling subject"
|
|
47
|
-
}),
|
|
47
|
+
}), p = i({
|
|
48
48
|
name: "comments.edit",
|
|
49
49
|
input: r.Struct({
|
|
50
50
|
commentId: r.String,
|
|
@@ -53,7 +53,7 @@ var s = o("comments"), c = r.Struct({
|
|
|
53
53
|
output: r.Struct({ ok: r.Boolean }),
|
|
54
54
|
error: r.Union(e, t, n),
|
|
55
55
|
openAccess: "author-scoped: only the comment's own author may edit, checked in the executor on top of the anchor access rule"
|
|
56
|
-
}),
|
|
56
|
+
}), m = i({
|
|
57
57
|
name: "comments.resolve",
|
|
58
58
|
input: r.Struct({
|
|
59
59
|
threadId: r.String,
|
|
@@ -62,13 +62,13 @@ var s = o("comments"), c = r.Struct({
|
|
|
62
62
|
output: r.Struct({ ok: r.Boolean }),
|
|
63
63
|
error: r.Union(e, t),
|
|
64
64
|
openAccess: "delegated: anchor access rule in the executor — anyone who may read the anchor may resolve/reopen (the Liveblocks semantic)"
|
|
65
|
-
}),
|
|
65
|
+
}), h = i({
|
|
66
66
|
name: "comments.delete",
|
|
67
67
|
input: r.Struct({ commentId: r.String }),
|
|
68
68
|
output: r.Struct({ ok: r.Boolean }),
|
|
69
69
|
error: r.Union(e, t, n),
|
|
70
70
|
openAccess: "author-scoped, with a comments:moderate scope override — both checked in the executor on top of the anchor access rule"
|
|
71
|
-
}),
|
|
71
|
+
}), g = i({
|
|
72
72
|
name: "comments.react",
|
|
73
73
|
input: r.Struct({
|
|
74
74
|
commentId: r.String,
|
|
@@ -77,13 +77,13 @@ var s = o("comments"), c = r.Struct({
|
|
|
77
77
|
output: r.Struct({ reacted: r.Boolean }),
|
|
78
78
|
error: r.Union(e, t),
|
|
79
79
|
openAccess: "delegated: anchor access rule in the executor; the toggle writes only the calling subject's own reaction row"
|
|
80
|
-
}),
|
|
80
|
+
}), _ = i({
|
|
81
81
|
name: "comments.markRead",
|
|
82
82
|
input: r.Struct({ threadId: r.String }),
|
|
83
83
|
output: r.Struct({ ok: r.Boolean }),
|
|
84
84
|
error: t,
|
|
85
85
|
openAccess: "self-scoped write: stamps only the calling subject's own read marker for the thread"
|
|
86
|
-
}),
|
|
86
|
+
}), v = a({
|
|
87
87
|
name: "comments.mentionSearch",
|
|
88
88
|
input: r.Struct({ query: r.String }),
|
|
89
89
|
output: r.Array(r.Struct({
|
|
@@ -91,7 +91,7 @@ var s = o("comments"), c = r.Struct({
|
|
|
91
91
|
label: r.String
|
|
92
92
|
})),
|
|
93
93
|
openAccess: "delegated: the app-declared mention resolver receives the CALLING subject and its default filters to the caller's tenant — cross-tenant subjects are structurally unreachable"
|
|
94
|
-
}),
|
|
94
|
+
}), y = [
|
|
95
95
|
{
|
|
96
96
|
tag: "comments.list",
|
|
97
97
|
kind: "query",
|
|
@@ -158,4 +158,4 @@ var s = o("comments"), c = r.Struct({
|
|
|
158
158
|
}
|
|
159
159
|
];
|
|
160
160
|
//#endregion
|
|
161
|
-
export { s as commentsFeed,
|
|
161
|
+
export { s as BASE_NAME, c as commentsFeed, y as commentsRpcClientImports, f as createDescriptor, h as deleteDescriptor, p as editDescriptor, d as listDescriptor, _ as markReadDescriptor, v as mentionSearchDescriptor, g as reactDescriptor, m as resolveDescriptor };
|
package/dist/web.js
CHANGED
|
@@ -1,35 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BASE_NAME as e } from "./rpc.js";
|
|
2
|
+
import { pluginTag as t } from "@voltro/protocol";
|
|
3
|
+
import { useMutation as n, useSubscription as r } from "@voltro/client";
|
|
2
4
|
//#region src/web.ts
|
|
3
|
-
var
|
|
4
|
-
let { data:
|
|
5
|
+
var i = (n) => t(e, n), a = (e, t = "app") => {
|
|
6
|
+
let { data: a, loading: o } = r(t, i("list"), { anchor: e }), s = n(t, i("create")), c = n(t, i("edit")), l = n(t, i("resolve")), u = n(t, i("delete")), d = n(t, i("react")), f = n(t, i("markRead")), p = a ?? [];
|
|
5
7
|
return {
|
|
6
|
-
threads:
|
|
7
|
-
pending:
|
|
8
|
-
unreadCount:
|
|
9
|
-
create: (
|
|
10
|
-
anchor:
|
|
11
|
-
body:
|
|
12
|
-
...
|
|
13
|
-
...
|
|
8
|
+
threads: p,
|
|
9
|
+
pending: o,
|
|
10
|
+
unreadCount: p.reduce((e, t) => e + t.unreadCount, 0),
|
|
11
|
+
create: (t, n) => s.mutate({
|
|
12
|
+
anchor: e,
|
|
13
|
+
body: t,
|
|
14
|
+
...n?.threadId === void 0 ? {} : { threadId: n.threadId },
|
|
15
|
+
...n?.mentions === void 0 ? {} : { mentions: n.mentions }
|
|
14
16
|
}),
|
|
15
|
-
edit: (e, t) =>
|
|
17
|
+
edit: (e, t) => c.mutate({
|
|
16
18
|
commentId: e,
|
|
17
19
|
body: t
|
|
18
20
|
}),
|
|
19
|
-
resolve: (e, t = !0) =>
|
|
21
|
+
resolve: (e, t = !0) => l.mutate({
|
|
20
22
|
threadId: e,
|
|
21
23
|
resolved: t
|
|
22
24
|
}),
|
|
23
|
-
remove: (e) =>
|
|
24
|
-
react: (e, t) =>
|
|
25
|
+
remove: (e) => u.mutate({ commentId: e }),
|
|
26
|
+
react: (e, t) => d.mutate({
|
|
25
27
|
commentId: e,
|
|
26
28
|
emoji: t
|
|
27
29
|
}),
|
|
28
|
-
markRead: (e) =>
|
|
30
|
+
markRead: (e) => f.mutate({ threadId: e })
|
|
29
31
|
};
|
|
30
|
-
},
|
|
31
|
-
let { data:
|
|
32
|
-
return
|
|
32
|
+
}, o = (e, t, n = "app") => a(e, n).threads.find((e) => e.id === t), s = (e, t = "app") => {
|
|
33
|
+
let { data: n } = r(t, i("mentionSearch"), { query: e });
|
|
34
|
+
return n ?? [];
|
|
33
35
|
};
|
|
34
36
|
//#endregion
|
|
35
|
-
export {
|
|
37
|
+
export { a as useComments, s as useMentionSearch, o as useThread };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-comments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "Comment threads on any anchor your app can name — replies, resolve/reopen, reactions, per-subject unread — live over the existing reactivity channel, with no second push mechanism. Access delegates to your app's own guard (access.viaEntity) or a scope and fails closed when neither is declared; @-mentions are re-filtered to the caller's tenant on search and re-validated at create, then delivered through plugin-notifications when it is installed. useComments / useThread / useMentionSearch in /web, an ejectable <CommentsThread> in @voltro/ui, plus inspect endpoints and a dashboard panel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"node": ">=24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@voltro/database": "0.
|
|
52
|
-
"@voltro/protocol": "0.
|
|
53
|
-
"@voltro/plugin-notifications": "0.
|
|
51
|
+
"@voltro/database": "0.56.0",
|
|
52
|
+
"@voltro/protocol": "0.56.0",
|
|
53
|
+
"@voltro/plugin-notifications": "0.56.0",
|
|
54
54
|
"effect": "^3.22.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|