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
@@ -1,291 +0,0 @@
1
- /**
2
- * Mock implementation of the path module for server tests
3
- * Provides comprehensive mocking of path manipulation utilities
4
- */
5
-
6
- const path = {
7
- // Path separator
8
- sep: '/',
9
- delimiter: ':',
10
-
11
- // POSIX and Windows path objects
12
- posix: null, // Will be set to this object
13
- win32: null, // Will be set to a Windows version
14
-
15
- // Path manipulation functions
16
- basename: jest.fn((path, ext) => {
17
- if (typeof path !== 'string') {
18
- throw new TypeError('Path must be a string')
19
- }
20
-
21
- const lastSlash = path.lastIndexOf('/')
22
- let base = lastSlash === -1 ? path : path.slice(lastSlash + 1)
23
-
24
- if (ext && base.endsWith(ext)) {
25
- base = base.slice(0, -ext.length)
26
- }
27
-
28
- return base
29
- }),
30
-
31
- dirname: jest.fn(path => {
32
- if (typeof path !== 'string') {
33
- throw new TypeError('Path must be a string')
34
- }
35
-
36
- if (path === '/') return '/'
37
- if (path === '') return '.'
38
-
39
- const lastSlash = path.lastIndexOf('/')
40
- if (lastSlash === -1) return '.'
41
- if (lastSlash === 0) return '/'
42
-
43
- return path.slice(0, lastSlash)
44
- }),
45
-
46
- extname: jest.fn(path => {
47
- if (typeof path !== 'string') {
48
- throw new TypeError('Path must be a string')
49
- }
50
-
51
- const lastDot = path.lastIndexOf('.')
52
- const lastSlash = path.lastIndexOf('/')
53
-
54
- if (lastDot === -1 || lastDot < lastSlash) {
55
- return ''
56
- }
57
-
58
- return path.slice(lastDot)
59
- }),
60
-
61
- format: jest.fn(pathObject => {
62
- if (typeof pathObject !== 'object' || pathObject === null) {
63
- throw new TypeError('Path object must be an object')
64
- }
65
-
66
- const {dir, root, base, name, ext} = pathObject
67
-
68
- if (base) {
69
- return dir ? `${dir}/${base}` : base
70
- }
71
-
72
- const filename = name + (ext || '')
73
-
74
- if (dir) {
75
- return `${dir}/${filename}`
76
- } else if (root) {
77
- return `${root}${filename}`
78
- }
79
-
80
- return filename
81
- }),
82
-
83
- isAbsolute: jest.fn(path => {
84
- if (typeof path !== 'string') {
85
- throw new TypeError('Path must be a string')
86
- }
87
-
88
- return path.startsWith('/')
89
- }),
90
-
91
- join: jest.fn((...paths) => {
92
- if (paths.length === 0) return '.'
93
-
94
- const filteredPaths = paths.filter(p => typeof p === 'string' && p !== '')
95
- if (filteredPaths.length === 0) return '.'
96
-
97
- let joined = filteredPaths.join('/')
98
-
99
- // Normalize the path
100
- return path.normalize(joined)
101
- }),
102
-
103
- normalize: jest.fn(path => {
104
- if (typeof path !== 'string') {
105
- throw new TypeError('Path must be a string')
106
- }
107
-
108
- if (path === '') return '.'
109
-
110
- const isAbsolute = path.startsWith('/')
111
- const trailingSlash = path.endsWith('/')
112
-
113
- // Split path into segments and process
114
- const segments = path.split('/').filter(segment => segment !== '' && segment !== '.')
115
- const normalizedSegments = []
116
-
117
- for (const segment of segments) {
118
- if (segment === '..') {
119
- if (normalizedSegments.length > 0 && normalizedSegments[normalizedSegments.length - 1] !== '..') {
120
- normalizedSegments.pop()
121
- } else if (!isAbsolute) {
122
- normalizedSegments.push('..')
123
- }
124
- } else {
125
- normalizedSegments.push(segment)
126
- }
127
- }
128
-
129
- let result = normalizedSegments.join('/')
130
-
131
- if (isAbsolute) {
132
- result = '/' + result
133
- } else if (result === '') {
134
- result = '.'
135
- }
136
-
137
- if (trailingSlash && result !== '/' && result !== '.') {
138
- result += '/'
139
- }
140
-
141
- return result
142
- }),
143
-
144
- parse: jest.fn(path => {
145
- if (typeof path !== 'string') {
146
- throw new TypeError('Path must be a string')
147
- }
148
-
149
- const isAbsolute = path.startsWith('/')
150
- const lastSlash = path.lastIndexOf('/')
151
- const lastDot = path.lastIndexOf('.')
152
-
153
- let dir = ''
154
- let base = ''
155
- let ext = ''
156
- let name = ''
157
- let root = ''
158
-
159
- if (isAbsolute) {
160
- root = '/'
161
- }
162
-
163
- if (lastSlash === -1) {
164
- base = path
165
- dir = isAbsolute ? '/' : ''
166
- } else {
167
- dir = path.slice(0, lastSlash) || (isAbsolute ? '/' : '')
168
- base = path.slice(lastSlash + 1)
169
- }
170
-
171
- if (lastDot > lastSlash && lastDot > 0) {
172
- ext = path.slice(lastDot)
173
- name = base.slice(0, base.length - ext.length)
174
- } else {
175
- name = base
176
- }
177
-
178
- return {root, dir, base, ext, name}
179
- }),
180
-
181
- relative: jest.fn((from, to) => {
182
- if (typeof from !== 'string' || typeof to !== 'string') {
183
- throw new TypeError('From and to must be strings')
184
- }
185
-
186
- if (from === to) return ''
187
-
188
- const fromParts = path
189
- .resolve(from)
190
- .split('/')
191
- .filter(p => p !== '')
192
- const toParts = path
193
- .resolve(to)
194
- .split('/')
195
- .filter(p => p !== '')
196
-
197
- // Find common prefix
198
- let commonLength = 0
199
- const minLength = Math.min(fromParts.length, toParts.length)
200
-
201
- for (let i = 0; i < minLength; i++) {
202
- if (fromParts[i] === toParts[i]) {
203
- commonLength++
204
- } else {
205
- break
206
- }
207
- }
208
-
209
- // Build relative path
210
- const upCount = fromParts.length - commonLength
211
- const relativeParts = Array(upCount).fill('..').concat(toParts.slice(commonLength))
212
-
213
- return relativeParts.join('/') || '.'
214
- }),
215
-
216
- resolve: jest.fn((...paths) => {
217
- let resolvedPath = ''
218
- let resolvedAbsolute = false
219
-
220
- for (let i = paths.length - 1; i >= 0 && !resolvedAbsolute; i--) {
221
- const path = paths[i]
222
-
223
- if (typeof path !== 'string') {
224
- throw new TypeError('Path must be a string')
225
- }
226
-
227
- if (path === '') continue
228
-
229
- resolvedPath = path + '/' + resolvedPath
230
- resolvedAbsolute = path.startsWith('/')
231
- }
232
-
233
- if (!resolvedAbsolute) {
234
- resolvedPath = '/mock/cwd/' + resolvedPath
235
- }
236
-
237
- // Normalize and remove trailing slash
238
- resolvedPath = path.normalize(resolvedPath)
239
- if (resolvedPath.endsWith('/') && resolvedPath !== '/') {
240
- resolvedPath = resolvedPath.slice(0, -1)
241
- }
242
-
243
- return resolvedPath
244
- }),
245
-
246
- // Test helpers
247
- __setPosix: () => {
248
- // Current implementation is already POSIX-like
249
- return path
250
- },
251
-
252
- __setWin32: () => {
253
- // Create a Windows version of path methods
254
- const win32Path = {...path}
255
-
256
- win32Path.sep = '\\'
257
- win32Path.delimiter = ';'
258
-
259
- win32Path.isAbsolute = jest.fn(path => {
260
- if (typeof path !== 'string') {
261
- throw new TypeError('Path must be a string')
262
- }
263
-
264
- return /^[a-zA-Z]:\\/.test(path) || path.startsWith('\\\\')
265
- })
266
-
267
- win32Path.normalize = jest.fn(path => {
268
- if (typeof path !== 'string') {
269
- throw new TypeError('Path must be a string')
270
- }
271
-
272
- return path.replace(/\//g, '\\')
273
- })
274
-
275
- return win32Path
276
- },
277
-
278
- __resetMocks: () => {
279
- Object.values(path).forEach(fn => {
280
- if (jest.isMockFunction(fn)) {
281
- fn.mockClear()
282
- }
283
- })
284
- }
285
- }
286
-
287
- // Set up posix and win32 references
288
- path.posix = path
289
- path.win32 = path.__setWin32()
290
-
291
- module.exports = path
@@ -1,8 +0,0 @@
1
- const mockSelfsigned = {
2
- generate: jest.fn().mockReturnValue({
3
- private: '-----BEGIN PRIVATE KEY-----\nmock-private-key\n-----END PRIVATE KEY-----',
4
- cert: '-----BEGIN CERTIFICATE-----\nmock-certificate\n-----END CERTIFICATE-----'
5
- })
6
- }
7
-
8
- module.exports = mockSelfsigned
@@ -1,100 +0,0 @@
1
- /**
2
- * Mock implementation of mail/server for server tests
3
- */
4
-
5
- const {createMockEventEmitter} = require('../../../testHelpers')
6
-
7
- class MockMailServer {
8
- constructor(options = {}) {
9
- Object.assign(this, createMockEventEmitter())
10
-
11
- this.options = options
12
- this.listening = false
13
- this.port = null
14
-
15
- // Store callbacks for testing
16
- this.onAuth = options.onAuth
17
- this.onAppend = options.onAppend
18
- this.onExpunge = options.onExpunge
19
- this.onCreate = options.onCreate
20
- this.onDelete = options.onDelete
21
- this.onRename = options.onRename
22
- this.onFetch = options.onFetch
23
- this.onList = options.onList
24
- this.onLsub = options.onLsub
25
- this.onSelect = options.onSelect
26
- this.onStore = options.onStore
27
- this.onError = options.onError
28
- }
29
-
30
- listen(port, hostname, callback) {
31
- if (typeof hostname === 'function') {
32
- callback = hostname
33
- hostname = undefined
34
- }
35
-
36
- this.port = port
37
- this.listening = true
38
-
39
- setTimeout(() => {
40
- this.emit('listening')
41
- if (callback) callback()
42
- }, 0)
43
-
44
- return this
45
- }
46
-
47
- close(callback) {
48
- this.listening = false
49
- this.port = null
50
-
51
- setTimeout(() => {
52
- this.emit('close')
53
- if (callback) callback()
54
- }, 0)
55
-
56
- return this
57
- }
58
-
59
- // Test helper methods
60
- simulateConnection(session = {}) {
61
- const mockSession = {
62
- remoteAddress: '127.0.0.1',
63
- remotePort: 12345,
64
- user: null,
65
- ...session
66
- }
67
-
68
- this.emit('connect', mockSession)
69
- return mockSession
70
- }
71
-
72
- simulateAuth(auth, session, callback) {
73
- if (this.onAuth) {
74
- this.onAuth(auth, session, callback)
75
- } else if (callback) {
76
- callback(null, {user: auth.username})
77
- }
78
- }
79
-
80
- simulateAppend(data, callback) {
81
- if (this.onAppend) {
82
- this.onAppend(data, callback)
83
- } else if (callback) {
84
- callback()
85
- }
86
- }
87
-
88
- simulateError(error) {
89
- if (this.onError) {
90
- this.onError(error)
91
- }
92
- this.emit('error', error)
93
- }
94
- }
95
-
96
- const mockMailServer = jest.fn().mockImplementation(options => {
97
- return new MockMailServer(options)
98
- })
99
-
100
- module.exports = mockMailServer
@@ -1,31 +0,0 @@
1
- /**
2
- * Mock implementation of mail/smtp for server tests
3
- */
4
-
5
- const smtp = {
6
- send: jest.fn(mailData => {
7
- // Mock successful send
8
- return Promise.resolve({
9
- messageId: mailData.messageId || '<mock@example.com>',
10
- accepted: mailData.to ? [mailData.to.value[0].address] : ['recipient@example.com'],
11
- rejected: [],
12
- pending: [],
13
- response: '250 Message accepted'
14
- })
15
- }),
16
-
17
- // Test helper methods
18
- __setMockResponse: response => {
19
- smtp.send.mockResolvedValue(response)
20
- },
21
-
22
- __setMockError: error => {
23
- smtp.send.mockRejectedValue(error)
24
- },
25
-
26
- __resetMock: () => {
27
- smtp.send.mockClear()
28
- }
29
- }
30
-
31
- module.exports = smtp
@@ -1,106 +0,0 @@
1
- /**
2
- * Mock implementation of smtp-server for server tests
3
- */
4
-
5
- const {createMockEventEmitter} = require('./testHelpers')
6
-
7
- class MockSMTPServer {
8
- constructor(options = {}) {
9
- Object.assign(this, createMockEventEmitter())
10
-
11
- this.options = options
12
- this.listening = false
13
- this.port = null
14
-
15
- // Store callbacks for testing
16
- this.onAuth = options.onAuth
17
- this.onData = options.onData
18
- this.onAppend = options.onAppend
19
- this.onExpunge = options.onExpunge
20
- this.onCreate = options.onCreate
21
- this.onDelete = options.onDelete
22
- this.onRename = options.onRename
23
- this.onFetch = options.onFetch
24
- this.onList = options.onList
25
- this.onLsub = options.onLsub
26
- this.onMailFrom = options.onMailFrom
27
- this.onRcptTo = options.onRcptTo
28
- this.onSelect = options.onSelect
29
- this.onStore = options.onStore
30
- this.onError = options.onError
31
- }
32
-
33
- listen(port, hostname, callback) {
34
- if (typeof hostname === 'function') {
35
- callback = hostname
36
- hostname = undefined
37
- }
38
-
39
- this.port = port
40
- this.listening = true
41
-
42
- setTimeout(() => {
43
- this.emit('listening')
44
- if (callback) callback()
45
- }, 0)
46
-
47
- return this
48
- }
49
-
50
- close(callback) {
51
- this.listening = false
52
- this.port = null
53
-
54
- setTimeout(() => {
55
- this.emit('close')
56
- if (callback) callback()
57
- }, 0)
58
-
59
- return this
60
- }
61
-
62
- // Test helper methods
63
- simulateConnection(session = {}) {
64
- const mockSession = {
65
- remoteAddress: '127.0.0.1',
66
- remotePort: 12345,
67
- user: null,
68
- ...session
69
- }
70
-
71
- this.emit('connect', mockSession)
72
- return mockSession
73
- }
74
-
75
- simulateAuth(auth, session, callback) {
76
- if (this.onAuth) {
77
- this.onAuth(auth, session, callback)
78
- } else if (callback) {
79
- callback(null, {user: auth.username})
80
- }
81
- }
82
-
83
- simulateData(stream, session, callback) {
84
- if (this.onData) {
85
- this.onData(stream, session, callback)
86
- } else if (callback) {
87
- callback()
88
- }
89
- }
90
-
91
- simulateError(error) {
92
- if (this.onError) {
93
- this.onError(error)
94
- }
95
- this.emit('error', error)
96
- }
97
- }
98
-
99
- const SMTPServer = jest.fn().mockImplementation(options => {
100
- return new MockSMTPServer(options)
101
- })
102
-
103
- module.exports = {
104
- SMTPServer,
105
- MockSMTPServer
106
- }