@molecule/api-resource-comment 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,457 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-08-04T01:49:10.106Z
7
+ -->
8
+
9
+ # @molecule/api-resource-comment
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ Threaded comments resource for molecule.dev.
16
+
17
+ Polymorphic comments that attach to any resource type. Supports threaded
18
+ replies, pagination, and ownership-based authorization.
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { routes, requestHandlerMap } from '@molecule/api-resource-comment'
24
+
25
+ // Wire routes into your Express app via mlcl inject
26
+ // POST /:resourceType/:resourceId/comments
27
+ // GET /:resourceType/:resourceId/comments
28
+ // GET /comments/:commentId
29
+ // PUT /comments/:commentId
30
+ // DELETE /comments/:commentId
31
+ // GET /comments/:commentId/replies
32
+ // GET /:resourceType/:resourceId/comments/count — cheap badge counts
33
+ ```
34
+
35
+ ## Type
36
+
37
+ `resource`
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @molecule/api-resource-comment @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource zod
43
+ ```
44
+
45
+ ## API
46
+
47
+ ### Interfaces
48
+
49
+ #### `Comment`
50
+
51
+ A comment attached to a resource, with optional threading via `parentId`.
52
+
53
+ ```typescript
54
+ interface Comment {
55
+ /** Unique comment identifier. */
56
+ id: string
57
+ /** The type of resource this comment is attached to (e.g. 'project', 'post'). */
58
+ resourceType: string
59
+ /** The ID of the resource this comment is attached to. */
60
+ resourceId: string
61
+ /** The ID of the user who created this comment. */
62
+ userId: string
63
+ /** The parent comment ID for threaded replies, or `null` for top-level comments. */
64
+ parentId: string | null
65
+ /** The comment body text. */
66
+ body: string
67
+ /** When the comment was last edited, or `null` if never edited. */
68
+ editedAt: string | null
69
+ /** When the comment was created (ISO 8601). */
70
+ createdAt: string
71
+ /** When the comment was last updated (ISO 8601). */
72
+ updatedAt: string
73
+ }
74
+ ```
75
+
76
+ #### `CreateCommentInput`
77
+
78
+ Input for creating a new comment.
79
+
80
+ ```typescript
81
+ interface CreateCommentInput {
82
+ /** The comment body text. */
83
+ body: string
84
+ /** Optional parent comment ID for threaded replies. */
85
+ parentId?: string
86
+ }
87
+ ```
88
+
89
+ #### `PaginatedResult`
90
+
91
+ A paginated result set.
92
+
93
+ ```typescript
94
+ interface PaginatedResult<T> {
95
+ /** The result items for the current page. */
96
+ data: T[]
97
+ /** Total number of matching items across all pages. */
98
+ total: number
99
+ /** Maximum number of results per page. */
100
+ limit: number
101
+ /** Number of results skipped. */
102
+ offset: number
103
+ }
104
+ ```
105
+
106
+ #### `PaginationOptions`
107
+
108
+ Options for paginated queries.
109
+
110
+ ```typescript
111
+ interface PaginationOptions {
112
+ /** Maximum number of results to return. */
113
+ limit?: number
114
+ /** Number of results to skip. */
115
+ offset?: number
116
+ }
117
+ ```
118
+
119
+ #### `UpdateCommentInput`
120
+
121
+ Input for updating an existing comment.
122
+
123
+ ```typescript
124
+ interface UpdateCommentInput {
125
+ /** The updated comment body text. */
126
+ body: string
127
+ }
128
+ ```
129
+
130
+ ### Functions
131
+
132
+ #### `commentCount(req, res)`
133
+
134
+ Returns the total comment count for a resource.
135
+
136
+ ```typescript
137
+ function commentCount(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
138
+ ```
139
+
140
+ - `req` — The request with `resourceType` and `resourceId` params.
141
+ - `res` — The response object.
142
+
143
+ #### `create(req, res)`
144
+
145
+ Creates a new comment on a resource.
146
+
147
+ ```typescript
148
+ function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
149
+ ```
150
+
151
+ - `req` — The request with `resourceType` and `resourceId` params and comment body.
152
+ - `res` — The response object.
153
+
154
+ #### `createComment(resourceType, resourceId, userId, data)`
155
+
156
+ Creates a new comment on a resource.
157
+
158
+ ```typescript
159
+ function createComment(
160
+ resourceType: string,
161
+ resourceId: string,
162
+ userId: string,
163
+ data: CreateCommentInput,
164
+ ): Promise<Comment>
165
+ ```
166
+
167
+ - `resourceType` — The type of resource being commented on.
168
+ - `resourceId` — The ID of the resource being commented on.
169
+ - `userId` — The ID of the commenting user.
170
+ - `data` — The comment creation input.
171
+
172
+ **Returns:** The created comment.
173
+
174
+ #### `del(req, res)`
175
+
176
+ Deletes a comment. Only the comment owner can delete.
177
+
178
+ ```typescript
179
+ function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
180
+ ```
181
+
182
+ - `req` — The request with `commentId` param.
183
+ - `res` — The response object.
184
+
185
+ #### `deleteComment(commentId, userId)`
186
+
187
+ Deletes a comment. Only the comment owner can delete.
188
+
189
+ ```typescript
190
+ function deleteComment(commentId: string, userId: string): Promise<boolean>
191
+ ```
192
+
193
+ - `commentId` — The comment ID to delete.
194
+ - `userId` — The requesting user's ID (must match comment owner).
195
+
196
+ **Returns:** `true` if deleted, `false` if not found or unauthorized.
197
+
198
+ #### `getCommentById(commentId)`
199
+
200
+ Retrieves a single comment by ID.
201
+
202
+ ```typescript
203
+ function getCommentById(commentId: string): Promise<Comment | null>
204
+ ```
205
+
206
+ - `commentId` — The comment ID to look up.
207
+
208
+ **Returns:** The comment or `null` if not found.
209
+
210
+ #### `getCommentCount(resourceType, resourceId)`
211
+
212
+ Returns the total number of comments on a resource (including replies).
213
+
214
+ ```typescript
215
+ function getCommentCount(resourceType: string, resourceId: string): Promise<number>
216
+ ```
217
+
218
+ - `resourceType` — The type of resource.
219
+ - `resourceId` — The ID of the resource.
220
+
221
+ **Returns:** The total comment count.
222
+
223
+ #### `getCommentsByResource(resourceType, resourceId, options)`
224
+
225
+ Retrieves paginated comments for a resource, ordered by creation date descending.
226
+ Only returns top-level comments (no replies).
227
+
228
+ ```typescript
229
+ function getCommentsByResource(
230
+ resourceType: string,
231
+ resourceId: string,
232
+ options?: PaginationOptions,
233
+ ): Promise<PaginatedResult<Comment>>
234
+ ```
235
+
236
+ - `resourceType` — The type of resource.
237
+ - `resourceId` — The ID of the resource.
238
+ - `options` — Pagination options.
239
+
240
+ **Returns:** A paginated result of comments.
241
+
242
+ #### `getReplies(commentId, options)`
243
+
244
+ Retrieves paginated replies to a comment, ordered by creation date ascending.
245
+
246
+ ```typescript
247
+ function getReplies(
248
+ commentId: string,
249
+ options?: PaginationOptions,
250
+ ): Promise<PaginatedResult<Comment>>
251
+ ```
252
+
253
+ - `commentId` — The parent comment ID.
254
+ - `options` — Pagination options.
255
+
256
+ **Returns:** A paginated result of reply comments.
257
+
258
+ #### `list(req, res)`
259
+
260
+ Lists paginated top-level comments for a resource.
261
+
262
+ ```typescript
263
+ function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
264
+ ```
265
+
266
+ - `req` — The request with `resourceType` and `resourceId` params.
267
+ - `res` — The response object.
268
+
269
+ #### `read(req, res)`
270
+
271
+ Retrieves a single comment by ID.
272
+
273
+ ```typescript
274
+ function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
275
+ ```
276
+
277
+ - `req` — The request with `commentId` param.
278
+ - `res` — The response object.
279
+
280
+ #### `replies(req, res)`
281
+
282
+ Lists paginated replies to a comment.
283
+
284
+ ```typescript
285
+ function replies(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
286
+ ```
287
+
288
+ - `req` — The request with `commentId` param.
289
+ - `res` — The response object.
290
+
291
+ #### `update(req, res)`
292
+
293
+ Updates an existing comment. Only the comment owner can update.
294
+
295
+ ```typescript
296
+ function update(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
297
+ ```
298
+
299
+ - `req` — The request with `commentId` param and update body.
300
+ - `res` — The response object.
301
+
302
+ #### `updateComment(commentId, userId, data)`
303
+
304
+ Updates a comment. Only the comment owner can update.
305
+
306
+ ```typescript
307
+ function updateComment(
308
+ commentId: string,
309
+ userId: string,
310
+ data: UpdateCommentInput,
311
+ ): Promise<Comment | null>
312
+ ```
313
+
314
+ - `commentId` — The comment ID to update.
315
+ - `userId` — The requesting user's ID (must match comment owner).
316
+ - `data` — The update input.
317
+
318
+ **Returns:** The updated comment or `null` if not found or unauthorized.
319
+
320
+ ### Constants
321
+
322
+ #### `createCommentSchema`
323
+
324
+ Schema for validating comment creation input.
325
+
326
+ ```typescript
327
+ const createCommentSchema: z.ZodObject<
328
+ { body: z.ZodString; parentId: z.ZodOptional<z.ZodString> },
329
+ z.core.$strip
330
+ >
331
+ ```
332
+
333
+ #### `requestHandlerMap`
334
+
335
+ Handler map for comment routes.
336
+
337
+ ```typescript
338
+ const requestHandlerMap: {
339
+ readonly create: typeof create
340
+ readonly list: typeof list
341
+ readonly commentCount: typeof commentCount
342
+ readonly read: typeof read
343
+ readonly update: typeof update
344
+ readonly del: typeof del
345
+ readonly replies: typeof replies
346
+ }
347
+ ```
348
+
349
+ #### `routes`
350
+
351
+ Routes for comment CRUD and threaded replies.
352
+
353
+ ```typescript
354
+ const routes: readonly [
355
+ {
356
+ readonly method: 'post'
357
+ readonly path: '/:resourceType/:resourceId/comments'
358
+ readonly handler: 'create'
359
+ readonly middlewares: readonly ['authenticate']
360
+ },
361
+ {
362
+ readonly method: 'get'
363
+ readonly path: '/:resourceType/:resourceId/comments'
364
+ readonly handler: 'list'
365
+ },
366
+ {
367
+ readonly method: 'get'
368
+ readonly path: '/:resourceType/:resourceId/comments/count'
369
+ readonly handler: 'commentCount'
370
+ },
371
+ { readonly method: 'get'; readonly path: '/comments/:commentId'; readonly handler: 'read' },
372
+ {
373
+ readonly method: 'put'
374
+ readonly path: '/comments/:commentId'
375
+ readonly handler: 'update'
376
+ readonly middlewares: readonly ['authenticate']
377
+ },
378
+ {
379
+ readonly method: 'delete'
380
+ readonly path: '/comments/:commentId'
381
+ readonly handler: 'del'
382
+ readonly middlewares: readonly ['authenticate']
383
+ },
384
+ {
385
+ readonly method: 'get'
386
+ readonly path: '/comments/:commentId/replies'
387
+ readonly handler: 'replies'
388
+ },
389
+ ]
390
+ ```
391
+
392
+ #### `updateCommentSchema`
393
+
394
+ Schema for validating comment update input.
395
+
396
+ ```typescript
397
+ const updateCommentSchema: z.ZodObject<{ body: z.ZodString }, z.core.$strip>
398
+ ```
399
+
400
+ ## Injection Notes
401
+
402
+ ### Requirements
403
+
404
+ Peer dependencies:
405
+
406
+ - `@molecule/api-database` ^1.0.1
407
+ - `@molecule/api-i18n` ^1.0.1
408
+ - `@molecule/api-logger` ^1.0.1
409
+ - `@molecule/api-resource` ^1.0.1
410
+ - `zod` ^4.0.0
411
+
412
+ ### Runtime Dependencies
413
+
414
+ - `@molecule/api-database`
415
+ - `@molecule/api-i18n`
416
+ - `@molecule/api-logger`
417
+ - `@molecule/api-resource`
418
+ - `zod`
419
+
420
+ - **List endpoints return a PAGINATED envelope** `{ data, total, limit, offset }`, not a
421
+ bare array — read the rows off `result.data` (server). On the client, `unwrapList(res)`
422
+ from `@molecule/app-http` normalizes this envelope (pass it the whole HttpResponse), so
423
+ the rows come back; reading the response as a bare array — or `res.data` alone (which is
424
+ the envelope) — yields an EMPTY list.
425
+ - **Migration required.** `src/__setup__/comments.sql` ships with this package
426
+ and must exist in the target database before use (scaffolded apps apply it
427
+ automatically; existing apps must apply it first).
428
+ - **Reads are PUBLIC by default.** `list`, `read`, `replies`, and `count` ship
429
+ with no auth middleware (comment threads on public content). If the
430
+ commented resources are private in your app, add an authorizer that checks
431
+ access to the PARENT resource before serving its comments.
432
+ - **The author is always the session user.** Create/update/delete require
433
+ `authenticate`; the create handler ignores any client-supplied author id,
434
+ and update/delete verify ownership in the handler — keep those properties
435
+ in any custom path.
436
+ - **The parent is polymorphic and unverified** (no FK on
437
+ `resourceType`/`resourceId`): validate that the target exists in your domain
438
+ code if orphaned threads matter, and reuse the same canonical type slugs as
439
+ your other polymorphic resources (bookmarks, activity feed).
440
+
441
+ ## E2E Tests
442
+
443
+ Integration checklist — drive the real UI (live preview, no mocks), adapt
444
+ each item to this app's actual screens/flows, and check every box off one
445
+ by one. A box you can't check is an integration bug to fix — not a skip:
446
+
447
+ - [ ] Posting a comment on a commentable resource shows it in the thread
448
+ immediately and it persists across a full reload.
449
+ - [ ] Replying to a comment renders the reply nested under its parent.
450
+ - [ ] The author can edit their own comment and the updated text persists;
451
+ a DIFFERENT signed-in user gets no edit/delete controls on it and a
452
+ direct attempt is denied.
453
+ - [ ] Deleting an own comment removes it per the app's policy (gone or
454
+ tombstone) and stays removed after reload.
455
+ - [ ] A long thread paginates ("load more" fetches older comments) without
456
+ duplicating or dropping entries.
457
+ - [ ] A resource with no comments shows a readable empty state.
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/handlers/list.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI/E;;;;;GAKG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BrF"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/handlers/list.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAO/E;;;;;GAKG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BrF"}
@@ -6,6 +6,8 @@
6
6
  import { t } from '@molecule/api-i18n';
7
7
  import { logger } from '@molecule/api-logger';
8
8
  import { getCommentsByResource } from '../service.js';
9
+ /** Upper bound for the list page size — an uncapped `limit` is a table-dump primitive (e.g. `?limit=999999999`). */
10
+ const MAX_LIST_LIMIT = 500;
9
11
  /**
10
12
  * Lists paginated top-level comments for a resource.
11
13
  *
@@ -23,7 +25,7 @@ export async function list(req, res) {
23
25
  });
24
26
  return;
25
27
  }
26
- const limit = parseInt(req.query.limit, 10) || 20;
28
+ const limit = Math.min(MAX_LIST_LIMIT, Math.max(1, parseInt(req.query.limit, 10) || 20));
27
29
  const offset = parseInt(req.query.offset, 10) || 0;
28
30
  try {
29
31
  const result = await getCommentsByResource(resourceType, resourceId, { limit, offset });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-resource-comment",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Threaded comments on any resource with polymorphic attachment and pagination",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,8 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "dist"
20
+ "dist",
21
+ "README.md"
21
22
  ],
22
23
  "keywords": [
23
24
  "molecule",
@@ -29,14 +30,14 @@
29
30
  "devDependencies": {
30
31
  "@types/node": "26.1.2",
31
32
  "typescript": "6.0.3",
32
- "vitest": "4.1.10",
33
+ "vitest": "4.1.11",
33
34
  "zod": "4.4.3"
34
35
  },
35
36
  "peerDependencies": {
36
- "@molecule/api-database": "^1.0.0",
37
- "@molecule/api-i18n": "^1.0.0",
38
- "@molecule/api-logger": "^1.0.0",
39
- "@molecule/api-resource": "^1.0.0",
37
+ "@molecule/api-database": "^1.0.1",
38
+ "@molecule/api-i18n": "^1.0.1",
39
+ "@molecule/api-logger": "^1.0.1",
40
+ "@molecule/api-resource": "^1.0.1",
40
41
  "zod": "^4.0.0"
41
42
  },
42
43
  "peerDependenciesMeta": {