odac 1.1.0 → 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/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 +67 -0
- package/README.md +16 -0
- package/bin/odac.js +182 -40
- package/client/odac.js +10 -4
- package/docs/backend/01-overview/03-development-server.md +38 -45
- package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
- package/docs/backend/03-config/00-configuration-overview.md +6 -6
- package/docs/backend/03-config/01-database-connection.md +2 -2
- 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/03-api-and-data-routes.md +18 -0
- package/docs/backend/04-routing/07-cron-jobs.md +17 -1
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
- package/docs/backend/05-controllers/03-controller-classes.md +40 -20
- 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/10-styling-and-tailwind.md +93 -0
- package/docs/backend/08-database/01-getting-started.md +2 -2
- package/docs/backend/10-authentication/03-register.md +1 -1
- package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
- package/docs/backend/10-authentication/05-session-management.md +15 -1
- package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
- package/docs/backend/10-authentication/07-magic-links.md +1 -1
- package/docs/index.json +5 -1
- package/jest.config.js +1 -1
- package/package.json +9 -5
- package/src/Auth.js +58 -23
- package/src/Config.js +7 -7
- package/src/Env.js +3 -1
- package/src/Ipc.js +7 -0
- package/src/Lang.js +9 -2
- package/src/Odac.js +44 -35
- package/src/Request.js +1 -1
- package/src/Route/Cron.js +58 -17
- package/src/Route/Internal.js +1 -1
- package/src/Route.js +282 -99
- package/src/Server.js +40 -3
- package/src/Storage.js +4 -0
- package/src/Token.js +6 -4
- package/src/Validator.js +1 -1
- package/src/Var.js +22 -6
- package/src/View/EarlyHints.js +43 -33
- package/src/View/Form.js +17 -11
- package/src/View.js +62 -6
- package/template/package.json +3 -1
- 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/test/cli/Cli.test.js +0 -36
- 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/Odac.test.js +0 -234
- 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
- /package/template/{config.json → odac.json} +0 -0
|
@@ -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
|
|
package/test/cli/Cli.test.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
const Cli = require('../../cli/src/Cli.js')
|
|
2
|
-
|
|
3
|
-
describe('CLI Prefix Argument Parsing', () => {
|
|
4
|
-
test('parseArg should extract value after prefix', () => {
|
|
5
|
-
const args = ['web', 'create', '-d', 'example.com', '--other', 'value']
|
|
6
|
-
|
|
7
|
-
expect(Cli.parseArg(args, ['-d', '--domain'])).toBe('example.com')
|
|
8
|
-
expect(Cli.parseArg(args, ['--other'])).toBe('value')
|
|
9
|
-
expect(Cli.parseArg(args, ['-x', '--missing'])).toBeNull()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
test('parseArg should handle multiple prefix options', () => {
|
|
13
|
-
const args = ['mail', 'create', '--email', 'test@example.com', '-p', 'password123']
|
|
14
|
-
|
|
15
|
-
expect(Cli.parseArg(args, ['-e', '--email'])).toBe('test@example.com')
|
|
16
|
-
expect(Cli.parseArg(args, ['-p', '--password'])).toBe('password123')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('parseArg should return null for missing arguments', () => {
|
|
20
|
-
const args = ['web', 'create']
|
|
21
|
-
|
|
22
|
-
expect(Cli.parseArg(args, ['-d', '--domain'])).toBeNull()
|
|
23
|
-
expect(Cli.parseArg(null, ['-d'])).toBeNull()
|
|
24
|
-
expect(Cli.parseArg(args, null)).toBeNull()
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('parseArg should handle edge cases', () => {
|
|
28
|
-
const args = ['command', '-d']
|
|
29
|
-
|
|
30
|
-
// Missing value after prefix
|
|
31
|
-
expect(Cli.parseArg(args, ['-d'])).toBeNull()
|
|
32
|
-
|
|
33
|
-
// Empty args
|
|
34
|
-
expect(Cli.parseArg([], ['-d'])).toBeNull()
|
|
35
|
-
})
|
|
36
|
-
})
|