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 +178 -115
- package/database/connection.js +21 -10
- package/database/schema-builder.js +18 -12
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/orm/Factory.js +20 -18
- package/orm/Model.d.ts +26 -3
- package/orm/Model.js +159 -68
- package/orm/QueryBuilder.d.ts +24 -2
- package/orm/QueryBuilder.js +437 -75
- package/orm/Relation.d.ts +15 -3
- package/orm/Relation.js +74 -11
- package/orm/Relation.mjs +1 -1
- package/package.json +10 -9
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
|
-
|
|
111
|
-
yarn add ilana-orm
|
|
110
|
+
### Database Drivers
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```
|
|
112
|
+
Install only the database driver you need:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# PostgreSQL
|
|
116
|
+
npm install pg
|
|
116
117
|
|
|
117
|
-
|
|
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
|
-
//
|
|
906
|
-
class
|
|
907
|
-
get(
|
|
908
|
-
|
|
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
|
-
//
|
|
973
|
+
// Keys listed in appends appear in toJSON() and are accessible as properties
|
|
967
974
|
appends = ['full_name', 'avatar_url'];
|
|
968
975
|
|
|
969
|
-
// Mutator
|
|
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
|
|
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
|
-
//
|
|
997
|
+
// Direct property access triggers the accessor
|
|
991
998
|
const user = await User.find(1);
|
|
992
|
-
console.log(user.
|
|
993
|
-
//
|
|
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.
|
|
1027
|
-
//
|
|
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
|
|
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
|
-
.
|
|
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
|
-
//
|
|
1678
|
-
const
|
|
1679
|
-
.
|
|
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
|
-
.
|
|
1764
|
-
.
|
|
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.
|
|
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
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
return this.morphTo('
|
|
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
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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
|
|
1987
|
-
|
|
1988
|
-
|
|
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
|
-
|
|
1995
|
-
comments() {
|
|
1996
|
-
|
|
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
|
|
2001
|
-
|
|
2002
|
-
|
|
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
|
|
3316
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
4438
|
-
query.
|
|
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.
|
package/database/connection.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
141
|
+
return this.knex.raw(`ALTER TABLE ?? COLLATE ??`, [tableName, collation]);
|
|
139
142
|
} else if (client === 'pg') {
|
|
140
|
-
return this.knex.raw(`ALTER TABLE
|
|
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
|
-
|
|
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
|
|
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