ilana-orm 1.0.14 → 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,36 +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
- if (cast === 'date' && val != null) {
294
- const config = this._getConfig();
295
- const timezone = this.constructor.timezone || config?.timezone || 'UTC';
296
-
297
- try {
298
- // Try moment-timezone first
299
- const moment = require('moment-timezone');
300
- // Parse the stored value as if it's in the configured timezone
301
- return moment.tz(val, timezone).format('YYYY-MM-DD HH:mm:ss');
302
- } catch (e) {
303
- // Fallback: return the stored value as-is since it's already in the correct timezone
304
- return val;
305
- }
306
- }
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);
307
383
  return val;
308
384
  }
309
385
 
310
386
  setAttribute(k, v) {
387
+ const mutator = `set${this._toPascalCase(k)}Attribute`;
388
+ if (typeof this[mutator] === 'function') {
389
+ v = this[mutator](v);
390
+ }
311
391
  const cast = this.casts[k];
312
392
  let val = v;
313
- 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') {
314
396
  val = typeof v === 'string' ? v : JSON.stringify(v);
315
- }
316
- if (cast === 'date' && v instanceof Date) {
317
- // Format date for database storage (YYYY-MM-DD HH:mm:ss)
397
+ } else if (cast === 'date' && v instanceof Date) {
318
398
  const year = v.getFullYear();
319
399
  const month = String(v.getMonth() + 1).padStart(2, '0');
320
400
  const day = String(v.getDate()).padStart(2, '0');
@@ -324,7 +404,6 @@ class Model {
324
404
  val = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
325
405
  }
326
406
  this.attributes[k] = val;
327
- // Only mark as dirty if not during initialization and model exists
328
407
  if (!this._deferred && this.exists) {
329
408
  this._dirty.add(k);
330
409
  }
@@ -336,39 +415,34 @@ class Model {
336
415
  this._dirty.clear();
337
416
  }
338
417
 
418
+ getOriginal(key) {
419
+ return key !== undefined ? this.original[key] : { ...this.original };
420
+ }
421
+
339
422
  _getCurrentTimestamp() {
423
+ const now = new Date();
340
424
  const config = this._getConfig();
341
425
  const timezone = this.constructor.timezone || config?.timezone || 'UTC';
342
-
426
+
427
+ if (timezone === 'UTC') return now;
428
+
343
429
  try {
344
- // Try to use moment-timezone if available
345
- const moment = require('moment-timezone');
346
- return moment().tz(timezone).toDate();
347
- } catch (e) {
348
- try {
349
- // Try to use date-fns-tz if available
350
- const { zonedTimeToUtc } = require('date-fns-tz');
351
- return zonedTimeToUtc(new Date(), timezone);
352
- } catch (e2) {
353
- // Fallback to simple but accurate method
354
- const now = new Date();
355
- if (timezone === 'UTC') return now;
356
-
357
- // Use toLocaleString for accurate timezone conversion
358
- const utcTime = now.getTime() + (now.getTimezoneOffset() * 60000);
359
- const targetTime = new Date(utcTime + (this._getTimezoneOffset(timezone, now) * 60000));
360
- return targetTime;
361
- }
362
- }
363
- }
364
-
365
- _getTimezoneOffset(timezone, date) {
366
- try {
367
- const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
368
- const targetDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
369
- return (targetDate.getTime() - utcDate.getTime()) / 60000;
370
- } catch (e) {
371
- return 0; // Default to UTC
430
+ const formatter = new Intl.DateTimeFormat('en-US', {
431
+ timeZone: timezone,
432
+ year: 'numeric',
433
+ month: '2-digit',
434
+ day: '2-digit',
435
+ hour: '2-digit',
436
+ minute: '2-digit',
437
+ second: '2-digit',
438
+ hour12: false
439
+ });
440
+ const parts = Object.fromEntries(
441
+ formatter.formatToParts(now).map(p => [p.type, p.value])
442
+ );
443
+ return new Date(`${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}`);
444
+ } catch {
445
+ return now;
372
446
  }
373
447
  }
374
448
 
@@ -401,9 +475,12 @@ class Model {
401
475
  if (await this.constructor.fireEvent('creating', this) === false) return false;
402
476
  if (await this.constructor.fireEvent('saving', this) === false) return false;
403
477
 
478
+ const createdAtCol = this.constructor.createdAt || 'created_at';
479
+ const updatedAtCol = this.constructor.updatedAt || 'updated_at';
480
+
404
481
  if (this.constructor.timestamps) {
405
482
  const now = this._getCurrentTimestamp();
406
- this.setAttribute('created_at', now).setAttribute('updated_at', now);
483
+ this.setAttribute(createdAtCol, now).setAttribute(updatedAtCol, now);
407
484
  }
408
485
 
409
486
  // Generate UUID if needed
@@ -425,16 +502,19 @@ class Model {
425
502
  await this.constructor.fireEvent('saved', this);
426
503
  this.syncOriginal();
427
504
  } else if (this.isDirty()) {
505
+ const createdAtCol = this.constructor.createdAt || 'created_at';
506
+ const updatedAtCol = this.constructor.updatedAt || 'updated_at';
507
+
428
508
  // Updating existing record
429
509
  if (await this.constructor.fireEvent('updating', this) === false) return false;
430
510
  if (await this.constructor.fireEvent('saving', this) === false) return false;
431
511
 
432
512
  if (this.constructor.timestamps) {
433
- this.setAttribute('updated_at', this._getCurrentTimestamp());
513
+ this.setAttribute(updatedAtCol, this._getCurrentTimestamp());
434
514
  }
435
515
 
436
516
  const updateData = this.getDirty();
437
- delete updateData.created_at; // Never update created_at
517
+ delete updateData[createdAtCol]; // Never update created_at
438
518
 
439
519
  if (Object.keys(updateData).length > 0) {
440
520
  await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).update(updateData);
@@ -470,8 +550,10 @@ class Model {
470
550
 
471
551
  await this.constructor.fireEvent('deleting', this);
472
552
 
553
+ const deletedAtCol = this.constructor.deletedAt || 'deleted_at';
554
+
473
555
  if (this.constructor.softDeletes) {
474
- this.setAttribute('deleted_at', new Date());
556
+ this.setAttribute(deletedAtCol, new Date());
475
557
  await this.save();
476
558
  } else {
477
559
  await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).delete();
@@ -483,13 +565,14 @@ class Model {
483
565
  }
484
566
 
485
567
  async restore() {
486
- if (!this.constructor.softDeletes || !this.getAttribute('deleted_at')) {
568
+ const deletedAtCol = this.constructor.deletedAt || 'deleted_at';
569
+ if (!this.constructor.softDeletes || !this.getAttribute(deletedAtCol)) {
487
570
  return false;
488
571
  }
489
572
 
490
573
  await this.constructor.fireEvent('restoring', this);
491
574
 
492
- this.setAttribute('deleted_at', null);
575
+ this.setAttribute(deletedAtCol, null);
493
576
  await this.save();
494
577
 
495
578
  await this.constructor.fireEvent('restored', this);
@@ -497,7 +580,8 @@ class Model {
497
580
  }
498
581
 
499
582
  trashed() {
500
- 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;
501
585
  }
502
586
 
503
587
  only(keys) {
@@ -578,15 +662,18 @@ class Model {
578
662
  // relations - convert classes to strings to avoid circular dependencies
579
663
  hasOne(related, fk, lk) {
580
664
  const relatedName = this._resolveRelatedName(related);
581
- 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);
582
666
  }
583
667
  hasMany(related, fk, lk) {
584
668
  const relatedName = this._resolveRelatedName(related);
585
- 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);
586
670
  }
587
671
  belongsTo(related, fk, ok) {
588
672
  const relatedName = this._resolveRelatedName(related);
589
- 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);
590
677
  }
591
678
  belongsToMany(related, pivot, fp, rp, pk, rk) {
592
679
  const relatedName = this._resolveRelatedName(related);
@@ -612,6 +699,10 @@ class Model {
612
699
  return new HasManyThrough(this, relatedName, throughName, fk, sk, lk, slk);
613
700
  }
614
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
+ }
615
706
  morphMany(related, type, id) {
616
707
  const relatedName = typeof related === 'function' && related.name ? related.name : related;
617
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>>;