@jokio/rpc 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +17 -17
- package/dist/index.d.ts +17 -17
- package/dist/index.js +89 -80
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +89 -80
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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,124 @@ 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) => {
|
|
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(
|
|
112
|
+
handlers.validation ? routeConfig?.response.parse(result) : result
|
|
113
|
+
);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
next(err);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
var registerExpressRoutes = (router, routes, handlers) => {
|
|
120
|
+
const { schemaFilePath } = handlers;
|
|
121
|
+
const expressMethodMap = {
|
|
122
|
+
GET: "get",
|
|
123
|
+
POST: "post",
|
|
124
|
+
PUT: "put",
|
|
125
|
+
PATCH: "patch",
|
|
126
|
+
DELETE: "delete",
|
|
127
|
+
QUERY: "all"
|
|
128
|
+
};
|
|
129
|
+
for (const [method, routerMethod] of Object.entries(expressMethodMap)) {
|
|
130
|
+
const methodKey = method;
|
|
131
|
+
const methodRoutes = routes[methodKey];
|
|
132
|
+
if (!methodRoutes) continue;
|
|
133
|
+
router = Object.keys(methodRoutes).reduce(
|
|
134
|
+
(r, route) => r[routerMethod](
|
|
135
|
+
route,
|
|
136
|
+
createRouteHandler(methodKey, routes, handlers, route)
|
|
137
|
+
),
|
|
138
|
+
router
|
|
139
|
+
);
|
|
140
|
+
}
|
|
115
141
|
if (schemaFilePath) {
|
|
116
142
|
router = router.get(
|
|
117
143
|
"/__schema",
|
|
118
144
|
async (_, res) => res.contentType("text/plain").send(await promises.readFile(schemaFilePath, "utf8"))
|
|
119
145
|
);
|
|
120
146
|
}
|
|
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
147
|
return router;
|
|
139
148
|
};
|
|
140
149
|
|
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":"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","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 validation?: boolean\n },\n route: string\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(\n handlers.validation ? routeConfig?.response.parse(result) : result\n )\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 } = 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)\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,124 @@ 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) => {
|
|
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(
|
|
110
|
+
handlers.validation ? routeConfig?.response.parse(result) : result
|
|
111
|
+
);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
next(err);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
var registerExpressRoutes = (router, routes, handlers) => {
|
|
118
|
+
const { schemaFilePath } = 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)
|
|
135
|
+
),
|
|
136
|
+
router
|
|
137
|
+
);
|
|
138
|
+
}
|
|
113
139
|
if (schemaFilePath) {
|
|
114
140
|
router = router.get(
|
|
115
141
|
"/__schema",
|
|
116
142
|
async (_, res) => res.contentType("text/plain").send(await readFile(schemaFilePath, "utf8"))
|
|
117
143
|
);
|
|
118
144
|
}
|
|
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
145
|
return router;
|
|
137
146
|
};
|
|
138
147
|
|
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 validation?: boolean\n },\n route: string\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(\n handlers.validation ? routeConfig?.response.parse(result) : result\n )\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 } = 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)\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"]}
|