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