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.
Files changed (96) hide show
  1. package/.agent/rules/memory.md +8 -0
  2. package/.github/workflows/release.yml +1 -1
  3. package/.releaserc.js +9 -2
  4. package/CHANGELOG.md +61 -0
  5. package/README.md +10 -0
  6. package/bin/odac.js +193 -2
  7. package/client/odac.js +32 -13
  8. package/docs/ai/skills/SKILL.md +4 -3
  9. package/docs/ai/skills/backend/authentication.md +7 -0
  10. package/docs/ai/skills/backend/config.md +7 -0
  11. package/docs/ai/skills/backend/controllers.md +7 -0
  12. package/docs/ai/skills/backend/cron.md +9 -2
  13. package/docs/ai/skills/backend/database.md +37 -2
  14. package/docs/ai/skills/backend/forms.md +112 -11
  15. package/docs/ai/skills/backend/ipc.md +7 -0
  16. package/docs/ai/skills/backend/mail.md +7 -0
  17. package/docs/ai/skills/backend/migrations.md +86 -0
  18. package/docs/ai/skills/backend/request_response.md +7 -0
  19. package/docs/ai/skills/backend/routing.md +7 -0
  20. package/docs/ai/skills/backend/storage.md +7 -0
  21. package/docs/ai/skills/backend/streaming.md +7 -0
  22. package/docs/ai/skills/backend/structure.md +8 -1
  23. package/docs/ai/skills/backend/translations.md +7 -0
  24. package/docs/ai/skills/backend/utilities.md +7 -0
  25. package/docs/ai/skills/backend/validation.md +138 -31
  26. package/docs/ai/skills/backend/views.md +7 -0
  27. package/docs/ai/skills/frontend/core.md +7 -0
  28. package/docs/ai/skills/frontend/forms.md +48 -13
  29. package/docs/ai/skills/frontend/navigation.md +7 -0
  30. package/docs/ai/skills/frontend/realtime.md +7 -0
  31. package/docs/backend/08-database/02-basics.md +49 -9
  32. package/docs/backend/08-database/04-migrations.md +259 -37
  33. package/package.json +1 -1
  34. package/src/Auth.js +82 -43
  35. package/src/Config.js +1 -1
  36. package/src/Database/ConnectionFactory.js +70 -0
  37. package/src/Database/Migration.js +1228 -0
  38. package/src/Database/nanoid.js +30 -0
  39. package/src/Database.js +157 -46
  40. package/src/Ipc.js +37 -0
  41. package/src/Odac.js +1 -1
  42. package/src/Route/Cron.js +11 -0
  43. package/src/Route.js +8 -0
  44. package/src/Server.js +77 -23
  45. package/src/Storage.js +15 -1
  46. package/src/Validator.js +22 -20
  47. package/template/schema/users.js +23 -0
  48. package/test/{Auth.test.js → Auth/check.test.js} +153 -6
  49. package/test/Client/data.test.js +91 -0
  50. package/test/Client/get.test.js +90 -0
  51. package/test/Client/storage.test.js +87 -0
  52. package/test/Client/token.test.js +82 -0
  53. package/test/Client/ws.test.js +86 -0
  54. package/test/Config/deepMerge.test.js +14 -0
  55. package/test/Config/init.test.js +66 -0
  56. package/test/Config/interpolate.test.js +35 -0
  57. package/test/Database/ConnectionFactory/buildConnectionConfig.test.js +13 -0
  58. package/test/Database/ConnectionFactory/buildConnections.test.js +31 -0
  59. package/test/Database/ConnectionFactory/resolveClient.test.js +12 -0
  60. package/test/Database/Migration/migrate_column.test.js +52 -0
  61. package/test/Database/Migration/migrate_files.test.js +70 -0
  62. package/test/Database/Migration/migrate_index.test.js +89 -0
  63. package/test/Database/Migration/migrate_nanoid.test.js +160 -0
  64. package/test/Database/Migration/migrate_seed.test.js +77 -0
  65. package/test/Database/Migration/migrate_table.test.js +88 -0
  66. package/test/Database/Migration/rollback.test.js +61 -0
  67. package/test/Database/Migration/snapshot.test.js +38 -0
  68. package/test/Database/Migration/status.test.js +41 -0
  69. package/test/Database/autoNanoid.test.js +215 -0
  70. package/test/Database/nanoid.test.js +19 -0
  71. package/test/Lang/constructor.test.js +25 -0
  72. package/test/Lang/get.test.js +65 -0
  73. package/test/Lang/set.test.js +49 -0
  74. package/test/Odac/init.test.js +42 -0
  75. package/test/Odac/instance.test.js +58 -0
  76. package/test/Route/{Middleware.test.js → Middleware/chaining.test.js} +5 -29
  77. package/test/Route/Middleware/use.test.js +35 -0
  78. package/test/{Route.test.js → Route/check.test.js} +4 -55
  79. package/test/Route/set.test.js +52 -0
  80. package/test/Route/ws.test.js +23 -0
  81. package/test/View/EarlyHints/cache.test.js +32 -0
  82. package/test/View/EarlyHints/extractFromHtml.test.js +143 -0
  83. package/test/View/EarlyHints/formatLinkHeader.test.js +33 -0
  84. package/test/View/EarlyHints/send.test.js +99 -0
  85. package/test/View/{Form.test.js → Form/generateFieldHtml.test.js} +2 -2
  86. package/test/View/constructor.test.js +22 -0
  87. package/test/View/print.test.js +19 -0
  88. package/test/WebSocket/Client/limits.test.js +55 -0
  89. package/test/WebSocket/Server/broadcast.test.js +33 -0
  90. package/test/WebSocket/Server/route.test.js +37 -0
  91. package/test/Client.test.js +0 -197
  92. package/test/Config.test.js +0 -112
  93. package/test/Lang.test.js +0 -92
  94. package/test/Odac.test.js +0 -88
  95. package/test/View/EarlyHints.test.js +0 -282
  96. package/test/WebSocket.test.js +0 -238
@@ -118,19 +118,59 @@ await Odac.DB.users.where('id', 1).delete();
118
118
 
119
119
  ODAC includes a built-in helper for generating robust, unique string IDs (NanoID) without needing external packages. Secure, URL-friendly, and collision-resistant.
120
120
 
121
+ ### Automatic Generation (Recommended)
122
+
123
+ When you define a column as `type: 'nanoid'` in your schema file, ODAC **automatically generates** the ID on every `insert()` — no manual code needed.
124
+
125
+ **Schema definition:**
121
126
  ```javascript
122
- // Generate a standard 21-character ID (e.g., "V1StGXR8_Z5jdHi6B-myT")
123
- const id = Odac.DB.nanoid();
127
+ // schema/posts.js
128
+ module.exports = {
129
+ columns: {
130
+ id: { type: 'nanoid', primary: true },
131
+ title: { type: 'string', length: 255 }
132
+ }
133
+ }
134
+ ```
124
135
 
125
- // Generate a custom length ID
126
- const shortId = Odac.DB.nanoid(10);
136
+ **Usage just insert, the ID is auto-generated:**
137
+ ```javascript
138
+ await Odac.DB.posts.insert({ title: 'My First Post' });
139
+ // → { id: 'V1StGXR8Z5jdHi6BmyTa', title: 'My First Post' }
127
140
  ```
128
141
 
129
- This is particularly useful when inserting records into tables that use string-based Primary Keys instead of auto-increment integers.
142
+ This works for single inserts and bulk inserts. If you provide an `id` explicitly, the auto-generation is skipped.
130
143
 
131
144
  ```javascript
132
- await Odac.DB.posts.insert({
133
- id: Odac.DB.nanoid(),
134
- title: 'My First Post'
135
- });
145
+ // Bulk insert — each row gets its own unique nanoid
146
+ await Odac.DB.posts.insert([
147
+ { title: 'Post A' },
148
+ { title: 'Post B' }
149
+ ]);
150
+
151
+ // Explicit ID — auto-generation is skipped
152
+ await Odac.DB.posts.insert({ id: 'my-custom-id', title: 'Custom' });
153
+ ```
154
+
155
+ You can also customize the ID length:
156
+ ```javascript
157
+ // schema/codes.js
158
+ module.exports = {
159
+ columns: {
160
+ code: { type: 'nanoid', length: 8, primary: true },
161
+ label: { type: 'string' }
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### Manual Generation
167
+
168
+ You can also generate NanoIDs manually when needed:
169
+
170
+ ```javascript
171
+ // Generate a standard 21-character ID
172
+ const id = Odac.DB.nanoid();
173
+
174
+ // Generate a custom length ID
175
+ const shortId = Odac.DB.nanoid(10);
136
176
  ```
@@ -1,48 +1,270 @@
1
- # Code-First Migrations
1
+ # Schema-First Migrations
2
2
 
3
- Migration files are great, but sometimes (especially in rapid development or zero-config apps) you want dependencies to define their own table structures automatically.
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
- ODAC uses the `.schema()` helper for this logic.
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
- ## Ensuring Tables Exist
7
+ ---
8
8
 
9
- The `.schema()` method checks if a table exists. If it **does not exist**, it runs the provided callback to create it. If it **already exists**, it does nothing.
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
+ | `nanoid` | NanoID string key (auto-generated on insert) | `length` (default: 21) |
95
+ | `integer` | Integer | `unsigned` |
96
+ | `bigInteger` | Big integer | `unsigned` |
97
+ | `float` | Floating point | `precision`, `scale` |
98
+ | `decimal` | Exact decimal | `precision`, `scale` |
99
+ | `string` | Varchar | `length` (default: 255) |
100
+ | `text` | Text blob | `textType` ('text', 'mediumtext', 'longtext') |
101
+ | `boolean` | Boolean | — |
102
+ | `date` | Date only | — |
103
+ | `datetime` | Date and time | — |
104
+ | `timestamp` | Timestamp | — |
105
+ | `timestamps` | Virtual: creates `created_at` + `updated_at` | — |
106
+ | `time` | Time only | — |
107
+ | `binary` | Binary data | `length` |
108
+ | `json` | JSON | — |
109
+ | `jsonb` | Binary JSON (PostgreSQL) | — |
110
+ | `uuid` | UUID | — |
111
+ | `enum` | Enumeration | `values` (array) |
112
+
113
+ ## Column Modifiers
114
+
115
+ ```javascript
116
+ {
117
+ type: 'string',
118
+ length: 100,
119
+ nullable: false, // NOT NULL constraint
120
+ default: 'untitled', // Default value
121
+ unsigned: true, // Unsigned integer
122
+ unique: true, // Unique constraint (inline)
123
+ primary: true, // Primary key
124
+ comment: 'The title', // Column comment
125
+ references: { // Foreign key
126
+ table: 'categories',
127
+ column: 'id'
128
+ },
129
+ onDelete: 'CASCADE',
130
+ onUpdate: 'CASCADE'
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Seed Data
137
+
138
+ Schema files can include declarative seed data that is applied idempotently on every migration:
10
139
 
11
140
  ```javascript
12
- // Ensure 'products' table exists on the fly
13
- await Odac.DB.products.schema(t => {
14
- t.increments('id');
15
- t.string('name').notNullable();
16
- t.decimal('price', 10, 2);
17
- t.boolean('is_active').defaultTo(true);
18
-
19
- // Automatic timestamps (created_at, updated_at)
20
- t.timestamps(true, true);
21
- });
22
- ```
23
-
24
- The `t` argument is a Schema Builder. You can define columns using standard types like:
25
- - `t.string()`
26
- - `t.integer()`
27
- - `t.boolean()`
28
- - `t.text()`
29
- - `t.date()`
30
- - `t.json()`
31
-
32
- ## Usage Example
33
-
34
- A typical pattern is to define schemas in your module's initialization or before the first insert.
141
+ // schema/roles.js
142
+ module.exports = {
143
+ columns: {
144
+ id: {type: 'increments'},
145
+ name: {type: 'string', length: 50},
146
+ level: {type: 'integer', default: 0}
147
+ },
148
+
149
+ indexes: [],
150
+
151
+ seed: [
152
+ {name: 'admin', level: 100},
153
+ {name: 'editor', level: 50},
154
+ {name: 'user', level: 1}
155
+ ],
156
+ seedKey: 'name'
157
+ }
158
+ ```
159
+
160
+ - **`seed`** — Array of rows to ensure exist
161
+ - **`seedKey`** — Column used for uniqueness check (required when `seed` is present)
162
+
163
+ **Behavior:** If the row exists (matched by `seedKey`), it updates if values differ. If not, it inserts. Safe to run repeatedly.
164
+
165
+ ---
166
+
167
+ ## Data Migrations
168
+
169
+ For **one-time data transformations** (splitting columns, backfilling, etc.), use imperative migration files:
170
+
171
+ ```
172
+ migration/
173
+ 20260225_001_split_names.js
174
+ ```
35
175
 
36
176
  ```javascript
37
- // In your controller or module
38
- async function init() {
39
- await Odac.DB.logs.schema(t => {
40
- t.string('level');
41
- t.text('message');
42
- t.timestamps();
43
- });
177
+ // migration/20260225_001_split_names.js
178
+ module.exports = {
179
+ async up(db) {
180
+ const users = await db('users').select('id', 'full_name')
181
+ for (const user of users) {
182
+ const [first, ...rest] = user.full_name.split(' ')
183
+ await db('users').where('id', user.id).update({
184
+ first_name: first,
185
+ last_name: rest.join(' ')
186
+ })
187
+ }
188
+ },
189
+
190
+ async down(db) {
191
+ const users = await db('users').select('id', 'first_name', 'last_name')
192
+ for (const user of users) {
193
+ await db('users').where('id', user.id).update({
194
+ full_name: `${user.first_name} ${user.last_name}`
195
+ })
196
+ }
197
+ }
198
+ }
199
+ ```
200
+
201
+ Migration files run **once** and are tracked in the `_odac_migrations` table.
202
+
203
+ ---
204
+
205
+ ## Multiple Databases
206
+
207
+ If your project has multiple database connections, organize schemas by connection:
208
+
209
+ ```
210
+ schema/
211
+ users.js ← default connection
212
+ posts.js ← default connection
213
+ analytics/ ← 'analytics' connection
214
+ events.js
215
+ pageviews.js
216
+ ```
217
+
218
+ The folder name matches the connection key in your `odac.json`:
219
+
220
+ ```json
221
+ {
222
+ "database": {
223
+ "default": {"type": "mysql", "database": "main_db"},
224
+ "analytics": {"type": "postgres", "database": "analytics_db"}
225
+ }
44
226
  }
227
+ ```
228
+
229
+ Migration files follow the same convention:
230
+
231
+ ```
232
+ migration/
233
+ 20260225_001_auto.js ← default connection
234
+ analytics/
235
+ 20260225_001_backfill.js ← analytics connection
236
+ ```
237
+
238
+ ---
239
+
240
+ ## CLI Commands
45
241
 
46
- // Later...
47
- await Odac.DB.logs.insert({ level: 'info', message: 'App started' });
242
+ ```bash
243
+ # Run all pending migrations (schema diff + files + seeds)
244
+ npx odac migrate
245
+
246
+ # Target a specific database connection
247
+ npx odac migrate --db=analytics
248
+
249
+ # Show pending changes without applying (dry-run)
250
+ npx odac migrate:status
251
+
252
+ # Rollback the last batch of migration files
253
+ npx odac migrate:rollback
254
+
255
+ # Reverse-engineer current database into schema/ files
256
+ npx odac migrate:snapshot
257
+ npx odac migrate:snapshot --db=analytics
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Snapshot — Importing Existing Databases
263
+
264
+ For existing projects, use `migrate:snapshot` to generate schema files from your current database:
265
+
266
+ ```bash
267
+ npx odac migrate:snapshot
48
268
  ```
269
+
270
+ 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
@@ -7,7 +7,7 @@
7
7
  "email": "mail@emre.red",
8
8
  "url": "https://emre.red"
9
9
  },
10
- "version": "1.4.0",
10
+ "version": "1.4.2",
11
11
  "license": "MIT",
12
12
  "engines": {
13
13
  "node": ">=18.0.0"
package/src/Auth.js CHANGED
@@ -140,30 +140,67 @@ 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
+
146
+ // WebSocket connections (res === null) cannot deliver Set-Cookie headers.
147
+ // Rotating a token during a WS upgrade would invalidate the browser's cookies
148
+ // with no way to deliver replacements, causing silent logout on the next HTTP request.
149
+ const canDeliverCookies = !!this.#request.res
150
+
143
151
  if (!isRotated) {
144
152
  if (shouldRotate && tokenAge > rotationAge) {
145
- // --- Token Rotation ---
146
- const newTokenX = nodeCrypto.randomBytes(32).toString('hex')
147
- const newTokenY = nodeCrypto.randomBytes(32).toString('hex')
148
- const newToken = {
149
- id: Odac.DB.nanoid(),
150
- user: sql_token[0].user,
151
- token_x: newTokenX,
152
- token_y: Odac.Var(newTokenY).hash(),
153
- browser: sql_token[0].browser,
154
- ip: this.#request.ip,
155
- date: new Date(),
156
- active: new Date()
153
+ if (canDeliverCookies) {
154
+ triggerRotation = true
155
+ } else {
156
+ // WebSocket: Can't deliver rotated cookies, refresh active timestamp instead
157
+ Odac.DB[tokenTable]
158
+ .where('id', sql_token[0].id)
159
+ .update({active: new Date()})
160
+ .catch(() => {})
157
161
  }
162
+ } else if (inactiveAge > updateAge) {
163
+ // Fallback simple active update if rotation is not triggered
164
+ Odac.DB[tokenTable]
165
+ .where('id', sql_token[0].id)
166
+ .update({active: new Date()})
167
+ .catch(() => {})
168
+ }
169
+ } else {
170
+ // Client still presenting a rotated (grace period) token.
171
+ // This means the previous rotation response was lost (network hiccup, page navigation, etc.)
172
+ // Give the client one more chance by re-issuing new credentials.
173
+ const timeSinceRotation = inactiveAge - maxAge + TOKEN_ROTATION_GRACE_PERIOD_MS
174
+ if (timeSinceRotation > 5000 && canDeliverCookies) {
175
+ triggerRotation = true
176
+ isRecoveryRotation = true
177
+ }
178
+ }
158
179
 
159
- // 1. Persist new token (await to ensure it exists before client uses new cookies)
160
- const insertOk = await Odac.DB[tokenTable].insert(newToken).catch(e => {
161
- console.error('Odac Auth Error: Token rotation failed', e.message)
162
- return false
163
- })
180
+ if (triggerRotation) {
181
+ // --- Token Rotation ---
182
+ const newTokenX = nodeCrypto.randomBytes(32).toString('hex')
183
+ const newTokenY = nodeCrypto.randomBytes(32).toString('hex')
184
+ const newToken = {
185
+ id: Odac.DB.nanoid(),
186
+ user: sql_token[0].user,
187
+ token_x: newTokenX,
188
+ token_y: Odac.Var(newTokenY).hash(),
189
+ browser: sql_token[0].browser,
190
+ ip: this.#request.ip,
191
+ date: new Date(),
192
+ active: new Date()
193
+ }
164
194
 
165
- if (insertOk !== false) {
166
- // 2. Mark old token as rotated and set exactly 60 seconds grace period
195
+ // 1. Persist new token (await to ensure it exists before client uses new cookies)
196
+ const insertOk = await Odac.DB[tokenTable].insert(newToken).catch(e => {
197
+ console.error('Odac Auth Error: Token rotation failed', e.message)
198
+ return false
199
+ })
200
+
201
+ if (insertOk !== false) {
202
+ if (!isRecoveryRotation) {
203
+ // 2a. Normal rotation: Mark old token as rotated with 60s grace period
167
204
  // Non-blocking I/O (Fire & Forget) -> High Throughput
168
205
  const rotatedActiveDate = new Date(now - maxAge + TOKEN_ROTATION_GRACE_PERIOD_MS)
169
206
  const epochDate = new Date(0)
@@ -175,27 +212,29 @@ class Auth {
175
212
  date: epochDate
176
213
  })
177
214
  .catch(() => {})
178
-
179
- // 3. Issue new cookies immediately
180
- this.#request.cookie('odac_x', newTokenX, {
181
- httpOnly: true,
182
- secure: true,
183
- sameSite: 'Lax',
184
- maxAge: maxAge
185
- })
186
- this.#request.cookie('odac_y', newTokenY, {
187
- httpOnly: true,
188
- secure: true,
189
- sameSite: 'Lax',
190
- maxAge: maxAge
191
- })
215
+ } else {
216
+ // 2b. Recovery rotation: Delete old rotated token immediately.
217
+ // Why: Prevents unbounded token multiplication. The old token already
218
+ // had its grace period; one recovery attempt is the maximum.
219
+ Odac.DB[tokenTable]
220
+ .where('id', sql_token[0].id)
221
+ .delete()
222
+ .catch(() => {})
192
223
  }
193
- } else if (inactiveAge > updateAge) {
194
- // Fallback simple active update if rotation is not triggered
195
- Odac.DB[tokenTable]
196
- .where('id', sql_token[0].id)
197
- .update({active: new Date()})
198
- .catch(() => {})
224
+
225
+ // 3. Issue new cookies immediately
226
+ this.#request.cookie('odac_x', newTokenX, {
227
+ httpOnly: true,
228
+ secure: true,
229
+ sameSite: 'Lax',
230
+ 'max-age': Math.floor(maxAge / 1000)
231
+ })
232
+ this.#request.cookie('odac_y', newTokenY, {
233
+ httpOnly: true,
234
+ secure: true,
235
+ sameSite: 'Lax',
236
+ 'max-age': Math.floor(maxAge / 1000)
237
+ })
199
238
  }
200
239
  }
201
240
 
@@ -238,13 +277,13 @@ class Auth {
238
277
  httpOnly: true,
239
278
  secure: true,
240
279
  sameSite: 'Lax',
241
- maxAge: maxAge
280
+ 'max-age': Math.floor(maxAge / 1000)
242
281
  })
243
282
  this.#request.cookie('odac_y', token_y, {
244
283
  httpOnly: true,
245
284
  secure: true,
246
285
  sameSite: 'Lax',
247
- maxAge: maxAge
286
+ 'max-age': Math.floor(maxAge / 1000)
248
287
  })
249
288
 
250
289
  // Knex insert returns ids on some dbs, promise resolves to result
@@ -392,8 +431,8 @@ class Auth {
392
431
  await Odac.DB[tokenTable].where('user', userId).where('browser', browser).delete()
393
432
  }
394
433
 
395
- this.#request.cookie('odac_x', '', {maxAge: -1})
396
- this.#request.cookie('odac_y', '', {maxAge: -1})
434
+ this.#request.cookie('odac_x', '', {'max-age': -1})
435
+ this.#request.cookie('odac_y', '', {'max-age': -1})
397
436
 
398
437
  this.#user = null
399
438
  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(/\$\{(\w+)\}/g, (_, key) => {
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,70 @@
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
+ connections[key]._odacConnectionKey = key
61
+ }
62
+
63
+ return connections
64
+ }
65
+
66
+ module.exports = {
67
+ buildConnections,
68
+ buildConnectionConfig,
69
+ resolveClient
70
+ }