@richie-rpc/server 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -10
- package/dist/cjs/index.cjs +17 -38
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +18 -39
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -97,7 +97,8 @@ Bun.serve({
|
|
|
97
97
|
- ✅ Path parameter matching
|
|
98
98
|
- ✅ Query parameter parsing
|
|
99
99
|
- ✅ JSON body parsing
|
|
100
|
-
- ✅
|
|
100
|
+
- ✅ File uploads with `multipart/form-data`
|
|
101
|
+
- ✅ Nested file structures in request bodies
|
|
101
102
|
- ✅ BasePath support for serving APIs under path prefixes
|
|
102
103
|
- ✅ Detailed validation errors
|
|
103
104
|
- ✅ 404 handling for unknown routes
|
|
@@ -186,21 +187,248 @@ const router = createRouter(contract, {
|
|
|
186
187
|
});
|
|
187
188
|
```
|
|
188
189
|
|
|
190
|
+
## Handling File Uploads
|
|
191
|
+
|
|
192
|
+
The server automatically handles `multipart/form-data` requests when the contract specifies `contentType: 'multipart/form-data'`. File objects are fully reconstructed and passed to your handler:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const contract = defineContract({
|
|
196
|
+
uploadDocuments: {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
path: '/upload',
|
|
199
|
+
contentType: 'multipart/form-data',
|
|
200
|
+
body: z.object({
|
|
201
|
+
documents: z.array(z.object({
|
|
202
|
+
file: z.instanceof(File),
|
|
203
|
+
name: z.string(),
|
|
204
|
+
tags: z.array(z.string()).optional(),
|
|
205
|
+
})),
|
|
206
|
+
category: z.string(),
|
|
207
|
+
}),
|
|
208
|
+
responses: {
|
|
209
|
+
[Status.Created]: z.object({
|
|
210
|
+
uploadedCount: z.number(),
|
|
211
|
+
totalSize: z.number(),
|
|
212
|
+
}),
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const router = createRouter(contract, {
|
|
218
|
+
uploadDocuments: async ({ body }) => {
|
|
219
|
+
// body.documents is fully typed with File objects
|
|
220
|
+
let totalSize = 0;
|
|
221
|
+
|
|
222
|
+
for (const doc of body.documents) {
|
|
223
|
+
// doc.file is a File object
|
|
224
|
+
const buffer = await doc.file.arrayBuffer();
|
|
225
|
+
totalSize += buffer.byteLength;
|
|
226
|
+
|
|
227
|
+
console.log(`Processing: ${doc.name} (${doc.file.name})`);
|
|
228
|
+
console.log(`Tags: ${doc.tags?.join(', ') ?? 'none'}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
status: Status.Created,
|
|
233
|
+
body: {
|
|
234
|
+
uploadedCount: body.documents.length,
|
|
235
|
+
totalSize,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The server automatically:
|
|
243
|
+
- Parses `multipart/form-data` requests
|
|
244
|
+
- Reconstructs nested structures with File objects
|
|
245
|
+
- Validates the body against your Zod schema
|
|
246
|
+
|
|
189
247
|
## Error Handling
|
|
190
248
|
|
|
191
|
-
The router
|
|
249
|
+
The router throws specific error classes that you can catch and handle. These errors are thrown before handlers are called, so you should wrap your router calls in try-catch blocks.
|
|
250
|
+
|
|
251
|
+
### Error Classes
|
|
252
|
+
|
|
253
|
+
#### `ValidationError`
|
|
192
254
|
|
|
193
|
-
|
|
194
|
-
- **Route Not Found** (404): Unknown endpoints
|
|
195
|
-
- **Internal Errors** (500): Uncaught exceptions
|
|
255
|
+
Thrown when request or response validation fails. Contains detailed Zod validation issues.
|
|
196
256
|
|
|
197
|
-
|
|
257
|
+
**Properties:**
|
|
258
|
+
- `field: string` - The field that failed validation (`"params"`, `"query"`, `"headers"`, `"body"`, or `"response[status]"`)
|
|
259
|
+
- `issues: z.ZodIssue[]` - Array of Zod validation issues with detailed error information
|
|
260
|
+
- `message: string` - Error message
|
|
261
|
+
|
|
262
|
+
**When thrown:**
|
|
263
|
+
- Invalid path parameters (params)
|
|
264
|
+
- Invalid query parameters (query)
|
|
265
|
+
- Invalid request headers (headers)
|
|
266
|
+
- Invalid request body (body)
|
|
267
|
+
- Invalid response body returned from handler (response validation)
|
|
268
|
+
|
|
269
|
+
**Example:**
|
|
198
270
|
|
|
199
271
|
```typescript
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
272
|
+
import { createRouter, ValidationError, RouteNotFoundError } from '@richie-rpc/server';
|
|
273
|
+
|
|
274
|
+
const router = createRouter(contract, handlers);
|
|
275
|
+
|
|
276
|
+
Bun.serve({
|
|
277
|
+
port: 3000,
|
|
278
|
+
async fetch(request) {
|
|
279
|
+
try {
|
|
280
|
+
return await router.handle(request);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error instanceof ValidationError) {
|
|
283
|
+
// Handle validation errors
|
|
284
|
+
return Response.json(
|
|
285
|
+
{
|
|
286
|
+
error: 'Validation Error',
|
|
287
|
+
field: error.field,
|
|
288
|
+
issues: error.issues,
|
|
289
|
+
},
|
|
290
|
+
{ status: 400 }
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (error instanceof RouteNotFoundError) {
|
|
295
|
+
// Handle route not found
|
|
296
|
+
return Response.json(
|
|
297
|
+
{
|
|
298
|
+
error: 'Not Found',
|
|
299
|
+
message: `Route ${error.method} ${error.path} not found`,
|
|
300
|
+
},
|
|
301
|
+
{ status: 404 }
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Handle unexpected errors
|
|
306
|
+
console.error('Unexpected error:', error);
|
|
307
|
+
return Response.json(
|
|
308
|
+
{ error: 'Internal Server Error' },
|
|
309
|
+
{ status: 500 }
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### `RouteNotFoundError`
|
|
317
|
+
|
|
318
|
+
Thrown when no matching route is found for the request.
|
|
319
|
+
|
|
320
|
+
**Properties:**
|
|
321
|
+
- `path: string` - The requested path
|
|
322
|
+
- `method: string` - The HTTP method (GET, POST, etc.)
|
|
323
|
+
|
|
324
|
+
**When thrown:**
|
|
325
|
+
- No endpoint in the contract matches the request method and path
|
|
326
|
+
|
|
327
|
+
**Example:**
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
try {
|
|
331
|
+
return await router.handle(request);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error instanceof RouteNotFoundError) {
|
|
334
|
+
return Response.json(
|
|
335
|
+
{
|
|
336
|
+
error: 'Not Found',
|
|
337
|
+
message: `Cannot ${error.method} ${error.path}`,
|
|
338
|
+
},
|
|
339
|
+
{ status: 404 }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
throw error; // Re-throw other errors
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Complete Error Handling Example
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import {
|
|
350
|
+
createRouter,
|
|
351
|
+
ValidationError,
|
|
352
|
+
RouteNotFoundError,
|
|
353
|
+
Status,
|
|
354
|
+
} from '@richie-rpc/server';
|
|
355
|
+
|
|
356
|
+
const router = createRouter(contract, handlers);
|
|
357
|
+
|
|
358
|
+
Bun.serve({
|
|
359
|
+
port: 3000,
|
|
360
|
+
async fetch(request) {
|
|
361
|
+
const url = new URL(request.url);
|
|
362
|
+
|
|
363
|
+
// Handle API routes
|
|
364
|
+
if (url.pathname.startsWith('/api/')) {
|
|
365
|
+
try {
|
|
366
|
+
return await router.handle(request);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error instanceof ValidationError) {
|
|
369
|
+
// Format validation errors for client
|
|
370
|
+
return Response.json(
|
|
371
|
+
{
|
|
372
|
+
error: 'Validation Error',
|
|
373
|
+
field: error.field,
|
|
374
|
+
issues: error.issues.map((issue) => ({
|
|
375
|
+
path: issue.path.join('.'),
|
|
376
|
+
message: issue.message,
|
|
377
|
+
code: issue.code,
|
|
378
|
+
})),
|
|
379
|
+
},
|
|
380
|
+
{ status: 400 }
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (error instanceof RouteNotFoundError) {
|
|
385
|
+
return Response.json(
|
|
386
|
+
{
|
|
387
|
+
error: 'Not Found',
|
|
388
|
+
message: `Route ${error.method} ${error.path} not found`,
|
|
389
|
+
},
|
|
390
|
+
{ status: 404 }
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Log unexpected errors
|
|
395
|
+
console.error('Unexpected error:', error);
|
|
396
|
+
return Response.json(
|
|
397
|
+
{ error: 'Internal Server Error' },
|
|
398
|
+
{ status: 500 }
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Handle other routes
|
|
404
|
+
return new Response('Not Found', { status: 404 });
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Handler-Level Errors
|
|
410
|
+
|
|
411
|
+
Errors thrown inside handlers are not automatically caught by the router. You should handle them within your handlers:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
const router = createRouter(contract, {
|
|
415
|
+
getUser: async ({ params }) => {
|
|
416
|
+
try {
|
|
417
|
+
const user = await db.getUser(params.id);
|
|
418
|
+
if (!user) {
|
|
419
|
+
return { status: Status.NotFound, body: { error: 'User not found' } };
|
|
420
|
+
}
|
|
421
|
+
return { status: Status.OK, body: user };
|
|
422
|
+
} catch (error) {
|
|
423
|
+
// Handle database errors, etc.
|
|
424
|
+
console.error('Database error:', error);
|
|
425
|
+
return {
|
|
426
|
+
status: Status.InternalServerError,
|
|
427
|
+
body: { error: 'Failed to fetch user' },
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
});
|
|
204
432
|
```
|
|
205
433
|
|
|
206
434
|
## Validation
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -96,12 +96,9 @@ async function parseRequest(request, endpoint, pathParams, context) {
|
|
|
96
96
|
let bodyData;
|
|
97
97
|
if (contentType.includes("application/json")) {
|
|
98
98
|
bodyData = await request.json();
|
|
99
|
-
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
100
|
-
const formData = await request.formData();
|
|
101
|
-
bodyData = Object.fromEntries(formData.entries());
|
|
102
99
|
} else if (contentType.includes("multipart/form-data")) {
|
|
103
100
|
const formData = await request.formData();
|
|
104
|
-
bodyData =
|
|
101
|
+
bodyData = import_core.formDataToObject(formData);
|
|
105
102
|
} else {
|
|
106
103
|
bodyData = await request.text();
|
|
107
104
|
}
|
|
@@ -137,20 +134,6 @@ function createResponse(endpoint, handlerResponse) {
|
|
|
137
134
|
headers: responseHeaders
|
|
138
135
|
});
|
|
139
136
|
}
|
|
140
|
-
function createErrorResponse(error) {
|
|
141
|
-
if (error instanceof ValidationError) {
|
|
142
|
-
return Response.json({
|
|
143
|
-
error: "Validation Error",
|
|
144
|
-
field: error.field,
|
|
145
|
-
issues: error.issues
|
|
146
|
-
}, { status: 400 });
|
|
147
|
-
}
|
|
148
|
-
if (error instanceof RouteNotFoundError) {
|
|
149
|
-
return Response.json({ error: "Not Found", message: error.message }, { status: 404 });
|
|
150
|
-
}
|
|
151
|
-
console.error("Internal server error:", error);
|
|
152
|
-
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
153
|
-
}
|
|
154
137
|
|
|
155
138
|
class Router {
|
|
156
139
|
contract;
|
|
@@ -181,26 +164,22 @@ class Router {
|
|
|
181
164
|
return null;
|
|
182
165
|
}
|
|
183
166
|
async handle(request) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
throw new RouteNotFoundError(path, method);
|
|
194
|
-
}
|
|
195
|
-
const { name, endpoint, params } = match;
|
|
196
|
-
const handler = this.handlers[name];
|
|
197
|
-
const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
|
|
198
|
-
const input = await parseRequest(request, endpoint, params, context);
|
|
199
|
-
const handlerResponse = await handler(input);
|
|
200
|
-
return createResponse(endpoint, handlerResponse);
|
|
201
|
-
} catch (error) {
|
|
202
|
-
return createErrorResponse(error);
|
|
167
|
+
const url = new URL(request.url);
|
|
168
|
+
const method = request.method;
|
|
169
|
+
let path = url.pathname;
|
|
170
|
+
if (this.basePath && path.startsWith(this.basePath)) {
|
|
171
|
+
path = path.slice(this.basePath.length) || "/";
|
|
172
|
+
}
|
|
173
|
+
const match = this.findEndpoint(method, path);
|
|
174
|
+
if (!match) {
|
|
175
|
+
throw new RouteNotFoundError(path, method);
|
|
203
176
|
}
|
|
177
|
+
const { name, endpoint, params } = match;
|
|
178
|
+
const handler = this.handlers[name];
|
|
179
|
+
const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
|
|
180
|
+
const input = await parseRequest(request, endpoint, params, context);
|
|
181
|
+
const handlerResponse = await handler(input);
|
|
182
|
+
return createResponse(endpoint, handlerResponse);
|
|
204
183
|
}
|
|
205
184
|
get fetch() {
|
|
206
185
|
return (request) => this.handle(request);
|
|
@@ -211,4 +190,4 @@ function createRouter(contract, handlers, options) {
|
|
|
211
190
|
}
|
|
212
191
|
})
|
|
213
192
|
|
|
214
|
-
//# debugId=
|
|
193
|
+
//# debugId=83E2D770B114736364756E2164756E21
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { matchPath, parseQuery, Status } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Re-export Status for convenience\nexport { Status };\n\n// Handler input types\nexport type HandlerInput<T extends EndpointDefinition, C = unknown> = {\n params: ExtractParams<T>;\n query: ExtractQuery<T>;\n headers: ExtractHeaders<T>;\n body: ExtractBody<T>;\n request: Request;\n context: C;\n};\n\n// Handler response type\nexport type HandlerResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n body: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n headers?: Record<string, string>;\n };\n}[keyof T['responses']];\n\n// Handler function type\nexport type Handler<T extends EndpointDefinition, C = unknown> = (\n input: HandlerInput<T, C>,\n) => Promise<HandlerResponse<T>> | HandlerResponse<T>;\n\n// Contract handlers mapping\nexport type ContractHandlers<T extends Contract, C = unknown> = {\n [K in keyof T]: Handler<T[K], C>;\n};\n\n// Error classes\nexport class ValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n message?: string,\n ) {\n super(message || `Validation failed for ${field}`);\n this.name = 'ValidationError';\n }\n}\n\nexport class RouteNotFoundError extends Error {\n constructor(\n public path: string,\n public method: string,\n ) {\n super(`Route not found: ${method} ${path}`);\n this.name = 'RouteNotFoundError';\n }\n}\n\n/**\n * Parse and validate request data\n */\nasync function parseRequest<T extends EndpointDefinition, C = unknown>(\n request: Request,\n endpoint: T,\n pathParams: Record<string, string>,\n context: C,\n): Promise<HandlerInput<T, C>> {\n const url = new URL(request.url);\n\n // Parse path params\n let params: any = pathParams;\n if (endpoint.params) {\n const result = endpoint.params.safeParse(pathParams);\n if (!result.success) {\n throw new ValidationError('params', result.error.issues);\n }\n params = result.data;\n }\n\n // Parse query params\n let query: any = {};\n if (endpoint.query) {\n const queryData = parseQuery(url.searchParams);\n const result = endpoint.query.safeParse(queryData);\n if (!result.success) {\n throw new ValidationError('query', result.error.issues);\n }\n query = result.data;\n }\n\n // Parse headers\n let headers: any = {};\n if (endpoint.headers) {\n const headersObj: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headersObj[key] = value;\n });\n const result = endpoint.headers.safeParse(headersObj);\n if (!result.success) {\n throw new ValidationError('headers', result.error.issues);\n }\n headers = result.data;\n }\n\n // Parse body\n let body: any;\n if (endpoint.body) {\n const contentType = request.headers.get('content-type') || '';\n let bodyData: any;\n\n if (contentType.includes('application/json')) {\n bodyData = await request.json();\n } else if (contentType.includes('
|
|
5
|
+
"import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { formDataToObject, matchPath, parseQuery, Status } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Re-export Status for convenience\nexport { Status };\n\n// Handler input types\nexport type HandlerInput<T extends EndpointDefinition, C = unknown> = {\n params: ExtractParams<T>;\n query: ExtractQuery<T>;\n headers: ExtractHeaders<T>;\n body: ExtractBody<T>;\n request: Request;\n context: C;\n};\n\n// Handler response type\nexport type HandlerResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n body: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n headers?: Record<string, string>;\n };\n}[keyof T['responses']];\n\n// Handler function type\nexport type Handler<T extends EndpointDefinition, C = unknown> = (\n input: HandlerInput<T, C>,\n) => Promise<HandlerResponse<T>> | HandlerResponse<T>;\n\n// Contract handlers mapping\nexport type ContractHandlers<T extends Contract, C = unknown> = {\n [K in keyof T]: Handler<T[K], C>;\n};\n\n// Error classes\nexport class ValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n message?: string,\n ) {\n super(message || `Validation failed for ${field}`);\n this.name = 'ValidationError';\n }\n}\n\nexport class RouteNotFoundError extends Error {\n constructor(\n public path: string,\n public method: string,\n ) {\n super(`Route not found: ${method} ${path}`);\n this.name = 'RouteNotFoundError';\n }\n}\n\n/**\n * Parse and validate request data\n */\nasync function parseRequest<T extends EndpointDefinition, C = unknown>(\n request: Request,\n endpoint: T,\n pathParams: Record<string, string>,\n context: C,\n): Promise<HandlerInput<T, C>> {\n const url = new URL(request.url);\n\n // Parse path params\n let params: any = pathParams;\n if (endpoint.params) {\n const result = endpoint.params.safeParse(pathParams);\n if (!result.success) {\n throw new ValidationError('params', result.error.issues);\n }\n params = result.data;\n }\n\n // Parse query params\n let query: any = {};\n if (endpoint.query) {\n const queryData = parseQuery(url.searchParams);\n const result = endpoint.query.safeParse(queryData);\n if (!result.success) {\n throw new ValidationError('query', result.error.issues);\n }\n query = result.data;\n }\n\n // Parse headers\n let headers: any = {};\n if (endpoint.headers) {\n const headersObj: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headersObj[key] = value;\n });\n const result = endpoint.headers.safeParse(headersObj);\n if (!result.success) {\n throw new ValidationError('headers', result.error.issues);\n }\n headers = result.data;\n }\n\n // Parse body\n let body: any;\n if (endpoint.body) {\n const contentType = request.headers.get('content-type') || '';\n let bodyData: any;\n\n if (contentType.includes('application/json')) {\n bodyData = await request.json();\n } else if (contentType.includes('multipart/form-data')) {\n const formData = await request.formData();\n bodyData = formDataToObject(formData as FormData);\n } else {\n bodyData = await request.text();\n }\n\n const result = endpoint.body.safeParse(bodyData);\n if (!result.success) {\n throw new ValidationError('body', result.error.issues);\n }\n body = result.data;\n }\n\n return { params, query, headers, body, request, context } as HandlerInput<T, C>;\n}\n\n/**\n * Validate and create response\n */\nfunction createResponse<T extends EndpointDefinition>(\n endpoint: T,\n handlerResponse: HandlerResponse<T>,\n): Response {\n const { status, body, headers: customHeaders } = handlerResponse;\n\n // Validate response body\n const responseSchema = endpoint.responses[status as keyof typeof endpoint.responses];\n if (responseSchema) {\n const result = responseSchema.safeParse(body);\n if (!result.success) {\n throw new ValidationError(`response[${String(status)}]`, result.error.issues);\n }\n }\n\n // Create response headers\n const responseHeaders = new Headers(customHeaders);\n\n // Handle 204 No Content - must have no body\n if (status === 204) {\n return new Response(null, {\n status: 204,\n headers: responseHeaders,\n });\n }\n\n // For all other responses, return JSON\n if (!responseHeaders.has('content-type')) {\n responseHeaders.set('content-type', 'application/json');\n }\n\n return new Response(JSON.stringify(body), {\n status: status as number,\n headers: responseHeaders,\n });\n}\n\n/**\n * Router configuration options\n */\nexport interface RouterOptions<C = unknown> {\n basePath?: string;\n context?: (request: Request, routeName?: string, endpoint?: EndpointDefinition) => C | Promise<C>;\n}\n\n/**\n * Router class that manages contract endpoints\n */\nexport class Router<T extends Contract, C = unknown> {\n private basePath: string;\n private contextFactory?: (\n request: Request,\n routeName: string,\n endpoint: EndpointDefinition,\n ) => C | Promise<C>;\n\n constructor(\n private contract: T,\n private handlers: ContractHandlers<T, C>,\n options?: RouterOptions<C>,\n ) {\n // Normalize basePath: ensure it starts with / and doesn't end with /\n const bp = options?.basePath || '';\n if (bp) {\n this.basePath = bp.startsWith('/') ? bp : `/${bp}`;\n this.basePath = this.basePath.endsWith('/') ? this.basePath.slice(0, -1) : this.basePath;\n } else {\n this.basePath = '';\n }\n this.contextFactory = options?.context;\n }\n\n /**\n * Find matching endpoint for a request\n */\n private findEndpoint(\n method: string,\n path: string,\n ): {\n name: keyof T;\n endpoint: EndpointDefinition;\n params: Record<string, string>;\n } | null {\n for (const [name, endpoint] of Object.entries(this.contract)) {\n if (endpoint.method === method) {\n const params = matchPath(endpoint.path, path);\n if (params !== null) {\n return { name, endpoint, params };\n }\n }\n }\n return null;\n }\n\n /**\n * Handle a request\n */\n async handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const method = request.method;\n let path = url.pathname;\n\n // Strip basePath if configured\n if (this.basePath && path.startsWith(this.basePath)) {\n path = path.slice(this.basePath.length) || '/';\n }\n\n const match = this.findEndpoint(method, path);\n if (!match) {\n throw new RouteNotFoundError(path, method);\n }\n\n const { name, endpoint, params } = match;\n const handler = this.handlers[name];\n\n // Create context if factory is provided\n const context = this.contextFactory\n ? await this.contextFactory(request, String(name), endpoint)\n : (undefined as C);\n\n // Parse and validate request\n const input = await parseRequest(request, endpoint, params, context);\n\n // Call handler\n const handlerResponse = await handler(input as any);\n\n // Create and validate response\n return createResponse(endpoint as T[keyof T], handlerResponse);\n }\n\n /**\n * Get fetch handler compatible with Bun.serve\n */\n get fetch() {\n return (request: Request) => this.handle(request);\n }\n}\n\n/**\n * Create a router from a contract and handlers\n */\nexport function createRouter<T extends Contract, C = unknown>(\n contract: T,\n handlers: ContractHandlers<T, C>,\n options?: RouterOptions<C>,\n): Router<T, C> {\n return new Router(contract, handlers, options);\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "83E2D770B114736364756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/server/index.ts
|
|
3
|
-
import { matchPath, parseQuery, Status } from "@richie-rpc/core";
|
|
3
|
+
import { formDataToObject, matchPath, parseQuery, Status } from "@richie-rpc/core";
|
|
4
4
|
class ValidationError extends Error {
|
|
5
5
|
field;
|
|
6
6
|
issues;
|
|
@@ -59,12 +59,9 @@ async function parseRequest(request, endpoint, pathParams, context) {
|
|
|
59
59
|
let bodyData;
|
|
60
60
|
if (contentType.includes("application/json")) {
|
|
61
61
|
bodyData = await request.json();
|
|
62
|
-
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
63
|
-
const formData = await request.formData();
|
|
64
|
-
bodyData = Object.fromEntries(formData.entries());
|
|
65
62
|
} else if (contentType.includes("multipart/form-data")) {
|
|
66
63
|
const formData = await request.formData();
|
|
67
|
-
bodyData =
|
|
64
|
+
bodyData = formDataToObject(formData);
|
|
68
65
|
} else {
|
|
69
66
|
bodyData = await request.text();
|
|
70
67
|
}
|
|
@@ -100,20 +97,6 @@ function createResponse(endpoint, handlerResponse) {
|
|
|
100
97
|
headers: responseHeaders
|
|
101
98
|
});
|
|
102
99
|
}
|
|
103
|
-
function createErrorResponse(error) {
|
|
104
|
-
if (error instanceof ValidationError) {
|
|
105
|
-
return Response.json({
|
|
106
|
-
error: "Validation Error",
|
|
107
|
-
field: error.field,
|
|
108
|
-
issues: error.issues
|
|
109
|
-
}, { status: 400 });
|
|
110
|
-
}
|
|
111
|
-
if (error instanceof RouteNotFoundError) {
|
|
112
|
-
return Response.json({ error: "Not Found", message: error.message }, { status: 404 });
|
|
113
|
-
}
|
|
114
|
-
console.error("Internal server error:", error);
|
|
115
|
-
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
116
|
-
}
|
|
117
100
|
|
|
118
101
|
class Router {
|
|
119
102
|
contract;
|
|
@@ -144,26 +127,22 @@ class Router {
|
|
|
144
127
|
return null;
|
|
145
128
|
}
|
|
146
129
|
async handle(request) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
throw new RouteNotFoundError(path, method);
|
|
157
|
-
}
|
|
158
|
-
const { name, endpoint, params } = match;
|
|
159
|
-
const handler = this.handlers[name];
|
|
160
|
-
const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
|
|
161
|
-
const input = await parseRequest(request, endpoint, params, context);
|
|
162
|
-
const handlerResponse = await handler(input);
|
|
163
|
-
return createResponse(endpoint, handlerResponse);
|
|
164
|
-
} catch (error) {
|
|
165
|
-
return createErrorResponse(error);
|
|
130
|
+
const url = new URL(request.url);
|
|
131
|
+
const method = request.method;
|
|
132
|
+
let path = url.pathname;
|
|
133
|
+
if (this.basePath && path.startsWith(this.basePath)) {
|
|
134
|
+
path = path.slice(this.basePath.length) || "/";
|
|
135
|
+
}
|
|
136
|
+
const match = this.findEndpoint(method, path);
|
|
137
|
+
if (!match) {
|
|
138
|
+
throw new RouteNotFoundError(path, method);
|
|
166
139
|
}
|
|
140
|
+
const { name, endpoint, params } = match;
|
|
141
|
+
const handler = this.handlers[name];
|
|
142
|
+
const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
|
|
143
|
+
const input = await parseRequest(request, endpoint, params, context);
|
|
144
|
+
const handlerResponse = await handler(input);
|
|
145
|
+
return createResponse(endpoint, handlerResponse);
|
|
167
146
|
}
|
|
168
147
|
get fetch() {
|
|
169
148
|
return (request) => this.handle(request);
|
|
@@ -180,4 +159,4 @@ export {
|
|
|
180
159
|
RouteNotFoundError
|
|
181
160
|
};
|
|
182
161
|
|
|
183
|
-
//# debugId=
|
|
162
|
+
//# debugId=2A54C105B1B2019764756E2164756E21
|
package/dist/mjs/index.mjs.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { matchPath, parseQuery, Status } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Re-export Status for convenience\nexport { Status };\n\n// Handler input types\nexport type HandlerInput<T extends EndpointDefinition, C = unknown> = {\n params: ExtractParams<T>;\n query: ExtractQuery<T>;\n headers: ExtractHeaders<T>;\n body: ExtractBody<T>;\n request: Request;\n context: C;\n};\n\n// Handler response type\nexport type HandlerResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n body: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n headers?: Record<string, string>;\n };\n}[keyof T['responses']];\n\n// Handler function type\nexport type Handler<T extends EndpointDefinition, C = unknown> = (\n input: HandlerInput<T, C>,\n) => Promise<HandlerResponse<T>> | HandlerResponse<T>;\n\n// Contract handlers mapping\nexport type ContractHandlers<T extends Contract, C = unknown> = {\n [K in keyof T]: Handler<T[K], C>;\n};\n\n// Error classes\nexport class ValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n message?: string,\n ) {\n super(message || `Validation failed for ${field}`);\n this.name = 'ValidationError';\n }\n}\n\nexport class RouteNotFoundError extends Error {\n constructor(\n public path: string,\n public method: string,\n ) {\n super(`Route not found: ${method} ${path}`);\n this.name = 'RouteNotFoundError';\n }\n}\n\n/**\n * Parse and validate request data\n */\nasync function parseRequest<T extends EndpointDefinition, C = unknown>(\n request: Request,\n endpoint: T,\n pathParams: Record<string, string>,\n context: C,\n): Promise<HandlerInput<T, C>> {\n const url = new URL(request.url);\n\n // Parse path params\n let params: any = pathParams;\n if (endpoint.params) {\n const result = endpoint.params.safeParse(pathParams);\n if (!result.success) {\n throw new ValidationError('params', result.error.issues);\n }\n params = result.data;\n }\n\n // Parse query params\n let query: any = {};\n if (endpoint.query) {\n const queryData = parseQuery(url.searchParams);\n const result = endpoint.query.safeParse(queryData);\n if (!result.success) {\n throw new ValidationError('query', result.error.issues);\n }\n query = result.data;\n }\n\n // Parse headers\n let headers: any = {};\n if (endpoint.headers) {\n const headersObj: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headersObj[key] = value;\n });\n const result = endpoint.headers.safeParse(headersObj);\n if (!result.success) {\n throw new ValidationError('headers', result.error.issues);\n }\n headers = result.data;\n }\n\n // Parse body\n let body: any;\n if (endpoint.body) {\n const contentType = request.headers.get('content-type') || '';\n let bodyData: any;\n\n if (contentType.includes('application/json')) {\n bodyData = await request.json();\n } else if (contentType.includes('
|
|
5
|
+
"import type {\n Contract,\n EndpointDefinition,\n ExtractBody,\n ExtractHeaders,\n ExtractParams,\n ExtractQuery,\n} from '@richie-rpc/core';\nimport { formDataToObject, matchPath, parseQuery, Status } from '@richie-rpc/core';\nimport type { z } from 'zod';\n\n// Re-export Status for convenience\nexport { Status };\n\n// Handler input types\nexport type HandlerInput<T extends EndpointDefinition, C = unknown> = {\n params: ExtractParams<T>;\n query: ExtractQuery<T>;\n headers: ExtractHeaders<T>;\n body: ExtractBody<T>;\n request: Request;\n context: C;\n};\n\n// Handler response type\nexport type HandlerResponse<T extends EndpointDefinition> = {\n [Status in keyof T['responses']]: {\n status: Status;\n body: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;\n headers?: Record<string, string>;\n };\n}[keyof T['responses']];\n\n// Handler function type\nexport type Handler<T extends EndpointDefinition, C = unknown> = (\n input: HandlerInput<T, C>,\n) => Promise<HandlerResponse<T>> | HandlerResponse<T>;\n\n// Contract handlers mapping\nexport type ContractHandlers<T extends Contract, C = unknown> = {\n [K in keyof T]: Handler<T[K], C>;\n};\n\n// Error classes\nexport class ValidationError extends Error {\n constructor(\n public field: string,\n public issues: z.ZodIssue[],\n message?: string,\n ) {\n super(message || `Validation failed for ${field}`);\n this.name = 'ValidationError';\n }\n}\n\nexport class RouteNotFoundError extends Error {\n constructor(\n public path: string,\n public method: string,\n ) {\n super(`Route not found: ${method} ${path}`);\n this.name = 'RouteNotFoundError';\n }\n}\n\n/**\n * Parse and validate request data\n */\nasync function parseRequest<T extends EndpointDefinition, C = unknown>(\n request: Request,\n endpoint: T,\n pathParams: Record<string, string>,\n context: C,\n): Promise<HandlerInput<T, C>> {\n const url = new URL(request.url);\n\n // Parse path params\n let params: any = pathParams;\n if (endpoint.params) {\n const result = endpoint.params.safeParse(pathParams);\n if (!result.success) {\n throw new ValidationError('params', result.error.issues);\n }\n params = result.data;\n }\n\n // Parse query params\n let query: any = {};\n if (endpoint.query) {\n const queryData = parseQuery(url.searchParams);\n const result = endpoint.query.safeParse(queryData);\n if (!result.success) {\n throw new ValidationError('query', result.error.issues);\n }\n query = result.data;\n }\n\n // Parse headers\n let headers: any = {};\n if (endpoint.headers) {\n const headersObj: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headersObj[key] = value;\n });\n const result = endpoint.headers.safeParse(headersObj);\n if (!result.success) {\n throw new ValidationError('headers', result.error.issues);\n }\n headers = result.data;\n }\n\n // Parse body\n let body: any;\n if (endpoint.body) {\n const contentType = request.headers.get('content-type') || '';\n let bodyData: any;\n\n if (contentType.includes('application/json')) {\n bodyData = await request.json();\n } else if (contentType.includes('multipart/form-data')) {\n const formData = await request.formData();\n bodyData = formDataToObject(formData as FormData);\n } else {\n bodyData = await request.text();\n }\n\n const result = endpoint.body.safeParse(bodyData);\n if (!result.success) {\n throw new ValidationError('body', result.error.issues);\n }\n body = result.data;\n }\n\n return { params, query, headers, body, request, context } as HandlerInput<T, C>;\n}\n\n/**\n * Validate and create response\n */\nfunction createResponse<T extends EndpointDefinition>(\n endpoint: T,\n handlerResponse: HandlerResponse<T>,\n): Response {\n const { status, body, headers: customHeaders } = handlerResponse;\n\n // Validate response body\n const responseSchema = endpoint.responses[status as keyof typeof endpoint.responses];\n if (responseSchema) {\n const result = responseSchema.safeParse(body);\n if (!result.success) {\n throw new ValidationError(`response[${String(status)}]`, result.error.issues);\n }\n }\n\n // Create response headers\n const responseHeaders = new Headers(customHeaders);\n\n // Handle 204 No Content - must have no body\n if (status === 204) {\n return new Response(null, {\n status: 204,\n headers: responseHeaders,\n });\n }\n\n // For all other responses, return JSON\n if (!responseHeaders.has('content-type')) {\n responseHeaders.set('content-type', 'application/json');\n }\n\n return new Response(JSON.stringify(body), {\n status: status as number,\n headers: responseHeaders,\n });\n}\n\n/**\n * Router configuration options\n */\nexport interface RouterOptions<C = unknown> {\n basePath?: string;\n context?: (request: Request, routeName?: string, endpoint?: EndpointDefinition) => C | Promise<C>;\n}\n\n/**\n * Router class that manages contract endpoints\n */\nexport class Router<T extends Contract, C = unknown> {\n private basePath: string;\n private contextFactory?: (\n request: Request,\n routeName: string,\n endpoint: EndpointDefinition,\n ) => C | Promise<C>;\n\n constructor(\n private contract: T,\n private handlers: ContractHandlers<T, C>,\n options?: RouterOptions<C>,\n ) {\n // Normalize basePath: ensure it starts with / and doesn't end with /\n const bp = options?.basePath || '';\n if (bp) {\n this.basePath = bp.startsWith('/') ? bp : `/${bp}`;\n this.basePath = this.basePath.endsWith('/') ? this.basePath.slice(0, -1) : this.basePath;\n } else {\n this.basePath = '';\n }\n this.contextFactory = options?.context;\n }\n\n /**\n * Find matching endpoint for a request\n */\n private findEndpoint(\n method: string,\n path: string,\n ): {\n name: keyof T;\n endpoint: EndpointDefinition;\n params: Record<string, string>;\n } | null {\n for (const [name, endpoint] of Object.entries(this.contract)) {\n if (endpoint.method === method) {\n const params = matchPath(endpoint.path, path);\n if (params !== null) {\n return { name, endpoint, params };\n }\n }\n }\n return null;\n }\n\n /**\n * Handle a request\n */\n async handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const method = request.method;\n let path = url.pathname;\n\n // Strip basePath if configured\n if (this.basePath && path.startsWith(this.basePath)) {\n path = path.slice(this.basePath.length) || '/';\n }\n\n const match = this.findEndpoint(method, path);\n if (!match) {\n throw new RouteNotFoundError(path, method);\n }\n\n const { name, endpoint, params } = match;\n const handler = this.handlers[name];\n\n // Create context if factory is provided\n const context = this.contextFactory\n ? await this.contextFactory(request, String(name), endpoint)\n : (undefined as C);\n\n // Parse and validate request\n const input = await parseRequest(request, endpoint, params, context);\n\n // Call handler\n const handlerResponse = await handler(input as any);\n\n // Create and validate response\n return createResponse(endpoint as T[keyof T], handlerResponse);\n }\n\n /**\n * Get fetch handler compatible with Bun.serve\n */\n get fetch() {\n return (request: Request) => this.handle(request);\n }\n}\n\n/**\n * Create a router from a contract and handlers\n */\nexport function createRouter<T extends Contract, C = unknown>(\n contract: T,\n handlers: ContractHandlers<T, C>,\n options?: RouterOptions<C>,\n): Router<T, C> {\n return new Router(contract, handlers, options);\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;AAQA;AAoCO,MAAM,wBAAwB,MAAM;AAAA,EAEhC;AAAA,EACA;AAAA,EAFT,WAAW,CACF,OACA,QACP,SACA;AAAA,IACA,MAAM,WAAW,yBAAyB,OAAO;AAAA,IAJ1C;AAAA,IACA;AAAA,IAIP,KAAK,OAAO;AAAA;AAEhB;AAAA;AAEO,MAAM,2BAA2B,MAAM;AAAA,EAEnC;AAAA,EACA;AAAA,EAFT,WAAW,CACF,MACA,QACP;AAAA,IACA,MAAM,oBAAoB,UAAU,MAAM;AAAA,IAHnC;AAAA,IACA;AAAA,IAGP,KAAK,OAAO;AAAA;AAEhB;AAKA,eAAe,YAAuD,CACpE,SACA,UACA,YACA,SAC6B;AAAA,EAC7B,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAAA,EAG/B,IAAI,SAAc;AAAA,EAClB,IAAI,SAAS,QAAQ;AAAA,IACnB,MAAM,SAAS,SAAS,OAAO,UAAU,UAAU;AAAA,IACnD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,gBAAgB,UAAU,OAAO,MAAM,MAAM;AAAA,IACzD;AAAA,IACA,SAAS,OAAO;AAAA,EAClB;AAAA,EAGA,IAAI,QAAa,CAAC;AAAA,EAClB,IAAI,SAAS,OAAO;AAAA,IAClB,MAAM,YAAY,WAAW,IAAI,YAAY;AAAA,IAC7C,MAAM,SAAS,SAAS,MAAM,UAAU,SAAS;AAAA,IACjD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,gBAAgB,SAAS,OAAO,MAAM,MAAM;AAAA,IACxD;AAAA,IACA,QAAQ,OAAO;AAAA,EACjB;AAAA,EAGA,IAAI,UAAe,CAAC;AAAA,EACpB,IAAI,SAAS,SAAS;AAAA,IACpB,MAAM,aAAqC,CAAC;AAAA,IAC5C,QAAQ,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AAAA,MACtC,WAAW,OAAO;AAAA,KACnB;AAAA,IACD,MAAM,SAAS,SAAS,QAAQ,UAAU,UAAU;AAAA,IACpD,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,gBAAgB,WAAW,OAAO,MAAM,MAAM;AAAA,IAC1D;AAAA,IACA,UAAU,OAAO;AAAA,EACnB;AAAA,EAGA,IAAI;AAAA,EACJ,IAAI,SAAS,MAAM;AAAA,IACjB,MAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc,KAAK;AAAA,IAC3D,IAAI;AAAA,IAEJ,IAAI,YAAY,SAAS,kBAAkB,GAAG;AAAA,MAC5C,WAAW,MAAM,QAAQ,KAAK;AAAA,IAChC,EAAO,SAAI,YAAY,SAAS,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "2A54C105B1B2019764756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/mjs/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@richie-rpc/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"main": "./dist/cjs/index.cjs",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@richie-rpc/core": "^1.2.
|
|
13
|
+
"@richie-rpc/core": "^1.2.2",
|
|
14
14
|
"typescript": "^5",
|
|
15
15
|
"zod": "^4.1.12"
|
|
16
16
|
},
|