ilana-orm 1.0.15 → 1.0.17

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
@@ -59,7 +59,7 @@ A fully-featured, Eloquent-style ORM for Node.js with automatic TypeScript suppo
59
59
  - Conditional queries with `when()`
60
60
  - Query scopes with automatic proxy support
61
61
  - JSON queries for PostgreSQL and MySQL
62
- - Date-specific queries (whereDate, whereMonth, whereYear)
62
+ - Date-specific queries (whereDate, whereMonth, whereYear, whereDay, whereTime) across all databases
63
63
 
64
64
  ### 🗄️ **Database Management**
65
65
 
@@ -899,8 +899,10 @@ const {
899
899
  } = require('ilana-orm/orm/CustomCasts');
900
900
 
901
901
  class Product extends Model {
902
+ // Cast class instances are called automatically — get() on read, set() on write
902
903
  casts = {
903
904
  price: new MoneyCast(),
905
+ // ⚠️ EncryptedCast uses base64 only — replace with real encryption for production
904
906
  secret_data: new EncryptedCast('your-encryption-key'),
905
907
  metadata: new JsonCast(),
906
908
  tags: new ArrayCast(),
@@ -908,15 +910,10 @@ class Product extends Model {
908
910
  };
909
911
  }
910
912
 
911
- // Define custom cast
912
- class MoneyCast {
913
- get(value) {
914
- return value ? parseFloat(value) / 100 : null;
915
- }
916
-
917
- set(value) {
918
- return value ? Math.round(value * 100) : null;
919
- }
913
+ // Custom cast — implement get(value) and set(value)
914
+ class SlugCast {
915
+ get(v) { return v; }
916
+ set(v) { return v.toLowerCase().replace(/\s+/g, '-'); }
920
917
  }
921
918
 
922
919
  // Generate custom cast with CLI
@@ -966,13 +963,17 @@ class MoneyCast {
966
963
 
967
964
  ### Mutators and Accessors
968
965
 
966
+ **Mutators** (`setXxxAttribute`) are called automatically whenever `setAttribute` runs — on direct assignment (`user.email = x`), via `fill()`, and via `update()`. They must return the transformed value.
967
+
968
+ **Accessors** (`getXxxAttribute`) are called automatically on direct property access (`user.full_name`) as long as the key exists in `appends`, `fillable`, or the model's `attributes`. List keys in `appends` to include them in `toJSON()` output.
969
+
969
970
  **JavaScript:**
970
971
  ```javascript
971
972
  class User extends Model {
972
- // Appended attributes (automatically included in JSON)
973
+ // Keys listed in appends appear in toJSON() and are accessible as properties
973
974
  appends = ['full_name', 'avatar_url'];
974
975
 
975
- // Mutator - transform data when setting
976
+ // Mutator called automatically on assignment / fill / update
976
977
  setPasswordAttribute(value) {
977
978
  return value ? bcrypt.hashSync(value, 10) : value;
978
979
  }
@@ -981,7 +982,7 @@ class User extends Model {
981
982
  return value ? value.toLowerCase().trim() : value;
982
983
  }
983
984
 
984
- // Accessor - transform data when getting
985
+ // Accessor called on direct property access and in toJSON()
985
986
  getFullNameAttribute() {
986
987
  return `${this.first_name} ${this.last_name}`;
987
988
  }
@@ -993,20 +994,22 @@ class User extends Model {
993
994
  }
994
995
  }
995
996
 
996
- // Usage
997
+ // Direct property access triggers the accessor
997
998
  const user = await User.find(1);
998
- console.log(user.toJSON());
999
- // Output includes: { id: 1, first_name: 'John', last_name: 'Doe', full_name: 'John Doe', avatar_url: '/images/default-avatar.png' }
999
+ console.log(user.full_name); // 'John Doe' — accessor called directly
1000
+ console.log(user.toJSON()); // includes full_name and avatar_url via appends
1001
+
1002
+ // Mutator is called automatically
1003
+ user.email = 'USER@EXAMPLE.COM';
1004
+ console.log(user.email); // 'user@example.com' — mutator ran on assignment
1000
1005
  ````
1001
1006
 
1002
1007
  **TypeScript:**
1003
1008
 
1004
1009
  ```typescript
1005
1010
  class User extends Model {
1006
- // Appended attributes (automatically included in JSON)
1007
1011
  protected appends: string[] = ["full_name", "avatar_url"];
1008
1012
 
1009
- // Mutator - transform data when setting
1010
1013
  setPasswordAttribute(value: string) {
1011
1014
  return value ? bcrypt.hashSync(value, 10) : value;
1012
1015
  }
@@ -1015,7 +1018,6 @@ class User extends Model {
1015
1018
  return value ? value.toLowerCase().trim() : value;
1016
1019
  }
1017
1020
 
1018
- // Accessor - transform data when getting
1019
1021
  getFullNameAttribute(): string {
1020
1022
  return `${this.first_name} ${this.last_name}`;
1021
1023
  }
@@ -1027,10 +1029,9 @@ class User extends Model {
1027
1029
  }
1028
1030
  }
1029
1031
 
1030
- // Usage
1031
1032
  const user = await User.find(1);
1032
- console.log(user.toJSON());
1033
- // Output includes: { id: 1, first_name: 'John', last_name: 'Doe', full_name: 'John Doe', avatar_url: '/images/default-avatar.png' }
1033
+ console.log(user.full_name); // accessor called directly
1034
+ console.log(user.toJSON()); // includes appended accessors
1034
1035
  ```
1035
1036
 
1036
1037
  ````
@@ -1621,6 +1622,31 @@ const users = await User.query()
1621
1622
  })
1622
1623
  .get();
1623
1624
 
1625
+ // Relation existence (WHERE EXISTS subquery, with optional constraint)
1626
+ const usersWithPosts = await User.query().whereHas('posts').get();
1627
+ const activeAuthors = await User.query()
1628
+ .whereHas('posts', (q) => q.where('published', true))
1629
+ .get();
1630
+ const usersWithoutPosts = await User.query().whereDoesntHave('posts').get();
1631
+
1632
+ // OR variants
1633
+ const users = await User.query()
1634
+ .whereNull('verified_at')
1635
+ .orWhereNull('deleted_at')
1636
+ .orWhereNotNull('banned_at')
1637
+ .orWhereIn('role', ['admin', 'mod'])
1638
+ .orWhereRaw('score > ?', [100])
1639
+ .get();
1640
+
1641
+ // Range
1642
+ const minors = await User.query().whereNotBetween('age', [18, 120]).get();
1643
+
1644
+ // Date helpers (DAY/TIME use MySQL-style raw SQL functions)
1645
+ const users = await User.query()
1646
+ .whereDay('created_at', 15)
1647
+ .whereTime('created_at', '>', '08:00:00')
1648
+ .get();
1649
+
1624
1650
  // Conditional queries
1625
1651
  const users = await User.query()
1626
1652
  .when(filters.role, (query, role) => {
@@ -1641,32 +1667,14 @@ const users = await User.query()
1641
1667
  .where("age", ">", 18)
1642
1668
  .get();
1643
1669
 
1644
- // Where with operator
1645
- const users = await User.query()
1646
- .where("age", ">=", 21)
1647
- .where("name", "like", "%john%")
1648
- .get();
1649
-
1650
- // Or where
1651
- const users = await User.query()
1652
- .where("role", "admin")
1653
- .orWhere("role", "moderator")
1654
- .get();
1655
-
1656
- // Where in
1670
+ // Where in / null / between
1657
1671
  const users = await User.query()
1658
1672
  .whereIn("role", ["admin", "editor", "author"])
1659
- .get();
1660
-
1661
- // Where null/not null
1662
- const users = await User.query()
1663
1673
  .whereNull("deleted_at")
1664
- .whereNotNull("email_verified_at")
1674
+ .whereBetween("age", [18, 65])
1675
+ .whereNotBetween("score", [0, 10])
1665
1676
  .get();
1666
1677
 
1667
- // Where between
1668
- const users = await User.query().whereBetween("age", [18, 65]).get();
1669
-
1670
1678
  // JSON queries (database-specific)
1671
1679
  const users = await User.query()
1672
1680
  .whereJsonContains("preferences", { theme: "dark" })
@@ -1678,23 +1686,21 @@ const users = await User.query()
1678
1686
  .whereDate("created_at", "2023-12-01")
1679
1687
  .whereMonth("created_at", 12)
1680
1688
  .whereYear("created_at", 2023)
1689
+ .whereDay("created_at", 15)
1690
+ .whereTime("created_at", ">", "08:00:00")
1681
1691
  .get();
1682
1692
 
1683
- // Exists queries
1684
- const users = await User.query()
1685
- .whereExists((query) => {
1686
- query.select("*").from("posts").whereRaw("posts.user_id = users.id");
1687
- })
1693
+ // Relation existence
1694
+ const authors = await User.query()
1695
+ .whereHas("posts", (q) => q.where("published", true))
1688
1696
  .get();
1697
+ const lurkers = await User.query().whereDoesntHave("posts").get();
1689
1698
 
1690
1699
  // Conditional queries
1691
1700
  const users = await User.query()
1692
1701
  .when(filters.role, (query, role) => {
1693
1702
  query.where("role", role);
1694
1703
  })
1695
- .when(filters.search, (query, search) => {
1696
- query.where("name", "like", `%${search}%`);
1697
- })
1698
1704
  .get();
1699
1705
  ```
1700
1706
 
@@ -1756,8 +1762,13 @@ const roleStats = await User.query()
1756
1762
  const users = await User.query()
1757
1763
  .orderBy('name')
1758
1764
  .orderBy('created_at', 'desc')
1765
+ .latest() // shorthand: orderBy('created_at', 'desc')
1766
+ .oldest() // shorthand: orderBy('created_at', 'asc')
1767
+ .inRandomOrder() // RAND() on MySQL, RANDOM() on PostgreSQL/SQLite
1759
1768
  .limit(10)
1760
1769
  .offset(20)
1770
+ .take(10).skip(20) // aliases for limit/offset
1771
+ .forPage(3, 15) // offset((3-1)*15).limit(15)
1761
1772
  .get();
1762
1773
  ````
1763
1774
 
@@ -1766,9 +1777,8 @@ const users = await User.query()
1766
1777
  ```typescript
1767
1778
  const users = await User.query()
1768
1779
  .orderBy("name")
1769
- .orderBy("created_at", "desc")
1770
- .limit(10)
1771
- .offset(20)
1780
+ .inRandomOrder()
1781
+ .forPage(2, 20)
1772
1782
  .get();
1773
1783
  ```
1774
1784
 
@@ -1927,13 +1937,26 @@ const roles = user.roles;
1927
1937
  roles.forEach((role) => {
1928
1938
  console.log(role.pivot.assigned_at);
1929
1939
  });
1940
+
1941
+ // Pivot management
1942
+ const rel = user.roles();
1943
+ await rel.attach(roleId);
1944
+ await rel.attach(roleId, { assigned_at: new Date() }); // with pivot attributes
1945
+ await rel.detach(roleId);
1946
+ await rel.detach(); // detach all
1947
+
1948
+ await rel.sync([1, 2, 3]); // detach all, then attach these IDs
1949
+ await rel.sync({ 1: { assigned_at: new Date() }, 2: {} }); // with pivot attrs per ID
1950
+
1951
+ await rel.toggle([1, 2]); // attach if missing, detach if present
1952
+
1953
+ await rel.updateExistingPivot(roleId, { assigned_at: new Date() }); // update pivot row
1930
1954
  ````
1931
1955
 
1932
1956
  **TypeScript:**
1933
1957
 
1934
1958
  ```typescript
1935
1959
  class User extends Model {
1936
- // User belongs to many roles
1937
1960
  roles() {
1938
1961
  return this.belongsToMany(Role, "user_roles", "user_id", "role_id")
1939
1962
  .withPivot("assigned_at", "assigned_by")
@@ -1941,73 +1964,75 @@ class User extends Model {
1941
1964
  }
1942
1965
  }
1943
1966
 
1944
- class Role extends Model {
1945
- // Role belongs to many users
1946
- users() {
1947
- return this.belongsToMany(User, "user_roles", "role_id", "user_id");
1948
- }
1949
- }
1950
-
1951
- // Usage
1952
- const user = await User.with("roles").first();
1953
- const roles = user.roles;
1954
-
1955
1967
  // Access pivot data
1956
- roles.forEach((role) => {
1968
+ const user = await User.with("roles").first();
1969
+ user.roles.forEach((role) => {
1957
1970
  console.log(role.pivot.assigned_at);
1958
1971
  });
1972
+
1973
+ // Pivot management
1974
+ const rel = user.roles();
1975
+ await rel.sync([1, 2, 3]);
1976
+ await rel.toggle(roleId);
1977
+ await rel.updateExistingPivot(roleId, { assigned_at: new Date() });
1959
1978
  ```
1960
1979
 
1961
1980
  ````
1962
1981
 
1963
1982
  ### Polymorphic Relationships
1964
1983
 
1984
+ Use string names for related models to avoid circular import issues. Every model involved in a polymorphic relation must call `static { this.register(); }`.
1985
+
1965
1986
  **JavaScript:**
1966
1987
  ```javascript
1967
- class Comment extends Model {
1968
- // Comment can belong to Post or Video
1969
- commentable() {
1970
- return this.morphTo('commentable');
1988
+ // One-to-one polymorphic (morphOne / morphTo)
1989
+ class Image extends Model {
1990
+ imageable() {
1991
+ return this.morphTo('imageable'); // reads imageable_type, imageable_id
1971
1992
  }
1993
+ static { this.register(); }
1972
1994
  }
1973
1995
 
1974
1996
  class Post extends Model {
1975
- // Post has many comments (polymorphic)
1976
- comments() {
1977
- return this.morphMany(Comment, 'commentable');
1997
+ image() {
1998
+ return this.morphOne('Image', 'imageable'); // stores imageable_type='Post', imageable_id=post.id
1978
1999
  }
2000
+ static { this.register(); }
2001
+ }
2002
+
2003
+ // One-to-many polymorphic (morphMany / morphTo)
2004
+ class Comment extends Model {
2005
+ commentable() {
2006
+ return this.morphTo('commentable');
2007
+ }
2008
+ static { this.register(); }
1979
2009
  }
1980
2010
 
1981
2011
  class Video extends Model {
1982
- // Video has many comments (polymorphic)
1983
2012
  comments() {
1984
- return this.morphMany(Comment, 'commentable');
2013
+ return this.morphMany('Comment', 'commentable');
1985
2014
  }
2015
+ static { this.register(); }
1986
2016
  }
1987
2017
  ````
1988
2018
 
1989
2019
  **TypeScript:**
1990
2020
 
1991
2021
  ```typescript
1992
- class Comment extends Model {
1993
- // Comment can belong to Post or Video
1994
- commentable() {
1995
- return this.morphTo("commentable");
1996
- }
2022
+ class Image extends Model {
2023
+ imageable() { return this.morphTo("imageable"); }
2024
+ static { this.register(); }
1997
2025
  }
1998
2026
 
1999
2027
  class Post extends Model {
2000
- // Post has many comments (polymorphic)
2001
- comments() {
2002
- return this.morphMany(Comment, "commentable");
2003
- }
2028
+ image() { return this.morphOne("Image", "imageable"); }
2029
+ comments() { return this.morphMany("Comment", "commentable"); }
2030
+ static { this.register(); }
2004
2031
  }
2005
2032
 
2006
- class Video extends Model {
2007
- // Video has many comments (polymorphic)
2008
- comments() {
2009
- return this.morphMany(Comment, "commentable");
2010
- }
2033
+ class Comment extends Model {
2034
+ commentable() { return this.morphTo("commentable"); }
2035
+ static { this.register(); }
2011
2036
  }
2012
2037
  ```
2013
2038
 
@@ -3317,9 +3342,9 @@ export default defineFactory(User, () => ({
3317
3342
  email_verified_at: faker.date.past(),
3318
3343
  phone_verified_at: faker.date.past(),
3319
3344
  }))
3320
- .state("with_profile", () => ({}))
3321
- .afterCreating(async (user, evaluator) => {
3322
- if (evaluator.hasState("with_profile")) {
3345
+ .state("with_profile", () => ({ _with_profile: true }))
3346
+ .afterCreating(async (user) => {
3347
+ if (user.attributes._with_profile) {
3323
3348
  await user.profile().create({
3324
3349
  bio: faker.lorem.paragraph(),
3325
3350
  website: faker.internet.url(),
@@ -4079,9 +4104,9 @@ class User extends Model {
4079
4104
  // Hide sensitive data
4080
4105
  protected hidden = ["password", "remember_token"];
4081
4106
 
4082
- // Use mutators for sensitive data
4107
+ // Use mutators for sensitive data — mutators must return the transformed value
4083
4108
  setPasswordAttribute(value: string) {
4084
- this.attributes.password = bcrypt.hashSync(value, 10);
4109
+ return bcrypt.hashSync(value, 10);
4085
4110
  }
4086
4111
  }
4087
4112
 
@@ -4255,8 +4280,8 @@ User.fireEvent(event, model); // Fire event
4255
4280
  // Scopes
4256
4281
  User.addGlobalScope(name, scope); // Add global scope
4257
4282
  User.removeGlobalScope(name); // Remove global scope
4258
- User.withoutGlobalScope(name); // Query without scope
4259
- User.withoutGlobalScope(name); // Query without scope
4283
+ User.withoutGlobalScope(name); // Query without one named scope
4284
+ User.withoutGlobalScopes(); // Query bypassing all global scopes
4260
4285
 
4261
4286
  // Factory
4262
4287
  User.factory(); // Get factory instance
@@ -4270,15 +4295,13 @@ User.register(); // Register in registry
4270
4295
  user.save(); // Save model
4271
4296
  user.update(attributes); // Update model
4272
4297
  user.delete(); // Delete model
4273
- user.forceDelete(); // Force delete
4274
- user.restore(); // Restore soft deleted
4275
- user.forceDelete(); // Force delete
4298
+ user.forceDelete(); // Force delete (ignores softDeletes)
4276
4299
  user.restore(); // Restore soft deleted
4277
4300
 
4278
4301
  // Attributes
4279
- user.fill(attributes); // Mass assign
4280
- user.getAttribute(key); // Get attribute
4281
- user.setAttribute(key, value); // Set attribute
4302
+ user.fill(attributes); // Mass assign (respects fillable/guarded)
4303
+ user.getAttribute(key); // Get attribute (calls accessor if defined)
4304
+ user.setAttribute(key, value); // Set attribute (calls mutator if defined)
4282
4305
  user.getKey(); // Get primary key value
4283
4306
  user.isFillable(key); // Check if fillable
4284
4307
  user.isDirty(key); // Check if dirty
@@ -4288,7 +4311,9 @@ user.syncOriginal(); // Sync original
4288
4311
  user.only(keys); // Get only specified attributes
4289
4312
  user.except(keys); // Get all except specified
4290
4313
  user.trashed(); // Check if soft deleted
4291
- user.trashed(); // Check if soft deleted
4314
+ user.makeHidden(keys); // Add keys to hidden list
4315
+ user.makeVisible(keys); // Remove keys from hidden list
4316
+ user.append(keys); // Add accessor keys to appends
4292
4317
 
4293
4318
  // Relations
4294
4319
  user.load(...relations); // Lazy load relations
@@ -4314,8 +4339,12 @@ query.where(column, value); // Equals operator
4314
4339
  query.orWhere(column, operator, value);
4315
4340
  query.whereIn(column, values);
4316
4341
  query.whereNotIn(column, values);
4342
+ query.orWhereIn(column, values);
4343
+ query.orWhereNotIn(column, values);
4317
4344
  query.whereNull(column);
4318
4345
  query.whereNotNull(column);
4346
+ query.orWhereNull(column);
4347
+ query.orWhereNotNull(column);
4319
4348
  query.whereBetween(column, [min, max]);
4320
4349
  query.whereNotBetween(column, [min, max]);
4321
4350
  query.whereRaw(sql, bindings);
@@ -4357,6 +4386,7 @@ query.innerJoin(table, first, operator, second);
4357
4386
 
4358
4387
  ```javascript
4359
4388
  query.orderBy(column, direction);
4389
+ query.orderByRaw(sql);
4360
4390
  query.latest(column);
4361
4391
  query.oldest(column);
4362
4392
  query.inRandomOrder();
@@ -4438,10 +4468,10 @@ query.upsert(data, uniqueBy, update);
4438
4468
  ```javascript
4439
4469
  query.with(...relations);
4440
4470
  query.withConstraints(relation, callback);
4441
- query.withCount(...relations);
4442
- query.whereHas(relation, callback);
4443
- query.whereDoesntHave(relation);
4444
- query.has(relation, operator, count);
4471
+ query.withCount(...relations); // adds relation_count subquery column per model
4472
+ query.whereHas(relation, callback); // WHERE EXISTS subquery
4473
+ query.doesntHave(relation); // WHERE NOT EXISTS subquery
4474
+ query.whereDoesntHave(relation, callback); // WHERE NOT EXISTS with constraint
4445
4475
  ```
4446
4476
 
4447
4477
  #### Locking
@@ -4489,6 +4519,7 @@ this.hasManyThrough(
4489
4519
 
4490
4520
  // Polymorphic
4491
4521
  this.morphTo(morphType, morphId);
4522
+ this.morphOne(related, morphType, morphId);
4492
4523
  this.morphMany(related, morphType, morphId);
4493
4524
  ```
4494
4525