odac 1.4.8 → 1.4.9
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/CHANGELOG.md +28 -0
- package/docs/ai/README.md +2 -1
- package/docs/ai/skills/SKILL.md +2 -1
- package/docs/ai/skills/backend/authentication.md +12 -6
- package/docs/ai/skills/backend/database.md +85 -5
- package/docs/ai/skills/backend/migrations.md +23 -0
- package/docs/ai/skills/backend/odac-var.md +155 -0
- package/docs/ai/skills/backend/utilities.md +1 -1
- package/docs/ai/skills/frontend/forms.md +23 -1
- package/docs/backend/04-routing/09-websocket-quick-reference.md +21 -1
- package/docs/backend/04-routing/09-websocket.md +22 -1
- package/docs/backend/08-database/06-read-through-cache.md +206 -0
- package/docs/backend/10-authentication/01-authentication-basics.md +53 -0
- package/docs/backend/10-authentication/05-session-management.md +12 -3
- package/docs/backend/13-utilities/01-odac-var.md +13 -19
- package/docs/frontend/03-forms/01-form-handling.md +15 -2
- package/docs/index.json +1 -1
- package/package.json +1 -1
- package/src/Auth.js +17 -0
- package/src/Database/Migration.js +219 -3
- package/src/Database/ReadCache.js +174 -0
- package/src/Database.js +63 -0
- package/src/Validator.js +1 -1
- package/src/Var.js +1 -0
- package/src/WebSocket.js +80 -23
- package/test/Database/Migration/migrate_column.test.js +168 -0
- package/test/Database/ReadCache/crossTable.test.js +179 -0
- package/test/Database/ReadCache/get.test.js +128 -0
- package/test/Database/ReadCache/invalidate.test.js +103 -0
- package/test/Database/ReadCache/proxy.test.js +184 -0
- package/test/Database/insert.test.js +98 -0
- package/test/WebSocket/Client/fragmentation.test.js +130 -0
- package/test/WebSocket/Client/limits.test.js +10 -4
- package/test/WebSocket/Client/readyState.test.js +154 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +0 -55
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const {WebSocketServer, WebSocketClient, READY_STATE} = require('../../../src/WebSocket.js')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper: creates a mock TCP socket with the minimum interface
|
|
5
|
+
* required by WebSocketClient.
|
|
6
|
+
*/
|
|
7
|
+
function createMockSocket() {
|
|
8
|
+
return {
|
|
9
|
+
pause: jest.fn(),
|
|
10
|
+
resume: jest.fn(),
|
|
11
|
+
on: jest.fn(),
|
|
12
|
+
write: jest.fn(),
|
|
13
|
+
end: jest.fn(),
|
|
14
|
+
removeAllListeners: jest.fn(),
|
|
15
|
+
writable: true
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('WebSocketClient readyState', () => {
|
|
20
|
+
let server
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
server = new WebSocketServer()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should expose static state constants matching READY_STATE enum', () => {
|
|
27
|
+
expect(WebSocketClient.CONNECTING).toBe(0)
|
|
28
|
+
expect(WebSocketClient.OPEN).toBe(1)
|
|
29
|
+
expect(WebSocketClient.CLOSING).toBe(2)
|
|
30
|
+
expect(WebSocketClient.CLOSED).toBe(3)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should export READY_STATE enum', () => {
|
|
34
|
+
expect(READY_STATE).toEqual({
|
|
35
|
+
CONNECTING: 0,
|
|
36
|
+
OPEN: 1,
|
|
37
|
+
CLOSING: 2,
|
|
38
|
+
CLOSED: 3
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should start in CONNECTING state', () => {
|
|
43
|
+
const socket = createMockSocket()
|
|
44
|
+
const client = new WebSocketClient(socket, server, 'rs-1')
|
|
45
|
+
expect(client.readyState).toBe(READY_STATE.CONNECTING)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should transition to OPEN on resume()', () => {
|
|
49
|
+
const socket = createMockSocket()
|
|
50
|
+
const client = new WebSocketClient(socket, server, 'rs-2')
|
|
51
|
+
|
|
52
|
+
client.resume()
|
|
53
|
+
|
|
54
|
+
expect(client.readyState).toBe(READY_STATE.OPEN)
|
|
55
|
+
expect(socket.resume).toHaveBeenCalled()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should transition to CLOSED after close()', () => {
|
|
59
|
+
const socket = createMockSocket()
|
|
60
|
+
const client = new WebSocketClient(socket, server, 'rs-3')
|
|
61
|
+
client.resume()
|
|
62
|
+
|
|
63
|
+
client.close()
|
|
64
|
+
|
|
65
|
+
expect(client.readyState).toBe(READY_STATE.CLOSED)
|
|
66
|
+
expect(socket.end).toHaveBeenCalled()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should be idempotent — second close() is a no-op', () => {
|
|
70
|
+
const socket = createMockSocket()
|
|
71
|
+
const client = new WebSocketClient(socket, server, 'rs-4')
|
|
72
|
+
client.resume()
|
|
73
|
+
|
|
74
|
+
client.close()
|
|
75
|
+
client.close()
|
|
76
|
+
|
|
77
|
+
expect(socket.end).toHaveBeenCalledTimes(1)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should not send data when in CONNECTING state', () => {
|
|
81
|
+
const socket = createMockSocket()
|
|
82
|
+
const client = new WebSocketClient(socket, server, 'rs-5')
|
|
83
|
+
|
|
84
|
+
client.send('hello')
|
|
85
|
+
|
|
86
|
+
expect(socket.write).not.toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should not send data when in CLOSED state', () => {
|
|
90
|
+
const socket = createMockSocket()
|
|
91
|
+
const client = new WebSocketClient(socket, server, 'rs-6')
|
|
92
|
+
client.resume()
|
|
93
|
+
client.close()
|
|
94
|
+
|
|
95
|
+
client.send('hello')
|
|
96
|
+
|
|
97
|
+
// Only the close frame write should exist, no data frame
|
|
98
|
+
const writes = socket.write.mock.calls
|
|
99
|
+
const lastWrite = writes[writes.length - 1]
|
|
100
|
+
// Close frame starts with 0x88
|
|
101
|
+
expect(lastWrite[0][0]).toBe(0x88)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should not send ping when not OPEN', () => {
|
|
105
|
+
const socket = createMockSocket()
|
|
106
|
+
const client = new WebSocketClient(socket, server, 'rs-7')
|
|
107
|
+
|
|
108
|
+
client.ping()
|
|
109
|
+
|
|
110
|
+
expect(socket.write).not.toHaveBeenCalled()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should not write when socket is not writable', () => {
|
|
114
|
+
const socket = createMockSocket()
|
|
115
|
+
socket.writable = false
|
|
116
|
+
const client = new WebSocketClient(socket, server, 'rs-8')
|
|
117
|
+
client.resume()
|
|
118
|
+
|
|
119
|
+
client.send('hello')
|
|
120
|
+
|
|
121
|
+
expect(socket.write).not.toHaveBeenCalled()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should transition to CLOSED when socket fires close event', () => {
|
|
125
|
+
const socket = createMockSocket()
|
|
126
|
+
const client = new WebSocketClient(socket, server, 'rs-9')
|
|
127
|
+
server.clients.set('rs-9', client)
|
|
128
|
+
client.resume()
|
|
129
|
+
|
|
130
|
+
// Simulate socket 'close' event
|
|
131
|
+
const closeHandler = socket.on.mock.calls.find(c => c[0] === 'close')[1]
|
|
132
|
+
closeHandler()
|
|
133
|
+
|
|
134
|
+
expect(client.readyState).toBe(READY_STATE.CLOSED)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should emit close event only once on double cleanup', () => {
|
|
138
|
+
const socket = createMockSocket()
|
|
139
|
+
const client = new WebSocketClient(socket, server, 'rs-10')
|
|
140
|
+
server.clients.set('rs-10', client)
|
|
141
|
+
client.resume()
|
|
142
|
+
|
|
143
|
+
const closeSpy = jest.fn()
|
|
144
|
+
client.on('close', closeSpy)
|
|
145
|
+
|
|
146
|
+
client.close()
|
|
147
|
+
|
|
148
|
+
// Simulate socket 'close' event firing after close() already cleaned up
|
|
149
|
+
const closeHandler = socket.on.mock.calls.find(c => c[0] === 'close')
|
|
150
|
+
if (closeHandler) closeHandler[1]()
|
|
151
|
+
|
|
152
|
+
expect(closeSpy).toHaveBeenCalledTimes(1)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
## 🔐 User Logins with `Auth.js`
|
|
2
|
-
|
|
3
|
-
The `Odac.Auth` service is your bouncer, managing who gets in and who stays out. It handles user login sessions for you.
|
|
4
|
-
|
|
5
|
-
#### Letting a User In
|
|
6
|
-
|
|
7
|
-
`Odac.Auth.login(userId, userData)`
|
|
8
|
-
|
|
9
|
-
* `userId`: A unique ID for the user (like their database ID).
|
|
10
|
-
* `userData`: An object with any user info you want to remember, like their username or role.
|
|
11
|
-
|
|
12
|
-
When you call this, `Auth` creates a secure session for the user.
|
|
13
|
-
|
|
14
|
-
> **💡 Enterprise Security:** ODAC automatically handles **Token Rotation** every 15 minutes (configurable) and includes built-in **CSRF protection** for all forms. Sessions are persistent across browser restarts by default.
|
|
15
|
-
|
|
16
|
-
#### Checking the Guest List
|
|
17
|
-
|
|
18
|
-
* `Odac.Auth.isLogin()`: Is the current user logged in? Returns `true` or `false`.
|
|
19
|
-
* `Odac.Auth.getId()`: Gets the ID of the logged-in user.
|
|
20
|
-
* `Odac.Auth.get('some-key')`: Grabs a specific piece of info from the `userData` you stored.
|
|
21
|
-
|
|
22
|
-
#### Showing a User Out
|
|
23
|
-
|
|
24
|
-
* `Odac.Auth.logout()`: Ends the user's session and logs them out.
|
|
25
|
-
|
|
26
|
-
#### Example: A Login Flow
|
|
27
|
-
```javascript
|
|
28
|
-
// Controller for your login form
|
|
29
|
-
module.exports = async function (Odac) {
|
|
30
|
-
const { username, password } = Odac.Request.post;
|
|
31
|
-
|
|
32
|
-
// IMPORTANT: You need to write your own code to find the user in your database!
|
|
33
|
-
const user = await yourDatabase.findUser(username, password);
|
|
34
|
-
|
|
35
|
-
if (user) {
|
|
36
|
-
// User is valid! Log them in.
|
|
37
|
-
Odac.Auth.login(user.id, { username: user.username });
|
|
38
|
-
return Odac.direct('/dashboard'); // Send them to their dashboard
|
|
39
|
-
} else {
|
|
40
|
-
// Bad credentials, send them back to the login page
|
|
41
|
-
return Odac.direct('/login?error=1');
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// A protected dashboard page
|
|
46
|
-
module.exports = function (Odac) {
|
|
47
|
-
// If they're not logged in, kick them back to the login page.
|
|
48
|
-
if (!Odac.Auth.isLogin()) {
|
|
49
|
-
return Odac.direct('/login');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const username = Odac.Auth.get('username');
|
|
53
|
-
return `Welcome back, ${username}!`;
|
|
54
|
-
}
|
|
55
|
-
```
|