convex-feedback 0.2.0-beta.1 → 0.2.0-beta.3
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 +10 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +2 -0
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +1 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/admin.d.ts.map +1 -1
- package/dist/component/admin.js +47 -6
- package/dist/component/admin.js.map +1 -1
- package/dist/component/entries.d.ts +1 -0
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +7 -0
- package/dist/component/entries.js.map +1 -1
- package/dist/component/roadmap.d.ts.map +1 -1
- package/dist/component/roadmap.js +11 -6
- package/dist/component/roadmap.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +3 -0
- package/src/client/index.ts +2 -0
- package/src/component/_generated/component.ts +1 -0
- package/src/component/admin.ts +70 -6
- package/src/component/entries.ts +7 -0
- package/src/component/roadmap.ts +11 -6
package/src/component/admin.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { stream } from "convex-helpers/server/stream";
|
|
1
2
|
import {
|
|
2
3
|
paginationOptsValidator,
|
|
3
4
|
paginationResultValidator,
|
|
5
|
+
type PaginationOptions,
|
|
6
|
+
type PaginationResult,
|
|
4
7
|
} from "convex/server";
|
|
5
8
|
import { ConvexError, v } from "convex/values";
|
|
6
|
-
import { stream } from "convex-helpers/server/stream";
|
|
7
9
|
|
|
8
10
|
import type { Doc } from "./_generated/dataModel.js";
|
|
9
11
|
import { query } from "./_generated/server.js";
|
|
@@ -19,6 +21,68 @@ import {
|
|
|
19
21
|
} from "./model.js";
|
|
20
22
|
import schema from "./schema.js";
|
|
21
23
|
|
|
24
|
+
const ADMIN_SEARCH_CURSOR_PREFIX = "convex-feedback:admin-search:";
|
|
25
|
+
|
|
26
|
+
type TakeQuery<T> = {
|
|
27
|
+
take(n: number): Promise<T[]>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function encodeAdminSearchCursor(offset: number): string {
|
|
31
|
+
return `${ADMIN_SEARCH_CURSOR_PREFIX}${offset}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodeAdminSearchCursor(cursor: string | null | undefined): number {
|
|
35
|
+
if (cursor === null || cursor === undefined) return 0;
|
|
36
|
+
if (!cursor.startsWith(ADMIN_SEARCH_CURSOR_PREFIX)) {
|
|
37
|
+
throw new ConvexError("InvalidCursor: invalid admin search cursor.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const offset = Number(cursor.slice(ADMIN_SEARCH_CURSOR_PREFIX.length));
|
|
41
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
42
|
+
throw new ConvexError("InvalidCursor: invalid admin search cursor.");
|
|
43
|
+
}
|
|
44
|
+
return offset;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function addAdminSearchCursorOffsets(left: number, right: number): number {
|
|
48
|
+
const result = left + right;
|
|
49
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
50
|
+
throw new ConvexError("InvalidCursor: admin search cursor is too large.");
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function paginateAdminSearchIndex(
|
|
56
|
+
query: TakeQuery<Doc<"entries">>,
|
|
57
|
+
paginationOpts: PaginationOptions,
|
|
58
|
+
): Promise<PaginationResult<Doc<"entries">>> {
|
|
59
|
+
const start = decodeAdminSearchCursor(paginationOpts.cursor);
|
|
60
|
+
const end = decodeAdminSearchCursor(paginationOpts.endCursor);
|
|
61
|
+
const requestedEnd =
|
|
62
|
+
paginationOpts.endCursor === undefined || paginationOpts.endCursor === null
|
|
63
|
+
? addAdminSearchCursorOffsets(start, paginationOpts.numItems)
|
|
64
|
+
: end;
|
|
65
|
+
|
|
66
|
+
if (requestedEnd < start) {
|
|
67
|
+
throw new ConvexError("InvalidCursor: admin search cursors are reversed.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Search-index queries cannot expose a component-safe continuation cursor.
|
|
71
|
+
// Read through the requested range and use the result offset as our opaque
|
|
72
|
+
// cursor instead.
|
|
73
|
+
const results = await query.take(
|
|
74
|
+
addAdminSearchCursorOffsets(requestedEnd, 1),
|
|
75
|
+
);
|
|
76
|
+
const page = results.slice(start, requestedEnd);
|
|
77
|
+
const nextOffset = addAdminSearchCursorOffsets(start, page.length);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
page,
|
|
81
|
+
isDone: results.length <= requestedEnd,
|
|
82
|
+
continueCursor: encodeAdminSearchCursor(nextOffset),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
22
86
|
function matchesFilters(
|
|
23
87
|
entry: Doc<"entries">,
|
|
24
88
|
kinds: EntryKind[] | undefined,
|
|
@@ -127,9 +191,8 @@ export const searchEntries = query({
|
|
|
127
191
|
const canUseSearchIndex = kinds === undefined || kinds.length === 1;
|
|
128
192
|
|
|
129
193
|
const result = canUseSearchIndex
|
|
130
|
-
? await
|
|
131
|
-
.query("entries")
|
|
132
|
-
.withSearchIndex("search", (q) => {
|
|
194
|
+
? await paginateAdminSearchIndex(
|
|
195
|
+
ctx.db.query("entries").withSearchIndex("search", (q) => {
|
|
133
196
|
const searched = q.search("searchText", searchQuery);
|
|
134
197
|
const withKind =
|
|
135
198
|
kinds?.length === 1 ? searched.eq("kind", kinds[0]!) : searched;
|
|
@@ -140,8 +203,9 @@ export const searchEntries = query({
|
|
|
140
203
|
return args.priority === undefined
|
|
141
204
|
? withStatus
|
|
142
205
|
: withStatus.eq("priority", args.priority);
|
|
143
|
-
})
|
|
144
|
-
|
|
206
|
+
}),
|
|
207
|
+
args.paginationOpts,
|
|
208
|
+
)
|
|
145
209
|
: await (
|
|
146
210
|
args.priority !== undefined
|
|
147
211
|
? stream(ctx.db, schema)
|
package/src/component/entries.ts
CHANGED
|
@@ -609,6 +609,7 @@ export const update = mutation({
|
|
|
609
609
|
args: {
|
|
610
610
|
actor: actorValidator,
|
|
611
611
|
entryId: v.id("entries"),
|
|
612
|
+
kind: v.optional(entryKindValidator),
|
|
612
613
|
title: v.string(),
|
|
613
614
|
body: v.string(),
|
|
614
615
|
editableByAuthor: v.boolean(),
|
|
@@ -625,6 +626,11 @@ export const update = mutation({
|
|
|
625
626
|
actorIsAdmin(args.actor) ||
|
|
626
627
|
(args.editableByAuthor && entry.actorId === args.actor.id);
|
|
627
628
|
if (!canEdit) throw new ConvexError("Not authorized to edit this entry.");
|
|
629
|
+
if (args.kind !== undefined && args.kind !== entry.kind) {
|
|
630
|
+
if (!actorIsAdmin(args.actor)) {
|
|
631
|
+
throw new ConvexError("Admin access is required to change entry kind.");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
628
634
|
|
|
629
635
|
const title = normalizeRequiredText(
|
|
630
636
|
args.title,
|
|
@@ -634,6 +640,7 @@ export const update = mutation({
|
|
|
634
640
|
const body = normalizeRequiredText(args.body, "Body", args.maxBodyLength);
|
|
635
641
|
|
|
636
642
|
await ctx.db.patch("entries", args.entryId, {
|
|
643
|
+
kind: args.kind ?? entry.kind,
|
|
637
644
|
title,
|
|
638
645
|
body,
|
|
639
646
|
normalizedTitle: normalizeTitle(title),
|
package/src/component/roadmap.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { paginator } from "convex-helpers/server/pagination";
|
|
1
2
|
import {
|
|
2
3
|
paginationOptsValidator,
|
|
3
4
|
paginationResultValidator,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
roadmapStatusValidator,
|
|
22
23
|
} from "./model.js";
|
|
23
24
|
import type { RoadmapStatus } from "./model.js";
|
|
25
|
+
import schema from "./schema.js";
|
|
24
26
|
|
|
25
27
|
const POSITION_STEP = 1_000_000;
|
|
26
28
|
const REBALANCE_GAP_THRESHOLD = 10;
|
|
@@ -128,13 +130,14 @@ export const list = query({
|
|
|
128
130
|
},
|
|
129
131
|
returns: paginationResultValidator(roadmapItemValidator),
|
|
130
132
|
handler: async (ctx, args) => {
|
|
133
|
+
const db = paginator(ctx.db, schema);
|
|
131
134
|
const status = args.status;
|
|
132
135
|
const state =
|
|
133
136
|
status === undefined ? null : await getRebalanceState(ctx, status);
|
|
134
137
|
const visibleGeneration = state?.visibleGeneration;
|
|
135
138
|
const roadmapQuery =
|
|
136
139
|
status !== undefined && visibleGeneration !== undefined
|
|
137
|
-
?
|
|
140
|
+
? db
|
|
138
141
|
.query("roadmap")
|
|
139
142
|
.withIndex("by_status_deleting_at_rebalance_id_position", (q) =>
|
|
140
143
|
q
|
|
@@ -143,12 +146,12 @@ export const list = query({
|
|
|
143
146
|
.eq("rebalanceId", visibleGeneration),
|
|
144
147
|
)
|
|
145
148
|
: status === undefined
|
|
146
|
-
?
|
|
149
|
+
? db
|
|
147
150
|
.query("roadmap")
|
|
148
151
|
.withIndex("by_deleting_at_and_position", (q) =>
|
|
149
152
|
q.eq("deletingAt", undefined),
|
|
150
153
|
)
|
|
151
|
-
:
|
|
154
|
+
: db
|
|
152
155
|
.query("roadmap")
|
|
153
156
|
.withIndex("by_status_deleting_at_position", (q) =>
|
|
154
157
|
q.eq("status", status).eq("deletingAt", undefined),
|
|
@@ -348,11 +351,12 @@ export const rebalanceBatch = internalMutation({
|
|
|
348
351
|
},
|
|
349
352
|
returns: v.null(),
|
|
350
353
|
handler: async (ctx, args) => {
|
|
354
|
+
const db = paginator(ctx.db, schema);
|
|
351
355
|
const state = await getRebalanceState(ctx, args.status);
|
|
352
356
|
if (state?.activeGeneration !== args.rebalanceId) return null;
|
|
353
357
|
|
|
354
358
|
if (args.phase === "mark") {
|
|
355
|
-
const result = await
|
|
359
|
+
const result = await db
|
|
356
360
|
.query("roadmap")
|
|
357
361
|
.withIndex("by_status_deleting_at_position", (q) =>
|
|
358
362
|
q.eq("status", args.status).eq("deletingAt", undefined),
|
|
@@ -391,7 +395,7 @@ export const rebalanceBatch = internalMutation({
|
|
|
391
395
|
return null;
|
|
392
396
|
}
|
|
393
397
|
|
|
394
|
-
const result = await
|
|
398
|
+
const result = await db
|
|
395
399
|
.query("roadmap")
|
|
396
400
|
.withIndex("by_status_rebalance_rank", (q) =>
|
|
397
401
|
q.eq("status", args.status).eq("rebalanceId", args.rebalanceId),
|
|
@@ -610,7 +614,8 @@ export const listFeedback = query({
|
|
|
610
614
|
},
|
|
611
615
|
returns: paginationResultValidator(publicEntryValidator),
|
|
612
616
|
handler: async (ctx, args) => {
|
|
613
|
-
const
|
|
617
|
+
const db = paginator(ctx.db, schema);
|
|
618
|
+
const result = await db
|
|
614
619
|
.query("entries")
|
|
615
620
|
.withIndex("by_roadmap_id", (q) => q.eq("roadmapId", args.roadmapId))
|
|
616
621
|
.order("desc")
|