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.
- package/.agent/rules/coding.md +27 -0
- package/.agent/rules/memory.md +33 -0
- package/.agent/rules/project.md +30 -0
- package/.agent/rules/workflow.md +16 -0
- package/.github/workflows/auto-pr-description.yml +3 -1
- package/.github/workflows/release.yml +42 -1
- package/.github/workflows/test-coverage.yml +6 -5
- package/.github/workflows/test-publish.yml +36 -0
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +13 -0
- package/.releaserc.js +3 -3
- package/CHANGELOG.md +184 -0
- package/README.md +53 -34
- package/bin/odac.js +181 -49
- package/client/odac.js +878 -995
- package/docs/backend/01-overview/03-development-server.md +39 -46
- package/docs/backend/02-structure/01-typical-project-layout.md +59 -25
- package/docs/backend/03-config/00-configuration-overview.md +15 -6
- package/docs/backend/03-config/01-database-connection.md +3 -3
- package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
- package/docs/backend/03-config/03-request-timeout.md +1 -1
- package/docs/backend/03-config/04-environment-variables.md +4 -4
- package/docs/backend/03-config/05-early-hints.md +2 -2
- package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
- package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
- package/docs/backend/04-routing/07-cron-jobs.md +17 -1
- package/docs/backend/04-routing/09-websocket.md +29 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
- package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
- package/docs/backend/05-controllers/03-controller-classes.md +61 -55
- package/docs/backend/05-forms/01-custom-forms.md +103 -95
- package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
- package/docs/backend/07-views/02-rendering-a-view.md +1 -1
- package/docs/backend/07-views/03-variables.md +5 -5
- package/docs/backend/07-views/04-request-data.md +1 -1
- package/docs/backend/07-views/08-backend-javascript.md +1 -1
- package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
- package/docs/backend/08-database/01-getting-started.md +100 -0
- package/docs/backend/08-database/02-basics.md +136 -0
- package/docs/backend/08-database/03-advanced.md +84 -0
- package/docs/backend/08-database/04-migrations.md +48 -0
- package/docs/backend/09-validation/01-the-validator-service.md +1 -0
- package/docs/backend/10-authentication/03-register.md +9 -2
- package/docs/backend/10-authentication/04-odac-register-forms.md +48 -48
- package/docs/backend/10-authentication/05-session-management.md +16 -2
- package/docs/backend/10-authentication/06-odac-login-forms.md +50 -50
- package/docs/backend/10-authentication/07-magic-links.md +134 -0
- package/docs/backend/11-mail/01-the-mail-service.md +118 -28
- package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
- package/docs/backend/13-utilities/01-odac-var.md +7 -7
- package/docs/backend/13-utilities/02-ipc.md +73 -0
- package/docs/frontend/01-overview/01-introduction.md +5 -1
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
- package/docs/index.json +21 -125
- package/eslint.config.mjs +5 -47
- package/jest.config.js +1 -1
- package/package.json +16 -7
- package/src/Auth.js +414 -121
- package/src/Config.js +12 -7
- package/src/Database.js +188 -0
- package/src/Env.js +3 -1
- package/src/Ipc.js +337 -0
- package/src/Lang.js +9 -2
- package/src/Mail.js +408 -37
- package/src/Odac.js +105 -40
- package/src/Request.js +71 -49
- package/src/Route/Cron.js +62 -18
- package/src/Route/Internal.js +215 -12
- package/src/Route/Middleware.js +7 -2
- package/src/Route.js +372 -109
- package/src/Server.js +118 -12
- package/src/Storage.js +169 -0
- package/src/Token.js +6 -4
- package/src/Validator.js +95 -3
- package/src/Var.js +22 -6
- package/src/View/EarlyHints.js +43 -33
- package/src/View/Form.js +210 -28
- package/src/View.js +108 -7
- package/src/WebSocket.js +18 -3
- package/template/odac.json +5 -0
- package/template/package.json +3 -1
- package/template/route/www.js +12 -10
- package/template/view/content/home.html +3 -3
- package/template/view/head/main.html +2 -2
- package/test/Client.test.js +168 -0
- package/test/Config.test.js +112 -0
- package/test/Lang.test.js +92 -0
- package/test/Odac.test.js +86 -0
- package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
- package/test/{framework/Route.test.js → Route.test.js} +1 -1
- package/test/{framework/View → View}/EarlyHints.test.js +1 -1
- package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
- package/test/scripts/check-coverage.js +4 -4
- package/docs/backend/08-database/01-database-connection.md +0 -99
- package/docs/backend/08-database/02-using-mysql.md +0 -322
- package/src/Mysql.js +0 -575
- package/template/config.json +0 -5
- package/test/cli/Cli.test.js +0 -36
- package/test/core/Candy.test.js +0 -234
- package/test/core/Commands.test.js +0 -538
- package/test/core/Config.test.js +0 -1432
- package/test/core/Lang.test.js +0 -250
- package/test/core/Process.test.js +0 -156
- package/test/server/Api.test.js +0 -647
- package/test/server/DNS.test.js +0 -2050
- package/test/server/DNS.test.js.bak +0 -2084
- package/test/server/Hub.test.js +0 -497
- package/test/server/Log.test.js +0 -73
- package/test/server/Mail.account.test_.js +0 -460
- package/test/server/Mail.init.test_.js +0 -411
- package/test/server/Mail.test_.js +0 -1340
- package/test/server/SSL.test_.js +0 -1491
- package/test/server/Server.test.js +0 -765
- package/test/server/Service.test_.js +0 -1127
- package/test/server/Subdomain.test.js +0 -440
- package/test/server/Web/Firewall.test.js +0 -175
- package/test/server/Web/Proxy.test.js +0 -397
- package/test/server/Web.test.js +0 -1494
- package/test/server/__mocks__/acme-client.js +0 -17
- package/test/server/__mocks__/bcrypt.js +0 -50
- package/test/server/__mocks__/child_process.js +0 -389
- package/test/server/__mocks__/crypto.js +0 -432
- package/test/server/__mocks__/fs.js +0 -450
- package/test/server/__mocks__/globalOdac.js +0 -227
- package/test/server/__mocks__/http.js +0 -575
- package/test/server/__mocks__/https.js +0 -272
- package/test/server/__mocks__/index.js +0 -249
- package/test/server/__mocks__/mail/server.js +0 -100
- package/test/server/__mocks__/mail/smtp.js +0 -31
- package/test/server/__mocks__/mailparser.js +0 -81
- package/test/server/__mocks__/net.js +0 -369
- package/test/server/__mocks__/node-forge.js +0 -328
- package/test/server/__mocks__/os.js +0 -320
- package/test/server/__mocks__/path.js +0 -291
- package/test/server/__mocks__/selfsigned.js +0 -8
- package/test/server/__mocks__/server/src/mail/server.js +0 -100
- package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
- package/test/server/__mocks__/smtp-server.js +0 -106
- package/test/server/__mocks__/sqlite3.js +0 -394
- package/test/server/__mocks__/testFactories.js +0 -299
- package/test/server/__mocks__/testHelpers.js +0 -363
- 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>
|
|
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/
|
|
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('../../
|
|
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
|
|
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 {WebSocketServer} = require('
|
|
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('
|
|
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('
|
|
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(/^(
|
|
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(/^(
|
|
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
|
|
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
|
-
```
|