masquerade-orm 0.1.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/universalTsInit.js +59 -0
- package/docs/deletion.md +186 -0
- package/docs/find.md +265 -0
- package/docs/getting-started-javascript.md +112 -0
- package/docs/getting-started-typescript.md +159 -0
- package/docs/in-depth-class-definitions.md +193 -0
- package/docs/jsdoc-ux-tips.md +17 -0
- package/docs/managing-the-database.md +37 -0
- package/docs/saving-to-database.md +37 -0
- package/index.d.ts +7 -0
- package/index.js +11 -0
- package/jsconfig.json +29 -0
- package/package.json +40 -0
- package/src/ORM/DbManager.js +76 -0
- package/src/ORM/ORM.js +181 -0
- package/src/ORM/bootOrm.js +929 -0
- package/src/changeLogger/changeLogger.js +55 -0
- package/src/changeLogger/save.js +237 -0
- package/src/changeLogger/sqlClients/postgres.js +97 -0
- package/src/changeLogger/sqlClients/sqlite.js +120 -0
- package/src/entity/delete/delete.js +20 -0
- package/src/entity/delete/getDependents.js +46 -0
- package/src/entity/entity.d.ts +46 -0
- package/src/entity/entity.js +228 -0
- package/src/entity/find/find.js +157 -0
- package/src/entity/find/joins.js +52 -0
- package/src/entity/find/queryBuilder.js +95 -0
- package/src/entity/find/relations.js +23 -0
- package/src/entity/find/scopeProxies.js +60 -0
- package/src/entity/find/sqlClients/postgresFuncs.js +171 -0
- package/src/entity/find/sqlClients/sqliteFuncs.js +211 -0
- package/src/entity/find/where/relationalWhere.js +142 -0
- package/src/entity/find/where/where.js +244 -0
- package/src/entity/find/where/whereArgsFunctions.js +49 -0
- package/src/misc/classes.js +63 -0
- package/src/misc/constants.js +42 -0
- package/src/misc/miscFunctions.js +167 -0
- package/src/misc/ormStore.js +18 -0
- package/src/misc/types.d.ts +560 -0
- package/src/proxies/instanceProxy.js +544 -0
- package/src/proxies/nonRelationalArrayProxy.js +76 -0
- package/src/proxies/objectProxy.js +50 -0
- package/src/proxies/relationalArrayProxy.js +130 -0
- package/src/webpack/masquerade-loader.js +14 -0
- package/src/webpack/plugin.js +43 -0
- package/src/webpack/store.js +2 -0
- package/src/webpack/webpack.config.js +41 -0
- package/testing/dev-doc.md +7 -0
- package/testing/generationFuncs.js +21 -0
- package/testing/miscFunctions.js +97 -0
- package/testing/postgres.test.js +254 -0
- package/testing/sqlite.test.js +257 -0
- package/testing/testing-classes.js +102 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Masquerade-Orm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="#">
|
|
3
|
+
<img
|
|
4
|
+
src="https://github.com/user-attachments/assets/3bf1ab31-f9c6-4362-b17d-1dfe7c414f17"
|
|
5
|
+
alt="Masquerade ORM Logo"
|
|
6
|
+
style="max-width: 50%; height: auto;"
|
|
7
|
+
/>
|
|
8
|
+
</a>
|
|
9
|
+
<br><br>
|
|
10
|
+
<a href="">
|
|
11
|
+
<br><br>
|
|
12
|
+
<img src="https://img.shields.io/badge/License-MIT-teal.svg" alt="MIT License"/>
|
|
13
|
+
</a>
|
|
14
|
+
<br><br>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
**MasqueradeORM** is a lightweight ORM for Node.js that works seamlessly with both TypeScript and JavaScript.
|
|
18
|
+
Its goal is to hide SQL complexity while letting you work naturally in JS/TS syntax.
|
|
19
|
+
Instead of forcing you into ORM-specific models, metadata systems, or decorators, MasqueradeORM lets you use **your own classes** directly, exactly as you normally would.
|
|
20
|
+
|
|
21
|
+
MasqueradeORM improves readability, maintainability, and workflow simplicity through a unified coding approach and extremely minimal setup.
|
|
22
|
+
No ORM offers a simpler start.
|
|
23
|
+
There’s no need to manage heavy configuration layers, maintain secondary schema systems, or even plan your database structure separately.
|
|
24
|
+
Your schema and tables are generated automatically from a single source of truth: **Your class definitions.**
|
|
25
|
+
|
|
26
|
+
MasqueradeORM currently supports the following SQL clients:
|
|
27
|
+
- **SQLite**
|
|
28
|
+
- **Postgresql**
|
|
29
|
+
|
|
30
|
+
# Features
|
|
31
|
+
- **Effortless setup** - no ORM-specific structures; just use your classes.
|
|
32
|
+
- **Zero schema planning** - tables and schema are generated automatically.
|
|
33
|
+
- **Powerful IntelliSense** - confidently build complex queries with real-time IDE feedback when something’s wrong.
|
|
34
|
+
- **Minimal memory usage** - one class instance per database row, minimizing memory usage and avoiding duplicates through smart state management.
|
|
35
|
+
- **Optimized querying** - fewer queries through intelligent transaction grouping without sacrificing data integrity.
|
|
36
|
+
- **Relational WHERE clauses** - easily write conditions that compare two columns within the same table or columns across different tables.
|
|
37
|
+
- **Write complex WHERE conditions using a template-literal helper** - enabling expressive comparisons like >=, LIKE, object-property access, and even array element matching, all without cluttering your query code.
|
|
38
|
+
- **SQL injection protection** - all queries are parameterized.
|
|
39
|
+
- **Lightweight** - minimal dependencies.
|
|
40
|
+
- **Strong typing even in JavaScript** - powered by JSDoc, no compile step required.
|
|
41
|
+
- **Reduced data transfer size** - improves performance in client-server setups (not applicable for embedded databases like SQLite).
|
|
42
|
+
- **Abstract and non-abstract inheritance** - enables the use of abstract classes, even in JavaScript.
|
|
43
|
+
- **Combines the convenience of embedded SQLite with the strict typing of RDBMS**
|
|
44
|
+
- **Eager and lazy relations**
|
|
45
|
+
- **Unidirectional, bidirectional, and self-referenced relations**
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Example Code Implementation
|
|
49
|
+
|
|
50
|
+
### Creating an ORM-Compatible Class
|
|
51
|
+
```ts
|
|
52
|
+
import { Entity } from 'masquerade'
|
|
53
|
+
|
|
54
|
+
type UserSettings = {
|
|
55
|
+
theme: 'light' | 'dark' | 'system'
|
|
56
|
+
twoStepVerification: boolean
|
|
57
|
+
locale: 'en' | 'es' | 'fr' | 'de'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class User extends Entity {
|
|
61
|
+
username: string
|
|
62
|
+
email: string
|
|
63
|
+
password: string
|
|
64
|
+
createdAt: Date = new Date()
|
|
65
|
+
friendList: User[] = []
|
|
66
|
+
settings: UserSettings & object = {
|
|
67
|
+
locale: "en",
|
|
68
|
+
theme: "system",
|
|
69
|
+
twoStepVerification: false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
constructor(username: string, email: string, password: string) {
|
|
73
|
+
super()
|
|
74
|
+
this.username = username
|
|
75
|
+
this.email = email
|
|
76
|
+
this.password = password
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Basic Find Example
|
|
82
|
+
```ts
|
|
83
|
+
// finds any User instance with email === lookupEmail
|
|
84
|
+
async function findUserByEmail(lookupEmail: string): Promise<User | undefined> {
|
|
85
|
+
const resultArray = await User.find({
|
|
86
|
+
where: { email: lookupEmail }
|
|
87
|
+
})
|
|
88
|
+
// the static 'find' method above is inherited from 'Entity'
|
|
89
|
+
|
|
90
|
+
return resultArray[0]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Saving Instances
|
|
95
|
+
```ts
|
|
96
|
+
// Creating a new table row in the User table
|
|
97
|
+
const newUser = new User('JohnDoe57', 'johnDoe@yahoo.com', 'passwordHash')
|
|
98
|
+
// newUser will be saved to the database automatically, no explicit save call is required.
|
|
99
|
+
|
|
100
|
+
// Finding a user by email
|
|
101
|
+
const user = await findUserByEmail('johnDoe@yahoo.com') // user's friendList is a promise
|
|
102
|
+
console.log(user.username === 'JohnDoe57') // true
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Mutating Data
|
|
106
|
+
All mutations are persisted implicitly and automatically, meaning that simply changing a value is enough for it to be reflected in the database.
|
|
107
|
+
|
|
108
|
+
**Mutating non-Relational Properties**
|
|
109
|
+
```ts
|
|
110
|
+
user.settings.theme = 'dark'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Mutating Relational Properties**
|
|
114
|
+
```ts
|
|
115
|
+
// lazy-load friendList
|
|
116
|
+
await user.friendList
|
|
117
|
+
// add a new relation
|
|
118
|
+
user.friendList.push(new User('JaneDoe33', 'janeDoe@yahoo.com', 'passwordHash2'))
|
|
119
|
+
// remove a relation
|
|
120
|
+
user.friendList.pop()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Further Reading
|
|
125
|
+
|
|
126
|
+
- **[Getting Started - Javascript](https://github.com/MasqueradeORM/MasqueradeORM/blob/master/docs/getting-started-javascript.md#class-definitions)**
|
|
127
|
+
- **[Getting Started - Typescript](https://github.com/MasqueradeORM/MasqueradeORM/blob/master/docs/getting-started-typescript.md#class-definitions)**
|
|
128
|
+
- **[Find Method](https://github.com/MasqueradeORM/MasqueradeORM/blob/master/docs/find.md#find)**
|
|
129
|
+
- **[Saving to Database](https://github.com/MasqueradeORM/MasqueradeORM/blob/master/docs/saving-to-database.md#saving-to-the-database)**
|
|
130
|
+
- **[Deleting Instances from the Database](https://github.com/MasqueradeORM/MasqueradeORM/blob/master/docs/deletion.md)**
|
|
131
|
+
- **[Managing Database Tables](https://github.com/MasqueradeORM/MasqueradeORM/blob/master/docs/managing-the-database.md)**
|
|
132
|
+
- **[Defining Classes: In-Depth](https://github.com/MasqueradeORM/MasqueradeORM/blob/master/docs/in-depth-class-definitions.md)** **(important read)**
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
<br>
|
|
137
|
+
<div align="center">
|
|
138
|
+
<strong>
|
|
139
|
+
© 2026
|
|
140
|
+
<a href="https://github.com/MasqueradeORM">MasqueradeORM </a>
|
|
141
|
+
-
|
|
142
|
+
Released under the MIT License
|
|
143
|
+
</strong>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import ts from "typescript"
|
|
4
|
+
import fs from "fs"
|
|
5
|
+
import path from "path"
|
|
6
|
+
import { createSourceFile, SyntaxKind, ScriptKind, ScriptTarget } from "typescript"
|
|
7
|
+
import { nodeArr2ClassDict } from "../src/ORM/bootOrm.js"
|
|
8
|
+
|
|
9
|
+
const configPath = ts.findConfigFile(
|
|
10
|
+
"./",
|
|
11
|
+
ts.sys.fileExists,
|
|
12
|
+
"tsconfig.json"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if (!configPath) {
|
|
16
|
+
throw new Error("tsconfig.json not found")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const configFile = ts.readConfigFile(
|
|
20
|
+
configPath,
|
|
21
|
+
ts.sys.readFile
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
25
|
+
configFile.config,
|
|
26
|
+
ts.sys,
|
|
27
|
+
"./"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const program = ts.createProgram({
|
|
31
|
+
rootNames: parsed.fileNames,
|
|
32
|
+
options: parsed.options
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const classNodesArr = []
|
|
36
|
+
|
|
37
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
38
|
+
if (sourceFile.isDeclarationFile) continue
|
|
39
|
+
const fileText = sourceFile.getFullText()
|
|
40
|
+
const fileNodesArr = createSourceFile('', fileText, ScriptTarget.Latest, true, ScriptKind.TSX).statements
|
|
41
|
+
|
|
42
|
+
for (const node of fileNodesArr) {
|
|
43
|
+
//@ts-ignore
|
|
44
|
+
if ((node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression) && node.heritageClauses)
|
|
45
|
+
classNodesArr.push(node)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const classDict = nodeArr2ClassDict(classNodesArr)
|
|
50
|
+
const filePath = path.join(
|
|
51
|
+
process.cwd(),
|
|
52
|
+
"ormTypeScriptSetup.js"
|
|
53
|
+
)
|
|
54
|
+
const content = `export function UniversalTsSetup() {globalThis.masqueradeClassDict_ = ${JSON.stringify(classDict)};\n}`
|
|
55
|
+
fs.writeFileSync(filePath, content, "utf8")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
package/docs/deletion.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Deleting Instances from the Database
|
|
2
|
+
|
|
3
|
+
### Soft Deletion
|
|
4
|
+
The ORM does not support soft deletion by default. To implement soft deletion, just create an abstract class like so:
|
|
5
|
+
|
|
6
|
+
**TypeScript**
|
|
7
|
+
```ts
|
|
8
|
+
export abstract class SoftDel extends Entity {
|
|
9
|
+
isDeleted: boolean = false
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export abstract class SoftDelUuid extends Entity {
|
|
17
|
+
// switch 'UUID' to 'INT' or 'BIGINT' as needed.
|
|
18
|
+
static ormClassSettings_ = { idType: 'UUID' }
|
|
19
|
+
isDeleted: boolean = false
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**JavaScript**
|
|
28
|
+
```js
|
|
29
|
+
export class SoftDel extends Entity {
|
|
30
|
+
/**@type {boolean}*/ isDeleted = false
|
|
31
|
+
|
|
32
|
+
/** @abstract */
|
|
33
|
+
constructor() {
|
|
34
|
+
super()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class SoftDelUuid extends Entity {
|
|
39
|
+
// switch 'UUID' to 'INT' or 'BIGINT' as needed.
|
|
40
|
+
static ormClassSettings_ = { idType: 'UUID' }
|
|
41
|
+
/**@type {boolean}*/ isDeleted = false
|
|
42
|
+
|
|
43
|
+
/** @abstract */
|
|
44
|
+
constructor() {
|
|
45
|
+
super()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every class that extends these `SoftDel` classes will have a boolean property `isDeleted` that is `false` by default, allowing instances to be soft-deleted by setting the `isDeleted` value to `true`.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
export class SoftDeletableUser extends SoftDelUuid {
|
|
54
|
+
username: string
|
|
55
|
+
email: string
|
|
56
|
+
password: string
|
|
57
|
+
|
|
58
|
+
constructor (username, email, password) {
|
|
59
|
+
this.username = username
|
|
60
|
+
this.email = email
|
|
61
|
+
this.password = password
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const softDelUser = new SoftDeletableUser('JohnDoe', 'JohnDoe@gmail.com', 'hashedPassword')
|
|
66
|
+
// softDelUser has an 'isDeleted' property that can be toggled to true for soft deletion
|
|
67
|
+
// and has an id type of UUID.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Note:** Unlike `Entity`, the `SoftDel` classes need to be passed into the `ORM.boot` method that is being called to init the ORM.
|
|
71
|
+
|
|
72
|
+
### Hard Deletion
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { sql } from "masquerade"
|
|
76
|
+
const twoYearsAgo = new Date().setFullYear(new Date().getFullYear() - 2)
|
|
77
|
+
|
|
78
|
+
// finds all instances that haven't been mutated in over two years
|
|
79
|
+
const instances4Deletion = await ExampleClass.find({where: {updatedAt: sql`< ${twoYearsAgo}`}})
|
|
80
|
+
|
|
81
|
+
// delete all instances
|
|
82
|
+
instances4Deletion.forEach(instance => instance.delete())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The `delete` method hard-deletes the instance from the database; the instance cannot be recovered afterwards.
|
|
86
|
+
|
|
87
|
+
<div align="center">
|
|
88
|
+
<strong style="font-size: 1.2em;">
|
|
89
|
+
** This method will throw an error in case a pre-deletion step is required. **
|
|
90
|
+
</strong>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Pre-Deletion Step
|
|
95
|
+
|
|
96
|
+
### When is a pre-deletion step required?
|
|
97
|
+
A pre-deletion step is required when the class instance has **dependents**.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
### What is a dependent?
|
|
101
|
+
A dependent is a class that has a required 1-to-1 relationship with another class, meaning the relation cannot be undefined.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// dependent 1-to-1 relation, 'sender' cannot be undefined
|
|
105
|
+
sender: User
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
For example, if a `Message` has a `sender` property of type `User`, then `Message` is dependent on `User`. As a result, a `User` instance cannot be safely deleted until all dependent `Message` instances are resolved of said dependency.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
sender?: User // optional relation
|
|
112
|
+
sender: User | undefined // optional relation
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If the property was optional or of type `User | undefined`, the `Message` class would **NOT** be dependent on `User`.
|
|
116
|
+
In such a case, when a `User` instance is deleted, any of its messages will have an `undefined` value in the `sender` property.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
### How to find an instance's dependents?
|
|
120
|
+
Assuming `User` has dependents:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// finds the user with a username of 'JohnDoe57'
|
|
124
|
+
const user = (await User.find({ where: {username: 'JohnDoe57'} }))[0]
|
|
125
|
+
const dependentsDict = await user.getDependents()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### What is the structure of dependentsDict?
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
type DependentsDict<T extends Entity> = {
|
|
132
|
+
[dependentClassName: string]: [
|
|
133
|
+
dependentInstances: T[],
|
|
134
|
+
dependentProps: string[]
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Assuming the classes `Payment` and `Message` are dependents of the `User` class, with `Payment` having a dependent property of `sentBy` and `receivedBy`, and `Message` having a dependent property of `sender`.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// structure of dependentsDict
|
|
143
|
+
dependentsDict = {
|
|
144
|
+
'Payment': [
|
|
145
|
+
// all Payment instances that depend on the deleted User instance
|
|
146
|
+
[paymentInstance1, paymentInstance2, ...],
|
|
147
|
+
// all properties on Payment that depend on the deleted User instance
|
|
148
|
+
['sentBy', 'receivedBy']
|
|
149
|
+
],
|
|
150
|
+
'Message': [
|
|
151
|
+
// all Message instances that depend on the deleted User instance
|
|
152
|
+
[messageInstance1, messageInstance2, ...],
|
|
153
|
+
// all properties on Message that depend on the deleted User instance
|
|
154
|
+
['sender']
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Resolving dependencies
|
|
160
|
+
To resolve the dependencies, access the data as such:
|
|
161
|
+
```ts
|
|
162
|
+
const user = (await User.find({ where: { username: 'JohnDoe57' } }))[0]
|
|
163
|
+
const dependentsDict = await user.getDependents()
|
|
164
|
+
const deletedUserId = user.id
|
|
165
|
+
|
|
166
|
+
for (const dependentClassName of Object.keys(dependentsDict)) {
|
|
167
|
+
const [dependentInstances, dependentProps] = dependentsDict[dependentClassName]
|
|
168
|
+
|
|
169
|
+
for (const instance of dependentInstances) {
|
|
170
|
+
dependentProps.forEach(prop => {
|
|
171
|
+
if (instance[prop].id !== deletedUserId) continue
|
|
172
|
+
// decoupling logic here...
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
<br>
|
|
179
|
+
<div align="center">
|
|
180
|
+
<strong>
|
|
181
|
+
© 2026
|
|
182
|
+
<a href="https://github.com/MasqueradeORM">MasqueradeORM </a>
|
|
183
|
+
-
|
|
184
|
+
Released under the MIT License
|
|
185
|
+
</strong>
|
|
186
|
+
</div>
|
package/docs/find.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Find
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
await ExampleClass.find(findObj)
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The `find` method is the most complex part of the ORM. **Fortunately, it is fully covered by IntelliSense, and you are strongly encouraged to rely on it.**
|
|
8
|
+
|
|
9
|
+
It accepts a single argument, findObj, which contains three optional fields.
|
|
10
|
+
Because all fields are optional, findObj itself may be an empty object (although this is rarely useful, as it would return all instances of ExampleClass).
|
|
11
|
+
|
|
12
|
+
The three optional fields are:
|
|
13
|
+
|
|
14
|
+
- relations
|
|
15
|
+
- where
|
|
16
|
+
- relationalWhere
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## The `relations` Field:
|
|
21
|
+
|
|
22
|
+
The `relations` field determines which relations are eagerly loaded from the database.
|
|
23
|
+
|
|
24
|
+
A crucial detail to understand is that relations are never filtered.
|
|
25
|
+
They are either loaded or not. The ORM never displays partial relational data.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
// assume that relationalProp is a property of type SomeClass or SomeClass[]
|
|
29
|
+
await ExampleClass.find(
|
|
30
|
+
{
|
|
31
|
+
relations: {relationalProp: true},
|
|
32
|
+
where: {
|
|
33
|
+
relationalProp: {
|
|
34
|
+
id: 57
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The example above translates to:
|
|
42
|
+
**“Fetch all instances of ExampleClass whose relationalProp contains a SomeClass instance with id = 57.”**
|
|
43
|
+
|
|
44
|
+
In other words, the condition matches when either of the following is true:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// 1-to-1 relationship case
|
|
48
|
+
exampleClassInstance.relationalProp === someClassId57
|
|
49
|
+
```
|
|
50
|
+
or
|
|
51
|
+
```js
|
|
52
|
+
// 1-to-many relationship case
|
|
53
|
+
exampleClassInstance.relationalProp.includes(someClassId57)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Lazy Loading
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// Assume the 'Chat' class has relational properties 'users' and 'messages'.
|
|
60
|
+
// Initially, we load only the 'messages' relation for a specific chat.
|
|
61
|
+
|
|
62
|
+
const resultArray = await Chat.find({
|
|
63
|
+
relations: { messages: true }, // eager load the 'messages' relation
|
|
64
|
+
where: { id: 123 } // fetch the chat with ID 123
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const someChat = resultArray[0]
|
|
68
|
+
|
|
69
|
+
// At this point, 'someChat.users' is not loaded.
|
|
70
|
+
// To load the 'users' relation, we need to await it.
|
|
71
|
+
await someChat.users
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## The `where` Field:
|
|
76
|
+
|
|
77
|
+
The `where` field is for filtering the root instances, in the following case, Chat instances.
|
|
78
|
+
```js
|
|
79
|
+
await Chat.find({
|
|
80
|
+
where: {
|
|
81
|
+
messages: {
|
|
82
|
+
sender: {
|
|
83
|
+
id: 12
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
Translation: **“Find all chats that contain a message from a user with the id 12, without loading messages.“**
|
|
90
|
+
|
|
91
|
+
- **Note:** The scope of the `where` condtions is agnostic to the scope of the `relations` (eager-loading).
|
|
92
|
+
It is completely safe to filter based on specific relations without having said relations passed into the `relations` field.
|
|
93
|
+
|
|
94
|
+
### Introduction to the `sql`, `OR` and `AND` functions
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { sql, AND, OR } from "masquerade"
|
|
98
|
+
|
|
99
|
+
await Angel.find({
|
|
100
|
+
where: {
|
|
101
|
+
// name is either "Micheal" OR "Gabriel"
|
|
102
|
+
name: OR('Micheal', 'Gabriel'),
|
|
103
|
+
|
|
104
|
+
// demonsSentToAbyss is greater than 12,000 AND less than 57,000
|
|
105
|
+
demonsSentToAbyss: AND(sql`> 12000`, sql`< 57000`)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Using the `sql` function with explicit column identifiers
|
|
111
|
+
In the previous example, the `sql` function implicitly inserted a column identifier (`#`) on the left side of the SQL statement.
|
|
112
|
+
```js
|
|
113
|
+
// these two statements are equivalent
|
|
114
|
+
sql`> 12000`
|
|
115
|
+
sql`# > 12000`
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
In next example, `#` identifiers must be written explicitly because the SQL string uses `AND` conditional operators directly, rather than using the `AND()` helper function.
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
import { sql } from "masquerade"
|
|
122
|
+
|
|
123
|
+
const twoYearsAgo = new Date().setFullYear(new Date().getFullYear() - 2)
|
|
124
|
+
const oneYearAgo = new Date().setFullYear(new Date().getFullYear() - 1)
|
|
125
|
+
|
|
126
|
+
await User.find({
|
|
127
|
+
where: {
|
|
128
|
+
// donations between 1,200 and 5,700 cents (exclusive)
|
|
129
|
+
donations: sql`1200 < # AND # < 5700`,
|
|
130
|
+
|
|
131
|
+
// account's age is between one and two years old.
|
|
132
|
+
createdAt: sql`${twoYearsAgo} <= # AND # <= ${oneYearAgo}`
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
// The ANDs are written directly inside the 'sql' string instead of
|
|
136
|
+
// having to rely on helper functions to achieve the same result.
|
|
137
|
+
// The 'sql' function gives you the ability to write powerful
|
|
138
|
+
// 'where' conditions in an easy-to-read and easy-to-write manner.
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Using the `sql` function to create a `LIKE` `WHERE` condition
|
|
142
|
+
```js
|
|
143
|
+
import { sql } from "masquerade"
|
|
144
|
+
|
|
145
|
+
await User.find({
|
|
146
|
+
where: {
|
|
147
|
+
// registered using a Gmail email
|
|
148
|
+
email: sql`LIKE '%@gmail.com%'`
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Using the `sql` function to create a `WHERE` condition for matching JSON values
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { Entity } from "masquerade"
|
|
157
|
+
|
|
158
|
+
type OrderOverview = {
|
|
159
|
+
status: "pending" | "completed" | "cancelled"
|
|
160
|
+
total: number
|
|
161
|
+
currency: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class Order extends Entity {
|
|
165
|
+
// other properties...
|
|
166
|
+
metadata: UserMetadata
|
|
167
|
+
// other properties + constructor...
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const completedOrders = await Order.find({
|
|
171
|
+
where:
|
|
172
|
+
{ overview: sql`json_extract(#, '$.status') = 'completed'` }
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- **Note:** for SQL-client specific guide for writing `WHERE` conditions involving JSON and array data, go to the bottom of this page or click **[here](https://github.com/MasqueradeORM/MasqueradeORM/blob/master/docs/find.md#array-and-json-where-conditions-guide)**.
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
## The `relationalWhere` Field:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
import { sql } from "masquerade"
|
|
183
|
+
|
|
184
|
+
// Finds users that have at least one chat that contains at least one message whose sender's username is 'Glory2Christ'.
|
|
185
|
+
await User.find({
|
|
186
|
+
relationalWhere: (user) => sql`${user.chats.messages.sender.username} = 'Glory2Christ'`
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
import { sql } from "masquerade"
|
|
192
|
+
|
|
193
|
+
// Identical to the previous example, but here the relational where is called from a different scope.
|
|
194
|
+
// note: the field has an underscore, to prevent any (rather impossible) name collisions.
|
|
195
|
+
|
|
196
|
+
await User.find({
|
|
197
|
+
where: {
|
|
198
|
+
chats: {
|
|
199
|
+
relationalWhere_: (chat) => sql`${chat.messages.sender.username} = 'Glory2Christ'`,
|
|
200
|
+
// can be combined with regular 'where' conditions - below is valid code
|
|
201
|
+
// chatName: 'The History of Orthodoxy'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Array and JSON `WHERE` Conditions Guide
|
|
208
|
+
|
|
209
|
+
The model we will use for the examples:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { Entity } from "masquerade"
|
|
213
|
+
|
|
214
|
+
type UserMetadata = {
|
|
215
|
+
roles: string[] // e.g., ["admin", "moderator"]
|
|
216
|
+
lastLogin?: string // optional, ISO date string
|
|
217
|
+
preferences?: {
|
|
218
|
+
theme?: "light" | "dark"
|
|
219
|
+
notifications?: boolean
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class User extends Entity {
|
|
224
|
+
// other properties...
|
|
225
|
+
metadata: UserMetadata
|
|
226
|
+
sessions: string[]
|
|
227
|
+
// other properties + constructor...
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Assuming we are writing the condition for the property `metadata` or `sessions` like so:
|
|
232
|
+
```ts
|
|
233
|
+
import { sql } from "masquerade"
|
|
234
|
+
// 'metadata' find
|
|
235
|
+
const users = await User.find({where: {metadata: sql`_OPERATION_STRING_`}})
|
|
236
|
+
|
|
237
|
+
// 'sessions' find
|
|
238
|
+
const users2 = await User.find({where: {sessions: sql`_OPERATION_STRING_`}})
|
|
239
|
+
|
|
240
|
+
// **if not specified, the default is the 'metadata' find
|
|
241
|
+
|
|
242
|
+
// replace _OPERATION_STRING_ with the appropriate
|
|
243
|
+
// operation string from the table below
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
<strong>**Operation String Table**
|
|
247
|
+
|
|
248
|
+
| Operation | SQLite | PostgreSQL |
|
|
249
|
+
|-------------|---------------|------------|
|
|
250
|
+
Array length <br>(example uses len = 2) | **'metadata' find** <br> `json_array_length(json_extract(#, '$.roles')) > 2` <br> **'sessions' find** <br> `json_array_length(json_extract(#)) > 2` | **'metadata' find** <br> `jsonb_array_length(#->'roles') > 2` <br> **'sessions' find** <br> `jsonb_array_length(#) > 2`|
|
|
251
|
+
| Access index `i` of array | **'metadata' find** <br>`json_extract(#, '$.roles[i]') = 'admin'`<br> **'sessions' find** <br>`json_extract(#, '$[i]') = 'SOME_SESSION_ID'` | **'metadata' find** <br>`#->'roles'->>i = 'admin''`<br> **'sessions' find** <br>`#->>i = 'admin'` |
|
|
252
|
+
| Check if array contains a value | `json_extract(#, '$.roles') LIKE '%"admin"%'` | `#->'roles' @> '["admin"]'::jsonb` |
|
|
253
|
+
Check nested field | `json_extract(#, '$.preferences.theme') = 'dark'` | `#->'preferences'->>'theme' = 'dark'` |
|
|
254
|
+
</strong>
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
<br>
|
|
258
|
+
<div align="center">
|
|
259
|
+
<strong>
|
|
260
|
+
© 2026
|
|
261
|
+
<a href="https://github.com/MasqueradeORM">MasqueradeORM </a>
|
|
262
|
+
-
|
|
263
|
+
Released under the MIT License
|
|
264
|
+
</strong>
|
|
265
|
+
</div>
|