vector-framework 1.1.1 → 1.2.1
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 +99 -628
- package/dist/auth/protected.d.ts +1 -0
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js +3 -0
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +1 -0
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +5 -7
- package/dist/cache/manager.js.map +1 -1
- package/dist/cli/graceful-shutdown.d.ts +15 -0
- package/dist/cli/graceful-shutdown.d.ts.map +1 -0
- package/dist/cli/graceful-shutdown.js +42 -0
- package/dist/cli/graceful-shutdown.js.map +1 -0
- package/dist/cli/index.js +46 -97
- 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 +3423 -660
- 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.map +1 -1
- package/dist/core/config-loader.js +7 -2
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +41 -17
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +432 -153
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +17 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +471 -31
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +8 -5
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +53 -14
- 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.map +1 -1
- package/dist/dev/route-scanner.js +1 -5
- 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 +34 -41
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1420 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1420 -8
- 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 +1425 -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 +502 -0
- package/dist/openapi/generator.js.map +1 -0
- package/dist/start-vector.d.ts +3 -0
- package/dist/start-vector.d.ts.map +1 -0
- package/dist/start-vector.js +38 -0
- package/dist/start-vector.js.map +1 -0
- package/dist/types/index.d.ts +95 -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/logger.js +1 -1
- package/dist/utils/path.d.ts +6 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +5 -0
- 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 +3 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +15 -12
- package/src/auth/protected.ts +7 -13
- package/src/cache/manager.ts +8 -18
- package/src/cli/graceful-shutdown.ts +60 -0
- package/src/cli/index.ts +52 -115
- package/src/cli/option-resolution.ts +40 -0
- package/src/constants/index.ts +7 -0
- package/src/core/config-loader.ts +7 -4
- package/src/core/router.ts +502 -156
- package/src/core/server.ts +610 -33
- package/src/core/vector.ts +87 -33
- package/src/dev/route-generator.ts +1 -3
- package/src/dev/route-scanner.ts +2 -9
- package/src/http.ts +85 -125
- package/src/index.ts +4 -3
- package/src/middleware/manager.ts +4 -0
- package/src/openapi/assets/favicon/android-chrome-192x192.png +0 -0
- package/src/openapi/assets/favicon/android-chrome-512x512.png +0 -0
- package/src/openapi/assets/favicon/apple-touch-icon.png +0 -0
- package/src/openapi/assets/favicon/favicon-16x16.png +0 -0
- package/src/openapi/assets/favicon/favicon-32x32.png +0 -0
- package/src/openapi/assets/favicon/favicon.ico +0 -0
- package/src/openapi/assets/favicon/site.webmanifest +11 -0
- package/src/openapi/assets/logo.svg +12 -0
- package/src/openapi/assets/logo_dark.svg +6 -0
- package/src/openapi/assets/logo_icon.png +0 -0
- package/src/openapi/assets/logo_white.svg +6 -0
- package/src/openapi/assets/tailwindcdn.js +83 -0
- package/src/openapi/docs-ui.ts +1435 -0
- package/src/openapi/generator.ts +586 -0
- package/src/start-vector.ts +50 -0
- package/src/types/index.ts +138 -17
- package/src/types/standard-schema.ts +147 -0
- package/src/utils/cors.ts +101 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/path.ts +6 -0
- package/src/utils/schema-validation.ts +123 -0
- package/src/utils/validation.ts +3 -0
package/dist/core/router.js
CHANGED
|
@@ -1,92 +1,169 @@
|
|
|
1
1
|
import { APIError, createResponse } from '../http';
|
|
2
|
+
import { STATIC_RESPONSES } from '../constants';
|
|
2
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
|
-
|
|
8
|
-
|
|
9
|
+
routeBooleanDefaults = {};
|
|
10
|
+
developmentMode = undefined;
|
|
11
|
+
routeDefinitions = [];
|
|
12
|
+
routeTable = Object.create(null);
|
|
13
|
+
routeMatchers = [];
|
|
14
|
+
corsHeadersEntries = null;
|
|
15
|
+
corsHandler = null;
|
|
9
16
|
constructor(middlewareManager, authManager, cacheManager) {
|
|
10
17
|
this.middlewareManager = middlewareManager;
|
|
11
18
|
this.authManager = authManager;
|
|
12
19
|
this.cacheManager = cacheManager;
|
|
13
20
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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];
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
if (this.isExactPath(path)) {
|
|
37
|
-
score += EXACT_MATCH_BONUS;
|
|
38
|
-
}
|
|
39
|
-
this.specificityCache.set(path, score);
|
|
40
|
-
return score;
|
|
42
|
+
return resolved;
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
});
|
|
44
56
|
}
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
});
|
|
47
73
|
}
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
bulkAddRoutes(entries) {
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
this.addRoute(entry);
|
|
77
|
+
}
|
|
50
78
|
}
|
|
51
|
-
|
|
52
|
-
|
|
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);
|
|
53
86
|
}
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
const pathA = this.extractPath(a);
|
|
57
|
-
const pathB = this.extractPath(b);
|
|
58
|
-
const scoreA = this.getRouteSpecificity(pathA);
|
|
59
|
-
const scoreB = this.getRouteSpecificity(pathB);
|
|
60
|
-
return scoreB - scoreA;
|
|
61
|
-
});
|
|
87
|
+
getRouteTable() {
|
|
88
|
+
return this.routeTable;
|
|
62
89
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
66
102
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const routeEntry = [
|
|
70
|
-
options.method.toUpperCase(),
|
|
71
|
-
this.createRouteRegex(options.path),
|
|
72
|
-
[wrappedHandler],
|
|
73
|
-
options.path,
|
|
74
|
-
];
|
|
75
|
-
this.routes.push(routeEntry);
|
|
76
|
-
this.sortRoutes(); // Sort routes after adding
|
|
77
|
-
return routeEntry;
|
|
103
|
+
getRouteDefinitions() {
|
|
104
|
+
return [...this.routeDefinitions];
|
|
78
105
|
}
|
|
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,26 +171,42 @@ 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
|
-
// Lazy cookie parsing — only parse the Cookie header when first accessed
|
|
117
210
|
if (!Object.getOwnPropertyDescriptor(request, 'cookies')) {
|
|
118
211
|
Object.defineProperty(request, 'cookies', {
|
|
119
212
|
get() {
|
|
@@ -140,86 +233,124 @@ export class VectorRouter {
|
|
|
140
233
|
});
|
|
141
234
|
}
|
|
142
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;
|
|
253
|
+
}
|
|
254
|
+
const matched = pathname.match(routeMatcher);
|
|
255
|
+
if (!matched?.groups) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
return matched.groups;
|
|
259
|
+
}
|
|
143
260
|
wrapHandler(options, handler) {
|
|
261
|
+
const routePath = options.path;
|
|
262
|
+
const routeMatcher = routePath.includes(':') ? buildRouteRegex(routePath) : null;
|
|
144
263
|
return async (request) => {
|
|
145
|
-
// Ensure request has required properties
|
|
146
264
|
const vectorRequest = request;
|
|
147
|
-
|
|
265
|
+
const fallbackParams = this.resolveFallbackParams(request, routeMatcher);
|
|
148
266
|
this.prepareRequest(vectorRequest, {
|
|
267
|
+
params: fallbackParams,
|
|
268
|
+
route: routePath,
|
|
149
269
|
metadata: options.metadata,
|
|
150
270
|
});
|
|
151
|
-
request = vectorRequest;
|
|
152
271
|
try {
|
|
153
|
-
// Default expose to true if not specified
|
|
154
272
|
if (options.expose === false) {
|
|
155
273
|
return APIError.forbidden('Forbidden');
|
|
156
274
|
}
|
|
157
|
-
const beforeResult = await this.middlewareManager.executeBefore(
|
|
275
|
+
const beforeResult = await this.middlewareManager.executeBefore(vectorRequest);
|
|
158
276
|
if (beforeResult instanceof Response) {
|
|
159
277
|
return beforeResult;
|
|
160
278
|
}
|
|
161
|
-
|
|
279
|
+
const req = beforeResult;
|
|
162
280
|
if (options.auth) {
|
|
163
281
|
try {
|
|
164
|
-
await this.authManager.authenticate(
|
|
282
|
+
await this.authManager.authenticate(req);
|
|
165
283
|
}
|
|
166
284
|
catch (error) {
|
|
167
285
|
return APIError.unauthorized(error instanceof Error ? error.message : 'Authentication failed', options.responseContentType);
|
|
168
286
|
}
|
|
169
287
|
}
|
|
170
|
-
if (!options.rawRequest &&
|
|
288
|
+
if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
|
|
289
|
+
let parsedContent = null;
|
|
171
290
|
try {
|
|
172
|
-
const contentType =
|
|
173
|
-
if (contentType?.
|
|
174
|
-
|
|
291
|
+
const contentType = req.headers.get('content-type');
|
|
292
|
+
if (contentType?.startsWith('application/json')) {
|
|
293
|
+
parsedContent = await req.json();
|
|
175
294
|
}
|
|
176
|
-
else if (contentType?.
|
|
177
|
-
|
|
295
|
+
else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
|
|
296
|
+
parsedContent = Object.fromEntries(await req.formData());
|
|
178
297
|
}
|
|
179
|
-
else if (contentType?.
|
|
180
|
-
|
|
298
|
+
else if (contentType?.startsWith('multipart/form-data')) {
|
|
299
|
+
parsedContent = await req.formData();
|
|
181
300
|
}
|
|
182
301
|
else {
|
|
183
|
-
|
|
302
|
+
parsedContent = await req.text();
|
|
184
303
|
}
|
|
185
304
|
}
|
|
186
305
|
catch {
|
|
187
|
-
|
|
306
|
+
parsedContent = null;
|
|
188
307
|
}
|
|
308
|
+
this.setContentAndBodyAlias(req, parsedContent);
|
|
309
|
+
}
|
|
310
|
+
const inputValidationResponse = await this.validateInputSchema(req, options);
|
|
311
|
+
if (inputValidationResponse) {
|
|
312
|
+
return inputValidationResponse;
|
|
189
313
|
}
|
|
190
314
|
let result;
|
|
191
315
|
const cacheOptions = options.cache;
|
|
192
|
-
// Create cache factory that handles Response objects
|
|
193
|
-
const cacheFactory = async () => {
|
|
194
|
-
const res = await handler(request);
|
|
195
|
-
// If Response, extract data for caching
|
|
196
|
-
if (res instanceof Response) {
|
|
197
|
-
return {
|
|
198
|
-
_isResponse: true,
|
|
199
|
-
body: await res.text(),
|
|
200
|
-
status: res.status,
|
|
201
|
-
headers: Object.fromEntries(res.headers.entries()),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return res;
|
|
205
|
-
};
|
|
206
316
|
if (cacheOptions && typeof cacheOptions === 'number' && cacheOptions > 0) {
|
|
207
|
-
const cacheKey = this.cacheManager.generateKey(
|
|
208
|
-
authUser:
|
|
317
|
+
const cacheKey = this.cacheManager.generateKey(req, {
|
|
318
|
+
authUser: req.authUser,
|
|
209
319
|
});
|
|
210
|
-
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);
|
|
211
332
|
}
|
|
212
333
|
else if (cacheOptions && typeof cacheOptions === 'object' && cacheOptions.ttl) {
|
|
213
334
|
const cacheKey = cacheOptions.key ||
|
|
214
|
-
this.cacheManager.generateKey(
|
|
215
|
-
authUser:
|
|
335
|
+
this.cacheManager.generateKey(req, {
|
|
336
|
+
authUser: req.authUser,
|
|
216
337
|
});
|
|
217
|
-
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);
|
|
218
350
|
}
|
|
219
351
|
else {
|
|
220
|
-
result = await handler(
|
|
352
|
+
result = await handler(req);
|
|
221
353
|
}
|
|
222
|
-
// Reconstruct Response if it was cached
|
|
223
354
|
if (result && typeof result === 'object' && result._isResponse === true) {
|
|
224
355
|
result = new Response(result.body, {
|
|
225
356
|
status: result.status,
|
|
@@ -233,7 +364,20 @@ export class VectorRouter {
|
|
|
233
364
|
else {
|
|
234
365
|
response = createResponse(200, result, options.responseContentType);
|
|
235
366
|
}
|
|
236
|
-
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
|
+
}
|
|
237
381
|
return response;
|
|
238
382
|
}
|
|
239
383
|
catch (error) {
|
|
@@ -245,52 +389,187 @@ export class VectorRouter {
|
|
|
245
389
|
}
|
|
246
390
|
};
|
|
247
391
|
}
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
|
|
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';
|
|
251
398
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
408
|
}
|
|
256
|
-
|
|
409
|
+
return {
|
|
410
|
+
params: request.params ?? {},
|
|
411
|
+
query: request.query ?? {},
|
|
412
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
413
|
+
cookies: request.cookies ?? {},
|
|
414
|
+
body,
|
|
415
|
+
};
|
|
257
416
|
}
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|
|
260
450
|
}
|
|
261
|
-
|
|
262
|
-
let url;
|
|
451
|
+
setContentAndBodyAlias(request, value) {
|
|
263
452
|
try {
|
|
264
|
-
|
|
453
|
+
request.content = value;
|
|
265
454
|
}
|
|
266
455
|
catch {
|
|
267
|
-
|
|
456
|
+
// Request.content can be readonly/non-configurable on some request objects.
|
|
457
|
+
return;
|
|
268
458
|
}
|
|
269
|
-
request
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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];
|
|
286
542
|
}
|
|
287
543
|
}
|
|
544
|
+
else {
|
|
545
|
+
query[key] = value;
|
|
546
|
+
}
|
|
288
547
|
}
|
|
289
|
-
return
|
|
548
|
+
return query;
|
|
290
549
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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;
|
|
294
573
|
}
|
|
295
574
|
}
|
|
296
575
|
//# sourceMappingURL=router.js.map
|