@richie-rpc/server 1.1.0 → 1.2.1
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 +27 -0
- package/dist/cjs/index.cjs +22 -8
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +22 -8
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +17 -7
- 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
|
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;
|
|
@@ -155,9 +155,19 @@ function createErrorResponse(error) {
|
|
|
155
155
|
class Router {
|
|
156
156
|
contract;
|
|
157
157
|
handlers;
|
|
158
|
-
|
|
158
|
+
basePath;
|
|
159
|
+
contextFactory;
|
|
160
|
+
constructor(contract, handlers, options) {
|
|
159
161
|
this.contract = contract;
|
|
160
162
|
this.handlers = handlers;
|
|
163
|
+
const bp = options?.basePath || "";
|
|
164
|
+
if (bp) {
|
|
165
|
+
this.basePath = bp.startsWith("/") ? bp : `/${bp}`;
|
|
166
|
+
this.basePath = this.basePath.endsWith("/") ? this.basePath.slice(0, -1) : this.basePath;
|
|
167
|
+
} else {
|
|
168
|
+
this.basePath = "";
|
|
169
|
+
}
|
|
170
|
+
this.contextFactory = options?.context;
|
|
161
171
|
}
|
|
162
172
|
findEndpoint(method, path) {
|
|
163
173
|
for (const [name, endpoint] of Object.entries(this.contract)) {
|
|
@@ -174,14 +184,18 @@ class Router {
|
|
|
174
184
|
try {
|
|
175
185
|
const url = new URL(request.url);
|
|
176
186
|
const method = request.method;
|
|
177
|
-
|
|
187
|
+
let path = url.pathname;
|
|
188
|
+
if (this.basePath && path.startsWith(this.basePath)) {
|
|
189
|
+
path = path.slice(this.basePath.length) || "/";
|
|
190
|
+
}
|
|
178
191
|
const match = this.findEndpoint(method, path);
|
|
179
192
|
if (!match) {
|
|
180
193
|
throw new RouteNotFoundError(path, method);
|
|
181
194
|
}
|
|
182
195
|
const { name, endpoint, params } = match;
|
|
183
196
|
const handler = this.handlers[name];
|
|
184
|
-
const
|
|
197
|
+
const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
|
|
198
|
+
const input = await parseRequest(request, endpoint, params, context);
|
|
185
199
|
const handlerResponse = await handler(input);
|
|
186
200
|
return createResponse(endpoint, handlerResponse);
|
|
187
201
|
} catch (error) {
|
|
@@ -192,9 +206,9 @@ class Router {
|
|
|
192
206
|
return (request) => this.handle(request);
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
|
-
function createRouter(contract, handlers) {
|
|
196
|
-
return new Router(contract, handlers);
|
|
209
|
+
function createRouter(contract, handlers, options) {
|
|
210
|
+
return new Router(contract, handlers, options);
|
|
197
211
|
}
|
|
198
212
|
})
|
|
199
213
|
|
|
200
|
-
//# debugId=
|
|
214
|
+
//# debugId=A1548B7B621C054764756E2164756E21
|
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 * Create error response\n */\nfunction createErrorResponse(error: unknown): Response {\n if (error instanceof ValidationError) {\n return Response.json(\n {\n error: 'Validation Error',\n field: error.field,\n issues: error.issues,\n },\n { status: 400 },\n );\n }\n\n if (error instanceof RouteNotFoundError) {\n return Response.json({ error: 'Not Found', message: error.message }, { status: 404 });\n }\n\n console.error('Internal server error:', error);\n return Response.json({ error: 'Internal Server Error' }, { status: 500 });\n}\n\n/**\n * Router class that manages contract endpoints\n */\nexport class Router<T extends Contract> {\n constructor(\n private contract: T,\n private handlers: ContractHandlers<T>,\n ) {}\n\n /**\n * Find matching endpoint for a request\n */\n private findEndpoint(\n method: string,\n path: string,\n ): { name: keyof T; endpoint: EndpointDefinition; params: Record<string, string> } | 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 try {\n const url = new URL(request.url);\n const method = request.method;\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 * Create error response\n */\nfunction createErrorResponse(error: unknown): Response {\n if (error instanceof ValidationError) {\n return Response.json(\n {\n error: 'Validation Error',\n field: error.field,\n issues: error.issues,\n },\n { status: 400 },\n );\n }\n\n if (error instanceof RouteNotFoundError) {\n return Response.json({ error: 'Not Found', message: error.message }, { status: 404 });\n }\n\n console.error('Internal server error:', error);\n return Response.json({ error: 'Internal Server Error' }, { status: 500 });\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 ): { name: keyof T; endpoint: EndpointDefinition; params: Record<string, string> } | 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 try {\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 } catch (error) {\n return createErrorResponse(error);\n }\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": "A1548B7B621C054764756E2164756E21",
|
|
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;
|
|
@@ -118,9 +118,19 @@ function createErrorResponse(error) {
|
|
|
118
118
|
class Router {
|
|
119
119
|
contract;
|
|
120
120
|
handlers;
|
|
121
|
-
|
|
121
|
+
basePath;
|
|
122
|
+
contextFactory;
|
|
123
|
+
constructor(contract, handlers, options) {
|
|
122
124
|
this.contract = contract;
|
|
123
125
|
this.handlers = handlers;
|
|
126
|
+
const bp = options?.basePath || "";
|
|
127
|
+
if (bp) {
|
|
128
|
+
this.basePath = bp.startsWith("/") ? bp : `/${bp}`;
|
|
129
|
+
this.basePath = this.basePath.endsWith("/") ? this.basePath.slice(0, -1) : this.basePath;
|
|
130
|
+
} else {
|
|
131
|
+
this.basePath = "";
|
|
132
|
+
}
|
|
133
|
+
this.contextFactory = options?.context;
|
|
124
134
|
}
|
|
125
135
|
findEndpoint(method, path) {
|
|
126
136
|
for (const [name, endpoint] of Object.entries(this.contract)) {
|
|
@@ -137,14 +147,18 @@ class Router {
|
|
|
137
147
|
try {
|
|
138
148
|
const url = new URL(request.url);
|
|
139
149
|
const method = request.method;
|
|
140
|
-
|
|
150
|
+
let path = url.pathname;
|
|
151
|
+
if (this.basePath && path.startsWith(this.basePath)) {
|
|
152
|
+
path = path.slice(this.basePath.length) || "/";
|
|
153
|
+
}
|
|
141
154
|
const match = this.findEndpoint(method, path);
|
|
142
155
|
if (!match) {
|
|
143
156
|
throw new RouteNotFoundError(path, method);
|
|
144
157
|
}
|
|
145
158
|
const { name, endpoint, params } = match;
|
|
146
159
|
const handler = this.handlers[name];
|
|
147
|
-
const
|
|
160
|
+
const context = this.contextFactory ? await this.contextFactory(request, String(name), endpoint) : undefined;
|
|
161
|
+
const input = await parseRequest(request, endpoint, params, context);
|
|
148
162
|
const handlerResponse = await handler(input);
|
|
149
163
|
return createResponse(endpoint, handlerResponse);
|
|
150
164
|
} catch (error) {
|
|
@@ -155,8 +169,8 @@ class Router {
|
|
|
155
169
|
return (request) => this.handle(request);
|
|
156
170
|
}
|
|
157
171
|
}
|
|
158
|
-
function createRouter(contract, handlers) {
|
|
159
|
-
return new Router(contract, handlers);
|
|
172
|
+
function createRouter(contract, handlers, options) {
|
|
173
|
+
return new Router(contract, handlers, options);
|
|
160
174
|
}
|
|
161
175
|
export {
|
|
162
176
|
createRouter,
|
|
@@ -166,4 +180,4 @@ export {
|
|
|
166
180
|
RouteNotFoundError
|
|
167
181
|
};
|
|
168
182
|
|
|
169
|
-
//# debugId=
|
|
183
|
+
//# debugId=F0AAA49C7D648D7164756E2164756E21
|
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 * Create error response\n */\nfunction createErrorResponse(error: unknown): Response {\n if (error instanceof ValidationError) {\n return Response.json(\n {\n error: 'Validation Error',\n field: error.field,\n issues: error.issues,\n },\n { status: 400 },\n );\n }\n\n if (error instanceof RouteNotFoundError) {\n return Response.json({ error: 'Not Found', message: error.message }, { status: 404 });\n }\n\n console.error('Internal server error:', error);\n return Response.json({ error: 'Internal Server Error' }, { status: 500 });\n}\n\n/**\n * Router class that manages contract endpoints\n */\nexport class Router<T extends Contract> {\n constructor(\n private contract: T,\n private handlers: ContractHandlers<T>,\n ) {}\n\n /**\n * Find matching endpoint for a request\n */\n private findEndpoint(\n method: string,\n path: string,\n ): { name: keyof T; endpoint: EndpointDefinition; params: Record<string, string> } | 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 try {\n const url = new URL(request.url);\n const method = request.method;\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 * Create error response\n */\nfunction createErrorResponse(error: unknown): Response {\n if (error instanceof ValidationError) {\n return Response.json(\n {\n error: 'Validation Error',\n field: error.field,\n issues: error.issues,\n },\n { status: 400 },\n );\n }\n\n if (error instanceof RouteNotFoundError) {\n return Response.json({ error: 'Not Found', message: error.message }, { status: 404 });\n }\n\n console.error('Internal server error:', error);\n return Response.json({ error: 'Internal Server Error' }, { status: 500 });\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 ): { name: keyof T; endpoint: EndpointDefinition; params: Record<string, string> } | 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 try {\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 } catch (error) {\n return createErrorResponse(error);\n }\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": "F0AAA49C7D648D7164756E2164756E21",
|
|
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;
|
|
@@ -30,13 +31,22 @@ export declare class RouteNotFoundError extends Error {
|
|
|
30
31
|
method: string;
|
|
31
32
|
constructor(path: string, method: string);
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Router configuration options
|
|
36
|
+
*/
|
|
37
|
+
export interface RouterOptions<C = unknown> {
|
|
38
|
+
basePath?: string;
|
|
39
|
+
context?: (request: Request, routeName?: string, endpoint?: EndpointDefinition) => C | Promise<C>;
|
|
40
|
+
}
|
|
33
41
|
/**
|
|
34
42
|
* Router class that manages contract endpoints
|
|
35
43
|
*/
|
|
36
|
-
export declare class Router<T extends Contract> {
|
|
44
|
+
export declare class Router<T extends Contract, C = unknown> {
|
|
37
45
|
private contract;
|
|
38
46
|
private handlers;
|
|
39
|
-
|
|
47
|
+
private basePath;
|
|
48
|
+
private contextFactory?;
|
|
49
|
+
constructor(contract: T, handlers: ContractHandlers<T, C>, options?: RouterOptions<C>);
|
|
40
50
|
/**
|
|
41
51
|
* Find matching endpoint for a request
|
|
42
52
|
*/
|
|
@@ -53,4 +63,4 @@ export declare class Router<T extends Contract> {
|
|
|
53
63
|
/**
|
|
54
64
|
* Create a router from a contract and handlers
|
|
55
65
|
*/
|
|
56
|
-
export declare function createRouter<T extends Contract>(contract: T, handlers: ContractHandlers<T>): 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.1
|
|
3
|
+
"version": "1.2.1",
|
|
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.1
|
|
13
|
+
"@richie-rpc/core": "^1.2.1",
|
|
14
14
|
"typescript": "^5",
|
|
15
15
|
"zod": "^4.1.12"
|
|
16
16
|
},
|