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.
Files changed (117) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +42 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/release.yml +68 -1
  6. package/.github/workflows/test-coverage.yml +6 -5
  7. package/.husky/pre-commit +10 -0
  8. package/.husky/pre-push +13 -0
  9. package/.releaserc.js +1 -1
  10. package/CHANGELOG.md +99 -0
  11. package/README.md +16 -0
  12. package/bin/odac.js +196 -38
  13. package/client/odac.js +238 -171
  14. package/docs/backend/01-overview/03-development-server.md +38 -45
  15. package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
  16. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  17. package/docs/backend/03-config/01-database-connection.md +2 -2
  18. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  19. package/docs/backend/03-config/03-request-timeout.md +1 -1
  20. package/docs/backend/03-config/04-environment-variables.md +4 -4
  21. package/docs/backend/03-config/05-early-hints.md +2 -2
  22. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  23. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  24. package/docs/backend/04-routing/09-websocket.md +14 -1
  25. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  26. package/docs/backend/05-controllers/03-controller-classes.md +40 -20
  27. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  28. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  29. package/docs/backend/08-database/01-getting-started.md +2 -2
  30. package/docs/backend/10-authentication/03-register.md +1 -1
  31. package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
  32. package/docs/backend/10-authentication/05-session-management.md +15 -1
  33. package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
  34. package/docs/backend/10-authentication/07-magic-links.md +1 -1
  35. package/docs/index.json +5 -1
  36. package/jest.config.js +1 -1
  37. package/package.json +18 -14
  38. package/src/Auth.js +58 -23
  39. package/src/Config.js +7 -7
  40. package/src/Database.js +1 -1
  41. package/src/Env.js +3 -1
  42. package/src/Ipc.js +7 -0
  43. package/src/Lang.js +9 -2
  44. package/src/Mail.js +19 -9
  45. package/src/Odac.js +56 -44
  46. package/src/Request.js +6 -2
  47. package/src/Route/Cron.js +58 -17
  48. package/src/Route/Internal.js +1 -1
  49. package/src/Route.js +375 -125
  50. package/src/Server.js +40 -3
  51. package/src/Storage.js +4 -0
  52. package/src/Token.js +6 -4
  53. package/src/Validator.js +24 -15
  54. package/src/Var.js +22 -6
  55. package/src/View/EarlyHints.js +43 -33
  56. package/src/View/Form.js +17 -11
  57. package/src/View.js +68 -12
  58. package/src/WebSocket.js +45 -12
  59. package/template/package.json +3 -1
  60. package/template/view/content/home.html +3 -3
  61. package/template/view/head/main.html +2 -2
  62. package/test/Client.test.js +168 -0
  63. package/test/Config.test.js +112 -0
  64. package/test/Lang.test.js +92 -0
  65. package/test/Odac.test.js +88 -0
  66. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  67. package/test/{framework/Route.test.js → Route.test.js} +105 -1
  68. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  69. package/test/WebSocket.test.js +238 -0
  70. package/test/scripts/check-coverage.js +4 -4
  71. package/test/cli/Cli.test.js +0 -36
  72. package/test/core/Commands.test.js +0 -538
  73. package/test/core/Config.test.js +0 -1432
  74. package/test/core/Lang.test.js +0 -250
  75. package/test/core/Odac.test.js +0 -234
  76. package/test/core/Process.test.js +0 -156
  77. package/test/framework/WebSocket.test.js +0 -100
  78. package/test/server/Api.test.js +0 -647
  79. package/test/server/DNS.test.js +0 -2050
  80. package/test/server/DNS.test.js.bak +0 -2084
  81. package/test/server/Hub.test.js +0 -497
  82. package/test/server/Log.test.js +0 -73
  83. package/test/server/Mail.account.test_.js +0 -460
  84. package/test/server/Mail.init.test_.js +0 -411
  85. package/test/server/Mail.test_.js +0 -1340
  86. package/test/server/SSL.test_.js +0 -1491
  87. package/test/server/Server.test.js +0 -765
  88. package/test/server/Service.test_.js +0 -1127
  89. package/test/server/Subdomain.test.js +0 -440
  90. package/test/server/Web/Firewall.test.js +0 -175
  91. package/test/server/Web/Proxy.test.js +0 -397
  92. package/test/server/Web.test.js +0 -1494
  93. package/test/server/__mocks__/acme-client.js +0 -17
  94. package/test/server/__mocks__/bcrypt.js +0 -50
  95. package/test/server/__mocks__/child_process.js +0 -389
  96. package/test/server/__mocks__/crypto.js +0 -432
  97. package/test/server/__mocks__/fs.js +0 -450
  98. package/test/server/__mocks__/globalOdac.js +0 -227
  99. package/test/server/__mocks__/http.js +0 -575
  100. package/test/server/__mocks__/https.js +0 -272
  101. package/test/server/__mocks__/index.js +0 -249
  102. package/test/server/__mocks__/mail/server.js +0 -100
  103. package/test/server/__mocks__/mail/smtp.js +0 -31
  104. package/test/server/__mocks__/mailparser.js +0 -81
  105. package/test/server/__mocks__/net.js +0 -369
  106. package/test/server/__mocks__/node-forge.js +0 -328
  107. package/test/server/__mocks__/os.js +0 -320
  108. package/test/server/__mocks__/path.js +0 -291
  109. package/test/server/__mocks__/selfsigned.js +0 -8
  110. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  111. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  112. package/test/server/__mocks__/smtp-server.js +0 -106
  113. package/test/server/__mocks__/sqlite3.js +0 -394
  114. package/test/server/__mocks__/testFactories.js +0 -299
  115. package/test/server/__mocks__/testHelpers.js +0 -363
  116. package/test/server/__mocks__/tls.js +0 -229
  117. /package/template/{config.json → odac.json} +0 -0
package/src/Odac.js CHANGED
@@ -64,36 +64,41 @@ module.exports = {
64
64
  _odac.Var = (...args) => new (require('./Var.js'))(...args)
65
65
 
66
66
  if (req) {
67
- _odac.Request = new (require('./Request.js'))(id, req, res, _odac)
68
- _odac.Auth = new (require('./Auth.js'))(_odac.Request)
69
- _odac.Token = new (require('./Token.js'))(_odac.Request)
70
- _odac.Lang = new (require('./Lang.js'))(_odac)
71
- if (res) {
72
- _odac.View = new (require('./View.js'))(_odac)
73
- }
74
-
75
- _odac._intervals = []
76
- _odac._timeouts = []
77
67
  _odac.setInterval = function (callback, delay, ...args) {
78
68
  const id = setInterval(callback, delay, ...args)
69
+ if (!_odac._intervals) _odac._intervals = []
79
70
  _odac._intervals.push(id)
80
71
  return id
81
72
  }
82
73
  _odac.setTimeout = function (callback, delay, ...args) {
83
74
  const id = setTimeout(callback, delay, ...args)
75
+ if (!_odac._timeouts) _odac._timeouts = []
84
76
  _odac._timeouts.push(id)
85
77
  return id
86
78
  }
87
79
  _odac.clearInterval = function (id) {
88
- const index = _odac._intervals.indexOf(id)
80
+ const index = _odac._intervals?.indexOf(id) ?? -1
89
81
  if (index > -1) _odac._intervals.splice(index, 1)
90
82
  clearInterval(id)
91
83
  }
92
84
  _odac.clearTimeout = function (id) {
93
- const index = _odac._timeouts.indexOf(id)
85
+ const index = _odac._timeouts?.indexOf(id) ?? -1
94
86
  if (index > -1) _odac._timeouts.splice(index, 1)
95
87
  clearTimeout(id)
96
88
  }
89
+
90
+ _odac._intervals = []
91
+ _odac._timeouts = []
92
+
93
+ if (typeof req === 'object') {
94
+ _odac.Request = new (require('./Request.js'))(id, req, res, _odac)
95
+ _odac.Auth = new (require('./Auth.js'))(_odac.Request)
96
+ _odac.Token = new (require('./Token.js'))(_odac.Request)
97
+ }
98
+ _odac.Lang = new (require('./Lang.js'))(_odac)
99
+ if (res) {
100
+ _odac.View = new (require('./View.js'))(_odac)
101
+ }
97
102
  _odac.cleanup = function () {
98
103
  for (const id of _odac._intervals) clearInterval(id)
99
104
  for (const id of _odac._timeouts) clearTimeout(id)
@@ -110,42 +115,49 @@ module.exports = {
110
115
  _odac.__ = function (...args) {
111
116
  return _odac.Lang.get(...args)
112
117
  }
113
- _odac.abort = function (code) {
114
- return _odac.Request.abort(code)
115
- }
116
- _odac.cookie = function (key, value, options) {
117
- return _odac.Request.cookie(key, value, options)
118
- }
119
- _odac.direct = function (url) {
120
- return _odac.Request.redirect(url)
118
+ if (typeof req === 'object') {
119
+ _odac.abort = function (code) {
120
+ return _odac.Request.abort(code)
121
+ }
122
+ _odac.cookie = function (key, value, options) {
123
+ return _odac.Request.cookie(key, value, options)
124
+ }
125
+ _odac.direct = function (url) {
126
+ return _odac.Request.redirect(url)
127
+ }
121
128
  }
122
129
  _odac.env = function (key, defaultValue) {
123
130
  return _odac.Env.get(key, defaultValue)
124
131
  }
125
- _odac.return = function (data) {
126
- return _odac.Request.end(data)
127
- }
128
- _odac.request = function (key) {
129
- return _odac.Request.request(key)
130
- }
131
- _odac.set = function (key, value) {
132
- return _odac.Request.set(key, value)
133
- }
134
- _odac.share = function (key, value) {
135
- return _odac.Request.share(key, value)
136
- }
137
- _odac.token = function (hash) {
138
- return hash ? _odac.Token.check(hash) : _odac.Token.generate()
139
- }
140
- _odac.validator = function () {
141
- return new (require('./Validator.js'))(_odac.Request)
142
- }
143
- _odac.write = function (value) {
144
- return _odac.Request.write(value)
145
- }
146
- _odac.stream = function (input) {
147
- _odac.Request.clearTimeout()
148
- return new (require('./Stream'))(_odac.Request.req, _odac.Request.res, input, _odac)
132
+ if (typeof req === 'object') {
133
+ _odac.return = function (data) {
134
+ return _odac.Request.end(data)
135
+ }
136
+ _odac.request = function (key) {
137
+ return _odac.Request.request(key)
138
+ }
139
+ _odac.set = function (key, value) {
140
+ return _odac.Request.set(key, value)
141
+ }
142
+ _odac.session = function (key, value) {
143
+ return _odac.Request.session(key, value)
144
+ }
145
+ _odac.share = function (key, value) {
146
+ return _odac.Request.share(key, value)
147
+ }
148
+ _odac.token = function (hash) {
149
+ return hash ? _odac.Token.check(hash) : _odac.Token.generate()
150
+ }
151
+ _odac.validator = function () {
152
+ return new (require('./Validator.js'))(_odac.Request)
153
+ }
154
+ _odac.write = function (value) {
155
+ return _odac.Request.write(value)
156
+ }
157
+ _odac.stream = function (input) {
158
+ _odac.Request.clearTimeout()
159
+ return new (require('./Stream'))(_odac.Request.req, _odac.Request.res, input, _odac)
160
+ }
149
161
  }
150
162
 
151
163
  if (global.Odac?.Route?.class) {
package/src/Request.js CHANGED
@@ -35,7 +35,11 @@ class OdacRequest {
35
35
  if (!Odac.Route.routes[route]) route = 'www'
36
36
  this.route = route
37
37
  if (this.res) {
38
- this.#timeout = setTimeout(() => !this.res.finished && this.abort(408), Odac.Config.request.timeout)
38
+ if (typeof this.#odac.setTimeout === 'function') {
39
+ this.#timeout = this.#odac.setTimeout(() => !this.res.finished && this.abort(408), Odac.Config.request.timeout)
40
+ } else {
41
+ this.#timeout = setTimeout(() => !this.res.finished && this.abort(408), Odac.Config.request.timeout)
42
+ }
39
43
  }
40
44
  this.#data()
41
45
  if (!Odac.Request) Odac.Request = {}
@@ -71,7 +75,7 @@ class OdacRequest {
71
75
  if (options.expires === undefined) options.expires = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString()
72
76
  if (options.secure === undefined) options.secure = true
73
77
  if (options.httpOnly === undefined) options.httpOnly = true
74
- if (options.sameSite === undefined) options.sameSite = 'Strict'
78
+ if (options.sameSite === undefined) options.sameSite = 'Lax'
75
79
  if (typeof value === 'object') value = JSON.stringify(value)
76
80
  let cookie = `${key}=${value}`
77
81
  for (const option of Object.keys(options)) if (options[option]) cookie += `; ${option}=${options[option]}`
package/src/Route/Cron.js CHANGED
@@ -60,7 +60,7 @@ class Cron {
60
60
  if (job.lastRun && Math.floor(unix / 86400) % condition.value !== 0) shouldRun = false
61
61
  break
62
62
  case 'everyWeekDay':
63
- if (job.lastRun && weekDay % condition.value !== 0) shouldRun = false
63
+ if (condition.value !== weekDay) shouldRun = false
64
64
  break
65
65
  case 'everyMonth':
66
66
  if (job.lastRun && (year * 12 + month) % condition.value !== 0) shouldRun = false
@@ -81,7 +81,9 @@ class Cron {
81
81
  if (job.function || fs.existsSync(job.path)) {
82
82
  if (!job.function) job.function = require(job.path)
83
83
  if (job.function && typeof job.function === 'function') {
84
- job.function()
84
+ const _odac = global.Odac.instance(null, 'cron')
85
+ job.function(_odac)
86
+ if (_odac.cleanup) _odac.cleanup()
85
87
  }
86
88
  }
87
89
  } catch (error) {
@@ -109,22 +111,61 @@ class Cron {
109
111
  updated: new Date()
110
112
  })
111
113
  let id = this.#jobs.length - 1
112
- return {
113
- minute: value => this.#jobs[id].condition.push({type: 'minute', value: value}),
114
- hour: value => this.#jobs[id].condition.push({type: 'hour', value: value}),
115
- day: value => this.#jobs[id].condition.push({type: 'day', value: value}),
116
- weekDay: value => this.#jobs[id].condition.push({type: 'weekDay', value: value}),
117
- month: value => this.#jobs[id].condition.push({type: 'month', value: value}),
118
- year: value => this.#jobs[id].condition.push({type: 'year', value: value}),
119
- yearDay: value => this.#jobs[id].condition.push({type: 'yearDay', value: value}),
120
- everyMinute: value => this.#jobs[id].condition.push({type: 'everyMinute', value: value}),
121
- everyHour: value => this.#jobs[id].condition.push({type: 'everyHour', value: value}),
122
- everyDay: value => this.#jobs[id].condition.push({type: 'everyDay', value: value}),
123
- everyWeekDay: value => this.#jobs[id].condition.push({type: 'everyWeekDay', value: value}),
124
- everyMonth: value => this.#jobs[id].condition.push({type: 'everyMonth', value: value}),
125
- everyYear: value => this.#jobs[id].condition.push({type: 'everyYear', value: value}),
126
- everyYearDay: value => this.#jobs[id].condition.push({type: 'everyYearDay', value: value})
114
+ const addCondition = (type, value) => {
115
+ this.#jobs[id].condition.push({type, value})
116
+ return chain
117
+ }
118
+
119
+ const chain = {
120
+ minute: value => addCondition('minute', value),
121
+ hour: value => addCondition('hour', value),
122
+ day: value => addCondition('day', value),
123
+ at: time => {
124
+ if (!/^\d{1,2}:\d{1,2}$/.test(time)) throw new Error('Invalid time format for .at(). Use HH:MM')
125
+ const [h, m] = time.split(':')
126
+ addCondition('hour', parseInt(h))
127
+ addCondition('minute', parseInt(m))
128
+ return chain
129
+ },
130
+ raw: pattern => {
131
+ const parts = pattern.split(' ').filter(p => p.trim() !== '')
132
+ if (parts.length !== 5) throw new Error('Invalid cron expression. Expected 5 fields (min hour day month weekDay)')
133
+
134
+ const [min, hour, day, month, weekDay] = parts
135
+ const parse = (val, type, everyType) => {
136
+ if (val === '*') return
137
+ if (val.startsWith('*/') && everyType) {
138
+ addCondition(everyType, parseInt(val.split('/')[1]))
139
+ return
140
+ }
141
+ if (!isNaN(val)) {
142
+ addCondition(type, parseInt(val))
143
+ return
144
+ }
145
+ throw new Error(`Unsupported cron value '${val}' for ${type}`)
146
+ }
147
+
148
+ parse(min, 'minute', 'everyMinute')
149
+ parse(hour, 'hour', 'everyHour')
150
+ parse(day, 'day', 'everyDay')
151
+ parse(month, 'month', 'everyMonth')
152
+ parse(weekDay, 'weekDay', null)
153
+
154
+ return chain
155
+ },
156
+ weekDay: value => addCondition('weekDay', value),
157
+ month: value => addCondition('month', value),
158
+ year: value => addCondition('year', value),
159
+ yearDay: value => addCondition('yearDay', value),
160
+ everyMinute: value => addCondition('everyMinute', value),
161
+ everyHour: value => addCondition('everyHour', value),
162
+ everyDay: value => addCondition('everyDay', value),
163
+ everyWeekDay: value => addCondition('everyWeekDay', value),
164
+ everyMonth: value => addCondition('everyMonth', value),
165
+ everyYear: value => addCondition('everyYear', value),
166
+ everyYearDay: value => addCondition('everyYearDay', value)
127
167
  }
168
+ return chain
128
169
  }
129
170
  }
130
171
 
@@ -528,7 +528,7 @@ class Internal {
528
528
  if (error.message === 'Database connection failed') {
529
529
  return Odac.return({
530
530
  result: {success: false},
531
- errors: {_odac_form: 'Database not configured. Please check your config.json'}
531
+ errors: {_odac_form: 'Database not configured. Please check your odac.json'}
532
532
  })
533
533
  }
534
534