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.
Files changed (128) hide show
  1. package/README.md +99 -628
  2. package/dist/auth/protected.d.ts +1 -0
  3. package/dist/auth/protected.d.ts.map +1 -1
  4. package/dist/auth/protected.js +3 -0
  5. package/dist/auth/protected.js.map +1 -1
  6. package/dist/cache/manager.d.ts +1 -0
  7. package/dist/cache/manager.d.ts.map +1 -1
  8. package/dist/cache/manager.js +5 -7
  9. package/dist/cache/manager.js.map +1 -1
  10. package/dist/cli/graceful-shutdown.d.ts +15 -0
  11. package/dist/cli/graceful-shutdown.d.ts.map +1 -0
  12. package/dist/cli/graceful-shutdown.js +42 -0
  13. package/dist/cli/graceful-shutdown.js.map +1 -0
  14. package/dist/cli/index.js +46 -97
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/option-resolution.d.ts +4 -0
  17. package/dist/cli/option-resolution.d.ts.map +1 -0
  18. package/dist/cli/option-resolution.js +28 -0
  19. package/dist/cli/option-resolution.js.map +1 -0
  20. package/dist/cli.js +3423 -660
  21. package/dist/constants/index.d.ts +3 -0
  22. package/dist/constants/index.d.ts.map +1 -1
  23. package/dist/constants/index.js +6 -0
  24. package/dist/constants/index.js.map +1 -1
  25. package/dist/core/config-loader.d.ts.map +1 -1
  26. package/dist/core/config-loader.js +7 -2
  27. package/dist/core/config-loader.js.map +1 -1
  28. package/dist/core/router.d.ts +41 -17
  29. package/dist/core/router.d.ts.map +1 -1
  30. package/dist/core/router.js +432 -153
  31. package/dist/core/router.js.map +1 -1
  32. package/dist/core/server.d.ts +17 -1
  33. package/dist/core/server.d.ts.map +1 -1
  34. package/dist/core/server.js +471 -31
  35. package/dist/core/server.js.map +1 -1
  36. package/dist/core/vector.d.ts +8 -5
  37. package/dist/core/vector.d.ts.map +1 -1
  38. package/dist/core/vector.js +53 -14
  39. package/dist/core/vector.js.map +1 -1
  40. package/dist/dev/route-generator.d.ts.map +1 -1
  41. package/dist/dev/route-generator.js.map +1 -1
  42. package/dist/dev/route-scanner.d.ts.map +1 -1
  43. package/dist/dev/route-scanner.js +1 -5
  44. package/dist/dev/route-scanner.js.map +1 -1
  45. package/dist/http.d.ts +14 -14
  46. package/dist/http.d.ts.map +1 -1
  47. package/dist/http.js +34 -41
  48. package/dist/http.js.map +1 -1
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1420 -8
  52. package/dist/index.js.map +1 -1
  53. package/dist/index.mjs +1420 -8
  54. package/dist/middleware/manager.d.ts.map +1 -1
  55. package/dist/middleware/manager.js +4 -0
  56. package/dist/middleware/manager.js.map +1 -1
  57. package/dist/openapi/docs-ui.d.ts +2 -0
  58. package/dist/openapi/docs-ui.d.ts.map +1 -0
  59. package/dist/openapi/docs-ui.js +1425 -0
  60. package/dist/openapi/docs-ui.js.map +1 -0
  61. package/dist/openapi/generator.d.ts +12 -0
  62. package/dist/openapi/generator.d.ts.map +1 -0
  63. package/dist/openapi/generator.js +502 -0
  64. package/dist/openapi/generator.js.map +1 -0
  65. package/dist/start-vector.d.ts +3 -0
  66. package/dist/start-vector.d.ts.map +1 -0
  67. package/dist/start-vector.js +38 -0
  68. package/dist/start-vector.js.map +1 -0
  69. package/dist/types/index.d.ts +95 -11
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/types/standard-schema.d.ts +118 -0
  72. package/dist/types/standard-schema.d.ts.map +1 -0
  73. package/dist/types/standard-schema.js +2 -0
  74. package/dist/types/standard-schema.js.map +1 -0
  75. package/dist/utils/cors.d.ts +13 -0
  76. package/dist/utils/cors.d.ts.map +1 -0
  77. package/dist/utils/cors.js +89 -0
  78. package/dist/utils/cors.js.map +1 -0
  79. package/dist/utils/logger.js +1 -1
  80. package/dist/utils/path.d.ts +6 -0
  81. package/dist/utils/path.d.ts.map +1 -1
  82. package/dist/utils/path.js +5 -0
  83. package/dist/utils/path.js.map +1 -1
  84. package/dist/utils/schema-validation.d.ts +31 -0
  85. package/dist/utils/schema-validation.d.ts.map +1 -0
  86. package/dist/utils/schema-validation.js +77 -0
  87. package/dist/utils/schema-validation.js.map +1 -0
  88. package/dist/utils/validation.d.ts.map +1 -1
  89. package/dist/utils/validation.js +3 -0
  90. package/dist/utils/validation.js.map +1 -1
  91. package/package.json +15 -12
  92. package/src/auth/protected.ts +7 -13
  93. package/src/cache/manager.ts +8 -18
  94. package/src/cli/graceful-shutdown.ts +60 -0
  95. package/src/cli/index.ts +52 -115
  96. package/src/cli/option-resolution.ts +40 -0
  97. package/src/constants/index.ts +7 -0
  98. package/src/core/config-loader.ts +7 -4
  99. package/src/core/router.ts +502 -156
  100. package/src/core/server.ts +610 -33
  101. package/src/core/vector.ts +87 -33
  102. package/src/dev/route-generator.ts +1 -3
  103. package/src/dev/route-scanner.ts +2 -9
  104. package/src/http.ts +85 -125
  105. package/src/index.ts +4 -3
  106. package/src/middleware/manager.ts +4 -0
  107. package/src/openapi/assets/favicon/android-chrome-192x192.png +0 -0
  108. package/src/openapi/assets/favicon/android-chrome-512x512.png +0 -0
  109. package/src/openapi/assets/favicon/apple-touch-icon.png +0 -0
  110. package/src/openapi/assets/favicon/favicon-16x16.png +0 -0
  111. package/src/openapi/assets/favicon/favicon-32x32.png +0 -0
  112. package/src/openapi/assets/favicon/favicon.ico +0 -0
  113. package/src/openapi/assets/favicon/site.webmanifest +11 -0
  114. package/src/openapi/assets/logo.svg +12 -0
  115. package/src/openapi/assets/logo_dark.svg +6 -0
  116. package/src/openapi/assets/logo_icon.png +0 -0
  117. package/src/openapi/assets/logo_white.svg +6 -0
  118. package/src/openapi/assets/tailwindcdn.js +83 -0
  119. package/src/openapi/docs-ui.ts +1435 -0
  120. package/src/openapi/generator.ts +586 -0
  121. package/src/start-vector.ts +50 -0
  122. package/src/types/index.ts +138 -17
  123. package/src/types/standard-schema.ts +147 -0
  124. package/src/utils/cors.ts +101 -0
  125. package/src/utils/logger.ts +1 -1
  126. package/src/utils/path.ts +6 -0
  127. package/src/utils/schema-validation.ts +123 -0
  128. package/src/utils/validation.ts +3 -0
@@ -1,5 +1,4 @@
1
1
  import type { Server } from 'bun';
2
- import type { RouteEntry } from 'itty-router';
3
2
  import { AuthManager } from '../auth/protected';
4
3
  import { CacheManager } from '../cache/manager';
5
4
  import { RouteGenerator } from '../dev/route-generator';
@@ -9,15 +8,24 @@ import { toFileUrl } from '../utils/path';
9
8
  import type {
10
9
  CacheHandler,
11
10
  DefaultVectorTypes,
11
+ InferRouteInputFromSchemaDefinition,
12
+ LegacyRouteEntry,
12
13
  ProtectedHandler,
13
14
  RouteHandler,
14
15
  RouteOptions,
16
+ RouteSchemaDefinition,
15
17
  VectorConfig,
16
18
  VectorTypes,
17
19
  } from '../types';
18
20
  import { VectorRouter } from './router';
19
21
  import { VectorServer } from './server';
20
22
 
23
+ interface LoadedRouteDefinition<TTypes extends VectorTypes = DefaultVectorTypes> {
24
+ entry: { method: string; path: string };
25
+ options: RouteOptions<TTypes>;
26
+ handler: RouteHandler<TTypes>;
27
+ }
28
+
21
29
  // Internal-only class - not exposed to users
22
30
  export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
23
31
  private static instance: Vector<any>;
@@ -31,16 +39,13 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
31
39
  private routeGenerator: RouteGenerator | null = null;
32
40
  private _protectedHandler: ProtectedHandler<TTypes> | null = null;
33
41
  private _cacheHandler: CacheHandler | null = null;
42
+ private shutdownPromise: Promise<void> | null = null;
34
43
 
35
44
  private constructor() {
36
45
  this.middlewareManager = new MiddlewareManager<TTypes>();
37
46
  this.authManager = new AuthManager<TTypes>();
38
47
  this.cacheManager = new CacheManager<TTypes>();
39
- this.router = new VectorRouter<TTypes>(
40
- this.middlewareManager,
41
- this.authManager,
42
- this.cacheManager
43
- );
48
+ this.router = new VectorRouter<TTypes>(this.middlewareManager, this.authManager, this.cacheManager);
44
49
  }
45
50
 
46
51
  // Internal use only - not exposed to users
@@ -52,9 +57,13 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
52
57
  }
53
58
 
54
59
  // Internal method to set protected handler
55
- setProtectedHandler(handler: ProtectedHandler<TTypes>) {
60
+ setProtectedHandler(handler: ProtectedHandler<TTypes> | null) {
56
61
  this._protectedHandler = handler;
57
- this.authManager.setProtectedHandler(handler);
62
+ if (handler) {
63
+ this.authManager.setProtectedHandler(handler);
64
+ return;
65
+ }
66
+ this.authManager.clearProtectedHandler();
58
67
  }
59
68
 
60
69
  getProtectedHandler(): ProtectedHandler<TTypes> | null {
@@ -62,9 +71,13 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
62
71
  }
63
72
 
64
73
  // Internal method to set cache handler
65
- setCacheHandler(handler: CacheHandler) {
74
+ setCacheHandler(handler: CacheHandler | null) {
66
75
  this._cacheHandler = handler;
67
- this.cacheManager.setCacheHandler(handler);
76
+ if (handler) {
77
+ this.cacheManager.setCacheHandler(handler);
78
+ return;
79
+ }
80
+ this.cacheManager.clearCacheHandler();
68
81
  }
69
82
 
70
83
  getCacheHandler(): CacheHandler | null {
@@ -72,13 +85,20 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
72
85
  }
73
86
 
74
87
  // Internal method to add route
75
- addRoute(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): RouteEntry {
76
- return this.router.route(options, handler);
88
+ addRoute<TSchemaDef extends RouteSchemaDefinition | undefined>(
89
+ options: Omit<RouteOptions<TTypes>, 'schema'> & { schema?: TSchemaDef },
90
+ handler: RouteHandler<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>>
91
+ ): void;
92
+ addRoute(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): void {
93
+ this.router.route(options, handler);
77
94
  }
78
95
 
79
96
  // Internal method to start server - only called by CLI
80
97
  async startServer(config?: VectorConfig<TTypes>): Promise<Server> {
81
98
  this.config = { ...this.config, ...config };
99
+ const routeDefaults = { ...this.config.defaults?.route };
100
+ this.router.setRouteBooleanDefaults(routeDefaults);
101
+ this.router.setDevelopmentMode(this.config.development);
82
102
 
83
103
  // Clear previous middleware to avoid accumulation across multiple starts
84
104
  this.middlewareManager.clear();
@@ -96,6 +116,10 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
96
116
  this.middlewareManager.addFinally(...config.finally);
97
117
  }
98
118
 
119
+ if (typeof this.config.startup === 'function') {
120
+ await this.config.startup();
121
+ }
122
+
99
123
  if (this.config.autoDiscover !== false) {
100
124
  await this.discoverRoutes();
101
125
  }
@@ -136,25 +160,21 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
136
160
  if (exported) {
137
161
  if (this.isRouteDefinition(exported)) {
138
162
  // Use router.route() to ensure middleware is applied
139
- const routeDef = exported as any;
140
- this.router.route(routeDef.options, routeDef.handler);
141
- this.logRouteLoaded(routeDef.options);
163
+ this.router.route(exported.options, exported.handler);
164
+ this.logRouteLoaded(exported.options);
142
165
  } else if (this.isRouteEntry(exported)) {
143
166
  // Legacy support for direct RouteEntry (won't have middleware)
144
- this.router.addRoute(exported as RouteEntry);
145
- this.logRouteLoaded(exported as RouteEntry);
167
+ this.router.addRoute(exported);
168
+ this.logRouteLoaded(exported);
146
169
  } else if (typeof exported === 'function') {
147
- this.router.route(route.options as any, exported);
148
- this.logRouteLoaded(route.options);
170
+ this.router.route(route.options as RouteOptions<TTypes>, exported as RouteHandler<TTypes>);
171
+ this.logRouteLoaded(route.options as RouteOptions<TTypes>);
149
172
  }
150
173
  }
151
174
  } catch (error) {
152
175
  console.error(`Failed to load route ${route.name} from ${route.path}:`, error);
153
176
  }
154
177
  }
155
-
156
- // Ensure routes are properly sorted after loading all
157
- this.router.sortRoutes();
158
178
  }
159
179
  } catch (error) {
160
180
  if ((error as any).code !== 'ENOENT' && (error as any).code !== 'ENOTDIR') {
@@ -166,36 +186,49 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
166
186
  async loadRoute(routeModule: any) {
167
187
  if (typeof routeModule === 'function') {
168
188
  const routeEntry = routeModule();
169
- if (Array.isArray(routeEntry)) {
170
- this.router.addRoute(routeEntry as RouteEntry);
189
+ if (this.isRouteEntry(routeEntry)) {
190
+ this.router.addRoute(routeEntry);
171
191
  }
172
192
  } else if (routeModule && typeof routeModule === 'object') {
173
193
  for (const [, value] of Object.entries(routeModule)) {
174
194
  if (typeof value === 'function') {
175
- const routeEntry = (value as any)();
176
- if (Array.isArray(routeEntry)) {
177
- this.router.addRoute(routeEntry as RouteEntry);
195
+ const routeEntry = value();
196
+ if (this.isRouteEntry(routeEntry)) {
197
+ this.router.addRoute(routeEntry);
178
198
  }
179
199
  }
180
200
  }
181
201
  }
182
202
  }
183
203
 
184
- private isRouteEntry(value: any): boolean {
185
- return Array.isArray(value) && value.length >= 3;
204
+ private isRouteEntry(value: unknown): value is LegacyRouteEntry {
205
+ if (!Array.isArray(value) || value.length < 3) {
206
+ return false;
207
+ }
208
+
209
+ const [method, matcher, handlers, path] = value;
210
+ return (
211
+ typeof method === 'string' &&
212
+ matcher instanceof RegExp &&
213
+ Array.isArray(handlers) &&
214
+ handlers.length > 0 &&
215
+ handlers.every((handler) => typeof handler === 'function') &&
216
+ (path === undefined || typeof path === 'string')
217
+ );
186
218
  }
187
219
 
188
- private isRouteDefinition(value: any): boolean {
220
+ private isRouteDefinition(value: unknown): value is LoadedRouteDefinition<TTypes> {
189
221
  return (
190
- value &&
222
+ value !== null &&
191
223
  typeof value === 'object' &&
192
224
  'entry' in value &&
193
225
  'options' in value &&
194
- 'handler' in value
226
+ 'handler' in value &&
227
+ typeof (value as LoadedRouteDefinition<TTypes>).handler === 'function'
195
228
  );
196
229
  }
197
230
 
198
- private logRouteLoaded(_: RouteEntry | RouteOptions): void {
231
+ private logRouteLoaded(_: RouteOptions<TTypes> | LegacyRouteEntry): void {
199
232
  // Silent - no logging
200
233
  }
201
234
 
@@ -208,6 +241,27 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
208
241
  // Routes will be cleared on next startServer() call
209
242
  }
210
243
 
244
+ async shutdown(): Promise<void> {
245
+ if (this.shutdownPromise) {
246
+ return this.shutdownPromise;
247
+ }
248
+
249
+ this.shutdownPromise = (async () => {
250
+ // Stop accepting new traffic first.
251
+ this.stop();
252
+
253
+ if (typeof this.config.shutdown === 'function') {
254
+ await this.config.shutdown();
255
+ }
256
+ })();
257
+
258
+ try {
259
+ await this.shutdownPromise;
260
+ } finally {
261
+ this.shutdownPromise = null;
262
+ }
263
+ }
264
+
211
265
  getServer(): VectorServer<TTypes> | null {
212
266
  return this.server;
213
267
  }
@@ -36,9 +36,7 @@ export class RouteGenerator {
36
36
 
37
37
  if (fileRoutes.some((r) => r.name === 'default')) {
38
38
  if (namedImports.length > 0) {
39
- imports.push(
40
- `import ${importName}, { ${namedImports.join(', ')} } from '${relativePath}';`
41
- );
39
+ imports.push(`import ${importName}, { ${namedImports.join(', ')} } from '${relativePath}';`);
42
40
  } else {
43
41
  imports.push(`import ${importName} from '${relativePath}';`);
44
42
  }
@@ -95,8 +95,7 @@ export class RouteScanner {
95
95
 
96
96
  try {
97
97
  // Convert Windows paths to URLs for import
98
- const importPath =
99
- process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
98
+ const importPath = process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
100
99
 
101
100
  const module = await import(importPath);
102
101
 
@@ -117,13 +116,7 @@ export class RouteScanner {
117
116
  if (name === 'default') continue;
118
117
 
119
118
  // Check for new RouteDefinition format
120
- if (
121
- value &&
122
- typeof value === 'object' &&
123
- 'entry' in value &&
124
- 'options' in value &&
125
- 'handler' in value
126
- ) {
119
+ if (value && typeof value === 'object' && 'entry' in value && 'options' in value && 'handler' in value) {
127
120
  const routeDef = value as any;
128
121
  routes.push({
129
122
  name,
package/src/http.ts CHANGED
@@ -1,81 +1,63 @@
1
- import { cors, type IRequest, type RouteEntry, withContent } from 'itty-router';
2
- import { buildRouteRegex } from './utils/path';
3
1
  import { CONTENT_TYPES, HTTP_STATUS } from './constants';
4
2
  import type {
5
3
  CacheOptions,
6
4
  DefaultVectorTypes,
7
5
  GetAuthType,
6
+ InferRouteInputFromSchemaDefinition,
7
+ RouteSchemaDefinition,
8
8
  VectorRequest,
9
9
  VectorTypes,
10
10
  } from './types';
11
11
  import { getVectorInstance } from './core/vector';
12
12
 
13
- export interface ProtectedRequest<TTypes extends VectorTypes = DefaultVectorTypes>
14
- extends IRequest {
15
- authUser?: GetAuthType<TTypes>;
16
- }
17
-
18
- export const { preflight, corsify } = cors({
19
- origin: '*',
20
- credentials: true,
21
- allowHeaders: 'Content-Type, Authorization',
22
- allowMethods: 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
23
- exposeHeaders: 'Authorization',
24
- maxAge: 86_400,
25
- });
26
-
27
- interface ExtendedApiOptions extends ApiOptions {
13
+ interface ExtendedApiOptions<TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined>
14
+ extends ApiOptions<TSchemaDef> {
28
15
  method: string;
29
16
  path: string;
30
17
  }
31
18
 
32
- export interface RouteDefinition<TTypes extends VectorTypes = DefaultVectorTypes> {
33
- entry: RouteEntry;
34
- options: ExtendedApiOptions;
35
- handler: (req: VectorRequest<TTypes>) => Promise<unknown>;
19
+ export interface RouteDefinition<
20
+ TTypes extends VectorTypes = DefaultVectorTypes,
21
+ TValidatedInput = undefined,
22
+ TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined,
23
+ > {
24
+ entry: { method: string; path: string };
25
+ options: ExtendedApiOptions<TSchemaDef>;
26
+ handler: (req: VectorRequest<TTypes, TValidatedInput>) => Promise<unknown> | unknown;
36
27
  }
37
28
 
38
- export function route<TTypes extends VectorTypes = DefaultVectorTypes>(
39
- options: ExtendedApiOptions,
40
- fn: (req: VectorRequest<TTypes>) => Promise<unknown>
41
- ): RouteDefinition<TTypes> {
42
- const handler = api(options, fn);
43
-
44
- const entry: RouteEntry = [
45
- options.method.toUpperCase(),
46
- buildRouteRegex(options.path),
47
- [handler],
48
- options.path,
49
- ];
50
-
29
+ export function route<
30
+ TTypes extends VectorTypes = DefaultVectorTypes,
31
+ TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined,
32
+ >(
33
+ options: ExtendedApiOptions<TSchemaDef>,
34
+ fn: (req: VectorRequest<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>>) => Promise<unknown> | unknown
35
+ ): RouteDefinition<TTypes, InferRouteInputFromSchemaDefinition<TSchemaDef>, TSchemaDef> {
51
36
  return {
52
- entry,
37
+ entry: {
38
+ method: options.method.toUpperCase(),
39
+ path: options.path,
40
+ },
53
41
  options,
54
42
  handler: fn,
55
43
  };
56
44
  }
57
45
 
58
- function hasBigInt(value: unknown, depth = 0): boolean {
59
- if (typeof value === 'bigint') return true;
60
- if (depth > 4 || value === null || typeof value !== 'object') return false;
61
- for (const v of Object.values(value as object)) {
62
- if (hasBigInt(v, depth + 1)) return true;
63
- }
64
- return false;
65
- }
66
-
67
46
  function stringifyData(data: unknown): string {
68
47
  const val = data ?? null;
69
- if (!hasBigInt(val)) return JSON.stringify(val);
70
- return JSON.stringify(val, (_key, value) =>
71
- typeof value === 'bigint' ? value.toString() : value
72
- );
48
+ try {
49
+ return JSON.stringify(val);
50
+ } catch (e) {
51
+ if (e instanceof TypeError && /\bbigint\b/i.test(e.message)) {
52
+ return JSON.stringify(val, (_key, value) => (typeof value === 'bigint' ? value.toString() : value));
53
+ }
54
+ throw e;
55
+ }
73
56
  }
74
57
 
75
58
  const ApiResponse = {
76
59
  success: <T>(data: T, contentType?: string) => createResponse(HTTP_STATUS.OK, data, contentType),
77
- created: <T>(data: T, contentType?: string) =>
78
- createResponse(HTTP_STATUS.CREATED, data, contentType),
60
+ created: <T>(data: T, contentType?: string) => createResponse(HTTP_STATUS.CREATED, data, contentType),
79
61
  };
80
62
 
81
63
  function createErrorResponse(code: number, message: string, contentType?: string): Response {
@@ -97,40 +79,29 @@ export const APIError = {
97
79
  unauthorized: (msg = 'Unauthorized', contentType?: string) =>
98
80
  createErrorResponse(HTTP_STATUS.UNAUTHORIZED, msg, contentType),
99
81
 
100
- paymentRequired: (msg = 'Payment Required', contentType?: string) =>
101
- createErrorResponse(402, msg, contentType),
82
+ paymentRequired: (msg = 'Payment Required', contentType?: string) => createErrorResponse(402, msg, contentType),
102
83
 
103
- forbidden: (msg = 'Forbidden', contentType?: string) =>
104
- createErrorResponse(HTTP_STATUS.FORBIDDEN, msg, contentType),
84
+ forbidden: (msg = 'Forbidden', contentType?: string) => createErrorResponse(HTTP_STATUS.FORBIDDEN, msg, contentType),
105
85
 
106
- notFound: (msg = 'Not Found', contentType?: string) =>
107
- createErrorResponse(HTTP_STATUS.NOT_FOUND, msg, contentType),
86
+ notFound: (msg = 'Not Found', contentType?: string) => createErrorResponse(HTTP_STATUS.NOT_FOUND, msg, contentType),
108
87
 
109
- methodNotAllowed: (msg = 'Method Not Allowed', contentType?: string) =>
110
- createErrorResponse(405, msg, contentType),
88
+ methodNotAllowed: (msg = 'Method Not Allowed', contentType?: string) => createErrorResponse(405, msg, contentType),
111
89
 
112
- notAcceptable: (msg = 'Not Acceptable', contentType?: string) =>
113
- createErrorResponse(406, msg, contentType),
90
+ notAcceptable: (msg = 'Not Acceptable', contentType?: string) => createErrorResponse(406, msg, contentType),
114
91
 
115
- requestTimeout: (msg = 'Request Timeout', contentType?: string) =>
116
- createErrorResponse(408, msg, contentType),
92
+ requestTimeout: (msg = 'Request Timeout', contentType?: string) => createErrorResponse(408, msg, contentType),
117
93
 
118
- conflict: (msg = 'Conflict', contentType?: string) =>
119
- createErrorResponse(HTTP_STATUS.CONFLICT, msg, contentType),
94
+ conflict: (msg = 'Conflict', contentType?: string) => createErrorResponse(HTTP_STATUS.CONFLICT, msg, contentType),
120
95
 
121
96
  gone: (msg = 'Gone', contentType?: string) => createErrorResponse(410, msg, contentType),
122
97
 
123
- lengthRequired: (msg = 'Length Required', contentType?: string) =>
124
- createErrorResponse(411, msg, contentType),
98
+ lengthRequired: (msg = 'Length Required', contentType?: string) => createErrorResponse(411, msg, contentType),
125
99
 
126
- preconditionFailed: (msg = 'Precondition Failed', contentType?: string) =>
127
- createErrorResponse(412, msg, contentType),
100
+ preconditionFailed: (msg = 'Precondition Failed', contentType?: string) => createErrorResponse(412, msg, contentType),
128
101
 
129
- payloadTooLarge: (msg = 'Payload Too Large', contentType?: string) =>
130
- createErrorResponse(413, msg, contentType),
102
+ payloadTooLarge: (msg = 'Payload Too Large', contentType?: string) => createErrorResponse(413, msg, contentType),
131
103
 
132
- uriTooLong: (msg = 'URI Too Long', contentType?: string) =>
133
- createErrorResponse(414, msg, contentType),
104
+ uriTooLong: (msg = 'URI Too Long', contentType?: string) => createErrorResponse(414, msg, contentType),
134
105
 
135
106
  unsupportedMediaType: (msg = 'Unsupported Media Type', contentType?: string) =>
136
107
  createErrorResponse(415, msg, contentType),
@@ -138,33 +109,27 @@ export const APIError = {
138
109
  rangeNotSatisfiable: (msg = 'Range Not Satisfiable', contentType?: string) =>
139
110
  createErrorResponse(416, msg, contentType),
140
111
 
141
- expectationFailed: (msg = 'Expectation Failed', contentType?: string) =>
142
- createErrorResponse(417, msg, contentType),
112
+ expectationFailed: (msg = 'Expectation Failed', contentType?: string) => createErrorResponse(417, msg, contentType),
143
113
 
144
- imATeapot: (msg = "I'm a teapot", contentType?: string) =>
145
- createErrorResponse(418, msg, contentType),
114
+ imATeapot: (msg = "I'm a teapot", contentType?: string) => createErrorResponse(418, msg, contentType),
146
115
 
147
- misdirectedRequest: (msg = 'Misdirected Request', contentType?: string) =>
148
- createErrorResponse(421, msg, contentType),
116
+ misdirectedRequest: (msg = 'Misdirected Request', contentType?: string) => createErrorResponse(421, msg, contentType),
149
117
 
150
118
  unprocessableEntity: (msg = 'Unprocessable Entity', contentType?: string) =>
151
119
  createErrorResponse(HTTP_STATUS.UNPROCESSABLE_ENTITY, msg, contentType),
152
120
 
153
121
  locked: (msg = 'Locked', contentType?: string) => createErrorResponse(423, msg, contentType),
154
122
 
155
- failedDependency: (msg = 'Failed Dependency', contentType?: string) =>
156
- createErrorResponse(424, msg, contentType),
123
+ failedDependency: (msg = 'Failed Dependency', contentType?: string) => createErrorResponse(424, msg, contentType),
157
124
 
158
125
  tooEarly: (msg = 'Too Early', contentType?: string) => createErrorResponse(425, msg, contentType),
159
126
 
160
- upgradeRequired: (msg = 'Upgrade Required', contentType?: string) =>
161
- createErrorResponse(426, msg, contentType),
127
+ upgradeRequired: (msg = 'Upgrade Required', contentType?: string) => createErrorResponse(426, msg, contentType),
162
128
 
163
129
  preconditionRequired: (msg = 'Precondition Required', contentType?: string) =>
164
130
  createErrorResponse(428, msg, contentType),
165
131
 
166
- tooManyRequests: (msg = 'Too Many Requests', contentType?: string) =>
167
- createErrorResponse(429, msg, contentType),
132
+ tooManyRequests: (msg = 'Too Many Requests', contentType?: string) => createErrorResponse(429, msg, contentType),
168
133
 
169
134
  requestHeaderFieldsTooLarge: (msg = 'Request Header Fields Too Large', contentType?: string) =>
170
135
  createErrorResponse(431, msg, contentType),
@@ -176,17 +141,13 @@ export const APIError = {
176
141
  internalServerError: (msg = 'Internal Server Error', contentType?: string) =>
177
142
  createErrorResponse(HTTP_STATUS.INTERNAL_SERVER_ERROR, msg, contentType),
178
143
 
179
- notImplemented: (msg = 'Not Implemented', contentType?: string) =>
180
- createErrorResponse(501, msg, contentType),
144
+ notImplemented: (msg = 'Not Implemented', contentType?: string) => createErrorResponse(501, msg, contentType),
181
145
 
182
- badGateway: (msg = 'Bad Gateway', contentType?: string) =>
183
- createErrorResponse(502, msg, contentType),
146
+ badGateway: (msg = 'Bad Gateway', contentType?: string) => createErrorResponse(502, msg, contentType),
184
147
 
185
- serviceUnavailable: (msg = 'Service Unavailable', contentType?: string) =>
186
- createErrorResponse(503, msg, contentType),
148
+ serviceUnavailable: (msg = 'Service Unavailable', contentType?: string) => createErrorResponse(503, msg, contentType),
187
149
 
188
- gatewayTimeout: (msg = 'Gateway Timeout', contentType?: string) =>
189
- createErrorResponse(504, msg, contentType),
150
+ gatewayTimeout: (msg = 'Gateway Timeout', contentType?: string) => createErrorResponse(504, msg, contentType),
190
151
 
191
152
  httpVersionNotSupported: (msg = 'HTTP Version Not Supported', contentType?: string) =>
192
153
  createErrorResponse(505, msg, contentType),
@@ -197,11 +158,9 @@ export const APIError = {
197
158
  insufficientStorage: (msg = 'Insufficient Storage', contentType?: string) =>
198
159
  createErrorResponse(507, msg, contentType),
199
160
 
200
- loopDetected: (msg = 'Loop Detected', contentType?: string) =>
201
- createErrorResponse(508, msg, contentType),
161
+ loopDetected: (msg = 'Loop Detected', contentType?: string) => createErrorResponse(508, msg, contentType),
202
162
 
203
- notExtended: (msg = 'Not Extended', contentType?: string) =>
204
- createErrorResponse(510, msg, contentType),
163
+ notExtended: (msg = 'Not Extended', contentType?: string) => createErrorResponse(510, msg, contentType),
205
164
 
206
165
  networkAuthenticationRequired: (msg = 'Network Authentication Required', contentType?: string) =>
207
166
  createErrorResponse(511, msg, contentType),
@@ -210,22 +169,15 @@ export const APIError = {
210
169
  invalidArgument: (msg = 'Invalid Argument', contentType?: string) =>
211
170
  createErrorResponse(HTTP_STATUS.UNPROCESSABLE_ENTITY, msg, contentType),
212
171
 
213
- rateLimitExceeded: (msg = 'Rate Limit Exceeded', contentType?: string) =>
214
- createErrorResponse(429, msg, contentType),
172
+ rateLimitExceeded: (msg = 'Rate Limit Exceeded', contentType?: string) => createErrorResponse(429, msg, contentType),
215
173
 
216
- maintenance: (msg = 'Service Under Maintenance', contentType?: string) =>
217
- createErrorResponse(503, msg, contentType),
174
+ maintenance: (msg = 'Service Under Maintenance', contentType?: string) => createErrorResponse(503, msg, contentType),
218
175
 
219
176
  // Helper to create custom error with any status code
220
- custom: (statusCode: number, msg: string, contentType?: string) =>
221
- createErrorResponse(statusCode, msg, contentType),
177
+ custom: (statusCode: number, msg: string, contentType?: string) => createErrorResponse(statusCode, msg, contentType),
222
178
  };
223
179
 
224
- export function createResponse(
225
- statusCode: number,
226
- data?: unknown,
227
- contentType: string = CONTENT_TYPES.JSON
228
- ): Response {
180
+ export function createResponse(statusCode: number, data?: unknown, contentType: string = CONTENT_TYPES.JSON): Response {
229
181
  const body = contentType === CONTENT_TYPES.JSON ? stringifyData(data) : data;
230
182
 
231
183
  return new Response(body as string, {
@@ -238,7 +190,6 @@ export const protectedRoute = async <TTypes extends VectorTypes = DefaultVectorT
238
190
  request: VectorRequest<TTypes>,
239
191
  responseContentType?: string
240
192
  ) => {
241
- // Get the Vector instance to access the protected handler
242
193
  const vector = getVectorInstance();
243
194
 
244
195
  const protectedHandler = vector.getProtectedHandler();
@@ -250,25 +201,24 @@ export const protectedRoute = async <TTypes extends VectorTypes = DefaultVectorT
250
201
  const authUser = await protectedHandler(request as any);
251
202
  request.authUser = authUser as GetAuthType<TTypes>;
252
203
  } catch (error) {
253
- throw APIError.unauthorized(
254
- error instanceof Error ? error.message : 'Authentication failed',
255
- responseContentType
256
- );
204
+ throw APIError.unauthorized(error instanceof Error ? error.message : 'Authentication failed', responseContentType);
257
205
  }
258
206
  };
259
207
 
260
- export interface ApiOptions {
208
+ export interface ApiOptions<TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined> {
261
209
  auth?: boolean;
262
210
  expose?: boolean;
263
211
  rawRequest?: boolean;
212
+ validate?: boolean;
264
213
  rawResponse?: boolean;
265
214
  cache?: CacheOptions | number | null;
266
215
  responseContentType?: string;
216
+ schema?: TSchemaDef;
267
217
  }
268
218
 
269
- export function api<TTypes extends VectorTypes = DefaultVectorTypes>(
219
+ export function api<TTypes extends VectorTypes = DefaultVectorTypes, TValidatedInput = undefined>(
270
220
  options: ApiOptions,
271
- fn: (request: VectorRequest<TTypes>) => Promise<unknown>
221
+ fn: (request: VectorRequest<TTypes, TValidatedInput>) => Promise<unknown> | unknown
272
222
  ) {
273
223
  const {
274
224
  auth = false,
@@ -278,32 +228,42 @@ export function api<TTypes extends VectorTypes = DefaultVectorTypes>(
278
228
  responseContentType = CONTENT_TYPES.JSON,
279
229
  } = options;
280
230
 
281
- // For backward compatibility with direct route usage (not auto-discovered)
282
- // This wrapper is only used when routes are NOT auto-discovered
283
- return async (request: IRequest) => {
231
+ return async (request: Request) => {
232
+ const req = request as unknown as VectorRequest<TTypes>;
233
+
284
234
  if (!expose) {
285
235
  return APIError.forbidden('Forbidden');
286
236
  }
287
237
 
288
238
  try {
289
239
  if (auth) {
290
- await protectedRoute(request as any as VectorRequest<TTypes>, responseContentType);
240
+ await protectedRoute(req, responseContentType);
291
241
  }
292
242
 
293
- if (!rawRequest) {
294
- await withContent(request);
243
+ if (!rawRequest && req.method !== 'GET' && req.method !== 'HEAD') {
244
+ try {
245
+ const contentType = req.headers.get('content-type');
246
+ if (contentType?.startsWith('application/json')) {
247
+ req.content = await req.json();
248
+ } else if (contentType?.startsWith('application/x-www-form-urlencoded')) {
249
+ req.content = Object.fromEntries(await req.formData());
250
+ } else if (contentType?.startsWith('multipart/form-data')) {
251
+ req.content = await req.formData();
252
+ } else {
253
+ req.content = await req.text();
254
+ }
255
+ } catch {
256
+ req.content = null;
257
+ }
295
258
  }
296
259
 
297
- // Cache handling is now done in the router
298
- const result = await fn(request as any as VectorRequest<TTypes>);
260
+ const result = await fn(req as unknown as VectorRequest<TTypes, TValidatedInput>);
299
261
 
300
262
  return rawResponse ? result : ApiResponse.success(result, responseContentType);
301
263
  } catch (err: unknown) {
302
- // Ensure we return a Response object
303
264
  if (err instanceof Response) {
304
265
  return err;
305
266
  }
306
- // For non-Response errors, wrap them
307
267
  return APIError.internalServerError(String(err), responseContentType);
308
268
  }
309
269
  };
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
- // Public exports for route definitions only
1
+ // Public exports for route definitions and Vector startup API
2
2
  import { route } from './http';
3
+ import { startVector } from './start-vector';
3
4
 
4
5
  // Export route function for defining routes
5
6
  export { route };
7
+ export { startVector };
6
8
 
7
9
  // Export utilities for route handlers
8
10
  export { APIError, createResponse } from './http';
@@ -10,5 +12,4 @@ export { APIError, createResponse } from './http';
10
12
  // Export types for TypeScript users
11
13
  export * from './types';
12
14
 
13
- // Note: Vector framework is now config-driven and runs via CLI
14
- // Usage: Create vector.config.ts and run 'vector dev' or 'vector start'
15
+ // Note: Vector is config-driven and can run via CLI or programmatic startup API
@@ -19,6 +19,8 @@ export class MiddlewareManager<TTypes extends VectorTypes = DefaultVectorTypes>
19
19
  }
20
20
 
21
21
  async executeBefore(request: VectorRequest<TTypes>): Promise<VectorRequest<TTypes> | Response> {
22
+ if (this.beforeHandlers.length === 0) return request;
23
+
22
24
  let currentRequest = request;
23
25
 
24
26
  for (const handler of this.beforeHandlers) {
@@ -35,6 +37,8 @@ export class MiddlewareManager<TTypes extends VectorTypes = DefaultVectorTypes>
35
37
  }
36
38
 
37
39
  async executeFinally(response: Response, request: VectorRequest<TTypes>): Promise<Response> {
40
+ if (this.finallyHandlers.length === 0) return response;
41
+
38
42
  let currentResponse = response;
39
43
 
40
44
  for (const handler of this.finallyHandlers) {