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,326 @@
|
|
|
1
|
+
// src/route-factory.js - Enhanced route factory with query parameter support
|
|
2
|
+
import { createPathCompiler } from './path-compiler.js';
|
|
3
|
+
|
|
4
|
+
// Shared path compiler instance for performance
|
|
5
|
+
const pathCompiler = createPathCompiler();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Factory function to create a new Route instance
|
|
9
|
+
* @param {string} method - HTTP method
|
|
10
|
+
* @param {string} path - Route path pattern (may include query parameters)
|
|
11
|
+
* @param {Function} handler - Route handler function
|
|
12
|
+
* @param {Array<Function>} middleware - Route-specific middleware
|
|
13
|
+
* @param {Object} options - Route options
|
|
14
|
+
* @returns {Route} Configured route instance
|
|
15
|
+
*/
|
|
16
|
+
export function createRoute(method, path, handler, middleware = [], options = {}) {
|
|
17
|
+
return new Route(method, path, handler, middleware, options);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Individual route class with path matching capabilities
|
|
22
|
+
* Now supports query parameter patterns
|
|
23
|
+
*/
|
|
24
|
+
class Route {
|
|
25
|
+
constructor(method, path, handler, middleware = [], options = {}) {
|
|
26
|
+
this.method = method.toUpperCase();
|
|
27
|
+
this.path = path;
|
|
28
|
+
this.handler = handler;
|
|
29
|
+
this.middleware = Array.isArray(middleware) ? middleware : [middleware];
|
|
30
|
+
this.options = {
|
|
31
|
+
parseQuery: false,
|
|
32
|
+
...options
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Check if path contains query parameters
|
|
36
|
+
const hasQuery = path.includes('?');
|
|
37
|
+
|
|
38
|
+
// Compile path to regex for fast matching
|
|
39
|
+
const compileOptions = {
|
|
40
|
+
...this.options,
|
|
41
|
+
parseQuery: hasQuery
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const compiled = pathCompiler.compile(path, compileOptions);
|
|
45
|
+
this.regex = compiled.regex;
|
|
46
|
+
this.keys = compiled.keys;
|
|
47
|
+
this.hasQuery = compiled.hasQuery || false;
|
|
48
|
+
this.queryPattern = compiled.queryPattern || null;
|
|
49
|
+
|
|
50
|
+
// Store original path parts for query parameter matching
|
|
51
|
+
if (hasQuery) {
|
|
52
|
+
const [pathPart, queryPart] = path.split('?');
|
|
53
|
+
this.pathPart = pathPart;
|
|
54
|
+
this.queryPart = queryPart;
|
|
55
|
+
} else {
|
|
56
|
+
this.pathPart = path;
|
|
57
|
+
this.queryPart = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Match this route against a request
|
|
63
|
+
* Now supports query parameter matching
|
|
64
|
+
* @param {string} method - HTTP method
|
|
65
|
+
* @param {string} url - Request URL (may include query string)
|
|
66
|
+
* @returns {Object|null} Match result or null
|
|
67
|
+
*/
|
|
68
|
+
match(method, url) {
|
|
69
|
+
// Check HTTP method
|
|
70
|
+
if (this.method !== 'ALL' && this.method !== method.toUpperCase()) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Split URL into path and query parts
|
|
75
|
+
const [path, queryString] = url.split('?');
|
|
76
|
+
|
|
77
|
+
// Match path against compiled regex
|
|
78
|
+
const pathMatch = this.regex.exec(path);
|
|
79
|
+
if (!pathMatch) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Extract path parameters
|
|
84
|
+
const params = {};
|
|
85
|
+
for (let i = 0; i < this.keys.length; i++) {
|
|
86
|
+
const key = this.keys[i];
|
|
87
|
+
if (key.type === 'path' && pathMatch[i + 1] !== undefined) {
|
|
88
|
+
params[key.name] = decodeURIComponent(pathMatch[i + 1]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract query parameters if route has query pattern
|
|
93
|
+
if (this.hasQuery && queryString) {
|
|
94
|
+
const queryParams = new URLSearchParams(queryString);
|
|
95
|
+
|
|
96
|
+
// Parse query pattern to extract parameter names
|
|
97
|
+
if (this.queryPattern) {
|
|
98
|
+
const patternParams = new URLSearchParams(this.queryPattern);
|
|
99
|
+
|
|
100
|
+
for (const [key, value] of patternParams) {
|
|
101
|
+
if (value.startsWith(':')) {
|
|
102
|
+
const paramName = value.slice(1).replace(/\?$/, '');
|
|
103
|
+
const isOptional = value.endsWith('?');
|
|
104
|
+
|
|
105
|
+
if (queryParams.has(key)) {
|
|
106
|
+
params[paramName] = queryParams.get(key);
|
|
107
|
+
} else if (!isOptional) {
|
|
108
|
+
// Required query parameter missing
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Static query parameter value must match exactly
|
|
113
|
+
if (!queryParams.has(key) || queryParams.get(key) !== value) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add all query parameters to context (optional)
|
|
121
|
+
if (this.options.includeAllQueryParams) {
|
|
122
|
+
for (const [key, value] of queryParams) {
|
|
123
|
+
if (!params[key]) { // Don't override path parameters
|
|
124
|
+
params[key] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
handler: this.handler,
|
|
132
|
+
params,
|
|
133
|
+
route: this,
|
|
134
|
+
queryString: queryString || null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if this route matches a URL with query parameters
|
|
140
|
+
* @param {string} method - HTTP method
|
|
141
|
+
* @param {string} url - Full URL with query string
|
|
142
|
+
* @returns {boolean} Whether the route matches
|
|
143
|
+
*/
|
|
144
|
+
matches(method, url) {
|
|
145
|
+
return this.match(method, url) !== null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Add middleware to this specific route
|
|
150
|
+
* @param {...Function} middleware - Middleware functions
|
|
151
|
+
* @returns {Route} Chainable route instance
|
|
152
|
+
*/
|
|
153
|
+
use(...middleware) {
|
|
154
|
+
this.middleware.push(...middleware);
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Clone the route with new options
|
|
160
|
+
* @param {Object} options - Options to override
|
|
161
|
+
* @returns {Route} Cloned route instance
|
|
162
|
+
*/
|
|
163
|
+
clone(options = {}) {
|
|
164
|
+
return new Route(
|
|
165
|
+
this.method,
|
|
166
|
+
this.path,
|
|
167
|
+
this.handler,
|
|
168
|
+
[...this.middleware],
|
|
169
|
+
{ ...this.options, ...options }
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get route information for debugging
|
|
175
|
+
* @returns {Object} Route metadata
|
|
176
|
+
*/
|
|
177
|
+
toJSON() {
|
|
178
|
+
return {
|
|
179
|
+
method: this.method,
|
|
180
|
+
path: this.path,
|
|
181
|
+
hasQuery: this.hasQuery,
|
|
182
|
+
queryPattern: this.queryPattern,
|
|
183
|
+
middlewareCount: this.middleware.length,
|
|
184
|
+
pattern: this.regex.toString(),
|
|
185
|
+
keys: this.keys.map(key => ({
|
|
186
|
+
name: key.name,
|
|
187
|
+
type: key.type || 'path',
|
|
188
|
+
optional: key.optional || false,
|
|
189
|
+
pattern: key.pattern || '[^\\/]+'
|
|
190
|
+
}))
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check if route has query parameters
|
|
196
|
+
* @returns {boolean} Whether route has query parameters
|
|
197
|
+
*/
|
|
198
|
+
hasQueryParams() {
|
|
199
|
+
return this.hasQuery;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get query parameter names
|
|
204
|
+
* @returns {Array<string>} Query parameter names
|
|
205
|
+
*/
|
|
206
|
+
getQueryParamNames() {
|
|
207
|
+
if (!this.hasQuery) return [];
|
|
208
|
+
|
|
209
|
+
const params = [];
|
|
210
|
+
this.keys.forEach(key => {
|
|
211
|
+
if (key.type === 'query') {
|
|
212
|
+
params.push(key.name);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return params;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get path parameter names
|
|
221
|
+
* @returns {Array<string>} Path parameter names
|
|
222
|
+
*/
|
|
223
|
+
getPathParamNames() {
|
|
224
|
+
const params = [];
|
|
225
|
+
this.keys.forEach(key => {
|
|
226
|
+
if (key.type === 'path') {
|
|
227
|
+
params.push(key.name);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return params;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Factory function to create a route from configuration object
|
|
237
|
+
* @param {Object} config - Route configuration
|
|
238
|
+
* @param {string} config.method - HTTP method
|
|
239
|
+
* @param {string} config.path - Route path (may include query parameters)
|
|
240
|
+
* @param {Function} config.handler - Route handler
|
|
241
|
+
* @param {Array<Function>} config.middleware - Route middleware
|
|
242
|
+
* @param {Object} config.options - Route options
|
|
243
|
+
* @returns {Route} Created route instance
|
|
244
|
+
*/
|
|
245
|
+
export function createRouteFromConfig(config) {
|
|
246
|
+
const {
|
|
247
|
+
method = 'GET',
|
|
248
|
+
path,
|
|
249
|
+
handler,
|
|
250
|
+
middleware = [],
|
|
251
|
+
options = {}
|
|
252
|
+
} = config;
|
|
253
|
+
|
|
254
|
+
return createRoute(method, path, handler, middleware, options);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Factory function to create multiple routes from array
|
|
259
|
+
* @param {Array<Object>} configs - Array of route configurations
|
|
260
|
+
* @returns {Array<Route>} Array of route instances
|
|
261
|
+
*/
|
|
262
|
+
export function createRoutesFromConfigs(configs) {
|
|
263
|
+
return configs.map(config => createRouteFromConfig(config));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Factory function to create RESTful resource routes with query parameter support
|
|
268
|
+
* @param {string} basePath - Base path for resource
|
|
269
|
+
* @param {Object} handlers - Resource handlers
|
|
270
|
+
* @param {Array<Function>} middleware - Resource middleware
|
|
271
|
+
* @param {Object} options - Route options
|
|
272
|
+
* @returns {Array<Route>} Array of RESTful routes
|
|
273
|
+
*/
|
|
274
|
+
export function createResourceRoutes(basePath, handlers, middleware = [], options = {}) {
|
|
275
|
+
const routes = [];
|
|
276
|
+
|
|
277
|
+
if (handlers.index) {
|
|
278
|
+
// GET /resource?page=:page&limit=:limit
|
|
279
|
+
routes.push(createRoute('GET', `${basePath}?page=:page?&limit=:limit?`, handlers.index, middleware, options));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (handlers.create) {
|
|
283
|
+
routes.push(createRoute('POST', basePath, handlers.create, middleware, options));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (handlers.show) {
|
|
287
|
+
// GET /resource/:id?fields=:fields?
|
|
288
|
+
routes.push(createRoute('GET', `${basePath}/:id?fields=:fields?`, handlers.show, middleware, options));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (handlers.update) {
|
|
292
|
+
routes.push(createRoute('PUT', `${basePath}/:id`, handlers.update, middleware, options));
|
|
293
|
+
routes.push(createRoute('PATCH', `${basePath}/:id`, handlers.update, middleware, options));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (handlers.destroy) {
|
|
297
|
+
routes.push(createRoute('DELETE', `${basePath}/:id`, handlers.destroy, middleware, options));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return routes;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Factory function to create route group with query parameter support
|
|
305
|
+
* @param {string} prefix - Route prefix
|
|
306
|
+
* @param {Array<Route>} routes - Routes to group
|
|
307
|
+
* @param {Object} options - Group options
|
|
308
|
+
* @returns {Array<Route>} Prefixed routes
|
|
309
|
+
*/
|
|
310
|
+
export function createRouteGroup(prefix, routes, options = {}) {
|
|
311
|
+
return routes.map(route => {
|
|
312
|
+
const prefixedPath = prefix + (route.pathPart.startsWith('/') ? route.pathPart : '/' + route.pathPart);
|
|
313
|
+
const fullPath = route.queryPart ? `${prefixedPath}?${route.queryPart}` : prefixedPath;
|
|
314
|
+
|
|
315
|
+
return createRoute(
|
|
316
|
+
route.method,
|
|
317
|
+
fullPath,
|
|
318
|
+
route.handler,
|
|
319
|
+
[...route.middleware],
|
|
320
|
+
{ ...route.options, ...options }
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Export class for type checking
|
|
326
|
+
export { Route };
|