vector-framework 1.1.1 → 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.
Files changed (98) hide show
  1. package/README.md +87 -635
  2. package/dist/auth/protected.d.ts.map +1 -1
  3. package/dist/auth/protected.js.map +1 -1
  4. package/dist/cache/manager.d.ts.map +1 -1
  5. package/dist/cache/manager.js +2 -7
  6. package/dist/cache/manager.js.map +1 -1
  7. package/dist/cli/index.js +17 -62
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/option-resolution.d.ts +4 -0
  10. package/dist/cli/option-resolution.d.ts.map +1 -0
  11. package/dist/cli/option-resolution.js +28 -0
  12. package/dist/cli/option-resolution.js.map +1 -0
  13. package/dist/cli.js +2721 -617
  14. package/dist/constants/index.d.ts +3 -0
  15. package/dist/constants/index.d.ts.map +1 -1
  16. package/dist/constants/index.js +6 -0
  17. package/dist/constants/index.js.map +1 -1
  18. package/dist/core/config-loader.d.ts.map +1 -1
  19. package/dist/core/config-loader.js +2 -0
  20. package/dist/core/config-loader.js.map +1 -1
  21. package/dist/core/router.d.ts +41 -17
  22. package/dist/core/router.d.ts.map +1 -1
  23. package/dist/core/router.js +432 -153
  24. package/dist/core/router.js.map +1 -1
  25. package/dist/core/server.d.ts +14 -1
  26. package/dist/core/server.d.ts.map +1 -1
  27. package/dist/core/server.js +250 -30
  28. package/dist/core/server.js.map +1 -1
  29. package/dist/core/vector.d.ts +4 -3
  30. package/dist/core/vector.d.ts.map +1 -1
  31. package/dist/core/vector.js +21 -12
  32. package/dist/core/vector.js.map +1 -1
  33. package/dist/dev/route-generator.d.ts.map +1 -1
  34. package/dist/dev/route-generator.js.map +1 -1
  35. package/dist/dev/route-scanner.d.ts.map +1 -1
  36. package/dist/dev/route-scanner.js +1 -5
  37. package/dist/dev/route-scanner.js.map +1 -1
  38. package/dist/http.d.ts +14 -14
  39. package/dist/http.d.ts.map +1 -1
  40. package/dist/http.js +34 -41
  41. package/dist/http.js.map +1 -1
  42. package/dist/index.js +1314 -8
  43. package/dist/index.mjs +1314 -8
  44. package/dist/middleware/manager.d.ts.map +1 -1
  45. package/dist/middleware/manager.js +4 -0
  46. package/dist/middleware/manager.js.map +1 -1
  47. package/dist/openapi/docs-ui.d.ts +2 -0
  48. package/dist/openapi/docs-ui.d.ts.map +1 -0
  49. package/dist/openapi/docs-ui.js +1313 -0
  50. package/dist/openapi/docs-ui.js.map +1 -0
  51. package/dist/openapi/generator.d.ts +12 -0
  52. package/dist/openapi/generator.d.ts.map +1 -0
  53. package/dist/openapi/generator.js +273 -0
  54. package/dist/openapi/generator.js.map +1 -0
  55. package/dist/types/index.d.ts +70 -11
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/standard-schema.d.ts +118 -0
  58. package/dist/types/standard-schema.d.ts.map +1 -0
  59. package/dist/types/standard-schema.js +2 -0
  60. package/dist/types/standard-schema.js.map +1 -0
  61. package/dist/utils/cors.d.ts +13 -0
  62. package/dist/utils/cors.d.ts.map +1 -0
  63. package/dist/utils/cors.js +89 -0
  64. package/dist/utils/cors.js.map +1 -0
  65. package/dist/utils/path.d.ts +6 -0
  66. package/dist/utils/path.d.ts.map +1 -1
  67. package/dist/utils/path.js +5 -0
  68. package/dist/utils/path.js.map +1 -1
  69. package/dist/utils/schema-validation.d.ts +31 -0
  70. package/dist/utils/schema-validation.d.ts.map +1 -0
  71. package/dist/utils/schema-validation.js +77 -0
  72. package/dist/utils/schema-validation.js.map +1 -0
  73. package/dist/utils/validation.d.ts.map +1 -1
  74. package/dist/utils/validation.js +1 -0
  75. package/dist/utils/validation.js.map +1 -1
  76. package/package.json +13 -12
  77. package/src/auth/protected.ts +3 -13
  78. package/src/cache/manager.ts +4 -18
  79. package/src/cli/index.ts +19 -75
  80. package/src/cli/option-resolution.ts +40 -0
  81. package/src/constants/index.ts +7 -0
  82. package/src/core/config-loader.ts +3 -3
  83. package/src/core/router.ts +502 -156
  84. package/src/core/server.ts +327 -32
  85. package/src/core/vector.ts +49 -29
  86. package/src/dev/route-generator.ts +1 -3
  87. package/src/dev/route-scanner.ts +2 -9
  88. package/src/http.ts +85 -125
  89. package/src/middleware/manager.ts +4 -0
  90. package/src/openapi/assets/tailwindcdn.js +83 -0
  91. package/src/openapi/docs-ui.ts +1317 -0
  92. package/src/openapi/generator.ts +359 -0
  93. package/src/types/index.ts +104 -17
  94. package/src/types/standard-schema.ts +147 -0
  95. package/src/utils/cors.ts +101 -0
  96. package/src/utils/path.ts +6 -0
  97. package/src/utils/schema-validation.ts +123 -0
  98. package/src/utils/validation.ts +1 -0
@@ -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
- routes = [];
8
- specificityCache = new Map();
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
- getRouteSpecificity(path) {
15
- const cached = this.specificityCache.get(path);
16
- if (cached !== undefined)
17
- return cached;
18
- const STATIC_SEGMENT_WEIGHT = 1000;
19
- const PARAM_SEGMENT_WEIGHT = 10;
20
- const WILDCARD_WEIGHT = 1;
21
- const EXACT_MATCH_BONUS = 10000;
22
- let score = 0;
23
- const segments = path.split('/').filter(Boolean);
24
- for (const segment of segments) {
25
- if (this.isStaticSegment(segment)) {
26
- score += STATIC_SEGMENT_WEIGHT;
27
- }
28
- else if (this.isParamSegment(segment)) {
29
- score += PARAM_SEGMENT_WEIGHT;
30
- }
31
- else if (this.isWildcardSegment(segment)) {
32
- score += WILDCARD_WEIGHT;
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
- score += path.length;
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
- isStaticSegment(segment) {
43
- return !segment.startsWith(':') && !segment.includes('*');
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
- isParamSegment(segment) {
46
- return segment.startsWith(':');
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
- isWildcardSegment(segment) {
49
- return segment.includes('*');
74
+ bulkAddRoutes(entries) {
75
+ for (const entry of entries) {
76
+ this.addRoute(entry);
77
+ }
50
78
  }
51
- isExactPath(path) {
52
- return !path.includes(':') && !path.includes('*');
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
- sortRoutes() {
55
- this.routes.sort((a, b) => {
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
- extractPath(route) {
64
- const PATH_INDEX = 3;
65
- return route[PATH_INDEX] || '';
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
- route(options, handler) {
68
- const wrappedHandler = this.wrapHandler(options, handler);
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
- createRouteRegex(path) {
80
- return buildRouteRegex(path);
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
- // Set params and route if provided
88
- if (options?.params !== undefined) {
89
- request.params = options.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
- // Parse query parameters from URL if not already parsed
98
- if (!request.query && request.url) {
99
- const url = request._parsedUrl ?? new URL(request.url);
100
- const query = {};
101
- for (const [key, value] of url.searchParams) {
102
- if (key in query) {
103
- if (Array.isArray(query[key])) {
104
- query[key].push(value);
105
- }
106
- else {
107
- query[key] = [query[key], value];
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
- else {
111
- query[key] = value;
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
- // Prepare the request with common logic
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(request);
275
+ const beforeResult = await this.middlewareManager.executeBefore(vectorRequest);
158
276
  if (beforeResult instanceof Response) {
159
277
  return beforeResult;
160
278
  }
161
- request = beforeResult;
279
+ const req = beforeResult;
162
280
  if (options.auth) {
163
281
  try {
164
- await this.authManager.authenticate(request);
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 && request.method !== 'GET' && request.method !== 'HEAD') {
288
+ if (!options.rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
289
+ let parsedContent = null;
171
290
  try {
172
- const contentType = request.headers.get('content-type');
173
- if (contentType?.includes('application/json')) {
174
- request.content = await request.json();
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?.includes('application/x-www-form-urlencoded')) {
177
- request.content = Object.fromEntries(await request.formData());
295
+ else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
296
+ parsedContent = Object.fromEntries(await req.formData());
178
297
  }
179
- else if (contentType?.includes('multipart/form-data')) {
180
- request.content = await request.formData();
298
+ else if (contentType?.startsWith('multipart/form-data')) {
299
+ parsedContent = await req.formData();
181
300
  }
182
301
  else {
183
- request.content = await request.text();
302
+ parsedContent = await req.text();
184
303
  }
185
304
  }
186
305
  catch {
187
- request.content = null;
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(request, {
208
- authUser: request.authUser,
317
+ const cacheKey = this.cacheManager.generateKey(req, {
318
+ authUser: req.authUser,
209
319
  });
210
- result = await this.cacheManager.get(cacheKey, cacheFactory, cacheOptions);
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(request, {
215
- authUser: request.authUser,
335
+ this.cacheManager.generateKey(req, {
336
+ authUser: req.authUser,
216
337
  });
217
- result = await this.cacheManager.get(cacheKey, cacheFactory, cacheOptions.ttl);
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(request);
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, request);
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
- addRoute(routeEntry) {
249
- this.routes.push(routeEntry);
250
- this.sortRoutes(); // Sort routes after adding a new one
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
- bulkAddRoutes(entries) {
253
- for (const entry of entries) {
254
- this.routes.push(entry);
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
- this.sortRoutes(); // Sort once after all routes are added — O(n log n) instead of O(n²)
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
- getRoutes() {
259
- return this.routes;
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
- async handle(request) {
262
- let url;
451
+ setContentAndBodyAlias(request, value) {
263
452
  try {
264
- url = new URL(request.url);
453
+ request.content = value;
265
454
  }
266
455
  catch {
267
- return APIError.badRequest('Malformed request URL');
456
+ // Request.content can be readonly/non-configurable on some request objects.
457
+ return;
268
458
  }
269
- request._parsedUrl = url;
270
- const pathname = url.pathname;
271
- for (const [method, regex, handlers, path] of this.routes) {
272
- if (request.method === 'OPTIONS' || request.method === method) {
273
- const match = pathname.match(regex);
274
- if (match) {
275
- const req = request;
276
- // Prepare the request with common logic
277
- this.prepareRequest(req, {
278
- params: match.groups || {},
279
- route: path || pathname,
280
- });
281
- for (const handler of handlers) {
282
- const response = await handler(req);
283
- if (response)
284
- return response;
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 APIError.notFound('Route not found');
548
+ return query;
290
549
  }
291
- clearRoutes() {
292
- this.routes = [];
293
- this.specificityCache.clear();
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