@cheetah.js/orm 0.1.151 → 0.2.2
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.
|
@@ -4,6 +4,7 @@ export declare class IdentityMap {
|
|
|
4
4
|
constructor();
|
|
5
5
|
get<T>(entityClass: Function, pk: any): T | undefined;
|
|
6
6
|
set<T extends object>(entity: T): void;
|
|
7
|
+
setByKey<T extends object>(entityClass: Function, pk: any, entity: T): void;
|
|
7
8
|
has(entityClass: Function, pk: any): boolean;
|
|
8
9
|
remove(entityClass: Function, pk: any): void;
|
|
9
10
|
clear(): void;
|
|
@@ -16,6 +16,10 @@ class IdentityMap {
|
|
|
16
16
|
const key = this.keyGenerator.generateForEntity(entity);
|
|
17
17
|
this.registry.set(key, entity);
|
|
18
18
|
}
|
|
19
|
+
setByKey(entityClass, pk, entity) {
|
|
20
|
+
const key = this.keyGenerator.generate(entityClass, pk);
|
|
21
|
+
this.registry.set(key, entity);
|
|
22
|
+
}
|
|
19
23
|
has(entityClass, pk) {
|
|
20
24
|
const key = this.keyGenerator.generate(entityClass, pk);
|
|
21
25
|
return this.registry.has(key);
|
|
@@ -9,41 +9,55 @@ class ModelTransformer {
|
|
|
9
9
|
this.entityStorage = entityStorage;
|
|
10
10
|
}
|
|
11
11
|
transform(model, statement, data) {
|
|
12
|
-
const instanceMap = this.createInstances(model, statement, data);
|
|
12
|
+
const { instanceMap, cachedAliases } = this.createInstances(model, statement, data);
|
|
13
13
|
const optionsMap = this.buildOptionsMap(instanceMap);
|
|
14
|
-
this.populateProperties(data, instanceMap, optionsMap);
|
|
14
|
+
this.populateProperties(data, instanceMap, optionsMap, cachedAliases);
|
|
15
15
|
this.linkJoinedEntities(statement, instanceMap, optionsMap);
|
|
16
|
-
this.resetChangedValues(instanceMap);
|
|
17
|
-
this.registerInstancesInIdentityMap(instanceMap);
|
|
16
|
+
this.resetChangedValues(instanceMap, cachedAliases);
|
|
17
|
+
this.registerInstancesInIdentityMap(instanceMap, cachedAliases);
|
|
18
18
|
return instanceMap[statement.alias];
|
|
19
19
|
}
|
|
20
|
-
registerInstancesInIdentityMap(instanceMap) {
|
|
21
|
-
Object.
|
|
20
|
+
registerInstancesInIdentityMap(instanceMap, cachedAliases) {
|
|
21
|
+
Object.entries(instanceMap).forEach(([alias, instance]) => {
|
|
22
|
+
// Skip registering entities that were already in cache
|
|
23
|
+
if (cachedAliases.has(alias)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
identity_map_1.IdentityMapIntegration.registerEntity(instance);
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
29
|
createInstances(model, statement, data) {
|
|
30
|
+
const cachedAliases = new Set();
|
|
26
31
|
const primaryKey = this.extractPrimaryKeyFromData(model, statement.alias, data);
|
|
27
|
-
const instance = this.createInstance(model, primaryKey);
|
|
32
|
+
const { instance, wasCached } = this.createInstance(model, primaryKey);
|
|
33
|
+
if (wasCached) {
|
|
34
|
+
cachedAliases.add(statement.alias);
|
|
35
|
+
}
|
|
28
36
|
const instanceMap = {
|
|
29
37
|
[statement.alias]: instance,
|
|
30
38
|
};
|
|
31
39
|
if (statement.join) {
|
|
32
|
-
this.addJoinedInstances(statement, instanceMap, data);
|
|
40
|
+
this.addJoinedInstances(statement, instanceMap, data, cachedAliases);
|
|
33
41
|
}
|
|
34
|
-
return instanceMap;
|
|
42
|
+
return { instanceMap, cachedAliases };
|
|
35
43
|
}
|
|
36
44
|
createInstance(model, primaryKey) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
instance
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const cached = identity_map_1.IdentityMapIntegration.getEntity(model, primaryKey);
|
|
46
|
+
if (cached) {
|
|
47
|
+
return { instance: cached, wasCached: true };
|
|
48
|
+
}
|
|
49
|
+
const instance = new model();
|
|
50
|
+
instance.$_isPersisted = true;
|
|
51
|
+
// Note: Registration happens later in registerInstancesInIdentityMap after properties are populated
|
|
52
|
+
return { instance, wasCached: false };
|
|
42
53
|
}
|
|
43
|
-
addJoinedInstances(statement, instanceMap, data) {
|
|
54
|
+
addJoinedInstances(statement, instanceMap, data, cachedAliases) {
|
|
44
55
|
statement.join.forEach(join => {
|
|
45
56
|
const primaryKey = this.extractPrimaryKeyFromData(join.joinEntity, join.joinAlias, data);
|
|
46
|
-
const joinInstance = this.createInstance(join.joinEntity, primaryKey);
|
|
57
|
+
const { instance: joinInstance, wasCached } = this.createInstance(join.joinEntity, primaryKey);
|
|
58
|
+
if (wasCached) {
|
|
59
|
+
cachedAliases.add(join.joinAlias);
|
|
60
|
+
}
|
|
47
61
|
instanceMap[join.joinAlias] = joinInstance;
|
|
48
62
|
});
|
|
49
63
|
}
|
|
@@ -84,13 +98,17 @@ class ModelTransformer {
|
|
|
84
98
|
}
|
|
85
99
|
return optionsMap;
|
|
86
100
|
}
|
|
87
|
-
populateProperties(data, instanceMap, optionsMap) {
|
|
101
|
+
populateProperties(data, instanceMap, optionsMap, cachedAliases) {
|
|
88
102
|
Object.entries(data).forEach(([key, value]) => {
|
|
89
103
|
const { alias, propertyName } = this.parseColumnKey(key);
|
|
90
104
|
const entity = instanceMap[alias];
|
|
91
105
|
if (!entity) {
|
|
92
106
|
return;
|
|
93
107
|
}
|
|
108
|
+
// Skip populating properties for cached entities to preserve in-memory changes
|
|
109
|
+
if (cachedAliases.has(alias)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
94
112
|
this.setPropertyValue(entity, propertyName, value, optionsMap.get(alias));
|
|
95
113
|
});
|
|
96
114
|
}
|
|
@@ -163,8 +181,12 @@ class ModelTransformer {
|
|
|
163
181
|
appendToArray(existingArray, newItem) {
|
|
164
182
|
return existingArray ? [...existingArray, newItem] : [newItem];
|
|
165
183
|
}
|
|
166
|
-
resetChangedValues(instanceMap) {
|
|
167
|
-
Object.
|
|
184
|
+
resetChangedValues(instanceMap, cachedAliases) {
|
|
185
|
+
Object.entries(instanceMap).forEach(([alias, instance]) => {
|
|
186
|
+
// Skip resetting changed values for cached entities to preserve in-memory changes
|
|
187
|
+
if (cachedAliases.has(alias)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
168
190
|
const currentValues = {};
|
|
169
191
|
for (const key in instance) {
|
|
170
192
|
if (!key.startsWith('_') && !key.startsWith('$')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cheetah.js/orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A simple ORM for Cheetah.js.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"uuid": "^9.0.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@cheetah.js/core": "^0.
|
|
40
|
+
"@cheetah.js/core": "^0.2.0"
|
|
41
41
|
},
|
|
42
42
|
"author": "",
|
|
43
43
|
"license": "MIT",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"bun",
|
|
56
56
|
"value-object"
|
|
57
57
|
],
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "7e01e4a49e89040af0c81bd3e5d1bd257fbe3e2a"
|
|
59
59
|
}
|
package/README.md
DELETED
|
@@ -1,669 +0,0 @@
|
|
|
1
|
-
# Cheetah.js ORM
|
|
2
|
-
Cheetah.js ORM is a simple and powerful ORM for Cheetah.js and Bun.
|
|
3
|
-
<br>We don't use any query builder like knex, we have our own query builder making us faster.
|
|
4
|
-
**In development.**
|
|
5
|
-
|
|
6
|
-
### Menu
|
|
7
|
-
- [Installation](#install)
|
|
8
|
-
- [Entities](#entities)
|
|
9
|
-
- [Value Objects](#value-objects)
|
|
10
|
-
- [Hooks](#hooks)
|
|
11
|
-
- [Usage](#usage)
|
|
12
|
-
- [Caching](#caching)
|
|
13
|
-
- [Identity Map](#identity-map)
|
|
14
|
-
- [Migrations](#migrations)
|
|
15
|
-
|
|
16
|
-
### [Installation](#install)
|
|
17
|
-
For install Cheetah.js ORM, run the command below:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
bun install @cheetah.js/orm
|
|
21
|
-
```
|
|
22
|
-
Create a configuration file for the ORM in the root of the project called "cheetah.config.ts" and configure the database connection, providers and entities:
|
|
23
|
-
|
|
24
|
-
```javascript
|
|
25
|
-
import { PgDriver } from '@cheetah.js/orm';
|
|
26
|
-
import { ConnectionSettings } from '@cheetah.js/orm/driver/driver.interface';
|
|
27
|
-
|
|
28
|
-
const config: ConnectionSettings<any> = {
|
|
29
|
-
host: 'localhost',
|
|
30
|
-
port: 5432,
|
|
31
|
-
database: 'postgres',
|
|
32
|
-
username: 'postgres',
|
|
33
|
-
password: 'postgres',
|
|
34
|
-
driver: PgDriver,
|
|
35
|
-
migrationPath: 'path_migrations',
|
|
36
|
-
entities: 'entity/*.ts' // or [User, Post, ...]
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export default config;
|
|
40
|
-
```
|
|
41
|
-
Actually, the ORM only supports PostgreSQL, but in the future it will support other databases.
|
|
42
|
-
- Entities: Path to entities. Accepts glob patterns or an array of Entity classes.
|
|
43
|
-
- MigrationPath: Path to migrations. Accepts glob patterns. Is optional.
|
|
44
|
-
- Connection pool (optional): `max`, `idleTimeout`, `maxLifetime`, `connectionTimeout` in seconds control the Bun SQL pool. Defaults are 20 connections, 20s idle timeout, 300s max lifetime, and 10s connection timeout.
|
|
45
|
-
<br/>
|
|
46
|
-
<br/>
|
|
47
|
-
After that, you need to import the ORM into the project and add it to the Cheetah.js instance:
|
|
48
|
-
|
|
49
|
-
```javascript
|
|
50
|
-
import { Cheetah } from '@cheetah.js/core';
|
|
51
|
-
import { CheetahOrm } from '@cheetah.js/orm';
|
|
52
|
-
|
|
53
|
-
new Cheetah().use(CheetahOrm).listen();
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### [Entities](#entities)
|
|
57
|
-
Entities are classes that map to database tables. Each entity must have a primary key.
|
|
58
|
-
|
|
59
|
-
#### Example:
|
|
60
|
-
```javascript
|
|
61
|
-
import { Entity, PrimaryKey, Property } from '@cheetah.js/orm';
|
|
62
|
-
|
|
63
|
-
@Entity()
|
|
64
|
-
export class User {
|
|
65
|
-
@PrimaryKey()
|
|
66
|
-
id: number;
|
|
67
|
-
|
|
68
|
-
@Property()
|
|
69
|
-
name: string;
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
#### PrimaryKey
|
|
74
|
-
The @PrimaryKey decorator is used to define the primary key of the entity.
|
|
75
|
-
|
|
76
|
-
#### Nullable property
|
|
77
|
-
For define a nullable property, add a parameter to the @Property decorator:
|
|
78
|
-
|
|
79
|
-
```javascript
|
|
80
|
-
@Entity()
|
|
81
|
-
export class User {
|
|
82
|
-
@PrimaryKey()
|
|
83
|
-
id: number;
|
|
84
|
-
|
|
85
|
-
@Property({ nullable: true })
|
|
86
|
-
name: string;
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
Cheetah ORM can also distinguish nullable properties automatically by adding the question mark to the end of the property name:
|
|
90
|
-
|
|
91
|
-
```javascript
|
|
92
|
-
export class User {
|
|
93
|
-
@PrimaryKey()
|
|
94
|
-
id: number;
|
|
95
|
-
|
|
96
|
-
@Property()
|
|
97
|
-
name?:string;
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
#### Unique property
|
|
102
|
-
For define a unique property, add a parameter to the @Property decorator:
|
|
103
|
-
|
|
104
|
-
```javascript
|
|
105
|
-
@Entity()
|
|
106
|
-
export class User {
|
|
107
|
-
@PrimaryKey()
|
|
108
|
-
id: number;
|
|
109
|
-
|
|
110
|
-
@Property({ unique: true })
|
|
111
|
-
name: string;
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
#### Index property
|
|
116
|
-
For define a index for a unique property, add a parameter to the @Property decorator:
|
|
117
|
-
|
|
118
|
-
```javascript
|
|
119
|
-
@Entity()
|
|
120
|
-
export class User {
|
|
121
|
-
@PrimaryKey()
|
|
122
|
-
id: number;
|
|
123
|
-
|
|
124
|
-
@Property({ index: true })
|
|
125
|
-
name: string;
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
For define a index for a multiple properties, add the @Index decorator. You can use it on a property or on the class. It accepts either an array of property names (legacy) or an object with a properties field (recommended):
|
|
130
|
-
|
|
131
|
-
```javascript
|
|
132
|
-
@Entity()
|
|
133
|
-
export class User {
|
|
134
|
-
@PrimaryKey()
|
|
135
|
-
id: number;
|
|
136
|
-
|
|
137
|
-
@Property()
|
|
138
|
-
name: string;
|
|
139
|
-
|
|
140
|
-
// Property-level (compound index)
|
|
141
|
-
@Index({ properties: ['name', 'email'] })
|
|
142
|
-
@Property()
|
|
143
|
-
email: string;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Or, at the class level
|
|
147
|
-
|
|
148
|
-
@Entity()
|
|
149
|
-
@Index<{ User }>({ properties: ['name', 'email'] })
|
|
150
|
-
export class User {
|
|
151
|
-
@PrimaryKey()
|
|
152
|
-
id: number;
|
|
153
|
-
|
|
154
|
-
@Property()
|
|
155
|
-
name: string;
|
|
156
|
-
|
|
157
|
-
@Property()
|
|
158
|
-
email: string;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Backward compatible usage (array):
|
|
162
|
-
// @Index(['name', 'email'])
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Partial indexes (Postgres only) can be declared with `where`. You can provide a raw SQL string or a typed callback that receives the column map (with autocomplete based on your entity):
|
|
166
|
-
|
|
167
|
-
```javascript
|
|
168
|
-
@Entity()
|
|
169
|
-
@Index<{ User }>({
|
|
170
|
-
properties: ['email'],
|
|
171
|
-
where: (columns) => `${columns.isActive} = true`,
|
|
172
|
-
})
|
|
173
|
-
export class User {
|
|
174
|
-
@PrimaryKey()
|
|
175
|
-
id: number;
|
|
176
|
-
|
|
177
|
-
@Property()
|
|
178
|
-
email: string;
|
|
179
|
-
|
|
180
|
-
@Property()
|
|
181
|
-
isActive: boolean;
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
You can also use the ORM filter syntax in `where` (with `$in`, `$or`, `$nor`, etc.):
|
|
186
|
-
|
|
187
|
-
```javascript
|
|
188
|
-
@Entity()
|
|
189
|
-
@Index<{ User }>({
|
|
190
|
-
properties: ['email'],
|
|
191
|
-
where: {
|
|
192
|
-
isActive: true,
|
|
193
|
-
status: { $in: ['active', 'pending'] },
|
|
194
|
-
},
|
|
195
|
-
})
|
|
196
|
-
export class User {
|
|
197
|
-
@PrimaryKey()
|
|
198
|
-
id: number;
|
|
199
|
-
|
|
200
|
-
@Property()
|
|
201
|
-
email: string;
|
|
202
|
-
|
|
203
|
-
@Property()
|
|
204
|
-
isActive: boolean;
|
|
205
|
-
|
|
206
|
-
@Property()
|
|
207
|
-
status: string;
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Note: MySQL does not support partial indexes; using `where` with the MySQL driver will throw.
|
|
212
|
-
|
|
213
|
-
#### Property options
|
|
214
|
-
| Option | Type | Description |
|
|
215
|
-
| ------ | ---- |--------------------------------------------------------------------------------------------|
|
|
216
|
-
| nullable | boolean | Defines if the property is nullable. |
|
|
217
|
-
| unique | boolean | Defines if the property is unique. |
|
|
218
|
-
| index | boolean | Defines if the property is index. |
|
|
219
|
-
| default | any | Defines the default value of the property. |
|
|
220
|
-
| length | number | Defines the length of the property. |
|
|
221
|
-
| onUpdate | string | Define the action to be taken for this property when updating the entity in the database |
|
|
222
|
-
| onInsert | string | Defines the action to be taken for this property when inserting the entity in the database |
|
|
223
|
-
|
|
224
|
-
#### Computed Properties
|
|
225
|
-
The `@Computed` decorator allows you to define properties that are included in serialization but NOT persisted to the database. This is useful for derived values, formatted data, or any computation based on existing properties.
|
|
226
|
-
|
|
227
|
-
Computed properties are evaluated when the entity is serialized (e.g., `JSON.stringify()`) and are perfect for transforming or combining existing data.
|
|
228
|
-
|
|
229
|
-
##### Example:
|
|
230
|
-
```javascript
|
|
231
|
-
import { Entity, PrimaryKey, Property, Computed } from '@cheetah.js/orm';
|
|
232
|
-
|
|
233
|
-
@Entity()
|
|
234
|
-
export class Article {
|
|
235
|
-
@PrimaryKey()
|
|
236
|
-
id: number;
|
|
237
|
-
|
|
238
|
-
@Property()
|
|
239
|
-
title: string;
|
|
240
|
-
|
|
241
|
-
@Property()
|
|
242
|
-
body: string;
|
|
243
|
-
|
|
244
|
-
@Computed()
|
|
245
|
-
get excerpt() {
|
|
246
|
-
return this.body?.substring(0, 50) || '';
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
@Computed()
|
|
250
|
-
get titleUppercase() {
|
|
251
|
-
return this.title?.toUpperCase() || '';
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
When you serialize an article:
|
|
257
|
-
```javascript
|
|
258
|
-
const article = await Article.create({
|
|
259
|
-
title: 'My Article',
|
|
260
|
-
body: 'This is the full body text of the article...'
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
console.log(JSON.stringify(article));
|
|
264
|
-
// Output: {"id":1,"title":"My Article","body":"This is the full body text of the article...","excerpt":"This is the full body text of the article...","titleUppercase":"MY ARTICLE"}
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
**Important Notes:**
|
|
268
|
-
- Computed properties are **NOT** stored in the database
|
|
269
|
-
- They are evaluated during serialization (toJSON)
|
|
270
|
-
- Best used with getter functions
|
|
271
|
-
- Can access other properties and even other computed properties
|
|
272
|
-
- Can return any JSON-serializable type (string, number, object, array, etc.)
|
|
273
|
-
|
|
274
|
-
### [Hooks](#hooks)
|
|
275
|
-
Cheetah ORM supports hooks for entities. The available hooks are: BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate, BeforeDelete, AfterDelete.
|
|
276
|
-
Hooks is only for modify the entity, not for create, update or delete another entities statements in database.
|
|
277
|
-
|
|
278
|
-
### Example:
|
|
279
|
-
```javascript
|
|
280
|
-
import { Entity, PrimaryKey, Property, BeforeCreate } from '@cheetah.js/orm';
|
|
281
|
-
|
|
282
|
-
@Entity()
|
|
283
|
-
export class User {
|
|
284
|
-
@PrimaryKey()
|
|
285
|
-
id: number;
|
|
286
|
-
|
|
287
|
-
@Property()
|
|
288
|
-
name: string;
|
|
289
|
-
|
|
290
|
-
@BeforeCreate()
|
|
291
|
-
static beforeCreate() {
|
|
292
|
-
this.name = 'John Doe';
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
#### Value Objects
|
|
298
|
-
A Value Object is an immutable type that is distinguishable only by the state of its properties. That is, unlike an Entity, which has a unique identifier and remains distinct even if its properties are otherwise identical, two Value Objects with the exact same properties can be considered equal.
|
|
299
|
-
Cheetah ORM Entities support Value Objects. To define a Value Object, extends the ValueObject class:
|
|
300
|
-
|
|
301
|
-
```javascript
|
|
302
|
-
import { ValueObject } from '@cheetah.js/orm';
|
|
303
|
-
|
|
304
|
-
export class Name extends ValueObject<string, Name> { // First type is a value scalar type,
|
|
305
|
-
// and second is a ValueObject
|
|
306
|
-
|
|
307
|
-
validate(value): boolean {
|
|
308
|
-
return value.length > 0; // Any validation
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const name = new Name('John Doe');
|
|
313
|
-
const name2 = Name.from('John Doe'); // Same as above
|
|
314
|
-
|
|
315
|
-
console.log(name.equals(name2)); // true
|
|
316
|
-
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Caching
|
|
320
|
-
You can cache SELECT queries by providing the `cache` option in find methods or QueryBuilder:
|
|
321
|
-
|
|
322
|
-
- `cache: true` keeps the result cached using the driver default policy
|
|
323
|
-
- `cache: number` sets a TTL in milliseconds
|
|
324
|
-
- `cache: Date` sets an absolute expiration date
|
|
325
|
-
|
|
326
|
-
Examples:
|
|
327
|
-
|
|
328
|
-
```ts
|
|
329
|
-
// Cache with TTL (5 seconds)
|
|
330
|
-
await repo.find({ where: { name: 'John' }, cache: 5000 });
|
|
331
|
-
|
|
332
|
-
// Cache until a specific date
|
|
333
|
-
await repo.find({ where: { name: 'John' }, cache: new Date(Date.now() + 60_000) });
|
|
334
|
-
|
|
335
|
-
// Infinite/driver-default cache
|
|
336
|
-
await repo.find({ where: { name: 'John' }, cache: true });
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### Identity Map
|
|
340
|
-
|
|
341
|
-
The Identity Map is an in-memory cache that ensures each entity is loaded only once per request context. This pattern reduces database queries and guarantees that all references to the same entity point to the same object instance.
|
|
342
|
-
|
|
343
|
-
#### Key Benefits
|
|
344
|
-
|
|
345
|
-
- **Reduced Database Queries**: When querying for an entity that was already loaded in the same context, the cached instance is returned instead of executing another query
|
|
346
|
-
- **Consistent Entity References**: All parts of your code working with the same entity will share the same instance
|
|
347
|
-
- **Memory Efficient**: Uses WeakRef internally, allowing garbage collection of unreferenced entities
|
|
348
|
-
- **Per-Request Isolation**: Each request has its own identity map, preventing data leakage between requests
|
|
349
|
-
|
|
350
|
-
#### Automatic Activation (Recommended)
|
|
351
|
-
|
|
352
|
-
**The identity map is automatically enabled for all routes when you use `CheetahOrm`!** No additional configuration needed:
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
import { Cheetah } from '@cheetah.js/core';
|
|
356
|
-
import { CheetahOrm } from '@cheetah.js/orm';
|
|
357
|
-
|
|
358
|
-
new Cheetah()
|
|
359
|
-
.use(CheetahOrm) // ← Identity map automatically active for all routes
|
|
360
|
-
.listen();
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
Now all your controllers automatically benefit from the identity map:
|
|
364
|
-
|
|
365
|
-
```typescript
|
|
366
|
-
import { Controller, Get } from '@cheetah.js/core';
|
|
367
|
-
|
|
368
|
-
@Controller('/users')
|
|
369
|
-
export class UserController {
|
|
370
|
-
@Get('/:id/posts')
|
|
371
|
-
async getUserPosts(id: number) {
|
|
372
|
-
// Identity map is AUTOMATICALLY active - no decorator needed!
|
|
373
|
-
const user = await User.findOne({ id });
|
|
374
|
-
const posts = await Post.findAll({ userId: id }, { load: ['user'] });
|
|
375
|
-
|
|
376
|
-
// posts[0].user === user (same instance, no extra query)
|
|
377
|
-
return { user, posts };
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
**That's it!** The identity map works transparently across your entire application.
|
|
383
|
-
|
|
384
|
-
#### Manual Usage (Advanced)
|
|
385
|
-
|
|
386
|
-
For custom scenarios, use `identityMapContext.run()` directly:
|
|
387
|
-
|
|
388
|
-
```typescript
|
|
389
|
-
import { identityMapContext } from '@cheetah.js/orm';
|
|
390
|
-
|
|
391
|
-
async function processUserData(userId: number) {
|
|
392
|
-
await identityMapContext.run(async () => {
|
|
393
|
-
// All queries within this context share the same identity map
|
|
394
|
-
const user = await User.findOne({ id: userId });
|
|
395
|
-
const posts = await Post.findAll({ userId }, { load: ['user'] });
|
|
396
|
-
|
|
397
|
-
// posts[0].user === user (same instance, no extra query)
|
|
398
|
-
return { user, posts };
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
#### How It Works
|
|
404
|
-
|
|
405
|
-
```typescript
|
|
406
|
-
await identityMapContext.run(async () => {
|
|
407
|
-
// First query - fetches from database and caches
|
|
408
|
-
const user1 = await User.findOne({ id: 1 });
|
|
409
|
-
|
|
410
|
-
// Second query - returns cached instance (no database query)
|
|
411
|
-
const user2 = await User.findOne({ id: 1 });
|
|
412
|
-
|
|
413
|
-
console.log(user1 === user2); // true - same object instance
|
|
414
|
-
|
|
415
|
-
// Modifications are reflected everywhere
|
|
416
|
-
user1.name = 'Updated Name';
|
|
417
|
-
console.log(user2.name); // 'Updated Name'
|
|
418
|
-
});
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
#### Relationship Loading
|
|
422
|
-
|
|
423
|
-
The identity map automatically caches entities loaded through relationships:
|
|
424
|
-
|
|
425
|
-
```typescript
|
|
426
|
-
await identityMapContext.run(async () => {
|
|
427
|
-
// Load user first
|
|
428
|
-
const user = await User.findOne({ id: 1 });
|
|
429
|
-
|
|
430
|
-
// Load posts with user relationship
|
|
431
|
-
const posts = await Post.findAll(
|
|
432
|
-
{ userId: 1 },
|
|
433
|
-
{ load: ['user'] }
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
// The user loaded through posts is the same cached instance
|
|
437
|
-
console.log(posts[0].user === user); // true
|
|
438
|
-
});
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
#### Context Isolation
|
|
442
|
-
|
|
443
|
-
Each `identityMapContext.run()` creates an isolated scope:
|
|
444
|
-
|
|
445
|
-
```typescript
|
|
446
|
-
let user1, user2;
|
|
447
|
-
|
|
448
|
-
// First context
|
|
449
|
-
await identityMapContext.run(async () => {
|
|
450
|
-
user1 = await User.findOne({ id: 1 });
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
// Second context - completely separate identity map
|
|
454
|
-
await identityMapContext.run(async () => {
|
|
455
|
-
user2 = await User.findOne({ id: 1 });
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
// Different contexts = different instances
|
|
459
|
-
console.log(user1 === user2); // false
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
#### Without Identity Map Context
|
|
463
|
-
|
|
464
|
-
When not using `identityMapContext.run()`, the ORM behaves normally without caching:
|
|
465
|
-
|
|
466
|
-
```typescript
|
|
467
|
-
// Without context wrapper
|
|
468
|
-
const user1 = await User.findOne({ id: 1 });
|
|
469
|
-
const user2 = await User.findOne({ id: 1 });
|
|
470
|
-
|
|
471
|
-
console.log(user1 === user2); // false - different instances
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
#### Advanced: Disabling or Customizing
|
|
475
|
-
|
|
476
|
-
If you need to disable the identity map for specific routes, you can use manual context management:
|
|
477
|
-
|
|
478
|
-
```typescript
|
|
479
|
-
import { Controller, Get, Middleware } from '@cheetah.js/core';
|
|
480
|
-
import { identityMapContext, IdentityMapMiddleware } from '@cheetah.js/orm';
|
|
481
|
-
|
|
482
|
-
@Controller('/users')
|
|
483
|
-
export class UserController {
|
|
484
|
-
@Get('/:id')
|
|
485
|
-
async getUser(id: number) {
|
|
486
|
-
// Identity map active (global middleware)
|
|
487
|
-
return User.findOne({ id });
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
@Get('/legacy')
|
|
491
|
-
async getLegacyUsers() {
|
|
492
|
-
// To bypass identity map, just query normally
|
|
493
|
-
// The global middleware is still active, but you can
|
|
494
|
-
// control when to use it via manual context management
|
|
495
|
-
return User.findAll({});
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
For other frameworks (Express, Fastify, etc.), use the manual approach:
|
|
501
|
-
|
|
502
|
-
```typescript
|
|
503
|
-
// Express example
|
|
504
|
-
import { identityMapContext } from '@cheetah.js/orm';
|
|
505
|
-
|
|
506
|
-
app.use(async (req, res, next) => {
|
|
507
|
-
await identityMapContext.run(async () => {
|
|
508
|
-
await next();
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
#### Performance Considerations
|
|
514
|
-
|
|
515
|
-
- The identity map uses O(1) lookup time via Map-based storage
|
|
516
|
-
- WeakRef ensures entities can be garbage collected when no longer referenced
|
|
517
|
-
- FinalizationRegistry automatically cleans up expired cache entries
|
|
518
|
-
- Per-request scope prevents memory buildup across requests
|
|
519
|
-
- No configuration needed - works transparently with existing queries
|
|
520
|
-
|
|
521
|
-
Is Required to implement the validate method, that returns a boolean value.
|
|
522
|
-
To use the Value Object in the Entity, just add the ValueObject type to the property:
|
|
523
|
-
|
|
524
|
-
```javascript
|
|
525
|
-
import { Entity, PrimaryKey, Property } from '@cheetah.js/orm';
|
|
526
|
-
|
|
527
|
-
@Entity()
|
|
528
|
-
export class User {
|
|
529
|
-
@PrimaryKey()
|
|
530
|
-
id: number;
|
|
531
|
-
|
|
532
|
-
@Property()
|
|
533
|
-
name: Name;
|
|
534
|
-
}
|
|
535
|
-
```
|
|
536
|
-
Cheetah ORM will automatically convert the Value Object to the database type and vice versa.<br>
|
|
537
|
-
Important: If you value object is different from string type, you need to define the database type in the @Property decorator, because the Cheetah ORM would not know the correct type of your value object:
|
|
538
|
-
|
|
539
|
-
```javascript
|
|
540
|
-
import { Entity, PrimaryKey, Property } from '@cheetah.js/orm';
|
|
541
|
-
|
|
542
|
-
@Entity()
|
|
543
|
-
export class User {
|
|
544
|
-
@PrimaryKey()
|
|
545
|
-
id: number;
|
|
546
|
-
|
|
547
|
-
@Property({ type: 'json' })
|
|
548
|
-
name: Name;
|
|
549
|
-
}
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
#### Relations
|
|
553
|
-
Cheetah ORM supports relations between entities. The available relations are: OneToMany, ManyToOne.
|
|
554
|
-
|
|
555
|
-
##### OneToMany
|
|
556
|
-
The OneToMany relation is used to define a one-to-many relationship between two entities. For example, a user can have multiple posts, but a post can only have one user.
|
|
557
|
-
|
|
558
|
-
```javascript
|
|
559
|
-
@Entity()
|
|
560
|
-
export class User {
|
|
561
|
-
@PrimaryKey()
|
|
562
|
-
id: number;
|
|
563
|
-
|
|
564
|
-
@Property()
|
|
565
|
-
name: string;
|
|
566
|
-
|
|
567
|
-
@OneToMany(() => Post, (post) => post.user)
|
|
568
|
-
posts: Post[];
|
|
569
|
-
}
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
#### ManyToOne
|
|
573
|
-
The owner side of the relation is the side that has the @ManyToOne decorator. The inverse side is the side that has the @OneToMany decorator. The owner side is always the side that has the foreign key.
|
|
574
|
-
|
|
575
|
-
```javascript
|
|
576
|
-
@Entity()
|
|
577
|
-
export class Post {
|
|
578
|
-
@PrimaryKey()
|
|
579
|
-
id: number;
|
|
580
|
-
|
|
581
|
-
@Property()
|
|
582
|
-
title: string;
|
|
583
|
-
|
|
584
|
-
@ManyToOne(() => User)
|
|
585
|
-
user: User;
|
|
586
|
-
}
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
### [Usage](#usage)
|
|
590
|
-
#### Create a new entity
|
|
591
|
-
```javascript
|
|
592
|
-
import { User } from './entity/user';
|
|
593
|
-
|
|
594
|
-
const user = User.create({ name: 'John Doe' });
|
|
595
|
-
|
|
596
|
-
// OR
|
|
597
|
-
const user = new User();
|
|
598
|
-
user.name = 'John Doe';
|
|
599
|
-
await user.save();
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
#### Find a entity
|
|
603
|
-
```javascript
|
|
604
|
-
import { User } from './entity/user';
|
|
605
|
-
|
|
606
|
-
const user = await User.findOne({
|
|
607
|
-
name: 'John Doe',
|
|
608
|
-
old: { $gte: 16, $lte: 30 }
|
|
609
|
-
});
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
#### Transactions
|
|
613
|
-
```typescript
|
|
614
|
-
import { Orm } from '@cheetah.js/orm';
|
|
615
|
-
|
|
616
|
-
const orm = Orm.getInstance();
|
|
617
|
-
|
|
618
|
-
await orm.transaction(async (tx) => {
|
|
619
|
-
await tx`INSERT INTO users (name) VALUES (${ 'Jane Doe' })`;
|
|
620
|
-
await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = ${ 1 }`;
|
|
621
|
-
});
|
|
622
|
-
```
|
|
623
|
-
The `transaction` method leverages the active driver implementation, ensuring consistent transactional semantics across supported databases.
|
|
624
|
-
|
|
625
|
-
#### List of supported operators
|
|
626
|
-
| Operator |Name | Description |
|
|
627
|
-
| ------ | ---- |--------------------------------------------------------------------------------------------|
|
|
628
|
-
| $eq | Equal | Matches values that are equal to a specified value. |
|
|
629
|
-
| $gt | Greater Than | Matches values that are greater than a specified value. |
|
|
630
|
-
| $gte | Greater Than or Equal | Matches values that are greater than or equal to a specified value. |
|
|
631
|
-
| $in | In | Matches any of the values specified in an array. |
|
|
632
|
-
| $lt | Less Than | Matches values that are less than a specified value. |
|
|
633
|
-
| $lte | Less Than or Equal | Matches values that are less than or equal to a specified value. |
|
|
634
|
-
| $ne | Not Equal | Matches all values that are not equal to a specified value. |
|
|
635
|
-
| $nin | Not In | Matches none of the values specified in an array. |
|
|
636
|
-
| $and | And | Joins query clauses with a logical AND returns all documents that match the conditions of both clauses. |
|
|
637
|
-
| $or | Or | Joins query clauses with a logical OR returns all documents that match the conditions of either clause. |
|
|
638
|
-
| $not | Not | Inverts the effect of a query expression and returns documents that do not match the query expression. |
|
|
639
|
-
|
|
640
|
-
#### Filter by Date columns
|
|
641
|
-
You can filter using JavaScript `Date` instances and the ORM translates them into precise SQL comparisons:
|
|
642
|
-
|
|
643
|
-
```typescript
|
|
644
|
-
const reinforcement = await User.findOne({
|
|
645
|
-
updatedAt: new Date('2024-06-01T00:00:00.000Z'),
|
|
646
|
-
});
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
### [Migrations](#migrations)
|
|
650
|
-
Cheetah ORM is capable of creating and running migrations.
|
|
651
|
-
To do this, you need to install our cli package:
|
|
652
|
-
|
|
653
|
-
```bash
|
|
654
|
-
bun install @cheetah.js/cli
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
You must have the connection configuration file in the project root "cheetah.config.ts".
|
|
658
|
-
To create a migration, run the command below:
|
|
659
|
-
|
|
660
|
-
```bash
|
|
661
|
-
bunx cli migration:generate
|
|
662
|
-
```
|
|
663
|
-
This command will create a migration file in the path defined in the configuration file, differentiating your entities created with the database.
|
|
664
|
-
|
|
665
|
-
#### Example:
|
|
666
|
-
```bash
|
|
667
|
-
bunx cli cheetah-orm migration:run
|
|
668
|
-
```
|
|
669
|
-
This command will run all migrations that have not yet been run.
|