odac 1.4.14 → 1.4.16
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 +30 -0
- package/bin/odac.js +1 -0
- package/client/odac.js +57 -1
- package/docs/ai/skills/backend/forms.md +54 -3
- package/docs/ai/skills/backend/validation.md +30 -2
- package/docs/ai/skills/frontend/core.md +5 -5
- package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +1 -1
- package/docs/backend/05-forms/01-custom-forms.md +96 -0
- package/docs/backend/09-validation/01-the-validator-service.md +40 -0
- package/docs/frontend/01-overview/01-introduction.md +8 -8
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +14 -14
- package/docs/frontend/02-ajax-navigation/02-configuration.md +4 -4
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +16 -16
- package/docs/frontend/03-forms/01-form-handling.md +84 -31
- package/docs/frontend/04-api-requests/01-get-post.md +25 -25
- package/docs/frontend/05-streaming/01-client-streaming.md +14 -14
- package/docs/frontend/06-websocket/00-overview.md +1 -1
- package/index.js +11 -1
- package/package.json +2 -1
- package/src/Auth.js +252 -24
- package/src/Config.js +5 -1
- package/src/Odac.js +3 -0
- package/src/Request.js +235 -34
- package/src/Route/Internal.js +32 -3
- package/src/Validator.js +143 -2
- package/src/View/Form.js +56 -0
- package/src/View/Image.js +15 -6
- package/src/View.js +2 -2
- package/test/Auth/check.test.js +219 -18
- package/test/Auth/verifyMagicLink.test.js +8 -1
- package/test/Odac/cache.test.js +5 -2
- package/test/Odac/image.test.js +8 -5
- package/test/Odac/instance.test.js +5 -2
- package/test/Request/cache.test.js +1 -0
- package/test/Request/multipart.test.js +164 -0
- package/test/Validator/file.test.js +172 -0
- package/test/View/Form/generateFieldHtml.test.js +56 -0
- package/test/View/Image/serve.test.js +9 -4
- package/test/View/Image/url.test.js +10 -4
- package/test/View/addNavigateAttribute.test.js +10 -1
- package/test/View/print.test.js +10 -1
|
@@ -6,31 +6,31 @@ Advanced techniques and patterns for AJAX navigation in Odac.
|
|
|
6
6
|
|
|
7
7
|
## Programmatic Navigation
|
|
8
8
|
|
|
9
|
-
### Using
|
|
9
|
+
### Using Odac.load()
|
|
10
10
|
|
|
11
11
|
Navigate programmatically from your code:
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
14
|
// Basic usage
|
|
15
|
-
|
|
15
|
+
Odac.load('/about')
|
|
16
16
|
|
|
17
17
|
// With callback
|
|
18
|
-
|
|
18
|
+
Odac.load('/about', function(page, variables) {
|
|
19
19
|
console.log('Loaded:', page)
|
|
20
20
|
console.log('Data:', variables)
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
// Without updating history
|
|
24
|
-
|
|
24
|
+
Odac.load('/about', callback, false)
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
### Use Cases
|
|
28
28
|
|
|
29
29
|
**Redirect after form submission:**
|
|
30
30
|
```javascript
|
|
31
|
-
|
|
31
|
+
Odac.form('#my-form', function(data) {
|
|
32
32
|
if (data.result.success) {
|
|
33
|
-
|
|
33
|
+
Odac.load('/success')
|
|
34
34
|
}
|
|
35
35
|
})
|
|
36
36
|
```
|
|
@@ -38,16 +38,16 @@ odac.form('#my-form', function(data) {
|
|
|
38
38
|
**Conditional navigation:**
|
|
39
39
|
```javascript
|
|
40
40
|
if (user.isLoggedIn) {
|
|
41
|
-
|
|
41
|
+
Odac.load('/dashboard')
|
|
42
42
|
} else {
|
|
43
|
-
|
|
43
|
+
Odac.load('/login')
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
**Timed navigation:**
|
|
48
48
|
```javascript
|
|
49
49
|
setTimeout(() => {
|
|
50
|
-
|
|
50
|
+
Odac.load('/next-page')
|
|
51
51
|
}, 3000)
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -76,8 +76,8 @@ Define all parts in your controller:
|
|
|
76
76
|
|
|
77
77
|
```javascript
|
|
78
78
|
module.exports = function(Odac) {
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Odac.View.skeleton('main')
|
|
80
|
+
Odac.View.set({
|
|
81
81
|
header: 'main',
|
|
82
82
|
content: 'dashboard',
|
|
83
83
|
sidebar: 'dashboard',
|
|
@@ -333,10 +333,10 @@ Odac.action({
|
|
|
333
333
|
Gracefully handle navigation errors:
|
|
334
334
|
|
|
335
335
|
```javascript
|
|
336
|
-
// Override
|
|
337
|
-
const originalLoad =
|
|
336
|
+
// Override Odac.load to add error handling
|
|
337
|
+
const originalLoad = Odac.load.bind(Odac)
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
Odac.load = function(url, callback, push) {
|
|
340
340
|
try {
|
|
341
341
|
originalLoad(url, function(page, variables) {
|
|
342
342
|
if (callback) callback(page, variables)
|
|
@@ -359,7 +359,7 @@ function loadWithRetry(url, maxRetries = 3) {
|
|
|
359
359
|
|
|
360
360
|
function attempt() {
|
|
361
361
|
attempts++
|
|
362
|
-
|
|
362
|
+
Odac.load(url,
|
|
363
363
|
(page, vars) => {
|
|
364
364
|
console.log('Success after', attempts, 'attempts')
|
|
365
365
|
},
|
|
@@ -491,7 +491,7 @@ describe('Navigation', () => {
|
|
|
491
491
|
})
|
|
492
492
|
|
|
493
493
|
// Simulate navigation
|
|
494
|
-
|
|
494
|
+
Odac.load('/about')
|
|
495
495
|
|
|
496
496
|
// Assert
|
|
497
497
|
expect(document.querySelector('.nav-link.active').href)
|
|
@@ -14,7 +14,7 @@ Learn how to handle forms with automatic AJAX submission, CSRF protection, and v
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
```javascript
|
|
17
|
-
|
|
17
|
+
Odac.form('#contact-form', function(data) {
|
|
18
18
|
if (data.result.success) {
|
|
19
19
|
alert('Form submitted successfully!')
|
|
20
20
|
}
|
|
@@ -35,7 +35,7 @@ That's it! odac.js handles:
|
|
|
35
35
|
### Basic Usage
|
|
36
36
|
|
|
37
37
|
```javascript
|
|
38
|
-
|
|
38
|
+
Odac.form('#my-form', function(data) {
|
|
39
39
|
console.log('Response:', data)
|
|
40
40
|
})
|
|
41
41
|
```
|
|
@@ -43,7 +43,7 @@ odac.form('#my-form', function(data) {
|
|
|
43
43
|
### With Options
|
|
44
44
|
|
|
45
45
|
```javascript
|
|
46
|
-
|
|
46
|
+
Odac.form({
|
|
47
47
|
form: '#my-form',
|
|
48
48
|
messages: true, // Show error/success messages
|
|
49
49
|
loading: function(percent) {
|
|
@@ -59,10 +59,37 @@ odac.form({
|
|
|
59
59
|
### Redirect After Submit
|
|
60
60
|
|
|
61
61
|
```javascript
|
|
62
|
-
|
|
62
|
+
Odac.form('#my-form', '/success-page')
|
|
63
63
|
// Redirects to /success-page on success
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Using with `<odac:form>` Components
|
|
67
|
+
|
|
68
|
+
`Odac.form()` works with both plain `<form>` elements and server-rendered
|
|
69
|
+
[`<odac:form>` components](../../backend/05-forms/01-custom-forms.md).
|
|
70
|
+
|
|
71
|
+
An `<odac:form>` already registers itself automatically, so you only call
|
|
72
|
+
`Odac.form()` when you want to add your own callback. Give the component an `id`
|
|
73
|
+
and bind to it:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<odac:form action="Contact.submit" id="contact-form">
|
|
77
|
+
<!-- fields -->
|
|
78
|
+
<odac:submit text="Send"/>
|
|
79
|
+
</odac:form>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
Odac.form('#contact-form', function(data) {
|
|
84
|
+
if (data.result.success) {
|
|
85
|
+
closeModal()
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Calling `Odac.form()` on an already-registered `<odac:form>` **re-binds** the
|
|
91
|
+
same form instead of adding a second handler, so it still submits only once.
|
|
92
|
+
|
|
66
93
|
## Error Handling
|
|
67
94
|
|
|
68
95
|
### Automatic Error Display
|
|
@@ -85,7 +112,7 @@ If you want to control where errors appear, add error elements manually:
|
|
|
85
112
|
### Custom Error Display
|
|
86
113
|
|
|
87
114
|
```javascript
|
|
88
|
-
|
|
115
|
+
Odac.form('#my-form', function(data) {
|
|
89
116
|
if (!data.result.success) {
|
|
90
117
|
// Custom error handling
|
|
91
118
|
Object.entries(data.errors).forEach(([field, message]) => {
|
|
@@ -137,7 +164,7 @@ If you want to control where success messages appear, add a success element manu
|
|
|
137
164
|
### Custom Success Message
|
|
138
165
|
|
|
139
166
|
```javascript
|
|
140
|
-
|
|
167
|
+
Odac.form('#my-form', function(data) {
|
|
141
168
|
if (data.result.success) {
|
|
142
169
|
document.querySelector('#custom-message').innerHTML =
|
|
143
170
|
'Thank you! Your form has been submitted.'
|
|
@@ -147,49 +174,66 @@ odac.form('#my-form', function(data) {
|
|
|
147
174
|
|
|
148
175
|
## File Uploads
|
|
149
176
|
|
|
150
|
-
### Basic File Upload
|
|
177
|
+
### Basic File Upload with `<odac:form>`
|
|
178
|
+
|
|
179
|
+
File upload in Odac forms is seamless — just add a `type="file"` input and validation rules:
|
|
151
180
|
|
|
152
181
|
```html
|
|
153
|
-
<form
|
|
154
|
-
<input
|
|
155
|
-
|
|
156
|
-
</
|
|
182
|
+
<odac:form action="Profile.saveAvatar" id="avatar-form">
|
|
183
|
+
<odac:input type="file" name="avatar" label="Profile Picture">
|
|
184
|
+
<odac:validate rule="required|maxsize:2MB|mimetype:image/png,image/jpeg" message="PNG/JPEG, max 2MB"/>
|
|
185
|
+
</odac:input>
|
|
186
|
+
<odac:submit text="Upload"/>
|
|
187
|
+
</odac:form>
|
|
157
188
|
```
|
|
158
189
|
|
|
159
190
|
```javascript
|
|
160
|
-
|
|
191
|
+
Odac.form('#avatar-form', function(data) {
|
|
161
192
|
if (data.result.success) {
|
|
162
|
-
|
|
193
|
+
alert('Avatar uploaded!')
|
|
163
194
|
}
|
|
164
195
|
})
|
|
165
196
|
```
|
|
166
197
|
|
|
167
|
-
|
|
198
|
+
**Features:**
|
|
199
|
+
- ✅ Client-side validation (size, MIME type, extension)
|
|
200
|
+
- ✅ Server-side validation with magic-byte sniffing for images
|
|
201
|
+
- ✅ Automatic multipart/form-data handling
|
|
202
|
+
- ✅ File size progress tracking
|
|
203
|
+
|
|
204
|
+
### Upload Progress Tracking
|
|
168
205
|
|
|
169
206
|
```javascript
|
|
170
|
-
|
|
171
|
-
form: '#
|
|
207
|
+
Odac.form({
|
|
208
|
+
form: '#avatar-form',
|
|
172
209
|
loading: function(percent) {
|
|
173
210
|
document.querySelector('#progress').style.width = percent + '%'
|
|
174
|
-
document.querySelector('#progress-text').textContent = percent + '%'
|
|
175
211
|
}
|
|
176
212
|
}, function(data) {
|
|
177
|
-
|
|
213
|
+
if (data.result.success) {
|
|
214
|
+
console.log('Upload complete!')
|
|
215
|
+
}
|
|
178
216
|
})
|
|
179
217
|
```
|
|
180
218
|
|
|
181
|
-
### Multiple
|
|
219
|
+
### Multiple File Upload
|
|
220
|
+
|
|
221
|
+
For multiple file selection, add the `multiple` attribute and use `maxfiles` validation:
|
|
182
222
|
|
|
183
223
|
```html
|
|
184
|
-
<input name="
|
|
224
|
+
<odac:input type="file" name="documents" label="Documents" multiple>
|
|
225
|
+
<odac:validate rule="maxfiles:5|ext:pdf,docx" message="Max 5 PDFs/Word docs"/>
|
|
226
|
+
</odac:input>
|
|
185
227
|
```
|
|
186
228
|
|
|
229
|
+
**Note:** File inputs are never repopulated on validation error (for security). Users must re-select files after an error.
|
|
230
|
+
|
|
187
231
|
## Advanced Features
|
|
188
232
|
|
|
189
233
|
### Disable Messages
|
|
190
234
|
|
|
191
235
|
```javascript
|
|
192
|
-
|
|
236
|
+
Odac.form({
|
|
193
237
|
form: '#my-form',
|
|
194
238
|
messages: false // Don't show automatic messages
|
|
195
239
|
}, function(data) {
|
|
@@ -201,7 +245,7 @@ odac.form({
|
|
|
201
245
|
|
|
202
246
|
```javascript
|
|
203
247
|
// Only show error messages, suppress success messages
|
|
204
|
-
|
|
248
|
+
Odac.form({
|
|
205
249
|
form: '#my-form',
|
|
206
250
|
messages: ['error']
|
|
207
251
|
}, function(data) {
|
|
@@ -211,7 +255,7 @@ odac.form({
|
|
|
211
255
|
})
|
|
212
256
|
|
|
213
257
|
// Only show success messages, suppress error messages
|
|
214
|
-
|
|
258
|
+
Odac.form({
|
|
215
259
|
form: '#my-form',
|
|
216
260
|
messages: ['success']
|
|
217
261
|
}, function(data) {
|
|
@@ -224,7 +268,7 @@ odac.form({
|
|
|
224
268
|
### Form Reset
|
|
225
269
|
|
|
226
270
|
```javascript
|
|
227
|
-
|
|
271
|
+
Odac.form('#my-form', function(data) {
|
|
228
272
|
if (data.result.success) {
|
|
229
273
|
// Reset the form
|
|
230
274
|
document.querySelector('#my-form').reset()
|
|
@@ -242,7 +286,7 @@ document.querySelector('#my-form').addEventListener('submit', function(e) {
|
|
|
242
286
|
}
|
|
243
287
|
})
|
|
244
288
|
|
|
245
|
-
|
|
289
|
+
Odac.form('#my-form', function(data) {
|
|
246
290
|
console.log('Submitted!')
|
|
247
291
|
})
|
|
248
292
|
```
|
|
@@ -254,8 +298,8 @@ odac.form('#my-form', function(data) {
|
|
|
254
298
|
```javascript
|
|
255
299
|
// controller/post/contact.js
|
|
256
300
|
module.exports = async function(Odac) {
|
|
257
|
-
const email = await
|
|
258
|
-
const message = await
|
|
301
|
+
const email = await Odac.Request.request('email')
|
|
302
|
+
const message = await Odac.Request.request('message')
|
|
259
303
|
|
|
260
304
|
// Validation
|
|
261
305
|
const errors = {}
|
|
@@ -285,7 +329,7 @@ module.exports = async function(Odac) {
|
|
|
285
329
|
|
|
286
330
|
```javascript
|
|
287
331
|
// route/www.js
|
|
288
|
-
|
|
332
|
+
Odac.Route.post('/api/contact', 'contact')
|
|
289
333
|
```
|
|
290
334
|
|
|
291
335
|
## Validation
|
|
@@ -306,7 +350,7 @@ Always validate on the server:
|
|
|
306
350
|
|
|
307
351
|
```javascript
|
|
308
352
|
module.exports = async function(Odac) {
|
|
309
|
-
const email = await
|
|
353
|
+
const email = await Odac.Request.request('email')
|
|
310
354
|
|
|
311
355
|
const errors = {}
|
|
312
356
|
|
|
@@ -349,7 +393,7 @@ module.exports = async function(Odac) {
|
|
|
349
393
|
**Note:** Error and success elements are auto-generated. Add them manually only if you need custom positioning or styling.
|
|
350
394
|
|
|
351
395
|
```javascript
|
|
352
|
-
|
|
396
|
+
Odac.form('#contact-form', function(data) {
|
|
353
397
|
if (data.result.success) {
|
|
354
398
|
document.querySelector('#contact-form').reset()
|
|
355
399
|
}
|
|
@@ -367,7 +411,7 @@ odac.form('#contact-form', function(data) {
|
|
|
367
411
|
```
|
|
368
412
|
|
|
369
413
|
```javascript
|
|
370
|
-
|
|
414
|
+
Odac.form('#login-form', function(data) {
|
|
371
415
|
if (data.result.success) {
|
|
372
416
|
// Redirect to dashboard
|
|
373
417
|
window.location.href = '/dashboard'
|
|
@@ -420,6 +464,15 @@ odac.form('#login-form', function(data) {
|
|
|
420
464
|
- Verify `messages` option is not set to `false`
|
|
421
465
|
- If using custom error elements, ensure `odac-form-error` attributes match field names exactly
|
|
422
466
|
|
|
467
|
+
### Form Submits Twice
|
|
468
|
+
|
|
469
|
+
- Make sure you don't register the **same form under two different selectors**
|
|
470
|
+
(e.g. `Odac.form('#my-form')` and `Odac.form('form#my-form')`) — use one
|
|
471
|
+
consistent selector.
|
|
472
|
+
- Binding `Odac.form()` to an `<odac:form>` is safe and will **not** double up;
|
|
473
|
+
it re-binds the existing handler. If you still see two submits, check that you
|
|
474
|
+
bind **after** the form is in the DOM, not before it renders.
|
|
475
|
+
|
|
423
476
|
### CSRF Token Errors
|
|
424
477
|
|
|
425
478
|
- CSRF tokens are handled automatically
|
|
@@ -7,7 +7,7 @@ Learn how to make GET and POST requests to your API endpoints using odac.js.
|
|
|
7
7
|
### Basic GET Request
|
|
8
8
|
|
|
9
9
|
```javascript
|
|
10
|
-
|
|
10
|
+
Odac.get('/api/data', function(response) {
|
|
11
11
|
console.log('Data:', response)
|
|
12
12
|
})
|
|
13
13
|
```
|
|
@@ -16,7 +16,7 @@ odac.get('/api/data', function(response) {
|
|
|
16
16
|
|
|
17
17
|
```javascript
|
|
18
18
|
const userId = 123
|
|
19
|
-
|
|
19
|
+
Odac.get(`/api/users/${userId}`, function(user) {
|
|
20
20
|
console.log('User:', user.name)
|
|
21
21
|
console.log('Email:', user.email)
|
|
22
22
|
})
|
|
@@ -25,7 +25,7 @@ odac.get(`/api/users/${userId}`, function(user) {
|
|
|
25
25
|
### Error Handling
|
|
26
26
|
|
|
27
27
|
```javascript
|
|
28
|
-
|
|
28
|
+
Odac.get('/api/data', function(response) {
|
|
29
29
|
if (response.error) {
|
|
30
30
|
console.error('Error:', response.error)
|
|
31
31
|
return
|
|
@@ -43,7 +43,7 @@ odac.get('/api/data', function(response) {
|
|
|
43
43
|
The recommended way to make POST requests is using `Odac.form()`:
|
|
44
44
|
|
|
45
45
|
```javascript
|
|
46
|
-
|
|
46
|
+
Odac.form('#my-form', function(data) {
|
|
47
47
|
if (data.result.success) {
|
|
48
48
|
console.log('Success!')
|
|
49
49
|
}
|
|
@@ -58,12 +58,12 @@ For custom POST requests without forms, use the internal AJAX method:
|
|
|
58
58
|
|
|
59
59
|
```javascript
|
|
60
60
|
// Note: This is an advanced pattern
|
|
61
|
-
// For most cases, use
|
|
61
|
+
// For most cases, use Odac.form() instead
|
|
62
62
|
|
|
63
63
|
const formData = new FormData()
|
|
64
64
|
formData.append('name', 'John')
|
|
65
65
|
formData.append('email', 'john@example.com')
|
|
66
|
-
formData.append('_token',
|
|
66
|
+
formData.append('_token', Odac.token())
|
|
67
67
|
|
|
68
68
|
fetch('/api/submit', {
|
|
69
69
|
method: 'POST',
|
|
@@ -83,12 +83,12 @@ odac.js automatically handles CSRF tokens:
|
|
|
83
83
|
|
|
84
84
|
```javascript
|
|
85
85
|
// Token is automatically included
|
|
86
|
-
|
|
86
|
+
Odac.get('/api/data', function(response) {
|
|
87
87
|
// ...
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
// Token is automatically included in forms
|
|
91
|
-
|
|
91
|
+
Odac.form('#my-form', function(data) {
|
|
92
92
|
// ...
|
|
93
93
|
})
|
|
94
94
|
```
|
|
@@ -98,7 +98,7 @@ odac.form('#my-form', function(data) {
|
|
|
98
98
|
If you need the token manually:
|
|
99
99
|
|
|
100
100
|
```javascript
|
|
101
|
-
const token =
|
|
101
|
+
const token = Odac.token()
|
|
102
102
|
console.log('Current token:', token)
|
|
103
103
|
```
|
|
104
104
|
|
|
@@ -108,7 +108,7 @@ console.log('Current token:', token)
|
|
|
108
108
|
|
|
109
109
|
```javascript
|
|
110
110
|
// Get user profile
|
|
111
|
-
|
|
111
|
+
Odac.get('/api/profile', function(profile) {
|
|
112
112
|
document.querySelector('#username').textContent = profile.name
|
|
113
113
|
document.querySelector('#email').textContent = profile.email
|
|
114
114
|
})
|
|
@@ -118,7 +118,7 @@ odac.get('/api/profile', function(profile) {
|
|
|
118
118
|
|
|
119
119
|
```javascript
|
|
120
120
|
// Get products
|
|
121
|
-
|
|
121
|
+
Odac.get('/api/products', function(products) {
|
|
122
122
|
const container = document.querySelector('#products')
|
|
123
123
|
|
|
124
124
|
products.forEach(product => {
|
|
@@ -143,7 +143,7 @@ searchInput.addEventListener('input', function() {
|
|
|
143
143
|
|
|
144
144
|
if (query.length < 3) return
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
Odac.get(`/api/search?q=${encodeURIComponent(query)}`, function(results) {
|
|
147
147
|
displayResults(results)
|
|
148
148
|
})
|
|
149
149
|
})
|
|
@@ -172,7 +172,7 @@ document.querySelector('#autocomplete').addEventListener('input', function() {
|
|
|
172
172
|
debounceTimer = setTimeout(() => {
|
|
173
173
|
if (value.length < 2) return
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
Odac.get(`/api/autocomplete?q=${value}`, function(suggestions) {
|
|
176
176
|
showSuggestions(suggestions)
|
|
177
177
|
})
|
|
178
178
|
}, 300)
|
|
@@ -200,7 +200,7 @@ function loadMore() {
|
|
|
200
200
|
loading = true
|
|
201
201
|
page++
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
Odac.get(`/api/posts?page=${page}`, function(posts) {
|
|
204
204
|
posts.forEach(post => {
|
|
205
205
|
appendPost(post)
|
|
206
206
|
})
|
|
@@ -214,7 +214,7 @@ function loadMore() {
|
|
|
214
214
|
```javascript
|
|
215
215
|
// Poll for updates every 30 seconds
|
|
216
216
|
setInterval(function() {
|
|
217
|
-
|
|
217
|
+
Odac.get('/api/notifications', function(notifications) {
|
|
218
218
|
updateNotificationBadge(notifications.count)
|
|
219
219
|
})
|
|
220
220
|
}, 30000)
|
|
@@ -239,7 +239,7 @@ module.exports = function(Odac) {
|
|
|
239
239
|
|
|
240
240
|
```javascript
|
|
241
241
|
// route/www.js
|
|
242
|
-
|
|
242
|
+
Odac.Route.get('/api/users', 'users')
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
### GET with Parameters
|
|
@@ -247,13 +247,13 @@ odac.Route.get('/api/users', 'users')
|
|
|
247
247
|
```javascript
|
|
248
248
|
// controller/get/user.js
|
|
249
249
|
module.exports = async function(Odac) {
|
|
250
|
-
const userId =
|
|
250
|
+
const userId = Odac.Request.data.url.id
|
|
251
251
|
|
|
252
252
|
// Fetch user from database
|
|
253
253
|
const user = await getUserById(userId)
|
|
254
254
|
|
|
255
255
|
if (!user) {
|
|
256
|
-
|
|
256
|
+
Odac.Request.status(404)
|
|
257
257
|
return {error: 'User not found'}
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -263,7 +263,7 @@ module.exports = async function(Odac) {
|
|
|
263
263
|
|
|
264
264
|
```javascript
|
|
265
265
|
// route/www.js
|
|
266
|
-
|
|
266
|
+
Odac.Route.get('/api/users/{id}', 'user')
|
|
267
267
|
```
|
|
268
268
|
|
|
269
269
|
### POST Endpoint
|
|
@@ -271,8 +271,8 @@ odac.Route.get('/api/users/{id}', 'user')
|
|
|
271
271
|
```javascript
|
|
272
272
|
// controller/post/create.js
|
|
273
273
|
module.exports = async function(Odac) {
|
|
274
|
-
const name = await
|
|
275
|
-
const email = await
|
|
274
|
+
const name = await Odac.Request.request('name')
|
|
275
|
+
const email = await Odac.Request.request('email')
|
|
276
276
|
|
|
277
277
|
// Validation
|
|
278
278
|
if (!name || !email) {
|
|
@@ -300,7 +300,7 @@ module.exports = async function(Odac) {
|
|
|
300
300
|
|
|
301
301
|
```javascript
|
|
302
302
|
// route/www.js
|
|
303
|
-
|
|
303
|
+
Odac.Route.post('/api/users/create', 'create')
|
|
304
304
|
```
|
|
305
305
|
|
|
306
306
|
## Response Format
|
|
@@ -357,7 +357,7 @@ function getCached(url, callback) {
|
|
|
357
357
|
return
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
Odac.get(url, function(data) {
|
|
361
361
|
cache[url] = data
|
|
362
362
|
callback(data)
|
|
363
363
|
})
|
|
@@ -386,7 +386,7 @@ function processQueue() {
|
|
|
386
386
|
processing = true
|
|
387
387
|
const {url, callback} = requestQueue.shift()
|
|
388
388
|
|
|
389
|
-
|
|
389
|
+
Odac.get(url, function(data) {
|
|
390
390
|
callback(data)
|
|
391
391
|
processing = false
|
|
392
392
|
processQueue()
|
|
@@ -403,7 +403,7 @@ function getWithRetry(url, callback, maxRetries = 3) {
|
|
|
403
403
|
function attempt() {
|
|
404
404
|
attempts++
|
|
405
405
|
|
|
406
|
-
|
|
406
|
+
Odac.get(url, function(data) {
|
|
407
407
|
if (data.error && attempts < maxRetries) {
|
|
408
408
|
setTimeout(attempt, 1000 * attempts)
|
|
409
409
|
} else {
|
|
@@ -6,7 +6,7 @@ Odac provides a simple client-side API for consuming Server-Sent Events (SSE) st
|
|
|
6
6
|
|
|
7
7
|
```javascript
|
|
8
8
|
// Simple callback
|
|
9
|
-
|
|
9
|
+
Odac.listen('/events', (data) => {
|
|
10
10
|
console.log('Received:', data)
|
|
11
11
|
})
|
|
12
12
|
```
|
|
@@ -14,7 +14,7 @@ odac.listen('/events', (data) => {
|
|
|
14
14
|
### With Options
|
|
15
15
|
|
|
16
16
|
```javascript
|
|
17
|
-
const stream =
|
|
17
|
+
const stream = Odac.listen('/events',
|
|
18
18
|
(data) => {
|
|
19
19
|
console.log('Message:', data)
|
|
20
20
|
},
|
|
@@ -34,7 +34,7 @@ stream.close()
|
|
|
34
34
|
|
|
35
35
|
```javascript
|
|
36
36
|
// Server sends: data: {"message": "Hello"}\n\n
|
|
37
|
-
|
|
37
|
+
Odac.listen('/events', (data) => {
|
|
38
38
|
console.log(data.message) // "Hello"
|
|
39
39
|
})
|
|
40
40
|
```
|
|
@@ -47,8 +47,8 @@ odac.listen('/events', (data) => {
|
|
|
47
47
|
|
|
48
48
|
```javascript
|
|
49
49
|
// Server
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
Odac.Route.get('/dashboard/stats', async (Odac) => {
|
|
51
|
+
Odac.stream((send) => {
|
|
52
52
|
const interval = setInterval(async () => {
|
|
53
53
|
const stats = await getServerStats()
|
|
54
54
|
send(stats)
|
|
@@ -62,7 +62,7 @@ odac.Route.get('/dashboard/stats', async (Odac) => {
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
// Client
|
|
65
|
-
|
|
65
|
+
Odac.listen('/dashboard/stats', (stats) => {
|
|
66
66
|
document.getElementById('cpu').textContent = stats.cpu + '%'
|
|
67
67
|
document.getElementById('memory').textContent = stats.memory + 'MB'
|
|
68
68
|
})
|
|
@@ -72,16 +72,16 @@ odac.listen('/dashboard/stats', (stats) => {
|
|
|
72
72
|
|
|
73
73
|
```javascript
|
|
74
74
|
// Server
|
|
75
|
-
|
|
76
|
-
const userId = await
|
|
75
|
+
Odac.Route.get('/notifications', async (Odac) => {
|
|
76
|
+
const userId = await Odac.request('userId')
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Odac.stream((send) => {
|
|
79
79
|
global.notificationStreams[userId] = { send }
|
|
80
80
|
})
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
// Client
|
|
84
|
-
|
|
84
|
+
Odac.listen('/notifications', (notification) => {
|
|
85
85
|
showToast(notification.message)
|
|
86
86
|
})
|
|
87
87
|
```
|
|
@@ -90,8 +90,8 @@ odac.listen('/notifications', (notification) => {
|
|
|
90
90
|
|
|
91
91
|
```javascript
|
|
92
92
|
// Server
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
Odac.Route.get('/build/logs', async (Odac) => {
|
|
94
|
+
Odac.stream(async function* () {
|
|
95
95
|
for await (const log of getBuildLogs()) {
|
|
96
96
|
yield { timestamp: Date.now(), message: log }
|
|
97
97
|
}
|
|
@@ -100,7 +100,7 @@ odac.Route.get('/build/logs', async (Odac) => {
|
|
|
100
100
|
|
|
101
101
|
// Client
|
|
102
102
|
const logs = []
|
|
103
|
-
|
|
103
|
+
Odac.listen('/build/logs', (log) => {
|
|
104
104
|
logs.push(log)
|
|
105
105
|
updateLogsUI(logs)
|
|
106
106
|
})
|
|
@@ -108,7 +108,7 @@ odac.listen('/build/logs', (log) => {
|
|
|
108
108
|
|
|
109
109
|
## API Reference
|
|
110
110
|
|
|
111
|
-
###
|
|
111
|
+
### Odac.listen(url, onMessage, options)
|
|
112
112
|
|
|
113
113
|
**Parameters:**
|
|
114
114
|
- `url` (string): Stream endpoint URL
|
|
@@ -6,7 +6,7 @@ Odac provides built-in WebSocket support with automatic reconnection and cross-t
|
|
|
6
6
|
|
|
7
7
|
**Backend (route/main.js):**
|
|
8
8
|
```javascript
|
|
9
|
-
|
|
9
|
+
Odac.Route.ws('/chat', (ws, Odac) => {
|
|
10
10
|
ws.on('message', data => {
|
|
11
11
|
ws.broadcast(data)
|
|
12
12
|
})
|
package/index.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
global.__dir = process.cwd()
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// Surface startup failures loudly. init() is async; without a .catch() any
|
|
6
|
+
// rejection (migration, DB connection, config parse) becomes an UnhandledPromiseRejection
|
|
7
|
+
// that the structured logger never sees — making a broken boot look like a clean start.
|
|
8
|
+
require('./src/Odac.js')
|
|
9
|
+
.init()
|
|
10
|
+
.catch(err => {
|
|
11
|
+
console.error('\x1b[31m[ODAC Fatal]\x1b[0m Startup failed:', err.message)
|
|
12
|
+
if (err.cause) console.error('\x1b[31m[ODAC Fatal]\x1b[0m Cause:', err.cause.message || err.cause)
|
|
13
|
+
process.exit(1)
|
|
14
|
+
})
|