convex-feedback 0.1.0 → 0.1.1
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 +281 -92
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +262 -67
- 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 +26 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -4
- 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 +5 -5
- package/dist/component/entries.d.ts +22 -22
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +198 -72
- 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 +170 -54
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +67 -4
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +315 -30
- package/src/client/config.ts +191 -29
- package/src/client/index.ts +32 -4
- package/src/component/_generated/component.ts +2 -2
- package/src/component/entries.ts +282 -101
- package/src/component/model.ts +158 -0
- package/src/react/index.ts +206 -6
package/src/client/index.ts
CHANGED
|
@@ -43,12 +43,36 @@ export {
|
|
|
43
43
|
type FeedbackConfigOverrides,
|
|
44
44
|
} from "./config.js";
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Minimal host context exposed to the actor resolver.
|
|
48
|
+
*
|
|
49
|
+
* The feedback component itself does not read host authentication. Use
|
|
50
|
+
* `auth` to resolve the current host identity into a `FeedbackActor`.
|
|
51
|
+
*/
|
|
46
52
|
export interface FeedbackAuthContext {
|
|
53
|
+
/** Host Convex authentication interface for the current request. */
|
|
47
54
|
auth: Auth;
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Configuration used when exposing feedback functions from the host Convex
|
|
59
|
+
* application.
|
|
60
|
+
*/
|
|
50
61
|
export interface ExposeFeedbackOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Resolves the current request into a stable feedback actor.
|
|
64
|
+
*
|
|
65
|
+
* Return `null` for an unauthenticated request. Read-only queries may still
|
|
66
|
+
* execute, but mutations requiring an actor will reject unauthenticated
|
|
67
|
+
* callers.
|
|
68
|
+
*/
|
|
51
69
|
actor: (ctx: FeedbackAuthContext) => Promise<FeedbackActor | null>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Optional component behavior overrides.
|
|
73
|
+
*
|
|
74
|
+
* Unspecified values use `defaultFeedbackConfig`.
|
|
75
|
+
*/
|
|
52
76
|
config?: FeedbackConfigOverrides;
|
|
53
77
|
}
|
|
54
78
|
|
|
@@ -95,19 +119,20 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
95
119
|
listEntries: queryGeneric({
|
|
96
120
|
args: {
|
|
97
121
|
paginationOpts: paginationOptsValidator,
|
|
98
|
-
|
|
122
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
99
123
|
status: v.optional(entryStatusValidator),
|
|
100
124
|
sort: v.optional(entrySortValidator),
|
|
101
125
|
},
|
|
102
126
|
returns: paginationResultValidator(publicEntryValidator),
|
|
103
127
|
handler: async (ctx, args) => {
|
|
104
128
|
const actor = await options.actor(ctx);
|
|
129
|
+
|
|
105
130
|
return await ctx.runQuery(component.entries.list, {
|
|
106
131
|
paginationOpts: clampPagination(
|
|
107
132
|
args.paginationOpts,
|
|
108
133
|
config.entries.maxPageSize,
|
|
109
134
|
),
|
|
110
|
-
...(args.
|
|
135
|
+
...(args.kinds === undefined ? {} : { kinds: args.kinds }),
|
|
111
136
|
...(args.status === undefined ? {} : { status: args.status }),
|
|
112
137
|
sort: args.sort ?? config.entries.defaultSort,
|
|
113
138
|
...actorIdFields(actor),
|
|
@@ -130,16 +155,17 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
130
155
|
searchEntries: queryGeneric({
|
|
131
156
|
args: {
|
|
132
157
|
searchQuery: v.string(),
|
|
133
|
-
|
|
158
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
134
159
|
status: v.optional(entryStatusValidator),
|
|
135
160
|
limit: v.optional(v.number()),
|
|
136
161
|
},
|
|
137
162
|
returns: v.array(publicEntryValidator),
|
|
138
163
|
handler: async (ctx, args) => {
|
|
139
164
|
const actor = await options.actor(ctx);
|
|
165
|
+
|
|
140
166
|
return await ctx.runQuery(component.entries.search, {
|
|
141
167
|
searchQuery: args.searchQuery,
|
|
142
|
-
...(args.
|
|
168
|
+
...(args.kinds === undefined ? {} : { kinds: args.kinds }),
|
|
143
169
|
...(args.status === undefined ? {} : { status: args.status }),
|
|
144
170
|
limit: clampPositive(
|
|
145
171
|
args.limit,
|
|
@@ -163,7 +189,9 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
163
189
|
if (!config.search.duplicateSuggestions) {
|
|
164
190
|
return { exact: [], similar: [] };
|
|
165
191
|
}
|
|
192
|
+
|
|
166
193
|
const actor = await options.actor(ctx);
|
|
194
|
+
|
|
167
195
|
return await ctx.runQuery(component.entries.similar, {
|
|
168
196
|
title: args.title,
|
|
169
197
|
body: args.body,
|
|
@@ -161,7 +161,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
161
161
|
"query",
|
|
162
162
|
"internal",
|
|
163
163
|
{
|
|
164
|
-
|
|
164
|
+
kinds?: Array<"feedback" | "feature_request" | "bug_report">;
|
|
165
165
|
paginationOpts: {
|
|
166
166
|
cursor: string | null;
|
|
167
167
|
endCursor?: string | null;
|
|
@@ -211,7 +211,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
211
211
|
"query",
|
|
212
212
|
"internal",
|
|
213
213
|
{
|
|
214
|
-
|
|
214
|
+
kinds?: Array<"feedback" | "feature_request" | "bug_report">;
|
|
215
215
|
limit: number;
|
|
216
216
|
searchQuery: string;
|
|
217
217
|
status?:
|
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,
|
|
@@ -117,7 +215,7 @@ export const get = query({
|
|
|
117
215
|
export const search = query({
|
|
118
216
|
args: {
|
|
119
217
|
searchQuery: v.string(),
|
|
120
|
-
|
|
218
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
121
219
|
status: v.optional(entryStatusValidator),
|
|
122
220
|
limit: v.number(),
|
|
123
221
|
viewerActorId: v.optional(v.string()),
|
|
@@ -125,42 +223,88 @@ export const search = query({
|
|
|
125
223
|
returns: v.array(publicEntryValidator),
|
|
126
224
|
handler: async (ctx, args) => {
|
|
127
225
|
const searchQuery = args.searchQuery.trim();
|
|
128
|
-
if (searchQuery.length === 0 || args.limit <= 0) return [];
|
|
129
|
-
const { kind, status } = args;
|
|
130
226
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
227
|
+
if (searchQuery.length === 0 || args.limit <= 0) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const kinds = normalizeKindFilter(args.kinds);
|
|
232
|
+
const { status } = args;
|
|
233
|
+
|
|
234
|
+
let entries: Doc<"entries">[];
|
|
235
|
+
|
|
236
|
+
if (kinds === undefined) {
|
|
237
|
+
entries =
|
|
238
|
+
status === undefined
|
|
239
|
+
? await ctx.db
|
|
240
|
+
.query("entries")
|
|
241
|
+
.withSearchIndex("search", (q) =>
|
|
242
|
+
q.search("searchText", searchQuery),
|
|
243
|
+
)
|
|
244
|
+
.take(args.limit)
|
|
245
|
+
: await ctx.db
|
|
246
|
+
.query("entries")
|
|
247
|
+
.withSearchIndex("search", (q) =>
|
|
248
|
+
q.search("searchText", searchQuery).eq("status", status),
|
|
249
|
+
)
|
|
250
|
+
.take(args.limit);
|
|
251
|
+
} else if (kinds.length === 1) {
|
|
252
|
+
const kind = kinds[0];
|
|
253
|
+
|
|
254
|
+
if (kind === undefined) {
|
|
255
|
+
throw new ConvexError("Invalid kind filter.");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
entries =
|
|
259
|
+
status === undefined
|
|
143
260
|
? await ctx.db
|
|
144
261
|
.query("entries")
|
|
145
262
|
.withSearchIndex("search", (q) =>
|
|
146
263
|
q.search("searchText", searchQuery).eq("kind", kind),
|
|
147
264
|
)
|
|
148
265
|
.take(args.limit)
|
|
149
|
-
:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
266
|
+
: await ctx.db
|
|
267
|
+
.query("entries")
|
|
268
|
+
.withSearchIndex("search", (q) =>
|
|
269
|
+
q
|
|
270
|
+
.search("searchText", searchQuery)
|
|
271
|
+
.eq("kind", kind)
|
|
272
|
+
.eq("status", status),
|
|
273
|
+
)
|
|
274
|
+
.take(args.limit);
|
|
275
|
+
} else {
|
|
276
|
+
const firstKind = kinds[0];
|
|
277
|
+
const secondKind = kinds[1];
|
|
278
|
+
|
|
279
|
+
if (firstKind === undefined || secondKind === undefined) {
|
|
280
|
+
throw new ConvexError("Invalid kind filter.");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const searchResults =
|
|
284
|
+
status === undefined
|
|
285
|
+
? ctx.db
|
|
286
|
+
.query("entries")
|
|
287
|
+
.withSearchIndex("search", (q) =>
|
|
288
|
+
q.search("searchText", searchQuery),
|
|
289
|
+
)
|
|
290
|
+
: ctx.db
|
|
291
|
+
.query("entries")
|
|
292
|
+
.withSearchIndex("search", (q) =>
|
|
293
|
+
q.search("searchText", searchQuery).eq("status", status),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
entries = await searchResults
|
|
297
|
+
// eslint-disable-next-line @convex-dev/no-filter-in-query
|
|
298
|
+
.filter((q) =>
|
|
299
|
+
q.or(
|
|
300
|
+
q.eq(q.field("kind"), firstKind),
|
|
301
|
+
q.eq(q.field("kind"), secondKind),
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
.take(args.limit);
|
|
305
|
+
}
|
|
162
306
|
|
|
163
|
-
return Promise.all(
|
|
307
|
+
return await Promise.all(
|
|
164
308
|
entries.map((entry) => serializeEntry(ctx, entry, args.viewerActorId)),
|
|
165
309
|
);
|
|
166
310
|
},
|
|
@@ -176,16 +320,27 @@ export const similar = query({
|
|
|
176
320
|
},
|
|
177
321
|
returns: similarEntriesValidator,
|
|
178
322
|
handler: async (ctx, args) => {
|
|
179
|
-
if (args.limit <= 0)
|
|
323
|
+
if (args.limit <= 0) {
|
|
324
|
+
return {
|
|
325
|
+
exact: [],
|
|
326
|
+
similar: [],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
180
329
|
|
|
181
330
|
const title = args.title.trim();
|
|
182
331
|
const body = args.body.trim();
|
|
183
|
-
const
|
|
332
|
+
const kind = args.kind;
|
|
333
|
+
|
|
184
334
|
if (title.length === 0 && body.length === 0) {
|
|
185
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
exact: [],
|
|
337
|
+
similar: [],
|
|
338
|
+
};
|
|
186
339
|
}
|
|
187
340
|
|
|
188
341
|
const normalizedTitle = normalizeTitle(title);
|
|
342
|
+
|
|
343
|
+
// Exact normalized-title matches always consume the available limit first.
|
|
189
344
|
const exactDocs =
|
|
190
345
|
title.length === 0
|
|
191
346
|
? []
|
|
@@ -203,38 +358,57 @@ export const similar = query({
|
|
|
203
358
|
)
|
|
204
359
|
.take(args.limit);
|
|
205
360
|
|
|
206
|
-
const
|
|
361
|
+
const exact = await Promise.all(
|
|
362
|
+
exactDocs.map((entry) => serializeEntry(ctx, entry, args.viewerActorId)),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const remainingLimit = args.limit - exactDocs.length;
|
|
366
|
+
|
|
367
|
+
if (remainingLimit <= 0) {
|
|
368
|
+
return {
|
|
369
|
+
exact,
|
|
370
|
+
similar: [],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
207
374
|
const searchText = `${title}\n${body}`.trim();
|
|
208
375
|
|
|
376
|
+
if (searchText.length === 0) {
|
|
377
|
+
return {
|
|
378
|
+
exact,
|
|
379
|
+
similar: [],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const exactIds = new Set(exactDocs.map((entry) => entry._id));
|
|
384
|
+
|
|
385
|
+
// We fetch enough candidates to account for exact matches also appearing
|
|
386
|
+
// in the full-text search results. Those duplicates are removed below.
|
|
387
|
+
const candidateLimit = remainingLimit + exactIds.size;
|
|
388
|
+
|
|
209
389
|
const similarDocs =
|
|
210
|
-
|
|
211
|
-
?
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
.take(args.limit + exactIds.size);
|
|
390
|
+
kind === undefined
|
|
391
|
+
? await ctx.db
|
|
392
|
+
.query("entries")
|
|
393
|
+
.withSearchIndex("search", (q) =>
|
|
394
|
+
q.search("searchText", searchText),
|
|
395
|
+
)
|
|
396
|
+
.take(candidateLimit)
|
|
397
|
+
: await ctx.db
|
|
398
|
+
.query("entries")
|
|
399
|
+
.withSearchIndex("search", (q) =>
|
|
400
|
+
q.search("searchText", searchText).eq("kind", kind),
|
|
401
|
+
)
|
|
402
|
+
.take(candidateLimit);
|
|
225
403
|
|
|
226
|
-
const
|
|
404
|
+
const similarDocsWithoutExactMatches = similarDocs
|
|
227
405
|
.filter((entry) => !exactIds.has(entry._id))
|
|
228
|
-
.slice(0,
|
|
406
|
+
.slice(0, remainingLimit);
|
|
229
407
|
|
|
230
408
|
return {
|
|
231
|
-
exact
|
|
232
|
-
exactDocs.map((entry) =>
|
|
233
|
-
serializeEntry(ctx, entry, args.viewerActorId),
|
|
234
|
-
),
|
|
235
|
-
),
|
|
409
|
+
exact,
|
|
236
410
|
similar: await Promise.all(
|
|
237
|
-
|
|
411
|
+
similarDocsWithoutExactMatches.map((entry) =>
|
|
238
412
|
serializeEntry(ctx, entry, args.viewerActorId),
|
|
239
413
|
),
|
|
240
414
|
),
|
|
@@ -267,7 +441,7 @@ export const create = mutation({
|
|
|
267
441
|
);
|
|
268
442
|
const body = normalizeRequiredText(args.body, "Body", args.maxBodyLength);
|
|
269
443
|
|
|
270
|
-
|
|
444
|
+
const entry = await ctx.db.insert("entries", {
|
|
271
445
|
actorId: args.actorId,
|
|
272
446
|
kind: args.kind,
|
|
273
447
|
status: args.defaultStatus,
|
|
@@ -275,9 +449,16 @@ export const create = mutation({
|
|
|
275
449
|
body,
|
|
276
450
|
normalizedTitle: normalizeTitle(title),
|
|
277
451
|
searchText: `${title}\n${body}`,
|
|
278
|
-
upvoteCount:
|
|
452
|
+
upvoteCount: 1,
|
|
279
453
|
commentCount: 0,
|
|
280
454
|
});
|
|
455
|
+
|
|
456
|
+
await ctx.db.insert("reactions", {
|
|
457
|
+
actorId: args.actorId,
|
|
458
|
+
entryId: entry,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
return entry;
|
|
281
462
|
},
|
|
282
463
|
});
|
|
283
464
|
|