odac 1.4.0 → 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 +8 -0
- package/.github/workflows/release.yml +1 -1
- package/.releaserc.js +9 -2
- package/CHANGELOG.md +61 -0
- package/README.md +10 -0
- package/bin/odac.js +193 -2
- package/client/odac.js +32 -13
- package/docs/ai/skills/SKILL.md +4 -3
- package/docs/ai/skills/backend/authentication.md +7 -0
- package/docs/ai/skills/backend/config.md +7 -0
- package/docs/ai/skills/backend/controllers.md +7 -0
- package/docs/ai/skills/backend/cron.md +9 -2
- package/docs/ai/skills/backend/database.md +37 -2
- package/docs/ai/skills/backend/forms.md +112 -11
- package/docs/ai/skills/backend/ipc.md +7 -0
- package/docs/ai/skills/backend/mail.md +7 -0
- package/docs/ai/skills/backend/migrations.md +86 -0
- package/docs/ai/skills/backend/request_response.md +7 -0
- package/docs/ai/skills/backend/routing.md +7 -0
- package/docs/ai/skills/backend/storage.md +7 -0
- package/docs/ai/skills/backend/streaming.md +7 -0
- package/docs/ai/skills/backend/structure.md +8 -1
- package/docs/ai/skills/backend/translations.md +7 -0
- package/docs/ai/skills/backend/utilities.md +7 -0
- package/docs/ai/skills/backend/validation.md +138 -31
- package/docs/ai/skills/backend/views.md +7 -0
- package/docs/ai/skills/frontend/core.md +7 -0
- package/docs/ai/skills/frontend/forms.md +48 -13
- package/docs/ai/skills/frontend/navigation.md +7 -0
- package/docs/ai/skills/frontend/realtime.md +7 -0
- package/docs/backend/08-database/02-basics.md +49 -9
- package/docs/backend/08-database/04-migrations.md +259 -37
- package/package.json +1 -1
- package/src/Auth.js +82 -43
- package/src/Config.js +1 -1
- package/src/Database/ConnectionFactory.js +70 -0
- package/src/Database/Migration.js +1228 -0
- package/src/Database/nanoid.js +30 -0
- package/src/Database.js +157 -46
- 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/template/schema/users.js +23 -0
- package/test/{Auth.test.js → Auth/check.test.js} +153 -6
- 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 -112
- package/test/Lang.test.js +0 -92
- 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
|
@@ -36,10 +36,18 @@ trigger: always_on
|
|
|
36
36
|
## Naming & Text Conventions
|
|
37
37
|
- **ODAC Casing:** Always write "ODAC" in uppercase letters when referring to the framework name in strings, comments, log messages, or user-facing text. **EXCEPTION:** The class name itself (`class Odac`) and variable references to it should remain `Odac` (PascalCase) as per code conventions.
|
|
38
38
|
|
|
39
|
+
## Documentation Standards
|
|
40
|
+
- **AI Skill Front Matter:** Every file under `docs/ai/skills/**/*.md` must start with YAML front matter containing `name`, `description`, and `metadata.tags`; values must be specific to that document's topic (never copied from generic examples).
|
|
41
|
+
|
|
39
42
|
## Testing & Validation
|
|
40
43
|
- **Mandatory Test Coverage:** Every new feature, method, or significant logic change MUST be accompanied by a corresponding unit or integration test.
|
|
41
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).
|
|
42
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.
|
|
43
51
|
|
|
44
52
|
## Client Library (odac.js)
|
|
45
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`.
|
|
@@ -62,6 +62,7 @@ jobs:
|
|
|
62
62
|
with:
|
|
63
63
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
64
64
|
michelangelo: ${{ secrets.MICHELANGELO }}
|
|
65
|
+
version: ${{ steps.version.outputs.version }}
|
|
65
66
|
timeout-minutes: 15
|
|
66
67
|
continue-on-error: true
|
|
67
68
|
|
|
@@ -96,7 +97,6 @@ jobs:
|
|
|
96
97
|
if: steps.ai_notes.outcome == 'success' && steps.version.outputs.version != ''
|
|
97
98
|
with:
|
|
98
99
|
tag_name: v${{ steps.version.outputs.version }}
|
|
99
|
-
name: ${{ steps.ai_notes.outputs.release-title }}
|
|
100
100
|
body_path: RELEASE_NOTE.md
|
|
101
101
|
draft: false
|
|
102
102
|
prerelease: false
|
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,64 @@
|
|
|
1
|
+
### doc
|
|
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
|
+
|
|
38
|
+
- enhance AI skills documentation with structured YAML front matter and detailed descriptions
|
|
39
|
+
|
|
40
|
+
### ⚙️ Engine Tuning
|
|
41
|
+
|
|
42
|
+
- **database:** centralize knex connection bootstrap for runtime and CLI
|
|
43
|
+
|
|
44
|
+
### 📚 Documentation
|
|
45
|
+
|
|
46
|
+
- add section for loading and updating AI skills in projects
|
|
47
|
+
|
|
48
|
+
### 🛠️ Fixes & Improvements
|
|
49
|
+
|
|
50
|
+
- **auth:** improve token rotation logic and ensure proper cookie attributes
|
|
51
|
+
- **cli:** parse .env values consistently in migration loader
|
|
52
|
+
- **config:** update interpolation regex to support variable names with hyphens
|
|
53
|
+
- **migration:** normalize column-level unique constraints and enhance idempotency in migrations
|
|
54
|
+
- **release:** add version output to release notes and update release title condition
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
61
|
+
|
|
1
62
|
### ⚙️ Engine Tuning
|
|
2
63
|
|
|
3
64
|
- Extract MIME type definitions into a dedicated module.
|
package/README.md
CHANGED
|
@@ -53,6 +53,16 @@ cd my-app
|
|
|
53
53
|
npm run dev
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## 🤖 AI Skills in Projects
|
|
57
|
+
|
|
58
|
+
Load or update ODAC skills from your project root with:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx odac skills
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This command syncs built-in skills to your selected AI tool folder and can be re-run anytime to update them.
|
|
65
|
+
|
|
56
66
|
## 📂 Project Structure
|
|
57
67
|
|
|
58
68
|
```
|
package/bin/odac.js
CHANGED
|
@@ -5,6 +5,8 @@ const path = require('node:path')
|
|
|
5
5
|
const readline = require('node:readline')
|
|
6
6
|
const {execSync, spawn} = require('node:child_process')
|
|
7
7
|
const cluster = require('node:cluster')
|
|
8
|
+
const Env = require('../src/Env')
|
|
9
|
+
const {buildConnections} = require('../src/Database/ConnectionFactory')
|
|
8
10
|
|
|
9
11
|
const command = process.argv[2]
|
|
10
12
|
const args = process.argv.slice(3)
|
|
@@ -208,7 +210,7 @@ async function manageSkills(targetDir = process.cwd()) {
|
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
const targetBase = path.resolve(targetDir, targetSubDir)
|
|
211
|
-
const targetPath = targetBase
|
|
213
|
+
const targetPath = copySkillsOnly ? path.join(targetBase, 'odac.js') : targetBase
|
|
212
214
|
|
|
213
215
|
try {
|
|
214
216
|
fs.mkdirSync(targetPath, {recursive: true})
|
|
@@ -220,13 +222,196 @@ async function manageSkills(targetDir = process.cwd()) {
|
|
|
220
222
|
fs.cpSync(aiSourceDir, targetPath, {recursive: true})
|
|
221
223
|
}
|
|
222
224
|
|
|
223
|
-
|
|
225
|
+
const finalSubDir = copySkillsOnly ? path.join(targetSubDir, 'odac.js') : targetSubDir
|
|
226
|
+
console.log(`\n✨ AI skills successfully synced to: \x1b[32m${finalSubDir}\x1b[0m`)
|
|
224
227
|
console.log('Your AI Agent now has full knowledge of the ODAC Framework. 🚀')
|
|
225
228
|
} catch (err) {
|
|
226
229
|
console.error('❌ Failed to sync AI skills:', err.message)
|
|
227
230
|
}
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Bootstraps the database and migration engine, then executes the requested migration command.
|
|
235
|
+
* Why: Migration commands need DB connections but not the full server stack.
|
|
236
|
+
* @param {string} cmd - The migration subcommand
|
|
237
|
+
* @param {string[]} cliArgs - CLI arguments (e.g. --db=analytics)
|
|
238
|
+
*/
|
|
239
|
+
async function runMigration(cmd, cliArgs) {
|
|
240
|
+
const projectDir = process.cwd()
|
|
241
|
+
const envPath = path.join(projectDir, '.env')
|
|
242
|
+
const configPath = path.join(projectDir, 'odac.json')
|
|
243
|
+
|
|
244
|
+
// Load .env
|
|
245
|
+
if (fs.existsSync(envPath)) {
|
|
246
|
+
const envContent = fs.readFileSync(envPath, 'utf8')
|
|
247
|
+
envContent.split('\n').forEach(line => {
|
|
248
|
+
line = line.trim()
|
|
249
|
+
if (!line || line.startsWith('#')) return
|
|
250
|
+
const idx = line.indexOf('=')
|
|
251
|
+
if (idx === -1) return
|
|
252
|
+
const key = line.slice(0, idx).trim()
|
|
253
|
+
const value = Env._parseValue(line.slice(idx + 1).trim())
|
|
254
|
+
if (process.env[key] === undefined) process.env[key] = value
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Load config
|
|
259
|
+
if (!fs.existsSync(configPath)) {
|
|
260
|
+
console.error('❌ No odac.json found in current directory.')
|
|
261
|
+
process.exit(1)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let config
|
|
265
|
+
try {
|
|
266
|
+
let raw = fs.readFileSync(configPath, 'utf8')
|
|
267
|
+
config = JSON.parse(raw)
|
|
268
|
+
// Interpolate env vars safely by traversing parsed object values.
|
|
269
|
+
const interpolateConfig = input => {
|
|
270
|
+
if (typeof input === 'string') {
|
|
271
|
+
return input.replace(/\$\{([^{}]+)\}/g, (_, key) => process.env[key] || '')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (Array.isArray(input)) {
|
|
275
|
+
return input.map(item => interpolateConfig(item))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (input && typeof input === 'object') {
|
|
279
|
+
const output = {}
|
|
280
|
+
for (const key of Object.keys(input)) {
|
|
281
|
+
output[key] = interpolateConfig(input[key])
|
|
282
|
+
}
|
|
283
|
+
return output
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return input
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
config = interpolateConfig(config)
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error('❌ Failed to parse odac.json:', err.message)
|
|
292
|
+
process.exit(1)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const dbConfig = config.database
|
|
296
|
+
if (!dbConfig) {
|
|
297
|
+
console.error('❌ No database configuration found in odac.json.')
|
|
298
|
+
process.exit(1)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const connections = buildConnections(dbConfig)
|
|
302
|
+
|
|
303
|
+
for (const key of Object.keys(connections)) {
|
|
304
|
+
try {
|
|
305
|
+
await connections[key].raw('SELECT 1')
|
|
306
|
+
} catch (e) {
|
|
307
|
+
console.error(`❌ Failed to connect to '${key}' database:`, e.message)
|
|
308
|
+
process.exit(1)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Parse --db flag
|
|
313
|
+
const dbFlag = cliArgs.find(a => a.startsWith('--db='))
|
|
314
|
+
const options = dbFlag ? {db: dbFlag.split('=')[1]} : {}
|
|
315
|
+
|
|
316
|
+
// Initialize migration engine
|
|
317
|
+
const Migration = require('../src/Database/Migration')
|
|
318
|
+
Migration.init(projectDir, connections)
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
if (cmd === 'migrate') {
|
|
322
|
+
console.log('🔄 Running migrations...\n')
|
|
323
|
+
const summary = await Migration.migrate(options)
|
|
324
|
+
printMigrationSummary(summary)
|
|
325
|
+
} else if (cmd === 'migrate:status') {
|
|
326
|
+
console.log('📋 Migration Status (dry-run):\n')
|
|
327
|
+
const summary = await Migration.status(options)
|
|
328
|
+
printMigrationSummary(summary)
|
|
329
|
+
} else if (cmd === 'migrate:rollback') {
|
|
330
|
+
console.log('⏪ Rolling back last batch...\n')
|
|
331
|
+
const summary = await Migration.rollback(options)
|
|
332
|
+
printMigrationSummary(summary)
|
|
333
|
+
} else if (cmd === 'migrate:snapshot') {
|
|
334
|
+
console.log('📸 Generating schema files from database...\n')
|
|
335
|
+
const result = await Migration.snapshot(options)
|
|
336
|
+
for (const [key, files] of Object.entries(result)) {
|
|
337
|
+
console.log(` \x1b[36m${key}\x1b[0m: ${files.length} schema file(s) generated`)
|
|
338
|
+
for (const f of files) {
|
|
339
|
+
console.log(` → ${path.relative(projectDir, f)}`)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
console.log('\n✅ Snapshot complete.')
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error('❌ Migration error:', err.message)
|
|
346
|
+
process.exit(1)
|
|
347
|
+
} finally {
|
|
348
|
+
for (const conn of Object.values(connections)) {
|
|
349
|
+
await conn.destroy()
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Formats and prints migration operation summary to stdout.
|
|
356
|
+
* @param {object} summary - Migration result per connection
|
|
357
|
+
*/
|
|
358
|
+
function printMigrationSummary(summary) {
|
|
359
|
+
let totalOps = 0
|
|
360
|
+
|
|
361
|
+
for (const [key, result] of Object.entries(summary)) {
|
|
362
|
+
console.log(` \x1b[36m[${key}]\x1b[0m`)
|
|
363
|
+
|
|
364
|
+
const allOps = [...(result.schema || []), ...(result.files || []), ...(result.seeds || [])]
|
|
365
|
+
|
|
366
|
+
if (allOps.length === 0) {
|
|
367
|
+
console.log(' Nothing to do.')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (const op of allOps) {
|
|
371
|
+
totalOps++
|
|
372
|
+
const label = formatOp(op)
|
|
373
|
+
console.log(` ${label}`)
|
|
374
|
+
}
|
|
375
|
+
console.log('')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.log(totalOps > 0 ? `✅ ${totalOps} operation(s) completed.` : '✅ Everything is up to date.')
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Formats a single migration operation for CLI display.
|
|
383
|
+
* @param {object} op - Operation descriptor
|
|
384
|
+
* @returns {string} Formatted label
|
|
385
|
+
*/
|
|
386
|
+
function formatOp(op) {
|
|
387
|
+
switch (op.type) {
|
|
388
|
+
case 'create_table':
|
|
389
|
+
return `\x1b[32m+ CREATE TABLE\x1b[0m ${op.table}`
|
|
390
|
+
case 'add_column':
|
|
391
|
+
return `\x1b[32m+ ADD COLUMN\x1b[0m ${op.table}.${op.column}`
|
|
392
|
+
case 'drop_column':
|
|
393
|
+
return `\x1b[31m- DROP COLUMN\x1b[0m ${op.table}.${op.column}`
|
|
394
|
+
case 'alter_column':
|
|
395
|
+
return `\x1b[33m~ ALTER COLUMN\x1b[0m ${op.table}.${op.column}`
|
|
396
|
+
case 'add_index':
|
|
397
|
+
return `\x1b[32m+ ADD INDEX\x1b[0m ${op.table} (${op.index.columns.join(', ')})`
|
|
398
|
+
case 'drop_index':
|
|
399
|
+
return `\x1b[31m- DROP INDEX\x1b[0m ${op.table} (${op.index.columns.join(', ')})`
|
|
400
|
+
case 'pending_file':
|
|
401
|
+
return `\x1b[33m⏳ PENDING\x1b[0m ${op.name}`
|
|
402
|
+
case 'applied_file':
|
|
403
|
+
return `\x1b[32m✓ APPLIED\x1b[0m ${op.name}`
|
|
404
|
+
case 'rolled_back':
|
|
405
|
+
return `\x1b[33m↩ ROLLED BACK\x1b[0m ${op.name}`
|
|
406
|
+
case 'seed_insert':
|
|
407
|
+
return `\x1b[32m+ SEED INSERT\x1b[0m ${op.table} (${op.key})`
|
|
408
|
+
case 'seed_update':
|
|
409
|
+
return `\x1b[33m~ SEED UPDATE\x1b[0m ${op.table} (${op.key})`
|
|
410
|
+
default:
|
|
411
|
+
return ` ${op.type}`
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
230
415
|
async function run() {
|
|
231
416
|
if (command === 'init') {
|
|
232
417
|
const projectName = args[0] || '.'
|
|
@@ -376,6 +561,8 @@ async function run() {
|
|
|
376
561
|
require('../index.js')
|
|
377
562
|
} else if (command === 'skills') {
|
|
378
563
|
await manageSkills()
|
|
564
|
+
} else if (command === 'migrate' || command === 'migrate:status' || command === 'migrate:rollback' || command === 'migrate:snapshot') {
|
|
565
|
+
await runMigration(command, args)
|
|
379
566
|
} else {
|
|
380
567
|
console.log('Usage:')
|
|
381
568
|
console.log(' npx odac init (Interactive mode)')
|
|
@@ -384,6 +571,10 @@ async function run() {
|
|
|
384
571
|
console.log(' npx odac build (Production build)')
|
|
385
572
|
console.log(' npx odac start (Start server)')
|
|
386
573
|
console.log(' npx odac skills (Sync AI Agent skills)')
|
|
574
|
+
console.log(' npx odac migrate (Run pending migrations)')
|
|
575
|
+
console.log(' npx odac migrate:status (Show pending changes)')
|
|
576
|
+
console.log(' npx odac migrate:rollback (Rollback last batch)')
|
|
577
|
+
console.log(' npx odac migrate:snapshot (Reverse-engineer DB to schema/)')
|
|
387
578
|
}
|
|
388
579
|
|
|
389
580
|
rl.close()
|
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()
|
package/docs/ai/skills/SKILL.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: odac-framework
|
|
3
|
-
description: Comprehensive AI
|
|
2
|
+
name: odac-framework-skill-catalog
|
|
3
|
+
description: Comprehensive ODAC backend and frontend AI skill index for architecture, security, and high-performance application development.
|
|
4
4
|
metadata:
|
|
5
|
-
tags:
|
|
5
|
+
tags: odac, skills, backend, frontend, architecture, security, performance
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## When to use
|
|
@@ -22,6 +22,7 @@ Read the specific rule files based on whether you are working on the Backend or
|
|
|
22
22
|
- [backend/forms.md](backend/forms.md) - Form processing and Validation logic
|
|
23
23
|
- [backend/ipc.md](backend/ipc.md) - Inter-Process Communication and state sharing
|
|
24
24
|
- [backend/mail.md](backend/mail.md) - Transactional email sending
|
|
25
|
+
- [backend/migrations.md](backend/migrations.md) - Schema-first, auto-run, cluster-safe DB migrations
|
|
25
26
|
- [backend/request_response.md](backend/request_response.md) - Handling Odac.Request and Odac.Response
|
|
26
27
|
- [backend/routing.md](backend/routing.md) - Route definitions, Middlewares, and Error Pages
|
|
27
28
|
- [backend/storage.md](backend/storage.md) - Persistent key-value storage (LMDB)
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-authentication-realtime-skill
|
|
3
|
+
description: Secure ODAC authentication patterns for sessions, guards, passwordless flows, and realtime channel protection.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: backend, authentication, session, auth-guard, magic-link, realtime, security
|
|
6
|
+
---
|
|
7
|
+
|
|
1
8
|
# Backend Authentication & Realtime Skill
|
|
2
9
|
|
|
3
10
|
Secure user authentication, session management, and bidirectional communication.
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-configuration-skill
|
|
3
|
+
description: ODAC configuration standards for odac.json usage, environment variable mapping, and secure runtime settings.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: backend, configuration, odac-json, environment, secrets, settings
|
|
6
|
+
---
|
|
7
|
+
|
|
1
8
|
# Backend Configuration Skill
|
|
2
9
|
|
|
3
10
|
Managing application settings using `odac.json` and environment variables.
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-controllers-skill
|
|
3
|
+
description: Best practices for ODAC controller architecture, class-based routing, and clean request handling boundaries.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: backend, controllers, class-based, route-mapping, architecture, maintainability
|
|
6
|
+
---
|
|
7
|
+
|
|
1
8
|
# Backend Controllers Skill
|
|
2
9
|
|
|
3
10
|
Controllers are the bridge between routes and views/services. This skill covers how to write clean, professional controllers in ODAC.
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-cron-jobs-skill
|
|
3
|
+
description: Reliable ODAC cron scheduling patterns for background automation, timing control, and safe execution design.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: backend, cron, scheduler, background-jobs, automation, timing
|
|
6
|
+
---
|
|
7
|
+
|
|
1
8
|
# Backend Cron Jobs Skill
|
|
2
9
|
|
|
3
10
|
ODAC provides a built-in cron system for automating background tasks without external dependencies.
|
|
@@ -21,14 +28,14 @@ Odac.Route.cron('cleanup').at('03:00'); // Runs daily at 03:00
|
|
|
21
28
|
// controller/cron/cleanup.js
|
|
22
29
|
module.exports = async (Odac) => {
|
|
23
30
|
console.log('Running nightly cleanup...');
|
|
24
|
-
await Odac.
|
|
31
|
+
await Odac.DB.table('logs').where('created_at', '<', 'NOW() - INTERVAL 30 DAY').delete();
|
|
25
32
|
};
|
|
26
33
|
```
|
|
27
34
|
|
|
28
35
|
### 2. Inline Function Cron
|
|
29
36
|
```javascript
|
|
30
37
|
Odac.Route.cron(async () => {
|
|
31
|
-
const stats = await Odac.
|
|
38
|
+
const stats = await Odac.DB.table('orders').count();
|
|
32
39
|
console.log('Current orders:', stats);
|
|
33
40
|
}).everyHour(1);
|
|
34
41
|
```
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-database-skill
|
|
3
|
+
description: High-performance ODAC database querying patterns using the built-in query builder with secure and efficient data access.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: backend, database, query-builder, sql, indexing, performance, security
|
|
6
|
+
---
|
|
7
|
+
|
|
1
8
|
# Backend Database Skill
|
|
2
9
|
|
|
3
10
|
High-performance database operations using the ODAC Query Builder.
|
|
@@ -9,13 +16,41 @@ High-performance database operations using the ODAC Query Builder.
|
|
|
9
16
|
|
|
10
17
|
## Patterns
|
|
11
18
|
```javascript
|
|
12
|
-
const user = await Odac.
|
|
19
|
+
const user = await Odac.DB.table('users')
|
|
13
20
|
.select('id', 'name', 'email')
|
|
14
21
|
.where('status', 'active')
|
|
15
22
|
.first();
|
|
16
23
|
|
|
17
|
-
await Odac.
|
|
24
|
+
await Odac.DB.table('posts').insert({
|
|
18
25
|
title: 'Hello',
|
|
19
26
|
user_id: 1
|
|
20
27
|
});
|
|
21
28
|
```
|
|
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
|
+
|
|
49
|
+
## Migration Awareness
|
|
50
|
+
1. **Schema-First**: Structural DB changes must be defined in `schema/*.js`.
|
|
51
|
+
2. **Auto-Migrate**: Migrations run automatically at startup from `Database.init()`.
|
|
52
|
+
3. **Cluster-Safe**: Migration execution is limited to primary process (`cluster.isPrimary`).
|
|
53
|
+
4. **Indexes**: Keep index definitions in schema so add/drop is managed automatically.
|
|
54
|
+
5. **Data Changes**: Use `migration/*.js` only for one-time data transformation.
|
|
55
|
+
|
|
56
|
+
See: [migrations.md](./migrations.md)
|