@routar/core 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +124 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +155 -101
- package/dist/index.d.ts +155 -101
- package/dist/index.js +123 -75
- package/dist/index.js.map +1 -1
- package/package.json +21 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
type HttpMethod =
|
|
1
|
+
type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
2
2
|
/** Options passed to {@link Executor.execute} on every HTTP call. */
|
|
3
3
|
interface ExecuteOptions {
|
|
4
4
|
method: HttpMethod;
|
|
5
5
|
url: string;
|
|
6
6
|
params?: Record<string, unknown>;
|
|
7
7
|
body?: unknown;
|
|
8
|
+
/**
|
|
9
|
+
* Per-request headers injected by middleware (e.g. `defineMiddleware`).
|
|
10
|
+
* Headers cannot be set from `createApi` call sites directly — use middleware
|
|
11
|
+
* to add dynamic headers such as `Authorization` or `X-Request-Id`.
|
|
12
|
+
*/
|
|
8
13
|
headers?: Record<string, string>;
|
|
9
14
|
signal?: AbortSignal;
|
|
10
15
|
}
|
|
@@ -82,7 +87,7 @@ interface EndpointSpec<TRequest extends RequestShape = RequestShape, TResponse e
|
|
|
82
87
|
* - With `adapter`: returns the adapter's output type.
|
|
83
88
|
* - Without `adapter`: returns `ValidatorOutput<TResponse>`.
|
|
84
89
|
*/
|
|
85
|
-
type InferResponse<TSpec extends EndpointSpec<any, any, any>> = TSpec[
|
|
90
|
+
type InferResponse<TSpec extends EndpointSpec<any, any, any>> = TSpec["adapter"] extends (raw: any) => infer R ? R : ValidatorOutput<TSpec["response"]>;
|
|
86
91
|
/**
|
|
87
92
|
* A single entry inside a {@link RouterEndpoints} map.
|
|
88
93
|
* Either a leaf endpoint spec or a nested {@link RouterDef}.
|
|
@@ -97,53 +102,156 @@ interface RouterDef<TEndpoints extends RouterEndpoints = RouterEndpoints> {
|
|
|
97
102
|
}
|
|
98
103
|
/**
|
|
99
104
|
* Extracts request/response types from a typed API client for use in query
|
|
100
|
-
* hooks or mutation handlers.
|
|
105
|
+
* hooks or mutation handlers. Supports nested router clients recursively.
|
|
101
106
|
*
|
|
102
107
|
* @example
|
|
103
108
|
* ```ts
|
|
104
109
|
* export type TodoApiTypes = ApiTypes<typeof todoApi>;
|
|
105
110
|
* type CreateRequest = TodoApiTypes['create']['request'];
|
|
106
111
|
* type CreateResponse = TodoApiTypes['create']['response'];
|
|
112
|
+
*
|
|
113
|
+
* // Nested router: api.users.todos.getList
|
|
114
|
+
* type NestedTypes = ApiTypes<typeof api>;
|
|
115
|
+
* type ListReq = NestedTypes['users']['todos']['getList']['request'];
|
|
107
116
|
* ```
|
|
108
117
|
*/
|
|
109
118
|
type ApiTypes<TApi> = {
|
|
110
119
|
[K in keyof TApi]: TApi[K] extends (...args: any[]) => Promise<infer R> ? {
|
|
111
120
|
request: Parameters<TApi[K]>[0];
|
|
112
121
|
response: R;
|
|
113
|
-
} : never;
|
|
122
|
+
} : TApi[K] extends object ? ApiTypes<TApi[K]> : never;
|
|
114
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Options for {@link createApi}.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* // Disable all validation in production
|
|
130
|
+
* createApi(executor, router, { validate: process.env.NODE_ENV !== 'production' });
|
|
131
|
+
*
|
|
132
|
+
* // Keep request validation (catch call-site bugs), skip response in prod
|
|
133
|
+
* createApi(executor, router, { validate: { request: true, response: false } });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
interface CreateApiOptions {
|
|
137
|
+
/**
|
|
138
|
+
* Controls whether request and response schemas are run at call time.
|
|
139
|
+
*
|
|
140
|
+
* - `true` (default) — validate both request and response.
|
|
141
|
+
* - `false` — skip both; raw params and raw response pass through.
|
|
142
|
+
* - `{ request?, response? }` — enable/disable each independently.
|
|
143
|
+
*/
|
|
144
|
+
validate?: boolean | {
|
|
145
|
+
request?: boolean;
|
|
146
|
+
response?: boolean;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
115
149
|
|
|
150
|
+
/** Callable type for a single endpoint on the generated API client. */
|
|
151
|
+
type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = TSpec["request"] extends {
|
|
152
|
+
parse: (data: unknown) => infer R;
|
|
153
|
+
} ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>> : (params?: RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
|
|
116
154
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
155
|
+
* Fully-typed API client produced by {@link createApi}.
|
|
156
|
+
* Nested {@link RouterDef} entries become nested sub-client objects.
|
|
157
|
+
*/
|
|
158
|
+
type ApiClient<TEndpoints extends RouterEndpoints> = {
|
|
159
|
+
[K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints> ? ApiClient<TNestedEndpoints> : TEndpoints[K] extends EndpointSpec<any, any, any> ? EndpointFn<TEndpoints[K]> : never;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Builds a fully-typed API client from an {@link Executor} and a router
|
|
163
|
+
* (or bare endpoint map).
|
|
119
164
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
165
|
+
* Three call signatures are supported:
|
|
166
|
+
* - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.
|
|
167
|
+
* - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.
|
|
168
|
+
* - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.
|
|
123
169
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
170
|
+
* Each key in `endpoints` becomes a typed async function on the returned client.
|
|
171
|
+
* The function validates the request with `spec.request.parse` (if present),
|
|
172
|
+
* resolves path parameters, calls the executor, validates the response with
|
|
173
|
+
* `spec.response.parse`, and applies `spec.adapter` (if present).
|
|
174
|
+
*
|
|
175
|
+
* @param executor - Transport to use for every HTTP call.
|
|
176
|
+
* @param router - A {@link RouterDef} produced by {@link defineRouter}.
|
|
177
|
+
* @param options - Optional settings (e.g. `validate` to skip schema parsing in production).
|
|
126
178
|
*
|
|
127
179
|
* @example
|
|
128
180
|
* ```ts
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
|
|
133
|
-
* create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),
|
|
134
|
-
* });
|
|
181
|
+
* const todoApi = createApi(executor, todoRouter);
|
|
182
|
+
* const todos = await todoApi.getList({});
|
|
183
|
+
* const todo = await todoApi.getDetail({ path: { id: 1 } });
|
|
135
184
|
*
|
|
136
|
-
* //
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* todos: defineRouter('/todos', {
|
|
140
|
-
* getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
|
|
141
|
-
* getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
|
|
142
|
-
* }),
|
|
185
|
+
* // Skip response validation in production
|
|
186
|
+
* const prodApi = createApi(executor, todoRouter, {
|
|
187
|
+
* validate: { request: true, response: process.env.NODE_ENV !== 'production' },
|
|
143
188
|
* });
|
|
144
189
|
* ```
|
|
145
190
|
*/
|
|
146
|
-
declare function
|
|
191
|
+
declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, router: RouterDef<TEndpoints>, options?: CreateApiOptions): ApiClient<TEndpoints>;
|
|
192
|
+
/**
|
|
193
|
+
* @param executor - Transport to use for every HTTP call.
|
|
194
|
+
* @param prefix - URL prefix prepended to every endpoint path.
|
|
195
|
+
* @param endpoints - Record of named endpoint specs.
|
|
196
|
+
* @param options - Optional settings.
|
|
197
|
+
*/
|
|
198
|
+
declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, prefix: string, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
|
|
199
|
+
/**
|
|
200
|
+
* @param executor - Transport to use for every HTTP call.
|
|
201
|
+
* @param endpoints - Record of named endpoint specs (no URL prefix).
|
|
202
|
+
* @param options - Optional settings.
|
|
203
|
+
*/
|
|
204
|
+
declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Creates an {@link Executor} by wrapping a transport function with an
|
|
208
|
+
* optional middleware chain.
|
|
209
|
+
*
|
|
210
|
+
* Middlewares are applied in declaration order — the first middleware is the
|
|
211
|
+
* outermost wrapper and runs first on each request.
|
|
212
|
+
*
|
|
213
|
+
* @param execute - The underlying transport function (fetch, axios, etc.).
|
|
214
|
+
* @param middlewares - Ordered list of middlewares to apply.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* const executor = createExecutor(
|
|
219
|
+
* async ({ method, url, body }) => {
|
|
220
|
+
* const res = await fetch(url, { method, body: JSON.stringify(body) });
|
|
221
|
+
* return res.json();
|
|
222
|
+
* },
|
|
223
|
+
* [withTimeout(5000), withRetry(3), withLogger()],
|
|
224
|
+
* );
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, middlewares?: ExecutorMiddleware[]): Executor;
|
|
228
|
+
/**
|
|
229
|
+
* Creates an {@link Executor} that selects the underlying transport at
|
|
230
|
+
* request time based on the result of `resolver`.
|
|
231
|
+
*
|
|
232
|
+
* Use this to unify SSR and CSR behind a single API client — the resolver
|
|
233
|
+
* picks the right executor per request, so `createApi` is called once and
|
|
234
|
+
* works in both environments without duplicate `*ServerApi` instances.
|
|
235
|
+
*
|
|
236
|
+
* The resolver receives the full {@link ExecuteOptions} so it can branch on
|
|
237
|
+
* environment, URL prefix, auth context, or any runtime condition.
|
|
238
|
+
*
|
|
239
|
+
* @param resolver - Called on every request; returns the executor to delegate to.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```ts
|
|
243
|
+
* // SSR vs CSR — pick transport based on environment
|
|
244
|
+
* const apiExecutor = dispatchExecutor(() =>
|
|
245
|
+
* typeof window === 'undefined' ? serverExecutor : clientExecutor,
|
|
246
|
+
* );
|
|
247
|
+
*
|
|
248
|
+
* // Route by URL prefix — internal routes use a different transport
|
|
249
|
+
* const apiExecutor = dispatchExecutor((opts) =>
|
|
250
|
+
* opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,
|
|
251
|
+
* );
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor): Executor;
|
|
147
255
|
|
|
148
256
|
/**
|
|
149
257
|
* Extracts `:param` segment names from a path template string as a union of
|
|
@@ -159,9 +267,7 @@ type PathParams<TPath extends string> = TPath extends `${string}:${infer Param}/
|
|
|
159
267
|
* When `TPath` contains dynamic segments (`:param`), requires `request.path`
|
|
160
268
|
* to include all extracted param names. No constraint for static paths.
|
|
161
269
|
*/
|
|
162
|
-
type PathConstraint<TPath extends string> = [
|
|
163
|
-
PathParams<TPath>
|
|
164
|
-
] extends [never] ? {} : {
|
|
270
|
+
type PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never] ? {} : {
|
|
165
271
|
path: Record<PathParams<TPath>, unknown>;
|
|
166
272
|
};
|
|
167
273
|
/**
|
|
@@ -242,77 +348,16 @@ declare function endpoint<TResponse extends Validator<unknown>>(spec: {
|
|
|
242
348
|
response: TResponse;
|
|
243
349
|
};
|
|
244
350
|
|
|
245
|
-
|
|
246
|
-
type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (params: TSpec['request'] extends {
|
|
247
|
-
parse: (data: unknown) => infer R;
|
|
248
|
-
} ? R : RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
|
|
249
|
-
/**
|
|
250
|
-
* Fully-typed API client produced by {@link createApi}.
|
|
251
|
-
* Nested {@link RouterDef} entries become nested sub-client objects.
|
|
252
|
-
*/
|
|
253
|
-
type ApiClient<TEndpoints extends RouterEndpoints> = {
|
|
254
|
-
[K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints> ? ApiClient<TNestedEndpoints> : TEndpoints[K] extends EndpointSpec<any, any, any> ? EndpointFn<TEndpoints[K]> : never;
|
|
255
|
-
};
|
|
256
|
-
/**
|
|
257
|
-
* Builds a fully-typed API client from an {@link Executor} and a router
|
|
258
|
-
* (or bare endpoint map).
|
|
259
|
-
*
|
|
260
|
-
* Three call signatures are supported:
|
|
261
|
-
* - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.
|
|
262
|
-
* - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.
|
|
263
|
-
* - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.
|
|
264
|
-
*
|
|
265
|
-
* Each key in `endpoints` becomes a typed async function on the returned client.
|
|
266
|
-
* The function validates the request with `spec.request.parse` (if present),
|
|
267
|
-
* resolves path parameters, calls the executor, validates the response with
|
|
268
|
-
* `spec.response.parse`, and applies `spec.adapter` (if present).
|
|
269
|
-
*
|
|
270
|
-
* @param executor - Transport to use for every HTTP call.
|
|
271
|
-
* @param router - A {@link RouterDef} produced by {@link defineRouter}.
|
|
272
|
-
*
|
|
273
|
-
* @example
|
|
274
|
-
* ```ts
|
|
275
|
-
* const todoApi = createApi(executor, todoRouter);
|
|
276
|
-
* const todos = await todoApi.getList({});
|
|
277
|
-
* const todo = await todoApi.getDetail({ path: { id: 1 } });
|
|
278
|
-
* ```
|
|
279
|
-
*/
|
|
280
|
-
declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, router: RouterDef<TEndpoints>): ApiClient<TEndpoints>;
|
|
281
|
-
/**
|
|
282
|
-
* @param executor - Transport to use for every HTTP call.
|
|
283
|
-
* @param prefix - URL prefix prepended to every endpoint path.
|
|
284
|
-
* @param endpoints - Record of named endpoint specs.
|
|
285
|
-
*/
|
|
286
|
-
declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, prefix: string, endpoints: TEndpoints): ApiClient<TEndpoints>;
|
|
287
|
-
/**
|
|
288
|
-
* @param executor - Transport to use for every HTTP call.
|
|
289
|
-
* @param endpoints - Record of named endpoint specs (no URL prefix).
|
|
290
|
-
*/
|
|
291
|
-
declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints): ApiClient<TEndpoints>;
|
|
351
|
+
declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
|
|
292
352
|
|
|
293
353
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* Middlewares are applied in declaration order — the first middleware is the
|
|
298
|
-
* outermost wrapper and runs first on each request.
|
|
299
|
-
*
|
|
300
|
-
* @param execute - The underlying transport function (fetch, axios, etc.).
|
|
301
|
-
* @param middlewares - Ordered list of middlewares to apply.
|
|
302
|
-
*
|
|
303
|
-
* @example
|
|
304
|
-
* ```ts
|
|
305
|
-
* const executor = createExecutor(
|
|
306
|
-
* async ({ method, url, body }) => {
|
|
307
|
-
* const res = await fetch(url, { method, body: JSON.stringify(body) });
|
|
308
|
-
* return res.json();
|
|
309
|
-
* },
|
|
310
|
-
* [withTimeout(5000), withRetry(3), withLogger()],
|
|
311
|
-
* );
|
|
312
|
-
* ```
|
|
354
|
+
* Thrown by {@link withTimeout} when a request exceeds the configured duration.
|
|
355
|
+
* Distinguishable from a user-initiated {@link AbortSignal} cancellation.
|
|
313
356
|
*/
|
|
314
|
-
declare
|
|
315
|
-
|
|
357
|
+
declare class TimeoutError extends Error {
|
|
358
|
+
readonly ms: number;
|
|
359
|
+
constructor(ms: number);
|
|
360
|
+
}
|
|
316
361
|
/**
|
|
317
362
|
* Identity helper that returns the middleware as-is.
|
|
318
363
|
*
|
|
@@ -335,6 +380,8 @@ declare function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware;
|
|
|
335
380
|
*
|
|
336
381
|
* @param count - Number of retries (not counting the initial attempt).
|
|
337
382
|
* @param options.shouldRetry - Return `false` to stop retrying early.
|
|
383
|
+
* Receives the error and a zero-based `attempt` index (0 = first failure,
|
|
384
|
+
* 1 = second failure, …) so you can limit retries by count or error type.
|
|
338
385
|
*
|
|
339
386
|
* @example
|
|
340
387
|
* ```ts
|
|
@@ -369,14 +416,21 @@ declare function withLogger(options?: {
|
|
|
369
416
|
log?: (message: string, data?: unknown) => void;
|
|
370
417
|
}): ExecutorMiddleware;
|
|
371
418
|
|
|
419
|
+
declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Joins URL path segments, normalising repeated slashes and trailing slashes.
|
|
423
|
+
*
|
|
424
|
+
* **Note:** Intended for relative API paths only. Absolute URLs containing
|
|
425
|
+
* `://` will be collapsed (`https://` → `https:/`). Pass absolute URLs
|
|
426
|
+
* directly to the executor instead of through this helper.
|
|
427
|
+
*/
|
|
372
428
|
declare function joinPaths(...segments: string[]): string;
|
|
373
429
|
declare function resolvePath(pathTemplate: string, params?: Record<string, unknown>): string;
|
|
374
430
|
|
|
375
|
-
declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
|
|
376
|
-
|
|
377
431
|
declare class ValidationError extends Error {
|
|
378
|
-
readonly cause?: unknown
|
|
379
|
-
constructor(message: string, cause?: unknown
|
|
432
|
+
readonly cause?: unknown;
|
|
433
|
+
constructor(message: string, cause?: unknown);
|
|
380
434
|
}
|
|
381
435
|
|
|
382
|
-
export { type ApiTypes, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, defineMiddleware, defineRouter, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
|
|
436
|
+
export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// src/define-router.ts
|
|
2
|
+
function isRouterDef(entry) {
|
|
3
|
+
return "prefix" in entry && "endpoints" in entry;
|
|
4
|
+
}
|
|
2
5
|
function defineRouter(prefix, endpoints) {
|
|
3
6
|
return { prefix, endpoints };
|
|
4
7
|
}
|
|
5
8
|
|
|
6
|
-
// src/define-endpoint.ts
|
|
7
|
-
function endpoint(spec) {
|
|
8
|
-
return spec;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
9
|
// src/utils/path.ts
|
|
12
10
|
function joinPaths(...segments) {
|
|
13
11
|
const joined = segments.filter((s) => s !== "").join("/").replace(/\/+/g, "/");
|
|
@@ -17,7 +15,7 @@ function resolvePath(pathTemplate, params) {
|
|
|
17
15
|
if (!params) return pathTemplate;
|
|
18
16
|
return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {
|
|
19
17
|
const value = params[key];
|
|
20
|
-
if (value == null) throw new Error(`Missing path parameter: ${key}`);
|
|
18
|
+
if (value == null || value === "") throw new Error(`Missing path parameter: ${key}`);
|
|
21
19
|
return encodeURIComponent(String(value));
|
|
22
20
|
});
|
|
23
21
|
}
|
|
@@ -26,89 +24,119 @@ function resolvePath(pathTemplate, params) {
|
|
|
26
24
|
var ValidationError = class extends Error {
|
|
27
25
|
constructor(message, cause) {
|
|
28
26
|
super(message);
|
|
29
|
-
this.cause = cause;
|
|
30
27
|
this.name = "ValidationError";
|
|
31
28
|
if (cause !== void 0) {
|
|
32
29
|
Object.defineProperty(this, "cause", {
|
|
33
30
|
value: cause,
|
|
34
|
-
writable:
|
|
35
|
-
enumerable:
|
|
31
|
+
writable: false,
|
|
32
|
+
enumerable: false,
|
|
33
|
+
configurable: true
|
|
36
34
|
});
|
|
37
35
|
}
|
|
38
36
|
}
|
|
39
37
|
};
|
|
40
38
|
|
|
41
39
|
// src/create-api.ts
|
|
42
|
-
function createApi(executor, routerOrPrefixOrEndpoints,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
function createApi(executor, routerOrPrefixOrEndpoints, endpointsArgOrOptions, optionsArg) {
|
|
41
|
+
const { prefix, endpoints, options } = resolveArgs(
|
|
42
|
+
routerOrPrefixOrEndpoints,
|
|
43
|
+
endpointsArgOrOptions,
|
|
44
|
+
optionsArg
|
|
45
|
+
);
|
|
46
|
+
return buildClient(executor, prefix, endpoints, options);
|
|
47
|
+
}
|
|
48
|
+
function resolveArgs(second, third, fourth) {
|
|
49
|
+
if (typeof second === "string") {
|
|
50
|
+
if (!third)
|
|
51
|
+
throw new Error("endpoints is required when prefix is provided");
|
|
52
|
+
return {
|
|
53
|
+
prefix: second,
|
|
54
|
+
endpoints: third,
|
|
55
|
+
options: fourth
|
|
56
|
+
};
|
|
55
57
|
}
|
|
56
|
-
|
|
58
|
+
if (isRouterDef(second)) {
|
|
59
|
+
return {
|
|
60
|
+
prefix: second.prefix,
|
|
61
|
+
endpoints: second.endpoints,
|
|
62
|
+
options: third
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
prefix: "",
|
|
67
|
+
endpoints: second,
|
|
68
|
+
options: third
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function shouldValidate(options, kind) {
|
|
72
|
+
const v = options?.validate;
|
|
73
|
+
if (v === void 0 || v === true) return true;
|
|
74
|
+
if (v === false) return false;
|
|
75
|
+
return v[kind] ?? true;
|
|
57
76
|
}
|
|
58
|
-
function buildClient(executor, prefix, endpoints) {
|
|
77
|
+
function buildClient(executor, prefix, endpoints, options) {
|
|
59
78
|
const client = {};
|
|
60
79
|
for (const [key, entry] of Object.entries(endpoints)) {
|
|
61
|
-
|
|
62
|
-
const nested = entry;
|
|
63
|
-
client[key] = buildClient(executor, joinPaths(prefix, nested.prefix), nested.endpoints);
|
|
64
|
-
} else {
|
|
65
|
-
const spec = entry;
|
|
66
|
-
client[key] = async (params = {}, signal) => {
|
|
67
|
-
let validatedParams = params;
|
|
68
|
-
if (spec.request) {
|
|
69
|
-
try {
|
|
70
|
-
validatedParams = spec.request.parse(params);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
throw new ValidationError("Request validation failed", err);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const url = resolvePath(
|
|
76
|
-
joinPaths(prefix, spec.path),
|
|
77
|
-
validatedParams?.path
|
|
78
|
-
);
|
|
79
|
-
const raw = await executor.execute({
|
|
80
|
-
method: spec.method,
|
|
81
|
-
url,
|
|
82
|
-
params: validatedParams?.query,
|
|
83
|
-
body: validatedParams?.body,
|
|
84
|
-
signal
|
|
85
|
-
});
|
|
86
|
-
let validated;
|
|
87
|
-
try {
|
|
88
|
-
validated = spec.response.parse(raw);
|
|
89
|
-
} catch (err) {
|
|
90
|
-
throw new ValidationError("Response validation failed", err);
|
|
91
|
-
}
|
|
92
|
-
if (spec.adapter) {
|
|
93
|
-
return spec.adapter(validated);
|
|
94
|
-
}
|
|
95
|
-
return validated;
|
|
96
|
-
};
|
|
97
|
-
}
|
|
80
|
+
client[key] = isRouterDef(entry) ? buildClient(executor, joinPaths(prefix, entry.prefix), entry.endpoints, options) : buildEndpointFn(executor, prefix, entry, options);
|
|
98
81
|
}
|
|
99
82
|
return client;
|
|
100
83
|
}
|
|
84
|
+
function buildEndpointFn(executor, prefix, spec, options) {
|
|
85
|
+
return async (params = {}, signal) => {
|
|
86
|
+
let validatedParams = params;
|
|
87
|
+
if (spec.request && shouldValidate(options, "request")) {
|
|
88
|
+
try {
|
|
89
|
+
validatedParams = spec.request.parse(params);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new ValidationError("Request validation failed", err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const url = resolvePath(joinPaths(prefix, spec.path), validatedParams?.path);
|
|
95
|
+
const raw = await executor.execute({
|
|
96
|
+
method: spec.method,
|
|
97
|
+
url,
|
|
98
|
+
params: validatedParams?.query,
|
|
99
|
+
body: validatedParams?.body,
|
|
100
|
+
signal
|
|
101
|
+
});
|
|
102
|
+
let result;
|
|
103
|
+
if (shouldValidate(options, "response")) {
|
|
104
|
+
try {
|
|
105
|
+
result = spec.response.parse(raw);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw new ValidationError("Response validation failed", err);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
result = raw;
|
|
111
|
+
}
|
|
112
|
+
return spec.adapter ? spec.adapter(result) : result;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
101
115
|
|
|
102
116
|
// src/create-executor.ts
|
|
103
117
|
function createExecutor(execute, middlewares = []) {
|
|
104
|
-
const chain = middlewares.reduceRight(
|
|
105
|
-
(next, mw) => (opts) => mw(opts, next),
|
|
106
|
-
execute
|
|
107
|
-
);
|
|
118
|
+
const chain = middlewares.reduceRight((next, mw) => (opts) => mw(opts, next), execute);
|
|
108
119
|
return { execute: chain };
|
|
109
120
|
}
|
|
121
|
+
function dispatchExecutor(resolver) {
|
|
122
|
+
return {
|
|
123
|
+
execute: (opts) => resolver(opts).execute(opts)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/define-endpoint.ts
|
|
128
|
+
function endpoint(spec) {
|
|
129
|
+
return spec;
|
|
130
|
+
}
|
|
110
131
|
|
|
111
132
|
// src/middleware.ts
|
|
133
|
+
var TimeoutError = class extends Error {
|
|
134
|
+
constructor(ms) {
|
|
135
|
+
super(`Request timed out after ${ms}ms`);
|
|
136
|
+
this.ms = ms;
|
|
137
|
+
this.name = "TimeoutError";
|
|
138
|
+
}
|
|
139
|
+
};
|
|
112
140
|
function defineMiddleware(fn) {
|
|
113
141
|
return fn;
|
|
114
142
|
}
|
|
@@ -130,12 +158,14 @@ function withRetry(count, options) {
|
|
|
130
158
|
function withTimeout(ms) {
|
|
131
159
|
return defineMiddleware(async (opts, next) => {
|
|
132
160
|
const controller = new AbortController();
|
|
133
|
-
const timer = setTimeout(() => controller.abort(), ms);
|
|
134
|
-
const signal = opts.signal ? anySignal([opts.signal, controller.signal]) : controller.signal
|
|
161
|
+
const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);
|
|
162
|
+
const { signal, cleanup } = opts.signal ? anySignal([opts.signal, controller.signal]) : { signal: controller.signal, cleanup: () => {
|
|
163
|
+
} };
|
|
135
164
|
try {
|
|
136
165
|
return await next({ ...opts, signal });
|
|
137
166
|
} finally {
|
|
138
167
|
clearTimeout(timer);
|
|
168
|
+
cleanup();
|
|
139
169
|
}
|
|
140
170
|
});
|
|
141
171
|
}
|
|
@@ -143,27 +173,41 @@ function withLogger(options) {
|
|
|
143
173
|
const log = options?.log ?? ((msg, data) => console.log(msg, data));
|
|
144
174
|
return defineMiddleware(async (opts, next) => {
|
|
145
175
|
const start = Date.now();
|
|
146
|
-
log(`[routar] ${opts.method} ${opts.url}`, {
|
|
176
|
+
log(`[routar] ${opts.method} ${opts.url}`, {
|
|
177
|
+
params: opts.params,
|
|
178
|
+
body: opts.body
|
|
179
|
+
});
|
|
147
180
|
try {
|
|
148
181
|
const result = await next(opts);
|
|
149
182
|
log(`[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - start}ms`);
|
|
150
183
|
return result;
|
|
151
184
|
} catch (err) {
|
|
152
|
-
log(
|
|
185
|
+
log(
|
|
186
|
+
`[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - start}ms`,
|
|
187
|
+
err
|
|
188
|
+
);
|
|
153
189
|
throw err;
|
|
154
190
|
}
|
|
155
191
|
});
|
|
156
192
|
}
|
|
157
193
|
function anySignal(signals) {
|
|
158
194
|
const controller = new AbortController();
|
|
159
|
-
|
|
160
|
-
|
|
195
|
+
const onAbort = () => controller.abort();
|
|
196
|
+
const attached = [];
|
|
197
|
+
for (const s of signals) {
|
|
198
|
+
if (s.aborted) {
|
|
161
199
|
controller.abort();
|
|
162
|
-
|
|
200
|
+
break;
|
|
163
201
|
}
|
|
164
|
-
|
|
202
|
+
s.addEventListener("abort", onAbort, { once: true });
|
|
203
|
+
attached.push(s);
|
|
165
204
|
}
|
|
166
|
-
return
|
|
205
|
+
return {
|
|
206
|
+
signal: controller.signal,
|
|
207
|
+
cleanup: () => attached.forEach((s) => {
|
|
208
|
+
s.removeEventListener("abort", onAbort);
|
|
209
|
+
})
|
|
210
|
+
};
|
|
167
211
|
}
|
|
168
212
|
|
|
169
213
|
// src/utils/params.ts
|
|
@@ -175,6 +219,10 @@ function serializeParams(params) {
|
|
|
175
219
|
for (const item of value) {
|
|
176
220
|
if (item != null) result.append(key, String(item));
|
|
177
221
|
}
|
|
222
|
+
} else if (typeof value === "object") {
|
|
223
|
+
throw new TypeError(
|
|
224
|
+
`serializeParams: value for key "${key}" is a plain object. Serialize it to a string before passing as a query parameter.`
|
|
225
|
+
);
|
|
178
226
|
} else {
|
|
179
227
|
result.append(key, String(value));
|
|
180
228
|
}
|
|
@@ -182,6 +230,6 @@ function serializeParams(params) {
|
|
|
182
230
|
return result;
|
|
183
231
|
}
|
|
184
232
|
|
|
185
|
-
export { ValidationError, createApi, createExecutor, defineMiddleware, defineRouter, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
|
|
233
|
+
export { TimeoutError, ValidationError, createApi, createExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
|
|
186
234
|
//# sourceMappingURL=index.js.map
|
|
187
235
|
//# sourceMappingURL=index.js.map
|