convex-feedback 0.2.0-beta.8 → 0.2.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 (81) hide show
  1. package/README.md +398 -7
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/client/api.d.ts +49 -9
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/config.d.ts +1 -3
  6. package/dist/client/config.d.ts.map +1 -1
  7. package/dist/client/config.js.map +1 -1
  8. package/dist/client/index.d.ts +707 -15
  9. package/dist/client/index.d.ts.map +1 -1
  10. package/dist/client/index.js +243 -12
  11. package/dist/client/index.js.map +1 -1
  12. package/dist/component/_generated/api.d.ts +2 -0
  13. package/dist/component/_generated/api.d.ts.map +1 -1
  14. package/dist/component/_generated/api.js.map +1 -1
  15. package/dist/component/_generated/component.d.ts +222 -4
  16. package/dist/component/_generated/component.d.ts.map +1 -1
  17. package/dist/component/admin.d.ts +5 -90
  18. package/dist/component/admin.d.ts.map +1 -1
  19. package/dist/component/admin.js +59 -29
  20. package/dist/component/admin.js.map +1 -1
  21. package/dist/component/comments.d.ts +107 -15
  22. package/dist/component/comments.d.ts.map +1 -1
  23. package/dist/component/comments.js +342 -28
  24. package/dist/component/comments.js.map +1 -1
  25. package/dist/component/crons.d.ts.map +1 -1
  26. package/dist/component/crons.js +4 -0
  27. package/dist/component/crons.js.map +1 -1
  28. package/dist/component/entries.d.ts +119 -91
  29. package/dist/component/entries.d.ts.map +1 -1
  30. package/dist/component/entries.js +348 -29
  31. package/dist/component/entries.js.map +1 -1
  32. package/dist/component/helpers.d.ts +32 -1
  33. package/dist/component/helpers.d.ts.map +1 -1
  34. package/dist/component/helpers.js +121 -4
  35. package/dist/component/helpers.js.map +1 -1
  36. package/dist/component/migrations.d.ts +24 -0
  37. package/dist/component/migrations.d.ts.map +1 -1
  38. package/dist/component/migrations.js +318 -0
  39. package/dist/component/migrations.js.map +1 -1
  40. package/dist/component/model.d.ts +455 -120
  41. package/dist/component/model.d.ts.map +1 -1
  42. package/dist/component/model.js +70 -2
  43. package/dist/component/model.js.map +1 -1
  44. package/dist/component/reactions.d.ts +37 -0
  45. package/dist/component/reactions.d.ts.map +1 -0
  46. package/dist/component/reactions.js +52 -0
  47. package/dist/component/reactions.js.map +1 -0
  48. package/dist/component/roadmap.d.ts +10 -46
  49. package/dist/component/roadmap.d.ts.map +1 -1
  50. package/dist/component/roadmap.js +239 -21
  51. package/dist/component/roadmap.js.map +1 -1
  52. package/dist/component/schema.d.ts +33 -5
  53. package/dist/component/schema.d.ts.map +1 -1
  54. package/dist/component/schema.js +40 -4
  55. package/dist/component/schema.js.map +1 -1
  56. package/dist/react/index.d.ts +26 -238
  57. package/dist/react/index.d.ts.map +1 -1
  58. package/dist/react/index.js +65 -2
  59. package/dist/react/index.js.map +1 -1
  60. package/dist/test.d.ts +23 -5
  61. package/dist/test.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. package/src/client/README.md +6 -1
  64. package/src/client/api.ts +89 -7
  65. package/src/client/config.ts +1 -3
  66. package/src/client/index.ts +1237 -34
  67. package/src/component/README.md +32 -4
  68. package/src/component/_generated/api.ts +2 -0
  69. package/src/component/_generated/component.ts +290 -8
  70. package/src/component/admin.ts +88 -30
  71. package/src/component/comments.ts +405 -26
  72. package/src/component/crons.ts +10 -0
  73. package/src/component/entries.ts +388 -27
  74. package/src/component/helpers.ts +163 -4
  75. package/src/component/migrations.ts +415 -0
  76. package/src/component/model.ts +381 -117
  77. package/src/component/reactions.ts +60 -0
  78. package/src/component/roadmap.ts +357 -35
  79. package/src/component/schema.ts +40 -4
  80. package/src/react/README.md +5 -0
  81. package/src/react/index.ts +90 -3
@@ -2,10 +2,12 @@
2
2
 
3
3
  This directory is the isolated Convex component backend.
4
4
 
5
- - `schema.ts`: four tables and query-driven indexes.
6
- - `entries.ts`: entry CRUD, full-text search, duplicate suggestions, entry voting, indexed pagination.
7
- - `comments.ts`: direct-child pagination, recursive write validation, soft deletion, comment likes.
8
- - `migrations.ts`: bounded, idempotent legacy-entry backfills.
5
+ - `schema.ts`: durable feedback domain tables plus internal roadmap-rebalance bookkeeping, all with query-driven indexes.
6
+ - `entries.ts`: entry CRUD, full-text search, duplicate suggestions, entry voting, indexed pagination, actor activity pagination, and bounded permanent-deletion cleanup.
7
+ - `comments.ts`: direct-child pagination, recursive write validation, bounded subtree deletion, comment likes, and actor activity pagination.
8
+ - `reactions.ts`: actor activity pagination with live entry/comment target resolution.
9
+ - `migrations.ts`: bounded, idempotent status backfills and legacy comment
10
+ cleanup.
9
11
  - `crons.ts`: immediate upgrade migration and temporary 30-day self-healing schedule.
10
12
  - `model.ts`: reusable validators and public model types.
11
13
  - `helpers.ts`: normalization, validation, and serializers.
@@ -13,3 +15,29 @@ This directory is the isolated Convex component backend.
13
15
  - `_generated/`: generated component API/server/data-model types. Regenerate after backend changes.
14
16
 
15
17
  Important: component public functions become internal references when installed by a host app. The host must wrap them before exposing them to clients. Component IDs become strings at that boundary.
18
+
19
+ Lifecycle callbacks belong to the host wrapper, not this component. The wrapper
20
+ may transform or reject creation input before calling a component mutation and
21
+ may run after callbacks with the mutation's returned context. Keep authentication,
22
+ host business rules, and notification providers outside the component; component
23
+ validation remains authoritative.
24
+
25
+ Actor activity component queries accept a trusted `actorId` and use the
26
+ `by_actor` indexes. `entries.listByActor` can additionally receive
27
+ `includeAdminContext: true` for server-side consumers that need retained
28
+ metadata, priority, or roadmap context. The host `listUser*` wrappers resolve
29
+ the actor from the host callback and expose only their public activity shapes.
30
+
31
+ Entry deletion is an asynchronous permanent cleanup. The entry is marked as
32
+ deleting and hidden immediately, its roadmap link is detached, and scheduled
33
+ batches remove entry reactions, comment reactions, and all comments before the
34
+ entry document is hard-deleted. Comment deletion also marks its subtree and
35
+ removes descendants and reactions in bounded scheduled batches. Activity reads
36
+ omit pending and completed deletions.
37
+
38
+ During the upgrade window, `comments.deletedAt` remains an optional internal
39
+ schema field only so older installations can deploy safely. The legacy comment
40
+ migration permanently removes tombstones, their descendants, and orphaned
41
+ reactions in bounded batches. Its progress markers and `deletedAt` field are
42
+ internal only. After supported installations have migrated, a future release
43
+ can remove those compatibility fields from the schema.
@@ -15,6 +15,7 @@ import type * as entries from "../entries.js";
15
15
  import type * as helpers from "../helpers.js";
16
16
  import type * as migrations from "../migrations.js";
17
17
  import type * as model from "../model.js";
18
+ import type * as reactions from "../reactions.js";
18
19
  import type * as roadmap from "../roadmap.js";
19
20
  import type * as types from "../types.js";
20
21
 
@@ -33,6 +34,7 @@ const fullApi: ApiFromModules<{
33
34
  helpers: typeof helpers;
34
35
  migrations: typeof migrations;
35
36
  model: typeof model;
37
+ reactions: typeof reactions;
36
38
  roadmap: typeof roadmap;
37
39
  types: typeof types;
38
40
  }> = anyApi as any;
@@ -87,6 +87,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
87
87
  | "in_progress"
88
88
  | "completed"
89
89
  | "closed";
90
+ statusFilter?: "open" | "closed";
90
91
  viewerActorId: string;
91
92
  },
92
93
  {
@@ -155,6 +156,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
155
156
  | "in_progress"
156
157
  | "completed"
157
158
  | "closed";
159
+ statusFilter?: "open" | "closed";
158
160
  viewerActorId: string;
159
161
  },
160
162
  {
@@ -210,11 +212,37 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
210
212
  actorId: string;
211
213
  body: string;
212
214
  entryId: string;
215
+ includeCallbackContext?: boolean;
213
216
  maxCommentLength: number;
214
217
  maxDepth: number;
215
218
  parentCommentId?: string;
216
219
  },
217
- string,
220
+ | { id: string }
221
+ | {
222
+ comment: {
223
+ actorId: string;
224
+ body: string;
225
+ depth: number;
226
+ entryId: string;
227
+ id: string;
228
+ parentCommentId?: string;
229
+ };
230
+ entry: {
231
+ actorId: string;
232
+ id: string;
233
+ kind: "feedback" | "feature_request" | "bug_report";
234
+ status:
235
+ | "open"
236
+ | "under_review"
237
+ | "planned"
238
+ | "in_progress"
239
+ | "completed"
240
+ | "closed";
241
+ title: string;
242
+ };
243
+ id: string;
244
+ parentComment?: { actorId: string; id: string };
245
+ },
218
246
  Name
219
247
  >;
220
248
  list: FunctionReference<
@@ -239,9 +267,8 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
239
267
  isDone: boolean;
240
268
  page: Array<{
241
269
  actorId: string;
242
- body: string | null;
270
+ body: string;
243
271
  creationTime: number;
244
- deletedAt?: number;
245
272
  depth: number;
246
273
  entryId: string;
247
274
  id: string;
@@ -256,6 +283,41 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
256
283
  },
257
284
  Name
258
285
  >;
286
+ listByActor: FunctionReference<
287
+ "query",
288
+ "internal",
289
+ {
290
+ actorId: string;
291
+ paginationOpts: {
292
+ cursor: string | null;
293
+ endCursor?: string | null;
294
+ id?: number;
295
+ maximumBytesRead?: number;
296
+ maximumRowsRead?: number;
297
+ numItems: number;
298
+ };
299
+ },
300
+ {
301
+ continueCursor: string;
302
+ isDone: boolean;
303
+ page: Array<{
304
+ actorId: string;
305
+ body: string;
306
+ creationTime: number;
307
+ depth: number;
308
+ entryId: string;
309
+ entryTitle: string;
310
+ id: string;
311
+ likeCount: number;
312
+ parentCommentId?: string;
313
+ replyCount: number;
314
+ updatedAt?: number;
315
+ }>;
316
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
317
+ splitCursor?: string | null;
318
+ },
319
+ Name
320
+ >;
259
321
  remove: FunctionReference<
260
322
  "mutation",
261
323
  "internal",
@@ -270,8 +332,34 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
270
332
  setLike: FunctionReference<
271
333
  "mutation",
272
334
  "internal",
273
- { actorId: string; commentId: string; desiredState: boolean },
274
- { active: boolean; likeCount: number },
335
+ {
336
+ actorId: string;
337
+ commentId: string;
338
+ desiredState: boolean;
339
+ includeCallbackContext?: boolean;
340
+ },
341
+ | { active: boolean; likeCount: number }
342
+ | {
343
+ active: boolean;
344
+ changed: false;
345
+ count: number;
346
+ previousCount: number;
347
+ transition: null;
348
+ }
349
+ | {
350
+ active: boolean;
351
+ changed: true;
352
+ comment: {
353
+ actorId: string;
354
+ body: string;
355
+ entryId: string;
356
+ id: string;
357
+ parentCommentId?: string;
358
+ };
359
+ count: number;
360
+ previousCount: number;
361
+ transition: "added" | "removed";
362
+ },
275
363
  Name
276
364
  >;
277
365
  update: FunctionReference<
@@ -303,6 +391,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
303
391
  | "completed"
304
392
  | "closed";
305
393
  enabledKinds: Array<"feedback" | "feature_request" | "bug_report">;
394
+ includeCallbackContext?: boolean;
306
395
  kind: "feedback" | "feature_request" | "bug_report";
307
396
  maxBodyLength: number;
308
397
  maxTitleLength: number;
@@ -312,7 +401,30 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
312
401
  };
313
402
  title: string;
314
403
  },
315
- string,
404
+ | { id: string }
405
+ | {
406
+ entry: {
407
+ actorId: string;
408
+ body: string;
409
+ commentCount: number;
410
+ id: string;
411
+ kind: "feedback" | "feature_request" | "bug_report";
412
+ metadata?: {
413
+ additional?: Record<string, string | number | boolean>;
414
+ standard?: Record<string, string | number | boolean>;
415
+ };
416
+ status:
417
+ | "open"
418
+ | "under_review"
419
+ | "planned"
420
+ | "in_progress"
421
+ | "completed"
422
+ | "closed";
423
+ title: string;
424
+ upvoteCount: number;
425
+ };
426
+ id: string;
427
+ },
316
428
  Name
317
429
  >;
318
430
  get: FunctionReference<
@@ -401,6 +513,73 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
401
513
  },
402
514
  Name
403
515
  >;
516
+ listByActor: FunctionReference<
517
+ "query",
518
+ "internal",
519
+ {
520
+ actorId: string;
521
+ includeAdminContext?: boolean;
522
+ paginationOpts: {
523
+ cursor: string | null;
524
+ endCursor?: string | null;
525
+ id?: number;
526
+ maximumBytesRead?: number;
527
+ maximumRowsRead?: number;
528
+ numItems: number;
529
+ };
530
+ },
531
+ {
532
+ continueCursor: string;
533
+ isDone: boolean;
534
+ page: Array<{
535
+ actorId: string;
536
+ body: string;
537
+ commentCount: number;
538
+ creationTime: number;
539
+ id: string;
540
+ kind: "feedback" | "feature_request" | "bug_report";
541
+ metadata?: {
542
+ additional?: Record<string, string | number | boolean>;
543
+ standard?: Record<string, string | number | boolean>;
544
+ };
545
+ priority?: "low" | "medium" | "high";
546
+ roadmap?: {
547
+ createdAt: number;
548
+ creationTime: number;
549
+ description?: string;
550
+ feedbackCount: number;
551
+ id: string;
552
+ position: number;
553
+ status: "planned" | "in_progress" | "shipped";
554
+ title: string;
555
+ updatedAt: number;
556
+ };
557
+ status:
558
+ | "open"
559
+ | "under_review"
560
+ | "planned"
561
+ | "in_progress"
562
+ | "completed"
563
+ | "closed";
564
+ title: string;
565
+ updatedAt?: number;
566
+ upvoteCount: number;
567
+ }>;
568
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
569
+ splitCursor?: string | null;
570
+ },
571
+ Name
572
+ >;
573
+ remove: FunctionReference<
574
+ "mutation",
575
+ "internal",
576
+ {
577
+ actor: { id: string; isAdmin?: boolean; isModerator?: boolean };
578
+ entryId: string;
579
+ },
580
+ null,
581
+ Name
582
+ >;
404
583
  search: FunctionReference<
405
584
  "query",
406
585
  "internal",
@@ -475,8 +654,40 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
475
654
  setUpvote: FunctionReference<
476
655
  "mutation",
477
656
  "internal",
478
- { actorId: string; desiredState: boolean; entryId: string },
479
- { active: boolean; upvoteCount: number },
657
+ {
658
+ actorId: string;
659
+ desiredState: boolean;
660
+ entryId: string;
661
+ includeCallbackContext?: boolean;
662
+ },
663
+ | { active: boolean; upvoteCount: number }
664
+ | {
665
+ active: boolean;
666
+ changed: false;
667
+ count: number;
668
+ previousCount: number;
669
+ transition: null;
670
+ }
671
+ | {
672
+ active: boolean;
673
+ changed: true;
674
+ count: number;
675
+ entry: {
676
+ actorId: string;
677
+ id: string;
678
+ kind: "feedback" | "feature_request" | "bug_report";
679
+ status:
680
+ | "open"
681
+ | "under_review"
682
+ | "planned"
683
+ | "in_progress"
684
+ | "completed"
685
+ | "closed";
686
+ title: string;
687
+ };
688
+ previousCount: number;
689
+ transition: "added" | "removed";
690
+ },
480
691
  Name
481
692
  >;
482
693
  similar: FunctionReference<
@@ -558,6 +769,60 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
558
769
  Name
559
770
  >;
560
771
  };
772
+ reactions: {
773
+ listByActor: FunctionReference<
774
+ "query",
775
+ "internal",
776
+ {
777
+ actorId: string;
778
+ paginationOpts: {
779
+ cursor: string | null;
780
+ endCursor?: string | null;
781
+ id?: number;
782
+ maximumBytesRead?: number;
783
+ maximumRowsRead?: number;
784
+ numItems: number;
785
+ };
786
+ },
787
+ {
788
+ continueCursor: string;
789
+ isDone: boolean;
790
+ page: Array<
791
+ | {
792
+ creationTime: number;
793
+ entry: {
794
+ id: string;
795
+ kind: "feedback" | "feature_request" | "bug_report";
796
+ status:
797
+ | "open"
798
+ | "under_review"
799
+ | "planned"
800
+ | "in_progress"
801
+ | "completed"
802
+ | "closed";
803
+ title: string;
804
+ };
805
+ id: string;
806
+ type: "entry_upvote";
807
+ }
808
+ | {
809
+ comment: {
810
+ body: string;
811
+ entryId: string;
812
+ entryTitle: string;
813
+ id: string;
814
+ };
815
+ creationTime: number;
816
+ id: string;
817
+ type: "comment_like";
818
+ }
819
+ >;
820
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
821
+ splitCursor?: string | null;
822
+ },
823
+ Name
824
+ >;
825
+ };
561
826
  roadmap: {
562
827
  attachFeedback: FunctionReference<
563
828
  "mutation",
@@ -605,6 +870,23 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
605
870
  null,
606
871
  Name
607
872
  >;
873
+ get: FunctionReference<
874
+ "query",
875
+ "internal",
876
+ { roadmapId: string },
877
+ {
878
+ createdAt: number;
879
+ creationTime: number;
880
+ description?: string;
881
+ feedbackCount: number;
882
+ id: string;
883
+ position: number;
884
+ status: "planned" | "in_progress" | "shipped";
885
+ title: string;
886
+ updatedAt: number;
887
+ } | null,
888
+ Name
889
+ >;
608
890
  list: FunctionReference<
609
891
  "query",
610
892
  "internal",
@@ -14,10 +14,12 @@ import {
14
14
  adminEntryValidator,
15
15
  entryKindValidator,
16
16
  entryPriorityValidator,
17
+ entryStatusFilterValidator,
17
18
  entryStatusValidator,
18
19
  type EntryKind,
19
20
  type EntryPriority,
20
21
  type EntryStatus,
22
+ type EntryStatusFilter,
21
23
  } from "./model.js";
22
24
  import schema from "./schema.js";
23
25
 
@@ -87,11 +89,14 @@ function matchesFilters(
87
89
  entry: Doc<"entries">,
88
90
  kinds: EntryKind[] | undefined,
89
91
  status: EntryStatus | undefined,
92
+ statusFilter: EntryStatusFilter | undefined,
90
93
  priority: EntryPriority | undefined,
91
94
  ): boolean {
92
95
  return (
96
+ entry.deletingAt === undefined &&
93
97
  (kinds === undefined || kinds.includes(entry.kind)) &&
94
98
  (status === undefined || entry.status === status) &&
99
+ (statusFilter === undefined || entry.statusFilter === statusFilter) &&
95
100
  (priority === undefined || entry.priority === priority)
96
101
  );
97
102
  }
@@ -110,7 +115,7 @@ export const getEntry = query({
110
115
  returns: v.union(adminEntryValidator, v.null()),
111
116
  handler: async (ctx, args) => {
112
117
  const entry = await ctx.db.get("entries", args.entryId);
113
- return entry === null
118
+ return entry === null || entry.deletingAt !== undefined
114
119
  ? null
115
120
  : await serializeAdminEntry(ctx, entry, args.viewerActorId);
116
121
  },
@@ -120,6 +125,7 @@ export const listEntries = query({
120
125
  args: {
121
126
  kinds: v.optional(v.array(entryKindValidator)),
122
127
  status: v.optional(entryStatusValidator),
128
+ statusFilter: v.optional(entryStatusFilterValidator),
123
129
  priority: v.optional(entryPriorityValidator),
124
130
  paginationOpts: paginationOptsValidator,
125
131
  viewerActorId: v.string(),
@@ -131,27 +137,50 @@ export const listEntries = query({
131
137
  if (kinds?.length === 0) {
132
138
  throw new ConvexError("`kinds` must contain at least one kind.");
133
139
  }
140
+ if (args.status !== undefined && args.statusFilter !== undefined) {
141
+ throw new ConvexError(
142
+ "`status` and `statusFilter` cannot be used together.",
143
+ );
144
+ }
134
145
 
135
146
  const entriesStream =
136
- args.priority !== undefined
137
- ? stream(ctx.db, schema)
138
- .query("entries")
139
- .withIndex("by_priority", (q) => q.eq("priority", args.priority))
140
- : args.status !== undefined
147
+ args.statusFilter !== undefined
148
+ ? kinds?.length === 1
149
+ ? stream(ctx.db, schema)
150
+ .query("entries")
151
+ .withIndex("by_kind_status_filter", (q) =>
152
+ q.eq("kind", kinds[0]!).eq("statusFilter", args.statusFilter),
153
+ )
154
+ : stream(ctx.db, schema)
155
+ .query("entries")
156
+ .withIndex("by_status_filter", (q) =>
157
+ q.eq("statusFilter", args.statusFilter),
158
+ )
159
+ : args.priority !== undefined
141
160
  ? stream(ctx.db, schema)
142
161
  .query("entries")
143
- .withIndex("by_status", (q) => q.eq("status", args.status!))
144
- : kinds?.length === 1
162
+ .withIndex("by_priority", (q) => q.eq("priority", args.priority))
163
+ : args.status !== undefined
145
164
  ? stream(ctx.db, schema)
146
165
  .query("entries")
147
- .withIndex("by_kind", (q) => q.eq("kind", kinds[0]!))
148
- : stream(ctx.db, schema).query("entries");
166
+ .withIndex("by_status", (q) => q.eq("status", args.status!))
167
+ : kinds?.length === 1
168
+ ? stream(ctx.db, schema)
169
+ .query("entries")
170
+ .withIndex("by_kind", (q) => q.eq("kind", kinds[0]!))
171
+ : stream(ctx.db, schema).query("entries");
149
172
 
150
173
  const result = await entriesStream
151
174
  .order("desc")
152
175
  .filterWith((entry) =>
153
176
  Promise.resolve(
154
- matchesFilters(entry, kinds, args.status, args.priority),
177
+ matchesFilters(
178
+ entry,
179
+ kinds,
180
+ args.status,
181
+ args.statusFilter,
182
+ args.priority,
183
+ ),
155
184
  ),
156
185
  )
157
186
  .paginate(args.paginationOpts);
@@ -172,12 +201,18 @@ export const searchEntries = query({
172
201
  searchQuery: v.string(),
173
202
  kinds: v.optional(v.array(entryKindValidator)),
174
203
  status: v.optional(entryStatusValidator),
204
+ statusFilter: v.optional(entryStatusFilterValidator),
175
205
  priority: v.optional(entryPriorityValidator),
176
206
  paginationOpts: paginationOptsValidator,
177
207
  viewerActorId: v.string(),
178
208
  },
179
209
  returns: paginationResultValidator(adminEntryValidator),
180
210
  handler: async (ctx, args) => {
211
+ if (args.status !== undefined && args.statusFilter !== undefined) {
212
+ throw new ConvexError(
213
+ "`status` and `statusFilter` cannot be used together.",
214
+ );
215
+ }
181
216
  const searchQuery = args.searchQuery.trim();
182
217
  if (searchQuery.length === 0) {
183
218
  return { page: [], isDone: true, continueCursor: "" };
@@ -197,37 +232,60 @@ export const searchEntries = query({
197
232
  const withKind =
198
233
  kinds?.length === 1 ? searched.eq("kind", kinds[0]!) : searched;
199
234
  const withStatus =
200
- args.status === undefined
201
- ? withKind
202
- : withKind.eq("status", args.status);
203
- return args.priority === undefined
204
- ? withStatus
205
- : withStatus.eq("priority", args.priority);
235
+ args.statusFilter === undefined
236
+ ? args.status === undefined
237
+ ? withKind
238
+ : withKind.eq("status", args.status)
239
+ : withKind.eq("statusFilter", args.statusFilter);
240
+ const withPriority =
241
+ args.priority === undefined
242
+ ? withStatus
243
+ : withStatus.eq("priority", args.priority);
244
+ return withPriority.eq("deletingAt", undefined);
206
245
  }),
207
246
  args.paginationOpts,
208
247
  )
209
248
  : await (
210
- args.priority !== undefined
211
- ? stream(ctx.db, schema)
212
- .query("entries")
213
- .withIndex("by_priority", (q) =>
214
- q.eq("priority", args.priority),
215
- )
216
- : args.status !== undefined
249
+ args.statusFilter !== undefined
250
+ ? kinds?.length === 1
251
+ ? stream(ctx.db, schema)
252
+ .query("entries")
253
+ .withIndex("by_kind_status_filter", (q) =>
254
+ q
255
+ .eq("kind", kinds[0]!)
256
+ .eq("statusFilter", args.statusFilter),
257
+ )
258
+ : stream(ctx.db, schema)
259
+ .query("entries")
260
+ .withIndex("by_status_filter", (q) =>
261
+ q.eq("statusFilter", args.statusFilter),
262
+ )
263
+ : args.priority !== undefined
217
264
  ? stream(ctx.db, schema)
218
265
  .query("entries")
219
- .withIndex("by_status", (q) => q.eq("status", args.status!))
220
- : kinds?.length === 1
266
+ .withIndex("by_priority", (q) =>
267
+ q.eq("priority", args.priority),
268
+ )
269
+ : args.status !== undefined
221
270
  ? stream(ctx.db, schema)
222
271
  .query("entries")
223
- .withIndex("by_kind", (q) => q.eq("kind", kinds[0]!))
224
- : stream(ctx.db, schema).query("entries")
272
+ .withIndex("by_status", (q) => q.eq("status", args.status!))
273
+ : kinds?.length === 1
274
+ ? stream(ctx.db, schema)
275
+ .query("entries")
276
+ .withIndex("by_kind", (q) => q.eq("kind", kinds[0]!))
277
+ : stream(ctx.db, schema).query("entries")
225
278
  )
226
279
  .order("desc")
227
280
  .filterWith((entry) =>
228
281
  Promise.resolve(
229
- matchesFilters(entry, kinds, args.status, args.priority) &&
230
- matchesSearch(entry, searchQuery),
282
+ matchesFilters(
283
+ entry,
284
+ kinds,
285
+ args.status,
286
+ args.statusFilter,
287
+ args.priority,
288
+ ) && matchesSearch(entry, searchQuery),
231
289
  ),
232
290
  )
233
291
  .paginate(args.paginationOpts);