odac 1.1.0 → 1.3.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/.agent/rules/coding.md +27 -0
- package/.agent/rules/memory.md +42 -0
- package/.agent/rules/project.md +30 -0
- package/.agent/rules/workflow.md +16 -0
- package/.github/workflows/release.yml +68 -1
- package/.github/workflows/test-coverage.yml +6 -5
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +13 -0
- package/.releaserc.js +1 -1
- package/CHANGELOG.md +99 -0
- package/README.md +16 -0
- package/bin/odac.js +196 -38
- package/client/odac.js +238 -171
- package/docs/backend/01-overview/03-development-server.md +38 -45
- package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
- package/docs/backend/03-config/00-configuration-overview.md +6 -6
- package/docs/backend/03-config/01-database-connection.md +2 -2
- package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
- package/docs/backend/03-config/03-request-timeout.md +1 -1
- package/docs/backend/03-config/04-environment-variables.md +4 -4
- package/docs/backend/03-config/05-early-hints.md +2 -2
- package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
- package/docs/backend/04-routing/07-cron-jobs.md +17 -1
- package/docs/backend/04-routing/09-websocket.md +14 -1
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
- package/docs/backend/05-controllers/03-controller-classes.md +40 -20
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
- package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
- package/docs/backend/08-database/01-getting-started.md +2 -2
- package/docs/backend/10-authentication/03-register.md +1 -1
- package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
- package/docs/backend/10-authentication/05-session-management.md +15 -1
- package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
- package/docs/backend/10-authentication/07-magic-links.md +1 -1
- package/docs/index.json +5 -1
- package/jest.config.js +1 -1
- package/package.json +18 -14
- package/src/Auth.js +58 -23
- package/src/Config.js +7 -7
- package/src/Database.js +1 -1
- package/src/Env.js +3 -1
- package/src/Ipc.js +7 -0
- package/src/Lang.js +9 -2
- package/src/Mail.js +19 -9
- package/src/Odac.js +56 -44
- package/src/Request.js +6 -2
- package/src/Route/Cron.js +58 -17
- package/src/Route/Internal.js +1 -1
- package/src/Route.js +375 -125
- package/src/Server.js +40 -3
- package/src/Storage.js +4 -0
- package/src/Token.js +6 -4
- package/src/Validator.js +24 -15
- package/src/Var.js +22 -6
- package/src/View/EarlyHints.js +43 -33
- package/src/View/Form.js +17 -11
- package/src/View.js +68 -12
- package/src/WebSocket.js +45 -12
- package/template/package.json +3 -1
- package/template/view/content/home.html +3 -3
- package/template/view/head/main.html +2 -2
- package/test/Client.test.js +168 -0
- package/test/Config.test.js +112 -0
- package/test/Lang.test.js +92 -0
- package/test/Odac.test.js +88 -0
- package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
- package/test/{framework/Route.test.js → Route.test.js} +105 -1
- package/test/{framework/View → View}/EarlyHints.test.js +1 -1
- package/test/WebSocket.test.js +238 -0
- package/test/scripts/check-coverage.js +4 -4
- package/test/cli/Cli.test.js +0 -36
- package/test/core/Commands.test.js +0 -538
- package/test/core/Config.test.js +0 -1432
- package/test/core/Lang.test.js +0 -250
- package/test/core/Odac.test.js +0 -234
- package/test/core/Process.test.js +0 -156
- package/test/framework/WebSocket.test.js +0 -100
- package/test/server/Api.test.js +0 -647
- package/test/server/DNS.test.js +0 -2050
- package/test/server/DNS.test.js.bak +0 -2084
- package/test/server/Hub.test.js +0 -497
- package/test/server/Log.test.js +0 -73
- package/test/server/Mail.account.test_.js +0 -460
- package/test/server/Mail.init.test_.js +0 -411
- package/test/server/Mail.test_.js +0 -1340
- package/test/server/SSL.test_.js +0 -1491
- package/test/server/Server.test.js +0 -765
- package/test/server/Service.test_.js +0 -1127
- package/test/server/Subdomain.test.js +0 -440
- package/test/server/Web/Firewall.test.js +0 -175
- package/test/server/Web/Proxy.test.js +0 -397
- package/test/server/Web.test.js +0 -1494
- package/test/server/__mocks__/acme-client.js +0 -17
- package/test/server/__mocks__/bcrypt.js +0 -50
- package/test/server/__mocks__/child_process.js +0 -389
- package/test/server/__mocks__/crypto.js +0 -432
- package/test/server/__mocks__/fs.js +0 -450
- package/test/server/__mocks__/globalOdac.js +0 -227
- package/test/server/__mocks__/http.js +0 -575
- package/test/server/__mocks__/https.js +0 -272
- package/test/server/__mocks__/index.js +0 -249
- package/test/server/__mocks__/mail/server.js +0 -100
- package/test/server/__mocks__/mail/smtp.js +0 -31
- package/test/server/__mocks__/mailparser.js +0 -81
- package/test/server/__mocks__/net.js +0 -369
- package/test/server/__mocks__/node-forge.js +0 -328
- package/test/server/__mocks__/os.js +0 -320
- package/test/server/__mocks__/path.js +0 -291
- package/test/server/__mocks__/selfsigned.js +0 -8
- package/test/server/__mocks__/server/src/mail/server.js +0 -100
- package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
- package/test/server/__mocks__/smtp-server.js +0 -106
- package/test/server/__mocks__/sqlite3.js +0 -394
- package/test/server/__mocks__/testFactories.js +0 -299
- package/test/server/__mocks__/testHelpers.js +0 -363
- package/test/server/__mocks__/tls.js +0 -229
- /package/template/{config.json → odac.json} +0 -0
package/src/Route.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
+
const fsPromises = fs.promises
|
|
2
3
|
|
|
3
4
|
const Cron = require('./Route/Cron.js')
|
|
4
5
|
const Internal = require('./Route/Internal.js')
|
|
@@ -68,7 +69,12 @@ class Route {
|
|
|
68
69
|
routes = {}
|
|
69
70
|
middlewares = {}
|
|
70
71
|
_pendingMiddlewares = []
|
|
72
|
+
_pendingRouteLoads = []
|
|
71
73
|
#wsServer = new WebSocketServer()
|
|
74
|
+
#configCache = {}
|
|
75
|
+
#paramIndex = {}
|
|
76
|
+
#paramIndexDirty = false
|
|
77
|
+
#publicCache = {}
|
|
72
78
|
auth = {
|
|
73
79
|
page: (path, authFile, file) => this.authPage(path, authFile, file),
|
|
74
80
|
post: (path, authFile, file) => this.authPost(path, authFile, file),
|
|
@@ -115,6 +121,21 @@ class Route {
|
|
|
115
121
|
const middlewareResult = await this.#runMiddlewares(Odac, controller.middlewares)
|
|
116
122
|
if (middlewareResult !== undefined) return middlewareResult
|
|
117
123
|
|
|
124
|
+
if (controller.action) {
|
|
125
|
+
const ControllerClass = controller.cache
|
|
126
|
+
try {
|
|
127
|
+
const instance = new ControllerClass(Odac)
|
|
128
|
+
if (typeof instance[controller.action] === 'function') {
|
|
129
|
+
return instance[controller.action](Odac)
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
if (typeof ControllerClass[controller.action] === 'function') {
|
|
133
|
+
return ControllerClass[controller.action](Odac)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return Odac.Request.abort(500)
|
|
137
|
+
}
|
|
138
|
+
|
|
118
139
|
if (typeof controller.cache === 'function') {
|
|
119
140
|
return controller.cache(Odac)
|
|
120
141
|
}
|
|
@@ -131,6 +152,7 @@ class Route {
|
|
|
131
152
|
if (['post', 'put', 'patch', 'delete'].includes(Odac.Request.method)) {
|
|
132
153
|
const formToken = await Odac.request('_odac_form_token')
|
|
133
154
|
if (formToken) {
|
|
155
|
+
Odac.Request.setSession()
|
|
134
156
|
await Internal.processForm(Odac)
|
|
135
157
|
}
|
|
136
158
|
}
|
|
@@ -157,20 +179,40 @@ class Route {
|
|
|
157
179
|
Odac.Request.isAjaxLoad = true
|
|
158
180
|
Odac.Request.clientSkeleton = Odac.Request.header('X-Odac-Skeleton')
|
|
159
181
|
}
|
|
160
|
-
if (Odac.Config
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
182
|
+
if (Odac.Config?.route?.[url]) {
|
|
183
|
+
// PROD CACHE HIT
|
|
184
|
+
if (!Odac.Config.debug && this.#configCache[url]) {
|
|
185
|
+
const cached = this.#configCache[url]
|
|
186
|
+
Odac.Request.header('Content-Type', cached.type)
|
|
187
|
+
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
188
|
+
Odac.Request.header('Content-Length', cached.size)
|
|
189
|
+
return cached.content
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const filePath = Odac.Config.route[url]
|
|
193
|
+
try {
|
|
194
|
+
const content = await fsPromises.readFile(filePath)
|
|
195
|
+
let type = 'text/html'
|
|
196
|
+
if (filePath.includes('.')) {
|
|
197
|
+
let arr = filePath.split('.')
|
|
198
|
+
type = mime[arr[arr.length - 1]]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// PROD CACHE SET
|
|
202
|
+
if (!Odac.Config.debug) {
|
|
203
|
+
this.#configCache[url] = {
|
|
204
|
+
content,
|
|
205
|
+
type,
|
|
206
|
+
size: content.length
|
|
168
207
|
}
|
|
169
|
-
Odac.Request.header('Content-Type', type)
|
|
170
|
-
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
171
|
-
Odac.Request.header('Content-Length', stat.size)
|
|
172
|
-
return fs.readFileSync(Odac.Config.route[url])
|
|
173
208
|
}
|
|
209
|
+
|
|
210
|
+
Odac.Request.header('Content-Type', type)
|
|
211
|
+
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
212
|
+
Odac.Request.header('Content-Length', content.length)
|
|
213
|
+
return content
|
|
214
|
+
} catch {
|
|
215
|
+
// File not found or error, continue routing
|
|
174
216
|
}
|
|
175
217
|
}
|
|
176
218
|
for (let method of ['#' + Odac.Request.method, Odac.Request.method]) {
|
|
@@ -207,65 +249,153 @@ class Route {
|
|
|
207
249
|
if (typeof page === 'string') Odac.Request.page = page
|
|
208
250
|
return await this.#executeController(Odac, pageController)
|
|
209
251
|
}
|
|
210
|
-
if (url && !url.includes('/../')
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
Odac.Request.header('Content-Type', type)
|
|
252
|
+
if (url && !url.includes('/../')) {
|
|
253
|
+
const publicPath = `${__dir}/public${url}`
|
|
254
|
+
|
|
255
|
+
// PROD CACHE HIT (Metadata)
|
|
256
|
+
if (!Odac.Config.debug && this.#publicCache[publicPath]) {
|
|
257
|
+
const cached = this.#publicCache[publicPath]
|
|
258
|
+
Odac.Request.header('Content-Type', cached.type)
|
|
219
259
|
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
220
|
-
Odac.Request.header('Content-Length',
|
|
221
|
-
return fs.createReadStream(
|
|
260
|
+
Odac.Request.header('Content-Length', cached.size)
|
|
261
|
+
return fs.createReadStream(publicPath)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const stat = await fsPromises.stat(publicPath)
|
|
266
|
+
if (stat.isFile()) {
|
|
267
|
+
let type = 'text/html'
|
|
268
|
+
if (url.includes('.')) {
|
|
269
|
+
let arr = url.split('.')
|
|
270
|
+
type = mime[arr[arr.length - 1]]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// PROD CACHE SET (Metadata Only)
|
|
274
|
+
if (!Odac.Config.debug) {
|
|
275
|
+
this.#publicCache[publicPath] = {
|
|
276
|
+
type,
|
|
277
|
+
size: stat.size
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
Odac.Request.header('Content-Type', type)
|
|
282
|
+
Odac.Request.header('Cache-Control', 'public, max-age=31536000')
|
|
283
|
+
Odac.Request.header('Content-Length', stat.size)
|
|
284
|
+
return fs.createReadStream(publicPath)
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// File not found in public
|
|
222
288
|
}
|
|
223
289
|
}
|
|
224
290
|
|
|
225
291
|
return Odac.Request.abort(404)
|
|
226
292
|
}
|
|
227
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Resolves a URL to its matching route controller using a two-phase strategy:
|
|
296
|
+
* Phase 1 — O(1) exact hash-map match for static routes.
|
|
297
|
+
* Phase 2 — Pre-indexed parametric match grouped by segment count to avoid full iteration.
|
|
298
|
+
*/
|
|
228
299
|
#controller(route, method, url) {
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
300
|
+
const methodRoutes = this.routes[route]?.[method]
|
|
301
|
+
if (!methodRoutes) return false
|
|
302
|
+
|
|
303
|
+
// Phase 1: O(1) exact match
|
|
304
|
+
if (methodRoutes[url]) return methodRoutes[url]
|
|
305
|
+
|
|
306
|
+
// Phase 2: Parametric match via pre-built index (lazy rebuild on dirty)
|
|
307
|
+
if (this.#paramIndexDirty) {
|
|
308
|
+
this.#buildParamIndex()
|
|
309
|
+
this.#paramIndexDirty = false
|
|
310
|
+
}
|
|
311
|
+
const methodIndex = this.#paramIndex[route]?.[method]
|
|
312
|
+
if (!methodIndex) return false
|
|
313
|
+
|
|
314
|
+
const urlSegments = url.split('/')
|
|
315
|
+
const candidates = methodIndex.get(urlSegments.length)
|
|
316
|
+
if (!candidates) return false
|
|
317
|
+
|
|
318
|
+
candidateLoop: for (const {paramMap, staticChecks, routeObj} of candidates) {
|
|
319
|
+
for (const {index, value} of staticChecks) {
|
|
320
|
+
if (urlSegments[index] !== value) continue candidateLoop
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const params = {}
|
|
324
|
+
for (const {index, name} of paramMap) {
|
|
325
|
+
params[name] = urlSegments[index]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
action: routeObj.action,
|
|
330
|
+
cache: routeObj.cache,
|
|
331
|
+
file: routeObj.file,
|
|
332
|
+
middlewares: routeObj.middlewares,
|
|
333
|
+
params,
|
|
334
|
+
token: routeObj.token
|
|
246
335
|
}
|
|
247
|
-
if (next) continue
|
|
248
|
-
if (arr.join('/') === key)
|
|
249
|
-
return {
|
|
250
|
-
params: params,
|
|
251
|
-
cache: this.routes[route][method][key].cache,
|
|
252
|
-
token: this.routes[route][method][key].token,
|
|
253
|
-
middlewares: this.routes[route][method][key].middlewares,
|
|
254
|
-
file: this.routes[route][method][key].file
|
|
255
|
-
}
|
|
256
336
|
}
|
|
337
|
+
|
|
257
338
|
return false
|
|
258
339
|
}
|
|
259
340
|
|
|
260
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Builds a segment-count-grouped index of parametric routes for O(1) lookup by segment count.
|
|
343
|
+
* Eliminates per-request string parsing by pre-computing segment metadata at init time.
|
|
344
|
+
* Called after every route initialization cycle (startup + hot-reload).
|
|
345
|
+
*/
|
|
346
|
+
#buildParamIndex() {
|
|
347
|
+
const index = {}
|
|
348
|
+
for (const route in this.routes) {
|
|
349
|
+
index[route] = {}
|
|
350
|
+
for (const method in this.routes[route]) {
|
|
351
|
+
const methodMap = new Map()
|
|
352
|
+
for (const key in this.routes[route][method]) {
|
|
353
|
+
if (!key.includes('{')) continue
|
|
354
|
+
const routeObj = this.routes[route][method][key]
|
|
355
|
+
if (!routeObj) continue
|
|
356
|
+
|
|
357
|
+
const segments = key.split('/')
|
|
358
|
+
const paramMap = []
|
|
359
|
+
const staticChecks = []
|
|
360
|
+
for (let i = 0; i < segments.length; i++) {
|
|
361
|
+
if (segments[i].startsWith('{') && segments[i].endsWith('}')) {
|
|
362
|
+
paramMap.push({index: i, name: segments[i].slice(1, -1)})
|
|
363
|
+
} else {
|
|
364
|
+
staticChecks.push({index: i, value: segments[i]})
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const count = segments.length
|
|
369
|
+
let bucket = methodMap.get(count)
|
|
370
|
+
if (!bucket) {
|
|
371
|
+
bucket = []
|
|
372
|
+
methodMap.set(count, bucket)
|
|
373
|
+
}
|
|
374
|
+
bucket.push({key, paramMap, routeObj, staticChecks})
|
|
375
|
+
}
|
|
376
|
+
if (methodMap.size > 0) {
|
|
377
|
+
index[route][method] = methodMap
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
this.#paramIndex = index
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async #loadMiddlewares() {
|
|
261
385
|
const middlewareDir = `${__dir}/middleware/`
|
|
262
|
-
|
|
386
|
+
try {
|
|
387
|
+
await fsPromises.access(middlewareDir)
|
|
388
|
+
} catch {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
263
391
|
|
|
264
|
-
|
|
392
|
+
const files = await fsPromises.readdir(middlewareDir)
|
|
393
|
+
for (const file of files) {
|
|
265
394
|
if (!file.endsWith('.js')) continue
|
|
266
395
|
const name = file.replace('.js', '')
|
|
267
396
|
const path = `${middlewareDir}${file}`
|
|
268
|
-
const
|
|
397
|
+
const stat = await fsPromises.stat(path)
|
|
398
|
+
const mtime = stat.mtimeMs
|
|
269
399
|
|
|
270
400
|
if (this.middlewares[name] && this.middlewares[name].mtime >= mtime - 1000) continue
|
|
271
401
|
|
|
@@ -278,70 +408,127 @@ class Route {
|
|
|
278
408
|
}
|
|
279
409
|
}
|
|
280
410
|
|
|
281
|
-
#init() {
|
|
411
|
+
async #init() {
|
|
282
412
|
if (this.loading) return
|
|
283
413
|
this.loading = true
|
|
284
|
-
this.#loadMiddlewares()
|
|
414
|
+
await this.#loadMiddlewares()
|
|
285
415
|
const classDir = `${__dir}/class/`
|
|
286
|
-
|
|
287
|
-
|
|
416
|
+
try {
|
|
417
|
+
await fsPromises.access(classDir)
|
|
418
|
+
const files = await fsPromises.readdir(classDir)
|
|
419
|
+
for (const file of files) {
|
|
288
420
|
if (!file.endsWith('.js')) continue
|
|
289
421
|
let name = file.replace('.js', '')
|
|
290
422
|
if (!Odac.Route.class) Odac.Route.class = {}
|
|
423
|
+
const filePath = `${__dir}/class/${file}`
|
|
424
|
+
|
|
425
|
+
let shouldLoad = true
|
|
426
|
+
let stat = null
|
|
427
|
+
|
|
291
428
|
if (Odac.Route.class[name]) {
|
|
292
|
-
|
|
293
|
-
if (Odac.Route.class[name].mtime >=
|
|
294
|
-
|
|
429
|
+
stat = await fsPromises.stat(Odac.Route.class[name].path)
|
|
430
|
+
if (Odac.Route.class[name].mtime >= stat.mtimeMs || Date.now() < stat.mtimeMs + 1000) {
|
|
431
|
+
shouldLoad = false
|
|
432
|
+
} else {
|
|
433
|
+
delete require.cache[require.resolve(Odac.Route.class[name].path)]
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
stat = await fsPromises.stat(filePath)
|
|
295
437
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
438
|
+
|
|
439
|
+
if (shouldLoad) {
|
|
440
|
+
Odac.Route.class[name] = {
|
|
441
|
+
path: filePath,
|
|
442
|
+
mtime: stat.mtimeMs,
|
|
443
|
+
module: require(filePath)
|
|
444
|
+
}
|
|
300
445
|
}
|
|
301
446
|
}
|
|
447
|
+
} catch {
|
|
448
|
+
// Class dir might not exist
|
|
302
449
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const dir = await fsPromises.readdir(`${__dir}/route/`)
|
|
453
|
+
for (const file of dir) {
|
|
454
|
+
if (!file.endsWith('.js')) continue
|
|
455
|
+
const filePath = `${__dir}/route/${file}`
|
|
456
|
+
const stat = await fsPromises.stat(filePath)
|
|
457
|
+
let mtime = stat.mtimeMs
|
|
458
|
+
Odac.Route.buff = file.replace('.js', '')
|
|
459
|
+
|
|
460
|
+
if (!routes2[Odac.Route.buff] || routes2[Odac.Route.buff] < mtime - 1000) {
|
|
461
|
+
delete require.cache[require.resolve(filePath)]
|
|
462
|
+
routes2[Odac.Route.buff] = mtime
|
|
463
|
+
const routeModule = require(filePath)
|
|
464
|
+
if (typeof routeModule === 'function') {
|
|
465
|
+
// routeModule calls .set(), which pushes promises to _pendingRouteLoads
|
|
466
|
+
routeModule(Odac)
|
|
467
|
+
}
|
|
314
468
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
469
|
+
|
|
470
|
+
// Wait for all route sets to complete for this file
|
|
471
|
+
await Promise.all(this._pendingRouteLoads)
|
|
472
|
+
this._pendingRouteLoads = []
|
|
473
|
+
|
|
474
|
+
// Clean up deleted routes logic
|
|
475
|
+
for (const type of ['page', '#page', 'post', '#post', 'get', '#get', 'error']) {
|
|
476
|
+
if (!this.routes[Odac.Route.buff]) continue
|
|
477
|
+
if (!this.routes[Odac.Route.buff][type]) continue
|
|
478
|
+
for (const route in this.routes[Odac.Route.buff][type]) {
|
|
479
|
+
const routeObj = this.routes[Odac.Route.buff][type][route]
|
|
480
|
+
if (!routeObj) continue
|
|
481
|
+
|
|
482
|
+
if (routes2[Odac.Route.buff] > routeObj.loaded) {
|
|
483
|
+
if (routeObj.path) {
|
|
484
|
+
try {
|
|
485
|
+
delete require.cache[require.resolve(routeObj.path)]
|
|
486
|
+
} catch {
|
|
487
|
+
// Silently fail
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
delete this.routes[Odac.Route.buff][type][route]
|
|
491
|
+
} else if (routeObj) {
|
|
492
|
+
if (typeof routeObj.type === 'function') continue
|
|
493
|
+
// Check if controller file modified
|
|
494
|
+
try {
|
|
495
|
+
const cStat = await fsPromises.stat(routeObj.path)
|
|
496
|
+
if (routeObj.mtime < cStat.mtimeMs) {
|
|
497
|
+
delete require.cache[require.resolve(routeObj.path)]
|
|
498
|
+
routeObj.cache = require(routeObj.path)
|
|
499
|
+
routeObj.mtime = cStat.mtimeMs
|
|
500
|
+
}
|
|
501
|
+
} catch {
|
|
502
|
+
// Controller file might have been deleted?
|
|
503
|
+
}
|
|
329
504
|
}
|
|
330
505
|
}
|
|
331
506
|
}
|
|
507
|
+
delete Odac.Route.buff
|
|
508
|
+
}
|
|
509
|
+
} catch (e) {
|
|
510
|
+
if (e.code === 'ENOENT') {
|
|
511
|
+
console.error(`[ODAC] Route directory not found: \x1b[33m${__dir}/route/\x1b[0m`)
|
|
512
|
+
} else {
|
|
513
|
+
console.error(e)
|
|
332
514
|
}
|
|
333
|
-
delete Odac.Route.buff
|
|
334
515
|
}
|
|
516
|
+
|
|
335
517
|
Cron.init()
|
|
518
|
+
this.#buildParamIndex()
|
|
336
519
|
this.loading = false
|
|
337
520
|
}
|
|
338
521
|
|
|
339
|
-
init() {
|
|
340
|
-
this.#init()
|
|
522
|
+
async init() {
|
|
523
|
+
await this.#init()
|
|
341
524
|
this.#registerInternalRoutes()
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
525
|
+
|
|
526
|
+
// Hot Reload only in Debug Mode
|
|
527
|
+
if (Odac.Config.debug) {
|
|
528
|
+
setInterval(async () => {
|
|
529
|
+
await this.#init()
|
|
530
|
+
}, 5000)
|
|
531
|
+
}
|
|
345
532
|
}
|
|
346
533
|
|
|
347
534
|
#registerInternalRoutes() {
|
|
@@ -456,6 +643,8 @@ class Route {
|
|
|
456
643
|
}
|
|
457
644
|
|
|
458
645
|
set(type, url, file, options = {}) {
|
|
646
|
+
const capturedMiddlewares = this._pendingMiddlewares.length > 0 ? [...this._pendingMiddlewares] : undefined
|
|
647
|
+
|
|
459
648
|
if (Array.isArray(type)) {
|
|
460
649
|
type = type.map(t => t.toLowerCase())
|
|
461
650
|
for (const t of type) {
|
|
@@ -473,39 +662,84 @@ class Route {
|
|
|
473
662
|
const isFunction = typeof file === 'function'
|
|
474
663
|
let path = `${__dir}/route/${Odac.Route.buff}.js`
|
|
475
664
|
|
|
665
|
+
let action = null
|
|
666
|
+
|
|
476
667
|
if (!isFunction && file) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
668
|
+
if (typeof file === 'string' && file.includes('@')) {
|
|
669
|
+
let arr = file.split('@')
|
|
670
|
+
file = arr[0]
|
|
671
|
+
action = arr[1]
|
|
672
|
+
path = `${__dir}/controller/${file.replace(/\./g, '/')}.js`
|
|
673
|
+
} else {
|
|
674
|
+
path = `${__dir}/controller/${type.replace('#', '')}/${file}.js`
|
|
675
|
+
if (typeof file === 'string' && file.includes('.')) {
|
|
676
|
+
let arr = file.split('.')
|
|
677
|
+
path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
|
|
678
|
+
}
|
|
481
679
|
}
|
|
482
680
|
}
|
|
483
681
|
|
|
484
682
|
if (!this.routes[Odac.Route.buff]) this.routes[Odac.Route.buff] = {}
|
|
485
683
|
if (!this.routes[Odac.Route.buff][type]) this.routes[Odac.Route.buff][type] = {}
|
|
486
684
|
|
|
487
|
-
|
|
488
|
-
this.routes[Odac.Route.buff][type][url]
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
685
|
+
const task = async () => {
|
|
686
|
+
if (this.routes[Odac.Route.buff][type][url]) {
|
|
687
|
+
this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
|
|
688
|
+
if (!isFunction) {
|
|
689
|
+
try {
|
|
690
|
+
const stat = await fsPromises.stat(path)
|
|
691
|
+
if (this.routes[Odac.Route.buff][type][url].mtime < stat.mtimeMs) {
|
|
692
|
+
delete this.routes[Odac.Route.buff][type][url]
|
|
693
|
+
delete require.cache[require.resolve(path)]
|
|
694
|
+
} else {
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
// File error, proceed to reload or re-set
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (isFunction) {
|
|
706
|
+
if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
|
|
707
|
+
this.routes[Odac.Route.buff][type][url].cache = file
|
|
708
|
+
this.routes[Odac.Route.buff][type][url].type = 'function'
|
|
709
|
+
this.routes[Odac.Route.buff][type][url].file = file
|
|
710
|
+
this.routes[Odac.Route.buff][type][url].mtime = Date.now()
|
|
711
|
+
this.routes[Odac.Route.buff][type][url].path = path
|
|
712
|
+
this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
|
|
713
|
+
this.routes[Odac.Route.buff][type][url].token = options.token ?? true
|
|
714
|
+
this.routes[Odac.Route.buff][type][url].action = action
|
|
715
|
+
|
|
716
|
+
this.routes[Odac.Route.buff][type][url].middlewares = capturedMiddlewares
|
|
717
|
+
} else {
|
|
718
|
+
try {
|
|
719
|
+
const stat = await fsPromises.stat(path)
|
|
720
|
+
if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
|
|
721
|
+
this.routes[Odac.Route.buff][type][url].cache = require(path)
|
|
722
|
+
this.routes[Odac.Route.buff][type][url].type = 'controller'
|
|
723
|
+
this.routes[Odac.Route.buff][type][url].file = file
|
|
724
|
+
this.routes[Odac.Route.buff][type][url].mtime = stat.mtimeMs
|
|
725
|
+
this.routes[Odac.Route.buff][type][url].path = path
|
|
726
|
+
this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
|
|
727
|
+
this.routes[Odac.Route.buff][type][url].token = options.token ?? true
|
|
728
|
+
this.routes[Odac.Route.buff][type][url].action = action
|
|
729
|
+
|
|
730
|
+
this.routes[Odac.Route.buff][type][url].middlewares = capturedMiddlewares
|
|
731
|
+
} catch {
|
|
732
|
+
if (file && typeof file === 'string') {
|
|
733
|
+
console.error(`\x1b[31m[Odac]\x1b[0m Controller not found: \x1b[33m${path}\x1b[0m`)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
493
737
|
}
|
|
494
738
|
|
|
495
|
-
|
|
496
|
-
if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
|
|
497
|
-
this.routes[Odac.Route.buff][type][url].cache = isFunction ? file : require(path)
|
|
498
|
-
this.routes[Odac.Route.buff][type][url].type = isFunction ? 'function' : 'controller'
|
|
499
|
-
this.routes[Odac.Route.buff][type][url].file = file
|
|
500
|
-
this.routes[Odac.Route.buff][type][url].mtime = isFunction ? Date.now() : fs.statSync(path).mtimeMs
|
|
501
|
-
this.routes[Odac.Route.buff][type][url].path = path
|
|
502
|
-
this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
|
|
503
|
-
this.routes[Odac.Route.buff][type][url].token = options.token ?? true
|
|
739
|
+
this._pendingRouteLoads.push(task())
|
|
504
740
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
console.error(`\x1b[31m[Odac]\x1b[0m Controller not found: \x1b[33m${path}\x1b[0m`)
|
|
508
|
-
}
|
|
741
|
+
// Mark parametric index for rebuild when route contains dynamic segments
|
|
742
|
+
if (url.includes('{')) this.#paramIndexDirty = true
|
|
509
743
|
|
|
510
744
|
return this
|
|
511
745
|
}
|
|
@@ -566,15 +800,31 @@ class Route {
|
|
|
566
800
|
return this
|
|
567
801
|
}
|
|
568
802
|
|
|
569
|
-
authPost(path, authFile, file) {
|
|
570
|
-
|
|
571
|
-
|
|
803
|
+
authPost(path, authFile, file, options) {
|
|
804
|
+
let opts = options
|
|
805
|
+
let publicFile = file
|
|
806
|
+
|
|
807
|
+
if (publicFile && typeof publicFile === 'object' && !opts) {
|
|
808
|
+
opts = publicFile
|
|
809
|
+
publicFile = undefined
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (authFile) this.set('#post', path, authFile, opts)
|
|
813
|
+
if (publicFile) this.post(path, publicFile, opts)
|
|
572
814
|
return this
|
|
573
815
|
}
|
|
574
816
|
|
|
575
|
-
authGet(path, authFile, file) {
|
|
576
|
-
|
|
577
|
-
|
|
817
|
+
authGet(path, authFile, file, options) {
|
|
818
|
+
let opts = options
|
|
819
|
+
let publicFile = file
|
|
820
|
+
|
|
821
|
+
if (publicFile && typeof publicFile === 'object' && !opts) {
|
|
822
|
+
opts = publicFile
|
|
823
|
+
publicFile = undefined
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (authFile) this.set('#get', path, authFile, opts)
|
|
827
|
+
if (publicFile) this.get(path, publicFile, opts)
|
|
578
828
|
return this
|
|
579
829
|
}
|
|
580
830
|
|
|
@@ -686,7 +936,7 @@ class Route {
|
|
|
686
936
|
return res
|
|
687
937
|
}
|
|
688
938
|
|
|
689
|
-
this.#wsServer.route(path, wrappedHandler)
|
|
939
|
+
this.#wsServer.route(path, wrappedHandler, options)
|
|
690
940
|
}
|
|
691
941
|
|
|
692
942
|
handleWebSocketUpgrade(req, socket, head, Odac) {
|