aetherjs-router 1.0.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.
- package/README.md +541 -0
- package/index.js +21 -0
- package/package.json +41 -0
- package/src/aether-adapter.js +98 -0
- package/src/examples/basic-router.js +796 -0
- package/src/path-compiler.js +660 -0
- package/src/route-factory.js +326 -0
- package/src/router-factory.js +840 -0
- package/src/test/benchmark/router-benchmark.test.js +561 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
// src/router-factory.js - Enhanced router factory with query parameter support
|
|
2
|
+
import { createRoute } from "./route-factory.js";
|
|
3
|
+
import { createPathCompiler } from "./path-compiler.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Factory function to create a new Router instance with enhanced features
|
|
7
|
+
* @param {Object} options - Configuration options for the router
|
|
8
|
+
* @param {string} options.prefix - URL prefix for all routes
|
|
9
|
+
* @param {boolean} options.caseSensitive - Whether path matching is case sensitive
|
|
10
|
+
* @param {boolean} options.strict - Whether trailing slashes are strict
|
|
11
|
+
* @param {number} options.cacheSize - Size of route matching cache
|
|
12
|
+
* @param {boolean} options.parseQuery - Enable query parameter parsing in route patterns
|
|
13
|
+
* @param {boolean} options.autoParseQuery - Automatically parse query parameters into ctx.query
|
|
14
|
+
* @param {boolean} options.enableVersioning - Enable versioning support
|
|
15
|
+
* @returns {Router} Configured router instance
|
|
16
|
+
*/
|
|
17
|
+
export function createRouter(options = {}) {
|
|
18
|
+
return new Router(options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* High-performance Router class with factory pattern support
|
|
23
|
+
* Enhanced with query parameter support, versioning, and advanced grouping
|
|
24
|
+
*/
|
|
25
|
+
class Router {
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
// Default configuration with performance optimizations
|
|
28
|
+
this.options = {
|
|
29
|
+
prefix: "",
|
|
30
|
+
caseSensitive: true,
|
|
31
|
+
strict: true,
|
|
32
|
+
cacheSize: 1000, // Cache 1000 most recent matches
|
|
33
|
+
parseQuery: false, // Enable query parameter pattern matching
|
|
34
|
+
autoParseQuery: true, // Automatically parse query params
|
|
35
|
+
enableVersioning: false, // Enable versioning support
|
|
36
|
+
...options,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Route storage by HTTP method for O(1) lookup
|
|
40
|
+
this._routesByMethod = {
|
|
41
|
+
GET: [],
|
|
42
|
+
POST: [],
|
|
43
|
+
PUT: [],
|
|
44
|
+
DELETE: [],
|
|
45
|
+
PATCH: [],
|
|
46
|
+
OPTIONS: [],
|
|
47
|
+
HEAD: [],
|
|
48
|
+
ALL: [], // Catch-all method
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Router-level middleware stack
|
|
52
|
+
this.middleware = [];
|
|
53
|
+
|
|
54
|
+
// LRU cache for route matching (performance optimization)
|
|
55
|
+
this._routeCache = new Map();
|
|
56
|
+
this._cacheSize = this.options.cacheSize;
|
|
57
|
+
|
|
58
|
+
// Performance metrics
|
|
59
|
+
this._cacheHits = 0;
|
|
60
|
+
this._cacheMisses = 0;
|
|
61
|
+
|
|
62
|
+
// Path compiler instance for regex pattern compilation
|
|
63
|
+
this._pathCompiler = createPathCompiler({
|
|
64
|
+
sensitive: this.options.caseSensitive,
|
|
65
|
+
strict: this.options.strict,
|
|
66
|
+
end: true,
|
|
67
|
+
parseQuery: this.options.parseQuery,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Version registry for versioned routes
|
|
71
|
+
this._versions = new Map();
|
|
72
|
+
|
|
73
|
+
// Query parameter parser cache
|
|
74
|
+
this._queryParserCache = new Map();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Factory method to add a route with middleware support
|
|
79
|
+
* Enhanced to support query parameter patterns like /users?id=:id&lang=:lang
|
|
80
|
+
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
81
|
+
* @param {string} path - Route path pattern (may include query parameters)
|
|
82
|
+
* @param {Function} handler - Route handler function
|
|
83
|
+
* @param {Array<Function>} middleware - Route-specific middleware
|
|
84
|
+
* @param {Object} routeOptions - Route-specific options
|
|
85
|
+
* @returns {Router} Chainable router instance
|
|
86
|
+
*/
|
|
87
|
+
addRoute(method, path, handler, middleware = [], routeOptions = {}) {
|
|
88
|
+
const fullPath = this.options.prefix + path;
|
|
89
|
+
const options = {
|
|
90
|
+
parseQuery: this.options.parseQuery,
|
|
91
|
+
...routeOptions,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const route = createRoute(method, fullPath, handler, middleware, options);
|
|
95
|
+
|
|
96
|
+
const methodKey = method.toUpperCase();
|
|
97
|
+
if (this._routesByMethod[methodKey]) {
|
|
98
|
+
this._routesByMethod[methodKey].push(route);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Clear cache when routes change
|
|
102
|
+
this._routeCache.clear();
|
|
103
|
+
|
|
104
|
+
// Emit route added event if event system is enabled
|
|
105
|
+
if (this._events) {
|
|
106
|
+
this._emit("route:added", { method, path: fullPath, route });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// HTTP method shortcut factories with enhanced query parameter support
|
|
113
|
+
get(path, handler, ...middleware) {
|
|
114
|
+
// Check if last argument is route options
|
|
115
|
+
const routeOptions =
|
|
116
|
+
typeof middleware[middleware.length - 1] === "object"
|
|
117
|
+
? middleware.pop()
|
|
118
|
+
: {};
|
|
119
|
+
return this.addRoute("GET", path, handler, middleware, routeOptions);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
post(path, handler, ...middleware) {
|
|
123
|
+
const routeOptions =
|
|
124
|
+
typeof middleware[middleware.length - 1] === "object"
|
|
125
|
+
? middleware.pop()
|
|
126
|
+
: {};
|
|
127
|
+
return this.addRoute("POST", path, handler, middleware, routeOptions);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
put(path, handler, ...middleware) {
|
|
131
|
+
const routeOptions =
|
|
132
|
+
typeof middleware[middleware.length - 1] === "object"
|
|
133
|
+
? middleware.pop()
|
|
134
|
+
: {};
|
|
135
|
+
return this.addRoute("PUT", path, handler, middleware, routeOptions);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
delete(path, handler, ...middleware) {
|
|
139
|
+
const routeOptions =
|
|
140
|
+
typeof middleware[middleware.length - 1] === "object"
|
|
141
|
+
? middleware.pop()
|
|
142
|
+
: {};
|
|
143
|
+
return this.addRoute("DELETE", path, handler, middleware, routeOptions);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
patch(path, handler, ...middleware) {
|
|
147
|
+
const routeOptions =
|
|
148
|
+
typeof middleware[middleware.length - 1] === "object"
|
|
149
|
+
? middleware.pop()
|
|
150
|
+
: {};
|
|
151
|
+
return this.addRoute("PATCH", path, handler, middleware, routeOptions);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
options(path, handler, ...middleware) {
|
|
155
|
+
const routeOptions =
|
|
156
|
+
typeof middleware[middleware.length - 1] === "object"
|
|
157
|
+
? middleware.pop()
|
|
158
|
+
: {};
|
|
159
|
+
return this.addRoute("OPTIONS", path, handler, middleware, routeOptions);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
head(path, handler, ...middleware) {
|
|
163
|
+
const routeOptions =
|
|
164
|
+
typeof middleware[middleware.length - 1] === "object"
|
|
165
|
+
? middleware.pop()
|
|
166
|
+
: {};
|
|
167
|
+
return this.addRoute("HEAD", path, handler, middleware, routeOptions);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
all(path, handler, ...middleware) {
|
|
171
|
+
const routeOptions =
|
|
172
|
+
typeof middleware[middleware.length - 1] === "object"
|
|
173
|
+
? middleware.pop()
|
|
174
|
+
: {};
|
|
175
|
+
return this.addRoute("ALL", path, handler, middleware, routeOptions);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Enhanced route grouping with shared prefix and middleware
|
|
180
|
+
* Supports nested grouping and versioning
|
|
181
|
+
* @param {string} prefix - Group path prefix
|
|
182
|
+
* @param {Function} callback - Configuration callback
|
|
183
|
+
* @param {Object} groupOptions - Group-specific options
|
|
184
|
+
* @returns {Router} Chainable router instance
|
|
185
|
+
*/
|
|
186
|
+
group(prefix, callback, groupOptions = {}) {
|
|
187
|
+
const router = createRouter({
|
|
188
|
+
...this.options,
|
|
189
|
+
prefix: this.options.prefix + prefix,
|
|
190
|
+
...groupOptions,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
callback(router);
|
|
194
|
+
|
|
195
|
+
// Merge routes from subgroup
|
|
196
|
+
Object.keys(this._routesByMethod).forEach((method) => {
|
|
197
|
+
this._routesByMethod[method].push(...router._routesByMethod[method]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Merge middleware from subgroup
|
|
201
|
+
this.middleware.push(...router.middleware);
|
|
202
|
+
|
|
203
|
+
// Emit group created event
|
|
204
|
+
if (this._events) {
|
|
205
|
+
this._emit("group:created", { prefix, router, options: groupOptions });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create versioned API routes
|
|
213
|
+
* @param {string|Array} versions - Version string or array of versions
|
|
214
|
+
* @param {Function} callback - Configuration callback
|
|
215
|
+
* @param {Object} versionOptions - Version-specific options
|
|
216
|
+
* @returns {Router} Chainable router instance
|
|
217
|
+
*/
|
|
218
|
+
version(versions, callback, versionOptions = {}) {
|
|
219
|
+
const versionList = Array.isArray(versions) ? versions : [versions];
|
|
220
|
+
|
|
221
|
+
versionList.forEach((version) => {
|
|
222
|
+
const versionPrefix = version.startsWith("v") ? version : `v${version}`;
|
|
223
|
+
this.group(
|
|
224
|
+
`/${versionPrefix}`,
|
|
225
|
+
(versionRouter) => {
|
|
226
|
+
// Store version metadata
|
|
227
|
+
versionRouter._version = version;
|
|
228
|
+
versionRouter._versionPrefix = versionPrefix;
|
|
229
|
+
|
|
230
|
+
// Add version to context
|
|
231
|
+
versionRouter.use((ctx, next) => {
|
|
232
|
+
ctx.version = version;
|
|
233
|
+
ctx.versionPrefix = versionPrefix;
|
|
234
|
+
return next();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
callback(versionRouter, version);
|
|
238
|
+
},
|
|
239
|
+
versionOptions,
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Create RESTful resource routes with query parameter support
|
|
248
|
+
* @param {string} resource - Resource name (plural)
|
|
249
|
+
* @param {Object} handlers - Resource handlers
|
|
250
|
+
* @param {Array} middleware - Resource middleware
|
|
251
|
+
* @param {Object} options - Resource options
|
|
252
|
+
* @returns {Router} Chainable router instance
|
|
253
|
+
*/
|
|
254
|
+
resource(resource, handlers, middleware = [], options = {}) {
|
|
255
|
+
const basePath = `/${resource}`;
|
|
256
|
+
const idPath = `/${resource}/:id`;
|
|
257
|
+
|
|
258
|
+
// Index route with query parameter support
|
|
259
|
+
if (handlers.index) {
|
|
260
|
+
this.get(basePath, handlers.index, ...middleware, {
|
|
261
|
+
...options,
|
|
262
|
+
queryParams: ["page", "limit", "sort", "order"],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Create route
|
|
267
|
+
if (handlers.create) {
|
|
268
|
+
this.post(basePath, handlers.create, ...middleware, options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Show route with query parameter support
|
|
272
|
+
if (handlers.show) {
|
|
273
|
+
this.get(idPath, handlers.show, ...middleware, {
|
|
274
|
+
...options,
|
|
275
|
+
queryParams: ["fields", "expand"],
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Update routes
|
|
280
|
+
if (handlers.update) {
|
|
281
|
+
this.put(idPath, handlers.update, ...middleware, options);
|
|
282
|
+
this.patch(idPath, handlers.update, ...middleware, options);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Destroy route
|
|
286
|
+
if (handlers.destroy) {
|
|
287
|
+
this.delete(idPath, handlers.destroy, ...middleware, options);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return this;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Factory method to add middleware or mount sub-routers
|
|
295
|
+
* Enhanced to support query parameter middleware
|
|
296
|
+
* @param {...Function|Router} args - Middleware functions or router instances
|
|
297
|
+
* @returns {Router} Chainable router instance
|
|
298
|
+
*/
|
|
299
|
+
use(...args) {
|
|
300
|
+
if (args.length === 1) {
|
|
301
|
+
const arg = args;
|
|
302
|
+
|
|
303
|
+
if (typeof arg === "function") {
|
|
304
|
+
// Add middleware function
|
|
305
|
+
this.middleware.push(arg);
|
|
306
|
+
} else if (arg instanceof Router) {
|
|
307
|
+
// Mount sub-router
|
|
308
|
+
this._mergeRouter(arg);
|
|
309
|
+
}
|
|
310
|
+
} else if (args.length === 2) {
|
|
311
|
+
const [path, router] = args;
|
|
312
|
+
|
|
313
|
+
if (typeof path === "string" && router instanceof Router) {
|
|
314
|
+
// Mount sub-router with path prefix
|
|
315
|
+
this._mergeRouter(router, path);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Add query parameter parsing middleware
|
|
324
|
+
* Automatically parses query parameters into ctx.query
|
|
325
|
+
* @returns {Router} Chainable router instance
|
|
326
|
+
*/
|
|
327
|
+
useQueryParser() {
|
|
328
|
+
this.middleware.push((ctx, next) => {
|
|
329
|
+
if (ctx.url && ctx.url.includes("?")) {
|
|
330
|
+
const [path, queryString] = ctx.url.split("?");
|
|
331
|
+
ctx.path = path;
|
|
332
|
+
ctx.query = this._parseQueryString(queryString);
|
|
333
|
+
ctx.originalUrl = ctx.url;
|
|
334
|
+
} else {
|
|
335
|
+
ctx.query = {};
|
|
336
|
+
}
|
|
337
|
+
return next();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return this;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Enhanced route matching with query parameter support
|
|
345
|
+
* @param {string} method - HTTP method
|
|
346
|
+
* @param {string} url - Request URL (may include query string)
|
|
347
|
+
* @returns {Object|null} Match result or null
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
match(method, url) {
|
|
351
|
+
// Separate path and query string for caching
|
|
352
|
+
const [path, queryString] = url.split("?");
|
|
353
|
+
const cacheKey = `${method}:${url}`;
|
|
354
|
+
|
|
355
|
+
// 1. Check cache first (performance optimization)
|
|
356
|
+
if (this._routeCache.has(cacheKey)) {
|
|
357
|
+
this._cacheHits++;
|
|
358
|
+
return this._routeCache.get(cacheKey);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this._cacheMisses++;
|
|
362
|
+
|
|
363
|
+
// 2. Look for method-specific routes
|
|
364
|
+
const methodRoutes = this._routesByMethod[method.toUpperCase()] || [];
|
|
365
|
+
for (const route of methodRoutes) {
|
|
366
|
+
const match = route.match(method, url);
|
|
367
|
+
if (match) {
|
|
368
|
+
// Parse query parameters if autoParseQuery is enabled
|
|
369
|
+
if (this.options.autoParseQuery && queryString) {
|
|
370
|
+
match.query = this._parseQueryString(queryString);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this._cacheResult(cacheKey, match);
|
|
374
|
+
return match;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 3. Look for ALL method routes
|
|
379
|
+
const allRoutes = this._routesByMethod.ALL || [];
|
|
380
|
+
for (const route of allRoutes) {
|
|
381
|
+
const match = route.match(method, url);
|
|
382
|
+
if (match) {
|
|
383
|
+
// Parse query parameters if autoParseQuery is enabled
|
|
384
|
+
if (this.options.autoParseQuery && queryString) {
|
|
385
|
+
match.query = this._parseQueryString(queryString);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this._cacheResult(cacheKey, match);
|
|
389
|
+
return match;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Parse query string into object
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
_parseQueryString(queryString) {
|
|
401
|
+
// Check cache first
|
|
402
|
+
if (this._queryParserCache.has(queryString)) {
|
|
403
|
+
return this._queryParserCache.get(queryString);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const params = new URLSearchParams(queryString);
|
|
407
|
+
const result = {};
|
|
408
|
+
|
|
409
|
+
for (const [key, value] of params) {
|
|
410
|
+
// Handle array parameters (e.g., ?tags=js&tags=node)
|
|
411
|
+
if (key.endsWith("[]")) {
|
|
412
|
+
const cleanKey = key.slice(0, -2);
|
|
413
|
+
if (!result[cleanKey]) {
|
|
414
|
+
result[cleanKey] = [];
|
|
415
|
+
}
|
|
416
|
+
result[cleanKey].push(value);
|
|
417
|
+
} else {
|
|
418
|
+
// Handle duplicate keys by using the last value
|
|
419
|
+
result[key] = value;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Cache the result
|
|
424
|
+
this._queryParserCache.set(queryString, result);
|
|
425
|
+
|
|
426
|
+
// Limit cache size
|
|
427
|
+
if (this._queryParserCache.size > 100) {
|
|
428
|
+
const firstKey = this._queryParserCache.keys().next().value;
|
|
429
|
+
this._queryParserCache.delete(firstKey);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Internal method to merge another router into this one
|
|
437
|
+
* Enhanced to preserve query parameter settings
|
|
438
|
+
* @private
|
|
439
|
+
*/
|
|
440
|
+
_mergeRouter(router, prefix = "") {
|
|
441
|
+
Object.keys(router._routesByMethod).forEach((method) => {
|
|
442
|
+
router._routesByMethod[method].forEach((route) => {
|
|
443
|
+
// Clone route with updated path
|
|
444
|
+
const clonedRoute = createRoute(
|
|
445
|
+
route.method,
|
|
446
|
+
this.options.prefix +
|
|
447
|
+
prefix +
|
|
448
|
+
route.path.replace(router.options.prefix, ""),
|
|
449
|
+
route.handler,
|
|
450
|
+
[...route.middleware],
|
|
451
|
+
{ ...route.options },
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
this._routesByMethod[method].push(clonedRoute);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Merge middleware from subgroup
|
|
459
|
+
this.middleware.push(...router.middleware);
|
|
460
|
+
|
|
461
|
+
// Merge version information if present
|
|
462
|
+
if (router._version) {
|
|
463
|
+
this._versions.set(router._version, {
|
|
464
|
+
prefix: router._versionPrefix,
|
|
465
|
+
router,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Cache management with LRU eviction
|
|
472
|
+
* Enhanced to handle query parameter caching
|
|
473
|
+
* @private
|
|
474
|
+
*/
|
|
475
|
+
_cacheResult(key, value) {
|
|
476
|
+
if (this._routeCache.size >= this._cacheSize) {
|
|
477
|
+
// LRU eviction - remove first entry
|
|
478
|
+
const firstKey = this._routeCache.keys().next().value;
|
|
479
|
+
this._routeCache.delete(firstKey);
|
|
480
|
+
}
|
|
481
|
+
this._routeCache.set(key, value);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Factory method to create AetherJS-compatible middleware
|
|
486
|
+
* Enhanced with query parameter support
|
|
487
|
+
* @returns {Function} Middleware function for AetherJS pipeline
|
|
488
|
+
*/
|
|
489
|
+
routes() {
|
|
490
|
+
const router = this;
|
|
491
|
+
|
|
492
|
+
return async function routerMiddleware(ctx, next) {
|
|
493
|
+
const match = router.match(ctx.method, ctx.url);
|
|
494
|
+
|
|
495
|
+
if (match) {
|
|
496
|
+
// Set route parameters on context
|
|
497
|
+
ctx.params = match.params || {};
|
|
498
|
+
|
|
499
|
+
// Set query parameters if available
|
|
500
|
+
if (match.query) {
|
|
501
|
+
ctx.query = match.query;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Set route reference
|
|
505
|
+
ctx.route = match.route;
|
|
506
|
+
|
|
507
|
+
// Combine router middleware with route-specific middleware
|
|
508
|
+
const middlewareChain = [
|
|
509
|
+
...router.middleware,
|
|
510
|
+
...match.route.middleware,
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
// Execute middleware chain
|
|
514
|
+
let index = -1;
|
|
515
|
+
|
|
516
|
+
async function dispatch(i) {
|
|
517
|
+
if (i <= index) {
|
|
518
|
+
throw new Error("next() called multiple times");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
index = i;
|
|
522
|
+
let fn = middlewareChain[i];
|
|
523
|
+
|
|
524
|
+
if (i === middlewareChain.length) {
|
|
525
|
+
fn = match.route.handler;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!fn) return Promise.resolve();
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
|
|
532
|
+
} catch (err) {
|
|
533
|
+
return Promise.reject(err);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
await dispatch(0);
|
|
538
|
+
} else {
|
|
539
|
+
// No route matched, continue to next middleware
|
|
540
|
+
if (typeof next === "function") {
|
|
541
|
+
await next();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Get all registered routes for debugging
|
|
549
|
+
* Enhanced to show query parameter support
|
|
550
|
+
* @returns {Array} Array of route information
|
|
551
|
+
*/
|
|
552
|
+
getRoutes() {
|
|
553
|
+
const routes = [];
|
|
554
|
+
|
|
555
|
+
Object.keys(this._routesByMethod).forEach((method) => {
|
|
556
|
+
this._routesByMethod[method].forEach((route) => {
|
|
557
|
+
routes.push({
|
|
558
|
+
method: route.method,
|
|
559
|
+
path: route.path,
|
|
560
|
+
hasQueryParams: route.hasQueryParams ? route.hasQueryParams() : false,
|
|
561
|
+
queryParamNames: route.getQueryParamNames
|
|
562
|
+
? route.getQueryParamNames()
|
|
563
|
+
: [],
|
|
564
|
+
pathParamNames: route.getPathParamNames
|
|
565
|
+
? route.getPathParamNames()
|
|
566
|
+
: [],
|
|
567
|
+
middlewareCount: route.middleware.length,
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
return routes;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get routes by version
|
|
577
|
+
* @param {string} version - Version identifier
|
|
578
|
+
* @returns {Array} Array of route information for the version
|
|
579
|
+
*/
|
|
580
|
+
getRoutesByVersion(version) {
|
|
581
|
+
const versionPrefix = version.startsWith("v") ? version : `v${version}`;
|
|
582
|
+
const prefix = `/${versionPrefix}`;
|
|
583
|
+
|
|
584
|
+
return this.getRoutes().filter((route) => route.path.startsWith(prefix));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Clear route matching cache
|
|
589
|
+
* Also clears query parser cache
|
|
590
|
+
*/
|
|
591
|
+
clearCache() {
|
|
592
|
+
this._routeCache.clear();
|
|
593
|
+
this._queryParserCache.clear();
|
|
594
|
+
this._cacheHits = 0;
|
|
595
|
+
this._cacheMisses = 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Get router statistics
|
|
600
|
+
* Enhanced with query parameter statistics
|
|
601
|
+
* @returns {Object} Router statistics
|
|
602
|
+
*/
|
|
603
|
+
getStats() {
|
|
604
|
+
let totalRoutes = 0;
|
|
605
|
+
let routesWithQueryParams = 0;
|
|
606
|
+
|
|
607
|
+
Object.keys(this._routesByMethod).forEach((method) => {
|
|
608
|
+
this._routesByMethod[method].forEach((route) => {
|
|
609
|
+
totalRoutes++;
|
|
610
|
+
if (route.hasQueryParams && route.hasQueryParams()) {
|
|
611
|
+
routesWithQueryParams++;
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
totalRoutes,
|
|
618
|
+
routesWithQueryParams,
|
|
619
|
+
cacheSize: this._routeCache.size,
|
|
620
|
+
cacheHits: this._cacheHits,
|
|
621
|
+
cacheMisses: this._cacheMisses,
|
|
622
|
+
cacheHitRate:
|
|
623
|
+
this._cacheHits + this._cacheMisses > 0
|
|
624
|
+
? (this._cacheHits / (this._cacheHits + this._cacheMisses)) * 100
|
|
625
|
+
: 0,
|
|
626
|
+
queryParserCacheSize: this._queryParserCache.size,
|
|
627
|
+
versions: Array.from(this._versions.keys()),
|
|
628
|
+
methods: Object.keys(this._routesByMethod).reduce((acc, method) => {
|
|
629
|
+
acc[method] = this._routesByMethod[method].length;
|
|
630
|
+
return acc;
|
|
631
|
+
}, {}),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Enable event system for router events
|
|
637
|
+
* @returns {Router} Chainable router instance
|
|
638
|
+
*/
|
|
639
|
+
enableEvents() {
|
|
640
|
+
this._events = new Map();
|
|
641
|
+
return this;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Add event listener
|
|
646
|
+
* @param {string} event - Event name
|
|
647
|
+
* @param {Function} listener - Event listener
|
|
648
|
+
* @returns {Router} Chainable router instance
|
|
649
|
+
*/
|
|
650
|
+
on(event, listener) {
|
|
651
|
+
if (!this._events) {
|
|
652
|
+
this._events = new Map();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (!this._events.has(event)) {
|
|
656
|
+
this._events.set(event, []);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
this._events.get(event).push(listener);
|
|
660
|
+
return this;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Remove event listener
|
|
665
|
+
* @param {string} event - Event name
|
|
666
|
+
* @param {Function} listener - Event listener
|
|
667
|
+
* @returns {Router} Chainable router instance
|
|
668
|
+
*/
|
|
669
|
+
off(event, listener) {
|
|
670
|
+
if (this._events && this._events.has(event)) {
|
|
671
|
+
const listeners = this._events.get(event);
|
|
672
|
+
const index = listeners.indexOf(listener);
|
|
673
|
+
if (index > -1) {
|
|
674
|
+
listeners.splice(index, 1);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return this;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Emit event
|
|
682
|
+
* @private
|
|
683
|
+
*/
|
|
684
|
+
_emit(event, data) {
|
|
685
|
+
if (this._events && this._events.has(event)) {
|
|
686
|
+
const listeners = this._events.get(event);
|
|
687
|
+
listeners.forEach((listener) => {
|
|
688
|
+
try {
|
|
689
|
+
listener(data);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
console.error(`Error in event listener for ${event}:`, err);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Factory function to create a router from route definitions
|
|
700
|
+
* @param {Array} routes - Array of route definitions
|
|
701
|
+
* @param {Object} options - Router options
|
|
702
|
+
* @returns {Router} Configured router instance
|
|
703
|
+
*/
|
|
704
|
+
export function createRouterFromRoutes(routes, options = {}) {
|
|
705
|
+
const router = createRouter(options);
|
|
706
|
+
|
|
707
|
+
routes.forEach((route) => {
|
|
708
|
+
const {
|
|
709
|
+
method = "GET",
|
|
710
|
+
path,
|
|
711
|
+
handler,
|
|
712
|
+
middleware = [],
|
|
713
|
+
routeOptions = {},
|
|
714
|
+
} = route;
|
|
715
|
+
|
|
716
|
+
router.addRoute(method, path, handler, middleware, routeOptions);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
return router;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Factory function to create a RESTful resource router
|
|
724
|
+
* @param {string} resource - Resource name
|
|
725
|
+
* @param {Object} handlers - Resource handlers
|
|
726
|
+
* @param {Array} middleware - Resource middleware
|
|
727
|
+
* @param {Object} options - Router options
|
|
728
|
+
* @returns {Router} RESTful resource router
|
|
729
|
+
*/
|
|
730
|
+
export function createResourceRouter(
|
|
731
|
+
resource,
|
|
732
|
+
handlers,
|
|
733
|
+
middleware = [],
|
|
734
|
+
options = {},
|
|
735
|
+
) {
|
|
736
|
+
const router = createRouter({
|
|
737
|
+
...options,
|
|
738
|
+
prefix: `/${resource}`,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Add RESTful routes with query parameter support
|
|
742
|
+
if (handlers.index) {
|
|
743
|
+
router.get("/", handlers.index, ...middleware, {
|
|
744
|
+
queryParams: ["page", "limit", "sort", "order", "filter"],
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (handlers.create) {
|
|
749
|
+
router.post("/", handlers.create, ...middleware);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (handlers.show) {
|
|
753
|
+
router.get("/:id", handlers.show, ...middleware, {
|
|
754
|
+
queryParams: ["fields", "expand", "include"],
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (handlers.update) {
|
|
759
|
+
router.put("/:id", handlers.update, ...middleware);
|
|
760
|
+
router.patch("/:id", handlers.update, ...middleware);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (handlers.destroy) {
|
|
764
|
+
router.delete("/:id", handlers.destroy, ...middleware);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Add nested routes if provided
|
|
768
|
+
if (handlers.nested) {
|
|
769
|
+
Object.keys(handlers.nested).forEach((nestedResource) => {
|
|
770
|
+
router.group(`/:id/${nestedResource}`, (nestedRouter) => {
|
|
771
|
+
const nestedHandlers = handlers.nested[nestedResource];
|
|
772
|
+
|
|
773
|
+
if (nestedHandlers.index) {
|
|
774
|
+
nestedRouter.get("/", nestedHandlers.index, ...middleware);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (nestedHandlers.create) {
|
|
778
|
+
nestedRouter.post("/", nestedHandlers.create, ...middleware);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (nestedHandlers.show) {
|
|
782
|
+
nestedRouter.get("/:nestedId", nestedHandlers.show, ...middleware);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return router;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Factory function to create a versioned API router
|
|
793
|
+
* @param {Object} versions - Version configuration
|
|
794
|
+
* @param {Object} options - Router options
|
|
795
|
+
* @returns {Router} Versioned API router
|
|
796
|
+
*/
|
|
797
|
+
export function createVersionedRouter(versions, options = {}) {
|
|
798
|
+
const router = createRouter(options);
|
|
799
|
+
|
|
800
|
+
Object.keys(versions).forEach((version) => {
|
|
801
|
+
router.group(`/v${version}`, (versionRouter) => {
|
|
802
|
+
const versionConfig = versions[version];
|
|
803
|
+
|
|
804
|
+
// Add version-specific middleware
|
|
805
|
+
if (versionConfig.middleware) {
|
|
806
|
+
versionRouter.use(...versionConfig.middleware);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Add version-specific routes
|
|
810
|
+
if (versionConfig.routes) {
|
|
811
|
+
versionConfig.routes.forEach((route) => {
|
|
812
|
+
versionRouter.addRoute(
|
|
813
|
+
route.method || "GET",
|
|
814
|
+
route.path,
|
|
815
|
+
route.handler,
|
|
816
|
+
route.middleware || [],
|
|
817
|
+
route.options || {},
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Add version-specific resources
|
|
823
|
+
if (versionConfig.resources) {
|
|
824
|
+
Object.keys(versionConfig.resources).forEach((resource) => {
|
|
825
|
+
const resourceConfig = versionConfig.resources[resource];
|
|
826
|
+
versionRouter.resource(
|
|
827
|
+
resource,
|
|
828
|
+
resourceConfig.handlers,
|
|
829
|
+
resourceConfig.middleware || [],
|
|
830
|
+
);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
return router;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Export class for type checking
|
|
840
|
+
export { Router };
|