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.
package/README.md CHANGED
@@ -5,7 +5,19 @@
5
5
 
6
6
  **Ìlànà** (pronounced "ee-LAH-nah") - A Yoruba word meaning "pattern," "system," or "protocol."
7
7
 
8
- A fully-featured, Eloquent-style ORM for Node.js with automatic TypeScript support. IlanaORM provides complete feature parity with Laravel's Eloquent ORM, following established patterns and protocols for database interaction, modeling, querying, relationships, events, casting, migrations, and more.
8
+ A fully-featured, Laravel Eloquent-style ORM for Node.js & TypeScript. If you know Eloquent, you already know IlanaORM — same API, same patterns, same conventions. MySQL, PostgreSQL, SQLite, Supabase, edge runtimes, and pgvector AI search out of the box.
9
+
10
+ | Feature | IlanaORM | Prisma | Drizzle | TypeORM |
11
+ |---|:---:|:---:|:---:|:---:|
12
+ | Eloquent-identical API | ✅ | ❌ | ❌ | ❌ |
13
+ | pgvector / AI search built-in | ✅ | ❌ | ❌ | ❌ |
14
+ | Edge runtime (Cloudflare, Next.js) | ✅ | ⚠️ | ✅ | ❌ |
15
+ | Supabase compatible | ✅ | ✅ | ✅ | ⚠️ |
16
+ | ULID primary keys | ✅ | ⚠️ | ⚠️ | ⚠️ |
17
+ | Factories & seeders built-in | ✅ | ❌ | ❌ | ❌ |
18
+ | Model events | ✅ | ⚠️ | ❌ | ✅ |
19
+ | Soft deletes | ✅ | ❌ | ❌ | ✅ |
20
+ | No code generation step | ✅ | ❌ | ✅ | ✅ |
9
21
 
10
22
  ## Table of Contents
11
23
 
@@ -24,6 +36,7 @@ A fully-featured, Eloquent-style ORM for Node.js with automatic TypeScript suppo
24
36
  - [Schema Builder](#schema-builder)
25
37
  - [Transactions](#transactions)
26
38
  - [Advanced Features](#advanced-features)
39
+ - [Supabase](#supabase)
27
40
  - [Complete API Reference](#complete-api-reference)
28
41
  - [TypeScript Support](#typescript-support)
29
42
  - [Performance & Best Practices](#performance--best-practices)
@@ -717,6 +730,11 @@ class User extends Model {
717
730
  fillable = ["name", "email", "password"];
718
731
  guarded = ["id", "created_at", "updated_at"];
719
732
 
733
+ // By default, fill()/update() silently drops any key not covered by
734
+ // fillable/guarded — e.g. update({ typo_column: x }) resolves successfully
735
+ // and writes nothing. Opt in to catch that instead of silently swallowing it:
736
+ static preventsSilentlyDiscardingAttributes = true; // throws MassAssignmentException on any discarded key
737
+
720
738
  // Hidden attributes (won't appear in JSON)
721
739
  hidden = ["password", "remember_token"];
722
740
 
@@ -793,47 +811,33 @@ export default class User extends Model {
793
811
 
794
812
  ### UUID Primary Keys
795
813
 
796
- **JavaScript:**
797
814
  ```javascript
798
815
  class User extends Model {
799
816
  static table = 'users';
800
- static keyType = 'string';
817
+ static keyType = 'uuid';
801
818
  static incrementing = false;
802
819
  }
803
820
 
804
- module.exports = User;
805
-
806
- // Usage
807
- const user = await User.create({
808
- name: 'John Doe',
809
- email: 'john@example.com',
810
- });
811
- // user.id will be a generated UUID
812
- ````
821
+ const user = await User.create({ name: 'John Doe', email: 'john@example.com' });
822
+ // user.id → "550e8400-e29b-41d4-a716-446655440000"
823
+ ```
813
824
 
814
- **TypeScript:**
825
+ ### ULID Primary Keys
815
826
 
816
- ```typescript
817
- export default class User extends Model {
818
- protected static table = "users";
819
- protected static keyType = "string" as const;
820
- protected static incrementing = false;
827
+ ULIDs are 26-character sortable identifiers — URL-safe, lexicographically ordered by creation time:
821
828
 
822
- // Attributes
823
- id!: string; // UUID primary key
824
- name!: string;
825
- email!: string;
829
+ ```javascript
830
+ class Order extends Model {
831
+ static table = 'orders';
832
+ static keyType = 'ulid';
833
+ static incrementing = false;
826
834
  }
827
835
 
828
- // Usage
829
- const user = await User.create({
830
- name: "John Doe",
831
- email: "john@example.com",
832
- });
833
- // user.id will be a generated UUID
836
+ const order = await Order.create({ total: 49.99 });
837
+ // order.id "01J3X7KQZB8YTPNMCHW4RSVFGE"
834
838
  ```
835
839
 
836
- ````
840
+ Use `char(26)` for the column type in migrations.
837
841
 
838
842
  ### Attribute Casting
839
843
 
@@ -4179,6 +4183,68 @@ const users = await User.query().select('id', 'name', 'email').values();
4179
4183
  // [{ id: 1, name: 'John', email: 'john@example.com' }, ...]
4180
4184
  ```
4181
4185
 
4186
+ ### Vector / Semantic Search (pgvector)
4187
+
4188
+ Semantic search matches by **meaning**, not exact keywords. It works by converting text into a list of numbers called an **embedding** using an AI embedding model, storing those numbers in the database, and finding records whose embeddings are mathematically closest to a search query.
4189
+
4190
+ **Requires:** PostgreSQL + pgvector extension. An external embedding model (OpenAI, Cohere, Ollama, etc.) is also required — IlanaORM does not include one. Does **not** work with MySQL or SQLite.
4191
+
4192
+ ```javascript
4193
+ // 1. Enable the extension in a migration
4194
+ await schema.enableVectorExtension();
4195
+
4196
+ // 2. Add a vector column
4197
+ await schema.table('posts', table => {
4198
+ table.specificType('embedding', 'vector(1536)'); // dimensions must match your model
4199
+ });
4200
+
4201
+ // 3. Configure the model
4202
+ class Post extends Model {
4203
+ static embeddingColumn = 'embedding';
4204
+ static embeddingProvider = async (text) => {
4205
+ // Must return Promise<number[]> — plug in any embedding API
4206
+ const res = await openai.embeddings.create({ model: 'text-embedding-ada-002', input: text });
4207
+ return res.data[0].embedding;
4208
+ };
4209
+ }
4210
+
4211
+ // 4. Store embeddings when creating records
4212
+ await Post.create({
4213
+ title: 'JavaScript tips',
4214
+ body: '...',
4215
+ embedding: JSON.stringify(await Post.embeddingProvider('JavaScript tips ...')),
4216
+ });
4217
+
4218
+ // 5. Search — converts text to vector, queries by similarity
4219
+ const posts = await Post.search('javascript performance tips', { limit: 5 });
4220
+
4221
+ // Or search by a raw vector you already have
4222
+ const posts = await Post.nearestTo(myVector, { distance: 'cosine', limit: 10 });
4223
+
4224
+ // Each result has a .distance attribute (lower = more similar)
4225
+ posts.forEach(p => console.log(p.title, p.distance));
4226
+ ```
4227
+
4228
+ Distance options: `'cosine'` (default, `<=>`), `'l2'` (`<->`), `'inner'` (`<#>`).
4229
+
4230
+ ### Edge Runtime
4231
+
4232
+ Import from `ilana-orm/edge` to skip the Node.js `fs`/`path` auto-loader. Required for Cloudflare Workers, Deno, Bun, and Next.js edge routes:
4233
+
4234
+ ```javascript
4235
+ import { Model, Database } from 'ilana-orm/edge';
4236
+
4237
+ // Must configure explicitly — no auto-loading of ilana.config.js
4238
+ Database.configure({
4239
+ default: 'pg',
4240
+ connections: {
4241
+ pg: { client: 'pg', connection: { connectionString: process.env.DATABASE_URL } },
4242
+ },
4243
+ });
4244
+
4245
+ const users = await User.all();
4246
+ ```
4247
+
4182
4248
  ## TypeScript Support
4183
4249
 
4184
4250
  ### Type-Safe Models
@@ -4231,6 +4297,32 @@ const user = await User.factory().create({
4231
4297
  });
4232
4298
  ```
4233
4299
 
4300
+ ## Supabase
4301
+
4302
+ Supabase is a hosted PostgreSQL platform. IlanaORM works with it out of the box using the `pg` driver — no special setup required.
4303
+
4304
+ ```javascript
4305
+ // ilana.config.js
4306
+ const { Database } = require('ilana-orm');
4307
+
4308
+ Database.configure({
4309
+ default: 'pg',
4310
+ connections: {
4311
+ pg: {
4312
+ client: 'pg',
4313
+ connection: {
4314
+ connectionString: process.env.DATABASE_URL, // from Supabase dashboard
4315
+ ssl: { rejectUnauthorized: false }, // required for Supabase
4316
+ },
4317
+ },
4318
+ },
4319
+ });
4320
+ ```
4321
+
4322
+ All features work including migrations, relations, soft deletes, and vector search (Supabase has pgvector built in).
4323
+
4324
+ For serverless or edge deployments with Supabase, use the [connection pooler URL](https://supabase.com/docs/guides/database/connecting-to-postgres) (port `6543`) and the `ilana-orm/edge` entry point for Cloudflare Workers or Next.js edge routes.
4325
+
4234
4326
  ## Performance & Best Practices
4235
4327
 
4236
4328
  ### Query Optimization
@@ -4410,10 +4502,14 @@ User.upsert(data, unique, update); // Upsert records
4410
4502
  User.destroy(ids); // Delete by IDs (soft-delete aware)
4411
4503
  User.truncate(); // Delete all rows in the table
4412
4504
  User.seed(n); // Create n records using the registered factory
4505
+ User.prune(); // Delete all records matching static prunable()
4413
4506
  User.withTrashed(); // Include soft deleted
4414
4507
  User.onlyTrashed(); // Only soft deleted
4415
4508
  User.withoutTrashed(); // Exclude soft deleted
4416
4509
 
4510
+ // Events
4511
+ User.withoutEvents(async () => { ... }); // Run without firing events
4512
+
4417
4513
  // Configuration
4418
4514
  User.getTableName(); // Get table name
4419
4515
  User.getPrimaryKey(); // Get primary key
@@ -4421,6 +4517,7 @@ User.getKeyType(); // Get key type
4421
4517
  User.getIncrementing(); // Get incrementing flag
4422
4518
  User.getConnectionName(); // Get connection name
4423
4519
  User.generateUuid(); // Generate UUID
4520
+ User.generateUlid(); // Generate ULID
4424
4521
 
4425
4522
  // Events
4426
4523
  User.creating(callback); // Before creating
@@ -4458,9 +4555,12 @@ user.forceDelete(); // Force delete (ignores softDeletes)
4458
4555
  user.restore(); // Restore soft deleted
4459
4556
  user.fresh(); // Re-fetch from DB and return new instance
4460
4557
  user.is(other); // Check if two instances are the same record
4558
+ user.isNot(other); // Inverse of is()
4559
+ user.replicate(except?); // Clone as unsaved record (excludes PK + timestamps)
4461
4560
 
4462
4561
  // Attributes
4463
4562
  user.fill(attributes); // Mass assign (respects fillable/guarded)
4563
+ user.forceFill(attributes); // Mass assign, bypassing fillable/guarded entirely
4464
4564
  user.getAttribute(key); // Get attribute (calls accessor if defined)
4465
4565
  user.setAttribute(key, value); // Set attribute (calls mutator if defined)
4466
4566
  user.getKey(); // Get primary key value
@@ -4642,9 +4742,10 @@ query.upsert(data, uniqueBy, update);
4642
4742
  query.with(...relations);
4643
4743
  query.withConstraints(relation, callback);
4644
4744
  query.withCount(...relations); // adds relation_count subquery column per model
4645
- query.whereHas(relation, callback); // WHERE EXISTS subquery
4646
- query.doesntHave(relation); // WHERE NOT EXISTS subquery
4647
- query.whereDoesntHave(relation, callback); // WHERE NOT EXISTS with constraint
4745
+ query.has(relation, operator?, count?); // WHERE EXISTS, or count-based (e.g. has('posts', '>', 5))
4746
+ query.whereHas(relation, callback); // WHERE EXISTS with constraint subquery
4747
+ query.doesntHave(relation); // WHERE NOT EXISTS subquery
4748
+ query.whereDoesntHave(relation, callback); // WHERE NOT EXISTS with constraint
4648
4749
  ```
4649
4750
 
4650
4751
  #### Locking
package/cli/ilana.js CHANGED
@@ -283,90 +283,20 @@ function getModelTemplate(className, tableName) {
283
283
 
284
284
  if (isTypeScriptProject()) {
285
285
  return `import Model from 'ilana-orm/orm/Model';
286
- // import { MoneyCast, EncryptedCast } from 'ilana-orm/orm/CustomCasts';
287
286
 
288
287
  export default class ${className} extends Model {
289
288
  protected static table = '${tableName}';
290
- protected static timestamps = true;
291
- protected static softDeletes = false;
292
-
293
- // For UUID primary keys, uncomment:
294
- // protected static keyType = 'string' as const;
295
- // protected static incrementing = false;
296
-
297
- protected fillable: string[] = [];
298
- protected hidden: string[] = [];
299
- protected appends: string[] = [];
300
- protected casts = {
301
- // Basic casts
302
- // is_active: 'boolean' as const,
303
- // metadata: 'json' as const,
304
- // tags: 'array' as const,
305
-
306
- // Custom casts
307
- // price: new MoneyCast(),
308
- // secret: new EncryptedCast('your-key'),
309
- };
310
-
311
- // Define relationships here
312
- // example() {
313
- // return this.hasMany(RelatedModel, 'foreign_key');
314
- // }
315
-
316
- // Define scopes here
317
- // static scopeActive(query: any) {
318
- // query.where('is_active', true);
319
- // }
320
-
321
- // Register for polymorphic relationships
322
- // static {
323
- // this.register();
324
- // }
289
+ protected static fillable: string[] = [];
325
290
  }
326
291
  `;
327
292
  }
328
293
 
329
294
  if (isESModule) {
330
295
  return `import Model from 'ilana-orm/orm/Model';
331
- // import { MoneyCast, EncryptedCast } from 'ilana-orm/orm/CustomCasts';
332
296
 
333
297
  class ${className} extends Model {
334
298
  static table = '${tableName}';
335
- static timestamps = true;
336
- static softDeletes = false;
337
-
338
- // For UUID primary keys, uncomment:
339
- // static keyType = 'string';
340
- // static incrementing = false;
341
-
342
- fillable = [];
343
- hidden = [];
344
- appends = [];
345
- casts = {
346
- // Basic casts
347
- // is_active: 'boolean',
348
- // metadata: 'json',
349
- // tags: 'array',
350
-
351
- // Custom casts
352
- // price: new MoneyCast(),
353
- // secret: new EncryptedCast('your-key'),
354
- };
355
-
356
- // Define relationships here
357
- // example() {
358
- // return this.hasMany(RelatedModel, 'foreign_key');
359
- // }
360
-
361
- // Define scopes here
362
- // static scopeActive(query) {
363
- // query.where('is_active', true);
364
- // }
365
-
366
- // Register for polymorphic relationships
367
- // static {
368
- // this.register();
369
- // }
299
+ static fillable = [];
370
300
  }
371
301
 
372
302
  export default ${className};
@@ -374,45 +304,10 @@ export default ${className};
374
304
  }
375
305
 
376
306
  return `const Model = require('ilana-orm/orm/Model');
377
- // const { MoneyCast, EncryptedCast } = require('ilana-orm/orm/CustomCasts');
378
307
 
379
308
  class ${className} extends Model {
380
309
  static table = '${tableName}';
381
- static timestamps = true;
382
- static softDeletes = false;
383
-
384
- // For UUID primary keys, uncomment:
385
- // static keyType = 'string';
386
- // static incrementing = false;
387
-
388
- fillable = [];
389
- hidden = [];
390
- appends = [];
391
- casts = {
392
- // Basic casts
393
- // is_active: 'boolean',
394
- // metadata: 'json',
395
- // tags: 'array',
396
-
397
- // Custom casts
398
- // price: new MoneyCast(),
399
- // secret: new EncryptedCast('your-key'),
400
- };
401
-
402
- // Define relationships here
403
- // example() {
404
- // return this.hasMany(RelatedModel, 'foreign_key');
405
- // }
406
-
407
- // Define scopes here
408
- // static scopeActive(query) {
409
- // query.where('is_active', true);
410
- // }
411
-
412
- // Register for polymorphic relationships
413
- // static {
414
- // this.register();
415
- // }
310
+ static fillable = [];
416
311
  }
417
312
 
418
313
  module.exports = ${className};
@@ -427,11 +322,7 @@ function getPivotModelTemplate(className, tableName) {
427
322
 
428
323
  export default class ${className} extends Model {
429
324
  protected static table = '${tableName}';
430
- protected static timestamps = true;
431
-
432
- protected fillable: string[] = [];
433
-
434
- // Define pivot relationships here
325
+ protected static fillable: string[] = [];
435
326
  }
436
327
  `;
437
328
  }
@@ -441,11 +332,7 @@ export default class ${className} extends Model {
441
332
 
442
333
  class ${className} extends Model {
443
334
  static table = '${tableName}';
444
- static timestamps = true;
445
-
446
- fillable = [];
447
-
448
- // Define pivot relationships here
335
+ static fillable = [];
449
336
  }
450
337
 
451
338
  export default ${className};
@@ -456,11 +343,7 @@ export default ${className};
456
343
 
457
344
  class ${className} extends Model {
458
345
  static table = '${tableName}';
459
- static timestamps = true;
460
-
461
- fillable = [];
462
-
463
- // Define pivot relationships here
346
+ static fillable = [];
464
347
  }
465
348
 
466
349
  module.exports = ${className};
@@ -477,29 +360,27 @@ function generateFactory(className) {
477
360
  }
478
361
 
479
362
  const modelPath = structure.hasSrc ? `../models/${className}.js` : `../../models/${className}.js`;
363
+ const isESModule = isESModuleProject();
480
364
  const template = isTypeScriptProject() ?
481
365
  `import { defineFactory } from 'ilana-orm/orm/Factory.js';
482
366
  import ${className} from '${modelPath}';
483
367
 
484
368
  export default defineFactory(${className}, (faker) => ({
485
- // Define your factory attributes here
486
369
  // name: faker.person.fullName(),
487
- // email: faker.internet.email(),
488
- }))
489
- .state('example', (faker) => ({
490
- // Define state modifications here
370
+ }));
371
+ ` : isESModule ?
372
+ `import { defineFactory } from 'ilana-orm/orm/Factory';
373
+ import ${className} from '${modelPath}';
374
+
375
+ export default defineFactory(${className}, (faker) => ({
376
+ // name: faker.person.fullName(),
491
377
  }));
492
378
  ` :
493
379
  `const { defineFactory } = require('ilana-orm/orm/Factory');
494
380
  const ${className} = require('${modelPath.replace('.js', '')}');
495
381
 
496
382
  module.exports = defineFactory(${className}, (faker) => ({
497
- // Define your factory attributes here
498
383
  // name: faker.person.fullName(),
499
- // email: faker.internet.email(),
500
- }))
501
- .state('example', (faker) => ({
502
- // Define state modifications here
503
384
  }));
504
385
  `;
505
386
 
@@ -517,34 +398,38 @@ function generateSeeder(className) {
517
398
  }
518
399
 
519
400
  const modelPath = structure.hasSrc ? `../models/${className}.js` : `../../models/${className}.js`;
401
+ const isESModule = isESModuleProject();
520
402
  const template = isTypeScriptProject() ?
521
403
  `import Seeder from 'ilana-orm/orm/Seeder.js';
404
+ import { factory } from 'ilana-orm/orm/Factory.js';
522
405
  import ${className} from '${modelPath}';
523
406
  import '../factories/${className}Factory.js';
524
407
 
525
408
  export default class ${className}Seeder extends Seeder {
526
409
  async run(): Promise<void> {
527
- console.log('Seeding ${className.toLowerCase()}s...');
528
-
529
- // Create sample data
530
- await ${className}.factory().times(10).create();
410
+ await factory(${className}).times(10).create();
411
+ }
412
+ }
413
+ ` : isESModule ?
414
+ `import Seeder from 'ilana-orm/orm/Seeder';
415
+ import { factory } from 'ilana-orm/orm/Factory';
416
+ import ${className} from '${modelPath}';
417
+ import '../factories/${className}Factory.js';
531
418
 
532
- console.log('${className}s seeded successfully');
419
+ export default class ${className}Seeder extends Seeder {
420
+ async run() {
421
+ await factory(${className}).times(10).create();
533
422
  }
534
423
  }
535
424
  ` :
536
425
  `const Seeder = require('ilana-orm/orm/Seeder');
426
+ const { factory } = require('ilana-orm/orm/Factory');
537
427
  const ${className} = require('${modelPath.replace('.js', '')}');
538
428
  require('../factories/${className}Factory');
539
429
 
540
430
  class ${className}Seeder extends Seeder {
541
431
  async run() {
542
- console.log('Seeding ${className.toLowerCase()}s...');
543
-
544
- // Create sample data
545
- await ${className}.factory().times(10).create();
546
-
547
- console.log('${className}s seeded successfully');
432
+ await factory(${className}).times(10).create();
548
433
  }
549
434
  }
550
435
 
@@ -1590,4 +1475,5 @@ if (require.main === module) {
1590
1475
  main();
1591
1476
  }
1592
1477
 
1593
- module.exports = commands;
1478
+ module.exports = commands;
1479
+ module.exports._templates = { getModelTemplate, getPivotModelTemplate };
@@ -207,6 +207,10 @@ class SchemaBuilder {
207
207
  return Promise.resolve();
208
208
  }
209
209
 
210
+ enableVectorExtension() {
211
+ return this.knex.raw('CREATE EXTENSION IF NOT EXISTS vector');
212
+ }
213
+
210
214
  createEnum(name, values) {
211
215
  if (this.knex.client.config.client === 'pg') {
212
216
  return this.knex.raw(`CREATE TYPE ${name} AS ENUM (${values.map(v => `'${v}'`).join(', ')})`);
package/index.d.ts CHANGED
@@ -22,5 +22,13 @@ export declare class ModelNotFoundException extends Error {
22
22
  toResponse(): { status: 404; message: string };
23
23
  }
24
24
 
25
+ export declare class MassAssignmentException extends Error {
26
+ name: 'MassAssignmentException';
27
+ model: string;
28
+ discardedKeys: string[];
29
+ constructor(model: string, discardedKeys: string[]);
30
+ toResponse(): { status: 422; message: string };
31
+ }
32
+
25
33
  // Default export
26
34
  export { default } from './orm/Model';
package/index.edge.js ADDED
@@ -0,0 +1,10 @@
1
+ // Edge runtime entry point — no auto-loading of ilana.config.js.
2
+ // Use this when running in Cloudflare Workers, Deno, Bun, or Next.js edge routes.
3
+ // You must call Database.configure(config) explicitly before using any models.
4
+ //
5
+ // import { Model, Database } from 'ilana-orm/edge';
6
+ // Database.configure({ default: 'pg', connections: { pg: { ... } } });
7
+
8
+ 'use strict';
9
+ global.__ILANA_EDGE__ = true;
10
+ module.exports = require('./index.js');
package/index.edge.mjs ADDED
@@ -0,0 +1,38 @@
1
+ // Edge runtime entry point — ESM wrapper.
2
+ // Sets __ILANA_EDGE__ before any model is loaded so the auto-loader is skipped.
3
+ import { createRequire } from 'module';
4
+ const require = createRequire(import.meta.url);
5
+
6
+ globalThis.__ILANA_EDGE__ = true;
7
+ const exports = require('./index.js');
8
+
9
+ export const {
10
+ Model,
11
+ QueryBuilder,
12
+ Collection,
13
+ Database,
14
+ DB,
15
+ SchemaBuilder,
16
+ MigrationRunner,
17
+ Seeder,
18
+ Factory,
19
+ defineFactory,
20
+ ModelNotFoundException,
21
+ MassAssignmentException,
22
+ F,
23
+ HasOne,
24
+ HasMany,
25
+ BelongsTo,
26
+ BelongsToMany,
27
+ HasManyThrough,
28
+ MorphTo,
29
+ MorphOne,
30
+ MorphMany,
31
+ MoneyCast,
32
+ EncryptedCast,
33
+ JsonCast,
34
+ ArrayCast,
35
+ DateCast,
36
+ } = exports;
37
+
38
+ export default exports.Model;
package/index.js CHANGED
@@ -10,7 +10,7 @@ const Seeder = require('./orm/Seeder');
10
10
  const Factory = require('./orm/Factory');
11
11
  const Relation = require('./orm/Relation');
12
12
  const CustomCasts = require('./orm/CustomCasts');
13
- const { ModelNotFoundException } = require('./orm/Errors');
13
+ const { ModelNotFoundException, MassAssignmentException } = require('./orm/Errors');
14
14
  const { F } = require('./orm/F');
15
15
 
16
16
  module.exports = {
@@ -25,6 +25,7 @@ module.exports = {
25
25
  Factory: Factory.Factory,
26
26
  defineFactory: Factory.defineFactory,
27
27
  ModelNotFoundException,
28
+ MassAssignmentException,
28
29
  F,
29
30
 
30
31
  // Relationships
package/index.mjs CHANGED
@@ -16,6 +16,7 @@ export const {
16
16
  Factory,
17
17
  defineFactory,
18
18
  ModelNotFoundException,
19
+ MassAssignmentException,
19
20
  F,
20
21
  Relation,
21
22
  HasOne,
package/orm/Errors.js CHANGED
@@ -15,4 +15,23 @@ class ModelNotFoundException extends Error {
15
15
  }
16
16
  }
17
17
 
18
- module.exports = { ModelNotFoundException };
18
+ class MassAssignmentException extends Error {
19
+ constructor(model, discardedKeys) {
20
+ const keys = discardedKeys.join(', ');
21
+ super(
22
+ `Mass assignment blocked on ${model}: [${keys}] are not fillable, so ${model}.preventsSilentlyDiscardingAttributes ` +
23
+ `stopped the call instead of dropping them silently. Add the intended column(s) to ${model}.fillable, adjust ` +
24
+ `${model}.guarded, or set the attribute(s) directly (e.g. instance.column = value) instead of going through fill()/update().`
25
+ );
26
+ this.name = 'MassAssignmentException';
27
+ this.model = model;
28
+ this.discardedKeys = discardedKeys;
29
+ if (Error.captureStackTrace) Error.captureStackTrace(this, MassAssignmentException);
30
+ }
31
+
32
+ toResponse() {
33
+ return { status: 422, message: this.message };
34
+ }
35
+ }
36
+
37
+ module.exports = { ModelNotFoundException, MassAssignmentException };
package/orm/Factory.js CHANGED
@@ -172,7 +172,7 @@ class Factory {
172
172
  const modelAttributes = this.makeRaw(attributes);
173
173
 
174
174
  const model = new this.model();
175
- model.fill(modelAttributes);
175
+ model.forceFill(modelAttributes);
176
176
 
177
177
  // Run afterMaking callbacks
178
178
  for (const callback of this._afterMakingCallbacks) {
@@ -364,7 +364,7 @@ class BulkFactory {
364
364
 
365
365
  for (let k = 0; k < batchData.length; k++) {
366
366
  const model = new this.model();
367
- model.fill({ ...batchData[k], id: insertedIds[k] });
367
+ model.forceFill({ ...batchData[k], id: insertedIds[k] });
368
368
  model.exists = true;
369
369
  results.push(model);
370
370
  }