ilana-orm 1.0.17 → 1.0.19

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
@@ -14,6 +14,9 @@ class Factory {
14
14
  this.count = 1;
15
15
  this.currentStates = [];
16
16
  this.relationships = new Map();
17
+ this._has = new Map();
18
+ this._for = new Map();
19
+ this._hasAttached = new Map();
17
20
  this._sequenceCount = 0;
18
21
  this.sequences = new Map();
19
22
  }
@@ -57,23 +60,30 @@ class Factory {
57
60
  return this;
58
61
  }
59
62
 
60
- for(relation, factory) {
61
- this.relationships.set(relation, factory);
63
+ for(relation, relFactory) {
64
+ this._for.set(relation, relFactory);
62
65
  return this;
63
66
  }
64
67
 
65
- has(factory, relation) {
66
- if (relation) {
67
- this.relationships.set(relation, factory);
68
- }
68
+ has(relFactory, relation) {
69
+ this._has.set(relation, relFactory);
69
70
  return this;
70
71
  }
71
72
 
72
- hasAttached(factory, relation) {
73
- this.relationships.set(relation, factory);
73
+ hasAttached(relFactory, relation) {
74
+ this._hasAttached.set(relation, relFactory);
74
75
  return this;
75
76
  }
76
77
 
78
+ _getRelation(relationName) {
79
+ try {
80
+ const dummy = new this.model({});
81
+ return typeof dummy[relationName] === 'function' ? dummy[relationName]() : null;
82
+ } catch (_) {
83
+ return null;
84
+ }
85
+ }
86
+
77
87
  sequence() {
78
88
  return ++this._sequenceCount;
79
89
  }
@@ -173,16 +183,40 @@ class Factory {
173
183
  }
174
184
 
175
185
  async createOne(attributes = {}) {
176
- const model = await this.makeOne(attributes);
177
-
178
- // Run beforeCreating callbacks
186
+ // Handle 'for' (belongsTo) — create parent first, inject FK into this model
187
+ const parentAttrs = {};
188
+ for (const [relationName, relFactory] of this._for) {
189
+ const parent = await relFactory.createOne({});
190
+ const rel = this._getRelation(relationName);
191
+ if (rel && rel.foreignKey) parentAttrs[rel.foreignKey] = parent.getKey();
192
+ }
193
+
194
+ const model = await this.makeOne({ ...parentAttrs, ...attributes });
195
+
179
196
  for (const callback of this._beforeCreatingCallbacks) {
180
197
  await callback(model);
181
198
  }
182
199
 
183
200
  await model.save();
184
201
 
185
- // Run afterCreating callbacks
202
+ // Handle 'has' (hasMany) — create children with FK pointing to this model
203
+ for (const [relationName, relFactory] of this._has) {
204
+ const rel = this._getRelation(relationName);
205
+ if (rel && rel.foreignKey) {
206
+ await relFactory.create({ [rel.foreignKey]: model.getKey() });
207
+ }
208
+ }
209
+
210
+ // Handle 'hasAttached' (belongsToMany) — create and attach via pivot
211
+ for (const [relationName, relFactory] of this._hasAttached) {
212
+ const related = await relFactory.create({});
213
+ const relatedArr = Array.isArray(related) ? related : [related];
214
+ const rel = this._getRelation(relationName);
215
+ if (rel && typeof rel.attach === 'function') {
216
+ for (const r of relatedArr) await rel.attach(r.getKey());
217
+ }
218
+ }
219
+
186
220
  for (const callback of this._afterCreatingCallbacks) {
187
221
  await callback(model);
188
222
  }
@@ -354,6 +388,9 @@ if (typeof Model !== 'undefined') {
354
388
  newFactory.states = new Map(existingFactory.states);
355
389
  newFactory._afterCreatingCallbacks = [...existingFactory._afterCreatingCallbacks];
356
390
  newFactory._beforeCreatingCallbacks = [...existingFactory._beforeCreatingCallbacks];
391
+ newFactory._has = new Map(existingFactory._has);
392
+ newFactory._for = new Map(existingFactory._for);
393
+ newFactory._hasAttached = new Map(existingFactory._hasAttached);
357
394
  return newFactory;
358
395
  }
359
396
  throw new Error(`No factory defined for model: ${this.name}`);
@@ -417,28 +417,20 @@ class MigrationRunner {
417
417
 
418
418
  if (isCreate || name.includes('create_')) {
419
419
  return isTS ?
420
- `import type { SchemaBuilder } from 'ilana-orm';
421
-
422
- export default class ${className} {
423
- // connection = 'mysql'; // Uncomment to use specific connection
424
-
425
- async up(schema: SchemaBuilder): Promise<void> {
420
+ `export default class ${className} {
421
+ async up(schema) {
426
422
  await schema.createTable('${table}', (table) => {
427
423
  table.increments('id');
428
424
  table.timestamps();
429
425
  });
430
426
  }
431
427
 
432
- async down(schema: SchemaBuilder): Promise<void> {
428
+ async down(schema) {
433
429
  await schema.dropTable('${table}');
434
430
  }
435
431
  }
436
432
  ` :
437
- `const { SchemaBuilder } = require('ilana-orm');
438
-
439
- class ${className} {
440
- // connection = 'mysql'; // Uncomment to use specific connection
441
-
433
+ `class ${className} {
442
434
  async up(schema) {
443
435
  await schema.createTable('${table}', (table) => {
444
436
  table.increments('id');
@@ -455,42 +447,30 @@ module.exports = ${className};
455
447
  `;
456
448
  } else if (tableName) {
457
449
  return isTS ?
458
- `import type { SchemaBuilder } from 'ilana-orm';
459
-
460
- export default class ${className} {
461
- // connection = 'mysql'; // Uncomment to use specific connection
462
-
463
- async up(schema: SchemaBuilder): Promise<void> {
450
+ `export default class ${className} {
451
+ async up(schema) {
464
452
  await schema.table('${table}', (table) => {
465
- // Add your column modifications here
466
- // table.string('new_column').nullable();
453
+ // table.string('column_name').nullable();
467
454
  });
468
455
  }
469
456
 
470
- async down(schema: SchemaBuilder): Promise<void> {
457
+ async down(schema) {
471
458
  await schema.table('${table}', (table) => {
472
- // Reverse your modifications here
473
- // table.dropColumn('new_column');
459
+ // table.dropColumn('column_name');
474
460
  });
475
461
  }
476
462
  }
477
463
  ` :
478
- `const { SchemaBuilder } = require('ilana-orm');
479
-
480
- class ${className} {
481
- // connection = 'mysql'; // Uncomment to use specific connection
482
-
464
+ `class ${className} {
483
465
  async up(schema) {
484
466
  await schema.table('${table}', (table) => {
485
- // Add your column modifications here
486
- // table.string('new_column').nullable();
467
+ // table.string('column_name').nullable();
487
468
  });
488
469
  }
489
470
 
490
471
  async down(schema) {
491
472
  await schema.table('${table}', (table) => {
492
- // Reverse your modifications here
493
- // table.dropColumn('new_column');
473
+ // table.dropColumn('column_name');
494
474
  });
495
475
  }
496
476
  }
@@ -500,31 +480,19 @@ module.exports = ${className};
500
480
  }
501
481
 
502
482
  return isTS ?
503
- `import type { SchemaBuilder } from 'ilana-orm';
504
-
505
- export default class ${className} {
506
- // connection = 'mysql'; // Uncomment to use specific connection
507
-
508
- async up(schema: SchemaBuilder): Promise<void> {
509
- // Add your migration logic here
483
+ `export default class ${className} {
484
+ async up(schema) {
510
485
  }
511
486
 
512
- async down(schema: SchemaBuilder): Promise<void> {
513
- // Add your rollback logic here
487
+ async down(schema) {
514
488
  }
515
489
  }
516
490
  ` :
517
- `const { SchemaBuilder } = require('ilana-orm');
518
-
519
- class ${className} {
520
- // connection = 'mysql'; // Uncomment to use specific connection
521
-
491
+ `class ${className} {
522
492
  async up(schema) {
523
- // Add your migration logic here
524
493
  }
525
494
 
526
495
  async down(schema) {
527
- // Add your rollback logic here
528
496
  }
529
497
  }
530
498
 
package/orm/Model.d.ts CHANGED
@@ -36,7 +36,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
36
36
  protected static table: string;
37
37
  protected static connection?: string;
38
38
  protected static primaryKey: string;
39
- protected static keyType: 'number' | 'string';
39
+ protected static keyType: 'number' | 'string' | 'uuid' | 'ulid';
40
40
  protected static incrementing: boolean;
41
41
  protected static timestamps: boolean;
42
42
  protected static createdAt: string;
@@ -50,6 +50,12 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
50
50
  protected static globalScopes: Map<string, (query: QueryBuilder) => void>;
51
51
  protected static appends: string[];
52
52
  protected static timezone: string;
53
+ static strictLoading: boolean;
54
+ static touches: string[];
55
+ static enums: { [column: string]: string[] };
56
+ static embeddingColumn: string;
57
+ static embeddingDimensions: number;
58
+ static embeddingProvider?: (text: string) => Promise<number[]>;
53
59
 
54
60
  // Instance properties
55
61
  attributes: ModelAttributes;
@@ -83,13 +89,25 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
83
89
  static oldest(column?: string): QueryBuilder;
84
90
  static withTrashed(): QueryBuilder;
85
91
  static onlyTrashed(): QueryBuilder;
92
+ static withoutTrashed(): QueryBuilder;
93
+ static findOrFail(id: number | string): Promise<Model>;
94
+ static insertGetId(data: { [key: string]: any }): Promise<number | string>;
86
95
  static upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
87
96
  static withoutGlobalScopes(): QueryBuilder;
88
97
  static make(attributes?: ModelAttributes): Model;
89
98
  static create(attributes?: ModelAttributes): Promise<Model>;
90
99
  static generateUuid(): string;
100
+ static generateUlid(): string;
101
+ static _generateKey(): string;
102
+ static withoutEvents<T>(callback: () => Promise<T>): Promise<T>;
103
+ static prunable(): QueryBuilder;
104
+ static prune(): Promise<number>;
91
105
  static insert(data: ModelAttributes | ModelAttributes[]): Promise<any>;
92
106
  static destroy(ids: any | any[]): Promise<number>;
107
+ static truncate(): Promise<void>;
108
+ static seed(count?: number): Promise<any[]>;
109
+ static nearestTo(vector: number[], options?: { limit?: number; column?: string; distance?: 'cosine' | 'l2' | 'inner' }): Promise<Collection<any>>;
110
+ static search(text: string, options?: { limit?: number; column?: string; distance?: 'cosine' | 'l2' | 'inner'; provider?: (text: string) => Promise<number[]> }): Promise<Collection<any>>;
93
111
  static firstOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
94
112
  static firstOrNew(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
95
113
  static updateOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
@@ -143,10 +161,16 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
143
161
  getDirty(): ModelAttributes;
144
162
  delete(): Promise<boolean>;
145
163
  restore(): Promise<boolean>;
164
+ increment(column: string, amount?: number): Promise<this>;
165
+ decrement(column: string, amount?: number): Promise<this>;
146
166
  trashed(): boolean;
147
167
  only(keys: string[]): ModelAttributes;
148
168
  except(keys: string[]): ModelAttributes;
149
169
  forceDelete(): Promise<boolean>;
170
+ fresh(): Promise<this | null>;
171
+ is(other: Model): boolean;
172
+ isNot(other: Model | null | undefined): boolean;
173
+ replicate(except?: string[]): this;
150
174
  toJSON(): any;
151
175
 
152
176
  // Relationships
package/orm/Model.js CHANGED
@@ -4,20 +4,22 @@ const { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, Morp
4
4
  const ModelRegistry = require('./ModelRegistry');
5
5
  const Database = require('../database/connection');
6
6
 
7
- // Auto-load configuration on first import
8
- (function autoLoadConfig() {
9
- const fs = require('fs');
10
- const path = require('path');
11
- const configPathJs = path.join(process.cwd(), 'ilana.config.js');
12
- const configPathMjs = path.join(process.cwd(), 'ilana.config.mjs');
13
-
14
- if (fs.existsSync(configPathJs)) {
15
- delete require.cache[configPathJs];
16
- require(configPathJs);
17
- } else if (fs.existsSync(configPathMjs)) {
18
- // For ES modules, we'll handle this in the _getConfig method
19
- }
20
- })();
7
+ // Auto-load configuration on first import (skipped in edge runtime)
8
+ if (typeof process !== 'undefined' && process.versions && process.versions.node && !global.__ILANA_EDGE__) {
9
+ (function autoLoadConfig() {
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const configPathJs = path.join(process.cwd(), 'ilana.config.js');
13
+ const configPathMjs = path.join(process.cwd(), 'ilana.config.mjs');
14
+
15
+ if (fs.existsSync(configPathJs)) {
16
+ delete require.cache[configPathJs];
17
+ require(configPathJs);
18
+ } else if (fs.existsSync(configPathMjs)) {
19
+ // For ES modules, handled in _getConfig
20
+ }
21
+ })();
22
+ }
21
23
 
22
24
  class Model {
23
25
  // --- Static defaults ---
@@ -35,6 +37,11 @@ class Model {
35
37
  static globalScopes = new Map();
36
38
  static appends = [];
37
39
  static timezone = 'UTC';
40
+ static strictLoading = false;
41
+ static touches = [];
42
+ static enums = {};
43
+ static embeddingColumn = 'embedding';
44
+ static embeddingDimensions = 1536;
38
45
 
39
46
  // --- Instance props ---
40
47
  attributes = {};
@@ -62,6 +69,25 @@ class Model {
62
69
  ? this.appends
63
70
  : this.constructor.appends;
64
71
 
72
+ // Wrap relations in a Proxy for strict loading enforcement
73
+ const modelClass = this.constructor;
74
+ this.relations = new Proxy({}, {
75
+ get(target, key) {
76
+ if (typeof key !== 'string') return target[key];
77
+ if (modelClass.strictLoading && !(key in target)) {
78
+ throw new Error(
79
+ `Strict loading violation: '${key}' was not eager loaded on ${modelClass.name}. ` +
80
+ `Use .with('${key}') in your query.`
81
+ );
82
+ }
83
+ return target[key];
84
+ },
85
+ set(target, key, value) { target[key] = value; return true; },
86
+ has(target, key) { return key in target; },
87
+ ownKeys(target) { return Object.keys(target); },
88
+ getOwnPropertyDescriptor(target, key) { return Object.getOwnPropertyDescriptor(target, key); },
89
+ });
90
+
65
91
  // defer attribute setting
66
92
  if (attrs && Object.keys(attrs).length) this._deferred = attrs;
67
93
 
@@ -79,6 +105,7 @@ class Model {
79
105
  this._deferred = null;
80
106
  // Recreate getters after initialization
81
107
  this._createAttributeGetters();
108
+ this._generateEnumHelpers();
82
109
  }
83
110
 
84
111
  _createAttributeGetters() {
@@ -106,6 +133,26 @@ class Model {
106
133
  }
107
134
  }
108
135
 
136
+ _generateEnumHelpers() {
137
+ const enums = this.constructor.enums || {};
138
+ for (const [column, values] of Object.entries(enums)) {
139
+ for (const value of values) {
140
+ const pascal = value.charAt(0).toUpperCase() + value.slice(1);
141
+ const isMethod = `is${pascal}`;
142
+ const makeMethod = `make${pascal}`;
143
+ if (!this[isMethod]) {
144
+ this[isMethod] = () => this.getAttribute(column) === value;
145
+ }
146
+ if (!this[makeMethod]) {
147
+ this[makeMethod] = async () => {
148
+ this.setAttribute(column, value);
149
+ return this.save();
150
+ };
151
+ }
152
+ }
153
+ }
154
+ }
155
+
109
156
  _toPascalCase(key) {
110
157
  return key.replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase());
111
158
  }
@@ -182,6 +229,9 @@ class Model {
182
229
  static oldest(col) { return this.query().oldest(col || this.createdAt || 'created_at'); }
183
230
  static withTrashed() { return this.query().withTrashed(); }
184
231
  static onlyTrashed() { return this.query().onlyTrashed(); }
232
+ static withoutTrashed() { return this.query().withoutTrashed(); }
233
+ static async findOrFail(id) { return this.query().findOrFail(id); }
234
+ static async insertGetId(data) { return this.query().insertGetId(data); }
185
235
  static async upsert(data, uniqueBy, update) { return this.query().upsert(data, uniqueBy, update); }
186
236
  static withoutGlobalScopes() {
187
237
  return new QueryBuilder(this.getTableName(), this, this.getConnectionName());
@@ -190,8 +240,8 @@ class Model {
190
240
  static make(attrs = {}) {
191
241
  const inst = new this(attrs);
192
242
  inst._initialize();
193
- if (!this.incrementing && this.keyType === 'string' && !inst.getKey()) {
194
- inst.setAttribute(this.primaryKey, this.generateUuid());
243
+ if (!this.incrementing && (this.keyType === 'string' || this.keyType === 'uuid' || this.keyType === 'ulid') && !inst.getKey()) {
244
+ inst.setAttribute(this.primaryKey, this._generateKey());
195
245
  }
196
246
  return inst;
197
247
  }
@@ -209,7 +259,67 @@ class Model {
209
259
  });
210
260
  }
211
261
 
262
+ static generateUlid() {
263
+ const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
264
+ const now = Date.now();
265
+ let t = now;
266
+ let ts = '';
267
+ for (let i = 9; i >= 0; i--) {
268
+ ts = CHARS[t % 32] + ts;
269
+ t = Math.floor(t / 32);
270
+ }
271
+ let rand = '';
272
+ for (let i = 0; i < 16; i++) rand += CHARS[Math.floor(Math.random() * 32)];
273
+ return ts + rand;
274
+ }
275
+
276
+ static _generateKey() {
277
+ if (this.keyType === 'ulid') return this.generateUlid();
278
+ return this.generateUuid();
279
+ }
280
+
212
281
  static async insert(data) { return this.query().insert(data); }
282
+ static async truncate() { return this.query().toKnex().truncate(); }
283
+
284
+ static _assertPgVector() {
285
+ const conn = Database.connection(this.connection);
286
+ const client = conn?.client?.config?.client || '';
287
+ if (!client.includes('pg')) {
288
+ throw new Error(
289
+ `${this.name}.search() and ${this.name}.nearestTo() require PostgreSQL with the pgvector extension. ` +
290
+ `Current database client is '${client || 'unknown'}'. ` +
291
+ `Vector search is not supported on MySQL or SQLite.`
292
+ );
293
+ }
294
+ }
295
+
296
+ static async nearestTo(vector, { limit = 10, column, distance = 'cosine' } = {}) {
297
+ this._assertPgVector();
298
+ const col = column || this.embeddingColumn;
299
+ const ops = { cosine: '<=>', l2: '<->', inner: '<#>' };
300
+ const op = ops[distance] || '<=>';
301
+ const vectorStr = `[${Array.from(vector).join(',')}]`;
302
+ return this.query()
303
+ .selectRaw(`*, (${col} ${op} ?) as distance`, [vectorStr])
304
+ .orderByRaw(`${col} ${op} ?`, [vectorStr])
305
+ .limit(limit)
306
+ .get();
307
+ }
308
+
309
+ static async search(text, { limit = 10, column, distance = 'cosine', provider } = {}) {
310
+ this._assertPgVector();
311
+ const embed = provider || this.embeddingProvider;
312
+ if (!embed) throw new Error(
313
+ `${this.name}.search() requires an embedding provider. ` +
314
+ `Pass { provider: async (text) => number[] } or set ${this.name}.embeddingProvider.`
315
+ );
316
+ const vector = await embed(text);
317
+ return this.nearestTo(vector, { limit, column, distance });
318
+ }
319
+ static async seed(count = 1) {
320
+ const { factory } = require('./Factory');
321
+ return factory(this).times(count).create();
322
+ }
213
323
  static async destroy(ids) {
214
324
  const idList = Array.isArray(ids) ? ids : [ids];
215
325
  if (this.softDeletes) {
@@ -293,6 +403,7 @@ class Model {
293
403
  }
294
404
  // static async fireEvent(evt, mdl) { for (const h of this.events[evt] || []) if (await h(mdl) === false) return false; }
295
405
  static async fireEvent(evt, mdl) {
406
+ if (this._mutingEvents) return true;
296
407
  const ownEvents = Object.hasOwn(this, 'events') ? this.events : {};
297
408
  const handlers = ownEvents[evt] || [];
298
409
  for (const handler of handlers) {
@@ -301,6 +412,31 @@ class Model {
301
412
  return true;
302
413
  }
303
414
 
415
+ static async withoutEvents(callback) {
416
+ this._mutingEvents = true;
417
+ try {
418
+ return await callback();
419
+ } finally {
420
+ this._mutingEvents = false;
421
+ }
422
+ }
423
+
424
+ static prunable() {
425
+ throw new Error(`${this.name} must implement a static prunable() method that returns a QueryBuilder.`);
426
+ }
427
+
428
+ static async prune() {
429
+ const query = this.prunable();
430
+ let pruned = 0;
431
+ await query.chunk(1000, async (models) => {
432
+ for (const model of models) {
433
+ await model.delete();
434
+ pruned++;
435
+ }
436
+ });
437
+ return pruned;
438
+ }
439
+
304
440
  // --- Instance methods ---
305
441
  static getTableName() { return this.table || this.name.toLowerCase() + 's'; }
306
442
  static getPrimaryKey() { return this.primaryKey; }
@@ -483,9 +619,10 @@ class Model {
483
619
  this.setAttribute(createdAtCol, now).setAttribute(updatedAtCol, now);
484
620
  }
485
621
 
486
- // Generate UUID if needed
487
- if (!this.constructor.incrementing && this.constructor.keyType === 'string' && !this.getKey()) {
488
- this.setAttribute(this.constructor.primaryKey, this.constructor.generateUuid());
622
+ // Generate UUID/ULID if needed
623
+ const kt = this.constructor.keyType;
624
+ if (!this.constructor.incrementing && (kt === 'string' || kt === 'uuid' || kt === 'ulid') && !this.getKey()) {
625
+ this.setAttribute(this.constructor.primaryKey, this.constructor._generateKey());
489
626
  }
490
627
 
491
628
  const qb = this.constructor.query();
@@ -525,9 +662,27 @@ class Model {
525
662
  this.syncOriginal();
526
663
  }
527
664
 
665
+ await this._touchRelations();
666
+
528
667
  return true;
529
668
  }
530
669
 
670
+ async _touchRelations() {
671
+ const touches = this.constructor.touches || [];
672
+ for (const relName of touches) {
673
+ if (typeof this[relName] !== 'function') continue;
674
+ const rel = this[relName]();
675
+ if (!rel || rel.constructor.name !== 'BelongsTo') continue;
676
+ const parentClass = rel.getRelatedClass();
677
+ const parentId = this.getAttribute(rel.foreignKey);
678
+ if (!parentId) continue;
679
+ const col = parentClass.updatedAt || 'updated_at';
680
+ await parentClass.query()
681
+ .where(parentClass.primaryKey || 'id', parentId)
682
+ .update({ [col]: new Date() });
683
+ }
684
+ }
685
+
531
686
  async update(attributes = {}) {
532
687
  this.fill(attributes);
533
688
  return await this.save();
@@ -604,6 +759,20 @@ class Model {
604
759
  return result;
605
760
  }
606
761
 
762
+ async increment(column, amount = 1) {
763
+ await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).increment(column, amount);
764
+ this.setAttribute(column, (this.getAttribute(column) || 0) + amount);
765
+ this.syncOriginal();
766
+ return this;
767
+ }
768
+
769
+ async decrement(column, amount = 1) {
770
+ await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).decrement(column, amount);
771
+ this.setAttribute(column, (this.getAttribute(column) || 0) - amount);
772
+ this.syncOriginal();
773
+ return this;
774
+ }
775
+
607
776
  async forceDelete() {
608
777
  if (!this.exists) return false;
609
778
 
@@ -614,6 +783,37 @@ class Model {
614
783
  return true;
615
784
  }
616
785
 
786
+ async fresh() {
787
+ if (!this.exists) return null;
788
+ return this.constructor.find(this.getKey());
789
+ }
790
+
791
+ is(other) {
792
+ if (!other || !(other instanceof this.constructor)) return false;
793
+ return this.getKey() === other.getKey();
794
+ }
795
+
796
+ isNot(other) {
797
+ return !this.is(other);
798
+ }
799
+
800
+ replicate(except = []) {
801
+ const exclude = new Set([
802
+ this.constructor.primaryKey,
803
+ ...(this.constructor.timestamps ? ['created_at', 'updated_at'] : []),
804
+ ...except,
805
+ ]);
806
+ const attrs = {};
807
+ for (const [k, v] of Object.entries(this.attributes)) {
808
+ if (!exclude.has(k)) attrs[k] = v;
809
+ }
810
+ const copy = new this.constructor(attrs);
811
+ copy._initialize();
812
+ copy.exists = false;
813
+ copy.wasRecentlyCreated = false;
814
+ return copy;
815
+ }
816
+
617
817
  // JSON serialization
618
818
  toJSON() {
619
819
  this._initialize();
@@ -104,12 +104,21 @@ export default class QueryBuilder {
104
104
  // Selection
105
105
  select(...columns: any[]): this;
106
106
  addSelect(...columns: any[]): this;
107
+ addSelect(subqueries: { [alias: string]: (query: QueryBuilder) => void }): this;
107
108
  distinct(): this;
108
109
 
109
110
  // Raw queries
110
111
  whereRaw(sql: string, bindings?: any[]): this;
111
112
  selectRaw(sql: string, bindings?: any[]): this;
112
113
 
114
+ // Advanced subqueries
115
+ orderBySubquery(callback: (query: QueryBuilder) => void, direction?: 'asc' | 'desc'): this;
116
+
117
+ // Pending attributes (scope defaults)
118
+ withPendingAttributes(attributes: { [key: string]: any }): this;
119
+ new(attributes?: { [key: string]: any }): Promise<import('./Model').default>;
120
+ create(attributes?: { [key: string]: any }): Promise<import('./Model').default>;
121
+
113
122
  // Aggregates
114
123
  count(column?: string): Promise<number>;
115
124
  sum(column: string): Promise<number>;
@@ -125,6 +134,7 @@ export default class QueryBuilder {
125
134
  whereHas(relation: string, callback?: (query: QueryBuilder) => void): this;
126
135
  doesntHave(relation: string): this;
127
136
  whereDoesntHave(relation: string, callback?: (query: QueryBuilder) => void): this;
137
+ has(relation: string, operator?: '=' | '!=' | '<' | '<=' | '>' | '>=', count?: number): this;
128
138
 
129
139
  // Execution methods
130
140
  get(): Promise<Collection<Model>>;
@@ -135,6 +145,8 @@ export default class QueryBuilder {
135
145
  pluck(column: string): Promise<any[]>;
136
146
  exists(): Promise<boolean>;
137
147
  doesntExist(): Promise<boolean>;
148
+ sole(): Promise<Model>;
149
+ tap(callback: (query: this) => void): this;
138
150
 
139
151
  // Pagination
140
152
  paginate(page?: number, perPage?: number): Promise<PaginationResult<Model>>;
@@ -150,6 +162,8 @@ export default class QueryBuilder {
150
162
  insert(data: any | any[]): Promise<any>;
151
163
  insertGetId(data: any): Promise<any>;
152
164
  update(data: any): Promise<number>;
165
+ increment(column: string, amount?: number): Promise<number>;
166
+ decrement(column: string, amount?: number): Promise<number>;
153
167
  delete(): Promise<number>;
154
168
  upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
155
169
 
@@ -167,6 +181,10 @@ export default class QueryBuilder {
167
181
  withTrashed(): this;
168
182
  onlyTrashed(): this;
169
183
  withoutTrashed(): this;
184
+ restore(): Promise<number>;
185
+
186
+ // Plain object result (no model hydration)
187
+ values(): Promise<any[]>;
170
188
 
171
189
  // Debug
172
190
  toSql(): string;