convex-feedback 0.2.0-beta.9 → 0.2.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/README.md +398 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +49 -9
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/config.d.ts +1 -3
- package/dist/client/config.d.ts.map +1 -1
- package/dist/client/config.js.map +1 -1
- package/dist/client/index.d.ts +707 -15
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +243 -12
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +222 -4
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/admin.d.ts +5 -90
- package/dist/component/admin.d.ts.map +1 -1
- package/dist/component/admin.js +59 -29
- package/dist/component/admin.js.map +1 -1
- package/dist/component/comments.d.ts +107 -15
- package/dist/component/comments.d.ts.map +1 -1
- package/dist/component/comments.js +342 -28
- package/dist/component/comments.js.map +1 -1
- package/dist/component/crons.d.ts.map +1 -1
- package/dist/component/crons.js +4 -0
- package/dist/component/crons.js.map +1 -1
- package/dist/component/entries.d.ts +119 -91
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +348 -29
- package/dist/component/entries.js.map +1 -1
- package/dist/component/helpers.d.ts +32 -1
- package/dist/component/helpers.d.ts.map +1 -1
- package/dist/component/helpers.js +121 -4
- package/dist/component/helpers.js.map +1 -1
- package/dist/component/migrations.d.ts +24 -0
- package/dist/component/migrations.d.ts.map +1 -1
- package/dist/component/migrations.js +318 -0
- package/dist/component/migrations.js.map +1 -1
- package/dist/component/model.d.ts +455 -120
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/model.js +70 -2
- package/dist/component/model.js.map +1 -1
- package/dist/component/reactions.d.ts +37 -0
- package/dist/component/reactions.d.ts.map +1 -0
- package/dist/component/reactions.js +52 -0
- package/dist/component/reactions.js.map +1 -0
- package/dist/component/roadmap.d.ts +10 -46
- package/dist/component/roadmap.d.ts.map +1 -1
- package/dist/component/roadmap.js +239 -21
- package/dist/component/roadmap.js.map +1 -1
- package/dist/component/schema.d.ts +33 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +40 -4
- package/dist/component/schema.js.map +1 -1
- package/dist/react/index.d.ts +26 -238
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +65 -2
- package/dist/react/index.js.map +1 -1
- package/dist/test.d.ts +23 -5
- package/dist/test.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/README.md +6 -1
- package/src/client/api.ts +89 -7
- package/src/client/config.ts +1 -3
- package/src/client/index.ts +1237 -34
- package/src/component/README.md +32 -4
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +290 -8
- package/src/component/admin.ts +88 -30
- package/src/component/comments.ts +405 -26
- package/src/component/crons.ts +10 -0
- package/src/component/entries.ts +388 -27
- package/src/component/helpers.ts +163 -4
- package/src/component/migrations.ts +415 -0
- package/src/component/model.ts +381 -117
- package/src/component/reactions.ts +60 -0
- package/src/component/roadmap.ts +357 -35
- package/src/component/schema.ts +40 -4
- package/src/react/README.md +5 -0
- package/src/react/index.ts +90 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
     
|
|
2
2
|
|
|
3
3
|
[Vite demo](https://convex-feedback-vite.vercel.app/) • [Expo demo](https://convex-feedback-expo.vercel.app/) • [React Native demo](https://convex-feedback-native.vercel.app/)
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
A headless, fully typed Convex component for product feedback, feature requests, bug reports, entry upvotes, lazy nested comments, comment likes, full-text search, duplicate suggestions, and admin workflows.
|
|
8
8
|
|
|
9
|
-
> Looking for a ready-made public interface? [
|
|
9
|
+
> Looking for a ready-made public interface? `[convex-feedback-ui](../convex-feedback-ui/README.md)` provides optional React DOM and React Native screens and compound primitives. For internal triage, fork the [Clerk admin panel](../../apps/admin/withClerk/README.md).
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
@@ -19,13 +19,18 @@ A headless, fully typed Convex component for product feedback, feature requests,
|
|
|
19
19
|
- Convex full-text search.
|
|
20
20
|
- Exact-title + full-text duplicate suggestions.
|
|
21
21
|
- Host-controlled authentication and admin permissions.
|
|
22
|
+
- Indexed actor-scoped activity for entries, comments, and reactions.
|
|
22
23
|
- Admin priority and roadmap workflows.
|
|
23
24
|
- Optional host-defined mutation rate limiting.
|
|
25
|
+
- Optional host-defined lifecycle callbacks for creation and reaction changes.
|
|
24
26
|
- Configurable limits and behavior
|
|
25
27
|
- Typed React hooks.
|
|
26
28
|
- `convex-test` helper entry point.
|
|
27
29
|
|
|
28
|
-
The component
|
|
30
|
+
The component persists feedback domain data in `entries`, `comments`, `reactions`,
|
|
31
|
+
and `roadmap`. It also uses `roadmapRebalances` as internal bookkeeping while
|
|
32
|
+
large roadmap reorder operations complete; that state is not part of the public
|
|
33
|
+
feedback model.
|
|
29
34
|
|
|
30
35
|
## Requirements
|
|
31
36
|
|
|
@@ -77,6 +82,17 @@ The same job repeats every 30 days for now as a temporary, low-frequency
|
|
|
77
82
|
self-healing safeguard for missed or legacy records. It should be removed in a
|
|
78
83
|
future release after supported installations have had enough time to upgrade.
|
|
79
84
|
|
|
85
|
+
### Legacy comment deletion migration
|
|
86
|
+
|
|
87
|
+
Older releases stored soft-deleted comments with an internal `deletedAt`
|
|
88
|
+
timestamp. The upgrade migration now drains those tombstones, their descendant
|
|
89
|
+
threads, comment reactions, and legacy orphan documents in bounded scheduled
|
|
90
|
+
batches. New comment deletion is permanent; `deletedAt` is retained only as an
|
|
91
|
+
optional internal compatibility field during this migration and is not exposed
|
|
92
|
+
by public types or UI. The migration's progress markers are internal as well.
|
|
93
|
+
After supported installations have migrated, a future release can remove these
|
|
94
|
+
compatibility fields from the schema.
|
|
95
|
+
|
|
80
96
|
## 2. Expose the component through your host API
|
|
81
97
|
|
|
82
98
|
A Convex component cannot make authorization decisions using your host application's authentication state directly. `convex-feedback` therefore exposes a host wrapper: your app resolves the current actor, and the wrapper passes the stable actor identity into the component.
|
|
@@ -95,6 +111,7 @@ export const {
|
|
|
95
111
|
findSimilarEntries,
|
|
96
112
|
createEntry,
|
|
97
113
|
updateEntry,
|
|
114
|
+
deleteEntry,
|
|
98
115
|
setEntryStatus,
|
|
99
116
|
isAdmin,
|
|
100
117
|
isAuthenticated,
|
|
@@ -102,13 +119,17 @@ export const {
|
|
|
102
119
|
adminGetEntry,
|
|
103
120
|
adminSearchEntries,
|
|
104
121
|
setEntryPriority,
|
|
122
|
+
listUserEntries,
|
|
105
123
|
setEntryUpvote,
|
|
106
124
|
listComments,
|
|
125
|
+
listUserComments,
|
|
126
|
+
listUserReactions,
|
|
107
127
|
createComment,
|
|
108
128
|
updateComment,
|
|
109
129
|
deleteComment,
|
|
110
130
|
setCommentLike,
|
|
111
131
|
listRoadmap,
|
|
132
|
+
getRoadmapItem,
|
|
112
133
|
searchRoadmap,
|
|
113
134
|
createRoadmap,
|
|
114
135
|
createRoadmapForEntry,
|
|
@@ -151,7 +172,7 @@ return {
|
|
|
151
172
|
|
|
152
173
|
## 3. Configure behavior
|
|
153
174
|
|
|
154
|
-
Configuration is optional static host code;
|
|
175
|
+
Configuration is optional static host code; the values shown are the default configuration.
|
|
155
176
|
|
|
156
177
|
```ts
|
|
157
178
|
export const feedbackApi = exposeFeedbackApi(components.feedback, {
|
|
@@ -251,6 +272,286 @@ export const feedbackApi = exposeFeedbackApi(components.feedback, {
|
|
|
251
272
|
});
|
|
252
273
|
```
|
|
253
274
|
|
|
275
|
+
### Lifecycle callbacks
|
|
276
|
+
|
|
277
|
+
Lifecycle callbacks are host-wrapper hooks for business logic around creation
|
|
278
|
+
and reaction changes. They are grouped by domain so entry, comment, and reaction
|
|
279
|
+
events stay discoverable and can evolve independently without a flat list of
|
|
280
|
+
unrelated callback names. Configure them at the top level of
|
|
281
|
+
`exposeFeedbackApi`; `config` remains for component behavior:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
callbacks: {
|
|
285
|
+
rejection,
|
|
286
|
+
entries: {
|
|
287
|
+
beforeCreate,
|
|
288
|
+
afterCreate,
|
|
289
|
+
},
|
|
290
|
+
comments: {
|
|
291
|
+
beforeCreate,
|
|
292
|
+
afterCreate,
|
|
293
|
+
},
|
|
294
|
+
reactions: {
|
|
295
|
+
afterChange,
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
For entry and comment creation, the lifecycle is:
|
|
301
|
+
|
|
302
|
+
```text
|
|
303
|
+
auth → rate limiting → beforeCreate → component mutation → afterCreate
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
For reactions, it is:
|
|
307
|
+
|
|
308
|
+
```text
|
|
309
|
+
auth → rate limiting → component mutation → afterChange (only if state changed)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`beforeCreate` executes inside the host mutation and receives the full host
|
|
313
|
+
mutation context, including `db`, `auth`, `storage`, `scheduler`, `runQuery`,
|
|
314
|
+
`runMutation`, and `meta`. It runs before the component mutation, so it can do
|
|
315
|
+
simple synchronous validation or business logic, query or mutate host data when
|
|
316
|
+
necessary, sanitize content, return a supported transform, or call `reject()`.
|
|
317
|
+
Callbacks may also be async when the host needs a nested Convex call. A returned
|
|
318
|
+
patch is still passed through the component's normal validation; a callback
|
|
319
|
+
cannot bypass enabled-kind, length, nesting, or other component rules. Entry
|
|
320
|
+
callbacks may patch `kind`, `title`, `body`, and `metadata`. Comment callbacks
|
|
321
|
+
may patch only `body`; `entryId` and `parentCommentId` remain component-owned.
|
|
322
|
+
Callback inputs are readonly snapshots. The wrapper builds component arguments
|
|
323
|
+
from the original mutation arguments plus only the explicitly returned patch,
|
|
324
|
+
so mutating an event object cannot leak changes into component relationships.
|
|
325
|
+
|
|
326
|
+
For example, a host can sanitize profanity and reject content that becomes
|
|
327
|
+
empty after sanitization:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { v } from "convex/values";
|
|
331
|
+
|
|
332
|
+
const moderationRejection = v.object({
|
|
333
|
+
kind: v.literal("content_rejected"),
|
|
334
|
+
reason: v.string(),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
export const feedbackApi = exposeFeedbackApi(components.feedback, {
|
|
338
|
+
actor: resolveFeedbackActor,
|
|
339
|
+
callbacks: {
|
|
340
|
+
rejection: { behavior: "return", returns: moderationRejection },
|
|
341
|
+
entries: {
|
|
342
|
+
beforeCreate: (ctx, event, handlers) => {
|
|
343
|
+
const title = removeProfanity(event.input.title);
|
|
344
|
+
const body = removeProfanity(event.input.body);
|
|
345
|
+
if (!title.trim() || !body.trim()) {
|
|
346
|
+
return handlers.reject({
|
|
347
|
+
kind: "content_rejected",
|
|
348
|
+
reason: "Entry content is empty after sanitization",
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return { title, body };
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
comments: {
|
|
355
|
+
beforeCreate: (ctx, event, handlers) => {
|
|
356
|
+
void ctx;
|
|
357
|
+
const body = removeProfanity(event.input.body);
|
|
358
|
+
if (!body.trim()) {
|
|
359
|
+
return handlers.reject({
|
|
360
|
+
kind: "content_rejected",
|
|
361
|
+
reason: "Comment content is empty after sanitization",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return { body };
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The default callback rejection behavior is
|
|
372
|
+
`callbacks.rejection.behavior = "throw"` (or omit `rejection`). It is equivalent
|
|
373
|
+
to:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
callbacks: {
|
|
377
|
+
rejection: { behavior: "throw" },
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
An explicit `reject(value)` becomes a `ConvexError`, and no component mutation
|
|
382
|
+
occurs after that rejection. To return a validated value instead, configure:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
callbacks: {
|
|
386
|
+
rejection: {
|
|
387
|
+
behavior: "return",
|
|
388
|
+
returns: moderationRejection,
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Only explicit `reject()` calls use this behavior. Normal exceptions thrown by a
|
|
394
|
+
callback still throw, including when return mode is enabled. Return mode changes
|
|
395
|
+
the TypeScript result union of the relevant create mutation:
|
|
396
|
+
`createEntry` and/or `createComment` become `string | CallbackRejection` (and
|
|
397
|
+
retain any rate-limit rejection type). If rate limiting also uses return mode,
|
|
398
|
+
the create mutation's union composes both the rate-limit and callback rejection
|
|
399
|
+
values. Other mutations receive only their configured rate-limit result.
|
|
400
|
+
|
|
401
|
+
After callbacks are awaited in the originating host mutation. There are three
|
|
402
|
+
important failure models:
|
|
403
|
+
|
|
404
|
+
1. **Throw or nested mutation failure.** An uncaught `afterCreate` or
|
|
405
|
+
`afterChange` error, including a failed nested `ctx.runMutation`, runs in the
|
|
406
|
+
same transaction and rolls back the originating host mutation and its
|
|
407
|
+
component write.
|
|
408
|
+
2. **Scheduled action.** Schedule external work from the callback when it must
|
|
409
|
+
happen after commit:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
await ctx.scheduler.runAfter(
|
|
413
|
+
0,
|
|
414
|
+
internal.notifications.sendNotificationToUser,
|
|
415
|
+
args,
|
|
416
|
+
);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Scheduling is committed atomically with the mutation. The action runs after
|
|
420
|
+
commit, so a later action failure cannot roll back persisted entries,
|
|
421
|
+
comments, or reactions. Scheduled actions are at-most-once and are not
|
|
422
|
+
automatically retried; use an app-owned outbox or durable workflow when
|
|
423
|
+
stronger delivery guarantees are required.
|
|
424
|
+
|
|
425
|
+
3. **Explicit catch.** A callback can catch an error itself when the failure is
|
|
426
|
+
intentionally non-blocking. There is no package-level option that silently
|
|
427
|
+
ignores callback exceptions; swallowing an error is an application decision.
|
|
428
|
+
|
|
429
|
+
Reaction `afterChange` runs only when the actor's desired state actually changes.
|
|
430
|
+
It runs for both additions and removals. The event exposes
|
|
431
|
+
`transition: "added" | "removed"`, `previousCount`, and `count` (the final
|
|
432
|
+
count), plus the reacting actor and the target author/context: `entry` for an
|
|
433
|
+
entry upvote, or `comment` for a comment like. Entry-upvote events also expose
|
|
434
|
+
`entryId`, while comment-like events expose `commentId` and `entryId`; these
|
|
435
|
+
top-level IDs always match the IDs in the nested target context. Comment-like
|
|
436
|
+
events include the comment author and `entryId` without reading or serializing
|
|
437
|
+
the parent entry.
|
|
438
|
+
|
|
439
|
+
Rich component callback context is requested only when the corresponding
|
|
440
|
+
`afterCreate` or `afterChange` callback is configured. With no after callback,
|
|
441
|
+
creation and reaction mutations keep their lean result path and avoid
|
|
442
|
+
callback-only reads and serialization.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
afterChange: async (ctx, event) => {
|
|
446
|
+
if (event.transition !== "added") return;
|
|
447
|
+
|
|
448
|
+
const thresholds = [5, 10, 20];
|
|
449
|
+
const crossed = thresholds.some(
|
|
450
|
+
(threshold) => event.previousCount < threshold && event.count >= threshold,
|
|
451
|
+
);
|
|
452
|
+
if (!crossed) return;
|
|
453
|
+
|
|
454
|
+
// event.count is authoritative; event.previousCount prevents duplicates
|
|
455
|
+
// when a count changes without crossing a configured threshold.
|
|
456
|
+
};
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
For notifications, keep the provider and recipient policy in the host app.
|
|
460
|
+
This small example uses an app-owned `sendNotificationToUser` internal action;
|
|
461
|
+
the component does not know about email, push, or notification providers:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
// convex/notifications.ts
|
|
465
|
+
import { internalAction } from "convex/server";
|
|
466
|
+
import { v } from "convex/values";
|
|
467
|
+
|
|
468
|
+
export const sendNotificationToUser = internalAction({
|
|
469
|
+
args: {
|
|
470
|
+
userId: v.string(),
|
|
471
|
+
title: v.string(),
|
|
472
|
+
body: v.string(),
|
|
473
|
+
},
|
|
474
|
+
handler: async (ctx, args) => {
|
|
475
|
+
void ctx;
|
|
476
|
+
// `notificationProvider` is an app-owned email/push integration.
|
|
477
|
+
await notificationProvider.send(args);
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
The host wrapper can then schedule admins, the entry author, the parent
|
|
483
|
+
comment author, and threshold notifications:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
// convex/feedback.ts
|
|
487
|
+
import { internal } from "./_generated/api";
|
|
488
|
+
|
|
489
|
+
const ADMIN_USER_IDS = ["admin-1", "admin-2"];
|
|
490
|
+
const REACTION_THRESHOLDS = [5, 10, 20];
|
|
491
|
+
|
|
492
|
+
export const { createEntry, createComment, setEntryUpvote, setCommentLike } =
|
|
493
|
+
exposeFeedbackApi(components.feedback, {
|
|
494
|
+
actor: resolveFeedbackActor,
|
|
495
|
+
callbacks: {
|
|
496
|
+
entries: {
|
|
497
|
+
afterCreate: async (ctx, event) => {
|
|
498
|
+
await Promise.all(
|
|
499
|
+
ADMIN_USER_IDS.map((userId) =>
|
|
500
|
+
ctx.scheduler.runAfter(
|
|
501
|
+
0,
|
|
502
|
+
internal.notifications.sendNotificationToUser,
|
|
503
|
+
{
|
|
504
|
+
userId,
|
|
505
|
+
title: "New feedback",
|
|
506
|
+
body: event.entry.title,
|
|
507
|
+
},
|
|
508
|
+
),
|
|
509
|
+
),
|
|
510
|
+
);
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
comments: {
|
|
514
|
+
afterCreate: async (ctx, event) => {
|
|
515
|
+
const userId = event.parentComment?.actorId ?? event.entry.actorId;
|
|
516
|
+
await ctx.scheduler.runAfter(
|
|
517
|
+
0,
|
|
518
|
+
internal.notifications.sendNotificationToUser,
|
|
519
|
+
{
|
|
520
|
+
userId,
|
|
521
|
+
title: event.parentComment ? "New reply" : "New comment",
|
|
522
|
+
body: event.comment.body,
|
|
523
|
+
},
|
|
524
|
+
);
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
reactions: {
|
|
528
|
+
afterChange: async (ctx, event) => {
|
|
529
|
+
if (event.transition !== "added") return;
|
|
530
|
+
const crossed = REACTION_THRESHOLDS.some(
|
|
531
|
+
(threshold) =>
|
|
532
|
+
event.previousCount < threshold && event.count >= threshold,
|
|
533
|
+
);
|
|
534
|
+
if (!crossed) return;
|
|
535
|
+
|
|
536
|
+
const userId =
|
|
537
|
+
event.type === "entry_upvote"
|
|
538
|
+
? event.entry.actorId
|
|
539
|
+
: event.comment.actorId;
|
|
540
|
+
await ctx.scheduler.runAfter(
|
|
541
|
+
0,
|
|
542
|
+
internal.notifications.sendNotificationToUser,
|
|
543
|
+
{
|
|
544
|
+
userId,
|
|
545
|
+
title: "Your feedback is getting attention",
|
|
546
|
+
body: `Reaction count: ${event.count}`,
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
254
555
|
## 4. Create typed React hooks
|
|
255
556
|
|
|
256
557
|
If your client uses React or React Native, bind the generated host API once. Calling `createFeedbackHooks()` without an API argument defaults to `anyApi.feedback`; pass the generated namespace explicitly when the component is exposed elsewhere:
|
|
@@ -299,6 +600,92 @@ Metadata is intentionally absent from entry lists, searches, and duplicate sugge
|
|
|
299
600
|
Public entry results include `viewerIsAuthor` when returned by the current wrapper deployment. It is computed from the server-resolved actor and the stored entry author; clients should use it only to present author-only UI such as an Edit action. `updateEntry` still rechecks ownership in the component
|
|
300
601
|
mutation, so a caller that is not the entry author is rejected. Admins retain their existing permission to edit entries through the admin workflow.
|
|
301
602
|
|
|
603
|
+
### Actor-scoped activity
|
|
604
|
+
|
|
605
|
+
The host wrapper also exposes cursor-paginated activity queries for the authenticated actor:
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
const userEntries = await listUserEntries({
|
|
609
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
610
|
+
});
|
|
611
|
+
const userComments = await listUserComments({
|
|
612
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
613
|
+
});
|
|
614
|
+
const userReactions = await listUserReactions({
|
|
615
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
616
|
+
});
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
A trusted Convex function can compose these wrappers through the generated host API. `ctx.runQuery` keeps the caller's authentication context, so the configured actor callback still selects the actor and no `actorId` is passed:
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
import { api } from "./_generated/api";
|
|
623
|
+
import { query } from "./_generated/server";
|
|
624
|
+
|
|
625
|
+
export const getMyActivity = query({
|
|
626
|
+
args: {},
|
|
627
|
+
handler: async (ctx) => {
|
|
628
|
+
const entryQuery = api.feedback.listUserEntries;
|
|
629
|
+
const commentQuery = api.feedback.listUserComments;
|
|
630
|
+
const reactionQuery = api.feedback.listUserReactions;
|
|
631
|
+
const opts = { cursor: null, numItems: 20 };
|
|
632
|
+
const entries = await ctx.runQuery(entryQuery, { paginationOpts: opts });
|
|
633
|
+
const comments = await ctx.runQuery(commentQuery, { paginationOpts: opts });
|
|
634
|
+
const reactions = await ctx.runQuery(reactionQuery, {
|
|
635
|
+
paginationOpts: opts,
|
|
636
|
+
});
|
|
637
|
+
return { entries, comments, reactions };
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
These wrappers do not accept an `actorId`; they always use the actor returned by the configured host callback. Entries include their own content, status, timestamps, and counts. Comments include their body, `parentCommentId`, and the parent entry title without loading a parent-comment body. Pending deletions are hidden immediately.
|
|
643
|
+
|
|
644
|
+
Reaction results are discriminated by `type` (`"entry_upvote"` or `"comment_like"`) and include reaction creation time plus resolved live target context. Permanent entry and comment deletion remove dependent reactions in scheduled cleanup batches, so completed deletions do not leave activity records or orphan targets behind.
|
|
645
|
+
|
|
646
|
+
Trusted server consumers can call the component-level actor query directly with a known `actorId`. `entries.listByActor` additionally accepts `includeAdminContext: true` when an export or other server-side workflow needs retained metadata, priority, or roadmap context; the normal `listUserEntries` wrapper always strips those private fields.
|
|
647
|
+
|
|
648
|
+
For example, an internal Convex function can query all three activity feeds for a known actor without requiring request authentication:
|
|
649
|
+
|
|
650
|
+
```ts
|
|
651
|
+
import { components } from "./_generated/api";
|
|
652
|
+
import { internalQuery } from "./_generated/server";
|
|
653
|
+
import { v } from "convex/values";
|
|
654
|
+
|
|
655
|
+
export const getActorActivity = internalQuery({
|
|
656
|
+
args: { actorId: v.string() },
|
|
657
|
+
handler: async (ctx, args) => {
|
|
658
|
+
const opts = { cursor: null, numItems: 100 };
|
|
659
|
+
const entries = await ctx.runQuery(
|
|
660
|
+
components.feedback.entries.listByActor,
|
|
661
|
+
{
|
|
662
|
+
actorId: args.actorId,
|
|
663
|
+
paginationOpts: opts,
|
|
664
|
+
includeAdminContext: true,
|
|
665
|
+
},
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
const comments = await ctx.runQuery(
|
|
669
|
+
components.feedback.comments.listByActor,
|
|
670
|
+
{
|
|
671
|
+
actorId: args.actorId,
|
|
672
|
+
paginationOpts: opts,
|
|
673
|
+
},
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const reactions = await ctx.runQuery(
|
|
677
|
+
components.feedback.reactions.listByActor,
|
|
678
|
+
{
|
|
679
|
+
actorId: args.actorId,
|
|
680
|
+
paginationOpts: opts,
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
return { entries, comments, reactions };
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
```
|
|
688
|
+
|
|
302
689
|
## Admin panel
|
|
303
690
|
|
|
304
691
|
The standalone [Vite and Expo Clerk admin apps](../../apps/admin/withClerk/README.md) provide an inbox, entry detail workflow, and a stage-based roadmap. They are reference applications to fork and deploy, not reusable UI exports.
|
|
@@ -319,7 +706,7 @@ actor: async (ctx) => {
|
|
|
319
706
|
|
|
320
707
|
Set the claim and Convex Clerk provider using Clerk's current integration instructions, then export the complete wrapper API shown above as `convex/feedback.ts`. Both reference apps use `anyApi.feedback` by default and reactively check `isAdmin` at their root, offering retry when the access check fails; every admin query and mutation still rechecks the actor on the server.
|
|
321
708
|
|
|
322
|
-
Admin entries may have an optional `low`, `medium`, or `high` priority and one roadmap relation. Deleting a roadmap item detaches all related feedback. Public entry queries and the existing public UI do not expose priority, while roadmap reads are public.
|
|
709
|
+
Admin entries may have an optional `low`, `medium`, or `high` priority and one roadmap relation. Deleting a roadmap item detaches all related feedback. Permanent entry deletion first hides the entry, detaches its roadmap relation, and schedules bounded cleanup of reactions and comments before hard deletion. Public entry queries and the existing public UI do not expose priority, while roadmap reads are public.
|
|
323
710
|
|
|
324
711
|
## Entry kinds and statuses
|
|
325
712
|
|
|
@@ -414,7 +801,7 @@ A comment query returns exactly one direct-child level. Opening a reply branch s
|
|
|
414
801
|
|
|
415
802
|
`replyCount` is the number of **direct children**. `entry.commentCount` is the total number of comments/replies belonging to the entry.
|
|
416
803
|
|
|
417
|
-
|
|
804
|
+
Deleting a comment permanently removes it, all descendants, and their reactions in bounded scheduled batches. The comment and its descendants are hidden as soon as deletion starts; `replyCount` and `entry.commentCount` are updated as documents are removed.
|
|
418
805
|
|
|
419
806
|
## Upvotes and likes
|
|
420
807
|
|
|
@@ -449,12 +836,16 @@ The wrapper exposes:
|
|
|
449
836
|
| `isAuthenticated` | query | Whether the current request has an actor |
|
|
450
837
|
| `createEntry` | mutation | Create feedback |
|
|
451
838
|
| `updateEntry` | mutation | Edit feedback; admins may also change its kind |
|
|
839
|
+
| `deleteEntry` | mutation | Permanently delete feedback; admins only |
|
|
452
840
|
| `setEntryStatus` | mutation | Admin workflow status change |
|
|
453
841
|
| `setEntryUpvote` | mutation | Idempotently set entry upvote state |
|
|
842
|
+
| `listUserEntries` | query | Entries created by the authenticated actor |
|
|
454
843
|
| `listComments` | query | One paginated direct-child comment level |
|
|
844
|
+
| `listUserComments` | query | Comments created by the authenticated actor |
|
|
845
|
+
| `listUserReactions` | query | Entry upvotes and comment likes by the actor |
|
|
455
846
|
| `createComment` | mutation | Create comment or reply |
|
|
456
847
|
| `updateComment` | mutation | Edit a comment |
|
|
457
|
-
| `deleteComment` | mutation |
|
|
848
|
+
| `deleteComment` | mutation | Permanently delete a comment subtree |
|
|
458
849
|
| `setCommentLike` | mutation | Idempotently set comment like state |
|
|
459
850
|
| `createRoadmapForEntry` | mutation | Create a roadmap item and attach an entry atomically |
|
|
460
851
|
|