@jokio/rpc 0.3.2 → 0.4.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 +34 -47
- package/dist/index.d.mts +8 -7
- package/dist/index.d.ts +8 -7
- package/dist/index.js +34 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +33 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@ A type-safe RPC framework for TypeScript with Zod validation, designed for Expre
|
|
|
4
4
|
|
|
5
5
|
<img width="400" height="400" alt="ChatGPT Image Jan 4, 2026 at 10_15_01 AM" src="https://github.com/user-attachments/assets/5ca6462a-4d3a-46d6-ac09-31ecbc4d06fb" />
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
## Features
|
|
9
8
|
|
|
10
9
|
- Full TypeScript type safety from server to client
|
|
@@ -23,18 +22,15 @@ npm install @jokio/rpc
|
|
|
23
22
|
|
|
24
23
|
## Usage
|
|
25
24
|
|
|
26
|
-
### 1. Define Your
|
|
25
|
+
### 1. Define Your Routes
|
|
27
26
|
|
|
28
27
|
```typescript
|
|
29
|
-
import {
|
|
28
|
+
import { defineRoutes } from "@jokio/rpc"
|
|
30
29
|
import { z } from "zod"
|
|
31
30
|
|
|
32
|
-
const
|
|
31
|
+
const routes = defineRoutes({
|
|
33
32
|
GET: {
|
|
34
|
-
"/
|
|
35
|
-
query: z.object({
|
|
36
|
-
include: z.enum(["posts", "comments"]).optional(),
|
|
37
|
-
}),
|
|
33
|
+
"/user/:id": {
|
|
38
34
|
result: z.object({
|
|
39
35
|
id: z.string(),
|
|
40
36
|
name: z.string(),
|
|
@@ -43,14 +39,11 @@ const routerConfig = defineRouterConfig({
|
|
|
43
39
|
},
|
|
44
40
|
},
|
|
45
41
|
POST: {
|
|
46
|
-
"/
|
|
42
|
+
"/user": {
|
|
47
43
|
body: z.object({
|
|
48
44
|
name: z.string(),
|
|
49
45
|
email: z.string().email(),
|
|
50
46
|
}),
|
|
51
|
-
query: z.object({
|
|
52
|
-
sendEmail: z.boolean().optional(),
|
|
53
|
-
}),
|
|
54
47
|
result: z.object({
|
|
55
48
|
id: z.string(),
|
|
56
49
|
name: z.string(),
|
|
@@ -65,34 +58,25 @@ const routerConfig = defineRouterConfig({
|
|
|
65
58
|
|
|
66
59
|
```typescript
|
|
67
60
|
import express from "express"
|
|
68
|
-
import {
|
|
61
|
+
import { registerExpressRoutes } from "@jokio/rpc"
|
|
69
62
|
|
|
70
63
|
const app = express()
|
|
71
64
|
app.use(express.json())
|
|
72
65
|
|
|
73
66
|
const router = express.Router()
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
// Optional: Define a context factory function
|
|
77
|
-
ctx: (req) => ({
|
|
78
|
-
userId: req.headers["x-user-id"] as string,
|
|
79
|
-
// Add other context properties here
|
|
80
|
-
}),
|
|
68
|
+
registerExpressRoutes(router, routes, {
|
|
81
69
|
GET: {
|
|
82
|
-
"/
|
|
83
|
-
// Handler implementation with context
|
|
84
|
-
console.log("Current user:", ctx.userId)
|
|
70
|
+
"/user/:id": async ({ params }) => {
|
|
85
71
|
return {
|
|
86
|
-
id:
|
|
72
|
+
id: params.id,
|
|
87
73
|
name: "John Doe",
|
|
88
74
|
email: "john@example.com",
|
|
89
75
|
}
|
|
90
76
|
},
|
|
91
77
|
},
|
|
92
78
|
POST: {
|
|
93
|
-
"/
|
|
94
|
-
// Handler implementation with context
|
|
95
|
-
console.log("Creating user, requested by:", ctx.userId)
|
|
79
|
+
"/user": async ({ body }) => {
|
|
96
80
|
return {
|
|
97
81
|
id: "2",
|
|
98
82
|
name: body.name,
|
|
@@ -111,52 +95,55 @@ app.listen(3000)
|
|
|
111
95
|
```typescript
|
|
112
96
|
import { createClient } from "@jokio/rpc"
|
|
113
97
|
|
|
114
|
-
const client = createClient(
|
|
98
|
+
const client = createClient(routes, {
|
|
115
99
|
baseUrl: "http://localhost:3000/api",
|
|
116
|
-
|
|
100
|
+
validate: true, // Optional: validate requests on client-side
|
|
117
101
|
})
|
|
118
102
|
|
|
119
103
|
// Fully typed API calls
|
|
120
|
-
const user = await client.GET("/users/23"
|
|
121
|
-
query: { include: "posts" },
|
|
122
|
-
})
|
|
104
|
+
const user = await client.GET("/users/23")
|
|
123
105
|
|
|
124
|
-
const newUser = await client.POST(
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
106
|
+
const newUser = await client.POST("/users", {
|
|
107
|
+
name: "Jane Doe",
|
|
108
|
+
email: "jane@example.com",
|
|
109
|
+
})
|
|
129
110
|
```
|
|
130
111
|
|
|
131
112
|
## API Reference
|
|
132
113
|
|
|
133
|
-
### `
|
|
114
|
+
### `defineRoutes(routes)`
|
|
134
115
|
|
|
135
|
-
Helper function to define
|
|
116
|
+
Helper function to define routes with type inference.
|
|
136
117
|
|
|
137
|
-
|
|
118
|
+
**Parameters:**
|
|
119
|
+
|
|
120
|
+
- `routes`: Route definitions object containing GET and POST route configurations
|
|
138
121
|
|
|
139
|
-
|
|
122
|
+
### `registerExpressRoutes(router, routes, handlers)`
|
|
123
|
+
|
|
124
|
+
Registers route handlers to an Express router with automatic validation.
|
|
140
125
|
|
|
141
126
|
**Parameters:**
|
|
142
127
|
|
|
143
128
|
- `router`: Express Router instance
|
|
144
|
-
- `
|
|
129
|
+
- `routes`: Route definitions object
|
|
145
130
|
- `handlers`: Handler functions for each route with optional context factory
|
|
146
131
|
- `ctx`: Optional function `(req: Request) => TContext` to provide context to handlers
|
|
147
132
|
- `GET`: Handler functions that receive `(data, ctx)` parameters
|
|
148
133
|
- `POST`: Handler functions that receive `(data, ctx)` parameters
|
|
149
134
|
|
|
150
|
-
### `createClient(
|
|
135
|
+
### `createClient(routes, options)`
|
|
151
136
|
|
|
152
137
|
Creates a type-safe HTTP client.
|
|
153
138
|
|
|
154
|
-
**
|
|
139
|
+
**Parameters:**
|
|
155
140
|
|
|
156
|
-
- `
|
|
157
|
-
- `
|
|
158
|
-
- `
|
|
159
|
-
- `
|
|
141
|
+
- `routes`: Route definitions object (same as used on the server)
|
|
142
|
+
- `options`: Client configuration options
|
|
143
|
+
- `baseUrl`: Base URL for API requests
|
|
144
|
+
- `getHeaders`: Optional function that returns headers (sync or async)
|
|
145
|
+
- `fetch`: Optional custom fetch function (useful for Node.js or testing)
|
|
146
|
+
- `validate`: Enable client-side request validation (default: false)
|
|
160
147
|
|
|
161
148
|
## Type Safety
|
|
162
149
|
|
package/dist/index.d.mts
CHANGED
|
@@ -13,7 +13,7 @@ type RouterConfig = {
|
|
|
13
13
|
type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "body">> = {
|
|
14
14
|
[K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never;
|
|
15
15
|
};
|
|
16
|
-
declare const
|
|
16
|
+
declare const defineRoutes: <T extends RouterConfig>(routes: T) => T;
|
|
17
17
|
|
|
18
18
|
type RouterClient<T extends RouterConfig> = {
|
|
19
19
|
GET: <K extends keyof T["GET"]>(path: K, options?: Omit<Omit<InferRouteConfig<T["GET"][K]>, "body">, "result">) => Promise<InferRouteConfig<T["GET"][K]>["result"]>;
|
|
@@ -22,18 +22,19 @@ type RouterClient<T extends RouterConfig> = {
|
|
|
22
22
|
type FetchFunction = (url: string, options: RequestInit) => Promise<Response>;
|
|
23
23
|
type CreateClientOptions = {
|
|
24
24
|
baseUrl: string;
|
|
25
|
-
|
|
25
|
+
getHeaders?: () => Promise<Record<string, string>> | Record<string, string>;
|
|
26
26
|
fetch?: FetchFunction;
|
|
27
|
-
|
|
27
|
+
validate?: boolean;
|
|
28
|
+
debug?: boolean;
|
|
28
29
|
};
|
|
29
|
-
declare const createClient: <T extends RouterConfig>(
|
|
30
|
+
declare const createClient: <T extends RouterConfig>(routes: T, options: CreateClientOptions) => RouterClient<T>;
|
|
30
31
|
|
|
31
32
|
type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? {
|
|
32
33
|
[K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string;
|
|
33
34
|
} : T extends `${infer _Start}:${infer Param}` ? {
|
|
34
35
|
[K in Param]: string;
|
|
35
36
|
} : Record<string, never>;
|
|
36
|
-
type
|
|
37
|
+
type RouteHandlers<T extends RouterConfig, TContext> = {
|
|
37
38
|
GET: {
|
|
38
39
|
[K in keyof T["GET"]]: (data: Omit<Omit<InferRouteConfig<T["GET"][K]>, "body">, "result"> & {
|
|
39
40
|
params: K extends string ? ExtractRouteParams<K> : unknown;
|
|
@@ -45,9 +46,9 @@ type RouterHandlerConfig<T extends RouterConfig, TContext> = {
|
|
|
45
46
|
}, ctx: TContext) => Promise<InferRouteConfig<T["POST"][K]>["result"]> | InferRouteConfig<T["POST"][K]>["result"];
|
|
46
47
|
};
|
|
47
48
|
};
|
|
48
|
-
declare const
|
|
49
|
+
declare const registerExpressRoutes: <T extends RouterConfig, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
|
|
49
50
|
ctx?: (req: Request) => TContext;
|
|
50
51
|
schemaFilePath?: string;
|
|
51
52
|
}) => Router;
|
|
52
53
|
|
|
53
|
-
export { type InferRouteConfig, type RouteConfig, type
|
|
54
|
+
export { type InferRouteConfig, type RouteConfig, type RouteHandlers, type RouterClient, type RouterConfig, createClient, defineRoutes, registerExpressRoutes };
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ type RouterConfig = {
|
|
|
13
13
|
type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "body">> = {
|
|
14
14
|
[K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never;
|
|
15
15
|
};
|
|
16
|
-
declare const
|
|
16
|
+
declare const defineRoutes: <T extends RouterConfig>(routes: T) => T;
|
|
17
17
|
|
|
18
18
|
type RouterClient<T extends RouterConfig> = {
|
|
19
19
|
GET: <K extends keyof T["GET"]>(path: K, options?: Omit<Omit<InferRouteConfig<T["GET"][K]>, "body">, "result">) => Promise<InferRouteConfig<T["GET"][K]>["result"]>;
|
|
@@ -22,18 +22,19 @@ type RouterClient<T extends RouterConfig> = {
|
|
|
22
22
|
type FetchFunction = (url: string, options: RequestInit) => Promise<Response>;
|
|
23
23
|
type CreateClientOptions = {
|
|
24
24
|
baseUrl: string;
|
|
25
|
-
|
|
25
|
+
getHeaders?: () => Promise<Record<string, string>> | Record<string, string>;
|
|
26
26
|
fetch?: FetchFunction;
|
|
27
|
-
|
|
27
|
+
validate?: boolean;
|
|
28
|
+
debug?: boolean;
|
|
28
29
|
};
|
|
29
|
-
declare const createClient: <T extends RouterConfig>(
|
|
30
|
+
declare const createClient: <T extends RouterConfig>(routes: T, options: CreateClientOptions) => RouterClient<T>;
|
|
30
31
|
|
|
31
32
|
type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? {
|
|
32
33
|
[K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string;
|
|
33
34
|
} : T extends `${infer _Start}:${infer Param}` ? {
|
|
34
35
|
[K in Param]: string;
|
|
35
36
|
} : Record<string, never>;
|
|
36
|
-
type
|
|
37
|
+
type RouteHandlers<T extends RouterConfig, TContext> = {
|
|
37
38
|
GET: {
|
|
38
39
|
[K in keyof T["GET"]]: (data: Omit<Omit<InferRouteConfig<T["GET"][K]>, "body">, "result"> & {
|
|
39
40
|
params: K extends string ? ExtractRouteParams<K> : unknown;
|
|
@@ -45,9 +46,9 @@ type RouterHandlerConfig<T extends RouterConfig, TContext> = {
|
|
|
45
46
|
}, ctx: TContext) => Promise<InferRouteConfig<T["POST"][K]>["result"]> | InferRouteConfig<T["POST"][K]>["result"];
|
|
46
47
|
};
|
|
47
48
|
};
|
|
48
|
-
declare const
|
|
49
|
+
declare const registerExpressRoutes: <T extends RouterConfig, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
|
|
49
50
|
ctx?: (req: Request) => TContext;
|
|
50
51
|
schemaFilePath?: string;
|
|
51
52
|
}) => Router;
|
|
52
53
|
|
|
53
|
-
export { type InferRouteConfig, type RouteConfig, type
|
|
54
|
+
export { type InferRouteConfig, type RouteConfig, type RouteHandlers, type RouterClient, type RouterConfig, createClient, defineRoutes, registerExpressRoutes };
|
package/dist/index.js
CHANGED
|
@@ -3,76 +3,80 @@
|
|
|
3
3
|
var promises = require('fs/promises');
|
|
4
4
|
|
|
5
5
|
// src/client.ts
|
|
6
|
-
var createClient = (
|
|
6
|
+
var createClient = (routes, options) => {
|
|
7
7
|
const {
|
|
8
8
|
baseUrl,
|
|
9
|
-
|
|
9
|
+
getHeaders = () => Promise.resolve({}),
|
|
10
10
|
fetch: customFetch = fetch,
|
|
11
|
-
|
|
11
|
+
validate = false
|
|
12
12
|
} = options;
|
|
13
13
|
const client = {
|
|
14
14
|
GET: {},
|
|
15
15
|
POST: {}
|
|
16
16
|
};
|
|
17
|
-
client.GET = async (path,
|
|
18
|
-
if (
|
|
19
|
-
|
|
17
|
+
client.GET = async (path, options2) => {
|
|
18
|
+
if (validate && options2?.query) {
|
|
19
|
+
routes.GET[path]?.query?.parse(options2.query);
|
|
20
20
|
}
|
|
21
|
-
const queryString =
|
|
21
|
+
const queryString = options2?.query ? "?" + new URLSearchParams(options2.query).toString() : "";
|
|
22
22
|
const response = await customFetch(`${baseUrl}${path}${queryString}`, {
|
|
23
23
|
method: "GET",
|
|
24
24
|
headers: {
|
|
25
25
|
"Content-Type": "application/json",
|
|
26
|
-
...
|
|
26
|
+
...await getHeaders()
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
if (!response.ok) {
|
|
30
30
|
const error = await response.json();
|
|
31
|
-
|
|
31
|
+
if (options2.debug) {
|
|
32
|
+
console.debug(error);
|
|
33
|
+
}
|
|
32
34
|
throw new Error(error.message);
|
|
33
35
|
}
|
|
34
36
|
const json = await response.json();
|
|
35
|
-
return
|
|
37
|
+
return routes.GET[path]?.result.parse(json);
|
|
36
38
|
};
|
|
37
|
-
client.POST = async (path, body,
|
|
38
|
-
if (
|
|
39
|
+
client.POST = async (path, body, options2) => {
|
|
40
|
+
if (validate) {
|
|
39
41
|
if (body) {
|
|
40
|
-
|
|
42
|
+
routes.POST[path]?.body?.parse(body);
|
|
41
43
|
}
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
+
if (options2?.query) {
|
|
45
|
+
routes.POST[path]?.query?.parse(options2.query);
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
const queryString =
|
|
48
|
+
const queryString = options2?.query ? "?" + new URLSearchParams(options2.query).toString() : "";
|
|
47
49
|
const response = await customFetch(`${baseUrl}${path}${queryString}`, {
|
|
48
50
|
method: "POST",
|
|
49
51
|
headers: {
|
|
50
52
|
"Content-Type": "application/json",
|
|
51
|
-
...
|
|
53
|
+
...await getHeaders()
|
|
52
54
|
},
|
|
53
55
|
body: JSON.stringify(body)
|
|
54
56
|
});
|
|
55
57
|
if (!response.ok) {
|
|
56
58
|
const error = await response.json();
|
|
57
|
-
|
|
59
|
+
if (options2.debug) {
|
|
60
|
+
console.debug(error);
|
|
61
|
+
}
|
|
58
62
|
throw new Error(error.message);
|
|
59
63
|
}
|
|
60
64
|
const json = await response.json();
|
|
61
|
-
return
|
|
65
|
+
return routes.POST[path]?.result.parse(json);
|
|
62
66
|
};
|
|
63
67
|
return client;
|
|
64
68
|
};
|
|
65
|
-
var
|
|
66
|
-
router = Object.keys(
|
|
69
|
+
var registerExpressRoutes = (router, routes, handlers) => {
|
|
70
|
+
router = Object.keys(routes.GET).reduce(
|
|
67
71
|
(r, x) => r.get(x, async (req, res, next) => {
|
|
68
72
|
try {
|
|
69
73
|
const ctx = handlers.ctx?.(req) ?? {};
|
|
70
74
|
const data = {
|
|
71
75
|
params: req.params,
|
|
72
|
-
query:
|
|
76
|
+
query: routes.GET[x]?.query?.parse(req.query)
|
|
73
77
|
};
|
|
74
78
|
const result = await handlers.GET[x]?.(data, ctx);
|
|
75
|
-
const validatedResult =
|
|
79
|
+
const validatedResult = routes.GET[x]?.result.parse(result);
|
|
76
80
|
res.json(validatedResult);
|
|
77
81
|
} catch (err) {
|
|
78
82
|
next(err);
|
|
@@ -86,17 +90,17 @@ var applyConfigToExpressRouter = (router, schema, handlers) => {
|
|
|
86
90
|
async (_, res) => res.contentType("text/plain").send(await promises.readFile(handlers.schemaFilePath, "utf8"))
|
|
87
91
|
);
|
|
88
92
|
}
|
|
89
|
-
router = Object.keys(
|
|
93
|
+
router = Object.keys(routes.POST).reduce(
|
|
90
94
|
(r, x) => r.post(x, async (req, res, next) => {
|
|
91
95
|
try {
|
|
92
96
|
const ctx = handlers.ctx?.(req) ?? {};
|
|
93
97
|
const data = {
|
|
94
98
|
params: req.params,
|
|
95
|
-
body:
|
|
96
|
-
query:
|
|
99
|
+
body: routes.POST[x]?.body.parse(req.body),
|
|
100
|
+
query: routes.POST[x]?.query?.parse(req.query)
|
|
97
101
|
};
|
|
98
102
|
const result = await handlers.POST[x]?.(data, ctx);
|
|
99
|
-
const validatedResult =
|
|
103
|
+
const validatedResult = routes.POST[x]?.result.parse(result);
|
|
100
104
|
res.json(validatedResult);
|
|
101
105
|
} catch (err) {
|
|
102
106
|
next(err);
|
|
@@ -108,10 +112,10 @@ var applyConfigToExpressRouter = (router, schema, handlers) => {
|
|
|
108
112
|
};
|
|
109
113
|
|
|
110
114
|
// src/types.ts
|
|
111
|
-
var
|
|
115
|
+
var defineRoutes = (routes) => routes;
|
|
112
116
|
|
|
113
|
-
exports.applyConfigToExpressRouter = applyConfigToExpressRouter;
|
|
114
117
|
exports.createClient = createClient;
|
|
115
|
-
exports.
|
|
118
|
+
exports.defineRoutes = defineRoutes;
|
|
119
|
+
exports.registerExpressRoutes = registerExpressRoutes;
|
|
116
120
|
//# sourceMappingURL=index.js.map
|
|
117
121
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["readFile"],"mappings":"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","file":"index.js","sourcesContent":["import type { InferRouteConfig, RouterConfig } from \"./types\"\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\">\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\" | \"result\">\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n headers?: Record<string, string>\n fetch?: FetchFunction\n validateRequest?: boolean\n}\n\nexport const createClient = <T extends RouterConfig>(\n config: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n headers = {},\n fetch: customFetch = fetch,\n validateRequest = false,\n } = options\n\n const client = {\n GET: {} as any,\n POST: {} as any,\n }\n\n client.GET = async (path: string, data?: any) => {\n if (validateRequest && data?.query) {\n config.GET[path]?.query?.parse(data.query)\n }\n const queryString = data?.query\n ? \"?\" + new URLSearchParams(data.query).toString()\n : \"\"\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n console.error(error)\n throw new Error(error.message)\n }\n\n const json = await response.json()\n\n return config.GET[path]?.result.parse(json)\n }\n\n client.POST = async (path: string, body: any, rest?: any) => {\n if (validateRequest) {\n if (body) {\n config.POST[path]?.body?.parse(body)\n }\n if (rest?.query) {\n config.POST[path]?.query?.parse(rest.query)\n }\n }\n const queryString = rest?.query\n ? \"?\" + new URLSearchParams(rest.query).toString()\n : \"\"\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n console.error(error)\n throw new Error(error.message)\n }\n\n const json = await response.json()\n\n return config.POST[path]?.result.parse(json)\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport type { InferRouteConfig, RouterConfig } from \"./types\"\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\ntype ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n\nexport type RouterHandlerConfig<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"GET\"][K]>[\"result\"]\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: Omit<InferRouteConfig<T[\"POST\"][K]>, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"POST\"][K]>[\"result\"]\n }\n}\n\nexport const applyConfigToExpressRouter = <T extends RouterConfig, TContext>(\n router: Router,\n schema: T,\n handlers: RouterHandlerConfig<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n }\n) => {\n router = Object.keys(schema.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 query: schema.GET[x]?.query?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n const validatedResult = schema.GET[x]?.result.parse(result)\n\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n if (handlers.schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(handlers.schemaFilePath!, \"utf8\"))\n )\n }\n\n router = Object.keys(schema.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: schema.POST[x]?.body.parse(req.body),\n query: schema.POST[x]?.query?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n const validatedResult = schema.POST[x]?.result.parse(result)\n res.json(validatedResult)\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 query?: z.ZodType\n result: 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 defineRouterSchema = <T extends RouterConfig>(config: T): T =>\n config\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 type { InferRouteConfig, RouterConfig } from \"./types\"\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\">\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\" | \"result\">\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\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\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?.query) {\n routes.GET[path]?.query?.parse(options.query)\n }\n\n const queryString = options?.query\n ? \"?\" + new URLSearchParams(options.query).toString()\n : \"\"\n\n const response = await customFetch(`${baseUrl}${path}${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 const json = await response.json()\n\n return routes.GET[path]?.result.parse(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?.query) {\n routes.POST[path]?.query?.parse(options.query)\n }\n }\n\n const queryString = options?.query\n ? \"?\" + new URLSearchParams(options.query).toString()\n : \"\"\n\n const response = await customFetch(`${baseUrl}${path}${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 const json = await response.json()\n\n return routes.POST[path]?.result.parse(json)\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport type { InferRouteConfig, RouterConfig } from \"./types\"\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\ntype ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n\nexport type RouteHandlers<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"GET\"][K]>[\"result\"]\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: Omit<InferRouteConfig<T[\"POST\"][K]>, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"POST\"][K]>[\"result\"]\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 }\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 query: routes.GET[x]?.query?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n const validatedResult = routes.GET[x]?.result.parse(result)\n\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n if (handlers.schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(handlers.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 query: routes.POST[x]?.query?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n const validatedResult = routes.POST[x]?.result.parse(result)\n res.json(validatedResult)\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 query?: z.ZodType\n result: 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 =>\n routes\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,76 +1,80 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
|
|
3
3
|
// src/client.ts
|
|
4
|
-
var createClient = (
|
|
4
|
+
var createClient = (routes, options) => {
|
|
5
5
|
const {
|
|
6
6
|
baseUrl,
|
|
7
|
-
|
|
7
|
+
getHeaders = () => Promise.resolve({}),
|
|
8
8
|
fetch: customFetch = fetch,
|
|
9
|
-
|
|
9
|
+
validate = false
|
|
10
10
|
} = options;
|
|
11
11
|
const client = {
|
|
12
12
|
GET: {},
|
|
13
13
|
POST: {}
|
|
14
14
|
};
|
|
15
|
-
client.GET = async (path,
|
|
16
|
-
if (
|
|
17
|
-
|
|
15
|
+
client.GET = async (path, options2) => {
|
|
16
|
+
if (validate && options2?.query) {
|
|
17
|
+
routes.GET[path]?.query?.parse(options2.query);
|
|
18
18
|
}
|
|
19
|
-
const queryString =
|
|
19
|
+
const queryString = options2?.query ? "?" + new URLSearchParams(options2.query).toString() : "";
|
|
20
20
|
const response = await customFetch(`${baseUrl}${path}${queryString}`, {
|
|
21
21
|
method: "GET",
|
|
22
22
|
headers: {
|
|
23
23
|
"Content-Type": "application/json",
|
|
24
|
-
...
|
|
24
|
+
...await getHeaders()
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
if (!response.ok) {
|
|
28
28
|
const error = await response.json();
|
|
29
|
-
|
|
29
|
+
if (options2.debug) {
|
|
30
|
+
console.debug(error);
|
|
31
|
+
}
|
|
30
32
|
throw new Error(error.message);
|
|
31
33
|
}
|
|
32
34
|
const json = await response.json();
|
|
33
|
-
return
|
|
35
|
+
return routes.GET[path]?.result.parse(json);
|
|
34
36
|
};
|
|
35
|
-
client.POST = async (path, body,
|
|
36
|
-
if (
|
|
37
|
+
client.POST = async (path, body, options2) => {
|
|
38
|
+
if (validate) {
|
|
37
39
|
if (body) {
|
|
38
|
-
|
|
40
|
+
routes.POST[path]?.body?.parse(body);
|
|
39
41
|
}
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
+
if (options2?.query) {
|
|
43
|
+
routes.POST[path]?.query?.parse(options2.query);
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
|
-
const queryString =
|
|
46
|
+
const queryString = options2?.query ? "?" + new URLSearchParams(options2.query).toString() : "";
|
|
45
47
|
const response = await customFetch(`${baseUrl}${path}${queryString}`, {
|
|
46
48
|
method: "POST",
|
|
47
49
|
headers: {
|
|
48
50
|
"Content-Type": "application/json",
|
|
49
|
-
...
|
|
51
|
+
...await getHeaders()
|
|
50
52
|
},
|
|
51
53
|
body: JSON.stringify(body)
|
|
52
54
|
});
|
|
53
55
|
if (!response.ok) {
|
|
54
56
|
const error = await response.json();
|
|
55
|
-
|
|
57
|
+
if (options2.debug) {
|
|
58
|
+
console.debug(error);
|
|
59
|
+
}
|
|
56
60
|
throw new Error(error.message);
|
|
57
61
|
}
|
|
58
62
|
const json = await response.json();
|
|
59
|
-
return
|
|
63
|
+
return routes.POST[path]?.result.parse(json);
|
|
60
64
|
};
|
|
61
65
|
return client;
|
|
62
66
|
};
|
|
63
|
-
var
|
|
64
|
-
router = Object.keys(
|
|
67
|
+
var registerExpressRoutes = (router, routes, handlers) => {
|
|
68
|
+
router = Object.keys(routes.GET).reduce(
|
|
65
69
|
(r, x) => r.get(x, async (req, res, next) => {
|
|
66
70
|
try {
|
|
67
71
|
const ctx = handlers.ctx?.(req) ?? {};
|
|
68
72
|
const data = {
|
|
69
73
|
params: req.params,
|
|
70
|
-
query:
|
|
74
|
+
query: routes.GET[x]?.query?.parse(req.query)
|
|
71
75
|
};
|
|
72
76
|
const result = await handlers.GET[x]?.(data, ctx);
|
|
73
|
-
const validatedResult =
|
|
77
|
+
const validatedResult = routes.GET[x]?.result.parse(result);
|
|
74
78
|
res.json(validatedResult);
|
|
75
79
|
} catch (err) {
|
|
76
80
|
next(err);
|
|
@@ -84,17 +88,17 @@ var applyConfigToExpressRouter = (router, schema, handlers) => {
|
|
|
84
88
|
async (_, res) => res.contentType("text/plain").send(await readFile(handlers.schemaFilePath, "utf8"))
|
|
85
89
|
);
|
|
86
90
|
}
|
|
87
|
-
router = Object.keys(
|
|
91
|
+
router = Object.keys(routes.POST).reduce(
|
|
88
92
|
(r, x) => r.post(x, async (req, res, next) => {
|
|
89
93
|
try {
|
|
90
94
|
const ctx = handlers.ctx?.(req) ?? {};
|
|
91
95
|
const data = {
|
|
92
96
|
params: req.params,
|
|
93
|
-
body:
|
|
94
|
-
query:
|
|
97
|
+
body: routes.POST[x]?.body.parse(req.body),
|
|
98
|
+
query: routes.POST[x]?.query?.parse(req.query)
|
|
95
99
|
};
|
|
96
100
|
const result = await handlers.POST[x]?.(data, ctx);
|
|
97
|
-
const validatedResult =
|
|
101
|
+
const validatedResult = routes.POST[x]?.result.parse(result);
|
|
98
102
|
res.json(validatedResult);
|
|
99
103
|
} catch (err) {
|
|
100
104
|
next(err);
|
|
@@ -106,8 +110,8 @@ var applyConfigToExpressRouter = (router, schema, handlers) => {
|
|
|
106
110
|
};
|
|
107
111
|
|
|
108
112
|
// src/types.ts
|
|
109
|
-
var
|
|
113
|
+
var defineRoutes = (routes) => routes;
|
|
110
114
|
|
|
111
|
-
export {
|
|
115
|
+
export { createClient, defineRoutes, registerExpressRoutes };
|
|
112
116
|
//# sourceMappingURL=index.mjs.map
|
|
113
117
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":[],"mappings":";;;AAuBO,IAAM,YAAA,GAAe,CAC1B,MAAA,EACA,OAAA,KACoB;AACpB,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,UAAU,EAAC;AAAA,IACX,OAAO,WAAA,GAAc,KAAA;AAAA,IACrB,eAAA,GAAkB;AAAA,GACpB,GAAI,OAAA;AAEJ,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,KAAK,EAAC;AAAA,IACN,MAAM;AAAC,GACT;AAEA,EAAA,MAAA,CAAO,GAAA,GAAM,OAAO,IAAA,EAAc,IAAA,KAAe;AAC/C,IAAA,IAAI,eAAA,IAAmB,MAAM,KAAA,EAAO;AAClC,MAAA,MAAA,CAAO,IAAI,IAAI,CAAA,EAAG,KAAA,EAAO,KAAA,CAAM,KAAK,KAAK,CAAA;AAAA,IAC3C;AACA,IAAA,MAAM,WAAA,GAAc,IAAA,EAAM,KAAA,GACtB,GAAA,GAAM,IAAI,gBAAgB,IAAA,CAAK,KAAK,CAAA,CAAE,QAAA,EAAS,GAC/C,EAAA;AACJ,IAAA,MAAM,QAAA,GAAW,MAAM,WAAA,CAAY,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,WAAW,CAAA,CAAA,EAAI;AAAA,MACpE,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,GAAG;AAAA;AACL,KACD,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,KAAA,GAAa,MAAM,QAAA,CAAS,IAAA,EAAK;AACvC,MAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AACnB,MAAA,MAAM,IAAI,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA;AAAA,IAC/B;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAEjC,IAAA,OAAO,OAAO,GAAA,CAAI,IAAI,CAAA,EAAG,MAAA,CAAO,MAAM,IAAI,CAAA;AAAA,EAC5C,CAAA;AAEA,EAAA,MAAA,CAAO,IAAA,GAAO,OAAO,IAAA,EAAc,IAAA,EAAW,IAAA,KAAe;AAC3D,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,IAAI,IAAA,EAAM;AACR,QAAA,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,IAAA,EAAM,MAAM,IAAI,CAAA;AAAA,MACrC;AACA,MAAA,IAAI,MAAM,KAAA,EAAO;AACf,QAAA,MAAA,CAAO,KAAK,IAAI,CAAA,EAAG,KAAA,EAAO,KAAA,CAAM,KAAK,KAAK,CAAA;AAAA,MAC5C;AAAA,IACF;AACA,IAAA,MAAM,WAAA,GAAc,IAAA,EAAM,KAAA,GACtB,GAAA,GAAM,IAAI,gBAAgB,IAAA,CAAK,KAAK,CAAA,CAAE,QAAA,EAAS,GAC/C,EAAA;AACJ,IAAA,MAAM,QAAA,GAAW,MAAM,WAAA,CAAY,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,WAAW,CAAA,CAAA,EAAI;AAAA,MACpE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,GAAG;AAAA,OACL;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;AACvC,MAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AACnB,MAAA,MAAM,IAAI,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA;AAAA,IAC/B;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAEjC,IAAA,OAAO,OAAO,IAAA,CAAK,IAAI,CAAA,EAAG,MAAA,CAAO,MAAM,IAAI,CAAA;AAAA,EAC7C,CAAA;AAEA,EAAA,OAAO,MAAA;AACT;AC9DO,IAAM,0BAAA,GAA6B,CACxC,MAAA,EACA,MAAA,EACA,QAAA,KAIG;AACH,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,KAAA,EAAO,OAAO,GAAA,CAAI,CAAC,GAAG,KAAA,EAAO,KAAA,CAAM,IAAI,KAAK;AAAA,SAC9C;AACA,QAAA,MAAM,SAAS,MAAM,QAAA,CAAS,IAAI,CAAC,CAAA,GAAI,MAAa,GAAG,CAAA;AACvD,QAAA,MAAM,kBAAkB,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,EAAG,MAAA,CAAO,MAAM,MAAM,CAAA;AAE1D,QAAA,GAAA,CAAI,KAAK,eAAe,CAAA;AAAA,MAC1B,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,GAAG,CAAA;AAAA,MACV;AAAA,IACF,CAAC,CAAA;AAAA,IACH;AAAA,GACF;AAEA,EAAA,IAAI,SAAS,cAAA,EAAgB;AAC3B,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,MAAM,QAAA,CAAS,QAAA,CAAS,cAAA,EAAiB,MAAM,CAAC;AAAA,KAC1D;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,KAAA,EAAO,OAAO,IAAA,CAAK,CAAC,GAAG,KAAA,EAAO,KAAA,CAAM,IAAI,KAAK;AAAA,SAC/C;AACA,QAAA,MAAM,SAAS,MAAM,QAAA,CAAS,KAAK,CAAC,CAAA,GAAI,MAAa,GAAG,CAAA;AACxD,QAAA,MAAM,kBAAkB,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,EAAG,MAAA,CAAO,MAAM,MAAM,CAAA;AAC3D,QAAA,GAAA,CAAI,KAAK,eAAe,CAAA;AAAA,MAC1B,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,GAAG,CAAA;AAAA,MACV;AAAA,IACF,CAAC,CAAA;AAAA,IACH;AAAA,GACF;AAEA,EAAA,OAAO,MAAA;AACT;;;AC5EO,IAAM,kBAAA,GAAqB,CAAyB,MAAA,KACzD","file":"index.mjs","sourcesContent":["import type { InferRouteConfig, RouterConfig } from \"./types\"\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\">\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\" | \"result\">\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n headers?: Record<string, string>\n fetch?: FetchFunction\n validateRequest?: boolean\n}\n\nexport const createClient = <T extends RouterConfig>(\n config: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n headers = {},\n fetch: customFetch = fetch,\n validateRequest = false,\n } = options\n\n const client = {\n GET: {} as any,\n POST: {} as any,\n }\n\n client.GET = async (path: string, data?: any) => {\n if (validateRequest && data?.query) {\n config.GET[path]?.query?.parse(data.query)\n }\n const queryString = data?.query\n ? \"?\" + new URLSearchParams(data.query).toString()\n : \"\"\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n console.error(error)\n throw new Error(error.message)\n }\n\n const json = await response.json()\n\n return config.GET[path]?.result.parse(json)\n }\n\n client.POST = async (path: string, body: any, rest?: any) => {\n if (validateRequest) {\n if (body) {\n config.POST[path]?.body?.parse(body)\n }\n if (rest?.query) {\n config.POST[path]?.query?.parse(rest.query)\n }\n }\n const queryString = rest?.query\n ? \"?\" + new URLSearchParams(rest.query).toString()\n : \"\"\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n console.error(error)\n throw new Error(error.message)\n }\n\n const json = await response.json()\n\n return config.POST[path]?.result.parse(json)\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport type { InferRouteConfig, RouterConfig } from \"./types\"\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\ntype ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n\nexport type RouterHandlerConfig<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"GET\"][K]>[\"result\"]\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: Omit<InferRouteConfig<T[\"POST\"][K]>, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"POST\"][K]>[\"result\"]\n }\n}\n\nexport const applyConfigToExpressRouter = <T extends RouterConfig, TContext>(\n router: Router,\n schema: T,\n handlers: RouterHandlerConfig<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n }\n) => {\n router = Object.keys(schema.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 query: schema.GET[x]?.query?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n const validatedResult = schema.GET[x]?.result.parse(result)\n\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n if (handlers.schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(handlers.schemaFilePath!, \"utf8\"))\n )\n }\n\n router = Object.keys(schema.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: schema.POST[x]?.body.parse(req.body),\n query: schema.POST[x]?.query?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n const validatedResult = schema.POST[x]?.result.parse(result)\n res.json(validatedResult)\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 query?: z.ZodType\n result: 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 defineRouterSchema = <T extends RouterConfig>(config: T): T =>\n config\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 type { InferRouteConfig, RouterConfig } from \"./types\"\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\">\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\" | \"result\">\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\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\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?.query) {\n routes.GET[path]?.query?.parse(options.query)\n }\n\n const queryString = options?.query\n ? \"?\" + new URLSearchParams(options.query).toString()\n : \"\"\n\n const response = await customFetch(`${baseUrl}${path}${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 const json = await response.json()\n\n return routes.GET[path]?.result.parse(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?.query) {\n routes.POST[path]?.query?.parse(options.query)\n }\n }\n\n const queryString = options?.query\n ? \"?\" + new URLSearchParams(options.query).toString()\n : \"\"\n\n const response = await customFetch(`${baseUrl}${path}${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 const json = await response.json()\n\n return routes.POST[path]?.result.parse(json)\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport type { InferRouteConfig, RouterConfig } from \"./types\"\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\ntype ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n\nexport type RouteHandlers<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"GET\"][K]>[\"result\"]\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: Omit<InferRouteConfig<T[\"POST\"][K]>, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"POST\"][K]>[\"result\"]\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 }\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 query: routes.GET[x]?.query?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n const validatedResult = routes.GET[x]?.result.parse(result)\n\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n if (handlers.schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(handlers.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 query: routes.POST[x]?.query?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n const validatedResult = routes.POST[x]?.result.parse(result)\n res.json(validatedResult)\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 query?: z.ZodType\n result: 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 =>\n routes\n"]}
|