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,5 +1,5 @@
1
1
  <meta charset="UTF-8" />
2
2
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3
- <title>Odac</title>
3
+ <title>ODAC</title>
4
4
  <meta name="description" content="A next-generation server and framework toolkit for modern web development" />
5
- <link rel="stylesheet" href="/assets/css/style.css" />
5
+ <link rel="stylesheet" href="/assets/css/app.css" />
@@ -0,0 +1,168 @@
1
+ describe('Client (odac.js)', () => {
2
+ let mockXhr
3
+
4
+ beforeEach(() => {
5
+ jest.resetModules()
6
+
7
+ mockXhr = {
8
+ open: jest.fn(),
9
+ setRequestHeader: jest.fn(),
10
+ send: jest.fn(),
11
+ getResponseHeader: jest.fn(),
12
+ status: 200,
13
+ responseText: '{}',
14
+ response: '{}',
15
+ onload: null,
16
+ onerror: null
17
+ }
18
+
19
+ const mockDocument = {
20
+ getElementById: jest.fn(),
21
+ querySelectorAll: jest.fn(() => []),
22
+ querySelector: jest.fn(),
23
+ addEventListener: jest.fn(),
24
+ removeEventListener: jest.fn(),
25
+ dispatchEvent: jest.fn(),
26
+ documentElement: {dataset: {}},
27
+ cookie: '',
28
+ readyState: 'complete',
29
+ createElement: jest.fn(() => ({
30
+ setAttribute: jest.fn(),
31
+ style: {},
32
+ appendChild: jest.fn(),
33
+ parentNode: {insertBefore: jest.fn()}
34
+ }))
35
+ }
36
+
37
+ const mockWindow = {
38
+ location: {
39
+ protocol: 'http:',
40
+ host: 'localhost',
41
+ href: 'http://localhost/'
42
+ },
43
+ history: {
44
+ pushState: jest.fn()
45
+ },
46
+ scrollTo: jest.fn(),
47
+ addEventListener: jest.fn(),
48
+ XMLHttpRequest: jest.fn(() => mockXhr),
49
+ localStorage: {
50
+ getItem: jest.fn(),
51
+ setItem: jest.fn(),
52
+ removeItem: jest.fn()
53
+ },
54
+ CustomEvent: jest.fn((name, detail) => ({name, detail})),
55
+ setTimeout: jest.fn(),
56
+ clearTimeout: jest.fn(),
57
+ requestAnimationFrame: jest.fn(cb => cb(Date.now())),
58
+ WebSocket: jest.fn(() => ({
59
+ send: jest.fn(),
60
+ close: jest.fn(),
61
+ readyState: 1 // OPEN
62
+ })),
63
+ FormData: jest.fn()
64
+ }
65
+
66
+ mockWindow.window = mockWindow
67
+ mockWindow.document = mockDocument
68
+ mockWindow.WebSocket.OPEN = 1
69
+ mockWindow.WebSocket.CLOSED = 3
70
+
71
+ global.window = mockWindow
72
+ global.document = mockDocument
73
+ global.location = mockWindow.location
74
+ global.XMLHttpRequest = mockWindow.XMLHttpRequest
75
+ global.localStorage = mockWindow.localStorage
76
+ global.CustomEvent = mockWindow.CustomEvent
77
+ global.WebSocket = mockWindow.WebSocket
78
+ global.setTimeout = mockWindow.setTimeout
79
+ global.clearTimeout = mockWindow.clearTimeout
80
+ global.requestAnimationFrame = mockWindow.requestAnimationFrame
81
+ global.FormData = mockWindow.FormData
82
+
83
+ delete require.cache[require.resolve('../client/odac.js')]
84
+ require('../client/odac.js')
85
+ })
86
+
87
+ afterEach(() => {
88
+ delete global.window
89
+ delete global.document
90
+ delete global.location
91
+ delete global.XMLHttpRequest
92
+ delete global.localStorage
93
+ delete global.CustomEvent
94
+ delete global.WebSocket
95
+ delete global.setTimeout
96
+ delete global.clearTimeout
97
+ delete global.requestAnimationFrame
98
+ delete global.FormData
99
+ delete global.Odac
100
+ })
101
+
102
+ test('Odac should be initialized on window', () => {
103
+ expect(window.Odac).toBeDefined()
104
+ })
105
+
106
+ describe('data()', () => {
107
+ test('should retrieve data from odac-data script tag', () => {
108
+ const mockData = {user: 'emre'}
109
+ document.getElementById.mockReturnValue({
110
+ textContent: JSON.stringify(mockData)
111
+ })
112
+
113
+ const result = window.Odac.data()
114
+ expect(result).toEqual(mockData)
115
+ expect(document.getElementById).toHaveBeenCalledWith('odac-data')
116
+ })
117
+
118
+ test('should return specific key from data', () => {
119
+ const mockData = {user: 'emre', role: 'admin'}
120
+ document.getElementById.mockReturnValue({
121
+ textContent: JSON.stringify(mockData)
122
+ })
123
+
124
+ expect(window.Odac.data('user')).toBe('emre')
125
+ expect(window.Odac.data('role')).toBe('admin')
126
+ })
127
+ })
128
+
129
+ describe('storage()', () => {
130
+ test('should get item from localStorage', () => {
131
+ localStorage.getItem.mockReturnValue('val')
132
+ expect(window.Odac.storage('key')).toBe('val')
133
+ expect(localStorage.getItem).toHaveBeenCalledWith('key')
134
+ })
135
+
136
+ test('should set item in localStorage', () => {
137
+ window.Odac.storage('key', 'val')
138
+ expect(localStorage.setItem).toHaveBeenCalledWith('key', 'val')
139
+ })
140
+ })
141
+
142
+ describe('token()', () => {
143
+ test('should fetch token via sync XHR if hash is empty', () => {
144
+ mockXhr.response = JSON.stringify({token: 'new-token'})
145
+ document.cookie = 'odac_client=abc'
146
+
147
+ const token = window.Odac.token()
148
+
149
+ expect(window.XMLHttpRequest).toHaveBeenCalled()
150
+ expect(token).toBe('new-token')
151
+ })
152
+ })
153
+
154
+ describe('OdacWebSocket', () => {
155
+ test('should connect to WebSocket and handle events', () => {
156
+ const ws = window.Odac.ws('/test-ws', {token: false})
157
+ expect(window.WebSocket).toHaveBeenCalled()
158
+
159
+ const openHandler = jest.fn()
160
+ ws.on('open', openHandler)
161
+
162
+ const socketInstance = WebSocket.mock.results[0].value
163
+ socketInstance.onopen()
164
+
165
+ expect(openHandler).toHaveBeenCalled()
166
+ })
167
+ })
168
+ })
@@ -0,0 +1,112 @@
1
+ const fs = require('fs')
2
+ const os = require('os')
3
+
4
+ const Config = require('../src/Config')
5
+
6
+ jest.mock('fs')
7
+ jest.mock('os')
8
+
9
+ describe('Config', () => {
10
+ beforeEach(() => {
11
+ jest.clearAllMocks()
12
+ // Reset global.__dir which is used in Config.js
13
+ global.__dir = '/mock/project'
14
+
15
+ // Reset Config properties to defaults before each test
16
+ Config.system = undefined
17
+ Config.encrypt.key = 'odac'
18
+ })
19
+
20
+ describe('init', () => {
21
+ it('should load system config from home directory', () => {
22
+ os.homedir.mockReturnValue('/home/user')
23
+ fs.readFileSync.mockImplementation(path => {
24
+ if (path === '/home/user/.odac/config.json') {
25
+ return JSON.stringify({deviceId: '123'})
26
+ }
27
+ return '{}'
28
+ })
29
+ fs.existsSync.mockReturnValue(false)
30
+
31
+ Config.init()
32
+
33
+ expect(Config.system).toEqual({deviceId: '123'})
34
+ expect(fs.readFileSync).toHaveBeenCalledWith('/home/user/.odac/config.json')
35
+ })
36
+
37
+ it('should load project config and merge it', () => {
38
+ os.homedir.mockReturnValue('/home/user')
39
+ fs.existsSync.mockImplementation(path => {
40
+ if (path === '/mock/project/odac.json') return true
41
+ return false
42
+ })
43
+ fs.readFileSync.mockImplementation(path => {
44
+ if (path === '/mock/project/odac.json') {
45
+ return JSON.stringify({encrypt: {key: 'secret'}})
46
+ }
47
+ return '{}'
48
+ })
49
+
50
+ Config.init()
51
+
52
+ // The key gets hashed in init(), so it won't be 'secret' anymore
53
+ expect(Config.encrypt.key).not.toBe('secret')
54
+ expect(Config.encrypt.key).toBeInstanceOf(Buffer)
55
+ })
56
+
57
+ it('should interpolate variables in config', () => {
58
+ process.env.TEST_VAR = 'env_value'
59
+ os.homedir.mockReturnValue('/home/user')
60
+ fs.existsSync.mockReturnValue(true)
61
+ fs.readFileSync.mockReturnValue(
62
+ JSON.stringify({
63
+ custom: 'value-${TEST_VAR}'
64
+ })
65
+ )
66
+
67
+ Config.init()
68
+
69
+ expect(Config.custom).toBe('value-env_value')
70
+ })
71
+ })
72
+
73
+ describe('_interpolate', () => {
74
+ it('should replace ${VAR} with environment variables', () => {
75
+ process.env.FOO = 'bar'
76
+ const result = Config._interpolate('hello-${FOO}')
77
+ expect(result).toBe('hello-bar')
78
+ })
79
+
80
+ it('should replace ${odac} with client path', () => {
81
+ // __dirname in Config.js is /.../src, so it replaces /src with /client
82
+ const result = Config._interpolate('path-${odac}')
83
+ expect(result).toMatch(/\/client$/)
84
+ })
85
+
86
+ it('should handle nested objects and arrays', () => {
87
+ process.env.VAR = 'x'
88
+ const obj = {
89
+ a: ['${VAR}'],
90
+ b: {c: '${VAR}'}
91
+ }
92
+ const result = Config._interpolate(obj)
93
+ expect(result).toEqual({
94
+ a: ['x'],
95
+ b: {c: 'x'}
96
+ })
97
+ })
98
+ })
99
+
100
+ describe('_deepMerge', () => {
101
+ it('should merge objects deeply', () => {
102
+ const target = {a: {b: 1}, c: 2}
103
+ const source = {a: {d: 3}, e: 4}
104
+ Config._deepMerge(target, source)
105
+ expect(target).toEqual({
106
+ a: {b: 1, d: 3},
107
+ c: 2,
108
+ e: 4
109
+ })
110
+ })
111
+ })
112
+ })
@@ -0,0 +1,92 @@
1
+ const fs = require('fs')
2
+ const Lang = require('../src/Lang')
3
+
4
+ jest.mock('fs')
5
+
6
+ describe('Lang', () => {
7
+ let mockOdac
8
+ let lang
9
+
10
+ beforeEach(() => {
11
+ jest.clearAllMocks()
12
+ global.__dir = '/mock/project'
13
+
14
+ mockOdac = {
15
+ Config: {lang: {default: 'en'}},
16
+ Var: jest.fn(val => ({
17
+ is: jest.fn(type => type === 'alpha' && /^[a-zA-Z]+$/.test(val))
18
+ })),
19
+ Request: {
20
+ header: jest.fn()
21
+ }
22
+ }
23
+
24
+ // Default fs mock behaviors
25
+ fs.existsSync.mockReturnValue(false)
26
+ fs.mkdirSync.mockImplementation(() => {})
27
+ fs.writeFileSync.mockImplementation(() => {})
28
+ fs.readFileSync.mockImplementation(() => {
29
+ throw new Error('ENOENT')
30
+ })
31
+
32
+ lang = new Lang(mockOdac)
33
+ })
34
+
35
+ describe('constructor and set', () => {
36
+ it('should default to en when no config or header', () => {
37
+ lang = new Lang(mockOdac)
38
+ // Private #lang is not accessible, but we can check where it tries to save
39
+ lang.get('test')
40
+ expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/en.json'), expect.any(String))
41
+ })
42
+
43
+ it('should use lang from header if available', () => {
44
+ mockOdac.Request.header.mockReturnValue('tr-TR')
45
+ lang = new Lang(mockOdac)
46
+ lang.get('test')
47
+ expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/tr.json'), expect.any(String))
48
+ })
49
+
50
+ it('should use explicit lang in set()', () => {
51
+ lang = new Lang(mockOdac)
52
+ lang.set('fr')
53
+ lang.get('test')
54
+ expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/fr.json'), expect.any(String))
55
+ })
56
+ })
57
+
58
+ describe('get', () => {
59
+ it('should return matching string and support placeholders', () => {
60
+ // Mock loading tr.json
61
+ fs.existsSync.mockImplementation(path => path.includes('/tr.json'))
62
+ fs.readFileSync.mockReturnValue(
63
+ JSON.stringify({
64
+ welcome: 'Merhaba %s!'
65
+ })
66
+ )
67
+
68
+ lang.set('tr')
69
+ expect(lang.get('welcome', 'Emre')).toBe('Merhaba Emre!')
70
+ })
71
+
72
+ it('should support numbered placeholders', () => {
73
+ fs.existsSync.mockImplementation(path => path.includes('/en.json'))
74
+ fs.readFileSync.mockReturnValue(
75
+ JSON.stringify({
76
+ order: 'First: %s1, Second: %s2'
77
+ })
78
+ )
79
+
80
+ lang.set('en')
81
+ expect(lang.get('order', 'A', 'B')).toBe('First: A, Second: B')
82
+ })
83
+
84
+ it('should auto-save new keys', () => {
85
+ lang.set('en')
86
+ const result = lang.get('new_key')
87
+
88
+ expect(result).toBe('new_key')
89
+ expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/en.json'), expect.stringContaining('"new_key": "new_key"'))
90
+ })
91
+ })
92
+ })
@@ -0,0 +1,86 @@
1
+ const Odac = require('../src/Odac')
2
+
3
+ // Mock all dependencies
4
+ jest.mock('../src/Storage', () => ({init: jest.fn()}))
5
+ jest.mock('../src/Env', () => ({init: jest.fn(), get: jest.fn()}))
6
+ jest.mock('../src/Config', () => ({
7
+ init: jest.fn(),
8
+ request: {timeout: 10000},
9
+ lang: {default: 'en'}
10
+ }))
11
+ jest.mock('../src/Database', () => ({init: jest.fn()}))
12
+ jest.mock('../src/Ipc', () => ({init: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn()}))
13
+ jest.mock('../src/Route', () => {
14
+ return jest.fn().mockImplementation(() => ({
15
+ init: jest.fn(),
16
+ routes: {
17
+ www: {}
18
+ }
19
+ }))
20
+ })
21
+ jest.mock('../src/Server', () => ({init: jest.fn()}))
22
+
23
+ describe('Odac', () => {
24
+ beforeEach(() => {
25
+ jest.clearAllMocks()
26
+ global.__dir = '/mock/project'
27
+ })
28
+
29
+ describe('init', () => {
30
+ it('should initialize all components and set global.Odac', async () => {
31
+ await Odac.init()
32
+ expect(global.Odac).toBeDefined()
33
+ expect(global.Odac.Storage).toBeDefined()
34
+ expect(global.Odac.Config).toBeDefined()
35
+ expect(global.Odac.Env).toBeDefined()
36
+ expect(global.Odac.Database).toBeDefined()
37
+ expect(global.Odac.Ipc).toBeDefined()
38
+ expect(global.Odac.Route).toBeDefined()
39
+ expect(global.Odac.Server).toBeDefined()
40
+ expect(typeof global.__).toBe('function')
41
+ })
42
+ })
43
+
44
+ describe('instance', () => {
45
+ it('should create a context object without req/res', () => {
46
+ const ctx = Odac.instance('id-123')
47
+ expect(ctx.Config).toBeDefined()
48
+ expect(ctx.Database).toBeDefined()
49
+ expect(ctx.Ipc).toBeDefined()
50
+ expect(ctx.Request).toBeUndefined()
51
+ })
52
+
53
+ it('should create a context object with req/res', () => {
54
+ const mockReq = {url: '/', method: 'GET', headers: {host: 'example.com'}, connection: {remoteAddress: '127.0.0.1'}, on: jest.fn()}
55
+ const mockRes = {}
56
+ const ctx = Odac.instance('id-123', mockReq, mockRes)
57
+ expect(ctx.Request).toBeDefined()
58
+ expect(ctx.Auth).toBeDefined()
59
+ expect(ctx.Token).toBeDefined()
60
+ expect(ctx.Lang).toBeDefined()
61
+ expect(ctx.View).toBeDefined()
62
+ })
63
+
64
+ it('should provide helper methods on the context', () => {
65
+ const mockReq = {url: '/', method: 'GET', headers: {host: 'example.com'}, connection: {remoteAddress: '127.0.0.1'}, on: jest.fn()}
66
+ const mockRes = {end: jest.fn(), write: jest.fn()}
67
+ const ctx = Odac.instance('id-123', mockReq, mockRes)
68
+
69
+ expect(typeof ctx.env).toBe('function')
70
+ expect(typeof ctx.return).toBe('function')
71
+ expect(typeof ctx.write).toBe('function')
72
+ })
73
+
74
+ it('should handle Ipc subscription through Proxy', async () => {
75
+ const ctx = Odac.instance('id-123')
76
+ const callback = jest.fn()
77
+ const IpcSingleton = require('../src/Ipc')
78
+ IpcSingleton.subscribe.mockResolvedValue('sub-id')
79
+
80
+ await ctx.Ipc.subscribe('test-channel', callback)
81
+
82
+ expect(IpcSingleton.subscribe).toHaveBeenCalledWith('test-channel', callback)
83
+ expect(ctx._ipcSubs).toHaveLength(1)
84
+ })
85
+ })
86
+ })
@@ -1,4 +1,4 @@
1
- const Route = require('../../framework/src/Route.js')
1
+ const Route = require('../../src/Route.js')
2
2
 
3
3
  describe('Middleware System', () => {
4
4
  let route
@@ -52,7 +52,7 @@ describe('Middleware System', () => {
52
52
 
53
53
  test('chaining should work: auth.use().page()', () => {
54
54
  route.auth.use('admin').page('/admin', () => {})
55
- expect(route.routes.test.page['/admin'].middlewares).toEqual(['admin'])
55
+ expect(route.routes.test['#page']['/admin'].middlewares).toEqual(['admin'])
56
56
  })
57
57
 
58
58
  test('middlewares should be attached to routes', () => {
@@ -1,4 +1,4 @@
1
- const Route = require('../../framework/src/Route')
1
+ const Route = require('../src/Route')
2
2
 
3
3
  describe('Route', () => {
4
4
  let route
@@ -1,4 +1,4 @@
1
- const EarlyHints = require('../../../framework/src/View/EarlyHints')
1
+ const EarlyHints = require('../../src/View/EarlyHints')
2
2
 
3
3
  describe('EarlyHints', () => {
4
4
  let earlyHints
@@ -1,4 +1,4 @@
1
- const {WebSocketServer} = require('../../framework/src/WebSocket.js')
1
+ const {WebSocketServer} = require('../src/WebSocket.js')
2
2
 
3
3
  describe('WebSocketServer', () => {
4
4
  let server
@@ -67,7 +67,7 @@ describe('WebSocketServer', () => {
67
67
  })
68
68
 
69
69
  describe('Route WebSocket Integration', () => {
70
- const Route = require('../../framework/src/Route.js')
70
+ const Route = require('../src/Route.js')
71
71
 
72
72
  beforeEach(() => {
73
73
  global.Odac = {
@@ -17,7 +17,7 @@ function getStagedFiles() {
17
17
  return output
18
18
  .split('\n')
19
19
  .filter(file => file.endsWith('.js'))
20
- .filter(file => file.startsWith('core/') || file.startsWith('server/'))
20
+ .filter(file => file.startsWith('src/') || file.startsWith('client/'))
21
21
  .filter(file => !file.includes('.test.js') && !file.includes('.spec.js'))
22
22
  } catch (err) {
23
23
  console.error('Error getting staged files:', err.message)
@@ -34,7 +34,7 @@ function checkTestFiles(changedFiles) {
34
34
  if (!fs.existsSync(file)) continue
35
35
 
36
36
  // Determine test file path
37
- const testFile = file.replace(/^(core|server)\//, 'test/$1/').replace(/\.js$/, '.test.js')
37
+ const testFile = file.replace(/^(src|client)\//, 'test/').replace(/\.js$/, '.test.js')
38
38
 
39
39
  if (!fs.existsSync(testFile)) {
40
40
  missingTests.push({
@@ -60,7 +60,7 @@ function runTestsForFiles(files) {
60
60
  // Create a pattern to match test files for changed source files
61
61
  const testPatterns = files
62
62
  .map(file => {
63
- const testFile = file.replace(/^(core|server)\//, 'test/$1/').replace(/\.js$/, '.test.js')
63
+ const testFile = file.replace(/^(src|client)\//, 'test/').replace(/\.js$/, '.test.js')
64
64
  return testFile
65
65
  })
66
66
  .filter(testFile => fs.existsSync(testFile))
@@ -98,7 +98,7 @@ function main() {
98
98
  const changedFiles = getStagedFiles()
99
99
 
100
100
  if (changedFiles.length === 0) {
101
- console.log('✓ No core or server files changed\n')
101
+ console.log('✓ No src or client files changed\n')
102
102
  process.exit(0)
103
103
  }
104
104
 
@@ -1,99 +0,0 @@
1
- ## 🔌 Database Connection
2
-
3
- Odac automatically connects to your MySQL database when you provide the configuration.
4
-
5
- ### Configuration
6
-
7
- Add your database credentials to `config.json`:
8
-
9
- ```json
10
- {
11
- "database": {
12
- "host": "localhost",
13
- "user": "your_username",
14
- "password": "your_password",
15
- "database": "your_database_name"
16
- }
17
- }
18
- ```
19
-
20
- ### Multiple Databases
21
-
22
- You can configure multiple database connections:
23
-
24
- ```json
25
- {
26
- "database": {
27
- "default": {
28
- "host": "localhost",
29
- "user": "user1",
30
- "password": "pass1",
31
- "database": "main_db"
32
- },
33
- "analytics": {
34
- "host": "analytics.example.com",
35
- "user": "user2",
36
- "password": "pass2",
37
- "database": "analytics_db"
38
- }
39
- }
40
- }
41
- ```
42
-
43
- Access different databases:
44
-
45
- ```javascript
46
- // Default database
47
- const users = await Odac.Mysql.table('users').get()
48
-
49
- // Specific database
50
- const stats = await Odac.Mysql.database('analytics').table('stats').get()
51
- ```
52
-
53
- ### Environment Variables
54
-
55
- For security, use environment variables for sensitive data:
56
-
57
- **.env file:**
58
- ```
59
- DB_HOST=localhost
60
- DB_USER=myuser
61
- DB_PASSWORD=mypassword
62
- DB_NAME=mydatabase
63
- ```
64
-
65
- **config.json:**
66
- ```json
67
- {
68
- "database": {
69
- "host": "${DB_HOST}",
70
- "user": "${DB_USER}",
71
- "password": "${DB_PASSWORD}",
72
- "database": "${DB_NAME}"
73
- }
74
- }
75
- ```
76
-
77
- ### Connection Options
78
-
79
- Available configuration options:
80
-
81
- - `host` - Database server hostname (default: `localhost`)
82
- - `user` - Database username
83
- - `password` - Database password
84
- - `database` - Database name
85
- - `type` - Database type (currently only `mysql` is supported)
86
-
87
- ### Automatic Connection
88
-
89
- The connection is established automatically when your application starts. You don't need to write any connection code - just use `Odac.Mysql` in your controllers.
90
-
91
- ```javascript
92
- module.exports = async function (Odac) {
93
- // Connection is already established
94
- const users = await Odac.Mysql.table('users').get()
95
-
96
- Odac.set('users', users)
97
- Odac.View.set({ skeleton: 'main', content: 'users' })
98
- }
99
- ```