vector-framework 1.0.0 → 1.2.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 +87 -634
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +5 -2
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +21 -12
- package/dist/cache/manager.js.map +1 -1
- package/dist/cli/index.js +60 -126
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/option-resolution.d.ts +4 -0
- package/dist/cli/option-resolution.d.ts.map +1 -0
- package/dist/cli/option-resolution.js +28 -0
- package/dist/cli/option-resolution.js.map +1 -0
- package/dist/cli.js +2774 -599
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/core/config-loader.d.ts +2 -2
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +18 -18
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +41 -15
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +465 -150
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +17 -3
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +274 -33
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +9 -8
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +40 -32
- package/dist/core/vector.js.map +1 -1
- package/dist/dev/route-generator.d.ts.map +1 -1
- package/dist/dev/route-generator.js.map +1 -1
- package/dist/dev/route-scanner.d.ts +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +37 -43
- package/dist/dev/route-scanner.js.map +1 -1
- package/dist/http.d.ts +14 -14
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +84 -84
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1314 -8
- package/dist/index.mjs +1314 -8
- package/dist/middleware/manager.d.ts +1 -1
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +4 -0
- package/dist/middleware/manager.js.map +1 -1
- package/dist/openapi/docs-ui.d.ts +2 -0
- package/dist/openapi/docs-ui.d.ts.map +1 -0
- package/dist/openapi/docs-ui.js +1313 -0
- package/dist/openapi/docs-ui.js.map +1 -0
- package/dist/openapi/generator.d.ts +12 -0
- package/dist/openapi/generator.d.ts.map +1 -0
- package/dist/openapi/generator.js +273 -0
- package/dist/openapi/generator.js.map +1 -0
- package/dist/types/index.d.ts +70 -11
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/standard-schema.d.ts +118 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/utils/cors.d.ts +13 -0
- package/dist/utils/cors.d.ts.map +1 -0
- package/dist/utils/cors.js +89 -0
- package/dist/utils/cors.js.map +1 -0
- package/dist/utils/path.d.ts +7 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +14 -3
- package/dist/utils/path.js.map +1 -1
- package/dist/utils/schema-validation.d.ts +31 -0
- package/dist/utils/schema-validation.d.ts.map +1 -0
- package/dist/utils/schema-validation.js +77 -0
- package/dist/utils/schema-validation.js.map +1 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +1 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +24 -19
- package/src/auth/protected.ts +3 -13
- package/src/cache/manager.ts +25 -30
- package/src/cli/index.ts +62 -141
- package/src/cli/option-resolution.ts +40 -0
- package/src/constants/index.ts +7 -0
- package/src/core/config-loader.ts +20 -22
- package/src/core/router.ts +535 -155
- package/src/core/server.ts +354 -45
- package/src/core/vector.ts +71 -61
- package/src/dev/route-generator.ts +1 -3
- package/src/dev/route-scanner.ts +38 -51
- package/src/http.ts +117 -187
- package/src/index.ts +3 -3
- package/src/middleware/manager.ts +8 -11
- package/src/openapi/assets/tailwindcdn.js +83 -0
- package/src/openapi/docs-ui.ts +1317 -0
- package/src/openapi/generator.ts +359 -0
- package/src/types/index.ts +104 -17
- package/src/types/standard-schema.ts +147 -0
- package/src/utils/cors.ts +101 -0
- package/src/utils/path.ts +19 -4
- package/src/utils/schema-validation.ts +123 -0
- package/src/utils/validation.ts +1 -0
package/dist/core/router.js
CHANGED
|
@@ -1,92 +1,169 @@
|
|
|
1
|
-
import { withCookies } from 'itty-router';
|
|
2
1
|
import { APIError, createResponse } from '../http';
|
|
2
|
+
import { STATIC_RESPONSES } from '../constants';
|
|
3
|
+
import { buildRouteRegex } from '../utils/path';
|
|
4
|
+
import { createValidationErrorPayload, extractThrownIssues, isStandardRouteSchema, normalizeValidationIssues, runStandardValidation, } from '../utils/schema-validation';
|
|
3
5
|
export class VectorRouter {
|
|
4
6
|
middlewareManager;
|
|
5
7
|
authManager;
|
|
6
8
|
cacheManager;
|
|
7
|
-
|
|
9
|
+
routeBooleanDefaults = {};
|
|
10
|
+
developmentMode = undefined;
|
|
11
|
+
routeDefinitions = [];
|
|
12
|
+
routeTable = Object.create(null);
|
|
13
|
+
routeMatchers = [];
|
|
14
|
+
corsHeadersEntries = null;
|
|
15
|
+
corsHandler = null;
|
|
8
16
|
constructor(middlewareManager, authManager, cacheManager) {
|
|
9
17
|
this.middlewareManager = middlewareManager;
|
|
10
18
|
this.authManager = authManager;
|
|
11
19
|
this.cacheManager = cacheManager;
|
|
12
20
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
setCorsHeaders(entries) {
|
|
22
|
+
this.corsHeadersEntries = entries;
|
|
23
|
+
}
|
|
24
|
+
setCorsHandler(handler) {
|
|
25
|
+
this.corsHandler = handler;
|
|
26
|
+
}
|
|
27
|
+
setRouteBooleanDefaults(defaults) {
|
|
28
|
+
this.routeBooleanDefaults = { ...defaults };
|
|
29
|
+
}
|
|
30
|
+
setDevelopmentMode(mode) {
|
|
31
|
+
this.developmentMode = mode;
|
|
32
|
+
}
|
|
33
|
+
applyRouteBooleanDefaults(options) {
|
|
34
|
+
const resolved = { ...options };
|
|
35
|
+
const defaults = this.routeBooleanDefaults;
|
|
36
|
+
const keys = ['auth', 'expose', 'rawRequest', 'validate', 'rawResponse'];
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
if (resolved[key] === undefined && defaults[key] !== undefined) {
|
|
39
|
+
resolved[key] = defaults[key];
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
|
-
|
|
32
|
-
if (this.isExactPath(path)) {
|
|
33
|
-
score += EXACT_MATCH_BONUS;
|
|
34
|
-
}
|
|
35
|
-
return score;
|
|
42
|
+
return resolved;
|
|
36
43
|
}
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
route(options, handler) {
|
|
45
|
+
const resolvedOptions = this.applyRouteBooleanDefaults(options);
|
|
46
|
+
const method = resolvedOptions.method.toUpperCase();
|
|
47
|
+
const path = resolvedOptions.path;
|
|
48
|
+
const wrappedHandler = this.wrapHandler(resolvedOptions, handler);
|
|
49
|
+
const methodMap = this.getOrCreateMethodMap(path);
|
|
50
|
+
methodMap[method] = wrappedHandler;
|
|
51
|
+
this.routeDefinitions.push({
|
|
52
|
+
method,
|
|
53
|
+
path,
|
|
54
|
+
options: resolvedOptions,
|
|
55
|
+
});
|
|
39
56
|
}
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
addRoute(entry) {
|
|
58
|
+
const [method, , handlers, path] = entry;
|
|
59
|
+
if (!path)
|
|
60
|
+
return;
|
|
61
|
+
const methodMap = this.getOrCreateMethodMap(path);
|
|
62
|
+
methodMap[method.toUpperCase()] = handlers[0];
|
|
63
|
+
const normalizedMethod = method.toUpperCase();
|
|
64
|
+
this.routeDefinitions.push({
|
|
65
|
+
method: normalizedMethod,
|
|
66
|
+
path,
|
|
67
|
+
options: {
|
|
68
|
+
method: normalizedMethod,
|
|
69
|
+
path,
|
|
70
|
+
expose: true,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
42
73
|
}
|
|
43
|
-
|
|
44
|
-
|
|
74
|
+
bulkAddRoutes(entries) {
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
this.addRoute(entry);
|
|
77
|
+
}
|
|
45
78
|
}
|
|
46
|
-
|
|
47
|
-
|
|
79
|
+
addStaticRoute(path, response) {
|
|
80
|
+
const existing = this.routeTable[path];
|
|
81
|
+
if (existing && !(existing instanceof Response)) {
|
|
82
|
+
throw new Error(`Cannot register static route for path "${path}" because method routes already exist.`);
|
|
83
|
+
}
|
|
84
|
+
this.routeTable[path] = response;
|
|
85
|
+
this.removeRouteMatcher(path);
|
|
48
86
|
}
|
|
49
|
-
|
|
50
|
-
this.
|
|
51
|
-
const pathA = this.extractPath(a);
|
|
52
|
-
const pathB = this.extractPath(b);
|
|
53
|
-
const scoreA = this.getRouteSpecificity(pathA);
|
|
54
|
-
const scoreB = this.getRouteSpecificity(pathB);
|
|
55
|
-
return scoreB - scoreA;
|
|
56
|
-
});
|
|
87
|
+
getRouteTable() {
|
|
88
|
+
return this.routeTable;
|
|
57
89
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
// Legacy compatibility: returns route entries in a flat list for tests
|
|
91
|
+
getRoutes() {
|
|
92
|
+
const routes = [];
|
|
93
|
+
for (const matcher of this.routeMatchers) {
|
|
94
|
+
const value = this.routeTable[matcher.path];
|
|
95
|
+
if (!value || value instanceof Response)
|
|
96
|
+
continue;
|
|
97
|
+
for (const [method, handler] of Object.entries(value)) {
|
|
98
|
+
routes.push([method, matcher.regex, [handler], matcher.path]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return routes;
|
|
61
102
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const routeEntry = [
|
|
65
|
-
options.method.toUpperCase(),
|
|
66
|
-
this.createRouteRegex(options.path),
|
|
67
|
-
[wrappedHandler],
|
|
68
|
-
options.path,
|
|
69
|
-
];
|
|
70
|
-
this.routes.push(routeEntry);
|
|
71
|
-
this.sortRoutes(); // Sort routes after adding
|
|
72
|
-
return routeEntry;
|
|
103
|
+
getRouteDefinitions() {
|
|
104
|
+
return [...this.routeDefinitions];
|
|
73
105
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
clearRoutes() {
|
|
107
|
+
this.routeTable = Object.create(null);
|
|
108
|
+
this.routeMatchers = [];
|
|
109
|
+
this.routeDefinitions = [];
|
|
110
|
+
}
|
|
111
|
+
// Legacy shim — no-op (Bun handles route priority natively)
|
|
112
|
+
sortRoutes() { }
|
|
113
|
+
// Compatibility handle() for unit tests — mirrors Bun's native routing without a server
|
|
114
|
+
async handle(request) {
|
|
115
|
+
let url;
|
|
116
|
+
try {
|
|
117
|
+
url = new URL(request.url);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return APIError.badRequest('Malformed request URL');
|
|
121
|
+
}
|
|
122
|
+
request._parsedUrl = url;
|
|
123
|
+
const pathname = url.pathname;
|
|
124
|
+
for (const matcher of this.routeMatchers) {
|
|
125
|
+
const path = matcher.path;
|
|
126
|
+
const value = this.routeTable[path];
|
|
127
|
+
if (!value)
|
|
128
|
+
continue;
|
|
129
|
+
if (value instanceof Response)
|
|
130
|
+
continue;
|
|
131
|
+
const methodMap = value;
|
|
132
|
+
if (request.method === 'OPTIONS' || request.method in methodMap) {
|
|
133
|
+
const match = pathname.match(matcher.regex);
|
|
134
|
+
if (match) {
|
|
135
|
+
try {
|
|
136
|
+
request.params = match.groups ?? {};
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Request.params can be readonly on Bun-native requests.
|
|
140
|
+
}
|
|
141
|
+
const handler = methodMap[request.method] ?? methodMap['GET'];
|
|
142
|
+
if (handler) {
|
|
143
|
+
const response = await handler(request);
|
|
144
|
+
if (response)
|
|
145
|
+
return response;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return STATIC_RESPONSES.NOT_FOUND.clone();
|
|
81
151
|
}
|
|
82
152
|
prepareRequest(request, options) {
|
|
83
|
-
// Initialize context if not present
|
|
84
153
|
if (!request.context) {
|
|
85
154
|
request.context = {};
|
|
86
155
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
request.params
|
|
156
|
+
const hasEmptyParamsObject = !!request.params &&
|
|
157
|
+
typeof request.params === 'object' &&
|
|
158
|
+
!Array.isArray(request.params) &&
|
|
159
|
+
Object.keys(request.params).length === 0;
|
|
160
|
+
if (options?.params !== undefined && (request.params === undefined || hasEmptyParamsObject)) {
|
|
161
|
+
try {
|
|
162
|
+
request.params = options.params;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// params is readonly (set by Bun natively) — use as-is
|
|
166
|
+
}
|
|
90
167
|
}
|
|
91
168
|
if (options?.route !== undefined) {
|
|
92
169
|
request.route = options.route;
|
|
@@ -94,114 +171,190 @@ export class VectorRouter {
|
|
|
94
171
|
if (options?.metadata !== undefined) {
|
|
95
172
|
request.metadata = options.metadata;
|
|
96
173
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
174
|
+
if (request.query == null && request.url) {
|
|
175
|
+
try {
|
|
176
|
+
Object.defineProperty(request, 'query', {
|
|
177
|
+
get() {
|
|
178
|
+
const url = this._parsedUrl ?? new URL(this.url);
|
|
179
|
+
const query = VectorRouter.parseQuery(url);
|
|
180
|
+
Object.defineProperty(this, 'query', {
|
|
181
|
+
value: query,
|
|
182
|
+
writable: true,
|
|
183
|
+
configurable: true,
|
|
184
|
+
enumerable: true,
|
|
185
|
+
});
|
|
186
|
+
return query;
|
|
187
|
+
},
|
|
188
|
+
set(value) {
|
|
189
|
+
Object.defineProperty(this, 'query', {
|
|
190
|
+
value,
|
|
191
|
+
writable: true,
|
|
192
|
+
configurable: true,
|
|
193
|
+
enumerable: true,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
configurable: true,
|
|
197
|
+
enumerable: true,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
const url = request._parsedUrl ?? new URL(request.url);
|
|
202
|
+
try {
|
|
203
|
+
request.query = VectorRouter.parseQuery(url);
|
|
109
204
|
}
|
|
110
|
-
|
|
111
|
-
query
|
|
205
|
+
catch {
|
|
206
|
+
// Leave query as-is when request shape is non-extensible.
|
|
112
207
|
}
|
|
113
208
|
}
|
|
114
|
-
request.query = query;
|
|
115
209
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
210
|
+
if (!Object.getOwnPropertyDescriptor(request, 'cookies')) {
|
|
211
|
+
Object.defineProperty(request, 'cookies', {
|
|
212
|
+
get() {
|
|
213
|
+
const cookieHeader = this.headers.get('cookie') ?? '';
|
|
214
|
+
const cookies = {};
|
|
215
|
+
if (cookieHeader) {
|
|
216
|
+
for (const pair of cookieHeader.split(';')) {
|
|
217
|
+
const idx = pair.indexOf('=');
|
|
218
|
+
if (idx > 0) {
|
|
219
|
+
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
Object.defineProperty(this, 'cookies', {
|
|
224
|
+
value: cookies,
|
|
225
|
+
writable: true,
|
|
226
|
+
configurable: true,
|
|
227
|
+
enumerable: true,
|
|
228
|
+
});
|
|
229
|
+
return cookies;
|
|
230
|
+
},
|
|
231
|
+
configurable: true,
|
|
232
|
+
enumerable: true,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
resolveFallbackParams(request, routeMatcher) {
|
|
237
|
+
if (!routeMatcher) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
const currentParams = request.params;
|
|
241
|
+
if (currentParams &&
|
|
242
|
+
typeof currentParams === 'object' &&
|
|
243
|
+
!Array.isArray(currentParams) &&
|
|
244
|
+
Object.keys(currentParams).length > 0) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
let pathname;
|
|
248
|
+
try {
|
|
249
|
+
pathname = (request._parsedUrl ?? new URL(request.url)).pathname;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return undefined;
|
|
119
253
|
}
|
|
254
|
+
const matched = pathname.match(routeMatcher);
|
|
255
|
+
if (!matched?.groups) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
return matched.groups;
|
|
120
259
|
}
|
|
121
260
|
wrapHandler(options, handler) {
|
|
261
|
+
const routePath = options.path;
|
|
262
|
+
const routeMatcher = routePath.includes(':') ? buildRouteRegex(routePath) : null;
|
|
122
263
|
return async (request) => {
|
|
123
|
-
// Ensure request has required properties
|
|
124
264
|
const vectorRequest = request;
|
|
125
|
-
|
|
265
|
+
const fallbackParams = this.resolveFallbackParams(request, routeMatcher);
|
|
126
266
|
this.prepareRequest(vectorRequest, {
|
|
127
|
-
|
|
267
|
+
params: fallbackParams,
|
|
268
|
+
route: routePath,
|
|
269
|
+
metadata: options.metadata,
|
|
128
270
|
});
|
|
129
|
-
request = vectorRequest;
|
|
130
271
|
try {
|
|
131
|
-
// Default expose to true if not specified
|
|
132
272
|
if (options.expose === false) {
|
|
133
273
|
return APIError.forbidden('Forbidden');
|
|
134
274
|
}
|
|
135
|
-
const beforeResult = await this.middlewareManager.executeBefore(
|
|
275
|
+
const beforeResult = await this.middlewareManager.executeBefore(vectorRequest);
|
|
136
276
|
if (beforeResult instanceof Response) {
|
|
137
277
|
return beforeResult;
|
|
138
278
|
}
|
|
139
|
-
|
|
279
|
+
const req = beforeResult;
|
|
140
280
|
if (options.auth) {
|
|
141
281
|
try {
|
|
142
|
-
await this.authManager.authenticate(
|
|
282
|
+
await this.authManager.authenticate(req);
|
|
143
283
|
}
|
|
144
284
|
catch (error) {
|
|
145
285
|
return APIError.unauthorized(error instanceof Error ? error.message : 'Authentication failed', options.responseContentType);
|
|
146
286
|
}
|
|
147
287
|
}
|
|
148
|
-
if (!options.rawRequest &&
|
|
288
|
+
if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
|
|
289
|
+
let parsedContent = null;
|
|
149
290
|
try {
|
|
150
|
-
const contentType =
|
|
151
|
-
if (contentType?.
|
|
152
|
-
|
|
291
|
+
const contentType = req.headers.get('content-type');
|
|
292
|
+
if (contentType?.startsWith('application/json')) {
|
|
293
|
+
parsedContent = await req.json();
|
|
153
294
|
}
|
|
154
|
-
else if (contentType?.
|
|
155
|
-
|
|
295
|
+
else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
|
|
296
|
+
parsedContent = Object.fromEntries(await req.formData());
|
|
156
297
|
}
|
|
157
|
-
else if (contentType?.
|
|
158
|
-
|
|
298
|
+
else if (contentType?.startsWith('multipart/form-data')) {
|
|
299
|
+
parsedContent = await req.formData();
|
|
159
300
|
}
|
|
160
301
|
else {
|
|
161
|
-
|
|
302
|
+
parsedContent = await req.text();
|
|
162
303
|
}
|
|
163
304
|
}
|
|
164
305
|
catch {
|
|
165
|
-
|
|
306
|
+
parsedContent = null;
|
|
166
307
|
}
|
|
308
|
+
this.setContentAndBodyAlias(req, parsedContent);
|
|
309
|
+
}
|
|
310
|
+
const inputValidationResponse = await this.validateInputSchema(req, options);
|
|
311
|
+
if (inputValidationResponse) {
|
|
312
|
+
return inputValidationResponse;
|
|
167
313
|
}
|
|
168
314
|
let result;
|
|
169
315
|
const cacheOptions = options.cache;
|
|
170
|
-
// Create cache factory that handles Response objects
|
|
171
|
-
const cacheFactory = async () => {
|
|
172
|
-
const res = await handler(request);
|
|
173
|
-
// If Response, extract data for caching
|
|
174
|
-
if (res instanceof Response) {
|
|
175
|
-
return {
|
|
176
|
-
_isResponse: true,
|
|
177
|
-
body: await res.text(),
|
|
178
|
-
status: res.status,
|
|
179
|
-
headers: Object.fromEntries(res.headers.entries())
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
return res;
|
|
183
|
-
};
|
|
184
316
|
if (cacheOptions && typeof cacheOptions === 'number' && cacheOptions > 0) {
|
|
185
|
-
const cacheKey = this.cacheManager.generateKey(
|
|
186
|
-
authUser:
|
|
317
|
+
const cacheKey = this.cacheManager.generateKey(req, {
|
|
318
|
+
authUser: req.authUser,
|
|
187
319
|
});
|
|
188
|
-
result = await this.cacheManager.get(cacheKey,
|
|
320
|
+
result = await this.cacheManager.get(cacheKey, async () => {
|
|
321
|
+
const res = await handler(req);
|
|
322
|
+
if (res instanceof Response) {
|
|
323
|
+
return {
|
|
324
|
+
_isResponse: true,
|
|
325
|
+
body: await res.text(),
|
|
326
|
+
status: res.status,
|
|
327
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return res;
|
|
331
|
+
}, cacheOptions);
|
|
189
332
|
}
|
|
190
333
|
else if (cacheOptions && typeof cacheOptions === 'object' && cacheOptions.ttl) {
|
|
191
334
|
const cacheKey = cacheOptions.key ||
|
|
192
|
-
this.cacheManager.generateKey(
|
|
193
|
-
authUser:
|
|
335
|
+
this.cacheManager.generateKey(req, {
|
|
336
|
+
authUser: req.authUser,
|
|
194
337
|
});
|
|
195
|
-
result = await this.cacheManager.get(cacheKey,
|
|
338
|
+
result = await this.cacheManager.get(cacheKey, async () => {
|
|
339
|
+
const res = await handler(req);
|
|
340
|
+
if (res instanceof Response) {
|
|
341
|
+
return {
|
|
342
|
+
_isResponse: true,
|
|
343
|
+
body: await res.text(),
|
|
344
|
+
status: res.status,
|
|
345
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return res;
|
|
349
|
+
}, cacheOptions.ttl);
|
|
196
350
|
}
|
|
197
351
|
else {
|
|
198
|
-
result = await handler(
|
|
352
|
+
result = await handler(req);
|
|
199
353
|
}
|
|
200
|
-
// Reconstruct Response if it was cached
|
|
201
354
|
if (result && typeof result === 'object' && result._isResponse === true) {
|
|
202
355
|
result = new Response(result.body, {
|
|
203
356
|
status: result.status,
|
|
204
|
-
headers: result.headers
|
|
357
|
+
headers: result.headers,
|
|
205
358
|
});
|
|
206
359
|
}
|
|
207
360
|
let response;
|
|
@@ -211,7 +364,20 @@ export class VectorRouter {
|
|
|
211
364
|
else {
|
|
212
365
|
response = createResponse(200, result, options.responseContentType);
|
|
213
366
|
}
|
|
214
|
-
response = await this.middlewareManager.executeFinally(response,
|
|
367
|
+
response = await this.middlewareManager.executeFinally(response, req);
|
|
368
|
+
// Apply pre-built CORS headers if configured
|
|
369
|
+
const entries = this.corsHeadersEntries;
|
|
370
|
+
if (entries) {
|
|
371
|
+
for (const [k, v] of entries) {
|
|
372
|
+
response.headers.set(k, v);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const dynamicCors = this.corsHandler;
|
|
377
|
+
if (dynamicCors) {
|
|
378
|
+
response = dynamicCors(response, req);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
215
381
|
return response;
|
|
216
382
|
}
|
|
217
383
|
catch (error) {
|
|
@@ -223,38 +389,187 @@ export class VectorRouter {
|
|
|
223
389
|
}
|
|
224
390
|
};
|
|
225
391
|
}
|
|
226
|
-
|
|
227
|
-
this.
|
|
228
|
-
|
|
392
|
+
isDevelopmentMode() {
|
|
393
|
+
if (this.developmentMode !== undefined) {
|
|
394
|
+
return this.developmentMode;
|
|
395
|
+
}
|
|
396
|
+
const nodeEnv = typeof Bun !== 'undefined' ? Bun.env.NODE_ENV : process.env.NODE_ENV;
|
|
397
|
+
return nodeEnv !== 'production';
|
|
229
398
|
}
|
|
230
|
-
|
|
231
|
-
|
|
399
|
+
async buildInputValidationPayload(request, options) {
|
|
400
|
+
let body = request.content;
|
|
401
|
+
if (options.rawRequest && request.method !== 'GET' && request.method !== 'HEAD') {
|
|
402
|
+
try {
|
|
403
|
+
body = await request.clone().text();
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
body = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
params: request.params ?? {},
|
|
411
|
+
query: request.query ?? {},
|
|
412
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
413
|
+
cookies: request.cookies ?? {},
|
|
414
|
+
body,
|
|
415
|
+
};
|
|
232
416
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
417
|
+
applyValidatedInput(request, validatedValue) {
|
|
418
|
+
request.validatedInput = validatedValue;
|
|
419
|
+
if (!validatedValue || typeof validatedValue !== 'object') {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const validated = validatedValue;
|
|
423
|
+
if ('params' in validated) {
|
|
424
|
+
try {
|
|
425
|
+
request.params = validated.params;
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// Request.params can be readonly on Bun-native requests.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if ('query' in validated) {
|
|
432
|
+
try {
|
|
433
|
+
request.query = validated.query;
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// Request.query can be readonly/non-configurable on some request objects.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if ('cookies' in validated) {
|
|
440
|
+
try {
|
|
441
|
+
request.cookies = validated.cookies;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// Request.cookies can be readonly/non-configurable on some request objects.
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if ('body' in validated) {
|
|
448
|
+
this.setContentAndBodyAlias(request, validated.body);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
setContentAndBodyAlias(request, value) {
|
|
452
|
+
try {
|
|
453
|
+
request.content = value;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// Request.content can be readonly/non-configurable on some request objects.
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.setBodyAlias(request, value);
|
|
460
|
+
}
|
|
461
|
+
setBodyAlias(request, value) {
|
|
462
|
+
try {
|
|
463
|
+
request.body = value;
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// Keep request.content as source of truth when body alias is readonly.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async validateInputSchema(request, options) {
|
|
470
|
+
const inputSchema = options.schema?.input;
|
|
471
|
+
if (!inputSchema) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
if (options.validate === false) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (!isStandardRouteSchema(inputSchema)) {
|
|
478
|
+
return APIError.internalServerError('Invalid route schema configuration', options.responseContentType);
|
|
479
|
+
}
|
|
480
|
+
const includeRawIssues = this.isDevelopmentMode();
|
|
481
|
+
const payload = await this.buildInputValidationPayload(request, options);
|
|
482
|
+
try {
|
|
483
|
+
const validation = await runStandardValidation(inputSchema, payload);
|
|
484
|
+
if (validation.success === false) {
|
|
485
|
+
const issues = normalizeValidationIssues(validation.issues, includeRawIssues);
|
|
486
|
+
return createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType);
|
|
487
|
+
}
|
|
488
|
+
this.applyValidatedInput(request, validation.value);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
const thrownIssues = extractThrownIssues(error);
|
|
493
|
+
if (thrownIssues) {
|
|
494
|
+
const issues = normalizeValidationIssues(thrownIssues, includeRawIssues);
|
|
495
|
+
return createResponse(422, createValidationErrorPayload('input', issues), options.responseContentType);
|
|
496
|
+
}
|
|
497
|
+
return APIError.internalServerError(error instanceof Error ? error.message : 'Validation failed', options.responseContentType);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
getOrCreateMethodMap(path) {
|
|
501
|
+
const existing = this.routeTable[path];
|
|
502
|
+
if (existing instanceof Response) {
|
|
503
|
+
throw new Error(`Cannot register method route for path "${path}" because a static route already exists.`);
|
|
504
|
+
}
|
|
505
|
+
if (existing) {
|
|
506
|
+
return existing;
|
|
507
|
+
}
|
|
508
|
+
const methodMap = Object.create(null);
|
|
509
|
+
this.routeTable[path] = methodMap;
|
|
510
|
+
this.addRouteMatcher(path);
|
|
511
|
+
return methodMap;
|
|
512
|
+
}
|
|
513
|
+
addRouteMatcher(path) {
|
|
514
|
+
if (this.routeMatchers.some((matcher) => matcher.path === path)) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
this.routeMatchers.push({
|
|
518
|
+
path,
|
|
519
|
+
regex: buildRouteRegex(path),
|
|
520
|
+
specificity: this.routeSpecificityScore(path),
|
|
521
|
+
});
|
|
522
|
+
this.routeMatchers.sort((a, b) => {
|
|
523
|
+
if (a.specificity !== b.specificity) {
|
|
524
|
+
return b.specificity - a.specificity;
|
|
525
|
+
}
|
|
526
|
+
return a.path.localeCompare(b.path);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
removeRouteMatcher(path) {
|
|
530
|
+
this.routeMatchers = this.routeMatchers.filter((matcher) => matcher.path !== path);
|
|
531
|
+
}
|
|
532
|
+
static parseQuery(url) {
|
|
533
|
+
const query = {};
|
|
534
|
+
for (const [key, value] of url.searchParams) {
|
|
535
|
+
if (key in query) {
|
|
536
|
+
const existing = query[key];
|
|
537
|
+
if (Array.isArray(existing)) {
|
|
538
|
+
existing.push(value);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
query[key] = [existing, value];
|
|
251
542
|
}
|
|
252
543
|
}
|
|
544
|
+
else {
|
|
545
|
+
query[key] = value;
|
|
546
|
+
}
|
|
253
547
|
}
|
|
254
|
-
return
|
|
548
|
+
return query;
|
|
255
549
|
}
|
|
256
|
-
|
|
257
|
-
|
|
550
|
+
routeSpecificityScore(path) {
|
|
551
|
+
const STATIC_SEGMENT_WEIGHT = 1000;
|
|
552
|
+
const PARAM_SEGMENT_WEIGHT = 10;
|
|
553
|
+
const WILDCARD_WEIGHT = 1;
|
|
554
|
+
const EXACT_MATCH_BONUS = 10000;
|
|
555
|
+
const segments = path.split('/').filter(Boolean);
|
|
556
|
+
let score = 0;
|
|
557
|
+
for (const segment of segments) {
|
|
558
|
+
if (segment.includes('*')) {
|
|
559
|
+
score += WILDCARD_WEIGHT;
|
|
560
|
+
}
|
|
561
|
+
else if (segment.startsWith(':')) {
|
|
562
|
+
score += PARAM_SEGMENT_WEIGHT;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
score += STATIC_SEGMENT_WEIGHT;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
score += path.length;
|
|
569
|
+
if (!path.includes(':') && !path.includes('*')) {
|
|
570
|
+
score += EXACT_MATCH_BONUS;
|
|
571
|
+
}
|
|
572
|
+
return score;
|
|
258
573
|
}
|
|
259
574
|
}
|
|
260
575
|
//# sourceMappingURL=router.js.map
|