convex-feedback 0.1.4 → 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 (79) hide show
  1. package/README.md +87 -27
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/client/api.d.ts +80 -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 +261 -8
  9. package/dist/client/index.js.map +1 -1
  10. package/dist/component/_generated/api.d.ts +8 -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 +295 -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 +4 -3
  22. package/dist/component/comments.js.map +1 -1
  23. package/dist/component/crons.d.ts +3 -0
  24. package/dist/component/crons.d.ts.map +1 -0
  25. package/dist/component/crons.js +8 -0
  26. package/dist/component/crons.js.map +1 -0
  27. package/dist/component/entries.d.ts +29 -16
  28. package/dist/component/entries.d.ts.map +1 -1
  29. package/dist/component/entries.js +136 -12
  30. package/dist/component/entries.js.map +1 -1
  31. package/dist/component/helpers.d.ts +3 -1
  32. package/dist/component/helpers.d.ts.map +1 -1
  33. package/dist/component/helpers.js +28 -0
  34. package/dist/component/helpers.js.map +1 -1
  35. package/dist/component/migrations.d.ts +11 -0
  36. package/dist/component/migrations.d.ts.map +1 -0
  37. package/dist/component/migrations.js +35 -0
  38. package/dist/component/migrations.js.map +1 -0
  39. package/dist/component/model.d.ts +144 -24
  40. package/dist/component/model.d.ts.map +1 -1
  41. package/dist/component/model.js +34 -1
  42. package/dist/component/model.js.map +1 -1
  43. package/dist/component/roadmap.d.ts +166 -0
  44. package/dist/component/roadmap.d.ts.map +1 -0
  45. package/dist/component/roadmap.js +534 -0
  46. package/dist/component/roadmap.js.map +1 -0
  47. package/dist/component/schema.d.ts +67 -5
  48. package/dist/component/schema.d.ts.map +1 -1
  49. package/dist/component/schema.js +57 -2
  50. package/dist/component/schema.js.map +1 -1
  51. package/dist/component/tags.d.ts +58 -0
  52. package/dist/component/tags.d.ts.map +1 -0
  53. package/dist/component/tags.js +216 -0
  54. package/dist/component/tags.js.map +1 -0
  55. package/dist/react/index.d.ts +173 -12
  56. package/dist/react/index.d.ts.map +1 -1
  57. package/dist/react/index.js +78 -1
  58. package/dist/react/index.js.map +1 -1
  59. package/dist/test.d.ts +67 -5
  60. package/dist/test.d.ts.map +1 -1
  61. package/package.json +6 -2
  62. package/src/client/README.md +1 -1
  63. package/src/client/api.ts +183 -0
  64. package/src/client/config.ts +1 -1
  65. package/src/client/index.ts +340 -13
  66. package/src/component/README.md +3 -1
  67. package/src/component/_generated/api.ts +8 -0
  68. package/src/component/_generated/component.ts +373 -9
  69. package/src/component/admin.ts +180 -0
  70. package/src/component/comments.ts +5 -2
  71. package/src/component/crons.ts +16 -0
  72. package/src/component/entries.ts +175 -11
  73. package/src/component/helpers.ts +44 -1
  74. package/src/component/migrations.ts +44 -0
  75. package/src/component/model.ts +79 -4
  76. package/src/component/roadmap.ts +627 -0
  77. package/src/component/schema.ts +61 -1
  78. package/src/react/README.md +2 -2
  79. package/src/react/index.ts +152 -4
package/src/client/api.ts CHANGED
@@ -9,9 +9,14 @@ import type {
9
9
  EntryKind,
10
10
  EntrySort,
11
11
  EntryStatus,
12
+ EntryStatusFilter,
12
13
  FeedbackComment,
13
14
  FeedbackEntry,
14
15
  FeedbackMetadata,
16
+ AdminFeedbackEntry,
17
+ EntryPriority,
18
+ RoadmapItem,
19
+ RoadmapStatus,
15
20
  SimilarEntriesResult,
16
21
  } from "../component/model.js";
17
22
 
@@ -40,6 +45,9 @@ export type ListEntriesArgs = {
40
45
  /** Restricts entries to this workflow status. */
41
46
  status?: EntryStatus;
42
47
 
48
+ /** Restricts entries to the indexed open or closed board bucket. */
49
+ statusFilter?: EntryStatusFilter;
50
+
43
51
  /**
44
52
  * Server-side ordering strategy.
45
53
  *
@@ -74,6 +82,9 @@ export type SearchEntriesArgs = {
74
82
  /** Optional workflow-status restriction. */
75
83
  status?: EntryStatus;
76
84
 
85
+ /** Optional indexed open or closed board bucket. */
86
+ statusFilter?: EntryStatusFilter;
87
+
77
88
  /**
78
89
  * Maximum number of results to return.
79
90
  *
@@ -165,6 +176,60 @@ export type SetEntryStatusArgs = {
165
176
  status: EntryStatus;
166
177
  };
167
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
+
168
233
  /**
169
234
  * Arguments for setting the current actor's entry-upvote state.
170
235
  */
@@ -301,6 +366,24 @@ export interface FeedbackPublicApi<
301
366
  Name extends string | undefined = string | undefined,
302
367
  RateLimitResult = never,
303
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
+
304
387
  /** Returns a cursor-paginated entry list. */
305
388
  listEntries: FunctionReference<
306
389
  "query",
@@ -364,6 +447,106 @@ export interface FeedbackPublicApi<
364
447
  Name
365
448
  >;
366
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
+
367
550
  /** Sets entry-upvote state, or returns a configured rate-limit rejection. */
368
551
  setEntryUpvote: FunctionReference<
369
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,14 +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,
31
+ entryStatusFilterValidator,
29
32
  entryStatusValidator,
30
33
  feedbackMetadataValidator,
31
34
  publicCommentValidator,
32
35
  publicEntryValidator,
36
+ roadmapItemValidator,
37
+ roadmapStatusValidator,
33
38
  similarEntriesValidator,
39
+ actorIsAdmin,
34
40
  type FeedbackActor,
35
41
  } from "../component/model.js";
36
42
  import type { FeedbackPublicApi } from "./api.js";
@@ -40,15 +46,20 @@ import {
40
46
  } from "./config.js";
41
47
 
42
48
  export type {
49
+ AdminFeedbackEntry,
43
50
  CommentSort,
44
51
  EntryKind,
52
+ EntryPriority,
45
53
  EntrySort,
46
54
  EntryStatus,
55
+ EntryStatusFilter,
47
56
  FeedbackActor,
48
57
  FeedbackComment,
49
58
  FeedbackEntry,
50
59
  FeedbackMetadata,
51
60
  FeedbackMetadataValue,
61
+ RoadmapItem,
62
+ RoadmapStatus,
52
63
  SimilarEntriesResult,
53
64
  } from "../component/model.js";
54
65
  export type { FeedbackPublicApi } from "./api.js";
@@ -125,10 +136,13 @@ export interface ThrowingFeedbackRateLimitConfig {
125
136
  behavior?: "throw";
126
137
 
127
138
  /**
128
- * Whether all configured limiter groups apply to moderators.
139
+ * Whether all configured limiter groups apply to admins.
129
140
  *
130
141
  * @default false
131
142
  */
143
+ limitAdmins?: boolean;
144
+
145
+ /** @deprecated Use `limitAdmins`. */
132
146
  limitModerators?: boolean;
133
147
 
134
148
  /** Not accepted in throwing mode; select `"return"` to provide a validator. */
@@ -168,10 +182,13 @@ export interface ReturningFeedbackRateLimitConfig<
168
182
  returns: ReturnsValidator;
169
183
 
170
184
  /**
171
- * Whether all configured limiter groups apply to moderators.
185
+ * Whether all configured limiter groups apply to admins.
172
186
  *
173
187
  * @default false
174
188
  */
189
+ limitAdmins?: boolean;
190
+
191
+ /** @deprecated Use `limitAdmins`. */
175
192
  limitModerators?: boolean;
176
193
  }
177
194
 
@@ -260,10 +277,10 @@ export type ThrowingExposeFeedbackOptions = Omit<
260
277
  /** Optional component behavior and throwing rate-limit overrides. */
261
278
  config?: FeedbackConfigOverrides & {
262
279
  /**
263
- * Controls throwing behavior and the moderator exemption.
280
+ * Controls throwing behavior and the admin exemption.
264
281
  *
265
282
  * Omit this object to use `behavior: "throw"` and
266
- * `limitModerators: false`.
283
+ * `limitAdmins: false`.
267
284
  */
268
285
  rateLimiting?: FeedbackRateLimitConfig;
269
286
  };
@@ -314,9 +331,9 @@ function requireActor(actor: FeedbackActor | null): FeedbackActor {
314
331
  return actor;
315
332
  }
316
333
 
317
- function requireModerator(actor: FeedbackActor): void {
318
- if (!actor.isModerator) {
319
- 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.");
320
337
  }
321
338
  }
322
339
 
@@ -347,10 +364,9 @@ async function applyRateLimiter<
347
364
  | ReturningFeedbackRateLimitConfig<FeedbackRateLimitReturnValidator>
348
365
  | undefined,
349
366
  ): Promise<RateLimitResult<ReturnsValidator> | undefined> {
350
- if (
351
- limiter === undefined ||
352
- (actor.isModerator && rateLimitConfig?.limitModerators !== true)
353
- ) {
367
+ const limitAdmins =
368
+ rateLimitConfig?.limitAdmins ?? rateLimitConfig?.limitModerators ?? false;
369
+ if (limiter === undefined || (actorIsAdmin(actor) && !limitAdmins)) {
354
370
  return undefined;
355
371
  }
356
372
 
@@ -421,13 +437,50 @@ function buildFeedbackApi<
421
437
  v.object({ active: v.boolean(), likeCount: v.number() }),
422
438
  rateLimitReturnValidator,
423
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
+ );
424
461
 
425
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
+
426
478
  listEntries: queryGeneric({
427
479
  args: {
428
480
  paginationOpts: paginationOptsValidator,
429
481
  kinds: v.optional(v.array(entryKindValidator)),
430
482
  status: v.optional(entryStatusValidator),
483
+ statusFilter: v.optional(entryStatusFilterValidator),
431
484
  sort: v.optional(entrySortValidator),
432
485
  },
433
486
  returns: paginationResultValidator(publicEntryValidator),
@@ -441,6 +494,9 @@ function buildFeedbackApi<
441
494
  ),
442
495
  ...(args.kinds === undefined ? {} : { kinds: args.kinds }),
443
496
  ...(args.status === undefined ? {} : { status: args.status }),
497
+ ...(args.statusFilter === undefined
498
+ ? {}
499
+ : { statusFilter: args.statusFilter }),
444
500
  sort: args.sort ?? config.entries.defaultSort,
445
501
  ...actorIdFields(actor),
446
502
  });
@@ -455,7 +511,9 @@ function buildFeedbackApi<
455
511
  return await ctx.runQuery(component.entries.get, {
456
512
  entryId: args.entryId,
457
513
  ...actorIdFields(actor),
458
- ...(actor?.isModerator === true ? { viewerIsModerator: true } : {}),
514
+ ...(actor !== null && actorIsAdmin(actor)
515
+ ? { viewerIsAdmin: true }
516
+ : {}),
459
517
  });
460
518
  },
461
519
  }),
@@ -465,6 +523,7 @@ function buildFeedbackApi<
465
523
  searchQuery: v.string(),
466
524
  kinds: v.optional(v.array(entryKindValidator)),
467
525
  status: v.optional(entryStatusValidator),
526
+ statusFilter: v.optional(entryStatusFilterValidator),
468
527
  limit: v.optional(v.number()),
469
528
  },
470
529
  returns: v.array(publicEntryValidator),
@@ -475,6 +534,9 @@ function buildFeedbackApi<
475
534
  searchQuery: args.searchQuery,
476
535
  ...(args.kinds === undefined ? {} : { kinds: args.kinds }),
477
536
  ...(args.status === undefined ? {} : { status: args.status }),
537
+ ...(args.statusFilter === undefined
538
+ ? {}
539
+ : { statusFilter: args.statusFilter }),
478
540
  limit: clampPositive(
479
541
  args.limit,
480
542
  config.search.defaultLimit,
@@ -578,7 +640,7 @@ function buildFeedbackApi<
578
640
  returns: nullReturns,
579
641
  handler: async (ctx, args) => {
580
642
  const actor = requireActor(await options.actor(ctx));
581
- requireModerator(actor);
643
+ requireAdmin(actor);
582
644
  const limited = await applyRateLimiter(
583
645
  asRateLimitContext(ctx),
584
646
  actor,
@@ -594,6 +656,87 @@ function buildFeedbackApi<
594
656
  },
595
657
  }),
596
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
+
597
740
  setEntryUpvote: mutationGeneric({
598
741
  args: { entryId: v.string(), desiredState: v.boolean() },
599
742
  returns: entryUpvoteReturns,
@@ -729,6 +872,190 @@ function buildFeedbackApi<
729
872
  });
730
873
  },
731
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
+ }),
732
1059
  } satisfies Record<keyof FeedbackPublicApi, unknown>;
733
1060
  }
734
1061