ilana-orm 1.0.18 → 1.0.20

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.
@@ -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
@@ -1,4 +1,5 @@
1
- import QueryBuilder from './QueryBuilder';
1
+ import QueryBuilder, { PaginationResult, SimplePaginationResult, CursorPaginationResult } from './QueryBuilder';
2
+ import Collection from './Collection';
2
3
  import { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphOne, MorphMany } from './Relation';
3
4
 
4
5
  export interface ModelAttributes {
@@ -36,7 +37,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
36
37
  protected static table: string;
37
38
  protected static connection?: string;
38
39
  protected static primaryKey: string;
39
- protected static keyType: 'number' | 'string';
40
+ protected static keyType: 'number' | 'string' | 'uuid' | 'ulid';
40
41
  protected static incrementing: boolean;
41
42
  protected static timestamps: boolean;
42
43
  protected static createdAt: string;
@@ -51,8 +52,12 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
51
52
  protected static appends: string[];
52
53
  protected static timezone: string;
53
54
  static strictLoading: boolean;
55
+ static preventsSilentlyDiscardingAttributes: boolean;
54
56
  static touches: string[];
55
57
  static enums: { [column: string]: string[] };
58
+ static embeddingColumn: string;
59
+ static embeddingDimensions: number;
60
+ static embeddingProvider?: (text: string) => Promise<number[]>;
56
61
 
57
62
  // Instance properties
58
63
  attributes: ModelAttributes;
@@ -72,6 +77,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
72
77
 
73
78
  // Static methods
74
79
  static register(): void;
80
+ static _autoRegister(): void;
75
81
  static resolveRelatedModel(related: string | typeof Model): typeof Model;
76
82
  static query(): QueryBuilder;
77
83
  static with(...relations: string[]): QueryBuilder;
@@ -86,19 +92,136 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
86
92
  static oldest(column?: string): QueryBuilder;
87
93
  static withTrashed(): QueryBuilder;
88
94
  static onlyTrashed(): QueryBuilder;
95
+ static withoutTrashed(): QueryBuilder;
96
+ static findOrFail(id: number | string): Promise<Model>;
97
+ static insertGetId(data: { [key: string]: any }): Promise<number | string>;
89
98
  static upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
90
99
  static withoutGlobalScopes(): QueryBuilder;
91
100
  static make(attributes?: ModelAttributes): Model;
92
101
  static create(attributes?: ModelAttributes): Promise<Model>;
93
102
  static generateUuid(): string;
103
+ static generateUlid(): string;
104
+ static _generateKey(): string;
105
+ static withoutEvents<T>(callback: () => Promise<T>): Promise<T>;
106
+ static prunable(): QueryBuilder;
107
+ static prune(): Promise<number>;
94
108
  static insert(data: ModelAttributes | ModelAttributes[]): Promise<any>;
95
109
  static destroy(ids: any | any[]): Promise<number>;
96
110
  static truncate(): Promise<void>;
97
111
  static seed(count?: number): Promise<any[]>;
112
+ static nearestTo(vector: number[], options?: { limit?: number; column?: string; distance?: 'cosine' | 'l2' | 'inner' }): Promise<Collection<any>>;
113
+ static search(text: string, options?: { limit?: number; column?: string; distance?: 'cosine' | 'l2' | 'inner'; provider?: (text: string) => Promise<number[]> }): Promise<Collection<any>>;
98
114
  static firstOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
99
115
  static firstOrNew(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
100
116
  static updateOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
101
117
 
118
+ // QueryBuilder methods forwarded from Model.query() — at runtime, any
119
+ // QueryBuilder method not already declared above is proxied automatically
120
+ // from Model.<method>() to Model.query().<method>() (see Model.js), so this
121
+ // list exists purely to give TypeScript users type information for it.
122
+ static where(column: string, value: any): QueryBuilder;
123
+ static where(column: string, operator: string, value: any): QueryBuilder;
124
+ static orWhere(column: string, value: any): QueryBuilder;
125
+ static orWhere(column: string, operator: string, value: any): QueryBuilder;
126
+ static whereIn(column: string, values: any[]): QueryBuilder;
127
+ static whereNotIn(column: string, values: any[]): QueryBuilder;
128
+ static whereNull(column: string): QueryBuilder;
129
+ static whereNotNull(column: string): QueryBuilder;
130
+ static whereBetween(column: string, range: [any, any]): QueryBuilder;
131
+ static whereNotBetween(column: string, range: [any, any]): QueryBuilder;
132
+ static whereJsonContains(column: string, value: any): QueryBuilder;
133
+ static whereJsonLength(column: string, operator: string, value: number): QueryBuilder;
134
+ static whereDate(column: string, value: string): QueryBuilder;
135
+ static whereDate(column: string, operator: string, value: string): QueryBuilder;
136
+ static whereMonth(column: string, month: number): QueryBuilder;
137
+ static whereYear(column: string, year: number): QueryBuilder;
138
+ static whereDay(column: string, operatorOrValue: any, value?: any): QueryBuilder;
139
+ static whereTime(column: string, operatorOrValue: any, value?: any): QueryBuilder;
140
+ static whereRaw(sql: string, bindings?: any[]): QueryBuilder;
141
+ static orWhereNull(column: string): QueryBuilder;
142
+ static orWhereNotNull(column: string): QueryBuilder;
143
+ static orWhereIn(column: string, values: any[]): QueryBuilder;
144
+ static orWhereNotIn(column: string, values: any[]): QueryBuilder;
145
+ static orWhereRaw(sql: string, bindings?: any[]): QueryBuilder;
146
+ static whereExists(callback: (query: QueryBuilder) => void): QueryBuilder;
147
+ static whereNotExists(callback: (query: QueryBuilder) => void): QueryBuilder;
148
+ static when<T>(condition: T, callback: (query: QueryBuilder, condition: T) => void, otherwise?: (query: QueryBuilder) => void): QueryBuilder;
149
+ static unless<T>(condition: T, callback: (query: QueryBuilder) => void, otherwise?: (query: QueryBuilder, condition: T) => void): QueryBuilder;
150
+
151
+ static join(table: string, first: string, operator: string, second: string): QueryBuilder;
152
+ static leftJoin(table: string, first: string, operator: string, second: string): QueryBuilder;
153
+ static rightJoin(table: string, first: string, operator: string, second: string): QueryBuilder;
154
+ static innerJoin(table: string, first: string, operator: string, second: string): QueryBuilder;
155
+ static crossJoin(table: string): QueryBuilder;
156
+
157
+ static orderBy(column: string, direction?: 'asc' | 'desc'): QueryBuilder;
158
+ static orderByRaw(sql: string): QueryBuilder;
159
+ static orderBySubquery(callback: (query: QueryBuilder) => void, direction?: 'asc' | 'desc'): QueryBuilder;
160
+ static inRandomOrder(): QueryBuilder;
161
+ static limit(count: number): QueryBuilder;
162
+ static offset(count: number): QueryBuilder;
163
+ static take(count: number): QueryBuilder;
164
+ static skip(count: number): QueryBuilder;
165
+ static from(table: string): QueryBuilder;
166
+ static forPage(page: number, perPage?: number): QueryBuilder;
167
+
168
+ static groupBy(...columns: string[]): QueryBuilder;
169
+ static having(column: string, operator: string, value: any): QueryBuilder;
170
+ static having(rawSql: string): QueryBuilder;
171
+ static havingRaw(sql: string, bindings?: any[]): QueryBuilder;
172
+
173
+ static lockForUpdate(): QueryBuilder;
174
+ static sharedLock(): QueryBuilder;
175
+ static skipLocked(): QueryBuilder;
176
+ static noWait(): QueryBuilder;
177
+
178
+ static select(...columns: any[]): QueryBuilder;
179
+ static addSelect(...columns: any[]): QueryBuilder;
180
+ static addSelect(subqueries: { [alias: string]: (query: QueryBuilder) => void }): QueryBuilder;
181
+ static distinct(): QueryBuilder;
182
+ static selectRaw(sql: string, bindings?: any[]): QueryBuilder;
183
+
184
+ static withPendingAttributes(attributes: { [key: string]: any }): QueryBuilder;
185
+ static withConstraints(relation: string, callback: (query: QueryBuilder) => void): QueryBuilder;
186
+ static withConstraints(relations: { [key: string]: (query: QueryBuilder) => void }): QueryBuilder;
187
+ static whereHas(relation: string, callback?: (query: QueryBuilder) => void): QueryBuilder;
188
+ static doesntHave(relation: string): QueryBuilder;
189
+ static whereDoesntHave(relation: string, callback?: (query: QueryBuilder) => void): QueryBuilder;
190
+ static has(relation: string, operator?: '=' | '!=' | '<' | '<=' | '>' | '>=', count?: number): QueryBuilder;
191
+
192
+ static count(column?: string): Promise<number>;
193
+ static sum(column: string): Promise<number>;
194
+ static avg(column: string): Promise<number>;
195
+ static min(column: string): Promise<any>;
196
+ static max(column: string): Promise<any>;
197
+
198
+ static pluck(column: string): Promise<any[]>;
199
+ static exists(): Promise<boolean>;
200
+ static doesntExist(): Promise<boolean>;
201
+ static sole(): Promise<Model>;
202
+ static tap(callback: (query: QueryBuilder) => void): QueryBuilder;
203
+ static get(): Promise<Collection<Model>>;
204
+
205
+ static paginate(page?: number, perPage?: number): Promise<PaginationResult<Model>>;
206
+ static simplePaginate(page?: number, perPage?: number): Promise<SimplePaginationResult<Model>>;
207
+ static cursorPaginate(perPage?: number, cursor?: string, column?: string, direction?: 'asc' | 'desc'): Promise<CursorPaginationResult<Model>>;
208
+
209
+ static chunk(size: number, callback: (models: Collection<Model>) => Promise<void>): Promise<void>;
210
+ static cursor(chunkSize?: number): AsyncGenerator<Model, void, unknown>;
211
+ static lazy(chunkSize?: number): AsyncGenerator<Model, void, unknown>;
212
+
213
+ static update(data: any): Promise<number>;
214
+ static increment(column: string, amount?: number): Promise<number>;
215
+ static decrement(column: string, amount?: number): Promise<number>;
216
+ static delete(): Promise<number>;
217
+ static restore(): Promise<number>;
218
+
219
+ static clone(): QueryBuilder;
220
+ static toKnex(): any;
221
+ static toSql(): string;
222
+ static values(): Promise<any[]>;
223
+ static new(attributes?: { [key: string]: any }): Promise<Model>;
224
+
102
225
  // Scopes
103
226
  static addGlobalScope(name: string, scope: (query: QueryBuilder) => void): void;
104
227
  static removeGlobalScope(name: string): void;
@@ -129,6 +252,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
129
252
  // Instance methods
130
253
  getKey(): any;
131
254
  fill(attributes: ModelAttributes): this;
255
+ forceFill(attributes: ModelAttributes): this;
132
256
  load(...relations: string[]): Promise<this>;
133
257
  loadMissing(...relations: string[]): Promise<this>;
134
258
  getRelation(key: string): any;
@@ -156,6 +280,8 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
156
280
  forceDelete(): Promise<boolean>;
157
281
  fresh(): Promise<this | null>;
158
282
  is(other: Model): boolean;
283
+ isNot(other: Model | null | undefined): boolean;
284
+ replicate(except?: string[]): this;
159
285
  toJSON(): any;
160
286
 
161
287
  // Relationships
package/orm/Model.js CHANGED
@@ -3,21 +3,24 @@ const QueryBuilder = require('./QueryBuilder');
3
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
-
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
- })();
6
+ const { MassAssignmentException } = require('./Errors');
7
+
8
+ // Auto-load configuration on first import (skipped in edge runtime)
9
+ if (typeof process !== 'undefined' && process.versions && process.versions.node && !global.__ILANA_EDGE__) {
10
+ (function autoLoadConfig() {
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const configPathJs = path.join(process.cwd(), 'ilana.config.js');
14
+ const configPathMjs = path.join(process.cwd(), 'ilana.config.mjs');
15
+
16
+ if (fs.existsSync(configPathJs)) {
17
+ delete require.cache[configPathJs];
18
+ require(configPathJs);
19
+ } else if (fs.existsSync(configPathMjs)) {
20
+ // For ES modules, handled in _getConfig
21
+ }
22
+ })();
23
+ }
21
24
 
22
25
  class Model {
23
26
  // --- Static defaults ---
@@ -36,8 +39,11 @@ class Model {
36
39
  static appends = [];
37
40
  static timezone = 'UTC';
38
41
  static strictLoading = false;
42
+ static preventsSilentlyDiscardingAttributes = false;
39
43
  static touches = [];
40
44
  static enums = {};
45
+ static embeddingColumn = 'embedding';
46
+ static embeddingDimensions = 1536;
41
47
 
42
48
  // --- Instance props ---
43
49
  attributes = {};
@@ -53,6 +59,8 @@ class Model {
53
59
  _deferred;
54
60
 
55
61
  constructor(attrs = {}) {
62
+ this.constructor._autoRegister();
63
+
56
64
  // instance-level fillable/guarded/casts
57
65
  this.fillable = Array.isArray(this.fillable) && this.fillable.length
58
66
  ? this.fillable
@@ -164,10 +172,21 @@ class Model {
164
172
  ModelRegistry.register(this.name, this);
165
173
  }
166
174
 
175
+ // Registers this class the first time it's actually used (constructed or
176
+ // queried), so string-based relations (this.hasMany('Post')) resolve
177
+ // without requiring an explicit Post.register() call, as long as Post gets
178
+ // used somewhere before the relation is loaded. Cheap to call repeatedly:
179
+ // skips the registry write once this class is already registered as itself.
180
+ static _autoRegister() {
181
+ if (ModelRegistry.get(this.name) !== this) {
182
+ ModelRegistry.register(this.name, this);
183
+ }
184
+ }
185
+
167
186
  static resolveRelatedModel(related) {
168
187
  if (typeof related === 'string') {
169
188
  const cls = ModelRegistry.get(related);
170
- if (!cls) throw new Error(`Model '${related}' not found. Make sure to call ${related}.register().`);
189
+ if (!cls) throw new Error(`Model '${related}' not found. It gets registered automatically the first time it's queried or constructed, so require/use ${related} somewhere before this point, or call ${related}.register() explicitly.`);
171
190
  return cls;
172
191
  }
173
192
 
@@ -189,6 +208,7 @@ class Model {
189
208
 
190
209
  // --- Query builder ---
191
210
  static query() {
211
+ this._autoRegister();
192
212
 
193
213
  const qb = new QueryBuilder(this.getTableName(), this, this.getConnectionName());
194
214
 
@@ -225,6 +245,9 @@ class Model {
225
245
  static oldest(col) { return this.query().oldest(col || this.createdAt || 'created_at'); }
226
246
  static withTrashed() { return this.query().withTrashed(); }
227
247
  static onlyTrashed() { return this.query().onlyTrashed(); }
248
+ static withoutTrashed() { return this.query().withoutTrashed(); }
249
+ static async findOrFail(id) { return this.query().findOrFail(id); }
250
+ static async insertGetId(data) { return this.query().insertGetId(data); }
228
251
  static async upsert(data, uniqueBy, update) { return this.query().upsert(data, uniqueBy, update); }
229
252
  static withoutGlobalScopes() {
230
253
  return new QueryBuilder(this.getTableName(), this, this.getConnectionName());
@@ -233,8 +256,8 @@ class Model {
233
256
  static make(attrs = {}) {
234
257
  const inst = new this(attrs);
235
258
  inst._initialize();
236
- if (!this.incrementing && this.keyType === 'string' && !inst.getKey()) {
237
- inst.setAttribute(this.primaryKey, this.generateUuid());
259
+ if (!this.incrementing && (this.keyType === 'string' || this.keyType === 'uuid' || this.keyType === 'ulid') && !inst.getKey()) {
260
+ inst.setAttribute(this.primaryKey, this._generateKey());
238
261
  }
239
262
  return inst;
240
263
  }
@@ -252,8 +275,63 @@ class Model {
252
275
  });
253
276
  }
254
277
 
278
+ static generateUlid() {
279
+ const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
280
+ const now = Date.now();
281
+ let t = now;
282
+ let ts = '';
283
+ for (let i = 9; i >= 0; i--) {
284
+ ts = CHARS[t % 32] + ts;
285
+ t = Math.floor(t / 32);
286
+ }
287
+ let rand = '';
288
+ for (let i = 0; i < 16; i++) rand += CHARS[Math.floor(Math.random() * 32)];
289
+ return ts + rand;
290
+ }
291
+
292
+ static _generateKey() {
293
+ if (this.keyType === 'ulid') return this.generateUlid();
294
+ return this.generateUuid();
295
+ }
296
+
255
297
  static async insert(data) { return this.query().insert(data); }
256
298
  static async truncate() { return this.query().toKnex().truncate(); }
299
+
300
+ static _assertPgVector() {
301
+ const conn = Database.connection(this.connection);
302
+ const client = conn?.client?.config?.client || '';
303
+ if (!client.includes('pg')) {
304
+ throw new Error(
305
+ `${this.name}.search() and ${this.name}.nearestTo() require PostgreSQL with the pgvector extension. ` +
306
+ `Current database client is '${client || 'unknown'}'. ` +
307
+ `Vector search is not supported on MySQL or SQLite.`
308
+ );
309
+ }
310
+ }
311
+
312
+ static async nearestTo(vector, { limit = 10, column, distance = 'cosine' } = {}) {
313
+ this._assertPgVector();
314
+ const col = column || this.embeddingColumn;
315
+ const ops = { cosine: '<=>', l2: '<->', inner: '<#>' };
316
+ const op = ops[distance] || '<=>';
317
+ const vectorStr = `[${Array.from(vector).join(',')}]`;
318
+ return this.query()
319
+ .selectRaw(`*, (${col} ${op} ?) as distance`, [vectorStr])
320
+ .orderByRaw(`${col} ${op} ?`, [vectorStr])
321
+ .limit(limit)
322
+ .get();
323
+ }
324
+
325
+ static async search(text, { limit = 10, column, distance = 'cosine', provider } = {}) {
326
+ this._assertPgVector();
327
+ const embed = provider || this.embeddingProvider;
328
+ if (!embed) throw new Error(
329
+ `${this.name}.search() requires an embedding provider. ` +
330
+ `Pass { provider: async (text) => number[] } or set ${this.name}.embeddingProvider.`
331
+ );
332
+ const vector = await embed(text);
333
+ return this.nearestTo(vector, { limit, column, distance });
334
+ }
257
335
  static async seed(count = 1) {
258
336
  const { factory } = require('./Factory');
259
337
  return factory(this).times(count).create();
@@ -341,6 +419,7 @@ class Model {
341
419
  }
342
420
  // static async fireEvent(evt, mdl) { for (const h of this.events[evt] || []) if (await h(mdl) === false) return false; }
343
421
  static async fireEvent(evt, mdl) {
422
+ if (this._mutingEvents) return true;
344
423
  const ownEvents = Object.hasOwn(this, 'events') ? this.events : {};
345
424
  const handlers = ownEvents[evt] || [];
346
425
  for (const handler of handlers) {
@@ -349,6 +428,31 @@ class Model {
349
428
  return true;
350
429
  }
351
430
 
431
+ static async withoutEvents(callback) {
432
+ this._mutingEvents = true;
433
+ try {
434
+ return await callback();
435
+ } finally {
436
+ this._mutingEvents = false;
437
+ }
438
+ }
439
+
440
+ static prunable() {
441
+ throw new Error(`${this.name} must implement a static prunable() method that returns a QueryBuilder.`);
442
+ }
443
+
444
+ static async prune() {
445
+ const query = this.prunable();
446
+ let pruned = 0;
447
+ await query.chunk(1000, async (models) => {
448
+ for (const model of models) {
449
+ await model.delete();
450
+ pruned++;
451
+ }
452
+ });
453
+ return pruned;
454
+ }
455
+
352
456
  // --- Instance methods ---
353
457
  static getTableName() { return this.table || this.name.toLowerCase() + 's'; }
354
458
  static getPrimaryKey() { return this.primaryKey; }
@@ -359,8 +463,29 @@ class Model {
359
463
  getKey() { return this.attributes[this.constructor.primaryKey]; }
360
464
 
361
465
  fill(attrs) {
466
+ const discarded = [];
467
+ for (const [k, v] of Object.entries(attrs)) {
468
+ if (!this.isFillable(k)) { discarded.push(k); continue; }
469
+ this.setAttribute(k, v);
470
+ }
471
+ // Off by default, matching every prior release: a key rejected by
472
+ // fillable/guarded is silently dropped, same as always. Opt in per-model
473
+ // with `static preventsSilentlyDiscardingAttributes = true` to instead
474
+ // throw on any discarded key — useful for catching a forgotten
475
+ // `fillable` declaration or a typo'd column name in update() calls,
476
+ // without changing behavior for anyone who hasn't asked for it.
477
+ if (discarded.length > 0 && this.constructor.preventsSilentlyDiscardingAttributes) {
478
+ throw new MassAssignmentException(this.constructor.name, discarded);
479
+ }
480
+ return this;
481
+ }
482
+
483
+ // Sets attributes bypassing fillable/guarded entirely, for trusted,
484
+ // programmatic data (factories, seeders, internal code) rather than
485
+ // user-supplied mass assignment — mirrors fill()'s counterpart in
486
+ // Eloquent-style ORMs.
487
+ forceFill(attrs) {
362
488
  for (const [k, v] of Object.entries(attrs)) {
363
- if (!this.isFillable(k)) continue;
364
489
  this.setAttribute(k, v);
365
490
  }
366
491
  return this;
@@ -531,9 +656,10 @@ class Model {
531
656
  this.setAttribute(createdAtCol, now).setAttribute(updatedAtCol, now);
532
657
  }
533
658
 
534
- // Generate UUID if needed
535
- if (!this.constructor.incrementing && this.constructor.keyType === 'string' && !this.getKey()) {
536
- this.setAttribute(this.constructor.primaryKey, this.constructor.generateUuid());
659
+ // Generate UUID/ULID if needed
660
+ const kt = this.constructor.keyType;
661
+ if (!this.constructor.incrementing && (kt === 'string' || kt === 'uuid' || kt === 'ulid') && !this.getKey()) {
662
+ this.setAttribute(this.constructor.primaryKey, this.constructor._generateKey());
537
663
  }
538
664
 
539
665
  const qb = this.constructor.query();
@@ -704,6 +830,27 @@ class Model {
704
830
  return this.getKey() === other.getKey();
705
831
  }
706
832
 
833
+ isNot(other) {
834
+ return !this.is(other);
835
+ }
836
+
837
+ replicate(except = []) {
838
+ const exclude = new Set([
839
+ this.constructor.primaryKey,
840
+ ...(this.constructor.timestamps ? ['created_at', 'updated_at'] : []),
841
+ ...except,
842
+ ]);
843
+ const attrs = {};
844
+ for (const [k, v] of Object.entries(this.attributes)) {
845
+ if (!exclude.has(k)) attrs[k] = v;
846
+ }
847
+ const copy = new this.constructor(attrs);
848
+ copy._initialize();
849
+ copy.exists = false;
850
+ copy.wasRecentlyCreated = false;
851
+ return copy;
852
+ }
853
+
707
854
  // JSON serialization
708
855
  toJSON() {
709
856
  this._initialize();
@@ -799,4 +946,21 @@ class Model {
799
946
  }
800
947
  }
801
948
 
802
- module.exports = Model;
949
+ // Any QueryBuilder instance method not already forwarded above (e.g. where(),
950
+ // orderBy(), whereIn(), paginate()...) is auto-forwarded from Model.<method>()
951
+ // to Model.query().<method>(), so new QueryBuilder methods don't need a
952
+ // matching static added here to be callable directly on a Model subclass.
953
+ const ModelStaticHandler = {
954
+ get(target, prop, receiver) {
955
+ if (typeof prop === 'symbol' || prop in target) {
956
+ return Reflect.get(target, prop, receiver);
957
+ }
958
+ const qbMethod = QueryBuilder.prototype[prop];
959
+ if (typeof prop === 'string' && prop[0] !== '_' && typeof qbMethod === 'function') {
960
+ return (...args) => receiver.query()[prop](...args);
961
+ }
962
+ return Reflect.get(target, prop, receiver);
963
+ }
964
+ };
965
+
966
+ module.exports = new Proxy(Model, ModelStaticHandler);
@@ -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>>;