@stacksjs/bun-router 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +134 -0
- package/dist/cache/lru-cache.d.ts +139 -0
- package/dist/cache/middleware-memoization.d.ts +151 -0
- package/dist/cache/route-cache-warmer.d.ts +161 -0
- package/dist/cache/sqlite-cache.d.ts +209 -0
- package/dist/cache/streaming-cache.d.ts +132 -0
- package/dist/cli/colors.d.ts +15 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/middleware.d.ts +31 -0
- package/dist/cli/openapi.d.ts +17 -0
- package/dist/cli/router.d.ts +15 -0
- package/dist/cli/routes.d.ts +29 -0
- package/dist/cli/utils.d.ts +74 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/container/container.d.ts +273 -0
- package/dist/container/contextual-binding.d.ts +240 -0
- package/dist/container/decorators.d.ts +141 -0
- package/dist/container/service-provider.d.ts +285 -0
- package/dist/development/hot-reload.d.ts +187 -0
- package/dist/development/index.d.ts +167 -0
- package/dist/development/performance-profiler.d.ts +217 -0
- package/dist/development/route-debugger.d.ts +154 -0
- package/dist/development/route-inspector.d.ts +211 -0
- package/dist/development/typescript-utilities.d.ts +209 -0
- package/dist/docs.d.ts +11 -0
- package/dist/errors/circuit-breaker.d.ts +195 -0
- package/dist/errors/error-handler.d.ts +88 -0
- package/dist/errors/error-reporting.d.ts +144 -0
- package/dist/errors/exceptions.d.ts +184 -0
- package/dist/errors/graceful-degradation.d.ts +154 -0
- package/dist/errors/index.d.ts +9 -0
- package/dist/errors/router-errors.d.ts +110 -0
- package/dist/file-serving/static-files.d.ts +143 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +319 -205
- package/dist/middleware/auth.d.ts +63 -0
- package/dist/middleware/content_security_policy.d.ts +53 -0
- package/dist/middleware/cors.d.ts +5 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +39 -0
- package/dist/middleware/file_security.d.ts +26 -0
- package/dist/middleware/file_upload.d.ts +38 -0
- package/dist/middleware/helmet.d.ts +54 -0
- package/dist/middleware/index.d.ts +44 -0
- package/dist/middleware/input_validation.d.ts +45 -0
- package/dist/middleware/json_body.d.ts +4 -0
- package/dist/middleware/performance_alerting.d.ts +87 -0
- package/dist/middleware/performance_dashboard.d.ts +87 -0
- package/dist/middleware/performance_monitor.d.ts +209 -0
- package/dist/middleware/pipeline.d.ts +131 -0
- package/dist/middleware/rate_limit.d.ts +36 -0
- package/dist/middleware/request_id.d.ts +4 -0
- package/dist/middleware/request_signing.d.ts +153 -0
- package/dist/middleware/request_tracer.d.ts +72 -0
- package/dist/middleware/response_cache.d.ts +97 -0
- package/dist/middleware/security.d.ts +69 -0
- package/dist/middleware/security_suite.d.ts +46 -0
- package/dist/middleware/session.d.ts +23 -0
- package/dist/model-binding/index.d.ts +2 -0
- package/dist/model-binding/model-middleware.d.ts +118 -0
- package/dist/model-binding/model-registry.d.ts +164 -0
- package/dist/model-binding.d.ts +185 -0
- package/dist/model-resolver-factory.d.ts +31 -0
- package/dist/observability/correlation.d.ts +187 -0
- package/dist/observability/health-checks.d.ts +186 -0
- package/dist/observability/index.d.ts +60 -0
- package/dist/observability/integration.d.ts +147 -0
- package/dist/observability/metrics.d.ts +184 -0
- package/dist/observability/tracing.d.ts +187 -0
- package/dist/optimization/bun-utilities.d.ts +224 -0
- package/dist/query-builder-integration.d.ts +36 -0
- package/dist/request/context.d.ts +34 -0
- package/dist/request/enhanced-request.d.ts +213 -0
- package/dist/request/macros.d.ts +300 -0
- package/dist/response/macros.d.ts +245 -0
- package/dist/response/response-factory.d.ts +133 -0
- package/dist/router/api-routes.d.ts +5 -0
- package/dist/router/file-based-routing.d.ts +118 -0
- package/dist/router/file-streaming.d.ts +5 -0
- package/dist/router/fluent-router.d.ts +315 -0
- package/dist/router/fluent-routing.d.ts +271 -0
- package/dist/router/group-organization.d.ts +5 -0
- package/dist/router/handler-resolver.d.ts +24 -0
- package/dist/router/http-methods.d.ts +10 -0
- package/dist/router/index.d.ts +113 -0
- package/dist/router/middleware-groups.d.ts +94 -0
- package/dist/router/middleware-integration.d.ts +112 -0
- package/dist/router/middleware.d.ts +5 -0
- package/dist/router/model-binding.d.ts +5 -0
- package/dist/router/optimized-route-matching.d.ts +6 -0
- package/dist/router/route-building.d.ts +5 -0
- package/dist/router/route-compiler.d.ts +153 -0
- package/dist/router/route-matching.d.ts +5 -0
- package/dist/router/route-trie.d.ts +114 -0
- package/dist/router/router.d.ts +276 -0
- package/dist/router/server.d.ts +5 -0
- package/dist/router/validation-integration.d.ts +170 -0
- package/dist/router/view-rendering.d.ts +5 -0
- package/dist/router/websocket.d.ts +5 -0
- package/dist/routing/route-caching.d.ts +129 -0
- package/dist/routing/route-throttling.d.ts +150 -0
- package/dist/routing/subdomain-routing.d.ts +206 -0
- package/dist/session/database-store.d.ts +51 -0
- package/dist/session/file-store.d.ts +22 -0
- package/dist/session/index.d.ts +106 -0
- package/dist/session/memory-store.d.ts +21 -0
- package/dist/session/redis-store.d.ts +31 -0
- package/dist/streaming/index.d.ts +3 -0
- package/dist/streaming/sse-handler.d.ts +138 -0
- package/dist/streaming/stream-handler.d.ts +114 -0
- package/dist/testing/auth-testing.d.ts +156 -0
- package/dist/testing/file-upload-testing.d.ts +190 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/middleware-testing.d.ts +138 -0
- package/dist/testing/model-binding-testing.d.ts +186 -0
- package/dist/testing/performance-testing.d.ts +235 -0
- package/dist/testing/test-client.d.ts +117 -0
- package/dist/testing/test-request.d.ts +85 -0
- package/dist/testing/test-response.d.ts +90 -0
- package/dist/testing/types.d.ts +207 -0
- package/dist/testing/websocket-testing.d.ts +227 -0
- package/dist/types/controller-types.d.ts +208 -0
- package/dist/types/core.d.ts +543 -0
- package/dist/types/middleware-types.d.ts +227 -0
- package/dist/types/request-response-augmentation.d.ts +261 -0
- package/dist/types/route-inference.d.ts +168 -0
- package/dist/types.d.ts +1776 -0
- package/dist/url.d.ts +56 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/query-preservation.d.ts +48 -0
- package/dist/utils.d.ts +69 -0
- package/dist/validation/validator.d.ts +140 -0
- package/dist/websocket/clustering.d.ts +185 -0
- package/package.json +3 -2
- package/src/auth.ts +469 -0
- package/src/cache/lru-cache.ts +457 -0
- package/src/cache/middleware-memoization.ts +531 -0
- package/src/cache/route-cache-warmer.ts +486 -0
- package/src/cache/sqlite-cache.ts +783 -0
- package/src/cache/streaming-cache.ts +572 -0
- package/src/cli/colors.ts +29 -0
- package/src/cli/index.ts +287 -0
- package/src/cli/middleware.ts +291 -0
- package/src/cli/openapi.ts +407 -0
- package/src/cli/router.ts +188 -0
- package/src/cli/routes.ts +265 -0
- package/src/cli/utils.ts +531 -0
- package/src/cli.ts +5 -0
- package/src/config.ts +322 -0
- package/src/container/container.ts +740 -0
- package/src/container/contextual-binding.ts +603 -0
- package/src/container/decorators.ts +359 -0
- package/src/container/service-provider.ts +596 -0
- package/src/development/hot-reload.ts +673 -0
- package/src/development/index.ts +499 -0
- package/src/development/performance-profiler.ts +717 -0
- package/src/development/route-debugger.ts +527 -0
- package/src/development/route-inspector.ts +749 -0
- package/src/development/typescript-utilities.ts +678 -0
- package/src/docs.ts +397 -0
- package/src/errors/circuit-breaker.ts +732 -0
- package/src/errors/error-handler.ts +569 -0
- package/src/errors/error-reporting.ts +672 -0
- package/src/errors/exceptions.ts +536 -0
- package/src/errors/graceful-degradation.ts +621 -0
- package/src/errors/index.ts +21 -0
- package/src/errors/router-errors.ts +632 -0
- package/src/file-serving/static-files.ts +581 -0
- package/src/index.ts +14 -0
- package/src/middleware/auth.ts +219 -0
- package/src/middleware/content_security_policy.ts +215 -0
- package/src/middleware/cors.ts +74 -0
- package/src/middleware/csrf.ts +108 -0
- package/src/middleware/ddos_protection.ts +255 -0
- package/src/middleware/file_security.ts +191 -0
- package/src/middleware/file_upload.ts +264 -0
- package/src/middleware/helmet.ts +268 -0
- package/src/middleware/index.ts +117 -0
- package/src/middleware/input_validation.ts +449 -0
- package/src/middleware/json_body.ts +37 -0
- package/src/middleware/performance_alerting.ts +538 -0
- package/src/middleware/performance_dashboard.ts +661 -0
- package/src/middleware/performance_monitor.ts +943 -0
- package/src/middleware/pipeline.ts +489 -0
- package/src/middleware/rate_limit.ts +245 -0
- package/src/middleware/request_id.ts +36 -0
- package/src/middleware/request_signing.ts +636 -0
- package/src/middleware/request_tracer.ts +636 -0
- package/src/middleware/response_cache.ts +743 -0
- package/src/middleware/security.ts +482 -0
- package/src/middleware/security_suite.ts +257 -0
- package/src/middleware/session.ts +91 -0
- package/src/model-binding/index.ts +18 -0
- package/src/model-binding/model-middleware.ts +425 -0
- package/src/model-binding/model-registry.ts +550 -0
- package/src/model-binding.ts +370 -0
- package/src/model-resolver-factory.ts +106 -0
- package/src/observability/correlation.ts +691 -0
- package/src/observability/health-checks.ts +729 -0
- package/src/observability/index.ts +184 -0
- package/src/observability/integration.ts +548 -0
- package/src/observability/metrics.ts +753 -0
- package/src/observability/tracing.ts +638 -0
- package/src/optimization/bun-utilities.ts +778 -0
- package/src/query-builder-integration.ts +137 -0
- package/src/request/context.ts +62 -0
- package/src/request/enhanced-request.ts +857 -0
- package/src/request/macros.ts +688 -0
- package/src/response/macros.ts +612 -0
- package/src/response/response-factory.ts +588 -0
- package/src/router/api-routes.ts +243 -0
- package/src/router/file-based-routing.ts +670 -0
- package/src/router/file-streaming.ts +372 -0
- package/src/router/fluent-router.ts +927 -0
- package/src/router/fluent-routing.ts +797 -0
- package/src/router/group-organization.ts +213 -0
- package/src/router/handler-resolver.ts +291 -0
- package/src/router/http-methods.ts +377 -0
- package/src/router/index.ts +185 -0
- package/src/router/middleware-groups.ts +222 -0
- package/src/router/middleware-integration.ts +399 -0
- package/src/router/middleware.ts +231 -0
- package/src/router/model-binding.ts +215 -0
- package/src/router/optimized-route-matching.ts +253 -0
- package/src/router/route-building.ts +212 -0
- package/src/router/route-compiler.ts +702 -0
- package/src/router/route-matching.ts +349 -0
- package/src/router/route-trie.ts +464 -0
- package/src/router/router.ts +1508 -0
- package/src/router/server.ts +406 -0
- package/src/router/validation-integration.ts +443 -0
- package/src/router/view-rendering.ts +233 -0
- package/src/router/websocket.ts +100 -0
- package/src/routing/route-caching.ts +402 -0
- package/src/routing/route-throttling.ts +469 -0
- package/src/routing/subdomain-routing.ts +492 -0
- package/src/session/database-store.ts +109 -0
- package/src/session/file-store.ts +148 -0
- package/src/session/index.ts +244 -0
- package/src/session/memory-store.ts +88 -0
- package/src/session/redis-store.ts +93 -0
- package/src/streaming/index.ts +17 -0
- package/src/streaming/sse-handler.ts +482 -0
- package/src/streaming/stream-handler.ts +552 -0
- package/src/testing/auth-testing.ts +446 -0
- package/src/testing/file-upload-testing.ts +543 -0
- package/src/testing/index.ts +10 -0
- package/src/testing/middleware-testing.ts +322 -0
- package/src/testing/model-binding-testing.ts +645 -0
- package/src/testing/performance-testing.ts +738 -0
- package/src/testing/test-client.ts +310 -0
- package/src/testing/test-request.ts +315 -0
- package/src/testing/test-response.ts +332 -0
- package/src/testing/types.ts +224 -0
- package/src/testing/websocket-testing.ts +590 -0
- package/src/types/controller-types.ts +385 -0
- package/src/types/core.ts +683 -0
- package/src/types/middleware-types.ts +419 -0
- package/src/types/request-response-augmentation.ts +489 -0
- package/src/types/route-inference.ts +357 -0
- package/src/types.ts +2049 -0
- package/src/url.ts +126 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/query-preservation.ts +201 -0
- package/src/utils.ts +327 -0
- package/src/validation/validator.ts +685 -0
- package/src/websocket/clustering.ts +762 -0
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
import type { Server } from 'bun'
|
|
2
|
+
import type { MiddlewareDependency, MiddlewarePipeline, MiddlewarePipelineStats, MiddlewareSkipCondition } from '../middleware/pipeline'
|
|
3
|
+
import type {
|
|
4
|
+
ActionHandler,
|
|
5
|
+
CookieOptions,
|
|
6
|
+
CookieToSet,
|
|
7
|
+
EnhancedRequest,
|
|
8
|
+
MiddlewareHandler,
|
|
9
|
+
NextFunction,
|
|
10
|
+
Route,
|
|
11
|
+
RouteGroup,
|
|
12
|
+
RouteHandler,
|
|
13
|
+
RouterConfig,
|
|
14
|
+
ThrottlePattern,
|
|
15
|
+
WebSocketConfig,
|
|
16
|
+
WebSocketData,
|
|
17
|
+
} from '../types'
|
|
18
|
+
import { createRateLimitMiddleware, parseThrottleString } from '../routing/route-throttling'
|
|
19
|
+
import { registerNamedRoute } from '../url'
|
|
20
|
+
import { extractParamNames, joinPaths, matchPath } from '../utils'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Route compiler interface for pattern matching
|
|
24
|
+
*/
|
|
25
|
+
export interface RouteCompiler {
|
|
26
|
+
compile: (path: string) => RegExp
|
|
27
|
+
match: (path: string, pattern: RegExp) => Record<string, string> | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Middleware cache info interface
|
|
32
|
+
*/
|
|
33
|
+
export interface MiddlewareCacheInfo {
|
|
34
|
+
size: number
|
|
35
|
+
routes: string[]
|
|
36
|
+
hitRate: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Re-export types for module augmentation
|
|
40
|
+
export type {
|
|
41
|
+
ActionHandler,
|
|
42
|
+
MiddlewareHandler,
|
|
43
|
+
Route,
|
|
44
|
+
RouteGroup,
|
|
45
|
+
RouteHandler,
|
|
46
|
+
RouterConfig,
|
|
47
|
+
WebSocketConfig,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Middleware condition type
|
|
51
|
+
export type MiddlewareCondition = (_req: EnhancedRequest) => boolean
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Unified Router class with advanced middleware patterns
|
|
55
|
+
*/
|
|
56
|
+
export class Router {
|
|
57
|
+
routes: Route[] = []
|
|
58
|
+
currentGroup: RouteGroup | null = null
|
|
59
|
+
globalMiddleware: MiddlewareHandler[] = []
|
|
60
|
+
namedRoutes: Map<string, Route> = new Map()
|
|
61
|
+
fallbackHandler: ActionHandler | null = null
|
|
62
|
+
patterns: Map<string, string> = new Map()
|
|
63
|
+
currentDomain: string | null = null
|
|
64
|
+
domains: Record<string, Route[]> = {}
|
|
65
|
+
serverInstance: Server<WebSocketData> | null = null
|
|
66
|
+
wsConfig: WebSocketConfig | null = null
|
|
67
|
+
errorHandler: ((_error: Error) => Response | Promise<Response>) | null = null
|
|
68
|
+
templateCache: Map<string, string> = new Map<string, string>()
|
|
69
|
+
routeCache: Map<string, { route: Route, params: Record<string, string> }> = new Map()
|
|
70
|
+
staticRoutes: Map<string, Map<string, Route>> = new Map()
|
|
71
|
+
staticResponses: Map<string, Response> = new Map()
|
|
72
|
+
precompiledPatterns: Map<string, RegExp> = new Map()
|
|
73
|
+
domainPatternCache: Map<string, RegExp> = new Map()
|
|
74
|
+
routeCompiler: RouteCompiler | null = null
|
|
75
|
+
|
|
76
|
+
// Advanced middleware features
|
|
77
|
+
private middlewareGroups: Map<string, MiddlewareHandler[]> = new Map()
|
|
78
|
+
private namedMiddleware: Map<string, (params?: string) => MiddlewareHandler> = new Map()
|
|
79
|
+
private conditionalMiddleware: Array<{ condition: MiddlewareCondition, middleware: MiddlewareHandler[] }> = []
|
|
80
|
+
|
|
81
|
+
// Middleware pipeline for advanced features
|
|
82
|
+
_middlewarePipeline?: MiddlewarePipeline
|
|
83
|
+
|
|
84
|
+
config: RouterConfig = {
|
|
85
|
+
verbose: false,
|
|
86
|
+
routesPath: 'routes',
|
|
87
|
+
apiRoutesPath: 'routes/api.ts',
|
|
88
|
+
webRoutesPath: 'routes/web.ts',
|
|
89
|
+
apiPrefix: '/api',
|
|
90
|
+
webPrefix: '',
|
|
91
|
+
actionsPath: 'actions',
|
|
92
|
+
controllersPath: 'controllers',
|
|
93
|
+
defaultMiddleware: {
|
|
94
|
+
api: [],
|
|
95
|
+
web: [],
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
constructor(config: Partial<RouterConfig> = {}) {
|
|
100
|
+
this.routes = []
|
|
101
|
+
this.config = { ...this.config, ...config }
|
|
102
|
+
this.initializeDefaultMiddleware()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Initialize default named middleware
|
|
107
|
+
*/
|
|
108
|
+
private initializeDefaultMiddleware(): void {
|
|
109
|
+
// Auth middleware
|
|
110
|
+
this.namedMiddleware.set('auth', () => async (req: EnhancedRequest, next: NextFunction) => {
|
|
111
|
+
const token = req.headers.get('authorization')?.replace('Bearer ', '')
|
|
112
|
+
if (!token) {
|
|
113
|
+
return new Response('Unauthorized', { status: 401 })
|
|
114
|
+
}
|
|
115
|
+
return await next()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Throttle middleware with parameters
|
|
119
|
+
this.namedMiddleware.set('throttle', (params?: string): MiddlewareHandler => {
|
|
120
|
+
const config = params ? parseThrottleString(params as ThrottlePattern) : { maxAttempts: 60, windowMs: 60000 }
|
|
121
|
+
return createRateLimitMiddleware({
|
|
122
|
+
maxAttempts: config.maxAttempts || 60,
|
|
123
|
+
windowMs: config.windowMs,
|
|
124
|
+
keyGenerator: (req: EnhancedRequest) => req.headers.get('x-forwarded-for') || 'anonymous',
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// CORS middleware
|
|
129
|
+
this.namedMiddleware.set('cors', () => async (_req: EnhancedRequest, next: NextFunction) => {
|
|
130
|
+
const response = await next()
|
|
131
|
+
if (response) {
|
|
132
|
+
response.headers.set('Access-Control-Allow-Origin', '*')
|
|
133
|
+
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
|
134
|
+
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
|
135
|
+
}
|
|
136
|
+
return response
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Register a middleware group
|
|
142
|
+
*/
|
|
143
|
+
middlewareGroup(name: string, middlewareNames: string[]): this {
|
|
144
|
+
const middlewareHandlers: MiddlewareHandler[] = []
|
|
145
|
+
|
|
146
|
+
for (const middlewareName of middlewareNames) {
|
|
147
|
+
const [name, params] = middlewareName.split(':')
|
|
148
|
+
const middlewareFactory = this.namedMiddleware.get(name)
|
|
149
|
+
|
|
150
|
+
if (middlewareFactory) {
|
|
151
|
+
middlewareHandlers.push(middlewareFactory(params))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.middlewareGroups.set(name, middlewareHandlers)
|
|
156
|
+
return this
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Apply middleware group to routes
|
|
161
|
+
*/
|
|
162
|
+
middlewareGroupRoutes(name: string): RouteGroupBuilder {
|
|
163
|
+
const middlewareHandlers = this.middlewareGroups.get(name) || []
|
|
164
|
+
return new RouteGroupBuilder(this, middlewareHandlers)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Conditional middleware execution
|
|
169
|
+
*/
|
|
170
|
+
when(condition: MiddlewareCondition): ConditionalBuilder {
|
|
171
|
+
return new ConditionalBuilder(this, condition)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Apply middleware with parameters
|
|
176
|
+
*/
|
|
177
|
+
middleware(middlewareName: string): MiddlewareBuilder {
|
|
178
|
+
const [name, params] = middlewareName.split(':')
|
|
179
|
+
const middlewareFactory = this.namedMiddleware.get(name)
|
|
180
|
+
|
|
181
|
+
if (!middlewareFactory) {
|
|
182
|
+
throw new Error(`Unknown middleware: ${name}`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const middlewareHandler = middlewareFactory(params)
|
|
186
|
+
return new MiddlewareBuilder(this, [middlewareHandler])
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get the server instance
|
|
191
|
+
*/
|
|
192
|
+
getServer(): Server<WebSocketData> | null {
|
|
193
|
+
return this.serverInstance
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extend the router with custom methods
|
|
198
|
+
*/
|
|
199
|
+
extend(methods: Record<string, (...args: unknown[]) => unknown>): Router {
|
|
200
|
+
for (const [name, method] of Object.entries(methods)) {
|
|
201
|
+
if (typeof method === 'function') {
|
|
202
|
+
// @ts-expect-error - dynamically extending the object
|
|
203
|
+
this[name] = method.bind(this)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return this
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Register routes from a package or module file.
|
|
211
|
+
* Loads the route file within an optional group (prefix + middleware).
|
|
212
|
+
*/
|
|
213
|
+
async register(routePath: string, options?: { prefix?: string, middleware?: MiddlewareHandler[] }): Promise<Router> {
|
|
214
|
+
const callback = async () => {
|
|
215
|
+
await import(routePath)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (options?.prefix || (options?.middleware && options.middleware.length > 0)) {
|
|
219
|
+
await this.group({
|
|
220
|
+
prefix: options.prefix,
|
|
221
|
+
middleware: options.middleware,
|
|
222
|
+
}, callback)
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
await callback()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return this
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Invalidate route caches
|
|
233
|
+
*/
|
|
234
|
+
invalidateCache(): void {
|
|
235
|
+
this.routeCache.clear()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Internal method to add a route with full HTTP method support
|
|
240
|
+
* This is the core route registration method used by get/post/put/patch/delete
|
|
241
|
+
*/
|
|
242
|
+
registerRoute(
|
|
243
|
+
method: string,
|
|
244
|
+
path: string,
|
|
245
|
+
handler: ActionHandler,
|
|
246
|
+
type?: 'api' | 'web',
|
|
247
|
+
name?: string,
|
|
248
|
+
middleware?: (string | MiddlewareHandler)[],
|
|
249
|
+
): Router {
|
|
250
|
+
// Apply current group settings if in a group
|
|
251
|
+
let routePath = path
|
|
252
|
+
let routeMiddleware: MiddlewareHandler[] = []
|
|
253
|
+
const routeType = type || 'web'
|
|
254
|
+
|
|
255
|
+
if (this.currentGroup) {
|
|
256
|
+
// Apply prefix if it exists
|
|
257
|
+
if (this.currentGroup.prefix) {
|
|
258
|
+
routePath = joinPaths(this.currentGroup.prefix, path)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Apply middleware if it exists
|
|
262
|
+
if (this.currentGroup.middleware && this.currentGroup.middleware.length > 0) {
|
|
263
|
+
routeMiddleware = [...this.currentGroup.middleware] as MiddlewareHandler[]
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Apply route-specific middleware if provided
|
|
268
|
+
if (middleware && middleware.length > 0) {
|
|
269
|
+
for (const middlewareItem of middleware) {
|
|
270
|
+
const resolved = this.resolveMiddleware(middlewareItem)
|
|
271
|
+
if (resolved) {
|
|
272
|
+
routeMiddleware.push(resolved)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Apply API/Web path prefixes
|
|
278
|
+
if (routeType === 'api' && this.config.apiPrefix) {
|
|
279
|
+
routePath = joinPaths(this.config.apiPrefix, routePath)
|
|
280
|
+
}
|
|
281
|
+
else if (routeType === 'web' && this.config.webPrefix) {
|
|
282
|
+
routePath = joinPaths(this.config.webPrefix, routePath)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Apply domain if in a domain group
|
|
286
|
+
let domain: string | undefined
|
|
287
|
+
if (this.currentDomain) {
|
|
288
|
+
domain = this.currentDomain
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Create the route
|
|
292
|
+
const route: Route = {
|
|
293
|
+
method: method.toUpperCase(),
|
|
294
|
+
path: routePath,
|
|
295
|
+
handler,
|
|
296
|
+
domain,
|
|
297
|
+
params: {},
|
|
298
|
+
middleware: routeMiddleware,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Apply constraints from patterns map
|
|
302
|
+
const paramNames = extractParamNames(routePath)
|
|
303
|
+
const constraints: Record<string, string> = {}
|
|
304
|
+
|
|
305
|
+
paramNames.forEach((param: string) => {
|
|
306
|
+
// Remove optional marker for constraint lookup
|
|
307
|
+
const baseParam = param.replace('?', '')
|
|
308
|
+
if (this.patterns.has(baseParam)) {
|
|
309
|
+
constraints[baseParam] = this.patterns.get(baseParam)!
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
if (Object.keys(constraints).length > 0) {
|
|
314
|
+
route.constraints = constraints
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Add pattern property for route matching
|
|
318
|
+
route.pattern = {
|
|
319
|
+
exec: (url: URL): { pathname: { groups: Record<string, string> } } | null => {
|
|
320
|
+
const params: Record<string, string> = {}
|
|
321
|
+
// Pass constraints directly to matchPath for more efficient matching
|
|
322
|
+
const constraintsRecord = route.constraints && !Array.isArray(route.constraints)
|
|
323
|
+
? route.constraints as Record<string, string>
|
|
324
|
+
: undefined
|
|
325
|
+
|
|
326
|
+
const isMatch = matchPath(routePath, url.pathname, params, constraintsRecord)
|
|
327
|
+
|
|
328
|
+
if (!isMatch) {
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
pathname: {
|
|
334
|
+
groups: params,
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Add to the appropriate collection
|
|
341
|
+
if (domain) {
|
|
342
|
+
if (!this.domains[domain]) {
|
|
343
|
+
this.domains[domain] = []
|
|
344
|
+
}
|
|
345
|
+
this.domains[domain].push(route)
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
this.routes.push(route)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Add to static routes map for fast lookup if it's a static route
|
|
352
|
+
if (!routePath.includes('{') && !routePath.includes('*')) {
|
|
353
|
+
if (!this.staticRoutes.has(method.toUpperCase())) {
|
|
354
|
+
this.staticRoutes.set(method.toUpperCase(), new Map())
|
|
355
|
+
}
|
|
356
|
+
this.staticRoutes.get(method.toUpperCase())!.set(routePath, route)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Add to named routes if name is provided
|
|
360
|
+
if (name) {
|
|
361
|
+
route.name = name
|
|
362
|
+
this.namedRoutes.set(name, route)
|
|
363
|
+
registerNamedRoute(name, route.path)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Clear route cache when new routes are added
|
|
367
|
+
this.routeCache.clear()
|
|
368
|
+
|
|
369
|
+
return this
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve middleware from string or handler (synchronous)
|
|
374
|
+
*/
|
|
375
|
+
resolveMiddleware(middleware: string | MiddlewareHandler): MiddlewareHandler | null {
|
|
376
|
+
if (typeof middleware === 'function') {
|
|
377
|
+
return middleware
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Parse middleware string like "auth:api" or "throttle:60,1"
|
|
381
|
+
const [name, params] = middleware.split(':')
|
|
382
|
+
const middlewareFactory = this.namedMiddleware.get(name)
|
|
383
|
+
|
|
384
|
+
if (middlewareFactory) {
|
|
385
|
+
return middlewareFactory(params)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return null
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* HTTP GET method
|
|
393
|
+
*/
|
|
394
|
+
get(path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
|
|
395
|
+
return this.registerRoute('GET', path, handler, type, name, middleware)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* HTTP POST method
|
|
400
|
+
*/
|
|
401
|
+
post(path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
|
|
402
|
+
return this.registerRoute('POST', path, handler, type, name, middleware)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* HTTP PUT method
|
|
407
|
+
*/
|
|
408
|
+
put(path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
|
|
409
|
+
return this.registerRoute('PUT', path, handler, type, name, middleware)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* HTTP PATCH method
|
|
414
|
+
*/
|
|
415
|
+
patch(path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
|
|
416
|
+
return this.registerRoute('PATCH', path, handler, type, name, middleware)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* HTTP DELETE method
|
|
421
|
+
*/
|
|
422
|
+
delete(path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
|
|
423
|
+
return this.registerRoute('DELETE', path, handler, type, name, middleware)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* HTTP OPTIONS method
|
|
428
|
+
*/
|
|
429
|
+
options(path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
|
|
430
|
+
return this.registerRoute('OPTIONS', path, handler, type, name, middleware)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Register route for multiple HTTP methods
|
|
435
|
+
*/
|
|
436
|
+
match(methods: string[], path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
|
|
437
|
+
for (const method of methods) {
|
|
438
|
+
this.registerRoute(method, path, handler, type, name, middleware)
|
|
439
|
+
}
|
|
440
|
+
return this
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Register route for any HTTP method
|
|
445
|
+
*/
|
|
446
|
+
any(path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
|
|
447
|
+
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
|
|
448
|
+
return this.match(methods, path, handler, type, name, middleware)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Set fallback handler for unmatched routes
|
|
453
|
+
*/
|
|
454
|
+
fallback(handler: ActionHandler): Router {
|
|
455
|
+
this.fallbackHandler = handler
|
|
456
|
+
return this
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Generate URL for a named route
|
|
461
|
+
*/
|
|
462
|
+
route(name: string, params: Record<string, string> = {}): string {
|
|
463
|
+
const route = this.namedRoutes.get(name)
|
|
464
|
+
if (!route) {
|
|
465
|
+
throw new Error(`Route with name "${name}" not found`)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let url = route.path
|
|
469
|
+
// Replace path parameters
|
|
470
|
+
for (const [param, value] of Object.entries(params)) {
|
|
471
|
+
url = url.replace(`{${param}}`, encodeURIComponent(value))
|
|
472
|
+
url = url.replace(`{${param}?}`, encodeURIComponent(value))
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return url
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Set error handler
|
|
480
|
+
*/
|
|
481
|
+
onError(handler: (error: Error) => Response | Promise<Response>): Router {
|
|
482
|
+
this.errorHandler = handler
|
|
483
|
+
return this
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Create redirect response
|
|
488
|
+
*/
|
|
489
|
+
redirect(url: string, status: 301 | 302 | 303 | 307 | 308 = 302): Response {
|
|
490
|
+
const headers = new Headers()
|
|
491
|
+
headers.set('Location', url)
|
|
492
|
+
return new Response(null, {
|
|
493
|
+
status,
|
|
494
|
+
headers,
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Create permanent redirect response
|
|
500
|
+
*/
|
|
501
|
+
permanentRedirect(url: string): Response {
|
|
502
|
+
return this.redirect(url, 301)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Register redirect route
|
|
507
|
+
*/
|
|
508
|
+
redirectRoute(from: string, to: string, status: 301 | 302 | 303 | 307 | 308 = 302): Router {
|
|
509
|
+
this.get(from, (_req: EnhancedRequest) => {
|
|
510
|
+
return this.redirect(to, status)
|
|
511
|
+
})
|
|
512
|
+
return this
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Start the HTTP server
|
|
517
|
+
*/
|
|
518
|
+
async serve(options?: { port?: number, hostname?: string }): Promise<Server<WebSocketData>> {
|
|
519
|
+
// Invalidate route cache before starting server
|
|
520
|
+
this.invalidateCache()
|
|
521
|
+
|
|
522
|
+
// Create server options - use type assertion for Bun.serve compatibility
|
|
523
|
+
const serverOptions = {
|
|
524
|
+
...options,
|
|
525
|
+
fetch: this.handleRequest.bind(this),
|
|
526
|
+
websocket: this.wsConfig,
|
|
527
|
+
} as Parameters<typeof Bun.serve>[0]
|
|
528
|
+
|
|
529
|
+
// Start the server
|
|
530
|
+
this.serverInstance = Bun.serve(serverOptions) as Server<WebSocketData>
|
|
531
|
+
|
|
532
|
+
if (this.config.verbose) {
|
|
533
|
+
const port = this.serverInstance.port
|
|
534
|
+
const hostname = this.serverInstance.hostname
|
|
535
|
+
console.log(`\n🚀 Server running at http://${hostname}:${port}\n`)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return this.serverInstance
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Handle an HTTP request
|
|
543
|
+
*/
|
|
544
|
+
async handleRequest(req: Request): Promise<Response> {
|
|
545
|
+
try {
|
|
546
|
+
// Create URL for route matching
|
|
547
|
+
const url = new URL(req.url)
|
|
548
|
+
|
|
549
|
+
if (this.config.verbose) {
|
|
550
|
+
console.log(`${req.method} ${url.pathname}`)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Handle CORS preflight OPTIONS requests - but check for registered OPTIONS routes first
|
|
554
|
+
// This ensures explicitly registered OPTIONS routes work while still providing CORS support
|
|
555
|
+
if (req.method === 'OPTIONS') {
|
|
556
|
+
const hostname = url.hostname || req.headers.get('host')?.split(':')[0] || 'localhost'
|
|
557
|
+
const optionsMatch = this.matchRoute(url.pathname, 'OPTIONS', hostname)
|
|
558
|
+
if (!optionsMatch) {
|
|
559
|
+
// No explicit OPTIONS route - return generic CORS preflight response
|
|
560
|
+
return new Response(null, {
|
|
561
|
+
status: 204,
|
|
562
|
+
headers: {
|
|
563
|
+
'Access-Control-Allow-Origin': '*',
|
|
564
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
565
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
|
|
566
|
+
'Access-Control-Max-Age': '86400',
|
|
567
|
+
},
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
// Let the registered OPTIONS route handle it (fall through to normal route matching)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Get domain from the host header
|
|
574
|
+
const hostname = url.hostname || req.headers.get('host')?.split(':')[0] || 'localhost'
|
|
575
|
+
|
|
576
|
+
// Find a matching route
|
|
577
|
+
const match = this.matchRoute(url.pathname, req.method as any, hostname)
|
|
578
|
+
|
|
579
|
+
// Enhance the request with params and other utilities
|
|
580
|
+
const enhancedReq = this.enhanceRequest(req, match?.params || {})
|
|
581
|
+
|
|
582
|
+
if (match) {
|
|
583
|
+
// Add the matched route to the request
|
|
584
|
+
enhancedReq.route = match.route
|
|
585
|
+
|
|
586
|
+
// Collect all middleware to run
|
|
587
|
+
const middlewareStack = [...this.globalMiddleware]
|
|
588
|
+
|
|
589
|
+
// Add route-specific middleware
|
|
590
|
+
if (match.route.middleware && match.route.middleware.length > 0) {
|
|
591
|
+
middlewareStack.push(...match.route.middleware)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Create a final middleware that executes the route handler
|
|
595
|
+
const routeHandlerMiddleware = async (req: EnhancedRequest, _next: NextFunction) => {
|
|
596
|
+
return await this.resolveHandler(match.route.handler, req)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Add the route handler as the final middleware
|
|
600
|
+
middlewareStack.push(routeHandlerMiddleware)
|
|
601
|
+
|
|
602
|
+
// Run middleware stack with the route handler at the end
|
|
603
|
+
const response = await this.runMiddleware(enhancedReq, middlewareStack)
|
|
604
|
+
|
|
605
|
+
// Apply modified cookies to the response
|
|
606
|
+
if (response) {
|
|
607
|
+
return this.applyModifiedCookies(response, enhancedReq)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// This should not happen since we're always returning a response now
|
|
611
|
+
return new Response('No response from middleware chain', { status: 500 })
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// No route found - check if the path exists with a different method (405 vs 404)
|
|
615
|
+
const allowedMethods = this.getAllowedMethods(url.pathname, hostname)
|
|
616
|
+
|
|
617
|
+
// If there are allowed methods, return 405 Method Not Allowed
|
|
618
|
+
if (allowedMethods.length > 0) {
|
|
619
|
+
const methodNotAllowedHandler = async (_req: EnhancedRequest, _next: NextFunction) => {
|
|
620
|
+
return new Response(JSON.stringify({ error: 'Method Not Allowed' }), {
|
|
621
|
+
status: 405,
|
|
622
|
+
headers: {
|
|
623
|
+
'Content-Type': 'application/json',
|
|
624
|
+
'Allow': allowedMethods.join(', '),
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (this.globalMiddleware.length > 0) {
|
|
630
|
+
const middlewareStack = [...this.globalMiddleware, methodNotAllowedHandler]
|
|
631
|
+
const response = await this.runMiddleware(enhancedReq, middlewareStack)
|
|
632
|
+
if (response) {
|
|
633
|
+
return this.applyModifiedCookies(response, enhancedReq)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return methodNotAllowedHandler(enhancedReq, async () => new Response(null))
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// No route found with any method - still run global middleware for CORS headers on 404
|
|
641
|
+
if (this.globalMiddleware.length > 0) {
|
|
642
|
+
const notFoundHandler = async (_req: EnhancedRequest, _next: NextFunction) => {
|
|
643
|
+
if (this.fallbackHandler) {
|
|
644
|
+
return await this.resolveHandler(this.fallbackHandler, enhancedReq)
|
|
645
|
+
}
|
|
646
|
+
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
|
647
|
+
status: 404,
|
|
648
|
+
headers: { 'Content-Type': 'application/json' },
|
|
649
|
+
})
|
|
650
|
+
}
|
|
651
|
+
const middlewareStack = [...this.globalMiddleware, notFoundHandler]
|
|
652
|
+
const response = await this.runMiddleware(enhancedReq, middlewareStack)
|
|
653
|
+
if (response) {
|
|
654
|
+
return this.applyModifiedCookies(response, enhancedReq)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// No global middleware, try the fallback handler directly
|
|
659
|
+
if (this.fallbackHandler) {
|
|
660
|
+
const response = await this.resolveHandler(this.fallbackHandler, enhancedReq)
|
|
661
|
+
return this.applyModifiedCookies(response, enhancedReq)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// No fallback handler, return a 404
|
|
665
|
+
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
|
666
|
+
status: 404,
|
|
667
|
+
headers: { 'Content-Type': 'application/json' },
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
console.error('Error handling request:', error)
|
|
672
|
+
|
|
673
|
+
// Still run global middleware for CORS headers on errors
|
|
674
|
+
if (this.globalMiddleware.length > 0) {
|
|
675
|
+
const enhancedReq = this.enhanceRequest(req, {})
|
|
676
|
+
const errorHandler = async (_req: EnhancedRequest, _next: NextFunction) => {
|
|
677
|
+
if (this.errorHandler) {
|
|
678
|
+
return this.errorHandler(error as Error)
|
|
679
|
+
}
|
|
680
|
+
return new Response(JSON.stringify({
|
|
681
|
+
error: 'Internal Server Error',
|
|
682
|
+
message: error instanceof Error ? error.message : String(error),
|
|
683
|
+
}), {
|
|
684
|
+
status: 500,
|
|
685
|
+
headers: { 'Content-Type': 'application/json' },
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
const middlewareStack = [...this.globalMiddleware, errorHandler]
|
|
689
|
+
try {
|
|
690
|
+
const response = await this.runMiddleware(enhancedReq, middlewareStack)
|
|
691
|
+
if (response) {
|
|
692
|
+
return response
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
// Middleware itself failed, fall through to default error
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Use custom error handler if available
|
|
701
|
+
if (this.errorHandler) {
|
|
702
|
+
return this.errorHandler(error as Error)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Default error response
|
|
706
|
+
return new Response(JSON.stringify({
|
|
707
|
+
error: 'Internal Server Error',
|
|
708
|
+
message: error instanceof Error ? error.message : String(error),
|
|
709
|
+
}), {
|
|
710
|
+
status: 500,
|
|
711
|
+
headers: { 'Content-Type': 'application/json' },
|
|
712
|
+
})
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Get all allowed HTTP methods for a given path
|
|
718
|
+
* Used to determine if a 405 Method Not Allowed should be returned instead of 404
|
|
719
|
+
*/
|
|
720
|
+
getAllowedMethods(path: string, domain?: string): string[] {
|
|
721
|
+
const url = new URL(path, 'http://localhost')
|
|
722
|
+
const methods: Set<string> = new Set()
|
|
723
|
+
const allMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
|
|
724
|
+
|
|
725
|
+
for (const method of allMethods) {
|
|
726
|
+
// Check static routes
|
|
727
|
+
if (this.staticRoutes.has(method)) {
|
|
728
|
+
const staticRoute = this.staticRoutes.get(method)!.get(url.pathname)
|
|
729
|
+
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
730
|
+
methods.add(method)
|
|
731
|
+
continue
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Get potential routes
|
|
736
|
+
const potentialRoutes: Route[] = domain && this.domains[domain]
|
|
737
|
+
? this.domains[domain]
|
|
738
|
+
: this.routes
|
|
739
|
+
|
|
740
|
+
// Filter routes to only those matching the HTTP method
|
|
741
|
+
const methodRoutes = potentialRoutes.filter((route: Route) => route.method === method)
|
|
742
|
+
|
|
743
|
+
// Check exact match
|
|
744
|
+
for (const route of methodRoutes) {
|
|
745
|
+
if (route.path === url.pathname) {
|
|
746
|
+
methods.add(method)
|
|
747
|
+
break
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Check pattern match if not already found
|
|
752
|
+
if (!methods.has(method)) {
|
|
753
|
+
for (const route of methodRoutes) {
|
|
754
|
+
if (route.pattern) {
|
|
755
|
+
const match = route.pattern.exec(url)
|
|
756
|
+
if (match) {
|
|
757
|
+
methods.add(method)
|
|
758
|
+
break
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Check wildcard routes if not already found
|
|
765
|
+
if (!methods.has(method)) {
|
|
766
|
+
const wildcardRoutes = potentialRoutes.filter((route: Route) =>
|
|
767
|
+
route.method === method && route.path.endsWith('*'),
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
for (const route of wildcardRoutes) {
|
|
771
|
+
const basePath = route.path.slice(0, -1)
|
|
772
|
+
if (url.pathname.startsWith(basePath)) {
|
|
773
|
+
methods.add(method)
|
|
774
|
+
break
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// If GET is allowed, HEAD is implicitly allowed too
|
|
781
|
+
if (methods.has('GET')) {
|
|
782
|
+
methods.add('HEAD')
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return Array.from(methods)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Match a route based on the path, method, and domain
|
|
790
|
+
*/
|
|
791
|
+
matchRoute(path: string, method: string, domain?: string): { route: Route, params: Record<string, string> } | undefined {
|
|
792
|
+
const url = new URL(path, 'http://localhost')
|
|
793
|
+
|
|
794
|
+
// Generate cache key
|
|
795
|
+
const cacheKey = `${domain || ''}:${method}:${url.pathname}`
|
|
796
|
+
|
|
797
|
+
// Check cache first
|
|
798
|
+
if (this.routeCache.has(cacheKey)) {
|
|
799
|
+
return this.routeCache.get(cacheKey)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Fast path for static routes
|
|
803
|
+
if (this.staticRoutes.has(method)) {
|
|
804
|
+
const staticRoute = this.staticRoutes.get(method)!.get(url.pathname)
|
|
805
|
+
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
806
|
+
const result = {
|
|
807
|
+
route: staticRoute,
|
|
808
|
+
params: {},
|
|
809
|
+
}
|
|
810
|
+
this.routeCache.set(cacheKey, result)
|
|
811
|
+
return result
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Get potential routes - either all routes or domain-specific routes
|
|
816
|
+
const potentialRoutes: Route[] = domain && this.domains[domain]
|
|
817
|
+
? this.domains[domain]
|
|
818
|
+
: this.routes
|
|
819
|
+
|
|
820
|
+
// Filter routes to only those matching the HTTP method
|
|
821
|
+
const methodRoutes = potentialRoutes.filter((route: Route) => route.method === method)
|
|
822
|
+
|
|
823
|
+
// First, try to find an exact match
|
|
824
|
+
for (const route of methodRoutes) {
|
|
825
|
+
if (route.path === url.pathname) {
|
|
826
|
+
const result = {
|
|
827
|
+
route,
|
|
828
|
+
params: {},
|
|
829
|
+
}
|
|
830
|
+
this.routeCache.set(cacheKey, result)
|
|
831
|
+
return result
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// If no exact match, try matching patterns
|
|
836
|
+
for (const route of methodRoutes) {
|
|
837
|
+
if (route.pattern) {
|
|
838
|
+
const match = route.pattern.exec(url)
|
|
839
|
+
if (match) {
|
|
840
|
+
const result = {
|
|
841
|
+
route,
|
|
842
|
+
params: match.pathname.groups,
|
|
843
|
+
}
|
|
844
|
+
this.routeCache.set(cacheKey, result)
|
|
845
|
+
return result
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// If still no match, try domain-specific * (wildcard) routes
|
|
851
|
+
if (domain && this.domains[domain]) {
|
|
852
|
+
const wildcardRoutes = this.domains[domain].filter((route: Route) =>
|
|
853
|
+
route.method === method && route.path.endsWith('*'),
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
for (const route of wildcardRoutes) {
|
|
857
|
+
const basePath = route.path.slice(0, -1) // Remove the '*'
|
|
858
|
+
if (url.pathname.startsWith(basePath)) {
|
|
859
|
+
const result = {
|
|
860
|
+
route,
|
|
861
|
+
params: {
|
|
862
|
+
wildcard: url.pathname.slice(basePath.length),
|
|
863
|
+
},
|
|
864
|
+
}
|
|
865
|
+
this.routeCache.set(cacheKey, result)
|
|
866
|
+
return result
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// If no match in domain-specific routes, try global wildcard routes
|
|
872
|
+
const globalWildcardRoutes = this.routes.filter((route: Route) =>
|
|
873
|
+
route.method === method && route.path.endsWith('*') && (!domain || !route.domain),
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
for (const route of globalWildcardRoutes) {
|
|
877
|
+
const basePath = route.path.slice(0, -1) // Remove the '*'
|
|
878
|
+
if (url.pathname.startsWith(basePath)) {
|
|
879
|
+
const result = {
|
|
880
|
+
route,
|
|
881
|
+
params: {
|
|
882
|
+
wildcard: url.pathname.slice(basePath.length),
|
|
883
|
+
},
|
|
884
|
+
}
|
|
885
|
+
this.routeCache.set(cacheKey, result)
|
|
886
|
+
return result
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// If no match for specific method, try HEAD for GET requests
|
|
891
|
+
if (method === 'HEAD') {
|
|
892
|
+
return this.matchRoute(path, 'GET', domain)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// No matching route found
|
|
896
|
+
return undefined
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Enhance a request with params and other utilities
|
|
901
|
+
*/
|
|
902
|
+
enhanceRequest(req: Request, params: Record<string, string> = {}): EnhancedRequest {
|
|
903
|
+
// Lazy cookie parsing
|
|
904
|
+
let parsedCookies: Record<string, string> | null = null
|
|
905
|
+
|
|
906
|
+
const getCookies = () => {
|
|
907
|
+
if (parsedCookies === null) {
|
|
908
|
+
parsedCookies = {}
|
|
909
|
+
const cookieHeader = req.headers.get('cookie') || ''
|
|
910
|
+
|
|
911
|
+
cookieHeader.split(';').forEach((cookie) => {
|
|
912
|
+
const parts = cookie.trim().split('=')
|
|
913
|
+
if (parts.length >= 2) {
|
|
914
|
+
const name = parts[0].trim()
|
|
915
|
+
const value = parts.slice(1).join('=').trim()
|
|
916
|
+
parsedCookies![name] = decodeURIComponent(value)
|
|
917
|
+
}
|
|
918
|
+
})
|
|
919
|
+
}
|
|
920
|
+
return parsedCookies
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Parse query string
|
|
924
|
+
const url = new URL(req.url)
|
|
925
|
+
const query: Record<string, string> = {}
|
|
926
|
+
url.searchParams.forEach((value, key) => {
|
|
927
|
+
query[key] = value
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
// Create a wrapper object that proxies to the original request
|
|
931
|
+
// This is necessary because native Request objects don't allow property assignment in Bun
|
|
932
|
+
const enhancedReq = {
|
|
933
|
+
// Proxy the native Request properties/methods
|
|
934
|
+
get url() { return req.url },
|
|
935
|
+
get method() { return req.method },
|
|
936
|
+
get headers() { return req.headers },
|
|
937
|
+
get body() { return req.body },
|
|
938
|
+
get bodyUsed() { return req.bodyUsed },
|
|
939
|
+
get cache() { return req.cache },
|
|
940
|
+
get credentials() { return req.credentials },
|
|
941
|
+
get destination() { return req.destination },
|
|
942
|
+
get integrity() { return req.integrity },
|
|
943
|
+
get keepalive() { return req.keepalive },
|
|
944
|
+
get mode() { return req.mode },
|
|
945
|
+
get redirect() { return req.redirect },
|
|
946
|
+
get referrer() { return req.referrer },
|
|
947
|
+
get referrerPolicy() { return req.referrerPolicy },
|
|
948
|
+
get signal() { return req.signal },
|
|
949
|
+
arrayBuffer: () => req.arrayBuffer(),
|
|
950
|
+
blob: () => req.blob(),
|
|
951
|
+
clone: () => req.clone(),
|
|
952
|
+
formData: () => req.formData(),
|
|
953
|
+
json: () => req.json(),
|
|
954
|
+
text: () => req.text(),
|
|
955
|
+
|
|
956
|
+
// Enhanced properties
|
|
957
|
+
params,
|
|
958
|
+
query,
|
|
959
|
+
jsonBody: null as any,
|
|
960
|
+
formBody: null as any,
|
|
961
|
+
_cookiesToSet: [] as CookieToSet[],
|
|
962
|
+
_cookiesToDelete: [] as { name: string, options: CookieOptions }[],
|
|
963
|
+
|
|
964
|
+
} as EnhancedRequest
|
|
965
|
+
|
|
966
|
+
// Helper to get all input data - needs to be defined before adding to object
|
|
967
|
+
const getAllInput = (): Record<string, any> => {
|
|
968
|
+
const input: Record<string, any> = {}
|
|
969
|
+
|
|
970
|
+
// Query parameters
|
|
971
|
+
for (const [key, value] of Object.entries(query)) {
|
|
972
|
+
input[key] = value
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// JSON body
|
|
976
|
+
if (enhancedReq.jsonBody && typeof enhancedReq.jsonBody === 'object') {
|
|
977
|
+
for (const [key, value] of Object.entries(enhancedReq.jsonBody)) {
|
|
978
|
+
input[key] = value
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Form body
|
|
983
|
+
if (enhancedReq.formBody && typeof enhancedReq.formBody === 'object') {
|
|
984
|
+
for (const [key, value] of Object.entries(enhancedReq.formBody)) {
|
|
985
|
+
input[key] = value
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Route params
|
|
990
|
+
for (const [key, value] of Object.entries(params)) {
|
|
991
|
+
input[key] = value
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return input
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Add cookie utilities
|
|
998
|
+
enhancedReq.cookies = {
|
|
999
|
+
get: (name: string) => getCookies()[name],
|
|
1000
|
+
set: (name: string, value: string, options: CookieOptions = {}) => {
|
|
1001
|
+
enhancedReq._cookiesToSet!.push({ name, value, options })
|
|
1002
|
+
},
|
|
1003
|
+
delete: (name: string, options: CookieOptions = {}) => {
|
|
1004
|
+
enhancedReq._cookiesToDelete!.push({ name, options })
|
|
1005
|
+
},
|
|
1006
|
+
getAll: () => ({ ...getCookies() }),
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Add Laravel-style request methods directly to the wrapper object
|
|
1010
|
+
// These are the methods that provide Laravel-like request handling
|
|
1011
|
+
;(enhancedReq as any).get = <T = any>(key: string, defaultValue?: T): T => {
|
|
1012
|
+
const input = getAllInput()
|
|
1013
|
+
const value = input[key]
|
|
1014
|
+
return (value !== undefined ? value : defaultValue) as T
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
;(enhancedReq as any).input = <T = any>(key: string, defaultValue?: T): T => {
|
|
1018
|
+
const input = getAllInput()
|
|
1019
|
+
const value = input[key]
|
|
1020
|
+
return (value !== undefined ? value : defaultValue) as T
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
;(enhancedReq as any).all = (): Record<string, any> => getAllInput()
|
|
1024
|
+
|
|
1025
|
+
;(enhancedReq as any).only = <T extends Record<string, unknown>>(keys: string[]): T => {
|
|
1026
|
+
const input = getAllInput()
|
|
1027
|
+
const result = {} as T
|
|
1028
|
+
for (const key of keys) {
|
|
1029
|
+
if (key in input) {
|
|
1030
|
+
(result as any)[key] = input[key]
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return result
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
;(enhancedReq as any).except = <T extends Record<string, unknown>>(keys: string[]): T => {
|
|
1037
|
+
const input = getAllInput()
|
|
1038
|
+
const result = { ...input } as T
|
|
1039
|
+
for (const key of keys) {
|
|
1040
|
+
delete (result as any)[key]
|
|
1041
|
+
}
|
|
1042
|
+
return result
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
;(enhancedReq as any).has = (key: string | string[]): boolean => {
|
|
1046
|
+
const input = getAllInput()
|
|
1047
|
+
if (Array.isArray(key)) {
|
|
1048
|
+
return key.every(k => k in input && input[k] !== undefined)
|
|
1049
|
+
}
|
|
1050
|
+
return key in input && input[key] !== undefined
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
;(enhancedReq as any).hasAny = (keys: string[]): boolean => {
|
|
1054
|
+
const input = getAllInput()
|
|
1055
|
+
return keys.some(k => k in input && input[k] !== undefined)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
;(enhancedReq as any).filled = (key: string | string[]): boolean => {
|
|
1059
|
+
const input = getAllInput()
|
|
1060
|
+
const isFilled = (k: string): boolean => {
|
|
1061
|
+
const value = input[k]
|
|
1062
|
+
return value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && value.length === 0)
|
|
1063
|
+
}
|
|
1064
|
+
if (Array.isArray(key)) {
|
|
1065
|
+
return key.every(isFilled)
|
|
1066
|
+
}
|
|
1067
|
+
return isFilled(key)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
;(enhancedReq as any).missing = (key: string | string[]): boolean => {
|
|
1071
|
+
const input = getAllInput()
|
|
1072
|
+
if (Array.isArray(key)) {
|
|
1073
|
+
return key.every(k => !(k in input) || input[k] === undefined)
|
|
1074
|
+
}
|
|
1075
|
+
return !(key in input) || input[key] === undefined
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
;(enhancedReq as any).string = (key: string, defaultValue: string = ''): string => {
|
|
1079
|
+
const input = getAllInput()
|
|
1080
|
+
const value = input[key]
|
|
1081
|
+
return value !== undefined && value !== null ? String(value) : defaultValue
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
;(enhancedReq as any).integer = (key: string, defaultValue: number = 0): number => {
|
|
1085
|
+
const input = getAllInput()
|
|
1086
|
+
const value = input[key]
|
|
1087
|
+
const parsed = Number.parseInt(String(value), 10)
|
|
1088
|
+
return Number.isNaN(parsed) ? defaultValue : parsed
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
;(enhancedReq as any).float = (key: string, defaultValue: number = 0): number => {
|
|
1092
|
+
const input = getAllInput()
|
|
1093
|
+
const value = input[key]
|
|
1094
|
+
const parsed = Number.parseFloat(String(value))
|
|
1095
|
+
return Number.isNaN(parsed) ? defaultValue : parsed
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
;(enhancedReq as any).boolean = (key: string, defaultValue: boolean = false): boolean => {
|
|
1099
|
+
const input = getAllInput()
|
|
1100
|
+
const value = input[key]
|
|
1101
|
+
if (value === undefined || value === null)
|
|
1102
|
+
return defaultValue
|
|
1103
|
+
if (typeof value === 'boolean')
|
|
1104
|
+
return value
|
|
1105
|
+
if (value === 'true' || value === '1' || value === 1)
|
|
1106
|
+
return true
|
|
1107
|
+
if (value === 'false' || value === '0' || value === 0)
|
|
1108
|
+
return false
|
|
1109
|
+
return defaultValue
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
;(enhancedReq as any).array = <T = unknown>(key: string): T[] => {
|
|
1113
|
+
const input = getAllInput()
|
|
1114
|
+
const value = input[key]
|
|
1115
|
+
if (Array.isArray(value))
|
|
1116
|
+
return value as T[]
|
|
1117
|
+
return value !== undefined && value !== null ? [value as T] : []
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
;(enhancedReq as any).bearerToken = (): string | null => {
|
|
1121
|
+
const authHeader
|
|
1122
|
+
= req.headers.get('authorization')
|
|
1123
|
+
|| req.headers.get('Authorization')
|
|
1124
|
+
|| ''
|
|
1125
|
+
if (authHeader.startsWith('Bearer '))
|
|
1126
|
+
return authHeader.substring(7)
|
|
1127
|
+
return null
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
;(enhancedReq as any).header = (name: string): string | null => {
|
|
1131
|
+
return req.headers.get(name) || req.headers.get(name.toLowerCase()) || null
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
;(enhancedReq as any).getParam = <T = string>(name: string, defaultValue?: T): T | undefined => {
|
|
1135
|
+
const value = params?.[name] as T | undefined
|
|
1136
|
+
return value !== undefined ? value : defaultValue
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
;(enhancedReq as any).params = params || {}
|
|
1140
|
+
|
|
1141
|
+
return enhancedReq as EnhancedRequest
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Apply modified cookies to a response
|
|
1146
|
+
*/
|
|
1147
|
+
applyModifiedCookies(response: Response, req: EnhancedRequest): Response {
|
|
1148
|
+
// Clone the response to modify headers
|
|
1149
|
+
const newResponse = new Response(response.body, {
|
|
1150
|
+
status: response.status,
|
|
1151
|
+
statusText: response.statusText,
|
|
1152
|
+
headers: response.headers,
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
// Apply cookies to set
|
|
1156
|
+
if (req._cookiesToSet && req._cookiesToSet.length > 0) {
|
|
1157
|
+
for (const { name, value, options } of req._cookiesToSet) {
|
|
1158
|
+
const cookieString = this.serializeCookie(name, value, options)
|
|
1159
|
+
newResponse.headers.append('Set-Cookie', cookieString)
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Apply cookies to delete
|
|
1164
|
+
if (req._cookiesToDelete && req._cookiesToDelete.length > 0) {
|
|
1165
|
+
for (const { name, options } of req._cookiesToDelete) {
|
|
1166
|
+
const deletionOptions = {
|
|
1167
|
+
...options,
|
|
1168
|
+
expires: new Date(0), // Set expiration to past date
|
|
1169
|
+
maxAge: 0,
|
|
1170
|
+
}
|
|
1171
|
+
const cookieString = this.serializeCookie(name, '', deletionOptions)
|
|
1172
|
+
newResponse.headers.append('Set-Cookie', cookieString)
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return newResponse
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Serialize a cookie for the Set-Cookie header
|
|
1181
|
+
*/
|
|
1182
|
+
serializeCookie(name: string, value: string, options: CookieOptions = {}): string {
|
|
1183
|
+
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
|
|
1184
|
+
|
|
1185
|
+
if (options.maxAge !== undefined) {
|
|
1186
|
+
cookie += `; Max-Age=${options.maxAge}`
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (options.expires && options.expires instanceof Date) {
|
|
1190
|
+
cookie += `; Expires=${options.expires.toUTCString()}`
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (options.path) {
|
|
1194
|
+
cookie += `; Path=${options.path}`
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
cookie += '; Path=/'
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (options.domain) {
|
|
1201
|
+
cookie += `; Domain=${options.domain}`
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (options.secure) {
|
|
1205
|
+
cookie += '; Secure'
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (options.httpOnly) {
|
|
1209
|
+
cookie += '; HttpOnly'
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (options.sameSite) {
|
|
1213
|
+
const sameSite = options.sameSite.toLowerCase()
|
|
1214
|
+
cookie += `; SameSite=${sameSite.charAt(0).toUpperCase() + sameSite.slice(1)}`
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return cookie
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Build an optimized middleware chain
|
|
1222
|
+
*/
|
|
1223
|
+
buildMiddlewareChain(middlewares: MiddlewareHandler[]): (req: EnhancedRequest) => Promise<Response | null> {
|
|
1224
|
+
if (middlewares.length === 0) {
|
|
1225
|
+
return async (_req: EnhancedRequest) => null
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Build the chain from the end to start for better performance
|
|
1229
|
+
let chain = async (_req: EnhancedRequest): Promise<Response | null> => null
|
|
1230
|
+
|
|
1231
|
+
for (let i = middlewares.length - 1; i >= 0; i--) {
|
|
1232
|
+
const middleware = middlewares[i]
|
|
1233
|
+
const nextChain = chain
|
|
1234
|
+
chain = async (req: EnhancedRequest): Promise<Response | null> => {
|
|
1235
|
+
const next = async (): Promise<Response> => {
|
|
1236
|
+
const result = await nextChain(req)
|
|
1237
|
+
return result || new Response(null, { status: 200 })
|
|
1238
|
+
}
|
|
1239
|
+
return middleware(req, next)
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
return chain
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Run middleware stack for a request
|
|
1248
|
+
*/
|
|
1249
|
+
async runMiddleware(req: EnhancedRequest, middlewareStack: MiddlewareHandler[]): Promise<Response | null> {
|
|
1250
|
+
if (middlewareStack.length === 0) {
|
|
1251
|
+
return null // No middleware to run
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
try {
|
|
1255
|
+
// Build and execute optimized middleware chain
|
|
1256
|
+
const chain = this.buildMiddlewareChain(middlewareStack)
|
|
1257
|
+
return await chain(req)
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
if (this.errorHandler) {
|
|
1261
|
+
return this.errorHandler(error as Error)
|
|
1262
|
+
}
|
|
1263
|
+
throw error
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Resolve an action handler
|
|
1269
|
+
*/
|
|
1270
|
+
async resolveHandler(handler: ActionHandler, req: EnhancedRequest): Promise<Response> {
|
|
1271
|
+
// If it's a function, call it with the request
|
|
1272
|
+
if (typeof handler === 'function' && !(handler as { prototype?: { handle?: unknown } }).prototype?.handle) {
|
|
1273
|
+
return await (handler as (req: EnhancedRequest) => Response | Promise<Response>)(req)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// If it's a class constructor, instantiate it and call handle
|
|
1277
|
+
if (typeof handler === 'function' && (handler as { prototype?: { handle?: unknown } }).prototype?.handle) {
|
|
1278
|
+
const HandlerClass = handler as new () => { handle: (req: EnhancedRequest) => Response | Promise<Response> }
|
|
1279
|
+
const handlerInstance = new HandlerClass()
|
|
1280
|
+
return await handlerInstance.handle(req)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// If it's an object with a handle method
|
|
1284
|
+
if (handler && typeof (handler as unknown as { handle?: unknown }).handle === 'function') {
|
|
1285
|
+
return await (handler as unknown as { handle: (req: EnhancedRequest) => Response | Promise<Response> }).handle(req)
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
throw new Error(`Invalid action handler: ${typeof handler}`)
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Add middleware to the router
|
|
1293
|
+
*/
|
|
1294
|
+
use(...middleware: (string | MiddlewareHandler)[]): Router {
|
|
1295
|
+
for (const mw of middleware) {
|
|
1296
|
+
const resolvedMiddleware = this.resolveMiddleware(mw)
|
|
1297
|
+
if (resolvedMiddleware) {
|
|
1298
|
+
this.globalMiddleware.push(resolvedMiddleware)
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return this
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Create a route group with prefix and middleware
|
|
1306
|
+
*/
|
|
1307
|
+
group(options: { prefix?: string, middleware?: (string | MiddlewareHandler)[] }, callback: () => void | Promise<void>): Router {
|
|
1308
|
+
// Save current group state
|
|
1309
|
+
const previousGroup = this.currentGroup
|
|
1310
|
+
|
|
1311
|
+
// Create new group
|
|
1312
|
+
this.currentGroup = {
|
|
1313
|
+
prefix: options.prefix || '',
|
|
1314
|
+
middleware: [],
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Resolve middleware if provided
|
|
1318
|
+
if (options.middleware) {
|
|
1319
|
+
for (const mw of options.middleware) {
|
|
1320
|
+
if (typeof mw === 'function') {
|
|
1321
|
+
this.currentGroup.middleware!.push(mw)
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Execute callback
|
|
1327
|
+
const result = callback()
|
|
1328
|
+
|
|
1329
|
+
// Handle async callbacks
|
|
1330
|
+
if (result instanceof Promise) {
|
|
1331
|
+
result.then(() => {
|
|
1332
|
+
this.currentGroup = previousGroup
|
|
1333
|
+
})
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
// Restore previous group state
|
|
1337
|
+
this.currentGroup = previousGroup
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return this
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Add route to the router
|
|
1345
|
+
*/
|
|
1346
|
+
addRoute(route: Route): this {
|
|
1347
|
+
this.routes.push(route)
|
|
1348
|
+
if (route.name) {
|
|
1349
|
+
this.namedRoutes.set(route.name, route)
|
|
1350
|
+
registerNamedRoute(route.name, route.path)
|
|
1351
|
+
}
|
|
1352
|
+
return this
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Register middleware dependency
|
|
1357
|
+
*/
|
|
1358
|
+
registerMiddlewareDependency(dependency: MiddlewareDependency): this {
|
|
1359
|
+
if (this._middlewarePipeline) {
|
|
1360
|
+
this._middlewarePipeline.registerDependency(dependency)
|
|
1361
|
+
}
|
|
1362
|
+
return this
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Register middleware skip conditions
|
|
1367
|
+
*/
|
|
1368
|
+
registerMiddlewareSkipConditions(_middlewareName: string, _conditions: MiddlewareSkipCondition[]): this {
|
|
1369
|
+
// Implementation would register skip conditions with the pipeline
|
|
1370
|
+
return this
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Get middleware statistics
|
|
1375
|
+
*/
|
|
1376
|
+
getMiddlewareStats(): MiddlewarePipelineStats | Record<string, never> {
|
|
1377
|
+
if (this._middlewarePipeline) {
|
|
1378
|
+
return this._middlewarePipeline.getStats()
|
|
1379
|
+
}
|
|
1380
|
+
return {}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Get middleware cache info
|
|
1385
|
+
*/
|
|
1386
|
+
getMiddlewareCacheInfo(): MiddlewareCacheInfo {
|
|
1387
|
+
// Return cache info based on compiled pipelines
|
|
1388
|
+
return {
|
|
1389
|
+
size: 0,
|
|
1390
|
+
routes: [],
|
|
1391
|
+
hitRate: 0,
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Clear middleware cache
|
|
1397
|
+
*/
|
|
1398
|
+
clearMiddlewareCache(): this {
|
|
1399
|
+
if (this._middlewarePipeline) {
|
|
1400
|
+
this._middlewarePipeline.clear()
|
|
1401
|
+
}
|
|
1402
|
+
return this
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Execute middleware pipeline
|
|
1407
|
+
*/
|
|
1408
|
+
async executeMiddleware(middleware: MiddlewareHandler[], request: EnhancedRequest, handler: () => Promise<Response>): Promise<Response> {
|
|
1409
|
+
if (middleware.length === 0) {
|
|
1410
|
+
return handler()
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
let currentIndex = 0
|
|
1414
|
+
|
|
1415
|
+
const next = async (): Promise<Response> => {
|
|
1416
|
+
if (currentIndex >= middleware.length) {
|
|
1417
|
+
return handler()
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
const mw = middleware[currentIndex++]
|
|
1421
|
+
const result = await mw(request, next)
|
|
1422
|
+
return result || new Response('No response from middleware', { status: 500 })
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return next()
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Builder for conditional middleware
|
|
1431
|
+
*/
|
|
1432
|
+
export class ConditionalBuilder {
|
|
1433
|
+
constructor(
|
|
1434
|
+
private router: Router,
|
|
1435
|
+
private condition: MiddlewareCondition,
|
|
1436
|
+
) {}
|
|
1437
|
+
|
|
1438
|
+
middleware(middlewareName: string): this {
|
|
1439
|
+
const [name, params] = middlewareName.split(':')
|
|
1440
|
+
const middlewareFactory = (this.router as any).namedMiddleware.get(name)
|
|
1441
|
+
|
|
1442
|
+
if (!middlewareFactory) {
|
|
1443
|
+
throw new Error(`Unknown middleware: ${name}`)
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const middlewareHandler = middlewareFactory(params);
|
|
1447
|
+
(this.router as any).conditionalMiddleware.push({
|
|
1448
|
+
condition: this.condition,
|
|
1449
|
+
middleware: [middlewareHandler],
|
|
1450
|
+
})
|
|
1451
|
+
|
|
1452
|
+
return this
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Builder for middleware with parameters
|
|
1458
|
+
*/
|
|
1459
|
+
export class MiddlewareBuilder {
|
|
1460
|
+
constructor(
|
|
1461
|
+
private router: Router,
|
|
1462
|
+
private middleware: MiddlewareHandler[],
|
|
1463
|
+
) {}
|
|
1464
|
+
|
|
1465
|
+
get(_path: string, _handler: RouteHandler): this {
|
|
1466
|
+
// This would integrate with the router's route registration
|
|
1467
|
+
return this
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
post(_path: string, _handler: RouteHandler): this {
|
|
1471
|
+
return this
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
put(_path: string, _handler: RouteHandler): this {
|
|
1475
|
+
return this
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
delete(_path: string, _handler: RouteHandler): this {
|
|
1479
|
+
return this
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Builder for route groups with middleware
|
|
1485
|
+
*/
|
|
1486
|
+
export class RouteGroupBuilder {
|
|
1487
|
+
constructor(
|
|
1488
|
+
private router: Router,
|
|
1489
|
+
private middleware: MiddlewareHandler[],
|
|
1490
|
+
) {}
|
|
1491
|
+
|
|
1492
|
+
get(_path: string, _handler: RouteHandler): this {
|
|
1493
|
+
// This would integrate with the router's route registration
|
|
1494
|
+
return this
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
post(_path: string, _handler: RouteHandler): this {
|
|
1498
|
+
return this
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
put(_path: string, _handler: RouteHandler): this {
|
|
1502
|
+
return this
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
delete(_path: string, _handler: RouteHandler): this {
|
|
1506
|
+
return this
|
|
1507
|
+
}
|
|
1508
|
+
}
|