ilana-orm 1.0.15 → 1.0.16

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/orm/Factory.js CHANGED
@@ -7,14 +7,14 @@ class Factory {
7
7
  this.definition = definition;
8
8
  this.faker = faker;
9
9
  this.states = new Map();
10
- this.afterCreating = [];
11
- this.afterMaking = [];
12
- this.beforeCreating = [];
13
- this.beforeMaking = [];
10
+ this._afterCreatingCallbacks = [];
11
+ this._afterMakingCallbacks = [];
12
+ this._beforeCreatingCallbacks = [];
13
+ this._beforeMakingCallbacks = [];
14
14
  this.count = 1;
15
15
  this.currentStates = [];
16
16
  this.relationships = new Map();
17
- this.sequence = 0;
17
+ this._sequenceCount = 0;
18
18
  this.sequences = new Map();
19
19
  }
20
20
 
@@ -28,22 +28,22 @@ class Factory {
28
28
  }
29
29
 
30
30
  afterCreating(callback) {
31
- this.afterCreating.push(callback);
31
+ this._afterCreatingCallbacks.push(callback);
32
32
  return this;
33
33
  }
34
34
 
35
35
  afterMaking(callback) {
36
- this.afterMaking.push(callback);
36
+ this._afterMakingCallbacks.push(callback);
37
37
  return this;
38
38
  }
39
39
 
40
40
  beforeCreating(callback) {
41
- this.beforeCreating.push(callback);
41
+ this._beforeCreatingCallbacks.push(callback);
42
42
  return this;
43
43
  }
44
44
 
45
45
  beforeMaking(callback) {
46
- this.beforeMaking.push(callback);
46
+ this._beforeMakingCallbacks.push(callback);
47
47
  return this;
48
48
  }
49
49
 
@@ -75,7 +75,7 @@ class Factory {
75
75
  }
76
76
 
77
77
  sequence() {
78
- return ++this.sequence;
78
+ return ++this._sequenceCount;
79
79
  }
80
80
 
81
81
  // Enhanced sequence methods
@@ -92,7 +92,7 @@ class Factory {
92
92
  this.sequences?.set(key, 0);
93
93
  } else {
94
94
  this.sequences?.clear();
95
- this.sequence = 0;
95
+ this._sequenceCount = 0;
96
96
  }
97
97
  return this;
98
98
  }
@@ -138,8 +138,6 @@ class Factory {
138
138
 
139
139
  if (typeof this.definition === 'function') {
140
140
  modelAttributes = this.definition(faker);
141
- } else if (this.definition === undefined && typeof this.definition === 'function') {
142
- modelAttributes = this.definition();
143
141
  } else {
144
142
  throw new Error('Factory must have a definition function');
145
143
  }
@@ -153,7 +151,7 @@ class Factory {
153
151
 
154
152
  modelAttributes = { ...modelAttributes, ...attributes };
155
153
 
156
- for (const callback of this.beforeMaking) {
154
+ for (const callback of this._beforeMakingCallbacks) {
157
155
  modelAttributes = callback(modelAttributes) || modelAttributes;
158
156
  }
159
157
 
@@ -167,7 +165,7 @@ class Factory {
167
165
  model.fill(modelAttributes);
168
166
 
169
167
  // Run afterMaking callbacks
170
- for (const callback of this.afterMaking) {
168
+ for (const callback of this._afterMakingCallbacks) {
171
169
  await callback(model);
172
170
  }
173
171
 
@@ -178,14 +176,14 @@ class Factory {
178
176
  const model = await this.makeOne(attributes);
179
177
 
180
178
  // Run beforeCreating callbacks
181
- for (const callback of this.beforeCreating) {
179
+ for (const callback of this._beforeCreatingCallbacks) {
182
180
  await callback(model);
183
181
  }
184
-
182
+
185
183
  await model.save();
186
184
 
187
185
  // Run afterCreating callbacks
188
- for (const callback of this.afterCreating) {
186
+ for (const callback of this._afterCreatingCallbacks) {
189
187
  await callback(model);
190
188
  }
191
189
 
@@ -243,6 +241,8 @@ class Factory {
243
241
  const batchFactory = new Factory(this.model, this.definition);
244
242
  batchFactory.count = currentBatchSize;
245
243
  batchFactory.currentStates = [...this.currentStates];
244
+ batchFactory._afterCreatingCallbacks = [...this._afterCreatingCallbacks];
245
+ batchFactory._beforeCreatingCallbacks = [...this._beforeCreatingCallbacks];
246
246
 
247
247
  const batch = await batchFactory.create(attributes);
248
248
  results.push(...(Array.isArray(batch) ? batch : [batch]));
@@ -352,6 +352,8 @@ if (typeof Model !== 'undefined') {
352
352
  // Return a new instance to avoid state pollution
353
353
  const newFactory = new Factory(this, existingFactory.definition);
354
354
  newFactory.states = new Map(existingFactory.states);
355
+ newFactory._afterCreatingCallbacks = [...existingFactory._afterCreatingCallbacks];
356
+ newFactory._beforeCreatingCallbacks = [...existingFactory._beforeCreatingCallbacks];
355
357
  return newFactory;
356
358
  }
357
359
  throw new Error(`No factory defined for model: ${this.name}`);
package/orm/Model.d.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import QueryBuilder from './QueryBuilder';
2
- import { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphMany } from './Relation';
2
+ import { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphOne, MorphMany } from './Relation';
3
3
 
4
4
  export interface ModelAttributes {
5
5
  [key: string]: any;
6
6
  }
7
7
 
8
+ export interface CastInstance {
9
+ get(value: any): any;
10
+ set(value: any): any;
11
+ }
12
+
8
13
  export interface ModelCasts {
9
- [key: string]: 'string' | 'number' | 'boolean' | 'date' | 'json' | 'array' | 'object' | 'float';
14
+ [key: string]: 'string' | 'number' | 'boolean' | 'date' | 'json' | 'array' | 'object' | 'float' | CastInstance;
10
15
  }
11
16
 
12
17
  export interface ModelEvents {
@@ -26,7 +31,7 @@ export interface Observer {
26
31
  restored?(model: any): Promise<void> | void;
27
32
  }
28
33
 
29
- export default class Model {
34
+ export default class Model<TAttributes extends ModelAttributes = ModelAttributes> {
30
35
  // Static properties
31
36
  protected static table: string;
32
37
  protected static connection?: string;
@@ -34,7 +39,10 @@ export default class Model {
34
39
  protected static keyType: 'number' | 'string';
35
40
  protected static incrementing: boolean;
36
41
  protected static timestamps: boolean;
42
+ protected static createdAt: string;
43
+ protected static updatedAt: string;
37
44
  protected static softDeletes: boolean;
45
+ protected static deletedAt: string;
38
46
  protected static fillable: string[];
39
47
  protected static guarded: string[];
40
48
  protected static casts: ModelCasts;
@@ -64,6 +72,7 @@ export default class Model {
64
72
  static resolveRelatedModel(related: string | typeof Model): typeof Model;
65
73
  static query(): QueryBuilder;
66
74
  static with(...relations: string[]): QueryBuilder;
75
+ static withCount(...relations: string[]): QueryBuilder;
67
76
  static on(connectionOrTrx: string | any): QueryBuilder;
68
77
  static all(): Promise<Model[]>;
69
78
  static find(id: any): Promise<Model | null>;
@@ -72,6 +81,10 @@ export default class Model {
72
81
  static firstOrFail(): Promise<Model>;
73
82
  static latest(column?: string): QueryBuilder;
74
83
  static oldest(column?: string): QueryBuilder;
84
+ static withTrashed(): QueryBuilder;
85
+ static onlyTrashed(): QueryBuilder;
86
+ static upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
87
+ static withoutGlobalScopes(): QueryBuilder;
75
88
  static make(attributes?: ModelAttributes): Model;
76
89
  static create(attributes?: ModelAttributes): Promise<Model>;
77
90
  static generateUuid(): string;
@@ -111,10 +124,19 @@ export default class Model {
111
124
  // Instance methods
112
125
  getKey(): any;
113
126
  fill(attributes: ModelAttributes): this;
127
+ load(...relations: string[]): Promise<this>;
128
+ loadMissing(...relations: string[]): Promise<this>;
129
+ getRelation(key: string): any;
130
+ relationLoaded(key: string): boolean;
131
+ makeHidden(keys: string | string[]): this;
132
+ makeVisible(keys: string | string[]): this;
133
+ append(keys: string | string[]): this;
114
134
  isFillable(key: string): boolean;
115
135
  getAttribute(key: string): any;
116
136
  setAttribute(key: string, value: any): this;
117
137
  syncOriginal(): void;
138
+ getOriginal(key: string): any;
139
+ getOriginal(): ModelAttributes;
118
140
  save(): Promise<boolean>;
119
141
  update(attributes?: ModelAttributes): Promise<boolean>;
120
142
  isDirty(key?: string): boolean;
@@ -148,6 +170,7 @@ export default class Model {
148
170
  secondLocalKey?: string
149
171
  ): HasManyThrough;
150
172
  morphTo(typeColumn?: string, idColumn?: string): MorphTo;
173
+ morphOne(related: string | typeof Model, typeColumn?: string, idColumn?: string): MorphOne;
151
174
  morphMany(related: string | typeof Model, typeColumn?: string, idColumn?: string): MorphMany;
152
175
 
153
176
  // Protected methods
package/orm/Model.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Model.js
2
2
  const QueryBuilder = require('./QueryBuilder');
3
- const { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphMany } = require('./Relation');
3
+ const { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphMany, MorphOne } = require('./Relation');
4
4
  const ModelRegistry = require('./ModelRegistry');
5
5
  const Database = require('../database/connection');
6
6
 
@@ -86,7 +86,8 @@ class Model {
86
86
  const allKeys = new Set([
87
87
  ...Object.keys(this.attributes || {}),
88
88
  ...this.fillable,
89
- ...(this._deferred ? Object.keys(this._deferred) : [])
89
+ ...(this._deferred ? Object.keys(this._deferred) : []),
90
+ ...(this.appends || [])
90
91
  ]);
91
92
 
92
93
  for (const key of allKeys) {
@@ -105,6 +106,16 @@ class Model {
105
106
  }
106
107
  }
107
108
 
109
+ _toPascalCase(key) {
110
+ return key.replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase());
111
+ }
112
+
113
+ _selfFk() {
114
+ return this.constructor.name
115
+ .replace(/([A-Z])/g, (m, l, i) => i === 0 ? l.toLowerCase() : '_' + l.toLowerCase())
116
+ + '_id';
117
+ }
118
+
108
119
  // --- Static registry & resolving ---
109
120
  static register() {
110
121
  ModelRegistry.register(this.name, this);
@@ -150,6 +161,7 @@ class Model {
150
161
  }
151
162
 
152
163
  static with(...rels) { return this.query().with(...rels); }
164
+ static withCount(...rels) { return this.query().withCount(...rels); }
153
165
  static on(connectionOrTrx) {
154
166
  if (connectionOrTrx && typeof connectionOrTrx.raw === 'function') {
155
167
  // It's a transaction object
@@ -166,8 +178,14 @@ class Model {
166
178
  static async findBy(column, value) { return this.query().where(column, value).first(); }
167
179
  static async first() { return this.query().first(); }
168
180
  static async firstOrFail() { return this.query().firstOrFail(); }
169
- static latest(col) { return this.query().latest(col || 'created_at'); }
170
- static oldest(col) { return this.query().oldest(col || 'created_at'); }
181
+ static latest(col) { return this.query().latest(col || this.createdAt || 'created_at'); }
182
+ static oldest(col) { return this.query().oldest(col || this.createdAt || 'created_at'); }
183
+ static withTrashed() { return this.query().withTrashed(); }
184
+ static onlyTrashed() { return this.query().onlyTrashed(); }
185
+ static async upsert(data, uniqueBy, update) { return this.query().upsert(data, uniqueBy, update); }
186
+ static withoutGlobalScopes() {
187
+ return new QueryBuilder(this.getTableName(), this, this.getConnectionName());
188
+ }
171
189
 
172
190
  static make(attrs = {}) {
173
191
  const inst = new this(attrs);
@@ -192,7 +210,16 @@ class Model {
192
210
  }
193
211
 
194
212
  static async insert(data) { return this.query().insert(data); }
195
- static async destroy(ids) { return this.query().whereIn(this.primaryKey, Array.isArray(ids) ? ids : [ids]).delete(); }
213
+ static async destroy(ids) {
214
+ const idList = Array.isArray(ids) ? ids : [ids];
215
+ if (this.softDeletes) {
216
+ const models = await this.query().whereIn(this.primaryKey, idList).get();
217
+ let count = 0;
218
+ for (const model of models) { await model.delete(); count++; }
219
+ return count;
220
+ }
221
+ return this.query().whereIn(this.primaryKey, idList).delete();
222
+ }
196
223
  static async firstOrCreate(a, v) { return (await this.query().where(a).first()) || this.create({ ...a, ...v }); }
197
224
  static async firstOrNew(a, v) {
198
225
  const existing = await this.query().where(a).first();
@@ -211,8 +238,13 @@ class Model {
211
238
  }
212
239
 
213
240
  // scopes
214
- static addGlobalScope(n, s) { this.globalScopes.set(n, s); }
215
- static removeGlobalScope(n) { this.globalScopes.delete(n); }
241
+ static addGlobalScope(n, s) {
242
+ if (!Object.hasOwn(this, 'globalScopes')) this.globalScopes = new Map();
243
+ this.globalScopes.set(n, s);
244
+ }
245
+ static removeGlobalScope(n) {
246
+ if (Object.hasOwn(this, 'globalScopes')) this.globalScopes.delete(n);
247
+ }
216
248
  static withoutGlobalScope(n) {
217
249
  const qb = new QueryBuilder(this.getTableName(), this, this.getConnectionName());
218
250
  const scopes = new Map(this.globalScopes);
@@ -220,10 +252,17 @@ class Model {
220
252
  scopes.forEach(s => s(qb));
221
253
  return qb;
222
254
  }
223
- static applyGlobalScopes(qb) { this.globalScopes.forEach(s => s(qb)); }
255
+ static applyGlobalScopes(qb) {
256
+ const scopes = Object.hasOwn(this, 'globalScopes') ? this.globalScopes : new Map();
257
+ scopes.forEach(s => s(qb));
258
+ }
224
259
 
225
260
  // events
226
- static _addEventHandler(evt, fn) { this.events[evt] = this.events[evt] || []; this.events[evt].push(fn); }
261
+ static _addEventHandler(evt, fn) {
262
+ if (!Object.hasOwn(this, 'events')) this.events = {};
263
+ this.events[evt] = this.events[evt] || [];
264
+ this.events[evt].push(fn);
265
+ }
227
266
  static creating(fn) { this._addEventHandler('creating', fn); }
228
267
  static created(fn) { this._addEventHandler('created', fn); }
229
268
  static updating(fn) { this._addEventHandler('updating', fn); }
@@ -254,7 +293,8 @@ class Model {
254
293
  }
255
294
  // static async fireEvent(evt, mdl) { for (const h of this.events[evt] || []) if (await h(mdl) === false) return false; }
256
295
  static async fireEvent(evt, mdl) {
257
- const handlers = this.events[evt] || [];
296
+ const ownEvents = Object.hasOwn(this, 'events') ? this.events : {};
297
+ const handlers = ownEvents[evt] || [];
258
298
  for (const handler of handlers) {
259
299
  if (await handler(mdl) === false) return false;
260
300
  }
@@ -278,6 +318,46 @@ class Model {
278
318
  return this;
279
319
  }
280
320
 
321
+ async load(...relations) {
322
+ const qb = new QueryBuilder(this.constructor.getTableName(), this.constructor, this.constructor.getConnectionName());
323
+ qb.eagerLoad = relations.flat();
324
+ await qb.loadRelations([this]);
325
+ return this;
326
+ }
327
+
328
+ async loadMissing(...relations) {
329
+ const toLoad = relations.flat().filter(r => !(r.split('.')[0] in this.relations));
330
+ if (toLoad.length) await this.load(...toLoad);
331
+ return this;
332
+ }
333
+
334
+ getRelation(key) {
335
+ return this.relations[key];
336
+ }
337
+
338
+ relationLoaded(key) {
339
+ return Object.prototype.hasOwnProperty.call(this.relations, key);
340
+ }
341
+
342
+ makeHidden(keys) {
343
+ const list = Array.isArray(keys) ? keys : [keys];
344
+ this.hidden = [...(this.hidden || []), ...list];
345
+ return this;
346
+ }
347
+
348
+ makeVisible(keys) {
349
+ const list = Array.isArray(keys) ? keys : [keys];
350
+ this.hidden = (this.hidden || []).filter(k => !list.includes(k));
351
+ return this;
352
+ }
353
+
354
+ append(keys) {
355
+ const list = Array.isArray(keys) ? keys : [keys];
356
+ this.appends = [...(this.appends || []), ...list];
357
+ this._createAttributeGetters();
358
+ return this;
359
+ }
360
+
281
361
  isFillable(k) {
282
362
  if (Array.isArray(this.fillable) && this.fillable.length) return this.fillable.includes(k);
283
363
  if (this.guarded.includes('*')) return false;
@@ -285,23 +365,36 @@ class Model {
285
365
  }
286
366
 
287
367
  getAttribute(k) {
368
+ const accessor = `get${this._toPascalCase(k)}Attribute`;
369
+ if (typeof this[accessor] === 'function') {
370
+ return this[accessor]();
371
+ }
288
372
  const val = this.attributes[k];
289
373
  const cast = this.casts[k];
374
+ if (cast && typeof cast === 'object' && typeof cast.get === 'function') {
375
+ return cast.get(val);
376
+ }
290
377
  if (cast === 'json' || cast === 'array') {
291
378
  try { return JSON.parse(val); } catch { return val; }
292
379
  }
293
380
  if (cast === 'date' && val != null) return val;
381
+ if (cast === 'boolean') return val == null ? val : Boolean(val);
382
+ if (cast === 'number' || cast === 'float') return val == null ? val : Number(val);
294
383
  return val;
295
384
  }
296
385
 
297
386
  setAttribute(k, v) {
387
+ const mutator = `set${this._toPascalCase(k)}Attribute`;
388
+ if (typeof this[mutator] === 'function') {
389
+ v = this[mutator](v);
390
+ }
298
391
  const cast = this.casts[k];
299
392
  let val = v;
300
- if (cast === 'json' || cast === 'array') {
393
+ if (cast && typeof cast === 'object' && typeof cast.set === 'function') {
394
+ val = cast.set(v);
395
+ } else if (cast === 'json' || cast === 'array') {
301
396
  val = typeof v === 'string' ? v : JSON.stringify(v);
302
- }
303
- if (cast === 'date' && v instanceof Date) {
304
- // Format date for database storage (YYYY-MM-DD HH:mm:ss)
397
+ } else if (cast === 'date' && v instanceof Date) {
305
398
  const year = v.getFullYear();
306
399
  const month = String(v.getMonth() + 1).padStart(2, '0');
307
400
  const day = String(v.getDate()).padStart(2, '0');
@@ -311,7 +404,6 @@ class Model {
311
404
  val = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
312
405
  }
313
406
  this.attributes[k] = val;
314
- // Only mark as dirty if not during initialization and model exists
315
407
  if (!this._deferred && this.exists) {
316
408
  this._dirty.add(k);
317
409
  }
@@ -323,6 +415,10 @@ class Model {
323
415
  this._dirty.clear();
324
416
  }
325
417
 
418
+ getOriginal(key) {
419
+ return key !== undefined ? this.original[key] : { ...this.original };
420
+ }
421
+
326
422
  _getCurrentTimestamp() {
327
423
  const now = new Date();
328
424
  const config = this._getConfig();
@@ -379,9 +475,12 @@ class Model {
379
475
  if (await this.constructor.fireEvent('creating', this) === false) return false;
380
476
  if (await this.constructor.fireEvent('saving', this) === false) return false;
381
477
 
478
+ const createdAtCol = this.constructor.createdAt || 'created_at';
479
+ const updatedAtCol = this.constructor.updatedAt || 'updated_at';
480
+
382
481
  if (this.constructor.timestamps) {
383
482
  const now = this._getCurrentTimestamp();
384
- this.setAttribute('created_at', now).setAttribute('updated_at', now);
483
+ this.setAttribute(createdAtCol, now).setAttribute(updatedAtCol, now);
385
484
  }
386
485
 
387
486
  // Generate UUID if needed
@@ -403,16 +502,19 @@ class Model {
403
502
  await this.constructor.fireEvent('saved', this);
404
503
  this.syncOriginal();
405
504
  } else if (this.isDirty()) {
505
+ const createdAtCol = this.constructor.createdAt || 'created_at';
506
+ const updatedAtCol = this.constructor.updatedAt || 'updated_at';
507
+
406
508
  // Updating existing record
407
509
  if (await this.constructor.fireEvent('updating', this) === false) return false;
408
510
  if (await this.constructor.fireEvent('saving', this) === false) return false;
409
511
 
410
512
  if (this.constructor.timestamps) {
411
- this.setAttribute('updated_at', this._getCurrentTimestamp());
513
+ this.setAttribute(updatedAtCol, this._getCurrentTimestamp());
412
514
  }
413
515
 
414
516
  const updateData = this.getDirty();
415
- delete updateData.created_at; // Never update created_at
517
+ delete updateData[createdAtCol]; // Never update created_at
416
518
 
417
519
  if (Object.keys(updateData).length > 0) {
418
520
  await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).update(updateData);
@@ -448,8 +550,10 @@ class Model {
448
550
 
449
551
  await this.constructor.fireEvent('deleting', this);
450
552
 
553
+ const deletedAtCol = this.constructor.deletedAt || 'deleted_at';
554
+
451
555
  if (this.constructor.softDeletes) {
452
- this.setAttribute('deleted_at', new Date());
556
+ this.setAttribute(deletedAtCol, new Date());
453
557
  await this.save();
454
558
  } else {
455
559
  await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).delete();
@@ -461,13 +565,14 @@ class Model {
461
565
  }
462
566
 
463
567
  async restore() {
464
- if (!this.constructor.softDeletes || !this.getAttribute('deleted_at')) {
568
+ const deletedAtCol = this.constructor.deletedAt || 'deleted_at';
569
+ if (!this.constructor.softDeletes || !this.getAttribute(deletedAtCol)) {
465
570
  return false;
466
571
  }
467
572
 
468
573
  await this.constructor.fireEvent('restoring', this);
469
574
 
470
- this.setAttribute('deleted_at', null);
575
+ this.setAttribute(deletedAtCol, null);
471
576
  await this.save();
472
577
 
473
578
  await this.constructor.fireEvent('restored', this);
@@ -475,7 +580,8 @@ class Model {
475
580
  }
476
581
 
477
582
  trashed() {
478
- return this.constructor.softDeletes && this.getAttribute('deleted_at') !== null;
583
+ const deletedAtCol = this.constructor.deletedAt || 'deleted_at';
584
+ return this.constructor.softDeletes && this.getAttribute(deletedAtCol) !== null;
479
585
  }
480
586
 
481
587
  only(keys) {
@@ -556,15 +662,18 @@ class Model {
556
662
  // relations - convert classes to strings to avoid circular dependencies
557
663
  hasOne(related, fk, lk) {
558
664
  const relatedName = this._resolveRelatedName(related);
559
- return new HasOne(this, relatedName, fk || `${this.constructor.table}_id`, lk || this.constructor.primaryKey);
665
+ return new HasOne(this, relatedName, fk || this._selfFk(), lk || this.constructor.primaryKey);
560
666
  }
561
667
  hasMany(related, fk, lk) {
562
668
  const relatedName = this._resolveRelatedName(related);
563
- return new HasMany(this, relatedName, fk || `${this.constructor.table}_id`, lk || this.constructor.primaryKey);
669
+ return new HasMany(this, relatedName, fk || this._selfFk(), lk || this.constructor.primaryKey);
564
670
  }
565
671
  belongsTo(related, fk, ok) {
566
672
  const relatedName = this._resolveRelatedName(related);
567
- return new BelongsTo(this, relatedName, fk, ok || this.constructor.primaryKey);
673
+ const defaultFk = typeof relatedName === 'string'
674
+ ? relatedName.replace(/([A-Z])/g, (m, l, i) => i === 0 ? l.toLowerCase() : '_' + l.toLowerCase()) + '_id'
675
+ : undefined;
676
+ return new BelongsTo(this, relatedName, fk || defaultFk, ok || this.constructor.primaryKey);
568
677
  }
569
678
  belongsToMany(related, pivot, fp, rp, pk, rk) {
570
679
  const relatedName = this._resolveRelatedName(related);
@@ -590,6 +699,10 @@ class Model {
590
699
  return new HasManyThrough(this, relatedName, throughName, fk, sk, lk, slk);
591
700
  }
592
701
  morphTo(type, id) { return new MorphTo(this, type, id); }
702
+ morphOne(related, type, id) {
703
+ const relatedName = typeof related === 'function' && related.name ? related.name : related;
704
+ return new MorphOne(this, relatedName, type, id, this.constructor.name);
705
+ }
593
706
  morphMany(related, type, id) {
594
707
  const relatedName = typeof related === 'function' && related.name ? related.name : related;
595
708
  return new MorphMany(this, relatedName, type, id, this.constructor.name);
@@ -56,27 +56,44 @@ export default class QueryBuilder {
56
56
  whereDate(column: string, operator: string, value: string): this;
57
57
  whereMonth(column: string, month: number): this;
58
58
  whereYear(column: string, year: number): this;
59
- whereExists(callback: (query: QueryBuilder) => void): this;
59
+ whereNotBetween(column: string, range: [any, any]): this;
60
+ orWhereNull(column: string): this;
61
+ orWhereNotNull(column: string): this;
62
+ orWhereIn(column: string, values: any[]): this;
63
+ orWhereNotIn(column: string, values: any[]): this;
64
+ orWhereRaw(sql: string, bindings?: any[]): this;
65
+ whereDay(column: string, operatorOrValue: any, value?: any): this;
66
+ whereTime(column: string, operatorOrValue: any, value?: any): this;
60
67
  when<T>(condition: T, callback: (query: this, condition: T) => void, otherwise?: (query: this) => void): this;
68
+ unless<T>(condition: T, callback: (query: this) => void, otherwise?: (query: this, condition: T) => void): this;
61
69
 
62
70
  // Joins
63
71
  join(table: string, first: string, operator: string, second: string): this;
64
72
  leftJoin(table: string, first: string, operator: string, second: string): this;
65
73
  rightJoin(table: string, first: string, operator: string, second: string): this;
74
+ innerJoin(table: string, first: string, operator: string, second: string): this;
75
+ crossJoin(table: string): this;
66
76
 
67
77
  // Ordering and limits
68
78
  orderBy(column: string, direction?: 'asc' | 'desc'): this;
79
+ orderByRaw(sql: string): this;
69
80
  latest(column?: string): this;
70
81
  oldest(column?: string): this;
82
+ inRandomOrder(): this;
71
83
  limit(count: number): this;
72
84
  offset(count: number): this;
73
85
  take(count: number): this;
74
86
  skip(count: number): this;
87
+ from(table: string): this;
88
+ forPage(page: number, perPage?: number): this;
75
89
 
76
90
  // Grouping
77
91
  groupBy(...columns: string[]): this;
78
92
  having(column: string, operator: string, value: any): this;
79
93
  having(rawSql: string): this;
94
+ havingRaw(sql: string, bindings?: any[]): this;
95
+ whereExists(callback: (query: QueryBuilder) => void): this;
96
+ whereNotExists(callback: (query: QueryBuilder) => void): this;
80
97
 
81
98
  // Locking
82
99
  lockForUpdate(): this;
@@ -85,7 +102,8 @@ export default class QueryBuilder {
85
102
  noWait(): this;
86
103
 
87
104
  // Selection
88
- select(...columns: string[]): this;
105
+ select(...columns: any[]): this;
106
+ addSelect(...columns: any[]): this;
89
107
  distinct(): this;
90
108
 
91
109
  // Raw queries
@@ -105,14 +123,18 @@ export default class QueryBuilder {
105
123
  withConstraints(relations: { [key: string]: (query: QueryBuilder) => void }): this;
106
124
  withCount(...relations: string[]): this;
107
125
  whereHas(relation: string, callback?: (query: QueryBuilder) => void): this;
126
+ doesntHave(relation: string): this;
127
+ whereDoesntHave(relation: string, callback?: (query: QueryBuilder) => void): this;
108
128
 
109
129
  // Execution methods
110
130
  get(): Promise<Collection<Model>>;
111
131
  first(): Promise<Model | null>;
132
+ firstOrFail(): Promise<Model>;
112
133
  find(id: any): Promise<Model | null>;
113
134
  findOrFail(id: any): Promise<Model>;
114
135
  pluck(column: string): Promise<any[]>;
115
136
  exists(): Promise<boolean>;
137
+ doesntExist(): Promise<boolean>;
116
138
 
117
139
  // Pagination
118
140
  paginate(page?: number, perPage?: number): Promise<PaginationResult<Model>>;