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.
@@ -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 };