convex-feedback 0.1.0 → 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 +351 -92
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +267 -68
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/config.d.ts +168 -29
- package/dist/client/config.d.ts.map +1 -1
- package/dist/client/config.js.map +1 -1
- package/dist/client/index.d.ts +167 -172
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +75 -14
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +2 -2
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/comments.d.ts +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 +30 -30
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +216 -97
- package/dist/component/entries.js.map +1 -1
- package/dist/component/model.d.ts +151 -0
- package/dist/component/model.d.ts.map +1 -1
- package/dist/react/index.d.ts +178 -56
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +70 -4
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +326 -36
- package/src/client/config.ts +191 -29
- package/src/client/index.ts +437 -16
- package/src/component/_generated/component.ts +2 -2
- package/src/component/comments.ts +43 -61
- package/src/component/entries.ts +300 -123
- package/src/component/model.ts +158 -0
- package/src/react/index.ts +217 -9
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 {
|
|
@@ -43,15 +56,254 @@ export {
|
|
|
43
56
|
type FeedbackConfigOverrides,
|
|
44
57
|
} from "./config.js";
|
|
45
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Minimal host context exposed to the actor resolver.
|
|
61
|
+
*
|
|
62
|
+
* The feedback component itself does not read host authentication. Use
|
|
63
|
+
* `auth` to resolve the current host identity into a `FeedbackActor`.
|
|
64
|
+
*/
|
|
46
65
|
export interface FeedbackAuthContext {
|
|
66
|
+
/** Host Convex authentication interface for the current request. */
|
|
47
67
|
auth: Auth;
|
|
48
68
|
}
|
|
49
69
|
|
|
50
|
-
|
|
70
|
+
/**
|
|
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.
|
|
138
|
+
*/
|
|
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 {
|
|
232
|
+
/**
|
|
233
|
+
* Resolves the current request into a stable feedback actor.
|
|
234
|
+
*
|
|
235
|
+
* Return `null` for an unauthenticated request. Read-only queries may still
|
|
236
|
+
* execute, but mutations requiring an actor will reject unauthenticated
|
|
237
|
+
* callers.
|
|
238
|
+
*/
|
|
51
239
|
actor: (ctx: FeedbackAuthContext) => Promise<FeedbackActor | null>;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Optional component behavior overrides.
|
|
243
|
+
*
|
|
244
|
+
* Unspecified values use `defaultFeedbackConfig`.
|
|
245
|
+
*/
|
|
52
246
|
config?: FeedbackConfigOverrides;
|
|
53
247
|
}
|
|
54
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
|
+
|
|
55
307
|
function requireActor(actor: FeedbackActor | null): FeedbackActor {
|
|
56
308
|
if (actor === null) {
|
|
57
309
|
throw new ConvexError("Authentication is required.");
|
|
@@ -59,6 +311,62 @@ function requireActor(actor: FeedbackActor | null): FeedbackActor {
|
|
|
59
311
|
return actor;
|
|
60
312
|
}
|
|
61
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
|
+
|
|
62
370
|
function clampPositive(
|
|
63
371
|
value: number | undefined,
|
|
64
372
|
fallback: number,
|
|
@@ -85,29 +393,50 @@ function actorIdFields(actor: FeedbackActor | null): {
|
|
|
85
393
|
return actor === null ? {} : { viewerActorId: actor.id };
|
|
86
394
|
}
|
|
87
395
|
|
|
88
|
-
|
|
396
|
+
function buildFeedbackApi<
|
|
397
|
+
Name extends string | undefined,
|
|
398
|
+
ReturnsValidator extends FeedbackRateLimitReturnValidator | undefined =
|
|
399
|
+
undefined,
|
|
400
|
+
>(
|
|
89
401
|
component: ComponentApi<Name>,
|
|
90
|
-
options: ExposeFeedbackOptions
|
|
402
|
+
options: ExposeFeedbackOptions<ReturnsValidator>,
|
|
91
403
|
) {
|
|
92
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
|
+
);
|
|
93
421
|
|
|
94
422
|
return {
|
|
95
423
|
listEntries: queryGeneric({
|
|
96
424
|
args: {
|
|
97
425
|
paginationOpts: paginationOptsValidator,
|
|
98
|
-
|
|
426
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
99
427
|
status: v.optional(entryStatusValidator),
|
|
100
428
|
sort: v.optional(entrySortValidator),
|
|
101
429
|
},
|
|
102
430
|
returns: paginationResultValidator(publicEntryValidator),
|
|
103
431
|
handler: async (ctx, args) => {
|
|
104
432
|
const actor = await options.actor(ctx);
|
|
433
|
+
|
|
105
434
|
return await ctx.runQuery(component.entries.list, {
|
|
106
435
|
paginationOpts: clampPagination(
|
|
107
436
|
args.paginationOpts,
|
|
108
437
|
config.entries.maxPageSize,
|
|
109
438
|
),
|
|
110
|
-
...(args.
|
|
439
|
+
...(args.kinds === undefined ? {} : { kinds: args.kinds }),
|
|
111
440
|
...(args.status === undefined ? {} : { status: args.status }),
|
|
112
441
|
sort: args.sort ?? config.entries.defaultSort,
|
|
113
442
|
...actorIdFields(actor),
|
|
@@ -130,16 +459,17 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
130
459
|
searchEntries: queryGeneric({
|
|
131
460
|
args: {
|
|
132
461
|
searchQuery: v.string(),
|
|
133
|
-
|
|
462
|
+
kinds: v.optional(v.array(entryKindValidator)),
|
|
134
463
|
status: v.optional(entryStatusValidator),
|
|
135
464
|
limit: v.optional(v.number()),
|
|
136
465
|
},
|
|
137
466
|
returns: v.array(publicEntryValidator),
|
|
138
467
|
handler: async (ctx, args) => {
|
|
139
468
|
const actor = await options.actor(ctx);
|
|
469
|
+
|
|
140
470
|
return await ctx.runQuery(component.entries.search, {
|
|
141
471
|
searchQuery: args.searchQuery,
|
|
142
|
-
...(args.
|
|
472
|
+
...(args.kinds === undefined ? {} : { kinds: args.kinds }),
|
|
143
473
|
...(args.status === undefined ? {} : { status: args.status }),
|
|
144
474
|
limit: clampPositive(
|
|
145
475
|
args.limit,
|
|
@@ -163,7 +493,9 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
163
493
|
if (!config.search.duplicateSuggestions) {
|
|
164
494
|
return { exact: [], similar: [] };
|
|
165
495
|
}
|
|
496
|
+
|
|
166
497
|
const actor = await options.actor(ctx);
|
|
498
|
+
|
|
167
499
|
return await ctx.runQuery(component.entries.similar, {
|
|
168
500
|
title: args.title,
|
|
169
501
|
body: args.body,
|
|
@@ -184,9 +516,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
184
516
|
title: v.string(),
|
|
185
517
|
body: v.string(),
|
|
186
518
|
},
|
|
187
|
-
returns:
|
|
519
|
+
returns: idReturns,
|
|
188
520
|
handler: async (ctx, args) => {
|
|
189
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;
|
|
190
529
|
return await ctx.runMutation(component.entries.create, {
|
|
191
530
|
actorId: actor.id,
|
|
192
531
|
kind: args.kind,
|
|
@@ -206,9 +545,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
206
545
|
title: v.string(),
|
|
207
546
|
body: v.string(),
|
|
208
547
|
},
|
|
209
|
-
returns:
|
|
548
|
+
returns: nullReturns,
|
|
210
549
|
handler: async (ctx, args) => {
|
|
211
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;
|
|
212
558
|
return await ctx.runMutation(component.entries.update, {
|
|
213
559
|
actor,
|
|
214
560
|
entryId: args.entryId,
|
|
@@ -223,9 +569,17 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
223
569
|
|
|
224
570
|
setEntryStatus: mutationGeneric({
|
|
225
571
|
args: { entryId: v.string(), status: entryStatusValidator },
|
|
226
|
-
returns:
|
|
572
|
+
returns: nullReturns,
|
|
227
573
|
handler: async (ctx, args) => {
|
|
228
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;
|
|
229
583
|
return await ctx.runMutation(component.entries.setStatus, {
|
|
230
584
|
actor,
|
|
231
585
|
entryId: args.entryId,
|
|
@@ -236,9 +590,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
236
590
|
|
|
237
591
|
setEntryUpvote: mutationGeneric({
|
|
238
592
|
args: { entryId: v.string(), desiredState: v.boolean() },
|
|
239
|
-
returns:
|
|
593
|
+
returns: entryUpvoteReturns,
|
|
240
594
|
handler: async (ctx, args) => {
|
|
241
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;
|
|
242
603
|
return await ctx.runMutation(component.entries.setUpvote, {
|
|
243
604
|
actorId: actor.id,
|
|
244
605
|
entryId: args.entryId,
|
|
@@ -278,9 +639,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
278
639
|
parentCommentId: v.optional(v.string()),
|
|
279
640
|
body: v.string(),
|
|
280
641
|
},
|
|
281
|
-
returns:
|
|
642
|
+
returns: idReturns,
|
|
282
643
|
handler: async (ctx, args) => {
|
|
283
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;
|
|
284
652
|
return await ctx.runMutation(component.comments.create, {
|
|
285
653
|
actorId: actor.id,
|
|
286
654
|
entryId: args.entryId,
|
|
@@ -296,9 +664,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
296
664
|
|
|
297
665
|
updateComment: mutationGeneric({
|
|
298
666
|
args: { commentId: v.string(), body: v.string() },
|
|
299
|
-
returns:
|
|
667
|
+
returns: nullReturns,
|
|
300
668
|
handler: async (ctx, args) => {
|
|
301
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;
|
|
302
677
|
return await ctx.runMutation(component.comments.update, {
|
|
303
678
|
actor,
|
|
304
679
|
commentId: args.commentId,
|
|
@@ -311,9 +686,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
311
686
|
|
|
312
687
|
deleteComment: mutationGeneric({
|
|
313
688
|
args: { commentId: v.string() },
|
|
314
|
-
returns:
|
|
689
|
+
returns: nullReturns,
|
|
315
690
|
handler: async (ctx, args) => {
|
|
316
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;
|
|
317
699
|
return await ctx.runMutation(component.comments.remove, {
|
|
318
700
|
actor,
|
|
319
701
|
commentId: args.commentId,
|
|
@@ -324,9 +706,16 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
324
706
|
|
|
325
707
|
setCommentLike: mutationGeneric({
|
|
326
708
|
args: { commentId: v.string(), desiredState: v.boolean() },
|
|
327
|
-
returns:
|
|
709
|
+
returns: commentLikeReturns,
|
|
328
710
|
handler: async (ctx, args) => {
|
|
329
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;
|
|
330
719
|
return await ctx.runMutation(component.comments.setLike, {
|
|
331
720
|
actorId: actor.id,
|
|
332
721
|
commentId: args.commentId,
|
|
@@ -336,3 +725,35 @@ export function exposeFeedbackApi<Name extends string | undefined>(
|
|
|
336
725
|
}),
|
|
337
726
|
} satisfies Record<keyof FeedbackPublicApi, unknown>;
|
|
338
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
|
+
}
|
|
@@ -161,7 +161,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
161
161
|
"query",
|
|
162
162
|
"internal",
|
|
163
163
|
{
|
|
164
|
-
|
|
164
|
+
kinds?: Array<"feedback" | "feature_request" | "bug_report">;
|
|
165
165
|
paginationOpts: {
|
|
166
166
|
cursor: string | null;
|
|
167
167
|
endCursor?: string | null;
|
|
@@ -211,7 +211,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
211
211
|
"query",
|
|
212
212
|
"internal",
|
|
213
213
|
{
|
|
214
|
-
|
|
214
|
+
kinds?: Array<"feedback" | "feature_request" | "bug_report">;
|
|
215
215
|
limit: number;
|
|
216
216
|
searchQuery: string;
|
|
217
217
|
status?:
|