@routar/core 1.6.0 → 1.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.cjs CHANGED
@@ -8,6 +8,96 @@ function defineRouter(prefix, endpoints) {
8
8
  return { prefix, endpoints };
9
9
  }
10
10
 
11
+ // src/middleware.ts
12
+ var TimeoutError = class extends Error {
13
+ constructor(ms) {
14
+ super(`Request timed out after ${ms}ms`);
15
+ this.ms = ms;
16
+ this.name = "TimeoutError";
17
+ }
18
+ };
19
+ function definePlugin(plugin) {
20
+ return plugin;
21
+ }
22
+ function logger(options) {
23
+ const log = options?.log ?? ((msg, data) => console.log(msg, data));
24
+ const timings = /* @__PURE__ */ new WeakMap();
25
+ return definePlugin({
26
+ name: "logger",
27
+ onRequest: (opts) => {
28
+ timings.set(opts, Date.now());
29
+ log(`[routar] ${opts.method} ${opts.url}`, {
30
+ params: opts.params,
31
+ body: opts.body
32
+ });
33
+ return opts;
34
+ },
35
+ onResponse: (res, opts) => {
36
+ log(
37
+ `[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - (timings.get(opts) ?? Date.now())}ms`
38
+ );
39
+ timings.delete(opts);
40
+ return res;
41
+ },
42
+ onError: (err, opts) => {
43
+ log(
44
+ `[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,
45
+ err
46
+ );
47
+ timings.delete(opts);
48
+ throw err;
49
+ }
50
+ });
51
+ }
52
+ function withRetry(count, options) {
53
+ return async (opts, next) => {
54
+ let lastError;
55
+ for (let attempt = 0; attempt <= count; attempt++) {
56
+ try {
57
+ return await next(opts);
58
+ } catch (err) {
59
+ lastError = err;
60
+ if (attempt === count) break;
61
+ if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;
62
+ }
63
+ }
64
+ throw lastError;
65
+ };
66
+ }
67
+ function withTimeout(ms) {
68
+ return async (opts, next) => {
69
+ const controller = new AbortController();
70
+ const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);
71
+ const { signal, cleanup } = opts.signal ? anySignal([opts.signal, controller.signal]) : { signal: controller.signal, cleanup: () => {
72
+ } };
73
+ try {
74
+ return await next({ ...opts, signal });
75
+ } finally {
76
+ clearTimeout(timer);
77
+ cleanup();
78
+ }
79
+ };
80
+ }
81
+ function anySignal(signals) {
82
+ const controller = new AbortController();
83
+ const onAbort = () => controller.abort();
84
+ const attached = [];
85
+ for (const s of signals) {
86
+ if (s.aborted) {
87
+ controller.abort();
88
+ break;
89
+ }
90
+ s.addEventListener("abort", onAbort, { once: true });
91
+ attached.push(s);
92
+ }
93
+ return {
94
+ signal: controller.signal,
95
+ cleanup: () => attached.forEach((s) => {
96
+ s.removeEventListener("abort", onAbort);
97
+ })
98
+ };
99
+ }
100
+
11
101
  // src/utils/path.ts
12
102
  function joinPaths(...segments) {
13
103
  const joined = segments.filter((s) => s !== "").join("/").replace(/\/+/g, "/");
@@ -37,6 +127,26 @@ var ValidationError = class extends Error {
37
127
  }
38
128
  }
39
129
  };
130
+ var StandardSchemaError = class extends Error {
131
+ constructor(issues) {
132
+ super(
133
+ issues.map((issue) => issue.message).join("; ") || "Standard Schema validation failed"
134
+ );
135
+ this.name = "StandardSchemaError";
136
+ this.issues = issues;
137
+ }
138
+ };
139
+
140
+ // src/utils/run-validator.ts
141
+ async function runValidator(validator, data) {
142
+ if (typeof validator.parse === "function") {
143
+ return validator.parse(data);
144
+ }
145
+ const standard = validator["~standard"];
146
+ const result = await standard.validate(data);
147
+ if (result.issues) throw new StandardSchemaError(result.issues);
148
+ return result.value;
149
+ }
40
150
 
41
151
  // src/create-api.ts
42
152
  function createApi(executor, routerOrPrefixOrEndpoints, endpointsArgOrOptions, optionsArg) {
@@ -75,11 +185,10 @@ function resolveArgs(second, third, fourth) {
75
185
  options: third
76
186
  };
77
187
  }
78
- function shouldValidate(options, kind) {
188
+ function validateMode(options, kind) {
79
189
  const v = options?.validate;
80
- if (v === void 0 || v === true) return true;
81
- if (v === false) return false;
82
- return v[kind] ?? true;
190
+ const mode = v === void 0 ? true : typeof v === "boolean" || v === "warn" ? v : v[kind] ?? true;
191
+ return mode === "warn" ? "warn" : mode ? "on" : "off";
83
192
  }
84
193
  function buildClient(executor, prefix, endpoints, options) {
85
194
  const client = {};
@@ -99,39 +208,117 @@ function buildClient(executor, prefix, endpoints, options) {
99
208
  return client;
100
209
  }
101
210
  function buildEndpointFn(executor, prefix, spec, options) {
102
- return async (params = {}, signal) => {
211
+ return async (params = {}, signalOrOptions) => {
212
+ const call = normalizeCallOptions(signalOrOptions);
213
+ const reqMode = validateMode(options, "request");
103
214
  let validatedParams = params;
104
- if (spec.request && shouldValidate(options, "request")) {
215
+ let requestError = null;
216
+ if (spec.request && reqMode !== "off") {
105
217
  try {
106
- validatedParams = spec.request.parse(params);
218
+ validatedParams = await runValidator(
219
+ spec.request,
220
+ params
221
+ );
107
222
  } catch (err) {
108
- throw new ValidationError("Request validation failed", err);
223
+ requestError = new ValidationError("Request validation failed", err);
224
+ if (reqMode !== "warn") throw requestError;
225
+ validatedParams = params;
109
226
  }
110
227
  }
111
228
  const url = resolvePath(
112
229
  joinPaths(prefix, spec.path),
113
230
  validatedParams?.path
114
231
  );
115
- const raw = await executor.execute({
232
+ if (requestError) {
233
+ options?.onValidationError?.(requestError, {
234
+ kind: "request",
235
+ method: spec.method,
236
+ url,
237
+ data: params
238
+ });
239
+ }
240
+ const raw = await executeWithTimeout(executor, call.timeout, {
116
241
  method: spec.method,
117
242
  url,
118
243
  params: validatedParams?.query,
119
244
  body: validatedParams?.body,
120
- signal
245
+ headers: call.headers,
246
+ signal: call.signal
121
247
  });
248
+ const resMode = validateMode(options, "response");
122
249
  let result;
123
- if (shouldValidate(options, "response")) {
250
+ if (resMode === "off") {
251
+ result = raw;
252
+ } else {
124
253
  try {
125
- result = spec.response.parse(raw);
254
+ result = await runValidator(
255
+ spec.response,
256
+ raw
257
+ );
126
258
  } catch (err) {
127
- throw new ValidationError("Response validation failed", err);
259
+ const responseError = new ValidationError(
260
+ "Response validation failed",
261
+ err
262
+ );
263
+ if (resMode !== "warn") throw responseError;
264
+ options?.onValidationError?.(responseError, {
265
+ kind: "response",
266
+ method: spec.method,
267
+ url,
268
+ data: raw
269
+ });
270
+ result = raw;
128
271
  }
129
- } else {
130
- result = raw;
131
272
  }
132
273
  return spec.adapter ? spec.adapter(result) : result;
133
274
  };
134
275
  }
276
+ function normalizeCallOptions(arg) {
277
+ if (arg == null) return {};
278
+ if (isAbortSignal(arg)) return { signal: arg };
279
+ return arg;
280
+ }
281
+ function isAbortSignal(value) {
282
+ if (typeof AbortSignal !== "undefined" && value instanceof AbortSignal) {
283
+ return true;
284
+ }
285
+ return typeof value === "object" && value !== null && typeof value.aborted === "boolean" && typeof value.addEventListener === "function";
286
+ }
287
+ function executeWithTimeout(executor, timeout, opts) {
288
+ if (timeout == null) return executor.execute(opts);
289
+ const controller = new AbortController();
290
+ const timer = setTimeout(
291
+ () => controller.abort(new TimeoutError(timeout)),
292
+ timeout
293
+ );
294
+ const { signal, cleanup } = combineSignals(opts.signal, controller.signal);
295
+ return (async () => {
296
+ try {
297
+ return await executor.execute({ ...opts, signal });
298
+ } finally {
299
+ clearTimeout(timer);
300
+ cleanup();
301
+ }
302
+ })();
303
+ }
304
+ function combineSignals(caller, timeoutSignal) {
305
+ if (!caller) return { signal: timeoutSignal, cleanup: () => {
306
+ } };
307
+ if (caller.aborted) return { signal: caller, cleanup: () => {
308
+ } };
309
+ const controller = new AbortController();
310
+ const onCallerAbort = () => controller.abort(caller.reason);
311
+ const onTimeoutAbort = () => controller.abort(timeoutSignal.reason);
312
+ caller.addEventListener("abort", onCallerAbort, { once: true });
313
+ timeoutSignal.addEventListener("abort", onTimeoutAbort, { once: true });
314
+ return {
315
+ signal: controller.signal,
316
+ cleanup: () => {
317
+ caller.removeEventListener("abort", onCallerAbort);
318
+ timeoutSignal.removeEventListener("abort", onTimeoutAbort);
319
+ }
320
+ };
321
+ }
135
322
 
136
323
  // src/create-executor.ts
137
324
  function pluginToMiddleware(plugin) {
@@ -170,96 +357,6 @@ function dispatchExecutor(resolver) {
170
357
  };
171
358
  }
172
359
 
173
- // src/middleware.ts
174
- var TimeoutError = class extends Error {
175
- constructor(ms) {
176
- super(`Request timed out after ${ms}ms`);
177
- this.ms = ms;
178
- this.name = "TimeoutError";
179
- }
180
- };
181
- function definePlugin(plugin) {
182
- return plugin;
183
- }
184
- function logger(options) {
185
- const log = options?.log ?? ((msg, data) => console.log(msg, data));
186
- const timings = /* @__PURE__ */ new WeakMap();
187
- return definePlugin({
188
- name: "logger",
189
- onRequest: (opts) => {
190
- timings.set(opts, Date.now());
191
- log(`[routar] ${opts.method} ${opts.url}`, {
192
- params: opts.params,
193
- body: opts.body
194
- });
195
- return opts;
196
- },
197
- onResponse: (res, opts) => {
198
- log(
199
- `[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - (timings.get(opts) ?? Date.now())}ms`
200
- );
201
- timings.delete(opts);
202
- return res;
203
- },
204
- onError: (err, opts) => {
205
- log(
206
- `[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,
207
- err
208
- );
209
- timings.delete(opts);
210
- throw err;
211
- }
212
- });
213
- }
214
- function withRetry(count, options) {
215
- return async (opts, next) => {
216
- let lastError;
217
- for (let attempt = 0; attempt <= count; attempt++) {
218
- try {
219
- return await next(opts);
220
- } catch (err) {
221
- lastError = err;
222
- if (attempt === count) break;
223
- if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;
224
- }
225
- }
226
- throw lastError;
227
- };
228
- }
229
- function withTimeout(ms) {
230
- return async (opts, next) => {
231
- const controller = new AbortController();
232
- const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);
233
- const { signal, cleanup } = opts.signal ? anySignal([opts.signal, controller.signal]) : { signal: controller.signal, cleanup: () => {
234
- } };
235
- try {
236
- return await next({ ...opts, signal });
237
- } finally {
238
- clearTimeout(timer);
239
- cleanup();
240
- }
241
- };
242
- }
243
- function anySignal(signals) {
244
- const controller = new AbortController();
245
- const onAbort = () => controller.abort();
246
- const attached = [];
247
- for (const s of signals) {
248
- if (s.aborted) {
249
- controller.abort();
250
- break;
251
- }
252
- s.addEventListener("abort", onAbort, { once: true });
253
- attached.push(s);
254
- }
255
- return {
256
- signal: controller.signal,
257
- cleanup: () => attached.forEach((s) => {
258
- s.removeEventListener("abort", onAbort);
259
- })
260
- };
261
- }
262
-
263
360
  // src/utils/params.ts
264
361
  function serializeParams(params) {
265
362
  const result = new URLSearchParams();
@@ -343,12 +440,82 @@ var HttpError = class extends Error {
343
440
  }
344
441
  };
345
442
 
443
+ // src/utils/compose-request.ts
444
+ var BUCKET_KEYS = ["path", "query", "body"];
445
+ function hasParse(v) {
446
+ return typeof v.parse === "function";
447
+ }
448
+ async function validateBucket(validator, value) {
449
+ if (hasParse(validator)) {
450
+ try {
451
+ return { value: validator.parse(value) };
452
+ } catch (err) {
453
+ const message = err instanceof Error ? err.message : String(err);
454
+ return { issues: [{ message }] };
455
+ }
456
+ }
457
+ return validator["~standard"].validate(value);
458
+ }
459
+ function composeRequest(buckets) {
460
+ const entries = BUCKET_KEYS.filter(
461
+ (k) => buckets[k] !== void 0
462
+ ).map((k) => [k, buckets[k]]);
463
+ const shape = {};
464
+ for (const [k, v] of entries) shape[k] = v;
465
+ const standard = {
466
+ "~standard": {
467
+ version: 1,
468
+ vendor: "routar",
469
+ validate: async (data) => {
470
+ const input = data ?? {};
471
+ const out = {};
472
+ const issues = [];
473
+ for (const [k, v] of entries) {
474
+ const result = await validateBucket(v, input[k]);
475
+ if (result.issues) {
476
+ for (const issue of result.issues) {
477
+ issues.push({ message: issue.message, path: [k, ...issue.path ?? []] });
478
+ }
479
+ } else {
480
+ out[k] = result.value;
481
+ }
482
+ }
483
+ return issues.length > 0 ? { issues } : { value: out };
484
+ }
485
+ }
486
+ };
487
+ if (entries.every(([, v]) => hasParse(v))) {
488
+ const parse = (data) => {
489
+ const input = data ?? {};
490
+ const out = {};
491
+ for (const [k, v] of entries) {
492
+ out[k] = v.parse(input[k]);
493
+ }
494
+ return out;
495
+ };
496
+ return Object.assign(standard, { parse, shape });
497
+ }
498
+ return Object.assign(standard, { shape });
499
+ }
500
+
346
501
  // src/define-endpoint.ts
347
502
  function endpoint(spec) {
503
+ if (spec.request === void 0 && (spec.pathParams !== void 0 || spec.query !== void 0 || spec.body !== void 0)) {
504
+ const { pathParams, query, body, ...rest } = spec;
505
+ return {
506
+ ...rest,
507
+ request: composeRequest({
508
+ path: pathParams,
509
+ query,
510
+ body
511
+ })
512
+ };
513
+ }
348
514
  return spec;
349
515
  }
350
516
 
351
517
  exports.HttpError = HttpError;
518
+ exports.StandardSchemaError = StandardSchemaError;
352
519
  exports.TimeoutError = TimeoutError;
353
520
  exports.ValidationError = ValidationError;
354
521
  exports.createApi = createApi;
@@ -362,6 +529,7 @@ exports.isRouterDef = isRouterDef;
362
529
  exports.joinPaths = joinPaths;
363
530
  exports.logger = logger;
364
531
  exports.resolvePath = resolvePath;
532
+ exports.runValidator = runValidator;
365
533
  exports.serializeParams = serializeParams;
366
534
  //# sourceMappingURL=index.cjs.map
367
535
  //# sourceMappingURL=index.cjs.map