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,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 };