convex-feedback 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +21 -17
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/index.d.ts +146 -174
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +71 -10
- package/dist/client/index.js.map +1 -1
- package/dist/component/comments.d.ts +12 -12
- package/dist/component/comments.d.ts.map +1 -1
- package/dist/component/comments.js +43 -60
- package/dist/component/comments.js.map +1 -1
- package/dist/component/entries.d.ts +29 -29
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +18 -25
- package/dist/component/entries.js.map +1 -1
- package/dist/react/index.d.ts +16 -10
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +21 -16
- package/src/client/index.ts +407 -14
- package/src/component/comments.ts +43 -61
- package/src/component/entries.ts +18 -22
- package/src/react/index.ts +11 -3
package/src/client/index.ts
CHANGED
|
@@ -4,9 +4,22 @@ import {
|
|
|
4
4
|
paginationResultValidator,
|
|
5
5
|
queryGeneric,
|
|
6
6
|
type Auth,
|
|
7
|
+
type DefaultFunctionArgs,
|
|
8
|
+
type FunctionReference,
|
|
9
|
+
type GenericDataModel,
|
|
10
|
+
type GenericMutationCtx,
|
|
7
11
|
type PaginationOptions,
|
|
12
|
+
type RegisteredMutation,
|
|
13
|
+
type RegisteredQuery,
|
|
8
14
|
} from "convex/server";
|
|
9
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ConvexError,
|
|
17
|
+
v,
|
|
18
|
+
type Infer,
|
|
19
|
+
type Validator,
|
|
20
|
+
type Value,
|
|
21
|
+
type VUnion,
|
|
22
|
+
} from "convex/values";
|
|
10
23
|
|
|
11
24
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
12
25
|
import {
|
|
@@ -55,10 +68,167 @@ export interface FeedbackAuthContext {
|
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
71
|
+
* Minimal host mutation context passed to a feedback rate limiter.
|
|
72
|
+
*
|
|
73
|
+
* This provides the nested function calls needed by Convex rate-limiter
|
|
74
|
+
* helpers without coupling limiters to the host application's data model.
|
|
75
|
+
*/
|
|
76
|
+
export type FeedbackRateLimitContext = Pick<
|
|
77
|
+
GenericMutationCtx<GenericDataModel>,
|
|
78
|
+
"runQuery" | "runMutation"
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A host-defined rate limiter keyed by the resolved feedback actor ID.
|
|
83
|
+
*
|
|
84
|
+
* In the default `"throw"` mode, return `undefined` when allowed and throw when
|
|
85
|
+
* rejected. In `"return"` mode, return `undefined` when allowed or a value
|
|
86
|
+
* matching the configured `returns` validator when rejected. `null` is never
|
|
87
|
+
* a valid return-mode result.
|
|
88
|
+
*
|
|
89
|
+
* @typeParam Result Rejection value used only by non-throwing limiters.
|
|
90
|
+
*/
|
|
91
|
+
export type FeedbackRateLimiter<Result = void> = (
|
|
92
|
+
ctx: FeedbackRateLimitContext,
|
|
93
|
+
key: string,
|
|
94
|
+
) => Promise<Result | undefined>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Rate limiter hooks shared by related feedback mutations.
|
|
98
|
+
*
|
|
99
|
+
* @typeParam Result Rejection value used only in `"return"` mode.
|
|
100
|
+
*/
|
|
101
|
+
export interface FeedbackRateLimiters<Result = void> {
|
|
102
|
+
/** Applied when creating an entry. */
|
|
103
|
+
createEntry?: FeedbackRateLimiter<Result>;
|
|
104
|
+
|
|
105
|
+
/** Applied when creating a comment or reply. */
|
|
106
|
+
createComment?: FeedbackRateLimiter<Result>;
|
|
107
|
+
|
|
108
|
+
/** Applied to entry edits, status changes, comment edits, and deletion. */
|
|
109
|
+
editContent?: FeedbackRateLimiter<Result>;
|
|
110
|
+
|
|
111
|
+
/** Applied to entry upvotes and comment likes. */
|
|
112
|
+
reactions?: FeedbackRateLimiter<Result>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Configuration for the default behavior, where limiters reject by throwing. */
|
|
116
|
+
export interface ThrowingFeedbackRateLimitConfig {
|
|
117
|
+
/**
|
|
118
|
+
* Configures limiter functions to reject requests by throwing.
|
|
119
|
+
*
|
|
120
|
+
* This is the default when `behavior` is omitted.
|
|
121
|
+
*/
|
|
122
|
+
behavior?: "throw";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether all configured limiter groups apply to moderators.
|
|
126
|
+
*
|
|
127
|
+
* @default false
|
|
128
|
+
*/
|
|
129
|
+
limitModerators?: boolean;
|
|
130
|
+
|
|
131
|
+
/** Not accepted in throwing mode; select `"return"` to provide a validator. */
|
|
132
|
+
returns?: never;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A required, non-optional Convex validator for a non-null rate-limit
|
|
137
|
+
* rejection.
|
|
60
138
|
*/
|
|
61
|
-
export
|
|
139
|
+
export type FeedbackRateLimitReturnValidator = Validator<
|
|
140
|
+
Exclude<Value, null>,
|
|
141
|
+
"required",
|
|
142
|
+
string
|
|
143
|
+
>;
|
|
144
|
+
|
|
145
|
+
type FeedbackFunctionReturnValidator = Validator<Value, "required", string>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Return behavior: a defined limiter result short-circuits the mutation.
|
|
149
|
+
*
|
|
150
|
+
* @typeParam ReturnsValidator Validator for the value returned on rejection.
|
|
151
|
+
*/
|
|
152
|
+
export interface ReturningFeedbackRateLimitConfig<
|
|
153
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator,
|
|
154
|
+
> {
|
|
155
|
+
/** Configures limiter functions to return rejected requests to the client. */
|
|
156
|
+
behavior: "return";
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Convex validator for the non-null rejection value returned by a limiter.
|
|
160
|
+
*
|
|
161
|
+
* This field is required when `behavior` is `"return"` and may not accept
|
|
162
|
+
* `null`. Its inferred value type is added to the result type of every
|
|
163
|
+
* exposed feedback mutation.
|
|
164
|
+
*/
|
|
165
|
+
returns: ReturnsValidator;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Whether all configured limiter groups apply to moderators.
|
|
169
|
+
*
|
|
170
|
+
* @default false
|
|
171
|
+
*/
|
|
172
|
+
limitModerators?: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Configuration for feedback rate-limit rejection behavior.
|
|
177
|
+
*
|
|
178
|
+
* Without a validator, this resolves to throwing behavior. Supplying a
|
|
179
|
+
* validator requires `behavior: "return"` and the same validator in `returns`.
|
|
180
|
+
*
|
|
181
|
+
* @typeParam ReturnsValidator Validator for a non-throwing rejection value.
|
|
182
|
+
*/
|
|
183
|
+
export type FeedbackRateLimitConfig<
|
|
184
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined =
|
|
185
|
+
undefined,
|
|
186
|
+
> = ReturnsValidator extends FeedbackRateLimitReturnValidator
|
|
187
|
+
? ReturningFeedbackRateLimitConfig<ReturnsValidator>
|
|
188
|
+
: ThrowingFeedbackRateLimitConfig;
|
|
189
|
+
|
|
190
|
+
type RateLimitResult<
|
|
191
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined,
|
|
192
|
+
> = ReturnsValidator extends FeedbackRateLimitReturnValidator
|
|
193
|
+
? Infer<ReturnsValidator>
|
|
194
|
+
: never;
|
|
195
|
+
|
|
196
|
+
type RateLimiterResult<
|
|
197
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined,
|
|
198
|
+
> = ReturnsValidator extends FeedbackRateLimitReturnValidator
|
|
199
|
+
? Infer<ReturnsValidator>
|
|
200
|
+
: void;
|
|
201
|
+
|
|
202
|
+
type RegisteredFeedbackFunction<Function> =
|
|
203
|
+
Function extends FunctionReference<
|
|
204
|
+
"mutation",
|
|
205
|
+
"public",
|
|
206
|
+
infer Args,
|
|
207
|
+
infer Result
|
|
208
|
+
>
|
|
209
|
+
? Args extends DefaultFunctionArgs
|
|
210
|
+
? RegisteredMutation<"public", Args, Promise<Result>>
|
|
211
|
+
: never
|
|
212
|
+
: Function extends FunctionReference<
|
|
213
|
+
"query",
|
|
214
|
+
"public",
|
|
215
|
+
infer Args,
|
|
216
|
+
infer Result
|
|
217
|
+
>
|
|
218
|
+
? Args extends DefaultFunctionArgs
|
|
219
|
+
? RegisteredQuery<"public", Args, Promise<Result>>
|
|
220
|
+
: never
|
|
221
|
+
: never;
|
|
222
|
+
|
|
223
|
+
type ExposedFeedbackApi<RateLimitResult> = {
|
|
224
|
+
[
|
|
225
|
+
FunctionName in keyof FeedbackPublicApi<string | undefined, RateLimitResult>
|
|
226
|
+
]: RegisteredFeedbackFunction<
|
|
227
|
+
FeedbackPublicApi<string | undefined, RateLimitResult>[FunctionName]
|
|
228
|
+
>;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
interface ExposeFeedbackOptionsBase {
|
|
62
232
|
/**
|
|
63
233
|
* Resolves the current request into a stable feedback actor.
|
|
64
234
|
*
|
|
@@ -76,6 +246,64 @@ export interface ExposeFeedbackOptions {
|
|
|
76
246
|
config?: FeedbackConfigOverrides;
|
|
77
247
|
}
|
|
78
248
|
|
|
249
|
+
/** Options for the default mode, where limiter functions reject by throwing. */
|
|
250
|
+
export type ThrowingExposeFeedbackOptions = Omit<
|
|
251
|
+
ExposeFeedbackOptionsBase,
|
|
252
|
+
"config"
|
|
253
|
+
> & {
|
|
254
|
+
/** Optional throwing rate limiters for the component's mutation groups. */
|
|
255
|
+
rateLimiters?: FeedbackRateLimiters;
|
|
256
|
+
|
|
257
|
+
/** Optional component behavior and throwing rate-limit overrides. */
|
|
258
|
+
config?: FeedbackConfigOverrides & {
|
|
259
|
+
/**
|
|
260
|
+
* Controls throwing behavior and the moderator exemption.
|
|
261
|
+
*
|
|
262
|
+
* Omit this object to use `behavior: "throw"` and
|
|
263
|
+
* `limitModerators: false`.
|
|
264
|
+
*/
|
|
265
|
+
rateLimiting?: FeedbackRateLimitConfig;
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** Options for returning a validated rejection value instead of throwing. */
|
|
270
|
+
export type ReturningExposeFeedbackOptions<
|
|
271
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator,
|
|
272
|
+
> = Omit<ExposeFeedbackOptionsBase, "config"> & {
|
|
273
|
+
/**
|
|
274
|
+
* Optional returning rate limiters for the component's mutation groups.
|
|
275
|
+
*
|
|
276
|
+
* A limiter returns `undefined` to allow the request. Any other returned
|
|
277
|
+
* value must match `config.rateLimiting.returns` and prevents the component
|
|
278
|
+
* mutation from running.
|
|
279
|
+
*/
|
|
280
|
+
rateLimiters?: FeedbackRateLimiters<NoInfer<Infer<ReturnsValidator>>>;
|
|
281
|
+
|
|
282
|
+
/** Component behavior plus the required non-throwing limiter configuration. */
|
|
283
|
+
config: FeedbackConfigOverrides & {
|
|
284
|
+
/**
|
|
285
|
+
* Configures non-throwing rejection behavior.
|
|
286
|
+
*
|
|
287
|
+
* Both `behavior: "return"` and a `returns` Convex validator are required.
|
|
288
|
+
*/
|
|
289
|
+
rateLimiting: FeedbackRateLimitConfig<ReturnsValidator>;
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Configuration used when exposing feedback functions from the host app.
|
|
295
|
+
*
|
|
296
|
+
* When `ReturnsValidator` is omitted, limiter functions use throwing behavior.
|
|
297
|
+
* Supplying a validator selects return behavior and adds its inferred value to
|
|
298
|
+
* the exposed mutation result types.
|
|
299
|
+
*/
|
|
300
|
+
export type ExposeFeedbackOptions<
|
|
301
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined =
|
|
302
|
+
undefined,
|
|
303
|
+
> = ReturnsValidator extends FeedbackRateLimitReturnValidator
|
|
304
|
+
? ReturningExposeFeedbackOptions<ReturnsValidator>
|
|
305
|
+
: ThrowingExposeFeedbackOptions;
|
|
306
|
+
|
|
79
307
|
function requireActor(actor: FeedbackActor | null): FeedbackActor {
|
|
80
308
|
if (actor === null) {
|
|
81
309
|
throw new ConvexError("Authentication is required.");
|
|
@@ -83,6 +311,62 @@ function requireActor(actor: FeedbackActor | null): FeedbackActor {
|
|
|
83
311
|
return actor;
|
|
84
312
|
}
|
|
85
313
|
|
|
314
|
+
function requireModerator(actor: FeedbackActor): void {
|
|
315
|
+
if (!actor.isModerator) {
|
|
316
|
+
throw new ConvexError("Moderator permissions are required.");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
type RateLimitedReturnsValidator<
|
|
321
|
+
Base extends FeedbackFunctionReturnValidator,
|
|
322
|
+
Limit extends FeedbackRateLimitReturnValidator | undefined,
|
|
323
|
+
> = Limit extends FeedbackRateLimitReturnValidator
|
|
324
|
+
? VUnion<Infer<Base> | Infer<Limit>, [Base, Limit]>
|
|
325
|
+
: Base;
|
|
326
|
+
|
|
327
|
+
function rateLimitedReturns<
|
|
328
|
+
Base extends FeedbackFunctionReturnValidator,
|
|
329
|
+
Limit extends FeedbackRateLimitReturnValidator | undefined,
|
|
330
|
+
>(base: Base, limit: Limit): RateLimitedReturnsValidator<Base, Limit> {
|
|
331
|
+
return (
|
|
332
|
+
limit === undefined ? base : v.union(base, limit)
|
|
333
|
+
) as RateLimitedReturnsValidator<Base, Limit>;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function applyRateLimiter<
|
|
337
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined,
|
|
338
|
+
>(
|
|
339
|
+
ctx: FeedbackRateLimitContext,
|
|
340
|
+
actor: FeedbackActor,
|
|
341
|
+
limiter: FeedbackRateLimiter<RateLimiterResult<ReturnsValidator>> | undefined,
|
|
342
|
+
rateLimitConfig:
|
|
343
|
+
| ThrowingFeedbackRateLimitConfig
|
|
344
|
+
| ReturningFeedbackRateLimitConfig<FeedbackRateLimitReturnValidator>
|
|
345
|
+
| undefined,
|
|
346
|
+
): Promise<RateLimitResult<ReturnsValidator> | undefined> {
|
|
347
|
+
if (
|
|
348
|
+
limiter === undefined ||
|
|
349
|
+
(actor.isModerator && rateLimitConfig?.limitModerators !== true)
|
|
350
|
+
) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const result = await limiter(ctx, actor.id);
|
|
355
|
+
if (rateLimitConfig?.behavior !== "return" || result === undefined) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
if (result === null) {
|
|
359
|
+
throw new ConvexError(
|
|
360
|
+
"A return-mode feedback rate limiter returned null. Return undefined to allow the request or a non-null value matching config.rateLimiting.returns to reject it.",
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return result as RateLimitResult<ReturnsValidator>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function asRateLimitContext(ctx: unknown): FeedbackRateLimitContext {
|
|
367
|
+
return ctx as FeedbackRateLimitContext;
|
|
368
|
+
}
|
|
369
|
+
|
|
86
370
|
function clampPositive(
|
|
87
371
|
value: number | undefined,
|
|
88
372
|
fallback: number,
|
|
@@ -109,11 +393,31 @@ function actorIdFields(actor: FeedbackActor | null): {
|
|
|
109
393
|
return actor === null ? {} : { viewerActorId: actor.id };
|
|
110
394
|
}
|
|
111
395
|
|
|
112
|
-
|
|
396
|
+
function buildFeedbackApi<
|
|
397
|
+
Name extends string | undefined,
|
|
398
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined =
|
|
399
|
+
undefined,
|
|
400
|
+
>(
|
|
113
401
|
component: ComponentApi<Name>,
|
|
114
|
-
options: ExposeFeedbackOptions
|
|
402
|
+
options: ExposeFeedbackOptions<ReturnsValidator>,
|
|
115
403
|
) {
|
|
116
404
|
const config = createFeedbackConfig(options.config);
|
|
405
|
+
const rateLimitConfig = options.config?.rateLimiting;
|
|
406
|
+
const rateLimitReturnValidator =
|
|
407
|
+
rateLimitConfig?.behavior === "return"
|
|
408
|
+
? rateLimitConfig.returns
|
|
409
|
+
: undefined;
|
|
410
|
+
|
|
411
|
+
const idReturns = rateLimitedReturns(v.string(), rateLimitReturnValidator);
|
|
412
|
+
const nullReturns = rateLimitedReturns(v.null(), rateLimitReturnValidator);
|
|
413
|
+
const entryUpvoteReturns = rateLimitedReturns(
|
|
414
|
+
v.object({ active: v.boolean(), upvoteCount: v.number() }),
|
|
415
|
+
rateLimitReturnValidator,
|
|
416
|
+
);
|
|
417
|
+
const commentLikeReturns = rateLimitedReturns(
|
|
418
|
+
v.object({ active: v.boolean(), likeCount: v.number() }),
|
|
419
|
+
rateLimitReturnValidator,
|
|
420
|
+
);
|
|
117
421
|
|
|
118
422
|
return {
|
|
119
423
|
listEntries: queryGeneric({
|
|
@@ -212,9 +516,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
212
516
|
title: v.string(),
|
|
213
517
|
body: v.string(),
|
|
214
518
|
},
|
|
215
|
-
returns:
|
|
519
|
+
returns: idReturns,
|
|
216
520
|
handler: async (ctx, args) => {
|
|
217
521
|
const actor = requireActor(await options.actor(ctx));
|
|
522
|
+
const limited = await applyRateLimiter(
|
|
523
|
+
asRateLimitContext(ctx),
|
|
524
|
+
actor,
|
|
525
|
+
options.rateLimiters?.createEntry,
|
|
526
|
+
rateLimitConfig,
|
|
527
|
+
);
|
|
528
|
+
if (limited !== undefined) return limited;
|
|
218
529
|
return await ctx.runMutation(component.entries.create, {
|
|
219
530
|
actorId: actor.id,
|
|
220
531
|
kind: args.kind,
|
|
@@ -234,9 +545,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
234
545
|
title: v.string(),
|
|
235
546
|
body: v.string(),
|
|
236
547
|
},
|
|
237
|
-
returns:
|
|
548
|
+
returns: nullReturns,
|
|
238
549
|
handler: async (ctx, args) => {
|
|
239
550
|
const actor = requireActor(await options.actor(ctx));
|
|
551
|
+
const limited = await applyRateLimiter(
|
|
552
|
+
asRateLimitContext(ctx),
|
|
553
|
+
actor,
|
|
554
|
+
options.rateLimiters?.editContent,
|
|
555
|
+
rateLimitConfig,
|
|
556
|
+
);
|
|
557
|
+
if (limited !== undefined) return limited;
|
|
240
558
|
return await ctx.runMutation(component.entries.update, {
|
|
241
559
|
actor,
|
|
242
560
|
entryId: args.entryId,
|
|
@@ -251,9 +569,17 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
251
569
|
|
|
252
570
|
setEntryStatus: mutationGeneric({
|
|
253
571
|
args: { entryId: v.string(), status: entryStatusValidator },
|
|
254
|
-
returns:
|
|
572
|
+
returns: nullReturns,
|
|
255
573
|
handler: async (ctx, args) => {
|
|
256
574
|
const actor = requireActor(await options.actor(ctx));
|
|
575
|
+
requireModerator(actor);
|
|
576
|
+
const limited = await applyRateLimiter(
|
|
577
|
+
asRateLimitContext(ctx),
|
|
578
|
+
actor,
|
|
579
|
+
options.rateLimiters?.editContent,
|
|
580
|
+
rateLimitConfig,
|
|
581
|
+
);
|
|
582
|
+
if (limited !== undefined) return limited;
|
|
257
583
|
return await ctx.runMutation(component.entries.setStatus, {
|
|
258
584
|
actor,
|
|
259
585
|
entryId: args.entryId,
|
|
@@ -264,9 +590,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
264
590
|
|
|
265
591
|
setEntryUpvote: mutationGeneric({
|
|
266
592
|
args: { entryId: v.string(), desiredState: v.boolean() },
|
|
267
|
-
returns:
|
|
593
|
+
returns: entryUpvoteReturns,
|
|
268
594
|
handler: async (ctx, args) => {
|
|
269
595
|
const actor = requireActor(await options.actor(ctx));
|
|
596
|
+
const limited = await applyRateLimiter(
|
|
597
|
+
asRateLimitContext(ctx),
|
|
598
|
+
actor,
|
|
599
|
+
options.rateLimiters?.reactions,
|
|
600
|
+
rateLimitConfig,
|
|
601
|
+
);
|
|
602
|
+
if (limited !== undefined) return limited;
|
|
270
603
|
return await ctx.runMutation(component.entries.setUpvote, {
|
|
271
604
|
actorId: actor.id,
|
|
272
605
|
entryId: args.entryId,
|
|
@@ -306,9 +639,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
306
639
|
parentCommentId: v.optional(v.string()),
|
|
307
640
|
body: v.string(),
|
|
308
641
|
},
|
|
309
|
-
returns:
|
|
642
|
+
returns: idReturns,
|
|
310
643
|
handler: async (ctx, args) => {
|
|
311
644
|
const actor = requireActor(await options.actor(ctx));
|
|
645
|
+
const limited = await applyRateLimiter(
|
|
646
|
+
asRateLimitContext(ctx),
|
|
647
|
+
actor,
|
|
648
|
+
options.rateLimiters?.createComment,
|
|
649
|
+
rateLimitConfig,
|
|
650
|
+
);
|
|
651
|
+
if (limited !== undefined) return limited;
|
|
312
652
|
return await ctx.runMutation(component.comments.create, {
|
|
313
653
|
actorId: actor.id,
|
|
314
654
|
entryId: args.entryId,
|
|
@@ -324,9 +664,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
324
664
|
|
|
325
665
|
updateComment: mutationGeneric({
|
|
326
666
|
args: { commentId: v.string(), body: v.string() },
|
|
327
|
-
returns:
|
|
667
|
+
returns: nullReturns,
|
|
328
668
|
handler: async (ctx, args) => {
|
|
329
669
|
const actor = requireActor(await options.actor(ctx));
|
|
670
|
+
const limited = await applyRateLimiter(
|
|
671
|
+
asRateLimitContext(ctx),
|
|
672
|
+
actor,
|
|
673
|
+
options.rateLimiters?.editContent,
|
|
674
|
+
rateLimitConfig,
|
|
675
|
+
);
|
|
676
|
+
if (limited !== undefined) return limited;
|
|
330
677
|
return await ctx.runMutation(component.comments.update, {
|
|
331
678
|
actor,
|
|
332
679
|
commentId: args.commentId,
|
|
@@ -339,9 +686,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
339
686
|
|
|
340
687
|
deleteComment: mutationGeneric({
|
|
341
688
|
args: { commentId: v.string() },
|
|
342
|
-
returns:
|
|
689
|
+
returns: nullReturns,
|
|
343
690
|
handler: async (ctx, args) => {
|
|
344
691
|
const actor = requireActor(await options.actor(ctx));
|
|
692
|
+
const limited = await applyRateLimiter(
|
|
693
|
+
asRateLimitContext(ctx),
|
|
694
|
+
actor,
|
|
695
|
+
options.rateLimiters?.editContent,
|
|
696
|
+
rateLimitConfig,
|
|
697
|
+
);
|
|
698
|
+
if (limited !== undefined) return limited;
|
|
345
699
|
return await ctx.runMutation(component.comments.remove, {
|
|
346
700
|
actor,
|
|
347
701
|
commentId: args.commentId,
|
|
@@ -352,9 +706,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
352
706
|
|
|
353
707
|
setCommentLike: mutationGeneric({
|
|
354
708
|
args: { commentId: v.string(), desiredState: v.boolean() },
|
|
355
|
-
returns:
|
|
709
|
+
returns: commentLikeReturns,
|
|
356
710
|
handler: async (ctx, args) => {
|
|
357
711
|
const actor = requireActor(await options.actor(ctx));
|
|
712
|
+
const limited = await applyRateLimiter(
|
|
713
|
+
asRateLimitContext(ctx),
|
|
714
|
+
actor,
|
|
715
|
+
options.rateLimiters?.reactions,
|
|
716
|
+
rateLimitConfig,
|
|
717
|
+
);
|
|
718
|
+
if (limited !== undefined) return limited;
|
|
358
719
|
return await ctx.runMutation(component.comments.setLike, {
|
|
359
720
|
actorId: actor.id,
|
|
360
721
|
commentId: args.commentId,
|
|
@@ -364,3 +725,35 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
364
725
|
}),
|
|
365
726
|
} satisfies Record<keyof FeedbackPublicApi, unknown>;
|
|
366
727
|
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Exposes the feedback component through host queries and mutations.
|
|
731
|
+
*
|
|
732
|
+
* Rate limiters use throwing behavior unless
|
|
733
|
+
* `config.rateLimiting.behavior` is `"return"`. Return behavior requires a
|
|
734
|
+
* `returns` validator and adds that validator's inferred type to every
|
|
735
|
+
* mutation result.
|
|
736
|
+
*/
|
|
737
|
+
export function exposeFeedbackApi<
|
|
738
|
+
Name extends string | undefined,
|
|
739
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator,
|
|
740
|
+
>(
|
|
741
|
+
component: ComponentApi<Name>,
|
|
742
|
+
options: ReturningExposeFeedbackOptions<ReturnsValidator>,
|
|
743
|
+
): ExposedFeedbackApi<Infer<ReturnsValidator>>;
|
|
744
|
+
|
|
745
|
+
/** Exposes feedback using optional throwing rate limiters. */
|
|
746
|
+
export function exposeFeedbackApi<Name extends string | undefined>(
|
|
747
|
+
component: ComponentApi<Name>,
|
|
748
|
+
options: ThrowingExposeFeedbackOptions,
|
|
749
|
+
): ExposedFeedbackApi<never>;
|
|
750
|
+
|
|
751
|
+
export function exposeFeedbackApi<
|
|
752
|
+
Name extends string | undefined,
|
|
753
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined,
|
|
754
|
+
>(
|
|
755
|
+
component: ComponentApi<Name>,
|
|
756
|
+
options: ExposeFeedbackOptions<ReturnsValidator>,
|
|
757
|
+
): ExposedFeedbackApi<RateLimitResult<ReturnsValidator>> {
|
|
758
|
+
return buildFeedbackApi(component, options);
|
|
759
|
+
}
|