kitcn 0.16.0 → 0.17.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/dist/aggregate/index.d.ts +1 -1
- package/dist/auth/client/index.js +1 -1
- package/dist/auth/index.js +19 -21
- package/dist/auth/nextjs/index.d.ts +1 -1
- package/dist/auth/nextjs/index.js +4 -4
- package/dist/{auth-store-ssZDPa37.js → auth-store-BnGZxmnY.js} +4 -1
- package/dist/{backend-core-DqPydYyx.mjs → backend-core-BsKP1LVg.mjs} +204 -164
- package/dist/{builder-DBgto1yn.js → builder-f4F_NRvK.js} +245 -153
- package/dist/{caller-factory-NEfgD5E0.js → caller-factory-DHywSoGZ.js} +7 -5
- package/dist/cli.mjs +14 -7
- package/dist/crpc/index.js +1 -127
- package/dist/{middleware-Bg-PdtrI.js → middleware-Cgrv2jIu.js} +1 -1
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js +486 -121
- package/dist/plugins/index.js +1 -1
- package/dist/{procedure-caller-9m6NBxQu.js → procedure-caller-Rj6z3ai7.js} +1 -1
- package/dist/{procedure-name-Cy1AxayA.d.ts → procedure-name-Bo5KMcqc.d.ts} +15 -3
- package/dist/{query-context-ydn9kb6P.js → query-context-C90vNlc9.js} +131 -30
- package/dist/query-options-C_eBSIXG.js +247 -0
- package/dist/ratelimit/index.d.ts +26 -6
- package/dist/ratelimit/index.js +427 -100
- package/dist/ratelimit/react/index.d.ts +14 -0
- package/dist/ratelimit/react/index.js +149 -16
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.js +48 -15
- package/dist/rsc/index.js +22 -33
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +3 -3
- package/dist/solid/index.js +19 -5
- package/dist/watcher.mjs +2 -2
- package/dist/{where-clause-compiler-WF9UcrAB.d.ts → where-clause-compiler-BRhLW1dp.d.ts} +51 -0
- package/package.json +1 -1
- package/skills/kitcn/SKILL.md +1 -0
- package/skills/kitcn/references/features/create-plugins.md +1 -1
- package/skills/kitcn/references/features/orm.md +11 -1
- package/skills/kitcn/references/features/ratelimit.md +105 -0
- package/skills/kitcn/references/setup/server.md +1 -1
- package/dist/query-options-C96zLANM.js +0 -121
package/dist/plugins/index.js
CHANGED
|
@@ -734,7 +734,12 @@ declare function createMiddlewareFactory<TDefaultContext, TMeta = object>(): <TC
|
|
|
734
734
|
/** Internal definition storing procedure state */
|
|
735
735
|
type ProcedureBuilderDef<TMeta = object> = {
|
|
736
736
|
middlewares: AnyMiddleware[];
|
|
737
|
-
|
|
737
|
+
/**
|
|
738
|
+
* Full `.input()` schemas, not their shapes: object-level checks
|
|
739
|
+
* (`.refine()`, `.superRefine()`, strict/catchall) only survive on the
|
|
740
|
+
* schema itself.
|
|
741
|
+
*/
|
|
742
|
+
inputSchemas: z.ZodObject<any>[];
|
|
738
743
|
outputSchema?: z.ZodTypeAny;
|
|
739
744
|
meta?: TMeta;
|
|
740
745
|
procedureName?: string;
|
|
@@ -764,7 +769,7 @@ declare class ProcedureBuilder<TBaseCtx, _TContext, TContextOverrides extends Un
|
|
|
764
769
|
protected _meta(value: TMeta): ProcedureBuilderDef<TMeta>;
|
|
765
770
|
/** Set server-only procedure name for middleware/logging */
|
|
766
771
|
protected _name(value: string): ProcedureBuilderDef<TMeta>;
|
|
767
|
-
/** Merge
|
|
772
|
+
/** Merge every `.input()` shape - used to build the Convex arg validator */
|
|
768
773
|
protected _getMergedInput(): Record<string, any> | undefined;
|
|
769
774
|
protected _createFunction(handler: any, baseFunction: any, customFn: typeof zCustomQuery | typeof zCustomMutation | typeof zCustomAction, fnType: 'query' | 'mutation' | 'action'): Record<string, unknown>;
|
|
770
775
|
}
|
|
@@ -1076,7 +1081,14 @@ type TokenResult = {
|
|
|
1076
1081
|
type GetTokenFn = (siteUrl: string, headers: Headers, opts?: unknown) => Promise<TokenResult>;
|
|
1077
1082
|
/** Auth options for server-side calls. */
|
|
1078
1083
|
type AuthOptions = {
|
|
1079
|
-
/** Function to extract auth token from request headers. */getToken: GetTokenFn;
|
|
1084
|
+
/** Function to extract auth token from request headers. */getToken: GetTokenFn;
|
|
1085
|
+
/**
|
|
1086
|
+
* Custom function to detect UNAUTHORIZED errors.
|
|
1087
|
+
* Set this to resolve those errors to `null` instead of throwing.
|
|
1088
|
+
*
|
|
1089
|
+
* Refreshing an expired cached token and retrying does not require this:
|
|
1090
|
+
* that always falls back to `defaultIsUnauthorized`.
|
|
1091
|
+
*/
|
|
1080
1092
|
isUnauthorized?: (error: unknown) => boolean;
|
|
1081
1093
|
};
|
|
1082
1094
|
type CreateCallerFactoryOptions<TApi> = {
|
|
@@ -23,6 +23,71 @@ function isFieldReference(value) {
|
|
|
23
23
|
return value && typeof value === "object" && value.__brand === "FieldReference";
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
+
* SQL `LIKE` semantics: `%` matches any run of characters, `_` matches exactly
|
|
27
|
+
* one, everything else is literal. Wildcards work anywhere in the pattern, not
|
|
28
|
+
* only at the ends.
|
|
29
|
+
*
|
|
30
|
+
* Matched with a single-backtrack scan rather than a compiled regex. A pattern
|
|
31
|
+
* is caller data — anything interpolated into `` `%${query}%` `` reaches here —
|
|
32
|
+
* and translating `%` to a regex quantifier makes `'%%%%%z'` backtrack
|
|
33
|
+
* exponentially, which would burn the whole Convex function CPU budget on one
|
|
34
|
+
* row. This scan remembers only the most recent `%`, so the worst case is
|
|
35
|
+
* O(value * pattern) with no catastrophic case at all.
|
|
36
|
+
*/
|
|
37
|
+
function matchLikePattern(value, pattern, caseInsensitive) {
|
|
38
|
+
const target = Array.from(caseInsensitive ? value.toLowerCase() : value);
|
|
39
|
+
const source = Array.from(caseInsensitive ? pattern.toLowerCase() : pattern);
|
|
40
|
+
let valueIndex = 0;
|
|
41
|
+
let patternIndex = 0;
|
|
42
|
+
let wildcardPatternIndex = -1;
|
|
43
|
+
let wildcardValueIndex = 0;
|
|
44
|
+
while (valueIndex < target.length) {
|
|
45
|
+
const patternChar = patternIndex < source.length ? source[patternIndex] : void 0;
|
|
46
|
+
if (patternChar === "_" || patternChar === target[valueIndex]) {
|
|
47
|
+
valueIndex += 1;
|
|
48
|
+
patternIndex += 1;
|
|
49
|
+
} else if (patternChar === "%") {
|
|
50
|
+
wildcardPatternIndex = patternIndex;
|
|
51
|
+
wildcardValueIndex = valueIndex;
|
|
52
|
+
patternIndex += 1;
|
|
53
|
+
} else if (wildcardPatternIndex === -1) return false;
|
|
54
|
+
else {
|
|
55
|
+
wildcardValueIndex += 1;
|
|
56
|
+
valueIndex = wildcardValueIndex;
|
|
57
|
+
patternIndex = wildcardPatternIndex + 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
while (patternIndex < source.length && source[patternIndex] === "%") patternIndex += 1;
|
|
61
|
+
return patternIndex === source.length;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Structural equality matching Convex's own `q.eq`. Arrays, objects and bytes
|
|
65
|
+
* are compared by content: reference identity would make every `eq`/`in` filter
|
|
66
|
+
* on a `custom(v.array(...))` / `custom(v.object(...))` column match nothing.
|
|
67
|
+
*/
|
|
68
|
+
function filterValuesEqual(a, b) {
|
|
69
|
+
if (a === b) return true;
|
|
70
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
71
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
72
|
+
if (!(Array.isArray(a) && Array.isArray(b)) || a.length !== b.length) return false;
|
|
73
|
+
return a.every((item, index) => filterValuesEqual(item, b[index]));
|
|
74
|
+
}
|
|
75
|
+
if (a instanceof ArrayBuffer || b instanceof ArrayBuffer) {
|
|
76
|
+
if (!(a instanceof ArrayBuffer && b instanceof ArrayBuffer) || a.byteLength !== b.byteLength) return false;
|
|
77
|
+
const left = new Uint8Array(a);
|
|
78
|
+
const right = new Uint8Array(b);
|
|
79
|
+
return left.every((byte, index) => byte === right[index]);
|
|
80
|
+
}
|
|
81
|
+
const aKeys = Object.keys(a);
|
|
82
|
+
const bKeys = Object.keys(b);
|
|
83
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
84
|
+
return aKeys.every((key) => Object.hasOwn(b, key) && filterValuesEqual(a[key], b[key]));
|
|
85
|
+
}
|
|
86
|
+
/** `inArray`/`notInArray` membership using {@link filterValuesEqual}. */
|
|
87
|
+
function filterValueInList(value, list) {
|
|
88
|
+
return list.some((candidate) => filterValuesEqual(candidate, value));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
26
91
|
* Create a column wrapper
|
|
27
92
|
* Used internally by query builder's _createColumnProxies
|
|
28
93
|
*/
|
|
@@ -1121,11 +1186,13 @@ var FlatMapStreamIterator = class {
|
|
|
1121
1186
|
#currentOuterItem = null;
|
|
1122
1187
|
#mapper;
|
|
1123
1188
|
#mappedIndexFields;
|
|
1124
|
-
|
|
1189
|
+
#emptyMappedKeyAllowed;
|
|
1190
|
+
constructor(outerStream, mapper, mappedIndexFields, emptyMappedKeyAllowed) {
|
|
1125
1191
|
this.#outerIterator = outerStream.iterWithKeys()[Symbol.asyncIterator]();
|
|
1126
1192
|
this.#outerStream = outerStream;
|
|
1127
1193
|
this.#mapper = mapper;
|
|
1128
1194
|
this.#mappedIndexFields = mappedIndexFields;
|
|
1195
|
+
this.#emptyMappedKeyAllowed = emptyMappedKeyAllowed;
|
|
1129
1196
|
}
|
|
1130
1197
|
singletonSkipInnerStream() {
|
|
1131
1198
|
const indexKey = this.#mappedIndexFields.map(() => null);
|
|
@@ -1136,7 +1203,7 @@ var FlatMapStreamIterator = class {
|
|
|
1136
1203
|
let innerStream;
|
|
1137
1204
|
if (t === null) innerStream = this.singletonSkipInnerStream();
|
|
1138
1205
|
else {
|
|
1139
|
-
innerStream = await this.#mapper(t);
|
|
1206
|
+
innerStream = await this.#mapper(t, indexKey);
|
|
1140
1207
|
if (!equalIndexFields(innerStream.getIndexFields(), this.#mappedIndexFields)) throw new Error(`FlatMapStream: inner stream has different index fields than expected: ${JSON.stringify(innerStream.getIndexFields())} vs ${JSON.stringify(this.#mappedIndexFields)}`);
|
|
1141
1208
|
if (innerStream.getOrder() !== this.#outerStream.getOrder()) throw new Error(`FlatMapStream: inner stream has different order than outer stream: ${innerStream.getOrder()} vs ${this.#outerStream.getOrder()}`);
|
|
1142
1209
|
}
|
|
@@ -1159,7 +1226,7 @@ var FlatMapStreamIterator = class {
|
|
|
1159
1226
|
}
|
|
1160
1227
|
const result = await this.#currentOuterItem.innerIterator.next();
|
|
1161
1228
|
if (result.done) {
|
|
1162
|
-
if (this.#currentOuterItem.count > 0) this.#currentOuterItem = null;
|
|
1229
|
+
if (this.#currentOuterItem.count > 0 || !this.#emptyMappedKeyAllowed(this.#currentOuterItem.indexKey)) this.#currentOuterItem = null;
|
|
1163
1230
|
else this.#currentOuterItem.innerIterator = this.singletonSkipInnerStream().iterWithKeys()[Symbol.asyncIterator]();
|
|
1164
1231
|
return await this.next();
|
|
1165
1232
|
}
|
|
@@ -1175,18 +1242,21 @@ var FlatMapStream = class FlatMapStream extends QueryStream {
|
|
|
1175
1242
|
#stream;
|
|
1176
1243
|
#mapper;
|
|
1177
1244
|
#mappedIndexFields;
|
|
1178
|
-
|
|
1245
|
+
#emptyMappedKeyAllowed;
|
|
1246
|
+
constructor(stream, mapper, mappedIndexFields, emptyMappedKeyAllowed = () => true) {
|
|
1179
1247
|
super();
|
|
1180
1248
|
this.#stream = stream;
|
|
1181
1249
|
this.#mapper = mapper;
|
|
1182
1250
|
this.#mappedIndexFields = mappedIndexFields;
|
|
1251
|
+
this.#emptyMappedKeyAllowed = emptyMappedKeyAllowed;
|
|
1183
1252
|
}
|
|
1184
1253
|
iterWithKeys() {
|
|
1185
1254
|
const outerStream = this.#stream;
|
|
1186
1255
|
const mapper = this.#mapper;
|
|
1187
1256
|
const mappedIndexFields = this.#mappedIndexFields;
|
|
1257
|
+
const emptyMappedKeyAllowed = this.#emptyMappedKeyAllowed;
|
|
1188
1258
|
return { [Symbol.asyncIterator]() {
|
|
1189
|
-
return new FlatMapStreamIterator(outerStream, mapper, mappedIndexFields);
|
|
1259
|
+
return new FlatMapStreamIterator(outerStream, mapper, mappedIndexFields, emptyMappedKeyAllowed);
|
|
1190
1260
|
} };
|
|
1191
1261
|
}
|
|
1192
1262
|
getOrder() {
|
|
@@ -1210,15 +1280,34 @@ var FlatMapStream = class FlatMapStream extends QueryStream {
|
|
|
1210
1280
|
upperBound: outerUpperBound,
|
|
1211
1281
|
upperBoundInclusive: innerUpperBound.length === 0 ? indexBounds.upperBoundInclusive : true
|
|
1212
1282
|
};
|
|
1213
|
-
const
|
|
1214
|
-
lowerBound:
|
|
1215
|
-
lowerBoundInclusive:
|
|
1216
|
-
upperBound:
|
|
1217
|
-
upperBoundInclusive:
|
|
1283
|
+
const unrestricted = {
|
|
1284
|
+
lowerBound: [],
|
|
1285
|
+
lowerBoundInclusive: true,
|
|
1286
|
+
upperBound: [],
|
|
1287
|
+
upperBoundInclusive: true
|
|
1218
1288
|
};
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1289
|
+
const isBoundaryParent = (parentKey, bound) => bound.length === outerLength && compareKeys({
|
|
1290
|
+
value: parentKey,
|
|
1291
|
+
kind: "exact"
|
|
1292
|
+
}, {
|
|
1293
|
+
value: bound,
|
|
1294
|
+
kind: "exact"
|
|
1295
|
+
}) === 0;
|
|
1296
|
+
const innerBoundsForParent = (parentKey) => {
|
|
1297
|
+
const atLowerBound = innerLowerBound.length > 0 && isBoundaryParent(parentKey, outerLowerBound);
|
|
1298
|
+
const atUpperBound = innerUpperBound.length > 0 && isBoundaryParent(parentKey, outerUpperBound);
|
|
1299
|
+
if (!(atLowerBound || atUpperBound)) return unrestricted;
|
|
1300
|
+
return {
|
|
1301
|
+
lowerBound: atLowerBound ? innerLowerBound : unrestricted.lowerBound,
|
|
1302
|
+
lowerBoundInclusive: atLowerBound ? indexBounds.lowerBoundInclusive : true,
|
|
1303
|
+
upperBound: atUpperBound ? innerUpperBound : unrestricted.upperBound,
|
|
1304
|
+
upperBoundInclusive: atUpperBound ? indexBounds.upperBoundInclusive : true
|
|
1305
|
+
};
|
|
1306
|
+
};
|
|
1307
|
+
const emptyMappedIndexKey = this.#mappedIndexFields.map(() => null);
|
|
1308
|
+
return new FlatMapStream(this.#stream.narrow(outerIndexBounds), async (t, parentKey) => {
|
|
1309
|
+
return (await this.#mapper(t, parentKey)).narrow(innerBoundsForParent(parentKey));
|
|
1310
|
+
}, this.#mappedIndexFields, (parentKey) => this.#emptyMappedKeyAllowed(parentKey) && indexKeyWithinBounds(emptyMappedIndexKey, innerBoundsForParent(parentKey)));
|
|
1222
1311
|
}
|
|
1223
1312
|
};
|
|
1224
1313
|
var SingletonStream = class SingletonStream extends QueryStream {
|
|
@@ -1264,25 +1353,37 @@ var SingletonStream = class SingletonStream extends QueryStream {
|
|
|
1264
1353
|
return this.#equalityIndexFilter;
|
|
1265
1354
|
}
|
|
1266
1355
|
narrow(indexBounds) {
|
|
1267
|
-
|
|
1268
|
-
value: indexBounds.lowerBound,
|
|
1269
|
-
kind: indexBounds.lowerBoundInclusive ? "exact" : "successor"
|
|
1270
|
-
}, {
|
|
1271
|
-
value: this.#indexKey,
|
|
1272
|
-
kind: "exact"
|
|
1273
|
-
});
|
|
1274
|
-
const compareUpperBound = compareKeys({
|
|
1275
|
-
value: this.#indexKey,
|
|
1276
|
-
kind: "exact"
|
|
1277
|
-
}, {
|
|
1278
|
-
value: indexBounds.upperBound,
|
|
1279
|
-
kind: indexBounds.upperBoundInclusive ? "exact" : "predecessor"
|
|
1280
|
-
});
|
|
1281
|
-
if (compareLowerBound <= 0 && compareUpperBound <= 0) return new SingletonStream(this.#value, this.#order, this.#indexFields, this.#indexKey, this.#equalityIndexFilter);
|
|
1356
|
+
if (indexKeyWithinBounds(this.#indexKey, indexBounds)) return new SingletonStream(this.#value, this.#order, this.#indexFields, this.#indexKey, this.#equalityIndexFilter);
|
|
1282
1357
|
return new EmptyStream(this.#order, this.#indexFields);
|
|
1283
1358
|
}
|
|
1284
1359
|
};
|
|
1285
1360
|
/**
|
|
1361
|
+
* True when `lowerBound <= indexKey <= upperBound`, honouring each bound's
|
|
1362
|
+
* inclusivity.
|
|
1363
|
+
*
|
|
1364
|
+
* Bounds are compared as `predecessor`/`successor` rather than `exact`, the
|
|
1365
|
+
* same convention `StreamQuery.narrow` uses. That is what lets a bound be a
|
|
1366
|
+
* prefix of the key — including the empty prefix that means unbounded — and it
|
|
1367
|
+
* agrees with `exact` whenever the two are the same length.
|
|
1368
|
+
*/
|
|
1369
|
+
function indexKeyWithinBounds(indexKey, indexBounds) {
|
|
1370
|
+
const compareLowerBound = compareKeys({
|
|
1371
|
+
value: indexBounds.lowerBound,
|
|
1372
|
+
kind: indexBounds.lowerBoundInclusive ? "predecessor" : "successor"
|
|
1373
|
+
}, {
|
|
1374
|
+
value: indexKey,
|
|
1375
|
+
kind: "exact"
|
|
1376
|
+
});
|
|
1377
|
+
const compareUpperBound = compareKeys({
|
|
1378
|
+
value: indexKey,
|
|
1379
|
+
kind: "exact"
|
|
1380
|
+
}, {
|
|
1381
|
+
value: indexBounds.upperBound,
|
|
1382
|
+
kind: indexBounds.upperBoundInclusive ? "successor" : "predecessor"
|
|
1383
|
+
});
|
|
1384
|
+
return compareLowerBound <= 0 && compareUpperBound <= 0;
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1286
1387
|
* This is a completely empty stream that yields no values, and in particular
|
|
1287
1388
|
* does not count towards maxScan.
|
|
1288
1389
|
* Compare to SingletonStream(null, ...), which yields no values but does count
|
|
@@ -1320,7 +1421,7 @@ var EmptyStream = class extends QueryStream {
|
|
|
1320
1421
|
}
|
|
1321
1422
|
};
|
|
1322
1423
|
function normalizeIndexFields(indexFields) {
|
|
1323
|
-
if (!indexFields.includes("_creationTime") &&
|
|
1424
|
+
if (!indexFields.includes("_creationTime") && !indexFields.includes("_id")) indexFields.push("_creationTime");
|
|
1324
1425
|
if (!indexFields.includes("_id")) indexFields.push("_id");
|
|
1325
1426
|
}
|
|
1326
1427
|
function* getOrderingIndexFields(stream) {
|
|
@@ -1515,4 +1616,4 @@ async function getByIdWithOrmQueryFallback(ctx, tableName, id) {
|
|
|
1515
1616
|
}
|
|
1516
1617
|
|
|
1517
1618
|
//#endregion
|
|
1518
|
-
export {
|
|
1619
|
+
export { like as A, or as B, gt as C, isFieldReference as D, inArray as E, not as F, notBetween as I, notIlike as L, lte as M, matchLikePattern as N, isNotNull as O, ne as P, notInArray as R, filterValuesEqual as S, ilike as T, startsWith as V, contains as _, indexKeyWithinBounds as a, fieldRef as b, streamIndexRange as c, and as d, arrayContained as f, column as g, between as h, getIndexFields as i, lt as j, isNull as k, isUnsetToken as l, arrayOverlaps as m, EmptyStream as n, mergedStream as o, arrayContains as p, QueryStream as r, stream as s, getByIdWithOrmQueryFallback as t, unsetToken as u, endsWith as v, gte as w, filterValueInList as x, eq as y, notLike as z };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { getFunctionName } from "convex/server";
|
|
2
|
+
|
|
3
|
+
//#region src/crpc/http-types.ts
|
|
4
|
+
/** HTTP client error */
|
|
5
|
+
var HttpClientError = class extends Error {
|
|
6
|
+
name = "HttpClientError";
|
|
7
|
+
code;
|
|
8
|
+
status;
|
|
9
|
+
procedureName;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
super(opts.message ?? `${opts.code}: ${opts.procedureName}`);
|
|
12
|
+
this.code = opts.code;
|
|
13
|
+
this.status = opts.status;
|
|
14
|
+
this.procedureName = opts.procedureName;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Type guard for HttpClientError */
|
|
18
|
+
const isHttpClientError = (error) => error instanceof HttpClientError;
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/crpc/http-client.ts
|
|
22
|
+
/**
|
|
23
|
+
* HTTP Client Helpers
|
|
24
|
+
*
|
|
25
|
+
* Framework-agnostic utilities for executing HTTP requests
|
|
26
|
+
* against Convex HTTP endpoints.
|
|
27
|
+
*/
|
|
28
|
+
/** Reserved keys that are not part of JSON body */
|
|
29
|
+
const RESERVED_KEYS = new Set([
|
|
30
|
+
"params",
|
|
31
|
+
"searchParams",
|
|
32
|
+
"form",
|
|
33
|
+
"fetch",
|
|
34
|
+
"init",
|
|
35
|
+
"headers"
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Replace URL path parameters with actual values.
|
|
39
|
+
* e.g., '/users/:id' with { id: '123' } -> '/users/123'
|
|
40
|
+
*/
|
|
41
|
+
function replaceUrlParam(url, params) {
|
|
42
|
+
return url.replace(/:(\w+)/g, (_, key) => {
|
|
43
|
+
const value = params[key];
|
|
44
|
+
return value !== void 0 ? encodeURIComponent(value) : `:${key}`;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build URLSearchParams from query object.
|
|
49
|
+
* Handles array values as multiple params with same key (like Hono).
|
|
50
|
+
*/
|
|
51
|
+
function buildSearchParams(query) {
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
for (const [key, value] of Object.entries(query)) if (Array.isArray(value)) for (const v of value) params.append(key, v);
|
|
54
|
+
else if (value !== void 0 && value !== null) params.append(key, value);
|
|
55
|
+
return params;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Hono-style HTTP request executor.
|
|
59
|
+
* Processes args in the same way as Hono's ClientRequestImpl.fetch().
|
|
60
|
+
*/
|
|
61
|
+
async function executeHttpRequest(opts) {
|
|
62
|
+
const { method, path } = opts.route;
|
|
63
|
+
const args = opts.args ?? {};
|
|
64
|
+
let rBody;
|
|
65
|
+
let cType;
|
|
66
|
+
if (args.form) {
|
|
67
|
+
const form = new FormData();
|
|
68
|
+
for (const [k, v] of Object.entries(args.form)) if (Array.isArray(v)) for (const v2 of v) form.append(k, v2);
|
|
69
|
+
else form.append(k, v);
|
|
70
|
+
rBody = form;
|
|
71
|
+
} else {
|
|
72
|
+
const jsonBody = {};
|
|
73
|
+
for (const [key, value] of Object.entries(args)) if (!RESERVED_KEYS.has(key) && value !== void 0) jsonBody[key] = value;
|
|
74
|
+
if (Object.keys(jsonBody).length > 0) {
|
|
75
|
+
rBody = JSON.stringify(opts.transformer.input.serialize(jsonBody));
|
|
76
|
+
cType = "application/json";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const argsClientOpts = {};
|
|
80
|
+
if (args.fetch) argsClientOpts.fetch = args.fetch;
|
|
81
|
+
if (args.init) argsClientOpts.init = args.init;
|
|
82
|
+
if (args.headers) argsClientOpts.headers = args.headers;
|
|
83
|
+
const mergedClientOpts = {
|
|
84
|
+
...opts.clientOpts,
|
|
85
|
+
...argsClientOpts
|
|
86
|
+
};
|
|
87
|
+
const resolvedBaseHeaders = typeof opts.baseHeaders === "function" ? await opts.baseHeaders() : opts.baseHeaders;
|
|
88
|
+
const headerValues = { ...typeof mergedClientOpts.headers === "function" ? await mergedClientOpts.headers() : mergedClientOpts.headers };
|
|
89
|
+
if (cType) headerValues["Content-Type"] = cType;
|
|
90
|
+
const finalHeaders = {};
|
|
91
|
+
if (resolvedBaseHeaders) {
|
|
92
|
+
for (const [key, value] of Object.entries(resolvedBaseHeaders)) if (value !== void 0) finalHeaders[key] = value;
|
|
93
|
+
}
|
|
94
|
+
Object.assign(finalHeaders, headerValues);
|
|
95
|
+
let url = opts.convexSiteUrl + path;
|
|
96
|
+
if (args.params) url = opts.convexSiteUrl + replaceUrlParam(path, args.params);
|
|
97
|
+
if (args.searchParams) {
|
|
98
|
+
const queryString = buildSearchParams(args.searchParams).toString();
|
|
99
|
+
if (queryString) url = `${url}?${queryString}`;
|
|
100
|
+
}
|
|
101
|
+
const methodUpperCase = method.toUpperCase();
|
|
102
|
+
const setBody = !(methodUpperCase === "GET" || methodUpperCase === "HEAD");
|
|
103
|
+
const response = await (mergedClientOpts.fetch ?? opts.baseFetch ?? globalThis.fetch)(url, {
|
|
104
|
+
body: setBody ? rBody : void 0,
|
|
105
|
+
method: methodUpperCase,
|
|
106
|
+
headers: finalHeaders,
|
|
107
|
+
...mergedClientOpts.init
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const errorData = await response.json().catch(() => ({ error: {
|
|
111
|
+
code: "UNKNOWN",
|
|
112
|
+
message: response.statusText
|
|
113
|
+
} }));
|
|
114
|
+
const errorCode = errorData?.error?.code || "UNKNOWN";
|
|
115
|
+
const errorMessage = errorData?.error?.message || response.statusText;
|
|
116
|
+
throw new HttpClientError({
|
|
117
|
+
code: errorCode,
|
|
118
|
+
status: response.status,
|
|
119
|
+
procedureName: opts.procedureName,
|
|
120
|
+
message: errorMessage
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (response.headers.get("content-length") === "0" || response.status === 204) return;
|
|
124
|
+
if ((response.headers.get("content-type") || "").includes("application/json")) return opts.transformer.output.deserialize(await response.json());
|
|
125
|
+
return response.text();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/crpc/query-options.ts
|
|
130
|
+
/**
|
|
131
|
+
* Query options factory for Convex query function subscriptions.
|
|
132
|
+
* Requires `convexQueryClient.queryFn()` set as the default `queryFn` globally.
|
|
133
|
+
*/
|
|
134
|
+
function convexQuery(funcRef, args, meta, opts) {
|
|
135
|
+
const finalArgs = args ?? {};
|
|
136
|
+
const isSkip = finalArgs === "skip";
|
|
137
|
+
const funcName = getFunctionName(funcRef);
|
|
138
|
+
const [namespace, fnName] = funcName.split(":");
|
|
139
|
+
const authType = meta?.[namespace]?.[fnName]?.auth;
|
|
140
|
+
const skipUnauth = opts?.skipUnauth;
|
|
141
|
+
return {
|
|
142
|
+
queryKey: [
|
|
143
|
+
"convexQuery",
|
|
144
|
+
funcName,
|
|
145
|
+
isSkip ? "skip" : finalArgs
|
|
146
|
+
],
|
|
147
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
148
|
+
refetchInterval: false,
|
|
149
|
+
refetchOnMount: false,
|
|
150
|
+
refetchOnReconnect: false,
|
|
151
|
+
refetchOnWindowFocus: false,
|
|
152
|
+
...isSkip ? { enabled: false } : {},
|
|
153
|
+
meta: {
|
|
154
|
+
authType,
|
|
155
|
+
skipUnauth,
|
|
156
|
+
subscribe: true
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Query options factory for Convex action functions.
|
|
162
|
+
* Actions are NOT reactive - they follow normal TanStack Query semantics.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* useQuery(convexAction(api.ai.generate, { prompt }))
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @example With additional options (use spread):
|
|
170
|
+
* ```ts
|
|
171
|
+
* useQuery({
|
|
172
|
+
* ...convexAction(api.files.process, { fileId }),
|
|
173
|
+
* staleTime: 60_000
|
|
174
|
+
* });
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
function convexAction(funcRef, args, meta, opts) {
|
|
178
|
+
const finalArgs = args ?? {};
|
|
179
|
+
const isSkip = finalArgs === "skip";
|
|
180
|
+
const funcName = getFunctionName(funcRef);
|
|
181
|
+
const [namespace, fnName] = funcName.split(":");
|
|
182
|
+
const authType = meta?.[namespace]?.[fnName]?.auth;
|
|
183
|
+
const skipUnauth = opts?.skipUnauth;
|
|
184
|
+
return {
|
|
185
|
+
queryKey: [
|
|
186
|
+
"convexAction",
|
|
187
|
+
funcName,
|
|
188
|
+
isSkip ? {} : finalArgs
|
|
189
|
+
],
|
|
190
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
191
|
+
refetchInterval: false,
|
|
192
|
+
refetchOnMount: false,
|
|
193
|
+
refetchOnReconnect: false,
|
|
194
|
+
refetchOnWindowFocus: false,
|
|
195
|
+
...isSkip ? { enabled: false } : {},
|
|
196
|
+
meta: {
|
|
197
|
+
authType,
|
|
198
|
+
skipUnauth,
|
|
199
|
+
subscribe: false
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Infinite query options factory for paginated Convex queries.
|
|
205
|
+
* Server-safe (non-hook) - can be used in RSC.
|
|
206
|
+
*
|
|
207
|
+
* Uses flat { cursor, limit } input like tRPC.
|
|
208
|
+
*/
|
|
209
|
+
function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
|
|
210
|
+
const { limit, skipUnauth, enabled, ...queryOptions } = opts;
|
|
211
|
+
const finalArgs = args === "skip" ? {} : args;
|
|
212
|
+
const isSkip = args === "skip";
|
|
213
|
+
const funcName = getFunctionName(funcRef);
|
|
214
|
+
const [namespace, fnName] = funcName.split(":");
|
|
215
|
+
const authType = (meta?.[namespace]?.[fnName])?.auth;
|
|
216
|
+
const firstPageArgs = {
|
|
217
|
+
...finalArgs,
|
|
218
|
+
cursor: null,
|
|
219
|
+
limit
|
|
220
|
+
};
|
|
221
|
+
const finalEnabled = isSkip ? false : enabled;
|
|
222
|
+
return {
|
|
223
|
+
queryKey: [
|
|
224
|
+
"convexQuery",
|
|
225
|
+
funcName,
|
|
226
|
+
firstPageArgs
|
|
227
|
+
],
|
|
228
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
229
|
+
refetchInterval: false,
|
|
230
|
+
refetchOnMount: false,
|
|
231
|
+
refetchOnReconnect: false,
|
|
232
|
+
refetchOnWindowFocus: false,
|
|
233
|
+
...queryOptions,
|
|
234
|
+
...finalEnabled === void 0 ? {} : { enabled: finalEnabled },
|
|
235
|
+
meta: {
|
|
236
|
+
authType,
|
|
237
|
+
skipUnauth,
|
|
238
|
+
subscribe: true,
|
|
239
|
+
queryName: funcName,
|
|
240
|
+
args: finalArgs,
|
|
241
|
+
limit
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
export { buildSearchParams as a, HttpClientError as c, RESERVED_KEYS as i, isHttpClientError as l, convexInfiniteQueryOptions as n, executeHttpRequest as o, convexQuery as r, replaceUrlParam as s, convexAction as t };
|
|
@@ -9,7 +9,7 @@ type Duration = number | DurationString;
|
|
|
9
9
|
declare function toMs(duration: Duration): number;
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/ratelimit/types.d.ts
|
|
12
|
-
type RatelimitReason = 'timeout' | 'cacheBlock' | 'denyList';
|
|
12
|
+
type RatelimitReason = 'timeout' | 'cacheBlock' | 'denyList' | 'requestTooLarge';
|
|
13
13
|
type RatelimitResponse = {
|
|
14
14
|
success: boolean;
|
|
15
15
|
ok: boolean;
|
|
@@ -28,17 +28,25 @@ type RemainingResponse = {
|
|
|
28
28
|
type DynamicLimitResponse = {
|
|
29
29
|
dynamicLimit: number | null;
|
|
30
30
|
};
|
|
31
|
-
type
|
|
31
|
+
type RatelimitStoredState = {
|
|
32
32
|
value: number;
|
|
33
33
|
ts: number;
|
|
34
34
|
auxValue?: number;
|
|
35
35
|
auxTs?: number;
|
|
36
36
|
};
|
|
37
|
+
type RatelimitShardState = {
|
|
38
|
+
shard: number;
|
|
39
|
+
state: RatelimitStoredState;
|
|
40
|
+
};
|
|
41
|
+
type RatelimitState = RatelimitStoredState & {
|
|
42
|
+
shards?: RatelimitShardState[];
|
|
43
|
+
};
|
|
37
44
|
type RatelimitSnapshot = {
|
|
38
45
|
value: number;
|
|
39
46
|
ts: number;
|
|
40
47
|
shard: number;
|
|
41
48
|
config: ResolvedAlgorithm;
|
|
49
|
+
state: RatelimitState;
|
|
42
50
|
};
|
|
43
51
|
type BaseAlgorithmOptions = {
|
|
44
52
|
shards?: number;
|
|
@@ -151,12 +159,22 @@ declare function applyDynamicLimit(algorithm: ResolvedAlgorithm, dynamicLimit: n
|
|
|
151
159
|
//#region src/ratelimit/core/calculate-rate-limit.d.ts
|
|
152
160
|
type EvaluationResult = {
|
|
153
161
|
state: RatelimitState;
|
|
154
|
-
retryAfter?: number;
|
|
155
|
-
remaining: number;
|
|
162
|
+
retryAfter?: number; /** Tokens left after this request, floored to `0`. */
|
|
163
|
+
remaining: number; /** Exact tokens left after this request. Negative when the request overdraws. */
|
|
164
|
+
remainingRaw: number;
|
|
156
165
|
reset: number;
|
|
157
166
|
limit: number;
|
|
158
167
|
};
|
|
159
168
|
declare function calculateRatelimit(state: RatelimitState | null, algorithm: ResolvedAlgorithm, now: number, count: number): EvaluationResult;
|
|
169
|
+
/**
|
|
170
|
+
* Convert a {@link RatelimitSnapshot} back into the `RatelimitState` shape that
|
|
171
|
+
* {@link calculateRatelimit} consumes.
|
|
172
|
+
*
|
|
173
|
+
* Snapshot `value` is always "tokens left". Fixed window and token bucket store
|
|
174
|
+
* that directly, but sliding window state stores the used count, so it has to be
|
|
175
|
+
* inverted before it can be replayed.
|
|
176
|
+
*/
|
|
177
|
+
declare function snapshotToState(snapshot: RatelimitSnapshot): RatelimitState;
|
|
160
178
|
//#endregion
|
|
161
179
|
//#region src/ratelimit/plugin.d.ts
|
|
162
180
|
type MaybePromise<T> = T | Promise<T>;
|
|
@@ -224,6 +242,7 @@ declare class Ratelimit {
|
|
|
224
242
|
private readonly blockCache?;
|
|
225
243
|
private readonly blockCacheSource?;
|
|
226
244
|
private readonly checkCache;
|
|
245
|
+
private cacheGeneration;
|
|
227
246
|
constructor(config: RatelimitConfig);
|
|
228
247
|
limit(identifier: string, request?: LimitRequest): Promise<RatelimitResponse>;
|
|
229
248
|
check(identifier: string, request?: CheckRequest): Promise<RatelimitResponse>;
|
|
@@ -246,9 +265,10 @@ declare class Ratelimit {
|
|
|
246
265
|
};
|
|
247
266
|
private withDb;
|
|
248
267
|
private evaluate;
|
|
268
|
+
private blockKey;
|
|
269
|
+
private readShards;
|
|
249
270
|
private evaluateCandidates;
|
|
250
271
|
private resolveAlgorithm;
|
|
251
|
-
private rawLimit;
|
|
252
272
|
private runWithTimeout;
|
|
253
273
|
private timeoutResponse;
|
|
254
274
|
}
|
|
@@ -265,4 +285,4 @@ declare const HOUR: number;
|
|
|
265
285
|
declare const DAY: number;
|
|
266
286
|
declare const WEEK: number;
|
|
267
287
|
//#endregion
|
|
268
|
-
export { type CheckRequest, type ConvexQueryBuilder, type ConvexRatelimitDbReader, type ConvexRatelimitDbWriter, DAY, type Duration, type DurationString, type DurationUnit, type DynamicLimitResponse, type FixedWindowAlgorithm, HOUR, type HookAPIOptions, type HookCheckValue, type LimitRequest, MINUTE, RATE_LIMIT_DYNAMIC_TABLE, RATE_LIMIT_HIT_TABLE, RATE_LIMIT_STATE_TABLE, Ratelimit, type RatelimitConfig, RatelimitPlugin, type RatelimitPluginOptions, type RatelimitReason, type RatelimitResponse, type RatelimitRow, type RatelimitSnapshot, type RatelimitState, type RemainingResponse, type ResolvedAlgorithm, SECOND, type SlidingWindowAlgorithm, type TokenBucketAlgorithm, WEEK, applyDynamicLimit, calculateRatelimit, fixedWindow, slidingWindow, toMs, tokenBucket };
|
|
288
|
+
export { type CheckRequest, type ConvexQueryBuilder, type ConvexRatelimitDbReader, type ConvexRatelimitDbWriter, DAY, type Duration, type DurationString, type DurationUnit, type DynamicLimitResponse, type EvaluationResult, type FixedWindowAlgorithm, HOUR, type HookAPIOptions, type HookCheckValue, type LimitRequest, MINUTE, RATE_LIMIT_DYNAMIC_TABLE, RATE_LIMIT_HIT_TABLE, RATE_LIMIT_STATE_TABLE, Ratelimit, type RatelimitConfig, RatelimitPlugin, type RatelimitPluginOptions, type RatelimitReason, type RatelimitResponse, type RatelimitRow, type RatelimitShardState, type RatelimitSnapshot, type RatelimitState, type RatelimitStoredState, type RemainingResponse, type ResolvedAlgorithm, SECOND, type SlidingWindowAlgorithm, type TokenBucketAlgorithm, WEEK, applyDynamicLimit, calculateRatelimit, fixedWindow, slidingWindow, snapshotToState, toMs, tokenBucket };
|