outlet-orm 2.5.0 → 3.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 +674 -313
- package/package.json +1 -1
- package/src/DatabaseConnection.js +464 -110
- package/src/Model.js +1118 -659
- package/src/QueryBuilder.js +794 -710
- package/src/index.js +9 -1
- package/types/index.d.ts +126 -16
package/src/Model.js
CHANGED
|
@@ -1,659 +1,1118 @@
|
|
|
1
|
-
const QueryBuilder = require('./QueryBuilder');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Base Model class inspired by Laravel Eloquent
|
|
5
|
-
*/
|
|
6
|
-
class Model {
|
|
7
|
-
static table = '';
|
|
8
|
-
static primaryKey = 'id';
|
|
9
|
-
static timestamps = true;
|
|
10
|
-
static fillable = [];
|
|
11
|
-
static hidden = [];
|
|
12
|
-
static casts = {};
|
|
13
|
-
static connection = null;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
*
|
|
94
|
-
* @param {string}
|
|
95
|
-
* @param {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
* @param {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
*
|
|
179
|
-
* @param {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
*
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
*
|
|
206
|
-
* @param {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
*
|
|
217
|
-
* @param {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
*
|
|
227
|
-
* @
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
*
|
|
279
|
-
* @returns {this}
|
|
280
|
-
*/
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
*
|
|
452
|
-
* @returns {
|
|
453
|
-
*/
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* @
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return this;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
*
|
|
488
|
-
* @param {string}
|
|
489
|
-
* @
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
*
|
|
523
|
-
* @
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
*
|
|
535
|
-
* @
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
*
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
*
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
*
|
|
582
|
-
* @param {
|
|
583
|
-
* @
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
*
|
|
600
|
-
* @param {
|
|
601
|
-
* @param {
|
|
602
|
-
* @
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
*
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
*
|
|
629
|
-
* @param {string}
|
|
630
|
-
* @
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
*
|
|
646
|
-
* @param {string}
|
|
647
|
-
* @
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1
|
+
const QueryBuilder = require('./QueryBuilder');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base Model class inspired by Laravel Eloquent
|
|
5
|
+
*/
|
|
6
|
+
class Model {
|
|
7
|
+
static table = '';
|
|
8
|
+
static primaryKey = 'id';
|
|
9
|
+
static timestamps = true;
|
|
10
|
+
static fillable = [];
|
|
11
|
+
static hidden = [];
|
|
12
|
+
static casts = {};
|
|
13
|
+
static connection = null;
|
|
14
|
+
|
|
15
|
+
// Soft Deletes
|
|
16
|
+
static softDeletes = false;
|
|
17
|
+
static DELETED_AT = 'deleted_at';
|
|
18
|
+
|
|
19
|
+
// Scopes
|
|
20
|
+
static globalScopes = {};
|
|
21
|
+
|
|
22
|
+
// Events/Hooks
|
|
23
|
+
static eventListeners = {
|
|
24
|
+
creating: [],
|
|
25
|
+
created: [],
|
|
26
|
+
updating: [],
|
|
27
|
+
updating: [],
|
|
28
|
+
saving: [],
|
|
29
|
+
saved: [],
|
|
30
|
+
deleting: [],
|
|
31
|
+
deleted: [],
|
|
32
|
+
restoring: [],
|
|
33
|
+
restored: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Validation rules
|
|
37
|
+
static rules = {};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensure a default database connection exists.
|
|
41
|
+
* If none is set, it will be initialized from environment (.env) lazily.
|
|
42
|
+
*/
|
|
43
|
+
static ensureConnection() {
|
|
44
|
+
if (!this.connection) {
|
|
45
|
+
// Lazy require to avoid circular dependencies
|
|
46
|
+
const DatabaseConnection = require('./DatabaseConnection');
|
|
47
|
+
this.connection = new DatabaseConnection();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the current database connection
|
|
53
|
+
* @returns {DatabaseConnection}
|
|
54
|
+
*/
|
|
55
|
+
static getConnection() {
|
|
56
|
+
this.ensureConnection();
|
|
57
|
+
return this.connection;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set the default database connection for all models
|
|
62
|
+
* @param {DatabaseConnection} connection
|
|
63
|
+
*/
|
|
64
|
+
static setConnection(connection) {
|
|
65
|
+
this.connection = connection;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set the morph map for polymorphic relations
|
|
70
|
+
* @param {Object} map
|
|
71
|
+
*/
|
|
72
|
+
static setMorphMap(map) {
|
|
73
|
+
this.morphMap = map;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
constructor(attributes = {}) {
|
|
77
|
+
// Auto-initialize connection on first model instantiation if missing
|
|
78
|
+
this.constructor.ensureConnection();
|
|
79
|
+
this.attributes = {};
|
|
80
|
+
this.original = {};
|
|
81
|
+
this.relations = {};
|
|
82
|
+
this.touches = [];
|
|
83
|
+
this.exists = false;
|
|
84
|
+
this._showHidden = false;
|
|
85
|
+
this._withTrashed = false;
|
|
86
|
+
this._onlyTrashed = false;
|
|
87
|
+
this.fill(attributes);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ==================== Events/Hooks ====================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register an event listener
|
|
94
|
+
* @param {string} event - Event name (creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored)
|
|
95
|
+
* @param {Function} callback - Callback function
|
|
96
|
+
*/
|
|
97
|
+
static on(event, callback) {
|
|
98
|
+
if (!this.eventListeners[event]) {
|
|
99
|
+
this.eventListeners[event] = [];
|
|
100
|
+
}
|
|
101
|
+
this.eventListeners[event].push(callback);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fire an event
|
|
106
|
+
* @param {string} event
|
|
107
|
+
* @param {Model} model
|
|
108
|
+
* @returns {Promise<boolean>} - Returns false if event should be cancelled
|
|
109
|
+
*/
|
|
110
|
+
static async fireEvent(event, model) {
|
|
111
|
+
const listeners = this.eventListeners[event] || [];
|
|
112
|
+
for (const listener of listeners) {
|
|
113
|
+
const result = await listener(model);
|
|
114
|
+
if (result === false) {
|
|
115
|
+
return false; // Cancel the operation
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Register a 'creating' event listener
|
|
123
|
+
* @param {Function} callback
|
|
124
|
+
*/
|
|
125
|
+
static creating(callback) {
|
|
126
|
+
this.on('creating', callback);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Register a 'created' event listener
|
|
131
|
+
* @param {Function} callback
|
|
132
|
+
*/
|
|
133
|
+
static created(callback) {
|
|
134
|
+
this.on('created', callback);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Register an 'updating' event listener
|
|
139
|
+
* @param {Function} callback
|
|
140
|
+
*/
|
|
141
|
+
static updating(callback) {
|
|
142
|
+
this.on('updating', callback);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Register an 'updated' event listener
|
|
147
|
+
* @param {Function} callback
|
|
148
|
+
*/
|
|
149
|
+
static updated(callback) {
|
|
150
|
+
this.on('updated', callback);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Register a 'saving' event listener (fires on both create and update)
|
|
155
|
+
* @param {Function} callback
|
|
156
|
+
*/
|
|
157
|
+
static saving(callback) {
|
|
158
|
+
this.on('saving', callback);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Register a 'saved' event listener (fires after both create and update)
|
|
163
|
+
* @param {Function} callback
|
|
164
|
+
*/
|
|
165
|
+
static saved(callback) {
|
|
166
|
+
this.on('saved', callback);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Register a 'deleting' event listener
|
|
171
|
+
* @param {Function} callback
|
|
172
|
+
*/
|
|
173
|
+
static deleting(callback) {
|
|
174
|
+
this.on('deleting', callback);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Register a 'deleted' event listener
|
|
179
|
+
* @param {Function} callback
|
|
180
|
+
*/
|
|
181
|
+
static deleted(callback) {
|
|
182
|
+
this.on('deleted', callback);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Register a 'restoring' event listener
|
|
187
|
+
* @param {Function} callback
|
|
188
|
+
*/
|
|
189
|
+
static restoring(callback) {
|
|
190
|
+
this.on('restoring', callback);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Register a 'restored' event listener
|
|
195
|
+
* @param {Function} callback
|
|
196
|
+
*/
|
|
197
|
+
static restored(callback) {
|
|
198
|
+
this.on('restored', callback);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ==================== Scopes ====================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Add a global scope
|
|
205
|
+
* @param {string} name - Scope name
|
|
206
|
+
* @param {Function} callback - Function that modifies the query builder
|
|
207
|
+
*/
|
|
208
|
+
static addGlobalScope(name, callback) {
|
|
209
|
+
if (!this.globalScopes) {
|
|
210
|
+
this.globalScopes = {};
|
|
211
|
+
}
|
|
212
|
+
this.globalScopes[name] = callback;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Remove a global scope
|
|
217
|
+
* @param {string} name - Scope name
|
|
218
|
+
*/
|
|
219
|
+
static removeGlobalScope(name) {
|
|
220
|
+
if (this.globalScopes && this.globalScopes[name]) {
|
|
221
|
+
delete this.globalScopes[name];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Query without a specific global scope
|
|
227
|
+
* @param {string} name - Scope name to exclude
|
|
228
|
+
* @returns {QueryBuilder}
|
|
229
|
+
*/
|
|
230
|
+
static withoutGlobalScope(name) {
|
|
231
|
+
const query = this.query();
|
|
232
|
+
query._excludedScopes = query._excludedScopes || [];
|
|
233
|
+
query._excludedScopes.push(name);
|
|
234
|
+
return query;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Query without all global scopes
|
|
239
|
+
* @returns {QueryBuilder}
|
|
240
|
+
*/
|
|
241
|
+
static withoutGlobalScopes() {
|
|
242
|
+
const query = this.query();
|
|
243
|
+
query._excludeAllScopes = true;
|
|
244
|
+
return query;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ==================== Soft Deletes ====================
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Query including soft deleted models
|
|
251
|
+
* @returns {QueryBuilder}
|
|
252
|
+
*/
|
|
253
|
+
static withTrashed() {
|
|
254
|
+
const query = this.query();
|
|
255
|
+
query._withTrashed = true;
|
|
256
|
+
return query;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Query only soft deleted models
|
|
261
|
+
* @returns {QueryBuilder}
|
|
262
|
+
*/
|
|
263
|
+
static onlyTrashed() {
|
|
264
|
+
const query = this.query();
|
|
265
|
+
query._onlyTrashed = true;
|
|
266
|
+
return query;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check if model is soft deleted
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
trashed() {
|
|
274
|
+
return this.constructor.softDeletes && this.attributes[this.constructor.DELETED_AT] !== null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Restore a soft deleted model
|
|
279
|
+
* @returns {Promise<this>}
|
|
280
|
+
*/
|
|
281
|
+
async restore() {
|
|
282
|
+
if (!this.constructor.softDeletes) {
|
|
283
|
+
throw new Error('This model does not use soft deletes');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Fire restoring event
|
|
287
|
+
const shouldContinue = await this.constructor.fireEvent('restoring', this);
|
|
288
|
+
if (!shouldContinue) return this;
|
|
289
|
+
|
|
290
|
+
this.setAttribute(this.constructor.DELETED_AT, null);
|
|
291
|
+
|
|
292
|
+
await this.constructor.connection.update(
|
|
293
|
+
this.constructor.table,
|
|
294
|
+
{ [this.constructor.DELETED_AT]: null },
|
|
295
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Fire restored event
|
|
299
|
+
await this.constructor.fireEvent('restored', this);
|
|
300
|
+
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Force delete a soft deleted model (permanent delete)
|
|
306
|
+
* @returns {Promise<boolean>}
|
|
307
|
+
*/
|
|
308
|
+
async forceDelete() {
|
|
309
|
+
// Fire deleting event
|
|
310
|
+
const shouldContinue = await this.constructor.fireEvent('deleting', this);
|
|
311
|
+
if (!shouldContinue) return false;
|
|
312
|
+
|
|
313
|
+
await this.constructor.connection.delete(
|
|
314
|
+
this.constructor.table,
|
|
315
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
this.exists = false;
|
|
319
|
+
|
|
320
|
+
// Fire deleted event
|
|
321
|
+
await this.constructor.fireEvent('deleted', this);
|
|
322
|
+
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ==================== Validation ====================
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Validate the model attributes
|
|
330
|
+
* @returns {Object} - { valid: boolean, errors: Object }
|
|
331
|
+
*/
|
|
332
|
+
validate() {
|
|
333
|
+
const rules = this.constructor.rules;
|
|
334
|
+
const errors = {};
|
|
335
|
+
let valid = true;
|
|
336
|
+
|
|
337
|
+
for (const [field, ruleString] of Object.entries(rules)) {
|
|
338
|
+
const fieldRules = typeof ruleString === 'string' ? ruleString.split('|') : ruleString;
|
|
339
|
+
const value = this.attributes[field];
|
|
340
|
+
|
|
341
|
+
for (const rule of fieldRules) {
|
|
342
|
+
const [ruleName, ruleParam] = rule.split(':');
|
|
343
|
+
const error = this._validateRule(field, value, ruleName, ruleParam);
|
|
344
|
+
if (error) {
|
|
345
|
+
if (!errors[field]) errors[field] = [];
|
|
346
|
+
errors[field].push(error);
|
|
347
|
+
valid = false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { valid, errors };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Validate a single rule
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
_validateRule(field, value, ruleName, ruleParam) {
|
|
360
|
+
switch (ruleName) {
|
|
361
|
+
case 'required':
|
|
362
|
+
if (value === undefined || value === null || value === '') {
|
|
363
|
+
return `${field} is required`;
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
case 'string':
|
|
368
|
+
if (value !== undefined && value !== null && typeof value !== 'string') {
|
|
369
|
+
return `${field} must be a string`;
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
|
|
373
|
+
case 'number':
|
|
374
|
+
case 'numeric':
|
|
375
|
+
if (value !== undefined && value !== null && typeof value !== 'number' && isNaN(Number(value))) {
|
|
376
|
+
return `${field} must be a number`;
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case 'email':
|
|
381
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
382
|
+
return `${field} must be a valid email`;
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case 'min':
|
|
387
|
+
if (typeof value === 'string' && value.length < parseInt(ruleParam, 10)) {
|
|
388
|
+
return `${field} must be at least ${ruleParam} characters`;
|
|
389
|
+
}
|
|
390
|
+
if (typeof value === 'number' && value < parseInt(ruleParam, 10)) {
|
|
391
|
+
return `${field} must be at least ${ruleParam}`;
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
|
|
395
|
+
case 'max':
|
|
396
|
+
if (typeof value === 'string' && value.length > parseInt(ruleParam, 10)) {
|
|
397
|
+
return `${field} must not exceed ${ruleParam} characters`;
|
|
398
|
+
}
|
|
399
|
+
if (typeof value === 'number' && value > parseInt(ruleParam, 10)) {
|
|
400
|
+
return `${field} must not exceed ${ruleParam}`;
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case 'in':
|
|
405
|
+
if (value !== undefined && value !== null) {
|
|
406
|
+
const allowed = ruleParam.split(',');
|
|
407
|
+
if (!allowed.includes(String(value))) {
|
|
408
|
+
return `${field} must be one of: ${ruleParam}`;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
|
|
413
|
+
case 'boolean':
|
|
414
|
+
if (value !== undefined && value !== null && typeof value !== 'boolean') {
|
|
415
|
+
return `${field} must be a boolean`;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case 'date':
|
|
420
|
+
if (value !== undefined && value !== null && isNaN(Date.parse(value))) {
|
|
421
|
+
return `${field} must be a valid date`;
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case 'regex':
|
|
426
|
+
if (value && !new RegExp(ruleParam).test(value)) {
|
|
427
|
+
return `${field} format is invalid`;
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Validate and throw if invalid
|
|
437
|
+
* @throws {Error}
|
|
438
|
+
*/
|
|
439
|
+
validateOrFail() {
|
|
440
|
+
const { valid, errors } = this.validate();
|
|
441
|
+
if (!valid) {
|
|
442
|
+
const error = new Error('Validation failed');
|
|
443
|
+
error.errors = errors;
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ==================== Query Builder ====================
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Begin querying the model
|
|
452
|
+
* @returns {QueryBuilder}
|
|
453
|
+
*/
|
|
454
|
+
static query() {
|
|
455
|
+
// Ensure a connection exists even when using static APIs without instantiation
|
|
456
|
+
this.ensureConnection();
|
|
457
|
+
return new QueryBuilder(this);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Get all records
|
|
462
|
+
* @returns {Promise<Array<Model>>}
|
|
463
|
+
*/
|
|
464
|
+
static all() {
|
|
465
|
+
return this.query().get();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Find a model by its primary key
|
|
470
|
+
* @param {any} id
|
|
471
|
+
* @returns {Promise<Model|null>}
|
|
472
|
+
*/
|
|
473
|
+
static find(id) {
|
|
474
|
+
return this.query().where(this.primaryKey, id).first();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Find a model by its primary key or throw an error
|
|
479
|
+
* @param {any} id
|
|
480
|
+
* @returns {Promise<Model>}
|
|
481
|
+
*/
|
|
482
|
+
static findOrFail(id) {
|
|
483
|
+
return this.query().where(this.primaryKey, id).firstOrFail();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Add a where clause
|
|
488
|
+
* @param {string} column
|
|
489
|
+
* @param {string|any} operator
|
|
490
|
+
* @param {any} value
|
|
491
|
+
* @returns {QueryBuilder}
|
|
492
|
+
*/
|
|
493
|
+
static where(column, operator, value) {
|
|
494
|
+
if (arguments.length === 2) {
|
|
495
|
+
value = operator;
|
|
496
|
+
operator = '=';
|
|
497
|
+
}
|
|
498
|
+
return this.query().where(column, operator, value);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Create a new model and save it
|
|
503
|
+
* @param {Object} attributes
|
|
504
|
+
* @returns {Promise<Model>}
|
|
505
|
+
*/
|
|
506
|
+
static create(attributes) {
|
|
507
|
+
const instance = new this(attributes);
|
|
508
|
+
return instance.save();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Insert data without creating model instances
|
|
513
|
+
* @param {Object|Array<Object>} data
|
|
514
|
+
* @returns {Promise<any>}
|
|
515
|
+
*/
|
|
516
|
+
static async insert(data) {
|
|
517
|
+
const query = this.query();
|
|
518
|
+
return query.insert(data);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Update records
|
|
523
|
+
* @param {Object} attributes
|
|
524
|
+
* @returns {Promise<any>}
|
|
525
|
+
*/
|
|
526
|
+
static async update(attributes) {
|
|
527
|
+
return this.query().update(attributes);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Update by primary key and fetch the updated model (optionally with relations)
|
|
532
|
+
* @param {any} id
|
|
533
|
+
* @param {Object} attributes
|
|
534
|
+
* @param {string[]} [relations]
|
|
535
|
+
* @returns {Promise<Model|null>}
|
|
536
|
+
*/
|
|
537
|
+
static async updateAndFetchById(id, attributes, relations = []) {
|
|
538
|
+
await this.query().where(this.primaryKey, id).update(attributes);
|
|
539
|
+
const qb = this.query().where(this.primaryKey, id);
|
|
540
|
+
if (relations && relations.length) qb.with(...relations);
|
|
541
|
+
return qb.first();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Update by primary key only (convenience)
|
|
546
|
+
* @param {any} id
|
|
547
|
+
* @param {Object} attributes
|
|
548
|
+
* @returns {Promise<any>}
|
|
549
|
+
*/
|
|
550
|
+
static async updateById(id, attributes) {
|
|
551
|
+
return this.query().where(this.primaryKey, id).update(attributes);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Delete records
|
|
556
|
+
* @returns {Promise<any>}
|
|
557
|
+
*/
|
|
558
|
+
static async delete() {
|
|
559
|
+
return this.query().delete();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Get the first record
|
|
564
|
+
* @returns {Promise<Model|null>}
|
|
565
|
+
*/
|
|
566
|
+
static first() {
|
|
567
|
+
return this.query().first();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Add an order by clause
|
|
572
|
+
* @param {string} column
|
|
573
|
+
* @param {string} direction
|
|
574
|
+
* @returns {QueryBuilder}
|
|
575
|
+
*/
|
|
576
|
+
static orderBy(column, direction = 'asc') {
|
|
577
|
+
return this.query().orderBy(column, direction);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Limit the number of results
|
|
582
|
+
* @param {number} value
|
|
583
|
+
* @returns {QueryBuilder}
|
|
584
|
+
*/
|
|
585
|
+
static limit(value) {
|
|
586
|
+
return this.query().limit(value);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Offset the results
|
|
591
|
+
* @param {number} value
|
|
592
|
+
* @returns {QueryBuilder}
|
|
593
|
+
*/
|
|
594
|
+
static offset(value) {
|
|
595
|
+
return this.query().offset(value);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Paginate the results
|
|
600
|
+
* @param {number} page
|
|
601
|
+
* @param {number} perPage
|
|
602
|
+
* @returns {Promise<Object>}
|
|
603
|
+
*/
|
|
604
|
+
static paginate(page = 1, perPage = 15) {
|
|
605
|
+
return this.query().paginate(page, perPage);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Add a where in clause
|
|
610
|
+
* @param {string} column
|
|
611
|
+
* @param {Array} values
|
|
612
|
+
* @returns {QueryBuilder}
|
|
613
|
+
*/
|
|
614
|
+
static whereIn(column, values) {
|
|
615
|
+
return this.query().whereIn(column, values);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Add a where null clause
|
|
620
|
+
* @param {string} column
|
|
621
|
+
* @returns {QueryBuilder}
|
|
622
|
+
*/
|
|
623
|
+
static whereNull(column) {
|
|
624
|
+
return this.query().whereNull(column);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Add a where not null clause
|
|
629
|
+
* @param {string} column
|
|
630
|
+
* @returns {QueryBuilder}
|
|
631
|
+
*/
|
|
632
|
+
static whereNotNull(column) {
|
|
633
|
+
return this.query().whereNotNull(column);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Count records
|
|
638
|
+
* @returns {Promise<number>}
|
|
639
|
+
*/
|
|
640
|
+
static count() {
|
|
641
|
+
return this.query().count();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Eager load relations on the query
|
|
646
|
+
* @param {...string} relations
|
|
647
|
+
* @returns {QueryBuilder}
|
|
648
|
+
*/
|
|
649
|
+
static with(...relations) {
|
|
650
|
+
return this.query().with(...relations);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Include hidden attributes in query results
|
|
655
|
+
* @returns {QueryBuilder}
|
|
656
|
+
*/
|
|
657
|
+
static withHidden() {
|
|
658
|
+
const query = this.query();
|
|
659
|
+
query._showHidden = true;
|
|
660
|
+
return query;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Control visibility of hidden attributes in query results
|
|
665
|
+
* @param {boolean} show - If false (default), hidden attributes will be hidden. If true, they will be shown.
|
|
666
|
+
* @returns {QueryBuilder}
|
|
667
|
+
*/
|
|
668
|
+
static withoutHidden(show = false) {
|
|
669
|
+
const query = this.query();
|
|
670
|
+
query._showHidden = show;
|
|
671
|
+
return query;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ==================== Instance Methods ====================
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Fill the model with attributes
|
|
678
|
+
* @param {Object} attributes
|
|
679
|
+
* @returns {this}
|
|
680
|
+
*/
|
|
681
|
+
fill(attributes) {
|
|
682
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
683
|
+
if (this.constructor.fillable.length === 0 || this.constructor.fillable.includes(key)) {
|
|
684
|
+
this.setAttribute(key, value);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Set an attribute
|
|
692
|
+
* @param {string} key
|
|
693
|
+
* @param {any} value
|
|
694
|
+
* @returns {this}
|
|
695
|
+
*/
|
|
696
|
+
setAttribute(key, value) {
|
|
697
|
+
this.attributes[key] = this.castAttribute(key, value);
|
|
698
|
+
return this;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Get an attribute
|
|
703
|
+
* @param {string} key
|
|
704
|
+
* @returns {any}
|
|
705
|
+
*/
|
|
706
|
+
getAttribute(key) {
|
|
707
|
+
if (this.relations[key]) {
|
|
708
|
+
return this.relations[key];
|
|
709
|
+
}
|
|
710
|
+
return this.castAttribute(key, this.attributes[key]);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Cast an attribute to the proper type
|
|
715
|
+
* @param {string} key
|
|
716
|
+
* @param {any} value
|
|
717
|
+
* @returns {any}
|
|
718
|
+
*/
|
|
719
|
+
castAttribute(key, value) {
|
|
720
|
+
const cast = this.constructor.casts[key];
|
|
721
|
+
if (!cast || value === null || value === undefined) return value;
|
|
722
|
+
|
|
723
|
+
switch (cast) {
|
|
724
|
+
case 'int':
|
|
725
|
+
case 'integer':
|
|
726
|
+
return parseInt(value, 10);
|
|
727
|
+
case 'float':
|
|
728
|
+
case 'double':
|
|
729
|
+
return parseFloat(value);
|
|
730
|
+
case 'string':
|
|
731
|
+
return String(value);
|
|
732
|
+
case 'bool':
|
|
733
|
+
case 'boolean':
|
|
734
|
+
return Boolean(value);
|
|
735
|
+
case 'array':
|
|
736
|
+
case 'json':
|
|
737
|
+
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
738
|
+
case 'date':
|
|
739
|
+
return value instanceof Date ? value : new Date(value);
|
|
740
|
+
default:
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Save the model
|
|
747
|
+
* @returns {Promise<this>}
|
|
748
|
+
*/
|
|
749
|
+
async save() {
|
|
750
|
+
// Fire saving event
|
|
751
|
+
const shouldContinue = await this.constructor.fireEvent('saving', this);
|
|
752
|
+
if (!shouldContinue) return this;
|
|
753
|
+
|
|
754
|
+
let result;
|
|
755
|
+
if (this.exists) {
|
|
756
|
+
result = await this.performUpdate();
|
|
757
|
+
} else {
|
|
758
|
+
result = await this.performInsert();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Fire saved event
|
|
762
|
+
await this.constructor.fireEvent('saved', this);
|
|
763
|
+
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Perform an insert operation
|
|
769
|
+
* @returns {Promise<this>}
|
|
770
|
+
*/
|
|
771
|
+
async performInsert() {
|
|
772
|
+
// Fire creating event
|
|
773
|
+
const shouldContinue = await this.constructor.fireEvent('creating', this);
|
|
774
|
+
if (!shouldContinue) return this;
|
|
775
|
+
|
|
776
|
+
if (this.constructor.timestamps) {
|
|
777
|
+
const now = new Date();
|
|
778
|
+
this.setAttribute('created_at', now);
|
|
779
|
+
this.setAttribute('updated_at', now);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const data = this.attributes;
|
|
783
|
+
const result = await this.constructor.connection.insert(this.constructor.table, data);
|
|
784
|
+
|
|
785
|
+
this.setAttribute(this.constructor.primaryKey, result.insertId);
|
|
786
|
+
this.exists = true;
|
|
787
|
+
this.original = { ...this.attributes };
|
|
788
|
+
|
|
789
|
+
await this.touchParents();
|
|
790
|
+
|
|
791
|
+
// Fire created event
|
|
792
|
+
await this.constructor.fireEvent('created', this);
|
|
793
|
+
|
|
794
|
+
return this;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Perform an update operation
|
|
799
|
+
* @returns {Promise<this>}
|
|
800
|
+
*/
|
|
801
|
+
async performUpdate() {
|
|
802
|
+
// Fire updating event
|
|
803
|
+
const shouldContinue = await this.constructor.fireEvent('updating', this);
|
|
804
|
+
if (!shouldContinue) return this;
|
|
805
|
+
|
|
806
|
+
if (this.constructor.timestamps) {
|
|
807
|
+
this.setAttribute('updated_at', new Date());
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const dirty = this.getDirty();
|
|
811
|
+
if (Object.keys(dirty).length === 0) {
|
|
812
|
+
return this;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
await this.constructor.connection.update(
|
|
816
|
+
this.constructor.table,
|
|
817
|
+
dirty,
|
|
818
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
this.original = { ...this.attributes };
|
|
822
|
+
|
|
823
|
+
await this.touchParents();
|
|
824
|
+
|
|
825
|
+
// Fire updated event
|
|
826
|
+
await this.constructor.fireEvent('updated', this);
|
|
827
|
+
|
|
828
|
+
return this;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Touch parent models for belongsTo relations with touches enabled
|
|
833
|
+
* @returns {Promise<void>}
|
|
834
|
+
*/
|
|
835
|
+
async touchParents() {
|
|
836
|
+
for (const relation of this.touches) {
|
|
837
|
+
if (relation.touchesParent) {
|
|
838
|
+
const foreignKeyValue = this.getAttribute(relation.foreignKey);
|
|
839
|
+
if (foreignKeyValue) {
|
|
840
|
+
await this.constructor.connection.update(
|
|
841
|
+
relation.related.table,
|
|
842
|
+
{ updated_at: new Date() },
|
|
843
|
+
{ wheres: [{ type: 'basic', column: relation.ownerKey, operator: '=', value: foreignKeyValue }] }
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Delete the model (soft delete if enabled)
|
|
852
|
+
* @returns {Promise<boolean>}
|
|
853
|
+
*/
|
|
854
|
+
async destroy() {
|
|
855
|
+
if (!this.exists) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Fire deleting event
|
|
860
|
+
const shouldContinue = await this.constructor.fireEvent('deleting', this);
|
|
861
|
+
if (!shouldContinue) return false;
|
|
862
|
+
|
|
863
|
+
// Soft delete if enabled
|
|
864
|
+
if (this.constructor.softDeletes) {
|
|
865
|
+
this.setAttribute(this.constructor.DELETED_AT, new Date());
|
|
866
|
+
await this.constructor.connection.update(
|
|
867
|
+
this.constructor.table,
|
|
868
|
+
{ [this.constructor.DELETED_AT]: new Date() },
|
|
869
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
870
|
+
);
|
|
871
|
+
} else {
|
|
872
|
+
await this.constructor.connection.delete(
|
|
873
|
+
this.constructor.table,
|
|
874
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
875
|
+
);
|
|
876
|
+
this.exists = false;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Fire deleted event
|
|
880
|
+
await this.constructor.fireEvent('deleted', this);
|
|
881
|
+
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Get the attributes that have been changed
|
|
887
|
+
* @returns {Object}
|
|
888
|
+
*/
|
|
889
|
+
getDirty() {
|
|
890
|
+
const dirty = {};
|
|
891
|
+
for (const [key, value] of Object.entries(this.attributes)) {
|
|
892
|
+
if (JSON.stringify(value) !== JSON.stringify(this.original[key])) {
|
|
893
|
+
dirty[key] = value;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return dirty;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Check if the model has been modified
|
|
901
|
+
* @returns {boolean}
|
|
902
|
+
*/
|
|
903
|
+
isDirty() {
|
|
904
|
+
return Object.keys(this.getDirty()).length > 0;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Convert the model to JSON
|
|
909
|
+
* @returns {Object}
|
|
910
|
+
*/
|
|
911
|
+
toJSON() {
|
|
912
|
+
const json = { ...this.attributes };
|
|
913
|
+
|
|
914
|
+
// Hide specified attributes unless _showHidden is true
|
|
915
|
+
if (!this._showHidden) {
|
|
916
|
+
this.constructor.hidden.forEach(key => {
|
|
917
|
+
delete json[key];
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Add relations
|
|
922
|
+
Object.assign(json, this.relations);
|
|
923
|
+
|
|
924
|
+
return json;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Load one or multiple relations on this model instance.
|
|
929
|
+
* Supports dot-notation for nested relations (e.g., 'posts.comments').
|
|
930
|
+
* @param {...string|Array<string>} relations
|
|
931
|
+
* @returns {Promise<this>}
|
|
932
|
+
*/
|
|
933
|
+
async load(...relations) {
|
|
934
|
+
const list = relations.length === 1 && Array.isArray(relations[0])
|
|
935
|
+
? relations[0]
|
|
936
|
+
: relations;
|
|
937
|
+
|
|
938
|
+
for (const rel of list) {
|
|
939
|
+
if (typeof rel !== 'string' || !rel) continue;
|
|
940
|
+
await this._loadRelationPath(rel);
|
|
941
|
+
}
|
|
942
|
+
return this;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Internal: load a relation path with optional nesting (a.b.c)
|
|
947
|
+
* @param {string} path
|
|
948
|
+
* @private
|
|
949
|
+
*/
|
|
950
|
+
async _loadRelationPath(path) {
|
|
951
|
+
const segments = path.split('.');
|
|
952
|
+
const head = segments[0];
|
|
953
|
+
const tail = segments.slice(1).join('.');
|
|
954
|
+
|
|
955
|
+
const relationFn = this[head];
|
|
956
|
+
if (typeof relationFn !== 'function') return;
|
|
957
|
+
|
|
958
|
+
const relation = relationFn.call(this);
|
|
959
|
+
if (!relation || typeof relation.get !== 'function') return;
|
|
960
|
+
|
|
961
|
+
const value = await relation.get();
|
|
962
|
+
this.relations[head] = value;
|
|
963
|
+
|
|
964
|
+
if (tail) {
|
|
965
|
+
if (Array.isArray(value)) {
|
|
966
|
+
await Promise.all(
|
|
967
|
+
value.map(v => (v && typeof v.load === 'function') ? v.load(tail) : null)
|
|
968
|
+
);
|
|
969
|
+
} else if (value && typeof value.load === 'function') {
|
|
970
|
+
await value.load(tail);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ==================== Relationships ====================
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Define a one-to-one relationship
|
|
979
|
+
* @param {typeof Model} related
|
|
980
|
+
* @param {string} foreignKey
|
|
981
|
+
* @param {string} localKey
|
|
982
|
+
* @returns {HasOneRelation}
|
|
983
|
+
*/
|
|
984
|
+
hasOne(related, foreignKey, localKey) {
|
|
985
|
+
const HasOneRelation = require('./Relations/HasOneRelation');
|
|
986
|
+
localKey = localKey || this.constructor.primaryKey;
|
|
987
|
+
foreignKey = foreignKey || `${this.constructor.table.slice(0, -1)}_id`;
|
|
988
|
+
|
|
989
|
+
return new HasOneRelation(this, related, foreignKey, localKey);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Define a one-to-many relationship
|
|
994
|
+
* @param {typeof Model} related
|
|
995
|
+
* @param {string} foreignKey
|
|
996
|
+
* @param {string} localKey
|
|
997
|
+
* @returns {HasManyRelation}
|
|
998
|
+
*/
|
|
999
|
+
hasMany(related, foreignKey, localKey) {
|
|
1000
|
+
const HasManyRelation = require('./Relations/HasManyRelation');
|
|
1001
|
+
localKey = localKey || this.constructor.primaryKey;
|
|
1002
|
+
foreignKey = foreignKey || `${this.constructor.table.slice(0, -1)}_id`;
|
|
1003
|
+
|
|
1004
|
+
return new HasManyRelation(this, related, foreignKey, localKey);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Define an inverse one-to-one or many relationship
|
|
1009
|
+
* @param {typeof Model} related
|
|
1010
|
+
* @param {string} foreignKey
|
|
1011
|
+
* @param {string} ownerKey
|
|
1012
|
+
* @returns {BelongsToRelation}
|
|
1013
|
+
*/
|
|
1014
|
+
belongsTo(related, foreignKey, ownerKey) {
|
|
1015
|
+
const BelongsToRelation = require('./Relations/BelongsToRelation');
|
|
1016
|
+
ownerKey = ownerKey || related.primaryKey;
|
|
1017
|
+
foreignKey = foreignKey || `${related.table.slice(0, -1)}_id`;
|
|
1018
|
+
|
|
1019
|
+
return new BelongsToRelation(this, related, foreignKey, ownerKey);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Define a many-to-many relationship
|
|
1024
|
+
* @param {typeof Model} related
|
|
1025
|
+
* @param {string} pivot
|
|
1026
|
+
* @param {string} foreignPivotKey
|
|
1027
|
+
* @param {string} relatedPivotKey
|
|
1028
|
+
* @param {string} parentKey
|
|
1029
|
+
* @param {string} relatedKey
|
|
1030
|
+
* @returns {BelongsToManyRelation}
|
|
1031
|
+
*/
|
|
1032
|
+
belongsToMany(related, pivot, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
|
|
1033
|
+
const BelongsToManyRelation = require('./Relations/BelongsToManyRelation');
|
|
1034
|
+
return new BelongsToManyRelation(
|
|
1035
|
+
this, related, pivot, foreignPivotKey, relatedPivotKey, parentKey, relatedKey
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Define a has-many-through relationship
|
|
1041
|
+
* @param {typeof Model} relatedFinal
|
|
1042
|
+
* @param {typeof Model} through
|
|
1043
|
+
* @param {string} [foreignKeyOnThrough]
|
|
1044
|
+
* @param {string} [throughKeyOnFinal]
|
|
1045
|
+
* @param {string} [localKey]
|
|
1046
|
+
* @param {string} [throughLocalKey]
|
|
1047
|
+
* @returns {HasManyThroughRelation}
|
|
1048
|
+
*/
|
|
1049
|
+
hasManyThrough(relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey) {
|
|
1050
|
+
const HasManyThroughRelation = require('./Relations/HasManyThroughRelation');
|
|
1051
|
+
return new HasManyThroughRelation(
|
|
1052
|
+
this, relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Define a has-one-through relationship
|
|
1058
|
+
* @param {typeof Model} relatedFinal
|
|
1059
|
+
* @param {typeof Model} through
|
|
1060
|
+
* @param {string} [foreignKeyOnThrough]
|
|
1061
|
+
* @param {string} [throughKeyOnFinal]
|
|
1062
|
+
* @param {string} [localKey]
|
|
1063
|
+
* @param {string} [throughLocalKey]
|
|
1064
|
+
* @returns {HasOneThroughRelation}
|
|
1065
|
+
*/
|
|
1066
|
+
hasOneThrough(relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey) {
|
|
1067
|
+
const HasOneThroughRelation = require('./Relations/HasOneThroughRelation');
|
|
1068
|
+
return new HasOneThroughRelation(
|
|
1069
|
+
this, relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Define a polymorphic inverse relationship
|
|
1075
|
+
* @param {string} name
|
|
1076
|
+
* @param {string} [typeColumn]
|
|
1077
|
+
* @param {string} [idColumn]
|
|
1078
|
+
* @returns {MorphToRelation}
|
|
1079
|
+
*/
|
|
1080
|
+
morphTo(name, typeColumn, idColumn) {
|
|
1081
|
+
const MorphToRelation = require('./Relations/MorphToRelation');
|
|
1082
|
+
return new MorphToRelation(this, name, typeColumn, idColumn);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Define a polymorphic one-to-one relationship
|
|
1087
|
+
* @param {typeof Model} related
|
|
1088
|
+
* @param {string} morphType
|
|
1089
|
+
* @param {string} [foreignKey]
|
|
1090
|
+
* @param {string} [localKey]
|
|
1091
|
+
* @returns {MorphOneRelation}
|
|
1092
|
+
*/
|
|
1093
|
+
morphOne(related, morphType, foreignKey, localKey) {
|
|
1094
|
+
const MorphOneRelation = require('./Relations/MorphOneRelation');
|
|
1095
|
+
localKey = localKey || this.constructor.primaryKey;
|
|
1096
|
+
foreignKey = foreignKey || `${morphType}_id`;
|
|
1097
|
+
|
|
1098
|
+
return new MorphOneRelation(this, related, morphType, foreignKey, localKey);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Define a polymorphic one-to-many relationship
|
|
1103
|
+
* @param {typeof Model} related
|
|
1104
|
+
* @param {string} morphType
|
|
1105
|
+
* @param {string} [foreignKey]
|
|
1106
|
+
* @param {string} [localKey]
|
|
1107
|
+
* @returns {MorphManyRelation}
|
|
1108
|
+
*/
|
|
1109
|
+
morphMany(related, morphType, foreignKey, localKey) {
|
|
1110
|
+
const MorphManyRelation = require('./Relations/MorphManyRelation');
|
|
1111
|
+
localKey = localKey || this.constructor.primaryKey;
|
|
1112
|
+
foreignKey = foreignKey || `${morphType}_id`;
|
|
1113
|
+
|
|
1114
|
+
return new MorphManyRelation(this, related, morphType, foreignKey, localKey);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
module.exports = Model;
|