convex-feedback 0.1.0 → 0.1.2
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 +351 -92
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +267 -68
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/config.d.ts +168 -29
- package/dist/client/config.d.ts.map +1 -1
- package/dist/client/config.js.map +1 -1
- package/dist/client/index.d.ts +167 -172
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +75 -14
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +2 -2
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/comments.d.ts +12 -12
- package/dist/component/comments.d.ts.map +1 -1
- package/dist/component/comments.js +43 -60
- package/dist/component/comments.js.map +1 -1
- package/dist/component/entries.d.ts +30 -30
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +216 -97
- package/dist/component/entries.js.map +1 -1
- package/dist/component/model.d.ts +151 -0
- package/dist/component/model.d.ts.map +1 -1
- package/dist/react/index.d.ts +178 -56
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +70 -4
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +326 -36
- package/src/client/config.ts +191 -29
- package/src/client/index.ts +437 -16
- package/src/component/_generated/component.ts +2 -2
- package/src/component/comments.ts +43 -61
- package/src/component/entries.ts +300 -123
- package/src/component/model.ts +158 -0
- package/src/react/index.ts +217 -9
package/src/component/entries.ts
CHANGED
|
@@ -2,9 +2,12 @@ import { paginator } from "convex-helpers/server/pagination";
|
|
|
2
2
|
import {
|
|
3
3
|
paginationOptsValidator,
|
|
4
4
|
paginationResultValidator,
|
|
5
|
+
type PaginationResult,
|
|
5
6
|
} from "convex/server";
|
|
6
7
|
import { ConvexError, v } from "convex/values";
|
|
7
8
|
|
|
9
|
+
import { mergedStream, stream } from "convex-helpers/server/stream";
|
|
10
|
+
import type { Doc } from "./_generated/dataModel.js";
|
|
8
11
|
import { mutation, query } from "./_generated/server.js";
|
|
9
12
|
import {
|
|
10
13
|
assertActorId,
|
|
@@ -19,13 +22,44 @@ import {
|
|
|
19
22
|
entryStatusValidator,
|
|
20
23
|
publicEntryValidator,
|
|
21
24
|
similarEntriesValidator,
|
|
25
|
+
type EntryKind,
|
|
22
26
|
} from "./model.js";
|
|
23
27
|
import schema from "./schema.js";
|
|
24
28
|
|
|
29
|
+
const allEntryKinds: readonly EntryKind[] = [
|
|
30
|
+
"feedback",
|
|
31
|
+
"feature_request",
|
|
32
|
+
"bug_report",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function normalizeKindFilter(
|
|
36
|
+
kinds: readonly EntryKind[] | undefined,
|
|
37
|
+
): EntryKind[] | undefined {
|
|
38
|
+
if (kinds === undefined) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const uniqueKinds = [...new Set(kinds)];
|
|
43
|
+
|
|
44
|
+
if (uniqueKinds.length === 0) {
|
|
45
|
+
throw new ConvexError(
|
|
46
|
+
"`kinds` must contain at least one entry kind when provided.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// All known kinds is equivalent to no kind filter and lets us use the
|
|
51
|
+
// simpler global indexes.
|
|
52
|
+
if (uniqueKinds.length === allEntryKinds.length) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return uniqueKinds;
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
export const list = query({
|
|
26
60
|
args: {
|
|
27
61
|
paginationOpts: paginationOptsValidator,
|
|
28
|
-
|
|
62
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
29
63
|
status: v.optional(entryStatusValidator),
|
|
30
64
|
sort: entrySortValidator,
|
|
31
65
|
viewerActorId: v.optional(v.string()),
|
|
@@ -33,59 +67,123 @@ export const list = query({
|
|
|
33
67
|
returns: paginationResultValidator(publicEntryValidator),
|
|
34
68
|
handler: async (ctx, args) => {
|
|
35
69
|
const db = paginator(ctx.db, schema);
|
|
36
|
-
const
|
|
70
|
+
const kinds = normalizeKindFilter(args.kinds);
|
|
71
|
+
const { status } = args;
|
|
37
72
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
let result: PaginationResult<Doc<"entries">>;
|
|
74
|
+
|
|
75
|
+
if (kinds === undefined) {
|
|
76
|
+
result =
|
|
77
|
+
args.sort === "top"
|
|
78
|
+
? status === undefined
|
|
79
|
+
? await db
|
|
80
|
+
.query("entries")
|
|
81
|
+
.withIndex("by_upvotes")
|
|
82
|
+
.order("desc")
|
|
83
|
+
.paginate(args.paginationOpts)
|
|
84
|
+
: await db
|
|
85
|
+
.query("entries")
|
|
86
|
+
.withIndex("by_status_upvotes", (q) => q.eq("status", status))
|
|
87
|
+
.order("desc")
|
|
88
|
+
.paginate(args.paginationOpts)
|
|
89
|
+
: status === undefined
|
|
90
|
+
? await db
|
|
91
|
+
.query("entries")
|
|
92
|
+
.order("desc")
|
|
93
|
+
.paginate(args.paginationOpts)
|
|
94
|
+
: await db
|
|
95
|
+
.query("entries")
|
|
96
|
+
.withIndex("by_status", (q) => q.eq("status", status))
|
|
97
|
+
.order("desc")
|
|
98
|
+
.paginate(args.paginationOpts);
|
|
99
|
+
} else if (kinds.length === 1) {
|
|
100
|
+
const kind = kinds[0];
|
|
101
|
+
|
|
102
|
+
if (kind === undefined) {
|
|
103
|
+
throw new ConvexError("Invalid kind filter.");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
result =
|
|
107
|
+
args.sort === "top"
|
|
108
|
+
? status === undefined
|
|
49
109
|
? await db
|
|
50
110
|
.query("entries")
|
|
51
111
|
.withIndex("by_kind_upvotes", (q) => q.eq("kind", kind))
|
|
52
112
|
.order("desc")
|
|
53
113
|
.paginate(args.paginationOpts)
|
|
54
|
-
:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.withIndex("by_upvotes")
|
|
63
|
-
.order("desc")
|
|
64
|
-
.paginate(args.paginationOpts)
|
|
65
|
-
: kind !== undefined && status !== undefined
|
|
66
|
-
? await db
|
|
67
|
-
.query("entries")
|
|
68
|
-
.withIndex("by_kind_status", (q) =>
|
|
69
|
-
q.eq("kind", kind).eq("status", status),
|
|
70
|
-
)
|
|
71
|
-
.order("desc")
|
|
72
|
-
.paginate(args.paginationOpts)
|
|
73
|
-
: kind !== undefined
|
|
114
|
+
: await db
|
|
115
|
+
.query("entries")
|
|
116
|
+
.withIndex("by_kind_status_upvotes", (q) =>
|
|
117
|
+
q.eq("kind", kind).eq("status", status),
|
|
118
|
+
)
|
|
119
|
+
.order("desc")
|
|
120
|
+
.paginate(args.paginationOpts)
|
|
121
|
+
: status === undefined
|
|
74
122
|
? await db
|
|
75
123
|
.query("entries")
|
|
76
124
|
.withIndex("by_kind", (q) => q.eq("kind", kind))
|
|
77
125
|
.order("desc")
|
|
78
126
|
.paginate(args.paginationOpts)
|
|
79
|
-
:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
: await db
|
|
128
|
+
.query("entries")
|
|
129
|
+
.withIndex("by_kind_status", (q) =>
|
|
130
|
+
q.eq("kind", kind).eq("status", status),
|
|
131
|
+
)
|
|
132
|
+
.order("desc")
|
|
133
|
+
.paginate(args.paginationOpts);
|
|
134
|
+
} else if (args.sort === "top") {
|
|
135
|
+
if (status === undefined) {
|
|
136
|
+
const streams = kinds.map((kind) =>
|
|
137
|
+
stream(ctx.db, schema)
|
|
138
|
+
.query("entries")
|
|
139
|
+
.withIndex("by_kind_upvotes", (q) => q.eq("kind", kind))
|
|
140
|
+
.order("desc"),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
result = await mergedStream(streams, [
|
|
144
|
+
"upvoteCount",
|
|
145
|
+
"_creationTime",
|
|
146
|
+
]).paginate(args.paginationOpts);
|
|
147
|
+
} else {
|
|
148
|
+
const streams = kinds.map((kind) =>
|
|
149
|
+
stream(ctx.db, schema)
|
|
150
|
+
.query("entries")
|
|
151
|
+
.withIndex("by_kind_status_upvotes", (q) =>
|
|
152
|
+
q.eq("kind", kind).eq("status", status),
|
|
153
|
+
)
|
|
154
|
+
.order("desc"),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
result = await mergedStream(streams, [
|
|
158
|
+
"upvoteCount",
|
|
159
|
+
"_creationTime",
|
|
160
|
+
]).paginate(args.paginationOpts);
|
|
161
|
+
}
|
|
162
|
+
} else if (status === undefined) {
|
|
163
|
+
const streams = kinds.map((kind) =>
|
|
164
|
+
stream(ctx.db, schema)
|
|
165
|
+
.query("entries")
|
|
166
|
+
.withIndex("by_kind", (q) => q.eq("kind", kind))
|
|
167
|
+
.order("desc"),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
result = await mergedStream(streams, ["_creationTime"]).paginate(
|
|
171
|
+
args.paginationOpts,
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
const streams = kinds.map((kind) =>
|
|
175
|
+
stream(ctx.db, schema)
|
|
176
|
+
.query("entries")
|
|
177
|
+
.withIndex("by_kind_status", (q) =>
|
|
178
|
+
q.eq("kind", kind).eq("status", status),
|
|
179
|
+
)
|
|
180
|
+
.order("desc"),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
result = await mergedStream(streams, ["_creationTime"]).paginate(
|
|
184
|
+
args.paginationOpts,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
89
187
|
|
|
90
188
|
return {
|
|
91
189
|
...result,
|
|
@@ -100,14 +198,12 @@ export const list = query({
|
|
|
100
198
|
|
|
101
199
|
export const get = query({
|
|
102
200
|
args: {
|
|
103
|
-
entryId: v.
|
|
201
|
+
entryId: v.id("entries"),
|
|
104
202
|
viewerActorId: v.optional(v.string()),
|
|
105
203
|
},
|
|
106
204
|
returns: v.union(publicEntryValidator, v.null()),
|
|
107
205
|
handler: async (ctx, args) => {
|
|
108
|
-
const
|
|
109
|
-
if (entryId === null) return null;
|
|
110
|
-
const entry = await ctx.db.get("entries", entryId);
|
|
206
|
+
const entry = await ctx.db.get("entries", args.entryId);
|
|
111
207
|
return entry === null
|
|
112
208
|
? null
|
|
113
209
|
: serializeEntry(ctx, entry, args.viewerActorId);
|
|
@@ -117,7 +213,7 @@ export const get = query({
|
|
|
117
213
|
export const search = query({
|
|
118
214
|
args: {
|
|
119
215
|
searchQuery: v.string(),
|
|
120
|
-
|
|
216
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
121
217
|
status: v.optional(entryStatusValidator),
|
|
122
218
|
limit: v.number(),
|
|
123
219
|
viewerActorId: v.optional(v.string()),
|
|
@@ -125,42 +221,88 @@ export const search = query({
|
|
|
125
221
|
returns: v.array(publicEntryValidator),
|
|
126
222
|
handler: async (ctx, args) => {
|
|
127
223
|
const searchQuery = args.searchQuery.trim();
|
|
128
|
-
if (searchQuery.length === 0 || args.limit <= 0) return [];
|
|
129
|
-
const { kind, status } = args;
|
|
130
224
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
225
|
+
if (searchQuery.length === 0 || args.limit <= 0) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const kinds = normalizeKindFilter(args.kinds);
|
|
230
|
+
const { status } = args;
|
|
231
|
+
|
|
232
|
+
let entries: Doc<"entries">[];
|
|
233
|
+
|
|
234
|
+
if (kinds === undefined) {
|
|
235
|
+
entries =
|
|
236
|
+
status === undefined
|
|
237
|
+
? await ctx.db
|
|
238
|
+
.query("entries")
|
|
239
|
+
.withSearchIndex("search", (q) =>
|
|
240
|
+
q.search("searchText", searchQuery),
|
|
241
|
+
)
|
|
242
|
+
.take(args.limit)
|
|
243
|
+
: await ctx.db
|
|
244
|
+
.query("entries")
|
|
245
|
+
.withSearchIndex("search", (q) =>
|
|
246
|
+
q.search("searchText", searchQuery).eq("status", status),
|
|
247
|
+
)
|
|
248
|
+
.take(args.limit);
|
|
249
|
+
} else if (kinds.length === 1) {
|
|
250
|
+
const kind = kinds[0];
|
|
251
|
+
|
|
252
|
+
if (kind === undefined) {
|
|
253
|
+
throw new ConvexError("Invalid kind filter.");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
entries =
|
|
257
|
+
status === undefined
|
|
143
258
|
? await ctx.db
|
|
144
259
|
.query("entries")
|
|
145
260
|
.withSearchIndex("search", (q) =>
|
|
146
261
|
q.search("searchText", searchQuery).eq("kind", kind),
|
|
147
262
|
)
|
|
148
263
|
.take(args.limit)
|
|
149
|
-
:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
264
|
+
: await ctx.db
|
|
265
|
+
.query("entries")
|
|
266
|
+
.withSearchIndex("search", (q) =>
|
|
267
|
+
q
|
|
268
|
+
.search("searchText", searchQuery)
|
|
269
|
+
.eq("kind", kind)
|
|
270
|
+
.eq("status", status),
|
|
271
|
+
)
|
|
272
|
+
.take(args.limit);
|
|
273
|
+
} else {
|
|
274
|
+
const firstKind = kinds[0];
|
|
275
|
+
const secondKind = kinds[1];
|
|
276
|
+
|
|
277
|
+
if (firstKind === undefined || secondKind === undefined) {
|
|
278
|
+
throw new ConvexError("Invalid kind filter.");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const searchResults =
|
|
282
|
+
status === undefined
|
|
283
|
+
? ctx.db
|
|
284
|
+
.query("entries")
|
|
285
|
+
.withSearchIndex("search", (q) =>
|
|
286
|
+
q.search("searchText", searchQuery),
|
|
287
|
+
)
|
|
288
|
+
: ctx.db
|
|
289
|
+
.query("entries")
|
|
290
|
+
.withSearchIndex("search", (q) =>
|
|
291
|
+
q.search("searchText", searchQuery).eq("status", status),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
entries = await searchResults
|
|
295
|
+
// eslint-disable-next-line @convex-dev/no-filter-in-query
|
|
296
|
+
.filter((q) =>
|
|
297
|
+
q.or(
|
|
298
|
+
q.eq(q.field("kind"), firstKind),
|
|
299
|
+
q.eq(q.field("kind"), secondKind),
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
.take(args.limit);
|
|
303
|
+
}
|
|
162
304
|
|
|
163
|
-
return Promise.all(
|
|
305
|
+
return await Promise.all(
|
|
164
306
|
entries.map((entry) => serializeEntry(ctx, entry, args.viewerActorId)),
|
|
165
307
|
);
|
|
166
308
|
},
|
|
@@ -176,16 +318,27 @@ export const similar = query({
|
|
|
176
318
|
},
|
|
177
319
|
returns: similarEntriesValidator,
|
|
178
320
|
handler: async (ctx, args) => {
|
|
179
|
-
if (args.limit <= 0)
|
|
321
|
+
if (args.limit <= 0) {
|
|
322
|
+
return {
|
|
323
|
+
exact: [],
|
|
324
|
+
similar: [],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
180
327
|
|
|
181
328
|
const title = args.title.trim();
|
|
182
329
|
const body = args.body.trim();
|
|
183
|
-
const
|
|
330
|
+
const kind = args.kind;
|
|
331
|
+
|
|
184
332
|
if (title.length === 0 && body.length === 0) {
|
|
185
|
-
return {
|
|
333
|
+
return {
|
|
334
|
+
exact: [],
|
|
335
|
+
similar: [],
|
|
336
|
+
};
|
|
186
337
|
}
|
|
187
338
|
|
|
188
339
|
const normalizedTitle = normalizeTitle(title);
|
|
340
|
+
|
|
341
|
+
// Exact normalized-title matches always consume the available limit first.
|
|
189
342
|
const exactDocs =
|
|
190
343
|
title.length === 0
|
|
191
344
|
? []
|
|
@@ -203,38 +356,57 @@ export const similar = query({
|
|
|
203
356
|
)
|
|
204
357
|
.take(args.limit);
|
|
205
358
|
|
|
206
|
-
const
|
|
359
|
+
const exact = await Promise.all(
|
|
360
|
+
exactDocs.map((entry) => serializeEntry(ctx, entry, args.viewerActorId)),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const remainingLimit = args.limit - exactDocs.length;
|
|
364
|
+
|
|
365
|
+
if (remainingLimit <= 0) {
|
|
366
|
+
return {
|
|
367
|
+
exact,
|
|
368
|
+
similar: [],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
207
372
|
const searchText = `${title}\n${body}`.trim();
|
|
208
373
|
|
|
374
|
+
if (searchText.length === 0) {
|
|
375
|
+
return {
|
|
376
|
+
exact,
|
|
377
|
+
similar: [],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const exactIds = new Set(exactDocs.map((entry) => entry._id));
|
|
382
|
+
|
|
383
|
+
// We fetch enough candidates to account for exact matches also appearing
|
|
384
|
+
// in the full-text search results. Those duplicates are removed below.
|
|
385
|
+
const candidateLimit = remainingLimit + exactIds.size;
|
|
386
|
+
|
|
209
387
|
const similarDocs =
|
|
210
|
-
|
|
211
|
-
?
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
.take(args.limit + exactIds.size);
|
|
388
|
+
kind === undefined
|
|
389
|
+
? await ctx.db
|
|
390
|
+
.query("entries")
|
|
391
|
+
.withSearchIndex("search", (q) =>
|
|
392
|
+
q.search("searchText", searchText),
|
|
393
|
+
)
|
|
394
|
+
.take(candidateLimit)
|
|
395
|
+
: await ctx.db
|
|
396
|
+
.query("entries")
|
|
397
|
+
.withSearchIndex("search", (q) =>
|
|
398
|
+
q.search("searchText", searchText).eq("kind", kind),
|
|
399
|
+
)
|
|
400
|
+
.take(candidateLimit);
|
|
225
401
|
|
|
226
|
-
const
|
|
402
|
+
const similarDocsWithoutExactMatches = similarDocs
|
|
227
403
|
.filter((entry) => !exactIds.has(entry._id))
|
|
228
|
-
.slice(0,
|
|
404
|
+
.slice(0, remainingLimit);
|
|
229
405
|
|
|
230
406
|
return {
|
|
231
|
-
exact
|
|
232
|
-
exactDocs.map((entry) =>
|
|
233
|
-
serializeEntry(ctx, entry, args.viewerActorId),
|
|
234
|
-
),
|
|
235
|
-
),
|
|
407
|
+
exact,
|
|
236
408
|
similar: await Promise.all(
|
|
237
|
-
|
|
409
|
+
similarDocsWithoutExactMatches.map((entry) =>
|
|
238
410
|
serializeEntry(ctx, entry, args.viewerActorId),
|
|
239
411
|
),
|
|
240
412
|
),
|
|
@@ -253,7 +425,7 @@ export const create = mutation({
|
|
|
253
425
|
maxTitleLength: v.number(),
|
|
254
426
|
maxBodyLength: v.number(),
|
|
255
427
|
},
|
|
256
|
-
returns: v.
|
|
428
|
+
returns: v.id("entries"),
|
|
257
429
|
handler: async (ctx, args) => {
|
|
258
430
|
assertActorId(args.actorId);
|
|
259
431
|
if (!args.enabledKinds.includes(args.kind)) {
|
|
@@ -267,7 +439,7 @@ export const create = mutation({
|
|
|
267
439
|
);
|
|
268
440
|
const body = normalizeRequiredText(args.body, "Body", args.maxBodyLength);
|
|
269
441
|
|
|
270
|
-
|
|
442
|
+
const entry = await ctx.db.insert("entries", {
|
|
271
443
|
actorId: args.actorId,
|
|
272
444
|
kind: args.kind,
|
|
273
445
|
status: args.defaultStatus,
|
|
@@ -275,16 +447,23 @@ export const create = mutation({
|
|
|
275
447
|
body,
|
|
276
448
|
normalizedTitle: normalizeTitle(title),
|
|
277
449
|
searchText: `${title}\n${body}`,
|
|
278
|
-
upvoteCount:
|
|
450
|
+
upvoteCount: 1,
|
|
279
451
|
commentCount: 0,
|
|
280
452
|
});
|
|
453
|
+
|
|
454
|
+
await ctx.db.insert("reactions", {
|
|
455
|
+
actorId: args.actorId,
|
|
456
|
+
entryId: entry,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return entry;
|
|
281
460
|
},
|
|
282
461
|
});
|
|
283
462
|
|
|
284
463
|
export const update = mutation({
|
|
285
464
|
args: {
|
|
286
465
|
actor: actorValidator,
|
|
287
|
-
entryId: v.
|
|
466
|
+
entryId: v.id("entries"),
|
|
288
467
|
title: v.string(),
|
|
289
468
|
body: v.string(),
|
|
290
469
|
editableByAuthor: v.boolean(),
|
|
@@ -294,9 +473,7 @@ export const update = mutation({
|
|
|
294
473
|
returns: v.null(),
|
|
295
474
|
handler: async (ctx, args) => {
|
|
296
475
|
assertActorId(args.actor.id);
|
|
297
|
-
const
|
|
298
|
-
if (entryId === null) throw new ConvexError("Entry not found.");
|
|
299
|
-
const entry = await ctx.db.get("entries", entryId);
|
|
476
|
+
const entry = await ctx.db.get("entries", args.entryId);
|
|
300
477
|
if (entry === null) throw new ConvexError("Entry not found.");
|
|
301
478
|
|
|
302
479
|
const canEdit =
|
|
@@ -311,7 +488,7 @@ export const update = mutation({
|
|
|
311
488
|
);
|
|
312
489
|
const body = normalizeRequiredText(args.body, "Body", args.maxBodyLength);
|
|
313
490
|
|
|
314
|
-
await ctx.db.patch("entries", entryId, {
|
|
491
|
+
await ctx.db.patch("entries", args.entryId, {
|
|
315
492
|
title,
|
|
316
493
|
body,
|
|
317
494
|
normalizedTitle: normalizeTitle(title),
|
|
@@ -325,7 +502,7 @@ export const update = mutation({
|
|
|
325
502
|
export const setStatus = mutation({
|
|
326
503
|
args: {
|
|
327
504
|
actor: actorValidator,
|
|
328
|
-
entryId: v.
|
|
505
|
+
entryId: v.id("entries"),
|
|
329
506
|
status: entryStatusValidator,
|
|
330
507
|
},
|
|
331
508
|
returns: v.null(),
|
|
@@ -333,11 +510,10 @@ export const setStatus = mutation({
|
|
|
333
510
|
if (!args.actor.isModerator) {
|
|
334
511
|
throw new ConvexError("Moderator access is required to change status.");
|
|
335
512
|
}
|
|
336
|
-
|
|
337
|
-
if (entryId === null || (await ctx.db.get("entries", entryId)) === null) {
|
|
513
|
+
if ((await ctx.db.get("entries", args.entryId)) === null) {
|
|
338
514
|
throw new ConvexError("Entry not found.");
|
|
339
515
|
}
|
|
340
|
-
await ctx.db.patch("entries", entryId, {
|
|
516
|
+
await ctx.db.patch("entries", args.entryId, {
|
|
341
517
|
status: args.status,
|
|
342
518
|
updatedAt: Date.now(),
|
|
343
519
|
});
|
|
@@ -348,35 +524,36 @@ export const setStatus = mutation({
|
|
|
348
524
|
export const setUpvote = mutation({
|
|
349
525
|
args: {
|
|
350
526
|
actorId: v.string(),
|
|
351
|
-
entryId: v.
|
|
527
|
+
entryId: v.id("entries"),
|
|
352
528
|
desiredState: v.boolean(),
|
|
353
529
|
},
|
|
354
530
|
returns: v.object({ active: v.boolean(), upvoteCount: v.number() }),
|
|
355
531
|
handler: async (ctx, args) => {
|
|
356
532
|
assertActorId(args.actorId);
|
|
357
|
-
const
|
|
358
|
-
if (entryId === null) throw new ConvexError("Entry not found.");
|
|
359
|
-
const entry = await ctx.db.get("entries", entryId);
|
|
533
|
+
const entry = await ctx.db.get("entries", args.entryId);
|
|
360
534
|
if (entry === null) throw new ConvexError("Entry not found.");
|
|
361
535
|
|
|
362
536
|
const existing = await ctx.db
|
|
363
537
|
.query("reactions")
|
|
364
538
|
.withIndex("by_entry_actor", (q) =>
|
|
365
|
-
q.eq("entryId", entryId).eq("actorId", args.actorId),
|
|
539
|
+
q.eq("entryId", args.entryId).eq("actorId", args.actorId),
|
|
366
540
|
)
|
|
367
541
|
.unique();
|
|
368
542
|
|
|
369
543
|
if (args.desiredState && existing === null) {
|
|
370
|
-
await ctx.db.insert("reactions", {
|
|
544
|
+
await ctx.db.insert("reactions", {
|
|
545
|
+
actorId: args.actorId,
|
|
546
|
+
entryId: args.entryId,
|
|
547
|
+
});
|
|
371
548
|
const upvoteCount = entry.upvoteCount + 1;
|
|
372
|
-
await ctx.db.patch("entries", entryId, { upvoteCount });
|
|
549
|
+
await ctx.db.patch("entries", args.entryId, { upvoteCount });
|
|
373
550
|
return { active: true, upvoteCount };
|
|
374
551
|
}
|
|
375
552
|
|
|
376
553
|
if (!args.desiredState && existing !== null) {
|
|
377
554
|
await ctx.db.delete("reactions", existing._id);
|
|
378
555
|
const upvoteCount = Math.max(0, entry.upvoteCount - 1);
|
|
379
|
-
await ctx.db.patch("entries", entryId, { upvoteCount });
|
|
556
|
+
await ctx.db.patch("entries", args.entryId, { upvoteCount });
|
|
380
557
|
return { active: false, upvoteCount };
|
|
381
558
|
}
|
|
382
559
|
|