voltjs-framework 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/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Template Renderer
|
|
3
|
+
*
|
|
4
|
+
* Built-in template engine with:
|
|
5
|
+
* - Mustache-like syntax {{ variable }}
|
|
6
|
+
* - Conditionals {#if} {#else} {/if}
|
|
7
|
+
* - Loops {#each items as item} {/each}
|
|
8
|
+
* - Partials {> partial}
|
|
9
|
+
* - Layouts {#layout "base"} {/layout}
|
|
10
|
+
* - Auto-escaping (XSS safe by default)
|
|
11
|
+
* - Raw output {{{ raw }}}
|
|
12
|
+
* - Slots {#slot "name"} {/slot}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
class Renderer {
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this._viewsDir = config ? config.get('views.dir', 'views') : 'views';
|
|
23
|
+
this._cache = new Map();
|
|
24
|
+
this._cacheEnabled = config ? config.get('views.cache', false) : false;
|
|
25
|
+
this._helpers = new Map();
|
|
26
|
+
this._layouts = new Map();
|
|
27
|
+
this._partials = new Map();
|
|
28
|
+
|
|
29
|
+
// Register default helpers
|
|
30
|
+
this._registerDefaultHelpers();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setViewsDir(dir) {
|
|
34
|
+
this._viewsDir = dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Register a template helper */
|
|
38
|
+
helper(name, fn) {
|
|
39
|
+
this._helpers.set(name, fn);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Register a layout */
|
|
44
|
+
layout(name, template) {
|
|
45
|
+
this._layouts.set(name, template);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Register a partial */
|
|
50
|
+
partial(name, template) {
|
|
51
|
+
this._partials.set(name, template);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Render a template string or file */
|
|
56
|
+
render(templateOrPath, data = {}) {
|
|
57
|
+
let template;
|
|
58
|
+
|
|
59
|
+
// Check if it's a file path
|
|
60
|
+
if (!templateOrPath.includes('{{') && !templateOrPath.includes('{#')) {
|
|
61
|
+
template = this._loadTemplate(templateOrPath);
|
|
62
|
+
} else {
|
|
63
|
+
template = templateOrPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Add helpers to data
|
|
67
|
+
const context = {
|
|
68
|
+
...data,
|
|
69
|
+
_helpers: Object.fromEntries(this._helpers),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Process the template
|
|
73
|
+
let result = this._process(template, context);
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Load template from file */
|
|
79
|
+
_loadTemplate(name) {
|
|
80
|
+
if (this._cacheEnabled && this._cache.has(name)) {
|
|
81
|
+
return this._cache.get(name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const extensions = ['.volt', '.html', '.htm', '.vt'];
|
|
85
|
+
const viewsDir = path.resolve(this._viewsDir);
|
|
86
|
+
|
|
87
|
+
for (const ext of extensions) {
|
|
88
|
+
const filePath = path.join(viewsDir, name + ext);
|
|
89
|
+
try {
|
|
90
|
+
if (fs.existsSync(filePath)) {
|
|
91
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
92
|
+
if (this._cacheEnabled) {
|
|
93
|
+
this._cache.set(name, content);
|
|
94
|
+
}
|
|
95
|
+
return content;
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Try without extension
|
|
101
|
+
const directPath = path.join(viewsDir, name);
|
|
102
|
+
try {
|
|
103
|
+
if (fs.existsSync(directPath)) {
|
|
104
|
+
return fs.readFileSync(directPath, 'utf-8');
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
|
|
108
|
+
// If no file found, treat the input as a template string
|
|
109
|
+
return `<!DOCTYPE html>
|
|
110
|
+
<html><head><title>VoltJS</title>
|
|
111
|
+
<style>
|
|
112
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 40px; background: #0a0a0a; color: #e0e0e0; }
|
|
113
|
+
.container { max-width: 800px; margin: 0 auto; }
|
|
114
|
+
h1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
115
|
+
</style></head>
|
|
116
|
+
<body><div class="container"><h1>${name}</h1></div></body></html>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Process template with data */
|
|
120
|
+
_process(template, data) {
|
|
121
|
+
let result = template;
|
|
122
|
+
|
|
123
|
+
// Process layouts: {#layout "name"} content {/layout}
|
|
124
|
+
result = this._processLayouts(result, data);
|
|
125
|
+
|
|
126
|
+
// Process partials: {> partialName}
|
|
127
|
+
result = this._processPartials(result, data);
|
|
128
|
+
|
|
129
|
+
// Process each loops: {#each items as item} ... {/each}
|
|
130
|
+
result = this._processEach(result, data);
|
|
131
|
+
|
|
132
|
+
// Process conditionals: {#if condition} ... {#else} ... {/if}
|
|
133
|
+
result = this._processIf(result, data);
|
|
134
|
+
|
|
135
|
+
// Process raw output: {{{ variable }}}
|
|
136
|
+
result = result.replace(/\{\{\{\s*(.+?)\s*\}\}\}/g, (_, key) => {
|
|
137
|
+
return this._resolve(key.trim(), data) || '';
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Process escaped output: {{ variable }}
|
|
141
|
+
result = result.replace(/\{\{\s*(.+?)\s*\}\}/g, (_, key) => {
|
|
142
|
+
const value = this._resolve(key.trim(), data);
|
|
143
|
+
return value !== undefined && value !== null ? this._escape(String(value)) : '';
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Process layout directives */
|
|
150
|
+
_processLayouts(template, data) {
|
|
151
|
+
const layoutRegex = /\{#layout\s+"([^"]+)"\s*\}([\s\S]*?)\{\/layout\}/g;
|
|
152
|
+
|
|
153
|
+
return template.replace(layoutRegex, (_, layoutName, content) => {
|
|
154
|
+
let layout = this._layouts.get(layoutName) || this._loadTemplate(`layouts/${layoutName}`);
|
|
155
|
+
|
|
156
|
+
// Replace {#slot "content"} in layout with the content
|
|
157
|
+
layout = layout.replace(/\{#slot\s+"content"\s*\}[\s\S]*?\{\/slot\}/g, content);
|
|
158
|
+
|
|
159
|
+
// Process named slots
|
|
160
|
+
const slotRegex = /\{#slot\s+"(\w+)"\s*\}([\s\S]*?)\{\/slot\}/g;
|
|
161
|
+
layout = layout.replace(slotRegex, (_, slotName, defaultContent) => {
|
|
162
|
+
const slotContentRegex = new RegExp(`\\{#fill\\s+"${slotName}"\\s*\\}([\\s\\S]*?)\\{\\/fill\\}`, 'g');
|
|
163
|
+
const slotMatch = slotContentRegex.exec(content);
|
|
164
|
+
return slotMatch ? slotMatch[1] : defaultContent;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return this._process(layout, data);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Process partial includes */
|
|
172
|
+
_processPartials(template, data) {
|
|
173
|
+
return template.replace(/\{>\s*(\S+)\s*\}/g, (_, partialName) => {
|
|
174
|
+
const partial = this._partials.get(partialName) || this._loadTemplate(`partials/${partialName}`);
|
|
175
|
+
return this._process(partial, data);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Process each loops */
|
|
180
|
+
_processEach(template, data) {
|
|
181
|
+
const eachRegex = /\{#each\s+(\S+)\s+as\s+(\S+)\s*\}([\s\S]*?)\{\/each\}/g;
|
|
182
|
+
|
|
183
|
+
return template.replace(eachRegex, (_, arrayKey, itemName, content) => {
|
|
184
|
+
const array = this._resolve(arrayKey, data);
|
|
185
|
+
if (!Array.isArray(array)) return '';
|
|
186
|
+
|
|
187
|
+
return array.map((item, index) => {
|
|
188
|
+
const itemData = {
|
|
189
|
+
...data,
|
|
190
|
+
[itemName]: item,
|
|
191
|
+
[`${itemName}_index`]: index,
|
|
192
|
+
[`${itemName}_first`]: index === 0,
|
|
193
|
+
[`${itemName}_last`]: index === array.length - 1,
|
|
194
|
+
};
|
|
195
|
+
return this._process(content, itemData);
|
|
196
|
+
}).join('');
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Process conditionals */
|
|
201
|
+
_processIf(template, data) {
|
|
202
|
+
// Handle nested if/else/elseif
|
|
203
|
+
const ifRegex = /\{#if\s+(.+?)\s*\}([\s\S]*?)\{\/if\}/g;
|
|
204
|
+
|
|
205
|
+
return template.replace(ifRegex, (_, condition, content) => {
|
|
206
|
+
// Split by else
|
|
207
|
+
const parts = content.split(/\{#else\s*\}/);
|
|
208
|
+
const ifContent = parts[0];
|
|
209
|
+
const elseContent = parts[1] || '';
|
|
210
|
+
|
|
211
|
+
// Evaluate condition
|
|
212
|
+
const result = this._evaluateCondition(condition, data);
|
|
213
|
+
|
|
214
|
+
const chosen = result ? ifContent : elseContent;
|
|
215
|
+
return this._process(chosen, data);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Evaluate a condition */
|
|
220
|
+
_evaluateCondition(condition, data) {
|
|
221
|
+
// Handle negation
|
|
222
|
+
if (condition.startsWith('!')) {
|
|
223
|
+
return !this._evaluateCondition(condition.slice(1).trim(), data);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Handle comparison operators
|
|
227
|
+
const operators = ['===', '!==', '>=', '<=', '>', '<', '==', '!='];
|
|
228
|
+
for (const op of operators) {
|
|
229
|
+
if (condition.includes(op)) {
|
|
230
|
+
const [left, right] = condition.split(op).map(s => s.trim());
|
|
231
|
+
const leftVal = this._resolve(left, data) ?? left;
|
|
232
|
+
const rightVal = this._resolve(right, data) ?? right;
|
|
233
|
+
|
|
234
|
+
// Remove quotes from string literals
|
|
235
|
+
const cleanLeft = typeof leftVal === 'string' ? leftVal.replace(/^['"]|['"]$/g, '') : leftVal;
|
|
236
|
+
const cleanRight = typeof rightVal === 'string' ? rightVal.replace(/^['"]|['"]$/g, '') : rightVal;
|
|
237
|
+
|
|
238
|
+
switch (op) {
|
|
239
|
+
case '===': case '==': return cleanLeft == cleanRight;
|
|
240
|
+
case '!==': case '!=': return cleanLeft != cleanRight;
|
|
241
|
+
case '>': return cleanLeft > cleanRight;
|
|
242
|
+
case '<': return cleanLeft < cleanRight;
|
|
243
|
+
case '>=': return cleanLeft >= cleanRight;
|
|
244
|
+
case '<=': return cleanLeft <= cleanRight;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle && and ||
|
|
250
|
+
if (condition.includes('&&')) {
|
|
251
|
+
return condition.split('&&').every(c => this._evaluateCondition(c.trim(), data));
|
|
252
|
+
}
|
|
253
|
+
if (condition.includes('||')) {
|
|
254
|
+
return condition.split('||').some(c => this._evaluateCondition(c.trim(), data));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Simple truthiness check
|
|
258
|
+
const value = this._resolve(condition, data);
|
|
259
|
+
return !!value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Resolve a dot-notation key from data */
|
|
263
|
+
_resolve(key, data) {
|
|
264
|
+
// Handle string literals
|
|
265
|
+
if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
|
|
266
|
+
return key.slice(1, -1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Handle numbers
|
|
270
|
+
if (!isNaN(key)) return Number(key);
|
|
271
|
+
|
|
272
|
+
// Handle booleans
|
|
273
|
+
if (key === 'true') return true;
|
|
274
|
+
if (key === 'false') return false;
|
|
275
|
+
if (key === 'null') return null;
|
|
276
|
+
|
|
277
|
+
// Handle helper calls: helperName(arg1, arg2)
|
|
278
|
+
const helperMatch = key.match(/^(\w+)\((.+)\)$/);
|
|
279
|
+
if (helperMatch) {
|
|
280
|
+
const helperName = helperMatch[1];
|
|
281
|
+
const helper = this._helpers.get(helperName);
|
|
282
|
+
if (helper) {
|
|
283
|
+
const args = helperMatch[2].split(',').map(a => {
|
|
284
|
+
const trimmed = a.trim();
|
|
285
|
+
return this._resolve(trimmed, data);
|
|
286
|
+
});
|
|
287
|
+
return helper(...args);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Dot-notation resolution
|
|
292
|
+
const parts = key.split('.');
|
|
293
|
+
let value = data;
|
|
294
|
+
for (const part of parts) {
|
|
295
|
+
if (value === undefined || value === null) return undefined;
|
|
296
|
+
value = value[part];
|
|
297
|
+
}
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** HTML escape (XSS protection) */
|
|
302
|
+
_escape(str) {
|
|
303
|
+
const escapeMap = {
|
|
304
|
+
'&': '&',
|
|
305
|
+
'<': '<',
|
|
306
|
+
'>': '>',
|
|
307
|
+
'"': '"',
|
|
308
|
+
"'": ''',
|
|
309
|
+
'`': '`',
|
|
310
|
+
};
|
|
311
|
+
return str.replace(/[&<>"'`]/g, c => escapeMap[c]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Register default helpers */
|
|
315
|
+
_registerDefaultHelpers() {
|
|
316
|
+
this.helper('uppercase', (str) => String(str).toUpperCase());
|
|
317
|
+
this.helper('lowercase', (str) => String(str).toLowerCase());
|
|
318
|
+
this.helper('capitalize', (str) => String(str).charAt(0).toUpperCase() + String(str).slice(1));
|
|
319
|
+
this.helper('truncate', (str, len) => {
|
|
320
|
+
const s = String(str);
|
|
321
|
+
return s.length > len ? s.substring(0, len) + '...' : s;
|
|
322
|
+
});
|
|
323
|
+
this.helper('date', (d) => new Date(d).toLocaleDateString());
|
|
324
|
+
this.helper('json', (v) => JSON.stringify(v, null, 2));
|
|
325
|
+
this.helper('length', (arr) => Array.isArray(arr) ? arr.length : String(arr).length);
|
|
326
|
+
this.helper('currency', (num, symbol = '$') => `${symbol}${Number(num).toFixed(2)}`);
|
|
327
|
+
this.helper('pluralize', (count, singular, plural) => count === 1 ? singular : (plural || singular + 's'));
|
|
328
|
+
this.helper('default', (val, defaultVal) => val || defaultVal);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Clear template cache */
|
|
332
|
+
clearCache() {
|
|
333
|
+
this._cache.clear();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = { Renderer };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Router
|
|
3
|
+
*
|
|
4
|
+
* High-performance router with support for:
|
|
5
|
+
* - Dynamic parameters (:id)
|
|
6
|
+
* - Wildcards (*)
|
|
7
|
+
* - Route groups with prefixes
|
|
8
|
+
* - Named routes
|
|
9
|
+
* - Route middleware
|
|
10
|
+
* - Optional parameters (:id?)
|
|
11
|
+
* - Regex constraints
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
class Router {
|
|
17
|
+
constructor(prefix = '') {
|
|
18
|
+
this._routes = [];
|
|
19
|
+
this._prefix = prefix;
|
|
20
|
+
this._namedRoutes = new Map();
|
|
21
|
+
this._routeMiddleware = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add a route
|
|
26
|
+
*/
|
|
27
|
+
add(method, routePath, handlers) {
|
|
28
|
+
const fullPath = this._normalizePath(this._prefix + routePath);
|
|
29
|
+
const route = {
|
|
30
|
+
method: method.toUpperCase(),
|
|
31
|
+
path: fullPath,
|
|
32
|
+
handlers: Array.isArray(handlers) ? handlers : [handlers],
|
|
33
|
+
pattern: this._buildPattern(fullPath),
|
|
34
|
+
paramNames: this._extractParamNames(fullPath),
|
|
35
|
+
name: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
this._routes.push(route);
|
|
39
|
+
|
|
40
|
+
// Return chainable object for naming routes
|
|
41
|
+
return {
|
|
42
|
+
name: (routeName) => {
|
|
43
|
+
route.name = routeName;
|
|
44
|
+
this._namedRoutes.set(routeName, route);
|
|
45
|
+
return this;
|
|
46
|
+
},
|
|
47
|
+
middleware: (...mw) => {
|
|
48
|
+
route.handlers = [...mw, ...route.handlers];
|
|
49
|
+
return this;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Match a request to a route
|
|
56
|
+
*/
|
|
57
|
+
match(method, requestPath) {
|
|
58
|
+
const normalizedPath = this._normalizePath(requestPath);
|
|
59
|
+
|
|
60
|
+
for (const route of this._routes) {
|
|
61
|
+
if (route.method !== method.toUpperCase() && route.method !== 'ALL') {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const match = normalizedPath.match(route.pattern);
|
|
66
|
+
if (match) {
|
|
67
|
+
const params = {};
|
|
68
|
+
route.paramNames.forEach((name, i) => {
|
|
69
|
+
params[name] = decodeURIComponent(match[i + 1]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
route,
|
|
74
|
+
params,
|
|
75
|
+
handlers: route.handlers.filter(h => typeof h === 'function'),
|
|
76
|
+
path: route.path,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a route group with shared prefix
|
|
86
|
+
*/
|
|
87
|
+
group(prefix) {
|
|
88
|
+
const groupRouter = new Router(this._prefix + prefix);
|
|
89
|
+
groupRouter._routes = this._routes; // Share routes array
|
|
90
|
+
groupRouter._namedRoutes = this._namedRoutes;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
get: (path, ...handlers) => groupRouter.add('GET', path, handlers),
|
|
94
|
+
post: (path, ...handlers) => groupRouter.add('POST', path, handlers),
|
|
95
|
+
put: (path, ...handlers) => groupRouter.add('PUT', path, handlers),
|
|
96
|
+
patch: (path, ...handlers) => groupRouter.add('PATCH', path, handlers),
|
|
97
|
+
delete: (path, ...handlers) => groupRouter.add('DELETE', path, handlers),
|
|
98
|
+
all: (path, ...handlers) => groupRouter.add('ALL', path, handlers),
|
|
99
|
+
group: (subPrefix) => groupRouter.group(subPrefix),
|
|
100
|
+
resource: (name, controller) => {
|
|
101
|
+
const base = `/${name}`;
|
|
102
|
+
groupRouter.add('GET', base, [controller.index || controller.list]);
|
|
103
|
+
groupRouter.add('GET', `${base}/:id`, [controller.show || controller.get]);
|
|
104
|
+
groupRouter.add('POST', base, [controller.create || controller.store]);
|
|
105
|
+
groupRouter.add('PUT', `${base}/:id`, [controller.update]);
|
|
106
|
+
groupRouter.add('PATCH', `${base}/:id`, [controller.update]);
|
|
107
|
+
groupRouter.add('DELETE', `${base}/:id`, [controller.destroy || controller.remove || controller.delete]);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get URL for named route
|
|
114
|
+
*/
|
|
115
|
+
url(name, params = {}) {
|
|
116
|
+
const route = this._namedRoutes.get(name);
|
|
117
|
+
if (!route) throw new Error(`Route "${name}" not found`);
|
|
118
|
+
|
|
119
|
+
let url = route.path;
|
|
120
|
+
for (const [key, value] of Object.entries(params)) {
|
|
121
|
+
url = url.replace(`:${key}`, encodeURIComponent(value));
|
|
122
|
+
}
|
|
123
|
+
return url;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get all registered routes
|
|
128
|
+
*/
|
|
129
|
+
getRoutes() {
|
|
130
|
+
return this._routes.map(r => ({
|
|
131
|
+
method: r.method,
|
|
132
|
+
path: r.path,
|
|
133
|
+
name: r.name,
|
|
134
|
+
handlers: r.handlers.length,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ===== INTERNAL =====
|
|
139
|
+
|
|
140
|
+
_normalizePath(p) {
|
|
141
|
+
return '/' + p.split('/').filter(Boolean).join('/');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_buildPattern(routePath) {
|
|
145
|
+
const parts = routePath.split('/').filter(Boolean);
|
|
146
|
+
const patternParts = parts.map(part => {
|
|
147
|
+
// Wildcard
|
|
148
|
+
if (part === '*') return '(.+)';
|
|
149
|
+
|
|
150
|
+
// Optional parameter :name?
|
|
151
|
+
if (part.startsWith(':') && part.endsWith('?')) {
|
|
152
|
+
const paramName = part.slice(1, -1);
|
|
153
|
+
return '([^/]*)?';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Dynamic parameter :name
|
|
157
|
+
if (part.startsWith(':')) {
|
|
158
|
+
return '([^/]+)';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Literal
|
|
162
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const pattern = '^/' + patternParts.join('/') + '/?$';
|
|
166
|
+
return new RegExp(pattern, 'i');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_extractParamNames(routePath) {
|
|
170
|
+
const names = [];
|
|
171
|
+
const parts = routePath.split('/').filter(Boolean);
|
|
172
|
+
for (const part of parts) {
|
|
173
|
+
if (part === '*') {
|
|
174
|
+
names.push('wildcard');
|
|
175
|
+
} else if (part.startsWith(':')) {
|
|
176
|
+
names.push(part.slice(1).replace('?', ''));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return names;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { Router };
|