convex-feedback 0.2.0-beta.8 → 0.2.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 +398 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +49 -9
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/config.d.ts +1 -3
- package/dist/client/config.d.ts.map +1 -1
- package/dist/client/config.js.map +1 -1
- package/dist/client/index.d.ts +707 -15
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +243 -12
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -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 +222 -4
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/admin.d.ts +5 -90
- package/dist/component/admin.d.ts.map +1 -1
- package/dist/component/admin.js +59 -29
- package/dist/component/admin.js.map +1 -1
- package/dist/component/comments.d.ts +107 -15
- package/dist/component/comments.d.ts.map +1 -1
- package/dist/component/comments.js +342 -28
- package/dist/component/comments.js.map +1 -1
- package/dist/component/crons.d.ts.map +1 -1
- package/dist/component/crons.js +4 -0
- package/dist/component/crons.js.map +1 -1
- package/dist/component/entries.d.ts +119 -91
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +348 -29
- package/dist/component/entries.js.map +1 -1
- package/dist/component/helpers.d.ts +32 -1
- package/dist/component/helpers.d.ts.map +1 -1
- package/dist/component/helpers.js +121 -4
- package/dist/component/helpers.js.map +1 -1
- package/dist/component/migrations.d.ts +24 -0
- package/dist/component/migrations.d.ts.map +1 -1
- package/dist/component/migrations.js +318 -0
- package/dist/component/migrations.js.map +1 -1
- package/dist/component/model.d.ts +455 -120
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/model.js +70 -2
- package/dist/component/model.js.map +1 -1
- package/dist/component/reactions.d.ts +37 -0
- package/dist/component/reactions.d.ts.map +1 -0
- package/dist/component/reactions.js +52 -0
- package/dist/component/reactions.js.map +1 -0
- package/dist/component/roadmap.d.ts +10 -46
- package/dist/component/roadmap.d.ts.map +1 -1
- package/dist/component/roadmap.js +239 -21
- package/dist/component/roadmap.js.map +1 -1
- package/dist/component/schema.d.ts +33 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +40 -4
- package/dist/component/schema.js.map +1 -1
- package/dist/react/index.d.ts +26 -238
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +65 -2
- package/dist/react/index.js.map +1 -1
- package/dist/test.d.ts +23 -5
- package/dist/test.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/README.md +6 -1
- package/src/client/api.ts +89 -7
- package/src/client/config.ts +1 -3
- package/src/client/index.ts +1237 -34
- package/src/component/README.md +32 -4
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +290 -8
- package/src/component/admin.ts +88 -30
- package/src/component/comments.ts +405 -26
- package/src/component/crons.ts +10 -0
- package/src/component/entries.ts +388 -27
- package/src/component/helpers.ts +163 -4
- package/src/component/migrations.ts +415 -0
- package/src/component/model.ts +381 -117
- package/src/component/reactions.ts +60 -0
- package/src/component/roadmap.ts +357 -35
- package/src/component/schema.ts +40 -4
- package/src/react/README.md +5 -0
- package/src/react/index.ts +90 -3
package/src/client/index.ts
CHANGED
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
} from "convex/values";
|
|
23
23
|
|
|
24
24
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
25
|
+
import { stripActivityEntryContext } from "../component/helpers.js";
|
|
25
26
|
import {
|
|
27
|
+
activityCommentValidator,
|
|
28
|
+
activityEntryValidator,
|
|
29
|
+
actorIsAdmin,
|
|
26
30
|
adminEntryValidator,
|
|
27
31
|
commentSortValidator,
|
|
28
32
|
entryKindValidator,
|
|
@@ -31,13 +35,16 @@ import {
|
|
|
31
35
|
entryStatusFilterValidator,
|
|
32
36
|
entryStatusValidator,
|
|
33
37
|
feedbackMetadataValidator,
|
|
38
|
+
feedbackReactionValidator,
|
|
34
39
|
publicCommentValidator,
|
|
35
40
|
publicEntryValidator,
|
|
36
41
|
roadmapItemValidator,
|
|
37
42
|
roadmapStatusValidator,
|
|
38
43
|
similarEntriesValidator,
|
|
39
|
-
|
|
44
|
+
type EntryKind,
|
|
45
|
+
type EntryStatus,
|
|
40
46
|
type FeedbackActor,
|
|
47
|
+
type FeedbackMetadata,
|
|
41
48
|
} from "../component/model.js";
|
|
42
49
|
import type { FeedbackPublicApi } from "./api.js";
|
|
43
50
|
import {
|
|
@@ -53,16 +60,48 @@ export type {
|
|
|
53
60
|
EntrySort,
|
|
54
61
|
EntryStatus,
|
|
55
62
|
EntryStatusFilter,
|
|
63
|
+
FeedbackActivityComment,
|
|
64
|
+
FeedbackActivityEntry,
|
|
65
|
+
FeedbackActivityEntryWithContext,
|
|
56
66
|
FeedbackActor,
|
|
57
67
|
FeedbackComment,
|
|
68
|
+
FeedbackCommentReactionTarget,
|
|
58
69
|
FeedbackEntry,
|
|
70
|
+
FeedbackEntryReactionTarget,
|
|
59
71
|
FeedbackMetadata,
|
|
60
72
|
FeedbackMetadataValue,
|
|
73
|
+
FeedbackReaction,
|
|
61
74
|
RoadmapItem,
|
|
62
75
|
RoadmapStatus,
|
|
63
76
|
SimilarEntriesResult,
|
|
64
77
|
} from "../component/model.js";
|
|
65
|
-
export type {
|
|
78
|
+
export type {
|
|
79
|
+
AdminListEntriesArgs,
|
|
80
|
+
AdminSearchEntriesArgs,
|
|
81
|
+
CreateCommentArgs,
|
|
82
|
+
CreateEntryArgs,
|
|
83
|
+
DeleteCommentArgs,
|
|
84
|
+
DeleteEntryArgs,
|
|
85
|
+
DetachFeedbackFromRoadmapArgs,
|
|
86
|
+
FeedbackPublicApi,
|
|
87
|
+
FindSimilarEntriesArgs,
|
|
88
|
+
GetEntryArgs,
|
|
89
|
+
GetRoadmapItemArgs,
|
|
90
|
+
ListCommentsArgs,
|
|
91
|
+
ListEntriesArgs,
|
|
92
|
+
ListRoadmapArgs,
|
|
93
|
+
ListRoadmapFeedbackArgs,
|
|
94
|
+
ListUserCommentsArgs,
|
|
95
|
+
ListUserEntriesArgs,
|
|
96
|
+
ListUserReactionsArgs,
|
|
97
|
+
SearchEntriesArgs,
|
|
98
|
+
SetCommentLikeArgs,
|
|
99
|
+
SetEntryPriorityArgs,
|
|
100
|
+
SetEntryStatusArgs,
|
|
101
|
+
SetEntryUpvoteArgs,
|
|
102
|
+
UpdateCommentArgs,
|
|
103
|
+
UpdateEntryArgs,
|
|
104
|
+
} from "./api.js";
|
|
66
105
|
export {
|
|
67
106
|
createFeedbackConfig,
|
|
68
107
|
defaultFeedbackConfig,
|
|
@@ -219,6 +258,742 @@ type RateLimiterResult<
|
|
|
219
258
|
? Infer<ReturnsValidator>
|
|
220
259
|
: void;
|
|
221
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Full host mutation context supplied to every lifecycle callback.
|
|
263
|
+
*
|
|
264
|
+
* This is the host mutation context for the request that triggered the
|
|
265
|
+
* callback. It includes the host database, authentication, storage,
|
|
266
|
+
* scheduler, and nested function-call helpers, so callbacks can apply host
|
|
267
|
+
* business rules or schedule follow-up work.
|
|
268
|
+
*
|
|
269
|
+
* When a callback needs to read or write feedback data, prefer a direct
|
|
270
|
+
* component reference through `ctx.runQuery(components.feedback....)` or
|
|
271
|
+
* `ctx.runMutation(components.feedback....)`. Calling an exposed host API
|
|
272
|
+
* through `api.feedback...` is also valid, but it re-enters the host wrapper
|
|
273
|
+
* and its actor/auth-resolution path. Use the host API when those host-facing
|
|
274
|
+
* semantics are specifically desired.
|
|
275
|
+
*
|
|
276
|
+
* @example Query component data from a callback
|
|
277
|
+
* ```ts
|
|
278
|
+
* import { components } from "./_generated/api";
|
|
279
|
+
*
|
|
280
|
+
* const entry = await ctx.runQuery(components.feedback.entries.get, {
|
|
281
|
+
* entryId: event.entryId,
|
|
282
|
+
* });
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
export type FeedbackMutationContext = GenericMutationCtx<GenericDataModel>;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Readonly entry creation event passed after auth/rate limiting and before any
|
|
289
|
+
* component work. Return a {@link FeedbackEntryCreatePatch} to transform only
|
|
290
|
+
* the supported fields; mutating `event.input` is neither supported nor used.
|
|
291
|
+
*/
|
|
292
|
+
export interface FeedbackEntryBeforeCreateEvent {
|
|
293
|
+
/** Actor resolved by the host for the request being created. */
|
|
294
|
+
readonly actor: Readonly<FeedbackActor>;
|
|
295
|
+
|
|
296
|
+
/** Readonly snapshot of the caller's validated mutation input. */
|
|
297
|
+
readonly input: {
|
|
298
|
+
/** Requested entry category. */
|
|
299
|
+
readonly kind: EntryKind;
|
|
300
|
+
|
|
301
|
+
/** Title submitted by the caller, before component normalization. */
|
|
302
|
+
readonly title: string;
|
|
303
|
+
|
|
304
|
+
/** Body submitted by the caller, before component normalization. */
|
|
305
|
+
readonly body: string;
|
|
306
|
+
|
|
307
|
+
/** Optional diagnostic metadata submitted with the entry. */
|
|
308
|
+
readonly metadata?: Readonly<{
|
|
309
|
+
/** Standard metadata collected by the host or UI. */
|
|
310
|
+
standard?: Readonly<Record<string, string | number | boolean>>;
|
|
311
|
+
|
|
312
|
+
/** Additional host-defined metadata. */
|
|
313
|
+
additional?: Readonly<Record<string, string | number | boolean>>;
|
|
314
|
+
}>;
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Explicit entry creation transformations. Omitted fields retain the original
|
|
320
|
+
* mutation arguments, and all returned values still undergo component
|
|
321
|
+
* validation. Actor identity, IDs, and configured defaults cannot be changed.
|
|
322
|
+
*/
|
|
323
|
+
export interface FeedbackEntryCreatePatch {
|
|
324
|
+
/** Replacement entry category. */
|
|
325
|
+
kind?: EntryKind;
|
|
326
|
+
|
|
327
|
+
/** Replacement title. */
|
|
328
|
+
title?: string;
|
|
329
|
+
|
|
330
|
+
/** Replacement body. */
|
|
331
|
+
body?: string;
|
|
332
|
+
|
|
333
|
+
/** Replacement diagnostic metadata. */
|
|
334
|
+
metadata?: FeedbackMetadata;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Readonly comment creation event passed after auth/rate limiting and before
|
|
339
|
+
* component work. `entryId` and `parentCommentId` are immutable relationship
|
|
340
|
+
* fields; only an explicitly returned `body` patch is applied.
|
|
341
|
+
*/
|
|
342
|
+
export interface FeedbackCommentBeforeCreateEvent {
|
|
343
|
+
/** Actor resolved by the host for the request being created. */
|
|
344
|
+
readonly actor: Readonly<FeedbackActor>;
|
|
345
|
+
|
|
346
|
+
/** Readonly snapshot of the caller's validated mutation input. */
|
|
347
|
+
readonly input: {
|
|
348
|
+
/** ID of the entry that will own the comment. */
|
|
349
|
+
readonly entryId: string;
|
|
350
|
+
|
|
351
|
+
/** ID of the parent comment when this input creates a reply. */
|
|
352
|
+
readonly parentCommentId?: string;
|
|
353
|
+
|
|
354
|
+
/** Comment body submitted by the caller, before normalization. */
|
|
355
|
+
readonly body: string;
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Explicit comment creation transformation. Only `body` is supported and the
|
|
361
|
+
* transformed value still undergoes length and permission validation.
|
|
362
|
+
*/
|
|
363
|
+
export interface FeedbackCommentCreatePatch {
|
|
364
|
+
/** Replacement comment body. */
|
|
365
|
+
body?: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Helpers supplied to before-create callbacks. `reject(value)` is the only
|
|
370
|
+
* exception translated by callback rejection configuration: it throws a
|
|
371
|
+
* `ConvexError` by default or returns the validated value in return mode.
|
|
372
|
+
* Unexpected callback errors always propagate normally.
|
|
373
|
+
* The third callback parameter is commonly named `handlers`.
|
|
374
|
+
*
|
|
375
|
+
* @typeParam Rejection Value accepted by `reject`. In return mode this is
|
|
376
|
+
* inferred from `callbacks.rejection.returns`.
|
|
377
|
+
*/
|
|
378
|
+
export interface FeedbackCallbackHelpers<Rejection = Value> {
|
|
379
|
+
/**
|
|
380
|
+
* Reject the pending entry or comment creation.
|
|
381
|
+
*
|
|
382
|
+
* The call never returns: it throws in the default mode or short-circuits
|
|
383
|
+
* with the validated rejection value in return mode.
|
|
384
|
+
*/
|
|
385
|
+
reject: (value: Rejection) => never;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
type MaybePromise<ValueType> = ValueType | Promise<ValueType>;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Runs after actor resolution and rate limiting, before the component entry
|
|
392
|
+
* mutation. It is awaited and may return an explicit creation patch or call
|
|
393
|
+
* `reject()`; it cannot replace normal component validation.
|
|
394
|
+
*
|
|
395
|
+
* @typeParam Rejection Value accepted by `handlers.reject`.
|
|
396
|
+
* @param ctx Host mutation context for the originating request. Use direct
|
|
397
|
+
* component references with `ctx.runQuery`/`ctx.runMutation` for feedback data.
|
|
398
|
+
* @param event Readonly event containing `actor` and `input` (`kind`, `title`,
|
|
399
|
+
* `body`, and optional `metadata`) for this request.
|
|
400
|
+
* @param handlers Callback handlers. Call `handlers.reject(value)` to reject
|
|
401
|
+
* creation.
|
|
402
|
+
* @returns A supported entry patch, or `undefined` to keep the original input.
|
|
403
|
+
*
|
|
404
|
+
* @example Validate and reject an entry before creation
|
|
405
|
+
* ```ts
|
|
406
|
+
* const beforeCreate: FeedbackEntryBeforeCreateCallback = (
|
|
407
|
+
* ctx,
|
|
408
|
+
* event,
|
|
409
|
+
* handlers,
|
|
410
|
+
* ) => {
|
|
411
|
+
* void ctx;
|
|
412
|
+
* if (event.input.title.trim() === "") {
|
|
413
|
+
* handlers.reject("An entry title is required");
|
|
414
|
+
* }
|
|
415
|
+
* };
|
|
416
|
+
* ```
|
|
417
|
+
*
|
|
418
|
+
* @example No-op entry before-create hook
|
|
419
|
+
* ```ts
|
|
420
|
+
* const beforeCreate: FeedbackEntryBeforeCreateCallback = (
|
|
421
|
+
* ctx,
|
|
422
|
+
* event,
|
|
423
|
+
* handlers,
|
|
424
|
+
* ) => {
|
|
425
|
+
* void ctx;
|
|
426
|
+
* void event;
|
|
427
|
+
* void handlers;
|
|
428
|
+
* };
|
|
429
|
+
* ```
|
|
430
|
+
*/
|
|
431
|
+
export type FeedbackEntryBeforeCreateCallback<Rejection = Value> = (
|
|
432
|
+
ctx: FeedbackMutationContext,
|
|
433
|
+
event: FeedbackEntryBeforeCreateEvent,
|
|
434
|
+
handlers: FeedbackCallbackHelpers<Rejection>,
|
|
435
|
+
) => MaybePromise<FeedbackEntryCreatePatch | undefined>;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Sanitized persisted entry passed to `entries.afterCreate`.
|
|
439
|
+
*
|
|
440
|
+
* The entry is the authoritative component result after creation and
|
|
441
|
+
* normalization. It is available without another component query.
|
|
442
|
+
*/
|
|
443
|
+
export interface FeedbackEntryAfterCreateEvent {
|
|
444
|
+
/** Actor resolved by the host for the creation request. */
|
|
445
|
+
readonly actor: FeedbackActor;
|
|
446
|
+
|
|
447
|
+
/** Persisted entry created by the component. */
|
|
448
|
+
readonly entry: {
|
|
449
|
+
/** Public component identifier for the created entry. */
|
|
450
|
+
readonly id: string;
|
|
451
|
+
|
|
452
|
+
/** Stable identifier of the actor who created the entry. */
|
|
453
|
+
readonly actorId: string;
|
|
454
|
+
|
|
455
|
+
/** Persisted entry category. */
|
|
456
|
+
readonly kind: EntryKind;
|
|
457
|
+
|
|
458
|
+
/** Initial workflow status assigned by the component. */
|
|
459
|
+
readonly status: EntryStatus;
|
|
460
|
+
|
|
461
|
+
/** Normalized persisted title. */
|
|
462
|
+
readonly title: string;
|
|
463
|
+
|
|
464
|
+
/** Normalized persisted body. */
|
|
465
|
+
readonly body: string;
|
|
466
|
+
|
|
467
|
+
/** Persisted diagnostic metadata, when supplied. */
|
|
468
|
+
readonly metadata?: FeedbackMetadata;
|
|
469
|
+
|
|
470
|
+
/** Authoritative number of upvotes immediately after creation. */
|
|
471
|
+
readonly upvoteCount: number;
|
|
472
|
+
|
|
473
|
+
/** Authoritative number of comments immediately after creation. */
|
|
474
|
+
readonly commentCount: number;
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Runs exactly once after successful component entry creation and is awaited
|
|
480
|
+
* in the same host mutation. An uncaught error rolls back creation. Rich entry
|
|
481
|
+
* context is requested from the component only when this callback is set.
|
|
482
|
+
*
|
|
483
|
+
* @param ctx Host mutation context for the originating request.
|
|
484
|
+
* @param event Persisted event containing `actor` and the created `entry`
|
|
485
|
+
* (`id`, `actorId`, `kind`, `status`, `title`, `body`, metadata, and counts).
|
|
486
|
+
* @returns Nothing. The callback may be synchronous or asynchronous.
|
|
487
|
+
*
|
|
488
|
+
* @example No-op entry after-create hook
|
|
489
|
+
* ```ts
|
|
490
|
+
* const afterCreate: FeedbackEntryAfterCreateCallback = async (ctx, event) => {
|
|
491
|
+
* void ctx;
|
|
492
|
+
* void event;
|
|
493
|
+
* };
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
export type FeedbackEntryAfterCreateCallback = (
|
|
497
|
+
ctx: FeedbackMutationContext,
|
|
498
|
+
event: FeedbackEntryAfterCreateEvent,
|
|
499
|
+
) => MaybePromise<void>;
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Runs after actor resolution and rate limiting, before the component comment
|
|
503
|
+
* mutation. It is awaited and may transform only `body` or call `reject()`.
|
|
504
|
+
*
|
|
505
|
+
* @typeParam Rejection Value accepted by `handlers.reject`.
|
|
506
|
+
* @param ctx Host mutation context for the originating request.
|
|
507
|
+
* @param event Readonly event containing `actor` and `input` (`entryId`,
|
|
508
|
+
* optional `parentCommentId`, and `body`) for this request.
|
|
509
|
+
* @param handlers Callback handlers. Call `handlers.reject(value)` to reject
|
|
510
|
+
* creation.
|
|
511
|
+
* @returns A supported comment patch, or `undefined` to keep the original
|
|
512
|
+
* input.
|
|
513
|
+
*
|
|
514
|
+
* @example No-op comment before-create hook
|
|
515
|
+
* ```ts
|
|
516
|
+
* const beforeCreate: FeedbackCommentBeforeCreateCallback = (
|
|
517
|
+
* ctx,
|
|
518
|
+
* event,
|
|
519
|
+
* handlers,
|
|
520
|
+
* ) => {
|
|
521
|
+
* void ctx;
|
|
522
|
+
* void event;
|
|
523
|
+
* void handlers;
|
|
524
|
+
* };
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
export type FeedbackCommentBeforeCreateCallback<Rejection = Value> = (
|
|
528
|
+
ctx: FeedbackMutationContext,
|
|
529
|
+
event: FeedbackCommentBeforeCreateEvent,
|
|
530
|
+
handlers: FeedbackCallbackHelpers<Rejection>,
|
|
531
|
+
) => MaybePromise<FeedbackCommentCreatePatch | undefined>;
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Persisted comment, entry, and optional parent context passed after creation.
|
|
535
|
+
*
|
|
536
|
+
* All IDs and content in this event come from the successful component write;
|
|
537
|
+
* the optional `parentComment` is present only when the created comment is a
|
|
538
|
+
* reply.
|
|
539
|
+
*/
|
|
540
|
+
export interface FeedbackCommentAfterCreateEvent {
|
|
541
|
+
/** Actor resolved by the host for the creation request. */
|
|
542
|
+
readonly actor: FeedbackActor;
|
|
543
|
+
|
|
544
|
+
/** Persisted comment created by the component. */
|
|
545
|
+
readonly comment: {
|
|
546
|
+
/** Public component identifier for the created comment. */
|
|
547
|
+
readonly id: string;
|
|
548
|
+
|
|
549
|
+
/** Stable identifier of the actor who created the comment. */
|
|
550
|
+
readonly actorId: string;
|
|
551
|
+
|
|
552
|
+
/** ID of the entry containing the comment. */
|
|
553
|
+
readonly entryId: string;
|
|
554
|
+
|
|
555
|
+
/** ID of the parent comment when this comment is a reply. */
|
|
556
|
+
readonly parentCommentId?: string;
|
|
557
|
+
|
|
558
|
+
/** Normalized persisted comment body. */
|
|
559
|
+
readonly body: string;
|
|
560
|
+
|
|
561
|
+
/** Nesting depth assigned by the component. */
|
|
562
|
+
readonly depth: number;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/** Persisted entry containing the created comment. */
|
|
566
|
+
readonly entry: {
|
|
567
|
+
/** Public component identifier for the containing entry. */
|
|
568
|
+
readonly id: string;
|
|
569
|
+
|
|
570
|
+
/** Stable identifier of the entry author. */
|
|
571
|
+
readonly actorId: string;
|
|
572
|
+
|
|
573
|
+
/** Entry category. */
|
|
574
|
+
readonly kind: EntryKind;
|
|
575
|
+
|
|
576
|
+
/** Current workflow status of the entry. */
|
|
577
|
+
readonly status: EntryStatus;
|
|
578
|
+
|
|
579
|
+
/** Entry title. */
|
|
580
|
+
readonly title: string;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Minimal persisted parent-comment context, present only for replies.
|
|
585
|
+
*/
|
|
586
|
+
readonly parentComment?: {
|
|
587
|
+
/** Public component identifier for the parent comment. */
|
|
588
|
+
readonly id: string;
|
|
589
|
+
|
|
590
|
+
/** Stable identifier of the parent comment's author. */
|
|
591
|
+
readonly actorId: string;
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Runs exactly once after successful component comment creation and is awaited
|
|
597
|
+
* in the same host mutation. An uncaught error rolls back creation. Rich
|
|
598
|
+
* comment/entry/parent context is requested only when this callback is set.
|
|
599
|
+
*
|
|
600
|
+
* @param ctx Host mutation context for the originating request.
|
|
601
|
+
* @param event Persisted event containing `actor`, `comment`, `entry`, and
|
|
602
|
+
* optional `parentComment` context returned by the component.
|
|
603
|
+
* @returns Nothing. The callback may be synchronous or asynchronous.
|
|
604
|
+
*
|
|
605
|
+
* @example React to a created comment
|
|
606
|
+
* ```ts
|
|
607
|
+
* const afterCreate: FeedbackCommentAfterCreateCallback = async (
|
|
608
|
+
* ctx,
|
|
609
|
+
* event,
|
|
610
|
+
* ) => {
|
|
611
|
+
* await ctx.scheduler.runAfter(0, internal.notifications.commentCreated, {
|
|
612
|
+
* commentId: event.comment.id,
|
|
613
|
+
* entryId: event.entry.id,
|
|
614
|
+
* });
|
|
615
|
+
* };
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
618
|
+
export type FeedbackCommentAfterCreateCallback = (
|
|
619
|
+
ctx: FeedbackMutationContext,
|
|
620
|
+
event: FeedbackCommentAfterCreateEvent,
|
|
621
|
+
) => MaybePromise<void>;
|
|
622
|
+
|
|
623
|
+
interface FeedbackReactionChangeBase {
|
|
624
|
+
/** Whether the actor's reaction was added or removed. */
|
|
625
|
+
transition: "added" | "removed";
|
|
626
|
+
|
|
627
|
+
/** Whether the actor's reaction is active after the transition. */
|
|
628
|
+
active: boolean;
|
|
629
|
+
|
|
630
|
+
/** Reaction count immediately before the transition. */
|
|
631
|
+
previousCount: number;
|
|
632
|
+
|
|
633
|
+
/** Authoritative reaction count immediately after the transition. */
|
|
634
|
+
count: number;
|
|
635
|
+
|
|
636
|
+
/** Actor who requested the reaction change. */
|
|
637
|
+
actor: FeedbackActor;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Discriminated reaction transition. Events exist only for real state changes:
|
|
642
|
+
* `added` is false→true and `removed` is true→false. `previousCount` is the
|
|
643
|
+
* persisted count immediately before the change and `count` is the final one.
|
|
644
|
+
* Comment-like events intentionally use comment context without reading or
|
|
645
|
+
* serializing the parent entry. The top-level target IDs are convenience
|
|
646
|
+
* aliases for the IDs in the nested target context and always match them.
|
|
647
|
+
*/
|
|
648
|
+
export type FeedbackReactionChangeEvent =
|
|
649
|
+
| (FeedbackReactionChangeBase & {
|
|
650
|
+
/** Discriminator for an entry-upvote transition. */
|
|
651
|
+
type: "entry_upvote";
|
|
652
|
+
/** ID of the upvoted entry; always equal to `entry.id`. */
|
|
653
|
+
entryId: string;
|
|
654
|
+
|
|
655
|
+
/** Persisted context for the upvoted entry. */
|
|
656
|
+
entry: {
|
|
657
|
+
/** Public component identifier for the upvoted entry. */
|
|
658
|
+
id: string;
|
|
659
|
+
|
|
660
|
+
/** Stable identifier of the entry author. */
|
|
661
|
+
actorId: string;
|
|
662
|
+
|
|
663
|
+
/** Entry category. */
|
|
664
|
+
kind: EntryKind;
|
|
665
|
+
|
|
666
|
+
/** Current workflow status of the entry. */
|
|
667
|
+
status: EntryStatus;
|
|
668
|
+
|
|
669
|
+
/** Entry title. */
|
|
670
|
+
title: string;
|
|
671
|
+
};
|
|
672
|
+
})
|
|
673
|
+
| (FeedbackReactionChangeBase & {
|
|
674
|
+
/** Discriminator for a comment-like transition. */
|
|
675
|
+
type: "comment_like";
|
|
676
|
+
/** ID of the liked comment; always equal to `comment.id`. */
|
|
677
|
+
commentId: string;
|
|
678
|
+
|
|
679
|
+
/** ID of the entry containing the liked comment. */
|
|
680
|
+
entryId: string;
|
|
681
|
+
|
|
682
|
+
/** Persisted context for the liked comment. */
|
|
683
|
+
comment: {
|
|
684
|
+
/** Public component identifier for the liked comment. */
|
|
685
|
+
id: string;
|
|
686
|
+
|
|
687
|
+
/** Stable identifier of the comment author. */
|
|
688
|
+
actorId: string;
|
|
689
|
+
|
|
690
|
+
/** ID of the entry containing the comment. */
|
|
691
|
+
entryId: string;
|
|
692
|
+
|
|
693
|
+
/** ID of the parent comment when the target is a reply. */
|
|
694
|
+
parentCommentId?: string;
|
|
695
|
+
|
|
696
|
+
/** Persisted comment body. */
|
|
697
|
+
body: string;
|
|
698
|
+
};
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Runs after a successful reaction mutation only for an actual transition and
|
|
703
|
+
* is awaited in the same transaction, so uncaught errors roll back the change.
|
|
704
|
+
* Extra reaction context is requested only when this callback is configured.
|
|
705
|
+
*
|
|
706
|
+
* @param ctx Host mutation context for the originating request. For feedback
|
|
707
|
+
* reads, prefer direct component references through `ctx.runQuery` or
|
|
708
|
+
* `ctx.runMutation`.
|
|
709
|
+
* @param event The authoritative event containing `type`, `transition`,
|
|
710
|
+
* `active`, `previousCount`, `count`, `actor`, and the target IDs/context.
|
|
711
|
+
* Use `event.transition` to distinguish additions from removals and the
|
|
712
|
+
* top-level target IDs for convenient lookups.
|
|
713
|
+
* @returns Nothing. The callback may be synchronous or asynchronous.
|
|
714
|
+
*
|
|
715
|
+
* @example Handle a reaction transition and query component data
|
|
716
|
+
* ```ts
|
|
717
|
+
* import { components } from "./_generated/api";
|
|
718
|
+
*
|
|
719
|
+
* const afterChange: FeedbackReactionAfterChangeCallback = async (ctx, event) => {
|
|
720
|
+
* if (event.transition !== "added") return;
|
|
721
|
+
*
|
|
722
|
+
* const entry = await ctx.runQuery(components.feedback.entries.get, {
|
|
723
|
+
* entryId: event.entryId,
|
|
724
|
+
* });
|
|
725
|
+
* if (entry === null) return;
|
|
726
|
+
*
|
|
727
|
+
* const targetId =
|
|
728
|
+
* event.type === "entry_upvote" ? event.entryId : event.commentId;
|
|
729
|
+
* // Use `targetId` and the component result for host-side work.
|
|
730
|
+
* };
|
|
731
|
+
* ```
|
|
732
|
+
*
|
|
733
|
+
* Calling the exposed host API through `api.feedback...` is also valid, but it
|
|
734
|
+
* re-enters the host wrapper and actor/auth-resolution path. Use that route
|
|
735
|
+
* only when those host-facing semantics are desired.
|
|
736
|
+
*
|
|
737
|
+
* @example No-op reaction after-change hook
|
|
738
|
+
* ```ts
|
|
739
|
+
* const afterChange: FeedbackReactionAfterChangeCallback = async (ctx, event) => {
|
|
740
|
+
* void ctx;
|
|
741
|
+
* void event;
|
|
742
|
+
* };
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
export type FeedbackReactionAfterChangeCallback = (
|
|
746
|
+
ctx: FeedbackMutationContext,
|
|
747
|
+
event: FeedbackReactionChangeEvent,
|
|
748
|
+
) => MaybePromise<void>;
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Convex validator for an explicit callback rejection returned to the client.
|
|
752
|
+
*
|
|
753
|
+
* Use this with `callbacks.rejection` when a before-create callback should
|
|
754
|
+
* return a typed result instead of throwing a `ConvexError`.
|
|
755
|
+
*/
|
|
756
|
+
export type FeedbackCallbackReturnValidator = Validator<
|
|
757
|
+
Value,
|
|
758
|
+
"required",
|
|
759
|
+
string
|
|
760
|
+
>;
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Default callback rejection mode: `handlers.reject(value)` throws a
|
|
764
|
+
* `ConvexError` and prevents the component mutation.
|
|
765
|
+
*/
|
|
766
|
+
export interface ThrowingFeedbackCallbackRejectionConfig {
|
|
767
|
+
/** Selects exception-based callback rejection. This is the default. */
|
|
768
|
+
behavior?: "throw";
|
|
769
|
+
|
|
770
|
+
/** Return-mode validators are not accepted in throwing mode. */
|
|
771
|
+
returns?: never;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Return mode: `handlers.reject(value)` short-circuits with a validated client
|
|
776
|
+
* result.
|
|
777
|
+
*
|
|
778
|
+
* @typeParam ReturnsValidator Validator for the value passed to `reject`.
|
|
779
|
+
*/
|
|
780
|
+
export interface ReturningFeedbackCallbackRejectionConfig<
|
|
781
|
+
ReturnsValidator extends FeedbackCallbackReturnValidator,
|
|
782
|
+
> {
|
|
783
|
+
/** Selects validated return-mode callback rejection. */
|
|
784
|
+
behavior: "return";
|
|
785
|
+
|
|
786
|
+
/** Validator for the value returned when a callback calls `reject`. */
|
|
787
|
+
returns: ReturnsValidator;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Controls only explicit `reject()` calls from entry/comment before callbacks.
|
|
792
|
+
* Runtime/programming errors are never converted. Return-mode inference is
|
|
793
|
+
* added only to `createEntry` and `createComment`.
|
|
794
|
+
*
|
|
795
|
+
* @typeParam ReturnsValidator Validator for the value returned to the client
|
|
796
|
+
* in callback rejection return mode.
|
|
797
|
+
*/
|
|
798
|
+
export type FeedbackCallbackRejectionConfig<
|
|
799
|
+
ReturnsValidator extends FeedbackCallbackReturnValidator | undefined =
|
|
800
|
+
undefined,
|
|
801
|
+
> = ReturnsValidator extends FeedbackCallbackReturnValidator
|
|
802
|
+
? ReturningFeedbackCallbackRejectionConfig<ReturnsValidator>
|
|
803
|
+
: ThrowingFeedbackCallbackRejectionConfig;
|
|
804
|
+
|
|
805
|
+
type CallbackRejectionValue<
|
|
806
|
+
ReturnsValidator extends FeedbackCallbackReturnValidator | undefined,
|
|
807
|
+
> = ReturnsValidator extends FeedbackCallbackReturnValidator
|
|
808
|
+
? Infer<ReturnsValidator>
|
|
809
|
+
: Value;
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Callback properties for entry creation and post-creation work.
|
|
813
|
+
*
|
|
814
|
+
* @typeParam Rejection Value accepted by `beforeCreate`'s `handlers.reject`.
|
|
815
|
+
*/
|
|
816
|
+
export interface FeedbackEntryCallbacks<Rejection = Value> {
|
|
817
|
+
/**
|
|
818
|
+
* Validate, reject, or transform an entry before the component creates it.
|
|
819
|
+
*
|
|
820
|
+
* The callback runs after actor resolution and rate limiting. Returned
|
|
821
|
+
* fields are still checked by the component's normal validation.
|
|
822
|
+
* It receives the host `ctx`, readonly `event` (`actor` and `input`), and
|
|
823
|
+
* `handlers` with `handlers.reject(value)`.
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```ts
|
|
827
|
+
* beforeCreate: (ctx, event, handlers) => {},
|
|
828
|
+
* ```
|
|
829
|
+
*/
|
|
830
|
+
beforeCreate?: FeedbackEntryBeforeCreateCallback<Rejection>;
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* React to an entry after the component has created it successfully.
|
|
834
|
+
*
|
|
835
|
+
* The callback is awaited in the originating host mutation; an uncaught
|
|
836
|
+
* error rolls back the entry creation.
|
|
837
|
+
* It receives the host `ctx` and persisted `event` containing the created
|
|
838
|
+
* actor and entry.
|
|
839
|
+
*
|
|
840
|
+
* @example
|
|
841
|
+
* ```ts
|
|
842
|
+
* afterCreate: async (ctx, event) => {},
|
|
843
|
+
* ```
|
|
844
|
+
*/
|
|
845
|
+
afterCreate?: FeedbackEntryAfterCreateCallback;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Callback properties for comment and reply creation.
|
|
850
|
+
*
|
|
851
|
+
* @typeParam Rejection Value accepted by `beforeCreate`'s `handlers.reject`.
|
|
852
|
+
*/
|
|
853
|
+
export interface FeedbackCommentCallbacks<Rejection = Value> {
|
|
854
|
+
/**
|
|
855
|
+
* Validate, reject, or transform a comment body before the component
|
|
856
|
+
* creates the comment or reply.
|
|
857
|
+
* It receives the host `ctx`, readonly `event` (`actor` and `input`), and
|
|
858
|
+
* `handlers` with `handlers.reject(value)`.
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* ```ts
|
|
862
|
+
* beforeCreate: (ctx, event, handlers) => {},
|
|
863
|
+
* ```
|
|
864
|
+
*/
|
|
865
|
+
beforeCreate?: FeedbackCommentBeforeCreateCallback<Rejection>;
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* React to a comment or reply after the component has created it
|
|
869
|
+
* successfully.
|
|
870
|
+
*
|
|
871
|
+
* The event includes the created comment, its containing entry, and parent
|
|
872
|
+
* comment context when the created comment is a reply.
|
|
873
|
+
* The parameters are the host `ctx` and persisted `event`.
|
|
874
|
+
*
|
|
875
|
+
* @example
|
|
876
|
+
* ```ts
|
|
877
|
+
* afterCreate: async (ctx, event) => {},
|
|
878
|
+
* ```
|
|
879
|
+
*/
|
|
880
|
+
afterCreate?: FeedbackCommentAfterCreateCallback;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Callback properties for entry upvotes and comment likes. */
|
|
884
|
+
export interface FeedbackReactionCallbacks {
|
|
885
|
+
/**
|
|
886
|
+
* React to a real reaction transition after the component mutation.
|
|
887
|
+
*
|
|
888
|
+
* The event contains authoritative counts and direct target IDs. It also
|
|
889
|
+
* retains the nested `entry` or `comment` context for existing consumers.
|
|
890
|
+
* The parameters are the host `ctx` and transition `event`.
|
|
891
|
+
*
|
|
892
|
+
* @example
|
|
893
|
+
* ```ts
|
|
894
|
+
* afterChange: async (ctx, event) => {},
|
|
895
|
+
* ```
|
|
896
|
+
*/
|
|
897
|
+
afterChange?: FeedbackReactionAfterChangeCallback;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Type of the lifecycle callback configuration accepted by
|
|
902
|
+
* `options.callbacks`.
|
|
903
|
+
*
|
|
904
|
+
* See the `entries`, `comments`, and `reactions` properties for the callback
|
|
905
|
+
* group documentation shown directly in configuration IntelliSense.
|
|
906
|
+
*
|
|
907
|
+
* @typeParam ReturnsValidator Validator that defines the value returned by
|
|
908
|
+
* `handlers.reject` when callback rejection uses return mode.
|
|
909
|
+
*/
|
|
910
|
+
export type FeedbackCallbacks<
|
|
911
|
+
ReturnsValidator extends FeedbackCallbackReturnValidator | undefined =
|
|
912
|
+
undefined,
|
|
913
|
+
> = {
|
|
914
|
+
/**
|
|
915
|
+
* Lifecycle callbacks for entries.
|
|
916
|
+
*
|
|
917
|
+
* `beforeCreate` runs after actor resolution and rate limiting, and can
|
|
918
|
+
* validate, reject, or transform an entry before the component writes it.
|
|
919
|
+
* `afterCreate` runs after a successful component write and is awaited in
|
|
920
|
+
* the originating host mutation.
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* ```ts
|
|
924
|
+
* entries: {
|
|
925
|
+
* beforeCreate: (ctx, event, handlers) => {
|
|
926
|
+
* const titleContainsProfanity = checkForProfanity(event.input.title);
|
|
927
|
+
* const bodyContainsProfanity = checkForProfanity(event.input.body);
|
|
928
|
+
*
|
|
929
|
+
* if (titleContainsProfanity || bodyContainsProfanity) {
|
|
930
|
+
* handlers.reject({
|
|
931
|
+
* kind: "profanity_detected",
|
|
932
|
+
* reason: "Entry content contains profanity",
|
|
933
|
+
* });
|
|
934
|
+
* }
|
|
935
|
+
* },
|
|
936
|
+
* }
|
|
937
|
+
* ```
|
|
938
|
+
*/
|
|
939
|
+
entries?: FeedbackEntryCallbacks<CallbackRejectionValue<ReturnsValidator>>;
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Lifecycle callbacks for comments and replies.
|
|
943
|
+
*
|
|
944
|
+
* `beforeCreate` can validate, reject, or transform the comment body.
|
|
945
|
+
* `afterCreate` receives the persisted comment, its containing entry, and
|
|
946
|
+
* parent-comment context when the created comment is a reply.
|
|
947
|
+
*
|
|
948
|
+
* @example
|
|
949
|
+
* ```ts
|
|
950
|
+
* comments: {
|
|
951
|
+
* afterCreate: async (ctx, event) => {
|
|
952
|
+
* await ctx.scheduler.runAfter(0, internal.notifications.commentCreated, {
|
|
953
|
+
* commentId: event.comment.id,
|
|
954
|
+
* entryId: event.entry.id,
|
|
955
|
+
* });
|
|
956
|
+
* },
|
|
957
|
+
* }
|
|
958
|
+
* ```
|
|
959
|
+
*/
|
|
960
|
+
comments?: FeedbackCommentCallbacks<CallbackRejectionValue<ReturnsValidator>>;
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Lifecycle callbacks for entry upvotes and comment likes.
|
|
964
|
+
*
|
|
965
|
+
* `afterChange` runs only for a real `added` or `removed` transition.
|
|
966
|
+
* Repeated requests for the current state remain idempotent and do not
|
|
967
|
+
* invoke it. Direct target IDs are available alongside nested context.
|
|
968
|
+
*
|
|
969
|
+
* @example
|
|
970
|
+
* ```ts
|
|
971
|
+
* reactions: {
|
|
972
|
+
* afterChange: async (ctx, event) => {
|
|
973
|
+
* if (event.transition !== "added") return;
|
|
974
|
+
*
|
|
975
|
+
* const docId = event.type === "entry_upvote" ? event.entryId : event.commentId;
|
|
976
|
+
* // Notify the target author using docId.
|
|
977
|
+
* },
|
|
978
|
+
* }
|
|
979
|
+
* ```
|
|
980
|
+
*/
|
|
981
|
+
reactions?: FeedbackReactionCallbacks;
|
|
982
|
+
} & (ReturnsValidator extends FeedbackCallbackReturnValidator
|
|
983
|
+
? {
|
|
984
|
+
/**
|
|
985
|
+
* Validated return-mode behavior for explicit `beforeCreate` rejects.
|
|
986
|
+
*/
|
|
987
|
+
rejection: ReturningFeedbackCallbackRejectionConfig<ReturnsValidator>;
|
|
988
|
+
}
|
|
989
|
+
: {
|
|
990
|
+
/**
|
|
991
|
+
* Optional rejection behavior for explicit `beforeCreate` rejects.
|
|
992
|
+
* Defaults to throwing a `ConvexError`.
|
|
993
|
+
*/
|
|
994
|
+
rejection?: ThrowingFeedbackCallbackRejectionConfig;
|
|
995
|
+
});
|
|
996
|
+
|
|
222
997
|
type RegisteredFeedbackFunction<Function> =
|
|
223
998
|
Function extends FunctionReference<
|
|
224
999
|
"mutation",
|
|
@@ -240,11 +1015,19 @@ type RegisteredFeedbackFunction<Function> =
|
|
|
240
1015
|
: never
|
|
241
1016
|
: never;
|
|
242
1017
|
|
|
243
|
-
type ExposedFeedbackApi<RateLimitResult> = {
|
|
1018
|
+
type ExposedFeedbackApi<RateLimitResult, CallbackRejectionResult> = {
|
|
244
1019
|
[
|
|
245
|
-
FunctionName in keyof FeedbackPublicApi<
|
|
1020
|
+
FunctionName in keyof FeedbackPublicApi<
|
|
1021
|
+
string | undefined,
|
|
1022
|
+
RateLimitResult,
|
|
1023
|
+
CallbackRejectionResult
|
|
1024
|
+
>
|
|
246
1025
|
]: RegisteredFeedbackFunction<
|
|
247
|
-
FeedbackPublicApi<
|
|
1026
|
+
FeedbackPublicApi<
|
|
1027
|
+
string | undefined,
|
|
1028
|
+
RateLimitResult,
|
|
1029
|
+
CallbackRejectionResult
|
|
1030
|
+
>[FunctionName]
|
|
248
1031
|
>;
|
|
249
1032
|
};
|
|
250
1033
|
|
|
@@ -266,11 +1049,82 @@ interface ExposeFeedbackOptionsBase {
|
|
|
266
1049
|
config?: FeedbackConfigOverrides;
|
|
267
1050
|
}
|
|
268
1051
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
1052
|
+
type FeedbackCallbackOptions<
|
|
1053
|
+
ReturnsValidator extends FeedbackCallbackReturnValidator | undefined,
|
|
1054
|
+
> = ReturnsValidator extends FeedbackCallbackReturnValidator
|
|
1055
|
+
? {
|
|
1056
|
+
/**
|
|
1057
|
+
* Host lifecycle callbacks grouped by domain with validated return-mode
|
|
1058
|
+
* rejection.
|
|
1059
|
+
*
|
|
1060
|
+
* Creation callbacks run in the order actor/auth → rate limiting →
|
|
1061
|
+
* `beforeCreate` → component mutation → `afterCreate`. Reaction
|
|
1062
|
+
* callbacks run after the component mutation only for a real transition.
|
|
1063
|
+
* All callbacks are awaited in the originating host mutation.
|
|
1064
|
+
*
|
|
1065
|
+
* For feedback data, prefer direct component references such as
|
|
1066
|
+
* `ctx.runQuery(components.feedback.entries.get, ...)`. Calling an
|
|
1067
|
+
* exposed host API through `api.feedback...` is also valid, but it
|
|
1068
|
+
* re-enters the host wrapper and actor/auth-resolution path; use it only
|
|
1069
|
+
* when those host-facing semantics are desired.
|
|
1070
|
+
*
|
|
1071
|
+
* @example Configure entry validation
|
|
1072
|
+
* ```ts
|
|
1073
|
+
* callbacks: {
|
|
1074
|
+
* entries: {
|
|
1075
|
+
* beforeCreate: (ctx, event, handlers) => {
|
|
1076
|
+
* void ctx;
|
|
1077
|
+
* if (event.input.title.trim() === "") {
|
|
1078
|
+
* handlers.reject("A title is required");
|
|
1079
|
+
* }
|
|
1080
|
+
* },
|
|
1081
|
+
* },
|
|
1082
|
+
* }
|
|
1083
|
+
* ```
|
|
1084
|
+
*/
|
|
1085
|
+
callbacks: FeedbackCallbacks<ReturnsValidator>;
|
|
1086
|
+
}
|
|
1087
|
+
: {
|
|
1088
|
+
/**
|
|
1089
|
+
* Optional host lifecycle callbacks grouped by domain. Explicit
|
|
1090
|
+
* rejection throws by default.
|
|
1091
|
+
*
|
|
1092
|
+
* Creation callbacks run in the order actor/auth → rate limiting →
|
|
1093
|
+
* `beforeCreate` → component mutation → `afterCreate`. Reaction
|
|
1094
|
+
* callbacks run after the component mutation only for a real transition.
|
|
1095
|
+
* All callbacks are awaited in the originating host mutation.
|
|
1096
|
+
*
|
|
1097
|
+
* For feedback data, prefer direct component references such as
|
|
1098
|
+
* `ctx.runQuery(components.feedback.entries.get, ...)`. Calling an
|
|
1099
|
+
* exposed host API through `api.feedback...` is also valid, but it
|
|
1100
|
+
* re-enters the host wrapper and actor/auth-resolution path; use it only
|
|
1101
|
+
* when those host-facing semantics are desired.
|
|
1102
|
+
*
|
|
1103
|
+
* @example Configure entry validation
|
|
1104
|
+
* ```ts
|
|
1105
|
+
* callbacks: {
|
|
1106
|
+
* entries: {
|
|
1107
|
+
* beforeCreate: (ctx, event, handlers) => {
|
|
1108
|
+
* void ctx;
|
|
1109
|
+
* if (event.input.title.trim() === "") {
|
|
1110
|
+
* handlers.reject("A title is required");
|
|
1111
|
+
* }
|
|
1112
|
+
* },
|
|
1113
|
+
* },
|
|
1114
|
+
* }
|
|
1115
|
+
* ```
|
|
1116
|
+
*/
|
|
1117
|
+
callbacks?: FeedbackCallbacks;
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Exposure options with throwing rate limits and optional lifecycle callbacks.
|
|
1122
|
+
* Callback execution/rollback semantics are documented by {@link FeedbackCallbacks}.
|
|
1123
|
+
*/
|
|
1124
|
+
export type ThrowingExposeFeedbackOptions<
|
|
1125
|
+
CallbackReturnsValidator extends FeedbackCallbackReturnValidator | undefined =
|
|
1126
|
+
undefined,
|
|
1127
|
+
> = Omit<ExposeFeedbackOptionsBase, "config"> & {
|
|
274
1128
|
/** Optional throwing rate limiters for the component's mutation groups. */
|
|
275
1129
|
rateLimiters?: FeedbackRateLimiters;
|
|
276
1130
|
|
|
@@ -284,11 +1138,16 @@ export type ThrowingExposeFeedbackOptions = Omit<
|
|
|
284
1138
|
*/
|
|
285
1139
|
rateLimiting?: FeedbackRateLimitConfig;
|
|
286
1140
|
};
|
|
287
|
-
}
|
|
1141
|
+
} & FeedbackCallbackOptions<CallbackReturnsValidator>;
|
|
288
1142
|
|
|
289
|
-
/**
|
|
1143
|
+
/**
|
|
1144
|
+
* Exposure options for validated rate-limit returns plus optional lifecycle
|
|
1145
|
+
* callbacks and independently inferred callback-rejection behavior.
|
|
1146
|
+
*/
|
|
290
1147
|
export type ReturningExposeFeedbackOptions<
|
|
291
1148
|
ReturnsValidator extends FeedbackRateLimitReturnValidator,
|
|
1149
|
+
CallbackReturnsValidator extends FeedbackCallbackReturnValidator | undefined =
|
|
1150
|
+
undefined,
|
|
292
1151
|
> = Omit<ExposeFeedbackOptionsBase, "config"> & {
|
|
293
1152
|
/**
|
|
294
1153
|
* Optional returning rate limiters for the component's mutation groups.
|
|
@@ -308,21 +1167,24 @@ export type ReturningExposeFeedbackOptions<
|
|
|
308
1167
|
*/
|
|
309
1168
|
rateLimiting: FeedbackRateLimitConfig<ReturnsValidator>;
|
|
310
1169
|
};
|
|
311
|
-
}
|
|
1170
|
+
} & FeedbackCallbackOptions<CallbackReturnsValidator>;
|
|
312
1171
|
|
|
313
1172
|
/**
|
|
314
1173
|
* Configuration used when exposing feedback functions from the host app.
|
|
315
1174
|
*
|
|
316
1175
|
* When `ReturnsValidator` is omitted, limiter functions use throwing behavior.
|
|
317
1176
|
* Supplying a validator selects return behavior and adds its inferred value to
|
|
318
|
-
* the exposed mutation result types.
|
|
1177
|
+
* the exposed mutation result types. `CallbackReturnsValidator` independently
|
|
1178
|
+
* controls explicit before-callback rejection inference.
|
|
319
1179
|
*/
|
|
320
1180
|
export type ExposeFeedbackOptions<
|
|
321
1181
|
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined =
|
|
322
1182
|
undefined,
|
|
1183
|
+
CallbackReturnsValidator extends FeedbackCallbackReturnValidator | undefined =
|
|
1184
|
+
undefined,
|
|
323
1185
|
> = ReturnsValidator extends FeedbackRateLimitReturnValidator
|
|
324
|
-
? ReturningExposeFeedbackOptions<ReturnsValidator>
|
|
325
|
-
: ThrowingExposeFeedbackOptions
|
|
1186
|
+
? ReturningExposeFeedbackOptions<ReturnsValidator, CallbackReturnsValidator>
|
|
1187
|
+
: ThrowingExposeFeedbackOptions<CallbackReturnsValidator>;
|
|
326
1188
|
|
|
327
1189
|
function requireActor(actor: FeedbackActor | null): FeedbackActor {
|
|
328
1190
|
if (actor === null) {
|
|
@@ -353,6 +1215,78 @@ function rateLimitedReturns<
|
|
|
353
1215
|
) as RateLimitedReturnsValidator<Base, Limit>;
|
|
354
1216
|
}
|
|
355
1217
|
|
|
1218
|
+
type CallbackLimitedReturnsValidator<
|
|
1219
|
+
Base extends FeedbackFunctionReturnValidator,
|
|
1220
|
+
Rejection extends FeedbackCallbackReturnValidator | undefined,
|
|
1221
|
+
> = Rejection extends FeedbackCallbackReturnValidator
|
|
1222
|
+
? VUnion<Infer<Base> | Infer<Rejection>, [Base, Rejection]>
|
|
1223
|
+
: Base;
|
|
1224
|
+
|
|
1225
|
+
function callbackLimitedReturns<
|
|
1226
|
+
Base extends FeedbackFunctionReturnValidator,
|
|
1227
|
+
Rejection extends FeedbackCallbackReturnValidator | undefined,
|
|
1228
|
+
>(
|
|
1229
|
+
base: Base,
|
|
1230
|
+
rejection: Rejection,
|
|
1231
|
+
): CallbackLimitedReturnsValidator<Base, Rejection> {
|
|
1232
|
+
return (
|
|
1233
|
+
rejection === undefined ? base : v.union(base, rejection)
|
|
1234
|
+
) as CallbackLimitedReturnsValidator<Base, Rejection>;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
type CallbackResult<
|
|
1238
|
+
ReturnsValidator extends FeedbackCallbackReturnValidator | undefined,
|
|
1239
|
+
> = ReturnsValidator extends FeedbackCallbackReturnValidator
|
|
1240
|
+
? Infer<ReturnsValidator>
|
|
1241
|
+
: never;
|
|
1242
|
+
|
|
1243
|
+
class ExplicitFeedbackCallbackRejection extends Error {
|
|
1244
|
+
constructor(readonly value: Value) {
|
|
1245
|
+
super("Feedback callback rejected creation.");
|
|
1246
|
+
this.name = "ExplicitFeedbackCallbackRejection";
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
async function runBeforeCreate<Patch, Event, Rejection extends Value>(
|
|
1251
|
+
ctx: FeedbackMutationContext,
|
|
1252
|
+
event: Event,
|
|
1253
|
+
callback:
|
|
1254
|
+
| ((
|
|
1255
|
+
ctx: FeedbackMutationContext,
|
|
1256
|
+
event: Event,
|
|
1257
|
+
helpers: FeedbackCallbackHelpers<Rejection>,
|
|
1258
|
+
) => MaybePromise<Patch | undefined>)
|
|
1259
|
+
| undefined,
|
|
1260
|
+
rejectionConfig:
|
|
1261
|
+
| ThrowingFeedbackCallbackRejectionConfig
|
|
1262
|
+
| ReturningFeedbackCallbackRejectionConfig<FeedbackCallbackReturnValidator>
|
|
1263
|
+
| undefined,
|
|
1264
|
+
): Promise<
|
|
1265
|
+
| { rejected: false; patch: Patch | undefined }
|
|
1266
|
+
| { rejected: true; value: Rejection }
|
|
1267
|
+
> {
|
|
1268
|
+
if (callback === undefined) {
|
|
1269
|
+
return { rejected: false, patch: undefined };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
const patch = await callback(ctx, event, {
|
|
1274
|
+
reject: (value) => {
|
|
1275
|
+
throw new ExplicitFeedbackCallbackRejection(value);
|
|
1276
|
+
},
|
|
1277
|
+
});
|
|
1278
|
+
return { rejected: false, patch };
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
if (!(error instanceof ExplicitFeedbackCallbackRejection)) {
|
|
1281
|
+
throw error;
|
|
1282
|
+
}
|
|
1283
|
+
if (rejectionConfig?.behavior === "return") {
|
|
1284
|
+
return { rejected: true, value: error.value as Rejection };
|
|
1285
|
+
}
|
|
1286
|
+
throw new ConvexError(error.value);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
356
1290
|
async function applyRateLimiter<
|
|
357
1291
|
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined,
|
|
358
1292
|
>(
|
|
@@ -386,6 +1320,10 @@ function asRateLimitContext(ctx: unknown): FeedbackRateLimitContext {
|
|
|
386
1320
|
return ctx as FeedbackRateLimitContext;
|
|
387
1321
|
}
|
|
388
1322
|
|
|
1323
|
+
function asMutationContext(ctx: unknown): FeedbackMutationContext {
|
|
1324
|
+
return ctx as FeedbackMutationContext;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
389
1327
|
function clampPositive(
|
|
390
1328
|
value: number | undefined,
|
|
391
1329
|
fallback: number,
|
|
@@ -412,13 +1350,29 @@ function actorIdFields(actor: FeedbackActor | null): {
|
|
|
412
1350
|
return actor === null ? {} : { viewerActorId: actor.id };
|
|
413
1351
|
}
|
|
414
1352
|
|
|
1353
|
+
function cloneFeedbackMetadata(
|
|
1354
|
+
metadata: FeedbackMetadata | undefined,
|
|
1355
|
+
): FeedbackMetadata | undefined {
|
|
1356
|
+
if (metadata === undefined) return undefined;
|
|
1357
|
+
return {
|
|
1358
|
+
...(metadata.standard === undefined
|
|
1359
|
+
? {}
|
|
1360
|
+
: { standard: { ...metadata.standard } }),
|
|
1361
|
+
...(metadata.additional === undefined
|
|
1362
|
+
? {}
|
|
1363
|
+
: { additional: { ...metadata.additional } }),
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
415
1367
|
function buildFeedbackApi<
|
|
416
1368
|
Name extends string | undefined,
|
|
417
1369
|
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined =
|
|
418
1370
|
undefined,
|
|
1371
|
+
CallbackReturnsValidator extends FeedbackCallbackReturnValidator | undefined =
|
|
1372
|
+
undefined,
|
|
419
1373
|
>(
|
|
420
1374
|
component: ComponentApi<Name>,
|
|
421
|
-
options: ExposeFeedbackOptions<ReturnsValidator>,
|
|
1375
|
+
options: ExposeFeedbackOptions<ReturnsValidator, CallbackReturnsValidator>,
|
|
422
1376
|
) {
|
|
423
1377
|
const config = createFeedbackConfig(options.config);
|
|
424
1378
|
const rateLimitConfig = options.config?.rateLimiting;
|
|
@@ -426,8 +1380,17 @@ function buildFeedbackApi<
|
|
|
426
1380
|
rateLimitConfig?.behavior === "return"
|
|
427
1381
|
? rateLimitConfig.returns
|
|
428
1382
|
: undefined;
|
|
1383
|
+
const callbackRejectionConfig = options.callbacks?.rejection;
|
|
1384
|
+
const callbackReturnValidator =
|
|
1385
|
+
callbackRejectionConfig?.behavior === "return"
|
|
1386
|
+
? callbackRejectionConfig.returns
|
|
1387
|
+
: undefined;
|
|
429
1388
|
|
|
430
1389
|
const idReturns = rateLimitedReturns(v.string(), rateLimitReturnValidator);
|
|
1390
|
+
const createIdReturns = callbackLimitedReturns(
|
|
1391
|
+
idReturns,
|
|
1392
|
+
callbackReturnValidator,
|
|
1393
|
+
);
|
|
431
1394
|
const nullReturns = rateLimitedReturns(v.null(), rateLimitReturnValidator);
|
|
432
1395
|
const entryUpvoteReturns = rateLimitedReturns(
|
|
433
1396
|
v.object({ active: v.boolean(), upvoteCount: v.number() }),
|
|
@@ -503,6 +1466,29 @@ function buildFeedbackApi<
|
|
|
503
1466
|
},
|
|
504
1467
|
}),
|
|
505
1468
|
|
|
1469
|
+
listUserEntries: queryGeneric({
|
|
1470
|
+
args: {
|
|
1471
|
+
paginationOpts: paginationOptsValidator,
|
|
1472
|
+
},
|
|
1473
|
+
returns: paginationResultValidator(activityEntryValidator),
|
|
1474
|
+
handler: async (ctx, args) => {
|
|
1475
|
+
const actor = requireActor(await options.actor(ctx));
|
|
1476
|
+
const result = await ctx.runQuery(component.entries.listByActor, {
|
|
1477
|
+
actorId: actor.id,
|
|
1478
|
+
paginationOpts: clampPagination(
|
|
1479
|
+
args.paginationOpts,
|
|
1480
|
+
config.entries.maxPageSize,
|
|
1481
|
+
),
|
|
1482
|
+
includeAdminContext: false,
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
return {
|
|
1486
|
+
...result,
|
|
1487
|
+
page: result.page.map(stripActivityEntryContext),
|
|
1488
|
+
};
|
|
1489
|
+
},
|
|
1490
|
+
}),
|
|
1491
|
+
|
|
506
1492
|
getEntry: queryGeneric({
|
|
507
1493
|
args: { entryId: v.string() },
|
|
508
1494
|
returns: v.union(publicEntryValidator, v.null()),
|
|
@@ -583,7 +1569,7 @@ function buildFeedbackApi<
|
|
|
583
1569
|
body: v.string(),
|
|
584
1570
|
metadata: v.optional(feedbackMetadataValidator),
|
|
585
1571
|
},
|
|
586
|
-
returns:
|
|
1572
|
+
returns: createIdReturns,
|
|
587
1573
|
handler: async (ctx, args) => {
|
|
588
1574
|
const actor = requireActor(await options.actor(ctx));
|
|
589
1575
|
const limited = await applyRateLimiter(
|
|
@@ -593,17 +1579,57 @@ function buildFeedbackApi<
|
|
|
593
1579
|
rateLimitConfig,
|
|
594
1580
|
);
|
|
595
1581
|
if (limited !== undefined) return limited;
|
|
596
|
-
|
|
1582
|
+
|
|
1583
|
+
const beforeCreate = options.callbacks?.entries?.beforeCreate;
|
|
1584
|
+
let patch: FeedbackEntryCreatePatch | undefined;
|
|
1585
|
+
if (beforeCreate !== undefined) {
|
|
1586
|
+
const callbackInput: FeedbackEntryBeforeCreateEvent["input"] = {
|
|
1587
|
+
kind: args.kind,
|
|
1588
|
+
title: args.title,
|
|
1589
|
+
body: args.body,
|
|
1590
|
+
...(args.metadata === undefined
|
|
1591
|
+
? {}
|
|
1592
|
+
: { metadata: cloneFeedbackMetadata(args.metadata) }),
|
|
1593
|
+
};
|
|
1594
|
+
const before = await runBeforeCreate(
|
|
1595
|
+
asMutationContext(ctx),
|
|
1596
|
+
{ actor: { ...actor }, input: callbackInput },
|
|
1597
|
+
beforeCreate,
|
|
1598
|
+
callbackRejectionConfig,
|
|
1599
|
+
);
|
|
1600
|
+
if (before.rejected) {
|
|
1601
|
+
return before.value as CallbackResult<CallbackReturnsValidator>;
|
|
1602
|
+
}
|
|
1603
|
+
patch = before.patch;
|
|
1604
|
+
}
|
|
1605
|
+
const metadata =
|
|
1606
|
+
patch !== undefined &&
|
|
1607
|
+
Object.prototype.hasOwnProperty.call(patch, "metadata")
|
|
1608
|
+
? patch.metadata
|
|
1609
|
+
: args.metadata;
|
|
1610
|
+
const afterCreate = options.callbacks?.entries?.afterCreate;
|
|
1611
|
+
const result = await ctx.runMutation(component.entries.create, {
|
|
597
1612
|
actorId: actor.id,
|
|
598
|
-
kind: args.kind,
|
|
599
|
-
title: args.title,
|
|
600
|
-
body: args.body,
|
|
1613
|
+
kind: patch?.kind ?? args.kind,
|
|
1614
|
+
title: patch?.title ?? args.title,
|
|
1615
|
+
body: patch?.body ?? args.body,
|
|
601
1616
|
defaultStatus: config.entries.defaultStatus,
|
|
602
1617
|
enabledKinds: [...config.entries.enabledKinds],
|
|
603
1618
|
maxTitleLength: config.limits.titleLength,
|
|
604
1619
|
maxBodyLength: config.limits.bodyLength,
|
|
605
|
-
...(
|
|
1620
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
1621
|
+
includeCallbackContext: afterCreate !== undefined,
|
|
606
1622
|
});
|
|
1623
|
+
if (afterCreate !== undefined) {
|
|
1624
|
+
if (!("entry" in result)) {
|
|
1625
|
+
throw new ConvexError("Entry callback context was not returned.");
|
|
1626
|
+
}
|
|
1627
|
+
await afterCreate(asMutationContext(ctx), {
|
|
1628
|
+
actor,
|
|
1629
|
+
entry: result.entry,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
return result.id;
|
|
607
1633
|
},
|
|
608
1634
|
}),
|
|
609
1635
|
|
|
@@ -637,6 +1663,23 @@ function buildFeedbackApi<
|
|
|
637
1663
|
},
|
|
638
1664
|
}),
|
|
639
1665
|
|
|
1666
|
+
deleteEntry: mutationGeneric({
|
|
1667
|
+
args: { entryId: v.string() },
|
|
1668
|
+
returns: nullReturns,
|
|
1669
|
+
handler: async (ctx, args) => {
|
|
1670
|
+
const actor = await requireAdminActor(ctx);
|
|
1671
|
+
const limited = await applyAdminEditLimit(
|
|
1672
|
+
asRateLimitContext(ctx),
|
|
1673
|
+
actor,
|
|
1674
|
+
);
|
|
1675
|
+
if (limited !== undefined) return limited;
|
|
1676
|
+
return await ctx.runMutation(component.entries.remove, {
|
|
1677
|
+
actor,
|
|
1678
|
+
entryId: args.entryId,
|
|
1679
|
+
});
|
|
1680
|
+
},
|
|
1681
|
+
}),
|
|
1682
|
+
|
|
640
1683
|
setEntryStatus: mutationGeneric({
|
|
641
1684
|
args: { entryId: v.string(), status: entryStatusValidator },
|
|
642
1685
|
returns: nullReturns,
|
|
@@ -663,6 +1706,7 @@ function buildFeedbackApi<
|
|
|
663
1706
|
paginationOpts: paginationOptsValidator,
|
|
664
1707
|
kinds: v.optional(v.array(entryKindValidator)),
|
|
665
1708
|
status: v.optional(entryStatusValidator),
|
|
1709
|
+
statusFilter: v.optional(entryStatusFilterValidator),
|
|
666
1710
|
priority: v.optional(entryPriorityValidator),
|
|
667
1711
|
},
|
|
668
1712
|
returns: paginationResultValidator(adminEntryValidator),
|
|
@@ -671,6 +1715,9 @@ function buildFeedbackApi<
|
|
|
671
1715
|
return await ctx.runQuery(component.admin.listEntries, {
|
|
672
1716
|
...(args.kinds === undefined ? {} : { kinds: args.kinds }),
|
|
673
1717
|
...(args.status === undefined ? {} : { status: args.status }),
|
|
1718
|
+
...(args.statusFilter === undefined
|
|
1719
|
+
? {}
|
|
1720
|
+
: { statusFilter: args.statusFilter }),
|
|
674
1721
|
...(args.priority === undefined ? {} : { priority: args.priority }),
|
|
675
1722
|
paginationOpts: clampPagination(
|
|
676
1723
|
args.paginationOpts,
|
|
@@ -699,6 +1746,7 @@ function buildFeedbackApi<
|
|
|
699
1746
|
searchQuery: v.string(),
|
|
700
1747
|
kinds: v.optional(v.array(entryKindValidator)),
|
|
701
1748
|
status: v.optional(entryStatusValidator),
|
|
1749
|
+
statusFilter: v.optional(entryStatusFilterValidator),
|
|
702
1750
|
priority: v.optional(entryPriorityValidator),
|
|
703
1751
|
},
|
|
704
1752
|
returns: paginationResultValidator(adminEntryValidator),
|
|
@@ -708,6 +1756,9 @@ function buildFeedbackApi<
|
|
|
708
1756
|
searchQuery: args.searchQuery,
|
|
709
1757
|
...(args.kinds === undefined ? {} : { kinds: args.kinds }),
|
|
710
1758
|
...(args.status === undefined ? {} : { status: args.status }),
|
|
1759
|
+
...(args.statusFilter === undefined
|
|
1760
|
+
? {}
|
|
1761
|
+
: { statusFilter: args.statusFilter }),
|
|
711
1762
|
...(args.priority === undefined ? {} : { priority: args.priority }),
|
|
712
1763
|
paginationOpts: clampPagination(
|
|
713
1764
|
args.paginationOpts,
|
|
@@ -751,11 +1802,32 @@ function buildFeedbackApi<
|
|
|
751
1802
|
rateLimitConfig,
|
|
752
1803
|
);
|
|
753
1804
|
if (limited !== undefined) return limited;
|
|
754
|
-
|
|
1805
|
+
const afterChange = options.callbacks?.reactions?.afterChange;
|
|
1806
|
+
const result = await ctx.runMutation(component.entries.setUpvote, {
|
|
755
1807
|
actorId: actor.id,
|
|
756
1808
|
entryId: args.entryId,
|
|
757
1809
|
desiredState: args.desiredState,
|
|
1810
|
+
includeCallbackContext: afterChange !== undefined,
|
|
758
1811
|
});
|
|
1812
|
+
if (
|
|
1813
|
+
"changed" in result &&
|
|
1814
|
+
result.changed &&
|
|
1815
|
+
afterChange !== undefined
|
|
1816
|
+
) {
|
|
1817
|
+
await afterChange(asMutationContext(ctx), {
|
|
1818
|
+
type: "entry_upvote",
|
|
1819
|
+
entryId: result.entry.id,
|
|
1820
|
+
transition: result.transition,
|
|
1821
|
+
active: result.active,
|
|
1822
|
+
previousCount: result.previousCount,
|
|
1823
|
+
count: result.count,
|
|
1824
|
+
actor,
|
|
1825
|
+
entry: result.entry,
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
return "upvoteCount" in result
|
|
1829
|
+
? result
|
|
1830
|
+
: { active: result.active, upvoteCount: result.count };
|
|
759
1831
|
},
|
|
760
1832
|
}),
|
|
761
1833
|
|
|
@@ -784,13 +1856,30 @@ function buildFeedbackApi<
|
|
|
784
1856
|
},
|
|
785
1857
|
}),
|
|
786
1858
|
|
|
1859
|
+
listUserComments: queryGeneric({
|
|
1860
|
+
args: {
|
|
1861
|
+
paginationOpts: paginationOptsValidator,
|
|
1862
|
+
},
|
|
1863
|
+
returns: paginationResultValidator(activityCommentValidator),
|
|
1864
|
+
handler: async (ctx, args) => {
|
|
1865
|
+
const actor = requireActor(await options.actor(ctx));
|
|
1866
|
+
return await ctx.runQuery(component.comments.listByActor, {
|
|
1867
|
+
actorId: actor.id,
|
|
1868
|
+
paginationOpts: clampPagination(
|
|
1869
|
+
args.paginationOpts,
|
|
1870
|
+
config.comments.maxPageSize,
|
|
1871
|
+
),
|
|
1872
|
+
});
|
|
1873
|
+
},
|
|
1874
|
+
}),
|
|
1875
|
+
|
|
787
1876
|
createComment: mutationGeneric({
|
|
788
1877
|
args: {
|
|
789
1878
|
entryId: v.string(),
|
|
790
1879
|
parentCommentId: v.optional(v.string()),
|
|
791
1880
|
body: v.string(),
|
|
792
1881
|
},
|
|
793
|
-
returns:
|
|
1882
|
+
returns: createIdReturns,
|
|
794
1883
|
handler: async (ctx, args) => {
|
|
795
1884
|
const actor = requireActor(await options.actor(ctx));
|
|
796
1885
|
const limited = await applyRateLimiter(
|
|
@@ -800,16 +1889,54 @@ function buildFeedbackApi<
|
|
|
800
1889
|
rateLimitConfig,
|
|
801
1890
|
);
|
|
802
1891
|
if (limited !== undefined) return limited;
|
|
803
|
-
|
|
1892
|
+
|
|
1893
|
+
const beforeCreate = options.callbacks?.comments?.beforeCreate;
|
|
1894
|
+
let patch: FeedbackCommentCreatePatch | undefined;
|
|
1895
|
+
if (beforeCreate !== undefined) {
|
|
1896
|
+
const callbackInput: FeedbackCommentBeforeCreateEvent["input"] = {
|
|
1897
|
+
entryId: args.entryId,
|
|
1898
|
+
...(args.parentCommentId === undefined
|
|
1899
|
+
? {}
|
|
1900
|
+
: { parentCommentId: args.parentCommentId }),
|
|
1901
|
+
body: args.body,
|
|
1902
|
+
};
|
|
1903
|
+
const before = await runBeforeCreate(
|
|
1904
|
+
asMutationContext(ctx),
|
|
1905
|
+
{ actor: { ...actor }, input: callbackInput },
|
|
1906
|
+
beforeCreate,
|
|
1907
|
+
callbackRejectionConfig,
|
|
1908
|
+
);
|
|
1909
|
+
if (before.rejected) {
|
|
1910
|
+
return before.value as CallbackResult<CallbackReturnsValidator>;
|
|
1911
|
+
}
|
|
1912
|
+
patch = before.patch;
|
|
1913
|
+
}
|
|
1914
|
+
const afterCreate = options.callbacks?.comments?.afterCreate;
|
|
1915
|
+
const result = await ctx.runMutation(component.comments.create, {
|
|
804
1916
|
actorId: actor.id,
|
|
805
1917
|
entryId: args.entryId,
|
|
806
1918
|
...(args.parentCommentId === undefined
|
|
807
1919
|
? {}
|
|
808
1920
|
: { parentCommentId: args.parentCommentId }),
|
|
809
|
-
body: args.body,
|
|
1921
|
+
body: patch?.body ?? args.body,
|
|
810
1922
|
maxDepth: config.comments.maxDepth,
|
|
811
1923
|
maxCommentLength: config.limits.commentLength,
|
|
1924
|
+
includeCallbackContext: afterCreate !== undefined,
|
|
812
1925
|
});
|
|
1926
|
+
if (afterCreate !== undefined) {
|
|
1927
|
+
if (!("comment" in result)) {
|
|
1928
|
+
throw new ConvexError("Comment callback context was not returned.");
|
|
1929
|
+
}
|
|
1930
|
+
await afterCreate(asMutationContext(ctx), {
|
|
1931
|
+
actor,
|
|
1932
|
+
comment: result.comment,
|
|
1933
|
+
entry: result.entry,
|
|
1934
|
+
...(result.parentComment === undefined
|
|
1935
|
+
? {}
|
|
1936
|
+
: { parentComment: result.parentComment }),
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
return result.id;
|
|
813
1940
|
},
|
|
814
1941
|
}),
|
|
815
1942
|
|
|
@@ -867,10 +1994,49 @@ function buildFeedbackApi<
|
|
|
867
1994
|
rateLimitConfig,
|
|
868
1995
|
);
|
|
869
1996
|
if (limited !== undefined) return limited;
|
|
870
|
-
|
|
1997
|
+
const afterChange = options.callbacks?.reactions?.afterChange;
|
|
1998
|
+
const result = await ctx.runMutation(component.comments.setLike, {
|
|
871
1999
|
actorId: actor.id,
|
|
872
2000
|
commentId: args.commentId,
|
|
873
2001
|
desiredState: args.desiredState,
|
|
2002
|
+
includeCallbackContext: afterChange !== undefined,
|
|
2003
|
+
});
|
|
2004
|
+
if (
|
|
2005
|
+
"changed" in result &&
|
|
2006
|
+
result.changed &&
|
|
2007
|
+
afterChange !== undefined
|
|
2008
|
+
) {
|
|
2009
|
+
await afterChange(asMutationContext(ctx), {
|
|
2010
|
+
type: "comment_like",
|
|
2011
|
+
commentId: result.comment.id,
|
|
2012
|
+
entryId: result.comment.entryId,
|
|
2013
|
+
transition: result.transition,
|
|
2014
|
+
active: result.active,
|
|
2015
|
+
previousCount: result.previousCount,
|
|
2016
|
+
count: result.count,
|
|
2017
|
+
actor,
|
|
2018
|
+
comment: result.comment,
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
return "likeCount" in result
|
|
2022
|
+
? result
|
|
2023
|
+
: { active: result.active, likeCount: result.count };
|
|
2024
|
+
},
|
|
2025
|
+
}),
|
|
2026
|
+
|
|
2027
|
+
listUserReactions: queryGeneric({
|
|
2028
|
+
args: {
|
|
2029
|
+
paginationOpts: paginationOptsValidator,
|
|
2030
|
+
},
|
|
2031
|
+
returns: paginationResultValidator(feedbackReactionValidator),
|
|
2032
|
+
handler: async (ctx, args) => {
|
|
2033
|
+
const actor = requireActor(await options.actor(ctx));
|
|
2034
|
+
return await ctx.runQuery(component.reactions.listByActor, {
|
|
2035
|
+
actorId: actor.id,
|
|
2036
|
+
paginationOpts: clampPagination(
|
|
2037
|
+
args.paginationOpts,
|
|
2038
|
+
config.entries.maxPageSize,
|
|
2039
|
+
),
|
|
874
2040
|
});
|
|
875
2041
|
},
|
|
876
2042
|
}),
|
|
@@ -892,6 +2058,14 @@ function buildFeedbackApi<
|
|
|
892
2058
|
},
|
|
893
2059
|
}),
|
|
894
2060
|
|
|
2061
|
+
getRoadmapItem: queryGeneric({
|
|
2062
|
+
args: { roadmapId: v.string() },
|
|
2063
|
+
returns: v.union(roadmapItemValidator, v.null()),
|
|
2064
|
+
handler: async (ctx, args) => {
|
|
2065
|
+
return await ctx.runQuery(component.roadmap.get, args);
|
|
2066
|
+
},
|
|
2067
|
+
}),
|
|
2068
|
+
|
|
895
2069
|
searchRoadmap: queryGeneric({
|
|
896
2070
|
args: { searchQuery: v.string(), limit: v.optional(v.number()) },
|
|
897
2071
|
returns: v.array(roadmapItemValidator),
|
|
@@ -1071,24 +2245,53 @@ function buildFeedbackApi<
|
|
|
1071
2245
|
*/
|
|
1072
2246
|
export function exposeFeedbackApi<
|
|
1073
2247
|
Name extends string | undefined,
|
|
1074
|
-
|
|
2248
|
+
RateReturnsValidator extends FeedbackRateLimitReturnValidator,
|
|
2249
|
+
CallbackReturnsValidator extends FeedbackCallbackReturnValidator,
|
|
1075
2250
|
>(
|
|
1076
2251
|
component: ComponentApi<Name>,
|
|
1077
|
-
options: ReturningExposeFeedbackOptions<
|
|
1078
|
-
|
|
2252
|
+
options: ReturningExposeFeedbackOptions<
|
|
2253
|
+
RateReturnsValidator,
|
|
2254
|
+
CallbackReturnsValidator
|
|
2255
|
+
>,
|
|
2256
|
+
): ExposedFeedbackApi<
|
|
2257
|
+
Infer<RateReturnsValidator>,
|
|
2258
|
+
Infer<CallbackReturnsValidator>
|
|
2259
|
+
>;
|
|
2260
|
+
|
|
2261
|
+
/** Exposes returning rate limits with throwing callback rejections. */
|
|
2262
|
+
export function exposeFeedbackApi<
|
|
2263
|
+
Name extends string | undefined,
|
|
2264
|
+
RateReturnsValidator extends FeedbackRateLimitReturnValidator,
|
|
2265
|
+
>(
|
|
2266
|
+
component: ComponentApi<Name>,
|
|
2267
|
+
options: ReturningExposeFeedbackOptions<RateReturnsValidator>,
|
|
2268
|
+
): ExposedFeedbackApi<Infer<RateReturnsValidator>, never>;
|
|
2269
|
+
|
|
2270
|
+
/** Exposes throwing rate limits with returning callback rejections. */
|
|
2271
|
+
export function exposeFeedbackApi<
|
|
2272
|
+
Name extends string | undefined,
|
|
2273
|
+
CallbackReturnsValidator extends FeedbackCallbackReturnValidator,
|
|
2274
|
+
>(
|
|
2275
|
+
component: ComponentApi<Name>,
|
|
2276
|
+
options: ThrowingExposeFeedbackOptions<CallbackReturnsValidator>,
|
|
2277
|
+
): ExposedFeedbackApi<never, Infer<CallbackReturnsValidator>>;
|
|
1079
2278
|
|
|
1080
2279
|
/** Exposes feedback using optional throwing rate limiters. */
|
|
1081
2280
|
export function exposeFeedbackApi<Name extends string | undefined>(
|
|
1082
2281
|
component: ComponentApi<Name>,
|
|
1083
2282
|
options: ThrowingExposeFeedbackOptions,
|
|
1084
|
-
): ExposedFeedbackApi<never>;
|
|
2283
|
+
): ExposedFeedbackApi<never, never>;
|
|
1085
2284
|
|
|
1086
2285
|
export function exposeFeedbackApi<
|
|
1087
2286
|
Name extends string | undefined,
|
|
1088
2287
|
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined,
|
|
2288
|
+
CallbackReturnsValidator extends FeedbackCallbackReturnValidator | undefined,
|
|
1089
2289
|
>(
|
|
1090
2290
|
component: ComponentApi<Name>,
|
|
1091
|
-
options: ExposeFeedbackOptions<ReturnsValidator>,
|
|
1092
|
-
): ExposedFeedbackApi<
|
|
2291
|
+
options: ExposeFeedbackOptions<ReturnsValidator, CallbackReturnsValidator>,
|
|
2292
|
+
): ExposedFeedbackApi<
|
|
2293
|
+
RateLimitResult<ReturnsValidator>,
|
|
2294
|
+
CallbackResult<CallbackReturnsValidator>
|
|
2295
|
+
> {
|
|
1093
2296
|
return buildFeedbackApi(component, options);
|
|
1094
2297
|
}
|