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.
- package/.agent/rules/memory.md +5 -0
- package/.releaserc.js +9 -2
- package/CHANGELOG.md +64 -0
- package/README.md +1 -1
- package/bin/odac.js +3 -2
- package/client/odac.js +124 -28
- package/docs/ai/skills/backend/database.md +19 -0
- package/docs/ai/skills/backend/forms.md +107 -13
- package/docs/ai/skills/backend/migrations.md +8 -2
- package/docs/ai/skills/backend/validation.md +132 -32
- package/docs/ai/skills/frontend/forms.md +43 -15
- package/docs/backend/08-database/02-basics.md +49 -9
- package/docs/backend/08-database/04-migrations.md +1 -0
- package/package.json +1 -1
- package/src/Auth.js +15 -2
- package/src/Database/ConnectionFactory.js +1 -0
- package/src/Database/Migration.js +26 -1
- package/src/Database/nanoid.js +30 -0
- package/src/Database.js +122 -11
- package/src/Ipc.js +37 -0
- package/src/Odac.js +1 -1
- package/src/Route/Cron.js +11 -0
- package/src/Route.js +49 -30
- package/src/Server.js +77 -23
- package/src/Storage.js +15 -1
- package/src/Validator.js +22 -20
- package/test/{Auth.test.js → Auth/check.test.js} +91 -5
- package/test/Client/data.test.js +91 -0
- package/test/Client/get.test.js +90 -0
- package/test/Client/storage.test.js +87 -0
- package/test/Client/token.test.js +82 -0
- package/test/Client/ws.test.js +118 -0
- package/test/Config/deepMerge.test.js +14 -0
- package/test/Config/init.test.js +66 -0
- package/test/Config/interpolate.test.js +35 -0
- package/test/Database/ConnectionFactory/buildConnectionConfig.test.js +13 -0
- package/test/Database/ConnectionFactory/buildConnections.test.js +31 -0
- package/test/Database/ConnectionFactory/resolveClient.test.js +12 -0
- package/test/Database/Migration/migrate_column.test.js +52 -0
- package/test/Database/Migration/migrate_files.test.js +70 -0
- package/test/Database/Migration/migrate_index.test.js +89 -0
- package/test/Database/Migration/migrate_nanoid.test.js +160 -0
- package/test/Database/Migration/migrate_seed.test.js +77 -0
- package/test/Database/Migration/migrate_table.test.js +88 -0
- package/test/Database/Migration/rollback.test.js +61 -0
- package/test/Database/Migration/snapshot.test.js +38 -0
- package/test/Database/Migration/status.test.js +41 -0
- package/test/Database/autoNanoid.test.js +215 -0
- package/test/Database/nanoid.test.js +19 -0
- package/test/Lang/constructor.test.js +25 -0
- package/test/Lang/get.test.js +65 -0
- package/test/Lang/set.test.js +49 -0
- package/test/Odac/init.test.js +42 -0
- package/test/Odac/instance.test.js +58 -0
- package/test/Route/{Middleware.test.js → Middleware/chaining.test.js} +5 -29
- package/test/Route/Middleware/use.test.js +35 -0
- package/test/{Route.test.js → Route/check.test.js} +100 -50
- package/test/Route/set.test.js +52 -0
- package/test/Route/ws.test.js +23 -0
- package/test/View/EarlyHints/cache.test.js +32 -0
- package/test/View/EarlyHints/extractFromHtml.test.js +143 -0
- package/test/View/EarlyHints/formatLinkHeader.test.js +33 -0
- package/test/View/EarlyHints/send.test.js +99 -0
- package/test/View/{Form.test.js → Form/generateFieldHtml.test.js} +2 -2
- package/test/View/constructor.test.js +22 -0
- package/test/View/print.test.js +19 -0
- package/test/WebSocket/Client/limits.test.js +55 -0
- package/test/WebSocket/Server/broadcast.test.js +33 -0
- package/test/WebSocket/Server/route.test.js +37 -0
- package/test/Client.test.js +0 -197
- package/test/Config.test.js +0 -119
- package/test/Database/ConnectionFactory.test.js +0 -80
- package/test/Lang.test.js +0 -92
- package/test/Migration.test.js +0 -943
- package/test/Odac.test.js +0 -88
- package/test/View/EarlyHints.test.js +0 -282
- 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
|
+
})
|
package/test/Client.test.js
DELETED
|
@@ -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
|
-
})
|
package/test/Config.test.js
DELETED
|
@@ -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
|
-
})
|