odac 1.4.1 → 1.4.2
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 +35 -0
- package/bin/odac.js +3 -2
- package/client/odac.js +32 -13
- 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 +8 -0
- 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 +86 -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} +4 -55
- 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,5 +1,40 @@
|
|
|
1
1
|
### doc
|
|
2
2
|
|
|
3
|
+
- **forms:** update backend and frontend forms documentation with practical usage patterns and improved descriptions
|
|
4
|
+
- **validation:** enhance backend validation documentation with detailed usage patterns and examples
|
|
5
|
+
|
|
6
|
+
### ⚙️ Engine Tuning
|
|
7
|
+
|
|
8
|
+
- **test:** restructure test suite into class-scoped directories and method-level atomic files
|
|
9
|
+
|
|
10
|
+
### ✨ What's New
|
|
11
|
+
|
|
12
|
+
- **database:** add debug logging for schema parsing failures in nanoid metadata loader
|
|
13
|
+
- **database:** introduce NanoID support for automatic ID generation in schema
|
|
14
|
+
- **release:** enhance commit analyzer with release rules and custom labels
|
|
15
|
+
- **shutdown:** implement graceful shutdown for IPC, Database, and Cron services
|
|
16
|
+
|
|
17
|
+
### 📚 Documentation
|
|
18
|
+
|
|
19
|
+
- **database:** remove underscore from nanoid example to reflect true alphanumeric output
|
|
20
|
+
|
|
21
|
+
### 🛠️ Fixes & Improvements
|
|
22
|
+
|
|
23
|
+
- **Auth:** handle token rotation for WebSocket connections and update active timestamp
|
|
24
|
+
- **core:** explicitly stop session GC interval during graceful shutdown
|
|
25
|
+
- **database:** namespace nanoid schema cache by connection to prevent table to prevent collisions
|
|
26
|
+
- **forms:** initialize ODAC form handlers on DOMContentLoaded and after AJAX navigation
|
|
27
|
+
- **manageSkills:** correct targetPath assignment for skill synchronization
|
|
28
|
+
- **Validator:** pass Odac instance to Validator for improved access to global methods
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
35
|
+
|
|
36
|
+
### doc
|
|
37
|
+
|
|
3
38
|
- enhance AI skills documentation with structured YAML front matter and detailed descriptions
|
|
4
39
|
|
|
5
40
|
### ⚙️ Engine Tuning
|
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
|
@@ -111,6 +111,12 @@ if (typeof window !== 'undefined') {
|
|
|
111
111
|
// In constructor we can't call this.data() easily if it uses 'this' for caching properly before init
|
|
112
112
|
// But based on original code logic:
|
|
113
113
|
this.#data = this.data()
|
|
114
|
+
|
|
115
|
+
if (document.readyState === 'loading') {
|
|
116
|
+
document.addEventListener('DOMContentLoaded', () => this.#initForms())
|
|
117
|
+
} else {
|
|
118
|
+
this.#initForms()
|
|
119
|
+
}
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
#ajax(options) {
|
|
@@ -795,6 +801,8 @@ if (typeof window !== 'undefined') {
|
|
|
795
801
|
}
|
|
796
802
|
|
|
797
803
|
#handleLoadComplete(data, callback) {
|
|
804
|
+
this.#initForms()
|
|
805
|
+
|
|
798
806
|
if (this.actions.load)
|
|
799
807
|
(Array.isArray(this.actions.load) ? this.actions.load : [this.actions.load]).forEach(fn => fn(this.page(), data.variables))
|
|
800
808
|
if (this.actions.page && this.actions.page[this.page()])
|
|
@@ -808,6 +816,30 @@ if (typeof window !== 'undefined') {
|
|
|
808
816
|
this.#isNavigating = false
|
|
809
817
|
}
|
|
810
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Scans the DOM for ODAC form components and registers submit handlers
|
|
821
|
+
* for any that haven't been initialized yet. Called on DOMContentLoaded
|
|
822
|
+
* and after every AJAX navigation to bind freshly rendered forms.
|
|
823
|
+
*/
|
|
824
|
+
#initForms() {
|
|
825
|
+
const formTypes = [
|
|
826
|
+
{cls: 'odac-register-form', attr: 'data-odac-register'},
|
|
827
|
+
{cls: 'odac-login-form', attr: 'data-odac-login'},
|
|
828
|
+
{cls: 'odac-magic-login-form', attr: 'data-odac-magic-login'},
|
|
829
|
+
{cls: 'odac-custom-form', attr: 'data-odac-form'}
|
|
830
|
+
]
|
|
831
|
+
|
|
832
|
+
for (const {cls, attr} of formTypes) {
|
|
833
|
+
document.querySelectorAll(`form.${cls}[${attr}]`).forEach(form => {
|
|
834
|
+
const token = form.getAttribute(attr)
|
|
835
|
+
const selector = `form[${attr}="${token}"]`
|
|
836
|
+
if (!this.#formSubmitHandlers.has(selector)) {
|
|
837
|
+
this.form({form: selector})
|
|
838
|
+
}
|
|
839
|
+
})
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
811
843
|
loader(selector, elements, callback) {
|
|
812
844
|
this.#loader.elements = elements
|
|
813
845
|
this.#loader.callback = callback
|
|
@@ -993,19 +1025,6 @@ if (typeof window !== 'undefined') {
|
|
|
993
1025
|
}
|
|
994
1026
|
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init()
|
|
995
1027
|
})()
|
|
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
1028
|
} else {
|
|
1010
1029
|
let socket = null
|
|
1011
1030
|
const ports = new Set()
|
|
@@ -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/
|
|
@@ -1,60 +1,160 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: backend-validation-skill
|
|
3
|
-
description:
|
|
3
|
+
description: Detailed ODAC Validator usage for request validation, security checks, brute-force protection, and consistent API responses.
|
|
4
4
|
metadata:
|
|
5
5
|
tags: backend, validation, fluent-api, input-security, brute-force, error-handling
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Backend Validation Skill
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Architectural Approach
|
|
13
|
-
Validation should happen as early as possible in the request lifecycle. The `Validator` service handles automatic error formatting and frontend integration.
|
|
10
|
+
ODAC validation should be centralized with the fluent `Validator` API and returned in framework-standard result format.
|
|
14
11
|
|
|
15
12
|
## Core Rules
|
|
16
|
-
1. **
|
|
17
|
-
2. **
|
|
18
|
-
3. **
|
|
19
|
-
4. **
|
|
13
|
+
1. **Create validator per request**: Use `const validator = Odac.validator()`.
|
|
14
|
+
2. **Fail fast**: Run all checks, then immediately return on `await validator.error()`.
|
|
15
|
+
3. **Field-first messages**: Assign a specific `.message(...)` per check chain.
|
|
16
|
+
4. **Use standard result shape**: Return `await validator.result('Validation failed')` for errors.
|
|
17
|
+
5. **Protect sensitive flows**: Add `await validator.brute(n)` on login/reset/auth endpoints.
|
|
18
|
+
|
|
19
|
+
## Minimal Flow
|
|
20
|
+
```javascript
|
|
21
|
+
module.exports = async Odac => {
|
|
22
|
+
const validator = Odac.validator()
|
|
23
|
+
|
|
24
|
+
validator.post('email').check('required|email').message('Valid email required')
|
|
25
|
+
validator.post('password').check('required|minlen:8').message('Password must be at least 8 characters')
|
|
26
|
+
|
|
27
|
+
if (await validator.error()) {
|
|
28
|
+
await validator.brute(5)
|
|
29
|
+
return await validator.result('Validation failed')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return await validator.success({ok: true})
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API Surface
|
|
37
|
+
- `post(key)`: Validate POST payload field.
|
|
38
|
+
- `get(key)`: Validate querystring field.
|
|
39
|
+
- `var(name, value)`: Validate computed/custom value.
|
|
40
|
+
- `file(name)`: Validate uploaded file object.
|
|
41
|
+
- `check(rules | boolean)`: Apply pipe rules or direct boolean validation.
|
|
42
|
+
- `message(text)`: Set message for the latest check on current field.
|
|
43
|
+
- `error()`: Runs validation and returns `true` if any error exists.
|
|
44
|
+
- `result(message?, data?)`: Returns ODAC-standard response object.
|
|
45
|
+
- `success(dataOrMessage?)`: Convenience wrapper for success payload.
|
|
46
|
+
- `brute(maxAttempts = 5)`: Tracks failed attempts per hour/page/ip.
|
|
47
|
+
|
|
48
|
+
## Rule Catalog
|
|
49
|
+
|
|
50
|
+
### Type & format rules
|
|
51
|
+
- `required`, `accepted`
|
|
52
|
+
- `numeric`, `float`
|
|
53
|
+
- `alpha`, `alphaspace`, `alphanumeric`, `alphanumericspace`, `username`
|
|
54
|
+
- `email`, `ip`, `mac`, `domain`, `url`
|
|
55
|
+
- `array`, `date`, `xss`
|
|
56
|
+
|
|
57
|
+
### Length & value rules
|
|
58
|
+
- `len:X`, `minlen:X`, `maxlen:X`
|
|
59
|
+
- `min:X`, `max:X`
|
|
60
|
+
- `equal:value`, `not:value`
|
|
61
|
+
- `same:field`, `different:field`
|
|
62
|
+
|
|
63
|
+
### String/date matching rules
|
|
64
|
+
- `in:substring`, `notin:substring`
|
|
65
|
+
- `regex:pattern`
|
|
66
|
+
- `mindate:YYYY-MM-DD`, `maxdate:YYYY-MM-DD`
|
|
67
|
+
|
|
68
|
+
### Auth/security rules
|
|
69
|
+
- `usercheck`: Must be authenticated.
|
|
70
|
+
- `user:field`: Input must match authenticated user field.
|
|
71
|
+
- `disposable`: Email must be disposable.
|
|
72
|
+
- `!disposable`: Email must not be disposable.
|
|
73
|
+
|
|
74
|
+
### Inverse rules
|
|
75
|
+
- Prefix any rule with `!` to invert: `!required`, `!email`, `!equal:admin`.
|
|
20
76
|
|
|
21
77
|
## Reference Patterns
|
|
22
78
|
|
|
23
|
-
### 1
|
|
79
|
+
### 1) Multi-check per field with specific errors
|
|
24
80
|
```javascript
|
|
25
|
-
module.exports = async
|
|
26
|
-
const validator = Odac.
|
|
81
|
+
module.exports = async Odac => {
|
|
82
|
+
const validator = Odac.validator()
|
|
27
83
|
|
|
28
84
|
validator
|
|
29
|
-
.post('
|
|
30
|
-
.
|
|
85
|
+
.post('password')
|
|
86
|
+
.check('required').message('Password is required')
|
|
87
|
+
.check('minlen:8').message('Minimum 8 characters')
|
|
88
|
+
.check('regex:[A-Z]').message('At least one uppercase letter')
|
|
89
|
+
.check('regex:[0-9]').message('At least one number')
|
|
31
90
|
|
|
32
91
|
if (await validator.error()) {
|
|
33
|
-
return validator.result('Please fix input errors')
|
|
92
|
+
return await validator.result('Please fix input errors')
|
|
34
93
|
}
|
|
35
94
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
95
|
+
return await validator.success('Success')
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2) GET + POST + VAR together
|
|
100
|
+
```javascript
|
|
101
|
+
module.exports = async Odac => {
|
|
102
|
+
const validator = Odac.validator()
|
|
103
|
+
const plan = await Odac.request('plan')
|
|
104
|
+
|
|
105
|
+
validator.get('page').check('numeric|min:1').message('Invalid page')
|
|
106
|
+
validator.post('email').check('required|email|!disposable').message('Corporate email required')
|
|
107
|
+
validator.var('plan', plan).check('in:pro').message('Only pro plan is allowed')
|
|
108
|
+
|
|
109
|
+
if (await validator.error()) return await validator.result('Validation failed')
|
|
110
|
+
return await validator.success({ok: true})
|
|
111
|
+
}
|
|
39
112
|
```
|
|
40
113
|
|
|
41
|
-
###
|
|
114
|
+
### 3) Boolean check for business rules
|
|
42
115
|
```javascript
|
|
43
|
-
|
|
116
|
+
module.exports = async Odac => {
|
|
117
|
+
const validator = Odac.validator()
|
|
118
|
+
const canPublish = await somePermissionCheck(Odac)
|
|
119
|
+
|
|
120
|
+
validator.post('title').check('required').message('Title required')
|
|
121
|
+
validator.var('permission', null).check(canPublish).message('No publish permission')
|
|
122
|
+
|
|
123
|
+
if (await validator.error()) return await validator.result('Validation failed')
|
|
124
|
+
return await validator.success('Published')
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 4) Brute-force on auth endpoint
|
|
129
|
+
```javascript
|
|
130
|
+
module.exports = async Odac => {
|
|
131
|
+
const validator = Odac.validator()
|
|
132
|
+
|
|
133
|
+
validator.post('email').check('required|email').message('Email required')
|
|
134
|
+
validator.post('password').check('required').message('Password required')
|
|
135
|
+
|
|
136
|
+
if (await validator.error()) {
|
|
137
|
+
await validator.brute(5)
|
|
138
|
+
return await validator.result('Login failed')
|
|
139
|
+
}
|
|
44
140
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
validator.var('auth', null).check('usercheck').message('Authentication required');
|
|
141
|
+
return await validator.success('OK')
|
|
142
|
+
}
|
|
48
143
|
```
|
|
49
144
|
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
145
|
+
## Response Contract
|
|
146
|
+
- **Success**:
|
|
147
|
+
- `result.success: true`
|
|
148
|
+
- optional `result.message`
|
|
149
|
+
- optional `data`
|
|
150
|
+
- **Failure**:
|
|
151
|
+
- `result.success: false`
|
|
152
|
+
- `errors.{field}` map
|
|
153
|
+
- global errors may use `errors._odac_form`
|
|
56
154
|
|
|
57
155
|
## Best Practices
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
156
|
+
- Keep validation at route/controller entry; do not defer to deep service layers.
|
|
157
|
+
- Use separate `check()` calls when you need rule-specific messages.
|
|
158
|
+
- Prefer `var()` for derived values instead of re-reading mutable request state.
|
|
159
|
+
- Use `xss` for text fields that can later be rendered in HTML.
|
|
160
|
+
- Always combine auth endpoints with `brute()` to reduce credential-stuffing risk.
|
|
@@ -1,28 +1,56 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frontend-forms-api-skill
|
|
3
|
-
description:
|
|
3
|
+
description: odac.js form submission patterns for parser-generated ODAC forms and predictable AJAX request handling.
|
|
4
4
|
metadata:
|
|
5
|
-
tags: frontend, forms, ajax,
|
|
5
|
+
tags: frontend, forms, ajax, odac-form, register, login, magic-login
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Frontend Forms & API Skill
|
|
9
9
|
|
|
10
|
-
Handling AJAX form submissions
|
|
10
|
+
Handling ODAC AJAX form submissions generated from server-side form parsing.
|
|
11
11
|
|
|
12
12
|
## Rules
|
|
13
|
-
1. **
|
|
14
|
-
2. **
|
|
15
|
-
3. **
|
|
13
|
+
1. **Bind by form selector**: Use `Odac.form({ form: 'selector' }, callback)` or short form `Odac.form('selector', callback)`.
|
|
14
|
+
2. **Leverage parser-generated forms**: `odac-register`, `odac-login`, `odac-magic-login`, and `odac-custom-form` are all auto-bound on page load and after every AJAX navigation.
|
|
15
|
+
3. **Expect JSON result shape**: Handle `result.success`, `result.message`, `result.redirect`, and `errors` in callback.
|
|
16
|
+
4. **Message/clear control**: Use `messages` and `clear` options for UX behavior.
|
|
16
17
|
|
|
17
18
|
## Patterns
|
|
18
19
|
```javascript
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
// 1) Bind a parsed custom form
|
|
21
|
+
Odac.form('form[data-odac-form]')
|
|
22
|
+
|
|
23
|
+
// 2) Bind parsed register/login forms explicitly (optional)
|
|
24
|
+
Odac.form('form[data-odac-register]')
|
|
25
|
+
Odac.form('form[data-odac-login]')
|
|
26
|
+
|
|
27
|
+
// 3) Bind parsed magic-login form manually
|
|
28
|
+
Odac.form('form[data-odac-magic-login]', response => {
|
|
29
|
+
if (response?.result?.success && !response.result.redirect) {
|
|
30
|
+
const info = document.querySelector('[data-status]')
|
|
31
|
+
if (info) info.textContent = response.result.message
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// 4) Advanced options: disable auto clear and hide success messages
|
|
36
|
+
Odac.form(
|
|
37
|
+
{form: 'form[data-odac-form]', clear: false, messages: ['error']},
|
|
38
|
+
response => {
|
|
39
|
+
if (response?.result?.success && response.result.redirect) {
|
|
40
|
+
window.location.href = response.result.redirect
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// 5) Manual GET request helper
|
|
46
|
+
Odac.get('/api/status', data => {
|
|
47
|
+
const status = document.querySelector('[data-api-status]')
|
|
48
|
+
if (status) status.textContent = String(data?.status ?? '')
|
|
49
|
+
})
|
|
28
50
|
```
|
|
51
|
+
|
|
52
|
+
## Response Handling Contract
|
|
53
|
+
- **Success**: `response.result.success === true`
|
|
54
|
+
- **Redirect**: `response.result.redirect` exists when server wants navigation
|
|
55
|
+
- **Form errors**: `response.errors.{fieldName}` maps to `odac-form-error="fieldName"`
|
|
56
|
+
- **Global form error**: `response.errors._odac_form`
|