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,250 +0,0 @@
1
- // Mock fs before requiring Lang
2
- jest.mock('fs', () => ({
3
- promises: {
4
- writeFile: jest.fn().mockResolvedValue()
5
- },
6
- readFileSync: jest.fn()
7
- }))
8
-
9
- const fs = require('fs')
10
- const path = require('path')
11
- const Lang = require('../../core/Lang')
12
-
13
- describe('Lang', () => {
14
- let originalConsoleError
15
-
16
- beforeEach(() => {
17
- // Reset mocks before each test
18
- fs.readFileSync.mockClear()
19
- fs.promises.writeFile.mockClear()
20
-
21
- // Mock console.error to prevent noise in tests
22
- originalConsoleError = console.error
23
- console.error = jest.fn()
24
- })
25
-
26
- afterEach(() => {
27
- // Restore console.error
28
- console.error = originalConsoleError
29
- })
30
-
31
- it("should return 'Odac' for the 'Odac' key", () => {
32
- const lang = new Lang()
33
- expect(lang.get('Odac')).toBe('Odac')
34
- })
35
-
36
- it('should return the translation for an existing key', () => {
37
- const mockStrings = {'test.key': 'Test Value'}
38
- fs.readFileSync.mockReturnValue(JSON.stringify(mockStrings))
39
-
40
- const lang = new Lang()
41
- expect(lang.get('test.key')).toBe('Test Value')
42
- })
43
-
44
- it('should replace placeholders with arguments', () => {
45
- const mockStrings = {greeting: 'Hello, %s! Welcome, %s.'}
46
- fs.readFileSync.mockReturnValue(JSON.stringify(mockStrings))
47
-
48
- const lang = new Lang()
49
- expect(lang.get('greeting', 'John', 'Jane')).toBe('Hello, John! Welcome, Jane.')
50
- })
51
-
52
- it('should return the key and try to save it if it does not exist', () => {
53
- fs.readFileSync.mockImplementation(() => {
54
- throw new Error('File not found')
55
- })
56
-
57
- const lang = new Lang()
58
- const key = 'non.existent.key'
59
-
60
- expect(lang.get(key)).toBe(key)
61
-
62
- // Verify that save was called
63
- expect(fs.promises.writeFile).toHaveBeenCalled()
64
- })
65
-
66
- it('should handle multiple placeholders correctly', () => {
67
- const mockStrings = {format: '%s %s %s'}
68
- fs.readFileSync.mockReturnValue(JSON.stringify(mockStrings))
69
- const lang = new Lang()
70
- expect(lang.get('format', 'a', 'b', 'c')).toBe('a b c')
71
- })
72
-
73
- describe('File saving behavior', () => {
74
- it('should not modify existing file when all keys exist', () => {
75
- const existingStrings = {
76
- 'existing.key1': 'Value 1',
77
- 'existing.key2': 'Value 2',
78
- 'existing.key3': 'Value 3'
79
- }
80
-
81
- fs.readFileSync.mockReturnValue(JSON.stringify(existingStrings))
82
-
83
- const lang = new Lang()
84
-
85
- // Access existing keys multiple times
86
- lang.get('existing.key1')
87
- lang.get('existing.key2')
88
- lang.get('existing.key3')
89
- lang.get('existing.key1') // Access again
90
-
91
- // Verify writeFile was only called once during initialization (if file didn't exist)
92
- // Since we're mocking readFileSync to return data, it should not call writeFile
93
- expect(fs.promises.writeFile).not.toHaveBeenCalled()
94
- })
95
-
96
- it('should save file with correct format when new key is added', () => {
97
- const existingStrings = {'existing.key': 'Existing Value'}
98
- fs.readFileSync.mockReturnValue(JSON.stringify(existingStrings))
99
-
100
- const lang = new Lang()
101
-
102
- // Add a new key
103
- const result = lang.get('new.key')
104
-
105
- expect(result).toBe('new.key')
106
- expect(fs.promises.writeFile).toHaveBeenCalledTimes(1)
107
-
108
- // Verify the file is saved with correct format and content
109
- const [filePath, content, encoding] = fs.promises.writeFile.mock.calls[0]
110
-
111
- expect(encoding).toBe('utf8')
112
- expect(filePath).toMatch(/locale\/.*\.json$/)
113
-
114
- const savedData = JSON.parse(content)
115
- expect(savedData).toEqual({
116
- 'existing.key': 'Existing Value',
117
- 'new.key': 'new.key'
118
- })
119
-
120
- // Verify JSON formatting (4 spaces indentation)
121
- expect(content).toMatch(/{\n /)
122
- })
123
-
124
- it('should preserve existing data when adding new keys', () => {
125
- const existingStrings = {
126
- key1: 'Value 1',
127
- key2: 'Value 2'
128
- }
129
-
130
- fs.readFileSync.mockReturnValue(JSON.stringify(existingStrings))
131
-
132
- const lang = new Lang()
133
-
134
- // Add multiple new keys
135
- lang.get('new.key1')
136
- lang.get('new.key2')
137
-
138
- expect(fs.promises.writeFile).toHaveBeenCalledTimes(2)
139
-
140
- // Check the final saved state
141
- const lastCall = fs.promises.writeFile.mock.calls[fs.promises.writeFile.mock.calls.length - 1]
142
- const savedContent = JSON.parse(lastCall[1])
143
-
144
- expect(savedContent).toEqual({
145
- key1: 'Value 1',
146
- key2: 'Value 2',
147
- 'new.key1': 'new.key1',
148
- 'new.key2': 'new.key2'
149
- })
150
- })
151
-
152
- it('should handle file save errors gracefully', () => {
153
- fs.readFileSync.mockImplementation(() => {
154
- throw new Error('File not found')
155
- })
156
-
157
- // Mock writeFile to throw synchronously (simulating immediate error)
158
- fs.promises.writeFile.mockImplementation(() => {
159
- throw new Error('Write permission denied')
160
- })
161
-
162
- const lang = new Lang()
163
-
164
- // This should not throw an error even if save fails
165
- expect(() => lang.get('test.key')).not.toThrow()
166
-
167
- // Verify error was logged
168
- expect(console.error).toHaveBeenCalledWith('Error saving language file:', expect.any(Error))
169
- })
170
-
171
- it('should create file with empty object when file does not exist', () => {
172
- fs.readFileSync.mockImplementation(() => {
173
- throw new Error('ENOENT: no such file or directory')
174
- })
175
-
176
- const lang = new Lang()
177
-
178
- // Should save empty object initially
179
- expect(fs.promises.writeFile).toHaveBeenCalledTimes(1)
180
-
181
- const [filePath, content, encoding] = fs.promises.writeFile.mock.calls[0]
182
- expect(content).toBe('{}')
183
- expect(encoding).toBe('utf8')
184
- })
185
-
186
- it('should not save duplicate keys', () => {
187
- const existingStrings = {'existing.key': 'Existing Value'}
188
- fs.readFileSync.mockReturnValue(JSON.stringify(existingStrings))
189
-
190
- const lang = new Lang()
191
-
192
- // Access the same new key multiple times
193
- lang.get('new.key')
194
- lang.get('new.key')
195
- lang.get('new.key')
196
-
197
- // Should only save once when the key is first added
198
- expect(fs.promises.writeFile).toHaveBeenCalledTimes(1)
199
- })
200
- })
201
-
202
- describe('Locale file path validation', () => {
203
- it('should use correct locale file path', () => {
204
- const mockLocale = 'en-US'
205
- const originalIntl = global.Intl
206
-
207
- // Mock Intl to return specific locale
208
- global.Intl = {
209
- DateTimeFormat: () => ({
210
- resolvedOptions: () => ({locale: mockLocale})
211
- })
212
- }
213
-
214
- fs.readFileSync.mockImplementation(() => {
215
- throw new Error('File not found')
216
- })
217
-
218
- new Lang()
219
-
220
- expect(fs.promises.writeFile).toHaveBeenCalledWith(expect.stringContaining(`locale/${mockLocale}.json`), expect.any(String), 'utf8')
221
-
222
- // Restore original Intl
223
- global.Intl = originalIntl
224
- })
225
- })
226
-
227
- describe('Mock verification', () => {
228
- it('should ensure fs operations are mocked and not writing real files', () => {
229
- // Verify that fs.promises.writeFile is a mock function
230
- expect(jest.isMockFunction(fs.promises.writeFile)).toBe(true)
231
- expect(jest.isMockFunction(fs.readFileSync)).toBe(true)
232
-
233
- // Create a Lang instance that would normally write to file
234
- fs.readFileSync.mockImplementation(() => {
235
- throw new Error('File not found')
236
- })
237
-
238
- const lang = new Lang()
239
- lang.get('test.mock.key')
240
-
241
- // Verify mock was called but no real file operation occurred
242
- expect(fs.promises.writeFile).toHaveBeenCalled()
243
-
244
- // The mock should have been called with the expected parameters
245
- const [filePath, content] = fs.promises.writeFile.mock.calls[0]
246
- expect(filePath).toMatch(/locale\/.*\.json$/)
247
- expect(content).toBe('{}')
248
- })
249
- })
250
- })
@@ -1,156 +0,0 @@
1
- const Process = require('../../core/Process')
2
- const findProcess = require('find-process')
3
-
4
- // Note: jest.mock is automatically hoisted.
5
- // Since Process.js uses `require('find-process').default`, jest will automatically
6
- // create a mock with a `default` property that is a jest.fn().
7
- jest.mock('find-process')
8
- const processKillSpy = jest.spyOn(process, 'kill').mockImplementation(() => {})
9
-
10
- global.Odac = {
11
- core: () => ({
12
- config: {}
13
- })
14
- }
15
-
16
- describe('Process', () => {
17
- let proc
18
-
19
- beforeEach(() => {
20
- proc = new Process()
21
- // The mock function is on the .default property of the module mock
22
- findProcess.default.mockClear()
23
- processKillSpy.mockClear()
24
- global.Odac.core = () => ({config: {}})
25
- })
26
-
27
- it('should be defined', () => {
28
- expect(proc).toBeDefined()
29
- })
30
-
31
- describe('stop(pid)', () => {
32
- it('should kill a "node" process with the given pid', async () => {
33
- const pid = 123
34
- findProcess.default.mockResolvedValue([{pid: pid, name: 'node'}])
35
-
36
- await proc.stop(pid)
37
-
38
- expect(findProcess.default).toHaveBeenCalledWith('pid', pid)
39
- expect(processKillSpy).toHaveBeenCalledWith(pid, 'SIGTERM')
40
- })
41
-
42
- it('should not kill a process if it is not a "node" process', async () => {
43
- const pid = 456
44
- findProcess.default.mockResolvedValue([{pid: pid, name: 'other-process'}])
45
-
46
- await proc.stop(pid)
47
-
48
- expect(findProcess.default).toHaveBeenCalledWith('pid', pid)
49
- expect(processKillSpy).not.toHaveBeenCalled()
50
- })
51
-
52
- it('should not try to kill a process if no process is found', async () => {
53
- const pid = 789
54
- findProcess.default.mockResolvedValue([])
55
-
56
- await proc.stop(pid)
57
-
58
- expect(findProcess.default).toHaveBeenCalledWith('pid', pid)
59
- expect(processKillSpy).not.toHaveBeenCalled()
60
- })
61
-
62
- it('should resolve even if find-process throws an error', async () => {
63
- const pid = 101
64
- findProcess.default.mockRejectedValue(new Error('find-process failed'))
65
-
66
- await expect(proc.stop(pid)).resolves.toBeUndefined()
67
- expect(processKillSpy).not.toHaveBeenCalled()
68
- })
69
- })
70
-
71
- describe('stopAll()', () => {
72
- let stopSpy
73
-
74
- beforeEach(() => {
75
- // It's safer to store the spy in a variable and restore it from there.
76
- stopSpy = jest.spyOn(proc, 'stop').mockResolvedValue(undefined)
77
- })
78
-
79
- afterEach(() => {
80
- stopSpy.mockRestore()
81
- })
82
-
83
- it('should call stop for all configured pids', async () => {
84
- global.Odac.core = () => ({
85
- config: {
86
- server: {
87
- watchdog: 100,
88
- pid: 200
89
- },
90
- websites: {
91
- 'example.com': {pid: 301},
92
- 'test.com': {pid: 302}
93
- },
94
- services: [
95
- {name: 'service1', pid: 401},
96
- {name: 'service2', pid: 402}
97
- ]
98
- }
99
- })
100
-
101
- await proc.stopAll()
102
-
103
- expect(stopSpy).toHaveBeenCalledTimes(6)
104
- expect(stopSpy).toHaveBeenCalledWith(100)
105
- expect(stopSpy).toHaveBeenCalledWith(200)
106
- expect(stopSpy).toHaveBeenCalledWith(301)
107
- expect(stopSpy).toHaveBeenCalledWith(302)
108
- expect(stopSpy).toHaveBeenCalledWith(401)
109
- expect(stopSpy).toHaveBeenCalledWith(402)
110
- })
111
-
112
- it('should handle partial configurations gracefully', async () => {
113
- global.Odac.core = () => ({
114
- config: {
115
- server: {
116
- pid: 200
117
- },
118
- websites: {
119
- 'example.com': {pid: 301}
120
- },
121
- services: []
122
- }
123
- })
124
-
125
- await proc.stopAll()
126
-
127
- expect(stopSpy).toHaveBeenCalledTimes(2)
128
- expect(stopSpy).toHaveBeenCalledWith(200)
129
- expect(stopSpy).toHaveBeenCalledWith(301)
130
- })
131
-
132
- it('should not call stop if no pids are configured', async () => {
133
- global.Odac.core = () => ({
134
- config: {
135
- server: {},
136
- websites: {},
137
- services: []
138
- }
139
- })
140
-
141
- await proc.stopAll()
142
-
143
- expect(stopSpy).not.toHaveBeenCalled()
144
- })
145
-
146
- it('should handle missing top-level config keys', async () => {
147
- global.Odac.core = () => ({
148
- config: {}
149
- })
150
-
151
- await proc.stopAll()
152
-
153
- expect(stopSpy).not.toHaveBeenCalled()
154
- })
155
- })
156
- })