@richie-rpc/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/dist/cjs/index.cjs +200 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/mjs/index.mjs +169 -0
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/types/index.d.ts +54 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @richie-rpc/server
|
|
2
|
+
|
|
3
|
+
Server implementation package for Richie RPC with Bun.serve compatibility.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @richie-rpc/server @richie-rpc/core zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Creating a Router
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createRouter } from '@richie-rpc/server';
|
|
17
|
+
import { contract } from './contract';
|
|
18
|
+
|
|
19
|
+
const router = createRouter(contract, {
|
|
20
|
+
getUser: async ({ params }) => {
|
|
21
|
+
// params is fully typed based on the contract
|
|
22
|
+
const user = await db.getUser(params.id);
|
|
23
|
+
|
|
24
|
+
if (!user) {
|
|
25
|
+
return { status: 404, body: { error: 'User not found' } };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { status: 200, body: user };
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
createUser: async ({ body }) => {
|
|
32
|
+
// body is fully typed and already validated
|
|
33
|
+
const user = await db.createUser(body);
|
|
34
|
+
return { status: 201, body: user };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Using with Bun.serve
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
Bun.serve({
|
|
43
|
+
port: 3000,
|
|
44
|
+
fetch: router.fetch
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or with custom routing:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
Bun.serve({
|
|
52
|
+
port: 3000,
|
|
53
|
+
fetch(request) {
|
|
54
|
+
const url = new URL(request.url);
|
|
55
|
+
|
|
56
|
+
if (url.pathname.startsWith('/api')) {
|
|
57
|
+
return router.handle(request);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return new Response('Not Found', { status: 404 });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
- ✅ Automatic request validation (params, query, headers, body)
|
|
68
|
+
- ✅ Automatic response validation
|
|
69
|
+
- ✅ Type-safe handler inputs
|
|
70
|
+
- ✅ Path parameter matching
|
|
71
|
+
- ✅ Query parameter parsing
|
|
72
|
+
- ✅ JSON body parsing
|
|
73
|
+
- ✅ Form data support
|
|
74
|
+
- ✅ Detailed validation errors
|
|
75
|
+
- ✅ 404 handling for unknown routes
|
|
76
|
+
- ✅ Error handling and reporting
|
|
77
|
+
|
|
78
|
+
## Handler Input
|
|
79
|
+
|
|
80
|
+
Each handler receives a typed input object with:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
{
|
|
84
|
+
params: Record<string, string>, // Path parameters
|
|
85
|
+
query: Record<string, any>, // Query parameters
|
|
86
|
+
headers: Record<string, string>, // Request headers
|
|
87
|
+
body: any, // Request body
|
|
88
|
+
request: Request // Original Request object
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Handler Response
|
|
93
|
+
|
|
94
|
+
Each handler must return a response object with:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
{
|
|
98
|
+
status: number, // HTTP status code (must match contract)
|
|
99
|
+
body: any, // Response body (must match schema)
|
|
100
|
+
headers?: Record<string, string> // Optional custom headers
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Error Handling
|
|
105
|
+
|
|
106
|
+
The router automatically handles:
|
|
107
|
+
|
|
108
|
+
- **Validation Errors** (400): Invalid request data
|
|
109
|
+
- **Route Not Found** (404): Unknown endpoints
|
|
110
|
+
- **Internal Errors** (500): Uncaught exceptions
|
|
111
|
+
|
|
112
|
+
Custom error responses:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
return {
|
|
116
|
+
status: 400,
|
|
117
|
+
body: { error: 'Bad Request', message: 'Invalid input' }
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Validation
|
|
122
|
+
|
|
123
|
+
Both request and response data are validated against the contract schemas:
|
|
124
|
+
|
|
125
|
+
- Request validation happens before calling the handler
|
|
126
|
+
- Response validation happens before sending to the client
|
|
127
|
+
- Validation errors return detailed Zod error information
|
|
128
|
+
|
|
129
|
+
## Links
|
|
130
|
+
|
|
131
|
+
- **npm:** https://www.npmjs.com/package/@richie-rpc/server
|
|
132
|
+
- **Repository:** https://github.com/ricsam/richie-rpc
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
137
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// @bun @bun-cjs
|
|
2
|
+
(function(exports, require, module, __filename, __dirname) {var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
|
+
var __toCommonJS = (from) => {
|
|
8
|
+
var entry = __moduleCache.get(from), desc;
|
|
9
|
+
if (entry)
|
|
10
|
+
return entry;
|
|
11
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
13
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
14
|
+
get: () => from[key],
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
}));
|
|
17
|
+
__moduleCache.set(from, entry);
|
|
18
|
+
return entry;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// packages/server/index.ts
|
|
31
|
+
var exports_server = {};
|
|
32
|
+
__export(exports_server, {
|
|
33
|
+
createRouter: () => createRouter,
|
|
34
|
+
ValidationError: () => ValidationError,
|
|
35
|
+
Router: () => Router,
|
|
36
|
+
RouteNotFoundError: () => RouteNotFoundError
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(exports_server);
|
|
39
|
+
var import_core = require("@richie-rpc/core");
|
|
40
|
+
|
|
41
|
+
class ValidationError extends Error {
|
|
42
|
+
field;
|
|
43
|
+
issues;
|
|
44
|
+
constructor(field, issues, message) {
|
|
45
|
+
super(message || `Validation failed for ${field}`);
|
|
46
|
+
this.field = field;
|
|
47
|
+
this.issues = issues;
|
|
48
|
+
this.name = "ValidationError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class RouteNotFoundError extends Error {
|
|
53
|
+
path;
|
|
54
|
+
method;
|
|
55
|
+
constructor(path, method) {
|
|
56
|
+
super(`Route not found: ${method} ${path}`);
|
|
57
|
+
this.path = path;
|
|
58
|
+
this.method = method;
|
|
59
|
+
this.name = "RouteNotFoundError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function parseRequest(request, endpoint, pathParams) {
|
|
63
|
+
const url = new URL(request.url);
|
|
64
|
+
let params = pathParams;
|
|
65
|
+
if (endpoint.params) {
|
|
66
|
+
const result = endpoint.params.safeParse(pathParams);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
throw new ValidationError("params", result.error.issues);
|
|
69
|
+
}
|
|
70
|
+
params = result.data;
|
|
71
|
+
}
|
|
72
|
+
let query = {};
|
|
73
|
+
if (endpoint.query) {
|
|
74
|
+
const queryData = import_core.parseQuery(url.searchParams);
|
|
75
|
+
const result = endpoint.query.safeParse(queryData);
|
|
76
|
+
if (!result.success) {
|
|
77
|
+
throw new ValidationError("query", result.error.issues);
|
|
78
|
+
}
|
|
79
|
+
query = result.data;
|
|
80
|
+
}
|
|
81
|
+
let headers = {};
|
|
82
|
+
if (endpoint.headers) {
|
|
83
|
+
const headersObj = {};
|
|
84
|
+
request.headers.forEach((value, key) => {
|
|
85
|
+
headersObj[key] = value;
|
|
86
|
+
});
|
|
87
|
+
const result = endpoint.headers.safeParse(headersObj);
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
throw new ValidationError("headers", result.error.issues);
|
|
90
|
+
}
|
|
91
|
+
headers = result.data;
|
|
92
|
+
}
|
|
93
|
+
let body;
|
|
94
|
+
if (endpoint.body) {
|
|
95
|
+
const contentType = request.headers.get("content-type") || "";
|
|
96
|
+
let bodyData;
|
|
97
|
+
if (contentType.includes("application/json")) {
|
|
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
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
103
|
+
const formData = await request.formData();
|
|
104
|
+
bodyData = Object.fromEntries(formData.entries());
|
|
105
|
+
} else {
|
|
106
|
+
bodyData = await request.text();
|
|
107
|
+
}
|
|
108
|
+
const result = endpoint.body.safeParse(bodyData);
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
throw new ValidationError("body", result.error.issues);
|
|
111
|
+
}
|
|
112
|
+
body = result.data;
|
|
113
|
+
}
|
|
114
|
+
return { params, query, headers, body, request };
|
|
115
|
+
}
|
|
116
|
+
function createResponse(endpoint, handlerResponse) {
|
|
117
|
+
const { status, body, headers: customHeaders } = handlerResponse;
|
|
118
|
+
const responseSchema = endpoint.responses[status];
|
|
119
|
+
if (responseSchema) {
|
|
120
|
+
const result = responseSchema.safeParse(body);
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
throw new ValidationError(`response[${String(status)}]`, result.error.issues);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const responseHeaders = new Headers(customHeaders);
|
|
126
|
+
if (status === 204) {
|
|
127
|
+
return new Response(null, {
|
|
128
|
+
status: 204,
|
|
129
|
+
headers: responseHeaders
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!responseHeaders.has("content-type")) {
|
|
133
|
+
responseHeaders.set("content-type", "application/json");
|
|
134
|
+
}
|
|
135
|
+
return new Response(JSON.stringify(body), {
|
|
136
|
+
status,
|
|
137
|
+
headers: responseHeaders
|
|
138
|
+
});
|
|
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
|
+
|
|
155
|
+
class Router {
|
|
156
|
+
contract;
|
|
157
|
+
handlers;
|
|
158
|
+
constructor(contract, handlers) {
|
|
159
|
+
this.contract = contract;
|
|
160
|
+
this.handlers = handlers;
|
|
161
|
+
}
|
|
162
|
+
findEndpoint(method, path) {
|
|
163
|
+
for (const [name, endpoint] of Object.entries(this.contract)) {
|
|
164
|
+
if (endpoint.method === method) {
|
|
165
|
+
const params = import_core.matchPath(endpoint.path, path);
|
|
166
|
+
if (params !== null) {
|
|
167
|
+
return { name, endpoint, params };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
async handle(request) {
|
|
174
|
+
try {
|
|
175
|
+
const url = new URL(request.url);
|
|
176
|
+
const method = request.method;
|
|
177
|
+
const path = url.pathname;
|
|
178
|
+
const match = this.findEndpoint(method, path);
|
|
179
|
+
if (!match) {
|
|
180
|
+
throw new RouteNotFoundError(path, method);
|
|
181
|
+
}
|
|
182
|
+
const { name, endpoint, params } = match;
|
|
183
|
+
const handler = this.handlers[name];
|
|
184
|
+
const input = await parseRequest(request, endpoint, params);
|
|
185
|
+
const handlerResponse = await handler(input);
|
|
186
|
+
return createResponse(endpoint, handlerResponse);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return createErrorResponse(error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
get fetch() {
|
|
192
|
+
return (request) => this.handle(request);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function createRouter(contract, handlers) {
|
|
196
|
+
return new Router(contract, handlers);
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
//# debugId=17A5462B0AD83EAC64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../index.ts"],
|
|
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 } from '@richie-rpc/core';\nimport type { z } from 'zod';\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 const path = url.pathname;\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 // Parse and validate request\n const input = await parseRequest(request, endpoint, params);\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>(\n contract: T,\n handlers: ContractHandlers<T>,\n): Router<T> {\n return new Router(contract, handlers);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "17A5462B0AD83EAC64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/index.ts
|
|
3
|
+
import { matchPath, parseQuery } from "@richie-rpc/core";
|
|
4
|
+
|
|
5
|
+
class ValidationError extends Error {
|
|
6
|
+
field;
|
|
7
|
+
issues;
|
|
8
|
+
constructor(field, issues, message) {
|
|
9
|
+
super(message || `Validation failed for ${field}`);
|
|
10
|
+
this.field = field;
|
|
11
|
+
this.issues = issues;
|
|
12
|
+
this.name = "ValidationError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class RouteNotFoundError extends Error {
|
|
17
|
+
path;
|
|
18
|
+
method;
|
|
19
|
+
constructor(path, method) {
|
|
20
|
+
super(`Route not found: ${method} ${path}`);
|
|
21
|
+
this.path = path;
|
|
22
|
+
this.method = method;
|
|
23
|
+
this.name = "RouteNotFoundError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function parseRequest(request, endpoint, pathParams) {
|
|
27
|
+
const url = new URL(request.url);
|
|
28
|
+
let params = pathParams;
|
|
29
|
+
if (endpoint.params) {
|
|
30
|
+
const result = endpoint.params.safeParse(pathParams);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
throw new ValidationError("params", result.error.issues);
|
|
33
|
+
}
|
|
34
|
+
params = result.data;
|
|
35
|
+
}
|
|
36
|
+
let query = {};
|
|
37
|
+
if (endpoint.query) {
|
|
38
|
+
const queryData = parseQuery(url.searchParams);
|
|
39
|
+
const result = endpoint.query.safeParse(queryData);
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
throw new ValidationError("query", result.error.issues);
|
|
42
|
+
}
|
|
43
|
+
query = result.data;
|
|
44
|
+
}
|
|
45
|
+
let headers = {};
|
|
46
|
+
if (endpoint.headers) {
|
|
47
|
+
const headersObj = {};
|
|
48
|
+
request.headers.forEach((value, key) => {
|
|
49
|
+
headersObj[key] = value;
|
|
50
|
+
});
|
|
51
|
+
const result = endpoint.headers.safeParse(headersObj);
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
throw new ValidationError("headers", result.error.issues);
|
|
54
|
+
}
|
|
55
|
+
headers = result.data;
|
|
56
|
+
}
|
|
57
|
+
let body;
|
|
58
|
+
if (endpoint.body) {
|
|
59
|
+
const contentType = request.headers.get("content-type") || "";
|
|
60
|
+
let bodyData;
|
|
61
|
+
if (contentType.includes("application/json")) {
|
|
62
|
+
bodyData = await request.json();
|
|
63
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
64
|
+
const formData = await request.formData();
|
|
65
|
+
bodyData = Object.fromEntries(formData.entries());
|
|
66
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
67
|
+
const formData = await request.formData();
|
|
68
|
+
bodyData = Object.fromEntries(formData.entries());
|
|
69
|
+
} else {
|
|
70
|
+
bodyData = await request.text();
|
|
71
|
+
}
|
|
72
|
+
const result = endpoint.body.safeParse(bodyData);
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
throw new ValidationError("body", result.error.issues);
|
|
75
|
+
}
|
|
76
|
+
body = result.data;
|
|
77
|
+
}
|
|
78
|
+
return { params, query, headers, body, request };
|
|
79
|
+
}
|
|
80
|
+
function createResponse(endpoint, handlerResponse) {
|
|
81
|
+
const { status, body, headers: customHeaders } = handlerResponse;
|
|
82
|
+
const responseSchema = endpoint.responses[status];
|
|
83
|
+
if (responseSchema) {
|
|
84
|
+
const result = responseSchema.safeParse(body);
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
throw new ValidationError(`response[${String(status)}]`, result.error.issues);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const responseHeaders = new Headers(customHeaders);
|
|
90
|
+
if (status === 204) {
|
|
91
|
+
return new Response(null, {
|
|
92
|
+
status: 204,
|
|
93
|
+
headers: responseHeaders
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (!responseHeaders.has("content-type")) {
|
|
97
|
+
responseHeaders.set("content-type", "application/json");
|
|
98
|
+
}
|
|
99
|
+
return new Response(JSON.stringify(body), {
|
|
100
|
+
status,
|
|
101
|
+
headers: responseHeaders
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function createErrorResponse(error) {
|
|
105
|
+
if (error instanceof ValidationError) {
|
|
106
|
+
return Response.json({
|
|
107
|
+
error: "Validation Error",
|
|
108
|
+
field: error.field,
|
|
109
|
+
issues: error.issues
|
|
110
|
+
}, { status: 400 });
|
|
111
|
+
}
|
|
112
|
+
if (error instanceof RouteNotFoundError) {
|
|
113
|
+
return Response.json({ error: "Not Found", message: error.message }, { status: 404 });
|
|
114
|
+
}
|
|
115
|
+
console.error("Internal server error:", error);
|
|
116
|
+
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class Router {
|
|
120
|
+
contract;
|
|
121
|
+
handlers;
|
|
122
|
+
constructor(contract, handlers) {
|
|
123
|
+
this.contract = contract;
|
|
124
|
+
this.handlers = handlers;
|
|
125
|
+
}
|
|
126
|
+
findEndpoint(method, path) {
|
|
127
|
+
for (const [name, endpoint] of Object.entries(this.contract)) {
|
|
128
|
+
if (endpoint.method === method) {
|
|
129
|
+
const params = matchPath(endpoint.path, path);
|
|
130
|
+
if (params !== null) {
|
|
131
|
+
return { name, endpoint, params };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
async handle(request) {
|
|
138
|
+
try {
|
|
139
|
+
const url = new URL(request.url);
|
|
140
|
+
const method = request.method;
|
|
141
|
+
const path = url.pathname;
|
|
142
|
+
const match = this.findEndpoint(method, path);
|
|
143
|
+
if (!match) {
|
|
144
|
+
throw new RouteNotFoundError(path, method);
|
|
145
|
+
}
|
|
146
|
+
const { name, endpoint, params } = match;
|
|
147
|
+
const handler = this.handlers[name];
|
|
148
|
+
const input = await parseRequest(request, endpoint, params);
|
|
149
|
+
const handlerResponse = await handler(input);
|
|
150
|
+
return createResponse(endpoint, handlerResponse);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return createErrorResponse(error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
get fetch() {
|
|
156
|
+
return (request) => this.handle(request);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function createRouter(contract, handlers) {
|
|
160
|
+
return new Router(contract, handlers);
|
|
161
|
+
}
|
|
162
|
+
export {
|
|
163
|
+
createRouter,
|
|
164
|
+
ValidationError,
|
|
165
|
+
Router,
|
|
166
|
+
RouteNotFoundError
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
//# debugId=8E946DD736505E1764756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../index.ts"],
|
|
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 } from '@richie-rpc/core';\nimport type { z } from 'zod';\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 const path = url.pathname;\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 // Parse and validate request\n const input = await parseRequest(request, endpoint, params);\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>(\n contract: T,\n handlers: ContractHandlers<T>,\n): Router<T> {\n return new Router(contract, handlers);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "8E946DD736505E1764756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Contract, EndpointDefinition, ExtractBody, ExtractHeaders, ExtractParams, ExtractQuery } from '@richie-rpc/core';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
export type HandlerInput<T extends EndpointDefinition> = {
|
|
4
|
+
params: ExtractParams<T>;
|
|
5
|
+
query: ExtractQuery<T>;
|
|
6
|
+
headers: ExtractHeaders<T>;
|
|
7
|
+
body: ExtractBody<T>;
|
|
8
|
+
request: Request;
|
|
9
|
+
};
|
|
10
|
+
export type HandlerResponse<T extends EndpointDefinition> = {
|
|
11
|
+
[Status in keyof T['responses']]: {
|
|
12
|
+
status: Status;
|
|
13
|
+
body: T['responses'][Status] extends z.ZodTypeAny ? z.infer<T['responses'][Status]> : never;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
};
|
|
16
|
+
}[keyof T['responses']];
|
|
17
|
+
export type Handler<T extends EndpointDefinition> = (input: HandlerInput<T>) => Promise<HandlerResponse<T>> | HandlerResponse<T>;
|
|
18
|
+
export type ContractHandlers<T extends Contract> = {
|
|
19
|
+
[K in keyof T]: Handler<T[K]>;
|
|
20
|
+
};
|
|
21
|
+
export declare class ValidationError extends Error {
|
|
22
|
+
field: string;
|
|
23
|
+
issues: z.ZodIssue[];
|
|
24
|
+
constructor(field: string, issues: z.ZodIssue[], message?: string);
|
|
25
|
+
}
|
|
26
|
+
export declare class RouteNotFoundError extends Error {
|
|
27
|
+
path: string;
|
|
28
|
+
method: string;
|
|
29
|
+
constructor(path: string, method: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Router class that manages contract endpoints
|
|
33
|
+
*/
|
|
34
|
+
export declare class Router<T extends Contract> {
|
|
35
|
+
private contract;
|
|
36
|
+
private handlers;
|
|
37
|
+
constructor(contract: T, handlers: ContractHandlers<T>);
|
|
38
|
+
/**
|
|
39
|
+
* Find matching endpoint for a request
|
|
40
|
+
*/
|
|
41
|
+
private findEndpoint;
|
|
42
|
+
/**
|
|
43
|
+
* Handle a request
|
|
44
|
+
*/
|
|
45
|
+
handle(request: Request): Promise<Response>;
|
|
46
|
+
/**
|
|
47
|
+
* Get fetch handler compatible with Bun.serve
|
|
48
|
+
*/
|
|
49
|
+
get fetch(): (request: Request) => Promise<Response>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a router from a contract and handlers
|
|
53
|
+
*/
|
|
54
|
+
export declare function createRouter<T extends Contract>(contract: T, handlers: ContractHandlers<T>): Router<T>;
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@richie-rpc/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./dist/cjs/index.cjs",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"require": "./dist/cjs/index.cjs",
|
|
9
|
+
"import": "./dist/mjs/index.mjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@richie-rpc/core": "^0.1.0",
|
|
14
|
+
"zod": "^3.23.8"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"typescript": "^5"
|
|
18
|
+
},
|
|
19
|
+
"author": "Richie <oss@ricsam.dev>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/ricsam/richie-rpc.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/ricsam/richie-rpc/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/ricsam/richie-rpc#readme",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"typescript",
|
|
31
|
+
"bun",
|
|
32
|
+
"zod",
|
|
33
|
+
"api",
|
|
34
|
+
"contract",
|
|
35
|
+
"rpc",
|
|
36
|
+
"rest",
|
|
37
|
+
"openapi",
|
|
38
|
+
"type-safe"
|
|
39
|
+
],
|
|
40
|
+
"description": "Server implementation for Bun.serve with automatic validation",
|
|
41
|
+
"module": "./dist/mjs/index.mjs",
|
|
42
|
+
"types": "./dist/types/index.d.ts",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"README.md"
|
|
49
|
+
]
|
|
50
|
+
}
|