odac 1.4.1 → 1.4.3

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 (77) hide show
  1. package/.agent/rules/memory.md +5 -0
  2. package/.releaserc.js +9 -2
  3. package/CHANGELOG.md +64 -0
  4. package/README.md +1 -1
  5. package/bin/odac.js +3 -2
  6. package/client/odac.js +124 -28
  7. package/docs/ai/skills/backend/database.md +19 -0
  8. package/docs/ai/skills/backend/forms.md +107 -13
  9. package/docs/ai/skills/backend/migrations.md +8 -2
  10. package/docs/ai/skills/backend/validation.md +132 -32
  11. package/docs/ai/skills/frontend/forms.md +43 -15
  12. package/docs/backend/08-database/02-basics.md +49 -9
  13. package/docs/backend/08-database/04-migrations.md +1 -0
  14. package/package.json +1 -1
  15. package/src/Auth.js +15 -2
  16. package/src/Database/ConnectionFactory.js +1 -0
  17. package/src/Database/Migration.js +26 -1
  18. package/src/Database/nanoid.js +30 -0
  19. package/src/Database.js +122 -11
  20. package/src/Ipc.js +37 -0
  21. package/src/Odac.js +1 -1
  22. package/src/Route/Cron.js +11 -0
  23. package/src/Route.js +49 -30
  24. package/src/Server.js +77 -23
  25. package/src/Storage.js +15 -1
  26. package/src/Validator.js +22 -20
  27. package/test/{Auth.test.js → Auth/check.test.js} +91 -5
  28. package/test/Client/data.test.js +91 -0
  29. package/test/Client/get.test.js +90 -0
  30. package/test/Client/storage.test.js +87 -0
  31. package/test/Client/token.test.js +82 -0
  32. package/test/Client/ws.test.js +118 -0
  33. package/test/Config/deepMerge.test.js +14 -0
  34. package/test/Config/init.test.js +66 -0
  35. package/test/Config/interpolate.test.js +35 -0
  36. package/test/Database/ConnectionFactory/buildConnectionConfig.test.js +13 -0
  37. package/test/Database/ConnectionFactory/buildConnections.test.js +31 -0
  38. package/test/Database/ConnectionFactory/resolveClient.test.js +12 -0
  39. package/test/Database/Migration/migrate_column.test.js +52 -0
  40. package/test/Database/Migration/migrate_files.test.js +70 -0
  41. package/test/Database/Migration/migrate_index.test.js +89 -0
  42. package/test/Database/Migration/migrate_nanoid.test.js +160 -0
  43. package/test/Database/Migration/migrate_seed.test.js +77 -0
  44. package/test/Database/Migration/migrate_table.test.js +88 -0
  45. package/test/Database/Migration/rollback.test.js +61 -0
  46. package/test/Database/Migration/snapshot.test.js +38 -0
  47. package/test/Database/Migration/status.test.js +41 -0
  48. package/test/Database/autoNanoid.test.js +215 -0
  49. package/test/Database/nanoid.test.js +19 -0
  50. package/test/Lang/constructor.test.js +25 -0
  51. package/test/Lang/get.test.js +65 -0
  52. package/test/Lang/set.test.js +49 -0
  53. package/test/Odac/init.test.js +42 -0
  54. package/test/Odac/instance.test.js +58 -0
  55. package/test/Route/{Middleware.test.js → Middleware/chaining.test.js} +5 -29
  56. package/test/Route/Middleware/use.test.js +35 -0
  57. package/test/{Route.test.js → Route/check.test.js} +100 -50
  58. package/test/Route/set.test.js +52 -0
  59. package/test/Route/ws.test.js +23 -0
  60. package/test/View/EarlyHints/cache.test.js +32 -0
  61. package/test/View/EarlyHints/extractFromHtml.test.js +143 -0
  62. package/test/View/EarlyHints/formatLinkHeader.test.js +33 -0
  63. package/test/View/EarlyHints/send.test.js +99 -0
  64. package/test/View/{Form.test.js → Form/generateFieldHtml.test.js} +2 -2
  65. package/test/View/constructor.test.js +22 -0
  66. package/test/View/print.test.js +19 -0
  67. package/test/WebSocket/Client/limits.test.js +55 -0
  68. package/test/WebSocket/Server/broadcast.test.js +33 -0
  69. package/test/WebSocket/Server/route.test.js +37 -0
  70. package/test/Client.test.js +0 -197
  71. package/test/Config.test.js +0 -119
  72. package/test/Database/ConnectionFactory.test.js +0 -80
  73. package/test/Lang.test.js +0 -92
  74. package/test/Migration.test.js +0 -943
  75. package/test/Odac.test.js +0 -88
  76. package/test/View/EarlyHints.test.js +0 -282
  77. package/test/WebSocket.test.js +0 -238
@@ -43,6 +43,11 @@ trigger: always_on
43
43
  - **Mandatory Test Coverage:** Every new feature, method, or significant logic change MUST be accompanied by a corresponding unit or integration test.
44
44
  - **Verify Correctness:** do not assume code works; prove it with a test that covers both success and failure scenarios (e.g., edge cases, error conditions).
45
45
  - **Update Existing Tests:** If a feature modifies existing behavior, update the relevant tests to reflect the new logic and ensure they pass.
46
+ - **Atomic Test Structure:**
47
+ - **Directory Mapping:** Each source class/module must have its own directory under `test/` (e.g., `src/Auth.js` -> `test/Auth/`).
48
+ - **Method-Level Files:** Every public method should have its own test file within the class directory (e.g., `test/Auth/check.test.js`).
49
+ - **Sub-module Context:** Nested modules should follow the same pattern (e.g., `src/View/Form.js` -> `test/View/Form/generateFieldHtml.test.js`).
50
+ - **Isolation & Parallelism:** This structure is mandatory to leverage Jest's multi-threaded execution and ensure strict isolation between test cases.
46
51
 
47
52
  ## Client Library (odac.js)
48
53
  - **Automatic JSON Parsing:** The `#ajax` method (and by extension `odac.get`) must automatically parse the response if the `Content-Type` header contains `application/json`, even if `dataType` is not explicitly set to `json`.
package/.releaserc.js CHANGED
@@ -4,7 +4,12 @@ module.exports = {
4
4
  [
5
5
  '@semantic-release/commit-analyzer',
6
6
  {
7
- preset: 'conventionalcommits'
7
+ preset: 'conventionalcommits',
8
+ releaseRules: [
9
+ {type: 'major', release: 'major'},
10
+ {type: 'minor', release: 'minor'},
11
+ {type: '*', release: 'patch'}
12
+ ]
8
13
  }
9
14
  ],
10
15
  [
@@ -16,6 +21,8 @@ module.exports = {
16
21
  const commit = JSON.parse(JSON.stringify(c))
17
22
 
18
23
  const map = {
24
+ major: '🚀 Major Updates',
25
+ minor: '🌟 Minor Updates',
19
26
  feat: "✨ What's New",
20
27
  fix: '🛠️ Fixes & Improvements',
21
28
  perf: '⚡️ Performance Upgrades',
@@ -134,4 +141,4 @@ Powered by [⚡ ODAC](https://odac.run)
134
141
  ],
135
142
  '@semantic-release/github'
136
143
  ]
137
- }
144
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,67 @@
1
+ ### ⚙️ Engine Tuning
2
+
3
+ - **client:** extract ws connection logic and fix recursive sharedworker reconnects reconnect bug
4
+ - **client:** remove redundant truthy check for websocket token
5
+ - **route:** remove useless variable assignment for decodedUrl
6
+
7
+ ### ⚡️ Performance Upgrades
8
+
9
+ - **client:** remove redundant token consumption during websocket initialization
10
+
11
+ ### 📚 Documentation
12
+
13
+ - **README:** enhance security section with detailed CSRF protection features
14
+
15
+ ### 🛠️ Fixes & Improvements
16
+
17
+ - **client:** enhance websocket reconnection logic with attempt tracking and timer management
18
+ - **client:** preserve existing websocket subprotocols & add try/catch layer to token provider
19
+ - **route:** improve URL decoding and public path handling for file requests
20
+ - **route:** sanitize decoded URL and improve public path validation
21
+ - **route:** use robust path.extname instead of string splitting for mime type resolution
22
+ - **websocket:** implement token provider for dynamic token handling on reconnect
23
+
24
+
25
+
26
+ ---
27
+
28
+ Powered by [⚡ ODAC](https://odac.run)
29
+
30
+ ### doc
31
+
32
+ - **forms:** update backend and frontend forms documentation with practical usage patterns and improved descriptions
33
+ - **validation:** enhance backend validation documentation with detailed usage patterns and examples
34
+
35
+ ### ⚙️ Engine Tuning
36
+
37
+ - **test:** restructure test suite into class-scoped directories and method-level atomic files
38
+
39
+ ### ✨ What's New
40
+
41
+ - **database:** add debug logging for schema parsing failures in nanoid metadata loader
42
+ - **database:** introduce NanoID support for automatic ID generation in schema
43
+ - **release:** enhance commit analyzer with release rules and custom labels
44
+ - **shutdown:** implement graceful shutdown for IPC, Database, and Cron services
45
+
46
+ ### 📚 Documentation
47
+
48
+ - **database:** remove underscore from nanoid example to reflect true alphanumeric output
49
+
50
+ ### 🛠️ Fixes & Improvements
51
+
52
+ - **Auth:** handle token rotation for WebSocket connections and update active timestamp
53
+ - **core:** explicitly stop session GC interval during graceful shutdown
54
+ - **database:** namespace nanoid schema cache by connection to prevent table to prevent collisions
55
+ - **forms:** initialize ODAC form handlers on DOMContentLoaded and after AJAX navigation
56
+ - **manageSkills:** correct targetPath assignment for skill synchronization
57
+ - **Validator:** pass Odac instance to Validator for improved access to global methods
58
+
59
+
60
+
61
+ ---
62
+
63
+ Powered by [⚡ ODAC](https://odac.run)
64
+
1
65
  ### doc
2
66
 
3
67
  - enhance AI skills documentation with structured YAML front matter and detailed descriptions
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  * 🎨 **Built-in Tailwind CSS:** Zero-config integration with Tailwind CSS v4. Automatic compilation and optimization out of the box.
10
10
  * 🔗 **Powerful Routing:** Create clean, custom URLs and manage infinite pages with a flexible routing system.
11
11
  * ✨ **Seamless SPA Experience:** Automatic AJAX handling for forms and page transitions eliminates the need for complex client-side code.
12
- * 🛡️ **Built-in Security:** Automatic CSRF protection and secure default headers keep your application safe.
12
+ * 🛡️ **Built-in Security:** Enterprise-grade security out of the box. Includes secure default headers and a **Multi-tab Safe, Single-Use CSRF Protection (Nonce)**. Tokens self-replenish in the background, ensuring maximum defense without ever interrupting the user experience.
13
13
  * 🔐 **Authentication:** Ready-to-use session management with enterprise-grade **Refresh Token Rotation**, secure password hashing, and authentication helpers.
14
14
  * 🗄️ **Database Agnostic:** Integrated support for major databases (PostgreSQL, MySQL, SQLite) and Redis via Knex.js.
15
15
  * 🌍 **i18n Support:** Native multi-language support to help you reach a global audience.
package/bin/odac.js CHANGED
@@ -210,7 +210,7 @@ async function manageSkills(targetDir = process.cwd()) {
210
210
  }
211
211
 
212
212
  const targetBase = path.resolve(targetDir, targetSubDir)
213
- const targetPath = targetBase
213
+ const targetPath = copySkillsOnly ? path.join(targetBase, 'odac.js') : targetBase
214
214
 
215
215
  try {
216
216
  fs.mkdirSync(targetPath, {recursive: true})
@@ -222,7 +222,8 @@ async function manageSkills(targetDir = process.cwd()) {
222
222
  fs.cpSync(aiSourceDir, targetPath, {recursive: true})
223
223
  }
224
224
 
225
- console.log(`\n✨ AI skills successfully synced to: \x1b[32m${targetSubDir}\x1b[0m`)
225
+ const finalSubDir = copySkillsOnly ? path.join(targetSubDir, 'odac.js') : targetSubDir
226
+ console.log(`\n✨ AI skills successfully synced to: \x1b[32m${finalSubDir}\x1b[0m`)
226
227
  console.log('Your AI Agent now has full knowledge of the ODAC Framework. 🚀')
227
228
  } catch (err) {
228
229
  console.error('❌ Failed to sync AI skills:', err.message)
package/client/odac.js CHANGED
@@ -7,10 +7,12 @@ class OdacWebSocket {
7
7
  #reconnectAttempts = 0
8
8
  #handlers = {}
9
9
  #isClosed = false
10
+ #tokenProvider = null
10
11
 
11
12
  constructor(url, protocols = [], options = {}) {
12
13
  this.#url = url
13
14
  this.#protocols = protocols
15
+ this.#tokenProvider = options.tokenProvider || null
14
16
  this.#options = {
15
17
  autoReconnect: true,
16
18
  reconnectDelay: 3000,
@@ -23,6 +25,19 @@ class OdacWebSocket {
23
25
  connect() {
24
26
  if (this.#isClosed) return
25
27
 
28
+ if (this.#tokenProvider) {
29
+ try {
30
+ const freshToken = this.#tokenProvider()
31
+ if (freshToken) {
32
+ let current = Array.isArray(this.#protocols) ? this.#protocols : this.#protocols ? [this.#protocols] : []
33
+ this.#protocols = current.filter(p => !p.startsWith('odac-token-'))
34
+ this.#protocols.push(`odac-token-${freshToken}`)
35
+ }
36
+ } catch (e) {
37
+ console.error('Odac WebSocket tokenProvider error:', e)
38
+ }
39
+ }
40
+
26
41
  this.#socket = this.#protocols.length > 0 ? new WebSocket(this.#url, this.#protocols) : new WebSocket(this.#url)
27
42
 
28
43
  this.#socket.onopen = () => {
@@ -111,6 +126,12 @@ if (typeof window !== 'undefined') {
111
126
  // In constructor we can't call this.data() easily if it uses 'this' for caching properly before init
112
127
  // But based on original code logic:
113
128
  this.#data = this.data()
129
+
130
+ if (document.readyState === 'loading') {
131
+ document.addEventListener('DOMContentLoaded', () => this.#initForms())
132
+ } else {
133
+ this.#initForms()
134
+ }
114
135
  }
115
136
 
116
137
  #ajax(options) {
@@ -795,6 +816,8 @@ if (typeof window !== 'undefined') {
795
816
  }
796
817
 
797
818
  #handleLoadComplete(data, callback) {
819
+ this.#initForms()
820
+
798
821
  if (this.actions.load)
799
822
  (Array.isArray(this.actions.load) ? this.actions.load : [this.actions.load]).forEach(fn => fn(this.page(), data.variables))
800
823
  if (this.actions.page && this.actions.page[this.page()])
@@ -808,6 +831,30 @@ if (typeof window !== 'undefined') {
808
831
  this.#isNavigating = false
809
832
  }
810
833
 
834
+ /**
835
+ * Scans the DOM for ODAC form components and registers submit handlers
836
+ * for any that haven't been initialized yet. Called on DOMContentLoaded
837
+ * and after every AJAX navigation to bind freshly rendered forms.
838
+ */
839
+ #initForms() {
840
+ const formTypes = [
841
+ {cls: 'odac-register-form', attr: 'data-odac-register'},
842
+ {cls: 'odac-login-form', attr: 'data-odac-login'},
843
+ {cls: 'odac-magic-login-form', attr: 'data-odac-magic-login'},
844
+ {cls: 'odac-custom-form', attr: 'data-odac-form'}
845
+ ]
846
+
847
+ for (const {cls, attr} of formTypes) {
848
+ document.querySelectorAll(`form.${cls}[${attr}]`).forEach(form => {
849
+ const token = form.getAttribute(attr)
850
+ const selector = `form[${attr}="${token}"]`
851
+ if (!this.#formSubmitHandlers.has(selector)) {
852
+ this.form({form: selector})
853
+ }
854
+ })
855
+ }
856
+ }
857
+
811
858
  loader(selector, elements, callback) {
812
859
  this.#loader.elements = elements
813
860
  this.#loader.callback = callback
@@ -892,12 +939,9 @@ if (typeof window !== 'undefined') {
892
939
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
893
940
  const wsUrl = `${protocol}//${window.location.host}${path}`
894
941
  const protocols = []
895
- if (token) {
896
- const csrfToken = this.token()
897
- if (csrfToken) protocols.push(`odac-token-${csrfToken}`)
898
- }
942
+ const tokenProvider = token ? () => this.token() : null
899
943
 
900
- return new OdacWebSocket(wsUrl, protocols, options)
944
+ return new OdacWebSocket(wsUrl, protocols, {...options, tokenProvider})
901
945
  }
902
946
 
903
947
  #createSharedWebSocket(path, options) {
@@ -934,6 +978,9 @@ if (typeof window !== 'undefined') {
934
978
  case 'error':
935
979
  emit('error', data)
936
980
  break
981
+ case 'requestToken':
982
+ worker.port.postMessage({type: 'provideToken', token: this.token()})
983
+ break
937
984
  }
938
985
  }
939
986
 
@@ -993,25 +1040,73 @@ if (typeof window !== 'undefined') {
993
1040
  }
994
1041
  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init()
995
1042
  })()
996
-
997
- document.addEventListener('DOMContentLoaded', () => {
998
- ;['register', 'login'].forEach(type => {
999
- document.querySelectorAll(`form.odac-${type}-form[data-odac-${type}]`).forEach(form => {
1000
- const token = form.getAttribute(`data-odac-${type}`)
1001
- window.Odac.form({form: `form[data-odac-${type}="${token}"]`})
1002
- })
1003
- })
1004
- document.querySelectorAll('form.odac-custom-form[data-odac-form]').forEach(form => {
1005
- const token = form.getAttribute('data-odac-form')
1006
- window.Odac.form({form: `form[data-odac-form="${token}"]`})
1007
- })
1008
- })
1009
1043
  } else {
1010
1044
  let socket = null
1011
1045
  const ports = new Set()
1012
1046
 
1013
1047
  const broadcast = (type, data) => ports.forEach(port => port.postMessage({type, data}))
1014
1048
 
1049
+ let wsConfig = null
1050
+ let reconnectAttempts = 0
1051
+ let reconnectTimer = null
1052
+
1053
+ const requestTokenFromPort = () => {
1054
+ return new Promise(resolve => {
1055
+ const firstPort = ports.values().next().value
1056
+ if (!firstPort) return resolve(null)
1057
+
1058
+ let timeoutTimer = null
1059
+
1060
+ const handler = event => {
1061
+ if (event.data.type === 'provideToken') {
1062
+ clearTimeout(timeoutTimer)
1063
+ firstPort.removeEventListener('message', handler)
1064
+ resolve(event.data.token)
1065
+ }
1066
+ }
1067
+
1068
+ timeoutTimer = setTimeout(() => {
1069
+ firstPort.removeEventListener('message', handler)
1070
+ resolve(null)
1071
+ }, 5000)
1072
+
1073
+ firstPort.addEventListener('message', handler)
1074
+ firstPort.postMessage({type: 'requestToken'})
1075
+ })
1076
+ }
1077
+
1078
+ const connectSocket = protocols => {
1079
+ if (!wsConfig || ports.size === 0) return
1080
+ const wsUrl = wsConfig.protocol + '//' + wsConfig.host + wsConfig.path
1081
+ socket = new OdacWebSocket(wsUrl, protocols, {
1082
+ ...wsConfig.options,
1083
+ tokenProvider: null
1084
+ })
1085
+ socket.on('open', () => {
1086
+ reconnectAttempts = 0
1087
+ broadcast('open')
1088
+ })
1089
+ socket.on('message', data => broadcast('message', data))
1090
+ socket.on('close', e => {
1091
+ broadcast('close', {code: e?.code, reason: e?.reason, wasClean: e?.wasClean})
1092
+ const maxAttempts = wsConfig.options.maxReconnectAttempts || 10
1093
+ if (wsConfig && wsConfig.options.autoReconnect !== false && ports.size > 0 && reconnectAttempts < maxAttempts) {
1094
+ if (socket) {
1095
+ socket.close()
1096
+ socket = null
1097
+ }
1098
+ reconnectAttempts++
1099
+ reconnectTimer = setTimeout(() => {
1100
+ requestTokenFromPort().then(freshToken => {
1101
+ if (!freshToken || ports.size === 0) return
1102
+ connectSocket(['odac-token-' + freshToken])
1103
+ })
1104
+ }, wsConfig.options.reconnectDelay || 1000)
1105
+ }
1106
+ })
1107
+ socket.on('error', e => broadcast('error', {message: e?.message || 'WebSocket error'}))
1108
+ }
1109
+
1015
1110
  self.onconnect = e => {
1016
1111
  const port = e.ports[0]
1017
1112
  ports.add(port)
@@ -1022,15 +1117,10 @@ if (typeof window !== 'undefined') {
1022
1117
  switch (type) {
1023
1118
  case 'connect':
1024
1119
  if (!socket) {
1025
- const wsUrl = protocol + '//' + host + path
1120
+ wsConfig = {host, path, protocol, options}
1026
1121
  const protocols = token ? ['odac-token-' + token] : []
1027
- socket = new OdacWebSocket(wsUrl, protocols, options)
1028
- socket.on('open', () => broadcast('open'))
1029
- socket.on('message', data => broadcast('message', data))
1030
- socket.on('close', e => broadcast('close', {code: e?.code, reason: e?.reason, wasClean: e?.wasClean}))
1031
- socket.on('error', e => broadcast('error', {message: e?.message || 'WebSocket error'}))
1122
+ connectSocket(protocols)
1032
1123
  } else if (socket.connected) {
1033
- // If already connected, notify the new port immediately
1034
1124
  port.postMessage({type: 'open'})
1035
1125
  }
1036
1126
  break
@@ -1039,9 +1129,15 @@ if (typeof window !== 'undefined') {
1039
1129
  break
1040
1130
  case 'close':
1041
1131
  ports.delete(port)
1042
- if (ports.size === 0 && socket) {
1043
- socket.close()
1044
- socket = null
1132
+ if (ports.size === 0) {
1133
+ if (reconnectTimer) {
1134
+ clearTimeout(reconnectTimer)
1135
+ reconnectTimer = null
1136
+ }
1137
+ if (socket) {
1138
+ socket.close()
1139
+ socket = null
1140
+ }
1045
1141
  }
1046
1142
  break
1047
1143
  }
@@ -27,6 +27,25 @@ await Odac.DB.table('posts').insert({
27
27
  });
28
28
  ```
29
29
 
30
+ ## ID Strategy (NanoID)
31
+ 1. **Schema-Driven**: Define string IDs with `type: 'nanoid'` in `schema/*.js`.
32
+ 2. **Auto Generation**: On `insert()`, ODAC auto-generates missing nanoid fields.
33
+ 3. **No Override**: If ID is explicitly provided, ODAC preserves it.
34
+ 4. **Bulk Safe**: Auto-generation works for both single and bulk inserts.
35
+
36
+ ```javascript
37
+ // schema/posts.js
38
+ module.exports = {
39
+ columns: {
40
+ id: {type: 'nanoid', primary: true},
41
+ title: {type: 'string', length: 255}
42
+ }
43
+ }
44
+
45
+ // ID is generated automatically
46
+ await Odac.DB.table('posts').insert({title: 'Hello'});
47
+ ```
48
+
30
49
  ## Migration Awareness
31
50
  1. **Schema-First**: Structural DB changes must be defined in `schema/*.js`.
32
51
  2. **Auto-Migrate**: Migrations run automatically at startup from `Database.init()`.
@@ -1,26 +1,120 @@
1
1
  ---
2
2
  name: backend-forms-validation-skill
3
- description: Secure ODAC form processing workflow with validation, CSRF protection, and safe request-to-database handling.
3
+ description: Practical ODAC form usage patterns for register/login, magic-login, custom actions, and automatic database insert.
4
4
  metadata:
5
- tags: backend, forms, validation, csrf, input-security, request-processing
5
+ tags: backend, forms, validation, register, login, magic-login, request-processing
6
6
  ---
7
7
 
8
8
  # Backend Forms & Validation Skill
9
9
 
10
- Processing form data securely and validating inputs.
10
+ ODAC forms for validation, authentication flows, and safe request handling.
11
11
 
12
12
  ## Rules
13
- 1. **Validator**: Always use `Odac.Validator` for input.
14
- 2. **Auto-save**: Use `Odac.DB.table().save(Odac.Request.post())` for quick inserts.
15
- 3. **CSRF**: Ensure `{{ TOKEN }}` is in your HTML forms.
13
+ 1. **Use ODAC form tags**: Prefer `<odac:register>`, `<odac:login>`, `<odac:magic-login>`, `<odac:form>` instead of manual raw form handlers.
14
+ 2. **Do not add manual hidden security fields**: Keep forms clean and use ODAC defaults.
15
+ 3. **Validation in template**: Define rules with `<odac:validate rule="..." message="..."/>`; they become both frontend HTML constraints and backend validator checks.
16
+ 4. **Server-side enrichment**: Use `<odac:set>` for trusted fields (`compute`, `value`, `callback`, `if-empty`) instead of taking these values from user input.
17
+ 5. **Action vs table**: In `<odac:form>`, use `table="..."` for automatic insert or `action="Class.method"` for custom business logic.
18
+
19
+ ## Form Type Variants
20
+
21
+ ### 1) Register Form
22
+ ```html
23
+ <odac:register redirect="/dashboard" autologin="true">
24
+ <odac:input name="email" type="email" label="Email">
25
+ <odac:validate rule="required|email" message="Valid email required"/>
26
+ </odac:input>
27
+
28
+ <odac:input name="password" type="password" label="Password">
29
+ <odac:validate rule="required|minlen:8" message="Min 8 chars"/>
30
+ </odac:input>
31
+
32
+ <odac:set name="created_at" compute="now"/>
33
+ <odac:submit text="Register" loading="Processing..."/>
34
+ </odac:register>
35
+ ```
36
+
37
+ ### 2) Login Form
38
+ ```html
39
+ <odac:login redirect="/panel">
40
+ <odac:input name="email" type="email" label="Email">
41
+ <odac:validate rule="required|email" message="Email required"/>
42
+ </odac:input>
43
+
44
+ <odac:input name="password" type="password" label="Password">
45
+ <odac:validate rule="required" message="Password required"/>
46
+ </odac:input>
47
+
48
+ <odac:submit text="Login" loading="Logging in..."/>
49
+ </odac:login>
50
+ ```
51
+
52
+ ### 3) Magic Login Form
53
+ ```html
54
+ <odac:magic-login redirect="/dashboard" email-label="Work Email" submit-text="Send Link" />
55
+ ```
56
+
57
+ ```html
58
+ <odac:magic-login redirect="/dashboard">
59
+ <odac:input name="email" type="email" label="Email">
60
+ <odac:validate rule="required|email" message="Valid email required"/>
61
+ </odac:input>
62
+ <odac:submit text="Send Magic Link" loading="Sending..."/>
63
+ </odac:magic-login>
64
+ ```
65
+
66
+ ### 4) Custom Form with Automatic DB Insert
67
+ ```html
68
+ <odac:form table="waitlist" redirect="/" success="Thank you!" clear="true">
69
+ <odac:input name="email" type="email" label="Email">
70
+ <odac:validate rule="required|email|unique" message="Email already exists"/>
71
+ </odac:input>
72
+
73
+ <odac:set name="created_at" compute="now"/>
74
+ <odac:set name="ip" compute="ip"/>
75
+ <odac:submit text="Join" loading="Joining..."/>
76
+ </odac:form>
77
+ ```
78
+
79
+ ### 5) Custom Form with Controller Action
80
+ ```html
81
+ <odac:form action="Contact.submit" clear="false">
82
+ <odac:input name="subject" type="text" label="Subject">
83
+ <odac:validate rule="required|minlen:3" message="Subject is too short"/>
84
+ </odac:input>
85
+ <odac:submit text="Send" loading="Sending..."/>
86
+ </odac:form>
87
+ ```
16
88
 
17
- ## Patterns
18
89
  ```javascript
19
- // Validation
20
- const check = Odac.Validator.run(Odac.Request.post(), {
21
- email: 'required|email',
22
- password: 'required|min:8'
23
- });
90
+ module.exports = class Contact {
91
+ constructor(Odac) {
92
+ this.Odac = Odac
93
+ }
94
+
95
+ async submit(form) {
96
+ const data = form.data
97
+ if (!data.subject) return form.error('subject', 'Subject required')
98
+ return form.success('Message sent', '/thank-you')
99
+ }
100
+ }
101
+ ```
24
102
 
25
- if (check.failed()) return Odac.Request.error(check.errors());
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.
107
+ - **Skip persistence**: Use `skip` on `<odac:input>` to validate a field but exclude it from final payload.
108
+ - **Unique shorthand**: `unique` attribute on `<odac:input>` enables auth-register uniqueness list.
109
+
110
+ ## Patterns
111
+ ```javascript
112
+ // Access validated data in action-driven custom form
113
+ Odac.Route.class.post('/contact', class Contact {
114
+ async submit(form) {
115
+ const {email, message} = form.data
116
+ if (!email || !message) return form.error('_odac_form', 'Missing input')
117
+ return form.success('Saved successfully')
118
+ }
119
+ })
26
120
  ```
@@ -19,7 +19,8 @@ ODAC migrations are **declarative**. The `schema/` directory is the single sourc
19
19
  4. **Index Sync**: Define indexes in schema; engine adds/removes them automatically.
20
20
  5. **Drop Behavior**: If a column/index is removed from schema, it is removed from DB on next startup.
21
21
  6. **Seeds**: Use `seed` + `seedKey` for idempotent reference data.
22
- 7. **Data Transformations**: Use imperative files under `migration/` only for one-time data migration logic.
22
+ 7. **NanoID Columns**: `type: 'nanoid'` maps to string columns and missing values are auto-generated on insert/seed.
23
+ 8. **Data Transformations**: Use imperative files under `migration/` only for one-time data migration logic.
23
24
 
24
25
  ## Reference Patterns
25
26
  ### 1. Schema File (Final State)
@@ -29,7 +30,7 @@ ODAC migrations are **declarative**. The `schema/` directory is the single sourc
29
30
 
30
31
  module.exports = {
31
32
  columns: {
32
- id: {type: 'increments'},
33
+ id: {type: 'nanoid', primary: true},
33
34
  email: {type: 'string', length: 255, nullable: false},
34
35
  role: {type: 'enum', values: ['admin', 'user'], default: 'user'},
35
36
  timestamps: {type: 'timestamps'}
@@ -44,6 +45,11 @@ module.exports = {
44
45
  }
45
46
  ```
46
47
 
48
+ ### NanoID Notes
49
+ - `length` can be customized: `{type: 'nanoid', length: 12, primary: true}`.
50
+ - If seed rows omit the nanoid field, ODAC fills it automatically.
51
+ - If seed rows provide an explicit nanoid value, ODAC keeps it unchanged.
52
+
47
53
  ### 2. Multi-Database Layout
48
54
  ```
49
55
  schema/