ilana-orm 1.0.19 → 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
 
@@ -718,6 +730,11 @@ class User extends Model {
718
730
  fillable = ["name", "email", "password"];
719
731
  guarded = ["id", "created_at", "updated_at"];
720
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
+
721
738
  // Hidden attributes (won't appear in JSON)
722
739
  hidden = ["password", "remember_token"];
723
740
 
@@ -4543,6 +4560,7 @@ user.replicate(except?); // Clone as unsaved record (excludes PK + timestamps)
4543
4560
 
4544
4561
  // Attributes
4545
4562
  user.fill(attributes); // Mass assign (respects fillable/guarded)
4563
+ user.forceFill(attributes); // Mass assign, bypassing fillable/guarded entirely
4546
4564
  user.getAttribute(key); // Get attribute (calls accessor if defined)
4547
4565
  user.setAttribute(key, value); // Set attribute (calls mutator if defined)
4548
4566
  user.getKey(); // Get primary key value
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.mjs CHANGED
@@ -18,6 +18,7 @@ export const {
18
18
  Factory,
19
19
  defineFactory,
20
20
  ModelNotFoundException,
21
+ MassAssignmentException,
21
22
  F,
22
23
  HasOne,
23
24
  HasMany,
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
  }
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 {
@@ -51,6 +52,7 @@ 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[] };
56
58
  static embeddingColumn: string;
@@ -75,6 +77,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
75
77
 
76
78
  // Static methods
77
79
  static register(): void;
80
+ static _autoRegister(): void;
78
81
  static resolveRelatedModel(related: string | typeof Model): typeof Model;
79
82
  static query(): QueryBuilder;
80
83
  static with(...relations: string[]): QueryBuilder;
@@ -112,6 +115,113 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
112
115
  static firstOrNew(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
113
116
  static updateOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
114
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
+
115
225
  // Scopes
116
226
  static addGlobalScope(name: string, scope: (query: QueryBuilder) => void): void;
117
227
  static removeGlobalScope(name: string): void;
@@ -142,6 +252,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
142
252
  // Instance methods
143
253
  getKey(): any;
144
254
  fill(attributes: ModelAttributes): this;
255
+ forceFill(attributes: ModelAttributes): this;
145
256
  load(...relations: string[]): Promise<this>;
146
257
  loadMissing(...relations: string[]): Promise<this>;
147
258
  getRelation(key: string): any;
package/orm/Model.js CHANGED
@@ -3,6 +3,7 @@ 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
+ const { MassAssignmentException } = require('./Errors');
6
7
 
7
8
  // Auto-load configuration on first import (skipped in edge runtime)
8
9
  if (typeof process !== 'undefined' && process.versions && process.versions.node && !global.__ILANA_EDGE__) {
@@ -38,6 +39,7 @@ class Model {
38
39
  static appends = [];
39
40
  static timezone = 'UTC';
40
41
  static strictLoading = false;
42
+ static preventsSilentlyDiscardingAttributes = false;
41
43
  static touches = [];
42
44
  static enums = {};
43
45
  static embeddingColumn = 'embedding';
@@ -57,6 +59,8 @@ class Model {
57
59
  _deferred;
58
60
 
59
61
  constructor(attrs = {}) {
62
+ this.constructor._autoRegister();
63
+
60
64
  // instance-level fillable/guarded/casts
61
65
  this.fillable = Array.isArray(this.fillable) && this.fillable.length
62
66
  ? this.fillable
@@ -168,10 +172,21 @@ class Model {
168
172
  ModelRegistry.register(this.name, this);
169
173
  }
170
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
+
171
186
  static resolveRelatedModel(related) {
172
187
  if (typeof related === 'string') {
173
188
  const cls = ModelRegistry.get(related);
174
- 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.`);
175
190
  return cls;
176
191
  }
177
192
 
@@ -193,6 +208,7 @@ class Model {
193
208
 
194
209
  // --- Query builder ---
195
210
  static query() {
211
+ this._autoRegister();
196
212
 
197
213
  const qb = new QueryBuilder(this.getTableName(), this, this.getConnectionName());
198
214
 
@@ -447,8 +463,29 @@ class Model {
447
463
  getKey() { return this.attributes[this.constructor.primaryKey]; }
448
464
 
449
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) {
450
488
  for (const [k, v] of Object.entries(attrs)) {
451
- if (!this.isFillable(k)) continue;
452
489
  this.setAttribute(k, v);
453
490
  }
454
491
  return this;
@@ -909,4 +946,21 @@ class Model {
909
946
  }
910
947
  }
911
948
 
912
- 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);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ilana-orm",
3
- "version": "1.0.19",
4
- "description": "A fully-featured, Eloquent-style ORM for Node.js with TypeScript support",
3
+ "version": "1.0.20",
4
+ "description": "A fully-featured, Laravel Eloquent-style ORM for Node.js & TypeScript. MySQL, PostgreSQL, SQLite, Supabase, edge runtimes, pgvector AI search.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "exports": {
@@ -80,18 +80,30 @@
80
80
  },
81
81
  "keywords": [
82
82
  "orm",
83
+ "eloquent",
84
+ "laravel",
85
+ "active-record",
83
86
  "nodejs",
84
87
  "javascript",
85
- "typescript-orm",
86
88
  "typescript",
89
+ "typescript-orm",
87
90
  "database",
88
- "eloquent",
91
+ "query-builder",
92
+ "migrations",
93
+ "relationships",
89
94
  "mysql",
90
95
  "postgresql",
91
96
  "sqlite",
92
- "query-builder",
93
- "migrations",
94
- "relationships"
97
+ "supabase",
98
+ "pgvector",
99
+ "vector-search",
100
+ "edge-runtime",
101
+ "cloudflare-workers",
102
+ "nextjs",
103
+ "ulid",
104
+ "knex",
105
+ "factory",
106
+ "seeder"
95
107
  ],
96
108
  "author": "Raphael Abayomi <raphyabak@gmail.com>",
97
109
  "license": "MIT",