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.
- package/README.md +32 -0
- package/bin/reverse.js +0 -1
- package/package.json +1 -1
- package/skills/outlet-orm/ADVANCED.md +29 -0
- package/skills/outlet-orm/API.md +30 -0
- package/skills/outlet-orm/MODELS.md +144 -2
- package/skills/outlet-orm/QUERIES.md +133 -0
- package/skills/outlet-orm/RELATIONS.md +44 -0
- package/skills/outlet-orm/SKILL.md +4 -2
- package/skills/outlet-orm/TYPESCRIPT.md +98 -0
- package/src/AI/AIManager.js +58 -58
- package/src/AI/AIQueryBuilder.js +2 -2
- package/src/AI/AIQueryOptimizer.js +2 -2
- package/src/AI/Contracts/AudioProviderContract.js +2 -2
- package/src/AI/Contracts/ChatProviderContract.js +3 -2
- package/src/AI/Contracts/EmbeddingsProviderContract.js +1 -1
- package/src/AI/Contracts/ImageProviderContract.js +1 -1
- package/src/AI/Contracts/ModelsProviderContract.js +1 -1
- package/src/AI/Contracts/ToolContract.js +1 -1
- package/src/AI/MCPServer.js +3 -3
- package/src/AI/Providers/CustomOpenAIProvider.js +0 -2
- package/src/AI/Providers/GeminiProvider.js +2 -2
- package/src/AI/Providers/OpenAIProvider.js +0 -5
- package/src/AI/Support/DocumentAttachmentMapper.js +37 -37
- package/src/AI/Support/FileSecurity.js +1 -1
- package/src/Backup/BackupManager.js +6 -6
- package/src/Backup/BackupScheduler.js +1 -1
- package/src/Backup/BackupSocketServer.js +2 -2
- package/src/DatabaseConnection.js +51 -0
- package/src/Model.js +245 -5
- package/src/QueryBuilder.js +191 -0
- package/src/Relations/HasOneRelation.js +114 -114
- package/src/Relations/HasOneThroughRelation.js +105 -105
- package/src/Relations/MorphOneRelation.js +4 -2
- package/src/Relations/Relation.js +35 -0
- 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
|
@@ -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
|
package/skills/outlet-orm/API.md
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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: "
|
|
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
|
-
> 🆕 **
|
|
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
|