convex-feedback 0.1.5 → 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 +70 -27
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +76 -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 +253 -8
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +4 -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 +293 -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 +3 -3
- package/dist/component/comments.js.map +1 -1
- package/dist/component/entries.d.ts +29 -18
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +27 -6
- 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/model.d.ts +140 -24
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/model.js +30 -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 +61 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +48 -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 +169 -12
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +72 -1
- package/dist/react/index.js.map +1 -1
- package/dist/test.d.ts +61 -5
- package/dist/test.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/README.md +1 -1
- package/src/client/api.ts +176 -0
- package/src/client/config.ts +1 -1
- package/src/client/index.ts +330 -13
- package/src/component/README.md +1 -1
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +371 -9
- package/src/component/admin.ts +180 -0
- package/src/component/comments.ts +3 -2
- package/src/component/entries.ts +29 -5
- package/src/component/helpers.ts +44 -1
- package/src/component/model.ts +65 -4
- package/src/component/roadmap.ts +627 -0
- package/src/component/schema.ts +51 -1
- package/src/react/README.md +2 -2
- package/src/react/index.ts +139 -4
package/src/client/api.ts
CHANGED
|
@@ -13,6 +13,10 @@ import type {
|
|
|
13
13
|
FeedbackComment,
|
|
14
14
|
FeedbackEntry,
|
|
15
15
|
FeedbackMetadata,
|
|
16
|
+
AdminFeedbackEntry,
|
|
17
|
+
EntryPriority,
|
|
18
|
+
RoadmapItem,
|
|
19
|
+
RoadmapStatus,
|
|
16
20
|
SimilarEntriesResult,
|
|
17
21
|
} from "../component/model.js";
|
|
18
22
|
|
|
@@ -172,6 +176,60 @@ export type SetEntryStatusArgs = {
|
|
|
172
176
|
status: EntryStatus;
|
|
173
177
|
};
|
|
174
178
|
|
|
179
|
+
export type AdminListEntriesArgs = {
|
|
180
|
+
/** Convex cursor-pagination options. */
|
|
181
|
+
paginationOpts: PaginationOptions;
|
|
182
|
+
kinds?: EntryKind[];
|
|
183
|
+
status?: EntryStatus;
|
|
184
|
+
priority?: EntryPriority;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type AdminSearchEntriesArgs = AdminListEntriesArgs & {
|
|
188
|
+
searchQuery: string;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type SetEntryPriorityArgs = {
|
|
192
|
+
entryId: string;
|
|
193
|
+
priority: EntryPriority | null;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type ListRoadmapArgs = {
|
|
197
|
+
/** Convex cursor-pagination options. */
|
|
198
|
+
paginationOpts: PaginationOptions;
|
|
199
|
+
status?: RoadmapStatus;
|
|
200
|
+
};
|
|
201
|
+
export type SearchRoadmapArgs = { searchQuery: string; limit?: number };
|
|
202
|
+
export type CreateRoadmapArgs = {
|
|
203
|
+
title: string;
|
|
204
|
+
description?: string;
|
|
205
|
+
status: RoadmapStatus;
|
|
206
|
+
};
|
|
207
|
+
export type CreateRoadmapForEntryArgs = CreateRoadmapArgs & {
|
|
208
|
+
entryId: string;
|
|
209
|
+
};
|
|
210
|
+
export type UpdateRoadmapArgs = {
|
|
211
|
+
roadmapId: string;
|
|
212
|
+
title: string;
|
|
213
|
+
description?: string;
|
|
214
|
+
};
|
|
215
|
+
export type DeleteRoadmapArgs = { roadmapId: string };
|
|
216
|
+
export type MoveRoadmapArgs = {
|
|
217
|
+
roadmapId: string;
|
|
218
|
+
status: RoadmapStatus;
|
|
219
|
+
previousItemId?: string;
|
|
220
|
+
nextItemId?: string;
|
|
221
|
+
};
|
|
222
|
+
export type AttachFeedbackToRoadmapArgs = {
|
|
223
|
+
roadmapId: string;
|
|
224
|
+
entryId: string;
|
|
225
|
+
};
|
|
226
|
+
export type DetachFeedbackFromRoadmapArgs = { entryId: string };
|
|
227
|
+
export type ListRoadmapFeedbackArgs = {
|
|
228
|
+
/** Convex cursor-pagination options. */
|
|
229
|
+
paginationOpts: PaginationOptions;
|
|
230
|
+
roadmapId: string;
|
|
231
|
+
};
|
|
232
|
+
|
|
175
233
|
/**
|
|
176
234
|
* Arguments for setting the current actor's entry-upvote state.
|
|
177
235
|
*/
|
|
@@ -308,6 +366,24 @@ export interface FeedbackPublicApi<
|
|
|
308
366
|
Name extends string | undefined = string | undefined,
|
|
309
367
|
RateLimitResult = never,
|
|
310
368
|
> {
|
|
369
|
+
/** Returns whether the host actor has admin permissions. */
|
|
370
|
+
isAdmin: FunctionReference<
|
|
371
|
+
"query",
|
|
372
|
+
"public",
|
|
373
|
+
Record<string, never>,
|
|
374
|
+
boolean,
|
|
375
|
+
Name
|
|
376
|
+
>;
|
|
377
|
+
|
|
378
|
+
/** Returns whether the current request has an authenticated host actor. */
|
|
379
|
+
isAuthenticated: FunctionReference<
|
|
380
|
+
"query",
|
|
381
|
+
"public",
|
|
382
|
+
Record<string, never>,
|
|
383
|
+
boolean,
|
|
384
|
+
Name
|
|
385
|
+
>;
|
|
386
|
+
|
|
311
387
|
/** Returns a cursor-paginated entry list. */
|
|
312
388
|
listEntries: FunctionReference<
|
|
313
389
|
"query",
|
|
@@ -371,6 +447,106 @@ export interface FeedbackPublicApi<
|
|
|
371
447
|
Name
|
|
372
448
|
>;
|
|
373
449
|
|
|
450
|
+
adminListEntries: FunctionReference<
|
|
451
|
+
"query",
|
|
452
|
+
"public",
|
|
453
|
+
AdminListEntriesArgs,
|
|
454
|
+
PaginationResult<AdminFeedbackEntry>,
|
|
455
|
+
Name
|
|
456
|
+
>;
|
|
457
|
+
adminGetEntry: FunctionReference<
|
|
458
|
+
"query",
|
|
459
|
+
"public",
|
|
460
|
+
GetEntryArgs,
|
|
461
|
+
AdminFeedbackEntry | null,
|
|
462
|
+
Name
|
|
463
|
+
>;
|
|
464
|
+
adminSearchEntries: FunctionReference<
|
|
465
|
+
"query",
|
|
466
|
+
"public",
|
|
467
|
+
AdminSearchEntriesArgs,
|
|
468
|
+
PaginationResult<AdminFeedbackEntry>,
|
|
469
|
+
Name
|
|
470
|
+
>;
|
|
471
|
+
setEntryPriority: FunctionReference<
|
|
472
|
+
"mutation",
|
|
473
|
+
"public",
|
|
474
|
+
SetEntryPriorityArgs,
|
|
475
|
+
null | RateLimitResult,
|
|
476
|
+
Name
|
|
477
|
+
>;
|
|
478
|
+
|
|
479
|
+
listRoadmap: FunctionReference<
|
|
480
|
+
"query",
|
|
481
|
+
"public",
|
|
482
|
+
ListRoadmapArgs,
|
|
483
|
+
PaginationResult<RoadmapItem>,
|
|
484
|
+
Name
|
|
485
|
+
>;
|
|
486
|
+
searchRoadmap: FunctionReference<
|
|
487
|
+
"query",
|
|
488
|
+
"public",
|
|
489
|
+
SearchRoadmapArgs,
|
|
490
|
+
RoadmapItem[],
|
|
491
|
+
Name
|
|
492
|
+
>;
|
|
493
|
+
createRoadmap: FunctionReference<
|
|
494
|
+
"mutation",
|
|
495
|
+
"public",
|
|
496
|
+
CreateRoadmapArgs,
|
|
497
|
+
string | RateLimitResult,
|
|
498
|
+
Name
|
|
499
|
+
>;
|
|
500
|
+
createRoadmapForEntry: FunctionReference<
|
|
501
|
+
"mutation",
|
|
502
|
+
"public",
|
|
503
|
+
CreateRoadmapForEntryArgs,
|
|
504
|
+
string | RateLimitResult,
|
|
505
|
+
Name
|
|
506
|
+
>;
|
|
507
|
+
updateRoadmap: FunctionReference<
|
|
508
|
+
"mutation",
|
|
509
|
+
"public",
|
|
510
|
+
UpdateRoadmapArgs,
|
|
511
|
+
null | RateLimitResult,
|
|
512
|
+
Name
|
|
513
|
+
>;
|
|
514
|
+
deleteRoadmap: FunctionReference<
|
|
515
|
+
"mutation",
|
|
516
|
+
"public",
|
|
517
|
+
DeleteRoadmapArgs,
|
|
518
|
+
null | RateLimitResult,
|
|
519
|
+
Name
|
|
520
|
+
>;
|
|
521
|
+
moveRoadmapItem: FunctionReference<
|
|
522
|
+
"mutation",
|
|
523
|
+
"public",
|
|
524
|
+
MoveRoadmapArgs,
|
|
525
|
+
number | RateLimitResult,
|
|
526
|
+
Name
|
|
527
|
+
>;
|
|
528
|
+
attachFeedbackToRoadmap: FunctionReference<
|
|
529
|
+
"mutation",
|
|
530
|
+
"public",
|
|
531
|
+
AttachFeedbackToRoadmapArgs,
|
|
532
|
+
null | RateLimitResult,
|
|
533
|
+
Name
|
|
534
|
+
>;
|
|
535
|
+
detachFeedbackFromRoadmap: FunctionReference<
|
|
536
|
+
"mutation",
|
|
537
|
+
"public",
|
|
538
|
+
DetachFeedbackFromRoadmapArgs,
|
|
539
|
+
null | RateLimitResult,
|
|
540
|
+
Name
|
|
541
|
+
>;
|
|
542
|
+
listRoadmapFeedback: FunctionReference<
|
|
543
|
+
"query",
|
|
544
|
+
"public",
|
|
545
|
+
ListRoadmapFeedbackArgs,
|
|
546
|
+
PaginationResult<FeedbackEntry>,
|
|
547
|
+
Name
|
|
548
|
+
>;
|
|
549
|
+
|
|
374
550
|
/** Sets entry-upvote state, or returns a configured rate-limit rejection. */
|
|
375
551
|
setEntryUpvote: FunctionReference<
|
|
376
552
|
"mutation",
|
package/src/client/config.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface FeedbackEntriesConfig {
|
|
|
42
42
|
/**
|
|
43
43
|
* Whether entry authors may edit their own title and body.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
45
|
+
* Admins are governed separately by the resolved actor permissions.
|
|
46
46
|
*
|
|
47
47
|
* @default true
|
|
48
48
|
*/
|
package/src/client/index.ts
CHANGED
|
@@ -23,15 +23,20 @@ import {
|
|
|
23
23
|
|
|
24
24
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
25
25
|
import {
|
|
26
|
+
adminEntryValidator,
|
|
26
27
|
commentSortValidator,
|
|
27
28
|
entryKindValidator,
|
|
29
|
+
entryPriorityValidator,
|
|
28
30
|
entrySortValidator,
|
|
29
31
|
entryStatusFilterValidator,
|
|
30
32
|
entryStatusValidator,
|
|
31
33
|
feedbackMetadataValidator,
|
|
32
34
|
publicCommentValidator,
|
|
33
35
|
publicEntryValidator,
|
|
36
|
+
roadmapItemValidator,
|
|
37
|
+
roadmapStatusValidator,
|
|
34
38
|
similarEntriesValidator,
|
|
39
|
+
actorIsAdmin,
|
|
35
40
|
type FeedbackActor,
|
|
36
41
|
} from "../component/model.js";
|
|
37
42
|
import type { FeedbackPublicApi } from "./api.js";
|
|
@@ -41,8 +46,10 @@ import {
|
|
|
41
46
|
} from "./config.js";
|
|
42
47
|
|
|
43
48
|
export type {
|
|
49
|
+
AdminFeedbackEntry,
|
|
44
50
|
CommentSort,
|
|
45
51
|
EntryKind,
|
|
52
|
+
EntryPriority,
|
|
46
53
|
EntrySort,
|
|
47
54
|
EntryStatus,
|
|
48
55
|
EntryStatusFilter,
|
|
@@ -51,6 +58,8 @@ export type {
|
|
|
51
58
|
FeedbackEntry,
|
|
52
59
|
FeedbackMetadata,
|
|
53
60
|
FeedbackMetadataValue,
|
|
61
|
+
RoadmapItem,
|
|
62
|
+
RoadmapStatus,
|
|
54
63
|
SimilarEntriesResult,
|
|
55
64
|
} from "../component/model.js";
|
|
56
65
|
export type { FeedbackPublicApi } from "./api.js";
|
|
@@ -127,10 +136,13 @@ export interface ThrowingFeedbackRateLimitConfig {
|
|
|
127
136
|
behavior?: "throw";
|
|
128
137
|
|
|
129
138
|
/**
|
|
130
|
-
* Whether all configured limiter groups apply to
|
|
139
|
+
* Whether all configured limiter groups apply to admins.
|
|
131
140
|
*
|
|
132
141
|
* @default false
|
|
133
142
|
*/
|
|
143
|
+
limitAdmins?: boolean;
|
|
144
|
+
|
|
145
|
+
/** @deprecated Use `limitAdmins`. */
|
|
134
146
|
limitModerators?: boolean;
|
|
135
147
|
|
|
136
148
|
/** Not accepted in throwing mode; select `"return"` to provide a validator. */
|
|
@@ -170,10 +182,13 @@ export interface ReturningFeedbackRateLimitConfig<
|
|
|
170
182
|
returns: ReturnsValidator;
|
|
171
183
|
|
|
172
184
|
/**
|
|
173
|
-
* Whether all configured limiter groups apply to
|
|
185
|
+
* Whether all configured limiter groups apply to admins.
|
|
174
186
|
*
|
|
175
187
|
* @default false
|
|
176
188
|
*/
|
|
189
|
+
limitAdmins?: boolean;
|
|
190
|
+
|
|
191
|
+
/** @deprecated Use `limitAdmins`. */
|
|
177
192
|
limitModerators?: boolean;
|
|
178
193
|
}
|
|
179
194
|
|
|
@@ -262,10 +277,10 @@ export type ThrowingExposeFeedbackOptions = Omit<
|
|
|
262
277
|
/** Optional component behavior and throwing rate-limit overrides. */
|
|
263
278
|
config?: FeedbackConfigOverrides & {
|
|
264
279
|
/**
|
|
265
|
-
* Controls throwing behavior and the
|
|
280
|
+
* Controls throwing behavior and the admin exemption.
|
|
266
281
|
*
|
|
267
282
|
* Omit this object to use `behavior: "throw"` and
|
|
268
|
-
* `
|
|
283
|
+
* `limitAdmins: false`.
|
|
269
284
|
*/
|
|
270
285
|
rateLimiting?: FeedbackRateLimitConfig;
|
|
271
286
|
};
|
|
@@ -316,9 +331,9 @@ function requireActor(actor: FeedbackActor | null): FeedbackActor {
|
|
|
316
331
|
return actor;
|
|
317
332
|
}
|
|
318
333
|
|
|
319
|
-
function
|
|
320
|
-
if (!actor
|
|
321
|
-
throw new ConvexError("
|
|
334
|
+
function requireAdmin(actor: FeedbackActor): void {
|
|
335
|
+
if (!actorIsAdmin(actor)) {
|
|
336
|
+
throw new ConvexError("Admin permissions are required.");
|
|
322
337
|
}
|
|
323
338
|
}
|
|
324
339
|
|
|
@@ -349,10 +364,9 @@ async function applyRateLimiter<
|
|
|
349
364
|
| ReturningFeedbackRateLimitConfig<FeedbackRateLimitReturnValidator>
|
|
350
365
|
| undefined,
|
|
351
366
|
): Promise<RateLimitResult<ReturnsValidator> | undefined> {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
) {
|
|
367
|
+
const limitAdmins =
|
|
368
|
+
rateLimitConfig?.limitAdmins ?? rateLimitConfig?.limitModerators ?? false;
|
|
369
|
+
if (limiter === undefined || (actorIsAdmin(actor) && !limitAdmins)) {
|
|
356
370
|
return undefined;
|
|
357
371
|
}
|
|
358
372
|
|
|
@@ -423,8 +437,44 @@ function buildFeedbackApi<
|
|
|
423
437
|
v.object({ active: v.boolean(), likeCount: v.number() }),
|
|
424
438
|
rateLimitReturnValidator,
|
|
425
439
|
);
|
|
440
|
+
const numberReturns = rateLimitedReturns(
|
|
441
|
+
v.number(),
|
|
442
|
+
rateLimitReturnValidator,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const requireAdminActor = async (ctx: FeedbackAuthContext) => {
|
|
446
|
+
const actor = requireActor(await options.actor(ctx));
|
|
447
|
+
requireAdmin(actor);
|
|
448
|
+
return actor;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const applyAdminEditLimit = async (
|
|
452
|
+
ctx: FeedbackRateLimitContext,
|
|
453
|
+
actor: FeedbackActor,
|
|
454
|
+
) =>
|
|
455
|
+
await applyRateLimiter(
|
|
456
|
+
ctx,
|
|
457
|
+
actor,
|
|
458
|
+
options.rateLimiters?.editContent,
|
|
459
|
+
rateLimitConfig,
|
|
460
|
+
);
|
|
426
461
|
|
|
427
462
|
return {
|
|
463
|
+
isAdmin: queryGeneric({
|
|
464
|
+
args: {},
|
|
465
|
+
returns: v.boolean(),
|
|
466
|
+
handler: async (ctx) => {
|
|
467
|
+
const actor = await options.actor(ctx);
|
|
468
|
+
return actor !== null && actorIsAdmin(actor);
|
|
469
|
+
},
|
|
470
|
+
}),
|
|
471
|
+
|
|
472
|
+
isAuthenticated: queryGeneric({
|
|
473
|
+
args: {},
|
|
474
|
+
returns: v.boolean(),
|
|
475
|
+
handler: async (ctx) => (await options.actor(ctx)) !== null,
|
|
476
|
+
}),
|
|
477
|
+
|
|
428
478
|
listEntries: queryGeneric({
|
|
429
479
|
args: {
|
|
430
480
|
paginationOpts: paginationOptsValidator,
|
|
@@ -461,7 +511,9 @@ function buildFeedbackApi<
|
|
|
461
511
|
return await ctx.runQuery(component.entries.get, {
|
|
462
512
|
entryId: args.entryId,
|
|
463
513
|
...actorIdFields(actor),
|
|
464
|
-
...(actor
|
|
514
|
+
...(actor !== null && actorIsAdmin(actor)
|
|
515
|
+
? { viewerIsAdmin: true }
|
|
516
|
+
: {}),
|
|
465
517
|
});
|
|
466
518
|
},
|
|
467
519
|
}),
|
|
@@ -588,7 +640,7 @@ function buildFeedbackApi<
|
|
|
588
640
|
returns: nullReturns,
|
|
589
641
|
handler: async (ctx, args) => {
|
|
590
642
|
const actor = requireActor(await options.actor(ctx));
|
|
591
|
-
|
|
643
|
+
requireAdmin(actor);
|
|
592
644
|
const limited = await applyRateLimiter(
|
|
593
645
|
asRateLimitContext(ctx),
|
|
594
646
|
actor,
|
|
@@ -604,6 +656,87 @@ function buildFeedbackApi<
|
|
|
604
656
|
},
|
|
605
657
|
}),
|
|
606
658
|
|
|
659
|
+
adminListEntries: queryGeneric({
|
|
660
|
+
args: {
|
|
661
|
+
paginationOpts: paginationOptsValidator,
|
|
662
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
663
|
+
status: v.optional(entryStatusValidator),
|
|
664
|
+
priority: v.optional(entryPriorityValidator),
|
|
665
|
+
},
|
|
666
|
+
returns: paginationResultValidator(adminEntryValidator),
|
|
667
|
+
handler: async (ctx, args) => {
|
|
668
|
+
const actor = await requireAdminActor(ctx);
|
|
669
|
+
return await ctx.runQuery(component.admin.listEntries, {
|
|
670
|
+
...(args.kinds === undefined ? {} : { kinds: args.kinds }),
|
|
671
|
+
...(args.status === undefined ? {} : { status: args.status }),
|
|
672
|
+
...(args.priority === undefined ? {} : { priority: args.priority }),
|
|
673
|
+
paginationOpts: clampPagination(
|
|
674
|
+
args.paginationOpts,
|
|
675
|
+
config.entries.maxPageSize,
|
|
676
|
+
),
|
|
677
|
+
viewerActorId: actor.id,
|
|
678
|
+
});
|
|
679
|
+
},
|
|
680
|
+
}),
|
|
681
|
+
|
|
682
|
+
adminGetEntry: queryGeneric({
|
|
683
|
+
args: { entryId: v.string() },
|
|
684
|
+
returns: v.union(adminEntryValidator, v.null()),
|
|
685
|
+
handler: async (ctx, args) => {
|
|
686
|
+
const actor = await requireAdminActor(ctx);
|
|
687
|
+
return await ctx.runQuery(component.admin.getEntry, {
|
|
688
|
+
entryId: args.entryId,
|
|
689
|
+
viewerActorId: actor.id,
|
|
690
|
+
});
|
|
691
|
+
},
|
|
692
|
+
}),
|
|
693
|
+
|
|
694
|
+
adminSearchEntries: queryGeneric({
|
|
695
|
+
args: {
|
|
696
|
+
paginationOpts: paginationOptsValidator,
|
|
697
|
+
searchQuery: v.string(),
|
|
698
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
699
|
+
status: v.optional(entryStatusValidator),
|
|
700
|
+
priority: v.optional(entryPriorityValidator),
|
|
701
|
+
},
|
|
702
|
+
returns: paginationResultValidator(adminEntryValidator),
|
|
703
|
+
handler: async (ctx, args) => {
|
|
704
|
+
const actor = await requireAdminActor(ctx);
|
|
705
|
+
return await ctx.runQuery(component.admin.searchEntries, {
|
|
706
|
+
searchQuery: args.searchQuery,
|
|
707
|
+
...(args.kinds === undefined ? {} : { kinds: args.kinds }),
|
|
708
|
+
...(args.status === undefined ? {} : { status: args.status }),
|
|
709
|
+
...(args.priority === undefined ? {} : { priority: args.priority }),
|
|
710
|
+
paginationOpts: clampPagination(
|
|
711
|
+
args.paginationOpts,
|
|
712
|
+
config.entries.maxPageSize,
|
|
713
|
+
),
|
|
714
|
+
viewerActorId: actor.id,
|
|
715
|
+
});
|
|
716
|
+
},
|
|
717
|
+
}),
|
|
718
|
+
|
|
719
|
+
setEntryPriority: mutationGeneric({
|
|
720
|
+
args: {
|
|
721
|
+
entryId: v.string(),
|
|
722
|
+
priority: v.union(entryPriorityValidator, v.null()),
|
|
723
|
+
},
|
|
724
|
+
returns: nullReturns,
|
|
725
|
+
handler: async (ctx, args) => {
|
|
726
|
+
const actor = await requireAdminActor(ctx);
|
|
727
|
+
const limited = await applyAdminEditLimit(
|
|
728
|
+
asRateLimitContext(ctx),
|
|
729
|
+
actor,
|
|
730
|
+
);
|
|
731
|
+
if (limited !== undefined) return limited;
|
|
732
|
+
return await ctx.runMutation(component.entries.setPriority, {
|
|
733
|
+
actor,
|
|
734
|
+
entryId: args.entryId,
|
|
735
|
+
priority: args.priority,
|
|
736
|
+
});
|
|
737
|
+
},
|
|
738
|
+
}),
|
|
739
|
+
|
|
607
740
|
setEntryUpvote: mutationGeneric({
|
|
608
741
|
args: { entryId: v.string(), desiredState: v.boolean() },
|
|
609
742
|
returns: entryUpvoteReturns,
|
|
@@ -739,6 +872,190 @@ function buildFeedbackApi<
|
|
|
739
872
|
});
|
|
740
873
|
},
|
|
741
874
|
}),
|
|
875
|
+
|
|
876
|
+
listRoadmap: queryGeneric({
|
|
877
|
+
args: {
|
|
878
|
+
paginationOpts: paginationOptsValidator,
|
|
879
|
+
status: v.optional(roadmapStatusValidator),
|
|
880
|
+
},
|
|
881
|
+
returns: paginationResultValidator(roadmapItemValidator),
|
|
882
|
+
handler: async (ctx, args) => {
|
|
883
|
+
return await ctx.runQuery(component.roadmap.list, {
|
|
884
|
+
...args,
|
|
885
|
+
paginationOpts: clampPagination(
|
|
886
|
+
args.paginationOpts,
|
|
887
|
+
config.entries.maxPageSize,
|
|
888
|
+
),
|
|
889
|
+
});
|
|
890
|
+
},
|
|
891
|
+
}),
|
|
892
|
+
|
|
893
|
+
searchRoadmap: queryGeneric({
|
|
894
|
+
args: { searchQuery: v.string(), limit: v.optional(v.number()) },
|
|
895
|
+
returns: v.array(roadmapItemValidator),
|
|
896
|
+
handler: async (ctx, args) => {
|
|
897
|
+
return await ctx.runQuery(component.roadmap.search, {
|
|
898
|
+
searchQuery: args.searchQuery,
|
|
899
|
+
limit: clampPositive(args.limit, 10, 50),
|
|
900
|
+
});
|
|
901
|
+
},
|
|
902
|
+
}),
|
|
903
|
+
|
|
904
|
+
createRoadmap: mutationGeneric({
|
|
905
|
+
args: {
|
|
906
|
+
title: v.string(),
|
|
907
|
+
description: v.optional(v.string()),
|
|
908
|
+
status: roadmapStatusValidator,
|
|
909
|
+
},
|
|
910
|
+
returns: idReturns,
|
|
911
|
+
handler: async (ctx, args) => {
|
|
912
|
+
const actor = await requireAdminActor(ctx);
|
|
913
|
+
const limited = await applyAdminEditLimit(
|
|
914
|
+
asRateLimitContext(ctx),
|
|
915
|
+
actor,
|
|
916
|
+
);
|
|
917
|
+
if (limited !== undefined) return limited;
|
|
918
|
+
return await ctx.runMutation(component.roadmap.create, {
|
|
919
|
+
actor,
|
|
920
|
+
...args,
|
|
921
|
+
});
|
|
922
|
+
},
|
|
923
|
+
}),
|
|
924
|
+
|
|
925
|
+
createRoadmapForEntry: mutationGeneric({
|
|
926
|
+
args: {
|
|
927
|
+
title: v.string(),
|
|
928
|
+
description: v.optional(v.string()),
|
|
929
|
+
status: roadmapStatusValidator,
|
|
930
|
+
entryId: v.string(),
|
|
931
|
+
},
|
|
932
|
+
returns: idReturns,
|
|
933
|
+
handler: async (ctx, args) => {
|
|
934
|
+
const actor = await requireAdminActor(ctx);
|
|
935
|
+
const limited = await applyAdminEditLimit(
|
|
936
|
+
asRateLimitContext(ctx),
|
|
937
|
+
actor,
|
|
938
|
+
);
|
|
939
|
+
if (limited !== undefined) return limited;
|
|
940
|
+
return await ctx.runMutation(component.roadmap.createForEntry, {
|
|
941
|
+
actor,
|
|
942
|
+
...args,
|
|
943
|
+
});
|
|
944
|
+
},
|
|
945
|
+
}),
|
|
946
|
+
|
|
947
|
+
updateRoadmap: mutationGeneric({
|
|
948
|
+
args: {
|
|
949
|
+
roadmapId: v.string(),
|
|
950
|
+
title: v.string(),
|
|
951
|
+
description: v.optional(v.string()),
|
|
952
|
+
},
|
|
953
|
+
returns: nullReturns,
|
|
954
|
+
handler: async (ctx, args) => {
|
|
955
|
+
const actor = await requireAdminActor(ctx);
|
|
956
|
+
const limited = await applyAdminEditLimit(
|
|
957
|
+
asRateLimitContext(ctx),
|
|
958
|
+
actor,
|
|
959
|
+
);
|
|
960
|
+
if (limited !== undefined) return limited;
|
|
961
|
+
return await ctx.runMutation(component.roadmap.update, {
|
|
962
|
+
actor,
|
|
963
|
+
...args,
|
|
964
|
+
});
|
|
965
|
+
},
|
|
966
|
+
}),
|
|
967
|
+
|
|
968
|
+
deleteRoadmap: mutationGeneric({
|
|
969
|
+
args: { roadmapId: v.string() },
|
|
970
|
+
returns: nullReturns,
|
|
971
|
+
handler: async (ctx, args) => {
|
|
972
|
+
const actor = await requireAdminActor(ctx);
|
|
973
|
+
const limited = await applyAdminEditLimit(
|
|
974
|
+
asRateLimitContext(ctx),
|
|
975
|
+
actor,
|
|
976
|
+
);
|
|
977
|
+
if (limited !== undefined) return limited;
|
|
978
|
+
return await ctx.runMutation(component.roadmap.remove, {
|
|
979
|
+
actor,
|
|
980
|
+
...args,
|
|
981
|
+
});
|
|
982
|
+
},
|
|
983
|
+
}),
|
|
984
|
+
|
|
985
|
+
moveRoadmapItem: mutationGeneric({
|
|
986
|
+
args: {
|
|
987
|
+
roadmapId: v.string(),
|
|
988
|
+
status: roadmapStatusValidator,
|
|
989
|
+
previousItemId: v.optional(v.string()),
|
|
990
|
+
nextItemId: v.optional(v.string()),
|
|
991
|
+
},
|
|
992
|
+
returns: numberReturns,
|
|
993
|
+
handler: async (ctx, args) => {
|
|
994
|
+
const actor = await requireAdminActor(ctx);
|
|
995
|
+
const limited = await applyAdminEditLimit(
|
|
996
|
+
asRateLimitContext(ctx),
|
|
997
|
+
actor,
|
|
998
|
+
);
|
|
999
|
+
if (limited !== undefined) return limited;
|
|
1000
|
+
return await ctx.runMutation(component.roadmap.move, {
|
|
1001
|
+
actor,
|
|
1002
|
+
...args,
|
|
1003
|
+
});
|
|
1004
|
+
},
|
|
1005
|
+
}),
|
|
1006
|
+
|
|
1007
|
+
attachFeedbackToRoadmap: mutationGeneric({
|
|
1008
|
+
args: { roadmapId: v.string(), entryId: v.string() },
|
|
1009
|
+
returns: nullReturns,
|
|
1010
|
+
handler: async (ctx, args) => {
|
|
1011
|
+
const actor = await requireAdminActor(ctx);
|
|
1012
|
+
const limited = await applyAdminEditLimit(
|
|
1013
|
+
asRateLimitContext(ctx),
|
|
1014
|
+
actor,
|
|
1015
|
+
);
|
|
1016
|
+
if (limited !== undefined) return limited;
|
|
1017
|
+
return await ctx.runMutation(component.roadmap.attachFeedback, {
|
|
1018
|
+
actor,
|
|
1019
|
+
...args,
|
|
1020
|
+
});
|
|
1021
|
+
},
|
|
1022
|
+
}),
|
|
1023
|
+
|
|
1024
|
+
detachFeedbackFromRoadmap: mutationGeneric({
|
|
1025
|
+
args: { entryId: v.string() },
|
|
1026
|
+
returns: nullReturns,
|
|
1027
|
+
handler: async (ctx, args) => {
|
|
1028
|
+
const actor = await requireAdminActor(ctx);
|
|
1029
|
+
const limited = await applyAdminEditLimit(
|
|
1030
|
+
asRateLimitContext(ctx),
|
|
1031
|
+
actor,
|
|
1032
|
+
);
|
|
1033
|
+
if (limited !== undefined) return limited;
|
|
1034
|
+
return await ctx.runMutation(component.roadmap.detachFeedback, {
|
|
1035
|
+
actor,
|
|
1036
|
+
...args,
|
|
1037
|
+
});
|
|
1038
|
+
},
|
|
1039
|
+
}),
|
|
1040
|
+
|
|
1041
|
+
listRoadmapFeedback: queryGeneric({
|
|
1042
|
+
args: {
|
|
1043
|
+
paginationOpts: paginationOptsValidator,
|
|
1044
|
+
roadmapId: v.string(),
|
|
1045
|
+
},
|
|
1046
|
+
returns: paginationResultValidator(publicEntryValidator),
|
|
1047
|
+
handler: async (ctx, args) => {
|
|
1048
|
+
const actor = await options.actor(ctx);
|
|
1049
|
+
return await ctx.runQuery(component.roadmap.listFeedback, {
|
|
1050
|
+
...args,
|
|
1051
|
+
paginationOpts: clampPagination(
|
|
1052
|
+
args.paginationOpts,
|
|
1053
|
+
config.entries.maxPageSize,
|
|
1054
|
+
),
|
|
1055
|
+
...(actor === null ? {} : { viewerActorId: actor.id }),
|
|
1056
|
+
});
|
|
1057
|
+
},
|
|
1058
|
+
}),
|
|
742
1059
|
} satisfies Record<keyof FeedbackPublicApi, unknown>;
|
|
743
1060
|
}
|
|
744
1061
|
|
package/src/component/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
8
|
- `migrations.ts`: bounded, idempotent legacy-entry backfills.
|
|
@@ -8,12 +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";
|
|
12
13
|
import type * as crons from "../crons.js";
|
|
13
14
|
import type * as entries from "../entries.js";
|
|
14
15
|
import type * as helpers from "../helpers.js";
|
|
15
16
|
import type * as migrations from "../migrations.js";
|
|
16
17
|
import type * as model from "../model.js";
|
|
18
|
+
import type * as roadmap from "../roadmap.js";
|
|
17
19
|
import type * as types from "../types.js";
|
|
18
20
|
|
|
19
21
|
import type {
|
|
@@ -24,12 +26,14 @@ import type {
|
|
|
24
26
|
import { anyApi, componentsGeneric } from "convex/server";
|
|
25
27
|
|
|
26
28
|
const fullApi: ApiFromModules<{
|
|
29
|
+
admin: typeof admin;
|
|
27
30
|
comments: typeof comments;
|
|
28
31
|
crons: typeof crons;
|
|
29
32
|
entries: typeof entries;
|
|
30
33
|
helpers: typeof helpers;
|
|
31
34
|
migrations: typeof migrations;
|
|
32
35
|
model: typeof model;
|
|
36
|
+
roadmap: typeof roadmap;
|
|
33
37
|
types: typeof types;
|
|
34
38
|
}> = anyApi as any;
|
|
35
39
|
|