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
@@ -2,9 +2,11 @@
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
+ - `migrations.ts`: bounded, idempotent legacy-entry backfills.
9
+ - `crons.ts`: immediate upgrade migration and temporary 30-day self-healing schedule.
8
10
  - `model.ts`: reusable validators and public model types.
9
11
  - `helpers.ts`: normalization, validation, and serializers.
10
12
  - `convex.config.ts`: packaged component definition.
@@ -8,10 +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";
13
+ import type * as crons from "../crons.js";
12
14
  import type * as entries from "../entries.js";
13
15
  import type * as helpers from "../helpers.js";
16
+ import type * as migrations from "../migrations.js";
14
17
  import type * as model from "../model.js";
18
+ import type * as roadmap from "../roadmap.js";
15
19
  import type * as types from "../types.js";
16
20
 
17
21
  import type {
@@ -22,10 +26,14 @@ import type {
22
26
  import { anyApi, componentsGeneric } from "convex/server";
23
27
 
24
28
  const fullApi: ApiFromModules<{
29
+ admin: typeof admin;
25
30
  comments: typeof comments;
31
+ crons: typeof crons;
26
32
  entries: typeof entries;
27
33
  helpers: typeof helpers;
34
+ migrations: typeof migrations;
28
35
  model: typeof model;
36
+ roadmap: typeof roadmap;
29
37
  types: typeof types;
30
38
  }> = anyApi as any;
31
39
 
@@ -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;
@@ -190,6 +362,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
190
362
  | "in_progress"
191
363
  | "completed"
192
364
  | "closed";
365
+ statusFilter?: "open" | "closed";
193
366
  viewerActorId?: string;
194
367
  },
195
368
  {
@@ -237,6 +410,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
237
410
  | "in_progress"
238
411
  | "completed"
239
412
  | "closed";
413
+ statusFilter?: "open" | "closed";
240
414
  viewerActorId?: string;
241
415
  },
242
416
  Array<{
@@ -264,11 +438,22 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
264
438
  }>,
265
439
  Name
266
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
+ >;
267
452
  setStatus: FunctionReference<
268
453
  "mutation",
269
454
  "internal",
270
455
  {
271
- actor: { id: string; isModerator: boolean };
456
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
272
457
  entryId: string;
273
458
  status:
274
459
  | "open"
@@ -352,7 +537,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
352
537
  "mutation",
353
538
  "internal",
354
539
  {
355
- actor: { id: string; isModerator: boolean };
540
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
356
541
  body: string;
357
542
  editableByAuthor: boolean;
358
543
  entryId: string;
@@ -364,4 +549,183 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
364
549
  Name
365
550
  >;
366
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
+ };
367
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,7 +14,9 @@ import {
14
14
  } from "./helpers.js";
15
15
  import {
16
16
  actorValidator,
17
+ actorIsAdmin,
17
18
  commentSortValidator,
19
+ entryStatusFilterForStatus,
18
20
  publicCommentValidator,
19
21
  } from "./model.js";
20
22
  import schema from "./schema.js";
@@ -121,6 +123,7 @@ export const create = mutation({
121
123
 
122
124
  await ctx.db.patch("entries", args.entryId, {
123
125
  commentCount: entry.commentCount + 1,
126
+ statusFilter: entryStatusFilterForStatus(entry.status),
124
127
  });
125
128
 
126
129
  if (args.parentCommentId !== undefined) {
@@ -154,7 +157,7 @@ export const update = mutation({
154
157
  }
155
158
 
156
159
  const canEdit =
157
- args.actor.isModerator ||
160
+ actorIsAdmin(args.actor) ||
158
161
  (args.editableByAuthor && comment.actorId === args.actor.id);
159
162
  if (!canEdit) throw new ConvexError("Not authorized to edit this comment.");
160
163
 
@@ -185,7 +188,7 @@ export const remove = mutation({
185
188
  if (comment.deletedAt !== undefined) return null;
186
189
 
187
190
  const canDelete =
188
- args.actor.isModerator ||
191
+ actorIsAdmin(args.actor) ||
189
192
  (args.deletableByAuthor && comment.actorId === args.actor.id);
190
193
  if (!canDelete) {
191
194
  throw new ConvexError("Not authorized to delete this comment.");