rouzer 1.0.0-beta.15 → 1.0.0-beta.17

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.
@@ -1,5 +1,7 @@
1
- import type { Promisable, RouteRequest } from '../types.js';
2
- export declare function createClient(config: {
1
+ import { Route } from '../route.js';
2
+ import type { InferRouteResponse, Promisable, RouteArgs, RouteRequest, RouteSchema } from '../types.js';
3
+ export type RouzerClient<TRoutes extends Record<string, Route> = Record<string, never>> = ReturnType<typeof createClient<TRoutes>>;
4
+ export declare function createClient<TRoutes extends Record<string, Route> = Record<string, never>>(config: {
3
5
  /**
4
6
  * Base URL to use for all requests.
5
7
  */
@@ -8,12 +10,21 @@ export declare function createClient(config: {
8
10
  * Default headers to send with every request.
9
11
  */
10
12
  headers?: Record<string, string>;
13
+ /**
14
+ * Pass in routes to attach them as methods on the client.
15
+ * @example
16
+ * ```ts
17
+ * const client = createClient({ baseURL: '/api/', routes: { helloRoute } })
18
+ * client.helloRoute.GET({ path: { name: 'world' } })
19
+ * ```
20
+ */
21
+ routes?: TRoutes;
11
22
  /**
12
23
  * Custom handler for non-200 response to a `.json()` request. By default, the
13
24
  * response is always parsed as JSON, regardless of the HTTP status code.
14
25
  */
15
26
  onJsonError?: (response: Response) => Promisable<Response>;
16
- }): {
27
+ }): { [K in keyof TRoutes]: TRoutes[K]["methods"] extends infer TMethods ? { [M in keyof TMethods]: RouteRequestFunction<Extract<TMethods[M], RouteSchema>>; } : never; } & {
17
28
  config: {
18
29
  /**
19
30
  * Base URL to use for all requests.
@@ -23,14 +34,27 @@ export declare function createClient(config: {
23
34
  * Default headers to send with every request.
24
35
  */
25
36
  headers?: Record<string, string> | undefined;
37
+ /**
38
+ * Pass in routes to attach them as methods on the client.
39
+ * @example
40
+ * ```ts
41
+ * const client = createClient({ baseURL: '/api/', routes: { helloRoute } })
42
+ * client.helloRoute.GET({ path: { name: 'world' } })
43
+ * ```
44
+ */
45
+ routes?: TRoutes | undefined;
26
46
  /**
27
47
  * Custom handler for non-200 response to a `.json()` request. By default, the
28
48
  * response is always parsed as JSON, regardless of the HTTP status code.
29
49
  */
30
50
  onJsonError?: ((response: Response) => Promisable<Response>) | undefined;
31
51
  };
32
- request<T extends RouteRequest>({ path: pathBuilder, method, args: { path, query, body, headers }, route, }: T): Promise<Response & {
52
+ request: <T extends RouteRequest>({ path: pathBuilder, method, args: { path, query, body, headers }, schema, }: T) => Promise<Response & {
33
53
  json(): Promise<T["$result"]>;
34
54
  }>;
35
- json<T extends RouteRequest>(request: T): Promise<T["$result"]>;
55
+ json: <T extends RouteRequest>(props: T) => Promise<T["$result"]>;
36
56
  };
57
+ type RouteRequestFunction<T extends RouteSchema> = (args: RouteArgs<T>) => Promise<T extends {
58
+ response: any;
59
+ } ? InferRouteResponse<T> : Response>;
60
+ export {};
@@ -1,55 +1,69 @@
1
- import { shake } from '../common.js';
1
+ import { mapValues, shake } from '../common.js';
2
2
  export function createClient(config) {
3
3
  const baseURL = config.baseURL.replace(/\/$/, '');
4
+ async function request({ path: pathBuilder, method, args: { path, query, body, headers }, schema, }) {
5
+ if (schema.path) {
6
+ path = schema.path.parse(path);
7
+ }
8
+ let url;
9
+ const href = pathBuilder.href(path);
10
+ if (href[0] === '/') {
11
+ url = new URL(baseURL);
12
+ url.pathname += pathBuilder.href(path);
13
+ }
14
+ else {
15
+ url = new URL(href);
16
+ }
17
+ if (schema.query) {
18
+ query = schema.query.parse(query ?? {});
19
+ url.search = new URLSearchParams(query).toString();
20
+ }
21
+ else if (query) {
22
+ throw new Error('Unexpected query parameters');
23
+ }
24
+ if (schema.body) {
25
+ body = schema.body.parse(body !== undefined ? body : {});
26
+ }
27
+ else if (body !== undefined) {
28
+ throw new Error('Unexpected body');
29
+ }
30
+ if (config.headers || headers) {
31
+ headers = {
32
+ ...config.headers,
33
+ ...(headers && shake(headers)),
34
+ };
35
+ }
36
+ if (schema.headers) {
37
+ headers = schema.headers.parse(headers);
38
+ }
39
+ return fetch(url, {
40
+ method,
41
+ body: body !== undefined ? JSON.stringify(body) : undefined,
42
+ headers: headers,
43
+ });
44
+ }
45
+ async function json(props) {
46
+ const response = await request(props);
47
+ if (!response.ok && config.onJsonError) {
48
+ return config.onJsonError(response);
49
+ }
50
+ return response.json();
51
+ }
4
52
  return {
53
+ ...(config.routes
54
+ ? mapValues(config.routes, route => connectRoute(route, request, json))
55
+ : null),
5
56
  config,
6
- request({ path: pathBuilder, method, args: { path, query, body, headers }, route, }) {
7
- if (route.path) {
8
- path = route.path.parse(path);
9
- }
10
- let url;
11
- const href = pathBuilder.href(path);
12
- if (href[0] === '/') {
13
- url = new URL(baseURL);
14
- url.pathname += pathBuilder.href(path);
15
- }
16
- else {
17
- url = new URL(href);
18
- }
19
- if (route.query) {
20
- query = route.query.parse(query ?? {});
21
- url.search = new URLSearchParams(query).toString();
22
- }
23
- else if (query) {
24
- throw new Error('Unexpected query parameters');
25
- }
26
- if (route.body) {
27
- body = route.body.parse(body !== undefined ? body : {});
28
- }
29
- else if (body !== undefined) {
30
- throw new Error('Unexpected body');
31
- }
32
- if (config.headers || headers) {
33
- headers = {
34
- ...config.headers,
35
- ...(headers && shake(headers)),
36
- };
37
- }
38
- if (route.headers) {
39
- headers = route.headers.parse(headers);
40
- }
41
- return fetch(url, {
42
- method,
43
- body: body !== undefined ? JSON.stringify(body) : undefined,
44
- headers: headers,
45
- });
46
- },
47
- async json(request) {
48
- const response = await this.request(request);
49
- if (!response.ok && config.onJsonError) {
50
- return config.onJsonError(response);
51
- }
52
- return response.json();
53
- },
57
+ request,
58
+ json,
59
+ };
60
+ }
61
+ function connectRoute(route, request, json) {
62
+ return {
63
+ ...route,
64
+ ...mapValues(route.methods, (schema, key) => {
65
+ const fetch = schema.response ? json : request;
66
+ return (args) => fetch(route[key](args));
67
+ }),
54
68
  };
55
69
  }
package/dist/route.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { RoutePattern } from '@remix-run/route-pattern';
2
- import type { MutationMethod, QueryMethod, RouteFunction, RouteMethods, Unchecked } from './types.js';
2
+ import type { RouteFunction, RouteSchema, RouteSchemaMap, Unchecked } from './types.js';
3
3
  export declare function $type<T>(): Unchecked<T>;
4
- export declare function route<P extends string, T extends RouteMethods>(pattern: P, methods: T): {
4
+ export type Route<P extends string = string, T extends RouteSchemaMap = RouteSchemaMap> = {
5
5
  path: RoutePattern<P>;
6
6
  methods: T;
7
- } & { [K in keyof T]: RouteFunction<Extract<T[K], MutationMethod | QueryMethod>>; };
7
+ } & {
8
+ [K in keyof T]: RouteFunction<Extract<T[K], RouteSchema>>;
9
+ };
10
+ export declare function route<P extends string, T extends RouteSchemaMap>(pattern: P, methods: T): Route<P, T>;
package/dist/route.js CHANGED
@@ -5,14 +5,17 @@ export function $type() {
5
5
  }
6
6
  export function route(pattern, methods) {
7
7
  const path = new RoutePattern(pattern);
8
- const createFetch = (method, route) => (args) => {
8
+ const createFetch = (method, schema) => (args) => {
9
9
  return {
10
- route,
10
+ schema,
11
11
  path,
12
12
  method,
13
13
  args,
14
14
  $result: undefined,
15
15
  };
16
16
  };
17
- return Object.assign({ path, methods }, mapEntries(methods, (method, route) => [method, createFetch(method, route)]));
17
+ return Object.assign({ path, methods }, mapEntries(methods, (method, schema) => [
18
+ method,
19
+ createFetch(method, schema),
20
+ ]));
18
21
  }
@@ -1,7 +1,7 @@
1
1
  import { type Params } from '@remix-run/route-pattern';
2
2
  import { chain, MiddlewareChain, type HattipContext, type MiddlewareContext } from 'alien-middleware';
3
3
  import * as z from 'zod/mini';
4
- import type { InferRouteResponse, MutationMethod, Promisable, QueryMethod, Routes } from '../types.js';
4
+ import type { InferRouteResponse, MutationRouteSchema, Promisable, QueryRouteSchema, Routes } from '../types.js';
5
5
  export { chain };
6
6
  type EmptyMiddlewareChain<TPlatform = unknown> = MiddlewareChain<{
7
7
  initial: {
@@ -82,18 +82,26 @@ interface CreateRouterConfig<TRoutes extends Routes, TMiddleware extends Middlew
82
82
  routes: TRoutes;
83
83
  middlewares?: TMiddleware;
84
84
  }
85
- export declare function createRouter<TRoutes extends Routes, TMiddleware extends MiddlewareChain = EmptyMiddlewareChain>(config: CreateRouterConfig<TRoutes, TMiddleware>): (handlers: { [K in keyof TRoutes]: { [M in keyof TRoutes[K]["methods"]]: TRoutes[K]["methods"][M] extends infer T ? T extends TRoutes[K]["methods"][M] ? T extends QueryMethod ? (context: MiddlewareContext<TMiddleware> & {
85
+ export declare function createRouter<TRoutes extends Routes, TMiddleware extends MiddlewareChain = EmptyMiddlewareChain>(config: CreateRouterConfig<TRoutes, TMiddleware>): (handlers: { [K in keyof TRoutes]: { [M in keyof TRoutes[K]["methods"]]: TRoutes[K]["methods"][M] extends infer T ? T extends TRoutes[K]["methods"][M] ? T extends QueryRouteSchema ? (context: MiddlewareContext<TMiddleware> & {
86
86
  path: T extends {
87
87
  path: any;
88
88
  } ? z.infer<T["path"]> : Params<TRoutes[K]["path"]["source"]>;
89
- query: z.infer<T["query"]>;
90
- headers: z.infer<T["headers"]>;
91
- }) => Promisable<Response | InferRouteResponse<T>> : T extends MutationMethod ? (context: MiddlewareContext<TMiddleware> & {
89
+ query: T extends {
90
+ query: any;
91
+ } ? z.infer<T["query"]> : undefined;
92
+ headers: T extends {
93
+ headers: any;
94
+ } ? z.infer<T["headers"]> : undefined;
95
+ }) => Promisable<Response | InferRouteResponse<T>> : T extends MutationRouteSchema ? (context: MiddlewareContext<TMiddleware> & {
92
96
  path: T extends {
93
97
  path: any;
94
98
  } ? z.infer<T["path"]> : Params<TRoutes[K]["path"]["source"]>;
95
- body: z.infer<T["body"]>;
96
- headers: z.infer<T["headers"]>;
99
+ body: T extends {
100
+ body: any;
101
+ } ? z.infer<T["body"]> : undefined;
102
+ headers: T extends {
103
+ headers: any;
104
+ } ? z.infer<T["headers"]> : undefined;
97
105
  }) => Promisable<Response | InferRouteResponse<T>> : never : never : never; }; }) => import("alien-middleware").ApplyMiddleware<TMiddleware, (context: HattipContext<TMiddleware extends MiddlewareChain<infer T extends {
98
106
  initial: {
99
107
  env: object;
@@ -1,7 +1,7 @@
1
1
  import { RoutePattern } from '@remix-run/route-pattern';
2
2
  import { chain, } from 'alien-middleware';
3
- import { mapValues } from '../common.js';
4
3
  import * as z from 'zod/mini';
4
+ import { mapValues } from '../common.js';
5
5
  export { chain };
6
6
  export function createRouter(config) {
7
7
  const keys = Object.keys(config.routes);
package/dist/types.d.ts CHANGED
@@ -4,31 +4,33 @@ export type Promisable<T> = T | Promise<T>;
4
4
  export type Unchecked<T> = {
5
5
  __unchecked__: T;
6
6
  };
7
- export type QueryMethod = {
7
+ export type QueryRouteSchema = {
8
8
  path?: z.ZodMiniObject<any>;
9
9
  query?: z.ZodMiniObject<any>;
10
10
  body?: never;
11
11
  headers?: z.ZodMiniObject<any>;
12
- response: Unchecked<any>;
12
+ response?: Unchecked<any>;
13
13
  };
14
- export type MutationMethod = {
14
+ export type MutationRouteSchema = {
15
15
  path?: z.ZodMiniObject<any>;
16
16
  query?: never;
17
17
  body?: z.ZodMiniType<any, any>;
18
18
  headers?: z.ZodMiniObject<any>;
19
19
  response?: Unchecked<any>;
20
20
  };
21
- export type RouteMethods = {
22
- GET?: QueryMethod;
23
- POST?: MutationMethod;
24
- PUT?: MutationMethod;
25
- PATCH?: MutationMethod;
26
- DELETE?: MutationMethod;
21
+ export type RouteSchemaMap = {
22
+ GET?: QueryRouteSchema;
23
+ POST?: MutationRouteSchema;
24
+ PUT?: MutationRouteSchema;
25
+ PATCH?: MutationRouteSchema;
26
+ DELETE?: MutationRouteSchema;
27
27
  };
28
+ export type Method = string & keyof RouteSchemaMap;
29
+ export type RouteSchema = QueryRouteSchema | MutationRouteSchema;
28
30
  export type Routes = {
29
31
  [key: string]: {
30
32
  path: RoutePattern;
31
- methods: RouteMethods;
33
+ methods: RouteSchemaMap;
32
34
  };
33
35
  };
34
36
  declare class Any {
@@ -41,23 +43,23 @@ type PathArgs<T> = T extends {
41
43
  } : {
42
44
  path: TParams;
43
45
  } : unknown : unknown;
44
- type QueryArgs<T> = T extends QueryMethod & {
46
+ type QueryArgs<T> = T extends QueryRouteSchema & {
45
47
  query: infer TQuery;
46
48
  } ? {} extends z.infer<TQuery> ? {
47
49
  query?: z.infer<TQuery>;
48
50
  } : {
49
51
  query: z.infer<TQuery>;
50
52
  } : unknown;
51
- type MutationArgs<T> = T extends MutationMethod & {
53
+ type MutationArgs<T> = T extends MutationRouteSchema ? T extends {
52
54
  body: infer TBody;
53
55
  } ? {} extends z.infer<TBody> ? {
54
56
  body?: z.infer<TBody>;
55
57
  } : {
56
58
  body: z.infer<TBody>;
59
+ } : {
60
+ body?: unknown;
57
61
  } : unknown;
58
- export type RouteArgs<T extends QueryMethod | MutationMethod = any> = ([
59
- T
60
- ] extends [Any] ? {
62
+ export type RouteArgs<T extends RouteSchema = any> = ([T] extends [Any] ? {
61
63
  query?: any;
62
64
  body?: any;
63
65
  path?: any;
@@ -65,7 +67,7 @@ export type RouteArgs<T extends QueryMethod | MutationMethod = any> = ([
65
67
  headers?: Record<string, string | undefined>;
66
68
  };
67
69
  export type RouteRequest<TResult = any> = {
68
- route: QueryMethod | MutationMethod;
70
+ schema: RouteSchema;
69
71
  path: RoutePattern;
70
72
  method: string;
71
73
  args: RouteArgs;
@@ -74,10 +76,10 @@ export type RouteRequest<TResult = any> = {
74
76
  export type RouteResponse<TResult = any> = Response & {
75
77
  json(): Promise<TResult>;
76
78
  };
77
- export type InferRouteResponse<T extends QueryMethod | MutationMethod> = T extends {
79
+ export type InferRouteResponse<T extends RouteSchema> = T extends {
78
80
  response: Unchecked<infer TResponse>;
79
81
  } ? TResponse : void;
80
- export type RouteFunction<T extends QueryMethod | MutationMethod> = {
82
+ export type RouteFunction<T extends RouteSchema> = {
81
83
  (args: RouteArgs<T>): RouteRequest<InferRouteResponse<T>>;
82
84
  $args: RouteArgs<T>;
83
85
  $response: InferRouteResponse<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rouzer",
3
- "version": "1.0.0-beta.15",
3
+ "version": "1.0.0-beta.17",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/readme.md CHANGED
@@ -85,7 +85,11 @@ export const handler = createRouter({
85
85
  middlewares,
86
86
  basePath: 'api/',
87
87
  cors: {
88
- allowOrigins: ['example.net', 'https://*.example.com', '*://localhost:3000'],
88
+ allowOrigins: [
89
+ 'example.net',
90
+ 'https://*.example.com',
91
+ '*://localhost:3000',
92
+ ],
89
93
  },
90
94
  debug: process.env.NODE_ENV === 'development',
91
95
  })({
@@ -125,6 +129,30 @@ const response = await client.request(
125
129
  const { message } = await response.json()
126
130
  ```
127
131
 
132
+ Optionally pass your routes map to `createClient` to get per-route methods on the client:
133
+
134
+ ```ts
135
+ import * as routes from './routes'
136
+
137
+ const client = createClient({
138
+ baseURL: '/api/',
139
+ routes,
140
+ })
141
+ ```
142
+
143
+ Routes that define a `response` type will call `client.json()` under the hood and return the parsed value; routes without one return the raw `Response`:
144
+
145
+ ```ts
146
+ // helloRoute has a response schema, so you get the parsed payload
147
+ const { message } = await client.helloRoute.GET({
148
+ path: { name: 'world' },
149
+ })
150
+
151
+ // imagine pingRoute has no response schema; you get a Response object
152
+ const pingResponse = await client.pingRoute.GET({})
153
+ const pingText = await pingResponse.text()
154
+ ```
155
+
128
156
  ## Add an endpoint
129
157
 
130
158
  1. Declare it in `routes.ts` with `route(...)` and `zod/mini` schemas.