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.
- package/README.md +87 -27
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +80 -1
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/config.d.ts +1 -1
- package/dist/client/index.d.ts +9 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +261 -8
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +8 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +295 -5
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/admin.d.ts +130 -0
- package/dist/component/admin.d.ts.map +1 -0
- package/dist/component/admin.js +125 -0
- package/dist/component/admin.js.map +1 -0
- package/dist/component/comments.d.ts +7 -5
- package/dist/component/comments.d.ts.map +1 -1
- package/dist/component/comments.js +4 -3
- package/dist/component/comments.js.map +1 -1
- package/dist/component/crons.d.ts +3 -0
- package/dist/component/crons.d.ts.map +1 -0
- package/dist/component/crons.js +8 -0
- package/dist/component/crons.js.map +1 -0
- package/dist/component/entries.d.ts +29 -16
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +136 -12
- package/dist/component/entries.js.map +1 -1
- package/dist/component/helpers.d.ts +3 -1
- package/dist/component/helpers.d.ts.map +1 -1
- package/dist/component/helpers.js +28 -0
- package/dist/component/helpers.js.map +1 -1
- package/dist/component/migrations.d.ts +11 -0
- package/dist/component/migrations.d.ts.map +1 -0
- package/dist/component/migrations.js +35 -0
- package/dist/component/migrations.js.map +1 -0
- package/dist/component/model.d.ts +144 -24
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/model.js +34 -1
- package/dist/component/model.js.map +1 -1
- package/dist/component/roadmap.d.ts +166 -0
- package/dist/component/roadmap.d.ts.map +1 -0
- package/dist/component/roadmap.js +534 -0
- package/dist/component/roadmap.js.map +1 -0
- package/dist/component/schema.d.ts +67 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +57 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/component/tags.d.ts +58 -0
- package/dist/component/tags.d.ts.map +1 -0
- package/dist/component/tags.js +216 -0
- package/dist/component/tags.js.map +1 -0
- package/dist/react/index.d.ts +173 -12
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +78 -1
- package/dist/react/index.js.map +1 -1
- package/dist/test.d.ts +67 -5
- package/dist/test.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/client/README.md +1 -1
- package/src/client/api.ts +183 -0
- package/src/client/config.ts +1 -1
- package/src/client/index.ts +340 -13
- package/src/component/README.md +3 -1
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +373 -9
- package/src/component/admin.ts +180 -0
- package/src/component/comments.ts +5 -2
- package/src/component/crons.ts +16 -0
- package/src/component/entries.ts +175 -11
- package/src/component/helpers.ts +44 -1
- package/src/component/migrations.ts +44 -0
- package/src/component/model.ts +79 -4
- package/src/component/roadmap.ts +627 -0
- package/src/component/schema.ts +61 -1
- package/src/react/README.md +2 -2
- package/src/react/index.ts +152 -4
package/src/component/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This directory is the isolated Convex component backend.
|
|
4
4
|
|
|
5
|
-
- `schema.ts`:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.");
|