vector-framework 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +87 -634
  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 +5 -2
  5. package/dist/cache/manager.d.ts.map +1 -1
  6. package/dist/cache/manager.js +21 -12
  7. package/dist/cache/manager.js.map +1 -1
  8. package/dist/cli/index.js +60 -126
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/option-resolution.d.ts +4 -0
  11. package/dist/cli/option-resolution.d.ts.map +1 -0
  12. package/dist/cli/option-resolution.js +28 -0
  13. package/dist/cli/option-resolution.js.map +1 -0
  14. package/dist/cli.js +2774 -599
  15. package/dist/constants/index.d.ts +3 -0
  16. package/dist/constants/index.d.ts.map +1 -1
  17. package/dist/constants/index.js +6 -0
  18. package/dist/constants/index.js.map +1 -1
  19. package/dist/core/config-loader.d.ts +2 -2
  20. package/dist/core/config-loader.d.ts.map +1 -1
  21. package/dist/core/config-loader.js +18 -18
  22. package/dist/core/config-loader.js.map +1 -1
  23. package/dist/core/router.d.ts +41 -15
  24. package/dist/core/router.d.ts.map +1 -1
  25. package/dist/core/router.js +465 -150
  26. package/dist/core/router.js.map +1 -1
  27. package/dist/core/server.d.ts +17 -3
  28. package/dist/core/server.d.ts.map +1 -1
  29. package/dist/core/server.js +274 -33
  30. package/dist/core/server.js.map +1 -1
  31. package/dist/core/vector.d.ts +9 -8
  32. package/dist/core/vector.d.ts.map +1 -1
  33. package/dist/core/vector.js +40 -32
  34. package/dist/core/vector.js.map +1 -1
  35. package/dist/dev/route-generator.d.ts.map +1 -1
  36. package/dist/dev/route-generator.js.map +1 -1
  37. package/dist/dev/route-scanner.d.ts +1 -1
  38. package/dist/dev/route-scanner.d.ts.map +1 -1
  39. package/dist/dev/route-scanner.js +37 -43
  40. package/dist/dev/route-scanner.js.map +1 -1
  41. package/dist/http.d.ts +14 -14
  42. package/dist/http.d.ts.map +1 -1
  43. package/dist/http.js +84 -84
  44. package/dist/http.js.map +1 -1
  45. package/dist/index.d.ts +3 -3
  46. package/dist/index.js +1314 -8
  47. package/dist/index.mjs +1314 -8
  48. package/dist/middleware/manager.d.ts +1 -1
  49. package/dist/middleware/manager.d.ts.map +1 -1
  50. package/dist/middleware/manager.js +4 -0
  51. package/dist/middleware/manager.js.map +1 -1
  52. package/dist/openapi/docs-ui.d.ts +2 -0
  53. package/dist/openapi/docs-ui.d.ts.map +1 -0
  54. package/dist/openapi/docs-ui.js +1313 -0
  55. package/dist/openapi/docs-ui.js.map +1 -0
  56. package/dist/openapi/generator.d.ts +12 -0
  57. package/dist/openapi/generator.d.ts.map +1 -0
  58. package/dist/openapi/generator.js +273 -0
  59. package/dist/openapi/generator.js.map +1 -0
  60. package/dist/types/index.d.ts +70 -11
  61. package/dist/types/index.d.ts.map +1 -1
  62. package/dist/types/standard-schema.d.ts +118 -0
  63. package/dist/types/standard-schema.d.ts.map +1 -0
  64. package/dist/types/standard-schema.js +2 -0
  65. package/dist/types/standard-schema.js.map +1 -0
  66. package/dist/utils/cors.d.ts +13 -0
  67. package/dist/utils/cors.d.ts.map +1 -0
  68. package/dist/utils/cors.js +89 -0
  69. package/dist/utils/cors.js.map +1 -0
  70. package/dist/utils/path.d.ts +7 -0
  71. package/dist/utils/path.d.ts.map +1 -1
  72. package/dist/utils/path.js +14 -3
  73. package/dist/utils/path.js.map +1 -1
  74. package/dist/utils/schema-validation.d.ts +31 -0
  75. package/dist/utils/schema-validation.d.ts.map +1 -0
  76. package/dist/utils/schema-validation.js +77 -0
  77. package/dist/utils/schema-validation.js.map +1 -0
  78. package/dist/utils/validation.d.ts.map +1 -1
  79. package/dist/utils/validation.js +1 -0
  80. package/dist/utils/validation.js.map +1 -1
  81. package/package.json +24 -19
  82. package/src/auth/protected.ts +3 -13
  83. package/src/cache/manager.ts +25 -30
  84. package/src/cli/index.ts +62 -141
  85. package/src/cli/option-resolution.ts +40 -0
  86. package/src/constants/index.ts +7 -0
  87. package/src/core/config-loader.ts +20 -22
  88. package/src/core/router.ts +535 -155
  89. package/src/core/server.ts +354 -45
  90. package/src/core/vector.ts +71 -61
  91. package/src/dev/route-generator.ts +1 -3
  92. package/src/dev/route-scanner.ts +38 -51
  93. package/src/http.ts +117 -187
  94. package/src/index.ts +3 -3
  95. package/src/middleware/manager.ts +8 -11
  96. package/src/openapi/assets/tailwindcdn.js +83 -0
  97. package/src/openapi/docs-ui.ts +1317 -0
  98. package/src/openapi/generator.ts +359 -0
  99. package/src/types/index.ts +104 -17
  100. package/src/types/standard-schema.ts +147 -0
  101. package/src/utils/cors.ts +101 -0
  102. package/src/utils/path.ts +19 -4
  103. package/src/utils/schema-validation.ts +123 -0
  104. package/src/utils/validation.ts +1 -0
@@ -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
  }
@@ -1,32 +1,31 @@
1
- import { existsSync, promises as fs } from "node:fs";
2
- import { join, relative, resolve, sep } from "node:path";
3
- import type { GeneratedRoute } from "../types";
1
+ import { existsSync, promises as fs } from 'node:fs';
2
+ import { join, relative, resolve, sep } from 'node:path';
3
+ import type { GeneratedRoute } from '../types';
4
4
 
5
5
  export class RouteScanner {
6
6
  private routesDir: string;
7
7
  private excludePatterns: string[];
8
8
  private static readonly DEFAULT_EXCLUDE_PATTERNS = [
9
- "*.test.ts",
10
- "*.test.js",
11
- "*.test.tsx",
12
- "*.test.jsx",
13
- "*.spec.ts",
14
- "*.spec.js",
15
- "*.spec.tsx",
16
- "*.spec.jsx",
17
- "*.tests.ts",
18
- "*.tests.js",
19
- "**/__tests__/**",
20
- "*.interface.ts",
21
- "*.type.ts",
22
- "*.d.ts",
9
+ '*.test.ts',
10
+ '*.test.js',
11
+ '*.test.tsx',
12
+ '*.test.jsx',
13
+ '*.spec.ts',
14
+ '*.spec.js',
15
+ '*.spec.tsx',
16
+ '*.spec.jsx',
17
+ '*.tests.ts',
18
+ '*.tests.js',
19
+ '**/__tests__/**',
20
+ '*.interface.ts',
21
+ '*.type.ts',
22
+ '*.d.ts',
23
23
  ];
24
24
 
25
- constructor(routesDir = "./routes", excludePatterns?: string[]) {
25
+ constructor(routesDir = './routes', excludePatterns?: string[]) {
26
26
  // Always resolve from the current working directory (user's project)
27
27
  this.routesDir = resolve(process.cwd(), routesDir);
28
- this.excludePatterns =
29
- excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
28
+ this.excludePatterns = excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
30
29
  }
31
30
 
32
31
  async scan(): Promise<GeneratedRoute[]> {
@@ -40,7 +39,7 @@ export class RouteScanner {
40
39
  try {
41
40
  await this.scanDirectory(this.routesDir, routes);
42
41
  } catch (error) {
43
- if ((error as any).code === "ENOENT") {
42
+ if ((error as any).code === 'ENOENT') {
44
43
  console.warn(` ✗ Routes directory not accessible: ${this.routesDir}`);
45
44
  return [];
46
45
  }
@@ -56,15 +55,16 @@ export class RouteScanner {
56
55
  for (const pattern of this.excludePatterns) {
57
56
  // Convert glob pattern to regex
58
57
  const regexPattern = pattern
59
- .replace(/\./g, "\\.") // Escape dots
60
- .replace(/\*/g, "[^/]*") // * matches anything except /
61
- .replace(/\*\*/g, ".*") // ** matches anything including /
62
- .replace(/\?/g, "."); // ? matches single character
58
+ .replace(/\./g, '\\.') // Escape dots
59
+ .replace(/\*\*/g, '__GLOBSTAR__') // protect ** before * replacement
60
+ .replace(/\*/g, '[^/]*') // * matches anything except /
61
+ .replace(/__GLOBSTAR__/g, '.*') // ** matches anything including /
62
+ .replace(/\?/g, '.'); // ? matches single character
63
63
 
64
64
  const regex = new RegExp(`^${regexPattern}$`);
65
65
 
66
66
  // Check both the full relative path and just the filename
67
- const filename = relativePath.split(sep).pop() || "";
67
+ const filename = relativePath.split(sep).pop() || '';
68
68
  if (regex.test(relativePath) || regex.test(filename)) {
69
69
  return true;
70
70
  }
@@ -73,11 +73,7 @@ export class RouteScanner {
73
73
  return false;
74
74
  }
75
75
 
76
- private async scanDirectory(
77
- dir: string,
78
- routes: GeneratedRoute[],
79
- basePath = ""
80
- ): Promise<void> {
76
+ private async scanDirectory(dir: string, routes: GeneratedRoute[], basePath = ''): Promise<void> {
81
77
  const entries = await fs.readdir(dir);
82
78
 
83
79
  for (const entry of entries) {
@@ -87,32 +83,29 @@ export class RouteScanner {
87
83
  if (stats.isDirectory()) {
88
84
  const newBasePath = basePath ? `${basePath}/${entry}` : entry;
89
85
  await this.scanDirectory(fullPath, routes, newBasePath);
90
- } else if (entry.endsWith(".ts") || entry.endsWith(".js")) {
86
+ } else if (entry.endsWith('.ts') || entry.endsWith('.js')) {
91
87
  // Skip excluded files (test files, etc.)
92
88
  if (this.isExcluded(fullPath)) {
93
89
  continue;
94
90
  }
95
91
  const routePath = relative(this.routesDir, fullPath)
96
- .replace(/\.(ts|js)$/, "")
92
+ .replace(/\.(ts|js)$/, '')
97
93
  .split(sep)
98
- .join("/");
94
+ .join('/');
99
95
 
100
96
  try {
101
97
  // Convert Windows paths to URLs for import
102
- const importPath =
103
- process.platform === "win32"
104
- ? `file:///${fullPath.replace(/\\/g, "/")}`
105
- : fullPath;
98
+ const importPath = process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
106
99
 
107
100
  const module = await import(importPath);
108
101
 
109
- if (module.default && typeof module.default === "function") {
102
+ if (module.default && typeof module.default === 'function') {
110
103
  routes.push({
111
- name: "default",
104
+ name: 'default',
112
105
  path: fullPath,
113
- method: "GET",
106
+ method: 'GET',
114
107
  options: {
115
- method: "GET",
108
+ method: 'GET',
116
109
  path: `/${routePath}`,
117
110
  expose: true,
118
111
  },
@@ -120,16 +113,10 @@ export class RouteScanner {
120
113
  }
121
114
 
122
115
  for (const [name, value] of Object.entries(module)) {
123
- if (name === "default") continue;
116
+ if (name === 'default') continue;
124
117
 
125
118
  // Check for new RouteDefinition format
126
- if (
127
- value &&
128
- typeof value === "object" &&
129
- "entry" in value &&
130
- "options" in value &&
131
- "handler" in value
132
- ) {
119
+ if (value && typeof value === 'object' && 'entry' in value && 'options' in value && 'handler' in value) {
133
120
  const routeDef = value as any;
134
121
  routes.push({
135
122
  name,
@@ -161,7 +148,7 @@ export class RouteScanner {
161
148
  }
162
149
 
163
150
  enableWatch(callback: () => void) {
164
- if (typeof Bun !== "undefined" && Bun.env.NODE_ENV === "development") {
151
+ if (typeof Bun !== 'undefined' && Bun.env.NODE_ENV === 'development') {
165
152
  console.log(`Watching for route changes in ${this.routesDir}`);
166
153
 
167
154
  setInterval(async () => {
package/src/http.ts CHANGED
@@ -1,89 +1,66 @@
1
- import { cors, type IRequest, type RouteEntry, withContent } from "itty-router";
2
- import { CONTENT_TYPES, HTTP_STATUS } from "./constants";
1
+ import { CONTENT_TYPES, HTTP_STATUS } from './constants';
3
2
  import type {
4
3
  CacheOptions,
5
4
  DefaultVectorTypes,
6
5
  GetAuthType,
6
+ InferRouteInputFromSchemaDefinition,
7
+ RouteSchemaDefinition,
7
8
  VectorRequest,
8
9
  VectorTypes,
9
- } from "./types";
10
- import { getVectorInstance } from "./core/vector";
10
+ } from './types';
11
+ import { getVectorInstance } from './core/vector';
11
12
 
12
- export interface ProtectedRequest<
13
- 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
19
  export interface RouteDefinition<
33
- TTypes extends VectorTypes = DefaultVectorTypes
20
+ TTypes extends VectorTypes = DefaultVectorTypes,
21
+ TValidatedInput = undefined,
22
+ TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined,
34
23
  > {
35
- entry: RouteEntry;
36
- options: ExtendedApiOptions;
37
- handler: (req: VectorRequest<TTypes>) => Promise<unknown>;
24
+ entry: { method: string; path: string };
25
+ options: ExtendedApiOptions<TSchemaDef>;
26
+ handler: (req: VectorRequest<TTypes, TValidatedInput>) => Promise<unknown> | unknown;
38
27
  }
39
28
 
40
- export function route<TTypes extends VectorTypes = DefaultVectorTypes>(
41
- options: ExtendedApiOptions,
42
- fn: (req: VectorRequest<TTypes>) => Promise<unknown>
43
- ): RouteDefinition<TTypes> {
44
- const handler = api(options, fn);
45
-
46
- const entry: RouteEntry = [
47
- options.method.toUpperCase(),
48
- RegExp(
49
- `^${
50
- options.path
51
- .replace(/\/+(\/|$)/g, "$1") // strip double & trailing splash
52
- .replace(/(\/?\.?):(\w+)\+/g, "($1(?<$2>*))") // greedy params
53
- .replace(/(\/?\.?):(\w+)/g, "($1(?<$2>[^$1/]+?))") // named params and image format
54
- .replace(/\./g, "\\.") // dot in path
55
- .replace(/(\/?)\*/g, "($1.*)?") // wildcard
56
- }/*$`
57
- ),
58
- [handler],
59
- options.path,
60
- ];
61
-
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> {
62
36
  return {
63
- entry,
37
+ entry: {
38
+ method: options.method.toUpperCase(),
39
+ path: options.path,
40
+ },
64
41
  options,
65
42
  handler: fn,
66
43
  };
67
44
  }
68
45
 
69
46
  function stringifyData(data: unknown): string {
70
- return JSON.stringify(data ?? null, (_key, value) =>
71
- typeof value === "bigint" ? value.toString() : value
72
- );
47
+ const val = data ?? null;
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
- success: <T>(data: T, contentType?: string) =>
77
- createResponse(HTTP_STATUS.OK, data, contentType),
78
- created: <T>(data: T, contentType?: string) =>
79
- createResponse(HTTP_STATUS.CREATED, data, contentType),
59
+ success: <T>(data: T, contentType?: string) => createResponse(HTTP_STATUS.OK, data, contentType),
60
+ created: <T>(data: T, contentType?: string) => createResponse(HTTP_STATUS.CREATED, data, contentType),
80
61
  };
81
62
 
82
- function createErrorResponse(
83
- code: number,
84
- message: string,
85
- contentType?: string
86
- ): Response {
63
+ function createErrorResponse(code: number, message: string, contentType?: string): Response {
87
64
  const errorBody = {
88
65
  error: true,
89
66
  message,
@@ -96,204 +73,152 @@ function createErrorResponse(
96
73
 
97
74
  export const APIError = {
98
75
  // 4xx Client Errors
99
- badRequest: (msg = "Bad Request", contentType?: string) =>
76
+ badRequest: (msg = 'Bad Request', contentType?: string) =>
100
77
  createErrorResponse(HTTP_STATUS.BAD_REQUEST, msg, contentType),
101
78
 
102
- unauthorized: (msg = "Unauthorized", contentType?: string) =>
79
+ unauthorized: (msg = 'Unauthorized', contentType?: string) =>
103
80
  createErrorResponse(HTTP_STATUS.UNAUTHORIZED, msg, contentType),
104
81
 
105
- paymentRequired: (msg = "Payment Required", contentType?: string) =>
106
- createErrorResponse(402, msg, contentType),
82
+ paymentRequired: (msg = 'Payment Required', contentType?: string) => createErrorResponse(402, msg, contentType),
107
83
 
108
- forbidden: (msg = "Forbidden", contentType?: string) =>
109
- createErrorResponse(HTTP_STATUS.FORBIDDEN, msg, contentType),
84
+ forbidden: (msg = 'Forbidden', contentType?: string) => createErrorResponse(HTTP_STATUS.FORBIDDEN, msg, contentType),
110
85
 
111
- notFound: (msg = "Not Found", contentType?: string) =>
112
- createErrorResponse(HTTP_STATUS.NOT_FOUND, msg, contentType),
86
+ notFound: (msg = 'Not Found', contentType?: string) => createErrorResponse(HTTP_STATUS.NOT_FOUND, msg, contentType),
113
87
 
114
- methodNotAllowed: (msg = "Method Not Allowed", contentType?: string) =>
115
- createErrorResponse(405, msg, contentType),
88
+ methodNotAllowed: (msg = 'Method Not Allowed', contentType?: string) => createErrorResponse(405, msg, contentType),
116
89
 
117
- notAcceptable: (msg = "Not Acceptable", contentType?: string) =>
118
- createErrorResponse(406, msg, contentType),
90
+ notAcceptable: (msg = 'Not Acceptable', contentType?: string) => createErrorResponse(406, msg, contentType),
119
91
 
120
- requestTimeout: (msg = "Request Timeout", contentType?: string) =>
121
- createErrorResponse(408, msg, contentType),
92
+ requestTimeout: (msg = 'Request Timeout', contentType?: string) => createErrorResponse(408, msg, contentType),
122
93
 
123
- conflict: (msg = "Conflict", contentType?: string) =>
124
- createErrorResponse(HTTP_STATUS.CONFLICT, msg, contentType),
94
+ conflict: (msg = 'Conflict', contentType?: string) => createErrorResponse(HTTP_STATUS.CONFLICT, msg, contentType),
125
95
 
126
- gone: (msg = "Gone", contentType?: string) =>
127
- createErrorResponse(410, msg, contentType),
96
+ gone: (msg = 'Gone', contentType?: string) => createErrorResponse(410, msg, contentType),
128
97
 
129
- lengthRequired: (msg = "Length Required", contentType?: string) =>
130
- createErrorResponse(411, msg, contentType),
98
+ lengthRequired: (msg = 'Length Required', contentType?: string) => createErrorResponse(411, msg, contentType),
131
99
 
132
- preconditionFailed: (msg = "Precondition Failed", contentType?: string) =>
133
- createErrorResponse(412, msg, contentType),
100
+ preconditionFailed: (msg = 'Precondition Failed', contentType?: string) => createErrorResponse(412, msg, contentType),
134
101
 
135
- payloadTooLarge: (msg = "Payload Too Large", contentType?: string) =>
136
- createErrorResponse(413, msg, contentType),
102
+ payloadTooLarge: (msg = 'Payload Too Large', contentType?: string) => createErrorResponse(413, msg, contentType),
137
103
 
138
- uriTooLong: (msg = "URI Too Long", contentType?: string) =>
139
- createErrorResponse(414, msg, contentType),
104
+ uriTooLong: (msg = 'URI Too Long', contentType?: string) => createErrorResponse(414, msg, contentType),
140
105
 
141
- unsupportedMediaType: (
142
- msg = "Unsupported Media Type",
143
- contentType?: string
144
- ) => createErrorResponse(415, msg, contentType),
106
+ unsupportedMediaType: (msg = 'Unsupported Media Type', contentType?: string) =>
107
+ createErrorResponse(415, msg, contentType),
145
108
 
146
- rangeNotSatisfiable: (msg = "Range Not Satisfiable", contentType?: string) =>
109
+ rangeNotSatisfiable: (msg = 'Range Not Satisfiable', contentType?: string) =>
147
110
  createErrorResponse(416, msg, contentType),
148
111
 
149
- expectationFailed: (msg = "Expectation Failed", contentType?: string) =>
150
- createErrorResponse(417, msg, contentType),
112
+ expectationFailed: (msg = 'Expectation Failed', contentType?: string) => createErrorResponse(417, msg, contentType),
151
113
 
152
- imATeapot: (msg = "I'm a teapot", contentType?: string) =>
153
- createErrorResponse(418, msg, contentType),
114
+ imATeapot: (msg = "I'm a teapot", contentType?: string) => createErrorResponse(418, msg, contentType),
154
115
 
155
- misdirectedRequest: (msg = "Misdirected Request", contentType?: string) =>
156
- createErrorResponse(421, msg, contentType),
116
+ misdirectedRequest: (msg = 'Misdirected Request', contentType?: string) => createErrorResponse(421, msg, contentType),
157
117
 
158
- unprocessableEntity: (msg = "Unprocessable Entity", contentType?: string) =>
118
+ unprocessableEntity: (msg = 'Unprocessable Entity', contentType?: string) =>
159
119
  createErrorResponse(HTTP_STATUS.UNPROCESSABLE_ENTITY, msg, contentType),
160
120
 
161
- locked: (msg = "Locked", contentType?: string) =>
162
- createErrorResponse(423, msg, contentType),
121
+ locked: (msg = 'Locked', contentType?: string) => createErrorResponse(423, msg, contentType),
163
122
 
164
- failedDependency: (msg = "Failed Dependency", contentType?: string) =>
165
- createErrorResponse(424, msg, contentType),
123
+ failedDependency: (msg = 'Failed Dependency', contentType?: string) => createErrorResponse(424, msg, contentType),
166
124
 
167
- tooEarly: (msg = "Too Early", contentType?: string) =>
168
- createErrorResponse(425, msg, contentType),
125
+ tooEarly: (msg = 'Too Early', contentType?: string) => createErrorResponse(425, msg, contentType),
169
126
 
170
- upgradeRequired: (msg = "Upgrade Required", contentType?: string) =>
171
- createErrorResponse(426, msg, contentType),
127
+ upgradeRequired: (msg = 'Upgrade Required', contentType?: string) => createErrorResponse(426, msg, contentType),
172
128
 
173
- preconditionRequired: (msg = "Precondition Required", contentType?: string) =>
129
+ preconditionRequired: (msg = 'Precondition Required', contentType?: string) =>
174
130
  createErrorResponse(428, msg, contentType),
175
131
 
176
- tooManyRequests: (msg = "Too Many Requests", contentType?: string) =>
177
- createErrorResponse(429, msg, contentType),
132
+ tooManyRequests: (msg = 'Too Many Requests', contentType?: string) => createErrorResponse(429, msg, contentType),
178
133
 
179
- requestHeaderFieldsTooLarge: (
180
- msg = "Request Header Fields Too Large",
181
- contentType?: string
182
- ) => createErrorResponse(431, msg, contentType),
134
+ requestHeaderFieldsTooLarge: (msg = 'Request Header Fields Too Large', contentType?: string) =>
135
+ createErrorResponse(431, msg, contentType),
183
136
 
184
- unavailableForLegalReasons: (
185
- msg = "Unavailable For Legal Reasons",
186
- contentType?: string
187
- ) => createErrorResponse(451, msg, contentType),
137
+ unavailableForLegalReasons: (msg = 'Unavailable For Legal Reasons', contentType?: string) =>
138
+ createErrorResponse(451, msg, contentType),
188
139
 
189
140
  // 5xx Server Errors
190
- internalServerError: (msg = "Internal Server Error", contentType?: string) =>
141
+ internalServerError: (msg = 'Internal Server Error', contentType?: string) =>
191
142
  createErrorResponse(HTTP_STATUS.INTERNAL_SERVER_ERROR, msg, contentType),
192
143
 
193
- notImplemented: (msg = "Not Implemented", contentType?: string) =>
194
- createErrorResponse(501, msg, contentType),
144
+ notImplemented: (msg = 'Not Implemented', contentType?: string) => createErrorResponse(501, msg, contentType),
195
145
 
196
- badGateway: (msg = "Bad Gateway", contentType?: string) =>
197
- createErrorResponse(502, msg, contentType),
146
+ badGateway: (msg = 'Bad Gateway', contentType?: string) => createErrorResponse(502, msg, contentType),
198
147
 
199
- serviceUnavailable: (msg = "Service Unavailable", contentType?: string) =>
200
- createErrorResponse(503, msg, contentType),
148
+ serviceUnavailable: (msg = 'Service Unavailable', contentType?: string) => createErrorResponse(503, msg, contentType),
201
149
 
202
- gatewayTimeout: (msg = "Gateway Timeout", contentType?: string) =>
203
- createErrorResponse(504, msg, contentType),
150
+ gatewayTimeout: (msg = 'Gateway Timeout', contentType?: string) => createErrorResponse(504, msg, contentType),
204
151
 
205
- httpVersionNotSupported: (
206
- msg = "HTTP Version Not Supported",
207
- contentType?: string
208
- ) => createErrorResponse(505, msg, contentType),
152
+ httpVersionNotSupported: (msg = 'HTTP Version Not Supported', contentType?: string) =>
153
+ createErrorResponse(505, msg, contentType),
209
154
 
210
- variantAlsoNegotiates: (
211
- msg = "Variant Also Negotiates",
212
- contentType?: string
213
- ) => createErrorResponse(506, msg, contentType),
155
+ variantAlsoNegotiates: (msg = 'Variant Also Negotiates', contentType?: string) =>
156
+ createErrorResponse(506, msg, contentType),
214
157
 
215
- insufficientStorage: (msg = "Insufficient Storage", contentType?: string) =>
158
+ insufficientStorage: (msg = 'Insufficient Storage', contentType?: string) =>
216
159
  createErrorResponse(507, msg, contentType),
217
160
 
218
- loopDetected: (msg = "Loop Detected", contentType?: string) =>
219
- createErrorResponse(508, msg, contentType),
161
+ loopDetected: (msg = 'Loop Detected', contentType?: string) => createErrorResponse(508, msg, contentType),
220
162
 
221
- notExtended: (msg = "Not Extended", contentType?: string) =>
222
- createErrorResponse(510, msg, contentType),
163
+ notExtended: (msg = 'Not Extended', contentType?: string) => createErrorResponse(510, msg, contentType),
223
164
 
224
- networkAuthenticationRequired: (
225
- msg = "Network Authentication Required",
226
- contentType?: string
227
- ) => createErrorResponse(511, msg, contentType),
165
+ networkAuthenticationRequired: (msg = 'Network Authentication Required', contentType?: string) =>
166
+ createErrorResponse(511, msg, contentType),
228
167
 
229
168
  // Aliases for common use cases
230
- invalidArgument: (msg = "Invalid Argument", contentType?: string) =>
169
+ invalidArgument: (msg = 'Invalid Argument', contentType?: string) =>
231
170
  createErrorResponse(HTTP_STATUS.UNPROCESSABLE_ENTITY, msg, contentType),
232
171
 
233
- rateLimitExceeded: (msg = "Rate Limit Exceeded", contentType?: string) =>
234
- createErrorResponse(429, msg, contentType),
172
+ rateLimitExceeded: (msg = 'Rate Limit Exceeded', contentType?: string) => createErrorResponse(429, msg, contentType),
235
173
 
236
- maintenance: (msg = "Service Under Maintenance", contentType?: string) =>
237
- createErrorResponse(503, msg, contentType),
174
+ maintenance: (msg = 'Service Under Maintenance', contentType?: string) => createErrorResponse(503, msg, contentType),
238
175
 
239
176
  // Helper to create custom error with any status code
240
- custom: (statusCode: number, msg: string, contentType?: string) =>
241
- createErrorResponse(statusCode, msg, contentType),
177
+ custom: (statusCode: number, msg: string, contentType?: string) => createErrorResponse(statusCode, msg, contentType),
242
178
  };
243
179
 
244
- export function createResponse(
245
- statusCode: number,
246
- data?: unknown,
247
- contentType: string = CONTENT_TYPES.JSON
248
- ): Response {
180
+ export function createResponse(statusCode: number, data?: unknown, contentType: string = CONTENT_TYPES.JSON): Response {
249
181
  const body = contentType === CONTENT_TYPES.JSON ? stringifyData(data) : data;
250
182
 
251
183
  return new Response(body as string, {
252
184
  status: statusCode,
253
- headers: { "content-type": contentType },
185
+ headers: { 'content-type': contentType },
254
186
  });
255
187
  }
256
188
 
257
- export const protectedRoute = async <
258
- TTypes extends VectorTypes = DefaultVectorTypes
259
- >(
189
+ export const protectedRoute = async <TTypes extends VectorTypes = DefaultVectorTypes>(
260
190
  request: VectorRequest<TTypes>,
261
191
  responseContentType?: string
262
192
  ) => {
263
- // Get the Vector instance to access the protected handler
264
193
  const vector = getVectorInstance();
265
194
 
266
195
  const protectedHandler = vector.getProtectedHandler();
267
196
  if (!protectedHandler) {
268
- throw APIError.unauthorized(
269
- "Authentication not configured",
270
- responseContentType
271
- );
197
+ throw APIError.unauthorized('Authentication not configured', responseContentType);
272
198
  }
273
199
 
274
200
  try {
275
201
  const authUser = await protectedHandler(request as any);
276
202
  request.authUser = authUser as GetAuthType<TTypes>;
277
203
  } catch (error) {
278
- throw APIError.unauthorized(
279
- error instanceof Error ? error.message : "Authentication failed",
280
- responseContentType
281
- );
204
+ throw APIError.unauthorized(error instanceof Error ? error.message : 'Authentication failed', responseContentType);
282
205
  }
283
206
  };
284
207
 
285
- export interface ApiOptions {
208
+ export interface ApiOptions<TSchemaDef extends RouteSchemaDefinition | undefined = RouteSchemaDefinition | undefined> {
286
209
  auth?: boolean;
287
210
  expose?: boolean;
288
211
  rawRequest?: boolean;
212
+ validate?: boolean;
289
213
  rawResponse?: boolean;
290
214
  cache?: CacheOptions | number | null;
291
215
  responseContentType?: string;
216
+ schema?: TSchemaDef;
292
217
  }
293
218
 
294
- export function api<TTypes extends VectorTypes = DefaultVectorTypes>(
219
+ export function api<TTypes extends VectorTypes = DefaultVectorTypes, TValidatedInput = undefined>(
295
220
  options: ApiOptions,
296
- fn: (request: VectorRequest<TTypes>) => Promise<unknown>
221
+ fn: (request: VectorRequest<TTypes, TValidatedInput>) => Promise<unknown> | unknown
297
222
  ) {
298
223
  const {
299
224
  auth = false,
@@ -303,37 +228,42 @@ export function api<TTypes extends VectorTypes = DefaultVectorTypes>(
303
228
  responseContentType = CONTENT_TYPES.JSON,
304
229
  } = options;
305
230
 
306
- // For backward compatibility with direct route usage (not auto-discovered)
307
- // This wrapper is only used when routes are NOT auto-discovered
308
- return async (request: IRequest) => {
231
+ return async (request: Request) => {
232
+ const req = request as unknown as VectorRequest<TTypes>;
233
+
309
234
  if (!expose) {
310
- return APIError.forbidden("Forbidden");
235
+ return APIError.forbidden('Forbidden');
311
236
  }
312
237
 
313
238
  try {
314
239
  if (auth) {
315
- await protectedRoute(
316
- request as any as VectorRequest<TTypes>,
317
- responseContentType
318
- );
240
+ await protectedRoute(req, responseContentType);
319
241
  }
320
242
 
321
- if (!rawRequest) {
322
- 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
+ }
323
258
  }
324
259
 
325
- // Cache handling is now done in the router
326
- const result = await fn(request as any as VectorRequest<TTypes>);
260
+ const result = await fn(req as unknown as VectorRequest<TTypes, TValidatedInput>);
327
261
 
328
- return rawResponse
329
- ? result
330
- : ApiResponse.success(result, responseContentType);
262
+ return rawResponse ? result : ApiResponse.success(result, responseContentType);
331
263
  } catch (err: unknown) {
332
- // Ensure we return a Response object
333
264
  if (err instanceof Response) {
334
265
  return err;
335
266
  }
336
- // For non-Response errors, wrap them
337
267
  return APIError.internalServerError(String(err), responseContentType);
338
268
  }
339
269
  };
package/src/index.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  // Public exports for route definitions only
2
- import { route } from "./http";
2
+ import { route } from './http';
3
3
 
4
4
  // Export route function for defining routes
5
5
  export { route };
6
6
 
7
7
  // Export utilities for route handlers
8
- export { APIError, createResponse } from "./http";
8
+ export { APIError, createResponse } from './http';
9
9
 
10
10
  // Export types for TypeScript users
11
- export * from "./types";
11
+ export * from './types';
12
12
 
13
13
  // Note: Vector framework is now config-driven and runs via CLI
14
14
  // Usage: Create vector.config.ts and run 'vector dev' or 'vector start'