create-rebe 1.0.0 → 3.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/package.json +1 -1
- package/template/.env.example +4 -0
- package/template/README.md +63 -8
- package/template/SECURITY.md +39 -0
- package/template/app/routes/api.route.js +1 -1
- package/template/app/routes/register.route.js +1 -1
- package/template/app/routes/web.route.js +1 -1
- package/template/app/socket/register.socket.js +0 -2
- package/template/config/express.config.js +8 -0
- package/template/core/common/string.js +1 -1
- package/template/core/cron.core.js +1 -0
- package/template/core/database.core.js +30 -17
- package/template/core/error.core.js +8 -4
- package/template/core/express.core.js +16 -4
- package/template/core/hooks.core.js +10 -7
- package/template/core/migrator.core.js +201 -0
- package/template/core/modules.core.js +167 -0
- package/template/core/queue.core.js +1 -0
- package/template/core/routing.core.d.ts +273 -0
- package/template/core/routing.core.js +666 -0
- package/template/core/seeder.core.js +105 -0
- package/template/core/socket.core.js +1 -0
- package/template/database/migrations/.gitkeep +0 -0
- package/template/database/seeders/.gitkeep +0 -0
- package/template/docs/Database.md +14 -8
- package/template/docs/Express.md +5 -2
- package/template/docs/Make.md +46 -0
- package/template/docs/Migration.md +56 -0
- package/template/docs/Modules.md +96 -0
- package/template/docs/README.md +5 -0
- package/template/docs/Routing.md +116 -0
- package/template/docs/Seeder.md +54 -0
- package/template/eslint.config.js +52 -0
- package/template/package-lock.json +1068 -70
- package/template/package.json +15 -8
- package/template/scripts/cli/args.js +39 -0
- package/template/scripts/cli/bootstrap.js +16 -0
- package/template/scripts/cli/db.js +79 -0
- package/template/scripts/cli/help.js +58 -0
- package/template/scripts/cli/keys.js +100 -0
- package/template/scripts/cli/log.js +58 -0
- package/template/scripts/cli/make.js +249 -0
- package/template/scripts/cli/names.js +51 -0
- package/template/scripts/cli/templates.js +358 -0
- package/template/scripts/cli.js +75 -234
- package/template/tests/http.test.js +99 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Laravel-style routing for Express, internalized into the core (previously the
|
|
4
|
+
// external package @refkinscallv/express-routing — authored by the same maintainer).
|
|
5
|
+
// The API is unchanged: define routes against this static class, then express.core
|
|
6
|
+
// calls Routes.apply(app, router) during boot.
|
|
7
|
+
//
|
|
8
|
+
// const Routes = require('@core/routing.core')
|
|
9
|
+
// Routes.group('api', () => {
|
|
10
|
+
// Routes.get('status', ({ res }) => res.json({ status: true }))
|
|
11
|
+
// })
|
|
12
|
+
//
|
|
13
|
+
// Every handler and handle()-based middleware receives the context { req, res, next,
|
|
14
|
+
// error }. The core never imports application code; route files are loaded through the
|
|
15
|
+
// register bridge, so the app -> core dependency arrow is preserved.
|
|
16
|
+
class Routes {
|
|
17
|
+
static routes = []
|
|
18
|
+
static prefix = ''
|
|
19
|
+
static groupMiddlewares = []
|
|
20
|
+
static globalMiddlewares = []
|
|
21
|
+
static _errorHandler = null
|
|
22
|
+
static _maintenanceMode = false
|
|
23
|
+
static _maintenanceHandler = null
|
|
24
|
+
static _fallbackHandler = null
|
|
25
|
+
static middlewareAliases = {}
|
|
26
|
+
static middlewareGroups = {}
|
|
27
|
+
|
|
28
|
+
// Register named middleware aliases (Laravel-style):
|
|
29
|
+
// Routes.registerMiddleware('auth', AuthMiddleware)
|
|
30
|
+
// Routes.registerMiddleware({ auth: AuthMiddleware, guest: GuestMiddleware })
|
|
31
|
+
static registerMiddleware(name, mw) {
|
|
32
|
+
if (name && typeof name === 'object') {
|
|
33
|
+
for (const key of Object.keys(name)) this.middlewareAliases[key] = name[key]
|
|
34
|
+
return this
|
|
35
|
+
}
|
|
36
|
+
this.middlewareAliases[name] = mw
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Register a named middleware group — a string that expands to several middlewares.
|
|
41
|
+
static middlewareGroup(name, list) {
|
|
42
|
+
this.middlewareGroups[name] = Array.isArray(list) ? list : [list]
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Expand a middleware list, resolving string entries to their registered alias
|
|
47
|
+
// (single) or group (many). Non-string entries pass through untouched.
|
|
48
|
+
static expandMiddleware(list) {
|
|
49
|
+
const out = []
|
|
50
|
+
for (const mw of list) {
|
|
51
|
+
if (typeof mw === 'string') {
|
|
52
|
+
if (Object.prototype.hasOwnProperty.call(this.middlewareGroups, mw)) {
|
|
53
|
+
out.push(...this.expandMiddleware(this.middlewareGroups[mw]))
|
|
54
|
+
} else if (Object.prototype.hasOwnProperty.call(this.middlewareAliases, mw)) {
|
|
55
|
+
out.push(this.middlewareAliases[mw])
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(`Unknown middleware "${mw}" — register it with Routes.registerMiddleware() or Routes.middlewareGroup()`)
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
out.push(mw)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Normalize a path: drop duplicate slashes and ensure a single leading slash.
|
|
67
|
+
static normalizePath(path) {
|
|
68
|
+
return '/' + path.split('/').filter(Boolean).join('/')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Convert a method/function name to a kebab-case URL segment.
|
|
72
|
+
// SamplePath -> sample-path · samplePath -> sample-path · sample_path -> sample-path
|
|
73
|
+
static nameToPath(name) {
|
|
74
|
+
let result = name.replace(/_/g, '-')
|
|
75
|
+
result = result.replace(/([a-z])([A-Z][a-z])/g, '$1-$2')
|
|
76
|
+
return result.toLowerCase()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resolve a controller (static class, instance class, or plain object) + method
|
|
80
|
+
// name into a bound callable.
|
|
81
|
+
static resolveHandler(Controller, method) {
|
|
82
|
+
if (typeof Controller === 'function' && typeof Controller[method] === 'function') {
|
|
83
|
+
return Controller[method].bind(Controller)
|
|
84
|
+
}
|
|
85
|
+
if (typeof Controller === 'function') {
|
|
86
|
+
const instance = new Controller()
|
|
87
|
+
if (typeof instance[method] === 'function') {
|
|
88
|
+
return instance[method].bind(instance)
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Method "${method}" not found in controller instance "${Controller.name}"`)
|
|
91
|
+
}
|
|
92
|
+
if (typeof Controller === 'object' && Controller !== null && typeof Controller[method] === 'function') {
|
|
93
|
+
return Controller[method].bind(Controller)
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Cannot resolve handler for method "${method}"`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Resolve a middleware's handle({ req, res, next, error }) into a bound callable,
|
|
99
|
+
// or return null when the middleware is a plain Express function. Accepts a class
|
|
100
|
+
// with a static handle(), a class with an instance handle() (instantiated once), or
|
|
101
|
+
// a plain object with handle().
|
|
102
|
+
static resolveHandle(mw) {
|
|
103
|
+
if (typeof mw === 'function') {
|
|
104
|
+
if (typeof mw.handle === 'function') {
|
|
105
|
+
return mw.handle.bind(mw)
|
|
106
|
+
}
|
|
107
|
+
if (mw.prototype && typeof mw.prototype.handle === 'function') {
|
|
108
|
+
const instance = new mw()
|
|
109
|
+
return instance.handle.bind(instance)
|
|
110
|
+
}
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
if (typeof mw === 'object' && mw !== null && typeof mw.handle === 'function') {
|
|
114
|
+
return mw.handle.bind(mw)
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Wrap a resolved handle() function into Express-compatible middleware.
|
|
120
|
+
static wrapHandle(handleFn) {
|
|
121
|
+
return (req, res, next) => {
|
|
122
|
+
try {
|
|
123
|
+
const result = handleFn({ req, res, next, error: null })
|
|
124
|
+
Promise.resolve(result).catch(next)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
next(err)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Normalize a middleware entry to an Express-compatible function. A plain Express
|
|
132
|
+
// function passes through; a class/object with handle() is auto-wrapped.
|
|
133
|
+
static normalizeMiddleware(mw) {
|
|
134
|
+
const handleFn = this.resolveHandle(mw)
|
|
135
|
+
if (handleFn) {
|
|
136
|
+
return this.wrapHandle(handleFn)
|
|
137
|
+
}
|
|
138
|
+
if (typeof mw === 'function') {
|
|
139
|
+
return mw
|
|
140
|
+
}
|
|
141
|
+
throw new Error('Invalid middleware: must be a function or an object/class with a "handle({ req, res, next, error })" method')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Strict normalization used in the chaining context — ONLY handle()-based
|
|
145
|
+
// middleware is allowed (plain Express functions are rejected).
|
|
146
|
+
static normalizeMiddlewareStrict(mw) {
|
|
147
|
+
const handleFn = this.resolveHandle(mw)
|
|
148
|
+
if (!handleFn) {
|
|
149
|
+
throw new Error('Chained middleware must implement a "handle({ req, res, next, error })" method. ' + 'Plain functions are not allowed in chaining syntax. Use the scoped callback form instead: ' + 'Routes.middleware([fn], () => { ... })')
|
|
150
|
+
}
|
|
151
|
+
return this.wrapHandle(handleFn)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Add a route with the given HTTP method(s), path, handler, and middlewares.
|
|
155
|
+
static add(methods, path, handler, middlewares = []) {
|
|
156
|
+
const methodArray = Array.isArray(methods) ? methods : [methods]
|
|
157
|
+
const fullPath = this.normalizePath(`${this.prefix}/${path}`)
|
|
158
|
+
const route = {
|
|
159
|
+
methods: methodArray,
|
|
160
|
+
path: fullPath,
|
|
161
|
+
handler,
|
|
162
|
+
middlewares: [...this.globalMiddlewares, ...this.groupMiddlewares, ...middlewares],
|
|
163
|
+
name: null,
|
|
164
|
+
constraints: {},
|
|
165
|
+
}
|
|
166
|
+
this.routes.push(route)
|
|
167
|
+
return this.registration(route)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Build a chainable registration handle for fluent configuration:
|
|
171
|
+
// Routes.get(...).name('users.show').whereNumber('id')
|
|
172
|
+
static registration(route) {
|
|
173
|
+
const handle = {
|
|
174
|
+
name(routeName) {
|
|
175
|
+
route.name = routeName
|
|
176
|
+
return handle
|
|
177
|
+
},
|
|
178
|
+
where(param, pattern) {
|
|
179
|
+
if (param && typeof param === 'object') {
|
|
180
|
+
Object.assign(route.constraints, param)
|
|
181
|
+
} else {
|
|
182
|
+
route.constraints[param] = pattern
|
|
183
|
+
}
|
|
184
|
+
return handle
|
|
185
|
+
},
|
|
186
|
+
whereNumber(param) {
|
|
187
|
+
route.constraints[param] = '[0-9]+'
|
|
188
|
+
return handle
|
|
189
|
+
},
|
|
190
|
+
whereAlpha(param) {
|
|
191
|
+
route.constraints[param] = '[A-Za-z]+'
|
|
192
|
+
return handle
|
|
193
|
+
},
|
|
194
|
+
whereAlphaNumeric(param) {
|
|
195
|
+
route.constraints[param] = '[A-Za-z0-9]+'
|
|
196
|
+
return handle
|
|
197
|
+
},
|
|
198
|
+
whereUuid(param) {
|
|
199
|
+
route.constraints[param] = '[0-9a-fA-F-]{36}'
|
|
200
|
+
return handle
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
return handle
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static get(path, handler, middlewares = []) {
|
|
207
|
+
return this.add('get', path, handler, middlewares)
|
|
208
|
+
}
|
|
209
|
+
static post(path, handler, middlewares = []) {
|
|
210
|
+
return this.add('post', path, handler, middlewares)
|
|
211
|
+
}
|
|
212
|
+
static put(path, handler, middlewares = []) {
|
|
213
|
+
return this.add('put', path, handler, middlewares)
|
|
214
|
+
}
|
|
215
|
+
static delete(path, handler, middlewares = []) {
|
|
216
|
+
return this.add('delete', path, handler, middlewares)
|
|
217
|
+
}
|
|
218
|
+
static patch(path, handler, middlewares = []) {
|
|
219
|
+
return this.add('patch', path, handler, middlewares)
|
|
220
|
+
}
|
|
221
|
+
static options(path, handler, middlewares = []) {
|
|
222
|
+
return this.add('options', path, handler, middlewares)
|
|
223
|
+
}
|
|
224
|
+
static head(path, handler, middlewares = []) {
|
|
225
|
+
return this.add('head', path, handler, middlewares)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Group routes under a common URL prefix with optional middleware.
|
|
229
|
+
static group(prefix, callback, middlewares = []) {
|
|
230
|
+
const previousPrefix = this.prefix
|
|
231
|
+
const previousMiddlewares = this.groupMiddlewares
|
|
232
|
+
|
|
233
|
+
const fullPrefix = [previousPrefix, prefix].filter(Boolean).join('/')
|
|
234
|
+
this.prefix = this.normalizePath(fullPrefix)
|
|
235
|
+
this.groupMiddlewares = [...previousMiddlewares, ...this.expandMiddleware(middlewares).map((mw) => this.normalizeMiddleware(mw))]
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
callback()
|
|
239
|
+
} finally {
|
|
240
|
+
// Always restore — even if the callback throws — so a definition-time error
|
|
241
|
+
// does not corrupt the prefix/middlewares for subsequent routes.
|
|
242
|
+
this.prefix = previousPrefix
|
|
243
|
+
this.groupMiddlewares = previousMiddlewares
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Apply global middlewares.
|
|
248
|
+
//
|
|
249
|
+
// SCOPED (with callback) — accepts plain functions OR handle() classes:
|
|
250
|
+
// Routes.middleware([Mw, fn], () => { Routes.get(...) })
|
|
251
|
+
//
|
|
252
|
+
// CHAINING (without callback) — STRICT: only handle() classes/objects:
|
|
253
|
+
// Routes.middleware([Mw1, Mw2]).get(path, handler)
|
|
254
|
+
// Routes.middleware([Mw1, Mw2]).group(prefix, callback)
|
|
255
|
+
// Each chained call is terminal — globalMiddlewares are restored after.
|
|
256
|
+
static middleware(middlewares, callback) {
|
|
257
|
+
const prevMiddlewares = this.globalMiddlewares
|
|
258
|
+
|
|
259
|
+
if (typeof callback === 'function') {
|
|
260
|
+
const normalized = this.expandMiddleware(middlewares).map((mw) => this.normalizeMiddleware(mw))
|
|
261
|
+
this.globalMiddlewares = [...prevMiddlewares, ...normalized]
|
|
262
|
+
try {
|
|
263
|
+
callback()
|
|
264
|
+
} finally {
|
|
265
|
+
this.globalMiddlewares = prevMiddlewares
|
|
266
|
+
}
|
|
267
|
+
return this
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Chaining mode: STRICT. Validate eagerly so a bad middleware throws immediately,
|
|
271
|
+
// but DO NOT mutate globalMiddlewares here — that mutation is scoped to each
|
|
272
|
+
// terminal call below (otherwise a non-terminal Routes.middleware([Mw]) would leak
|
|
273
|
+
// the middleware into every later route).
|
|
274
|
+
const normalized = this.expandMiddleware(middlewares).map((mw) => this.normalizeMiddlewareStrict(mw))
|
|
275
|
+
|
|
276
|
+
const self = this
|
|
277
|
+
|
|
278
|
+
const withChained = (action) => {
|
|
279
|
+
const prev = self.globalMiddlewares
|
|
280
|
+
self.globalMiddlewares = [...prev, ...normalized]
|
|
281
|
+
try {
|
|
282
|
+
return action()
|
|
283
|
+
} finally {
|
|
284
|
+
self.globalMiddlewares = prev
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
group(prefix, groupCallback, groupMiddlewares = []) {
|
|
290
|
+
return withChained(() => self.group(prefix, groupCallback, groupMiddlewares))
|
|
291
|
+
},
|
|
292
|
+
add(methods, path, handler, mws = []) {
|
|
293
|
+
return withChained(() => self.add(methods, path, handler, mws))
|
|
294
|
+
},
|
|
295
|
+
get(path, handler, mws = []) {
|
|
296
|
+
return withChained(() => self.add('get', path, handler, mws))
|
|
297
|
+
},
|
|
298
|
+
post(path, handler, mws = []) {
|
|
299
|
+
return withChained(() => self.add('post', path, handler, mws))
|
|
300
|
+
},
|
|
301
|
+
put(path, handler, mws = []) {
|
|
302
|
+
return withChained(() => self.add('put', path, handler, mws))
|
|
303
|
+
},
|
|
304
|
+
delete(path, handler, mws = []) {
|
|
305
|
+
return withChained(() => self.add('delete', path, handler, mws))
|
|
306
|
+
},
|
|
307
|
+
patch(path, handler, mws = []) {
|
|
308
|
+
return withChained(() => self.add('patch', path, handler, mws))
|
|
309
|
+
},
|
|
310
|
+
options(path, handler, mws = []) {
|
|
311
|
+
return withChained(() => self.add('options', path, handler, mws))
|
|
312
|
+
},
|
|
313
|
+
head(path, handler, mws = []) {
|
|
314
|
+
return withChained(() => self.add('head', path, handler, mws))
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Register a global error handler. Receives { req, res, next, error }.
|
|
320
|
+
static errorHandler(handler) {
|
|
321
|
+
if (Array.isArray(handler) && handler.length === 2) {
|
|
322
|
+
const [Controller, method] = handler
|
|
323
|
+
this._errorHandler = this.resolveHandler(Controller, method)
|
|
324
|
+
} else if (typeof handler === 'function') {
|
|
325
|
+
this._errorHandler = handler
|
|
326
|
+
} else {
|
|
327
|
+
throw new Error('Routes.errorHandler: invalid handler — must be a function or [Controller, "method"]')
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Enable/disable maintenance mode. When enabled, all requests get a 503 before any
|
|
332
|
+
// route runs. Handler receives { req, res, next, error }.
|
|
333
|
+
static maintenance(enabled, handler) {
|
|
334
|
+
this._maintenanceMode = Boolean(enabled)
|
|
335
|
+
|
|
336
|
+
if (handler) {
|
|
337
|
+
if (Array.isArray(handler) && handler.length === 2) {
|
|
338
|
+
const [Controller, method] = handler
|
|
339
|
+
this._maintenanceHandler = this.resolveHandler(Controller, method)
|
|
340
|
+
} else if (typeof handler === 'function') {
|
|
341
|
+
this._maintenanceHandler = handler
|
|
342
|
+
} else {
|
|
343
|
+
throw new Error('Routes.maintenance: invalid handler — must be a function or [Controller, "method"]')
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Auto-register every public method of a controller as a route.
|
|
349
|
+
// index -> base path · other methods -> kebab-case segment
|
|
350
|
+
// HTTP prefix detection: post_create -> POST /<base>/create
|
|
351
|
+
// Methods starting with "_" are private helpers and are never exposed.
|
|
352
|
+
static controller(basePath, Controller, methodMiddlewares = {}) {
|
|
353
|
+
const HTTP_PREFIXES = ['post', 'put', 'delete', 'patch', 'options', 'head']
|
|
354
|
+
|
|
355
|
+
let methods
|
|
356
|
+
|
|
357
|
+
// For class controllers, instance methods share a SINGLE instance so the
|
|
358
|
+
// constructor runs once and `this` state is shared across all routes.
|
|
359
|
+
let sharedInstance = null
|
|
360
|
+
const getInstance = () => {
|
|
361
|
+
if (!sharedInstance) sharedInstance = new Controller()
|
|
362
|
+
return sharedInstance
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (typeof Controller === 'function') {
|
|
366
|
+
const staticMethods = Object.getOwnPropertyNames(Controller).filter((n) => typeof Controller[n] === 'function' && !['length', 'name', 'prototype'].includes(n))
|
|
367
|
+
const proto = Controller.prototype
|
|
368
|
+
const instanceMethods = proto ? Object.getOwnPropertyNames(proto).filter((n) => n !== 'constructor' && typeof proto[n] === 'function') : []
|
|
369
|
+
methods = [...new Set([...staticMethods, ...instanceMethods])]
|
|
370
|
+
} else if (typeof Controller === 'object' && Controller !== null) {
|
|
371
|
+
methods = Object.getOwnPropertyNames(Controller).filter((n) => typeof Controller[n] === 'function')
|
|
372
|
+
} else {
|
|
373
|
+
throw new Error('Routes.controller: invalid Controller — must be a class, instance, or plain object')
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
methods = methods.filter((name) => !name.startsWith('_'))
|
|
377
|
+
|
|
378
|
+
for (const methodName of methods) {
|
|
379
|
+
let httpMethod = 'get'
|
|
380
|
+
let pathSegment = methodName
|
|
381
|
+
|
|
382
|
+
const matchedPrefix = HTTP_PREFIXES.find((p) => methodName.toLowerCase().startsWith(p + '_') || methodName.toLowerCase() === p)
|
|
383
|
+
if (matchedPrefix) {
|
|
384
|
+
httpMethod = matchedPrefix
|
|
385
|
+
pathSegment = methodName.slice(matchedPrefix.length).replace(/^_/, '')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let routePath
|
|
389
|
+
if (pathSegment === '' || pathSegment.toLowerCase() === 'index') {
|
|
390
|
+
routePath = this.normalizePath(basePath)
|
|
391
|
+
} else {
|
|
392
|
+
routePath = this.normalizePath(`${basePath}/${this.nameToPath(pathSegment)}`)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const specificMws = methodMiddlewares[methodName] ? (Array.isArray(methodMiddlewares[methodName]) ? methodMiddlewares[methodName] : [methodMiddlewares[methodName]]) : []
|
|
396
|
+
|
|
397
|
+
let handler
|
|
398
|
+
if (typeof Controller === 'function') {
|
|
399
|
+
if (typeof Controller[methodName] === 'function') {
|
|
400
|
+
handler = Controller[methodName].bind(Controller)
|
|
401
|
+
} else {
|
|
402
|
+
const instance = getInstance()
|
|
403
|
+
handler = instance[methodName].bind(instance)
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
handler = Controller[methodName].bind(Controller)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
this.routes.push({
|
|
410
|
+
methods: [httpMethod],
|
|
411
|
+
path: routePath,
|
|
412
|
+
handler,
|
|
413
|
+
middlewares: [...this.globalMiddlewares, ...this.groupMiddlewares, ...specificMws],
|
|
414
|
+
_resolved: true,
|
|
415
|
+
name: null,
|
|
416
|
+
constraints: {},
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Register the seven RESTful resource routes (Laravel-style). Only actions the
|
|
422
|
+
// controller actually implements are registered.
|
|
423
|
+
static resource(name, Controller, options = {}) {
|
|
424
|
+
const param = options.parameter || 'id'
|
|
425
|
+
const base = this.normalizePath(`${this.prefix}/${name}`)
|
|
426
|
+
const nameBase = name.split('/').filter(Boolean).join('.')
|
|
427
|
+
const resolve = this.makeMethodResolver(Controller)
|
|
428
|
+
|
|
429
|
+
const specMws = options.middleware ? (Array.isArray(options.middleware) ? options.middleware : [options.middleware]) : []
|
|
430
|
+
|
|
431
|
+
const definitions = [
|
|
432
|
+
['index', ['get'], base],
|
|
433
|
+
['create', ['get'], `${base}/create`],
|
|
434
|
+
['store', ['post'], base],
|
|
435
|
+
['show', ['get'], `${base}/:${param}`],
|
|
436
|
+
['edit', ['get'], `${base}/:${param}/edit`],
|
|
437
|
+
['update', ['put', 'patch'], `${base}/:${param}`],
|
|
438
|
+
['destroy', ['delete'], `${base}/:${param}`],
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
let allowed = definitions.map((d) => d[0])
|
|
442
|
+
if (options.api) allowed = allowed.filter((a) => a !== 'create' && a !== 'edit')
|
|
443
|
+
if (Array.isArray(options.only)) allowed = allowed.filter((a) => options.only.includes(a))
|
|
444
|
+
if (Array.isArray(options.except)) allowed = allowed.filter((a) => !options.except.includes(a))
|
|
445
|
+
|
|
446
|
+
for (const [action, methods, routePath] of definitions) {
|
|
447
|
+
if (!allowed.includes(action)) continue
|
|
448
|
+
const handler = resolve(action)
|
|
449
|
+
if (!handler) continue // controller doesn't implement this action — skip
|
|
450
|
+
this.routes.push({
|
|
451
|
+
methods,
|
|
452
|
+
path: this.normalizePath(routePath),
|
|
453
|
+
handler,
|
|
454
|
+
middlewares: [...this.globalMiddlewares, ...this.groupMiddlewares, ...specMws],
|
|
455
|
+
_resolved: true,
|
|
456
|
+
name: `${nameBase}.${action}`,
|
|
457
|
+
constraints: {},
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
return this
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// API resource — resource() without the HTML-form create/edit routes.
|
|
464
|
+
static apiResource(name, Controller, options = {}) {
|
|
465
|
+
return this.resource(name, Controller, { ...options, api: true })
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Build a resolver binding a controller method to the class (static), a single
|
|
469
|
+
// shared instance (instance method), or the object — null when it does not exist.
|
|
470
|
+
static makeMethodResolver(Controller) {
|
|
471
|
+
let instance = null
|
|
472
|
+
return (methodName) => {
|
|
473
|
+
if (typeof Controller === 'function') {
|
|
474
|
+
if (typeof Controller[methodName] === 'function') {
|
|
475
|
+
return Controller[methodName].bind(Controller)
|
|
476
|
+
}
|
|
477
|
+
const proto = Controller.prototype
|
|
478
|
+
if (proto && typeof proto[methodName] === 'function') {
|
|
479
|
+
if (!instance) instance = new Controller()
|
|
480
|
+
return instance[methodName].bind(instance)
|
|
481
|
+
}
|
|
482
|
+
return null
|
|
483
|
+
}
|
|
484
|
+
if (typeof Controller === 'object' && Controller !== null && typeof Controller[methodName] === 'function') {
|
|
485
|
+
return Controller[methodName].bind(Controller)
|
|
486
|
+
}
|
|
487
|
+
return null
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Register a redirect route (default status 302).
|
|
492
|
+
static redirect(from, to, status = 302) {
|
|
493
|
+
return this.add('get', from, ({ res }) => res.redirect(status, to))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Register a route that renders a view via the Express view engine (res.render).
|
|
497
|
+
static view(path, view, data = {}) {
|
|
498
|
+
return this.add('get', path, ({ res }) => res.render(view, data))
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Register a fallback handler invoked when no other route matches (Laravel-style).
|
|
502
|
+
static fallback(handler) {
|
|
503
|
+
if (Array.isArray(handler) && handler.length === 2) {
|
|
504
|
+
const [Controller, method] = handler
|
|
505
|
+
this._fallbackHandler = this.resolveHandler(Controller, method)
|
|
506
|
+
} else if (typeof handler === 'function') {
|
|
507
|
+
this._fallbackHandler = handler
|
|
508
|
+
} else {
|
|
509
|
+
throw new Error('Routes.fallback: invalid handler — must be a function or [Controller, "method"]')
|
|
510
|
+
}
|
|
511
|
+
return this
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Generate a URL for a named route, substituting :param segments and appending any
|
|
515
|
+
// extra keys as a query string.
|
|
516
|
+
static url(name, params = {}) {
|
|
517
|
+
const route = this.routes.find((r) => r.name === name)
|
|
518
|
+
if (!route) {
|
|
519
|
+
throw new Error(`Route name "${name}" not found`)
|
|
520
|
+
}
|
|
521
|
+
const used = new Set()
|
|
522
|
+
let path = route.path.replace(/:([A-Za-z0-9_]+)(\?)?/g, (match, key, optional) => {
|
|
523
|
+
used.add(key)
|
|
524
|
+
if (params[key] === undefined || params[key] === null) {
|
|
525
|
+
if (optional) return ''
|
|
526
|
+
throw new Error(`Missing parameter "${key}" for route "${name}"`)
|
|
527
|
+
}
|
|
528
|
+
return encodeURIComponent(params[key])
|
|
529
|
+
})
|
|
530
|
+
path = this.normalizePath(path)
|
|
531
|
+
const query = Object.keys(params)
|
|
532
|
+
.filter((key) => !used.has(key) && params[key] !== undefined && params[key] !== null)
|
|
533
|
+
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
|
534
|
+
return query.length ? `${path}?${query.join('&')}` : path
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Alias of Routes.url() — matches Laravel's route() helper.
|
|
538
|
+
static route(name, params = {}) {
|
|
539
|
+
return this.url(name, params)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Info for all registered routes.
|
|
543
|
+
static allRoutes() {
|
|
544
|
+
return this.routes.map((route) => ({
|
|
545
|
+
methods: route.methods,
|
|
546
|
+
path: route.path,
|
|
547
|
+
name: route.name || null,
|
|
548
|
+
middlewareCount: route.middlewares.length,
|
|
549
|
+
handlerType: typeof route.handler === 'function' && !route._resolved ? 'function' : 'controller',
|
|
550
|
+
}))
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Apply all registered routes to Express.
|
|
554
|
+
// Routes.apply(app) — mount directly on app
|
|
555
|
+
// Routes.apply(app, router) — mount on router, then app.use(router)
|
|
556
|
+
static async apply(appOrRouter, router) {
|
|
557
|
+
const target = router || appOrRouter
|
|
558
|
+
|
|
559
|
+
// Maintenance mode — intercept all requests before routes.
|
|
560
|
+
if (this._maintenanceMode) {
|
|
561
|
+
const maintenanceFn =
|
|
562
|
+
this._maintenanceHandler ||
|
|
563
|
+
(({ res }) =>
|
|
564
|
+
res.status(503).json({
|
|
565
|
+
status: false,
|
|
566
|
+
code: 503,
|
|
567
|
+
message: 'Service Unavailable - Maintenance Mode',
|
|
568
|
+
}))
|
|
569
|
+
|
|
570
|
+
target.use((req, res, next) => {
|
|
571
|
+
try {
|
|
572
|
+
const result = maintenanceFn({ req, res, next, error: null })
|
|
573
|
+
Promise.resolve(result).catch(next)
|
|
574
|
+
} catch (err) {
|
|
575
|
+
next(err)
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
if (router) appOrRouter.use(router)
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const ALLOWED_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']
|
|
584
|
+
|
|
585
|
+
for (const route of this.routes) {
|
|
586
|
+
let handlerFunction
|
|
587
|
+
|
|
588
|
+
if (route._resolved) {
|
|
589
|
+
handlerFunction = route.handler
|
|
590
|
+
} else if (typeof route.handler === 'function') {
|
|
591
|
+
handlerFunction = route.handler
|
|
592
|
+
} else if (Array.isArray(route.handler) && route.handler.length === 2) {
|
|
593
|
+
const [Controller, method] = route.handler
|
|
594
|
+
handlerFunction = this.resolveHandler(Controller, method)
|
|
595
|
+
} else {
|
|
596
|
+
throw new Error(`Invalid handler format for route: ${route.path}`)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!handlerFunction) continue
|
|
600
|
+
|
|
601
|
+
// Parameter constraints (Routes...where()) — a request whose param does not
|
|
602
|
+
// match is skipped with next('route') so a later route can still match.
|
|
603
|
+
const constraintKeys = Object.keys(route.constraints || {})
|
|
604
|
+
const constraintGuard = constraintKeys.length
|
|
605
|
+
? (req, res, next) => {
|
|
606
|
+
for (const key of constraintKeys) {
|
|
607
|
+
const pattern = route.constraints[key]
|
|
608
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(`^(?:${pattern})$`)
|
|
609
|
+
const value = req.params[key] != null ? String(req.params[key]) : ''
|
|
610
|
+
if (!re.test(value)) return next('route')
|
|
611
|
+
}
|
|
612
|
+
next()
|
|
613
|
+
}
|
|
614
|
+
: null
|
|
615
|
+
|
|
616
|
+
for (const method of route.methods) {
|
|
617
|
+
if (!ALLOWED_METHODS.includes(method)) {
|
|
618
|
+
throw new Error(`Invalid HTTP method "${method}" for route: ${route.path}`)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const normalizedMiddlewares = this.expandMiddleware(route.middlewares).map((mw) => this.normalizeMiddleware(mw))
|
|
622
|
+
const chain = constraintGuard ? [constraintGuard, ...normalizedMiddlewares] : normalizedMiddlewares
|
|
623
|
+
|
|
624
|
+
target[method](route.path, ...chain, async (req, res, next) => {
|
|
625
|
+
try {
|
|
626
|
+
const result = handlerFunction({ req, res, next, error: null })
|
|
627
|
+
await Promise.resolve(result)
|
|
628
|
+
} catch (error) {
|
|
629
|
+
next(error)
|
|
630
|
+
}
|
|
631
|
+
})
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Fallback — runs when no route above matched (Laravel-style).
|
|
636
|
+
if (this._fallbackHandler) {
|
|
637
|
+
const fallbackFn = this._fallbackHandler
|
|
638
|
+
target.use((req, res, next) => {
|
|
639
|
+
try {
|
|
640
|
+
const result = fallbackFn({ req, res, next, error: null })
|
|
641
|
+
Promise.resolve(result).catch(next)
|
|
642
|
+
} catch (err) {
|
|
643
|
+
next(err)
|
|
644
|
+
}
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Global error handler.
|
|
649
|
+
if (this._errorHandler) {
|
|
650
|
+
const errFn = this._errorHandler
|
|
651
|
+
target.use((error, req, res, next) => {
|
|
652
|
+
try {
|
|
653
|
+
const result = errFn({ req, res, next, error })
|
|
654
|
+
Promise.resolve(result).catch((err) => next(err))
|
|
655
|
+
} catch (err) {
|
|
656
|
+
next(err)
|
|
657
|
+
}
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (router) appOrRouter.use(router)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
module.exports = Routes
|
|
666
|
+
module.exports.default = Routes
|