ilana-orm 1.0.14 → 1.0.16

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
 
@@ -104,17 +104,23 @@ A fully-featured, Eloquent-style ORM for Node.js with automatic TypeScript suppo
104
104
  ## Installation
105
105
 
106
106
  ```bash
107
- # npm
108
107
  npm install ilana-orm
108
+ ```
109
109
 
110
- # yarn
111
- yarn add ilana-orm
110
+ ### Database Drivers
112
111
 
113
- # pnpm
114
- pnpm add ilana-orm
115
- ```
112
+ Install only the database driver you need:
113
+
114
+ ```bash
115
+ # PostgreSQL
116
+ npm install pg
116
117
 
117
- **All database drivers (PostgreSQL, MySQL, SQLite) and dotenv are included by default** - no additional installation required!
118
+ # MySQL
119
+ npm install mysql2
120
+
121
+ # SQLite
122
+ npm install sqlite3
123
+ ```
118
124
 
119
125
  ## Quick Start
120
126
 
@@ -893,8 +899,10 @@ const {
893
899
  } = require('ilana-orm/orm/CustomCasts');
894
900
 
895
901
  class Product extends Model {
902
+ // Cast class instances are called automatically — get() on read, set() on write
896
903
  casts = {
897
904
  price: new MoneyCast(),
905
+ // ⚠️ EncryptedCast uses base64 only — replace with real encryption for production
898
906
  secret_data: new EncryptedCast('your-encryption-key'),
899
907
  metadata: new JsonCast(),
900
908
  tags: new ArrayCast(),
@@ -902,15 +910,10 @@ class Product extends Model {
902
910
  };
903
911
  }
904
912
 
905
- // Define custom cast
906
- class MoneyCast {
907
- get(value) {
908
- return value ? parseFloat(value) / 100 : null;
909
- }
910
-
911
- set(value) {
912
- return value ? Math.round(value * 100) : null;
913
- }
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, '-'); }
914
917
  }
915
918
 
916
919
  // Generate custom cast with CLI
@@ -960,13 +963,17 @@ class MoneyCast {
960
963
 
961
964
  ### Mutators and Accessors
962
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
+
963
970
  **JavaScript:**
964
971
  ```javascript
965
972
  class User extends Model {
966
- // Appended attributes (automatically included in JSON)
973
+ // Keys listed in appends appear in toJSON() and are accessible as properties
967
974
  appends = ['full_name', 'avatar_url'];
968
975
 
969
- // Mutator - transform data when setting
976
+ // Mutator called automatically on assignment / fill / update
970
977
  setPasswordAttribute(value) {
971
978
  return value ? bcrypt.hashSync(value, 10) : value;
972
979
  }
@@ -975,7 +982,7 @@ class User extends Model {
975
982
  return value ? value.toLowerCase().trim() : value;
976
983
  }
977
984
 
978
- // Accessor - transform data when getting
985
+ // Accessor called on direct property access and in toJSON()
979
986
  getFullNameAttribute() {
980
987
  return `${this.first_name} ${this.last_name}`;
981
988
  }
@@ -987,20 +994,22 @@ class User extends Model {
987
994
  }
988
995
  }
989
996
 
990
- // Usage
997
+ // Direct property access triggers the accessor
991
998
  const user = await User.find(1);
992
- console.log(user.toJSON());
993
- // 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
994
1005
  ````
995
1006
 
996
1007
  **TypeScript:**
997
1008
 
998
1009
  ```typescript
999
1010
  class User extends Model {
1000
- // Appended attributes (automatically included in JSON)
1001
1011
  protected appends: string[] = ["full_name", "avatar_url"];
1002
1012
 
1003
- // Mutator - transform data when setting
1004
1013
  setPasswordAttribute(value: string) {
1005
1014
  return value ? bcrypt.hashSync(value, 10) : value;
1006
1015
  }
@@ -1009,7 +1018,6 @@ class User extends Model {
1009
1018
  return value ? value.toLowerCase().trim() : value;
1010
1019
  }
1011
1020
 
1012
- // Accessor - transform data when getting
1013
1021
  getFullNameAttribute(): string {
1014
1022
  return `${this.first_name} ${this.last_name}`;
1015
1023
  }
@@ -1021,10 +1029,9 @@ class User extends Model {
1021
1029
  }
1022
1030
  }
1023
1031
 
1024
- // Usage
1025
1032
  const user = await User.find(1);
1026
- console.log(user.toJSON());
1027
- // 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
1028
1035
  ```
1029
1036
 
1030
1037
  ````
@@ -1615,6 +1622,31 @@ const users = await User.query()
1615
1622
  })
1616
1623
  .get();
1617
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
+
1618
1650
  // Conditional queries
1619
1651
  const users = await User.query()
1620
1652
  .when(filters.role, (query, role) => {
@@ -1635,32 +1667,14 @@ const users = await User.query()
1635
1667
  .where("age", ">", 18)
1636
1668
  .get();
1637
1669
 
1638
- // Where with operator
1639
- const users = await User.query()
1640
- .where("age", ">=", 21)
1641
- .where("name", "like", "%john%")
1642
- .get();
1643
-
1644
- // Or where
1645
- const users = await User.query()
1646
- .where("role", "admin")
1647
- .orWhere("role", "moderator")
1648
- .get();
1649
-
1650
- // Where in
1670
+ // Where in / null / between
1651
1671
  const users = await User.query()
1652
1672
  .whereIn("role", ["admin", "editor", "author"])
1653
- .get();
1654
-
1655
- // Where null/not null
1656
- const users = await User.query()
1657
1673
  .whereNull("deleted_at")
1658
- .whereNotNull("email_verified_at")
1674
+ .whereBetween("age", [18, 65])
1675
+ .whereNotBetween("score", [0, 10])
1659
1676
  .get();
1660
1677
 
1661
- // Where between
1662
- const users = await User.query().whereBetween("age", [18, 65]).get();
1663
-
1664
1678
  // JSON queries (database-specific)
1665
1679
  const users = await User.query()
1666
1680
  .whereJsonContains("preferences", { theme: "dark" })
@@ -1672,23 +1686,21 @@ const users = await User.query()
1672
1686
  .whereDate("created_at", "2023-12-01")
1673
1687
  .whereMonth("created_at", 12)
1674
1688
  .whereYear("created_at", 2023)
1689
+ .whereDay("created_at", 15)
1690
+ .whereTime("created_at", ">", "08:00:00")
1675
1691
  .get();
1676
1692
 
1677
- // Exists queries
1678
- const users = await User.query()
1679
- .whereExists((query) => {
1680
- query.select("*").from("posts").whereRaw("posts.user_id = users.id");
1681
- })
1693
+ // Relation existence
1694
+ const authors = await User.query()
1695
+ .whereHas("posts", (q) => q.where("published", true))
1682
1696
  .get();
1697
+ const lurkers = await User.query().whereDoesntHave("posts").get();
1683
1698
 
1684
1699
  // Conditional queries
1685
1700
  const users = await User.query()
1686
1701
  .when(filters.role, (query, role) => {
1687
1702
  query.where("role", role);
1688
1703
  })
1689
- .when(filters.search, (query, search) => {
1690
- query.where("name", "like", `%${search}%`);
1691
- })
1692
1704
  .get();
1693
1705
  ```
1694
1706
 
@@ -1750,8 +1762,13 @@ const roleStats = await User.query()
1750
1762
  const users = await User.query()
1751
1763
  .orderBy('name')
1752
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
1753
1768
  .limit(10)
1754
1769
  .offset(20)
1770
+ .take(10).skip(20) // aliases for limit/offset
1771
+ .forPage(3, 15) // offset((3-1)*15).limit(15)
1755
1772
  .get();
1756
1773
  ````
1757
1774
 
@@ -1760,9 +1777,8 @@ const users = await User.query()
1760
1777
  ```typescript
1761
1778
  const users = await User.query()
1762
1779
  .orderBy("name")
1763
- .orderBy("created_at", "desc")
1764
- .limit(10)
1765
- .offset(20)
1780
+ .inRandomOrder()
1781
+ .forPage(2, 20)
1766
1782
  .get();
1767
1783
  ```
1768
1784
 
@@ -1921,13 +1937,26 @@ const roles = user.roles;
1921
1937
  roles.forEach((role) => {
1922
1938
  console.log(role.pivot.assigned_at);
1923
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
1924
1954
  ````
1925
1955
 
1926
1956
  **TypeScript:**
1927
1957
 
1928
1958
  ```typescript
1929
1959
  class User extends Model {
1930
- // User belongs to many roles
1931
1960
  roles() {
1932
1961
  return this.belongsToMany(Role, "user_roles", "user_id", "role_id")
1933
1962
  .withPivot("assigned_at", "assigned_by")
@@ -1935,73 +1964,75 @@ class User extends Model {
1935
1964
  }
1936
1965
  }
1937
1966
 
1938
- class Role extends Model {
1939
- // Role belongs to many users
1940
- users() {
1941
- return this.belongsToMany(User, "user_roles", "role_id", "user_id");
1942
- }
1943
- }
1944
-
1945
- // Usage
1946
- const user = await User.with("roles").first();
1947
- const roles = user.roles;
1948
-
1949
1967
  // Access pivot data
1950
- roles.forEach((role) => {
1968
+ const user = await User.with("roles").first();
1969
+ user.roles.forEach((role) => {
1951
1970
  console.log(role.pivot.assigned_at);
1952
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() });
1953
1978
  ```
1954
1979
 
1955
1980
  ````
1956
1981
 
1957
1982
  ### Polymorphic Relationships
1958
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
+
1959
1986
  **JavaScript:**
1960
1987
  ```javascript
1961
- class Comment extends Model {
1962
- // Comment can belong to Post or Video
1963
- commentable() {
1964
- 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
1965
1992
  }
1993
+ static { this.register(); }
1966
1994
  }
1967
1995
 
1968
1996
  class Post extends Model {
1969
- // Post has many comments (polymorphic)
1970
- comments() {
1971
- return this.morphMany(Comment, 'commentable');
1997
+ image() {
1998
+ return this.morphOne('Image', 'imageable'); // stores imageable_type='Post', imageable_id=post.id
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');
1972
2007
  }
2008
+ static { this.register(); }
1973
2009
  }
1974
2010
 
1975
2011
  class Video extends Model {
1976
- // Video has many comments (polymorphic)
1977
2012
  comments() {
1978
- return this.morphMany(Comment, 'commentable');
2013
+ return this.morphMany('Comment', 'commentable');
1979
2014
  }
2015
+ static { this.register(); }
1980
2016
  }
1981
2017
  ````
1982
2018
 
1983
2019
  **TypeScript:**
1984
2020
 
1985
2021
  ```typescript
1986
- class Comment extends Model {
1987
- // Comment can belong to Post or Video
1988
- commentable() {
1989
- return this.morphTo("commentable");
1990
- }
2022
+ class Image extends Model {
2023
+ imageable() { return this.morphTo("imageable"); }
2024
+ static { this.register(); }
1991
2025
  }
1992
2026
 
1993
2027
  class Post extends Model {
1994
- // Post has many comments (polymorphic)
1995
- comments() {
1996
- return this.morphMany(Comment, "commentable");
1997
- }
2028
+ image() { return this.morphOne("Image", "imageable"); }
2029
+ comments() { return this.morphMany("Comment", "commentable"); }
2030
+ static { this.register(); }
1998
2031
  }
1999
2032
 
2000
- class Video extends Model {
2001
- // Video has many comments (polymorphic)
2002
- comments() {
2003
- return this.morphMany(Comment, "commentable");
2004
- }
2033
+ class Comment extends Model {
2034
+ commentable() { return this.morphTo("commentable"); }
2035
+ static { this.register(); }
2005
2036
  }
2006
2037
  ```
2007
2038
 
@@ -3311,9 +3342,9 @@ export default defineFactory(User, () => ({
3311
3342
  email_verified_at: faker.date.past(),
3312
3343
  phone_verified_at: faker.date.past(),
3313
3344
  }))
3314
- .state("with_profile", () => ({}))
3315
- .afterCreating(async (user, evaluator) => {
3316
- if (evaluator.hasState("with_profile")) {
3345
+ .state("with_profile", () => ({ _with_profile: true }))
3346
+ .afterCreating(async (user) => {
3347
+ if (user.attributes._with_profile) {
3317
3348
  await user.profile().create({
3318
3349
  bio: faker.lorem.paragraph(),
3319
3350
  website: faker.internet.url(),
@@ -4073,9 +4104,9 @@ class User extends Model {
4073
4104
  // Hide sensitive data
4074
4105
  protected hidden = ["password", "remember_token"];
4075
4106
 
4076
- // Use mutators for sensitive data
4107
+ // Use mutators for sensitive data — mutators must return the transformed value
4077
4108
  setPasswordAttribute(value: string) {
4078
- this.attributes.password = bcrypt.hashSync(value, 10);
4109
+ return bcrypt.hashSync(value, 10);
4079
4110
  }
4080
4111
  }
4081
4112
 
@@ -4249,8 +4280,8 @@ User.fireEvent(event, model); // Fire event
4249
4280
  // Scopes
4250
4281
  User.addGlobalScope(name, scope); // Add global scope
4251
4282
  User.removeGlobalScope(name); // Remove global scope
4252
- User.withoutGlobalScope(name); // Query without scope
4253
- User.withoutGlobalScope(name); // Query without scope
4283
+ User.withoutGlobalScope(name); // Query without one named scope
4284
+ User.withoutGlobalScopes(); // Query bypassing all global scopes
4254
4285
 
4255
4286
  // Factory
4256
4287
  User.factory(); // Get factory instance
@@ -4264,15 +4295,13 @@ User.register(); // Register in registry
4264
4295
  user.save(); // Save model
4265
4296
  user.update(attributes); // Update model
4266
4297
  user.delete(); // Delete model
4267
- user.forceDelete(); // Force delete
4268
- user.restore(); // Restore soft deleted
4269
- user.forceDelete(); // Force delete
4298
+ user.forceDelete(); // Force delete (ignores softDeletes)
4270
4299
  user.restore(); // Restore soft deleted
4271
4300
 
4272
4301
  // Attributes
4273
- user.fill(attributes); // Mass assign
4274
- user.getAttribute(key); // Get attribute
4275
- 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)
4276
4305
  user.getKey(); // Get primary key value
4277
4306
  user.isFillable(key); // Check if fillable
4278
4307
  user.isDirty(key); // Check if dirty
@@ -4282,7 +4311,9 @@ user.syncOriginal(); // Sync original
4282
4311
  user.only(keys); // Get only specified attributes
4283
4312
  user.except(keys); // Get all except specified
4284
4313
  user.trashed(); // Check if soft deleted
4285
- 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
4286
4317
 
4287
4318
  // Relations
4288
4319
  user.load(...relations); // Lazy load relations
@@ -4308,8 +4339,12 @@ query.where(column, value); // Equals operator
4308
4339
  query.orWhere(column, operator, value);
4309
4340
  query.whereIn(column, values);
4310
4341
  query.whereNotIn(column, values);
4342
+ query.orWhereIn(column, values);
4343
+ query.orWhereNotIn(column, values);
4311
4344
  query.whereNull(column);
4312
4345
  query.whereNotNull(column);
4346
+ query.orWhereNull(column);
4347
+ query.orWhereNotNull(column);
4313
4348
  query.whereBetween(column, [min, max]);
4314
4349
  query.whereNotBetween(column, [min, max]);
4315
4350
  query.whereRaw(sql, bindings);
@@ -4351,6 +4386,7 @@ query.innerJoin(table, first, operator, second);
4351
4386
 
4352
4387
  ```javascript
4353
4388
  query.orderBy(column, direction);
4389
+ query.orderByRaw(sql);
4354
4390
  query.latest(column);
4355
4391
  query.oldest(column);
4356
4392
  query.inRandomOrder();
@@ -4432,10 +4468,10 @@ query.upsert(data, uniqueBy, update);
4432
4468
  ```javascript
4433
4469
  query.with(...relations);
4434
4470
  query.withConstraints(relation, callback);
4435
- query.withCount(...relations);
4436
- query.whereHas(relation, callback);
4437
- query.whereDoesntHave(relation);
4438
- 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
4439
4475
  ```
4440
4476
 
4441
4477
  #### Locking
@@ -4483,6 +4519,7 @@ this.hasManyThrough(
4483
4519
 
4484
4520
  // Polymorphic
4485
4521
  this.morphTo(morphType, morphId);
4522
+ this.morphOne(related, morphType, morphId);
4486
4523
  this.morphMany(related, morphType, morphId);
4487
4524
  ```
4488
4525
 
@@ -4771,6 +4808,32 @@ class CustomCast {
4771
4808
 
4772
4809
  We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
4773
4810
 
4811
+ ## Security
4812
+
4813
+ ### Reporting Vulnerabilities
4814
+
4815
+ If you discover a security vulnerability, please email raphyabak@gmail.com with:
4816
+ - Description of the vulnerability
4817
+ - Steps to reproduce
4818
+ - Potential impact
4819
+
4820
+ We will respond within 48 hours.
4821
+
4822
+ ### Security Considerations
4823
+
4824
+ **Database Drivers**: Database drivers (pg, mysql2, sqlite3) are optional dependencies. Install only what you need to reduce your security surface:
4825
+ ```bash
4826
+ npm install pg # PostgreSQL
4827
+ npm install mysql2 # MySQL
4828
+ npm install sqlite3 # SQLite
4829
+ ```
4830
+
4831
+ **SQL Injection Protection**: IlanaORM uses parameterized queries via Knex.js. Always use query builder methods instead of raw SQL.
4832
+
4833
+ **Filesystem Access**: The CLI and migration tools require filesystem access. Review migration files before running.
4834
+
4835
+ **Network Access**: Database drivers make network connections. Ensure proper firewall and connection security.
4836
+
4774
4837
  ## License
4775
4838
 
4776
4839
  MIT License - see the [LICENSE](LICENSE) file for details.
@@ -10,17 +10,28 @@ class Database {
10
10
 
11
11
  // Initialize all configured connections
12
12
  for (const [name, connConfig] of Object.entries(config.connections)) {
13
- const connection = knex({
14
- ...connConfig,
15
- migrations: config.migrations || {
16
- directory: './migrations',
17
- tableName: 'migrations'
18
- },
19
- seeds: config.seeds || {
20
- directory: './seeds'
13
+ try {
14
+ const connection = knex({
15
+ ...connConfig,
16
+ migrations: config.migrations || {
17
+ directory: './migrations',
18
+ tableName: 'migrations'
19
+ },
20
+ seeds: config.seeds || {
21
+ directory: './seeds'
22
+ }
23
+ });
24
+ this.connections.set(name, connection);
25
+ } catch (error) {
26
+ if (error.code === 'MODULE_NOT_FOUND' && error.message.includes(connConfig.client)) {
27
+ const driverMap = { pg: 'pg', mysql2: 'mysql2', sqlite3: 'sqlite3' };
28
+ const driver = driverMap[connConfig.client] || connConfig.client;
29
+ throw new Error(
30
+ `Database driver '${driver}' not installed. Install it with: npm install ${driver}`
31
+ );
21
32
  }
22
- });
23
- this.connections.set(name, connection);
33
+ throw error;
34
+ }
24
35
  }
25
36
 
26
37
  // Set default instance after all connections are created
@@ -104,10 +104,9 @@ class SchemaBuilder {
104
104
 
105
105
  checkPositive(column) {
106
106
  const client = this.knex.client.config.client;
107
- if (client === 'pg') {
108
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD CONSTRAINT ${column}_positive CHECK (${column} > 0)`);
109
- } else if (client === 'mysql2') {
110
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD CONSTRAINT ${column}_positive CHECK (${column} > 0)`);
107
+ if (client === 'pg' || client === 'mysql2') {
108
+ return this.knex.raw(`ALTER TABLE ?? ADD CONSTRAINT ?? CHECK (?? > 0)`,
109
+ [this.currentTable, `${column}_positive`, column]);
111
110
  }
112
111
  return Promise.resolve();
113
112
  }
@@ -115,9 +114,11 @@ class SchemaBuilder {
115
114
  checkRegex(column, pattern) {
116
115
  const client = this.knex.client.config.client;
117
116
  if (client === 'pg') {
118
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD CONSTRAINT ${column}_regex CHECK (${column} ~ '${pattern}')`);
117
+ return this.knex.raw(`ALTER TABLE ?? ADD CONSTRAINT ?? CHECK (?? ~ ?)`,
118
+ [this.currentTable, `${column}_regex`, column, pattern]);
119
119
  } else if (client === 'mysql2') {
120
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD CONSTRAINT ${column}_regex CHECK (${column} REGEXP '${pattern}')`);
120
+ return this.knex.raw(`ALTER TABLE ?? ADD CONSTRAINT ?? CHECK (?? REGEXP ?)`,
121
+ [this.currentTable, `${column}_regex`, column, pattern]);
121
122
  }
122
123
  return Promise.resolve();
123
124
  }
@@ -125,9 +126,11 @@ class SchemaBuilder {
125
126
  generatedAs(column, expression) {
126
127
  const client = this.knex.client.config.client;
127
128
  if (client === 'mysql2') {
128
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD ${column} VARCHAR(255) GENERATED ALWAYS AS (${expression}) STORED`);
129
+ return this.knex.raw(`ALTER TABLE ?? ADD ?? VARCHAR(255) GENERATED ALWAYS AS (${expression}) STORED`,
130
+ [this.currentTable, column]);
129
131
  } else if (client === 'pg') {
130
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD ${column} TEXT GENERATED ALWAYS AS (${expression}) STORED`);
132
+ return this.knex.raw(`ALTER TABLE ?? ADD ?? TEXT GENERATED ALWAYS AS (${expression}) STORED`,
133
+ [this.currentTable, column]);
131
134
  }
132
135
  return Promise.resolve();
133
136
  }
@@ -135,9 +138,9 @@ class SchemaBuilder {
135
138
  collate(tableName, collation) {
136
139
  const client = this.knex.client.config.client;
137
140
  if (client === 'mysql2') {
138
- return this.knex.raw(`ALTER TABLE ${tableName} COLLATE ${collation}`);
141
+ return this.knex.raw(`ALTER TABLE ?? COLLATE ??`, [tableName, collation]);
139
142
  } else if (client === 'pg') {
140
- return this.knex.raw(`ALTER TABLE ${tableName} ALTER COLUMN name TYPE TEXT COLLATE "${collation}"`);
143
+ return this.knex.raw(`ALTER TABLE ?? ALTER COLUMN name TYPE TEXT COLLATE ??`, [tableName, collation]);
141
144
  }
142
145
  return Promise.resolve();
143
146
  }
@@ -166,7 +169,9 @@ class SchemaBuilder {
166
169
  fulltext(columns, indexName) {
167
170
  if (this.knex.client.config.client === 'mysql2') {
168
171
  const name = indexName || `${columns.join('_')}_fulltext`;
169
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD FULLTEXT INDEX ${name} (${columns.join(', ')})`);
172
+ const colRefs = columns.map(() => '??').join(', ');
173
+ return this.knex.raw(`ALTER TABLE ?? ADD FULLTEXT INDEX ?? (${colRefs})`,
174
+ [this.currentTable, name, ...columns]);
170
175
  }
171
176
  return Promise.resolve();
172
177
  }
@@ -174,7 +179,8 @@ class SchemaBuilder {
174
179
  spatial(column, indexName) {
175
180
  if (this.knex.client.config.client === 'mysql2') {
176
181
  const name = indexName || `${column}_spatial`;
177
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD SPATIAL INDEX ${name} (${column})`);
182
+ return this.knex.raw(`ALTER TABLE ?? ADD SPATIAL INDEX ?? (??)`,
183
+ [this.currentTable, name, column]);
178
184
  }
179
185
  return Promise.resolve();
180
186
  }
package/index.js CHANGED
@@ -24,6 +24,7 @@ module.exports = {
24
24
  defineFactory: Factory.defineFactory,
25
25
 
26
26
  // Relationships
27
+ MorphOne: Relation.MorphOne,
27
28
  ...Relation,
28
29
 
29
30
  // Custom Casts
package/index.mjs CHANGED
@@ -22,6 +22,7 @@ export const {
22
22
  BelongsToMany,
23
23
  HasManyThrough,
24
24
  MorphTo,
25
+ MorphOne,
25
26
  MorphMany,
26
27
  MoneyCast,
27
28
  EncryptedCast,