odac 1.4.0 → 1.4.1
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 +3 -0
- package/.github/workflows/release.yml +1 -1
- package/CHANGELOG.md +26 -0
- package/README.md +10 -0
- package/bin/odac.js +190 -0
- 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 +18 -2
- package/docs/ai/skills/backend/forms.md +8 -1
- 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 +80 -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 +7 -0
- 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 +7 -0
- package/docs/ai/skills/frontend/navigation.md +7 -0
- package/docs/ai/skills/frontend/realtime.md +7 -0
- package/docs/backend/08-database/04-migrations.md +258 -37
- package/package.json +1 -1
- package/src/Auth.js +70 -44
- package/src/Config.js +1 -1
- package/src/Database/ConnectionFactory.js +69 -0
- package/src/Database/Migration.js +1203 -0
- package/src/Database.js +35 -35
- package/template/schema/users.js +23 -0
- package/test/Auth.test.js +64 -3
- package/test/Config.test.js +7 -0
- package/test/Database/ConnectionFactory.test.js +80 -0
- package/test/Migration.test.js +943 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-forms-api-skill
|
|
3
|
+
description: AJAX form and API request patterns in odac.js for interactive UX and predictable frontend data flows.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: frontend, forms, ajax, api-requests, odac-get, odac-post
|
|
6
|
+
---
|
|
7
|
+
|
|
1
8
|
# Frontend Forms & API Skill
|
|
2
9
|
|
|
3
10
|
Handling AJAX form submissions and API requests.
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-navigation-spa-skill
|
|
3
|
+
description: Single-page navigation patterns in odac.js for smooth transitions, route control, and lifecycle-safe execution.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: frontend, navigation, spa, ajax-navigation, page-lifecycle, transitions
|
|
6
|
+
---
|
|
7
|
+
|
|
1
8
|
# Frontend Navigation & SPA Skill
|
|
2
9
|
|
|
3
10
|
Smooth transitions and single-page application behavior using `odac.js`.
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-realtime-websocket-skill
|
|
3
|
+
description: Realtime frontend communication patterns in odac.js using shared WebSockets, SSE, and resilient reconnect behavior.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: frontend, realtime, websocket, sse, sharedworker, auto-reconnect
|
|
6
|
+
---
|
|
7
|
+
|
|
1
8
|
# Frontend Realtime & WebSocket Skill
|
|
2
9
|
|
|
3
10
|
Real-time bidirectional communication and server-sent events with high efficiency.
|
|
@@ -1,48 +1,269 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Schema-First Migrations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ODAC uses a **declarative, schema-first** approach to database migrations. Instead of writing sequential migration files, you define the **desired final state** of each table in a schema file. The engine automatically diffs the schema against your database and applies the necessary changes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **AI Agent Friendly:** A single schema file per table = instant understanding of the final database state. No need to scan hundreds of migration files.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### 1. Define Your Schema
|
|
12
|
+
|
|
13
|
+
Create a file in the `schema/` directory for each table:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// schema/users.js
|
|
17
|
+
'use strict'
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
columns: {
|
|
21
|
+
id: {type: 'increments'},
|
|
22
|
+
name: {type: 'string', length: 255, nullable: false},
|
|
23
|
+
email: {type: 'string', length: 255, nullable: false},
|
|
24
|
+
role: {type: 'enum', values: ['admin', 'user'], default: 'user'},
|
|
25
|
+
is_active: {type: 'boolean', default: true},
|
|
26
|
+
timestamps: {type: 'timestamps'}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
indexes: [
|
|
30
|
+
{columns: ['email'], unique: true},
|
|
31
|
+
{columns: ['role', 'is_active']}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Start Your App
|
|
37
|
+
|
|
38
|
+
Migrations run **automatically** when the application starts. No manual commands needed:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx odac dev # Development
|
|
42
|
+
npx odac start # Production
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
On startup, the engine detects `schema/users.js`, creates the table, and applies indexes — all before the server accepts traffic.
|
|
46
|
+
|
|
47
|
+
> **Zero-Config:** Just define the schema file and deploy. The framework handles the rest.
|
|
48
|
+
|
|
49
|
+
You can also run migrations manually via CLI for inspection or rollback:
|
|
50
|
+
|
|
51
|
+
### 3. Modify Your Schema
|
|
52
|
+
|
|
53
|
+
Simply edit the schema file. Add a column, remove a column, add an index — the engine handles the rest:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
// schema/users.js — added 'bio' column, removed 'is_active'
|
|
57
|
+
module.exports = {
|
|
58
|
+
columns: {
|
|
59
|
+
id: {type: 'increments'},
|
|
60
|
+
name: {type: 'string', length: 255, nullable: false},
|
|
61
|
+
email: {type: 'string', length: 255, nullable: false},
|
|
62
|
+
role: {type: 'enum', values: ['admin', 'user'], default: 'user'},
|
|
63
|
+
bio: {type: 'text', nullable: true},
|
|
64
|
+
timestamps: {type: 'timestamps'}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
indexes: [
|
|
68
|
+
{columns: ['email'], unique: true}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx odac migrate
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
[default]
|
|
79
|
+
+ ADD COLUMN users.bio
|
|
80
|
+
- DROP COLUMN users.is_active
|
|
81
|
+
- DROP INDEX users (role, is_active)
|
|
82
|
+
|
|
83
|
+
✅ 3 operation(s) completed.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Column Types
|
|
89
|
+
|
|
90
|
+
| Type | Usage | Options |
|
|
91
|
+
|------|-------|---------|
|
|
92
|
+
| `increments` | Auto-increment primary key | — |
|
|
93
|
+
| `bigIncrements` | Big auto-increment | — |
|
|
94
|
+
| `integer` | Integer | `unsigned` |
|
|
95
|
+
| `bigInteger` | Big integer | `unsigned` |
|
|
96
|
+
| `float` | Floating point | `precision`, `scale` |
|
|
97
|
+
| `decimal` | Exact decimal | `precision`, `scale` |
|
|
98
|
+
| `string` | Varchar | `length` (default: 255) |
|
|
99
|
+
| `text` | Text blob | `textType` ('text', 'mediumtext', 'longtext') |
|
|
100
|
+
| `boolean` | Boolean | — |
|
|
101
|
+
| `date` | Date only | — |
|
|
102
|
+
| `datetime` | Date and time | — |
|
|
103
|
+
| `timestamp` | Timestamp | — |
|
|
104
|
+
| `timestamps` | Virtual: creates `created_at` + `updated_at` | — |
|
|
105
|
+
| `time` | Time only | — |
|
|
106
|
+
| `binary` | Binary data | `length` |
|
|
107
|
+
| `json` | JSON | — |
|
|
108
|
+
| `jsonb` | Binary JSON (PostgreSQL) | — |
|
|
109
|
+
| `uuid` | UUID | — |
|
|
110
|
+
| `enum` | Enumeration | `values` (array) |
|
|
111
|
+
|
|
112
|
+
## Column Modifiers
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
{
|
|
116
|
+
type: 'string',
|
|
117
|
+
length: 100,
|
|
118
|
+
nullable: false, // NOT NULL constraint
|
|
119
|
+
default: 'untitled', // Default value
|
|
120
|
+
unsigned: true, // Unsigned integer
|
|
121
|
+
unique: true, // Unique constraint (inline)
|
|
122
|
+
primary: true, // Primary key
|
|
123
|
+
comment: 'The title', // Column comment
|
|
124
|
+
references: { // Foreign key
|
|
125
|
+
table: 'categories',
|
|
126
|
+
column: 'id'
|
|
127
|
+
},
|
|
128
|
+
onDelete: 'CASCADE',
|
|
129
|
+
onUpdate: 'CASCADE'
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Seed Data
|
|
136
|
+
|
|
137
|
+
Schema files can include declarative seed data that is applied idempotently on every migration:
|
|
10
138
|
|
|
11
139
|
```javascript
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
140
|
+
// schema/roles.js
|
|
141
|
+
module.exports = {
|
|
142
|
+
columns: {
|
|
143
|
+
id: {type: 'increments'},
|
|
144
|
+
name: {type: 'string', length: 50},
|
|
145
|
+
level: {type: 'integer', default: 0}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
indexes: [],
|
|
149
|
+
|
|
150
|
+
seed: [
|
|
151
|
+
{name: 'admin', level: 100},
|
|
152
|
+
{name: 'editor', level: 50},
|
|
153
|
+
{name: 'user', level: 1}
|
|
154
|
+
],
|
|
155
|
+
seedKey: 'name'
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- **`seed`** — Array of rows to ensure exist
|
|
160
|
+
- **`seedKey`** — Column used for uniqueness check (required when `seed` is present)
|
|
161
|
+
|
|
162
|
+
**Behavior:** If the row exists (matched by `seedKey`), it updates if values differ. If not, it inserts. Safe to run repeatedly.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Data Migrations
|
|
167
|
+
|
|
168
|
+
For **one-time data transformations** (splitting columns, backfilling, etc.), use imperative migration files:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
migration/
|
|
172
|
+
20260225_001_split_names.js
|
|
173
|
+
```
|
|
35
174
|
|
|
36
175
|
```javascript
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
176
|
+
// migration/20260225_001_split_names.js
|
|
177
|
+
module.exports = {
|
|
178
|
+
async up(db) {
|
|
179
|
+
const users = await db('users').select('id', 'full_name')
|
|
180
|
+
for (const user of users) {
|
|
181
|
+
const [first, ...rest] = user.full_name.split(' ')
|
|
182
|
+
await db('users').where('id', user.id).update({
|
|
183
|
+
first_name: first,
|
|
184
|
+
last_name: rest.join(' ')
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async down(db) {
|
|
190
|
+
const users = await db('users').select('id', 'first_name', 'last_name')
|
|
191
|
+
for (const user of users) {
|
|
192
|
+
await db('users').where('id', user.id).update({
|
|
193
|
+
full_name: `${user.first_name} ${user.last_name}`
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Migration files run **once** and are tracked in the `_odac_migrations` table.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Multiple Databases
|
|
205
|
+
|
|
206
|
+
If your project has multiple database connections, organize schemas by connection:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
schema/
|
|
210
|
+
users.js ← default connection
|
|
211
|
+
posts.js ← default connection
|
|
212
|
+
analytics/ ← 'analytics' connection
|
|
213
|
+
events.js
|
|
214
|
+
pageviews.js
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The folder name matches the connection key in your `odac.json`:
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"database": {
|
|
222
|
+
"default": {"type": "mysql", "database": "main_db"},
|
|
223
|
+
"analytics": {"type": "postgres", "database": "analytics_db"}
|
|
224
|
+
}
|
|
44
225
|
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Migration files follow the same convention:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
migration/
|
|
232
|
+
20260225_001_auto.js ← default connection
|
|
233
|
+
analytics/
|
|
234
|
+
20260225_001_backfill.js ← analytics connection
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## CLI Commands
|
|
45
240
|
|
|
46
|
-
|
|
47
|
-
|
|
241
|
+
```bash
|
|
242
|
+
# Run all pending migrations (schema diff + files + seeds)
|
|
243
|
+
npx odac migrate
|
|
244
|
+
|
|
245
|
+
# Target a specific database connection
|
|
246
|
+
npx odac migrate --db=analytics
|
|
247
|
+
|
|
248
|
+
# Show pending changes without applying (dry-run)
|
|
249
|
+
npx odac migrate:status
|
|
250
|
+
|
|
251
|
+
# Rollback the last batch of migration files
|
|
252
|
+
npx odac migrate:rollback
|
|
253
|
+
|
|
254
|
+
# Reverse-engineer current database into schema/ files
|
|
255
|
+
npx odac migrate:snapshot
|
|
256
|
+
npx odac migrate:snapshot --db=analytics
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Snapshot — Importing Existing Databases
|
|
262
|
+
|
|
263
|
+
For existing projects, use `migrate:snapshot` to generate schema files from your current database:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
npx odac migrate:snapshot
|
|
48
267
|
```
|
|
268
|
+
|
|
269
|
+
This creates a schema file for each table. Review and adjust the generated files, then use them as your source of truth going forward.
|
package/package.json
CHANGED
package/src/Auth.js
CHANGED
|
@@ -140,30 +140,54 @@ class Auth {
|
|
|
140
140
|
this.#user = await Odac.DB[this.#table].where(primaryKey, sql_token[0].user).first()
|
|
141
141
|
if (!this.#user) return false
|
|
142
142
|
|
|
143
|
+
let triggerRotation = false
|
|
144
|
+
let isRecoveryRotation = false
|
|
145
|
+
|
|
143
146
|
if (!isRotated) {
|
|
144
147
|
if (shouldRotate && tokenAge > rotationAge) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
id
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
148
|
+
triggerRotation = true
|
|
149
|
+
} else if (inactiveAge > updateAge) {
|
|
150
|
+
// Fallback simple active update if rotation is not triggered
|
|
151
|
+
Odac.DB[tokenTable]
|
|
152
|
+
.where('id', sql_token[0].id)
|
|
153
|
+
.update({active: new Date()})
|
|
154
|
+
.catch(() => {})
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Client still presenting a rotated (grace period) token.
|
|
158
|
+
// This means the previous rotation response was lost (network hiccup, page navigation, etc.)
|
|
159
|
+
// Give the client one more chance by re-issuing new credentials.
|
|
160
|
+
const timeSinceRotation = inactiveAge - maxAge + TOKEN_ROTATION_GRACE_PERIOD_MS
|
|
161
|
+
if (timeSinceRotation > 5000) {
|
|
162
|
+
triggerRotation = true
|
|
163
|
+
isRecoveryRotation = true
|
|
164
|
+
}
|
|
165
|
+
}
|
|
158
166
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
if (triggerRotation) {
|
|
168
|
+
// --- Token Rotation ---
|
|
169
|
+
const newTokenX = nodeCrypto.randomBytes(32).toString('hex')
|
|
170
|
+
const newTokenY = nodeCrypto.randomBytes(32).toString('hex')
|
|
171
|
+
const newToken = {
|
|
172
|
+
id: Odac.DB.nanoid(),
|
|
173
|
+
user: sql_token[0].user,
|
|
174
|
+
token_x: newTokenX,
|
|
175
|
+
token_y: Odac.Var(newTokenY).hash(),
|
|
176
|
+
browser: sql_token[0].browser,
|
|
177
|
+
ip: this.#request.ip,
|
|
178
|
+
date: new Date(),
|
|
179
|
+
active: new Date()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 1. Persist new token (await to ensure it exists before client uses new cookies)
|
|
183
|
+
const insertOk = await Odac.DB[tokenTable].insert(newToken).catch(e => {
|
|
184
|
+
console.error('Odac Auth Error: Token rotation failed', e.message)
|
|
185
|
+
return false
|
|
186
|
+
})
|
|
164
187
|
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
if (insertOk !== false) {
|
|
189
|
+
if (!isRecoveryRotation) {
|
|
190
|
+
// 2a. Normal rotation: Mark old token as rotated with 60s grace period
|
|
167
191
|
// Non-blocking I/O (Fire & Forget) -> High Throughput
|
|
168
192
|
const rotatedActiveDate = new Date(now - maxAge + TOKEN_ROTATION_GRACE_PERIOD_MS)
|
|
169
193
|
const epochDate = new Date(0)
|
|
@@ -175,27 +199,29 @@ class Auth {
|
|
|
175
199
|
date: epochDate
|
|
176
200
|
})
|
|
177
201
|
.catch(() => {})
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.#request.cookie('odac_y', newTokenY, {
|
|
187
|
-
httpOnly: true,
|
|
188
|
-
secure: true,
|
|
189
|
-
sameSite: 'Lax',
|
|
190
|
-
maxAge: maxAge
|
|
191
|
-
})
|
|
202
|
+
} else {
|
|
203
|
+
// 2b. Recovery rotation: Delete old rotated token immediately.
|
|
204
|
+
// Why: Prevents unbounded token multiplication. The old token already
|
|
205
|
+
// had its grace period; one recovery attempt is the maximum.
|
|
206
|
+
Odac.DB[tokenTable]
|
|
207
|
+
.where('id', sql_token[0].id)
|
|
208
|
+
.delete()
|
|
209
|
+
.catch(() => {})
|
|
192
210
|
}
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
211
|
+
|
|
212
|
+
// 3. Issue new cookies immediately
|
|
213
|
+
this.#request.cookie('odac_x', newTokenX, {
|
|
214
|
+
httpOnly: true,
|
|
215
|
+
secure: true,
|
|
216
|
+
sameSite: 'Lax',
|
|
217
|
+
'max-age': Math.floor(maxAge / 1000)
|
|
218
|
+
})
|
|
219
|
+
this.#request.cookie('odac_y', newTokenY, {
|
|
220
|
+
httpOnly: true,
|
|
221
|
+
secure: true,
|
|
222
|
+
sameSite: 'Lax',
|
|
223
|
+
'max-age': Math.floor(maxAge / 1000)
|
|
224
|
+
})
|
|
199
225
|
}
|
|
200
226
|
}
|
|
201
227
|
|
|
@@ -238,13 +264,13 @@ class Auth {
|
|
|
238
264
|
httpOnly: true,
|
|
239
265
|
secure: true,
|
|
240
266
|
sameSite: 'Lax',
|
|
241
|
-
|
|
267
|
+
'max-age': Math.floor(maxAge / 1000)
|
|
242
268
|
})
|
|
243
269
|
this.#request.cookie('odac_y', token_y, {
|
|
244
270
|
httpOnly: true,
|
|
245
271
|
secure: true,
|
|
246
272
|
sameSite: 'Lax',
|
|
247
|
-
|
|
273
|
+
'max-age': Math.floor(maxAge / 1000)
|
|
248
274
|
})
|
|
249
275
|
|
|
250
276
|
// Knex insert returns ids on some dbs, promise resolves to result
|
|
@@ -392,8 +418,8 @@ class Auth {
|
|
|
392
418
|
await Odac.DB[tokenTable].where('user', userId).where('browser', browser).delete()
|
|
393
419
|
}
|
|
394
420
|
|
|
395
|
-
this.#request.cookie('odac_x', '', {
|
|
396
|
-
this.#request.cookie('odac_y', '', {
|
|
421
|
+
this.#request.cookie('odac_x', '', {'max-age': -1})
|
|
422
|
+
this.#request.cookie('odac_y', '', {'max-age': -1})
|
|
397
423
|
|
|
398
424
|
this.#user = null
|
|
399
425
|
return true
|
package/src/Config.js
CHANGED
|
@@ -46,7 +46,7 @@ module.exports = {
|
|
|
46
46
|
|
|
47
47
|
_interpolate: function (obj) {
|
|
48
48
|
if (typeof obj === 'string') {
|
|
49
|
-
return obj.replace(/\$\{(
|
|
49
|
+
return obj.replace(/\$\{([^{}]+)\}/g, (_, key) => {
|
|
50
50
|
// Special variables
|
|
51
51
|
if (key === 'odac') {
|
|
52
52
|
return __dirname.replace(/\/src$/, '/client')
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const knex = require('knex')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves knex client driver from ODAC database type.
|
|
7
|
+
* Why: Keeps connection driver mapping consistent across runtime and CLI migration paths.
|
|
8
|
+
* @param {string} type Database type from config.
|
|
9
|
+
* @returns {string} Knex client name.
|
|
10
|
+
*/
|
|
11
|
+
function resolveClient(type) {
|
|
12
|
+
if (type === 'postgres' || type === 'pg' || type === 'postgresql') return 'pg'
|
|
13
|
+
if (type === 'sqlite' || type === 'sqlite3') return 'sqlite3'
|
|
14
|
+
return 'mysql2'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds knex connection config from ODAC database node.
|
|
19
|
+
* Why: Normalizes connection options for all call sites and avoids drift.
|
|
20
|
+
* @param {object} db Single database config node.
|
|
21
|
+
* @param {string} client Knex client name.
|
|
22
|
+
* @returns {object} Knex connection object.
|
|
23
|
+
*/
|
|
24
|
+
function buildConnectionConfig(db, client) {
|
|
25
|
+
if (client === 'sqlite3') {
|
|
26
|
+
return {filename: db.filename || db.database || './dev.sqlite3'}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
host: db.host || '127.0.0.1',
|
|
31
|
+
user: db.user,
|
|
32
|
+
password: db.password,
|
|
33
|
+
database: db.database,
|
|
34
|
+
port: db.port
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates knex connections map from ODAC database config.
|
|
40
|
+
* Why: Centralizes zero-config connection bootstrap used by runtime and migration CLI.
|
|
41
|
+
* @param {object} databaseConfig ODAC database config (single or multiple).
|
|
42
|
+
* @returns {Record<string, any>} Knex connections by key.
|
|
43
|
+
*/
|
|
44
|
+
function buildConnections(databaseConfig) {
|
|
45
|
+
const isMultiple = typeof databaseConfig[Object.keys(databaseConfig)[0]] === 'object'
|
|
46
|
+
const dbs = isMultiple ? databaseConfig : {default: databaseConfig}
|
|
47
|
+
const connections = {}
|
|
48
|
+
|
|
49
|
+
for (const key of Object.keys(dbs)) {
|
|
50
|
+
const db = dbs[key]
|
|
51
|
+
const client = resolveClient(db.type)
|
|
52
|
+
const connection = buildConnectionConfig(db, client)
|
|
53
|
+
|
|
54
|
+
connections[key] = knex({
|
|
55
|
+
client,
|
|
56
|
+
connection,
|
|
57
|
+
pool: {min: 0, max: db.connectionLimit || 10},
|
|
58
|
+
useNullAsDefault: true
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return connections
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
buildConnections,
|
|
67
|
+
buildConnectionConfig,
|
|
68
|
+
resolveClient
|
|
69
|
+
}
|