convex-feedback 0.1.5 → 0.2.0-beta.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.
Files changed (69) hide show
  1. package/README.md +70 -27
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/client/api.d.ts +76 -1
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/config.d.ts +1 -1
  6. package/dist/client/index.d.ts +9 -5
  7. package/dist/client/index.d.ts.map +1 -1
  8. package/dist/client/index.js +253 -8
  9. package/dist/client/index.js.map +1 -1
  10. package/dist/component/_generated/api.d.ts +4 -0
  11. package/dist/component/_generated/api.d.ts.map +1 -1
  12. package/dist/component/_generated/api.js.map +1 -1
  13. package/dist/component/_generated/component.d.ts +293 -5
  14. package/dist/component/_generated/component.d.ts.map +1 -1
  15. package/dist/component/admin.d.ts +130 -0
  16. package/dist/component/admin.d.ts.map +1 -0
  17. package/dist/component/admin.js +125 -0
  18. package/dist/component/admin.js.map +1 -0
  19. package/dist/component/comments.d.ts +7 -5
  20. package/dist/component/comments.d.ts.map +1 -1
  21. package/dist/component/comments.js +3 -3
  22. package/dist/component/comments.js.map +1 -1
  23. package/dist/component/entries.d.ts +29 -18
  24. package/dist/component/entries.d.ts.map +1 -1
  25. package/dist/component/entries.js +27 -6
  26. package/dist/component/entries.js.map +1 -1
  27. package/dist/component/helpers.d.ts +3 -1
  28. package/dist/component/helpers.d.ts.map +1 -1
  29. package/dist/component/helpers.js +28 -0
  30. package/dist/component/helpers.js.map +1 -1
  31. package/dist/component/model.d.ts +140 -24
  32. package/dist/component/model.d.ts.map +1 -1
  33. package/dist/component/model.js +30 -1
  34. package/dist/component/model.js.map +1 -1
  35. package/dist/component/roadmap.d.ts +166 -0
  36. package/dist/component/roadmap.d.ts.map +1 -0
  37. package/dist/component/roadmap.js +534 -0
  38. package/dist/component/roadmap.js.map +1 -0
  39. package/dist/component/schema.d.ts +61 -5
  40. package/dist/component/schema.d.ts.map +1 -1
  41. package/dist/component/schema.js +48 -2
  42. package/dist/component/schema.js.map +1 -1
  43. package/dist/component/tags.d.ts +58 -0
  44. package/dist/component/tags.d.ts.map +1 -0
  45. package/dist/component/tags.js +216 -0
  46. package/dist/component/tags.js.map +1 -0
  47. package/dist/react/index.d.ts +169 -12
  48. package/dist/react/index.d.ts.map +1 -1
  49. package/dist/react/index.js +72 -1
  50. package/dist/react/index.js.map +1 -1
  51. package/dist/test.d.ts +61 -5
  52. package/dist/test.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/client/README.md +1 -1
  55. package/src/client/api.ts +176 -0
  56. package/src/client/config.ts +1 -1
  57. package/src/client/index.ts +330 -13
  58. package/src/component/README.md +1 -1
  59. package/src/component/_generated/api.ts +4 -0
  60. package/src/component/_generated/component.ts +371 -9
  61. package/src/component/admin.ts +180 -0
  62. package/src/component/comments.ts +3 -2
  63. package/src/component/entries.ts +29 -5
  64. package/src/component/helpers.ts +44 -1
  65. package/src/component/model.ts +65 -4
  66. package/src/component/roadmap.ts +627 -0
  67. package/src/component/schema.ts +51 -1
  68. package/src/react/README.md +2 -2
  69. package/src/react/index.ts +139 -4
package/src/client/api.ts CHANGED
@@ -13,6 +13,10 @@ import type {
13
13
  FeedbackComment,
14
14
  FeedbackEntry,
15
15
  FeedbackMetadata,
16
+ AdminFeedbackEntry,
17
+ EntryPriority,
18
+ RoadmapItem,
19
+ RoadmapStatus,
16
20
  SimilarEntriesResult,
17
21
  } from "../component/model.js";
18
22
 
@@ -172,6 +176,60 @@ export type SetEntryStatusArgs = {
172
176
  status: EntryStatus;
173
177
  };
174
178
 
179
+ export type AdminListEntriesArgs = {
180
+ /** Convex cursor-pagination options. */
181
+ paginationOpts: PaginationOptions;
182
+ kinds?: EntryKind[];
183
+ status?: EntryStatus;
184
+ priority?: EntryPriority;
185
+ };
186
+
187
+ export type AdminSearchEntriesArgs = AdminListEntriesArgs & {
188
+ searchQuery: string;
189
+ };
190
+
191
+ export type SetEntryPriorityArgs = {
192
+ entryId: string;
193
+ priority: EntryPriority | null;
194
+ };
195
+
196
+ export type ListRoadmapArgs = {
197
+ /** Convex cursor-pagination options. */
198
+ paginationOpts: PaginationOptions;
199
+ status?: RoadmapStatus;
200
+ };
201
+ export type SearchRoadmapArgs = { searchQuery: string; limit?: number };
202
+ export type CreateRoadmapArgs = {
203
+ title: string;
204
+ description?: string;
205
+ status: RoadmapStatus;
206
+ };
207
+ export type CreateRoadmapForEntryArgs = CreateRoadmapArgs & {
208
+ entryId: string;
209
+ };
210
+ export type UpdateRoadmapArgs = {
211
+ roadmapId: string;
212
+ title: string;
213
+ description?: string;
214
+ };
215
+ export type DeleteRoadmapArgs = { roadmapId: string };
216
+ export type MoveRoadmapArgs = {
217
+ roadmapId: string;
218
+ status: RoadmapStatus;
219
+ previousItemId?: string;
220
+ nextItemId?: string;
221
+ };
222
+ export type AttachFeedbackToRoadmapArgs = {
223
+ roadmapId: string;
224
+ entryId: string;
225
+ };
226
+ export type DetachFeedbackFromRoadmapArgs = { entryId: string };
227
+ export type ListRoadmapFeedbackArgs = {
228
+ /** Convex cursor-pagination options. */
229
+ paginationOpts: PaginationOptions;
230
+ roadmapId: string;
231
+ };
232
+
175
233
  /**
176
234
  * Arguments for setting the current actor's entry-upvote state.
177
235
  */
@@ -308,6 +366,24 @@ export interface FeedbackPublicApi<
308
366
  Name extends string | undefined = string | undefined,
309
367
  RateLimitResult = never,
310
368
  > {
369
+ /** Returns whether the host actor has admin permissions. */
370
+ isAdmin: FunctionReference<
371
+ "query",
372
+ "public",
373
+ Record<string, never>,
374
+ boolean,
375
+ Name
376
+ >;
377
+
378
+ /** Returns whether the current request has an authenticated host actor. */
379
+ isAuthenticated: FunctionReference<
380
+ "query",
381
+ "public",
382
+ Record<string, never>,
383
+ boolean,
384
+ Name
385
+ >;
386
+
311
387
  /** Returns a cursor-paginated entry list. */
312
388
  listEntries: FunctionReference<
313
389
  "query",
@@ -371,6 +447,106 @@ export interface FeedbackPublicApi<
371
447
  Name
372
448
  >;
373
449
 
450
+ adminListEntries: FunctionReference<
451
+ "query",
452
+ "public",
453
+ AdminListEntriesArgs,
454
+ PaginationResult<AdminFeedbackEntry>,
455
+ Name
456
+ >;
457
+ adminGetEntry: FunctionReference<
458
+ "query",
459
+ "public",
460
+ GetEntryArgs,
461
+ AdminFeedbackEntry | null,
462
+ Name
463
+ >;
464
+ adminSearchEntries: FunctionReference<
465
+ "query",
466
+ "public",
467
+ AdminSearchEntriesArgs,
468
+ PaginationResult<AdminFeedbackEntry>,
469
+ Name
470
+ >;
471
+ setEntryPriority: FunctionReference<
472
+ "mutation",
473
+ "public",
474
+ SetEntryPriorityArgs,
475
+ null | RateLimitResult,
476
+ Name
477
+ >;
478
+
479
+ listRoadmap: FunctionReference<
480
+ "query",
481
+ "public",
482
+ ListRoadmapArgs,
483
+ PaginationResult<RoadmapItem>,
484
+ Name
485
+ >;
486
+ searchRoadmap: FunctionReference<
487
+ "query",
488
+ "public",
489
+ SearchRoadmapArgs,
490
+ RoadmapItem[],
491
+ Name
492
+ >;
493
+ createRoadmap: FunctionReference<
494
+ "mutation",
495
+ "public",
496
+ CreateRoadmapArgs,
497
+ string | RateLimitResult,
498
+ Name
499
+ >;
500
+ createRoadmapForEntry: FunctionReference<
501
+ "mutation",
502
+ "public",
503
+ CreateRoadmapForEntryArgs,
504
+ string | RateLimitResult,
505
+ Name
506
+ >;
507
+ updateRoadmap: FunctionReference<
508
+ "mutation",
509
+ "public",
510
+ UpdateRoadmapArgs,
511
+ null | RateLimitResult,
512
+ Name
513
+ >;
514
+ deleteRoadmap: FunctionReference<
515
+ "mutation",
516
+ "public",
517
+ DeleteRoadmapArgs,
518
+ null | RateLimitResult,
519
+ Name
520
+ >;
521
+ moveRoadmapItem: FunctionReference<
522
+ "mutation",
523
+ "public",
524
+ MoveRoadmapArgs,
525
+ number | RateLimitResult,
526
+ Name
527
+ >;
528
+ attachFeedbackToRoadmap: FunctionReference<
529
+ "mutation",
530
+ "public",
531
+ AttachFeedbackToRoadmapArgs,
532
+ null | RateLimitResult,
533
+ Name
534
+ >;
535
+ detachFeedbackFromRoadmap: FunctionReference<
536
+ "mutation",
537
+ "public",
538
+ DetachFeedbackFromRoadmapArgs,
539
+ null | RateLimitResult,
540
+ Name
541
+ >;
542
+ listRoadmapFeedback: FunctionReference<
543
+ "query",
544
+ "public",
545
+ ListRoadmapFeedbackArgs,
546
+ PaginationResult<FeedbackEntry>,
547
+ Name
548
+ >;
549
+
374
550
  /** Sets entry-upvote state, or returns a configured rate-limit rejection. */
375
551
  setEntryUpvote: FunctionReference<
376
552
  "mutation",
@@ -42,7 +42,7 @@ export interface FeedbackEntriesConfig {
42
42
  /**
43
43
  * Whether entry authors may edit their own title and body.
44
44
  *
45
- * Moderators are governed separately by the resolved actor permissions.
45
+ * Admins are governed separately by the resolved actor permissions.
46
46
  *
47
47
  * @default true
48
48
  */
@@ -23,15 +23,20 @@ import {
23
23
 
24
24
  import type { ComponentApi } from "../component/_generated/component.js";
25
25
  import {
26
+ adminEntryValidator,
26
27
  commentSortValidator,
27
28
  entryKindValidator,
29
+ entryPriorityValidator,
28
30
  entrySortValidator,
29
31
  entryStatusFilterValidator,
30
32
  entryStatusValidator,
31
33
  feedbackMetadataValidator,
32
34
  publicCommentValidator,
33
35
  publicEntryValidator,
36
+ roadmapItemValidator,
37
+ roadmapStatusValidator,
34
38
  similarEntriesValidator,
39
+ actorIsAdmin,
35
40
  type FeedbackActor,
36
41
  } from "../component/model.js";
37
42
  import type { FeedbackPublicApi } from "./api.js";
@@ -41,8 +46,10 @@ import {
41
46
  } from "./config.js";
42
47
 
43
48
  export type {
49
+ AdminFeedbackEntry,
44
50
  CommentSort,
45
51
  EntryKind,
52
+ EntryPriority,
46
53
  EntrySort,
47
54
  EntryStatus,
48
55
  EntryStatusFilter,
@@ -51,6 +58,8 @@ export type {
51
58
  FeedbackEntry,
52
59
  FeedbackMetadata,
53
60
  FeedbackMetadataValue,
61
+ RoadmapItem,
62
+ RoadmapStatus,
54
63
  SimilarEntriesResult,
55
64
  } from "../component/model.js";
56
65
  export type { FeedbackPublicApi } from "./api.js";
@@ -127,10 +136,13 @@ export interface ThrowingFeedbackRateLimitConfig {
127
136
  behavior?: "throw";
128
137
 
129
138
  /**
130
- * Whether all configured limiter groups apply to moderators.
139
+ * Whether all configured limiter groups apply to admins.
131
140
  *
132
141
  * @default false
133
142
  */
143
+ limitAdmins?: boolean;
144
+
145
+ /** @deprecated Use `limitAdmins`. */
134
146
  limitModerators?: boolean;
135
147
 
136
148
  /** Not accepted in throwing mode; select `"return"` to provide a validator. */
@@ -170,10 +182,13 @@ export interface ReturningFeedbackRateLimitConfig<
170
182
  returns: ReturnsValidator;
171
183
 
172
184
  /**
173
- * Whether all configured limiter groups apply to moderators.
185
+ * Whether all configured limiter groups apply to admins.
174
186
  *
175
187
  * @default false
176
188
  */
189
+ limitAdmins?: boolean;
190
+
191
+ /** @deprecated Use `limitAdmins`. */
177
192
  limitModerators?: boolean;
178
193
  }
179
194
 
@@ -262,10 +277,10 @@ export type ThrowingExposeFeedbackOptions = Omit<
262
277
  /** Optional component behavior and throwing rate-limit overrides. */
263
278
  config?: FeedbackConfigOverrides & {
264
279
  /**
265
- * Controls throwing behavior and the moderator exemption.
280
+ * Controls throwing behavior and the admin exemption.
266
281
  *
267
282
  * Omit this object to use `behavior: "throw"` and
268
- * `limitModerators: false`.
283
+ * `limitAdmins: false`.
269
284
  */
270
285
  rateLimiting?: FeedbackRateLimitConfig;
271
286
  };
@@ -316,9 +331,9 @@ function requireActor(actor: FeedbackActor | null): FeedbackActor {
316
331
  return actor;
317
332
  }
318
333
 
319
- function requireModerator(actor: FeedbackActor): void {
320
- if (!actor.isModerator) {
321
- throw new ConvexError("Moderator permissions are required.");
334
+ function requireAdmin(actor: FeedbackActor): void {
335
+ if (!actorIsAdmin(actor)) {
336
+ throw new ConvexError("Admin permissions are required.");
322
337
  }
323
338
  }
324
339
 
@@ -349,10 +364,9 @@ async function applyRateLimiter<
349
364
  | ReturningFeedbackRateLimitConfig<FeedbackRateLimitReturnValidator>
350
365
  | undefined,
351
366
  ): Promise<RateLimitResult<ReturnsValidator> | undefined> {
352
- if (
353
- limiter === undefined ||
354
- (actor.isModerator && rateLimitConfig?.limitModerators !== true)
355
- ) {
367
+ const limitAdmins =
368
+ rateLimitConfig?.limitAdmins ?? rateLimitConfig?.limitModerators ?? false;
369
+ if (limiter === undefined || (actorIsAdmin(actor) && !limitAdmins)) {
356
370
  return undefined;
357
371
  }
358
372
 
@@ -423,8 +437,44 @@ function buildFeedbackApi<
423
437
  v.object({ active: v.boolean(), likeCount: v.number() }),
424
438
  rateLimitReturnValidator,
425
439
  );
440
+ const numberReturns = rateLimitedReturns(
441
+ v.number(),
442
+ rateLimitReturnValidator,
443
+ );
444
+
445
+ const requireAdminActor = async (ctx: FeedbackAuthContext) => {
446
+ const actor = requireActor(await options.actor(ctx));
447
+ requireAdmin(actor);
448
+ return actor;
449
+ };
450
+
451
+ const applyAdminEditLimit = async (
452
+ ctx: FeedbackRateLimitContext,
453
+ actor: FeedbackActor,
454
+ ) =>
455
+ await applyRateLimiter(
456
+ ctx,
457
+ actor,
458
+ options.rateLimiters?.editContent,
459
+ rateLimitConfig,
460
+ );
426
461
 
427
462
  return {
463
+ isAdmin: queryGeneric({
464
+ args: {},
465
+ returns: v.boolean(),
466
+ handler: async (ctx) => {
467
+ const actor = await options.actor(ctx);
468
+ return actor !== null && actorIsAdmin(actor);
469
+ },
470
+ }),
471
+
472
+ isAuthenticated: queryGeneric({
473
+ args: {},
474
+ returns: v.boolean(),
475
+ handler: async (ctx) => (await options.actor(ctx)) !== null,
476
+ }),
477
+
428
478
  listEntries: queryGeneric({
429
479
  args: {
430
480
  paginationOpts: paginationOptsValidator,
@@ -461,7 +511,9 @@ function buildFeedbackApi<
461
511
  return await ctx.runQuery(component.entries.get, {
462
512
  entryId: args.entryId,
463
513
  ...actorIdFields(actor),
464
- ...(actor?.isModerator === true ? { viewerIsModerator: true } : {}),
514
+ ...(actor !== null && actorIsAdmin(actor)
515
+ ? { viewerIsAdmin: true }
516
+ : {}),
465
517
  });
466
518
  },
467
519
  }),
@@ -588,7 +640,7 @@ function buildFeedbackApi<
588
640
  returns: nullReturns,
589
641
  handler: async (ctx, args) => {
590
642
  const actor = requireActor(await options.actor(ctx));
591
- requireModerator(actor);
643
+ requireAdmin(actor);
592
644
  const limited = await applyRateLimiter(
593
645
  asRateLimitContext(ctx),
594
646
  actor,
@@ -604,6 +656,87 @@ function buildFeedbackApi<
604
656
  },
605
657
  }),
606
658
 
659
+ adminListEntries: queryGeneric({
660
+ args: {
661
+ paginationOpts: paginationOptsValidator,
662
+ kinds: v.optional(v.array(entryKindValidator)),
663
+ status: v.optional(entryStatusValidator),
664
+ priority: v.optional(entryPriorityValidator),
665
+ },
666
+ returns: paginationResultValidator(adminEntryValidator),
667
+ handler: async (ctx, args) => {
668
+ const actor = await requireAdminActor(ctx);
669
+ return await ctx.runQuery(component.admin.listEntries, {
670
+ ...(args.kinds === undefined ? {} : { kinds: args.kinds }),
671
+ ...(args.status === undefined ? {} : { status: args.status }),
672
+ ...(args.priority === undefined ? {} : { priority: args.priority }),
673
+ paginationOpts: clampPagination(
674
+ args.paginationOpts,
675
+ config.entries.maxPageSize,
676
+ ),
677
+ viewerActorId: actor.id,
678
+ });
679
+ },
680
+ }),
681
+
682
+ adminGetEntry: queryGeneric({
683
+ args: { entryId: v.string() },
684
+ returns: v.union(adminEntryValidator, v.null()),
685
+ handler: async (ctx, args) => {
686
+ const actor = await requireAdminActor(ctx);
687
+ return await ctx.runQuery(component.admin.getEntry, {
688
+ entryId: args.entryId,
689
+ viewerActorId: actor.id,
690
+ });
691
+ },
692
+ }),
693
+
694
+ adminSearchEntries: queryGeneric({
695
+ args: {
696
+ paginationOpts: paginationOptsValidator,
697
+ searchQuery: v.string(),
698
+ kinds: v.optional(v.array(entryKindValidator)),
699
+ status: v.optional(entryStatusValidator),
700
+ priority: v.optional(entryPriorityValidator),
701
+ },
702
+ returns: paginationResultValidator(adminEntryValidator),
703
+ handler: async (ctx, args) => {
704
+ const actor = await requireAdminActor(ctx);
705
+ return await ctx.runQuery(component.admin.searchEntries, {
706
+ searchQuery: args.searchQuery,
707
+ ...(args.kinds === undefined ? {} : { kinds: args.kinds }),
708
+ ...(args.status === undefined ? {} : { status: args.status }),
709
+ ...(args.priority === undefined ? {} : { priority: args.priority }),
710
+ paginationOpts: clampPagination(
711
+ args.paginationOpts,
712
+ config.entries.maxPageSize,
713
+ ),
714
+ viewerActorId: actor.id,
715
+ });
716
+ },
717
+ }),
718
+
719
+ setEntryPriority: mutationGeneric({
720
+ args: {
721
+ entryId: v.string(),
722
+ priority: v.union(entryPriorityValidator, v.null()),
723
+ },
724
+ returns: nullReturns,
725
+ handler: async (ctx, args) => {
726
+ const actor = await requireAdminActor(ctx);
727
+ const limited = await applyAdminEditLimit(
728
+ asRateLimitContext(ctx),
729
+ actor,
730
+ );
731
+ if (limited !== undefined) return limited;
732
+ return await ctx.runMutation(component.entries.setPriority, {
733
+ actor,
734
+ entryId: args.entryId,
735
+ priority: args.priority,
736
+ });
737
+ },
738
+ }),
739
+
607
740
  setEntryUpvote: mutationGeneric({
608
741
  args: { entryId: v.string(), desiredState: v.boolean() },
609
742
  returns: entryUpvoteReturns,
@@ -739,6 +872,190 @@ function buildFeedbackApi<
739
872
  });
740
873
  },
741
874
  }),
875
+
876
+ listRoadmap: queryGeneric({
877
+ args: {
878
+ paginationOpts: paginationOptsValidator,
879
+ status: v.optional(roadmapStatusValidator),
880
+ },
881
+ returns: paginationResultValidator(roadmapItemValidator),
882
+ handler: async (ctx, args) => {
883
+ return await ctx.runQuery(component.roadmap.list, {
884
+ ...args,
885
+ paginationOpts: clampPagination(
886
+ args.paginationOpts,
887
+ config.entries.maxPageSize,
888
+ ),
889
+ });
890
+ },
891
+ }),
892
+
893
+ searchRoadmap: queryGeneric({
894
+ args: { searchQuery: v.string(), limit: v.optional(v.number()) },
895
+ returns: v.array(roadmapItemValidator),
896
+ handler: async (ctx, args) => {
897
+ return await ctx.runQuery(component.roadmap.search, {
898
+ searchQuery: args.searchQuery,
899
+ limit: clampPositive(args.limit, 10, 50),
900
+ });
901
+ },
902
+ }),
903
+
904
+ createRoadmap: mutationGeneric({
905
+ args: {
906
+ title: v.string(),
907
+ description: v.optional(v.string()),
908
+ status: roadmapStatusValidator,
909
+ },
910
+ returns: idReturns,
911
+ handler: async (ctx, args) => {
912
+ const actor = await requireAdminActor(ctx);
913
+ const limited = await applyAdminEditLimit(
914
+ asRateLimitContext(ctx),
915
+ actor,
916
+ );
917
+ if (limited !== undefined) return limited;
918
+ return await ctx.runMutation(component.roadmap.create, {
919
+ actor,
920
+ ...args,
921
+ });
922
+ },
923
+ }),
924
+
925
+ createRoadmapForEntry: mutationGeneric({
926
+ args: {
927
+ title: v.string(),
928
+ description: v.optional(v.string()),
929
+ status: roadmapStatusValidator,
930
+ entryId: v.string(),
931
+ },
932
+ returns: idReturns,
933
+ handler: async (ctx, args) => {
934
+ const actor = await requireAdminActor(ctx);
935
+ const limited = await applyAdminEditLimit(
936
+ asRateLimitContext(ctx),
937
+ actor,
938
+ );
939
+ if (limited !== undefined) return limited;
940
+ return await ctx.runMutation(component.roadmap.createForEntry, {
941
+ actor,
942
+ ...args,
943
+ });
944
+ },
945
+ }),
946
+
947
+ updateRoadmap: mutationGeneric({
948
+ args: {
949
+ roadmapId: v.string(),
950
+ title: v.string(),
951
+ description: v.optional(v.string()),
952
+ },
953
+ returns: nullReturns,
954
+ handler: async (ctx, args) => {
955
+ const actor = await requireAdminActor(ctx);
956
+ const limited = await applyAdminEditLimit(
957
+ asRateLimitContext(ctx),
958
+ actor,
959
+ );
960
+ if (limited !== undefined) return limited;
961
+ return await ctx.runMutation(component.roadmap.update, {
962
+ actor,
963
+ ...args,
964
+ });
965
+ },
966
+ }),
967
+
968
+ deleteRoadmap: mutationGeneric({
969
+ args: { roadmapId: v.string() },
970
+ returns: nullReturns,
971
+ handler: async (ctx, args) => {
972
+ const actor = await requireAdminActor(ctx);
973
+ const limited = await applyAdminEditLimit(
974
+ asRateLimitContext(ctx),
975
+ actor,
976
+ );
977
+ if (limited !== undefined) return limited;
978
+ return await ctx.runMutation(component.roadmap.remove, {
979
+ actor,
980
+ ...args,
981
+ });
982
+ },
983
+ }),
984
+
985
+ moveRoadmapItem: mutationGeneric({
986
+ args: {
987
+ roadmapId: v.string(),
988
+ status: roadmapStatusValidator,
989
+ previousItemId: v.optional(v.string()),
990
+ nextItemId: v.optional(v.string()),
991
+ },
992
+ returns: numberReturns,
993
+ handler: async (ctx, args) => {
994
+ const actor = await requireAdminActor(ctx);
995
+ const limited = await applyAdminEditLimit(
996
+ asRateLimitContext(ctx),
997
+ actor,
998
+ );
999
+ if (limited !== undefined) return limited;
1000
+ return await ctx.runMutation(component.roadmap.move, {
1001
+ actor,
1002
+ ...args,
1003
+ });
1004
+ },
1005
+ }),
1006
+
1007
+ attachFeedbackToRoadmap: mutationGeneric({
1008
+ args: { roadmapId: v.string(), entryId: v.string() },
1009
+ returns: nullReturns,
1010
+ handler: async (ctx, args) => {
1011
+ const actor = await requireAdminActor(ctx);
1012
+ const limited = await applyAdminEditLimit(
1013
+ asRateLimitContext(ctx),
1014
+ actor,
1015
+ );
1016
+ if (limited !== undefined) return limited;
1017
+ return await ctx.runMutation(component.roadmap.attachFeedback, {
1018
+ actor,
1019
+ ...args,
1020
+ });
1021
+ },
1022
+ }),
1023
+
1024
+ detachFeedbackFromRoadmap: mutationGeneric({
1025
+ args: { entryId: v.string() },
1026
+ returns: nullReturns,
1027
+ handler: async (ctx, args) => {
1028
+ const actor = await requireAdminActor(ctx);
1029
+ const limited = await applyAdminEditLimit(
1030
+ asRateLimitContext(ctx),
1031
+ actor,
1032
+ );
1033
+ if (limited !== undefined) return limited;
1034
+ return await ctx.runMutation(component.roadmap.detachFeedback, {
1035
+ actor,
1036
+ ...args,
1037
+ });
1038
+ },
1039
+ }),
1040
+
1041
+ listRoadmapFeedback: queryGeneric({
1042
+ args: {
1043
+ paginationOpts: paginationOptsValidator,
1044
+ roadmapId: v.string(),
1045
+ },
1046
+ returns: paginationResultValidator(publicEntryValidator),
1047
+ handler: async (ctx, args) => {
1048
+ const actor = await options.actor(ctx);
1049
+ return await ctx.runQuery(component.roadmap.listFeedback, {
1050
+ ...args,
1051
+ paginationOpts: clampPagination(
1052
+ args.paginationOpts,
1053
+ config.entries.maxPageSize,
1054
+ ),
1055
+ ...(actor === null ? {} : { viewerActorId: actor.id }),
1056
+ });
1057
+ },
1058
+ }),
742
1059
  } satisfies Record<keyof FeedbackPublicApi, unknown>;
743
1060
  }
744
1061
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  This directory is the isolated Convex component backend.
4
4
 
5
- - `schema.ts`: three tables and query-driven indexes.
5
+ - `schema.ts`: four tables and query-driven indexes.
6
6
  - `entries.ts`: entry CRUD, full-text search, duplicate suggestions, entry voting, indexed pagination.
7
7
  - `comments.ts`: direct-child pagination, recursive write validation, soft deletion, comment likes.
8
8
  - `migrations.ts`: bounded, idempotent legacy-entry backfills.
@@ -8,12 +8,14 @@
8
8
  * @module
9
9
  */
10
10
 
11
+ import type * as admin from "../admin.js";
11
12
  import type * as comments from "../comments.js";
12
13
  import type * as crons from "../crons.js";
13
14
  import type * as entries from "../entries.js";
14
15
  import type * as helpers from "../helpers.js";
15
16
  import type * as migrations from "../migrations.js";
16
17
  import type * as model from "../model.js";
18
+ import type * as roadmap from "../roadmap.js";
17
19
  import type * as types from "../types.js";
18
20
 
19
21
  import type {
@@ -24,12 +26,14 @@ import type {
24
26
  import { anyApi, componentsGeneric } from "convex/server";
25
27
 
26
28
  const fullApi: ApiFromModules<{
29
+ admin: typeof admin;
27
30
  comments: typeof comments;
28
31
  crons: typeof crons;
29
32
  entries: typeof entries;
30
33
  helpers: typeof helpers;
31
34
  migrations: typeof migrations;
32
35
  model: typeof model;
36
+ roadmap: typeof roadmap;
33
37
  types: typeof types;
34
38
  }> = anyApi as any;
35
39