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.
- package/.agent/rules/memory.md +5 -0
- package/.releaserc.js +9 -2
- package/CHANGELOG.md +64 -0
- package/README.md +1 -1
- package/bin/odac.js +3 -2
- package/client/odac.js +124 -28
- package/docs/ai/skills/backend/database.md +19 -0
- package/docs/ai/skills/backend/forms.md +107 -13
- package/docs/ai/skills/backend/migrations.md +8 -2
- package/docs/ai/skills/backend/validation.md +132 -32
- package/docs/ai/skills/frontend/forms.md +43 -15
- package/docs/backend/08-database/02-basics.md +49 -9
- package/docs/backend/08-database/04-migrations.md +1 -0
- package/package.json +1 -1
- package/src/Auth.js +15 -2
- package/src/Database/ConnectionFactory.js +1 -0
- package/src/Database/Migration.js +26 -1
- package/src/Database/nanoid.js +30 -0
- package/src/Database.js +122 -11
- package/src/Ipc.js +37 -0
- package/src/Odac.js +1 -1
- package/src/Route/Cron.js +11 -0
- package/src/Route.js +49 -30
- package/src/Server.js +77 -23
- package/src/Storage.js +15 -1
- package/src/Validator.js +22 -20
- package/test/{Auth.test.js → Auth/check.test.js} +91 -5
- package/test/Client/data.test.js +91 -0
- package/test/Client/get.test.js +90 -0
- package/test/Client/storage.test.js +87 -0
- package/test/Client/token.test.js +82 -0
- package/test/Client/ws.test.js +118 -0
- package/test/Config/deepMerge.test.js +14 -0
- package/test/Config/init.test.js +66 -0
- package/test/Config/interpolate.test.js +35 -0
- package/test/Database/ConnectionFactory/buildConnectionConfig.test.js +13 -0
- package/test/Database/ConnectionFactory/buildConnections.test.js +31 -0
- package/test/Database/ConnectionFactory/resolveClient.test.js +12 -0
- package/test/Database/Migration/migrate_column.test.js +52 -0
- package/test/Database/Migration/migrate_files.test.js +70 -0
- package/test/Database/Migration/migrate_index.test.js +89 -0
- package/test/Database/Migration/migrate_nanoid.test.js +160 -0
- package/test/Database/Migration/migrate_seed.test.js +77 -0
- package/test/Database/Migration/migrate_table.test.js +88 -0
- package/test/Database/Migration/rollback.test.js +61 -0
- package/test/Database/Migration/snapshot.test.js +38 -0
- package/test/Database/Migration/status.test.js +41 -0
- package/test/Database/autoNanoid.test.js +215 -0
- package/test/Database/nanoid.test.js +19 -0
- package/test/Lang/constructor.test.js +25 -0
- package/test/Lang/get.test.js +65 -0
- package/test/Lang/set.test.js +49 -0
- package/test/Odac/init.test.js +42 -0
- package/test/Odac/instance.test.js +58 -0
- package/test/Route/{Middleware.test.js → Middleware/chaining.test.js} +5 -29
- package/test/Route/Middleware/use.test.js +35 -0
- package/test/{Route.test.js → Route/check.test.js} +100 -50
- package/test/Route/set.test.js +52 -0
- package/test/Route/ws.test.js +23 -0
- package/test/View/EarlyHints/cache.test.js +32 -0
- package/test/View/EarlyHints/extractFromHtml.test.js +143 -0
- package/test/View/EarlyHints/formatLinkHeader.test.js +33 -0
- package/test/View/EarlyHints/send.test.js +99 -0
- package/test/View/{Form.test.js → Form/generateFieldHtml.test.js} +2 -2
- package/test/View/constructor.test.js +22 -0
- package/test/View/print.test.js +19 -0
- package/test/WebSocket/Client/limits.test.js +55 -0
- package/test/WebSocket/Server/broadcast.test.js +33 -0
- package/test/WebSocket/Server/route.test.js +37 -0
- package/test/Client.test.js +0 -197
- package/test/Config.test.js +0 -119
- package/test/Database/ConnectionFactory.test.js +0 -80
- package/test/Lang.test.js +0 -92
- package/test/Migration.test.js +0 -943
- package/test/Odac.test.js +0 -88
- package/test/View/EarlyHints.test.js +0 -282
- package/test/WebSocket.test.js +0 -238
package/.agent/rules/memory.md
CHANGED
|
@@ -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:**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1120
|
+
wsConfig = {host, path, protocol, options}
|
|
1026
1121
|
const protocols = token ? ['odac-token-' + token] : []
|
|
1027
|
-
|
|
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
|
|
1043
|
-
|
|
1044
|
-
|
|
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:
|
|
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,
|
|
5
|
+
tags: backend, forms, validation, register, login, magic-login, request-processing
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Backend Forms & Validation Skill
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
ODAC forms for validation, authentication flows, and safe request handling.
|
|
11
11
|
|
|
12
12
|
## Rules
|
|
13
|
-
1. **
|
|
14
|
-
2. **
|
|
15
|
-
3. **
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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. **
|
|
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: '
|
|
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/
|