convex-feedback 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,91 +1,125 @@
1
+ [![npm version](https://badge.fury.io/js/convex-feedback.svg)](https://badge.fury.io/js/convex-feedback) ![NPM License](https://img.shields.io/npm/l/convex-feedback) ![NPM Downloads](https://img.shields.io/npm/dw/convex-feedback) ![GitHub forks](https://img.shields.io/github/forks/moumen-io/convex-feedback) ![GitHub Repo stars](https://img.shields.io/github/stars/moumen-io/convex-feedback)
2
+
1
3
  # convex-feedback
2
4
 
3
- Headless Convex component for product feedback, feature requests, bug reports, entry upvotes, recursive discussions, comment likes, and duplicate suggestions.
5
+ A headless, fully typed Convex component for product feedback, feature requests, bug reports, entry upvotes, lazy nested comments, comment likes, full-text search, and duplicate suggestions.
4
6
 
5
- ## Data model
7
+ > Looking for a ready-made interface? `[convex-feedback-ui](../convex-feedback-ui/README.md)` provides optional React DOM and React Native screens and compound primitives on top of this package.
6
8
 
7
- The component owns exactly three tables.
9
+ ## Features
8
10
 
9
- ### `entries`
11
+ - Feedback, feature requests, and bug reports.
12
+ - Canny-style entry upvotes.
13
+ - Recursive comments loaded one level at a time.
14
+ - Comment likes.
15
+ - Indexed `top` / `newest` entry ordering.
16
+ - Indexed `top` / `newest` / `oldest` comment ordering.
17
+ - Convex full-text search.
18
+ - Exact-title + full-text duplicate suggestions.
19
+ - Host-controlled authentication and moderator permissions.
20
+ - Configurable limits and behavior
21
+ - Typed React hooks.
22
+ - `convex-test` helper entry point.
10
23
 
11
- Stores the feedback body and list-query counters:
24
+ The component owns only three tables: `entries`, `comments`, and `reactions`.
12
25
 
13
- - `kind`: `feedback | feature_request | bug_report`
14
- - `status`: `open | under_review | planned | in_progress | completed | closed`
15
- - `actorId`, `title`, `body`
16
- - `normalizedTitle`, `searchText`
17
- - `upvoteCount`, `commentCount`
18
- - optional `updatedAt`
26
+ ## Requirements
19
27
 
20
- `_creationTime` is used instead of a duplicate `createdAt` field.
28
+ - An existing Convex application.
29
+ - `convex` installed in the host project.
30
+ - React is required only when using `convex-feedback/react`.
21
31
 
22
- ### `comments`
32
+ ## Installation
23
33
 
24
- Stores one recursive adjacency-list node per comment:
34
+ ```bash
35
+ npm install convex-feedback
36
+ ```
25
37
 
26
- - `entryId`
27
- - optional `parentCommentId`
28
- - `actorId`, `depth`, `body`
29
- - `likeCount`
30
- - `replyCount` (direct children only)
31
- - optional `updatedAt`, `deletedAt`
38
+ ## 1. Install the component in Convex
32
39
 
33
- A deleted comment remains as a tombstone so descendants keep their place in the tree. Public serializers return `body: null` after deletion.
40
+ Create or update your host application's `convex/convex.config.ts`:
34
41
 
35
- ### `reactions`
42
+ ```ts
43
+ import { defineApp } from "convex/server";
44
+ import feedback from "convex-feedback/convex.config.js";
36
45
 
37
- A single sparse table stores both entry upvotes and comment likes. A record has either `entryId` or `commentId`, never both. Query-specific indexes enforce one reaction lookup per actor/target.
46
+ const app = defineApp();
47
+ app.use(feedback);
38
48
 
39
- ## Lazy comments
49
+ export default app;
50
+ ```
40
51
 
41
- `listComments` accepts an optional `parentCommentId` and returns only direct children of that parent. Omitting `parentCommentId` returns top-level comments. Each level is independently paginated.
52
+ You can install multiple independent instances by giving them different component names using Convex's normal component configuration APIs.
42
53
 
43
- Default UI behavior is therefore:
54
+ Run Convex so the host application's component references are generated:
44
55
 
45
- ```text
46
- root comments query
47
- comment A
48
- click "View replies"
49
- direct replies query for A
50
- reply A1
51
- click "View replies"
52
- direct replies query for A1
56
+ ```bash
57
+ npx convex dev
53
58
  ```
54
59
 
55
- `maxDepth` is enforced when a reply is created. The default host config is `5`.
60
+ ## 2. Expose the component through your host API
56
61
 
57
- ## Ordering
62
+ 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.
58
63
 
59
- Comments support three global server-side sorts:
64
+ Create a host module such as `convex/feedback.ts`:
60
65
 
61
- - `top`: `likeCount DESC`, then Convex index `_creationTime DESC`
62
- - `newest`: `_creationTime DESC`
63
- - `oldest`: `_creationTime ASC`
66
+ ```ts
67
+ import { exposeFeedbackApi } from "convex-feedback";
68
+
69
+ import { components } from "./_generated/api";
70
+
71
+ export const {
72
+ listEntries,
73
+ getEntry,
74
+ searchEntries,
75
+ findSimilarEntries,
76
+ createEntry,
77
+ updateEntry,
78
+ setEntryStatus,
79
+ setEntryUpvote,
80
+ listComments,
81
+ createComment,
82
+ updateComment,
83
+ deleteComment,
84
+ setCommentLike,
85
+ } = exposeFeedbackApi(components.feedback, {
86
+ actor: async (ctx) => {
87
+ const identity = await ctx.auth.getUserIdentity();
88
+
89
+ if (identity === null) return null;
90
+
91
+ return {
92
+ id: identity.tokenIdentifier,
93
+ isModerator: false,
94
+ };
95
+ },
96
+ });
97
+ ```
98
+
99
+ ### Actor IDs
64
100
 
65
- All three are index-backed. Do not add an arbitrary server comparator: it cannot preserve correct cursor pagination without an index. UI clients may transform already-loaded comments locally.
101
+ `actor.id` should be stable for the same user.
66
102
 
67
- Entries support `top` and `newest` with filter-specific indexes for kind/status combinations.
103
+ The component does not store a user/profile table. Keep display names, avatars, roles, and profile data in your application.
68
104
 
69
- ## Search and duplicates
105
+ ### Moderators
70
106
 
71
- The component stores one derived `searchText` (`title + body`) and uses a Convex search index. `searchEntries` returns full-text results. `findSimilarEntries` returns:
107
+ Return `isModerator: true` for actors that may perform moderator-only operations such as status changes.
72
108
 
73
109
  ```ts
74
- {
75
- exact: FeedbackEntry[];
76
- similar: FeedbackEntry[];
77
- }
110
+ return {
111
+ id: identity.tokenIdentifier,
112
+ isModerator: await isFeedbackModerator(ctx, identity.tokenIdentifier),
113
+ };
78
114
  ```
79
115
 
80
- `exact` uses normalized title equality. `similar` uses full-text relevance and excludes exact matches. These are lexical suggestions, not semantic/AI duplicate claims.
81
-
82
- ## Static configuration
116
+ ## 3. Configure behavior
83
117
 
84
- Configuration is host code, not a database table.
118
+ Configuration is optional static host code; The values shown are the default configuration.
85
119
 
86
120
  ```ts
87
- exposeFeedbackApi(components.feedback, {
88
- actor: resolveActor,
121
+ export const feedbackApi = exposeFeedbackApi(components.feedback, {
122
+ actor: resolveFeedbackActor,
89
123
  config: {
90
124
  entries: {
91
125
  enabledKinds: ["feedback", "feature_request", "bug_report"],
@@ -109,75 +143,230 @@ exposeFeedbackApi(components.feedback, {
109
143
  },
110
144
  limits: {
111
145
  titleLength: 160,
112
- bodyLength: 10000,
113
- commentLength: 5000,
146
+ bodyLength: 10_000,
147
+ commentLength: 5_000,
114
148
  },
115
149
  },
116
150
  });
117
151
  ```
118
152
 
119
- All nested values are optional in overrides; defaults are merged by `createFeedbackConfig`.
153
+ All configuration fields are documented in the exported TypeScript types and appear in editor IntelliSense.
120
154
 
121
- ## Authentication boundary
155
+ ## 4. Create typed React hooks
122
156
 
123
- A Convex component is isolated from the host app. The host wrapper receives a resolver:
157
+ If your client uses React or React Native, bind the generated host API once:
124
158
 
125
159
  ```ts
126
- actor: async (ctx) => {
127
- const identity = await ctx.auth.getUserIdentity();
128
- if (identity === null) return null;
129
- return { id: identity.subject, isModerator: false };
130
- };
160
+ // src/feedback.ts
161
+ import { createFeedbackHooks } from "convex-feedback/react";
162
+
163
+ import { api } from "../convex/_generated/api";
164
+
165
+ export const feedbackHooks = createFeedbackHooks(api.feedback, {
166
+ entryPageSize: 20,
167
+ commentPageSize: 20,
168
+ replyPageSize: 10,
169
+ });
170
+ ```
171
+
172
+ Then use the hooks directly or pass `feedbackHooks` to `convex-feedback-ui`.
173
+
174
+ ```tsx
175
+ const entries = feedbackHooks.useEntries({
176
+ kinds: ["feature_request", "bug_report"],
177
+ sort: "top",
178
+ });
179
+
180
+ const createEntry = feedbackHooks.useCreateEntry();
181
+ ```
182
+
183
+ ## Entry kinds and statuses
184
+
185
+ ### Kinds
186
+
187
+ ```ts
188
+ type EntryKind = "feedback" | "feature_request" | "bug_report";
189
+ ```
190
+
191
+ ### Statuses
192
+
193
+ ```ts
194
+ type EntryStatus =
195
+ "open" | "under_review" | "planned" | "in_progress" | "completed" | "closed";
196
+ ```
197
+
198
+ The values are intentionally fixed for type safety. UI labels and presentation can be localized/customized in `convex-feedback-ui`.
199
+
200
+ ## Search
201
+
202
+ Full-text search is backed by Convex search indexes:
203
+
204
+ ```ts
205
+ const results = feedbackHooks.useSearchEntries({
206
+ searchQuery: "dark mode",
207
+ kinds: ["feature_request"],
208
+ limit: 10,
209
+ });
210
+ ```
211
+
212
+ ## Duplicate suggestions
213
+
214
+ ```ts
215
+ const result = feedbackHooks.useSimilarEntries({
216
+ title,
217
+ body,
218
+ kind: "feature_request",
219
+ limit: 3,
220
+ });
221
+ ```
222
+
223
+ The result has two groups:
224
+
225
+ ```ts
226
+ {
227
+ exact: FeedbackEntry[];
228
+ similar: FeedbackEntry[];
229
+ }
230
+ ```
231
+
232
+ `limit` is the **maximum combined number of suggestions**. Exact normalized-title matches consume the limit first; only remaining slots are filled by relevance-ranked full-text matches.
233
+
234
+ For `limit: 3`:
235
+
236
+ - 3 exact matches → 3 exact, 0 similar;
237
+ - 2 exact matches → 2 exact, at most 1 similar;
238
+ - 0 exact matches → at most 3 similar.
239
+
240
+ Exact matches are never duplicated in `similar`.
241
+
242
+ This is lexical/full-text duplicate detection.
243
+
244
+ ## Comments and replies
245
+
246
+ Comments are recursive but deliberately lazy.
247
+
248
+ ```ts
249
+ // Top-level comments
250
+ const comments = feedbackHooks.useComments({
251
+ entryId,
252
+ sort: "top",
253
+ });
254
+
255
+ // Direct replies to one comment
256
+ const replies = feedbackHooks.useComments({
257
+ entryId,
258
+ parentCommentId: comment.id,
259
+ sort: "top",
260
+ });
131
261
  ```
132
262
 
133
- Reads can be anonymous. Writes call `requireActor`. Status changes require `isModerator: true`. Author edit/delete rules are still checked inside the component using the stable actor id passed by the host.
263
+ A comment query returns exactly one direct-child level. Opening a reply branch should mount another query for that child's direct replies.
264
+
265
+ `replyCount` is the number of **direct children**. `entry.commentCount` is the total number of comments/replies belonging to the entry.
266
+
267
+ Soft-deleted comments remain as tombstones so descendants keep their position in the thread.
268
+
269
+ ## Upvotes and likes
270
+
271
+ Entry upvotes and comment likes have separate public APIs even though the component stores both efficiently in one reactions table.
134
272
 
135
- ## Host-facing API
273
+ Both state mutations are idempotent and accept a desired final state:
136
274
 
137
- `exposeFeedbackApi` returns:
275
+ ```ts
276
+ await setEntryUpvote({
277
+ entryId,
278
+ desiredState: true,
279
+ });
138
280
 
139
- - `listEntries`
140
- - `getEntry`
141
- - `searchEntries`
142
- - `findSimilarEntries`
143
- - `createEntry`
144
- - `updateEntry`
145
- - `setEntryStatus`
146
- - `setEntryUpvote`
147
- - `listComments`
148
- - `createComment`
149
- - `updateComment`
150
- - `deleteComment`
151
- - `setCommentLike`
281
+ await setCommentLike({
282
+ commentId,
283
+ desiredState: false,
284
+ });
285
+ ```
152
286
 
153
- Export these from one of your host Convex modules so your generated host `api` can expose them to clients.
287
+ The mutation result uses `active` to report the authoritative final state returned by the server.
154
288
 
155
- ## React hooks
289
+ ## Host API
156
290
 
157
- `convex-feedback/react` exports `createFeedbackHooks(api, options)`. It binds the generated host references once and returns hooks for every read/write operation. Client page sizes are configured there (`entryPageSize`, `commentPageSize`, and `replyPageSize`) while the host config enforces the hard maximum page sizes. The resolved sizes are also exposed as `hooks.pageSizes` for UI layers.
291
+ The wrapper exposes:
158
292
 
159
- Pagination uses the `convex-helpers/react` pagination hook because component pagination uses the stream/paginator helper and needs `endCursor` stitching for reactive, gap-free pages.
293
+ | Function | Type | Purpose |
294
+ | -------------------- | -------- | -------------------------------------------------- |
295
+ | `listEntries` | query | Paginated entry list with server-side filters/sort |
296
+ | `getEntry` | query | Fetch one entry |
297
+ | `searchEntries` | query | Full-text search |
298
+ | `findSimilarEntries` | query | Exact + similar duplicate suggestions |
299
+ | `createEntry` | mutation | Create feedback |
300
+ | `updateEntry` | mutation | Edit author-owned/moderated feedback |
301
+ | `setEntryStatus` | mutation | Moderator workflow status change |
302
+ | `setEntryUpvote` | mutation | Idempotently set entry upvote state |
303
+ | `listComments` | query | One paginated direct-child comment level |
304
+ | `createComment` | mutation | Create comment or reply |
305
+ | `updateComment` | mutation | Edit a comment |
306
+ | `deleteComment` | mutation | Soft-delete a comment |
307
+ | `setCommentLike` | mutation | Idempotently set comment like state |
160
308
 
161
- ## Testing
309
+ Every public argument/result type is exported and documented for editor IntelliSense.
162
310
 
163
- The package exports `convex-feedback/test`:
311
+ ## Package entry points
164
312
 
165
313
  ```ts
314
+ // Host wrapper, configuration, public model/API types
315
+ import { exposeFeedbackApi } from "convex-feedback";
316
+
317
+ // React / React Native hooks
318
+ import { createFeedbackHooks } from "convex-feedback/react";
319
+
320
+ // Convex component definition
321
+ import feedback from "convex-feedback/convex.config.js";
322
+
323
+ // ComponentApi type generated by Convex
324
+ import type { ComponentApi } from "convex-feedback/_generated/component";
325
+
326
+ // convex-test helper
166
327
  import feedbackTest from "convex-feedback/test";
328
+ ```
329
+
330
+ ## Testing host integrations
331
+
332
+ The package exposes a `/test` entry point for `convex-test`.
333
+
334
+ ```ts
167
335
  import { convexTest } from "convex-test";
336
+ import feedbackTest from "convex-feedback/test";
337
+
338
+ import schema from "./convex/schema";
339
+
340
+ const modules = import.meta.glob("./convex/**/*.ts");
341
+ const t = convexTest(schema, modules);
168
342
 
169
- const t = convexTest();
170
343
  feedbackTest.register(t, "feedback");
171
344
  ```
172
345
 
173
- The repository tests cover idempotent entry upvotes, direct-child comment loading, max depth, idempotent comment likes/top ordering, exact duplicate normalization, and config merging.
346
+ Use host-level tests when you need to verify your authentication wrapper and public host API in the same shape your client will consume.
347
+
348
+ ## UI package
349
+
350
+ `convex-feedback` is intentionally headless. For a complete board or customizable primitives, install:
351
+
352
+ ```bash
353
+ npm install convex-feedback-ui
354
+ ```
355
+
356
+ Then read `[convex-feedback-ui](../convex-feedback-ui/README.md)`.
174
357
 
175
- ## Generated files
358
+ ## Development in this repository
176
359
 
177
- The committed `_generated` files are starter/bootstrap output. After installing dependencies, regenerate them:
360
+ From the monorepo root:
178
361
 
179
362
  ```bash
180
- npm run codegen -w convex-feedback
363
+ npm install
364
+ npm run codegen
365
+ npm run test:all
181
366
  ```
182
367
 
183
- Do not manually maintain generated API/data-model definitions.
368
+ When changing the component's schema or functions, regenerate component code before committing generated output.
369
+
370
+ ## License
371
+
372
+ Apache-2.0