vector-framework 0.9.7 → 0.9.9

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.
@@ -1,4 +1,5 @@
1
1
  import type { RouteEntry } from 'itty-router';
2
+ import { withCookies } from 'itty-router';
2
3
  import type { AuthManager } from '../auth/protected';
3
4
  import type { CacheManager } from '../cache/manager';
4
5
  import { APIError, createResponse } from '../http';
@@ -113,30 +114,63 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
113
114
  );
114
115
  }
115
116
 
116
- private wrapHandler(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>) {
117
- return async (request: any) => {
118
- // Ensure request has required properties
119
- const vectorRequest = request as VectorRequest<TTypes>;
117
+ private prepareRequest(
118
+ request: VectorRequest<TTypes>,
119
+ options?: {
120
+ params?: Record<string, string>;
121
+ route?: string;
122
+ metadata?: any;
123
+ }
124
+ ): void {
125
+ // Initialize context if not present
126
+ if (!request.context) {
127
+ request.context = {} as any;
128
+ }
120
129
 
121
- // Initialize context if not present
122
- if (!vectorRequest.context) {
123
- vectorRequest.context = {} as any;
124
- }
130
+ // Set params and route if provided
131
+ if (options?.params !== undefined) {
132
+ request.params = options.params;
133
+ }
134
+ if (options?.route !== undefined) {
135
+ request.route = options.route;
136
+ }
137
+ if (options?.metadata !== undefined) {
138
+ request.metadata = options.metadata;
139
+ }
125
140
 
126
- // Parse query parameters from URL (handles duplicate params as arrays)
127
- if (!vectorRequest.query && vectorRequest.url) {
128
- const url = new URL(vectorRequest.url);
129
- const query: Record<string, string | string[]> = {};
130
- for (let [k, v] of url.searchParams) {
131
- query[k] = query[k] ? ([] as string[]).concat(query[k], v) : v;
141
+ // Parse query parameters from URL if not already parsed
142
+ if (!request.query && request.url) {
143
+ const url = new URL(request.url);
144
+ const query: Record<string, string | string[]> = {};
145
+ for (const [key, value] of url.searchParams) {
146
+ if (key in query) {
147
+ if (Array.isArray(query[key])) {
148
+ (query[key] as string[]).push(value);
149
+ } else {
150
+ query[key] = [query[key] as string, value];
151
+ }
152
+ } else {
153
+ query[key] = value;
132
154
  }
133
- vectorRequest.query = query;
134
155
  }
156
+ request.query = query;
157
+ }
135
158
 
136
- // Add metadata to request if provided
137
- if (options.metadata) {
138
- vectorRequest.metadata = options.metadata;
139
- }
159
+ // Parse cookies if not already parsed
160
+ if (!request.cookies) {
161
+ withCookies(request as any);
162
+ }
163
+ }
164
+
165
+ private wrapHandler(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>) {
166
+ return async (request: any) => {
167
+ // Ensure request has required properties
168
+ const vectorRequest = request as VectorRequest<TTypes>;
169
+
170
+ // Prepare the request with common logic
171
+ this.prepareRequest(vectorRequest, {
172
+ metadata: options.metadata
173
+ });
140
174
 
141
175
  request = vectorRequest;
142
176
  try {
@@ -182,22 +216,45 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
182
216
  let result;
183
217
  const cacheOptions = options.cache;
184
218
 
219
+ // Create cache factory that handles Response objects
220
+ const cacheFactory = async () => {
221
+ const res = await handler(request);
222
+ // If Response, extract data for caching
223
+ if (res instanceof Response) {
224
+ return {
225
+ _isResponse: true,
226
+ body: await res.text(),
227
+ status: res.status,
228
+ headers: Object.fromEntries(res.headers.entries())
229
+ };
230
+ }
231
+ return res;
232
+ };
233
+
185
234
  if (cacheOptions && typeof cacheOptions === 'number' && cacheOptions > 0) {
186
235
  const cacheKey = this.cacheManager.generateKey(request as any, {
187
236
  authUser: request.authUser,
188
237
  });
189
- result = await this.cacheManager.get(cacheKey, () => handler(request), cacheOptions);
238
+ result = await this.cacheManager.get(cacheKey, cacheFactory, cacheOptions);
190
239
  } else if (cacheOptions && typeof cacheOptions === 'object' && cacheOptions.ttl) {
191
240
  const cacheKey =
192
241
  cacheOptions.key ||
193
242
  this.cacheManager.generateKey(request as any, {
194
243
  authUser: request.authUser,
195
244
  });
196
- result = await this.cacheManager.get(cacheKey, () => handler(request), cacheOptions.ttl);
245
+ result = await this.cacheManager.get(cacheKey, cacheFactory, cacheOptions.ttl);
197
246
  } else {
198
247
  result = await handler(request);
199
248
  }
200
249
 
250
+ // Reconstruct Response if it was cached
251
+ if (result && typeof result === 'object' && result._isResponse === true) {
252
+ result = new Response(result.body, {
253
+ status: result.status,
254
+ headers: result.headers
255
+ });
256
+ }
257
+
201
258
  let response: Response;
202
259
  if (options.rawResponse || result instanceof Response) {
203
260
  response = result instanceof Response ? result : new Response(result);
@@ -235,16 +292,17 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
235
292
  const url = new URL(request.url);
236
293
  const pathname = url.pathname;
237
294
 
238
- for (const [method, regex, handlers] of this.routes) {
295
+ for (const [method, regex, handlers, path] of this.routes) {
239
296
  if (request.method === 'OPTIONS' || request.method === method) {
240
297
  const match = pathname.match(regex);
241
298
  if (match) {
242
299
  const req = request as any as VectorRequest<TTypes>;
243
- // Initialize context for new request
244
- if (!req.context) {
245
- req.context = {} as any;
246
- }
247
- req.params = match.groups || {};
300
+
301
+ // Prepare the request with common logic
302
+ this.prepareRequest(req, {
303
+ params: match.groups || {},
304
+ route: path || pathname
305
+ });
248
306
 
249
307
  for (const handler of handlers) {
250
308
  const response = await handler(req as any);
@@ -86,6 +86,11 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
86
86
  // Clear previous middleware to avoid accumulation across multiple starts
87
87
  this.middlewareManager.clear();
88
88
 
89
+ // Only clear routes if we're doing auto-discovery
90
+ if (this.config.autoDiscover !== false) {
91
+ this.router.clearRoutes();
92
+ }
93
+
89
94
  if (config?.before) {
90
95
  this.middlewareManager.addBefore(...config.before);
91
96
  }
@@ -106,10 +111,11 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
106
111
 
107
112
  private async discoverRoutes() {
108
113
  const routesDir = this.config.routesDir || "./routes";
114
+ const excludePatterns = this.config.routeExcludePatterns;
109
115
 
110
116
  // Always create a new RouteScanner with the current config's routesDir
111
117
  // to ensure we're using the correct path from the user's config
112
- this.routeScanner = new RouteScanner(routesDir);
118
+ this.routeScanner = new RouteScanner(routesDir, excludePatterns);
113
119
 
114
120
  if (!this.routeGenerator) {
115
121
  this.routeGenerator = new RouteGenerator();
@@ -208,9 +214,8 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
208
214
  this.server.stop();
209
215
  this.server = null;
210
216
  }
211
- // Don't reset managers - they should persist for the singleton
212
- // Only clear route-specific state if needed
213
- this.router.clearRoutes();
217
+ // Don't reset managers or routes - they persist for the singleton
218
+ // Routes will be cleared on next startServer() call
214
219
  }
215
220
 
216
221
  getServer(): VectorServer<TTypes> | null {
@@ -1,13 +1,32 @@
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
-
8
- constructor(routesDir = './routes') {
7
+ private excludePatterns: string[];
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",
23
+ ];
24
+
25
+ constructor(routesDir = "./routes", excludePatterns?: string[]) {
9
26
  // Always resolve from the current working directory (user's project)
10
27
  this.routesDir = resolve(process.cwd(), routesDir);
28
+ this.excludePatterns =
29
+ excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
11
30
  }
12
31
 
13
32
  async scan(): Promise<GeneratedRoute[]> {
@@ -21,7 +40,7 @@ export class RouteScanner {
21
40
  try {
22
41
  await this.scanDirectory(this.routesDir, routes);
23
42
  } catch (error) {
24
- if ((error as any).code === 'ENOENT') {
43
+ if ((error as any).code === "ENOENT") {
25
44
  console.warn(` ✗ Routes directory not accessible: ${this.routesDir}`);
26
45
  return [];
27
46
  }
@@ -31,7 +50,34 @@ export class RouteScanner {
31
50
  return routes;
32
51
  }
33
52
 
34
- private async scanDirectory(dir: string, routes: GeneratedRoute[], basePath = ''): Promise<void> {
53
+ private isExcluded(filePath: string): boolean {
54
+ const relativePath = relative(this.routesDir, filePath);
55
+
56
+ for (const pattern of this.excludePatterns) {
57
+ // Convert glob pattern to regex
58
+ 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
63
+
64
+ const regex = new RegExp(`^${regexPattern}$`);
65
+
66
+ // Check both the full relative path and just the filename
67
+ const filename = relativePath.split(sep).pop() || "";
68
+ if (regex.test(relativePath) || regex.test(filename)) {
69
+ return true;
70
+ }
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ private async scanDirectory(
77
+ dir: string,
78
+ routes: GeneratedRoute[],
79
+ basePath = ""
80
+ ): Promise<void> {
35
81
  const entries = await fs.readdir(dir);
36
82
 
37
83
  for (const entry of entries) {
@@ -41,26 +87,32 @@ export class RouteScanner {
41
87
  if (stats.isDirectory()) {
42
88
  const newBasePath = basePath ? `${basePath}/${entry}` : entry;
43
89
  await this.scanDirectory(fullPath, routes, newBasePath);
44
- } else if (entry.endsWith('.ts') || entry.endsWith('.js')) {
90
+ } else if (entry.endsWith(".ts") || entry.endsWith(".js")) {
91
+ // Skip excluded files (test files, etc.)
92
+ if (this.isExcluded(fullPath)) {
93
+ continue;
94
+ }
45
95
  const routePath = relative(this.routesDir, fullPath)
46
- .replace(/\.(ts|js)$/, '')
96
+ .replace(/\.(ts|js)$/, "")
47
97
  .split(sep)
48
- .join('/');
98
+ .join("/");
49
99
 
50
100
  try {
51
101
  // Convert Windows paths to URLs for import
52
102
  const importPath =
53
- process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
103
+ process.platform === "win32"
104
+ ? `file:///${fullPath.replace(/\\/g, "/")}`
105
+ : fullPath;
54
106
 
55
107
  const module = await import(importPath);
56
108
 
57
- if (module.default && typeof module.default === 'function') {
109
+ if (module.default && typeof module.default === "function") {
58
110
  routes.push({
59
- name: 'default',
111
+ name: "default",
60
112
  path: fullPath,
61
- method: 'GET',
113
+ method: "GET",
62
114
  options: {
63
- method: 'GET',
115
+ method: "GET",
64
116
  path: `/${routePath}`,
65
117
  expose: true,
66
118
  },
@@ -68,10 +120,16 @@ export class RouteScanner {
68
120
  }
69
121
 
70
122
  for (const [name, value] of Object.entries(module)) {
71
- if (name === 'default') continue;
123
+ if (name === "default") continue;
72
124
 
73
125
  // Check for new RouteDefinition format
74
- if (value && typeof value === 'object' && 'entry' in value && 'options' in value && 'handler' in value) {
126
+ if (
127
+ value &&
128
+ typeof value === "object" &&
129
+ "entry" in value &&
130
+ "options" in value &&
131
+ "handler" in value
132
+ ) {
75
133
  const routeDef = value as any;
76
134
  routes.push({
77
135
  name,
@@ -103,7 +161,7 @@ export class RouteScanner {
103
161
  }
104
162
 
105
163
  enableWatch(callback: () => void) {
106
- if (typeof Bun !== 'undefined' && Bun.env.NODE_ENV === 'development') {
164
+ if (typeof Bun !== "undefined" && Bun.env.NODE_ENV === "development") {
107
165
  console.log(`Watching for route changes in ${this.routesDir}`);
108
166
 
109
167
  setInterval(async () => {
package/src/http.ts CHANGED
@@ -1,17 +1,13 @@
1
- import {
2
- cors,
3
- type IRequest,
4
- type RouteEntry,
5
- withContent,
6
- withCookies,
7
- } from "itty-router";
1
+ import { cors, type IRequest, type RouteEntry, withContent } from "itty-router";
8
2
  import { CONTENT_TYPES, HTTP_STATUS } from "./constants";
9
3
  import type {
4
+ CacheOptions,
10
5
  DefaultVectorTypes,
11
6
  GetAuthType,
12
7
  VectorRequest,
13
8
  VectorTypes,
14
9
  } from "./types";
10
+ import { getVectorInstance } from "./core/vector";
15
11
 
16
12
  export interface ProtectedRequest<
17
13
  TTypes extends VectorTypes = DefaultVectorTypes
@@ -33,7 +29,9 @@ interface ExtendedApiOptions extends ApiOptions {
33
29
  path: string;
34
30
  }
35
31
 
36
- export interface RouteDefinition<TTypes extends VectorTypes = DefaultVectorTypes> {
32
+ export interface RouteDefinition<
33
+ TTypes extends VectorTypes = DefaultVectorTypes
34
+ > {
37
35
  entry: RouteEntry;
38
36
  options: ExtendedApiOptions;
39
37
  handler: (req: VectorRequest<TTypes>) => Promise<unknown>;
@@ -64,7 +62,7 @@ export function route<TTypes extends VectorTypes = DefaultVectorTypes>(
64
62
  return {
65
63
  entry,
66
64
  options,
67
- handler: fn
65
+ handler: fn,
68
66
  };
69
67
  }
70
68
 
@@ -263,7 +261,6 @@ export const protectedRoute = async <
263
261
  responseContentType?: string
264
262
  ) => {
265
263
  // Get the Vector instance to access the protected handler
266
- const { getVectorInstance } = await import("./core/vector");
267
264
  const vector = getVectorInstance();
268
265
 
269
266
  const protectedHandler = vector.getProtectedHandler();
@@ -290,7 +287,7 @@ export interface ApiOptions {
290
287
  expose?: boolean;
291
288
  rawRequest?: boolean;
292
289
  rawResponse?: boolean;
293
- cache?: number | null;
290
+ cache?: CacheOptions | number | null;
294
291
  responseContentType?: string;
295
292
  }
296
293
 
@@ -325,8 +322,6 @@ export function api<TTypes extends VectorTypes = DefaultVectorTypes>(
325
322
  await withContent(request);
326
323
  }
327
324
 
328
- withCookies(request);
329
-
330
325
  // Cache handling is now done in the router
331
326
  const result = await fn(request as any as VectorRequest<TTypes>);
332
327
 
@@ -45,7 +45,13 @@ export class MiddlewareManager<
45
45
  let currentResponse = response;
46
46
 
47
47
  for (const handler of this.finallyHandlers) {
48
- currentResponse = await handler(currentResponse, request);
48
+ try {
49
+ currentResponse = await handler(currentResponse, request);
50
+ } catch (error) {
51
+ // Log but don't throw - we don't want to break the response chain
52
+ console.error('After middleware error:', error);
53
+ // Continue with the current response
54
+ }
49
55
  }
50
56
 
51
57
  return currentResponse;
@@ -51,6 +51,9 @@ export interface VectorRequest<TTypes extends VectorTypes = DefaultVectorTypes>
51
51
  metadata?: GetMetadataType<TTypes>;
52
52
  content?: any;
53
53
  params?: Record<string, string>;
54
+ query: { [key: string]: string | string[] | undefined };
55
+ headers: Headers;
56
+ cookies?: Record<string, string>;
54
57
  startTime?: number;
55
58
  [key: string]: any;
56
59
  }
@@ -82,6 +85,7 @@ export interface VectorConfig<TTypes extends VectorTypes = DefaultVectorTypes> {
82
85
  before?: BeforeMiddlewareHandler<TTypes>[];
83
86
  finally?: AfterMiddlewareHandler<TTypes>[];
84
87
  routesDir?: string;
88
+ routeExcludePatterns?: string[];
85
89
  autoDiscover?: boolean;
86
90
  idleTimeout?: number;
87
91
  }
@@ -94,6 +98,7 @@ export interface VectorConfigSchema<TTypes extends VectorTypes = DefaultVectorTy
94
98
  reusePort?: boolean;
95
99
  development?: boolean;
96
100
  routesDir?: string;
101
+ routeExcludePatterns?: string[];
97
102
  idleTimeout?: number;
98
103
 
99
104
  // Middleware functions