odac 1.4.15 → 1.4.17
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 +29 -0
- package/client/odac.js +38 -1
- package/docs/ai/skills/backend/forms.md +54 -3
- package/docs/ai/skills/backend/validation.md +30 -2
- package/docs/backend/05-forms/01-custom-forms.md +52 -0
- package/docs/backend/09-validation/01-the-validator-service.md +40 -0
- package/docs/backend/13-utilities/02-ipc.md +26 -1
- package/docs/frontend/03-forms/01-form-handling.md +30 -13
- package/package.json +2 -1
- package/src/Auth.js +252 -24
- package/src/Config.js +5 -1
- package/src/Ipc.js +87 -14
- package/src/Odac.js +23 -4
- 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/src/WebSocket.js +20 -1
- package/test/Auth/check.test.js +219 -18
- package/test/Auth/verifyMagicLink.test.js +8 -1
- package/test/Ipc/subscribe.test.js +154 -0
- package/test/Ipc/subscribeRedis.test.js +141 -0
- 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
- package/test/WebSocket/Client/fragmentation.test.js +24 -5
- package/test/WebSocket/Client/limits.test.js +21 -2
- package/test/WebSocket/Client/readyState.test.js +29 -10
- package/test/WebSocket/Client/send.test.js +148 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
### 🛠️ Fixes & Improvements
|
|
2
2
|
|
|
3
|
+
- repair broken redis pub/sub delivery and align unsubscribe() across drivers
|
|
4
|
+
- send() no longer JSON-serializes Buffer/TypedArray into TEXT frames
|
|
5
|
+
- **test:** close WebSocketClient instances left open after each test
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
12
|
+
|
|
13
|
+
### ✨ What's New
|
|
14
|
+
|
|
15
|
+
- add multipart file upload support with validation
|
|
16
|
+
|
|
17
|
+
### 🛠️ Fixes & Improvements
|
|
18
|
+
|
|
19
|
+
- **auth:** enhance token validation and anomaly detection mechanisms
|
|
20
|
+
- patch file descriptor leak, polling anti-pattern, mimetype guard and duplicate move check
|
|
21
|
+
- **test:** improve temp file cleanup logic in multipart tests for reliability
|
|
22
|
+
- update cache directory handling to use global.__dir for consistency
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
29
|
+
|
|
30
|
+
### 🛠️ Fixes & Improvements
|
|
31
|
+
|
|
3
32
|
- **docs:** update references from 'odac' to 'Odac' for consistency
|
|
4
33
|
- **form:** improve form submission handler ownership to prevent duplicate submissions
|
|
5
34
|
- **form:** prevent multiple submit handlers on re-bound forms
|
package/client/odac.js
CHANGED
|
@@ -479,6 +479,43 @@ if (typeof window !== 'undefined') {
|
|
|
479
479
|
errorSpan.textContent = ''
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
if (input.type === 'file') {
|
|
483
|
+
const files = input.files
|
|
484
|
+
if (input.hasAttribute('required') && files.length === 0) {
|
|
485
|
+
showError(input, 'required')
|
|
486
|
+
break
|
|
487
|
+
}
|
|
488
|
+
const maxFiles = parseInt(input.getAttribute('data-maxfiles'))
|
|
489
|
+
if (maxFiles && files.length > maxFiles) {
|
|
490
|
+
showError(input, 'maxfiles')
|
|
491
|
+
break
|
|
492
|
+
}
|
|
493
|
+
const maxSize = parseInt(input.getAttribute('data-maxsize'))
|
|
494
|
+
if (maxSize && [...files].some(f => f.size > maxSize)) {
|
|
495
|
+
showError(input, 'maxsize')
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
const minSize = parseInt(input.getAttribute('data-minsize'))
|
|
499
|
+
if (minSize && [...files].some(f => f.size < minSize)) {
|
|
500
|
+
showError(input, 'minsize')
|
|
501
|
+
break
|
|
502
|
+
}
|
|
503
|
+
const accept = input.getAttribute('accept')
|
|
504
|
+
if (accept && files.length) {
|
|
505
|
+
const tokens = accept.split(',').map(t => t.trim().toLowerCase())
|
|
506
|
+
const ok = [...files].every(f =>
|
|
507
|
+
tokens.some(t =>
|
|
508
|
+
t.startsWith('.') ? f.name.toLowerCase().endsWith(t) : t.endsWith('/*') ? f.type.startsWith(t.slice(0, -1)) : f.type === t
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
if (!ok) {
|
|
512
|
+
showError(input, 'accept')
|
|
513
|
+
break
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
continue
|
|
517
|
+
}
|
|
518
|
+
|
|
482
519
|
if (input.hasAttribute('required')) {
|
|
483
520
|
const isEmpty = input.type === 'checkbox' || input.type === 'radio' ? !input.checked : !input.value.trim()
|
|
484
521
|
if (isEmpty) {
|
|
@@ -523,7 +560,7 @@ if (typeof window !== 'undefined') {
|
|
|
523
560
|
let datastring, cache, contentType, processData
|
|
524
561
|
if (formElement.querySelector('input[type=file]')) {
|
|
525
562
|
datastring = new FormData(formElement)
|
|
526
|
-
datastring.append('
|
|
563
|
+
datastring.append('_token', this.token())
|
|
527
564
|
cache = false
|
|
528
565
|
contentType = false
|
|
529
566
|
processData = false
|
|
@@ -101,12 +101,63 @@ module.exports = class Contact {
|
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
## Field-Level Variants
|
|
104
|
-
- **Input types**: `text`, `email`, `password`, `number`, `url`, `textarea`, `checkbox`.
|
|
105
|
-
- **Validation mapping**: `required|minlen|maxlen|min|max|alpha|alphanumeric|numeric|email|url|accepted`.
|
|
106
|
-
- **Pass-through attrs**: Unrecognized `<odac:input ...>` attributes are preserved into generated HTML input/textarea.
|
|
104
|
+
- **Input types**: `text`, `email`, `password`, `number`, `url`, `textarea`, `checkbox`, `file`.
|
|
105
|
+
- **Validation mapping**: `required|minlen|maxlen|min|max|alpha|alphanumeric|numeric|email|url|accepted|maxsize|minsize|mimetype|ext|maxfiles`.
|
|
106
|
+
- **Pass-through attrs**: Unrecognized `<odac:input ...>` attributes are preserved into generated HTML input/textarea/input[type=file].
|
|
107
107
|
- **Skip persistence**: Use `skip` on `<odac:input>` to validate a field but exclude it from final payload.
|
|
108
108
|
- **Unique shorthand**: `unique` attribute on `<odac:input>` enables auth-register uniqueness list.
|
|
109
109
|
|
|
110
|
+
## File Upload Fields
|
|
111
|
+
|
|
112
|
+
File inputs work end-to-end with zero-config validation, like all other input types:
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<odac:form action="Profile.saveAvatar" enctype="multipart/form-data">
|
|
116
|
+
<odac:input type="file" name="avatar" label="Avatar">
|
|
117
|
+
<odac:validate rule="required|maxsize:2MB|mimetype:image/png,image/jpeg" message="PNG/JPEG, max 2MB"/>
|
|
118
|
+
</odac:input>
|
|
119
|
+
<odac:submit text="Save"/>
|
|
120
|
+
</odac:form>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### File Rules
|
|
124
|
+
- **`required`**: file must be present (at least one file for `multiple`).
|
|
125
|
+
- **`maxsize:2MB`** / **`minsize:10KB`**: size constraints (supports B, KB, MB, GB suffixes); enforced server-side via busboy limits + client-side byte checks.
|
|
126
|
+
- **`mimetype:image/png,image/jpeg`** (alias **`accept:`**): comma-separated MIME list; wildcards like `image/*` supported. Validated via claimed MIME, extension map, and (for images) magic-byte sniffing.
|
|
127
|
+
- **`ext:jpg,png`**: file extension whitelist (case-insensitive).
|
|
128
|
+
- **`maxfiles:3`**: max file count for `multiple` inputs.
|
|
129
|
+
|
|
130
|
+
### Controller Access
|
|
131
|
+
In action forms, access uploaded files via `form.file(name)`:
|
|
132
|
+
```javascript
|
|
133
|
+
async saveAvatar(form) {
|
|
134
|
+
const avatar = await form.file('avatar')
|
|
135
|
+
if (!avatar) return form.error('avatar', 'No file uploaded')
|
|
136
|
+
|
|
137
|
+
const dest = await avatar.move(`${__dir}/storage/avatars/${user.id}.${avatar.ext}`)
|
|
138
|
+
user.avatar = dest
|
|
139
|
+
return form.success('Avatar saved')
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Table Forms
|
|
144
|
+
For `table="..."` forms, file fields are auto-stored relative to `uploadDir` (configured in `odac.json` or `$ODAC_UPLOAD_DIR`) and the path string is persisted in the column.
|
|
145
|
+
|
|
146
|
+
### File Object Shape
|
|
147
|
+
```javascript
|
|
148
|
+
{
|
|
149
|
+
field: 'avatar',
|
|
150
|
+
name: 'photo.jpg', // original filename
|
|
151
|
+
ext: 'jpg', // extension (lowercase)
|
|
152
|
+
mimetype: 'image/jpeg', // client-claimed
|
|
153
|
+
size: 123456, // bytes
|
|
154
|
+
path: '/tmp/odac-...', // temp path (null if truncated)
|
|
155
|
+
truncated: false, // true if size > maxFileSize
|
|
156
|
+
stored: false, // true after move()
|
|
157
|
+
async move(dest) {...} // move to permanent location
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
110
161
|
## Patterns
|
|
111
162
|
```javascript
|
|
112
163
|
// Custom form action in class/Contact.js
|
|
@@ -37,7 +37,7 @@ module.exports = async Odac => {
|
|
|
37
37
|
- `post(key)`: Validate POST payload field.
|
|
38
38
|
- `get(key)`: Validate querystring field.
|
|
39
39
|
- `var(name, value)`: Validate computed/custom value.
|
|
40
|
-
- `file(name)`: Validate uploaded file object.
|
|
40
|
+
- `file(name)`: Validate uploaded file(s) object.
|
|
41
41
|
- `check(rules | boolean)`: Apply pipe rules or direct boolean validation.
|
|
42
42
|
- `message(text)`: Set message for the latest check on current field.
|
|
43
43
|
- `error()`: Runs validation and returns `true` if any error exists.
|
|
@@ -65,6 +65,14 @@ module.exports = async Odac => {
|
|
|
65
65
|
- `regex:pattern`
|
|
66
66
|
- `mindate:YYYY-MM-DD`, `maxdate:YYYY-MM-DD`
|
|
67
67
|
|
|
68
|
+
### File upload rules (for `file()` fields)
|
|
69
|
+
- `required`: file must be present.
|
|
70
|
+
- `maxsize:2MB` / `minsize:10KB`: size limits (B, KB, MB, GB suffixes).
|
|
71
|
+
- `mimetype:image/png,image/jpeg`: MIME type whitelist; wildcards like `image/*` supported. Validated via claimed MIME, extension map, and (for images) magic-byte sniffing to detect spoofing.
|
|
72
|
+
- `accept:...`: alias for `mimetype:`.
|
|
73
|
+
- `ext:jpg,png`: file extension whitelist (case-insensitive, comma-separated).
|
|
74
|
+
- `maxfiles:N`: max file count for `multiple` file inputs.
|
|
75
|
+
|
|
68
76
|
### Auth/security rules
|
|
69
77
|
- `usercheck`: Must be authenticated.
|
|
70
78
|
- `user:field`: Input must match authenticated user field.
|
|
@@ -125,7 +133,27 @@ module.exports = async Odac => {
|
|
|
125
133
|
}
|
|
126
134
|
```
|
|
127
135
|
|
|
128
|
-
### 4)
|
|
136
|
+
### 4) File upload validation
|
|
137
|
+
```javascript
|
|
138
|
+
module.exports = async Odac => {
|
|
139
|
+
const validator = Odac.validator()
|
|
140
|
+
|
|
141
|
+
validator.file('avatar').check('required|maxsize:2MB|mimetype:image/png,image/jpeg')
|
|
142
|
+
.message('Avatar required (PNG/JPEG, max 2MB)')
|
|
143
|
+
validator.file('documents').check('maxfiles:5|ext:pdf,docx')
|
|
144
|
+
.message('Max 5 PDFs/Word docs allowed')
|
|
145
|
+
|
|
146
|
+
if (await validator.error()) return await validator.result('Upload validation failed')
|
|
147
|
+
|
|
148
|
+
// Access files in controller
|
|
149
|
+
const avatar = await Odac.file('avatar')
|
|
150
|
+
await avatar.move(`${__dir}/storage/avatars/${user.id}.${avatar.ext}`)
|
|
151
|
+
|
|
152
|
+
return await validator.success('Files uploaded')
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 5) Brute-force on auth endpoint
|
|
129
157
|
```javascript
|
|
130
158
|
module.exports = async Odac => {
|
|
131
159
|
const validator = Odac.validator()
|
|
@@ -105,6 +105,16 @@ Supports all standard HTML input types:
|
|
|
105
105
|
<odac:validate rule="minlen:2" message="Name must be at least 2 characters"/>
|
|
106
106
|
<odac:validate rule="maxlen:50" message="Name is too long"/>
|
|
107
107
|
</odac:input>
|
|
108
|
+
|
|
109
|
+
<!-- File upload -->
|
|
110
|
+
<odac:input name="avatar" type="file" label="Profile Picture">
|
|
111
|
+
<odac:validate rule="required|maxsize:2MB|mimetype:image/png,image/jpeg" message="PNG or JPEG, max 2MB"/>
|
|
112
|
+
</odac:input>
|
|
113
|
+
|
|
114
|
+
<!-- Multiple file upload -->
|
|
115
|
+
<odac:input name="documents" type="file" label="Documents" multiple>
|
|
116
|
+
<odac:validate rule="maxfiles:5|ext:pdf,docx" message="Max 5 PDFs or Word docs"/>
|
|
117
|
+
</odac:input>
|
|
108
118
|
```
|
|
109
119
|
|
|
110
120
|
### Field Attributes
|
|
@@ -130,6 +140,7 @@ Add validation rules to fields:
|
|
|
130
140
|
|
|
131
141
|
### Available Rules
|
|
132
142
|
|
|
143
|
+
#### Text/Input Rules
|
|
133
144
|
- `required` - Field is required
|
|
134
145
|
- `email` - Must be valid email
|
|
135
146
|
- `url` - Must be valid URL
|
|
@@ -142,6 +153,15 @@ Add validation rules to fields:
|
|
|
142
153
|
- `alphanumeric` - Letters and numbers only
|
|
143
154
|
- `accepted` - Checkbox must be checked
|
|
144
155
|
|
|
156
|
+
#### File Upload Rules
|
|
157
|
+
- `required` - File must be provided
|
|
158
|
+
- `maxsize:2MB` - Maximum file size (supports B, KB, MB, GB)
|
|
159
|
+
- `minsize:10KB` - Minimum file size
|
|
160
|
+
- `mimetype:image/png,image/jpeg` - Allowed MIME types (comma-separated; wildcards like `image/*` supported)
|
|
161
|
+
- `accept:...` - Alias for `mimetype:`
|
|
162
|
+
- `ext:jpg,png` - Allowed file extensions (case-insensitive)
|
|
163
|
+
- `maxfiles:5` - Maximum number of files for `multiple` uploads
|
|
164
|
+
|
|
145
165
|
### Multiple Rules
|
|
146
166
|
|
|
147
167
|
Combine rules with `|`:
|
|
@@ -306,6 +326,38 @@ module.exports = class Contact {
|
|
|
306
326
|
|
|
307
327
|
**Note:** No route definition is needed for the action! The form system handles the routing securely.
|
|
308
328
|
|
|
329
|
+
## File Upload Handling
|
|
330
|
+
|
|
331
|
+
Access uploaded files in your controller via `form.file(name)`:
|
|
332
|
+
|
|
333
|
+
```javascript
|
|
334
|
+
module.exports = class Profile {
|
|
335
|
+
constructor(Odac) {
|
|
336
|
+
this.Odac = Odac
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async updateAvatar(form) {
|
|
340
|
+
const avatar = await form.file('avatar')
|
|
341
|
+
if (!avatar) {
|
|
342
|
+
return form.error('avatar', 'No file uploaded')
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Store the file permanently
|
|
346
|
+
const userId = this.Odac.Auth.user('id')
|
|
347
|
+
const dest = await avatar.move(`${__dir}/../storage/avatars/${userId}.${avatar.ext}`)
|
|
348
|
+
|
|
349
|
+
// Update user record with file path
|
|
350
|
+
// await this.Odac.DB.users.where('id', userId).update({avatar: dest})
|
|
351
|
+
|
|
352
|
+
return form.success('Avatar updated!', '/profile')
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
For `table="..."` forms with file fields, files are automatically stored in the configured `uploadDir` and the file path is saved to the database column.
|
|
358
|
+
|
|
359
|
+
> **Security:** `move(dest)` writes to whatever path you pass, just like `fs` — the file object's own `.name` is already stripped to a bare filename, but if you build `dest` from raw user input, sanitize it yourself to avoid path traversal. Server-generated names (as in the example above) are always safe. Uploads are validated for size/type before your handler runs, but content sniffing only covers raster images (JPEG/PNG/GIF/WebP); other types are trusted by extension + declared MIME, and `image/*` also permits SVG (an XSS vector) — list explicit types like `mimetype:image/png,image/jpeg` when you need to exclude it.
|
|
360
|
+
|
|
309
361
|
## Automatic Database Insert
|
|
310
362
|
|
|
311
363
|
Forms can automatically insert data into database without writing a controller:
|
|
@@ -72,6 +72,15 @@ if (await validator.error()) {
|
|
|
72
72
|
- `regex:pattern` - Must match regex pattern
|
|
73
73
|
- `!disposable` - Block disposable/temporary email providers (List is automatically updated daily)
|
|
74
74
|
|
|
75
|
+
**File Upload Validation:**
|
|
76
|
+
- `required` - File must be uploaded
|
|
77
|
+
- `maxsize:2MB` - Maximum file size (B, KB, MB, GB)
|
|
78
|
+
- `minsize:10KB` - Minimum file size
|
|
79
|
+
- `mimetype:image/png,image/jpeg` - Allowed MIME types (comma-separated; wildcards like `image/*` supported)
|
|
80
|
+
- `accept:...` - Alias for `mimetype:`
|
|
81
|
+
- `ext:jpg,png` - Allowed file extensions (case-insensitive)
|
|
82
|
+
- `maxfiles:5` - Max files for multi-upload
|
|
83
|
+
|
|
75
84
|
**Security:**
|
|
76
85
|
- `xss` - Check for HTML tags (XSS protection)
|
|
77
86
|
- `usercheck` - User must be authenticated
|
|
@@ -358,6 +367,37 @@ module.exports = async function (Odac) {
|
|
|
358
367
|
}
|
|
359
368
|
```
|
|
360
369
|
|
|
370
|
+
#### Example: File Upload Validation
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
module.exports = async function (Odac) {
|
|
374
|
+
const validator = Odac.Validator
|
|
375
|
+
|
|
376
|
+
validator
|
|
377
|
+
.file('avatar')
|
|
378
|
+
.check('required').message('Avatar is required')
|
|
379
|
+
.check('maxsize:2MB').message('Avatar must not exceed 2MB')
|
|
380
|
+
.check('mimetype:image/png,image/jpeg').message('Avatar must be PNG or JPEG')
|
|
381
|
+
|
|
382
|
+
validator
|
|
383
|
+
.file('documents')
|
|
384
|
+
.check('maxfiles:5').message('Maximum 5 documents allowed')
|
|
385
|
+
.check('ext:pdf,docx').message('Only PDF and Word documents allowed')
|
|
386
|
+
|
|
387
|
+
if (await validator.error()) {
|
|
388
|
+
return validator.result('Upload validation failed')
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Access and store the files
|
|
392
|
+
const avatar = await Odac.file('avatar')
|
|
393
|
+
const userId = Odac.Auth.user('id')
|
|
394
|
+
|
|
395
|
+
await avatar.move(`${__dir}/../storage/avatars/${userId}.${avatar.ext}`)
|
|
396
|
+
|
|
397
|
+
return validator.success('Files uploaded successfully')
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
361
401
|
#### Frontend Integration
|
|
362
402
|
|
|
363
403
|
When using `Odac.form()` on the frontend, validation errors are automatically displayed:
|
|
@@ -72,6 +72,29 @@ await Odac.Ipc.publish('chat:global', { user: 'Emre', text: 'Hello World' });
|
|
|
72
72
|
> [!TIP]
|
|
73
73
|
> When using `memory` driver, the subscription listener is registered in the current worker. When a message is published, it goes to the Main process and is then broadcasted to all subscribed workers.
|
|
74
74
|
|
|
75
|
+
#### Unsubscribing
|
|
76
|
+
|
|
77
|
+
A channel can carry several independent subscribers, so a subscription is identified by its callback — not by the channel name alone. `subscribe()` returns a handle that removes exactly the subscription it created, which saves you from holding on to the callback yourself.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
const stats = await Odac.Ipc.subscribe('server:1:stream', onStats); // long-lived
|
|
81
|
+
const ack = await Odac.Ipc.subscribe('server:1:stream', onAck); // short-lived
|
|
82
|
+
|
|
83
|
+
await ack.unsubscribe(); // only onAck stops; onStats keeps receiving
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`unsubscribe(channel, callback)` does the same thing when you already hold the callback. To drop every subscriber on a channel at once, ask for it explicitly:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
await Odac.Ipc.unsubscribeAll('server:1:stream');
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
> [!WARNING]
|
|
93
|
+
> `unsubscribe(channel)` without a callback cannot tell which subscriber is leaving, so it does nothing and logs a warning. It will throw in the next major version — pass the callback or the handle, or use `unsubscribeAll()`.
|
|
94
|
+
|
|
95
|
+
> [!TIP]
|
|
96
|
+
> Subscriptions created through the request-scoped `Odac.Ipc` are released automatically when the request ends, so you only need to unsubscribe by hand for subscriptions that should end earlier than the request.
|
|
97
|
+
|
|
75
98
|
### Atomic Counters
|
|
76
99
|
|
|
77
100
|
Use `incrBy` / `decrBy` to atomically increment or decrement a numeric key. These are safe to call from multiple workers simultaneously — no read-then-write race conditions.
|
|
@@ -186,5 +209,7 @@ try {
|
|
|
186
209
|
| `srem(key, ...members)` | Remove members from a set |
|
|
187
210
|
| `lock(key, ttl)` | Acquire a mutex lock |
|
|
188
211
|
| `unlock(key)` | Release a mutex lock |
|
|
189
|
-
| `subscribe(channel, handler)` | Subscribe to a Pub/Sub channel |
|
|
212
|
+
| `subscribe(channel, handler)` | Subscribe to a Pub/Sub channel; returns a handle with `unsubscribe()` |
|
|
213
|
+
| `unsubscribe(channel, handler)` | Remove a single subscription, leaving other subscribers intact |
|
|
214
|
+
| `unsubscribeAll(channel)` | Remove every subscription on a channel |
|
|
190
215
|
| `publish(channel, message)` | Publish a message to a channel |
|
|
@@ -174,43 +174,60 @@ Odac.form('#my-form', function(data) {
|
|
|
174
174
|
|
|
175
175
|
## File Uploads
|
|
176
176
|
|
|
177
|
-
### 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:
|
|
178
180
|
|
|
179
181
|
```html
|
|
180
|
-
<form
|
|
181
|
-
<input
|
|
182
|
-
|
|
183
|
-
</
|
|
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>
|
|
184
188
|
```
|
|
185
189
|
|
|
186
190
|
```javascript
|
|
187
|
-
Odac.form('#
|
|
191
|
+
Odac.form('#avatar-form', function(data) {
|
|
188
192
|
if (data.result.success) {
|
|
189
|
-
|
|
193
|
+
alert('Avatar uploaded!')
|
|
190
194
|
}
|
|
191
195
|
})
|
|
192
196
|
```
|
|
193
197
|
|
|
194
|
-
|
|
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
|
|
195
205
|
|
|
196
206
|
```javascript
|
|
197
207
|
Odac.form({
|
|
198
|
-
form: '#
|
|
208
|
+
form: '#avatar-form',
|
|
199
209
|
loading: function(percent) {
|
|
200
210
|
document.querySelector('#progress').style.width = percent + '%'
|
|
201
|
-
document.querySelector('#progress-text').textContent = percent + '%'
|
|
202
211
|
}
|
|
203
212
|
}, function(data) {
|
|
204
|
-
|
|
213
|
+
if (data.result.success) {
|
|
214
|
+
console.log('Upload complete!')
|
|
215
|
+
}
|
|
205
216
|
})
|
|
206
217
|
```
|
|
207
218
|
|
|
208
|
-
### Multiple
|
|
219
|
+
### Multiple File Upload
|
|
220
|
+
|
|
221
|
+
For multiple file selection, add the `multiple` attribute and use `maxfiles` validation:
|
|
209
222
|
|
|
210
223
|
```html
|
|
211
|
-
<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>
|
|
212
227
|
```
|
|
213
228
|
|
|
229
|
+
**Note:** File inputs are never repopulated on validation error (for security). Users must re-select files after an error.
|
|
230
|
+
|
|
214
231
|
## Advanced Features
|
|
215
232
|
|
|
216
233
|
### Disable Messages
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"email": "mail@emre.red",
|
|
8
8
|
"url": "https://emre.red"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.4.
|
|
10
|
+
"version": "1.4.17",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=18.0.0"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@tailwindcss/cli": "^4.1.18",
|
|
25
|
+
"busboy": "^1.6.0",
|
|
25
26
|
"esbuild": "^0.25.12",
|
|
26
27
|
"knex": "^3.1.0",
|
|
27
28
|
"lmdb": "^3.4.4",
|