kitcn 0.0.1 → 0.12.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/bin/intent.js +3 -0
- package/dist/aggregate/index.d.ts +388 -0
- package/dist/aggregate/index.js +37 -0
- package/dist/api-entry-BckXqaLb.js +66 -0
- package/dist/auth/client/index.d.ts +37 -0
- package/dist/auth/client/index.js +217 -0
- package/dist/auth/config/index.d.ts +45 -0
- package/dist/auth/config/index.js +24 -0
- package/dist/auth/generated/index.d.ts +2 -0
- package/dist/auth/generated/index.js +3 -0
- package/dist/auth/http/index.d.ts +64 -0
- package/dist/auth/http/index.js +461 -0
- package/dist/auth/index.d.ts +221 -0
- package/dist/auth/index.js +1398 -0
- package/dist/auth/nextjs/index.d.ts +50 -0
- package/dist/auth/nextjs/index.js +81 -0
- package/dist/auth-store-Cljlmdmi.js +197 -0
- package/dist/builder-CBdG5W6A.js +1974 -0
- package/dist/caller-factory-cTXNvYdz.js +216 -0
- package/dist/cli.mjs +13255 -0
- package/dist/codegen-lF80HSWu.mjs +3416 -0
- package/dist/context-utils-HPC5nXzx.d.ts +17 -0
- package/dist/create-schema-odyF4kCy.js +156 -0
- package/dist/create-schema-orm-DOyiNDCx.js +246 -0
- package/dist/crpc/index.d.ts +105 -0
- package/dist/crpc/index.js +169 -0
- package/dist/customFunctions-C0voKmtx.js +144 -0
- package/dist/error-BZEnI7Sq.js +41 -0
- package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
- package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
- package/dist/http-types-DqJubRPJ.d.ts +292 -0
- package/dist/meta-utils-0Pu0Nrap.js +117 -0
- package/dist/middleware-BUybuv9n.d.ts +34 -0
- package/dist/middleware-C2qTZ3V7.js +84 -0
- package/dist/orm/index.d.ts +17 -0
- package/dist/orm/index.js +10713 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +3 -0
- package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
- package/dist/procedure-caller-MWcxhQDv.js +349 -0
- package/dist/query-context-B8o6-8kC.js +1518 -0
- package/dist/query-context-CFZqIvD7.d.ts +42 -0
- package/dist/query-options-Dw7cOyXl.js +121 -0
- package/dist/ratelimit/index.d.ts +269 -0
- package/dist/ratelimit/index.js +856 -0
- package/dist/ratelimit/react/index.d.ts +76 -0
- package/dist/ratelimit/react/index.js +183 -0
- package/dist/react/index.d.ts +1284 -0
- package/dist/react/index.js +2526 -0
- package/dist/rsc/index.d.ts +276 -0
- package/dist/rsc/index.js +233 -0
- package/dist/runtime-CtvJPkur.js +2453 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +6 -0
- package/dist/solid/index.d.ts +1221 -0
- package/dist/solid/index.js +2940 -0
- package/dist/transformer-DtDhR3Lc.js +194 -0
- package/dist/types-BTb_4BaU.d.ts +42 -0
- package/dist/types-BiJE7qxR.d.ts +4 -0
- package/dist/types-DEJpkIhw.d.ts +88 -0
- package/dist/types-HhO_R6pd.d.ts +213 -0
- package/dist/validators-B7oIJCAp.js +279 -0
- package/dist/validators-vzRKjBJC.d.ts +88 -0
- package/dist/watcher.mjs +96 -0
- package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
- package/package.json +107 -35
- package/skills/convex/SKILL.md +486 -0
- package/skills/convex/references/features/aggregates.md +353 -0
- package/skills/convex/references/features/auth-admin.md +446 -0
- package/skills/convex/references/features/auth-organizations.md +1141 -0
- package/skills/convex/references/features/auth-polar.md +579 -0
- package/skills/convex/references/features/auth.md +470 -0
- package/skills/convex/references/features/create-plugins.md +153 -0
- package/skills/convex/references/features/http.md +676 -0
- package/skills/convex/references/features/migrations.md +162 -0
- package/skills/convex/references/features/orm.md +1166 -0
- package/skills/convex/references/features/react.md +657 -0
- package/skills/convex/references/features/scheduling.md +267 -0
- package/skills/convex/references/features/testing.md +209 -0
- package/skills/convex/references/setup/auth.md +501 -0
- package/skills/convex/references/setup/biome.md +190 -0
- package/skills/convex/references/setup/doc-guidelines.md +145 -0
- package/skills/convex/references/setup/index.md +759 -0
- package/skills/convex/references/setup/next.md +116 -0
- package/skills/convex/references/setup/react.md +175 -0
- package/skills/convex/references/setup/server.md +473 -0
- package/skills/convex/references/setup/start.md +67 -0
- package/LICENSE +0 -21
- package/README.md +0 -0
- package/dist/index.d.mts +0 -5
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -6
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,1974 @@
|
|
|
1
|
+
import { l as pick, o as vRequired, t as addFieldsToValidator } from "./validators-B7oIJCAp.js";
|
|
2
|
+
import { n as customCtx, t as NoOp } from "./customFunctions-C0voKmtx.js";
|
|
3
|
+
import { s as getTransformer } from "./transformer-DtDhR3Lc.js";
|
|
4
|
+
import { ConvexError, v } from "convex/values";
|
|
5
|
+
import { HttpRouter, actionGeneric, httpActionGeneric, internalActionGeneric, internalMutationGeneric, internalQueryGeneric, mutationGeneric, queryGeneric } from "convex/server";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import * as z$1 from "zod/v4";
|
|
8
|
+
import * as zCore from "zod/v4/core";
|
|
9
|
+
|
|
10
|
+
//#region src/internal/upstream/server/zod4.ts
|
|
11
|
+
/**
|
|
12
|
+
* zCustomQuery is like customQuery, but allows validation via zod.
|
|
13
|
+
* You can define custom behavior on top of `query` or `internalQuery`
|
|
14
|
+
* by passing a function that modifies the ctx and args. Or NoOp to do nothing.
|
|
15
|
+
*
|
|
16
|
+
* Example usage:
|
|
17
|
+
* ```ts
|
|
18
|
+
* const myQueryBuilder = zCustomQuery(query, {
|
|
19
|
+
* args: { sessionId: v.id("sessions") },
|
|
20
|
+
* input: async (ctx, args) => {
|
|
21
|
+
* const user = await getUserOrNull(ctx);
|
|
22
|
+
* const session = await db.get(sessionId);
|
|
23
|
+
* const db = wrapDatabaseReader({ user }, ctx.db, rlsRules);
|
|
24
|
+
* return { ctx: { db, user, session }, args: {} };
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Using the custom builder
|
|
29
|
+
* export const getSomeData = myQueryBuilder({
|
|
30
|
+
* args: { someArg: z.string() },
|
|
31
|
+
* handler: async (ctx, args) => {
|
|
32
|
+
* const { db, user, session, scheduler } = ctx;
|
|
33
|
+
* const { someArg } = args;
|
|
34
|
+
* // ...
|
|
35
|
+
* }
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Simple usage only modifying ctx:
|
|
40
|
+
* ```ts
|
|
41
|
+
* const myInternalQuery = zCustomQuery(
|
|
42
|
+
* internalQuery,
|
|
43
|
+
* customCtx(async (ctx) => {
|
|
44
|
+
* return {
|
|
45
|
+
* // Throws an exception if the user isn't logged in
|
|
46
|
+
* user: await getUserByTokenIdentifier(ctx),
|
|
47
|
+
* };
|
|
48
|
+
* })
|
|
49
|
+
* );
|
|
50
|
+
*
|
|
51
|
+
* // Using it
|
|
52
|
+
* export const getUser = myInternalQuery({
|
|
53
|
+
* args: { email: z.string().email() },
|
|
54
|
+
* handler: async (ctx, args) => {
|
|
55
|
+
* console.log(args.email);
|
|
56
|
+
* return ctx.user;
|
|
57
|
+
* },
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* @param query The query to be modified. Usually `query` or `internalQuery`
|
|
61
|
+
* from `_generated/server`.
|
|
62
|
+
* @param customization The customization to be applied to the query, changing ctx and args.
|
|
63
|
+
* @returns A new query builder using zod validation to define queries.
|
|
64
|
+
*/
|
|
65
|
+
function zCustomQuery(query, customization) {
|
|
66
|
+
return customFnBuilder(query, customization);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* zCustomMutation is like customMutation, but allows validation via zod.
|
|
70
|
+
* You can define custom behavior on top of `mutation` or `internalMutation`
|
|
71
|
+
* by passing a function that modifies the ctx and args. Or NoOp to do nothing.
|
|
72
|
+
*
|
|
73
|
+
* Example usage:
|
|
74
|
+
* ```ts
|
|
75
|
+
* const myMutationBuilder = zCustomMutation(mutation, {
|
|
76
|
+
* args: { sessionId: v.id("sessions") },
|
|
77
|
+
* input: async (ctx, args) => {
|
|
78
|
+
* const user = await getUserOrNull(ctx);
|
|
79
|
+
* const session = await db.get(sessionId);
|
|
80
|
+
* const db = wrapDatabaseReader({ user }, ctx.db, rlsRules);
|
|
81
|
+
* return { ctx: { db, user, session }, args: {} };
|
|
82
|
+
* },
|
|
83
|
+
* });
|
|
84
|
+
*
|
|
85
|
+
* // Using the custom builder
|
|
86
|
+
* export const getSomeData = myMutationBuilder({
|
|
87
|
+
* args: { someArg: z.string() },
|
|
88
|
+
* handler: async (ctx, args) => {
|
|
89
|
+
* const { db, user, session, scheduler } = ctx;
|
|
90
|
+
* const { someArg } = args;
|
|
91
|
+
* // ...
|
|
92
|
+
* }
|
|
93
|
+
* });
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* Simple usage only modifying ctx:
|
|
97
|
+
* ```ts
|
|
98
|
+
* const myInternalMutation = zCustomMutation(
|
|
99
|
+
* internalMutation,
|
|
100
|
+
* customCtx(async (ctx) => {
|
|
101
|
+
* return {
|
|
102
|
+
* // Throws an exception if the user isn't logged in
|
|
103
|
+
* user: await getUserByTokenIdentifier(ctx),
|
|
104
|
+
* };
|
|
105
|
+
* })
|
|
106
|
+
* );
|
|
107
|
+
*
|
|
108
|
+
* // Using it
|
|
109
|
+
* export const getUser = myInternalMutation({
|
|
110
|
+
* args: { email: z.string().email() },
|
|
111
|
+
* handler: async (ctx, args) => {
|
|
112
|
+
* console.log(args.email);
|
|
113
|
+
* return ctx.user;
|
|
114
|
+
* },
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* @param mutation The mutation to be modified. Usually `mutation` or `internalMutation`
|
|
118
|
+
* from `_generated/server`.
|
|
119
|
+
* @param customization The customization to be applied to the mutation, changing ctx and args.
|
|
120
|
+
* @returns A new mutation builder using zod validation to define queries.
|
|
121
|
+
*/
|
|
122
|
+
function zCustomMutation(mutation, customization) {
|
|
123
|
+
return customFnBuilder(mutation, customization);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* zCustomAction is like customAction, but allows validation via zod.
|
|
127
|
+
* You can define custom behavior on top of `action` or `internalAction`
|
|
128
|
+
* by passing a function that modifies the ctx and args. Or NoOp to do nothing.
|
|
129
|
+
*
|
|
130
|
+
* Example usage:
|
|
131
|
+
* ```ts
|
|
132
|
+
* const myActionBuilder = zCustomAction(action, {
|
|
133
|
+
* args: { sessionId: v.id("sessions") },
|
|
134
|
+
* input: async (ctx, args) => {
|
|
135
|
+
* const user = await getUserOrNull(ctx);
|
|
136
|
+
* const session = await db.get(sessionId);
|
|
137
|
+
* const db = wrapDatabaseReader({ user }, ctx.db, rlsRules);
|
|
138
|
+
* return { ctx: { db, user, session }, args: {} };
|
|
139
|
+
* },
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* // Using the custom builder
|
|
143
|
+
* export const getSomeData = myActionBuilder({
|
|
144
|
+
* args: { someArg: z.string() },
|
|
145
|
+
* handler: async (ctx, args) => {
|
|
146
|
+
* const { db, user, session, scheduler } = ctx;
|
|
147
|
+
* const { someArg } = args;
|
|
148
|
+
* // ...
|
|
149
|
+
* }
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* Simple usage only modifying ctx:
|
|
154
|
+
* ```ts
|
|
155
|
+
* const myInternalAction = zCustomAction(
|
|
156
|
+
* internalAction,
|
|
157
|
+
* customCtx(async (ctx) => {
|
|
158
|
+
* return {
|
|
159
|
+
* // Throws an exception if the user isn't logged in
|
|
160
|
+
* user: await getUserByTokenIdentifier(ctx),
|
|
161
|
+
* };
|
|
162
|
+
* })
|
|
163
|
+
* );
|
|
164
|
+
*
|
|
165
|
+
* // Using it
|
|
166
|
+
* export const getUser = myInternalAction({
|
|
167
|
+
* args: { email: z.string().email() },
|
|
168
|
+
* handler: async (ctx, args) => {
|
|
169
|
+
* console.log(args.email);
|
|
170
|
+
* return ctx.user;
|
|
171
|
+
* },
|
|
172
|
+
* });
|
|
173
|
+
*
|
|
174
|
+
* @param action The action to be modified. Usually `action` or `internalAction`
|
|
175
|
+
* from `_generated/server`.
|
|
176
|
+
* @param customization The customization to be applied to the action, changing ctx and args.
|
|
177
|
+
* @returns A new action builder using zod validation to define queries.
|
|
178
|
+
*/
|
|
179
|
+
function zCustomAction(action, customization) {
|
|
180
|
+
return customFnBuilder(action, customization);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Creates a validator for a Convex `Id`.
|
|
184
|
+
*
|
|
185
|
+
* - When **used within Zod**, it will only check that the ID is a string.
|
|
186
|
+
* - When **converted to a Convex validator** (e.g. through {@link zodToConvex}),
|
|
187
|
+
* it will check that it's for the right table.
|
|
188
|
+
*
|
|
189
|
+
* @param tableName - The table that the `Id` references. i.e. `Id<tableName>`
|
|
190
|
+
* @returns A Zod schema representing a Convex `Id`
|
|
191
|
+
*/
|
|
192
|
+
const zid = (tableName) => {
|
|
193
|
+
const result = z$1.custom((val) => typeof val === "string");
|
|
194
|
+
_zidRegistry.add(result, { tableName });
|
|
195
|
+
return result;
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Turns a Zod or Zod Mini validator into a Convex validator.
|
|
199
|
+
*
|
|
200
|
+
* The Convex validator will be as close to possible to the Zod validator,
|
|
201
|
+
* but might be broader than the Zod validator:
|
|
202
|
+
*
|
|
203
|
+
* ```ts
|
|
204
|
+
* zodToConvex(z.string().email()) // → v.string()
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* This function is useful when running the Zod validator _after_ running the Convex validator
|
|
208
|
+
* (i.e. the Convex validator validates the input of the Zod validator). Hence, the Convex types
|
|
209
|
+
* will match the _input type_ of Zod transformations:
|
|
210
|
+
* ```ts
|
|
211
|
+
* zodToConvex(z.object({
|
|
212
|
+
* name: z.string().default("Nicolas"),
|
|
213
|
+
* })) // → v.object({ name: v.optional(v.string()) })
|
|
214
|
+
*
|
|
215
|
+
* zodToConvex(z.object({
|
|
216
|
+
* name: z.string().transform(s => s.length)
|
|
217
|
+
* })) // → v.object({ name: v.string() })
|
|
218
|
+
* ````
|
|
219
|
+
*
|
|
220
|
+
* This function is useful for:
|
|
221
|
+
* * **Validating function arguments with Zod**: through {@link zCustomQuery},
|
|
222
|
+
* {@link zCustomMutation} and {@link zCustomAction}, you can define the argument validation logic
|
|
223
|
+
* using Zod validators instead of Convex validators. `zodToConvex` will generate a Convex validator
|
|
224
|
+
* from your Zod validator. This will allow you to:
|
|
225
|
+
* - validate at run time that Convex IDs are from the right table (using {@link zid})
|
|
226
|
+
* - allow some features of Convex to understand the expected shape of the arguments
|
|
227
|
+
* (e.g. argument validation/prefilling in the function runner on the Convex dashboard)
|
|
228
|
+
* - still run the full Zod validation when the function runs
|
|
229
|
+
* (which is useful for more advanced Zod validators like `z.string().email()`)
|
|
230
|
+
* * **Validating data after reading it from the database**: if you want to write your DB schema
|
|
231
|
+
* with Zod, you can run Zod whenever you read from the database to check that the data
|
|
232
|
+
* still matches the schema. Note that this approach won’t ensure that the data stored in the DB
|
|
233
|
+
* matches the Zod schema; see
|
|
234
|
+
* https://stack.convex.dev/typescript-zod-function-validation#can-i-use-zod-to-define-my-database-types-too
|
|
235
|
+
* for more details.
|
|
236
|
+
*
|
|
237
|
+
* Note that some values might be valid in Zod but not in Convex,
|
|
238
|
+
* in the same way that valid JavaScript values might not be valid
|
|
239
|
+
* Convex values for the corresponding Convex type.
|
|
240
|
+
* (see the limits of Convex data types on https://docs.convex.dev/database/types).
|
|
241
|
+
*
|
|
242
|
+
* ```
|
|
243
|
+
* ┌─────────────────────────────────────┬─────────────────────────────────────┐
|
|
244
|
+
* │ **zodToConvex** │ zodOutputToConvex │
|
|
245
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
246
|
+
* │ For when the Zod validator runs │ For when the Zod validator runs │
|
|
247
|
+
* │ _after_ the Convex validator │ _before_ the Convex validator │
|
|
248
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
249
|
+
* │ Convex types use the _input types_ │ Convex types use the _return types_ │
|
|
250
|
+
* │ of Zod transformations │ of Zod transformations │
|
|
251
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
252
|
+
* │ The Convex validator can be less │ The Convex validator can be less │
|
|
253
|
+
* │ strict (i.e. some inputs might be │ strict (i.e. the type in Convex can │
|
|
254
|
+
* │ accepted by Convex then rejected │ be less precise than the type in │
|
|
255
|
+
* │ by Zod) │ the Zod output) │
|
|
256
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
257
|
+
* │ When using Zod schemas │ When using Zod schemas │
|
|
258
|
+
* │ for function definitions: │ for function definitions: │
|
|
259
|
+
* │ used for _arguments_ │ used for _return values_ │
|
|
260
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
261
|
+
* │ When validating contents of the │ When validating contents of the │
|
|
262
|
+
* │ database with a Zod schema: │ database with a Zod schema: │
|
|
263
|
+
* │ used to validate data │ used to validate data │
|
|
264
|
+
* │ _after reading_ │ _before writing_ │
|
|
265
|
+
* └─────────────────────────────────────┴─────────────────────────────────────┘
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @param zod Zod validator can be a Zod object, or a Zod type like `z.string()`
|
|
269
|
+
* @returns Convex Validator (e.g. `v.string()` from "convex/values")
|
|
270
|
+
* @throws If there is no equivalent Convex validator for the value (e.g. `z.date()`)
|
|
271
|
+
*/
|
|
272
|
+
function zodToConvex(validator) {
|
|
273
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
274
|
+
function zodToConvexInner(validator) {
|
|
275
|
+
if (visited.has(validator)) return v.any();
|
|
276
|
+
visited.add(validator);
|
|
277
|
+
const result = validator instanceof zCore.$ZodDefault ? v.optional(zodToConvexInner(validator._zod.def.innerType)) : validator instanceof zCore.$ZodPipe ? zodToConvexInner(validator._zod.def.in) : zodToConvexCommon(validator, zodToConvexInner);
|
|
278
|
+
visited.delete(validator);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
return zodToConvexInner(validator);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Converts a Zod or Zod Mini validator to a Convex validator that checks the value _after_
|
|
285
|
+
* it has been validated (and possibly transformed) by the Zod validator.
|
|
286
|
+
*
|
|
287
|
+
* This is similar to {@link zodToConvex}, but is meant for cases where the Convex
|
|
288
|
+
* validator runs _after_ the Zod validator. Thus, the Convex type refers to the
|
|
289
|
+
* _output_ type of the Zod transformations:
|
|
290
|
+
* ```ts
|
|
291
|
+
* zodOutputToConvex(z.object({
|
|
292
|
+
* name: z.string().default("Nicolas"),
|
|
293
|
+
* })) // → v.object({ name: v.string() })
|
|
294
|
+
*
|
|
295
|
+
* zodOutputToConvex(z.object({
|
|
296
|
+
* name: z.string().transform(s => s.length)
|
|
297
|
+
* })) // → v.object({ name: v.number() })
|
|
298
|
+
* ````
|
|
299
|
+
*
|
|
300
|
+
* This function can be useful for:
|
|
301
|
+
* - **Validating function return values with Zod**: through {@link zCustomQuery},
|
|
302
|
+
* {@link zCustomMutation} and {@link zCustomAction}, you can define the `returns` property
|
|
303
|
+
* of a function using Zod validators instead of Convex validators.
|
|
304
|
+
* - **Validating data after reading it from the database**: if you want to write your DB schema
|
|
305
|
+
* Zod validators, you can run Zod whenever you write to the database to ensure your data matches
|
|
306
|
+
* the expected format. Note that this approach won’t ensure that the data stored in the DB
|
|
307
|
+
* isn’t modified manually in a way that doesn’t match your Zod schema; see
|
|
308
|
+
* https://stack.convex.dev/typescript-zod-function-validation#can-i-use-zod-to-define-my-database-types-too
|
|
309
|
+
* for more details.
|
|
310
|
+
*
|
|
311
|
+
* ```
|
|
312
|
+
* ┌─────────────────────────────────────┬─────────────────────────────────────┐
|
|
313
|
+
* │ zodToConvex │ **zodOutputToConvex** │
|
|
314
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
315
|
+
* │ For when the Zod validator runs │ For when the Zod validator runs │
|
|
316
|
+
* │ _after_ the Convex validator │ _before_ the Convex validator │
|
|
317
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
318
|
+
* │ Convex types use the _input types_ │ Convex types use the _return types_ │
|
|
319
|
+
* │ of Zod transformations │ of Zod transformations │
|
|
320
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
321
|
+
* │ The Convex validator can be less │ The Convex validator can be less │
|
|
322
|
+
* │ strict (i.e. some inputs might be │ strict (i.e. the type in Convex can │
|
|
323
|
+
* │ accepted by Convex then rejected │ be less precise than the type in │
|
|
324
|
+
* │ by Zod) │ the Zod output) │
|
|
325
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
326
|
+
* │ When using Zod schemas │ When using Zod schemas │
|
|
327
|
+
* │ for function definitions: │ for function definitions: │
|
|
328
|
+
* │ used for _arguments_ │ used for _return values_ │
|
|
329
|
+
* ├─────────────────────────────────────┼─────────────────────────────────────┤
|
|
330
|
+
* │ When validating contents of the │ When validating contents of the │
|
|
331
|
+
* │ database with a Zod schema: │ database with a Zod schema: │
|
|
332
|
+
* │ used to validate data │ used to validate data │
|
|
333
|
+
* │ _after reading_ │ _before writing_ │
|
|
334
|
+
* └─────────────────────────────────────┴─────────────────────────────────────┘
|
|
335
|
+
* ```
|
|
336
|
+
*
|
|
337
|
+
* @param z The zod validator
|
|
338
|
+
* @returns Convex Validator (e.g. `v.string()` from "convex/values")
|
|
339
|
+
* @throws If there is no equivalent Convex validator for the value (e.g. `z.date()`)
|
|
340
|
+
*/
|
|
341
|
+
function zodOutputToConvex(validator) {
|
|
342
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
343
|
+
function zodOutputToConvexInner(validator) {
|
|
344
|
+
if (visited.has(validator)) return v.any();
|
|
345
|
+
visited.add(validator);
|
|
346
|
+
const result = validator instanceof zCore.$ZodDefault ? zodOutputToConvexInner(validator._zod.def.innerType) : validator instanceof zCore.$ZodPipe ? zodOutputToConvexInner(validator._zod.def.out) : validator instanceof zCore.$ZodTransform ? v.any() : zodToConvexCommon(validator, zodOutputToConvexInner);
|
|
347
|
+
visited.delete(validator);
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
return zodOutputToConvexInner(validator);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Like {@link zodToConvex}, but it takes in a bare object, as expected by Convex
|
|
354
|
+
* function arguments, or the argument to {@link defineTable}.
|
|
355
|
+
*
|
|
356
|
+
* ```ts
|
|
357
|
+
* zodToConvexFields({
|
|
358
|
+
* name: z.string().default("Nicolas"),
|
|
359
|
+
* }) // → { name: v.optional(v.string()) }
|
|
360
|
+
* ```
|
|
361
|
+
*
|
|
362
|
+
* @param fields Object with string keys and Zod validators as values
|
|
363
|
+
* @returns Object with the same keys, but with Convex validators as values
|
|
364
|
+
*/
|
|
365
|
+
function zodToConvexFields(fields) {
|
|
366
|
+
return Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, zodToConvex(v)]));
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Like {@link zodOutputToConvex}, but it takes in a bare object, as expected by
|
|
370
|
+
* Convex function arguments, or the argument to {@link defineTable}.
|
|
371
|
+
*
|
|
372
|
+
* ```ts
|
|
373
|
+
* zodOutputToConvexFields({
|
|
374
|
+
* name: z.string().default("Nicolas"),
|
|
375
|
+
* }) // → { name: v.string() }
|
|
376
|
+
* ```
|
|
377
|
+
*
|
|
378
|
+
* This is different from {@link zodToConvexFields} because it generates the
|
|
379
|
+
* Convex validator for the output of the Zod validator, not the input;
|
|
380
|
+
* see the documentation of {@link zodToConvex} and {@link zodOutputToConvex}
|
|
381
|
+
* for more details.
|
|
382
|
+
*
|
|
383
|
+
* @param zod Object with string keys and Zod validators as values
|
|
384
|
+
* @returns Object with the same keys, but with Convex validators as values
|
|
385
|
+
*/
|
|
386
|
+
function zodOutputToConvexFields(fields) {
|
|
387
|
+
return Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, zodOutputToConvex(v)]));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Turns a Convex validator into a Zod validator.
|
|
391
|
+
*
|
|
392
|
+
* This is useful when you want to use types you defined using Convex validators
|
|
393
|
+
* with external libraries that expect to receive a Zod validator.
|
|
394
|
+
*
|
|
395
|
+
* ```ts
|
|
396
|
+
* convexToZod(v.string()) // → z.string()
|
|
397
|
+
* ```
|
|
398
|
+
*
|
|
399
|
+
* This function returns Zod validators, not Zod Mini validators.
|
|
400
|
+
*
|
|
401
|
+
* @param convexValidator Convex validator can be any validator from "convex/values" e.g. `v.string()`
|
|
402
|
+
* @returns Zod validator (e.g. `z.string()`) with inferred type matching the Convex validator
|
|
403
|
+
*/
|
|
404
|
+
function convexToZod(convexValidator) {
|
|
405
|
+
const isOptional = convexValidator.isOptional === "optional";
|
|
406
|
+
let zodValidator;
|
|
407
|
+
const { kind } = convexValidator;
|
|
408
|
+
switch (kind) {
|
|
409
|
+
case "id":
|
|
410
|
+
zodValidator = zid(convexValidator.tableName);
|
|
411
|
+
break;
|
|
412
|
+
case "string":
|
|
413
|
+
zodValidator = z$1.string();
|
|
414
|
+
break;
|
|
415
|
+
case "float64":
|
|
416
|
+
zodValidator = z$1.number();
|
|
417
|
+
break;
|
|
418
|
+
case "int64":
|
|
419
|
+
zodValidator = z$1.bigint();
|
|
420
|
+
break;
|
|
421
|
+
case "boolean":
|
|
422
|
+
zodValidator = z$1.boolean();
|
|
423
|
+
break;
|
|
424
|
+
case "null":
|
|
425
|
+
zodValidator = z$1.null();
|
|
426
|
+
break;
|
|
427
|
+
case "any":
|
|
428
|
+
zodValidator = z$1.any();
|
|
429
|
+
break;
|
|
430
|
+
case "array":
|
|
431
|
+
zodValidator = z$1.array(convexToZod(convexValidator.element));
|
|
432
|
+
break;
|
|
433
|
+
case "object":
|
|
434
|
+
zodValidator = z$1.object(convexToZodFields(convexValidator.fields));
|
|
435
|
+
break;
|
|
436
|
+
case "union": {
|
|
437
|
+
if (convexValidator.members.length === 0) {
|
|
438
|
+
zodValidator = z$1.never();
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
if (convexValidator.members.length === 1) {
|
|
442
|
+
zodValidator = convexToZod(convexValidator.members[0]);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
const memberValidators = convexValidator.members.map((member) => convexToZod(member));
|
|
446
|
+
zodValidator = z$1.union([...memberValidators]);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case "literal": {
|
|
450
|
+
const literalValidator = convexValidator;
|
|
451
|
+
zodValidator = z$1.literal(literalValidator.value);
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case "record":
|
|
455
|
+
zodValidator = z$1.record(convexToZod(convexValidator.key), convexToZod(convexValidator.value));
|
|
456
|
+
break;
|
|
457
|
+
case "bytes": throw new Error("v.bytes() is not supported");
|
|
458
|
+
default: throw new Error(`Unknown convex validator type: ${kind}`);
|
|
459
|
+
}
|
|
460
|
+
return isOptional ? z$1.optional(zodValidator) : zodValidator;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Like {@link convexToZod}, but it takes in a bare object, as expected by Convex
|
|
464
|
+
* function arguments, or the argument to {@link defineTable}.
|
|
465
|
+
*
|
|
466
|
+
* ```ts
|
|
467
|
+
* convexToZodFields({
|
|
468
|
+
* name: v.string(),
|
|
469
|
+
* }) // → { name: z.string() }
|
|
470
|
+
* ```
|
|
471
|
+
*
|
|
472
|
+
* @param convexValidators Object with string keys and Convex validators as values
|
|
473
|
+
* @returns Object with the same keys, but with Zod validators as values
|
|
474
|
+
*/
|
|
475
|
+
function convexToZodFields(convexValidators) {
|
|
476
|
+
return Object.fromEntries(Object.entries(convexValidators).map(([k, v]) => [k, convexToZod(v)]));
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Zod helper for adding Convex system fields to a record to return.
|
|
480
|
+
*
|
|
481
|
+
* ```ts
|
|
482
|
+
* withSystemFields("users", {
|
|
483
|
+
* name: z.string(),
|
|
484
|
+
* })
|
|
485
|
+
* // → {
|
|
486
|
+
* // name: z.string(),
|
|
487
|
+
* // _id: zid("users"),
|
|
488
|
+
* // _creationTime: z.number(),
|
|
489
|
+
* // }
|
|
490
|
+
* ```
|
|
491
|
+
*
|
|
492
|
+
* @param tableName - The table where records are from, i.e. Doc<tableName>
|
|
493
|
+
* @param zObject - Validators for the user-defined fields on the document.
|
|
494
|
+
* @returns Zod shape for use with `z.object(shape)` that includes system fields.
|
|
495
|
+
*/
|
|
496
|
+
function withSystemFields(tableName, zObject) {
|
|
497
|
+
return {
|
|
498
|
+
...zObject,
|
|
499
|
+
_id: zid(tableName),
|
|
500
|
+
_creationTime: z$1.number()
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function customFnBuilder(builder, customization) {
|
|
504
|
+
const customInput = customization.input ?? NoOp.input;
|
|
505
|
+
const inputArgs = customization.args ?? NoOp.args;
|
|
506
|
+
return function customBuilder(fn) {
|
|
507
|
+
const { args, handler = fn, skipConvexValidation = false, returns: maybeObject, ...extra } = fn;
|
|
508
|
+
const returns = maybeObject && !(maybeObject instanceof zCore.$ZodType) ? z$1.object(maybeObject) : maybeObject;
|
|
509
|
+
const returnValidator = returns && !skipConvexValidation ? { returns: zodOutputToConvex(returns) } : null;
|
|
510
|
+
if (args) {
|
|
511
|
+
let argsValidator = args;
|
|
512
|
+
if (argsValidator instanceof zCore.$ZodType) if (argsValidator instanceof zCore.$ZodObject) argsValidator = argsValidator._zod.def.shape;
|
|
513
|
+
else throw new Error("Unsupported zod type as args validator: " + argsValidator.constructor.name);
|
|
514
|
+
const convexValidator = zodToConvexFields(argsValidator);
|
|
515
|
+
return builder({
|
|
516
|
+
args: skipConvexValidation ? void 0 : addFieldsToValidator(convexValidator, inputArgs),
|
|
517
|
+
...returnValidator,
|
|
518
|
+
handler: async (ctx, allArgs) => {
|
|
519
|
+
const added = await customInput(ctx, pick(allArgs, Object.keys(inputArgs)), extra);
|
|
520
|
+
const rawArgs = pick(allArgs, Object.keys(argsValidator));
|
|
521
|
+
const parsed = await z$1.object(argsValidator).safeParseAsync(rawArgs);
|
|
522
|
+
if (!parsed.success) throw new ConvexError({ ZodError: JSON.parse(JSON.stringify(parsed.error.issues, null, 2)) });
|
|
523
|
+
const args = parsed.data;
|
|
524
|
+
const ret = await handler({
|
|
525
|
+
...ctx,
|
|
526
|
+
...added.ctx
|
|
527
|
+
}, {
|
|
528
|
+
...args,
|
|
529
|
+
...added.args
|
|
530
|
+
});
|
|
531
|
+
const result = returns ? await returns.parseAsync(ret === void 0 ? null : ret) : ret;
|
|
532
|
+
if (added.onSuccess) await added.onSuccess({
|
|
533
|
+
ctx,
|
|
534
|
+
args,
|
|
535
|
+
result
|
|
536
|
+
});
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (skipConvexValidation && Object.keys(inputArgs).length > 0) throw new Error("If you're using a custom function with arguments for the input customization, you cannot skip convex validation.");
|
|
542
|
+
return builder({
|
|
543
|
+
...returnValidator,
|
|
544
|
+
handler: async (ctx, args) => {
|
|
545
|
+
const added = await customInput(ctx, args, extra);
|
|
546
|
+
const ret = await handler({
|
|
547
|
+
...ctx,
|
|
548
|
+
...added.ctx
|
|
549
|
+
}, {
|
|
550
|
+
...args,
|
|
551
|
+
...added.args
|
|
552
|
+
});
|
|
553
|
+
const result = returns ? await returns.parseAsync(ret === void 0 ? null : ret) : ret;
|
|
554
|
+
if (added.onSuccess) await added.onSuccess({
|
|
555
|
+
ctx,
|
|
556
|
+
args,
|
|
557
|
+
result
|
|
558
|
+
});
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function zodToConvexCommon(validator, toConvex) {
|
|
565
|
+
if (validator instanceof zCore.$ZodString) return v.string();
|
|
566
|
+
if (validator instanceof zCore.$ZodNumber || validator instanceof zCore.$ZodNaN) return v.number();
|
|
567
|
+
if (validator instanceof zCore.$ZodBigInt) return v.int64();
|
|
568
|
+
if (validator instanceof zCore.$ZodBoolean) return v.boolean();
|
|
569
|
+
if (validator instanceof zCore.$ZodNull) return v.null();
|
|
570
|
+
if (validator instanceof zCore.$ZodAny || validator instanceof zCore.$ZodUnknown) return v.any();
|
|
571
|
+
if (validator instanceof zCore.$ZodArray) {
|
|
572
|
+
const inner = toConvex(validator._zod.def.element);
|
|
573
|
+
if (inner.isOptional === "optional") throw new Error("Arrays of optional values are not supported");
|
|
574
|
+
return v.array(inner);
|
|
575
|
+
}
|
|
576
|
+
if (validator instanceof zCore.$ZodObject) return v.object(Object.fromEntries(Object.entries(validator._zod.def.shape).map(([k, v]) => [k, toConvex(v)])));
|
|
577
|
+
if (validator instanceof zCore.$ZodUnion) return v.union(...validator._zod.def.options.map(toConvex));
|
|
578
|
+
if (validator instanceof zCore.$ZodNever) return v.union();
|
|
579
|
+
if (validator instanceof zCore.$ZodTuple) {
|
|
580
|
+
const { items, rest } = validator._zod.def;
|
|
581
|
+
return v.array(v.union(...[...items, ...rest !== null ? [rest] : []].map(toConvex)));
|
|
582
|
+
}
|
|
583
|
+
if (validator instanceof zCore.$ZodLiteral) {
|
|
584
|
+
const { values } = validator._zod.def;
|
|
585
|
+
if (values.length === 1) return convexToZodLiteral(values[0]);
|
|
586
|
+
return v.union(...values.map(convexToZodLiteral));
|
|
587
|
+
}
|
|
588
|
+
if (validator instanceof zCore.$ZodEnum) return v.union(...Object.entries(validator._zod.def.entries).filter(([key, value]) => key === value || isNaN(Number(key))).map(([_key, value]) => v.literal(value)));
|
|
589
|
+
if (validator instanceof zCore.$ZodOptional) return v.optional(toConvex(validator._zod.def.innerType));
|
|
590
|
+
if (validator instanceof zCore.$ZodNonOptional) return vRequired(toConvex(validator._zod.def.innerType));
|
|
591
|
+
if (validator instanceof zCore.$ZodNullable) {
|
|
592
|
+
const inner = toConvex(validator._zod.def.innerType);
|
|
593
|
+
if (inner.isOptional === "optional") return v.optional(v.union(vRequired(inner), v.null()));
|
|
594
|
+
return v.union(inner, v.null());
|
|
595
|
+
}
|
|
596
|
+
if (validator instanceof zCore.$ZodRecord) {
|
|
597
|
+
const { keyType, valueType } = validator._zod.def;
|
|
598
|
+
const isPartial = keyType._zod.values === void 0;
|
|
599
|
+
const valueValidator = toConvex(valueType);
|
|
600
|
+
const keyValidator = toConvex(keyType);
|
|
601
|
+
const stringLiterals = extractStringLiterals(keyValidator);
|
|
602
|
+
if (stringLiterals !== null) {
|
|
603
|
+
const fieldValue = isPartial || valueValidator.isOptional === "optional" ? v.optional(valueValidator) : vRequired(valueValidator);
|
|
604
|
+
const fields = {};
|
|
605
|
+
for (const literal of stringLiterals) fields[literal] = fieldValue;
|
|
606
|
+
return v.object(fields);
|
|
607
|
+
}
|
|
608
|
+
return v.record(isValidRecordKey(keyValidator) ? keyValidator : v.string(), vRequired(valueValidator));
|
|
609
|
+
}
|
|
610
|
+
if (validator instanceof zCore.$ZodReadonly) return toConvex(validator._zod.def.innerType);
|
|
611
|
+
if (validator instanceof zCore.$ZodLazy) return toConvex(validator._zod.def.getter());
|
|
612
|
+
if (validator instanceof zCore.$ZodTemplateLiteral) return v.string();
|
|
613
|
+
if (validator instanceof zCore.$ZodCustom) {
|
|
614
|
+
const idTableName = _zidRegistry.get(validator);
|
|
615
|
+
if (idTableName !== void 0 && typeof idTableName.tableName === "string") return v.id(idTableName.tableName);
|
|
616
|
+
return v.any();
|
|
617
|
+
}
|
|
618
|
+
if (validator instanceof zCore.$ZodIntersection) return v.any();
|
|
619
|
+
if (validator instanceof zCore.$ZodCatch) return toConvex(validator._zod.def.innerType);
|
|
620
|
+
if (validator instanceof zCore.$ZodDate || validator instanceof zCore.$ZodSymbol || validator instanceof zCore.$ZodMap || validator instanceof zCore.$ZodSet || validator instanceof zCore.$ZodPromise || validator instanceof zCore.$ZodFile || validator instanceof zCore.$ZodFunction || validator instanceof zCore.$ZodVoid || validator instanceof zCore.$ZodUndefined) throw new Error(`Validator ${validator.constructor.name} is not supported in Convex`);
|
|
621
|
+
return v.any();
|
|
622
|
+
}
|
|
623
|
+
function convexToZodLiteral(literal) {
|
|
624
|
+
if (literal === void 0) throw new Error("undefined is not a valid Convex value");
|
|
625
|
+
if (literal === null) return v.null();
|
|
626
|
+
return v.literal(literal);
|
|
627
|
+
}
|
|
628
|
+
function extractStringLiterals(validator) {
|
|
629
|
+
if (validator.kind === "literal") {
|
|
630
|
+
const literalValidator = validator;
|
|
631
|
+
if (typeof literalValidator.value === "string") return [literalValidator.value];
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
if (validator.kind === "union") {
|
|
635
|
+
const unionValidator = validator;
|
|
636
|
+
const literals = [];
|
|
637
|
+
for (const member of unionValidator.members) {
|
|
638
|
+
const memberLiterals = extractStringLiterals(member);
|
|
639
|
+
if (memberLiterals === null) return null;
|
|
640
|
+
literals.push(...memberLiterals);
|
|
641
|
+
}
|
|
642
|
+
return literals;
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function isValidRecordKey(validator) {
|
|
647
|
+
if (validator.kind === "string" || validator.kind === "id") return true;
|
|
648
|
+
if (validator.kind === "union") return validator.members.every(isValidRecordKey);
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
/** Stores the table names for each `Zid` instance that is created. */
|
|
652
|
+
const _zidRegistry = zCore.registry();
|
|
653
|
+
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/server/error.ts
|
|
656
|
+
/**
|
|
657
|
+
* CRPC Error - tRPC-style error handling for Convex
|
|
658
|
+
*
|
|
659
|
+
* Extends ConvexError with typed error codes and HTTP status mapping.
|
|
660
|
+
*/
|
|
661
|
+
/** JSON-RPC 2.0 error codes (tRPC-style) */
|
|
662
|
+
const CRPC_ERROR_CODES_BY_KEY = {
|
|
663
|
+
PARSE_ERROR: -32700,
|
|
664
|
+
BAD_REQUEST: -32600,
|
|
665
|
+
INTERNAL_SERVER_ERROR: -32603,
|
|
666
|
+
NOT_IMPLEMENTED: -32603,
|
|
667
|
+
BAD_GATEWAY: -32603,
|
|
668
|
+
SERVICE_UNAVAILABLE: -32603,
|
|
669
|
+
GATEWAY_TIMEOUT: -32603,
|
|
670
|
+
UNAUTHORIZED: -32001,
|
|
671
|
+
PAYMENT_REQUIRED: -32002,
|
|
672
|
+
FORBIDDEN: -32003,
|
|
673
|
+
NOT_FOUND: -32004,
|
|
674
|
+
METHOD_NOT_SUPPORTED: -32005,
|
|
675
|
+
TIMEOUT: -32008,
|
|
676
|
+
CONFLICT: -32009,
|
|
677
|
+
PRECONDITION_FAILED: -32012,
|
|
678
|
+
PAYLOAD_TOO_LARGE: -32013,
|
|
679
|
+
UNSUPPORTED_MEDIA_TYPE: -32015,
|
|
680
|
+
UNPROCESSABLE_CONTENT: -32022,
|
|
681
|
+
PRECONDITION_REQUIRED: -32028,
|
|
682
|
+
TOO_MANY_REQUESTS: -32029,
|
|
683
|
+
CLIENT_CLOSED_REQUEST: -32099
|
|
684
|
+
};
|
|
685
|
+
/** Map error codes to HTTP status codes */
|
|
686
|
+
const CRPC_ERROR_CODE_TO_HTTP = {
|
|
687
|
+
PARSE_ERROR: 400,
|
|
688
|
+
BAD_REQUEST: 400,
|
|
689
|
+
UNAUTHORIZED: 401,
|
|
690
|
+
PAYMENT_REQUIRED: 402,
|
|
691
|
+
FORBIDDEN: 403,
|
|
692
|
+
NOT_FOUND: 404,
|
|
693
|
+
METHOD_NOT_SUPPORTED: 405,
|
|
694
|
+
TIMEOUT: 408,
|
|
695
|
+
CONFLICT: 409,
|
|
696
|
+
PRECONDITION_FAILED: 412,
|
|
697
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
698
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
699
|
+
UNPROCESSABLE_CONTENT: 422,
|
|
700
|
+
PRECONDITION_REQUIRED: 428,
|
|
701
|
+
TOO_MANY_REQUESTS: 429,
|
|
702
|
+
CLIENT_CLOSED_REQUEST: 499,
|
|
703
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
704
|
+
NOT_IMPLEMENTED: 501,
|
|
705
|
+
BAD_GATEWAY: 502,
|
|
706
|
+
SERVICE_UNAVAILABLE: 503,
|
|
707
|
+
GATEWAY_TIMEOUT: 504
|
|
708
|
+
};
|
|
709
|
+
/** Extract Error from unknown cause (from tRPC) */
|
|
710
|
+
function getCauseFromUnknown(cause) {
|
|
711
|
+
if (cause instanceof Error) return cause;
|
|
712
|
+
if (typeof cause === "undefined" || typeof cause === "function" || cause === null) return;
|
|
713
|
+
if (typeof cause !== "object") return new Error(String(cause));
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* tRPC-style error extending ConvexError
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* ```typescript
|
|
720
|
+
* throw new CRPCError({
|
|
721
|
+
* code: 'BAD_REQUEST',
|
|
722
|
+
* message: 'Invalid input',
|
|
723
|
+
* cause: originalError,
|
|
724
|
+
* });
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
var CRPCError = class extends ConvexError {
|
|
728
|
+
code;
|
|
729
|
+
cause;
|
|
730
|
+
constructor(opts) {
|
|
731
|
+
const cause = getCauseFromUnknown(opts.cause);
|
|
732
|
+
const message = opts.message ?? cause?.message ?? opts.code;
|
|
733
|
+
super({
|
|
734
|
+
code: opts.code,
|
|
735
|
+
message
|
|
736
|
+
});
|
|
737
|
+
this.name = "CRPCError";
|
|
738
|
+
this.code = opts.code;
|
|
739
|
+
this.cause = cause;
|
|
740
|
+
this.message = message;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
function isOrmNotFoundErrorLike(cause) {
|
|
744
|
+
return cause instanceof Error && cause.name === "OrmNotFoundError";
|
|
745
|
+
}
|
|
746
|
+
function isApiErrorLike(cause) {
|
|
747
|
+
return cause instanceof Error && cause.name === "APIError" && typeof cause.statusCode === "number";
|
|
748
|
+
}
|
|
749
|
+
function mapHttpStatusCodeToCRPCCode(statusCode) {
|
|
750
|
+
switch (statusCode) {
|
|
751
|
+
case 400: return "BAD_REQUEST";
|
|
752
|
+
case 401: return "UNAUTHORIZED";
|
|
753
|
+
case 402: return "PAYMENT_REQUIRED";
|
|
754
|
+
case 403: return "FORBIDDEN";
|
|
755
|
+
case 404: return "NOT_FOUND";
|
|
756
|
+
case 405: return "METHOD_NOT_SUPPORTED";
|
|
757
|
+
case 408: return "TIMEOUT";
|
|
758
|
+
case 409: return "CONFLICT";
|
|
759
|
+
case 412: return "PRECONDITION_FAILED";
|
|
760
|
+
case 413: return "PAYLOAD_TOO_LARGE";
|
|
761
|
+
case 415: return "UNSUPPORTED_MEDIA_TYPE";
|
|
762
|
+
case 422: return "UNPROCESSABLE_CONTENT";
|
|
763
|
+
case 428: return "PRECONDITION_REQUIRED";
|
|
764
|
+
case 429: return "TOO_MANY_REQUESTS";
|
|
765
|
+
case 499: return "CLIENT_CLOSED_REQUEST";
|
|
766
|
+
default: return "INTERNAL_SERVER_ERROR";
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function getApiErrorMessage(cause) {
|
|
770
|
+
const body = cause.body;
|
|
771
|
+
if (body && typeof body === "object") {
|
|
772
|
+
const message = body.message;
|
|
773
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
774
|
+
}
|
|
775
|
+
if (typeof cause.message === "string" && cause.message.length > 0) return cause.message;
|
|
776
|
+
return "Request failed";
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Convert known framework/library errors into CRPCError.
|
|
780
|
+
*
|
|
781
|
+
* Intended for cRPC internals so callers don't need per-endpoint try/catch.
|
|
782
|
+
*/
|
|
783
|
+
function toCRPCError(cause) {
|
|
784
|
+
if (cause instanceof CRPCError) return cause;
|
|
785
|
+
if (cause instanceof Error && cause.name === "CRPCError") return cause;
|
|
786
|
+
if (isOrmNotFoundErrorLike(cause)) {
|
|
787
|
+
const err = new CRPCError({
|
|
788
|
+
code: "NOT_FOUND",
|
|
789
|
+
message: cause.message,
|
|
790
|
+
cause
|
|
791
|
+
});
|
|
792
|
+
if (cause.stack) err.stack = cause.stack;
|
|
793
|
+
return err;
|
|
794
|
+
}
|
|
795
|
+
if (isApiErrorLike(cause)) {
|
|
796
|
+
const status = cause.status;
|
|
797
|
+
const statusCode = cause.statusCode;
|
|
798
|
+
const err = new CRPCError({
|
|
799
|
+
code: typeof status === "string" && status in CRPC_ERROR_CODES_BY_KEY ? status : typeof statusCode === "number" ? mapHttpStatusCodeToCRPCCode(statusCode) : "INTERNAL_SERVER_ERROR",
|
|
800
|
+
message: getApiErrorMessage(cause),
|
|
801
|
+
cause
|
|
802
|
+
});
|
|
803
|
+
if (cause.stack) err.stack = cause.stack;
|
|
804
|
+
return err;
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Wrap unknown error in CRPCError (from tRPC)
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```typescript
|
|
813
|
+
* try {
|
|
814
|
+
* await someOperation();
|
|
815
|
+
* } catch (error) {
|
|
816
|
+
* throw getCRPCErrorFromUnknown(error);
|
|
817
|
+
* }
|
|
818
|
+
* ```
|
|
819
|
+
*/
|
|
820
|
+
function getCRPCErrorFromUnknown(cause) {
|
|
821
|
+
const handled = toCRPCError(cause);
|
|
822
|
+
if (handled) return handled;
|
|
823
|
+
const error = new CRPCError({
|
|
824
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
825
|
+
cause
|
|
826
|
+
});
|
|
827
|
+
if (cause instanceof Error && cause.stack) error.stack = cause.stack;
|
|
828
|
+
return error;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Get HTTP status code from CRPCError
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* ```typescript
|
|
835
|
+
* const httpStatus = getHTTPStatusCodeFromError(error); // 400
|
|
836
|
+
* ```
|
|
837
|
+
*/
|
|
838
|
+
function getHTTPStatusCodeFromError(error) {
|
|
839
|
+
return CRPC_ERROR_CODE_TO_HTTP[error.code] ?? 500;
|
|
840
|
+
}
|
|
841
|
+
/** Type guard for CRPCError */
|
|
842
|
+
function isCRPCError(error) {
|
|
843
|
+
return error instanceof CRPCError;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/server/http-builder.ts
|
|
848
|
+
function extractPathParams(path) {
|
|
849
|
+
const matches = path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
850
|
+
return matches ? matches.map((m) => m.slice(1)) : [];
|
|
851
|
+
}
|
|
852
|
+
function matchPathParams(template, pathname) {
|
|
853
|
+
const templateParts = template.split("/").filter(Boolean);
|
|
854
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
855
|
+
if (templateParts.length !== pathParts.length) return null;
|
|
856
|
+
const params = {};
|
|
857
|
+
for (let i = 0; i < templateParts.length; i++) {
|
|
858
|
+
const templatePart = templateParts[i];
|
|
859
|
+
const pathPart = pathParts[i];
|
|
860
|
+
if (templatePart.startsWith(":")) params[templatePart.slice(1)] = decodeURIComponent(pathPart);
|
|
861
|
+
else if (templatePart !== pathPart) return null;
|
|
862
|
+
}
|
|
863
|
+
return params;
|
|
864
|
+
}
|
|
865
|
+
function handleHttpError(error) {
|
|
866
|
+
if (error instanceof CRPCError) {
|
|
867
|
+
const status = {
|
|
868
|
+
BAD_REQUEST: 400,
|
|
869
|
+
UNAUTHORIZED: 401,
|
|
870
|
+
FORBIDDEN: 403,
|
|
871
|
+
NOT_FOUND: 404,
|
|
872
|
+
METHOD_NOT_SUPPORTED: 405,
|
|
873
|
+
CONFLICT: 409,
|
|
874
|
+
UNPROCESSABLE_CONTENT: 422,
|
|
875
|
+
TOO_MANY_REQUESTS: 429,
|
|
876
|
+
INTERNAL_SERVER_ERROR: 500
|
|
877
|
+
}[error.code] ?? 500;
|
|
878
|
+
return Response.json({ error: {
|
|
879
|
+
code: error.code,
|
|
880
|
+
message: error.message
|
|
881
|
+
} }, { status });
|
|
882
|
+
}
|
|
883
|
+
console.error("Unhandled HTTP error:", error);
|
|
884
|
+
return Response.json({ error: {
|
|
885
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
886
|
+
message: "An unexpected error occurred"
|
|
887
|
+
} }, { status: 500 });
|
|
888
|
+
}
|
|
889
|
+
function getBaseSchema(schema) {
|
|
890
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) return getBaseSchema(schema.unwrap());
|
|
891
|
+
if (schema instanceof z.ZodDefault) return getBaseSchema(schema._def.innerType);
|
|
892
|
+
return schema;
|
|
893
|
+
}
|
|
894
|
+
function isArraySchema(schema) {
|
|
895
|
+
return getBaseSchema(schema) instanceof z.ZodArray;
|
|
896
|
+
}
|
|
897
|
+
function isNumberSchema(schema) {
|
|
898
|
+
return getBaseSchema(schema) instanceof z.ZodNumber;
|
|
899
|
+
}
|
|
900
|
+
function isBooleanSchema(schema) {
|
|
901
|
+
return getBaseSchema(schema) instanceof z.ZodBoolean;
|
|
902
|
+
}
|
|
903
|
+
function parseQueryParams(url, schema) {
|
|
904
|
+
const params = {};
|
|
905
|
+
const keys = new Set(url.searchParams.keys());
|
|
906
|
+
const shape = schema instanceof z.ZodObject ? schema.shape : {};
|
|
907
|
+
for (const key of keys) {
|
|
908
|
+
const values = url.searchParams.getAll(key);
|
|
909
|
+
const fieldSchema = shape[key];
|
|
910
|
+
if (fieldSchema) if (isArraySchema(fieldSchema)) params[key] = values;
|
|
911
|
+
else if (isNumberSchema(fieldSchema)) params[key] = Number(values[0]);
|
|
912
|
+
else if (isBooleanSchema(fieldSchema)) {
|
|
913
|
+
const val = values[0].toLowerCase();
|
|
914
|
+
params[key] = val === "true" || val === "1";
|
|
915
|
+
} else params[key] = values.length === 1 ? values[0] : values;
|
|
916
|
+
else params[key] = values.length === 1 ? values[0] : values;
|
|
917
|
+
}
|
|
918
|
+
return params;
|
|
919
|
+
}
|
|
920
|
+
/** Factory function to create a new builder with merged def */
|
|
921
|
+
function createNewHttpBuilder(def1, def2) {
|
|
922
|
+
return createHttpBuilder({
|
|
923
|
+
...def1,
|
|
924
|
+
...def2
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
/** Internal method to create the HTTP procedure */
|
|
928
|
+
function createProcedure(def, handler, _type) {
|
|
929
|
+
if (!def.route) throw new Error("Route must be defined before action. Use .route(path, method) first.");
|
|
930
|
+
/**
|
|
931
|
+
* Hono-compatible handler function.
|
|
932
|
+
* When used with HttpRouterWithHono, Convex ctx is passed via c.env.
|
|
933
|
+
*/
|
|
934
|
+
const honoHandler = async (c) => {
|
|
935
|
+
const convexCtx = c.env;
|
|
936
|
+
const request = c.req.raw;
|
|
937
|
+
try {
|
|
938
|
+
const url = new URL(request.url);
|
|
939
|
+
const pathParams = c.req.param() ?? matchPathParams(def.route.path, url.pathname) ?? {};
|
|
940
|
+
let ctx = def.functionConfig.createContext(convexCtx);
|
|
941
|
+
const getRawInput = async () => {
|
|
942
|
+
if ((request.headers.get("content-type") ?? "").includes("application/json")) return request.clone().json();
|
|
943
|
+
return null;
|
|
944
|
+
};
|
|
945
|
+
let currentInput;
|
|
946
|
+
for (const middleware of def.middlewares) {
|
|
947
|
+
const result = await middleware({
|
|
948
|
+
ctx,
|
|
949
|
+
input: currentInput,
|
|
950
|
+
getRawInput,
|
|
951
|
+
next: async (opts) => {
|
|
952
|
+
if (opts?.ctx) ctx = {
|
|
953
|
+
...ctx,
|
|
954
|
+
...opts.ctx
|
|
955
|
+
};
|
|
956
|
+
if (opts?.input !== void 0) currentInput = opts.input;
|
|
957
|
+
return {
|
|
958
|
+
ctx,
|
|
959
|
+
marker: void 0
|
|
960
|
+
};
|
|
961
|
+
},
|
|
962
|
+
meta: def.meta
|
|
963
|
+
});
|
|
964
|
+
if (result?.ctx) ctx = {
|
|
965
|
+
...ctx,
|
|
966
|
+
...result.ctx
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
let parsedParams;
|
|
970
|
+
if (def.paramsSchema) try {
|
|
971
|
+
parsedParams = def.paramsSchema.parse(pathParams);
|
|
972
|
+
} catch (error) {
|
|
973
|
+
if (error instanceof z.ZodError) throw new CRPCError({
|
|
974
|
+
code: "BAD_REQUEST",
|
|
975
|
+
message: "Invalid path params",
|
|
976
|
+
cause: error
|
|
977
|
+
});
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
980
|
+
let parsedQuery;
|
|
981
|
+
if (def.querySchema) {
|
|
982
|
+
const queryParams = parseQueryParams(url, def.querySchema);
|
|
983
|
+
try {
|
|
984
|
+
parsedQuery = def.querySchema.parse(queryParams);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
if (error instanceof z.ZodError) throw new CRPCError({
|
|
987
|
+
code: "BAD_REQUEST",
|
|
988
|
+
message: "Invalid query params",
|
|
989
|
+
cause: error
|
|
990
|
+
});
|
|
991
|
+
throw error;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
let parsedInput;
|
|
995
|
+
if (def.inputSchema && request.method !== "GET") {
|
|
996
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
997
|
+
let body;
|
|
998
|
+
if (contentType.includes("application/json")) body = await request.json();
|
|
999
|
+
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1000
|
+
const formData = await request.formData();
|
|
1001
|
+
body = Object.fromEntries(formData.entries());
|
|
1002
|
+
} else body = await request.json().catch(() => ({}));
|
|
1003
|
+
try {
|
|
1004
|
+
parsedInput = def.inputSchema.parse(def.functionConfig.transformer.input.deserialize(body));
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
if (error instanceof z.ZodError) throw new CRPCError({
|
|
1007
|
+
code: "BAD_REQUEST",
|
|
1008
|
+
message: "Invalid input",
|
|
1009
|
+
cause: error
|
|
1010
|
+
});
|
|
1011
|
+
throw error;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
let parsedForm;
|
|
1015
|
+
if (def.formSchema && request.method !== "GET") {
|
|
1016
|
+
const formData = await request.formData();
|
|
1017
|
+
const formObj = {};
|
|
1018
|
+
for (const [key, value] of formData.entries()) formObj[key] = value;
|
|
1019
|
+
try {
|
|
1020
|
+
parsedForm = def.formSchema.parse(formObj);
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
if (error instanceof z.ZodError) throw new CRPCError({
|
|
1023
|
+
code: "BAD_REQUEST",
|
|
1024
|
+
message: "Invalid form data",
|
|
1025
|
+
cause: error
|
|
1026
|
+
});
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const handlerOpts = {
|
|
1031
|
+
ctx,
|
|
1032
|
+
c
|
|
1033
|
+
};
|
|
1034
|
+
if (parsedInput !== void 0) handlerOpts.input = parsedInput;
|
|
1035
|
+
if (parsedParams !== void 0) handlerOpts.params = parsedParams;
|
|
1036
|
+
if (parsedQuery !== void 0) handlerOpts.searchParams = parsedQuery;
|
|
1037
|
+
if (parsedForm !== void 0) handlerOpts.form = parsedForm;
|
|
1038
|
+
const result = await handler(handlerOpts);
|
|
1039
|
+
if (result instanceof Response) return result;
|
|
1040
|
+
const output = def.outputSchema ? def.outputSchema.parse(result) : result;
|
|
1041
|
+
return c.json(def.functionConfig.transformer.output.serialize(output));
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
return handleHttpError(error);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
honoHandler._crpcRoute = {
|
|
1047
|
+
path: def.route.path,
|
|
1048
|
+
method: def.route.method
|
|
1049
|
+
};
|
|
1050
|
+
const procedure = def.functionConfig.base(async (convexCtx, request) => {
|
|
1051
|
+
return honoHandler({
|
|
1052
|
+
env: convexCtx,
|
|
1053
|
+
req: {
|
|
1054
|
+
raw: request,
|
|
1055
|
+
param: () => matchPathParams(def.route.path, new URL(request.url).pathname) ?? {}
|
|
1056
|
+
},
|
|
1057
|
+
json: (data, status) => Response.json(data, { status }),
|
|
1058
|
+
text: (text, status) => new Response(text, { status }),
|
|
1059
|
+
body: (body, init) => new Response(body, init),
|
|
1060
|
+
html: (html, status) => new Response(html, {
|
|
1061
|
+
status,
|
|
1062
|
+
headers: { "Content-Type": "text/html" }
|
|
1063
|
+
}),
|
|
1064
|
+
redirect: (url, status) => Response.redirect(url, status ?? 302),
|
|
1065
|
+
header: (_name, _value) => {}
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
procedure.isHttp = true;
|
|
1069
|
+
procedure._crpcHttpRoute = def.route;
|
|
1070
|
+
procedure._def = {
|
|
1071
|
+
inputSchema: def.inputSchema,
|
|
1072
|
+
outputSchema: def.outputSchema,
|
|
1073
|
+
paramsSchema: def.paramsSchema,
|
|
1074
|
+
querySchema: def.querySchema,
|
|
1075
|
+
formSchema: def.formSchema
|
|
1076
|
+
};
|
|
1077
|
+
procedure._honoHandler = honoHandler;
|
|
1078
|
+
return procedure;
|
|
1079
|
+
}
|
|
1080
|
+
/** Create the builder implementation object */
|
|
1081
|
+
function createHttpBuilder(def) {
|
|
1082
|
+
return {
|
|
1083
|
+
_def: def,
|
|
1084
|
+
use(middlewareOrBuilder) {
|
|
1085
|
+
const middlewares = "_middlewares" in middlewareOrBuilder ? middlewareOrBuilder._middlewares : [middlewareOrBuilder];
|
|
1086
|
+
return createNewHttpBuilder(def, { middlewares: [...def.middlewares, ...middlewares] });
|
|
1087
|
+
},
|
|
1088
|
+
meta(value) {
|
|
1089
|
+
return createNewHttpBuilder(def, { meta: def.meta ? {
|
|
1090
|
+
...def.meta,
|
|
1091
|
+
...value
|
|
1092
|
+
} : value });
|
|
1093
|
+
},
|
|
1094
|
+
route(path, method) {
|
|
1095
|
+
const pathParamNames = extractPathParams(path);
|
|
1096
|
+
return createNewHttpBuilder(def, { route: {
|
|
1097
|
+
path,
|
|
1098
|
+
method,
|
|
1099
|
+
pathParamNames,
|
|
1100
|
+
usePathPrefix: pathParamNames.length > 0
|
|
1101
|
+
} });
|
|
1102
|
+
},
|
|
1103
|
+
get(path) {
|
|
1104
|
+
const pathParamNames = extractPathParams(path);
|
|
1105
|
+
return createNewHttpBuilder(def, { route: {
|
|
1106
|
+
path,
|
|
1107
|
+
method: "GET",
|
|
1108
|
+
pathParamNames,
|
|
1109
|
+
usePathPrefix: pathParamNames.length > 0
|
|
1110
|
+
} });
|
|
1111
|
+
},
|
|
1112
|
+
post(path) {
|
|
1113
|
+
const pathParamNames = extractPathParams(path);
|
|
1114
|
+
return createNewHttpBuilder(def, { route: {
|
|
1115
|
+
path,
|
|
1116
|
+
method: "POST",
|
|
1117
|
+
pathParamNames,
|
|
1118
|
+
usePathPrefix: pathParamNames.length > 0
|
|
1119
|
+
} });
|
|
1120
|
+
},
|
|
1121
|
+
put(path) {
|
|
1122
|
+
const pathParamNames = extractPathParams(path);
|
|
1123
|
+
return createNewHttpBuilder(def, { route: {
|
|
1124
|
+
path,
|
|
1125
|
+
method: "PUT",
|
|
1126
|
+
pathParamNames,
|
|
1127
|
+
usePathPrefix: pathParamNames.length > 0
|
|
1128
|
+
} });
|
|
1129
|
+
},
|
|
1130
|
+
patch(path) {
|
|
1131
|
+
const pathParamNames = extractPathParams(path);
|
|
1132
|
+
return createNewHttpBuilder(def, { route: {
|
|
1133
|
+
path,
|
|
1134
|
+
method: "PATCH",
|
|
1135
|
+
pathParamNames,
|
|
1136
|
+
usePathPrefix: pathParamNames.length > 0
|
|
1137
|
+
} });
|
|
1138
|
+
},
|
|
1139
|
+
delete(path) {
|
|
1140
|
+
const pathParamNames = extractPathParams(path);
|
|
1141
|
+
return createNewHttpBuilder(def, { route: {
|
|
1142
|
+
path,
|
|
1143
|
+
method: "DELETE",
|
|
1144
|
+
pathParamNames,
|
|
1145
|
+
usePathPrefix: pathParamNames.length > 0
|
|
1146
|
+
} });
|
|
1147
|
+
},
|
|
1148
|
+
params(schema) {
|
|
1149
|
+
return createNewHttpBuilder(def, { paramsSchema: schema });
|
|
1150
|
+
},
|
|
1151
|
+
searchParams(schema) {
|
|
1152
|
+
return createNewHttpBuilder(def, { querySchema: schema });
|
|
1153
|
+
},
|
|
1154
|
+
input(schema) {
|
|
1155
|
+
return createNewHttpBuilder(def, { inputSchema: schema });
|
|
1156
|
+
},
|
|
1157
|
+
output(schema) {
|
|
1158
|
+
return createNewHttpBuilder(def, { outputSchema: schema });
|
|
1159
|
+
},
|
|
1160
|
+
form(schema) {
|
|
1161
|
+
return createNewHttpBuilder(def, { formSchema: schema });
|
|
1162
|
+
},
|
|
1163
|
+
query(handler) {
|
|
1164
|
+
return createProcedure(def, handler, "query");
|
|
1165
|
+
},
|
|
1166
|
+
mutation(handler) {
|
|
1167
|
+
return createProcedure(def, handler, "mutation");
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Create initial HttpProcedureBuilder
|
|
1173
|
+
*/
|
|
1174
|
+
function createHttpProcedureBuilder(config) {
|
|
1175
|
+
return createHttpBuilder({
|
|
1176
|
+
middlewares: [],
|
|
1177
|
+
meta: config.meta,
|
|
1178
|
+
functionConfig: {
|
|
1179
|
+
base: config.base,
|
|
1180
|
+
createContext: config.createContext,
|
|
1181
|
+
transformer: getTransformer(config.transformer)
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
//#endregion
|
|
1187
|
+
//#region src/server/http-router.ts
|
|
1188
|
+
/**
|
|
1189
|
+
* Check if an export is a cRPC HTTP procedure
|
|
1190
|
+
* Note: Procedures are functions with attached properties, not plain objects
|
|
1191
|
+
*/
|
|
1192
|
+
function isCRPCHttpProcedure(value) {
|
|
1193
|
+
return typeof value === "function" && "isHttp" in value && value.isHttp === true && "_crpcHttpRoute" in value;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Check if a value is a cRPC HTTP router
|
|
1197
|
+
*/
|
|
1198
|
+
function isCRPCHttpRouter(value) {
|
|
1199
|
+
return typeof value === "object" && value !== null && "_def" in value && value._def?.router === true;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* HTTP Router that wraps a Hono app for use with Convex.
|
|
1203
|
+
* Internal class - use `createHttpRouter()` factory instead.
|
|
1204
|
+
*/
|
|
1205
|
+
var HttpRouterWithHono = class extends HttpRouter {
|
|
1206
|
+
_app;
|
|
1207
|
+
_handler;
|
|
1208
|
+
constructor(app) {
|
|
1209
|
+
super();
|
|
1210
|
+
this._app = app;
|
|
1211
|
+
this._handler = httpActionGeneric(async (ctx, request) => {
|
|
1212
|
+
return await app.fetch(request, ctx);
|
|
1213
|
+
});
|
|
1214
|
+
const parentGetRoutes = this.getRoutes.bind(this);
|
|
1215
|
+
const parentLookup = this.lookup.bind(this);
|
|
1216
|
+
/**
|
|
1217
|
+
* Get routes from the Hono app for Convex dashboard display.
|
|
1218
|
+
* Returns route definitions in the format expected by Convex.
|
|
1219
|
+
*/
|
|
1220
|
+
this.getRoutes = () => {
|
|
1221
|
+
const parentRoutes = parentGetRoutes();
|
|
1222
|
+
const honoRoutes = [];
|
|
1223
|
+
for (const route of this._app.routes) {
|
|
1224
|
+
const method = route.method.toUpperCase();
|
|
1225
|
+
if ([
|
|
1226
|
+
"GET",
|
|
1227
|
+
"POST",
|
|
1228
|
+
"PUT",
|
|
1229
|
+
"PATCH",
|
|
1230
|
+
"DELETE",
|
|
1231
|
+
"OPTIONS",
|
|
1232
|
+
"HEAD"
|
|
1233
|
+
].includes(method)) honoRoutes.push([
|
|
1234
|
+
route.path,
|
|
1235
|
+
method,
|
|
1236
|
+
this._handler
|
|
1237
|
+
]);
|
|
1238
|
+
}
|
|
1239
|
+
return [...parentRoutes.map((r) => [...r]), ...honoRoutes];
|
|
1240
|
+
};
|
|
1241
|
+
/**
|
|
1242
|
+
* Look up the handler for a given path and method.
|
|
1243
|
+
* Checks traditional routes first, then delegates to Hono's router.
|
|
1244
|
+
*/
|
|
1245
|
+
this.lookup = (path, method) => {
|
|
1246
|
+
const parentMatch = parentLookup(path, method);
|
|
1247
|
+
if (parentMatch !== null) return parentMatch;
|
|
1248
|
+
const normalizedMethod = method === "HEAD" ? "GET" : method;
|
|
1249
|
+
const matchResult = this._app.router.match(normalizedMethod, path);
|
|
1250
|
+
if (matchResult && matchResult[0].length > 0) return [
|
|
1251
|
+
this._handler,
|
|
1252
|
+
normalizedMethod,
|
|
1253
|
+
path
|
|
1254
|
+
];
|
|
1255
|
+
return null;
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
/**
|
|
1260
|
+
* Create a router factory function (like tRPC's createRouterFactory)
|
|
1261
|
+
*
|
|
1262
|
+
* @example
|
|
1263
|
+
* ```ts
|
|
1264
|
+
* // In crpc.ts
|
|
1265
|
+
* export const router = c.router;
|
|
1266
|
+
*
|
|
1267
|
+
* // In api/todos.ts
|
|
1268
|
+
* export const todosRouter = router({
|
|
1269
|
+
* get: publicRoute.get('/api/todos/:id')...,
|
|
1270
|
+
* create: authRoute.post('/api/todos')...,
|
|
1271
|
+
* });
|
|
1272
|
+
*
|
|
1273
|
+
* // In http.ts
|
|
1274
|
+
* export const httpRouter = router({
|
|
1275
|
+
* todos: todosRouter,
|
|
1276
|
+
* health,
|
|
1277
|
+
* });
|
|
1278
|
+
* export type AppRouter = typeof httpRouter;
|
|
1279
|
+
* ```
|
|
1280
|
+
*/
|
|
1281
|
+
function createHttpRouterFactory() {
|
|
1282
|
+
return function router(record) {
|
|
1283
|
+
const procedures = {};
|
|
1284
|
+
/**
|
|
1285
|
+
* Recursively flatten procedures with dot-notation paths
|
|
1286
|
+
* Like tRPC's step() function in router.ts
|
|
1287
|
+
*/
|
|
1288
|
+
function step(obj, path = []) {
|
|
1289
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1290
|
+
const newPath = [...path, key];
|
|
1291
|
+
const pathKey = newPath.join(".");
|
|
1292
|
+
if (isCRPCHttpProcedure(value)) procedures[pathKey] = value;
|
|
1293
|
+
else if (isCRPCHttpRouter(value)) for (const [procPath, proc] of Object.entries(value._def.procedures)) procedures[`${pathKey}.${procPath}`] = proc;
|
|
1294
|
+
else if (typeof value === "object" && value !== null) step(value, newPath);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
step(record);
|
|
1298
|
+
return { _def: {
|
|
1299
|
+
router: true,
|
|
1300
|
+
procedures,
|
|
1301
|
+
record
|
|
1302
|
+
} };
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Create an HTTP router with cRPC routes registered.
|
|
1307
|
+
*
|
|
1308
|
+
* @example
|
|
1309
|
+
* ```ts
|
|
1310
|
+
* import { Hono } from 'hono';
|
|
1311
|
+
* import { cors } from 'hono/cors';
|
|
1312
|
+
* import { createHttpRouter } from 'kitcn/server';
|
|
1313
|
+
*
|
|
1314
|
+
* const app = new Hono();
|
|
1315
|
+
* app.use('/api/*', cors({ origin: process.env.SITE_URL, credentials: true }));
|
|
1316
|
+
*
|
|
1317
|
+
* export default createHttpRouter(app, httpRouter);
|
|
1318
|
+
* ```
|
|
1319
|
+
*/
|
|
1320
|
+
function createHttpRouter(app, router) {
|
|
1321
|
+
for (const procedure of Object.values(router._def.procedures)) {
|
|
1322
|
+
const { path, method } = procedure._crpcHttpRoute;
|
|
1323
|
+
const honoHandler = procedure._honoHandler;
|
|
1324
|
+
if (!honoHandler) {
|
|
1325
|
+
console.warn(`Procedure at ${path} does not have a Hono handler. Make sure you are using the latest version of kitcn.`);
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
switch (method) {
|
|
1329
|
+
case "GET":
|
|
1330
|
+
app.get(path, honoHandler);
|
|
1331
|
+
break;
|
|
1332
|
+
case "POST":
|
|
1333
|
+
app.post(path, honoHandler);
|
|
1334
|
+
break;
|
|
1335
|
+
case "PUT":
|
|
1336
|
+
app.put(path, honoHandler);
|
|
1337
|
+
break;
|
|
1338
|
+
case "PATCH":
|
|
1339
|
+
app.patch(path, honoHandler);
|
|
1340
|
+
break;
|
|
1341
|
+
case "DELETE":
|
|
1342
|
+
app.delete(path, honoHandler);
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return new HttpRouterWithHono(app);
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Extract route map from procedures for client runtime
|
|
1350
|
+
*
|
|
1351
|
+
* @example
|
|
1352
|
+
* ```ts
|
|
1353
|
+
* export const httpRoutes = extractRouteMap(httpRouter._def.procedures);
|
|
1354
|
+
* ```
|
|
1355
|
+
*/
|
|
1356
|
+
function extractRouteMap(procedures) {
|
|
1357
|
+
const result = {};
|
|
1358
|
+
for (const [name, proc] of Object.entries(procedures)) if (isCRPCHttpProcedure(proc)) result[name] = {
|
|
1359
|
+
path: proc._crpcHttpRoute.path,
|
|
1360
|
+
method: proc._crpcHttpRoute.method
|
|
1361
|
+
};
|
|
1362
|
+
return result;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
//#endregion
|
|
1366
|
+
//#region src/server/builder.ts
|
|
1367
|
+
/**
|
|
1368
|
+
* CRPC - Convex RPC Builder
|
|
1369
|
+
* A tRPC-style fluent API for Convex functions
|
|
1370
|
+
*
|
|
1371
|
+
* Core library - no project-specific dependencies
|
|
1372
|
+
*/
|
|
1373
|
+
z.object({
|
|
1374
|
+
cursor: z.union([z.string(), z.null()]),
|
|
1375
|
+
limit: z.number()
|
|
1376
|
+
});
|
|
1377
|
+
z.object({
|
|
1378
|
+
cursor: z.union([z.string(), z.null()]).optional(),
|
|
1379
|
+
limit: z.number().optional()
|
|
1380
|
+
});
|
|
1381
|
+
/**
|
|
1382
|
+
* Create a middleware factory for building reusable middleware chains
|
|
1383
|
+
*
|
|
1384
|
+
* @example
|
|
1385
|
+
* ```typescript
|
|
1386
|
+
* const loggedIn = c.middleware(({ ctx, next }) => {
|
|
1387
|
+
* if (!ctx.userId) throw new CRPCError({ code: 'UNAUTHORIZED' });
|
|
1388
|
+
* return next({ ctx });
|
|
1389
|
+
* });
|
|
1390
|
+
*
|
|
1391
|
+
* const isAdmin = loggedIn.pipe(({ ctx, next }) => {
|
|
1392
|
+
* if (!ctx.user.isAdmin) throw new CRPCError({ code: 'FORBIDDEN' });
|
|
1393
|
+
* return next({ ctx });
|
|
1394
|
+
* });
|
|
1395
|
+
* ```
|
|
1396
|
+
*/
|
|
1397
|
+
function createMiddlewareFactory() {
|
|
1398
|
+
function createMiddlewareInner(middlewares) {
|
|
1399
|
+
return {
|
|
1400
|
+
_middlewares: middlewares,
|
|
1401
|
+
pipe(fn) {
|
|
1402
|
+
return createMiddlewareInner([...middlewares, fn]);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
return function createMiddleware(fn) {
|
|
1407
|
+
return createMiddlewareInner([fn]);
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
/** Execute middleware chain recursively with input access */
|
|
1411
|
+
async function executeMiddlewares(middlewares, ctx, meta, input, getRawInput, index = 0) {
|
|
1412
|
+
if (index >= middlewares.length) return {
|
|
1413
|
+
marker: void 0,
|
|
1414
|
+
ctx,
|
|
1415
|
+
input
|
|
1416
|
+
};
|
|
1417
|
+
const middleware = middlewares[index];
|
|
1418
|
+
let currentInput = input;
|
|
1419
|
+
const next = async (opts) => {
|
|
1420
|
+
const nextCtx = opts?.ctx ?? ctx;
|
|
1421
|
+
const nextInput = opts?.input ?? currentInput;
|
|
1422
|
+
if (opts?.input !== void 0) currentInput = opts.input;
|
|
1423
|
+
return await executeMiddlewares(middlewares, nextCtx, meta, nextInput, getRawInput, index + 1);
|
|
1424
|
+
};
|
|
1425
|
+
return {
|
|
1426
|
+
marker: void 0,
|
|
1427
|
+
ctx: (await middleware({
|
|
1428
|
+
ctx,
|
|
1429
|
+
meta,
|
|
1430
|
+
input,
|
|
1431
|
+
getRawInput,
|
|
1432
|
+
next
|
|
1433
|
+
})).ctx ?? ctx,
|
|
1434
|
+
input: currentInput
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
const isPlainObject = (value) => !!value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
1438
|
+
const toConvexSafeValue = (value) => {
|
|
1439
|
+
if (value instanceof Date) return value.getTime();
|
|
1440
|
+
if (Array.isArray(value)) {
|
|
1441
|
+
let serialized;
|
|
1442
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1443
|
+
const nested = value[index];
|
|
1444
|
+
const encoded = toConvexSafeValue(nested);
|
|
1445
|
+
const normalized = encoded === void 0 ? null : encoded;
|
|
1446
|
+
if (normalized !== nested) {
|
|
1447
|
+
if (!serialized) serialized = value.slice();
|
|
1448
|
+
serialized[index] = normalized;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return serialized ?? value;
|
|
1452
|
+
}
|
|
1453
|
+
if (!isPlainObject(value)) return value;
|
|
1454
|
+
let serialized;
|
|
1455
|
+
for (const key in value) {
|
|
1456
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
1457
|
+
const nested = value[key];
|
|
1458
|
+
const encoded = toConvexSafeValue(nested);
|
|
1459
|
+
if (encoded === void 0) {
|
|
1460
|
+
if (!serialized) serialized = { ...value };
|
|
1461
|
+
delete serialized[key];
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (encoded !== nested) {
|
|
1465
|
+
if (!serialized) serialized = { ...value };
|
|
1466
|
+
serialized[key] = encoded;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return serialized ?? value;
|
|
1470
|
+
};
|
|
1471
|
+
const wrapTwoArgRunner = (runner, owner) => {
|
|
1472
|
+
if (typeof runner !== "function") return;
|
|
1473
|
+
return (functionReference, args) => Reflect.apply(runner, owner, [functionReference, toConvexSafeValue(args)]);
|
|
1474
|
+
};
|
|
1475
|
+
const wrapSchedulerRunner = (runner, owner) => {
|
|
1476
|
+
if (typeof runner !== "function") return;
|
|
1477
|
+
return (first, functionReference, args) => Reflect.apply(runner, owner, [
|
|
1478
|
+
first,
|
|
1479
|
+
functionReference,
|
|
1480
|
+
toConvexSafeValue(args)
|
|
1481
|
+
]);
|
|
1482
|
+
};
|
|
1483
|
+
const withConvexSafeRunners = (ctx) => {
|
|
1484
|
+
if (!ctx || typeof ctx !== "object") return ctx;
|
|
1485
|
+
const contextObject = ctx;
|
|
1486
|
+
let changed = false;
|
|
1487
|
+
const wrappedContext = { ...contextObject };
|
|
1488
|
+
const runMutation = wrapTwoArgRunner(contextObject.runMutation, contextObject);
|
|
1489
|
+
if (runMutation) {
|
|
1490
|
+
wrappedContext.runMutation = runMutation;
|
|
1491
|
+
changed = true;
|
|
1492
|
+
}
|
|
1493
|
+
const runQuery = wrapTwoArgRunner(contextObject.runQuery, contextObject);
|
|
1494
|
+
if (runQuery) {
|
|
1495
|
+
wrappedContext.runQuery = runQuery;
|
|
1496
|
+
changed = true;
|
|
1497
|
+
}
|
|
1498
|
+
const runAction = wrapTwoArgRunner(contextObject.runAction, contextObject);
|
|
1499
|
+
if (runAction) {
|
|
1500
|
+
wrappedContext.runAction = runAction;
|
|
1501
|
+
changed = true;
|
|
1502
|
+
}
|
|
1503
|
+
const scheduler = contextObject.scheduler;
|
|
1504
|
+
if (scheduler && typeof scheduler === "object") {
|
|
1505
|
+
const schedulerObject = scheduler;
|
|
1506
|
+
let schedulerChanged = false;
|
|
1507
|
+
const wrappedScheduler = { ...schedulerObject };
|
|
1508
|
+
const runAfter = wrapSchedulerRunner(schedulerObject.runAfter, schedulerObject);
|
|
1509
|
+
if (runAfter) {
|
|
1510
|
+
wrappedScheduler.runAfter = runAfter;
|
|
1511
|
+
schedulerChanged = true;
|
|
1512
|
+
}
|
|
1513
|
+
const runAt = wrapSchedulerRunner(schedulerObject.runAt, schedulerObject);
|
|
1514
|
+
if (runAt) {
|
|
1515
|
+
wrappedScheduler.runAt = runAt;
|
|
1516
|
+
schedulerChanged = true;
|
|
1517
|
+
}
|
|
1518
|
+
if (schedulerChanged) {
|
|
1519
|
+
wrappedContext.scheduler = wrappedScheduler;
|
|
1520
|
+
changed = true;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return changed ? wrappedContext : contextObject;
|
|
1524
|
+
};
|
|
1525
|
+
const replaceUnencodableOutputTypes = (schema) => {
|
|
1526
|
+
if (schema instanceof z.ZodDate) return z.any();
|
|
1527
|
+
if (schema instanceof z.ZodArray) return z.array(replaceUnencodableOutputTypes(schema.element));
|
|
1528
|
+
if (schema instanceof z.ZodObject) {
|
|
1529
|
+
const nextShape = {};
|
|
1530
|
+
for (const [key, value] of Object.entries(schema.shape)) nextShape[key] = replaceUnencodableOutputTypes(value);
|
|
1531
|
+
return z.object(nextShape);
|
|
1532
|
+
}
|
|
1533
|
+
if (schema instanceof z.ZodUnion) return z.union(schema.options.map((option) => replaceUnencodableOutputTypes(option)));
|
|
1534
|
+
if (schema instanceof z.ZodOptional) return replaceUnencodableOutputTypes(schema.unwrap()).optional();
|
|
1535
|
+
if (schema instanceof z.ZodNullable) return replaceUnencodableOutputTypes(schema.unwrap()).nullable();
|
|
1536
|
+
if (schema instanceof z.ZodRecord) return z.record(schema.keyType, replaceUnencodableOutputTypes(schema.valueType));
|
|
1537
|
+
return schema;
|
|
1538
|
+
};
|
|
1539
|
+
const replaceUnencodableInputTypes = (schema) => {
|
|
1540
|
+
if (schema instanceof z.ZodDate) return z.any();
|
|
1541
|
+
if (schema instanceof z.ZodArray) return z.array(replaceUnencodableInputTypes(schema.element));
|
|
1542
|
+
if (schema instanceof z.ZodObject) {
|
|
1543
|
+
const nextShape = {};
|
|
1544
|
+
for (const [key, value] of Object.entries(schema.shape)) nextShape[key] = replaceUnencodableInputTypes(value);
|
|
1545
|
+
return z.object(nextShape);
|
|
1546
|
+
}
|
|
1547
|
+
if (schema instanceof z.ZodUnion) return z.union(schema.options.map((option) => replaceUnencodableInputTypes(option)));
|
|
1548
|
+
if (schema instanceof z.ZodOptional) return replaceUnencodableInputTypes(schema.unwrap()).optional();
|
|
1549
|
+
if (schema instanceof z.ZodNullable) return replaceUnencodableInputTypes(schema.unwrap()).nullable();
|
|
1550
|
+
if (schema instanceof z.ZodRecord) return z.record(schema.keyType, replaceUnencodableInputTypes(schema.valueType));
|
|
1551
|
+
if (schema instanceof z.ZodDefault) return replaceUnencodableInputTypes(schema.removeDefault());
|
|
1552
|
+
return schema;
|
|
1553
|
+
};
|
|
1554
|
+
const resolveConvexArgsShape = (inputShape) => {
|
|
1555
|
+
if (!inputShape) return;
|
|
1556
|
+
const rawSchema = z.object(inputShape);
|
|
1557
|
+
try {
|
|
1558
|
+
zodToConvex(rawSchema);
|
|
1559
|
+
return inputShape;
|
|
1560
|
+
} catch {
|
|
1561
|
+
const compatibleSchema = replaceUnencodableInputTypes(rawSchema);
|
|
1562
|
+
try {
|
|
1563
|
+
zodToConvex(compatibleSchema);
|
|
1564
|
+
return compatibleSchema.shape;
|
|
1565
|
+
} catch {
|
|
1566
|
+
return Object.fromEntries(Object.keys(inputShape).map((key) => [key, z.any()]));
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
const resolveConvexReturnsSchema = (schema) => {
|
|
1571
|
+
if (!schema) return;
|
|
1572
|
+
try {
|
|
1573
|
+
zodOutputToConvex(schema);
|
|
1574
|
+
return schema;
|
|
1575
|
+
} catch {
|
|
1576
|
+
const compatibleSchema = replaceUnencodableOutputTypes(schema);
|
|
1577
|
+
try {
|
|
1578
|
+
zodOutputToConvex(compatibleSchema);
|
|
1579
|
+
return compatibleSchema;
|
|
1580
|
+
} catch {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
/**
|
|
1586
|
+
* Fluent procedure builder with full type inference
|
|
1587
|
+
*
|
|
1588
|
+
* @typeParam TBaseCtx - Base context type from config
|
|
1589
|
+
* @typeParam TContext - Current context type (starts as TBaseCtx)
|
|
1590
|
+
* @typeParam TContextOverrides - Accumulated context from middleware (starts as UnsetMarker)
|
|
1591
|
+
* @typeParam TInput - Input schema (starts as UnsetMarker)
|
|
1592
|
+
* @typeParam TOutput - Output schema (starts as UnsetMarker)
|
|
1593
|
+
* @typeParam TMeta - Procedure metadata type
|
|
1594
|
+
*/
|
|
1595
|
+
var ProcedureBuilder = class {
|
|
1596
|
+
_def;
|
|
1597
|
+
constructor(def) {
|
|
1598
|
+
this._def = def;
|
|
1599
|
+
}
|
|
1600
|
+
/** Add middleware that transforms the context - to be overridden by subclasses */
|
|
1601
|
+
_use(middlewareOrBuilder) {
|
|
1602
|
+
const middlewares = "_middlewares" in middlewareOrBuilder ? middlewareOrBuilder._middlewares : [middlewareOrBuilder];
|
|
1603
|
+
return {
|
|
1604
|
+
...this._def,
|
|
1605
|
+
middlewares: [...this._def.middlewares, ...middlewares]
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
/** Define input schema (chainable - schemas are merged) - to be overridden by subclasses */
|
|
1609
|
+
_input(schema) {
|
|
1610
|
+
return {
|
|
1611
|
+
...this._def,
|
|
1612
|
+
inputSchemas: [...this._def.inputSchemas, schema.shape]
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
/** Define output schema - to be overridden by subclasses */
|
|
1616
|
+
_output(schema) {
|
|
1617
|
+
return {
|
|
1618
|
+
...this._def,
|
|
1619
|
+
outputSchema: schema
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
/** Set procedure metadata (shallow merged when chained) - to be overridden by subclasses */
|
|
1623
|
+
_meta(value) {
|
|
1624
|
+
return {
|
|
1625
|
+
...this._def,
|
|
1626
|
+
meta: this._def.meta ? {
|
|
1627
|
+
...this._def.meta,
|
|
1628
|
+
...value
|
|
1629
|
+
} : value
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
/** Merge all input schemas into one */
|
|
1633
|
+
_getMergedInput() {
|
|
1634
|
+
const { inputSchemas } = this._def;
|
|
1635
|
+
if (inputSchemas.length === 0) return;
|
|
1636
|
+
return Object.assign({}, ...inputSchemas);
|
|
1637
|
+
}
|
|
1638
|
+
_createFunction(handler, baseFunction, customFn, fnType) {
|
|
1639
|
+
const { middlewares, outputSchema, meta, functionConfig, isInternal } = this._def;
|
|
1640
|
+
const mergedInput = this._getMergedInput();
|
|
1641
|
+
const inputSchema = mergedInput ? z.object(mergedInput) : void 0;
|
|
1642
|
+
const convexArgs = resolveConvexArgsShape(mergedInput);
|
|
1643
|
+
const customFunction = customFn(baseFunction, customCtx(async (_ctx) => withConvexSafeRunners(await functionConfig.createContext(_ctx))));
|
|
1644
|
+
const returnsSchema = resolveConvexReturnsSchema(outputSchema);
|
|
1645
|
+
const typedReturnsSchema = returnsSchema;
|
|
1646
|
+
const typedArgs = convexArgs ?? {};
|
|
1647
|
+
const shouldValidateOutputWithZod = !!outputSchema && returnsSchema !== outputSchema;
|
|
1648
|
+
const fn = customFunction({
|
|
1649
|
+
args: typedArgs,
|
|
1650
|
+
...typedReturnsSchema ? { returns: typedReturnsSchema } : {},
|
|
1651
|
+
handler: async (ctx, rawInput) => {
|
|
1652
|
+
const decodedInput = functionConfig.transformer.input.deserialize(rawInput);
|
|
1653
|
+
const parsedInput = inputSchema ? inputSchema.parse(decodedInput) : decodedInput;
|
|
1654
|
+
const getRawInput = async () => parsedInput;
|
|
1655
|
+
try {
|
|
1656
|
+
const result = await executeMiddlewares(middlewares, ctx, meta, parsedInput, getRawInput);
|
|
1657
|
+
const handlerInput = result.input === parsedInput ? parsedInput : functionConfig.transformer.input.deserialize(result.input ?? parsedInput);
|
|
1658
|
+
const output = await handler({
|
|
1659
|
+
ctx: result.ctx,
|
|
1660
|
+
input: handlerInput
|
|
1661
|
+
});
|
|
1662
|
+
const validatedOutput = shouldValidateOutputWithZod ? outputSchema.parse(output) : output;
|
|
1663
|
+
return functionConfig.transformer.output.serialize(validatedOutput);
|
|
1664
|
+
} catch (cause) {
|
|
1665
|
+
const err = toCRPCError(cause);
|
|
1666
|
+
if (err) throw err;
|
|
1667
|
+
throw cause;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
fn._crpcMeta = {
|
|
1672
|
+
type: fnType,
|
|
1673
|
+
internal: isInternal ?? false,
|
|
1674
|
+
...meta
|
|
1675
|
+
};
|
|
1676
|
+
fn.__kitcnTransformer = functionConfig.transformer;
|
|
1677
|
+
fn.__kitcnRawHandler = (opts) => handler(opts);
|
|
1678
|
+
return fn;
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
/**
|
|
1682
|
+
* Query-specific procedure builder
|
|
1683
|
+
* Only exposes .query() and .internalQuery() methods
|
|
1684
|
+
*/
|
|
1685
|
+
var QueryProcedureBuilder = class QueryProcedureBuilder extends ProcedureBuilder {
|
|
1686
|
+
/**
|
|
1687
|
+
* Add middleware that transforms the context
|
|
1688
|
+
* Middleware receives typed input if called after .input(), unknown otherwise
|
|
1689
|
+
* $ContextOverridesOut is inferred from next()
|
|
1690
|
+
*/
|
|
1691
|
+
use(middlewareOrBuilder) {
|
|
1692
|
+
return new QueryProcedureBuilder(this._use(middlewareOrBuilder));
|
|
1693
|
+
}
|
|
1694
|
+
/** Set procedure metadata (shallow merged when chained) */
|
|
1695
|
+
meta(value) {
|
|
1696
|
+
return new QueryProcedureBuilder(this._meta(value));
|
|
1697
|
+
}
|
|
1698
|
+
/** Define input schema (chainable - schemas are merged) */
|
|
1699
|
+
input(schema) {
|
|
1700
|
+
return new QueryProcedureBuilder(this._input(schema));
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Add pagination input (chainable before .query())
|
|
1704
|
+
*
|
|
1705
|
+
* Creates flat { cursor, limit } input like tRPC and auto-wraps output.
|
|
1706
|
+
* User accesses args.cursor and args.limit directly.
|
|
1707
|
+
*
|
|
1708
|
+
* @param opts.limit - Default/max items per page
|
|
1709
|
+
* @param opts.item - Zod schema for each item in the page array
|
|
1710
|
+
*/
|
|
1711
|
+
paginated(opts) {
|
|
1712
|
+
const paginationSchemaWithDefault = z.object({
|
|
1713
|
+
cursor: z.union([z.string(), z.null()]).default(null),
|
|
1714
|
+
limit: z.number().default(opts.limit).transform((n) => Math.min(n, opts.limit))
|
|
1715
|
+
});
|
|
1716
|
+
const outputSchema = z.object({
|
|
1717
|
+
continueCursor: z.union([z.string(), z.null()]),
|
|
1718
|
+
isDone: z.boolean(),
|
|
1719
|
+
page: z.array(opts.item)
|
|
1720
|
+
});
|
|
1721
|
+
return new QueryProcedureBuilder({
|
|
1722
|
+
...this._def,
|
|
1723
|
+
inputSchemas: [...this._def.inputSchemas, paginationSchemaWithDefault.shape],
|
|
1724
|
+
outputSchema,
|
|
1725
|
+
meta: {
|
|
1726
|
+
...this._def.meta,
|
|
1727
|
+
limit: opts.limit
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
/** Define output schema */
|
|
1732
|
+
output(schema) {
|
|
1733
|
+
return new QueryProcedureBuilder(this._output(schema));
|
|
1734
|
+
}
|
|
1735
|
+
/** Create a query */
|
|
1736
|
+
query(handler) {
|
|
1737
|
+
return this._createFunction(handler, this._def.functionConfig.base, zCustomQuery, "query");
|
|
1738
|
+
}
|
|
1739
|
+
/** Mark as internal - returns chainable builder using internal function */
|
|
1740
|
+
internal() {
|
|
1741
|
+
const internal = this._def.functionConfig.internal;
|
|
1742
|
+
if (!internal) throw new Error("internalQuery base function not configured");
|
|
1743
|
+
return new QueryProcedureBuilder({
|
|
1744
|
+
...this._def,
|
|
1745
|
+
isInternal: true,
|
|
1746
|
+
functionConfig: {
|
|
1747
|
+
...this._def.functionConfig,
|
|
1748
|
+
base: internal
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
/**
|
|
1754
|
+
* Mutation-specific procedure builder
|
|
1755
|
+
* Only exposes .mutation() and .internalMutation() methods
|
|
1756
|
+
*/
|
|
1757
|
+
var MutationProcedureBuilder = class MutationProcedureBuilder extends ProcedureBuilder {
|
|
1758
|
+
/**
|
|
1759
|
+
* Add middleware that transforms the context
|
|
1760
|
+
* Middleware receives typed input if called after .input(), unknown otherwise
|
|
1761
|
+
* $ContextOverridesOut is inferred from next()
|
|
1762
|
+
*/
|
|
1763
|
+
use(middlewareOrBuilder) {
|
|
1764
|
+
return new MutationProcedureBuilder(this._use(middlewareOrBuilder));
|
|
1765
|
+
}
|
|
1766
|
+
/** Set procedure metadata (shallow merged when chained) */
|
|
1767
|
+
meta(value) {
|
|
1768
|
+
return new MutationProcedureBuilder(this._meta(value));
|
|
1769
|
+
}
|
|
1770
|
+
/** Define input schema (chainable - schemas are merged) */
|
|
1771
|
+
input(schema) {
|
|
1772
|
+
return new MutationProcedureBuilder(this._input(schema));
|
|
1773
|
+
}
|
|
1774
|
+
/** Define output schema */
|
|
1775
|
+
output(schema) {
|
|
1776
|
+
return new MutationProcedureBuilder(this._output(schema));
|
|
1777
|
+
}
|
|
1778
|
+
/** Create a mutation */
|
|
1779
|
+
mutation(handler) {
|
|
1780
|
+
return this._createFunction(handler, this._def.functionConfig.base, zCustomMutation, "mutation");
|
|
1781
|
+
}
|
|
1782
|
+
/** Mark as internal - returns chainable builder using internal function */
|
|
1783
|
+
internal() {
|
|
1784
|
+
const internal = this._def.functionConfig.internal;
|
|
1785
|
+
if (!internal) throw new Error("internalMutation base function not configured");
|
|
1786
|
+
return new MutationProcedureBuilder({
|
|
1787
|
+
...this._def,
|
|
1788
|
+
isInternal: true,
|
|
1789
|
+
functionConfig: {
|
|
1790
|
+
...this._def.functionConfig,
|
|
1791
|
+
base: internal
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
/**
|
|
1797
|
+
* Action-specific procedure builder
|
|
1798
|
+
* Only exposes .action() and .internalAction() methods
|
|
1799
|
+
*/
|
|
1800
|
+
var ActionProcedureBuilder = class ActionProcedureBuilder extends ProcedureBuilder {
|
|
1801
|
+
/** Add middleware that transforms the context - $ContextOverridesOut is inferred from next() */
|
|
1802
|
+
use(middlewareOrBuilder) {
|
|
1803
|
+
return new ActionProcedureBuilder(this._use(middlewareOrBuilder));
|
|
1804
|
+
}
|
|
1805
|
+
/** Set procedure metadata (shallow merged when chained) */
|
|
1806
|
+
meta(value) {
|
|
1807
|
+
return new ActionProcedureBuilder(this._meta(value));
|
|
1808
|
+
}
|
|
1809
|
+
/** Define input schema (chainable - schemas are merged) */
|
|
1810
|
+
input(schema) {
|
|
1811
|
+
return new ActionProcedureBuilder(this._input(schema));
|
|
1812
|
+
}
|
|
1813
|
+
/** Define output schema */
|
|
1814
|
+
output(schema) {
|
|
1815
|
+
return new ActionProcedureBuilder(this._output(schema));
|
|
1816
|
+
}
|
|
1817
|
+
/** Create an action */
|
|
1818
|
+
action(handler) {
|
|
1819
|
+
return this._createFunction(handler, this._def.functionConfig.base, zCustomAction, "action");
|
|
1820
|
+
}
|
|
1821
|
+
/** Mark as internal - returns chainable builder using internal function */
|
|
1822
|
+
internal() {
|
|
1823
|
+
const internal = this._def.functionConfig.internal;
|
|
1824
|
+
if (!internal) throw new Error("internalAction base function not configured");
|
|
1825
|
+
return new ActionProcedureBuilder({
|
|
1826
|
+
...this._def,
|
|
1827
|
+
isInternal: true,
|
|
1828
|
+
functionConfig: {
|
|
1829
|
+
...this._def.functionConfig,
|
|
1830
|
+
base: internal
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
/**
|
|
1836
|
+
* Builder with context configured, ready to create instance
|
|
1837
|
+
*/
|
|
1838
|
+
var CRPCBuilderWithContext = class {
|
|
1839
|
+
contextConfig;
|
|
1840
|
+
constructor(contextConfig) {
|
|
1841
|
+
this.contextConfig = contextConfig;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Define the metadata type for procedures (can be called after context)
|
|
1845
|
+
*/
|
|
1846
|
+
meta() {
|
|
1847
|
+
return this;
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Create the CRPC instance with function builders
|
|
1851
|
+
*/
|
|
1852
|
+
create(config) {
|
|
1853
|
+
const { defaultMeta = {}, query = queryGeneric, internalQuery = internalQueryGeneric, mutation = mutationGeneric, internalMutation = internalMutationGeneric, action = actionGeneric, internalAction = internalActionGeneric, httpAction = httpActionGeneric, transformer: transformerOptions } = config ?? {};
|
|
1854
|
+
const transformer = getTransformer(transformerOptions);
|
|
1855
|
+
const mutationCreateContext = this.contextConfig.mutation ?? ((ctx) => ctx);
|
|
1856
|
+
return {
|
|
1857
|
+
query: new QueryProcedureBuilder({
|
|
1858
|
+
middlewares: [],
|
|
1859
|
+
inputSchemas: [],
|
|
1860
|
+
meta: defaultMeta,
|
|
1861
|
+
functionConfig: {
|
|
1862
|
+
base: query,
|
|
1863
|
+
internal: internalQuery,
|
|
1864
|
+
createContext: this.contextConfig.query ?? ((ctx) => ctx),
|
|
1865
|
+
transformer
|
|
1866
|
+
}
|
|
1867
|
+
}),
|
|
1868
|
+
mutation: new MutationProcedureBuilder({
|
|
1869
|
+
middlewares: [],
|
|
1870
|
+
inputSchemas: [],
|
|
1871
|
+
meta: defaultMeta,
|
|
1872
|
+
functionConfig: {
|
|
1873
|
+
base: mutation,
|
|
1874
|
+
internal: internalMutation,
|
|
1875
|
+
createContext: mutationCreateContext,
|
|
1876
|
+
transformer
|
|
1877
|
+
}
|
|
1878
|
+
}),
|
|
1879
|
+
action: new ActionProcedureBuilder({
|
|
1880
|
+
middlewares: [],
|
|
1881
|
+
inputSchemas: [],
|
|
1882
|
+
meta: defaultMeta,
|
|
1883
|
+
functionConfig: {
|
|
1884
|
+
base: action,
|
|
1885
|
+
internal: internalAction,
|
|
1886
|
+
createContext: this.contextConfig.action ?? ((ctx) => ctx),
|
|
1887
|
+
transformer
|
|
1888
|
+
}
|
|
1889
|
+
}),
|
|
1890
|
+
httpAction: createHttpProcedureBuilder({
|
|
1891
|
+
base: httpAction,
|
|
1892
|
+
createContext: this.contextConfig.action ?? ((ctx) => ctx),
|
|
1893
|
+
meta: defaultMeta,
|
|
1894
|
+
transformer: transformerOptions
|
|
1895
|
+
}),
|
|
1896
|
+
middleware: createMiddlewareFactory(),
|
|
1897
|
+
router: createHttpRouterFactory()
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
/**
|
|
1902
|
+
* Builder with meta type configured
|
|
1903
|
+
*/
|
|
1904
|
+
var CRPCBuilderWithMeta = class {
|
|
1905
|
+
/**
|
|
1906
|
+
* Configure context creators for each function type
|
|
1907
|
+
*/
|
|
1908
|
+
context(config) {
|
|
1909
|
+
return new CRPCBuilderWithContext(config);
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Create the CRPC instance directly (uses default passthrough context)
|
|
1913
|
+
*/
|
|
1914
|
+
create(config) {
|
|
1915
|
+
return new CRPCBuilderWithContext({}).create(config);
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
/**
|
|
1919
|
+
* Initial CRPC builder - configure meta and context
|
|
1920
|
+
*/
|
|
1921
|
+
var CRPCBuilder = class {
|
|
1922
|
+
/**
|
|
1923
|
+
* Define the metadata type for procedures
|
|
1924
|
+
*/
|
|
1925
|
+
meta() {
|
|
1926
|
+
return new CRPCBuilderWithMeta();
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Configure context creators for each function type
|
|
1930
|
+
*/
|
|
1931
|
+
context(config) {
|
|
1932
|
+
return new CRPCBuilderWithContext(config);
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Create the CRPC instance directly (uses default passthrough context)
|
|
1936
|
+
*/
|
|
1937
|
+
create(config) {
|
|
1938
|
+
return new CRPCBuilderWithContext({}).create(config);
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
/**
|
|
1942
|
+
* CRPC entry point - tRPC-style object
|
|
1943
|
+
*
|
|
1944
|
+
* @example
|
|
1945
|
+
* ```typescript
|
|
1946
|
+
* // With explicit DataModel type
|
|
1947
|
+
* const c = initCRPC
|
|
1948
|
+
* .dataModel<DataModel>()
|
|
1949
|
+
* .context({...})
|
|
1950
|
+
* .create();
|
|
1951
|
+
*
|
|
1952
|
+
* // Without DataModel (uses GenericDataModel)
|
|
1953
|
+
* const c = initCRPC
|
|
1954
|
+
* .context({...})
|
|
1955
|
+
* .create();
|
|
1956
|
+
* ```
|
|
1957
|
+
*/
|
|
1958
|
+
const initCRPC = {
|
|
1959
|
+
dataModel() {
|
|
1960
|
+
return new CRPCBuilder();
|
|
1961
|
+
},
|
|
1962
|
+
meta() {
|
|
1963
|
+
return new CRPCBuilderWithMeta();
|
|
1964
|
+
},
|
|
1965
|
+
context(config) {
|
|
1966
|
+
return new CRPCBuilderWithContext(config);
|
|
1967
|
+
},
|
|
1968
|
+
create(config) {
|
|
1969
|
+
return new CRPCBuilderWithContext({}).create(config);
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
//#endregion
|
|
1974
|
+
export { zodOutputToConvexFields as A, convexToZodFields as C, zCustomQuery as D, zCustomMutation as E, zodToConvexFields as M, zid as O, convexToZod as S, zCustomAction as T, CRPC_ERROR_CODE_TO_HTTP as _, createMiddlewareFactory as a, isCRPCError as b, createHttpRouter as c, createHttpProcedureBuilder as d, extractPathParams as f, CRPC_ERROR_CODES_BY_KEY as g, CRPCError as h, QueryProcedureBuilder as i, zodToConvex as j, zodOutputToConvex as k, createHttpRouterFactory as l, matchPathParams as m, MutationProcedureBuilder as n, initCRPC as o, handleHttpError as p, ProcedureBuilder as r, HttpRouterWithHono as s, ActionProcedureBuilder as t, extractRouteMap as u, getCRPCErrorFromUnknown as v, withSystemFields as w, toCRPCError as x, getHTTPStatusCodeFromError as y };
|