odac 1.4.1 → 1.4.3

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 (77) hide show
  1. package/.agent/rules/memory.md +5 -0
  2. package/.releaserc.js +9 -2
  3. package/CHANGELOG.md +64 -0
  4. package/README.md +1 -1
  5. package/bin/odac.js +3 -2
  6. package/client/odac.js +124 -28
  7. package/docs/ai/skills/backend/database.md +19 -0
  8. package/docs/ai/skills/backend/forms.md +107 -13
  9. package/docs/ai/skills/backend/migrations.md +8 -2
  10. package/docs/ai/skills/backend/validation.md +132 -32
  11. package/docs/ai/skills/frontend/forms.md +43 -15
  12. package/docs/backend/08-database/02-basics.md +49 -9
  13. package/docs/backend/08-database/04-migrations.md +1 -0
  14. package/package.json +1 -1
  15. package/src/Auth.js +15 -2
  16. package/src/Database/ConnectionFactory.js +1 -0
  17. package/src/Database/Migration.js +26 -1
  18. package/src/Database/nanoid.js +30 -0
  19. package/src/Database.js +122 -11
  20. package/src/Ipc.js +37 -0
  21. package/src/Odac.js +1 -1
  22. package/src/Route/Cron.js +11 -0
  23. package/src/Route.js +49 -30
  24. package/src/Server.js +77 -23
  25. package/src/Storage.js +15 -1
  26. package/src/Validator.js +22 -20
  27. package/test/{Auth.test.js → Auth/check.test.js} +91 -5
  28. package/test/Client/data.test.js +91 -0
  29. package/test/Client/get.test.js +90 -0
  30. package/test/Client/storage.test.js +87 -0
  31. package/test/Client/token.test.js +82 -0
  32. package/test/Client/ws.test.js +118 -0
  33. package/test/Config/deepMerge.test.js +14 -0
  34. package/test/Config/init.test.js +66 -0
  35. package/test/Config/interpolate.test.js +35 -0
  36. package/test/Database/ConnectionFactory/buildConnectionConfig.test.js +13 -0
  37. package/test/Database/ConnectionFactory/buildConnections.test.js +31 -0
  38. package/test/Database/ConnectionFactory/resolveClient.test.js +12 -0
  39. package/test/Database/Migration/migrate_column.test.js +52 -0
  40. package/test/Database/Migration/migrate_files.test.js +70 -0
  41. package/test/Database/Migration/migrate_index.test.js +89 -0
  42. package/test/Database/Migration/migrate_nanoid.test.js +160 -0
  43. package/test/Database/Migration/migrate_seed.test.js +77 -0
  44. package/test/Database/Migration/migrate_table.test.js +88 -0
  45. package/test/Database/Migration/rollback.test.js +61 -0
  46. package/test/Database/Migration/snapshot.test.js +38 -0
  47. package/test/Database/Migration/status.test.js +41 -0
  48. package/test/Database/autoNanoid.test.js +215 -0
  49. package/test/Database/nanoid.test.js +19 -0
  50. package/test/Lang/constructor.test.js +25 -0
  51. package/test/Lang/get.test.js +65 -0
  52. package/test/Lang/set.test.js +49 -0
  53. package/test/Odac/init.test.js +42 -0
  54. package/test/Odac/instance.test.js +58 -0
  55. package/test/Route/{Middleware.test.js → Middleware/chaining.test.js} +5 -29
  56. package/test/Route/Middleware/use.test.js +35 -0
  57. package/test/{Route.test.js → Route/check.test.js} +100 -50
  58. package/test/Route/set.test.js +52 -0
  59. package/test/Route/ws.test.js +23 -0
  60. package/test/View/EarlyHints/cache.test.js +32 -0
  61. package/test/View/EarlyHints/extractFromHtml.test.js +143 -0
  62. package/test/View/EarlyHints/formatLinkHeader.test.js +33 -0
  63. package/test/View/EarlyHints/send.test.js +99 -0
  64. package/test/View/{Form.test.js → Form/generateFieldHtml.test.js} +2 -2
  65. package/test/View/constructor.test.js +22 -0
  66. package/test/View/print.test.js +19 -0
  67. package/test/WebSocket/Client/limits.test.js +55 -0
  68. package/test/WebSocket/Server/broadcast.test.js +33 -0
  69. package/test/WebSocket/Server/route.test.js +37 -0
  70. package/test/Client.test.js +0 -197
  71. package/test/Config.test.js +0 -119
  72. package/test/Database/ConnectionFactory.test.js +0 -80
  73. package/test/Lang.test.js +0 -92
  74. package/test/Migration.test.js +0 -943
  75. package/test/Odac.test.js +0 -88
  76. package/test/View/EarlyHints.test.js +0 -282
  77. package/test/WebSocket.test.js +0 -238
@@ -0,0 +1,33 @@
1
+ const {WebSocketServer} = require('../../../src/WebSocket.js')
2
+
3
+ describe('WebSocketServer Broadcast', () => {
4
+ let server
5
+
6
+ beforeEach(() => {
7
+ server = new WebSocketServer()
8
+ })
9
+
10
+ it('should send message to all connected clients', () => {
11
+ const client1 = {id: 'c1', send: jest.fn()}
12
+ const client2 = {id: 'c2', send: jest.fn()}
13
+
14
+ server.clients.set('c1', client1)
15
+ server.clients.set('c2', client2)
16
+
17
+ server.broadcast('hello')
18
+ expect(client1.send).toHaveBeenCalledWith('hello')
19
+ expect(client2.send).toHaveBeenCalledWith('hello')
20
+ })
21
+
22
+ it('should exclude specified client from broadcast', () => {
23
+ const client1 = {id: 'c1', send: jest.fn()}
24
+ const client2 = {id: 'c2', send: jest.fn()}
25
+
26
+ server.clients.set('c1', client1)
27
+ server.clients.set('c2', client2)
28
+
29
+ server.broadcast('hello', 'c1')
30
+ expect(client1.send).not.toHaveBeenCalled()
31
+ expect(client2.send).toHaveBeenCalledWith('hello')
32
+ })
33
+ })
@@ -0,0 +1,37 @@
1
+ const {WebSocketServer} = require('../../../src/WebSocket.js')
2
+
3
+ describe('WebSocketServer Route Management', () => {
4
+ let server
5
+
6
+ beforeEach(() => {
7
+ server = new WebSocketServer()
8
+ })
9
+
10
+ it('should register a route', () => {
11
+ const handler = jest.fn()
12
+ server.route('/chat', handler)
13
+ expect(server.getRoute('/chat').handler).toBe(handler)
14
+ })
15
+
16
+ it('should return null for unregistered route', () => {
17
+ expect(server.getRoute('/unknown')).toBeNull()
18
+ })
19
+
20
+ it('should match parameterized routes', () => {
21
+ const handler = jest.fn()
22
+ server.route('/room/{id}', handler)
23
+
24
+ const result = server.getRoute('/room/123')
25
+ expect(result).toBeDefined()
26
+ expect(result.handler).toBe(handler)
27
+ expect(result.params).toEqual({id: '123'})
28
+ })
29
+
30
+ it('should match multiple parameters', () => {
31
+ const handler = jest.fn()
32
+ server.route('/chat/{room}/user/{userId}', handler)
33
+
34
+ const result = server.getRoute('/chat/general/user/42')
35
+ expect(result.params).toEqual({room: 'general', userId: '42'})
36
+ })
37
+ })
@@ -1,197 +0,0 @@
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('get()', () => {
155
- test('should automatically parse JSON if Content-Type is application/json', () => {
156
- const mockCallback = jest.fn()
157
- const mockData = {success: true}
158
-
159
- mockXhr.open.mockClear()
160
- mockXhr.send.mockClear()
161
-
162
- // Mock token() to avoid side effects and XHR calls
163
- jest.spyOn(window.Odac, 'token').mockReturnValue('mock-token')
164
-
165
- // Setup response for the get request
166
- mockXhr.responseText = JSON.stringify(mockData)
167
- mockXhr.status = 200
168
- mockXhr.statusText = 'OK'
169
- mockXhr.getResponseHeader.mockImplementation(header => {
170
- if (header === 'Content-Type') return 'application/json'
171
- return null
172
- })
173
-
174
- window.Odac.get('/api/test', mockCallback)
175
-
176
- // Trigger the completion of the get request
177
- if (mockXhr.onload) mockXhr.onload()
178
-
179
- expect(mockCallback).toHaveBeenCalledWith(mockData, expect.anything(), expect.anything())
180
- })
181
- })
182
-
183
- describe('OdacWebSocket', () => {
184
- test('should connect to WebSocket and handle events', () => {
185
- const ws = window.Odac.ws('/test-ws', {token: false})
186
- expect(window.WebSocket).toHaveBeenCalled()
187
-
188
- const openHandler = jest.fn()
189
- ws.on('open', openHandler)
190
-
191
- const socketInstance = WebSocket.mock.results[0].value
192
- socketInstance.onopen()
193
-
194
- expect(openHandler).toHaveBeenCalled()
195
- })
196
- })
197
- })
@@ -1,119 +0,0 @@
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 ${VAR} when variable name includes hyphen', () => {
81
- process.env['MY-VAR'] = 'hyphen-value'
82
- const result = Config._interpolate('hello-${MY-VAR}')
83
- expect(result).toBe('hello-hyphen-value')
84
- delete process.env['MY-VAR']
85
- })
86
-
87
- it('should replace ${odac} with client path', () => {
88
- // __dirname in Config.js is /.../src, so it replaces /src with /client
89
- const result = Config._interpolate('path-${odac}')
90
- expect(result).toMatch(/\/client$/)
91
- })
92
-
93
- it('should handle nested objects and arrays', () => {
94
- process.env.VAR = 'x'
95
- const obj = {
96
- a: ['${VAR}'],
97
- b: {c: '${VAR}'}
98
- }
99
- const result = Config._interpolate(obj)
100
- expect(result).toEqual({
101
- a: ['x'],
102
- b: {c: 'x'}
103
- })
104
- })
105
- })
106
-
107
- describe('_deepMerge', () => {
108
- it('should merge objects deeply', () => {
109
- const target = {a: {b: 1}, c: 2}
110
- const source = {a: {d: 3}, e: 4}
111
- Config._deepMerge(target, source)
112
- expect(target).toEqual({
113
- a: {b: 1, d: 3},
114
- c: 2,
115
- e: 4
116
- })
117
- })
118
- })
119
- })
@@ -1,80 +0,0 @@
1
- const mockKnex = jest.fn()
2
-
3
- jest.mock(
4
- 'knex',
5
- () =>
6
- (...args) =>
7
- mockKnex(...args)
8
- )
9
-
10
- const {buildConnections, buildConnectionConfig, resolveClient} = require('../../src/Database/ConnectionFactory')
11
-
12
- describe('Database ConnectionFactory', () => {
13
- beforeEach(() => {
14
- mockKnex.mockReset()
15
- mockKnex.mockImplementation(options => ({
16
- options,
17
- raw: jest.fn()
18
- }))
19
- })
20
-
21
- it('resolveClient should map known database aliases', () => {
22
- expect(resolveClient('postgres')).toBe('pg')
23
- expect(resolveClient('postgresql')).toBe('pg')
24
- expect(resolveClient('pg')).toBe('pg')
25
- expect(resolveClient('sqlite')).toBe('sqlite3')
26
- expect(resolveClient('sqlite3')).toBe('sqlite3')
27
- expect(resolveClient('mysql')).toBe('mysql2')
28
- })
29
-
30
- it('buildConnectionConfig should create sqlite filename config', () => {
31
- const config = buildConnectionConfig({database: 'db.sqlite3'}, 'sqlite3')
32
- expect(config).toEqual({filename: 'db.sqlite3'})
33
- })
34
-
35
- it('buildConnectionConfig should create host based config for non-sqlite', () => {
36
- const config = buildConnectionConfig(
37
- {
38
- user: 'root',
39
- password: 'secret',
40
- database: 'app',
41
- port: 3306
42
- },
43
- 'mysql2'
44
- )
45
-
46
- expect(config).toEqual({
47
- host: '127.0.0.1',
48
- user: 'root',
49
- password: 'secret',
50
- database: 'app',
51
- port: 3306
52
- })
53
- })
54
-
55
- it('buildConnections should support single database config', () => {
56
- const connections = buildConnections({
57
- type: 'mysql',
58
- user: 'root',
59
- database: 'app'
60
- })
61
-
62
- expect(Object.keys(connections)).toEqual(['default'])
63
- expect(mockKnex).toHaveBeenCalledTimes(1)
64
- expect(mockKnex.mock.calls[0][0]).toMatchObject({
65
- client: 'mysql2',
66
- pool: {min: 0, max: 10},
67
- useNullAsDefault: true
68
- })
69
- })
70
-
71
- it('buildConnections should support multi database config', () => {
72
- const connections = buildConnections({
73
- analytics: {type: 'postgres', user: 'u', database: 'a'},
74
- default: {type: 'sqlite', filename: './dev.sqlite3'}
75
- })
76
-
77
- expect(Object.keys(connections).sort()).toEqual(['analytics', 'default'])
78
- expect(mockKnex).toHaveBeenCalledTimes(2)
79
- })
80
- })
package/test/Lang.test.js DELETED
@@ -1,92 +0,0 @@
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
- })