@jokio/rpc 0.6.2 → 0.7.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 +100 -12
- package/dist/index.d.mts +17 -17
- package/dist/index.d.ts +17 -17
- package/dist/index.js +87 -80
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +87 -80
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ A type-safe RPC framework for TypeScript with Zod validation, designed for Expre
|
|
|
14
14
|
- Runtime validation using Zod schemas
|
|
15
15
|
- Express.js integration for server-side
|
|
16
16
|
- Flexible fetch-based client with custom fetch support
|
|
17
|
-
-
|
|
17
|
+
- Support for multiple HTTP methods (GET, POST, PUT, PATCH, DELETE, QUERY)
|
|
18
|
+
- Path parameters, query parameters, and request body validation
|
|
18
19
|
- Automatic response validation
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
@@ -34,6 +35,9 @@ import { z } from "zod"
|
|
|
34
35
|
const routes = defineRoutes({
|
|
35
36
|
GET: {
|
|
36
37
|
"/user/:id": {
|
|
38
|
+
queryParams: z.object({
|
|
39
|
+
include: z.string().optional(),
|
|
40
|
+
}),
|
|
37
41
|
response: z.object({
|
|
38
42
|
id: z.string(),
|
|
39
43
|
name: z.string(),
|
|
@@ -54,6 +58,19 @@ const routes = defineRoutes({
|
|
|
54
58
|
}),
|
|
55
59
|
},
|
|
56
60
|
},
|
|
61
|
+
PUT: {
|
|
62
|
+
"/user/:id": {
|
|
63
|
+
body: z.object({
|
|
64
|
+
name: z.string().optional(),
|
|
65
|
+
email: z.string().email().optional(),
|
|
66
|
+
}),
|
|
67
|
+
response: z.object({
|
|
68
|
+
id: z.string(),
|
|
69
|
+
name: z.string(),
|
|
70
|
+
email: z.string(),
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
57
74
|
})
|
|
58
75
|
```
|
|
59
76
|
|
|
@@ -70,7 +87,9 @@ const router = express.Router()
|
|
|
70
87
|
|
|
71
88
|
registerExpressRoutes(router, routes, {
|
|
72
89
|
GET: {
|
|
73
|
-
"/user/:id": async ({ params }) => {
|
|
90
|
+
"/user/:id": async ({ params, queryParams }) => {
|
|
91
|
+
// params.id is type-safe and contains the :id from the path
|
|
92
|
+
// queryParams.include is validated by Zod
|
|
74
93
|
return {
|
|
75
94
|
id: params.id,
|
|
76
95
|
name: "John Doe",
|
|
@@ -80,6 +99,7 @@ registerExpressRoutes(router, routes, {
|
|
|
80
99
|
},
|
|
81
100
|
POST: {
|
|
82
101
|
"/user": async ({ body }) => {
|
|
102
|
+
// body is validated by Zod
|
|
83
103
|
return {
|
|
84
104
|
id: "2",
|
|
85
105
|
name: body.name,
|
|
@@ -87,6 +107,16 @@ registerExpressRoutes(router, routes, {
|
|
|
87
107
|
}
|
|
88
108
|
},
|
|
89
109
|
},
|
|
110
|
+
PUT: {
|
|
111
|
+
"/user/:id": async ({ params, body }) => {
|
|
112
|
+
// Both params and body are type-safe
|
|
113
|
+
return {
|
|
114
|
+
id: params.id,
|
|
115
|
+
name: body.name ?? "John Doe",
|
|
116
|
+
email: body.email ?? "john@example.com",
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
90
120
|
})
|
|
91
121
|
|
|
92
122
|
app.use("/api", router)
|
|
@@ -103,13 +133,27 @@ const client = createClient(routes, {
|
|
|
103
133
|
validate: true, // Optional: validate requests on client-side
|
|
104
134
|
})
|
|
105
135
|
|
|
106
|
-
// Fully typed API calls
|
|
107
|
-
const user = await client.GET("/
|
|
136
|
+
// Fully typed API calls with path parameters
|
|
137
|
+
const user = await client.GET("/user/:id", {
|
|
138
|
+
params: { id: "23" },
|
|
139
|
+
queryParams: { include: "profile" },
|
|
140
|
+
})
|
|
108
141
|
|
|
109
|
-
|
|
142
|
+
// POST request with body
|
|
143
|
+
const newUser = await client.POST("/user", {
|
|
110
144
|
name: "Jane Doe",
|
|
111
145
|
email: "jane@example.com",
|
|
112
146
|
})
|
|
147
|
+
|
|
148
|
+
// PUT request with path parameters and body
|
|
149
|
+
const updatedUser = await client.PUT("/user/:id",
|
|
150
|
+
{
|
|
151
|
+
name: "Jane Smith",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
params: { id: "23" },
|
|
155
|
+
}
|
|
156
|
+
)
|
|
113
157
|
```
|
|
114
158
|
|
|
115
159
|
## API Reference
|
|
@@ -120,7 +164,12 @@ Helper function to define routes with type inference.
|
|
|
120
164
|
|
|
121
165
|
**Parameters:**
|
|
122
166
|
|
|
123
|
-
- `routes`: Route definitions object containing GET
|
|
167
|
+
- `routes`: Route definitions object containing method configurations (GET, POST, PUT, PATCH, DELETE, QUERY)
|
|
168
|
+
|
|
169
|
+
**Route Configuration:**
|
|
170
|
+
- `body`: Zod schema for request body (not available for GET)
|
|
171
|
+
- `queryParams`: Zod schema for query parameters (optional)
|
|
172
|
+
- `response`: Zod schema for response data
|
|
124
173
|
|
|
125
174
|
### `registerExpressRoutes(router, routes, handlers)`
|
|
126
175
|
|
|
@@ -130,10 +179,14 @@ Registers route handlers to an Express router with automatic validation.
|
|
|
130
179
|
|
|
131
180
|
- `router`: Express Router instance
|
|
132
181
|
- `routes`: Route definitions object
|
|
133
|
-
- `handlers`: Handler functions for each route with optional
|
|
182
|
+
- `handlers`: Handler functions for each route with optional configuration
|
|
134
183
|
- `ctx`: Optional function `(req: Request) => TContext` to provide context to handlers
|
|
135
|
-
- `
|
|
136
|
-
- `
|
|
184
|
+
- `validation`: Optional boolean to enable response validation (default: false)
|
|
185
|
+
- `schemaFilePath`: Optional path to expose route schemas at `/__schema` endpoint
|
|
186
|
+
- `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `QUERY`: Handler functions that receive `(data, ctx)` parameters
|
|
187
|
+
- `data.params`: Path parameters (e.g., `:id` in `/user/:id`)
|
|
188
|
+
- `data.body`: Request body (validated by Zod)
|
|
189
|
+
- `data.queryParams`: Query parameters (validated by Zod)
|
|
137
190
|
|
|
138
191
|
### `createClient(routes, options)`
|
|
139
192
|
|
|
@@ -146,7 +199,21 @@ Creates a type-safe HTTP client.
|
|
|
146
199
|
- `baseUrl`: Base URL for API requests
|
|
147
200
|
- `getHeaders`: Optional function that returns headers (sync or async)
|
|
148
201
|
- `fetch`: Optional custom fetch function (useful for Node.js or testing)
|
|
149
|
-
- `validate`: Enable client-side request validation (default:
|
|
202
|
+
- `validate`: Enable client-side request validation (default: true)
|
|
203
|
+
- `debug`: Enable debug logging (default: false)
|
|
204
|
+
|
|
205
|
+
**Client Methods:**
|
|
206
|
+
|
|
207
|
+
Each HTTP method has a type-safe method on the client:
|
|
208
|
+
|
|
209
|
+
- `GET(path, options?)`: For GET requests
|
|
210
|
+
- `options.params`: Path parameters
|
|
211
|
+
- `options.queryParams`: Query parameters
|
|
212
|
+
- `POST(path, body, options?)`: For POST requests
|
|
213
|
+
- `PUT(path, body, options?)`: For PUT requests
|
|
214
|
+
- `PATCH(path, body, options?)`: For PATCH requests
|
|
215
|
+
- `DELETE(path, body, options?)`: For DELETE requests
|
|
216
|
+
- `QUERY(path, body, options?)`: For QUERY requests (custom method)
|
|
150
217
|
|
|
151
218
|
## Type Safety
|
|
152
219
|
|
|
@@ -154,13 +221,24 @@ The library provides end-to-end type safety:
|
|
|
154
221
|
|
|
155
222
|
```typescript
|
|
156
223
|
// TypeScript knows the exact shape of requests and responses
|
|
157
|
-
const result = await client.POST("/
|
|
224
|
+
const result = await client.POST("/user", {
|
|
158
225
|
name: "John",
|
|
159
226
|
email: "invalid-email", // Zod will catch this at runtime
|
|
160
227
|
})
|
|
161
228
|
|
|
162
229
|
// result is typed as { id: string; name: string; email: string }
|
|
163
230
|
console.log(result.id)
|
|
231
|
+
|
|
232
|
+
// Path parameters are type-safe
|
|
233
|
+
const user = await client.GET("/user/:id", {
|
|
234
|
+
params: { id: "123" }, // TypeScript enforces correct parameter names
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// Query parameters are validated
|
|
238
|
+
const users = await client.GET("/user/:id", {
|
|
239
|
+
params: { id: "123" },
|
|
240
|
+
queryParams: { include: "profile" }, // Must match Zod schema
|
|
241
|
+
})
|
|
164
242
|
```
|
|
165
243
|
|
|
166
244
|
## Error Handling
|
|
@@ -169,13 +247,23 @@ The library throws errors for:
|
|
|
169
247
|
|
|
170
248
|
- HTTP errors (non-2xx responses)
|
|
171
249
|
- Validation errors (invalid request/response data)
|
|
250
|
+
- Missing path parameters
|
|
172
251
|
|
|
173
252
|
```typescript
|
|
174
253
|
try {
|
|
175
|
-
await client.POST("/
|
|
254
|
+
await client.POST("/user", invalidData)
|
|
176
255
|
} catch (error) {
|
|
177
256
|
// Handle validation or HTTP errors
|
|
178
257
|
}
|
|
258
|
+
|
|
259
|
+
// Missing path parameters will throw an error
|
|
260
|
+
try {
|
|
261
|
+
await client.GET("/user/:id", {
|
|
262
|
+
params: {}, // Missing 'id' parameter
|
|
263
|
+
})
|
|
264
|
+
} catch (error) {
|
|
265
|
+
// Error: Missing required parameter: "id" for path "/user/:id"
|
|
266
|
+
}
|
|
179
267
|
```
|
|
180
268
|
|
|
181
269
|
## License
|
package/dist/index.d.mts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
import { Router, Request } from 'express';
|
|
3
3
|
|
|
4
|
+
type RouterConfig = {
|
|
5
|
+
GET: Record<string, Omit<RouteConfig, "body">>;
|
|
6
|
+
QUERY: Record<string, RouteConfig>;
|
|
7
|
+
POST: Record<string, RouteConfig>;
|
|
8
|
+
PUT: Record<string, RouteConfig>;
|
|
9
|
+
PATCH: Record<string, RouteConfig>;
|
|
10
|
+
DELETE: Record<string, RouteConfig>;
|
|
11
|
+
};
|
|
4
12
|
type RouteConfig = {
|
|
5
13
|
body: z.ZodType;
|
|
6
14
|
queryParams?: z.ZodType;
|
|
7
15
|
response: z.ZodType;
|
|
8
16
|
};
|
|
9
|
-
type RouterConfig = {
|
|
10
|
-
GET: Record<string, Omit<RouteConfig, "body">>;
|
|
11
|
-
POST: Record<string, RouteConfig>;
|
|
12
|
-
};
|
|
13
17
|
type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "body">> = {
|
|
14
18
|
[K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never;
|
|
15
19
|
};
|
|
16
|
-
declare const defineRoutes: <T extends RouterConfig
|
|
20
|
+
declare const defineRoutes: <T extends Partial<RouterConfig>>(routes: T) => T;
|
|
17
21
|
type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? Rest extends `:${string}` ? {
|
|
18
22
|
[K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string;
|
|
19
23
|
} : Rest extends `${string}/:${string}` ? {
|
|
@@ -27,9 +31,8 @@ type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer P
|
|
|
27
31
|
type ClientOptions<TConfig, K> = Omit<TConfig, "response"> & {
|
|
28
32
|
params?: K extends string ? ExtractRouteParams<K> : unknown;
|
|
29
33
|
};
|
|
30
|
-
type RouterClient<T extends RouterConfig
|
|
31
|
-
|
|
32
|
-
POST: <K extends keyof T["POST"]>(path: K, body: InferRouteConfig<T["POST"][K]>["body"], options?: ClientOptions<Omit<InferRouteConfig<T["POST"][K]>, "body">, K>) => Promise<InferRouteConfig<T["POST"][K]>["response"]>;
|
|
34
|
+
type RouterClient<T extends Partial<RouterConfig>> = {
|
|
35
|
+
[M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? M extends "GET" ? <K extends keyof T[M]>(path: K, options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "body">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : <K extends keyof T[M]>(path: K, body: InferRouteConfig<T[M][K]>["body"], options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "body">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : never;
|
|
33
36
|
};
|
|
34
37
|
type FetchFunction = (url: string, options: RequestInit) => Promise<Response>;
|
|
35
38
|
type CreateClientOptions = {
|
|
@@ -39,21 +42,18 @@ type CreateClientOptions = {
|
|
|
39
42
|
validate?: boolean;
|
|
40
43
|
debug?: boolean;
|
|
41
44
|
};
|
|
42
|
-
declare const createClient: <T extends RouterConfig
|
|
45
|
+
declare const createClient: <T extends Partial<RouterConfig>>(routes: T, options: CreateClientOptions) => RouterClient<T>;
|
|
43
46
|
|
|
44
47
|
type MaybePromise<T> = Promise<T> | T;
|
|
45
48
|
type HandlerData<TConfig, K> = Omit<TConfig, "response"> & {
|
|
46
49
|
params: K extends string ? ExtractRouteParams<K> : unknown;
|
|
47
50
|
};
|
|
48
|
-
type RouteHandlers<T extends RouterConfig
|
|
49
|
-
|
|
50
|
-
[K in keyof T[
|
|
51
|
-
};
|
|
52
|
-
POST: {
|
|
53
|
-
[K in keyof T["POST"]]: (data: HandlerData<InferRouteConfig<T["POST"][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T["POST"][K]>["response"]>;
|
|
54
|
-
};
|
|
51
|
+
type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {
|
|
52
|
+
[M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? {
|
|
53
|
+
[K in keyof T[M]]: T[M][K] extends RouteConfig | Omit<RouteConfig, "body"> ? (data: M extends "GET" ? HandlerData<Omit<InferRouteConfig<T[M][K]>, "body">, K> : HandlerData<InferRouteConfig<T[M][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T[M][K]>["response"]> : never;
|
|
54
|
+
} : never;
|
|
55
55
|
};
|
|
56
|
-
declare const registerExpressRoutes: <T extends RouterConfig
|
|
56
|
+
declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
|
|
57
57
|
ctx?: (req: Request) => TContext;
|
|
58
58
|
schemaFilePath?: string;
|
|
59
59
|
validation?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
import { Router, Request } from 'express';
|
|
3
3
|
|
|
4
|
+
type RouterConfig = {
|
|
5
|
+
GET: Record<string, Omit<RouteConfig, "body">>;
|
|
6
|
+
QUERY: Record<string, RouteConfig>;
|
|
7
|
+
POST: Record<string, RouteConfig>;
|
|
8
|
+
PUT: Record<string, RouteConfig>;
|
|
9
|
+
PATCH: Record<string, RouteConfig>;
|
|
10
|
+
DELETE: Record<string, RouteConfig>;
|
|
11
|
+
};
|
|
4
12
|
type RouteConfig = {
|
|
5
13
|
body: z.ZodType;
|
|
6
14
|
queryParams?: z.ZodType;
|
|
7
15
|
response: z.ZodType;
|
|
8
16
|
};
|
|
9
|
-
type RouterConfig = {
|
|
10
|
-
GET: Record<string, Omit<RouteConfig, "body">>;
|
|
11
|
-
POST: Record<string, RouteConfig>;
|
|
12
|
-
};
|
|
13
17
|
type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "body">> = {
|
|
14
18
|
[K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never;
|
|
15
19
|
};
|
|
16
|
-
declare const defineRoutes: <T extends RouterConfig
|
|
20
|
+
declare const defineRoutes: <T extends Partial<RouterConfig>>(routes: T) => T;
|
|
17
21
|
type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? Rest extends `:${string}` ? {
|
|
18
22
|
[K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string;
|
|
19
23
|
} : Rest extends `${string}/:${string}` ? {
|
|
@@ -27,9 +31,8 @@ type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer P
|
|
|
27
31
|
type ClientOptions<TConfig, K> = Omit<TConfig, "response"> & {
|
|
28
32
|
params?: K extends string ? ExtractRouteParams<K> : unknown;
|
|
29
33
|
};
|
|
30
|
-
type RouterClient<T extends RouterConfig
|
|
31
|
-
|
|
32
|
-
POST: <K extends keyof T["POST"]>(path: K, body: InferRouteConfig<T["POST"][K]>["body"], options?: ClientOptions<Omit<InferRouteConfig<T["POST"][K]>, "body">, K>) => Promise<InferRouteConfig<T["POST"][K]>["response"]>;
|
|
34
|
+
type RouterClient<T extends Partial<RouterConfig>> = {
|
|
35
|
+
[M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? M extends "GET" ? <K extends keyof T[M]>(path: K, options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "body">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : <K extends keyof T[M]>(path: K, body: InferRouteConfig<T[M][K]>["body"], options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "body">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : never;
|
|
33
36
|
};
|
|
34
37
|
type FetchFunction = (url: string, options: RequestInit) => Promise<Response>;
|
|
35
38
|
type CreateClientOptions = {
|
|
@@ -39,21 +42,18 @@ type CreateClientOptions = {
|
|
|
39
42
|
validate?: boolean;
|
|
40
43
|
debug?: boolean;
|
|
41
44
|
};
|
|
42
|
-
declare const createClient: <T extends RouterConfig
|
|
45
|
+
declare const createClient: <T extends Partial<RouterConfig>>(routes: T, options: CreateClientOptions) => RouterClient<T>;
|
|
43
46
|
|
|
44
47
|
type MaybePromise<T> = Promise<T> | T;
|
|
45
48
|
type HandlerData<TConfig, K> = Omit<TConfig, "response"> & {
|
|
46
49
|
params: K extends string ? ExtractRouteParams<K> : unknown;
|
|
47
50
|
};
|
|
48
|
-
type RouteHandlers<T extends RouterConfig
|
|
49
|
-
|
|
50
|
-
[K in keyof T[
|
|
51
|
-
};
|
|
52
|
-
POST: {
|
|
53
|
-
[K in keyof T["POST"]]: (data: HandlerData<InferRouteConfig<T["POST"][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T["POST"][K]>["response"]>;
|
|
54
|
-
};
|
|
51
|
+
type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {
|
|
52
|
+
[M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? {
|
|
53
|
+
[K in keyof T[M]]: T[M][K] extends RouteConfig | Omit<RouteConfig, "body"> ? (data: M extends "GET" ? HandlerData<Omit<InferRouteConfig<T[M][K]>, "body">, K> : HandlerData<InferRouteConfig<T[M][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T[M][K]>["response"]> : never;
|
|
54
|
+
} : never;
|
|
55
55
|
};
|
|
56
|
-
declare const registerExpressRoutes: <T extends RouterConfig
|
|
56
|
+
declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
|
|
57
57
|
ctx?: (req: Request) => TContext;
|
|
58
58
|
schemaFilePath?: string;
|
|
59
59
|
validation?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -26,115 +26,122 @@ var createClient = (routes, options) => {
|
|
|
26
26
|
baseUrl,
|
|
27
27
|
getHeaders = () => Promise.resolve({}),
|
|
28
28
|
fetch: customFetch = fetch,
|
|
29
|
-
validate =
|
|
29
|
+
validate = true
|
|
30
30
|
} = options;
|
|
31
|
-
const
|
|
32
|
-
GET: {},
|
|
33
|
-
POST: {}
|
|
34
|
-
};
|
|
35
|
-
client.GET = async (path, options2) => {
|
|
36
|
-
if (validate && options2?.queryParams) {
|
|
37
|
-
routes.GET[path]?.queryParams?.parse(options2.queryParams);
|
|
38
|
-
}
|
|
31
|
+
const buildUrl = (path, options2) => {
|
|
39
32
|
const queryString = options2?.queryParams ? "?" + new URLSearchParams(options2.queryParams).toString() : "";
|
|
40
33
|
const finalPath = path.includes(":") ? replacePathParams(path, options2?.params ?? {}) : path;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
return `${baseUrl}${finalPath}${queryString}`;
|
|
35
|
+
};
|
|
36
|
+
const handleValidation = (method, path, body, options2) => {
|
|
37
|
+
if (!validate) return;
|
|
38
|
+
const routeConfig = routes[method]?.[path];
|
|
39
|
+
if (body && routeConfig?.body) {
|
|
40
|
+
routeConfig.body.parse(body);
|
|
41
|
+
}
|
|
42
|
+
if (options2?.queryParams && routeConfig?.queryParams) {
|
|
43
|
+
routeConfig.queryParams.parse(options2.queryParams);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const handleResponse = async (method, path, response, options2) => {
|
|
48
47
|
if (!response.ok) {
|
|
49
48
|
const error = await response.json();
|
|
50
|
-
if (options2
|
|
49
|
+
if (options2?.debug) {
|
|
51
50
|
console.debug(error);
|
|
52
51
|
}
|
|
53
52
|
throw new Error(error.message);
|
|
54
53
|
}
|
|
55
|
-
|
|
54
|
+
const routeConfig = routes[method]?.[path];
|
|
55
|
+
if (routeConfig?.response?.type === "void") {
|
|
56
56
|
await response.text();
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
const json = await response.json();
|
|
60
|
-
return validate ?
|
|
60
|
+
return validate && routeConfig?.response ? routeConfig.response.parse(json) : json;
|
|
61
61
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (options2?.queryParams) {
|
|
68
|
-
routes.POST[path]?.queryParams?.parse(options2.queryParams);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const queryString = options2?.queryParams ? "?" + new URLSearchParams(options2.queryParams).toString() : "";
|
|
72
|
-
const finalPath = path.includes(":") ? replacePathParams(path, options2?.params ?? {}) : path;
|
|
73
|
-
const response = await customFetch(`${baseUrl}${finalPath}${queryString}`, {
|
|
74
|
-
method: "POST",
|
|
62
|
+
const makeRequest = async (method, path, body, options2) => {
|
|
63
|
+
handleValidation(method, path, body, options2);
|
|
64
|
+
const url = buildUrl(path, options2);
|
|
65
|
+
const fetchOptions = {
|
|
66
|
+
method,
|
|
75
67
|
headers: {
|
|
76
68
|
"Content-Type": "application/json",
|
|
77
69
|
...await getHeaders()
|
|
78
|
-
},
|
|
79
|
-
body: JSON.stringify(body)
|
|
80
|
-
});
|
|
81
|
-
if (!response.ok) {
|
|
82
|
-
const error = await response.json();
|
|
83
|
-
if (options2.debug) {
|
|
84
|
-
console.debug(error);
|
|
85
70
|
}
|
|
86
|
-
|
|
71
|
+
};
|
|
72
|
+
if (body !== void 0) {
|
|
73
|
+
fetchOptions.body = JSON.stringify(body);
|
|
87
74
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const json = await response.json();
|
|
93
|
-
return validate ? routes.POST[path]?.response.parse(json) : json;
|
|
75
|
+
const response = await customFetch(url, fetchOptions);
|
|
76
|
+
return handleResponse(method, path, response, options2);
|
|
94
77
|
};
|
|
78
|
+
const client = {};
|
|
79
|
+
const methodHandlers = {
|
|
80
|
+
GET: async (path, options2) => makeRequest("GET", path, void 0, options2),
|
|
81
|
+
QUERY: async (path, body, options2) => makeRequest("QUERY", path, body, options2),
|
|
82
|
+
POST: async (path, body, options2) => makeRequest("POST", path, body, options2),
|
|
83
|
+
PUT: async (path, body, options2) => makeRequest("PUT", path, body, options2),
|
|
84
|
+
PATCH: async (path, body, options2) => makeRequest("PATCH", path, body, options2),
|
|
85
|
+
DELETE: async (path, body, options2) => makeRequest("DELETE", path, body, options2)
|
|
86
|
+
};
|
|
87
|
+
for (const method of Object.keys(routes)) {
|
|
88
|
+
if (method in methodHandlers) {
|
|
89
|
+
client[method] = methodHandlers[method];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
95
92
|
return client;
|
|
96
93
|
};
|
|
97
|
-
var
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const data = {
|
|
104
|
-
params: req.params,
|
|
105
|
-
queryParams: routes.GET[x]?.queryParams?.parse(req.query)
|
|
106
|
-
};
|
|
107
|
-
const result = await handlers.GET[x]?.(data, ctx);
|
|
108
|
-
res.json(validation ? routes.GET[x]?.response.parse(result) : result);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
next(err);
|
|
94
|
+
var createRouteHandler = (method, routes, handlers, route, validation) => {
|
|
95
|
+
return async (req, res, next) => {
|
|
96
|
+
try {
|
|
97
|
+
if (method === "QUERY" && req.method !== "QUERY") {
|
|
98
|
+
res.status(405).send("Method Not Allowed");
|
|
99
|
+
return;
|
|
111
100
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
const ctx = handlers.ctx?.(req) ?? {};
|
|
102
|
+
const routeConfig = routes[method][route];
|
|
103
|
+
const data = {
|
|
104
|
+
params: req.params,
|
|
105
|
+
...routeConfig?.body && { body: routeConfig.body.parse(req.body) },
|
|
106
|
+
...routeConfig?.queryParams && {
|
|
107
|
+
queryParams: routeConfig.queryParams.parse(req.query)
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const result = await handlers[method][route]?.(data, ctx);
|
|
111
|
+
res.json(validation ? routeConfig?.response.parse(result) : result);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
next(err);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
var registerExpressRoutes = (router, routes, handlers) => {
|
|
118
|
+
const { schemaFilePath, validation = true } = handlers;
|
|
119
|
+
const expressMethodMap = {
|
|
120
|
+
GET: "get",
|
|
121
|
+
POST: "post",
|
|
122
|
+
PUT: "put",
|
|
123
|
+
PATCH: "patch",
|
|
124
|
+
DELETE: "delete",
|
|
125
|
+
QUERY: "all"
|
|
126
|
+
};
|
|
127
|
+
for (const [method, routerMethod] of Object.entries(expressMethodMap)) {
|
|
128
|
+
const methodKey = method;
|
|
129
|
+
const methodRoutes = routes[methodKey];
|
|
130
|
+
if (!methodRoutes) continue;
|
|
131
|
+
router = Object.keys(methodRoutes).reduce(
|
|
132
|
+
(r, route) => r[routerMethod](
|
|
133
|
+
route,
|
|
134
|
+
createRouteHandler(methodKey, routes, handlers, route, validation)
|
|
135
|
+
),
|
|
136
|
+
router
|
|
137
|
+
);
|
|
138
|
+
}
|
|
115
139
|
if (schemaFilePath) {
|
|
116
140
|
router = router.get(
|
|
117
141
|
"/__schema",
|
|
118
142
|
async (_, res) => res.contentType("text/plain").send(await promises.readFile(schemaFilePath, "utf8"))
|
|
119
143
|
);
|
|
120
144
|
}
|
|
121
|
-
router = Object.keys(routes.POST).reduce(
|
|
122
|
-
(r, x) => r.post(x, async (req, res, next) => {
|
|
123
|
-
try {
|
|
124
|
-
const ctx = handlers.ctx?.(req) ?? {};
|
|
125
|
-
const data = {
|
|
126
|
-
params: req.params,
|
|
127
|
-
body: routes.POST[x]?.body.parse(req.body),
|
|
128
|
-
queryParams: routes.POST[x]?.queryParams?.parse(req.query)
|
|
129
|
-
};
|
|
130
|
-
const result = await handlers.POST[x]?.(data, ctx);
|
|
131
|
-
res.json(validation ? routes.POST[x]?.response.parse(result) : result);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
next(err);
|
|
134
|
-
}
|
|
135
|
-
}),
|
|
136
|
-
router
|
|
137
|
-
);
|
|
138
145
|
return router;
|
|
139
146
|
};
|
|
140
147
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options","readFile"],"mappings":";;;;;AAyCO,IAAM,iBAAA,GAAoB,CAC/B,IAAA,EACA,MAAA,KACW;AACX,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAY;AACnC,EAAA,MAAM,YAAA,GAAe,WAAA;AACrB,EAAA,IAAI,KAAA;AAGJ,EAAA,OAAA,CAAQ,KAAA,GAAQ,YAAA,CAAa,IAAA,CAAK,IAAI,OAAO,IAAA,EAAM;AACjD,IAAA,UAAA,CAAW,GAAA,CAAI,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,EACzB;AAGA,EAAA,KAAA,MAAW,aAAa,UAAA,EAAY;AAClC,IAAA,IAAI,EAAE,aAAa,MAAA,CAAA,EAAS;AAC1B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,6BAAA,EAAgC,SAAS,CAAA,YAAA,EAAe,IAAI,CAAA,CAAA;AAAA,OAC9D;AAAA,IACF;AAAA,EACF;AAGA,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,CAAC,GAAG,SAAA,KAAc;AACjD,IAAA,OAAO,MAAA,CAAO,MAAA,CAAO,SAAS,CAAC,CAAA;AAAA,EACjC,CAAC,CAAA;AACH,CAAA;AAEO,IAAM,YAAA,GAAe,CAC1B,MAAA,EACA,OAAA,KACoB;AACpB,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,UAAA,GAAa,MAAM,OAAA,CAAQ,OAAA,CAAQ,EAAE,CAAA;AAAA,IACrC,OAAO,WAAA,GAAc,KAAA;AAAA,IACrB,QAAA,GAAW;AAAA,GACb,GAAI,OAAA;AAEJ,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,KAAK,EAAC;AAAA,IACN,MAAM;AAAC,GACT;AAEA,EAAA,MAAA,CAAO,GAAA,GAAM,OAAO,IAAA,EAAcA,QAAAA,KAAkB;AAClD,IAAA,IAAI,QAAA,IAAYA,UAAS,WAAA,EAAa;AACpC,MAAA,MAAA,CAAO,IAAI,IAAI,CAAA,EAAG,WAAA,EAAa,KAAA,CAAMA,SAAQ,WAAW,CAAA;AAAA,IAC1D;AAEA,IAAA,MAAM,WAAA,GAAcA,QAAAA,EAAS,WAAA,GACzB,GAAA,GAAM,IAAI,gBAAgBA,QAAAA,CAAQ,WAAW,CAAA,CAAE,QAAA,EAAS,GACxD,EAAA;AAEJ,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,GAC/B,iBAAA,CAAkB,IAAA,EAAMA,QAAAA,EAAS,MAAA,IAAU,EAAE,CAAA,GAC7C,IAAA;AAEJ,IAAA,MAAM,QAAA,GAAW,MAAM,WAAA,CAAY,CAAA,EAAG,OAAO,CAAA,EAAG,SAAS,CAAA,EAAG,WAAW,CAAA,CAAA,EAAI;AAAA,MACzE,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,GAAI,MAAM,UAAA;AAAW;AACvB,KACD,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,KAAA,GAAa,MAAM,QAAA,CAAS,IAAA,EAAK;AAEvC,MAAA,IAAIA,SAAQ,KAAA,EAAO;AACjB,QAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AAAA,MACrB;AAEA,MAAA,MAAM,IAAI,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA;AAAA,IAC/B;AAEA,IAAA,IAAI,OAAO,GAAA,CAAI,IAAI,CAAA,CAAE,QAAA,CAAS,SAAS,MAAA,EAAQ;AAC7C,MAAA,MAAM,SAAS,IAAA,EAAK;AACpB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAEjC,IAAA,OAAO,QAAA,GAAW,OAAO,GAAA,CAAI,IAAI,GAAG,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAAA,EAC7D,CAAA;AAEA,EAAA,MAAA,CAAO,IAAA,GAAO,OAAO,IAAA,EAAc,IAAA,EAAWA,QAAAA,KAAkB;AAC9D,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,IAAI,IAAA,EAAM;AACR,QAAA,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,IAAA,EAAM,MAAM,IAAI,CAAA;AAAA,MACrC;AACA,MAAA,IAAIA,UAAS,WAAA,EAAa;AACxB,QAAA,MAAA,CAAO,KAAK,IAAI,CAAA,EAAG,WAAA,EAAa,KAAA,CAAMA,SAAQ,WAAW,CAAA;AAAA,MAC3D;AAAA,IACF;AAEA,IAAA,MAAM,WAAA,GAAcA,QAAAA,EAAS,WAAA,GACzB,GAAA,GAAM,IAAI,gBAAgBA,QAAAA,CAAQ,WAAW,CAAA,CAAE,QAAA,EAAS,GACxD,EAAA;AAEJ,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,GAC/B,iBAAA,CAAkB,IAAA,EAAMA,QAAAA,EAAS,MAAA,IAAU,EAAE,CAAA,GAC7C,IAAA;AAEJ,IAAA,MAAM,QAAA,GAAW,MAAM,WAAA,CAAY,CAAA,EAAG,OAAO,CAAA,EAAG,SAAS,CAAA,EAAG,WAAW,CAAA,CAAA,EAAI;AAAA,MACzE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,GAAI,MAAM,UAAA;AAAW,OACvB;AAAA,MACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAAA,KAC1B,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,KAAA,GAAa,MAAM,QAAA,CAAS,IAAA,EAAK;AAEvC,MAAA,IAAIA,SAAQ,KAAA,EAAO;AACjB,QAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AAAA,MACrB;AAEA,MAAA,MAAM,IAAI,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA;AAAA,IAC/B;AAEA,IAAA,IAAI,OAAO,IAAA,CAAK,IAAI,CAAA,CAAE,QAAA,CAAS,SAAS,MAAA,EAAQ;AAC9C,MAAA,MAAM,SAAS,IAAA,EAAK;AACpB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAEjC,IAAA,OAAO,QAAA,GAAW,OAAO,IAAA,CAAK,IAAI,GAAG,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAAA,EAC9D,CAAA;AAEA,EAAA,OAAO,MAAA;AACT;AC/IO,IAAM,qBAAA,GAAwB,CACnC,MAAA,EACA,MAAA,EACA,QAAA,KAKG;AACH,EAAA,MAAM,EAAE,UAAA,GAAa,KAAA,EAAO,cAAA,EAAe,GAAI,QAAA;AAE/C,EAAA,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,GAAG,CAAA,CAAE,MAAA;AAAA,IAC/B,CAAC,GAAG,CAAA,KACF,CAAA,CAAE,IAAI,CAAA,EAAG,OAAO,GAAA,EAAK,GAAA,EAAK,IAAA,KAAS;AACjC,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAO,QAAA,CAAS,GAAA,GAAM,GAAG,KAAK,EAAC;AAErC,QAAA,MAAM,IAAA,GAAO;AAAA,UACX,QAAQ,GAAA,CAAI,MAAA;AAAA,UACZ,WAAA,EAAa,OAAO,GAAA,CAAI,CAAC,GAAG,WAAA,EAAa,KAAA,CAAM,IAAI,KAAK;AAAA,SAC1D;AACA,QAAA,MAAM,SAAS,MAAM,QAAA,CAAS,IAAI,CAAC,CAAA,GAAI,MAAa,GAAG,CAAA;AAEvD,QAAA,GAAA,CAAI,IAAA,CAAK,UAAA,GAAa,MAAA,CAAO,GAAA,CAAI,CAAC,GAAG,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA,GAAI,MAAM,CAAA;AAAA,MACtE,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,GAAG,CAAA;AAAA,MACV;AAAA,IACF,CAAC,CAAA;AAAA,IACH;AAAA,GACF;AAEA,EAAA,IAAI,cAAA,EAAgB;AAClB,IAAA,MAAA,GAAS,MAAA,CAAO,GAAA;AAAA,MAAI,WAAA;AAAA,MAAa,OAAO,CAAA,EAAG,GAAA,KACzC,GAAA,CACG,WAAA,CAAY,YAAY,CAAA,CACxB,IAAA,CAAK,MAAMC,iBAAA,CAAS,cAAA,EAAiB,MAAM,CAAC;AAAA,KACjD;AAAA,EACF;AAEA,EAAA,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,IAAI,CAAA,CAAE,MAAA;AAAA,IAChC,CAAC,GAAG,CAAA,KACF,CAAA,CAAE,KAAK,CAAA,EAAG,OAAO,GAAA,EAAK,GAAA,EAAK,IAAA,KAAS;AAClC,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAO,QAAA,CAAS,GAAA,GAAM,GAAG,KAAK,EAAC;AAErC,QAAA,MAAM,IAAA,GAAO;AAAA,UACX,QAAQ,GAAA,CAAI,MAAA;AAAA,UACZ,IAAA,EAAM,OAAO,IAAA,CAAK,CAAC,GAAG,IAAA,CAAK,KAAA,CAAM,IAAI,IAAI,CAAA;AAAA,UACzC,WAAA,EAAa,OAAO,IAAA,CAAK,CAAC,GAAG,WAAA,EAAa,KAAA,CAAM,IAAI,KAAK;AAAA,SAC3D;AACA,QAAA,MAAM,SAAS,MAAM,QAAA,CAAS,KAAK,CAAC,CAAA,GAAI,MAAa,GAAG,CAAA;AAExD,QAAA,GAAA,CAAI,IAAA,CAAK,UAAA,GAAa,MAAA,CAAO,IAAA,CAAK,CAAC,GAAG,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA,GAAI,MAAM,CAAA;AAAA,MACvE,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,GAAG,CAAA;AAAA,MACV;AAAA,IACF,CAAC,CAAA;AAAA,IACH;AAAA,GACF;AAEA,EAAA,OAAO,MAAA;AACT;;;ACzEO,IAAM,YAAA,GAAe,CAAyB,MAAA,KAAiB","file":"index.js","sourcesContent":["import type {\n ExtractRouteParams,\n InferRouteConfig,\n RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"response\"]>\n\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"response\"]>\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends RouterConfig>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = false,\n } = options\n\n const client = {\n GET: {} as any,\n POST: {} as any,\n }\n\n client.GET = async (path: string, options?: any) => {\n if (validate && options?.queryParams) {\n routes.GET[path]?.queryParams?.parse(options.queryParams)\n }\n\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n const response = await customFetch(`${baseUrl}${finalPath}${queryString}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n if (routes.GET[path].response.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate ? routes.GET[path]?.response.parse(json) : json\n }\n\n client.POST = async (path: string, body: any, options?: any) => {\n if (validate) {\n if (body) {\n routes.POST[path]?.body?.parse(body)\n }\n if (options?.queryParams) {\n routes.POST[path]?.queryParams?.parse(options.queryParams)\n }\n }\n\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n const response = await customFetch(`${baseUrl}${finalPath}${queryString}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n if (routes.POST[path].response.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate ? routes.POST[path]?.response.parse(json) : json\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport type {\n ExtractRouteParams,\n InferRouteConfig,\n RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: HandlerData<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[\"GET\"][K]>[\"response\"]>\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: HandlerData<InferRouteConfig<T[\"POST\"][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[\"POST\"][K]>[\"response\"]>\n }\n}\n\nexport const registerExpressRoutes = <T extends RouterConfig, TContext>(\n router: Router,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n validation?: boolean\n }\n) => {\n const { validation = false, schemaFilePath } = handlers\n\n router = Object.keys(routes.GET).reduce(\n (r, x) =>\n r.get(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n queryParams: routes.GET[x]?.queryParams?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n\n res.json(validation ? routes.GET[x]?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n if (schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(schemaFilePath!, \"utf8\"))\n )\n }\n\n router = Object.keys(routes.POST).reduce(\n (r, x) =>\n r.post(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n body: routes.POST[x]?.body.parse(req.body),\n queryParams: routes.POST[x]?.queryParams?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n\n res.json(validation ? routes.POST[x]?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n POST: Record<string, RouteConfig>\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends RouterConfig>(routes: T): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options","readFile"],"mappings":"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","file":"index.js","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n body: InferRouteConfig<T[M][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = true,\n } = options\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n if (!validate) return\n\n const routeConfig = (routes[method] as any)?.[path]\n if (body && routeConfig?.body) {\n routeConfig.body.parse(body)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n handleValidation(method, path, body, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (body !== undefined) {\n fetchOptions.body = JSON.stringify(body)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const client = {} as RouterClient<T>\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, body: any, options?: any) =>\n makeRequest(\"QUERY\", path, body, options),\n POST: async (path: any, body: any, options?: any) =>\n makeRequest(\"POST\", path, body, options),\n PUT: async (path: any, body: any, options?: any) =>\n makeRequest(\"PUT\", path, body, options),\n PATCH: async (path: any, body: any, options?: any) =>\n makeRequest(\"PATCH\", path, body, options),\n DELETE: async (path: any, body: any, options?: any) =>\n makeRequest(\"DELETE\", path, body, options),\n }\n\n for (const method of Object.keys(routes) as Array<\n keyof T & keyof RouterConfig\n >) {\n if (method in methodHandlers) {\n ;(client as any)[method] = methodHandlers[method]\n }\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"body\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>\n>(\n method: M,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n },\n route: string,\n validation: boolean\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n const routeConfig = (routes[method] as any)[route]\n\n const data = {\n params: req.params,\n ...(routeConfig?.body && { body: routeConfig.body.parse(req.body) }),\n ...(routeConfig?.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(validation ? routeConfig?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext\n>(\n router: Router,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n validation?: boolean\n }\n) => {\n const { schemaFilePath, validation = true } = handlers\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = routes[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(methodKey, routes, handlers, route, validation)\n ),\n router\n )\n }\n\n if (schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(schemaFilePath!, \"utf8\"))\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(\n routes: T\n): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -24,115 +24,122 @@ var createClient = (routes, options) => {
|
|
|
24
24
|
baseUrl,
|
|
25
25
|
getHeaders = () => Promise.resolve({}),
|
|
26
26
|
fetch: customFetch = fetch,
|
|
27
|
-
validate =
|
|
27
|
+
validate = true
|
|
28
28
|
} = options;
|
|
29
|
-
const
|
|
30
|
-
GET: {},
|
|
31
|
-
POST: {}
|
|
32
|
-
};
|
|
33
|
-
client.GET = async (path, options2) => {
|
|
34
|
-
if (validate && options2?.queryParams) {
|
|
35
|
-
routes.GET[path]?.queryParams?.parse(options2.queryParams);
|
|
36
|
-
}
|
|
29
|
+
const buildUrl = (path, options2) => {
|
|
37
30
|
const queryString = options2?.queryParams ? "?" + new URLSearchParams(options2.queryParams).toString() : "";
|
|
38
31
|
const finalPath = path.includes(":") ? replacePathParams(path, options2?.params ?? {}) : path;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
return `${baseUrl}${finalPath}${queryString}`;
|
|
33
|
+
};
|
|
34
|
+
const handleValidation = (method, path, body, options2) => {
|
|
35
|
+
if (!validate) return;
|
|
36
|
+
const routeConfig = routes[method]?.[path];
|
|
37
|
+
if (body && routeConfig?.body) {
|
|
38
|
+
routeConfig.body.parse(body);
|
|
39
|
+
}
|
|
40
|
+
if (options2?.queryParams && routeConfig?.queryParams) {
|
|
41
|
+
routeConfig.queryParams.parse(options2.queryParams);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const handleResponse = async (method, path, response, options2) => {
|
|
46
45
|
if (!response.ok) {
|
|
47
46
|
const error = await response.json();
|
|
48
|
-
if (options2
|
|
47
|
+
if (options2?.debug) {
|
|
49
48
|
console.debug(error);
|
|
50
49
|
}
|
|
51
50
|
throw new Error(error.message);
|
|
52
51
|
}
|
|
53
|
-
|
|
52
|
+
const routeConfig = routes[method]?.[path];
|
|
53
|
+
if (routeConfig?.response?.type === "void") {
|
|
54
54
|
await response.text();
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
const json = await response.json();
|
|
58
|
-
return validate ?
|
|
58
|
+
return validate && routeConfig?.response ? routeConfig.response.parse(json) : json;
|
|
59
59
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (options2?.queryParams) {
|
|
66
|
-
routes.POST[path]?.queryParams?.parse(options2.queryParams);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const queryString = options2?.queryParams ? "?" + new URLSearchParams(options2.queryParams).toString() : "";
|
|
70
|
-
const finalPath = path.includes(":") ? replacePathParams(path, options2?.params ?? {}) : path;
|
|
71
|
-
const response = await customFetch(`${baseUrl}${finalPath}${queryString}`, {
|
|
72
|
-
method: "POST",
|
|
60
|
+
const makeRequest = async (method, path, body, options2) => {
|
|
61
|
+
handleValidation(method, path, body, options2);
|
|
62
|
+
const url = buildUrl(path, options2);
|
|
63
|
+
const fetchOptions = {
|
|
64
|
+
method,
|
|
73
65
|
headers: {
|
|
74
66
|
"Content-Type": "application/json",
|
|
75
67
|
...await getHeaders()
|
|
76
|
-
},
|
|
77
|
-
body: JSON.stringify(body)
|
|
78
|
-
});
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
const error = await response.json();
|
|
81
|
-
if (options2.debug) {
|
|
82
|
-
console.debug(error);
|
|
83
68
|
}
|
|
84
|
-
|
|
69
|
+
};
|
|
70
|
+
if (body !== void 0) {
|
|
71
|
+
fetchOptions.body = JSON.stringify(body);
|
|
85
72
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const json = await response.json();
|
|
91
|
-
return validate ? routes.POST[path]?.response.parse(json) : json;
|
|
73
|
+
const response = await customFetch(url, fetchOptions);
|
|
74
|
+
return handleResponse(method, path, response, options2);
|
|
92
75
|
};
|
|
76
|
+
const client = {};
|
|
77
|
+
const methodHandlers = {
|
|
78
|
+
GET: async (path, options2) => makeRequest("GET", path, void 0, options2),
|
|
79
|
+
QUERY: async (path, body, options2) => makeRequest("QUERY", path, body, options2),
|
|
80
|
+
POST: async (path, body, options2) => makeRequest("POST", path, body, options2),
|
|
81
|
+
PUT: async (path, body, options2) => makeRequest("PUT", path, body, options2),
|
|
82
|
+
PATCH: async (path, body, options2) => makeRequest("PATCH", path, body, options2),
|
|
83
|
+
DELETE: async (path, body, options2) => makeRequest("DELETE", path, body, options2)
|
|
84
|
+
};
|
|
85
|
+
for (const method of Object.keys(routes)) {
|
|
86
|
+
if (method in methodHandlers) {
|
|
87
|
+
client[method] = methodHandlers[method];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
93
90
|
return client;
|
|
94
91
|
};
|
|
95
|
-
var
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const data = {
|
|
102
|
-
params: req.params,
|
|
103
|
-
queryParams: routes.GET[x]?.queryParams?.parse(req.query)
|
|
104
|
-
};
|
|
105
|
-
const result = await handlers.GET[x]?.(data, ctx);
|
|
106
|
-
res.json(validation ? routes.GET[x]?.response.parse(result) : result);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
next(err);
|
|
92
|
+
var createRouteHandler = (method, routes, handlers, route, validation) => {
|
|
93
|
+
return async (req, res, next) => {
|
|
94
|
+
try {
|
|
95
|
+
if (method === "QUERY" && req.method !== "QUERY") {
|
|
96
|
+
res.status(405).send("Method Not Allowed");
|
|
97
|
+
return;
|
|
109
98
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
const ctx = handlers.ctx?.(req) ?? {};
|
|
100
|
+
const routeConfig = routes[method][route];
|
|
101
|
+
const data = {
|
|
102
|
+
params: req.params,
|
|
103
|
+
...routeConfig?.body && { body: routeConfig.body.parse(req.body) },
|
|
104
|
+
...routeConfig?.queryParams && {
|
|
105
|
+
queryParams: routeConfig.queryParams.parse(req.query)
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const result = await handlers[method][route]?.(data, ctx);
|
|
109
|
+
res.json(validation ? routeConfig?.response.parse(result) : result);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
next(err);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
var registerExpressRoutes = (router, routes, handlers) => {
|
|
116
|
+
const { schemaFilePath, validation = true } = handlers;
|
|
117
|
+
const expressMethodMap = {
|
|
118
|
+
GET: "get",
|
|
119
|
+
POST: "post",
|
|
120
|
+
PUT: "put",
|
|
121
|
+
PATCH: "patch",
|
|
122
|
+
DELETE: "delete",
|
|
123
|
+
QUERY: "all"
|
|
124
|
+
};
|
|
125
|
+
for (const [method, routerMethod] of Object.entries(expressMethodMap)) {
|
|
126
|
+
const methodKey = method;
|
|
127
|
+
const methodRoutes = routes[methodKey];
|
|
128
|
+
if (!methodRoutes) continue;
|
|
129
|
+
router = Object.keys(methodRoutes).reduce(
|
|
130
|
+
(r, route) => r[routerMethod](
|
|
131
|
+
route,
|
|
132
|
+
createRouteHandler(methodKey, routes, handlers, route, validation)
|
|
133
|
+
),
|
|
134
|
+
router
|
|
135
|
+
);
|
|
136
|
+
}
|
|
113
137
|
if (schemaFilePath) {
|
|
114
138
|
router = router.get(
|
|
115
139
|
"/__schema",
|
|
116
140
|
async (_, res) => res.contentType("text/plain").send(await readFile(schemaFilePath, "utf8"))
|
|
117
141
|
);
|
|
118
142
|
}
|
|
119
|
-
router = Object.keys(routes.POST).reduce(
|
|
120
|
-
(r, x) => r.post(x, async (req, res, next) => {
|
|
121
|
-
try {
|
|
122
|
-
const ctx = handlers.ctx?.(req) ?? {};
|
|
123
|
-
const data = {
|
|
124
|
-
params: req.params,
|
|
125
|
-
body: routes.POST[x]?.body.parse(req.body),
|
|
126
|
-
queryParams: routes.POST[x]?.queryParams?.parse(req.query)
|
|
127
|
-
};
|
|
128
|
-
const result = await handlers.POST[x]?.(data, ctx);
|
|
129
|
-
res.json(validation ? routes.POST[x]?.response.parse(result) : result);
|
|
130
|
-
} catch (err) {
|
|
131
|
-
next(err);
|
|
132
|
-
}
|
|
133
|
-
}),
|
|
134
|
-
router
|
|
135
|
-
);
|
|
136
143
|
return router;
|
|
137
144
|
};
|
|
138
145
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.mjs","sourcesContent":["import type {\n ExtractRouteParams,\n InferRouteConfig,\n RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"response\"]>\n\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"response\"]>\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends RouterConfig>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = false,\n } = options\n\n const client = {\n GET: {} as any,\n POST: {} as any,\n }\n\n client.GET = async (path: string, options?: any) => {\n if (validate && options?.queryParams) {\n routes.GET[path]?.queryParams?.parse(options.queryParams)\n }\n\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n const response = await customFetch(`${baseUrl}${finalPath}${queryString}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n if (routes.GET[path].response.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate ? routes.GET[path]?.response.parse(json) : json\n }\n\n client.POST = async (path: string, body: any, options?: any) => {\n if (validate) {\n if (body) {\n routes.POST[path]?.body?.parse(body)\n }\n if (options?.queryParams) {\n routes.POST[path]?.queryParams?.parse(options.queryParams)\n }\n }\n\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n const response = await customFetch(`${baseUrl}${finalPath}${queryString}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n if (routes.POST[path].response.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate ? routes.POST[path]?.response.parse(json) : json\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport type {\n ExtractRouteParams,\n InferRouteConfig,\n RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: HandlerData<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[\"GET\"][K]>[\"response\"]>\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: HandlerData<InferRouteConfig<T[\"POST\"][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[\"POST\"][K]>[\"response\"]>\n }\n}\n\nexport const registerExpressRoutes = <T extends RouterConfig, TContext>(\n router: Router,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n validation?: boolean\n }\n) => {\n const { validation = false, schemaFilePath } = handlers\n\n router = Object.keys(routes.GET).reduce(\n (r, x) =>\n r.get(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n queryParams: routes.GET[x]?.queryParams?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n\n res.json(validation ? routes.GET[x]?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n if (schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(schemaFilePath!, \"utf8\"))\n )\n }\n\n router = Object.keys(routes.POST).reduce(\n (r, x) =>\n r.post(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n body: routes.POST[x]?.body.parse(req.body),\n queryParams: routes.POST[x]?.queryParams?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n\n res.json(validation ? routes.POST[x]?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n POST: Record<string, RouteConfig>\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends RouterConfig>(routes: T): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.mjs","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n body: InferRouteConfig<T[M][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = true,\n } = options\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n if (!validate) return\n\n const routeConfig = (routes[method] as any)?.[path]\n if (body && routeConfig?.body) {\n routeConfig.body.parse(body)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n handleValidation(method, path, body, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (body !== undefined) {\n fetchOptions.body = JSON.stringify(body)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const client = {} as RouterClient<T>\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, body: any, options?: any) =>\n makeRequest(\"QUERY\", path, body, options),\n POST: async (path: any, body: any, options?: any) =>\n makeRequest(\"POST\", path, body, options),\n PUT: async (path: any, body: any, options?: any) =>\n makeRequest(\"PUT\", path, body, options),\n PATCH: async (path: any, body: any, options?: any) =>\n makeRequest(\"PATCH\", path, body, options),\n DELETE: async (path: any, body: any, options?: any) =>\n makeRequest(\"DELETE\", path, body, options),\n }\n\n for (const method of Object.keys(routes) as Array<\n keyof T & keyof RouterConfig\n >) {\n if (method in methodHandlers) {\n ;(client as any)[method] = methodHandlers[method]\n }\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"body\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>\n>(\n method: M,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n },\n route: string,\n validation: boolean\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n const routeConfig = (routes[method] as any)[route]\n\n const data = {\n params: req.params,\n ...(routeConfig?.body && { body: routeConfig.body.parse(req.body) }),\n ...(routeConfig?.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(validation ? routeConfig?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext\n>(\n router: Router,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n validation?: boolean\n }\n) => {\n const { schemaFilePath, validation = true } = handlers\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = routes[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(methodKey, routes, handlers, route, validation)\n ),\n router\n )\n }\n\n if (schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(schemaFilePath!, \"utf8\"))\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(\n routes: T\n): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
|