outlet-orm 6.0.0 → 7.0.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 +4 -2
- package/bin/init.js +122 -0
- package/bin/mcp.js +78 -0
- package/bin/migrate.js +25 -0
- package/docs/skills/outlet-orm/ADVANCED.md +575 -0
- package/docs/skills/outlet-orm/AI.md +220 -0
- package/docs/skills/outlet-orm/API.md +522 -0
- package/docs/skills/outlet-orm/BACKUP.md +150 -0
- package/docs/skills/outlet-orm/MIGRATIONS.md +605 -0
- package/docs/skills/outlet-orm/MODELS.md +427 -0
- package/docs/skills/outlet-orm/QUERIES.md +345 -0
- package/docs/skills/outlet-orm/RELATIONS.md +555 -0
- package/docs/skills/outlet-orm/SECURITY.md +386 -0
- package/docs/skills/outlet-orm/SEEDS.md +98 -0
- package/docs/skills/outlet-orm/SKILL.md +205 -0
- package/docs/skills/outlet-orm/TYPESCRIPT.md +480 -0
- package/package.json +7 -3
- package/src/AI/AISafetyGuardrails.js +146 -0
- package/src/AI/MCPServer.js +685 -0
- package/src/AI/PromptGenerator.js +318 -0
- package/src/Model.js +154 -2
- package/src/QueryBuilder.js +82 -0
- package/src/index.js +11 -1
- package/types/index.d.ts +147 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
# Outlet ORM - Advanced Features
|
|
2
|
+
|
|
3
|
+
[← Back to Index](SKILL.md) | [Previous: Migrations](MIGRATIONS.md) | [Next: Security →](SECURITY.md)
|
|
4
|
+
|
|
5
|
+
> 📘 **TypeScript** : Use`ModelEventName`for events,`ValidationRule`for validation. See [TYPESCRIPT.md](TYPESCRIPT.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Transactions
|
|
10
|
+
|
|
11
|
+
### Automatic Transaction (Recommended)
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const { Model } = require('outlet-orm');
|
|
15
|
+
|
|
16
|
+
const db = Model.getConnection();
|
|
17
|
+
|
|
18
|
+
const result = await db.transaction(async (connection) => {
|
|
19
|
+
const user = await User.create({
|
|
20
|
+
name: 'John',
|
|
21
|
+
email: 'john@example.com'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await Account.create({
|
|
25
|
+
user_id: user.getAttribute('id'),
|
|
26
|
+
balance: 0
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await UserSettings.create({
|
|
30
|
+
user_id: user.getAttribute('id')
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return user;
|
|
34
|
+
});
|
|
35
|
+
// Auto-commit on success, auto-rollback on error
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Manual Transaction
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const db = Model.getConnection();
|
|
42
|
+
|
|
43
|
+
await db.beginTransaction();
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await User.create({ name: 'Jane' });
|
|
47
|
+
await Profile.create({ user_id: 1 });
|
|
48
|
+
await db.commit();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
await db.rollback();
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Best Practices
|
|
56
|
+
|
|
57
|
+
- Keep transactions short to avoid locks
|
|
58
|
+
- Use automatic transactions when possible
|
|
59
|
+
- Always handle errors properly
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Soft Deletes
|
|
64
|
+
|
|
65
|
+
### Enable Soft Deletes
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
class Post extends Model {
|
|
69
|
+
static table = 'posts';
|
|
70
|
+
static softDeletes = true;
|
|
71
|
+
// static DELETED_AT = 'deleted_at'; // Custom column name
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Basic Operations
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// Regular queries exclude deleted records
|
|
79
|
+
const posts = await Post.all(); // Only non-deleted
|
|
80
|
+
|
|
81
|
+
// Soft delete
|
|
82
|
+
const post = await Post.find(1);
|
|
83
|
+
await post.destroy(); // Sets deleted_at
|
|
84
|
+
|
|
85
|
+
// Check if soft deleted
|
|
86
|
+
if (post.trashed()) {
|
|
87
|
+
console.log('Post is soft deleted');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Restore
|
|
91
|
+
await post.restore();
|
|
92
|
+
|
|
93
|
+
// Permanent delete
|
|
94
|
+
await post.forceDelete();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Query Modifiers
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
// Include deleted records
|
|
101
|
+
const allPosts = await Post.withTrashed().get();
|
|
102
|
+
|
|
103
|
+
// Only deleted records
|
|
104
|
+
const trashedPosts = await Post.onlyTrashed().get();
|
|
105
|
+
|
|
106
|
+
// With conditions
|
|
107
|
+
const deletedByUser = await Post
|
|
108
|
+
.onlyTrashed()
|
|
109
|
+
.where('user_id', 1)
|
|
110
|
+
.get();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Scopes
|
|
116
|
+
|
|
117
|
+
### Global Scopes
|
|
118
|
+
|
|
119
|
+
Applied automatically to all queries.
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
class Post extends Model {
|
|
123
|
+
static table = 'posts';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add global scope
|
|
127
|
+
Post.addGlobalScope('published', (query) => {
|
|
128
|
+
query.where('status', 'published');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// All queries filter automatically
|
|
132
|
+
const posts = await Post.all(); // Only published
|
|
133
|
+
|
|
134
|
+
// Disable scope temporarily
|
|
135
|
+
const allPosts = await Post.withoutGlobalScope('published').get();
|
|
136
|
+
|
|
137
|
+
// Disable all scopes
|
|
138
|
+
const rawPosts = await Post.withoutGlobalScopes().get();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Remove Global Scope
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
Post.removeGlobalScope('published');
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Common Use Cases
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
// Active records only
|
|
151
|
+
User.addGlobalScope('active', (q) => q.where('is_active', true));
|
|
152
|
+
|
|
153
|
+
// Non-deleted (without soft deletes)
|
|
154
|
+
Log.addGlobalScope('recent', (q) => q.where('created_at', '>', '2024-01-01'));
|
|
155
|
+
|
|
156
|
+
// Tenant isolation
|
|
157
|
+
Model.addGlobalScope('tenant', (q) => q.where('tenant_id', currentTenantId));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Events / Hooks
|
|
163
|
+
|
|
164
|
+
### Available Events
|
|
165
|
+
|
|
166
|
+
| Event | Trigger |
|
|
167
|
+
|-------|---------|
|
|
168
|
+
|`creating`| Before insert |
|
|
169
|
+
|`created`| After insert |
|
|
170
|
+
|`updating`| Before update |
|
|
171
|
+
|`updated`| After update |
|
|
172
|
+
|`saving`| Before insert OR update |
|
|
173
|
+
|`saved`| After insert OR update |
|
|
174
|
+
|`deleting`| Before delete |
|
|
175
|
+
|`deleted`| After delete |
|
|
176
|
+
|`restoring`| Before restore (soft delete) |
|
|
177
|
+
|`restored`| After restore (soft delete) |
|
|
178
|
+
|
|
179
|
+
### Register Event Handlers
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
class User extends Model {
|
|
183
|
+
static table = 'users';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Before creation
|
|
187
|
+
User.creating((user) => {
|
|
188
|
+
user.setAttribute('uuid', generateUUID());
|
|
189
|
+
// Return false to cancel the operation
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// After creation
|
|
193
|
+
User.created((user) => {
|
|
194
|
+
console.log(`User ${user.getAttribute('id')} created`);
|
|
195
|
+
// Send welcome email
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Before update
|
|
199
|
+
User.updating((user) => {
|
|
200
|
+
user.setAttribute('updated_at', new Date());
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// After update
|
|
204
|
+
User.updated((user) => {
|
|
205
|
+
// Invalidate cache
|
|
206
|
+
cache.forget(`user:${user.getAttribute('id')}`);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Saving (create AND update)
|
|
210
|
+
User.saving((user) => {
|
|
211
|
+
// Sanitize data
|
|
212
|
+
const email = user.getAttribute('email');
|
|
213
|
+
user.setAttribute('email', email.toLowerCase().trim());
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
User.saved((user) => {
|
|
217
|
+
// Log activity
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Before delete
|
|
221
|
+
User.deleting((user) => {
|
|
222
|
+
// Check permissions
|
|
223
|
+
if (user.getAttribute('is_admin')) {
|
|
224
|
+
return false; // Cancel deletion
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// After delete
|
|
229
|
+
User.deleted((user) => {
|
|
230
|
+
// Cleanup related data
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Soft delete events
|
|
234
|
+
User.restoring((user) => {});
|
|
235
|
+
User.restored((user) => {});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Generic Event Registration
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
User.on('created', (user) => {
|
|
242
|
+
console.log('User created');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
User.on('updated', (user) => {
|
|
246
|
+
console.log('User updated');
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Validation
|
|
253
|
+
|
|
254
|
+
### Define Rules
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
class User extends Model {
|
|
258
|
+
static table = 'users';
|
|
259
|
+
|
|
260
|
+
static rules = {
|
|
261
|
+
name: 'required|string|min:2|max:100',
|
|
262
|
+
email: 'required|email',
|
|
263
|
+
age: 'numeric|min:0|max:150',
|
|
264
|
+
role: 'in:admin,user,guest',
|
|
265
|
+
password: 'required|min:8',
|
|
266
|
+
website: 'regex:^https?://'
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Available Rules
|
|
272
|
+
|
|
273
|
+
| Rule | Description |
|
|
274
|
+
|------|-------------|
|
|
275
|
+
|`required`| Field is required |
|
|
276
|
+
|`string`| Must be a string |
|
|
277
|
+
|`number`/`numeric`| Must be a number |
|
|
278
|
+
|`email`| Valid email format |
|
|
279
|
+
|`boolean`| Must be boolean |
|
|
280
|
+
|`date`| Valid date |
|
|
281
|
+
|`min:N`| Minimum N (length or value) |
|
|
282
|
+
|`max:N`| Maximum N (length or value) |
|
|
283
|
+
|`in:a,b,c`| Value in list |
|
|
284
|
+
|`regex:pattern`| Match regex pattern |
|
|
285
|
+
|
|
286
|
+
### Validate
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
const user = new User({
|
|
290
|
+
name: 'J',
|
|
291
|
+
email: 'invalid-email',
|
|
292
|
+
age: 200
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Get validation result
|
|
296
|
+
const { valid, errors } = user.validate();
|
|
297
|
+
|
|
298
|
+
console.log(valid); // false
|
|
299
|
+
console.log(errors);
|
|
300
|
+
// {
|
|
301
|
+
// name: ['name must be at least 2 characters'],
|
|
302
|
+
// email: ['email must be a valid email'],
|
|
303
|
+
// age: ['age must not exceed 150']
|
|
304
|
+
// }
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Validate or Throw
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
try {
|
|
311
|
+
user.validateOrFail();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.log(error.errors);
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Validate Before Save
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
const user = new User({ name: 'John', email: 'john@example.com' });
|
|
321
|
+
|
|
322
|
+
const { valid, errors } = user.validate();
|
|
323
|
+
if (valid) {
|
|
324
|
+
await user.save();
|
|
325
|
+
} else {
|
|
326
|
+
console.log('Validation failed:', errors);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Or with exception
|
|
330
|
+
try {
|
|
331
|
+
user.validateOrFail();
|
|
332
|
+
await user.save();
|
|
333
|
+
} catch (error) {
|
|
334
|
+
res.status(400).json({ errors: error.errors });
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Query Logging
|
|
341
|
+
|
|
342
|
+
### Enable Logging
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
const { Model } = require('outlet-orm');
|
|
346
|
+
|
|
347
|
+
const db = Model.getConnection();
|
|
348
|
+
db.enableQueryLog();
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Execute Queries
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
await User.where('status', 'active').get();
|
|
355
|
+
await Post.with('author').get();
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Get Query Log
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
const queries = db.getQueryLog();
|
|
362
|
+
|
|
363
|
+
console.log(queries);
|
|
364
|
+
// [
|
|
365
|
+
// {
|
|
366
|
+
// sql: 'SELECT * FROM users WHERE status = ?',
|
|
367
|
+
// params: ['active'],
|
|
368
|
+
// duration: 15,
|
|
369
|
+
// timestamp: Date
|
|
370
|
+
// },
|
|
371
|
+
// {
|
|
372
|
+
// sql: 'SELECT * FROM posts',
|
|
373
|
+
// params: [],
|
|
374
|
+
// duration: 8,
|
|
375
|
+
// timestamp: Date
|
|
376
|
+
// }
|
|
377
|
+
// ]
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Clear and Disable
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
// Clear log
|
|
384
|
+
db.flushQueryLog();
|
|
385
|
+
|
|
386
|
+
// Disable logging
|
|
387
|
+
db.disableQueryLog();
|
|
388
|
+
|
|
389
|
+
// Check if logging
|
|
390
|
+
if (db.isLogging()) {
|
|
391
|
+
console.log('Query logging is enabled');
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Best Practices
|
|
398
|
+
|
|
399
|
+
### 1. Use Eager Loading
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
// ❌ Bad: N+1 queries
|
|
403
|
+
const users = await User.all();
|
|
404
|
+
for (const user of users) {
|
|
405
|
+
const posts = await user.posts().get(); // Query per user!
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ✅ Good: 2 queries total
|
|
409
|
+
const users = await User.with('posts').get();
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### 2. Define Fillable
|
|
413
|
+
|
|
414
|
+
```javascript
|
|
415
|
+
class User extends Model {
|
|
416
|
+
static fillable = ['name', 'email'];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 'role' ignored - protected from mass assignment
|
|
420
|
+
const user = await User.create({
|
|
421
|
+
name: 'John',
|
|
422
|
+
role: 'admin' // Ignored!
|
|
423
|
+
});
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### 3. Hide Sensitive Data
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
class User extends Model {
|
|
430
|
+
static hidden = ['password', 'api_token'];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
user.toJSON(); // password excluded
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 4. Use Type Casts
|
|
437
|
+
|
|
438
|
+
```javascript
|
|
439
|
+
class User extends Model {
|
|
440
|
+
static casts = {
|
|
441
|
+
id: 'int',
|
|
442
|
+
is_active: 'boolean',
|
|
443
|
+
settings: 'json'
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### 5. Implement down() in Migrations
|
|
449
|
+
|
|
450
|
+
```javascript
|
|
451
|
+
async down() {
|
|
452
|
+
// Always reversible
|
|
453
|
+
await schema.dropIfExists('users');
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 6. Use Transactions for Multi-Table Operations
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
await db.transaction(async () => {
|
|
461
|
+
await User.create({ name: 'John' });
|
|
462
|
+
await Profile.create({ user_id: 1 });
|
|
463
|
+
await Account.create({ user_id: 1 });
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### 7. Close Connections
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
const db = new DatabaseConnection(config);
|
|
471
|
+
// ... use connection ...
|
|
472
|
+
await db.close();
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### 8. Validate Input
|
|
476
|
+
|
|
477
|
+
```javascript
|
|
478
|
+
const user = new User(req.body);
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
user.validateOrFail();
|
|
482
|
+
await user.save();
|
|
483
|
+
} catch (error) {
|
|
484
|
+
res.status(400).json({ errors: error.errors });
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Complete Example: Blog Service
|
|
491
|
+
|
|
492
|
+
```javascript
|
|
493
|
+
const { Model } = require('outlet-orm');
|
|
494
|
+
|
|
495
|
+
// Models
|
|
496
|
+
class User extends Model {
|
|
497
|
+
static table = 'users';
|
|
498
|
+
static softDeletes = true;
|
|
499
|
+
static hidden = ['password'];
|
|
500
|
+
static rules = { email: 'required|email', password: 'required|min:8' };
|
|
501
|
+
static casts = { id: 'int', is_admin: 'boolean' };
|
|
502
|
+
|
|
503
|
+
posts() { return this.hasMany(Post, 'user_id'); }
|
|
504
|
+
profile() { return this.hasOne(Profile, 'user_id'); }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
class Post extends Model {
|
|
508
|
+
static table = 'posts';
|
|
509
|
+
static softDeletes = true;
|
|
510
|
+
static fillable = ['title', 'content', 'status'];
|
|
511
|
+
static casts = { views: 'int' };
|
|
512
|
+
|
|
513
|
+
author() { return this.belongsTo(User, 'user_id'); }
|
|
514
|
+
tags() { return this.belongsToMany(Tag, 'post_tag', 'post_id', 'tag_id'); }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Global scope: only published
|
|
518
|
+
Post.addGlobalScope('published', (q) => q.where('status', 'published'));
|
|
519
|
+
|
|
520
|
+
// Events
|
|
521
|
+
User.created(async (user) => {
|
|
522
|
+
await Profile.create({ user_id: user.getAttribute('id') });
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
Post.creating((post) => {
|
|
526
|
+
post.setAttribute('slug', slugify(post.getAttribute('title')));
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Service
|
|
530
|
+
class BlogService {
|
|
531
|
+
async createPost(userId, data, tagIds) {
|
|
532
|
+
const db = Model.getConnection();
|
|
533
|
+
|
|
534
|
+
return db.transaction(async () => {
|
|
535
|
+
const post = new Post({
|
|
536
|
+
...data,
|
|
537
|
+
user_id: userId
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
post.validateOrFail();
|
|
541
|
+
await post.save();
|
|
542
|
+
|
|
543
|
+
if (tagIds?.length) {
|
|
544
|
+
await post.tags().attach(tagIds);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
await post.load('author', 'tags');
|
|
548
|
+
return post;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async getPublishedPosts(page = 1) {
|
|
553
|
+
return Post
|
|
554
|
+
.with('author.profile', 'tags')
|
|
555
|
+
.orderBy('created_at', 'desc')
|
|
556
|
+
.paginate(page, 15);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async getAuthorPosts(userId) {
|
|
560
|
+
return Post
|
|
561
|
+
.withoutGlobalScope('published')
|
|
562
|
+
.where('user_id', userId)
|
|
563
|
+
.with('tags')
|
|
564
|
+
.orderBy('created_at', 'desc')
|
|
565
|
+
.get();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Next Steps
|
|
573
|
+
|
|
574
|
+
- [API Reference →](API.md)
|
|
575
|
+
- [Back to Index →](SKILL.md)
|