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/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
- * Ensure a default database connection exists.
17
- * If none is set, it will be initialized from environment (.env) lazily.
18
- */
19
- static ensureConnection() {
20
- if (!this.connection) {
21
- // Lazy require to avoid circular dependencies
22
- const DatabaseConnection = require('./DatabaseConnection');
23
- this.connection = new DatabaseConnection();
24
- }
25
- }
26
-
27
- /**
28
- * Set the default database connection for all models
29
- * @param {DatabaseConnection} connection
30
- */
31
- static setConnection(connection) {
32
- this.connection = connection;
33
- }
34
-
35
- /**
36
- * Set the morph map for polymorphic relations
37
- * @param {Object} map
38
- */
39
- static setMorphMap(map) {
40
- this.morphMap = map;
41
- }
42
-
43
- constructor(attributes = {}) {
44
- // Auto-initialize connection on first model instantiation if missing
45
- this.constructor.ensureConnection();
46
- this.attributes = {};
47
- this.original = {};
48
- this.relations = {};
49
- this.touches = [];
50
- this.exists = false;
51
- this.fill(attributes);
52
- }
53
-
54
- // ==================== Query Builder ====================
55
-
56
- /**
57
- * Begin querying the model
58
- * @returns {QueryBuilder}
59
- */
60
- static query() {
61
- // Ensure a connection exists even when using static APIs without instantiation
62
- this.ensureConnection();
63
- return new QueryBuilder(this);
64
- }
65
-
66
- /**
67
- * Get all records
68
- * @returns {Promise<Array<Model>>}
69
- */
70
- static all() {
71
- return this.query().get();
72
- }
73
-
74
- /**
75
- * Find a model by its primary key
76
- * @param {any} id
77
- * @returns {Promise<Model|null>}
78
- */
79
- static find(id) {
80
- return this.query().where(this.primaryKey, id).first();
81
- }
82
-
83
- /**
84
- * Find a model by its primary key or throw an error
85
- * @param {any} id
86
- * @returns {Promise<Model>}
87
- */
88
- static findOrFail(id) {
89
- return this.query().where(this.primaryKey, id).firstOrFail();
90
- }
91
-
92
- /**
93
- * Add a where clause
94
- * @param {string} column
95
- * @param {string|any} operator
96
- * @param {any} value
97
- * @returns {QueryBuilder}
98
- */
99
- static where(column, operator, value) {
100
- if (arguments.length === 2) {
101
- value = operator;
102
- operator = '=';
103
- }
104
- return this.query().where(column, operator, value);
105
- }
106
-
107
- /**
108
- * Create a new model and save it
109
- * @param {Object} attributes
110
- * @returns {Promise<Model>}
111
- */
112
- static create(attributes) {
113
- const instance = new this(attributes);
114
- return instance.save();
115
- }
116
-
117
- /**
118
- * Insert data without creating model instances
119
- * @param {Object|Array<Object>} data
120
- * @returns {Promise<any>}
121
- */
122
- static async insert(data) {
123
- const query = this.query();
124
- return query.insert(data);
125
- }
126
-
127
- /**
128
- * Update records
129
- * @param {Object} attributes
130
- * @returns {Promise<any>}
131
- */
132
- static async update(attributes) {
133
- return this.query().update(attributes);
134
- }
135
-
136
- /**
137
- * Update by primary key and fetch the updated model (optionally with relations)
138
- * @param {any} id
139
- * @param {Object} attributes
140
- * @param {string[]} [relations]
141
- * @returns {Promise<Model|null>}
142
- */
143
- static async updateAndFetchById(id, attributes, relations = []) {
144
- await this.query().where(this.primaryKey, id).update(attributes);
145
- const qb = this.query().where(this.primaryKey, id);
146
- if (relations && relations.length) qb.with(...relations);
147
- return qb.first();
148
- }
149
-
150
- /**
151
- * Update by primary key only (convenience)
152
- * @param {any} id
153
- * @param {Object} attributes
154
- * @returns {Promise<any>}
155
- */
156
- static async updateById(id, attributes) {
157
- return this.query().where(this.primaryKey, id).update(attributes);
158
- }
159
-
160
- /**
161
- * Delete records
162
- * @returns {Promise<any>}
163
- */
164
- static async delete() {
165
- return this.query().delete();
166
- }
167
-
168
- /**
169
- * Get the first record
170
- * @returns {Promise<Model|null>}
171
- */
172
- static first() {
173
- return this.query().first();
174
- }
175
-
176
- /**
177
- * Add an order by clause
178
- * @param {string} column
179
- * @param {string} direction
180
- * @returns {QueryBuilder}
181
- */
182
- static orderBy(column, direction = 'asc') {
183
- return this.query().orderBy(column, direction);
184
- }
185
-
186
- /**
187
- * Limit the number of results
188
- * @param {number} value
189
- * @returns {QueryBuilder}
190
- */
191
- static limit(value) {
192
- return this.query().limit(value);
193
- }
194
-
195
- /**
196
- * Offset the results
197
- * @param {number} value
198
- * @returns {QueryBuilder}
199
- */
200
- static offset(value) {
201
- return this.query().offset(value);
202
- }
203
-
204
- /**
205
- * Paginate the results
206
- * @param {number} page
207
- * @param {number} perPage
208
- * @returns {Promise<Object>}
209
- */
210
- static paginate(page = 1, perPage = 15) {
211
- return this.query().paginate(page, perPage);
212
- }
213
-
214
- /**
215
- * Add a where in clause
216
- * @param {string} column
217
- * @param {Array} values
218
- * @returns {QueryBuilder}
219
- */
220
- static whereIn(column, values) {
221
- return this.query().whereIn(column, values);
222
- }
223
-
224
- /**
225
- * Add a where null clause
226
- * @param {string} column
227
- * @returns {QueryBuilder}
228
- */
229
- static whereNull(column) {
230
- return this.query().whereNull(column);
231
- }
232
-
233
- /**
234
- * Add a where not null clause
235
- * @param {string} column
236
- * @returns {QueryBuilder}
237
- */
238
- static whereNotNull(column) {
239
- return this.query().whereNotNull(column);
240
- }
241
-
242
- /**
243
- * Count records
244
- * @returns {Promise<number>}
245
- */
246
- static count() {
247
- return this.query().count();
248
- }
249
-
250
- /**
251
- * Eager load relations on the query
252
- * @param {...string} relations
253
- * @returns {QueryBuilder}
254
- */
255
- static with(...relations) {
256
- return this.query().with(...relations);
257
- }
258
-
259
- // ==================== Instance Methods ====================
260
-
261
- /**
262
- * Fill the model with attributes
263
- * @param {Object} attributes
264
- * @returns {this}
265
- */
266
- fill(attributes) {
267
- for (const [key, value] of Object.entries(attributes)) {
268
- if (this.constructor.fillable.length === 0 || this.constructor.fillable.includes(key)) {
269
- this.setAttribute(key, value);
270
- }
271
- }
272
- return this;
273
- }
274
-
275
- /**
276
- * Set an attribute
277
- * @param {string} key
278
- * @param {any} value
279
- * @returns {this}
280
- */
281
- setAttribute(key, value) {
282
- this.attributes[key] = this.castAttribute(key, value);
283
- return this;
284
- }
285
-
286
- /**
287
- * Get an attribute
288
- * @param {string} key
289
- * @returns {any}
290
- */
291
- getAttribute(key) {
292
- if (this.relations[key]) {
293
- return this.relations[key];
294
- }
295
- return this.castAttribute(key, this.attributes[key]);
296
- }
297
-
298
- /**
299
- * Cast an attribute to the proper type
300
- * @param {string} key
301
- * @param {any} value
302
- * @returns {any}
303
- */
304
- castAttribute(key, value) {
305
- const cast = this.constructor.casts[key];
306
- if (!cast || value === null || value === undefined) return value;
307
-
308
- switch (cast) {
309
- case 'int':
310
- case 'integer':
311
- return parseInt(value, 10);
312
- case 'float':
313
- case 'double':
314
- return parseFloat(value);
315
- case 'string':
316
- return String(value);
317
- case 'bool':
318
- case 'boolean':
319
- return Boolean(value);
320
- case 'array':
321
- case 'json':
322
- return typeof value === 'string' ? JSON.parse(value) : value;
323
- case 'date':
324
- return value instanceof Date ? value : new Date(value);
325
- default:
326
- return value;
327
- }
328
- }
329
-
330
- /**
331
- * Save the model
332
- * @returns {Promise<this>}
333
- */
334
- async save() {
335
- if (this.exists) {
336
- return this.performUpdate();
337
- }
338
- return this.performInsert();
339
- }
340
-
341
- /**
342
- * Perform an insert operation
343
- * @returns {Promise<this>}
344
- */
345
- async performInsert() {
346
- if (this.constructor.timestamps) {
347
- const now = new Date();
348
- this.setAttribute('created_at', now);
349
- this.setAttribute('updated_at', now);
350
- }
351
-
352
- const data = this.attributes;
353
- const result = await this.constructor.connection.insert(this.constructor.table, data);
354
-
355
- this.setAttribute(this.constructor.primaryKey, result.insertId);
356
- this.exists = true;
357
- this.original = { ...this.attributes };
358
-
359
- await this.touchParents();
360
-
361
- return this;
362
- }
363
-
364
- /**
365
- * Perform an update operation
366
- * @returns {Promise<this>}
367
- */
368
- async performUpdate() {
369
- if (this.constructor.timestamps) {
370
- this.setAttribute('updated_at', new Date());
371
- }
372
-
373
- const dirty = this.getDirty();
374
- if (Object.keys(dirty).length === 0) {
375
- return this;
376
- }
377
-
378
- await this.constructor.connection.update(
379
- this.constructor.table,
380
- dirty,
381
- { [this.constructor.primaryKey]: this.getAttribute(this.constructor.primaryKey) }
382
- );
383
-
384
- this.original = { ...this.attributes };
385
-
386
- await this.touchParents();
387
-
388
- return this;
389
- }
390
-
391
- /**
392
- * Touch parent models for belongsTo relations with touches enabled
393
- * @returns {Promise<void>}
394
- */
395
- async touchParents() {
396
- for (const relation of this.touches) {
397
- if (relation.touchesParent) {
398
- const foreignKeyValue = this.getAttribute(relation.foreignKey);
399
- if (foreignKeyValue) {
400
- await this.constructor.connection.update(
401
- relation.related.table,
402
- { updated_at: new Date() },
403
- { [relation.ownerKey]: foreignKeyValue }
404
- );
405
- }
406
- }
407
- }
408
- }
409
-
410
- /**
411
- * Delete the model
412
- * @returns {Promise<boolean>}
413
- */
414
- async destroy() {
415
- if (!this.exists) {
416
- return false;
417
- }
418
-
419
- await this.constructor.connection.delete(
420
- this.constructor.table,
421
- { [this.constructor.primaryKey]: this.getAttribute(this.constructor.primaryKey) }
422
- );
423
-
424
- this.exists = false;
425
- return true;
426
- }
427
-
428
- /**
429
- * Get the attributes that have been changed
430
- * @returns {Object}
431
- */
432
- getDirty() {
433
- const dirty = {};
434
- for (const [key, value] of Object.entries(this.attributes)) {
435
- if (JSON.stringify(value) !== JSON.stringify(this.original[key])) {
436
- dirty[key] = value;
437
- }
438
- }
439
- return dirty;
440
- }
441
-
442
- /**
443
- * Check if the model has been modified
444
- * @returns {boolean}
445
- */
446
- isDirty() {
447
- return Object.keys(this.getDirty()).length > 0;
448
- }
449
-
450
- /**
451
- * Convert the model to JSON
452
- * @returns {Object}
453
- */
454
- toJSON() {
455
- const json = { ...this.attributes };
456
-
457
- // Hide specified attributes
458
- this.constructor.hidden.forEach(key => {
459
- delete json[key];
460
- });
461
-
462
- // Add relations
463
- Object.assign(json, this.relations);
464
-
465
- return json;
466
- }
467
-
468
- /**
469
- * Load one or multiple relations on this model instance.
470
- * Supports dot-notation for nested relations (e.g., 'posts.comments').
471
- * @param {...string|Array<string>} relations
472
- * @returns {Promise<this>}
473
- */
474
- async load(...relations) {
475
- const list = relations.length === 1 && Array.isArray(relations[0])
476
- ? relations[0]
477
- : relations;
478
-
479
- for (const rel of list) {
480
- if (typeof rel !== 'string' || !rel) continue;
481
- await this._loadRelationPath(rel);
482
- }
483
- return this;
484
- }
485
-
486
- /**
487
- * Internal: load a relation path with optional nesting (a.b.c)
488
- * @param {string} path
489
- * @private
490
- */
491
- async _loadRelationPath(path) {
492
- const segments = path.split('.');
493
- const head = segments[0];
494
- const tail = segments.slice(1).join('.');
495
-
496
- const relationFn = this[head];
497
- if (typeof relationFn !== 'function') return;
498
-
499
- const relation = relationFn.call(this);
500
- if (!relation || typeof relation.get !== 'function') return;
501
-
502
- const value = await relation.get();
503
- this.relations[head] = value;
504
-
505
- if (tail) {
506
- if (Array.isArray(value)) {
507
- await Promise.all(
508
- value.map(v => (v && typeof v.load === 'function') ? v.load(tail) : null)
509
- );
510
- } else if (value && typeof value.load === 'function') {
511
- await value.load(tail);
512
- }
513
- }
514
- }
515
-
516
- // ==================== Relationships ====================
517
-
518
- /**
519
- * Define a one-to-one relationship
520
- * @param {typeof Model} related
521
- * @param {string} foreignKey
522
- * @param {string} localKey
523
- * @returns {HasOneRelation}
524
- */
525
- hasOne(related, foreignKey, localKey) {
526
- const HasOneRelation = require('./Relations/HasOneRelation');
527
- localKey = localKey || this.constructor.primaryKey;
528
- foreignKey = foreignKey || `${this.constructor.table.slice(0, -1)}_id`;
529
-
530
- return new HasOneRelation(this, related, foreignKey, localKey);
531
- }
532
-
533
- /**
534
- * Define a one-to-many relationship
535
- * @param {typeof Model} related
536
- * @param {string} foreignKey
537
- * @param {string} localKey
538
- * @returns {HasManyRelation}
539
- */
540
- hasMany(related, foreignKey, localKey) {
541
- const HasManyRelation = require('./Relations/HasManyRelation');
542
- localKey = localKey || this.constructor.primaryKey;
543
- foreignKey = foreignKey || `${this.constructor.table.slice(0, -1)}_id`;
544
-
545
- return new HasManyRelation(this, related, foreignKey, localKey);
546
- }
547
-
548
- /**
549
- * Define an inverse one-to-one or many relationship
550
- * @param {typeof Model} related
551
- * @param {string} foreignKey
552
- * @param {string} ownerKey
553
- * @returns {BelongsToRelation}
554
- */
555
- belongsTo(related, foreignKey, ownerKey) {
556
- const BelongsToRelation = require('./Relations/BelongsToRelation');
557
- ownerKey = ownerKey || related.primaryKey;
558
- foreignKey = foreignKey || `${related.table.slice(0, -1)}_id`;
559
-
560
- return new BelongsToRelation(this, related, foreignKey, ownerKey);
561
- }
562
-
563
- /**
564
- * Define a many-to-many relationship
565
- * @param {typeof Model} related
566
- * @param {string} pivot
567
- * @param {string} foreignPivotKey
568
- * @param {string} relatedPivotKey
569
- * @param {string} parentKey
570
- * @param {string} relatedKey
571
- * @returns {BelongsToManyRelation}
572
- */
573
- belongsToMany(related, pivot, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
574
- const BelongsToManyRelation = require('./Relations/BelongsToManyRelation');
575
- return new BelongsToManyRelation(
576
- this, related, pivot, foreignPivotKey, relatedPivotKey, parentKey, relatedKey
577
- );
578
- }
579
-
580
- /**
581
- * Define a has-many-through relationship
582
- * @param {typeof Model} relatedFinal
583
- * @param {typeof Model} through
584
- * @param {string} [foreignKeyOnThrough]
585
- * @param {string} [throughKeyOnFinal]
586
- * @param {string} [localKey]
587
- * @param {string} [throughLocalKey]
588
- * @returns {HasManyThroughRelation}
589
- */
590
- hasManyThrough(relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey) {
591
- const HasManyThroughRelation = require('./Relations/HasManyThroughRelation');
592
- return new HasManyThroughRelation(
593
- this, relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey
594
- );
595
- }
596
-
597
- /**
598
- * Define a has-one-through relationship
599
- * @param {typeof Model} relatedFinal
600
- * @param {typeof Model} through
601
- * @param {string} [foreignKeyOnThrough]
602
- * @param {string} [throughKeyOnFinal]
603
- * @param {string} [localKey]
604
- * @param {string} [throughLocalKey]
605
- * @returns {HasOneThroughRelation}
606
- */
607
- hasOneThrough(relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey) {
608
- const HasOneThroughRelation = require('./Relations/HasOneThroughRelation');
609
- return new HasOneThroughRelation(
610
- this, relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey
611
- );
612
- }
613
-
614
- /**
615
- * Define a polymorphic inverse relationship
616
- * @param {string} name
617
- * @param {string} [typeColumn]
618
- * @param {string} [idColumn]
619
- * @returns {MorphToRelation}
620
- */
621
- morphTo(name, typeColumn, idColumn) {
622
- const MorphToRelation = require('./Relations/MorphToRelation');
623
- return new MorphToRelation(this, name, typeColumn, idColumn);
624
- }
625
-
626
- /**
627
- * Define a polymorphic one-to-one relationship
628
- * @param {typeof Model} related
629
- * @param {string} morphType
630
- * @param {string} [foreignKey]
631
- * @param {string} [localKey]
632
- * @returns {MorphOneRelation}
633
- */
634
- morphOne(related, morphType, foreignKey, localKey) {
635
- const MorphOneRelation = require('./Relations/MorphOneRelation');
636
- localKey = localKey || this.constructor.primaryKey;
637
- foreignKey = foreignKey || `${morphType}_id`;
638
-
639
- return new MorphOneRelation(this, related, morphType, foreignKey, localKey);
640
- }
641
-
642
- /**
643
- * Define a polymorphic one-to-many relationship
644
- * @param {typeof Model} related
645
- * @param {string} morphType
646
- * @param {string} [foreignKey]
647
- * @param {string} [localKey]
648
- * @returns {MorphManyRelation}
649
- */
650
- morphMany(related, morphType, foreignKey, localKey) {
651
- const MorphManyRelation = require('./Relations/MorphManyRelation');
652
- localKey = localKey || this.constructor.primaryKey;
653
- foreignKey = foreignKey || `${morphType}_id`;
654
-
655
- return new MorphManyRelation(this, related, morphType, foreignKey, localKey);
656
- }
657
- }
658
-
659
- module.exports = Model;
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;