outlet-orm 2.5.0 → 2.5.1

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