create-phoenixjs 0.1.3 → 0.1.5

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 (59) hide show
  1. package/package.json +1 -1
  2. package/template/app/controllers/HealthController.ts +22 -0
  3. package/template/app/gateways/EchoGateway.ts +58 -0
  4. package/template/bootstrap/app.ts +2 -7
  5. package/template/config/app.ts +12 -0
  6. package/template/config/database.ts +13 -1
  7. package/template/database/migrations/2024_01_01_000000_create_test_users_cli_table.ts +16 -0
  8. package/template/database/migrations/20260108164611_TestCliMigration.ts +16 -0
  9. package/template/database/migrations/2026_01_08_16_46_11_CreateTestMigrationsTable.ts +21 -0
  10. package/template/framework/cli/artisan.ts +12 -0
  11. package/template/framework/core/Application.ts +15 -0
  12. package/template/framework/database/DatabaseManager.ts +133 -0
  13. package/template/framework/database/connection/Connection.ts +71 -0
  14. package/template/framework/database/connection/ConnectionFactory.ts +30 -0
  15. package/template/framework/database/connection/PostgresConnection.ts +159 -0
  16. package/template/framework/database/console/MakeMigrationCommand.ts +58 -0
  17. package/template/framework/database/console/MigrateCommand.ts +32 -0
  18. package/template/framework/database/console/MigrateResetCommand.ts +31 -0
  19. package/template/framework/database/console/MigrateRollbackCommand.ts +31 -0
  20. package/template/framework/database/console/MigrateStatusCommand.ts +38 -0
  21. package/template/framework/database/migrations/DatabaseMigrationRepository.ts +122 -0
  22. package/template/framework/database/migrations/Migration.ts +5 -0
  23. package/template/framework/database/migrations/MigrationRepository.ts +46 -0
  24. package/template/framework/database/migrations/Migrator.ts +249 -0
  25. package/template/framework/database/migrations/index.ts +4 -0
  26. package/template/framework/database/orm/BelongsTo.ts +246 -0
  27. package/template/framework/database/orm/BelongsToMany.ts +570 -0
  28. package/template/framework/database/orm/Builder.ts +160 -0
  29. package/template/framework/database/orm/EagerLoadingBuilder.ts +324 -0
  30. package/template/framework/database/orm/HasMany.ts +303 -0
  31. package/template/framework/database/orm/HasManyThrough.ts +282 -0
  32. package/template/framework/database/orm/HasOne.ts +201 -0
  33. package/template/framework/database/orm/HasOneThrough.ts +281 -0
  34. package/template/framework/database/orm/Model.ts +1766 -0
  35. package/template/framework/database/orm/Relation.ts +342 -0
  36. package/template/framework/database/orm/Scope.ts +14 -0
  37. package/template/framework/database/orm/SoftDeletes.ts +160 -0
  38. package/template/framework/database/orm/index.ts +54 -0
  39. package/template/framework/database/orm/scopes/SoftDeletingScope.ts +58 -0
  40. package/template/framework/database/pagination/LengthAwarePaginator.ts +55 -0
  41. package/template/framework/database/pagination/Paginator.ts +110 -0
  42. package/template/framework/database/pagination/index.ts +2 -0
  43. package/template/framework/database/query/Builder.ts +918 -0
  44. package/template/framework/database/query/DB.ts +139 -0
  45. package/template/framework/database/query/grammars/Grammar.ts +430 -0
  46. package/template/framework/database/query/grammars/PostgresGrammar.ts +224 -0
  47. package/template/framework/database/query/grammars/index.ts +6 -0
  48. package/template/framework/database/query/index.ts +8 -0
  49. package/template/framework/database/query/types.ts +196 -0
  50. package/template/framework/database/schema/Blueprint.ts +478 -0
  51. package/template/framework/database/schema/Schema.ts +149 -0
  52. package/template/framework/database/schema/SchemaBuilder.ts +152 -0
  53. package/template/framework/database/schema/grammars/PostgresSchemaGrammar.ts +293 -0
  54. package/template/framework/database/schema/grammars/index.ts +5 -0
  55. package/template/framework/database/schema/index.ts +9 -0
  56. package/template/framework/log/Logger.ts +195 -0
  57. package/template/package.json +4 -1
  58. package/template/routes/api.ts +13 -35
  59. package/template/app/controllers/ExampleController.ts +0 -61
@@ -0,0 +1,1766 @@
1
+ import { DB } from '../query/DB';
2
+ import { Builder } from './Builder';
3
+ import type { Binding, InsertValues, UpdateValues } from '../query/types';
4
+
5
+ // Relationship imports
6
+ import { HasOne } from './HasOne';
7
+ import { BelongsTo } from './BelongsTo';
8
+ import { HasMany } from './HasMany';
9
+ import { BelongsToMany } from './BelongsToMany';
10
+ import { HasOneThrough } from './HasOneThrough';
11
+ import { HasManyThrough } from './HasManyThrough';
12
+
13
+ // Eager loading import
14
+ import { EagerLoadingBuilder } from './EagerLoadingBuilder';
15
+
16
+ // Pagination imports
17
+ import { Paginator } from '../pagination/Paginator';
18
+ import { LengthAwarePaginator } from '../pagination/LengthAwarePaginator';
19
+
20
+
21
+ /**
22
+ * ModelNotFoundException - Thrown when a model is not found.
23
+ */
24
+ export class ModelNotFoundException extends Error {
25
+ constructor(model: string, id: string | number) {
26
+ super(`No query results for model [${model}] with ID [${id}]`);
27
+ this.name = 'ModelNotFoundException';
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Model event types for lifecycle hooks.
33
+ */
34
+ export type ModelEventType =
35
+ | 'saving'
36
+ | 'saved'
37
+ | 'creating'
38
+ | 'created'
39
+ | 'updating'
40
+ | 'updated'
41
+ | 'deleting'
42
+ | 'deleted';
43
+
44
+ /**
45
+ * Event callback function type.
46
+ */
47
+ export type ModelEventCallback<T extends Model = Model> = (model: T) => void | boolean | Promise<void | boolean>;
48
+
49
+ /**
50
+ * Base Model class for Active Record pattern.
51
+ * Provides CRUD operations and query building capabilities.
52
+ */
53
+ export abstract class Model {
54
+ /**
55
+ * The table associated with the model.
56
+ */
57
+ protected table?: string;
58
+
59
+ /**
60
+ * The primary key for the model.
61
+ */
62
+ protected primaryKey: string = 'id';
63
+
64
+ /**
65
+ * The model's attributes.
66
+ */
67
+ protected attributes: Record<string, unknown> = {};
68
+
69
+ /**
70
+ * The model's original attributes (for dirty checking).
71
+ */
72
+ protected original: Record<string, unknown> = {};
73
+
74
+ /**
75
+ * The attributes that are mass assignable.
76
+ *
77
+ * @var string[]
78
+ */
79
+ protected _fillable: string[] = [];
80
+
81
+ public get fillable(): string[] {
82
+ return this._fillable;
83
+ }
84
+
85
+ public set fillable(value: string[]) {
86
+ this._fillable = value;
87
+ }
88
+
89
+ /**
90
+ * The attributes that aren't mass assignable.
91
+ *
92
+ * @var string[] // Default to guarding everything
93
+ */
94
+ protected _guarded: string[] = ['*'];
95
+
96
+ public get guarded(): string[] {
97
+ return this._guarded;
98
+ }
99
+
100
+ public set guarded(value: string[]) {
101
+ this._guarded = value;
102
+ }
103
+
104
+ /**
105
+ * Indicates if the model is totally guarded.
106
+ *
107
+ * @var boolean
108
+ */
109
+ protected static totallyGuarded: boolean = false;
110
+
111
+ /**
112
+ * Indicates if the model exists in the database.
113
+ */
114
+ public exists: boolean = false;
115
+
116
+ /**
117
+ * Indicates if the model should be timestamped.
118
+ */
119
+ protected timestamps: boolean = true;
120
+
121
+ /**
122
+ * The name of the "created at" column.
123
+ */
124
+ protected static CREATED_AT: string = 'created_at';
125
+
126
+ /**
127
+ * The name of the "updated at" column.
128
+ */
129
+ protected static UPDATED_AT: string = 'updated_at';
130
+
131
+ /**
132
+ * The event listeners for the model.
133
+ * Stored per model class to avoid cross-contamination.
134
+ */
135
+ protected static eventListeners: Map<string, Map<ModelEventType, ModelEventCallback[]>> = new Map();
136
+
137
+ /**
138
+ * The loaded relationships for this model.
139
+ * Used for eager loading to store pre-loaded relationships.
140
+ */
141
+ protected relations: Map<string, Model | Model[] | null> = new Map();
142
+
143
+ /**
144
+ * Global scopes for the model.
145
+ */
146
+ protected static globalScopes: Map<string, any> = new Map();
147
+
148
+ /**
149
+ * Booted models.
150
+ */
151
+ protected static booted: Map<string, boolean> = new Map();
152
+
153
+ /**
154
+ * The attributes that should be cast to native types.
155
+ */
156
+ protected casts: Record<string, string> = {};
157
+
158
+ /**
159
+ * The attributes that should be hidden for arrays.
160
+ */
161
+ protected hidden: string[] = [];
162
+
163
+ /**
164
+ * The attributes that should be visible in arrays.
165
+ */
166
+ protected visible: string[] = [];
167
+
168
+ /**
169
+ * The set of traits callbacks.
170
+ */
171
+ protected static traitInitializers: Map<string, any[]> = new Map();
172
+
173
+ /**
174
+ * Boot the model.
175
+ */
176
+ public static boot(): void {
177
+ if (this.booted.has(this.name)) {
178
+ return;
179
+ }
180
+
181
+ this.booted.set(this.name, true);
182
+
183
+ // Boot traits
184
+ this.bootTraits();
185
+ }
186
+
187
+ /**
188
+ * Boot all of the bootable traits on the model.
189
+ */
190
+ protected static bootTraits(): void {
191
+ const booted = new Set<string>();
192
+ let current = this;
193
+
194
+ while (current && current.name !== 'Model') {
195
+ const keys = Object.getOwnPropertyNames(current);
196
+ for (const key of keys) {
197
+ if (key.startsWith('boot') && key !== 'boot' && key !== 'bootTraits') {
198
+ if (!booted.has(key) && typeof (this as any)[key] === 'function') {
199
+ (this as any)[key]();
200
+ booted.add(key);
201
+ }
202
+ }
203
+ }
204
+ current = Object.getPrototypeOf(current);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Register a new global scope on the model.
210
+ */
211
+ public static addGlobalScope(scope: any, implementation?: any): void {
212
+ if (typeof scope === 'string' && implementation) {
213
+ this.globalScopes.set(scope, implementation);
214
+ } else if (scope.constructor) {
215
+ this.globalScopes.set(scope.constructor.name, scope);
216
+ }
217
+ }
218
+
219
+
220
+ /**
221
+ * Create a new model instance.
222
+ */
223
+ constructor(attributes: Record<string, unknown> = {}) {
224
+ this.fill(attributes);
225
+
226
+ // Return a proxy to handle attribute access directly on the model instance
227
+ return new Proxy(this, {
228
+ get: (target, prop: string | symbol, receiver) => {
229
+ // Return class properties/methods first
230
+ if (prop in target) {
231
+ return Reflect.get(target, prop, receiver);
232
+ }
233
+
234
+ // Then try attributes
235
+ if (typeof prop === 'string') {
236
+ // console.log(`Proxy get attribute: ${prop}`);
237
+ return target.getAttribute(prop);
238
+ }
239
+
240
+ return undefined;
241
+ },
242
+ set: (target, prop: string | symbol, value, receiver) => {
243
+ // If property exists on class, set it
244
+ if (prop in target) {
245
+ return Reflect.set(target, prop, value, receiver);
246
+ }
247
+
248
+ // Otherwise set attribute
249
+ if (typeof prop === 'string') {
250
+ if (prop === 'table' || prop === 'primaryKey' || prop === 'forceDeleting') {
251
+ console.log(`Preventing ${prop} from being set as attribute. defining on target.`);
252
+ Object.defineProperty(target, prop, { value, writable: true, configurable: true, enumerable: true });
253
+ return true;
254
+ }
255
+ target.setAttribute(prop, value);
256
+ return true;
257
+ }
258
+
259
+ return false;
260
+ }
261
+ });
262
+ }
263
+
264
+ // ==========================================
265
+ // Attribute Handling
266
+ // ==========================================
267
+
268
+ /**
269
+ * Fill the model with an array of attributes.
270
+ *
271
+ * @throws Error if mass assignment protection is violated (optional, traditionally just ignored)
272
+ */
273
+ public fill(attributes: Record<string, unknown>): this {
274
+ const totallyGuarded = this.guarded.length === 1 && this.guarded[0] === '*';
275
+ console.log(`Filling model ${this.constructor.name}:`, attributes);
276
+ console.log(`Guarded:`, this.guarded);
277
+ console.log(`Totally Guarded:`, totallyGuarded);
278
+
279
+ for (const key in attributes) {
280
+ const fillable = this.isFillable(key);
281
+ console.log(`Key '${key}' fillable:`, fillable);
282
+ if (fillable) {
283
+ this.setAttribute(key, attributes[key]);
284
+ } else if (totallyGuarded) {
285
+ // Optional: We could throw an error here or log a warning
286
+ // console.warn(`Mass assignment to guarded attribute '${key}' on model '${this.constructor.name}'`);
287
+ }
288
+ }
289
+ return this;
290
+ }
291
+
292
+ /**
293
+ * Force fill the model with an array of attributes.
294
+ * This bypasses mass assignment protection.
295
+ */
296
+ public forceFill(attributes: Record<string, unknown>): this {
297
+ for (const key in attributes) {
298
+ this.setAttribute(key, attributes[key]);
299
+ }
300
+ return this;
301
+ }
302
+
303
+ /**
304
+ * Determine if the given attribute may be mass assigned.
305
+ */
306
+ public isFillable(key: string): boolean {
307
+ // If the key is in the fillable array, it can be filled
308
+ if (this.fillable.includes(key)) {
309
+ return true;
310
+ }
311
+
312
+ // If the model is totally guarded, return false
313
+ if (this.guarded.length === 1 && this.guarded[0] === '*') {
314
+ return false;
315
+ }
316
+
317
+ // If the model is not guarded and the key is not in the guarded array, it can be filled
318
+ if (!this.guarded.includes(key) && !this.guarded.includes('*')) {
319
+ return true;
320
+ }
321
+
322
+ return false;
323
+ }
324
+
325
+ /**
326
+ * Get an attribute from the model.
327
+ */
328
+ public getAttribute(key: string): unknown {
329
+ if (!key) {
330
+ return;
331
+ }
332
+
333
+ // If the attribute has an accessor, we will call that to get the value
334
+ if (this.hasGetMutator(key)) {
335
+ return this.mutateAttribute(key, this.attributes[key]);
336
+ }
337
+
338
+ // If the attribute exists in the attributes array or has a "get" mutator
339
+ if (key in this.attributes) {
340
+ return this.getAttributeValue(key);
341
+ }
342
+
343
+ // If the attribute is a relation
344
+ if (this.relations.has(key)) {
345
+ return this.relations.get(key);
346
+ }
347
+
348
+ // Check if it's a method on the model (e.g., relationship method)
349
+ if (key in this && typeof (this as any)[key] === 'function') {
350
+ return (this as any)[key];
351
+ }
352
+
353
+ return undefined;
354
+ }
355
+
356
+ /**
357
+ * Get a plain attribute (not a relationship).
358
+ */
359
+ public getAttributeValue(key: string): unknown {
360
+ const value = this.attributes[key];
361
+
362
+ if (this.hasCast(key)) {
363
+ return this.castAttribute(key, value);
364
+ }
365
+
366
+ return value;
367
+ }
368
+
369
+ /**
370
+ * Set a given attribute on the model.
371
+ */
372
+ public setAttribute(key: string, value: unknown): this {
373
+ // First check for a mutator
374
+ if (this.hasSetMutator(key)) {
375
+ this.setMutatedAttributeValue(key, value);
376
+ return this;
377
+ }
378
+
379
+ // If the attribute is JSON, we might want to cast it before setting,
380
+ // but usually we cast on read. However, if it's explicitly 'json',
381
+ // we might want to ensure it's stored correctly if the driver doesn't handle object->json automatically.
382
+ // For postgres.js, objects are usually handled, but let's be safe.
383
+ // Actually, standard Eloquent casts on read mostly, but for setting 'json', it ensures it's an object if passed as string?
384
+ // or vice versa?
385
+ // In strict casting, we might want to serialize objects to string if the DB column expects string.
386
+ // But postgres.js handles JSONB columns with objects directly.
387
+ // So we will stick to simple assignment unless specific logic is needed.
388
+
389
+ this.attributes[key] = value;
390
+ return this;
391
+ }
392
+
393
+ /**
394
+ * Get all attributes.
395
+ */
396
+ public getAttributes(): Record<string, unknown> {
397
+ return { ...this.attributes };
398
+ }
399
+
400
+ /**
401
+ * Get the original attributes.
402
+ */
403
+ public getOriginal(): Record<string, unknown> {
404
+ return { ...this.original };
405
+ }
406
+
407
+ /**
408
+ * Sync the original attributes with the current attributes.
409
+ */
410
+ public syncOriginal(): this {
411
+ this.original = { ...this.attributes };
412
+ return this;
413
+ }
414
+
415
+ /**
416
+ * Get the attributes that have been changed.
417
+ */
418
+ public getDirty(): Record<string, unknown> {
419
+ const dirty: Record<string, unknown> = {};
420
+ for (const key in this.attributes) {
421
+ if (this.attributes[key] !== this.original[key]) {
422
+ dirty[key] = this.attributes[key];
423
+ }
424
+ }
425
+ return dirty;
426
+ }
427
+
428
+ /**
429
+ * Check if the model or a given attribute has been modified.
430
+ */
431
+ public isDirty(attribute?: string): boolean {
432
+ if (attribute) {
433
+ return this.attributes[attribute] !== this.original[attribute];
434
+ }
435
+ return Object.keys(this.getDirty()).length > 0;
436
+ }
437
+
438
+ // ==========================================
439
+ // Eager Loading / Relation Storage
440
+ // ==========================================
441
+
442
+ /**
443
+ * Set a loaded relationship on the model.
444
+ * Used internally by eager loading to store pre-loaded relationships.
445
+ *
446
+ * @param relation - The name of the relationship
447
+ * @param value - The loaded model(s) or null
448
+ */
449
+ public setRelation(relation: string, value: Model | Model[] | null): this {
450
+ this.relations.set(relation, value);
451
+ return this;
452
+ }
453
+
454
+ /**
455
+ * Get a loaded relationship from the model.
456
+ * Returns undefined if the relationship hasn't been loaded.
457
+ *
458
+ * @param relation - The name of the relationship
459
+ * @returns The loaded relationship value or undefined
460
+ */
461
+ public getRelation(relation: string): Model | Model[] | null | undefined {
462
+ return this.relations.get(relation);
463
+ }
464
+
465
+ /**
466
+ * Check if a relationship has been loaded.
467
+ *
468
+ * @param relation - The name of the relationship
469
+ * @returns True if the relationship has been loaded
470
+ */
471
+ public relationLoaded(relation: string): boolean {
472
+ return this.relations.has(relation);
473
+ }
474
+
475
+ /**
476
+ * Get all loaded relationships.
477
+ *
478
+ * @returns Map of all loaded relationships
479
+ */
480
+ public getRelations(): Map<string, Model | Model[] | null> {
481
+ return new Map(this.relations);
482
+ }
483
+
484
+ /**
485
+ * Set multiple relationships at once.
486
+ *
487
+ * @param relations - Map of relationship names to values
488
+ */
489
+ public setRelations(relations: Map<string, Model | Model[] | null>): this {
490
+ for (const [name, value] of relations) {
491
+ this.relations.set(name, value);
492
+ }
493
+ return this;
494
+ }
495
+
496
+ // ==========================================
497
+ // Table & Key Information
498
+ // ==========================================
499
+
500
+
501
+ /**
502
+ * Get the table associated with the model.
503
+ */
504
+ public getTable(): string {
505
+ if (!this.table) {
506
+ this.table = this.inferTableName();
507
+ }
508
+ return this.table;
509
+ }
510
+
511
+ /**
512
+ * Get the primary key for the model.
513
+ */
514
+ public getKeyName(): string {
515
+ return this.primaryKey;
516
+ }
517
+
518
+ /**
519
+ * Get the value of the model's primary key.
520
+ */
521
+ public getKey(): unknown {
522
+ return this.getAttribute(this.getKeyName());
523
+ }
524
+
525
+ /**
526
+ * Get the default foreign key name for the model.
527
+ */
528
+ public getForeignKey(): string {
529
+ return this.constructor.name.toLowerCase() + '_id';
530
+ }
531
+
532
+ /**
533
+ * Convert the model instance to JSON.
534
+ */
535
+ public toJSON(): Record<string, unknown> {
536
+ return this.toArray();
537
+ }
538
+
539
+ /**
540
+ * Convert the model instance to an array.
541
+ */
542
+ public toArray(): Record<string, unknown> {
543
+ return {
544
+ ...this.attributesToArray(),
545
+ ...this.relationsToArray(),
546
+ };
547
+ }
548
+
549
+ /**
550
+ * Convert the model's attributes to an array.
551
+ */
552
+ public attributesToArray(): Record<string, unknown> {
553
+ const attributes: Record<string, unknown> = {};
554
+
555
+ // Get all attributes, processing accessors
556
+ const keys = Object.keys(this.attributes);
557
+
558
+ // Also look for accessors that don't have matching attributes
559
+ const prototype = Object.getPrototypeOf(this);
560
+ const methodNames = Object.getOwnPropertyNames(prototype);
561
+ for (const name of methodNames) {
562
+ if (name.startsWith('get') && name.endsWith('Attribute') && name !== 'getAttribute') {
563
+ const key = this.snakeCase(name.slice(3, -9));
564
+ if (!keys.includes(key)) {
565
+ keys.push(key);
566
+ }
567
+ }
568
+ }
569
+
570
+ for (const key of keys) {
571
+ if (!this.isVisible(key)) {
572
+ continue;
573
+ }
574
+ attributes[key] = this.getAttribute(key);
575
+ }
576
+
577
+ return attributes;
578
+ }
579
+
580
+ /**
581
+ * Get the model's relationships in array form.
582
+ */
583
+ public relationsToArray(): Record<string, unknown> {
584
+ const attributes: Record<string, unknown> = {};
585
+
586
+ for (const [key, value] of this.relations) {
587
+ if (!this.isVisible(key)) {
588
+ continue;
589
+ }
590
+
591
+ if (value instanceof Model) {
592
+ attributes[key] = value.toArray();
593
+ } else if (Array.isArray(value)) {
594
+ attributes[key] = value.map((model) => model instanceof Model ? model.toArray() : model);
595
+ } else {
596
+ attributes[key] = value;
597
+ }
598
+ }
599
+
600
+ return attributes;
601
+ }
602
+
603
+ /**
604
+ * Determine if an attribute should be visible.
605
+ */
606
+ protected isVisible(key: string): boolean {
607
+ if (this.visible.length > 0) {
608
+ return this.visible.includes(key);
609
+ }
610
+
611
+ if (this.hidden.length > 0) {
612
+ return !this.hidden.includes(key);
613
+ }
614
+
615
+ return true;
616
+ }
617
+
618
+ /**
619
+ * Infer the table name from the class name.
620
+ */
621
+ protected inferTableName(): string {
622
+ return this.snakeCase(this.constructor.name) + 's';
623
+ }
624
+
625
+ /**
626
+ * Convert a string to snake case.
627
+ */
628
+ protected snakeCase(str: string): string {
629
+ return str.replace(/[A-Z]/g, (letter, index) => {
630
+ return index === 0 ? letter.toLowerCase() : '_' + letter.toLowerCase();
631
+ });
632
+ }
633
+
634
+ // ==========================================
635
+ // Casting & Mutators
636
+ // ==========================================
637
+
638
+ /**
639
+ * Determine if an attribute has a cast.
640
+ */
641
+ public hasCast(key: string): boolean {
642
+ return key in this.casts;
643
+ }
644
+
645
+ /**
646
+ * Cast an attribute to a native PHP type.
647
+ */
648
+ protected castAttribute(key: string, value: unknown): unknown {
649
+ if (value === null || value === undefined) {
650
+ return value;
651
+ }
652
+
653
+ const type = this.casts[key].trim().toLowerCase();
654
+
655
+ switch (type) {
656
+ case 'int':
657
+ case 'integer':
658
+ return typeof value === 'string' ? parseInt(value, 10) : Math.floor(value as number);
659
+ case 'real':
660
+ case 'float':
661
+ case 'double':
662
+ return typeof value === 'string' ? parseFloat(value) : value;
663
+ case 'string':
664
+ return String(value);
665
+ case 'bool':
666
+ case 'boolean':
667
+ return Boolean(value);
668
+ case 'object':
669
+ case 'json':
670
+ if (typeof value === 'string') {
671
+ try {
672
+ return JSON.parse(value);
673
+ } catch (e) {
674
+ return value;
675
+ }
676
+ }
677
+ return value;
678
+ case 'date':
679
+ case 'datetime':
680
+ if (value instanceof Date) {
681
+ return value;
682
+ }
683
+ return new Date(value as string | number);
684
+ default:
685
+ return value;
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Determine if a get mutator exists for an attribute.
691
+ */
692
+ public hasGetMutator(key: string): boolean {
693
+ return typeof (this as any)[this.getMutatorMethodName(key)] === 'function';
694
+ }
695
+
696
+ /**
697
+ * Determine if a set mutator exists for an attribute.
698
+ */
699
+ public hasSetMutator(key: string): boolean {
700
+ return typeof (this as any)[this.setMutatorMethodName(key)] === 'function';
701
+ }
702
+
703
+ /**
704
+ * Get the value of an attribute using its mutator.
705
+ */
706
+ protected mutateAttribute(key: string, value: unknown): unknown {
707
+ return (this as any)[this.getMutatorMethodName(key)](value);
708
+ }
709
+
710
+ /**
711
+ * Set the value of an attribute using its mutator.
712
+ */
713
+ protected setMutatedAttributeValue(key: string, value: unknown): void {
714
+ (this as any)[this.setMutatorMethodName(key)](value);
715
+ }
716
+
717
+ /**
718
+ * Get the mutator method name for a "get" mutator.
719
+ */
720
+ protected getMutatorMethodName(key: string): string {
721
+ const pascalKey = key
722
+ .split('_')
723
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
724
+ .join('');
725
+ return `get${pascalKey}Attribute`;
726
+ }
727
+
728
+ /**
729
+ * Get the mutator method name for a "set" mutator.
730
+ */
731
+ protected setMutatorMethodName(key: string): string {
732
+ const pascalKey = key
733
+ .split('_')
734
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
735
+ .join('');
736
+ return `set${pascalKey}Attribute`;
737
+ }
738
+
739
+ // ==========================================
740
+ // Timestamps
741
+ // ==========================================
742
+
743
+ /**
744
+ * Get a fresh timestamp for the model.
745
+ */
746
+ public freshTimestamp(): Date {
747
+ return new Date();
748
+ }
749
+
750
+ /**
751
+ * Get a fresh timestamp string for the model.
752
+ */
753
+ public freshTimestampString(): string {
754
+ return this.freshTimestamp().toISOString();
755
+ }
756
+
757
+ /**
758
+ * Get the name of the "created at" column.
759
+ */
760
+ public getCreatedAtColumn(): string {
761
+ return (this.constructor as typeof Model).CREATED_AT;
762
+ }
763
+
764
+ /**
765
+ * Get the name of the "updated at" column.
766
+ */
767
+ public getUpdatedAtColumn(): string {
768
+ return (this.constructor as typeof Model).UPDATED_AT;
769
+ }
770
+
771
+ /**
772
+ * Determine if the model uses timestamps.
773
+ */
774
+ public usesTimestamps(): boolean {
775
+ return this.timestamps;
776
+ }
777
+
778
+ /**
779
+ * Set the timestamps for a create operation.
780
+ */
781
+ protected setCreatedAt(): this {
782
+ if (!this.usesTimestamps()) {
783
+ return this;
784
+ }
785
+
786
+ const timestamp = this.freshTimestamp();
787
+ this.setAttribute(this.getCreatedAtColumn(), timestamp);
788
+ return this;
789
+ }
790
+
791
+ /**
792
+ * Set the timestamps for an update operation.
793
+ */
794
+ protected setUpdatedAt(): this {
795
+ if (!this.usesTimestamps()) {
796
+ return this;
797
+ }
798
+
799
+ const timestamp = this.freshTimestamp();
800
+ this.setAttribute(this.getUpdatedAtColumn(), timestamp);
801
+ return this;
802
+ }
803
+
804
+ // ==========================================
805
+ // Model Events
806
+ // ==========================================
807
+
808
+ /**
809
+ * Get the event listeners for the model class.
810
+ */
811
+ protected static getModelListeners(modelName: string): Map<ModelEventType, ModelEventCallback[]> {
812
+ if (!this.eventListeners.has(modelName)) {
813
+ this.eventListeners.set(modelName, new Map());
814
+ }
815
+ return this.eventListeners.get(modelName)!;
816
+ }
817
+
818
+ /**
819
+ * Register a model event callback.
820
+ */
821
+ protected static registerModelEvent(event: ModelEventType, callback: ModelEventCallback): void {
822
+ const listeners = this.getModelListeners(this.name);
823
+ if (!listeners.has(event)) {
824
+ listeners.set(event, []);
825
+ }
826
+ listeners.get(event)!.push(callback);
827
+ }
828
+
829
+ /**
830
+ * Fire the given model event.
831
+ * Returns false if any listener returned false to halt the action.
832
+ */
833
+ protected async fireModelEvent(event: ModelEventType): Promise<boolean> {
834
+ const modelName = this.constructor.name;
835
+ const listeners = (this.constructor as typeof Model).getModelListeners(modelName);
836
+ const eventListeners = listeners.get(event) || [];
837
+
838
+ for (const callback of eventListeners) {
839
+ const result = await callback(this);
840
+ // If callback returns false, halt the action
841
+ if (result === false) {
842
+ return false;
843
+ }
844
+ }
845
+
846
+ return true;
847
+ }
848
+
849
+ /**
850
+ * Register a saving event listener.
851
+ */
852
+ static saving<T extends Model>(
853
+ this: new (attrs?: Record<string, unknown>) => T,
854
+ callback: ModelEventCallback<T>
855
+ ): void {
856
+ (this as unknown as typeof Model).registerModelEvent('saving', callback as ModelEventCallback);
857
+ }
858
+
859
+ /**
860
+ * Register a saved event listener.
861
+ */
862
+ static saved<T extends Model>(
863
+ this: new (attrs?: Record<string, unknown>) => T,
864
+ callback: ModelEventCallback<T>
865
+ ): void {
866
+ (this as unknown as typeof Model).registerModelEvent('saved', callback as ModelEventCallback);
867
+ }
868
+
869
+ /**
870
+ * Register a creating event listener.
871
+ */
872
+ static creating<T extends Model>(
873
+ this: new (attrs?: Record<string, unknown>) => T,
874
+ callback: ModelEventCallback<T>
875
+ ): void {
876
+ (this as unknown as typeof Model).registerModelEvent('creating', callback as ModelEventCallback);
877
+ }
878
+
879
+ /**
880
+ * Register a created event listener.
881
+ */
882
+ static created<T extends Model>(
883
+ this: new (attrs?: Record<string, unknown>) => T,
884
+ callback: ModelEventCallback<T>
885
+ ): void {
886
+ (this as unknown as typeof Model).registerModelEvent('created', callback as ModelEventCallback);
887
+ }
888
+
889
+ /**
890
+ * Register an updating event listener.
891
+ */
892
+ static updating<T extends Model>(
893
+ this: new (attrs?: Record<string, unknown>) => T,
894
+ callback: ModelEventCallback<T>
895
+ ): void {
896
+ (this as unknown as typeof Model).registerModelEvent('updating', callback as ModelEventCallback);
897
+ }
898
+
899
+ /**
900
+ * Register an updated event listener.
901
+ */
902
+ static updated<T extends Model>(
903
+ this: new (attrs?: Record<string, unknown>) => T,
904
+ callback: ModelEventCallback<T>
905
+ ): void {
906
+ (this as unknown as typeof Model).registerModelEvent('updated', callback as ModelEventCallback);
907
+ }
908
+
909
+ /**
910
+ * Register a deleting event listener.
911
+ */
912
+ static deleting<T extends Model>(
913
+ this: new (attrs?: Record<string, unknown>) => T,
914
+ callback: ModelEventCallback<T>
915
+ ): void {
916
+ (this as unknown as typeof Model).registerModelEvent('deleting', callback as ModelEventCallback);
917
+ }
918
+
919
+ /**
920
+ * Register a deleted event listener.
921
+ */
922
+ static deleted<T extends Model>(
923
+ this: new (attrs?: Record<string, unknown>) => T,
924
+ callback: ModelEventCallback<T>
925
+ ): void {
926
+ (this as unknown as typeof Model).registerModelEvent('deleted', callback as ModelEventCallback);
927
+ }
928
+
929
+ /**
930
+ * Clear all event listeners for this model class.
931
+ */
932
+ static clearEventListeners<T extends Model>(
933
+ this: new (attrs?: Record<string, unknown>) => T
934
+ ): void {
935
+ const modelClass = this as unknown as typeof Model;
936
+ modelClass.eventListeners.delete(modelClass.name);
937
+ }
938
+
939
+ // ==========================================
940
+ // CRUD Operations (Instance Methods)
941
+ // ==========================================
942
+
943
+ /**
944
+ * Save the model to the database.
945
+ * If the model exists, it updates; otherwise, it inserts.
946
+ */
947
+ public async save(): Promise<boolean> {
948
+ // Fire 'saving' event
949
+ if (!(await this.fireModelEvent('saving'))) {
950
+ return false;
951
+ }
952
+
953
+ let result: boolean;
954
+
955
+ if (this.exists) {
956
+ result = await this.performUpdate();
957
+ } else {
958
+ result = await this.performInsert();
959
+ }
960
+
961
+ if (result) {
962
+ // Fire 'saved' event
963
+ await this.fireModelEvent('saved');
964
+ }
965
+
966
+ return result;
967
+ }
968
+
969
+ /**
970
+ * Perform an INSERT operation for a new model.
971
+ */
972
+ protected async performInsert(): Promise<boolean> {
973
+ // Fire 'creating' event
974
+ if (!(await this.fireModelEvent('creating'))) {
975
+ return false;
976
+ }
977
+
978
+ // Set timestamps
979
+ this.setCreatedAt();
980
+ this.setUpdatedAt();
981
+
982
+ const query = this.newQuery();
983
+
984
+ // Insert and get the ID back
985
+ const result = await query.insertReturning<Record<string, unknown>>(
986
+ this.attributes as InsertValues,
987
+ ['*'] // Return all columns
988
+ );
989
+
990
+ if (result) {
991
+ // Update model with returned values (including generated ID)
992
+ this.fill(result);
993
+ this.exists = true;
994
+ this.syncOriginal();
995
+
996
+ // Fire 'created' event
997
+ await this.fireModelEvent('created');
998
+
999
+ return true;
1000
+ }
1001
+
1002
+ return false;
1003
+ }
1004
+
1005
+ /**
1006
+ * Perform an UPDATE operation for an existing model.
1007
+ */
1008
+ protected async performUpdate(): Promise<boolean> {
1009
+ // Fire 'updating' event
1010
+ if (!(await this.fireModelEvent('updating'))) {
1011
+ return false;
1012
+ }
1013
+
1014
+ // Set updated_at timestamp
1015
+ this.setUpdatedAt();
1016
+
1017
+ const dirty = this.getDirty();
1018
+
1019
+ // Nothing to update (even after setting updated_at)
1020
+ if (Object.keys(dirty).length === 0) {
1021
+ return true;
1022
+ }
1023
+
1024
+ const query = this.newQuery();
1025
+ const affected = await query
1026
+ .where(this.getKeyName(), '=', this.getKey() as Binding)
1027
+ .update(dirty as UpdateValues);
1028
+
1029
+ if (affected > 0) {
1030
+ this.syncOriginal();
1031
+
1032
+ // Fire 'updated' event
1033
+ await this.fireModelEvent('updated');
1034
+
1035
+ return true;
1036
+ }
1037
+
1038
+ return false;
1039
+ }
1040
+
1041
+ /**
1042
+ * Update the model in the database.
1043
+ */
1044
+ public async update(attributes: Record<string, unknown>): Promise<boolean> {
1045
+ if (!this.exists) {
1046
+ return false;
1047
+ }
1048
+
1049
+ this.fill(attributes);
1050
+ return this.save();
1051
+ }
1052
+
1053
+ /**
1054
+ * Delete the model from the database.
1055
+ */
1056
+ public async delete(): Promise<boolean> {
1057
+ if (!this.exists) {
1058
+ return false;
1059
+ }
1060
+
1061
+ // Fire 'deleting' event
1062
+ if (!(await this.fireModelEvent('deleting'))) {
1063
+ return false;
1064
+ }
1065
+
1066
+ const query = this.newQuery();
1067
+ const affected = await query
1068
+ .where(this.getKeyName(), '=', this.getKey() as Binding)
1069
+ .delete();
1070
+
1071
+ if (affected > 0) {
1072
+ this.exists = false;
1073
+
1074
+ // Fire 'deleted' event
1075
+ await this.fireModelEvent('deleted');
1076
+
1077
+ return true;
1078
+ }
1079
+
1080
+ return false;
1081
+ }
1082
+
1083
+ /**
1084
+ * Refresh the model from the database.
1085
+ */
1086
+ public async refresh(): Promise<this | null> {
1087
+ if (!this.exists) {
1088
+ return null;
1089
+ }
1090
+
1091
+ const fresh = await this.newQuery()
1092
+ .where(this.getKeyName(), '=', this.getKey() as Binding)
1093
+ .first();
1094
+
1095
+ if (fresh) {
1096
+ this.fill(fresh.getAttributes());
1097
+ this.syncOriginal();
1098
+ return this;
1099
+ }
1100
+
1101
+ return null;
1102
+ }
1103
+
1104
+ // ==========================================
1105
+ // Query Builder Integration
1106
+ // ==========================================
1107
+
1108
+
1109
+
1110
+ /**
1111
+ * Get a new query builder for the model's table.
1112
+ */
1113
+ public newQuery(): Builder<this> {
1114
+ const query = DB.table(this.getTable());
1115
+ const builder = new Builder<this>(query);
1116
+
1117
+ builder.setModel(this);
1118
+
1119
+ // Apply global scopes
1120
+ const scopes = (this.constructor as typeof Model).globalScopes;
1121
+ if (scopes.size > 0) {
1122
+ for (const [name, scope] of scopes.entries()) {
1123
+ builder.withGlobalScope(name, scope);
1124
+ }
1125
+ }
1126
+
1127
+ return builder;
1128
+ }
1129
+
1130
+ /**
1131
+ * Create a new instance of the model with the given attributes.
1132
+ */
1133
+ public newInstance(
1134
+ attributes: Record<string, unknown> = {},
1135
+ exists: boolean = false
1136
+ ): this {
1137
+ const ModelClass = this.constructor as new (attrs?: Record<string, unknown>) => this;
1138
+
1139
+ // If hydrating (exists=true), bypass constructor assignment to avoid mass assignment protection
1140
+ // and use forceFill() instead.
1141
+ const instance = new ModelClass(exists ? {} : attributes);
1142
+
1143
+ if (exists) {
1144
+ instance.forceFill(attributes);
1145
+ instance.exists = true;
1146
+ instance.syncOriginal();
1147
+ }
1148
+
1149
+ return instance;
1150
+ }
1151
+
1152
+ /**
1153
+ * Convert a database row to a model instance.
1154
+ */
1155
+ protected hydrateModel(row: Record<string, unknown>): this {
1156
+ return this.newInstance(row, true);
1157
+ }
1158
+
1159
+ /**
1160
+ * Convert multiple database rows to model instances.
1161
+ */
1162
+ protected hydrateModels(rows: Record<string, unknown>[]): this[] {
1163
+ return rows.map((row) => this.hydrateModel(row));
1164
+ }
1165
+
1166
+ // ==========================================
1167
+ // Static CRUD Methods
1168
+ // ==========================================
1169
+
1170
+ /**
1171
+ * Create a new model and persist it to the database.
1172
+ */
1173
+ static async create<T extends Model>(
1174
+ this: new (attrs?: Record<string, unknown>) => T,
1175
+ attributes: Record<string, unknown>
1176
+ ): Promise<T> {
1177
+ const instance = new this(attributes);
1178
+ // Ensure boot is called
1179
+ (this as unknown as typeof Model).boot();
1180
+
1181
+ await instance.save();
1182
+ return instance;
1183
+ }
1184
+
1185
+ /**
1186
+ * Insert multiple records into the database.
1187
+ */
1188
+ static async insert<T extends Model>(
1189
+ this: new (attrs?: Record<string, unknown>) => T,
1190
+ records: InsertValues[]
1191
+ ): Promise<boolean> {
1192
+ const instance = new this();
1193
+ return instance.newQuery().insert(records);
1194
+ }
1195
+
1196
+ /**
1197
+ * Find a model by its primary key.
1198
+ */
1199
+ static async find<T extends Model>(
1200
+ this: new (attrs?: Record<string, unknown>) => T,
1201
+ id: number | string
1202
+ ): Promise<T | null> {
1203
+ const instance = new this();
1204
+ return instance.newQuery()
1205
+ .where(instance.getKeyName(), '=', id)
1206
+ .first();
1207
+ }
1208
+
1209
+ /**
1210
+ * Find a model by its primary key or throw an exception.
1211
+ */
1212
+ static async findOrFail<T extends Model>(
1213
+ this: new (attrs?: Record<string, unknown>) => T,
1214
+ id: number | string
1215
+ ): Promise<T> {
1216
+ const result = await (this as unknown as typeof Model).find.call(this, id);
1217
+
1218
+ if (!result) {
1219
+ throw new ModelNotFoundException(this.name, id);
1220
+ }
1221
+
1222
+ return result as T;
1223
+ }
1224
+
1225
+ /**
1226
+ * Get all models from the database.
1227
+ */
1228
+ static async all<T extends Model>(
1229
+ this: new (attrs?: Record<string, unknown>) => T
1230
+ ): Promise<T[]> {
1231
+ const instance = new this();
1232
+ return instance.newQuery().get();
1233
+ }
1234
+
1235
+ /**
1236
+ * Get the first model from the database.
1237
+ */
1238
+ static async first<T extends Model>(
1239
+ this: new (attrs?: Record<string, unknown>) => T
1240
+ ): Promise<T | null> {
1241
+ const instance = new this();
1242
+ return instance.newQuery().first();
1243
+ }
1244
+
1245
+ /**
1246
+ * Destroy model(s) by primary key.
1247
+ */
1248
+ static async destroy<T extends Model>(
1249
+ this: new (attrs?: Record<string, unknown>) => T,
1250
+ ids: number | string | (number | string)[]
1251
+ ): Promise<number> {
1252
+ const instance = new this();
1253
+ const idArray = Array.isArray(ids) ? ids : [ids];
1254
+
1255
+ if (idArray.length === 0) {
1256
+ return 0;
1257
+ }
1258
+
1259
+ return instance.newQuery()
1260
+ .whereIn(instance.getKeyName(), idArray)
1261
+ .delete();
1262
+ }
1263
+
1264
+ /**
1265
+ * Get a fresh query builder for the model.
1266
+ */
1267
+ static query<T extends Model>(
1268
+ this: new (attrs?: Record<string, unknown>) => T
1269
+ ): Builder {
1270
+ const instance = new this();
1271
+ return instance.newQuery();
1272
+ }
1273
+
1274
+ /**
1275
+ * Add a WHERE clause and return the query builder.
1276
+ */
1277
+ static where<T extends Model>(
1278
+ this: new (attrs?: Record<string, unknown>) => T,
1279
+ column: string,
1280
+ operatorOrValue?: string | number | boolean | null,
1281
+ value?: string | number | boolean | null
1282
+ ): Builder {
1283
+ const instance = new this();
1284
+ return instance.newQuery().where(column, operatorOrValue, value);
1285
+ }
1286
+
1287
+ /**
1288
+ * Add a WHERE IN clause and return the query builder.
1289
+ */
1290
+ static whereIn<T extends Model>(
1291
+ this: new (attrs?: Record<string, unknown>) => T,
1292
+ column: string,
1293
+ values: (string | number | boolean | null)[]
1294
+ ): Builder {
1295
+ const instance = new this();
1296
+ return instance.newQuery().whereIn(column, values);
1297
+ }
1298
+
1299
+ /**
1300
+ * Add a WHERE NULL clause and return the query builder.
1301
+ */
1302
+ static whereNull<T extends Model>(
1303
+ this: new (attrs?: Record<string, unknown>) => T,
1304
+ column: string
1305
+ ): Builder {
1306
+ const instance = new this();
1307
+ return instance.newQuery().whereNull(column);
1308
+ }
1309
+
1310
+ /**
1311
+ * Add a WHERE NOT NULL clause and return the query builder.
1312
+ */
1313
+ static whereNotNull<T extends Model>(
1314
+ this: new (attrs?: Record<string, unknown>) => T,
1315
+ column: string
1316
+ ): Builder {
1317
+ const instance = new this();
1318
+ return instance.newQuery().whereNotNull(column);
1319
+ }
1320
+
1321
+ /**
1322
+ * Add an ORDER BY clause and return the query builder.
1323
+ */
1324
+ static orderBy<T extends Model>(
1325
+ this: new (attrs?: Record<string, unknown>) => T,
1326
+ column: string,
1327
+ direction: 'asc' | 'desc' = 'asc'
1328
+ ): Builder {
1329
+ const instance = new this();
1330
+ return instance.newQuery().orderBy(column, direction);
1331
+ }
1332
+
1333
+ /**
1334
+ * Add a LIMIT clause and return the query builder.
1335
+ */
1336
+ static limit<T extends Model>(
1337
+ this: new (attrs?: Record<string, unknown>) => T,
1338
+ value: number
1339
+ ): Builder {
1340
+ const instance = new this();
1341
+ return instance.newQuery().limit(value);
1342
+ }
1343
+
1344
+ // ==========================================
1345
+ // Eager Loading
1346
+ // ==========================================
1347
+
1348
+ /**
1349
+ * Begin a query with eager loading for the specified relationships.
1350
+ *
1351
+ * This solves the N+1 query problem by loading all related models in bulk
1352
+ * rather than executing a separate query for each parent model.
1353
+ *
1354
+ * Usage:
1355
+ * ```typescript
1356
+ * // Load users with their posts
1357
+ * const users = await User.with('posts').get();
1358
+ *
1359
+ * // Load users with multiple relationships
1360
+ * const users = await User.with('posts', 'profile').get();
1361
+ *
1362
+ * // Load users with nested relationships (posts and their comments)
1363
+ * const users = await User.with('posts.comments').get();
1364
+ *
1365
+ * // Chain with query constraints
1366
+ * const users = await User.with('posts')
1367
+ * .where('active', true)
1368
+ * .orderBy('created_at', 'desc')
1369
+ * .limit(10)
1370
+ * .get();
1371
+ *
1372
+ * // Access eagerly loaded relations
1373
+ * for (const user of users) {
1374
+ * const posts = user.getRelation('posts'); // Already loaded, no query
1375
+ * console.log(posts);
1376
+ * }
1377
+ * ```
1378
+ *
1379
+ * @param relations - The relationship names to eager load (can be nested with dots)
1380
+ * @returns An EagerLoadingBuilder for chaining
1381
+ */
1382
+ static with<T extends Model>(
1383
+ this: new (attrs?: Record<string, unknown>) => T,
1384
+ ...relations: string[]
1385
+ ): EagerLoadingBuilder<T> {
1386
+ const instance = new this();
1387
+ const builder = instance.newQuery();
1388
+ return new EagerLoadingBuilder<T>(this, builder, relations);
1389
+ }
1390
+
1391
+ // ==========================================
1392
+ // Relationship Methods
1393
+ // ==========================================
1394
+
1395
+
1396
+ /**
1397
+ * Define a one-to-one relationship.
1398
+ *
1399
+ * The foreign key is on the RELATED model's table, referencing this model.
1400
+ *
1401
+ * Example: User has one Profile (profiles.user_id references users.id)
1402
+ *
1403
+ * Usage:
1404
+ * ```typescript
1405
+ * class User extends Model {
1406
+ * profile() {
1407
+ * return this.hasOne(Profile, 'user_id', 'id');
1408
+ * }
1409
+ * }
1410
+ *
1411
+ * const user = await User.find(1);
1412
+ * const profile = await user.profile().get();
1413
+ * ```
1414
+ *
1415
+ * @param RelatedClass - The related model class constructor
1416
+ * @param foreignKey - The foreign key column on the related model (defaults to {thisModel}_id)
1417
+ * @param localKey - The local key column on this model (defaults to 'id')
1418
+ * @returns A HasOne relationship instance
1419
+ */
1420
+ protected hasOne<TRelated extends Model>(
1421
+ RelatedClass: new (attrs?: Record<string, unknown>) => TRelated,
1422
+ foreignKey?: string,
1423
+ localKey?: string
1424
+ ): HasOne<TRelated> {
1425
+ const relatedInstance = new RelatedClass();
1426
+
1427
+ // Default foreign key: user_id (for User model)
1428
+ const fk = foreignKey || this.getForeignKey();
1429
+
1430
+ // Default local key: id
1431
+ const lk = localKey || this.getKeyName();
1432
+
1433
+ return new HasOne<TRelated>(this, relatedInstance, fk, lk);
1434
+ }
1435
+
1436
+ /**
1437
+ * Define a belongs-to relationship (inverse of hasOne or hasMany).
1438
+ *
1439
+ * The foreign key is on THIS model's table, referencing the parent.
1440
+ *
1441
+ * Example: Profile belongs to User (profiles.user_id references users.id)
1442
+ *
1443
+ * Usage:
1444
+ * ```typescript
1445
+ * class Profile extends Model {
1446
+ * user() {
1447
+ * return this.belongsTo(User, 'user_id', 'id');
1448
+ * }
1449
+ * }
1450
+ *
1451
+ * const profile = await Profile.find(1);
1452
+ * const user = await profile.user().get();
1453
+ * ```
1454
+ *
1455
+ * @param RelatedClass - The parent model class constructor
1456
+ * @param foreignKey - The foreign key column on this model (defaults to {parent}_id)
1457
+ * @param ownerKey - The owner key column on the parent model (defaults to 'id')
1458
+ * @returns A BelongsTo relationship instance
1459
+ */
1460
+ protected belongsTo<TRelated extends Model>(
1461
+ RelatedClass: new (attrs?: Record<string, unknown>) => TRelated,
1462
+ foreignKey?: string,
1463
+ ownerKey?: string
1464
+ ): BelongsTo<TRelated> {
1465
+ const relatedInstance = new RelatedClass();
1466
+
1467
+ // Default foreign key: user_id (for User parent model)
1468
+ const fk = foreignKey || relatedInstance.getForeignKey();
1469
+
1470
+ // Default owner key: id
1471
+ const ok = ownerKey || relatedInstance.getKeyName();
1472
+
1473
+ return new BelongsTo<TRelated>(this, relatedInstance, fk, ok);
1474
+ }
1475
+
1476
+ /**
1477
+ * Define a one-to-many relationship.
1478
+ *
1479
+ * The foreign key is on the RELATED model's table, referencing this model.
1480
+ *
1481
+ * Example: User has many Posts (posts.user_id references users.id)
1482
+ *
1483
+ * Usage:
1484
+ * ```typescript
1485
+ * class User extends Model {
1486
+ * posts() {
1487
+ * return this.hasMany(Post, 'user_id', 'id');
1488
+ * }
1489
+ * }
1490
+ *
1491
+ * const user = await User.find(1);
1492
+ * const posts = await user.posts().get(); // Get all posts
1493
+ * const recentPosts = await user.posts()
1494
+ * .where('published', true)
1495
+ * .orderBy('created_at', 'desc')
1496
+ * .limit(5)
1497
+ * .get();
1498
+ * ```
1499
+ *
1500
+ * @param RelatedClass - The related model class constructor
1501
+ * @param foreignKey - The foreign key column on the related model (defaults to {thisModel}_id)
1502
+ * @param localKey - The local key column on this model (defaults to 'id')
1503
+ * @returns A HasMany relationship instance
1504
+ */
1505
+ protected hasMany<TRelated extends Model>(
1506
+ RelatedClass: new (attrs?: Record<string, unknown>) => TRelated,
1507
+ foreignKey?: string,
1508
+ localKey?: string
1509
+ ): HasMany<TRelated> {
1510
+ const relatedInstance = new RelatedClass();
1511
+
1512
+ // Default foreign key: user_id (for User model)
1513
+ const fk = foreignKey || this.getForeignKey();
1514
+
1515
+ // Default local key: id
1516
+ const lk = localKey || this.getKeyName();
1517
+
1518
+ return new HasMany<TRelated>(this, relatedInstance, fk, lk);
1519
+ }
1520
+
1521
+ /**
1522
+ * Define a many-to-many relationship with a pivot table.
1523
+ *
1524
+ * Uses an intermediate (pivot) table to connect two models.
1525
+ *
1526
+ * Example: Users belong to many Roles through role_user pivot table
1527
+ *
1528
+ * Usage:
1529
+ * ```typescript
1530
+ * class User extends Model {
1531
+ * roles() {
1532
+ * return this.belongsToMany(Role, 'role_user', 'user_id', 'role_id');
1533
+ * }
1534
+ * }
1535
+ *
1536
+ * const user = await User.find(1);
1537
+ * const roles = await user.roles().get(); // Get all roles
1538
+ * await user.roles().attach(1); // Attach a role
1539
+ * await user.roles().detach([1, 2]); // Detach roles
1540
+ * await user.roles().sync([1, 2, 3]); // Sync roles
1541
+ * ```
1542
+ *
1543
+ * @param RelatedClass - The related model class constructor
1544
+ * @param table - The pivot table name (defaults to alphabetically sorted model names: role_user)
1545
+ * @param foreignPivotKey - The foreign key for this model on the pivot table (defaults to {thisModel}_id)
1546
+ * @param relatedPivotKey - The foreign key for the related model on the pivot table (defaults to {related}_id)
1547
+ * @param parentKey - The primary key on this model (defaults to 'id')
1548
+ * @param relatedKey - The primary key on the related model (defaults to 'id')
1549
+ * @returns A BelongsToMany relationship instance
1550
+ */
1551
+ protected belongsToMany<TRelated extends Model>(
1552
+ RelatedClass: new (attrs?: Record<string, unknown>) => TRelated,
1553
+ table?: string,
1554
+ foreignPivotKey?: string,
1555
+ relatedPivotKey?: string,
1556
+ parentKey?: string,
1557
+ relatedKey?: string
1558
+ ): BelongsToMany<TRelated> {
1559
+ const relatedInstance = new RelatedClass();
1560
+
1561
+ // Generate default pivot table name (alphabetically sorted, e.g., role_user)
1562
+ const thisTable = this.inferTableName().replace(/s$/, ''); // Remove trailing 's'
1563
+ const relatedTable = relatedInstance.getTable().replace(/s$/, '');
1564
+ const defaultTable = [thisTable, relatedTable].sort().join('_');
1565
+
1566
+ const pivotTable = table || defaultTable;
1567
+ const fpk = foreignPivotKey || this.getForeignKey();
1568
+ const rpk = relatedPivotKey || relatedInstance.getForeignKey();
1569
+ const pk = parentKey || this.getKeyName();
1570
+ const rk = relatedKey || relatedInstance.getKeyName();
1571
+
1572
+ return new BelongsToMany<TRelated>(
1573
+ this,
1574
+ relatedInstance,
1575
+ pivotTable,
1576
+ fpk,
1577
+ rpk,
1578
+ pk,
1579
+ rk
1580
+ );
1581
+ }
1582
+
1583
+ /**
1584
+ * Define a has-one-through relationship.
1585
+ *
1586
+ * Retrieves a single related model through an intermediate model.
1587
+ *
1588
+ * Example: Country has one Profile through User
1589
+ *
1590
+ * Usage:
1591
+ * ```typescript
1592
+ * class Country extends Model {
1593
+ * userProfile() {
1594
+ * return this.hasOneThrough(
1595
+ * Profile, // Final model
1596
+ * User, // Intermediate model
1597
+ * 'country_id', // Foreign key on users (links to countries)
1598
+ * 'user_id', // Foreign key on profiles (links to users)
1599
+ * 'id', // Local key on countries
1600
+ * 'id' // Local key on users
1601
+ * );
1602
+ * }
1603
+ * }
1604
+ *
1605
+ * const country = await Country.find(1);
1606
+ * const profile = await country.userProfile().get();
1607
+ * ```
1608
+ *
1609
+ * @param RelatedClass - The final related model class constructor
1610
+ * @param ThroughClass - The intermediate model class constructor
1611
+ * @param firstKey - The foreign key on the intermediate table referencing this model
1612
+ * @param secondKey - The foreign key on the final table referencing the intermediate model
1613
+ * @param localKey - The primary key on this model (defaults to 'id')
1614
+ * @param secondLocalKey - The primary key on the intermediate model (defaults to 'id')
1615
+ * @returns A HasOneThrough relationship instance
1616
+ */
1617
+ protected hasOneThrough<TRelated extends Model, TThrough extends Model>(
1618
+ RelatedClass: new (attrs?: Record<string, unknown>) => TRelated,
1619
+ ThroughClass: new (attrs?: Record<string, unknown>) => TThrough,
1620
+ firstKey?: string,
1621
+ secondKey?: string,
1622
+ localKey?: string,
1623
+ secondLocalKey?: string
1624
+ ): HasOneThrough<TRelated> {
1625
+ const relatedInstance = new RelatedClass();
1626
+ const throughInstance = new ThroughClass();
1627
+
1628
+ // Default keys
1629
+ const fk = firstKey || this.getForeignKey();
1630
+ const sk = secondKey || throughInstance.getForeignKey();
1631
+ const lk = localKey || this.getKeyName();
1632
+ const slk = secondLocalKey || throughInstance.getKeyName();
1633
+
1634
+ return new HasOneThrough<TRelated>(
1635
+ this,
1636
+ relatedInstance,
1637
+ throughInstance,
1638
+ fk,
1639
+ sk,
1640
+ lk,
1641
+ slk
1642
+ );
1643
+ }
1644
+
1645
+ /**
1646
+ * Define a has-many-through relationship.
1647
+ *
1648
+ * Retrieves multiple related models through an intermediate model.
1649
+ *
1650
+ * Example: Country has many Posts through Users
1651
+ *
1652
+ * Usage:
1653
+ * ```typescript
1654
+ * class Country extends Model {
1655
+ * posts() {
1656
+ * return this.hasManyThrough(
1657
+ * Post, // Final model
1658
+ * User, // Intermediate model
1659
+ * 'country_id', // Foreign key on users (links to countries)
1660
+ * 'user_id', // Foreign key on posts (links to users)
1661
+ * 'id', // Local key on countries
1662
+ * 'id' // Local key on users
1663
+ * );
1664
+ * }
1665
+ * }
1666
+ *
1667
+ * const country = await Country.find(1);
1668
+ * const posts = await country.posts().get(); // All posts from users in this country
1669
+ * ```
1670
+ *
1671
+ * @param RelatedClass - The final related model class constructor
1672
+ * @param ThroughClass - The intermediate model class constructor
1673
+ * @param firstKey - The foreign key on the intermediate table referencing this model
1674
+ * @param secondKey - The foreign key on the final table referencing the intermediate model
1675
+ * @param localKey - The primary key on this model (defaults to 'id')
1676
+ * @param secondLocalKey - The primary key on the intermediate model (defaults to 'id')
1677
+ * @returns A HasManyThrough relationship instance
1678
+ */
1679
+ protected hasManyThrough<TRelated extends Model, TThrough extends Model>(
1680
+ RelatedClass: new (attrs?: Record<string, unknown>) => TRelated,
1681
+ ThroughClass: new (attrs?: Record<string, unknown>) => TThrough,
1682
+ firstKey?: string,
1683
+ secondKey?: string,
1684
+ localKey?: string,
1685
+ secondLocalKey?: string
1686
+ ): HasManyThrough<TRelated> {
1687
+ const relatedInstance = new RelatedClass();
1688
+ const throughInstance = new ThroughClass();
1689
+
1690
+ // Default keys
1691
+ const fk = firstKey || this.getForeignKey();
1692
+ const sk = secondKey || throughInstance.getForeignKey();
1693
+ const lk = localKey || this.getKeyName();
1694
+ const slk = secondLocalKey || throughInstance.getKeyName();
1695
+
1696
+ return new HasManyThrough<TRelated>(
1697
+ this,
1698
+ relatedInstance,
1699
+ throughInstance,
1700
+ fk,
1701
+ sk,
1702
+ lk,
1703
+ slk
1704
+ );
1705
+ }
1706
+
1707
+ /**
1708
+ * Paginate the model query.
1709
+ */
1710
+ static async paginate<T extends Model>(
1711
+ this: new (attrs?: Record<string, unknown>) => T,
1712
+ perPage: number = 15,
1713
+ columns: string[] = ['*'],
1714
+ pageName: string = 'page',
1715
+ page?: number
1716
+ ): Promise<LengthAwarePaginator<T>> {
1717
+ const instance = new this();
1718
+ const paginator = await instance.newQuery().paginate<Record<string, unknown>>(
1719
+ perPage,
1720
+ columns,
1721
+ pageName,
1722
+ page
1723
+ );
1724
+
1725
+ return new LengthAwarePaginator<T>(
1726
+ paginator.items as unknown as T[],
1727
+ paginator.total,
1728
+ paginator.perPage,
1729
+ paginator.currentPage,
1730
+ {
1731
+ path: paginator.path,
1732
+ query: paginator.query,
1733
+ }
1734
+ );
1735
+ }
1736
+
1737
+ /**
1738
+ * Simple pagination for the model query.
1739
+ */
1740
+ static async simplePaginate<T extends Model>(
1741
+ this: new (attrs?: Record<string, unknown>) => T,
1742
+ perPage: number = 15,
1743
+ columns: string[] = ['*'],
1744
+ pageName: string = 'page',
1745
+ page?: number
1746
+ ): Promise<Paginator<T>> {
1747
+ const instance = new this();
1748
+ const paginator = await instance.newQuery().simplePaginate<Record<string, unknown>>(
1749
+ perPage,
1750
+ columns,
1751
+ pageName,
1752
+ page
1753
+ );
1754
+
1755
+ return new Paginator<T>(
1756
+ paginator.items as unknown as T[],
1757
+ paginator.perPage,
1758
+ paginator.currentPage,
1759
+ {
1760
+ path: paginator.path,
1761
+ query: paginator.query,
1762
+ hasMore: paginator.hasMore,
1763
+ }
1764
+ );
1765
+ }
1766
+ }