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.
Files changed (41) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/bin/odac.js +1 -0
  3. package/client/odac.js +57 -1
  4. package/docs/ai/skills/backend/forms.md +54 -3
  5. package/docs/ai/skills/backend/validation.md +30 -2
  6. package/docs/ai/skills/frontend/core.md +5 -5
  7. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +1 -1
  8. package/docs/backend/05-forms/01-custom-forms.md +96 -0
  9. package/docs/backend/09-validation/01-the-validator-service.md +40 -0
  10. package/docs/frontend/01-overview/01-introduction.md +8 -8
  11. package/docs/frontend/02-ajax-navigation/01-quick-start.md +14 -14
  12. package/docs/frontend/02-ajax-navigation/02-configuration.md +4 -4
  13. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +16 -16
  14. package/docs/frontend/03-forms/01-form-handling.md +84 -31
  15. package/docs/frontend/04-api-requests/01-get-post.md +25 -25
  16. package/docs/frontend/05-streaming/01-client-streaming.md +14 -14
  17. package/docs/frontend/06-websocket/00-overview.md +1 -1
  18. package/index.js +11 -1
  19. package/package.json +2 -1
  20. package/src/Auth.js +252 -24
  21. package/src/Config.js +5 -1
  22. package/src/Odac.js +3 -0
  23. package/src/Request.js +235 -34
  24. package/src/Route/Internal.js +32 -3
  25. package/src/Validator.js +143 -2
  26. package/src/View/Form.js +56 -0
  27. package/src/View/Image.js +15 -6
  28. package/src/View.js +2 -2
  29. package/test/Auth/check.test.js +219 -18
  30. package/test/Auth/verifyMagicLink.test.js +8 -1
  31. package/test/Odac/cache.test.js +5 -2
  32. package/test/Odac/image.test.js +8 -5
  33. package/test/Odac/instance.test.js +5 -2
  34. package/test/Request/cache.test.js +1 -0
  35. package/test/Request/multipart.test.js +164 -0
  36. package/test/Validator/file.test.js +172 -0
  37. package/test/View/Form/generateFieldHtml.test.js +56 -0
  38. package/test/View/Image/serve.test.js +9 -4
  39. package/test/View/Image/url.test.js +10 -4
  40. package/test/View/addNavigateAttribute.test.js +10 -1
  41. package/test/View/print.test.js +10 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ### ✨ What's New
2
+
3
+ - add multipart file upload support with validation
4
+
5
+ ### 🛠️ Fixes & Improvements
6
+
7
+ - **auth:** enhance token validation and anomaly detection mechanisms
8
+ - patch file descriptor leak, polling anti-pattern, mimetype guard and duplicate move check
9
+ - **test:** improve temp file cleanup logic in multipart tests for reliability
10
+ - update cache directory handling to use global.__dir for consistency
11
+
12
+
13
+
14
+ ---
15
+
16
+ Powered by [⚡ ODAC](https://odac.run)
17
+
18
+ ### 🛠️ Fixes & Improvements
19
+
20
+ - **docs:** update references from 'odac' to 'Odac' for consistency
21
+ - **form:** improve form submission handler ownership to prevent duplicate submissions
22
+ - **form:** prevent multiple submit handlers on re-bound forms
23
+ - **startup:** surface init/migration errors instead of swallowing them
24
+
25
+
26
+
27
+ ---
28
+
29
+ Powered by [⚡ ODAC](https://odac.run)
30
+
1
31
  ### 🛠️ Fixes & Improvements
2
32
 
3
33
  - **docs:** update frontend scripts documentation to include code obfuscation details and enhance clarity
package/bin/odac.js CHANGED
@@ -532,6 +532,7 @@ async function runMigration(cmd, cliArgs) {
532
532
  }
533
533
  } catch (err) {
534
534
  console.error('❌ Migration error:', err.message)
535
+ if (err.cause) console.error(' Cause:', err.cause.message || err.cause)
535
536
  process.exit(1)
536
537
  } finally {
537
538
  for (const conn of Object.values(connections)) {
package/client/odac.js CHANGED
@@ -119,6 +119,7 @@ if (typeof window !== 'undefined') {
119
119
  #page = null
120
120
  #token = {hash: [], data: false}
121
121
  #formSubmitHandlers = new Map()
122
+ #formElementSelectors = new WeakMap()
122
123
  #loader = {elements: {}, callback: null, parts: {}}
123
124
  #isNavigating = false
124
125
 
@@ -412,6 +413,20 @@ if (typeof window !== 'undefined') {
412
413
  const formElement = e.target.closest(formSelector)
413
414
  if (!formElement) return
414
415
 
416
+ // Ownership guard: a single form can be matched by more than one
417
+ // registered selector — e.g. an <odac:form> auto-bound by #initForms
418
+ // under its data-odac-form token, then re-bound by the app via
419
+ // Odac.form('#id', cb) to attach a callback. The form is tagged with the
420
+ // selector it was last registered under; only that handler may process
421
+ // the submit, so the form fires exactly once instead of 2x. We don't
422
+ // tear down the other selector's listener (it may still serve *other*
423
+ // forms — e.g. a shared '.ajax-form' selector), we just let those
424
+ // handlers ignore this form. Forms never explicitly (re-)bound aren't
425
+ // tracked and fall through normally, preserving delegation for forms
426
+ // added to the DOM after registration.
427
+ const owner = this.#formElementSelectors.get(formElement)
428
+ if (owner && owner !== formSelector) return
429
+
415
430
  e.preventDefault()
416
431
 
417
432
  if (obj.messages !== false) {
@@ -464,6 +479,43 @@ if (typeof window !== 'undefined') {
464
479
  errorSpan.textContent = ''
465
480
  }
466
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
+
467
519
  if (input.hasAttribute('required')) {
468
520
  const isEmpty = input.type === 'checkbox' || input.type === 'radio' ? !input.checked : !input.value.trim()
469
521
  if (isEmpty) {
@@ -508,7 +560,7 @@ if (typeof window !== 'undefined') {
508
560
  let datastring, cache, contentType, processData
509
561
  if (formElement.querySelector('input[type=file]')) {
510
562
  datastring = new FormData(formElement)
511
- datastring.append('token', this.token())
563
+ datastring.append('_token', this.token())
512
564
  cache = false
513
565
  contentType = false
514
566
  processData = false
@@ -680,6 +732,7 @@ if (typeof window !== 'undefined') {
680
732
 
681
733
  document.addEventListener('submit', handler)
682
734
  this.#formSubmitHandlers.set(formSelector, handler)
735
+ document.querySelectorAll(formSelector).forEach(formEl => this.#formElementSelectors.set(formEl, formSelector))
683
736
  }
684
737
 
685
738
  get(url, callback) {
@@ -997,6 +1050,9 @@ if (typeof window !== 'undefined') {
997
1050
 
998
1051
  for (const {cls, attr} of formTypes) {
999
1052
  document.querySelectorAll(`form.${cls}[${attr}]`).forEach(form => {
1053
+ // Skip forms the app already bound via Odac.form(...) — re-registering
1054
+ // here would attach a second handler and cause 2x submit.
1055
+ if (this.#formElementSelectors.has(form)) return
1000
1056
  const token = form.getAttribute(attr)
1001
1057
  const selector = `form[${attr}="${token}"]`
1002
1058
  if (!this.#formSubmitHandlers.has(selector)) {
@@ -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) Brute-force on auth endpoint
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()
@@ -18,7 +18,7 @@ Frontend logic is organized into **Actions** using `Odac.action()`. This method
18
18
  - `start`: Fires once when the script initializes.
19
19
  - `load`: Fires after every page load (including AJAX navigations).
20
20
  3. **Page Scoping**: Use the `page: { name: fn }` object to isolate code to specific routes.
21
- 4. **Persistent Storage**: Use `odac.storage(key, value)` for a secure LocalStorage wrapper.
21
+ 4. **Persistent Storage**: Use `Odac.storage(key, value)` for a secure LocalStorage wrapper.
22
22
 
23
23
  ## Reference Patterns
24
24
 
@@ -60,11 +60,11 @@ Odac.action({
60
60
  ### 3. Data Utilities
61
61
  ```javascript
62
62
  // Accessing data shared from backend (Odac.share)
63
- const user = odac.data('user')
63
+ const user = Odac.data('user')
64
64
 
65
65
  // Using Storage wrapper
66
- odac.storage('theme', 'dark')
67
- const theme = odac.storage('theme')
66
+ Odac.storage('theme', 'dark')
67
+ const theme = Odac.storage('theme')
68
68
  ```
69
69
 
70
70
  ### 4. Programmatic Navigation
@@ -84,4 +84,4 @@ Odac.load('/dashboard', function(page, vars) {
84
84
  ## Best Practices
85
85
  - **Clean Selectors**: Use ID or specific data-attributes for event listeners to avoid conflicts.
86
86
  - **No Inline JS**: Move all logic from HTML attributes (onclick, etc.) into `Odac.action()`.
87
- - **Shared State**: Use `odac.data()` to pass complex objects from the backend once per request.
87
+ - **Shared State**: Use `Odac.data()` to pass complex objects from the backend once per request.
@@ -15,7 +15,7 @@ Remember the `Odac` object? It's your best friend inside a controller. It's pass
15
15
  * `Odac.return(data)`: Send back a response.
16
16
  * `Odac.direct(url)`: Redirect the user to a new page.
17
17
  * `Odac.set(key, value)`: Pass variables to your View template.
18
- * `Odac.share(key, value)`: Share data directly with frontend JavaScript (`odac.data()`).
18
+ * `Odac.share(key, value)`: Share data directly with frontend JavaScript (`Odac.data()`).
19
19
  * `Odac.cookie(key, value)`: Set a browser cookie.
20
20
  * `Odac.validator()`: Check user input easily.
21
21
  * `Odac.setInterval(callback, delay)`: Schedule repeating tasks (auto-cleanup).
@@ -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 `|`:
@@ -204,6 +224,50 @@ Automatically set field values without user input:
204
224
  <odac:submit text="Save" loading="Saving..." class="btn btn-primary" id="save-btn"/>
205
225
  ```
206
226
 
227
+ ## Client-Side Callbacks (Optional)
228
+
229
+ `<odac:form>` works fully on its own — submission, CSRF, validation, loading
230
+ states, success/error messages and redirects are all wired up automatically,
231
+ **no JavaScript required.**
232
+
233
+ If you also need to run your own code after a submit (analytics, closing a
234
+ modal, updating the UI, etc.), give the form an `id` and attach a callback with
235
+ `Odac.form()`:
236
+
237
+ ```html
238
+ <odac:form action="Account.unlink" id="server-unlink">
239
+ <!-- fields -->
240
+ <odac:submit text="Unlink"/>
241
+ </odac:form>
242
+ ```
243
+
244
+ ```javascript
245
+ Odac.form('#server-unlink', function(data) {
246
+ if (data.result.success) {
247
+ closeModal()
248
+ }
249
+ })
250
+ ```
251
+
252
+ This is **safe to combine** with the automatic registration — calling
253
+ `Odac.form()` on an `<odac:form>` re-binds the same form rather than adding a
254
+ second handler, so the form still submits only once. (Internally the dedup is
255
+ keyed by the form element, not the selector string.)
256
+
257
+ > **Note:** Call `Odac.form()` after the form exists in the DOM (e.g. on
258
+ > `DOMContentLoaded` or after AJAX navigation). Binding before the element is
259
+ > rendered can leave the automatic registration in place as a second handler.
260
+
261
+ You can trigger submission programmatically with the native API:
262
+
263
+ ```javascript
264
+ document.getElementById('server-unlink').requestSubmit()
265
+ ```
266
+
267
+ For the full callback/options API (`messages`, `loading`, redirect strings,
268
+ file-upload progress) see
269
+ [Form Handling with odac.js](../../frontend/03-forms/01-form-handling.md).
270
+
207
271
  ## Controller Handler (Server Actions)
208
272
 
209
273
  Handle form submission directly in your controller. The action is defined as `ControllerName.methodName`.
@@ -262,6 +326,38 @@ module.exports = class Contact {
262
326
 
263
327
  **Note:** No route definition is needed for the action! The form system handles the routing securely.
264
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
+
265
361
  ## Automatic Database Insert
266
362
 
267
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:
@@ -88,13 +88,13 @@ Odac.action({
88
88
 
89
89
  ```javascript
90
90
  // Basic usage
91
- odac.form('#my-form', function(data) {
91
+ Odac.form('#my-form', function(data) {
92
92
  // This callback is executed on success
93
93
  console.log('Form submitted successfully!', data);
94
94
  });
95
95
 
96
96
  // With options
97
- odac.form({
97
+ Odac.form({
98
98
  form: '#my-form',
99
99
  messages: ['success', 'error'], // Show both success and error messages
100
100
  loading: function(percent) {
@@ -119,7 +119,7 @@ To display validation errors, you can add elements with the `odac-form-error` at
119
119
  For simple GET requests, you can use the `Odac.get()` method.
120
120
 
121
121
  ```javascript
122
- odac.get('/api/users', function(data) {
122
+ Odac.get('/api/users', function(data) {
123
123
  console.log('Users:', data);
124
124
  });
125
125
  ```
@@ -133,18 +133,18 @@ odac.get('/api/users', function(data) {
133
133
  - **`Odac.client()`**: Returns a unique client identifier from a cookie.
134
134
  - **`Odac.data(key)`**: Returns shared data passed from the backend via `Odac.share`. You can get the full data object or a specific key:
135
135
  ```javascript
136
- let allData = odac.data();
137
- let user = odac.data('user'); // Returns null if not exists
136
+ let allData = Odac.data();
137
+ let user = Odac.data('user'); // Returns null if not exists
138
138
  ```
139
139
  - **`Odac.page()`**: Returns the identifier of the current page. This is the controller name (e.g., `'user'`) or view name (e.g., `'dashboard'`) set by the backend. Use this to conditionally run code for specific pages.
140
140
  - **`Odac.storage()`**: A wrapper for `localStorage`.
141
141
  ```javascript
142
142
  // Set a value
143
- odac.storage('my-key', 'my-value');
143
+ Odac.storage('my-key', 'my-value');
144
144
 
145
145
  // Get a value
146
- let value = odac.storage('my-key');
146
+ let value = Odac.storage('my-key');
147
147
 
148
148
  // Remove a value
149
- odac.storage('my-key', null);
149
+ Odac.storage('my-key', null);
150
150
  ```
@@ -126,17 +126,17 @@ Controllers automatically support AJAX loading. Use `Odac.View.skeleton()` to sp
126
126
  ```javascript
127
127
  module.exports = function (Odac) {
128
128
  // Define the skeleton template
129
- odac.View.skeleton('main')
129
+ Odac.View.skeleton('main')
130
130
 
131
131
  // Set view parts - lowercase keys map to UPPERCASE placeholders
132
- odac.View.set({
132
+ Odac.View.set({
133
133
  header: 'main', // Loads view/header/main.html into {{ HEADER }}
134
134
  content: 'about', // Loads view/content/about.html into {{ CONTENT }}
135
135
  footer: 'main' // Loads view/footer/main.html into {{ FOOTER }}
136
136
  })
137
137
 
138
138
  // Optional: Send variables to frontend (AJAX only)
139
- odac.set({
139
+ Odac.set({
140
140
  pageTitle: 'About',
141
141
  data: {foo: 'bar'}
142
142
  }, true) // true = include in AJAX responses
@@ -318,13 +318,13 @@ Both methods work automatically - no additional configuration needed!
318
318
  }
319
319
  ```
320
320
 
321
- ### odac.loader(selector, elements, callback)
321
+ ### Odac.loader(selector, elements, callback)
322
322
 
323
323
  Low-level method for direct initialization (not recommended for new code).
324
324
 
325
325
  **Parameters:** Same as navigate configuration, but as separate arguments.
326
326
 
327
- ### odac.load(url, callback, push)
327
+ ### Odac.load(url, callback, push)
328
328
 
329
329
  Programmatically load a page via AJAX.
330
330
 
@@ -335,7 +335,7 @@ Programmatically load a page via AJAX.
335
335
 
336
336
  **Example:**
337
337
  ```javascript
338
- odac.load('/about', function(page, variables) {
338
+ Odac.load('/about', function(page, variables) {
339
339
  console.log('Loaded:', page)
340
340
  })
341
341
  ```
@@ -377,7 +377,7 @@ Send data from server to client in AJAX responses:
377
377
 
378
378
  ```javascript
379
379
  // In controller
380
- odac.set({
380
+ Odac.set({
381
381
  user: {name: 'John', role: 'admin'},
382
382
  stats: {views: 1234}
383
383
  }, true) // true = include in AJAX
@@ -423,12 +423,12 @@ Odac.action({
423
423
  navigate: {
424
424
  update: 'main',
425
425
  on: function(page, variables) {
426
- odac.fn.updateActiveNav(window.location.pathname)
426
+ Odac.fn.updateActiveNav(window.location.pathname)
427
427
  console.log('Navigated to:', page)
428
428
  }
429
429
  },
430
430
 
431
- // Custom functions (accessible as odac.fn.functionName)
431
+ // Custom functions (accessible as Odac.fn.functionName)
432
432
  function: {
433
433
  updateActiveNav: function(url) {
434
434
  document.querySelectorAll('nav a').forEach(link => {
@@ -440,14 +440,14 @@ Odac.action({
440
440
  // App initialization
441
441
  load: function() {
442
442
  console.log('App initialized')
443
- odac.fn.updateActiveNav(window.location.pathname)
443
+ Odac.fn.updateActiveNav(window.location.pathname)
444
444
  },
445
445
 
446
446
  // Page-specific code
447
447
  page: {
448
448
  index: function(variables) {
449
449
  // Home page specific code
450
- odac.form('#contact-form', function(data) {
450
+ Odac.form('#contact-form', function(data) {
451
451
  if (data.result.success) {
452
452
  alert('Message sent!')
453
453
  }
@@ -463,7 +463,7 @@ Odac.action({
463
463
  // Event handlers
464
464
  click: {
465
465
  '#refresh-btn': function() {
466
- odac.load(window.location.pathname)
466
+ Odac.load(window.location.pathname)
467
467
  }
468
468
  }
469
469
  })
@@ -595,8 +595,8 @@ This is usually caused by mismatched keys between your skeleton, controller, and
595
595
 
596
596
  2. **Controller** (`controller/page/about.js`):
597
597
  ```javascript
598
- odac.View.skeleton('main')
599
- odac.View.set({
598
+ Odac.View.skeleton('main')
599
+ Odac.View.set({
600
600
  header: 'main', // Lowercase → {{ HEADER }}
601
601
  content: 'about' // Lowercase → {{ CONTENT }}
602
602
  })
@@ -129,7 +129,7 @@ The keys (content, header, etc.) must match the view parts defined in your contr
129
129
 
130
130
  ```javascript
131
131
  // In controller
132
- odac.View.set({
132
+ Odac.View.set({
133
133
  header: 'main',
134
134
  content: 'about',
135
135
  sidebar: 'main'
@@ -219,14 +219,14 @@ In your controller, send data to the client:
219
219
  ```javascript
220
220
  module.exports = function(Odac) {
221
221
  // Set variables for AJAX responses
222
- odac.set({
222
+ Odac.set({
223
223
  title: 'About Page',
224
224
  user: {name: 'John', role: 'admin'},
225
225
  stats: {views: 1234}
226
226
  }, true) // true = include in AJAX
227
227
 
228
- odac.View.skeleton('main')
229
- odac.View.set({
228
+ Odac.View.skeleton('main')
229
+ Odac.View.set({
230
230
  header: 'main',
231
231
  content: 'about',
232
232
  footer: 'main'