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.
@@ -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)