@arikajs/database 0.0.4 → 0.0.6

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.
Files changed (92) hide show
  1. package/README.md +392 -25
  2. package/dist/Connections/MongoDBConnection.d.ts +81 -0
  3. package/dist/Connections/MongoDBConnection.d.ts.map +1 -0
  4. package/dist/Connections/MongoDBConnection.js +203 -0
  5. package/dist/Connections/MongoDBConnection.js.map +1 -0
  6. package/dist/Connections/MySQLConnection.d.ts +1 -0
  7. package/dist/Connections/MySQLConnection.d.ts.map +1 -1
  8. package/dist/Connections/MySQLConnection.js +47 -10
  9. package/dist/Connections/MySQLConnection.js.map +1 -1
  10. package/dist/Connections/PostgreSQLConnection.d.ts +1 -0
  11. package/dist/Connections/PostgreSQLConnection.d.ts.map +1 -1
  12. package/dist/Connections/PostgreSQLConnection.js +43 -9
  13. package/dist/Connections/PostgreSQLConnection.js.map +1 -1
  14. package/dist/Connections/SQLiteConnection.d.ts +1 -0
  15. package/dist/Connections/SQLiteConnection.d.ts.map +1 -1
  16. package/dist/Connections/SQLiteConnection.js +38 -7
  17. package/dist/Connections/SQLiteConnection.js.map +1 -1
  18. package/dist/Contracts/Database.d.ts +71 -4
  19. package/dist/Contracts/Database.d.ts.map +1 -1
  20. package/dist/Contracts/Schema.d.ts +4 -0
  21. package/dist/Contracts/Schema.d.ts.map +1 -1
  22. package/dist/Database.d.ts +30 -3
  23. package/dist/Database.d.ts.map +1 -1
  24. package/dist/Database.js +39 -1
  25. package/dist/Database.js.map +1 -1
  26. package/dist/DatabaseManager.d.ts +17 -3
  27. package/dist/DatabaseManager.d.ts.map +1 -1
  28. package/dist/DatabaseManager.js +27 -11
  29. package/dist/DatabaseManager.js.map +1 -1
  30. package/dist/Migrations/Migrator.d.ts.map +1 -1
  31. package/dist/Migrations/Migrator.js +35 -3
  32. package/dist/Migrations/Migrator.js.map +1 -1
  33. package/dist/Model/GlobalScope.d.ts +44 -0
  34. package/dist/Model/GlobalScope.d.ts.map +1 -0
  35. package/dist/Model/GlobalScope.js +64 -0
  36. package/dist/Model/GlobalScope.js.map +1 -0
  37. package/dist/Model/Model.d.ts +168 -4
  38. package/dist/Model/Model.d.ts.map +1 -1
  39. package/dist/Model/Model.js +476 -16
  40. package/dist/Model/Model.js.map +1 -1
  41. package/dist/Model/Observer.d.ts +39 -0
  42. package/dist/Model/Observer.d.ts.map +1 -0
  43. package/dist/Model/Observer.js +48 -0
  44. package/dist/Model/Observer.js.map +1 -0
  45. package/dist/Model/Relations.d.ts +201 -10
  46. package/dist/Model/Relations.d.ts.map +1 -1
  47. package/dist/Model/Relations.js +472 -27
  48. package/dist/Model/Relations.js.map +1 -1
  49. package/dist/Query/Expression.d.ts +16 -0
  50. package/dist/Query/Expression.d.ts.map +1 -0
  51. package/dist/Query/Expression.js +25 -0
  52. package/dist/Query/Expression.js.map +1 -0
  53. package/dist/Query/QueryBuilder.d.ts +64 -6
  54. package/dist/Query/QueryBuilder.d.ts.map +1 -1
  55. package/dist/Query/QueryBuilder.js +234 -15
  56. package/dist/Query/QueryBuilder.js.map +1 -1
  57. package/dist/Query/QueryLogger.d.ts +55 -0
  58. package/dist/Query/QueryLogger.d.ts.map +1 -0
  59. package/dist/Query/QueryLogger.js +82 -0
  60. package/dist/Query/QueryLogger.js.map +1 -0
  61. package/dist/Schema/Grammars/Grammar.d.ts +5 -0
  62. package/dist/Schema/Grammars/Grammar.d.ts.map +1 -1
  63. package/dist/Schema/Grammars/Grammar.js.map +1 -1
  64. package/dist/Schema/Grammars/MySQLGrammar.d.ts +1 -0
  65. package/dist/Schema/Grammars/MySQLGrammar.d.ts.map +1 -1
  66. package/dist/Schema/Grammars/MySQLGrammar.js +42 -0
  67. package/dist/Schema/Grammars/MySQLGrammar.js.map +1 -1
  68. package/dist/Schema/Grammars/PostgreSQLGrammar.d.ts +1 -0
  69. package/dist/Schema/Grammars/PostgreSQLGrammar.d.ts.map +1 -1
  70. package/dist/Schema/Grammars/PostgreSQLGrammar.js +46 -0
  71. package/dist/Schema/Grammars/PostgreSQLGrammar.js.map +1 -1
  72. package/dist/Schema/Grammars/SQLiteGrammar.d.ts +1 -0
  73. package/dist/Schema/Grammars/SQLiteGrammar.d.ts.map +1 -1
  74. package/dist/Schema/Grammars/SQLiteGrammar.js +31 -0
  75. package/dist/Schema/Grammars/SQLiteGrammar.js.map +1 -1
  76. package/dist/Schema/Schema.d.ts +6 -0
  77. package/dist/Schema/Schema.d.ts.map +1 -1
  78. package/dist/Schema/Schema.js +10 -0
  79. package/dist/Schema/Schema.js.map +1 -1
  80. package/dist/Schema/SchemaBuilder.d.ts +4 -0
  81. package/dist/Schema/SchemaBuilder.d.ts.map +1 -1
  82. package/dist/Schema/SchemaBuilder.js +15 -0
  83. package/dist/Schema/SchemaBuilder.js.map +1 -1
  84. package/dist/Transactions/TransactionManager.d.ts +28 -0
  85. package/dist/Transactions/TransactionManager.d.ts.map +1 -0
  86. package/dist/Transactions/TransactionManager.js +68 -0
  87. package/dist/Transactions/TransactionManager.js.map +1 -0
  88. package/dist/index.d.ts +11 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +21 -1
  91. package/dist/index.js.map +1 -1
  92. package/package.json +10 -6
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ModelQueryBuilder = exports.Model = void 0;
4
4
  const Database_1 = require("../Database");
5
5
  const Relations_1 = require("./Relations");
6
+ const Observer_1 = require("./Observer");
7
+ const GlobalScope_1 = require("./GlobalScope");
6
8
  /**
7
9
  * Base Model class for Active Record pattern
8
10
  */
@@ -32,11 +34,79 @@ class Model {
32
34
  * Loaded relationships
33
35
  */
34
36
  this.relations = {};
37
+ /**
38
+ * The attributes that should be hidden for serialization.
39
+ */
40
+ this.hidden = [];
41
+ /**
42
+ * The attributes that should be visible for serialization.
43
+ */
44
+ this.visible = [];
45
+ /**
46
+ * The attributes that should be cast to native types.
47
+ */
48
+ this.casts = {};
49
+ return new Proxy(this, {
50
+ get(target, prop, receiver) {
51
+ if (Reflect.has(target, prop)) {
52
+ return Reflect.get(target, prop, receiver);
53
+ }
54
+ if (typeof prop === 'string') {
55
+ // Check if it's a loaded relationship first
56
+ if (target.relations && prop in target.relations) {
57
+ return target.relations[prop];
58
+ }
59
+ return target.getAttribute(prop);
60
+ }
61
+ return undefined;
62
+ },
63
+ set(target, prop, value, receiver) {
64
+ if (Reflect.has(target, prop)) {
65
+ return Reflect.set(target, prop, value, receiver);
66
+ }
67
+ if (typeof prop === 'string') {
68
+ target.setAttribute(prop, value);
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+ });
35
74
  }
36
75
  /**
37
76
  * Create a new query builder for the model
38
77
  */
39
78
  static query() {
79
+ const queryBuilder = Database_1.Database.table(this.getTableName(), this.getConnectionName());
80
+ const mqb = new ModelQueryBuilder(queryBuilder, this);
81
+ // Apply all registered global scopes
82
+ GlobalScope_1.GlobalScopeRegistry.applyAll(this.name, mqb, this);
83
+ return mqb;
84
+ }
85
+ /**
86
+ * Register an observer for this model
87
+ */
88
+ static observe(observer) {
89
+ Observer_1.ObserverRegistry.register(this.name, observer);
90
+ }
91
+ /**
92
+ * Add a global scope to this model
93
+ */
94
+ static addGlobalScope(name, scope) {
95
+ const resolvedScope = typeof scope === 'function'
96
+ ? new GlobalScope_1.CallbackGlobalScope(scope)
97
+ : scope;
98
+ GlobalScope_1.GlobalScopeRegistry.register(this.name, name, resolvedScope);
99
+ }
100
+ /**
101
+ * Remove a named global scope from this model
102
+ */
103
+ static removeGlobalScope(name) {
104
+ GlobalScope_1.GlobalScopeRegistry.remove(this.name, name);
105
+ }
106
+ /**
107
+ * Run a query without any global scopes
108
+ */
109
+ static withoutGlobalScopes() {
40
110
  const queryBuilder = Database_1.Database.table(this.getTableName(), this.getConnectionName());
41
111
  return new ModelQueryBuilder(queryBuilder, this);
42
112
  }
@@ -122,6 +192,12 @@ class Model {
122
192
  static async create(data) {
123
193
  return this.query().create(data);
124
194
  }
195
+ /**
196
+ * Insert a record or multiple records (Bulk Insert)
197
+ */
198
+ static async insert(data) {
199
+ return this.query().insert(data);
200
+ }
125
201
  /**
126
202
  * Get the first record
127
203
  */
@@ -134,12 +210,24 @@ class Model {
134
210
  static async get() {
135
211
  return this.query().get();
136
212
  }
213
+ /**
214
+ * Paginate the models
215
+ */
216
+ static async paginate(page = 1, perPage = 15) {
217
+ return this.query().paginate(page, perPage);
218
+ }
137
219
  /**
138
220
  * Get the count of records
139
221
  */
140
222
  static async count(column) {
141
223
  return this.query().count(column);
142
224
  }
225
+ /**
226
+ * Cache the query results
227
+ */
228
+ static cache(ttl, key) {
229
+ return this.query().cache(ttl, key);
230
+ }
143
231
  /**
144
232
  * Eager load relationships
145
233
  */
@@ -194,18 +282,109 @@ class Model {
194
282
  const constructor = this.constructor;
195
283
  return constructor.getPrimaryKeyName();
196
284
  }
285
+ // ==================== Casts & Mutators ====================
286
+ /**
287
+ * Convert string into studly case (e.g. first_name -> FirstName)
288
+ */
289
+ studly(value) {
290
+ return value.replace(/(?:^|_)(.)/g, (_, c) => c.toUpperCase());
291
+ }
292
+ /**
293
+ * Cast an attribute to a native type.
294
+ */
295
+ castAttribute(key, value) {
296
+ if (value === null || value === undefined) {
297
+ return value;
298
+ }
299
+ const castType = this.casts[key];
300
+ if (!castType) {
301
+ return value;
302
+ }
303
+ switch (castType) {
304
+ case 'int':
305
+ case 'integer':
306
+ return parseInt(value, 10);
307
+ case 'real':
308
+ case 'float':
309
+ case 'double':
310
+ return parseFloat(value);
311
+ case 'string':
312
+ return String(value);
313
+ case 'bool':
314
+ case 'boolean':
315
+ return value === 'true' || value === '1' || value === 1 || value === true;
316
+ case 'object':
317
+ case 'array':
318
+ case 'json':
319
+ return typeof value === 'string' ? JSON.parse(value) : value;
320
+ case 'date':
321
+ case 'datetime':
322
+ case 'timestamp':
323
+ return new Date(value);
324
+ default:
325
+ return value;
326
+ }
327
+ }
328
+ /**
329
+ * Cast an attribute to a native type for DB.
330
+ */
331
+ castAttributeForSave(key, value) {
332
+ if (value === null || value === undefined) {
333
+ return value;
334
+ }
335
+ const castType = this.casts[key];
336
+ // Always format dates even if not explicitly in casts for timestamps
337
+ if (value instanceof Date) {
338
+ return this.formatDate(value);
339
+ }
340
+ if (!castType) {
341
+ return value;
342
+ }
343
+ switch (castType) {
344
+ case 'object':
345
+ case 'array':
346
+ case 'json':
347
+ return typeof value === 'object' ? JSON.stringify(value) : value;
348
+ case 'date':
349
+ case 'datetime':
350
+ case 'timestamp':
351
+ return value instanceof Date ? this.formatDate(value) : value;
352
+ default:
353
+ return value;
354
+ }
355
+ }
356
+ /**
357
+ * Format a date for database and JSON serialization
358
+ */
359
+ formatDate(date) {
360
+ if (this.constructor.serializeDateAsUtc) {
361
+ return date.toISOString();
362
+ }
363
+ const pad = (n) => n.toString().padStart(2, '0');
364
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
365
+ `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
366
+ }
197
367
  // ==================== Instance Methods ====================
198
368
  /**
199
369
  * Get an attribute value
200
370
  */
201
371
  getAttribute(key) {
202
- return this.attributes[key];
372
+ const accessorMethod = `get${this.studly(key)}Attribute`;
373
+ if (typeof this[accessorMethod] === 'function') {
374
+ return this[accessorMethod](this.attributes[key]);
375
+ }
376
+ return this.castAttribute(key, this.attributes[key]);
203
377
  }
204
378
  /**
205
379
  * Set an attribute value
206
380
  */
207
381
  setAttribute(key, value) {
208
- this.attributes[key] = value;
382
+ const mutatorMethod = `set${this.studly(key)}Attribute`;
383
+ if (typeof this[mutatorMethod] === 'function') {
384
+ this[mutatorMethod](value);
385
+ return;
386
+ }
387
+ this.attributes[key] = this.castAttributeForSave(key, value);
209
388
  }
210
389
  /**
211
390
  * Fill the model with an array of attributes
@@ -220,39 +399,67 @@ class Model {
220
399
  * Save the model to the database
221
400
  */
222
401
  async save() {
402
+ const modelName = this.constructor.name;
223
403
  const query = Database_1.Database.table(this.getTable(), this.getConnection());
224
404
  if (this.exists) {
225
- // Update existing record
405
+ // Fire saving + updating events
406
+ if (await Observer_1.ObserverRegistry.fire(modelName, 'saving', this) === false)
407
+ return false;
408
+ if (await Observer_1.ObserverRegistry.fire(modelName, 'updating', this) === false)
409
+ return false;
226
410
  const primaryKey = this.getPrimaryKey();
227
411
  const id = this.attributes[primaryKey];
228
412
  if (!id) {
229
413
  throw new Error('Cannot update model without primary key value');
230
414
  }
415
+ if (this.usesTimestamps()) {
416
+ this.updateTimestamps();
417
+ }
231
418
  const dirty = this.getDirty();
232
419
  if (Object.keys(dirty).length === 0) {
233
420
  return true; // No changes to save
234
421
  }
235
422
  await query.where(primaryKey, id).update(dirty);
236
423
  this.syncOriginal();
424
+ // Fire saved + updated events
425
+ await Observer_1.ObserverRegistry.fire(modelName, 'updated', this);
426
+ await Observer_1.ObserverRegistry.fire(modelName, 'saved', this);
237
427
  return true;
238
428
  }
239
429
  else {
240
- // Insert new record
430
+ // Fire saving + creating events
431
+ if (await Observer_1.ObserverRegistry.fire(modelName, 'saving', this) === false)
432
+ return false;
433
+ if (await Observer_1.ObserverRegistry.fire(modelName, 'creating', this) === false)
434
+ return false;
435
+ if (this.usesTimestamps()) {
436
+ this.updateTimestamps();
437
+ }
241
438
  const result = await query.insert(this.attributes);
242
- // Set the primary key if it was auto-generated
243
439
  const primaryKey = this.getPrimaryKey();
244
440
  if (result && result.insertId && !this.attributes[primaryKey]) {
245
441
  this.attributes[primaryKey] = result.insertId;
246
442
  }
247
443
  this.exists = true;
248
444
  this.syncOriginal();
445
+ // Fire created + saved events
446
+ await Observer_1.ObserverRegistry.fire(modelName, 'created', this);
447
+ await Observer_1.ObserverRegistry.fire(modelName, 'saved', this);
249
448
  return true;
250
449
  }
251
450
  }
451
+ /**
452
+ * Update the model with an array of attributes
453
+ */
454
+ async update(attributes) {
455
+ this.fill(attributes);
456
+ return await this.save();
457
+ }
252
458
  /**
253
459
  * Delete the model from the database
254
460
  */
255
- async deleteInstance() {
461
+ async delete() {
462
+ const modelName = this.constructor.name;
256
463
  if (!this.exists) {
257
464
  return false;
258
465
  }
@@ -261,9 +468,14 @@ class Model {
261
468
  if (!id) {
262
469
  throw new Error('Cannot delete model without primary key value');
263
470
  }
471
+ // Fire deleting event
472
+ if (await Observer_1.ObserverRegistry.fire(modelName, 'deleting', this) === false)
473
+ return false;
264
474
  const query = Database_1.Database.table(this.getTable(), this.getConnection());
265
475
  await query.where(primaryKey, id).delete();
266
476
  this.exists = false;
477
+ // Fire deleted event
478
+ await Observer_1.ObserverRegistry.fire(modelName, 'deleted', this);
267
479
  return true;
268
480
  }
269
481
  /**
@@ -312,6 +524,24 @@ class Model {
312
524
  syncOriginal() {
313
525
  this.original = { ...this.attributes };
314
526
  }
527
+ /**
528
+ * Check if the model uses timestamps
529
+ */
530
+ usesTimestamps() {
531
+ return this.constructor.timestamps;
532
+ }
533
+ /**
534
+ * Update the model's timestamps
535
+ */
536
+ updateTimestamps() {
537
+ const now = new Date();
538
+ if (this.usesTimestamps()) {
539
+ this.setAttribute('updated_at', now);
540
+ if (!this.exists && !this.getAttribute('created_at')) {
541
+ this.setAttribute('created_at', now);
542
+ }
543
+ }
544
+ }
315
545
  /**
316
546
  * Mark the model as existing
317
547
  */
@@ -323,10 +553,42 @@ class Model {
323
553
  * Convert the model to a plain object
324
554
  */
325
555
  toJSON() {
326
- return {
327
- ...this.attributes,
328
- ...this.relations,
556
+ let attributes = { ...this.attributes };
557
+ // Handle date formatting in attributes
558
+ for (const [key, value] of Object.entries(attributes)) {
559
+ if (value instanceof Date) {
560
+ attributes[key] = this.formatDate(value);
561
+ }
562
+ }
563
+ let relations = { ...this.relations };
564
+ // Handle date formatting in relations (recursive for nested models)
565
+ for (const [key, value] of Object.entries(relations)) {
566
+ if (value instanceof Model) {
567
+ relations[key] = value.toJSON();
568
+ }
569
+ else if (Array.isArray(value)) {
570
+ relations[key] = value.map(item => item instanceof Model ? item.toJSON() : item);
571
+ }
572
+ }
573
+ let result = {
574
+ ...attributes,
575
+ ...relations,
329
576
  };
577
+ // Filter hidden/visible attributes
578
+ if (this.visible.length > 0) {
579
+ result = Object.keys(result)
580
+ .filter(key => this.visible.includes(key))
581
+ .reduce((obj, key) => {
582
+ obj[key] = result[key];
583
+ return obj;
584
+ }, {});
585
+ }
586
+ else if (this.hidden.length > 0) {
587
+ this.hidden.forEach(key => {
588
+ delete result[key];
589
+ });
590
+ }
591
+ return result;
330
592
  }
331
593
  // ==================== Relationship Methods ====================
332
594
  /**
@@ -367,6 +629,52 @@ class Model {
367
629
  const rk = relatedKey || 'id';
368
630
  return new Relations_1.BelongsToMany(related, this, pivot, fpk, rpk, pk, rk);
369
631
  }
632
+ /**
633
+ * Define a has-one-through relationship
634
+ *
635
+ * Example: Country → hasOneThrough(Capital, Province, 'country_id', 'province_id')
636
+ */
637
+ hasOneThrough(related, through, firstKey, secondKey, localKey = 'id', secondLocalKey = 'id') {
638
+ const throughTable = through.table;
639
+ const fk = firstKey || `${this.getTable()}_id`;
640
+ const sk = secondKey || `${throughTable}_id`;
641
+ return new Relations_1.HasOneThrough(related, this, through, fk, sk, localKey, secondLocalKey);
642
+ }
643
+ /**
644
+ * Define a has-many-through relationship
645
+ *
646
+ * Example: Country → hasManyThrough(Post, User, 'country_id', 'user_id')
647
+ */
648
+ hasManyThrough(related, through, firstKey, secondKey, localKey = 'id', secondLocalKey = 'id') {
649
+ const throughTable = through.table;
650
+ const fk = firstKey || `${this.getTable()}_id`;
651
+ const sk = secondKey || `${throughTable}_id`;
652
+ return new Relations_1.HasManyThrough(related, this, through, fk, sk, localKey, secondLocalKey);
653
+ }
654
+ /**
655
+ * Define a polymorphic has-one relationship
656
+ *
657
+ * Example: Post → morphOne(Image, 'imageable')
658
+ */
659
+ morphOne(related, morphName, localKey = 'id') {
660
+ return new Relations_1.MorphOne(related, this, morphName, `${morphName}_type`, `${morphName}_id`, localKey);
661
+ }
662
+ /**
663
+ * Define a polymorphic has-many relationship
664
+ *
665
+ * Example: Post → morphMany(Comment, 'commentable')
666
+ */
667
+ morphMany(related, morphName, localKey = 'id') {
668
+ return new Relations_1.MorphMany(related, this, morphName, `${morphName}_type`, `${morphName}_id`, localKey);
669
+ }
670
+ /**
671
+ * Define the inverse of a polymorphic relationship
672
+ *
673
+ * Example: Comment → morphTo('commentable')
674
+ */
675
+ morphTo(morphName, morphMap = {}) {
676
+ return new Relations_1.MorphTo(this, morphName, `${morphName}_type`, `${morphName}_id`, morphMap);
677
+ }
370
678
  /**
371
679
  * Get a relationship value
372
680
  */
@@ -381,13 +689,25 @@ class Model {
381
689
  return this;
382
690
  }
383
691
  /**
384
- * Load a relationship
692
+ * Eagerly load a relationship on this instance
693
+ */
694
+ async load(...relationNames) {
695
+ for (const relation of relationNames) {
696
+ if (typeof this[relation] === 'function') {
697
+ const relationInstance = this[relation]();
698
+ const result = await relationInstance.get();
699
+ this.setRelation(relation, result);
700
+ }
701
+ }
702
+ return this;
703
+ }
704
+ /**
705
+ * Load relationships only if they haven't been loaded yet
385
706
  */
386
- async load(relation) {
387
- if (typeof this[relation] === 'function') {
388
- const relationInstance = this[relation]();
389
- const result = await relationInstance.get();
390
- this.setRelation(relation, result);
707
+ async loadMissing(...relationNames) {
708
+ const toLoad = relationNames.filter(name => !(name in this.relations));
709
+ if (toLoad.length > 0) {
710
+ await this.load(...toLoad);
391
711
  }
392
712
  return this;
393
713
  }
@@ -401,6 +721,14 @@ Model.table = '';
401
721
  * The primary key for the model
402
722
  */
403
723
  Model.primaryKey = 'id';
724
+ /**
725
+ * Indicates if the model should use timestamps
726
+ */
727
+ Model.timestamps = true;
728
+ /**
729
+ * Indicates if dates should be serialized to UTC (ISO string) or Local string
730
+ */
731
+ Model.serializeDateAsUtc = false;
404
732
  /**
405
733
  * Model query builder wrapper
406
734
  */
@@ -437,8 +765,14 @@ class ModelQueryBuilder {
437
765
  * Create a new record
438
766
  */
439
767
  async create(data) {
768
+ let instance = new this.modelClass();
769
+ if (instance.usesTimestamps()) {
770
+ const now = new Date();
771
+ data['created_at'] = data['created_at'] || now;
772
+ data['updated_at'] = data['updated_at'] || now;
773
+ }
440
774
  const result = await this.queryBuilder.insert(data);
441
- const instance = this.hydrate(data);
775
+ instance = this.hydrate(data);
442
776
  // Set the primary key if it was auto-generated
443
777
  const primaryKey = instance['getPrimaryKey']();
444
778
  if (result && result.insertId && !data[primaryKey]) {
@@ -448,6 +782,12 @@ class ModelQueryBuilder {
448
782
  instance['syncOriginal']();
449
783
  return instance;
450
784
  }
785
+ /**
786
+ * Insert a record or multiple records natively (Bulk Insert)
787
+ */
788
+ async insert(data) {
789
+ return await this.queryBuilder.insert(data);
790
+ }
451
791
  /**
452
792
  * Add a where clause
453
793
  */
@@ -553,6 +893,13 @@ class ModelQueryBuilder {
553
893
  async count(column) {
554
894
  return await this.queryBuilder.count(column);
555
895
  }
896
+ /**
897
+ * Cache the query results
898
+ */
899
+ cache(ttl, key) {
900
+ this.queryBuilder.cache(ttl, key);
901
+ return this;
902
+ }
556
903
  /**
557
904
  * Eager load relationships
558
905
  */
@@ -570,6 +917,119 @@ class ModelQueryBuilder {
570
917
  }
571
918
  return this;
572
919
  }
920
+ /**
921
+ * Include subqueries to count given relationships
922
+ */
923
+ withCount(relations) {
924
+ const relationNames = Array.isArray(relations) ? relations : [relations];
925
+ const dummy = new this.modelClass();
926
+ const outerTable = dummy.getTable();
927
+ for (const rel of relationNames) {
928
+ if (typeof dummy[rel] !== 'function') {
929
+ throw new Error(`Relation ${rel} does not exist on model ${dummy.constructor.name}`);
930
+ }
931
+ const relationInstance = dummy[rel]();
932
+ if (typeof relationInstance.getRelationCountQuery !== 'function') {
933
+ throw new Error(`Relation ${rel} does not support withCount natively`);
934
+ }
935
+ const { sql, bindings } = relationInstance.getRelationCountQuery(outerTable);
936
+ this.queryBuilder.selectRaw(`${sql} AS ${rel}_count`, bindings);
937
+ }
938
+ return this;
939
+ }
940
+ /**
941
+ * Add a basic where clause using a related model (WHERE EXISTS)
942
+ */
943
+ whereHas(relation, callback) {
944
+ const dummy = new this.modelClass();
945
+ const outerTable = dummy.getTable();
946
+ if (typeof dummy[relation] !== 'function') {
947
+ throw new Error(`Relation ${relation} does not exist on model ${dummy.constructor.name}`);
948
+ }
949
+ const relationInstance = dummy[relation]();
950
+ this.queryBuilder.whereExists((q) => {
951
+ const { sql, bindings } = relationInstance.getRelationCountQuery(outerTable);
952
+ // Replace SELECT COUNT(*) with SELECT 1 for EXISTS optimization
953
+ const existsSql = sql.replace(/^\(SELECT COUNT\(\*\)/i, '(SELECT 1');
954
+ q.selectRaw(existsSql.slice(1, -1), bindings); // remove outer parens because whereExists adds them
955
+ if (callback) {
956
+ callback(q);
957
+ }
958
+ });
959
+ return this;
960
+ }
961
+ /**
962
+ * Add a basic where clause for absence of a related model (WHERE NOT EXISTS)
963
+ */
964
+ whereDoesntHave(relation, callback) {
965
+ const dummy = new this.modelClass();
966
+ const outerTable = dummy.getTable();
967
+ if (typeof dummy[relation] !== 'function') {
968
+ throw new Error(`Relation ${relation} does not exist on model ${dummy.constructor.name}`);
969
+ }
970
+ const relationInstance = dummy[relation]();
971
+ this.queryBuilder.whereNotExists((q) => {
972
+ const { sql, bindings } = relationInstance.getRelationCountQuery(outerTable);
973
+ const existsSql = sql.replace(/^\(SELECT COUNT\(\*\)/i, '(SELECT 1');
974
+ q.selectRaw(existsSql.slice(1, -1), bindings);
975
+ if (callback) {
976
+ callback(q);
977
+ }
978
+ });
979
+ return this;
980
+ }
981
+ /**
982
+ * Paginate the query results
983
+ */
984
+ async paginate(page = 1, perPage = 15, path = '/') {
985
+ const paginatedResult = await this.queryBuilder.paginate(page, perPage, path);
986
+ // Map and load relations
987
+ const models = paginatedResult.data.map(data => this.hydrate(data));
988
+ const finalModels = await this.loadRelations(models);
989
+ return {
990
+ data: finalModels,
991
+ meta: paginatedResult.meta,
992
+ links: paginatedResult.links
993
+ };
994
+ }
995
+ /**
996
+ * Paginate the query results without counting total pages
997
+ */
998
+ async simplePaginate(page = 1, perPage = 15, path = '/') {
999
+ const paginatedResult = await this.queryBuilder.simplePaginate(page, perPage, path);
1000
+ // Map and load relations
1001
+ const models = paginatedResult.data.map(data => this.hydrate(data));
1002
+ const finalModels = await this.loadRelations(models);
1003
+ return {
1004
+ data: finalModels,
1005
+ meta: paginatedResult.meta,
1006
+ links: paginatedResult.links
1007
+ };
1008
+ }
1009
+ /**
1010
+ * Cursor paginate for high performance
1011
+ */
1012
+ async cursorPaginate(cursor = null, perPage = 15, cursorColumn = 'id', path = '/') {
1013
+ const paginatedResult = await this.queryBuilder.cursorPaginate(cursor, perPage, cursorColumn, path);
1014
+ // Map and load relations
1015
+ const models = paginatedResult.data.map(data => this.hydrate(data));
1016
+ const finalModels = await this.loadRelations(models);
1017
+ return {
1018
+ data: finalModels,
1019
+ meta: paginatedResult.meta,
1020
+ links: paginatedResult.links
1021
+ };
1022
+ }
1023
+ /**
1024
+ * Chunk the query results
1025
+ */
1026
+ async chunk(count, callback) {
1027
+ return this.queryBuilder.chunk(count, async (results, page) => {
1028
+ const models = results.map(data => this.hydrate(data));
1029
+ const finalModels = await this.loadRelations(models);
1030
+ return callback(finalModels, page);
1031
+ });
1032
+ }
573
1033
  /**
574
1034
  * Hydrate a model instance from data
575
1035
  */