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.
Files changed (113) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +33 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/release.yml +42 -1
  6. package/.github/workflows/test-coverage.yml +6 -5
  7. package/.github/workflows/test-publish.yml +36 -0
  8. package/.husky/pre-commit +10 -0
  9. package/.husky/pre-push +13 -0
  10. package/.releaserc.js +3 -3
  11. package/CHANGELOG.md +67 -0
  12. package/README.md +16 -0
  13. package/bin/odac.js +182 -40
  14. package/client/odac.js +10 -4
  15. package/docs/backend/01-overview/03-development-server.md +38 -45
  16. package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
  17. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  18. package/docs/backend/03-config/01-database-connection.md +2 -2
  19. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  20. package/docs/backend/03-config/03-request-timeout.md +1 -1
  21. package/docs/backend/03-config/04-environment-variables.md +4 -4
  22. package/docs/backend/03-config/05-early-hints.md +2 -2
  23. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  24. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  25. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  26. package/docs/backend/05-controllers/03-controller-classes.md +40 -20
  27. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  28. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  29. package/docs/backend/08-database/01-getting-started.md +2 -2
  30. package/docs/backend/10-authentication/03-register.md +1 -1
  31. package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
  32. package/docs/backend/10-authentication/05-session-management.md +15 -1
  33. package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
  34. package/docs/backend/10-authentication/07-magic-links.md +1 -1
  35. package/docs/index.json +5 -1
  36. package/jest.config.js +1 -1
  37. package/package.json +9 -5
  38. package/src/Auth.js +58 -23
  39. package/src/Config.js +7 -7
  40. package/src/Env.js +3 -1
  41. package/src/Ipc.js +7 -0
  42. package/src/Lang.js +9 -2
  43. package/src/Odac.js +44 -35
  44. package/src/Request.js +1 -1
  45. package/src/Route/Cron.js +58 -17
  46. package/src/Route/Internal.js +1 -1
  47. package/src/Route.js +282 -99
  48. package/src/Server.js +40 -3
  49. package/src/Storage.js +4 -0
  50. package/src/Token.js +6 -4
  51. package/src/Validator.js +1 -1
  52. package/src/Var.js +22 -6
  53. package/src/View/EarlyHints.js +43 -33
  54. package/src/View/Form.js +17 -11
  55. package/src/View.js +62 -6
  56. package/template/package.json +3 -1
  57. package/template/view/content/home.html +3 -3
  58. package/template/view/head/main.html +2 -2
  59. package/test/Client.test.js +168 -0
  60. package/test/Config.test.js +112 -0
  61. package/test/Lang.test.js +92 -0
  62. package/test/Odac.test.js +86 -0
  63. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  64. package/test/{framework/Route.test.js → Route.test.js} +1 -1
  65. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  66. package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
  67. package/test/scripts/check-coverage.js +4 -4
  68. package/test/cli/Cli.test.js +0 -36
  69. package/test/core/Commands.test.js +0 -538
  70. package/test/core/Config.test.js +0 -1432
  71. package/test/core/Lang.test.js +0 -250
  72. package/test/core/Odac.test.js +0 -234
  73. package/test/core/Process.test.js +0 -156
  74. package/test/server/Api.test.js +0 -647
  75. package/test/server/DNS.test.js +0 -2050
  76. package/test/server/DNS.test.js.bak +0 -2084
  77. package/test/server/Hub.test.js +0 -497
  78. package/test/server/Log.test.js +0 -73
  79. package/test/server/Mail.account.test_.js +0 -460
  80. package/test/server/Mail.init.test_.js +0 -411
  81. package/test/server/Mail.test_.js +0 -1340
  82. package/test/server/SSL.test_.js +0 -1491
  83. package/test/server/Server.test.js +0 -765
  84. package/test/server/Service.test_.js +0 -1127
  85. package/test/server/Subdomain.test.js +0 -440
  86. package/test/server/Web/Firewall.test.js +0 -175
  87. package/test/server/Web/Proxy.test.js +0 -397
  88. package/test/server/Web.test.js +0 -1494
  89. package/test/server/__mocks__/acme-client.js +0 -17
  90. package/test/server/__mocks__/bcrypt.js +0 -50
  91. package/test/server/__mocks__/child_process.js +0 -389
  92. package/test/server/__mocks__/crypto.js +0 -432
  93. package/test/server/__mocks__/fs.js +0 -450
  94. package/test/server/__mocks__/globalOdac.js +0 -227
  95. package/test/server/__mocks__/http.js +0 -575
  96. package/test/server/__mocks__/https.js +0 -272
  97. package/test/server/__mocks__/index.js +0 -249
  98. package/test/server/__mocks__/mail/server.js +0 -100
  99. package/test/server/__mocks__/mail/smtp.js +0 -31
  100. package/test/server/__mocks__/mailparser.js +0 -81
  101. package/test/server/__mocks__/net.js +0 -369
  102. package/test/server/__mocks__/node-forge.js +0 -328
  103. package/test/server/__mocks__/os.js +0 -320
  104. package/test/server/__mocks__/path.js +0 -291
  105. package/test/server/__mocks__/selfsigned.js +0 -8
  106. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  107. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  108. package/test/server/__mocks__/smtp-server.js +0 -106
  109. package/test/server/__mocks__/sqlite3.js +0 -394
  110. package/test/server/__mocks__/testFactories.js +0 -299
  111. package/test/server/__mocks__/testHelpers.js +0 -363
  112. package/test/server/__mocks__/tls.js +0 -229
  113. /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('../../framework/src/Route.js')
1
+ const Route = require('../../src/Route.js')
2
2
 
3
3
  describe('Middleware System', () => {
4
4
  let route
@@ -52,7 +52,7 @@ describe('Middleware System', () => {
52
52
 
53
53
  test('chaining should work: auth.use().page()', () => {
54
54
  route.auth.use('admin').page('/admin', () => {})
55
- expect(route.routes.test.page['/admin'].middlewares).toEqual(['admin'])
55
+ expect(route.routes.test['#page']['/admin'].middlewares).toEqual(['admin'])
56
56
  })
57
57
 
58
58
  test('middlewares should be attached to routes', () => {
@@ -1,4 +1,4 @@
1
- const Route = require('../../framework/src/Route')
1
+ const Route = require('../src/Route')
2
2
 
3
3
  describe('Route', () => {
4
4
  let route
@@ -1,4 +1,4 @@
1
- const EarlyHints = require('../../../framework/src/View/EarlyHints')
1
+ const EarlyHints = require('../../src/View/EarlyHints')
2
2
 
3
3
  describe('EarlyHints', () => {
4
4
  let earlyHints
@@ -1,4 +1,4 @@
1
- const {WebSocketServer} = require('../../framework/src/WebSocket.js')
1
+ const {WebSocketServer} = require('../src/WebSocket.js')
2
2
 
3
3
  describe('WebSocketServer', () => {
4
4
  let server
@@ -67,7 +67,7 @@ describe('WebSocketServer', () => {
67
67
  })
68
68
 
69
69
  describe('Route WebSocket Integration', () => {
70
- const Route = require('../../framework/src/Route.js')
70
+ const Route = require('../src/Route.js')
71
71
 
72
72
  beforeEach(() => {
73
73
  global.Odac = {
@@ -17,7 +17,7 @@ function getStagedFiles() {
17
17
  return output
18
18
  .split('\n')
19
19
  .filter(file => file.endsWith('.js'))
20
- .filter(file => file.startsWith('core/') || file.startsWith('server/'))
20
+ .filter(file => file.startsWith('src/') || file.startsWith('client/'))
21
21
  .filter(file => !file.includes('.test.js') && !file.includes('.spec.js'))
22
22
  } catch (err) {
23
23
  console.error('Error getting staged files:', err.message)
@@ -34,7 +34,7 @@ function checkTestFiles(changedFiles) {
34
34
  if (!fs.existsSync(file)) continue
35
35
 
36
36
  // Determine test file path
37
- const testFile = file.replace(/^(core|server)\//, 'test/$1/').replace(/\.js$/, '.test.js')
37
+ const testFile = file.replace(/^(src|client)\//, 'test/').replace(/\.js$/, '.test.js')
38
38
 
39
39
  if (!fs.existsSync(testFile)) {
40
40
  missingTests.push({
@@ -60,7 +60,7 @@ function runTestsForFiles(files) {
60
60
  // Create a pattern to match test files for changed source files
61
61
  const testPatterns = files
62
62
  .map(file => {
63
- const testFile = file.replace(/^(core|server)\//, 'test/$1/').replace(/\.js$/, '.test.js')
63
+ const testFile = file.replace(/^(src|client)\//, 'test/').replace(/\.js$/, '.test.js')
64
64
  return testFile
65
65
  })
66
66
  .filter(testFile => fs.existsSync(testFile))
@@ -98,7 +98,7 @@ function main() {
98
98
  const changedFiles = getStagedFiles()
99
99
 
100
100
  if (changedFiles.length === 0) {
101
- console.log('✓ No core or server files changed\n')
101
+ console.log('✓ No src or client files changed\n')
102
102
  process.exit(0)
103
103
  }
104
104
 
@@ -1,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
- })