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
@@ -23,6 +23,182 @@ import type { FunctionReference } from "convex/server";
23
23
  */
24
24
  export type ComponentApi<Name extends string | undefined = string | undefined> =
25
25
  {
26
+ admin: {
27
+ getEntry: FunctionReference<
28
+ "query",
29
+ "internal",
30
+ { entryId: string; viewerActorId: string },
31
+ {
32
+ actorId: string;
33
+ body: string;
34
+ commentCount: number;
35
+ creationTime: number;
36
+ id: string;
37
+ kind: "feedback" | "feature_request" | "bug_report";
38
+ metadata?: {
39
+ additional?: Record<string, string | number | boolean>;
40
+ standard?: Record<string, string | number | boolean>;
41
+ };
42
+ priority?: "low" | "medium" | "high";
43
+ roadmap?: {
44
+ createdAt: number;
45
+ creationTime: number;
46
+ description?: string;
47
+ feedbackCount: number;
48
+ id: string;
49
+ position: number;
50
+ status: "planned" | "in_progress" | "shipped";
51
+ title: string;
52
+ updatedAt: number;
53
+ };
54
+ status:
55
+ | "open"
56
+ | "under_review"
57
+ | "planned"
58
+ | "in_progress"
59
+ | "completed"
60
+ | "closed";
61
+ title: string;
62
+ updatedAt?: number;
63
+ upvoteCount: number;
64
+ viewerHasUpvoted: boolean;
65
+ } | null,
66
+ Name
67
+ >;
68
+ listEntries: FunctionReference<
69
+ "query",
70
+ "internal",
71
+ {
72
+ kinds?: Array<"feedback" | "feature_request" | "bug_report">;
73
+ paginationOpts: {
74
+ cursor: string | null;
75
+ endCursor?: string | null;
76
+ id?: number;
77
+ maximumBytesRead?: number;
78
+ maximumRowsRead?: number;
79
+ numItems: number;
80
+ };
81
+ priority?: "low" | "medium" | "high";
82
+ status?:
83
+ | "open"
84
+ | "under_review"
85
+ | "planned"
86
+ | "in_progress"
87
+ | "completed"
88
+ | "closed";
89
+ viewerActorId: string;
90
+ },
91
+ {
92
+ continueCursor: string;
93
+ isDone: boolean;
94
+ page: Array<{
95
+ actorId: string;
96
+ body: string;
97
+ commentCount: number;
98
+ creationTime: number;
99
+ id: string;
100
+ kind: "feedback" | "feature_request" | "bug_report";
101
+ metadata?: {
102
+ additional?: Record<string, string | number | boolean>;
103
+ standard?: Record<string, string | number | boolean>;
104
+ };
105
+ priority?: "low" | "medium" | "high";
106
+ roadmap?: {
107
+ createdAt: number;
108
+ creationTime: number;
109
+ description?: string;
110
+ feedbackCount: number;
111
+ id: string;
112
+ position: number;
113
+ status: "planned" | "in_progress" | "shipped";
114
+ title: string;
115
+ updatedAt: number;
116
+ };
117
+ status:
118
+ | "open"
119
+ | "under_review"
120
+ | "planned"
121
+ | "in_progress"
122
+ | "completed"
123
+ | "closed";
124
+ title: string;
125
+ updatedAt?: number;
126
+ upvoteCount: number;
127
+ viewerHasUpvoted: boolean;
128
+ }>;
129
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
130
+ splitCursor?: string | null;
131
+ },
132
+ Name
133
+ >;
134
+ searchEntries: FunctionReference<
135
+ "query",
136
+ "internal",
137
+ {
138
+ kinds?: Array<"feedback" | "feature_request" | "bug_report">;
139
+ paginationOpts: {
140
+ cursor: string | null;
141
+ endCursor?: string | null;
142
+ id?: number;
143
+ maximumBytesRead?: number;
144
+ maximumRowsRead?: number;
145
+ numItems: number;
146
+ };
147
+ priority?: "low" | "medium" | "high";
148
+ searchQuery: string;
149
+ status?:
150
+ | "open"
151
+ | "under_review"
152
+ | "planned"
153
+ | "in_progress"
154
+ | "completed"
155
+ | "closed";
156
+ viewerActorId: string;
157
+ },
158
+ {
159
+ continueCursor: string;
160
+ isDone: boolean;
161
+ page: Array<{
162
+ actorId: string;
163
+ body: string;
164
+ commentCount: number;
165
+ creationTime: number;
166
+ id: string;
167
+ kind: "feedback" | "feature_request" | "bug_report";
168
+ metadata?: {
169
+ additional?: Record<string, string | number | boolean>;
170
+ standard?: Record<string, string | number | boolean>;
171
+ };
172
+ priority?: "low" | "medium" | "high";
173
+ roadmap?: {
174
+ createdAt: number;
175
+ creationTime: number;
176
+ description?: string;
177
+ feedbackCount: number;
178
+ id: string;
179
+ position: number;
180
+ status: "planned" | "in_progress" | "shipped";
181
+ title: string;
182
+ updatedAt: number;
183
+ };
184
+ status:
185
+ | "open"
186
+ | "under_review"
187
+ | "planned"
188
+ | "in_progress"
189
+ | "completed"
190
+ | "closed";
191
+ title: string;
192
+ updatedAt?: number;
193
+ upvoteCount: number;
194
+ viewerHasUpvoted: boolean;
195
+ }>;
196
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
197
+ splitCursor?: string | null;
198
+ },
199
+ Name
200
+ >;
201
+ };
26
202
  comments: {
27
203
  create: FunctionReference<
28
204
  "mutation",
@@ -81,7 +257,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
81
257
  "mutation",
82
258
  "internal",
83
259
  {
84
- actor: { id: string; isModerator: boolean };
260
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
85
261
  commentId: string;
86
262
  deletableByAuthor: boolean;
87
263
  },
@@ -99,7 +275,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
99
275
  "mutation",
100
276
  "internal",
101
277
  {
102
- actor: { id: string; isModerator: boolean };
278
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
103
279
  body: string;
104
280
  commentId: string;
105
281
  editableByAuthor: boolean;
@@ -139,11 +315,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
139
315
  get: FunctionReference<
140
316
  "query",
141
317
  "internal",
142
- {
143
- entryId: string;
144
- viewerActorId?: string;
145
- viewerIsModerator?: boolean;
146
- },
318
+ { entryId: string; viewerActorId?: string; viewerIsAdmin?: boolean },
147
319
  {
148
320
  actorId: string;
149
321
  body: string;
@@ -266,11 +438,22 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
266
438
  }>,
267
439
  Name
268
440
  >;
441
+ setPriority: FunctionReference<
442
+ "mutation",
443
+ "internal",
444
+ {
445
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
446
+ entryId: string;
447
+ priority: "low" | "medium" | "high" | null;
448
+ },
449
+ null,
450
+ Name
451
+ >;
269
452
  setStatus: FunctionReference<
270
453
  "mutation",
271
454
  "internal",
272
455
  {
273
- actor: { id: string; isModerator: boolean };
456
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
274
457
  entryId: string;
275
458
  status:
276
459
  | "open"
@@ -354,7 +537,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
354
537
  "mutation",
355
538
  "internal",
356
539
  {
357
- actor: { id: string; isModerator: boolean };
540
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
358
541
  body: string;
359
542
  editableByAuthor: boolean;
360
543
  entryId: string;
@@ -366,4 +549,183 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
366
549
  Name
367
550
  >;
368
551
  };
552
+ roadmap: {
553
+ attachFeedback: FunctionReference<
554
+ "mutation",
555
+ "internal",
556
+ {
557
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
558
+ entryId: string;
559
+ roadmapId: string;
560
+ },
561
+ null,
562
+ Name
563
+ >;
564
+ create: FunctionReference<
565
+ "mutation",
566
+ "internal",
567
+ {
568
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
569
+ description?: string;
570
+ status: "planned" | "in_progress" | "shipped";
571
+ title: string;
572
+ },
573
+ string,
574
+ Name
575
+ >;
576
+ createForEntry: FunctionReference<
577
+ "mutation",
578
+ "internal",
579
+ {
580
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
581
+ description?: string;
582
+ entryId: string;
583
+ status: "planned" | "in_progress" | "shipped";
584
+ title: string;
585
+ },
586
+ string,
587
+ Name
588
+ >;
589
+ detachFeedback: FunctionReference<
590
+ "mutation",
591
+ "internal",
592
+ {
593
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
594
+ entryId: string;
595
+ },
596
+ null,
597
+ Name
598
+ >;
599
+ list: FunctionReference<
600
+ "query",
601
+ "internal",
602
+ {
603
+ paginationOpts: {
604
+ cursor: string | null;
605
+ endCursor?: string | null;
606
+ id?: number;
607
+ maximumBytesRead?: number;
608
+ maximumRowsRead?: number;
609
+ numItems: number;
610
+ };
611
+ status?: "planned" | "in_progress" | "shipped";
612
+ },
613
+ {
614
+ continueCursor: string;
615
+ isDone: boolean;
616
+ page: Array<{
617
+ createdAt: number;
618
+ creationTime: number;
619
+ description?: string;
620
+ feedbackCount: number;
621
+ id: string;
622
+ position: number;
623
+ status: "planned" | "in_progress" | "shipped";
624
+ title: string;
625
+ updatedAt: number;
626
+ }>;
627
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
628
+ splitCursor?: string | null;
629
+ },
630
+ Name
631
+ >;
632
+ listFeedback: FunctionReference<
633
+ "query",
634
+ "internal",
635
+ {
636
+ paginationOpts: {
637
+ cursor: string | null;
638
+ endCursor?: string | null;
639
+ id?: number;
640
+ maximumBytesRead?: number;
641
+ maximumRowsRead?: number;
642
+ numItems: number;
643
+ };
644
+ roadmapId: string;
645
+ viewerActorId?: string;
646
+ },
647
+ {
648
+ continueCursor: string;
649
+ isDone: boolean;
650
+ page: Array<{
651
+ actorId: string;
652
+ body: string;
653
+ commentCount: number;
654
+ creationTime: number;
655
+ id: string;
656
+ kind: "feedback" | "feature_request" | "bug_report";
657
+ metadata?: {
658
+ additional?: Record<string, string | number | boolean>;
659
+ standard?: Record<string, string | number | boolean>;
660
+ };
661
+ status:
662
+ | "open"
663
+ | "under_review"
664
+ | "planned"
665
+ | "in_progress"
666
+ | "completed"
667
+ | "closed";
668
+ title: string;
669
+ updatedAt?: number;
670
+ upvoteCount: number;
671
+ viewerHasUpvoted: boolean;
672
+ }>;
673
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
674
+ splitCursor?: string | null;
675
+ },
676
+ Name
677
+ >;
678
+ move: FunctionReference<
679
+ "mutation",
680
+ "internal",
681
+ {
682
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
683
+ nextItemId?: string;
684
+ previousItemId?: string;
685
+ roadmapId: string;
686
+ status: "planned" | "in_progress" | "shipped";
687
+ },
688
+ number,
689
+ Name
690
+ >;
691
+ remove: FunctionReference<
692
+ "mutation",
693
+ "internal",
694
+ {
695
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
696
+ roadmapId: string;
697
+ },
698
+ null,
699
+ Name
700
+ >;
701
+ search: FunctionReference<
702
+ "query",
703
+ "internal",
704
+ { limit: number; searchQuery: string },
705
+ Array<{
706
+ createdAt: number;
707
+ creationTime: number;
708
+ description?: string;
709
+ feedbackCount: number;
710
+ id: string;
711
+ position: number;
712
+ status: "planned" | "in_progress" | "shipped";
713
+ title: string;
714
+ updatedAt: number;
715
+ }>,
716
+ Name
717
+ >;
718
+ update: FunctionReference<
719
+ "mutation",
720
+ "internal",
721
+ {
722
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
723
+ description?: string;
724
+ roadmapId: string;
725
+ title: string;
726
+ },
727
+ null,
728
+ Name
729
+ >;
730
+ };
369
731
  };
@@ -0,0 +1,180 @@
1
+ import {
2
+ paginationOptsValidator,
3
+ paginationResultValidator,
4
+ } from "convex/server";
5
+ import { ConvexError, v } from "convex/values";
6
+ import { stream } from "convex-helpers/server/stream";
7
+
8
+ import type { Doc } from "./_generated/dataModel.js";
9
+ import { query } from "./_generated/server.js";
10
+ import { serializeAdminEntry } from "./helpers.js";
11
+ import {
12
+ adminEntryValidator,
13
+ entryKindValidator,
14
+ entryPriorityValidator,
15
+ entryStatusValidator,
16
+ type EntryKind,
17
+ type EntryPriority,
18
+ type EntryStatus,
19
+ } from "./model.js";
20
+ import schema from "./schema.js";
21
+
22
+ function matchesFilters(
23
+ entry: Doc<"entries">,
24
+ kinds: EntryKind[] | undefined,
25
+ status: EntryStatus | undefined,
26
+ priority: EntryPriority | undefined,
27
+ ): boolean {
28
+ return (
29
+ (kinds === undefined || kinds.includes(entry.kind)) &&
30
+ (status === undefined || entry.status === status) &&
31
+ (priority === undefined || entry.priority === priority)
32
+ );
33
+ }
34
+
35
+ function matchesSearch(entry: Doc<"entries">, searchQuery: string): boolean {
36
+ const haystack = entry.searchText.toLocaleLowerCase("en-US");
37
+ const terms = searchQuery
38
+ .toLocaleLowerCase("en-US")
39
+ .split(/\s+/)
40
+ .filter((term) => term.length > 0);
41
+ return terms.every((term) => haystack.includes(term));
42
+ }
43
+
44
+ export const getEntry = query({
45
+ args: { entryId: v.id("entries"), viewerActorId: v.string() },
46
+ returns: v.union(adminEntryValidator, v.null()),
47
+ handler: async (ctx, args) => {
48
+ const entry = await ctx.db.get("entries", args.entryId);
49
+ return entry === null
50
+ ? null
51
+ : await serializeAdminEntry(ctx, entry, args.viewerActorId);
52
+ },
53
+ });
54
+
55
+ export const listEntries = query({
56
+ args: {
57
+ kinds: v.optional(v.array(entryKindValidator)),
58
+ status: v.optional(entryStatusValidator),
59
+ priority: v.optional(entryPriorityValidator),
60
+ paginationOpts: paginationOptsValidator,
61
+ viewerActorId: v.string(),
62
+ },
63
+ returns: paginationResultValidator(adminEntryValidator),
64
+ handler: async (ctx, args) => {
65
+ const kinds =
66
+ args.kinds === undefined ? undefined : [...new Set(args.kinds)];
67
+ if (kinds?.length === 0) {
68
+ throw new ConvexError("`kinds` must contain at least one kind.");
69
+ }
70
+
71
+ const entriesStream =
72
+ args.priority !== undefined
73
+ ? stream(ctx.db, schema)
74
+ .query("entries")
75
+ .withIndex("by_priority", (q) => q.eq("priority", args.priority))
76
+ : args.status !== undefined
77
+ ? stream(ctx.db, schema)
78
+ .query("entries")
79
+ .withIndex("by_status", (q) => q.eq("status", args.status!))
80
+ : kinds?.length === 1
81
+ ? stream(ctx.db, schema)
82
+ .query("entries")
83
+ .withIndex("by_kind", (q) => q.eq("kind", kinds[0]!))
84
+ : stream(ctx.db, schema).query("entries");
85
+
86
+ const result = await entriesStream
87
+ .order("desc")
88
+ .filterWith((entry) =>
89
+ Promise.resolve(
90
+ matchesFilters(entry, kinds, args.status, args.priority),
91
+ ),
92
+ )
93
+ .paginate(args.paginationOpts);
94
+
95
+ return {
96
+ ...result,
97
+ page: await Promise.all(
98
+ result.page.map((entry) =>
99
+ serializeAdminEntry(ctx, entry, args.viewerActorId),
100
+ ),
101
+ ),
102
+ };
103
+ },
104
+ });
105
+
106
+ export const searchEntries = query({
107
+ args: {
108
+ searchQuery: v.string(),
109
+ kinds: v.optional(v.array(entryKindValidator)),
110
+ status: v.optional(entryStatusValidator),
111
+ priority: v.optional(entryPriorityValidator),
112
+ paginationOpts: paginationOptsValidator,
113
+ viewerActorId: v.string(),
114
+ },
115
+ returns: paginationResultValidator(adminEntryValidator),
116
+ handler: async (ctx, args) => {
117
+ const searchQuery = args.searchQuery.trim();
118
+ if (searchQuery.length === 0) {
119
+ return { page: [], isDone: true, continueCursor: "" };
120
+ }
121
+ const kinds =
122
+ args.kinds === undefined ? undefined : [...new Set(args.kinds)];
123
+ if (kinds?.length === 0) {
124
+ throw new ConvexError("`kinds` must contain at least one kind.");
125
+ }
126
+
127
+ const canUseSearchIndex = kinds === undefined || kinds.length === 1;
128
+
129
+ const result = canUseSearchIndex
130
+ ? await ctx.db
131
+ .query("entries")
132
+ .withSearchIndex("search", (q) => {
133
+ const searched = q.search("searchText", searchQuery);
134
+ const withKind =
135
+ kinds?.length === 1 ? searched.eq("kind", kinds[0]!) : searched;
136
+ const withStatus =
137
+ args.status === undefined
138
+ ? withKind
139
+ : withKind.eq("status", args.status);
140
+ return args.priority === undefined
141
+ ? withStatus
142
+ : withStatus.eq("priority", args.priority);
143
+ })
144
+ .paginate(args.paginationOpts)
145
+ : await (
146
+ args.priority !== undefined
147
+ ? stream(ctx.db, schema)
148
+ .query("entries")
149
+ .withIndex("by_priority", (q) =>
150
+ q.eq("priority", args.priority),
151
+ )
152
+ : args.status !== undefined
153
+ ? stream(ctx.db, schema)
154
+ .query("entries")
155
+ .withIndex("by_status", (q) => q.eq("status", args.status!))
156
+ : kinds?.length === 1
157
+ ? stream(ctx.db, schema)
158
+ .query("entries")
159
+ .withIndex("by_kind", (q) => q.eq("kind", kinds[0]!))
160
+ : stream(ctx.db, schema).query("entries")
161
+ )
162
+ .order("desc")
163
+ .filterWith((entry) =>
164
+ Promise.resolve(
165
+ matchesFilters(entry, kinds, args.status, args.priority) &&
166
+ matchesSearch(entry, searchQuery),
167
+ ),
168
+ )
169
+ .paginate(args.paginationOpts);
170
+
171
+ return {
172
+ ...result,
173
+ page: await Promise.all(
174
+ result.page.map((entry) =>
175
+ serializeAdminEntry(ctx, entry, args.viewerActorId),
176
+ ),
177
+ ),
178
+ };
179
+ },
180
+ });
@@ -14,6 +14,7 @@ import {
14
14
  } from "./helpers.js";
15
15
  import {
16
16
  actorValidator,
17
+ actorIsAdmin,
17
18
  commentSortValidator,
18
19
  entryStatusFilterForStatus,
19
20
  publicCommentValidator,
@@ -156,7 +157,7 @@ export const update = mutation({
156
157
  }
157
158
 
158
159
  const canEdit =
159
- args.actor.isModerator ||
160
+ actorIsAdmin(args.actor) ||
160
161
  (args.editableByAuthor && comment.actorId === args.actor.id);
161
162
  if (!canEdit) throw new ConvexError("Not authorized to edit this comment.");
162
163
 
@@ -187,7 +188,7 @@ export const remove = mutation({
187
188
  if (comment.deletedAt !== undefined) return null;
188
189
 
189
190
  const canDelete =
190
- args.actor.isModerator ||
191
+ actorIsAdmin(args.actor) ||
191
192
  (args.deletableByAuthor && comment.actorId === args.actor.id);
192
193
  if (!canDelete) {
193
194
  throw new ConvexError("Not authorized to delete this comment.");
@@ -18,7 +18,9 @@ import {
18
18
  } from "./helpers.js";
19
19
  import {
20
20
  actorValidator,
21
+ actorIsAdmin,
21
22
  entryKindValidator,
23
+ entryPriorityValidator,
22
24
  entrySortValidator,
23
25
  entryStatusFilterForStatus,
24
26
  entryStatusFilterValidator,
@@ -276,7 +278,7 @@ export const get = query({
276
278
  args: {
277
279
  entryId: v.id("entries"),
278
280
  viewerActorId: v.optional(v.string()),
279
- viewerIsModerator: v.optional(v.boolean()),
281
+ viewerIsAdmin: v.optional(v.boolean()),
280
282
  },
281
283
  returns: v.union(publicEntryValidator, v.null()),
282
284
  handler: async (ctx, args) => {
@@ -287,7 +289,7 @@ export const get = query({
287
289
  ctx,
288
290
  entry,
289
291
  args.viewerActorId,
290
- args.viewerIsModerator === true,
292
+ args.viewerIsAdmin === true,
291
293
  );
292
294
  },
293
295
  });
@@ -620,7 +622,7 @@ export const update = mutation({
620
622
  if (entry === null) throw new ConvexError("Entry not found.");
621
623
 
622
624
  const canEdit =
623
- args.actor.isModerator ||
625
+ actorIsAdmin(args.actor) ||
624
626
  (args.editableByAuthor && entry.actorId === args.actor.id);
625
627
  if (!canEdit) throw new ConvexError("Not authorized to edit this entry.");
626
628
 
@@ -651,8 +653,8 @@ export const setStatus = mutation({
651
653
  },
652
654
  returns: v.null(),
653
655
  handler: async (ctx, args) => {
654
- if (!args.actor.isModerator) {
655
- throw new ConvexError("Moderator access is required to change status.");
656
+ if (!actorIsAdmin(args.actor)) {
657
+ throw new ConvexError("Admin access is required to change status.");
656
658
  }
657
659
  if ((await ctx.db.get("entries", args.entryId)) === null) {
658
660
  throw new ConvexError("Entry not found.");
@@ -666,6 +668,28 @@ export const setStatus = mutation({
666
668
  },
667
669
  });
668
670
 
671
+ export const setPriority = mutation({
672
+ args: {
673
+ actor: actorValidator,
674
+ entryId: v.id("entries"),
675
+ priority: v.union(entryPriorityValidator, v.null()),
676
+ },
677
+ returns: v.null(),
678
+ handler: async (ctx, args) => {
679
+ if (!actorIsAdmin(args.actor)) {
680
+ throw new ConvexError("Admin access is required to change priority.");
681
+ }
682
+ if ((await ctx.db.get("entries", args.entryId)) === null) {
683
+ throw new ConvexError("Entry not found.");
684
+ }
685
+ await ctx.db.patch("entries", args.entryId, {
686
+ priority: args.priority === null ? undefined : args.priority,
687
+ updatedAt: Date.now(),
688
+ });
689
+ return null;
690
+ },
691
+ });
692
+
669
693
  export const setUpvote = mutation({
670
694
  args: {
671
695
  actorId: v.string(),