odac 1.0.1 → 1.2.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.
Files changed (143) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +33 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/auto-pr-description.yml +3 -1
  6. package/.github/workflows/release.yml +42 -1
  7. package/.github/workflows/test-coverage.yml +6 -5
  8. package/.github/workflows/test-publish.yml +36 -0
  9. package/.husky/pre-commit +10 -0
  10. package/.husky/pre-push +13 -0
  11. package/.releaserc.js +3 -3
  12. package/CHANGELOG.md +184 -0
  13. package/README.md +53 -34
  14. package/bin/odac.js +181 -49
  15. package/client/odac.js +878 -995
  16. package/docs/backend/01-overview/03-development-server.md +39 -46
  17. package/docs/backend/02-structure/01-typical-project-layout.md +59 -25
  18. package/docs/backend/03-config/00-configuration-overview.md +15 -6
  19. package/docs/backend/03-config/01-database-connection.md +3 -3
  20. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  21. package/docs/backend/03-config/03-request-timeout.md +1 -1
  22. package/docs/backend/03-config/04-environment-variables.md +4 -4
  23. package/docs/backend/03-config/05-early-hints.md +2 -2
  24. package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
  25. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  26. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  27. package/docs/backend/04-routing/09-websocket.md +29 -0
  28. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  29. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
  30. package/docs/backend/05-controllers/03-controller-classes.md +61 -55
  31. package/docs/backend/05-forms/01-custom-forms.md +103 -95
  32. package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
  33. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  34. package/docs/backend/07-views/02-rendering-a-view.md +1 -1
  35. package/docs/backend/07-views/03-variables.md +5 -5
  36. package/docs/backend/07-views/04-request-data.md +1 -1
  37. package/docs/backend/07-views/08-backend-javascript.md +1 -1
  38. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  39. package/docs/backend/08-database/01-getting-started.md +100 -0
  40. package/docs/backend/08-database/02-basics.md +136 -0
  41. package/docs/backend/08-database/03-advanced.md +84 -0
  42. package/docs/backend/08-database/04-migrations.md +48 -0
  43. package/docs/backend/09-validation/01-the-validator-service.md +1 -0
  44. package/docs/backend/10-authentication/03-register.md +9 -2
  45. package/docs/backend/10-authentication/04-odac-register-forms.md +48 -48
  46. package/docs/backend/10-authentication/05-session-management.md +16 -2
  47. package/docs/backend/10-authentication/06-odac-login-forms.md +50 -50
  48. package/docs/backend/10-authentication/07-magic-links.md +134 -0
  49. package/docs/backend/11-mail/01-the-mail-service.md +118 -28
  50. package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
  51. package/docs/backend/13-utilities/01-odac-var.md +7 -7
  52. package/docs/backend/13-utilities/02-ipc.md +73 -0
  53. package/docs/frontend/01-overview/01-introduction.md +5 -1
  54. package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
  55. package/docs/index.json +21 -125
  56. package/eslint.config.mjs +5 -47
  57. package/jest.config.js +1 -1
  58. package/package.json +16 -7
  59. package/src/Auth.js +414 -121
  60. package/src/Config.js +12 -7
  61. package/src/Database.js +188 -0
  62. package/src/Env.js +3 -1
  63. package/src/Ipc.js +337 -0
  64. package/src/Lang.js +9 -2
  65. package/src/Mail.js +408 -37
  66. package/src/Odac.js +105 -40
  67. package/src/Request.js +71 -49
  68. package/src/Route/Cron.js +62 -18
  69. package/src/Route/Internal.js +215 -12
  70. package/src/Route/Middleware.js +7 -2
  71. package/src/Route.js +372 -109
  72. package/src/Server.js +118 -12
  73. package/src/Storage.js +169 -0
  74. package/src/Token.js +6 -4
  75. package/src/Validator.js +95 -3
  76. package/src/Var.js +22 -6
  77. package/src/View/EarlyHints.js +43 -33
  78. package/src/View/Form.js +210 -28
  79. package/src/View.js +108 -7
  80. package/src/WebSocket.js +18 -3
  81. package/template/odac.json +5 -0
  82. package/template/package.json +3 -1
  83. package/template/route/www.js +12 -10
  84. package/template/view/content/home.html +3 -3
  85. package/template/view/head/main.html +2 -2
  86. package/test/Client.test.js +168 -0
  87. package/test/Config.test.js +112 -0
  88. package/test/Lang.test.js +92 -0
  89. package/test/Odac.test.js +86 -0
  90. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  91. package/test/{framework/Route.test.js → Route.test.js} +1 -1
  92. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  93. package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
  94. package/test/scripts/check-coverage.js +4 -4
  95. package/docs/backend/08-database/01-database-connection.md +0 -99
  96. package/docs/backend/08-database/02-using-mysql.md +0 -322
  97. package/src/Mysql.js +0 -575
  98. package/template/config.json +0 -5
  99. package/test/cli/Cli.test.js +0 -36
  100. package/test/core/Candy.test.js +0 -234
  101. package/test/core/Commands.test.js +0 -538
  102. package/test/core/Config.test.js +0 -1432
  103. package/test/core/Lang.test.js +0 -250
  104. package/test/core/Process.test.js +0 -156
  105. package/test/server/Api.test.js +0 -647
  106. package/test/server/DNS.test.js +0 -2050
  107. package/test/server/DNS.test.js.bak +0 -2084
  108. package/test/server/Hub.test.js +0 -497
  109. package/test/server/Log.test.js +0 -73
  110. package/test/server/Mail.account.test_.js +0 -460
  111. package/test/server/Mail.init.test_.js +0 -411
  112. package/test/server/Mail.test_.js +0 -1340
  113. package/test/server/SSL.test_.js +0 -1491
  114. package/test/server/Server.test.js +0 -765
  115. package/test/server/Service.test_.js +0 -1127
  116. package/test/server/Subdomain.test.js +0 -440
  117. package/test/server/Web/Firewall.test.js +0 -175
  118. package/test/server/Web/Proxy.test.js +0 -397
  119. package/test/server/Web.test.js +0 -1494
  120. package/test/server/__mocks__/acme-client.js +0 -17
  121. package/test/server/__mocks__/bcrypt.js +0 -50
  122. package/test/server/__mocks__/child_process.js +0 -389
  123. package/test/server/__mocks__/crypto.js +0 -432
  124. package/test/server/__mocks__/fs.js +0 -450
  125. package/test/server/__mocks__/globalOdac.js +0 -227
  126. package/test/server/__mocks__/http.js +0 -575
  127. package/test/server/__mocks__/https.js +0 -272
  128. package/test/server/__mocks__/index.js +0 -249
  129. package/test/server/__mocks__/mail/server.js +0 -100
  130. package/test/server/__mocks__/mail/smtp.js +0 -31
  131. package/test/server/__mocks__/mailparser.js +0 -81
  132. package/test/server/__mocks__/net.js +0 -369
  133. package/test/server/__mocks__/node-forge.js +0 -328
  134. package/test/server/__mocks__/os.js +0 -320
  135. package/test/server/__mocks__/path.js +0 -291
  136. package/test/server/__mocks__/selfsigned.js +0 -8
  137. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  138. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  139. package/test/server/__mocks__/smtp-server.js +0 -106
  140. package/test/server/__mocks__/sqlite3.js +0 -394
  141. package/test/server/__mocks__/testFactories.js +0 -299
  142. package/test/server/__mocks__/testHelpers.js +0 -363
  143. package/test/server/__mocks__/tls.js +0 -229
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,13 +69,16 @@ class Route {
68
69
  routes = {}
69
70
  middlewares = {}
70
71
  _pendingMiddlewares = []
72
+ _pendingRouteLoads = []
71
73
  #wsServer = new WebSocketServer()
74
+ #configCache = {}
75
+ #publicCache = {}
72
76
  auth = {
73
77
  page: (path, authFile, file) => this.authPage(path, authFile, file),
74
78
  post: (path, authFile, file) => this.authPost(path, authFile, file),
75
79
  get: (path, authFile, file) => this.authGet(path, authFile, file),
76
80
  ws: (path, handler, options) => this.authWs(path, handler, options),
77
- use: (...middlewares) => new MiddlewareChain(this, [...middlewares.flat()])
81
+ use: (...middlewares) => new MiddlewareChain(this, [...middlewares.flat()], true)
78
82
  }
79
83
 
80
84
  async #runMiddlewares(Odac, middlewares) {
@@ -90,8 +94,13 @@ class Route {
90
94
 
91
95
  const result = await middleware(Odac)
92
96
 
97
+ if (Odac.Request.res.finished) {
98
+ return false
99
+ }
100
+
93
101
  if (result === false) {
94
- return Odac.Request.abort(403)
102
+ await Odac.Request.abort(403)
103
+ return false
95
104
  }
96
105
 
97
106
  if (result !== undefined && result !== true) {
@@ -101,15 +110,30 @@ class Route {
101
110
  }
102
111
 
103
112
  async #executeController(Odac, controller) {
104
- const middlewareResult = await this.#runMiddlewares(Odac, controller.middlewares)
105
- if (middlewareResult !== undefined) return middlewareResult
106
-
107
113
  if (controller.params) {
108
114
  for (let key in controller.params) {
109
115
  Odac.Request.data.url[key] = controller.params[key]
110
116
  }
111
117
  }
112
118
 
119
+ const middlewareResult = await this.#runMiddlewares(Odac, controller.middlewares)
120
+ if (middlewareResult !== undefined) return middlewareResult
121
+
122
+ if (controller.action) {
123
+ const ControllerClass = controller.cache
124
+ try {
125
+ const instance = new ControllerClass(Odac)
126
+ if (typeof instance[controller.action] === 'function') {
127
+ return instance[controller.action](Odac)
128
+ }
129
+ } catch {
130
+ if (typeof ControllerClass[controller.action] === 'function') {
131
+ return ControllerClass[controller.action](Odac)
132
+ }
133
+ }
134
+ return Odac.Request.abort(500)
135
+ }
136
+
113
137
  if (typeof controller.cache === 'function') {
114
138
  return controller.cache(Odac)
115
139
  }
@@ -126,6 +150,7 @@ class Route {
126
150
  if (['post', 'put', 'patch', 'delete'].includes(Odac.Request.method)) {
127
151
  const formToken = await Odac.request('_odac_form_token')
128
152
  if (formToken) {
153
+ Odac.Request.setSession()
129
154
  await Internal.processForm(Odac)
130
155
  }
131
156
  }
@@ -152,21 +177,40 @@ class Route {
152
177
  Odac.Request.isAjaxLoad = true
153
178
  Odac.Request.clientSkeleton = Odac.Request.header('X-Odac-Skeleton')
154
179
  }
155
- if (Odac.Config && Odac.Config.route && Odac.Config.route[url]) {
156
- Odac.Config.route[url] = Odac.Config.route[url].replace('${odac}', `${__dir}/node_modules/odac`)
157
- if (fs.existsSync(Odac.Config.route[url])) {
158
- let stat = fs.lstatSync(Odac.Config.route[url])
159
- if (stat.isFile()) {
160
- let type = 'text/html'
161
- if (Odac.Config.route[url].includes('.')) {
162
- let arr = Odac.Config.route[url].split('.')
163
- type = mime[arr[arr.length - 1]]
180
+ if (Odac.Config?.route?.[url]) {
181
+ // PROD CACHE HIT
182
+ if (!Odac.Config.debug && this.#configCache[url]) {
183
+ const cached = this.#configCache[url]
184
+ Odac.Request.header('Content-Type', cached.type)
185
+ Odac.Request.header('Cache-Control', 'public, max-age=31536000')
186
+ Odac.Request.header('Content-Length', cached.size)
187
+ return cached.content
188
+ }
189
+
190
+ const filePath = Odac.Config.route[url]
191
+ try {
192
+ const content = await fsPromises.readFile(filePath)
193
+ let type = 'text/html'
194
+ if (filePath.includes('.')) {
195
+ let arr = filePath.split('.')
196
+ type = mime[arr[arr.length - 1]]
197
+ }
198
+
199
+ // PROD CACHE SET
200
+ if (!Odac.Config.debug) {
201
+ this.#configCache[url] = {
202
+ content,
203
+ type,
204
+ size: content.length
164
205
  }
165
- Odac.Request.header('Content-Type', type)
166
- Odac.Request.header('Cache-Control', 'public, max-age=31536000')
167
- Odac.Request.header('Content-Length', stat.size)
168
- return fs.readFileSync(Odac.Config.route[url])
169
206
  }
207
+
208
+ Odac.Request.header('Content-Type', type)
209
+ Odac.Request.header('Cache-Control', 'public, max-age=31536000')
210
+ Odac.Request.header('Content-Length', content.length)
211
+ return content
212
+ } catch {
213
+ // File not found or error, continue routing
170
214
  }
171
215
  }
172
216
  for (let method of ['#' + Odac.Request.method, Odac.Request.method]) {
@@ -174,6 +218,10 @@ class Route {
174
218
  if (controller) {
175
219
  if (!method.startsWith('#') || (await Odac.Auth.check())) {
176
220
  Odac.Request.header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
221
+ Odac.Request.setSession()
222
+ const page = controller.cache?.file || controller.file
223
+ if (typeof page === 'string') Odac.Request.page = page
224
+
177
225
  if (
178
226
  ['post', 'get'].includes(Odac.Request.method) &&
179
227
  controller.token &&
@@ -187,28 +235,57 @@ class Route {
187
235
  }
188
236
  let authPageController = this.#controller(Odac.Request.route, '#page', url)
189
237
  if (authPageController && (await Odac.Auth.check())) {
190
- Odac.Request.page = authPageController.cache?.file || authPageController.file
238
+ Odac.Request.setSession()
239
+ const page = authPageController.cache?.file || authPageController.file
240
+ if (typeof page === 'string') Odac.Request.page = page
191
241
  return await this.#executeController(Odac, authPageController)
192
242
  }
193
243
  let pageController = this.#controller(Odac.Request.route, 'page', url)
194
244
  if (pageController) {
195
- Odac.Request.page = pageController.cache?.file || pageController.file
245
+ Odac.Request.setSession()
246
+ const page = pageController.cache?.file || pageController.file
247
+ if (typeof page === 'string') Odac.Request.page = page
196
248
  return await this.#executeController(Odac, pageController)
197
249
  }
198
- if (url && !url.includes('/../') && fs.existsSync(`${__dir}/public${url}`)) {
199
- let stat = fs.lstatSync(`${__dir}/public${url}`)
200
- if (stat.isFile()) {
201
- let type = 'text/html'
202
- if (url.includes('.')) {
203
- let arr = url.split('.')
204
- type = mime[arr[arr.length - 1]]
205
- }
206
- Odac.Request.header('Content-Type', type)
250
+ if (url && !url.includes('/../')) {
251
+ const publicPath = `${__dir}/public${url}`
252
+
253
+ // PROD CACHE HIT (Metadata)
254
+ if (!Odac.Config.debug && this.#publicCache[publicPath]) {
255
+ const cached = this.#publicCache[publicPath]
256
+ Odac.Request.header('Content-Type', cached.type)
207
257
  Odac.Request.header('Cache-Control', 'public, max-age=31536000')
208
- Odac.Request.header('Content-Length', stat.size)
209
- return fs.readFileSync(`${__dir}/public${url}`)
258
+ Odac.Request.header('Content-Length', cached.size)
259
+ return fs.createReadStream(publicPath)
260
+ }
261
+
262
+ try {
263
+ const stat = await fsPromises.stat(publicPath)
264
+ if (stat.isFile()) {
265
+ let type = 'text/html'
266
+ if (url.includes('.')) {
267
+ let arr = url.split('.')
268
+ type = mime[arr[arr.length - 1]]
269
+ }
270
+
271
+ // PROD CACHE SET (Metadata Only)
272
+ if (!Odac.Config.debug) {
273
+ this.#publicCache[publicPath] = {
274
+ type,
275
+ size: stat.size
276
+ }
277
+ }
278
+
279
+ Odac.Request.header('Content-Type', type)
280
+ Odac.Request.header('Cache-Control', 'public, max-age=31536000')
281
+ Odac.Request.header('Content-Length', stat.size)
282
+ return fs.createReadStream(publicPath)
283
+ }
284
+ } catch {
285
+ // File not found in public
210
286
  }
211
287
  }
288
+
212
289
  return Odac.Request.abort(404)
213
290
  }
214
291
 
@@ -237,21 +314,28 @@ class Route {
237
314
  params: params,
238
315
  cache: this.routes[route][method][key].cache,
239
316
  token: this.routes[route][method][key].token,
240
- middlewares: this.routes[route][method][key].middlewares
317
+ middlewares: this.routes[route][method][key].middlewares,
318
+ file: this.routes[route][method][key].file
241
319
  }
242
320
  }
243
321
  return false
244
322
  }
245
323
 
246
- #loadMiddlewares() {
324
+ async #loadMiddlewares() {
247
325
  const middlewareDir = `${__dir}/middleware/`
248
- if (!fs.existsSync(middlewareDir)) return
326
+ try {
327
+ await fsPromises.access(middlewareDir)
328
+ } catch {
329
+ return
330
+ }
249
331
 
250
- for (const file of fs.readdirSync(middlewareDir)) {
332
+ const files = await fsPromises.readdir(middlewareDir)
333
+ for (const file of files) {
251
334
  if (!file.endsWith('.js')) continue
252
335
  const name = file.replace('.js', '')
253
336
  const path = `${middlewareDir}${file}`
254
- const mtime = fs.statSync(path).mtimeMs
337
+ const stat = await fsPromises.stat(path)
338
+ const mtime = stat.mtimeMs
255
339
 
256
340
  if (this.middlewares[name] && this.middlewares[name].mtime >= mtime - 1000) continue
257
341
 
@@ -264,63 +348,123 @@ class Route {
264
348
  }
265
349
  }
266
350
 
267
- #init() {
351
+ async #init() {
268
352
  if (this.loading) return
269
353
  this.loading = true
270
- this.#loadMiddlewares()
271
- for (const file of fs.readdirSync(`${__dir}/controller/`)) {
272
- if (!file.endsWith('.js')) continue
273
- let name = file.replace('.js', '')
274
- if (!Odac.Route.class) Odac.Route.class = {}
275
- if (Odac.Route.class[name]) {
276
- if (Odac.Route.class[name].mtime >= fs.statSync(Odac.Route.class[name].path).mtimeMs + 1000) continue
277
- delete require.cache[require.resolve(Odac.Route.class[name].path)]
278
- }
279
- Odac.Route.class[name] = {
280
- path: `${__dir}/controller/${file}`,
281
- mtime: fs.statSync(`${__dir}/controller/${file}`).mtimeMs,
282
- module: require(`${__dir}/controller/${file}`)
354
+ await this.#loadMiddlewares()
355
+ const classDir = `${__dir}/class/`
356
+ try {
357
+ await fsPromises.access(classDir)
358
+ const files = await fsPromises.readdir(classDir)
359
+ for (const file of files) {
360
+ if (!file.endsWith('.js')) continue
361
+ let name = file.replace('.js', '')
362
+ if (!Odac.Route.class) Odac.Route.class = {}
363
+ const filePath = `${__dir}/class/${file}`
364
+
365
+ let shouldLoad = true
366
+ let stat = null
367
+
368
+ if (Odac.Route.class[name]) {
369
+ stat = await fsPromises.stat(Odac.Route.class[name].path)
370
+ if (Odac.Route.class[name].mtime >= stat.mtimeMs || Date.now() < stat.mtimeMs + 1000) {
371
+ shouldLoad = false
372
+ } else {
373
+ delete require.cache[require.resolve(Odac.Route.class[name].path)]
374
+ }
375
+ } else {
376
+ stat = await fsPromises.stat(filePath)
377
+ }
378
+
379
+ if (shouldLoad) {
380
+ Odac.Route.class[name] = {
381
+ path: filePath,
382
+ mtime: stat.mtimeMs,
383
+ module: require(filePath)
384
+ }
385
+ }
283
386
  }
387
+ } catch {
388
+ // Class dir might not exist
284
389
  }
285
- let dir = fs.readdirSync(`${__dir}/route/`)
286
- for (const file of dir) {
287
- if (!file.endsWith('.js')) continue
288
- let mtime = fs.statSync(`${__dir}/route/${file}`).mtimeMs
289
- Odac.Route.buff = file.replace('.js', '')
290
- if (!routes2[Odac.Route.buff] || routes2[Odac.Route.buff] < mtime - 1000) {
291
- delete require.cache[require.resolve(`${__dir}/route/${file}`)]
292
- routes2[Odac.Route.buff] = mtime
293
- require(`${__dir}/route/${file}`)
294
- }
295
- for (const type of ['page', '#page', 'post', '#post', 'get', '#get', 'error']) {
296
- if (!this.routes[Odac.Route.buff]) continue
297
- if (!this.routes[Odac.Route.buff][type]) continue
298
- for (const route in this.routes[Odac.Route.buff][type]) {
299
- if (routes2[Odac.Route.buff] > this.routes[Odac.Route.buff][type][route].loaded) {
300
- delete require.cache[require.resolve(this.routes[Odac.Route.buff][type][route].path)]
301
- delete this.routes[Odac.Route.buff][type][route]
302
- } else if (this.routes[Odac.Route.buff][type][route]) {
303
- if (typeof this.routes[Odac.Route.buff][type][route].type === 'function') continue
304
- if (this.routes[Odac.Route.buff][type][route].mtime < fs.statSync(this.routes[Odac.Route.buff][type][route].path).mtimeMs) {
305
- delete require.cache[require.resolve(this.routes[Odac.Route.buff][type][route].path)]
306
- this.routes[Odac.Route.buff][type][route].cache = require(this.routes[Odac.Route.buff][type][route].path)
307
- this.routes[Odac.Route.buff][type][route].mtime = fs.statSync(this.routes[Odac.Route.buff][type][route].path).mtimeMs
390
+
391
+ try {
392
+ const dir = await fsPromises.readdir(`${__dir}/route/`)
393
+ for (const file of dir) {
394
+ if (!file.endsWith('.js')) continue
395
+ const filePath = `${__dir}/route/${file}`
396
+ const stat = await fsPromises.stat(filePath)
397
+ let mtime = stat.mtimeMs
398
+ Odac.Route.buff = file.replace('.js', '')
399
+
400
+ if (!routes2[Odac.Route.buff] || routes2[Odac.Route.buff] < mtime - 1000) {
401
+ delete require.cache[require.resolve(filePath)]
402
+ routes2[Odac.Route.buff] = mtime
403
+ const routeModule = require(filePath)
404
+ if (typeof routeModule === 'function') {
405
+ // routeModule calls .set(), which pushes promises to _pendingRouteLoads
406
+ routeModule(Odac)
407
+ }
408
+ }
409
+
410
+ // Wait for all route sets to complete for this file
411
+ await Promise.all(this._pendingRouteLoads)
412
+ this._pendingRouteLoads = []
413
+
414
+ // Clean up deleted routes logic
415
+ for (const type of ['page', '#page', 'post', '#post', 'get', '#get', 'error']) {
416
+ if (!this.routes[Odac.Route.buff]) continue
417
+ if (!this.routes[Odac.Route.buff][type]) continue
418
+ for (const route in this.routes[Odac.Route.buff][type]) {
419
+ const routeObj = this.routes[Odac.Route.buff][type][route]
420
+ if (!routeObj) continue
421
+
422
+ if (routes2[Odac.Route.buff] > routeObj.loaded) {
423
+ if (routeObj.path) {
424
+ try {
425
+ delete require.cache[require.resolve(routeObj.path)]
426
+ } catch {
427
+ // Silently fail
428
+ }
429
+ }
430
+ delete this.routes[Odac.Route.buff][type][route]
431
+ } else if (routeObj) {
432
+ if (typeof routeObj.type === 'function') continue
433
+ // Check if controller file modified
434
+ try {
435
+ const cStat = await fsPromises.stat(routeObj.path)
436
+ if (routeObj.mtime < cStat.mtimeMs) {
437
+ delete require.cache[require.resolve(routeObj.path)]
438
+ routeObj.cache = require(routeObj.path)
439
+ routeObj.mtime = cStat.mtimeMs
440
+ }
441
+ } catch {
442
+ // Controller file might have been deleted?
443
+ }
308
444
  }
309
445
  }
310
446
  }
447
+ delete Odac.Route.buff
311
448
  }
312
- delete Odac.Route.buff
449
+ } catch (e) {
450
+ // route dir issue?
451
+ console.error(e)
313
452
  }
453
+
314
454
  Cron.init()
315
455
  this.loading = false
316
456
  }
317
457
 
318
- init() {
319
- this.#init()
458
+ async init() {
459
+ await this.#init()
320
460
  this.#registerInternalRoutes()
321
- setInterval(() => {
322
- this.#init()
323
- }, 5000)
461
+
462
+ // Hot Reload only in Debug Mode
463
+ if (Odac.Config.debug) {
464
+ setInterval(async () => {
465
+ await this.#init()
466
+ }, 5000)
467
+ }
324
468
  }
325
469
 
326
470
  #registerInternalRoutes() {
@@ -354,7 +498,7 @@ class Route {
354
498
  )
355
499
 
356
500
  this.set(
357
- ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'],
501
+ 'POST',
358
502
  '/_odac/form',
359
503
  async Odac => {
360
504
  const csrfToken = await Odac.request('_token')
@@ -375,6 +519,28 @@ class Route {
375
519
  {token: true}
376
520
  )
377
521
 
522
+ this.set(
523
+ 'POST',
524
+ '/_odac/magic-login',
525
+ async Odac => {
526
+ const csrfToken = await Odac.request('_token')
527
+ if (!csrfToken || !Odac.token(csrfToken)) {
528
+ return Odac.Request.abort(401)
529
+ }
530
+ return await Internal.magicLogin(Odac)
531
+ },
532
+ {token: true}
533
+ )
534
+
535
+ this.set(
536
+ 'GET',
537
+ '/_odac/magic-verify',
538
+ async Odac => {
539
+ return await Internal.magicVerify(Odac)
540
+ },
541
+ {token: false}
542
+ )
543
+
378
544
  delete Odac.Route.buff
379
545
  }
380
546
 
@@ -387,6 +553,11 @@ class Route {
387
553
  if (result instanceof Promise) result = await result
388
554
  const Stream = require('./Stream.js')
389
555
  if (result instanceof Stream) return
556
+ if (result && typeof result.pipe === 'function') {
557
+ param.Request.print()
558
+ result.pipe(param.Request.res)
559
+ return
560
+ }
390
561
  if (param.Request.res.finished || param.Request.res.writableEnded) {
391
562
  param.cleanup()
392
563
  return
@@ -408,6 +579,8 @@ class Route {
408
579
  }
409
580
 
410
581
  set(type, url, file, options = {}) {
582
+ const capturedMiddlewares = this._pendingMiddlewares.length > 0 ? [...this._pendingMiddlewares] : undefined
583
+
411
584
  if (Array.isArray(type)) {
412
585
  type = type.map(t => t.toLowerCase())
413
586
  for (const t of type) {
@@ -425,43 +598,89 @@ class Route {
425
598
  const isFunction = typeof file === 'function'
426
599
  let path = `${__dir}/route/${Odac.Route.buff}.js`
427
600
 
601
+ let action = null
602
+
428
603
  if (!isFunction && file) {
429
- path = `${__dir}/controller/${type.replace('#', '')}/${file}.js`
430
- if (typeof file === 'string' && file.includes('.')) {
431
- let arr = file.split('.')
432
- path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
604
+ if (typeof file === 'string' && file.includes('@')) {
605
+ let arr = file.split('@')
606
+ file = arr[0]
607
+ action = arr[1]
608
+ path = `${__dir}/controller/${file.replace(/\./g, '/')}.js`
609
+ } else {
610
+ path = `${__dir}/controller/${type.replace('#', '')}/${file}.js`
611
+ if (typeof file === 'string' && file.includes('.')) {
612
+ let arr = file.split('.')
613
+ path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
614
+ }
433
615
  }
434
616
  }
435
617
 
436
618
  if (!this.routes[Odac.Route.buff]) this.routes[Odac.Route.buff] = {}
437
619
  if (!this.routes[Odac.Route.buff][type]) this.routes[Odac.Route.buff][type] = {}
438
620
 
439
- if (this.routes[Odac.Route.buff][type][url]) {
440
- this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
441
- if (!isFunction && this.routes[Odac.Route.buff][type][url].mtime < fs.statSync(path).mtimeMs) {
442
- delete this.routes[Odac.Route.buff][type][url]
443
- delete require.cache[require.resolve(path)]
444
- } else return this
445
- }
621
+ const task = async () => {
622
+ if (this.routes[Odac.Route.buff][type][url]) {
623
+ this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
624
+ if (!isFunction) {
625
+ try {
626
+ const stat = await fsPromises.stat(path)
627
+ if (this.routes[Odac.Route.buff][type][url].mtime < stat.mtimeMs) {
628
+ delete this.routes[Odac.Route.buff][type][url]
629
+ delete require.cache[require.resolve(path)]
630
+ } else {
631
+ return
632
+ }
633
+ } catch {
634
+ // File error, proceed to reload or re-set
635
+ }
636
+ } else {
637
+ return
638
+ }
639
+ }
446
640
 
447
- if (isFunction || fs.existsSync(path)) {
448
- if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
449
- this.routes[Odac.Route.buff][type][url].cache = isFunction ? file : require(path)
450
- this.routes[Odac.Route.buff][type][url].type = isFunction ? 'function' : 'controller'
451
- this.routes[Odac.Route.buff][type][url].file = file
452
- this.routes[Odac.Route.buff][type][url].mtime = isFunction ? Date.now() : fs.statSync(path).mtimeMs
453
- this.routes[Odac.Route.buff][type][url].path = path
454
- this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
455
- this.routes[Odac.Route.buff][type][url].token = options.token ?? true
456
- this.routes[Odac.Route.buff][type][url].middlewares = this._pendingMiddlewares.length > 0 ? [...this._pendingMiddlewares] : undefined
641
+ if (isFunction) {
642
+ if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
643
+ this.routes[Odac.Route.buff][type][url].cache = file
644
+ this.routes[Odac.Route.buff][type][url].type = 'function'
645
+ this.routes[Odac.Route.buff][type][url].file = file
646
+ this.routes[Odac.Route.buff][type][url].mtime = Date.now()
647
+ this.routes[Odac.Route.buff][type][url].path = path
648
+ this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
649
+ this.routes[Odac.Route.buff][type][url].token = options.token ?? true
650
+ this.routes[Odac.Route.buff][type][url].action = action
651
+
652
+ this.routes[Odac.Route.buff][type][url].middlewares = capturedMiddlewares
653
+ } else {
654
+ try {
655
+ const stat = await fsPromises.stat(path)
656
+ if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
657
+ this.routes[Odac.Route.buff][type][url].cache = require(path)
658
+ this.routes[Odac.Route.buff][type][url].type = 'controller'
659
+ this.routes[Odac.Route.buff][type][url].file = file
660
+ this.routes[Odac.Route.buff][type][url].mtime = stat.mtimeMs
661
+ this.routes[Odac.Route.buff][type][url].path = path
662
+ this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
663
+ this.routes[Odac.Route.buff][type][url].token = options.token ?? true
664
+ this.routes[Odac.Route.buff][type][url].action = action
665
+
666
+ this.routes[Odac.Route.buff][type][url].middlewares = capturedMiddlewares
667
+ } catch {
668
+ if (file && typeof file === 'string') {
669
+ console.error(`\x1b[31m[Odac]\x1b[0m Controller not found: \x1b[33m${path}\x1b[0m`)
670
+ }
671
+ }
672
+ }
457
673
  }
458
674
 
675
+ this._pendingRouteLoads.push(task())
676
+
459
677
  return this
460
678
  }
461
679
 
462
680
  page(path, file) {
463
681
  if (typeof file === 'object' && !Array.isArray(file)) {
464
682
  this.set('page', path, _odac => {
683
+ _odac.set(file)
465
684
  _odac.View.set(file)
466
685
  return
467
686
  })
@@ -482,16 +701,20 @@ class Route {
482
701
  }
483
702
 
484
703
  authPage(path, authFile, file) {
485
- if (typeof authFile === 'object' && !Array.isArray(authFile)) {
704
+ if (typeof authFile === 'object' && authFile !== null && !Array.isArray(authFile)) {
486
705
  this.set('#page', path, _odac => {
706
+ _odac.set(authFile)
487
707
  _odac.View.set(authFile)
488
708
  return
489
709
  })
490
710
  if (typeof file === 'object' && !Array.isArray(file)) {
491
711
  this.set('page', path, _odac => {
712
+ _odac.set(file)
492
713
  _odac.View.set(file)
493
714
  return
494
715
  })
716
+ } else if (file) {
717
+ this.set('page', path, file)
495
718
  }
496
719
  return this
497
720
  }
@@ -499,6 +722,7 @@ class Route {
499
722
  if (file) {
500
723
  if (typeof file === 'object' && !Array.isArray(file)) {
501
724
  this.set('page', path, _odac => {
725
+ _odac.set(file)
502
726
  _odac.View.set(file)
503
727
  return
504
728
  })
@@ -509,15 +733,31 @@ class Route {
509
733
  return this
510
734
  }
511
735
 
512
- authPost(path, authFile, file) {
513
- if (authFile) this.set('#post', path, authFile)
514
- if (file) this.post(path, file)
736
+ authPost(path, authFile, file, options) {
737
+ let opts = options
738
+ let publicFile = file
739
+
740
+ if (publicFile && typeof publicFile === 'object' && !opts) {
741
+ opts = publicFile
742
+ publicFile = undefined
743
+ }
744
+
745
+ if (authFile) this.set('#post', path, authFile, opts)
746
+ if (publicFile) this.post(path, publicFile, opts)
515
747
  return this
516
748
  }
517
749
 
518
- authGet(path, authFile, file) {
519
- if (authFile) this.set('#get', path, authFile)
520
- if (file) this.get(path, file)
750
+ authGet(path, authFile, file, options) {
751
+ let opts = options
752
+ let publicFile = file
753
+
754
+ if (publicFile && typeof publicFile === 'object' && !opts) {
755
+ opts = publicFile
756
+ publicFile = undefined
757
+ }
758
+
759
+ if (authFile) this.set('#get', path, authFile, opts)
760
+ if (publicFile) this.get(path, publicFile, opts)
521
761
  return this
522
762
  }
523
763
 
@@ -546,6 +786,26 @@ class Route {
546
786
  const {token = true} = options
547
787
  const requireAuth = type === '#ws'
548
788
 
789
+ if (typeof handler !== 'function') {
790
+ let path = `${__dir}/controller/${type.replace('#', '')}/${handler}.js`
791
+ if (typeof handler === 'string' && handler.includes('.')) {
792
+ let arr = handler.split('.')
793
+ path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
794
+ }
795
+
796
+ if (fs.existsSync(path)) {
797
+ handler = require(path)
798
+ } else {
799
+ console.error(`\x1b[31m[Odac]\x1b[0m WebSocket Controller not found: \x1b[33m${path}\x1b[0m`)
800
+ return
801
+ }
802
+ }
803
+
804
+ if (typeof handler !== 'function') {
805
+ console.error(`\x1b[31m[Odac]\x1b[0m Invalid WebSocket handler (not a function).`)
806
+ return
807
+ }
808
+
549
809
  const wrappedHandler = async (ws, Odac) => {
550
810
  Odac.ws = ws
551
811
 
@@ -603,7 +863,10 @@ class Route {
603
863
  }
604
864
  }
605
865
  }
606
- return handler(Odac)
866
+ const res = handler(Odac)
867
+ if (res instanceof Promise) await res
868
+ ws.resume()
869
+ return res
607
870
  }
608
871
 
609
872
  this.#wsServer.route(path, wrappedHandler)