outlet-orm 10.0.0 → 11.1.0

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 (36) hide show
  1. package/README.md +32 -0
  2. package/bin/reverse.js +0 -1
  3. package/package.json +1 -1
  4. package/skills/outlet-orm/ADVANCED.md +29 -0
  5. package/skills/outlet-orm/API.md +30 -0
  6. package/skills/outlet-orm/MODELS.md +144 -2
  7. package/skills/outlet-orm/QUERIES.md +133 -0
  8. package/skills/outlet-orm/RELATIONS.md +44 -0
  9. package/skills/outlet-orm/SKILL.md +4 -2
  10. package/skills/outlet-orm/TYPESCRIPT.md +98 -0
  11. package/src/AI/AIManager.js +58 -58
  12. package/src/AI/AIQueryBuilder.js +2 -2
  13. package/src/AI/AIQueryOptimizer.js +2 -2
  14. package/src/AI/Contracts/AudioProviderContract.js +2 -2
  15. package/src/AI/Contracts/ChatProviderContract.js +3 -2
  16. package/src/AI/Contracts/EmbeddingsProviderContract.js +1 -1
  17. package/src/AI/Contracts/ImageProviderContract.js +1 -1
  18. package/src/AI/Contracts/ModelsProviderContract.js +1 -1
  19. package/src/AI/Contracts/ToolContract.js +1 -1
  20. package/src/AI/MCPServer.js +3 -3
  21. package/src/AI/Providers/CustomOpenAIProvider.js +0 -2
  22. package/src/AI/Providers/GeminiProvider.js +2 -2
  23. package/src/AI/Providers/OpenAIProvider.js +0 -5
  24. package/src/AI/Support/DocumentAttachmentMapper.js +37 -37
  25. package/src/AI/Support/FileSecurity.js +1 -1
  26. package/src/Backup/BackupManager.js +6 -6
  27. package/src/Backup/BackupScheduler.js +1 -1
  28. package/src/Backup/BackupSocketServer.js +2 -2
  29. package/src/DatabaseConnection.js +51 -0
  30. package/src/Model.js +245 -5
  31. package/src/QueryBuilder.js +191 -0
  32. package/src/Relations/HasOneRelation.js +114 -114
  33. package/src/Relations/HasOneThroughRelation.js +105 -105
  34. package/src/Relations/MorphOneRelation.js +4 -2
  35. package/src/Relations/Relation.js +35 -0
  36. package/types/index.d.ts +67 -1
package/README.md CHANGED
@@ -274,6 +274,17 @@ async store(req, res) {
274
274
  - **`.env` configuration** (loaded automatically)
275
275
  - **Multi-database**: MySQL, PostgreSQL, and SQLite
276
276
  - **Complete TypeScript types** with Generic Model and typed Schema Builder (v4.0.0+)
277
+ - **🆕 Property-style attribute access** (v11.0.0): `user.name` instead of `user.getAttribute('name')`
278
+ - **🆕 Computed Appends** (v11.0.0): `static appends` for virtual attributes auto-included in `toJSON()`
279
+ - **🆕 Model Utility Methods** (v11.0.0): `fresh()`, `refresh()`, `replicate()`, `is()` / `isNot()`, `only()` / `except()`
280
+ - **🆕 Instance-Level Visibility** (v11.0.0): `makeVisible()` / `makeHidden()` per-instance control
281
+ - **🆕 Change Tracking** (v11.0.0): `wasChanged()` / `getChanges()` after `save()`
282
+ - **🆕 Batch Processing** (v11.0.0): `chunk(size, callback)` for memory-efficient iteration
283
+ - **🆕 Conditional Queries** (v11.0.0): `when(condition, callback)` and `tap(callback)`
284
+ - **🆕 Query Debugging** (v11.0.0): `dd()` dumps SQL + bindings to console and throws
285
+ - **🆕 Fluent Local Scopes** (v11.0.0): `static scopeActive(query)` → `User.query().active()`
286
+ - **🆕 Relation Defaults** (v11.0.0): `withDefault()` on HasOne/MorphOne/HasOneThrough
287
+ - **🆕 Aggregates & Pluck** (v11.0.0): `sum()`, `avg()`, `min()`, `max()`, `value()`, keyed `pluck(col, key)`
277
288
 
278
289
  ## ⚡ Quick Start
279
290
 
@@ -1100,6 +1111,7 @@ if (db.isLogging()) {
1100
1111
  | `execute(sql, params?)` | Raw query (native driver results) |
1101
1112
  | `increment(table, column, query, amount?)` | Atomic increment |
1102
1113
  | `decrement(table, column, query, amount?)` | Atomic decrement |
1114
+ | `aggregate(table, fn, column, query)` | Execute aggregate function (SUM/AVG/MIN/MAX) |
1103
1115
  | `close()` / `disconnect()` | Closes the connection |
1104
1116
  | **Query Logging (static)** | |
1105
1117
  | `enableQueryLog()` | Enables query logging |
@@ -1166,6 +1178,17 @@ if (db.isLogging()) {
1166
1178
  | `getDirty()` | Modified attributes |
1167
1179
  | `isDirty()` | Has been modified? |
1168
1180
  | `toJSON()` | Convert to plain object |
1181
+ | `fresh()` | Reload from DB (new instance) |
1182
+ | `refresh()` | Reload in place |
1183
+ | `replicate()` | Clone without ID/timestamps |
1184
+ | `is(model)` | Same type and primary key? |
1185
+ | `isNot(model)` | Negation of `is()` |
1186
+ | `only(...keys)` | Subset of attributes |
1187
+ | `except(...keys)` | All attributes except listed |
1188
+ | `makeVisible(...attrs)` | Unhide attributes for this instance |
1189
+ | `makeHidden(...attrs)` | Hide attributes for this instance |
1190
+ | `wasChanged(attr?)` | Changed after last `save()`? |
1191
+ | `getChanges()` | Attributes changed by last `save()` |
1169
1192
  | **Soft Deletes** | |
1170
1193
  | `trashed()` | Is deleted? |
1171
1194
  | `restore()` | Restore the model |
@@ -1216,6 +1239,15 @@ if (db.isLogging()) {
1216
1239
  | `delete()` | Delete |
1217
1240
  | `increment(col, amount?)` | Atomic increment |
1218
1241
  | `decrement(col, amount?)` | Atomic decrement |
1242
+ | `pluck(col, keyCol?)` | Array of values or keyed object |
1243
+ | `value(col)` | Single scalar value |
1244
+ | `sum(col)` / `avg(col)` | Aggregate sum / average |
1245
+ | `min(col)` / `max(col)` | Aggregate min / max |
1246
+ | `chunk(size, callback)` | Process results in batches |
1247
+ | `when(cond, cb, fallback?)` | Conditional query building |
1248
+ | `tap(callback)` | Inspect builder without modifying |
1249
+ | `toSQL()` | Returns `{ sql, bindings }` |
1250
+ | `dd()` | Dumps SQL + bindings and throws |
1219
1251
  | `clone()` | Clones the query builder |
1220
1252
 
1221
1253
  ## 🛠️ CLI tools
package/bin/reverse.js CHANGED
@@ -82,7 +82,6 @@ function parseCreateTable(sql) {
82
82
  for (const line of splitDefinitions(body)) {
83
83
  const trimmed = line.trim();
84
84
  if (!trimmed) continue;
85
- const upperTrimmed = trimmed.toUpperCase();
86
85
 
87
86
  // ── Table-level FOREIGN KEY constraint ───────────────────────────────────
88
87
  if (/^FOREIGN\s+KEY/i.test(trimmed) || /^CONSTRAINT\s+\S+\s+FOREIGN\s+KEY/i.test(trimmed)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "outlet-orm",
3
- "version": "10.0.0",
3
+ "version": "11.1.0",
4
4
  "description": "A Laravel Eloquent-inspired ORM for Node.js with support for MySQL, PostgreSQL, and SQLite",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -157,6 +157,35 @@ Log.addGlobalScope('recent', (q) => q.where('created_at', '>', '2024-01-01'));
157
157
  Model.addGlobalScope('tenant', (q) => q.where('tenant_id', currentTenantId));
158
158
  ```
159
159
 
160
+ ### Local Scopes (v11.0.0)
161
+
162
+ Define reusable query constraints as static `scopeXxx` methods. They become fluent methods on the QueryBuilder:
163
+
164
+ ```javascript
165
+ class User extends Model {
166
+ static table = 'users';
167
+
168
+ static scopeActive(query) {
169
+ return query.where('status', 'active');
170
+ }
171
+
172
+ static scopeRole(query, role) {
173
+ return query.where('role', role);
174
+ }
175
+
176
+ static scopeRecent(query, days = 7) {
177
+ const date = new Date(Date.now() - days * 86400000).toISOString();
178
+ return query.where('created_at', '>', date);
179
+ }
180
+ }
181
+
182
+ // Fluent usage — scopes become methods on the query builder
183
+ const users = await User.query().active().role('admin').recent(30).get();
184
+ const count = await User.query().active().count();
185
+ ```
186
+
187
+ > **Note**: The legacy `static scopes = {}` and `.scope('name')` syntax still works. Fluent local scopes are the recommended approach from v11.
188
+
160
189
  ---
161
190
 
162
191
  ## Events / Hooks
@@ -54,6 +54,7 @@ const db = new DatabaseConnection({
54
54
  |`update(table, data, query)`| Update records |
55
55
  |`delete(table, query)`| Delete records |
56
56
  |`count(table, query)`| Count records |
57
+ |`aggregate(table, func, col, query)`| Run aggregate function (v11) |
57
58
  |`executeRawQuery(sql, params?)`| Raw query (normalised results) |
58
59
  |`execute(sql, params?)`| Raw query (native driver results) |
59
60
  |`increment(table, column, query, amount?)`| Atomic increment |
@@ -191,10 +192,27 @@ const db = new DatabaseConnection({
191
192
  |`fill(attrs)`| Fill attributes |
192
193
  |`setAttribute(key, val)`| Set single attribute |
193
194
  |`getAttribute(key)`| Get single attribute |
195
+ |`user.key` (Proxy)| Property-style get/set (v11) |
194
196
  |`getDirty()`| Get modified attributes |
195
197
  |`isDirty()`| Check if modified |
198
+ |`wasChanged(key?)`| Check if changed after save (v11) |
199
+ |`getChanges()`| Get post-save changes (v11) |
200
+ |`only(...keys)`| Subset of attributes (v11) |
201
+ |`except(...keys)`| All attributes except keys (v11) |
202
+ |`makeVisible(...keys)`| Show hidden attrs on instance (v11) |
203
+ |`makeHidden(...keys)`| Hide attrs on instance (v11) |
196
204
  |`toJSON()`| Convert to plain object |
197
205
 
206
+ ### Model Utility (v11.0.0)
207
+
208
+ | Method | Description |
209
+ |--------|-------------|
210
+ |`fresh()`| New reloaded instance from DB |
211
+ |`refresh()`| Reload current instance in-place |
212
+ |`replicate()`| Clone without primary key |
213
+ |`is(model)`| Same table + same PK |
214
+ |`isNot(model)`| Different identity |
215
+
198
216
  ### Persistence
199
217
 
200
218
  | Method | Description |
@@ -322,6 +340,13 @@ const db = new DatabaseConnection({
322
340
  |`paginate(page, perPage)`| Paginated results |
323
341
  |`count()`| Count results |
324
342
  |`exists()`| Check existence |
343
+ |`sum(col)`| Sum of column (v11) |
344
+ |`avg(col)`| Average of column (v11) |
345
+ |`min(col)`| Minimum of column (v11) |
346
+ |`max(col)`| Maximum of column (v11) |
347
+ |`pluck(col)`| Array of column values (v11) |
348
+ |`value(col)`| Single value from first row (v11) |
349
+ |`chunk(size, callback)`| Batch processing (v11) |
325
350
 
326
351
  ### Mutations
327
352
 
@@ -339,6 +364,10 @@ const db = new DatabaseConnection({
339
364
  | Method | Description |
340
365
  |--------|-------------|
341
366
  |`clone()`| Clone QueryBuilder |
367
+ |`when(condition, callback)`| Conditional clause (v11) |
368
+ |`tap(callback)`| Debug callback (v11) |
369
+ |`toSQL()`| Get SQL + bindings (v11) |
370
+ |`dd()`| Dump & die debug (v11) |
342
371
 
343
372
  ---
344
373
 
@@ -464,6 +493,7 @@ const db = new DatabaseConnection({
464
493
  |`hidden`| array |`[]`| Hidden from JSON |
465
494
  |`casts`| object |`{}`| Type casting |
466
495
  |`rules`| object |`{}`| Validation rules |
496
+ |`appends`| array |`[]`| Computed attrs in toJSON (v11) |
467
497
  |`connection`| object |`null`| Custom connection |
468
498
 
469
499
  ---
@@ -109,6 +109,7 @@ class User extends Model {
109
109
  |`hidden`| array |`[]`| Hidden from JSON |
110
110
  |`casts`| object |`{}`| Type casting definitions |
111
111
  |`rules`| object |`{}`| Validation rules |
112
+ |`appends`| array |`[]`| Computed attributes included in toJSON (v11) |
112
113
  |`connection`| object |`null`| Custom DB connection |
113
114
 
114
115
  ---
@@ -256,7 +257,10 @@ await user.forceDelete();
256
257
  ```javascript
257
258
  const user = await User.find(1);
258
259
 
259
- // Get single attribute
260
+ // Property access (v11+)
261
+ const name = user.name;
262
+
263
+ // Method access
260
264
  const name = user.getAttribute('name');
261
265
 
262
266
  // Get all attributes as object
@@ -272,7 +276,10 @@ const dirty = user.getDirty(); // Get modified attributes
272
276
  ```javascript
273
277
  const user = new User();
274
278
 
275
- // Set single attribute
279
+ // Property access (v11+)
280
+ user.name = 'John';
281
+
282
+ // Method access
276
283
  user.setAttribute('name', 'John');
277
284
 
278
285
  // Fill multiple attributes
@@ -310,6 +317,141 @@ if (user && await bcrypt.compare(password, user.getAttribute('password'))) {
310
317
  }
311
318
  ```
312
319
 
320
+ ### Instance-Level Visibility (v11.0.0)
321
+
322
+ ```javascript
323
+ const user = await User.find(1);
324
+
325
+ // Temporarily show hidden attributes on this instance
326
+ user.makeVisible('password', 'secret_token');
327
+ console.log(user.toJSON()); // password & secret_token included
328
+
329
+ // Temporarily hide additional attributes on this instance
330
+ user.makeHidden('email', 'phone');
331
+ console.log(user.toJSON()); // email & phone excluded
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Property Access (v11.0.0)
337
+
338
+ Access model attributes directly as properties via Proxy, in addition to `getAttribute()`/`setAttribute()`:
339
+
340
+ ```javascript
341
+ const user = await User.find(1);
342
+
343
+ // Before v11 - only method access
344
+ const name = user.getAttribute('name');
345
+ user.setAttribute('name', 'New Name');
346
+
347
+ // v11+ - property-style access (equivalent)
348
+ const name = user.name;
349
+ user.name = 'New Name';
350
+ await user.save();
351
+
352
+ // Works with casts
353
+ console.log(user.email_verified); // boolean (thanks to casts)
354
+ console.log(user.metadata); // object (JSON cast)
355
+
356
+ // Check dirty state
357
+ user.name = 'Changed';
358
+ console.log(user.isDirty()); // true
359
+ ```
360
+
361
+ > **Note**: Native Model methods and properties (`save`, `destroy`, `fill`, etc.) always take precedence over attribute names.
362
+
363
+ ---
364
+
365
+ ## Computed Appends (v11.0.0)
366
+
367
+ Include computed attributes in `toJSON()` output:
368
+
369
+ ```javascript
370
+ class User extends Model {
371
+ static table = 'users';
372
+ static appends = ['full_name', 'is_admin'];
373
+
374
+ // Accessor for computed attribute
375
+ getFullNameAttribute() {
376
+ return `${this.getAttribute('first_name')} ${this.getAttribute('last_name')}`;
377
+ }
378
+
379
+ getIsAdminAttribute() {
380
+ return this.getAttribute('role') === 'admin';
381
+ }
382
+ }
383
+
384
+ const user = await User.find(1);
385
+ console.log(user.toJSON());
386
+ // { id: 1, first_name: 'John', last_name: 'Doe', full_name: 'John Doe', is_admin: false, ... }
387
+ ```
388
+
389
+ ---
390
+
391
+ ## Model Utility Methods (v11.0.0)
392
+
393
+ ### fresh() / refresh()
394
+
395
+ ```javascript
396
+ const user = await User.find(1);
397
+
398
+ // fresh() returns a NEW instance reloaded from DB (original unchanged)
399
+ const freshUser = await user.fresh();
400
+
401
+ // refresh() reloads the CURRENT instance in-place
402
+ user.name = 'temp';
403
+ await user.refresh();
404
+ console.log(user.name); // Back to DB value
405
+ ```
406
+
407
+ ### replicate()
408
+
409
+ ```javascript
410
+ const user = await User.find(1);
411
+ const clone = user.replicate();
412
+ // clone has same attributes but NO primary key
413
+ clone.name = 'Clone of ' + user.name;
414
+ await clone.save(); // Inserts as new record
415
+ ```
416
+
417
+ ### is() / isNot()
418
+
419
+ ```javascript
420
+ const user1 = await User.find(1);
421
+ const user2 = await User.find(1);
422
+ const user3 = await User.find(2);
423
+
424
+ user1.is(user2); // true (same table + same PK)
425
+ user1.isNot(user3); // true (different PK)
426
+ ```
427
+
428
+ ### only() / except()
429
+
430
+ ```javascript
431
+ const user = await User.find(1);
432
+
433
+ // Get a subset of attributes
434
+ const subset = user.only('name', 'email');
435
+ // { name: 'John', email: 'john@example.com' }
436
+
437
+ // Get all attributes except some
438
+ const filtered = user.except('password', 'secret_token');
439
+ // { id: 1, name: 'John', email: 'john@example.com', ... }
440
+ ```
441
+
442
+ ### wasChanged() / getChanges()
443
+
444
+ ```javascript
445
+ const user = await User.find(1);
446
+ user.name = 'Updated';
447
+ await user.save();
448
+
449
+ user.wasChanged(); // true
450
+ user.wasChanged('name'); // true
451
+ user.wasChanged('email'); // false
452
+ user.getChanges(); // { name: 'Updated' }
453
+ ```
454
+
313
455
  ---
314
456
 
315
457
  ## Timestamps
@@ -211,6 +211,30 @@ if (hasAdmins) {
211
211
  }
212
212
  ```
213
213
 
214
+ ### Sum / Avg / Min / Max (v11.0.0)
215
+
216
+ ```javascript
217
+ const totalBalance = await User.query().sum('balance');
218
+ const averageAge = await User.query().avg('age');
219
+ const youngest = await User.query().min('age');
220
+ const oldest = await User.query().max('age');
221
+
222
+ // With conditions
223
+ const activeTotal = await User.where('status', 'active').sum('balance');
224
+ ```
225
+
226
+ ### Pluck / Value (v11.0.0)
227
+
228
+ ```javascript
229
+ // pluck() — get an array of values from a single column
230
+ const emails = await User.query().pluck('email');
231
+ // ['john@example.com', 'jane@example.com', ...]
232
+
233
+ // value() — get a single value from the first row
234
+ const name = await User.where('id', 1).value('name');
235
+ // 'John Doe'
236
+ ```
237
+
214
238
  ### Group By & Having
215
239
 
216
240
  ```javascript
@@ -280,6 +304,104 @@ await User.where('id', 1).decrement('credits', 50);
280
304
 
281
305
  ---
282
306
 
307
+ ## Batch Processing — chunk() (v11.0.0)
308
+
309
+ Process large datasets in manageable batches:
310
+
311
+ ```javascript
312
+ // Process 100 records at a time
313
+ await User.query().chunk(100, async (users) => {
314
+ for (const user of users) {
315
+ await sendNewsletter(user);
316
+ }
317
+ });
318
+
319
+ // With conditions
320
+ await User.where('status', 'active').chunk(50, async (batch) => {
321
+ console.log(`Processing ${batch.length} users`);
322
+ });
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Conditional Queries — when() / tap() (v11.0.0)
328
+
329
+ ### when()
330
+
331
+ Conditionally apply query clauses:
332
+
333
+ ```javascript
334
+ const status = req.query.status; // may be undefined
335
+
336
+ const users = await User.query()
337
+ .when(status, (query, value) => query.where('status', value))
338
+ .when(req.query.role, (query, value) => query.where('role', value))
339
+ .get();
340
+ ```
341
+
342
+ ### tap()
343
+
344
+ Execute a callback for debugging without modifying the query:
345
+
346
+ ```javascript
347
+ const users = await User.query()
348
+ .where('status', 'active')
349
+ .tap((query) => console.log('Query so far:', query.toSQL()))
350
+ .orderBy('name')
351
+ .get();
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Query Debugging — toSQL() / dd() (v11.0.0)
357
+
358
+ ```javascript
359
+ // toSQL() — get the SQL string and bindings
360
+ const { sql, bindings } = User.where('status', 'active').toSQL();
361
+ console.log(sql); // 'SELECT * FROM users WHERE status = ?'
362
+ console.log(bindings); // ['active']
363
+
364
+ // dd() — dump and die (logs to console and throws)
365
+ User.where('status', 'active').dd();
366
+ // Logs: { sql: '...', bindings: [...] } then throws
367
+ ```
368
+
369
+ ---
370
+
371
+ ## Fluent Local Scopes (v11.0.0)
372
+
373
+ Define reusable query constraints as static methods on the model:
374
+
375
+ ```javascript
376
+ class User extends Model {
377
+ static table = 'users';
378
+
379
+ // Define scope as static scopeXxx(query, ...params)
380
+ static scopeActive(query) {
381
+ return query.where('status', 'active');
382
+ }
383
+
384
+ static scopeRole(query, role) {
385
+ return query.where('role', role);
386
+ }
387
+
388
+ static scopeRecent(query, days = 7) {
389
+ const date = new Date(Date.now() - days * 86400000).toISOString();
390
+ return query.where('created_at', '>', date);
391
+ }
392
+ }
393
+
394
+ // Use fluently on the query builder
395
+ const users = await User.query().active().role('admin').recent(30).get();
396
+
397
+ // Combine with other query methods
398
+ const count = await User.query().active().count();
399
+ ```
400
+
401
+ > See [ADVANCED.md](ADVANCED.md) for more details on global and local scopes.
402
+
403
+ ---
404
+
283
405
  ## Raw Queries
284
406
 
285
407
  ```javascript
@@ -394,6 +516,17 @@ See [AI.md](AI.md) for full details.
394
516
  |`paginate(page, perPage)`| Pagination |
395
517
  |`count()`| Count results |
396
518
  |`exists()`| Check existence |
519
+ |`sum(col)`| Sum of column (v11) |
520
+ |`avg(col)`| Average of column (v11) |
521
+ |`min(col)`| Minimum of column (v11) |
522
+ |`max(col)`| Maximum of column (v11) |
523
+ |`pluck(col)`| Array of column values (v11) |
524
+ |`value(col)`| Single value from first row (v11) |
525
+ |`chunk(size, callback)`| Batch processing (v11) |
526
+ |`when(condition, callback)`| Conditional clause (v11) |
527
+ |`tap(callback)`| Debug callback (v11) |
528
+ |`toSQL()`| Get SQL + bindings (v11) |
529
+ |`dd()`| Dump & die debug (v11) |
397
530
  |`insert(data)`| Insert record(s) |
398
531
  |`update(attrs)`| Update records |
399
532
  |`delete()`| Delete records |
@@ -546,6 +546,50 @@ class Category extends Model {
546
546
  |`attach(ids)`| Attach (many-to-many) |
547
547
  |`detach(ids?)`| Detach (many-to-many) |
548
548
  |`sync(ids)`| Sync (many-to-many) |
549
+ |`withDefault(attrs?)`| Default for empty HasOne/MorphOne (v11) |
550
+
551
+ ---
552
+
553
+ ## Relation Defaults — withDefault() (v11.0.0)
554
+
555
+ Return a default model instead of `null` when a `hasOne`, `morphOne` or `hasOneThrough` relationship is empty:
556
+
557
+ ```javascript
558
+ class User extends Model {
559
+ static table = 'users';
560
+
561
+ profile() {
562
+ // Returns empty Profile instance instead of null
563
+ return this.hasOne(Profile, 'user_id').withDefault();
564
+ }
565
+
566
+ avatar() {
567
+ // Returns a Profile with default values
568
+ return this.morphOne(Image, 'imageable').withDefault({
569
+ url: '/images/default-avatar.png'
570
+ });
571
+ }
572
+
573
+ settings() {
574
+ // Dynamic defaults via callback
575
+ return this.hasOne(Settings, 'user_id').withDefault((model) => {
576
+ model.setAttribute('theme', 'light');
577
+ model.setAttribute('locale', 'en');
578
+ });
579
+ }
580
+ }
581
+
582
+ // Usage
583
+ const user = await User.with('profile').find(1);
584
+ console.log(user.relationships.profile); // Profile instance (never null)
585
+ ```
586
+
587
+ **Signatures:**
588
+ - `withDefault()` — empty model instance
589
+ - `withDefault({ key: value, ... })` — model with attributes
590
+ - `withDefault((model) => { ... })` — dynamic defaults via callback
591
+
592
+ **Supported on:** `hasOne`, `morphOne`, `hasOneThrough`
549
593
 
550
594
  ---
551
595
 
@@ -4,7 +4,7 @@ description: Outlet ORM is a Laravel Eloquent-inspired ORM for Node.js with MySQ
4
4
  license: MIT
5
5
  metadata:
6
6
  author: omgbwa-yasse
7
- version: "9.0.0"
7
+ version: "11.0.0"
8
8
  source: https://github.com/omgbwa-yasse/outlet-orm
9
9
  npm: https://www.npmjs.com/package/outlet-orm
10
10
  ---
@@ -13,7 +13,9 @@ npm: https://www.npmjs.com/package/outlet-orm
13
13
 
14
14
  Comprehensive guide for using Outlet ORM - a Laravel Eloquent-inspired ORM for Node.js/TypeScript with support for MySQL, PostgreSQL, and SQLite.
15
15
 
16
- > 🆕 **v9.0.0**: Complete AI documentation AI multi-provider LLM, AI Query Builder, AI Seeder, AI Optimizer, AI Prompt Enhancer, AI Safety Guardrails. See [AI.md](AI.md).
16
+ > 🆕 **v11.0.0**: Proxy property access (`user.name`), Eloquent-style model methods (`fresh`, `refresh`, `replicate`, `is`, `isNot`, `only`, `except`, `makeVisible`, `makeHidden`, `wasChanged`, `getChanges`), computed `appends`, QueryBuilder enhancements (`value`, `chunk`, `when`, `tap`, `toSQL`, `dd`), fluent local scopes (`static scopeActive(query)` → `User.query().active()`), relation `withDefault()`. See [MODELS.md](MODELS.md), [QUERIES.md](QUERIES.md), [ADVANCED.md](ADVANCED.md), [RELATIONS.md](RELATIONS.md).
17
+ >
18
+ > 🔖 **v9.0.0**: Complete AI documentation — AI multi-provider LLM, AI Query Builder, AI Seeder, AI Optimizer, AI Prompt Enhancer, AI Safety Guardrails. See [AI.md](AI.md).
17
19
  >
18
20
  > 🔖 **v8.0.0**: AI multi-provider LLM abstraction (9 providers), AI Query Builder, AI Seeder, AI Query Optimizer, AI Prompt Enhancer.
19
21
  >
@@ -384,6 +384,104 @@ table.foreign('user_id')
384
384
 
385
385
  ---
386
386
 
387
+ ## v11.0.0 Type Additions
388
+
389
+ ### Property-Style Attribute Access
390
+
391
+ Model instances are wrapped in a `Proxy`, so attributes can be read and written as direct properties:
392
+
393
+ ```typescript
394
+ const user = await User.find(1);
395
+
396
+ // Property access (via Proxy)
397
+ console.log(user.name); // equivalent to user.getAttribute('name')
398
+ user.email = 'new@mail.com'; // equivalent to user.setAttribute('email', ...)
399
+
400
+ // Both styles coexist
401
+ user.getAttribute('name'); // still works
402
+ ```
403
+
404
+ The index signature `[K: string]: any` on `Model` enables this in TypeScript.
405
+
406
+ ### Computed Appends
407
+
408
+ ```typescript
409
+ class User extends Model<UserAttributes> {
410
+ static table = 'users';
411
+ static appends = ['full_name'] as const;
412
+
413
+ getFullNameAttribute(): string {
414
+ return `${this.attributes.first_name} ${this.attributes.last_name}`;
415
+ }
416
+ }
417
+
418
+ // full_name is included in toJSON() output
419
+ ```
420
+
421
+ ### New Model Instance Methods
422
+
423
+ ```typescript
424
+ // Reload from DB
425
+ const freshUser: User | null = await user.fresh('posts');
426
+ await user.refresh(); // mutates in place
427
+
428
+ // Clone without primary key
429
+ const clone: User = user.replicate('id', 'created_at');
430
+
431
+ // Identity comparison
432
+ user.is(otherUser); // same table + same PK
433
+ user.isNot(otherUser);
434
+
435
+ // Attribute subsets
436
+ const partial: Partial<UserAttributes> = user.only('name', 'email');
437
+ const rest: Partial<UserAttributes> = user.except('password');
438
+
439
+ // Instance-level visibility
440
+ user.makeVisible('password');
441
+ user.makeHidden('email');
442
+
443
+ // Change tracking (after save)
444
+ user.wasChanged('name'); // boolean
445
+ user.getChanges(); // { name: 'new value' }
446
+ ```
447
+
448
+ ### New QueryBuilder Methods
449
+
450
+ ```typescript
451
+ // Single column value
452
+ const email: any = await User.where('id', 1).value('email');
453
+
454
+ // Batch processing
455
+ await User.where('active', true).chunk(100, (users, page) => {
456
+ console.log(`Page ${page}:`, users.length);
457
+ // return false to stop
458
+ });
459
+
460
+ // Conditional query building
461
+ User.query()
462
+ .when(role, (qb, val) => qb.where('role', val))
463
+ .tap(qb => console.log(qb.toSQL()))
464
+ .get();
465
+
466
+ // Debug
467
+ const sql = User.where('active', true).toSQL();
468
+ User.where('active', true).dd(); // dumps SQL + throws
469
+ ```
470
+
471
+ ### withDefault on Relations
472
+
473
+ ```typescript
474
+ class User extends Model<UserAttributes> {
475
+ profile(): HasOneRelation<Profile> {
476
+ return this.hasOne(Profile, 'user_id').withDefault();
477
+ // or .withDefault({ bio: 'N/A' })
478
+ // or .withDefault(() => new Profile({ bio: 'N/A' }))
479
+ }
480
+ }
481
+ ```
482
+
483
+ ---
484
+
387
485
  ## Common Patterns
388
486
 
389
487
  ### Repository Pattern