@richie-rpc/server 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -9
- package/dist/cjs/index.cjs +20 -35
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +20 -35
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +11 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,6 +36,32 @@ const router = createRouter(contract, {
|
|
|
36
36
|
});
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
### Router with basePath
|
|
40
|
+
|
|
41
|
+
You can serve your API under a path prefix (e.g., `/api`) using the `basePath` option:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const router = createRouter(contract, handlers, {
|
|
45
|
+
basePath: '/api'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
Bun.serve({
|
|
49
|
+
port: 3000,
|
|
50
|
+
fetch(request) {
|
|
51
|
+
const url = new URL(request.url);
|
|
52
|
+
|
|
53
|
+
// Route all /api/* requests to the router
|
|
54
|
+
if (url.pathname.startsWith('/api/')) {
|
|
55
|
+
return router.fetch(request);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Response('Not Found', { status: 404 });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The router will automatically strip the basePath prefix before matching routes. For example, if your contract defines a route at `/users`, and you set `basePath: '/api'`, the actual URL will be `/api/users`.
|
|
64
|
+
|
|
39
65
|
### Using with Bun.serve
|
|
40
66
|
|
|
41
67
|
```typescript
|
|
@@ -72,6 +98,7 @@ Bun.serve({
|
|
|
72
98
|
- ✅ Query parameter parsing
|
|
73
99
|
- ✅ JSON body parsing
|
|
74
100
|
- ✅ Form data support
|
|
101
|
+
- ✅ BasePath support for serving APIs under path prefixes
|
|
75
102
|
- ✅ Detailed validation errors
|
|
76
103
|
- ✅ 404 handling for unknown routes
|
|
77
104
|
- ✅ Error handling and reporting
|
|
@@ -161,19 +188,189 @@ const router = createRouter(contract, {
|
|
|
161
188
|
|
|
162
189
|
## Error Handling
|
|
163
190
|
|
|
164
|
-
The router
|
|
191
|
+
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.
|
|
192
|
+
|
|
193
|
+
### Error Classes
|
|
194
|
+
|
|
195
|
+
#### `ValidationError`
|
|
196
|
+
|
|
197
|
+
Thrown when request or response validation fails. Contains detailed Zod validation issues.
|
|
198
|
+
|
|
199
|
+
**Properties:**
|
|
200
|
+
- `field: string` - The field that failed validation (`"params"`, `"query"`, `"headers"`, `"body"`, or `"response[status]"`)
|
|
201
|
+
- `issues: z.ZodIssue[]` - Array of Zod validation issues with detailed error information
|
|
202
|
+
- `message: string` - Error message
|
|
203
|
+
|
|
204
|
+
**When thrown:**
|
|
205
|
+
- Invalid path parameters (params)
|
|
206
|
+
- Invalid query parameters (query)
|
|
207
|
+
- Invalid request headers (headers)
|
|
208
|
+
- Invalid request body (body)
|
|
209
|
+
- Invalid response body returned from handler (response validation)
|
|
210
|
+
|
|
211
|
+
**Example:**
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { createRouter, ValidationError, RouteNotFoundError } from '@richie-rpc/server';
|
|
215
|
+
|
|
216
|
+
const router = createRouter(contract, handlers);
|
|
217
|
+
|
|
218
|
+
Bun.serve({
|
|
219
|
+
port: 3000,
|
|
220
|
+
async fetch(request) {
|
|
221
|
+
try {
|
|
222
|
+
return await router.handle(request);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error instanceof ValidationError) {
|
|
225
|
+
// Handle validation errors
|
|
226
|
+
return Response.json(
|
|
227
|
+
{
|
|
228
|
+
error: 'Validation Error',
|
|
229
|
+
field: error.field,
|
|
230
|
+
issues: error.issues,
|
|
231
|
+
},
|
|
232
|
+
{ status: 400 }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (error instanceof RouteNotFoundError) {
|
|
237
|
+
// Handle route not found
|
|
238
|
+
return Response.json(
|
|
239
|
+
{
|
|
240
|
+
error: 'Not Found',
|
|
241
|
+
message: `Route ${error.method} ${error.path} not found`,
|
|
242
|
+
},
|
|
243
|
+
{ status: 404 }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Handle unexpected errors
|
|
248
|
+
console.error('Unexpected error:', error);
|
|
249
|
+
return Response.json(
|
|
250
|
+
{ error: 'Internal Server Error' },
|
|
251
|
+
{ status: 500 }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### `RouteNotFoundError`
|
|
259
|
+
|
|
260
|
+
Thrown when no matching route is found for the request.
|
|
261
|
+
|
|
262
|
+
**Properties:**
|
|
263
|
+
- `path: string` - The requested path
|
|
264
|
+
- `method: string` - The HTTP method (GET, POST, etc.)
|
|
265
|
+
|
|
266
|
+
**When thrown:**
|
|
267
|
+
- No endpoint in the contract matches the request method and path
|
|
268
|
+
|
|
269
|
+
**Example:**
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
try {
|
|
273
|
+
return await router.handle(request);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error instanceof RouteNotFoundError) {
|
|
276
|
+
return Response.json(
|
|
277
|
+
{
|
|
278
|
+
error: 'Not Found',
|
|
279
|
+
message: `Cannot ${error.method} ${error.path}`,
|
|
280
|
+
},
|
|
281
|
+
{ status: 404 }
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
throw error; // Re-throw other errors
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Complete Error Handling Example
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import {
|
|
292
|
+
createRouter,
|
|
293
|
+
ValidationError,
|
|
294
|
+
RouteNotFoundError,
|
|
295
|
+
Status,
|
|
296
|
+
} from '@richie-rpc/server';
|
|
297
|
+
|
|
298
|
+
const router = createRouter(contract, handlers);
|
|
165
299
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
300
|
+
Bun.serve({
|
|
301
|
+
port: 3000,
|
|
302
|
+
async fetch(request) {
|
|
303
|
+
const url = new URL(request.url);
|
|
304
|
+
|
|
305
|
+
// Handle API routes
|
|
306
|
+
if (url.pathname.startsWith('/api/')) {
|
|
307
|
+
try {
|
|
308
|
+
return await router.handle(request);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof ValidationError) {
|
|
311
|
+
// Format validation errors for client
|
|
312
|
+
return Response.json(
|
|
313
|
+
{
|
|
314
|
+
error: 'Validation Error',
|
|
315
|
+
field: error.field,
|
|
316
|
+
issues: error.issues.map((issue) => ({
|
|
317
|
+
path: issue.path.join('.'),
|
|
318
|
+
message: issue.message,
|
|
319
|
+
code: issue.code,
|
|
320
|
+
})),
|
|
321
|
+
},
|
|
322
|
+
{ status: 400 }
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (error instanceof RouteNotFoundError) {
|
|
327
|
+
return Response.json(
|
|
328
|
+
{
|
|
329
|
+
error: 'Not Found',
|
|
330
|
+
message: `Route ${error.method} ${error.path} not found`,
|
|
331
|
+
},
|
|
332
|
+
{ status: 404 }
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Log unexpected errors
|
|
337
|
+
console.error('Unexpected error:', error);
|
|
338
|
+
return Response.json(
|
|
339
|
+
{ error: 'Internal Server Error' },
|
|
340
|
+
{ status: 500 }
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Handle other routes
|
|
346
|
+
return new Response('Not Found', { status: 404 });
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
```
|
|
169
350
|
|
|
170
|
-
|
|
351
|
+
### Handler-Level Errors
|
|
352
|
+
|
|
353
|
+
Errors thrown inside handlers are not automatically caught by the router. You should handle them within your handlers:
|
|
171
354
|
|
|
172
355
|
```typescript
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
356
|
+
const router = createRouter(contract, {
|
|
357
|
+
getUser: async ({ params }) => {
|
|
358
|
+
try {
|
|
359
|
+
const user = await db.getUser(params.id);
|
|
360
|
+
if (!user) {
|
|
361
|
+
return { status: Status.NotFound, body: { error: 'User not found' } };
|
|
362
|
+
}
|
|
363
|
+
return { status: Status.OK, body: user };
|
|
364
|
+
} catch (error) {
|
|
365
|
+
// Handle database errors, etc.
|
|
366
|
+
console.error('Database error:', error);
|
|
367
|
+
return {
|
|
368
|
+
status: Status.InternalServerError,
|
|
369
|
+
body: { error: 'Failed to fetch user' },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
});
|
|
177
374
|
```
|
|
178
375
|
|
|
179
376
|
## Validation
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -59,7 +59,7 @@ class RouteNotFoundError extends Error {
|
|
|
59
59
|
this.name = "RouteNotFoundError";
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
async function parseRequest(request, endpoint, pathParams) {
|
|
62
|
+
async function parseRequest(request, endpoint, pathParams, context) {
|
|
63
63
|
const url = new URL(request.url);
|
|
64
64
|
let params = pathParams;
|
|
65
65
|
if (endpoint.params) {
|
|
@@ -111,7 +111,7 @@ async function parseRequest(request, endpoint, pathParams) {
|
|
|
111
111
|
}
|
|
112
112
|
body = result.data;
|
|
113
113
|
}
|
|
114
|
-
return { params, query, headers, body, request };
|
|
114
|
+
return { params, query, headers, body, request, context };
|
|
115
115
|
}
|
|
116
116
|
function createResponse(endpoint, handlerResponse) {
|
|
117
117
|
const { status, body, headers: customHeaders } = handlerResponse;
|
|
@@ -137,25 +137,12 @@ function createResponse(endpoint, handlerResponse) {
|
|
|
137
137
|
headers: responseHeaders
|
|
138
138
|
});
|
|
139
139
|
}
|
|
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
140
|
|
|
155
141
|
class Router {
|
|
156
142
|
contract;
|
|
157
143
|
handlers;
|
|
158
144
|
basePath;
|
|
145
|
+
contextFactory;
|
|
159
146
|
constructor(contract, handlers, options) {
|
|
160
147
|
this.contract = contract;
|
|
161
148
|
this.handlers = handlers;
|
|
@@ -166,6 +153,7 @@ class Router {
|
|
|
166
153
|
} else {
|
|
167
154
|
this.basePath = "";
|
|
168
155
|
}
|
|
156
|
+
this.contextFactory = options?.context;
|
|
169
157
|
}
|
|
170
158
|
findEndpoint(method, path) {
|
|
171
159
|
for (const [name, endpoint] of Object.entries(this.contract)) {
|
|
@@ -179,25 +167,22 @@ class Router {
|
|
|
179
167
|
return null;
|
|
180
168
|
}
|
|
181
169
|
async handle(request) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
throw new RouteNotFoundError(path, method);
|
|
192
|
-
}
|
|
193
|
-
const { name, endpoint, params } = match;
|
|
194
|
-
const handler = this.handlers[name];
|
|
195
|
-
const input = await parseRequest(request, endpoint, params);
|
|
196
|
-
const handlerResponse = await handler(input);
|
|
197
|
-
return createResponse(endpoint, handlerResponse);
|
|
198
|
-
} catch (error) {
|
|
199
|
-
return createErrorResponse(error);
|
|
170
|
+
const url = new URL(request.url);
|
|
171
|
+
const method = request.method;
|
|
172
|
+
let path = url.pathname;
|
|
173
|
+
if (this.basePath && path.startsWith(this.basePath)) {
|
|
174
|
+
path = path.slice(this.basePath.length) || "/";
|
|
175
|
+
}
|
|
176
|
+
const match = this.findEndpoint(method, path);
|
|
177
|
+
if (!match) {
|
|
178
|
+
throw new RouteNotFoundError(path, method);
|
|
200
179
|
}
|
|
180
|
+
const { name, endpoint, params } = match;
|
|
181
|
+
const handler = this.handlers[name];
|
|
182
|
+
const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
|
|
183
|
+
const input = await parseRequest(request, endpoint, params, context);
|
|
184
|
+
const handlerResponse = await handler(input);
|
|
185
|
+
return createResponse(endpoint, handlerResponse);
|
|
201
186
|
}
|
|
202
187
|
get fetch() {
|
|
203
188
|
return (request) => this.handle(request);
|
|
@@ -208,4 +193,4 @@ function createRouter(contract, handlers, options) {
|
|
|
208
193
|
}
|
|
209
194
|
})
|
|
210
195
|
|
|
211
|
-
//# debugId=
|
|
196
|
+
//# debugId=CAA7CA78B3A5423864756E2164756E21
|
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> = {\n params: ExtractParams<T>;\n query: ExtractQuery<T>;\n headers: ExtractHeaders<T>;\n body: ExtractBody<T>;\n request: Request;\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> = (\n input: HandlerInput<T>,\n) => Promise<HandlerResponse<T>> | HandlerResponse<T>;\n\n// Contract handlers mapping\nexport type ContractHandlers<T extends Contract> = {\n [K in keyof T]: Handler<T[K]>;\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>(\n request: Request,\n endpoint: T,\n pathParams: Record<string, string>,\n): Promise<HandlerInput<T>> {\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('application/x-www-form-urlencoded')) {\n const formData = await request.formData();\n bodyData = Object.fromEntries(formData.entries());\n } else if (contentType.includes('multipart/form-data')) {\n const formData = await request.formData();\n bodyData = Object.fromEntries(formData.entries());\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 } as HandlerInput<T>;\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 *
|
|
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('application/x-www-form-urlencoded')) {\n const formData = await request.formData();\n bodyData = Object.fromEntries(formData.entries());\n } else if (contentType.includes('multipart/form-data')) {\n const formData = await request.formData();\n bodyData = Object.fromEntries(formData.entries());\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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQ8C,IAA9C;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "CAA7CA78B3A5423864756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/index.mjs
CHANGED
|
@@ -22,7 +22,7 @@ class RouteNotFoundError extends Error {
|
|
|
22
22
|
this.name = "RouteNotFoundError";
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
async function parseRequest(request, endpoint, pathParams) {
|
|
25
|
+
async function parseRequest(request, endpoint, pathParams, context) {
|
|
26
26
|
const url = new URL(request.url);
|
|
27
27
|
let params = pathParams;
|
|
28
28
|
if (endpoint.params) {
|
|
@@ -74,7 +74,7 @@ async function parseRequest(request, endpoint, pathParams) {
|
|
|
74
74
|
}
|
|
75
75
|
body = result.data;
|
|
76
76
|
}
|
|
77
|
-
return { params, query, headers, body, request };
|
|
77
|
+
return { params, query, headers, body, request, context };
|
|
78
78
|
}
|
|
79
79
|
function createResponse(endpoint, handlerResponse) {
|
|
80
80
|
const { status, body, headers: customHeaders } = handlerResponse;
|
|
@@ -100,25 +100,12 @@ function createResponse(endpoint, handlerResponse) {
|
|
|
100
100
|
headers: responseHeaders
|
|
101
101
|
});
|
|
102
102
|
}
|
|
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
103
|
|
|
118
104
|
class Router {
|
|
119
105
|
contract;
|
|
120
106
|
handlers;
|
|
121
107
|
basePath;
|
|
108
|
+
contextFactory;
|
|
122
109
|
constructor(contract, handlers, options) {
|
|
123
110
|
this.contract = contract;
|
|
124
111
|
this.handlers = handlers;
|
|
@@ -129,6 +116,7 @@ class Router {
|
|
|
129
116
|
} else {
|
|
130
117
|
this.basePath = "";
|
|
131
118
|
}
|
|
119
|
+
this.contextFactory = options?.context;
|
|
132
120
|
}
|
|
133
121
|
findEndpoint(method, path) {
|
|
134
122
|
for (const [name, endpoint] of Object.entries(this.contract)) {
|
|
@@ -142,25 +130,22 @@ class Router {
|
|
|
142
130
|
return null;
|
|
143
131
|
}
|
|
144
132
|
async handle(request) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
throw new RouteNotFoundError(path, method);
|
|
155
|
-
}
|
|
156
|
-
const { name, endpoint, params } = match;
|
|
157
|
-
const handler = this.handlers[name];
|
|
158
|
-
const input = await parseRequest(request, endpoint, params);
|
|
159
|
-
const handlerResponse = await handler(input);
|
|
160
|
-
return createResponse(endpoint, handlerResponse);
|
|
161
|
-
} catch (error) {
|
|
162
|
-
return createErrorResponse(error);
|
|
133
|
+
const url = new URL(request.url);
|
|
134
|
+
const method = request.method;
|
|
135
|
+
let path = url.pathname;
|
|
136
|
+
if (this.basePath && path.startsWith(this.basePath)) {
|
|
137
|
+
path = path.slice(this.basePath.length) || "/";
|
|
138
|
+
}
|
|
139
|
+
const match = this.findEndpoint(method, path);
|
|
140
|
+
if (!match) {
|
|
141
|
+
throw new RouteNotFoundError(path, method);
|
|
163
142
|
}
|
|
143
|
+
const { name, endpoint, params } = match;
|
|
144
|
+
const handler = this.handlers[name];
|
|
145
|
+
const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
|
|
146
|
+
const input = await parseRequest(request, endpoint, params, context);
|
|
147
|
+
const handlerResponse = await handler(input);
|
|
148
|
+
return createResponse(endpoint, handlerResponse);
|
|
164
149
|
}
|
|
165
150
|
get fetch() {
|
|
166
151
|
return (request) => this.handle(request);
|
|
@@ -177,4 +162,4 @@ export {
|
|
|
177
162
|
RouteNotFoundError
|
|
178
163
|
};
|
|
179
164
|
|
|
180
|
-
//# debugId=
|
|
165
|
+
//# debugId=6B1D2B1212041FB364756E2164756E21
|
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> = {\n params: ExtractParams<T>;\n query: ExtractQuery<T>;\n headers: ExtractHeaders<T>;\n body: ExtractBody<T>;\n request: Request;\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> = (\n input: HandlerInput<T>,\n) => Promise<HandlerResponse<T>> | HandlerResponse<T>;\n\n// Contract handlers mapping\nexport type ContractHandlers<T extends Contract> = {\n [K in keyof T]: Handler<T[K]>;\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>(\n request: Request,\n endpoint: T,\n pathParams: Record<string, string>,\n): Promise<HandlerInput<T>> {\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('application/x-www-form-urlencoded')) {\n const formData = await request.formData();\n bodyData = Object.fromEntries(formData.entries());\n } else if (contentType.includes('multipart/form-data')) {\n const formData = await request.formData();\n bodyData = Object.fromEntries(formData.entries());\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 } as HandlerInput<T>;\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 *
|
|
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('application/x-www-form-urlencoded')) {\n const formData = await request.formData();\n bodyData = Object.fromEntries(formData.entries());\n } else if (contentType.includes('multipart/form-data')) {\n const formData = await request.formData();\n bodyData = Object.fromEntries(formData.entries());\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;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "6B1D2B1212041FB364756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/mjs/package.json
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { Contract, EndpointDefinition, ExtractBody, ExtractHeaders, Extract
|
|
|
2
2
|
import { Status } from '@richie-rpc/core';
|
|
3
3
|
import type { z } from 'zod';
|
|
4
4
|
export { Status };
|
|
5
|
-
export type HandlerInput<T extends EndpointDefinition> = {
|
|
5
|
+
export type HandlerInput<T extends EndpointDefinition, C = unknown> = {
|
|
6
6
|
params: ExtractParams<T>;
|
|
7
7
|
query: ExtractQuery<T>;
|
|
8
8
|
headers: ExtractHeaders<T>;
|
|
9
9
|
body: ExtractBody<T>;
|
|
10
10
|
request: Request;
|
|
11
|
+
context: C;
|
|
11
12
|
};
|
|
12
13
|
export type HandlerResponse<T extends EndpointDefinition> = {
|
|
13
14
|
[Status in keyof T['responses']]: {
|
|
@@ -16,9 +17,9 @@ export type HandlerResponse<T extends EndpointDefinition> = {
|
|
|
16
17
|
headers?: Record<string, string>;
|
|
17
18
|
};
|
|
18
19
|
}[keyof T['responses']];
|
|
19
|
-
export type Handler<T extends EndpointDefinition> = (input: HandlerInput<T>) => Promise<HandlerResponse<T>> | HandlerResponse<T>;
|
|
20
|
-
export type ContractHandlers<T extends Contract> = {
|
|
21
|
-
[K in keyof T]: Handler<T[K]>;
|
|
20
|
+
export type Handler<T extends EndpointDefinition, C = unknown> = (input: HandlerInput<T, C>) => Promise<HandlerResponse<T>> | HandlerResponse<T>;
|
|
21
|
+
export type ContractHandlers<T extends Contract, C = unknown> = {
|
|
22
|
+
[K in keyof T]: Handler<T[K], C>;
|
|
22
23
|
};
|
|
23
24
|
export declare class ValidationError extends Error {
|
|
24
25
|
field: string;
|
|
@@ -33,17 +34,19 @@ export declare class RouteNotFoundError extends Error {
|
|
|
33
34
|
/**
|
|
34
35
|
* Router configuration options
|
|
35
36
|
*/
|
|
36
|
-
export interface RouterOptions {
|
|
37
|
+
export interface RouterOptions<C = unknown> {
|
|
37
38
|
basePath?: string;
|
|
39
|
+
context?: (request: Request, routeName?: string, endpoint?: EndpointDefinition) => C | Promise<C>;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* Router class that manages contract endpoints
|
|
41
43
|
*/
|
|
42
|
-
export declare class Router<T extends Contract> {
|
|
44
|
+
export declare class Router<T extends Contract, C = unknown> {
|
|
43
45
|
private contract;
|
|
44
46
|
private handlers;
|
|
45
47
|
private basePath;
|
|
46
|
-
|
|
48
|
+
private contextFactory?;
|
|
49
|
+
constructor(contract: T, handlers: ContractHandlers<T, C>, options?: RouterOptions<C>);
|
|
47
50
|
/**
|
|
48
51
|
* Find matching endpoint for a request
|
|
49
52
|
*/
|
|
@@ -60,4 +63,4 @@ export declare class Router<T extends Contract> {
|
|
|
60
63
|
/**
|
|
61
64
|
* Create a router from a contract and handlers
|
|
62
65
|
*/
|
|
63
|
-
export declare function createRouter<T extends Contract>(contract: T, handlers: ContractHandlers<T>, options?: RouterOptions): Router<T>;
|
|
66
|
+
export declare function createRouter<T extends Contract, C = unknown>(contract: T, handlers: ContractHandlers<T, C>, options?: RouterOptions<C>): Router<T, C>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@richie-rpc/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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.1",
|
|
14
14
|
"typescript": "^5",
|
|
15
15
|
"zod": "^4.1.12"
|
|
16
16
|
},
|