@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 +273 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -24
- package/dist/index.d.ts +252 -24
- package/dist/index.js +272 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
188
|
+
function validateMode(options, kind) {
|
|
79
189
|
const v = options?.validate;
|
|
80
|
-
|
|
81
|
-
|
|
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 = {},
|
|
211
|
+
return async (params = {}, signalOrOptions) => {
|
|
212
|
+
const call = normalizeCallOptions(signalOrOptions);
|
|
213
|
+
const reqMode = validateMode(options, "request");
|
|
103
214
|
let validatedParams = params;
|
|
104
|
-
|
|
215
|
+
let requestError = null;
|
|
216
|
+
if (spec.request && reqMode !== "off") {
|
|
105
217
|
try {
|
|
106
|
-
validatedParams =
|
|
218
|
+
validatedParams = await runValidator(
|
|
219
|
+
spec.request,
|
|
220
|
+
params
|
|
221
|
+
);
|
|
107
222
|
} catch (err) {
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
+
headers: call.headers,
|
|
246
|
+
signal: call.signal
|
|
121
247
|
});
|
|
248
|
+
const resMode = validateMode(options, "response");
|
|
122
249
|
let result;
|
|
123
|
-
if (
|
|
250
|
+
if (resMode === "off") {
|
|
251
|
+
result = raw;
|
|
252
|
+
} else {
|
|
124
253
|
try {
|
|
125
|
-
result =
|
|
254
|
+
result = await runValidator(
|
|
255
|
+
spec.response,
|
|
256
|
+
raw
|
|
257
|
+
);
|
|
126
258
|
} catch (err) {
|
|
127
|
-
|
|
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
|