@cheetah.js/orm 0.1.144 → 0.1.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,8 @@ Cheetah.js ORM is a simple and powerful ORM for Cheetah.js and Bun.
9
9
  - [Value Objects](#value-objects)
10
10
  - [Hooks](#hooks)
11
11
  - [Usage](#usage)
12
+ - [Caching](#caching)
13
+ - [Identity Map](#identity-map)
12
14
  - [Migrations](#migrations)
13
15
 
14
16
  ### [Installation](#install)
@@ -124,7 +126,7 @@ export class User {
124
126
  }
125
127
  ```
126
128
 
127
- 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):
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):
128
130
 
129
131
  ```javascript
130
132
  @Entity()
@@ -156,60 +158,60 @@ export class User {
156
158
  email: string;
157
159
  }
158
160
 
159
- // Backward compatible usage (array):
160
- // @Index(['name', 'email'])
161
- ```
162
-
163
- 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):
164
-
165
- ```javascript
166
- @Entity()
167
- @Index<{ User }>({
168
- properties: ['email'],
169
- where: (columns) => `${columns.isActive} = true`,
170
- })
171
- export class User {
172
- @PrimaryKey()
173
- id: number;
174
-
175
- @Property()
176
- email: string;
177
-
178
- @Property()
179
- isActive: boolean;
180
- }
181
- ```
182
-
183
- You can also use the ORM filter syntax in `where` (with `$in`, `$or`, `$nor`, etc.):
184
-
185
- ```javascript
186
- @Entity()
187
- @Index<{ User }>({
188
- properties: ['email'],
189
- where: {
190
- isActive: true,
191
- status: { $in: ['active', 'pending'] },
192
- },
193
- })
194
- export class User {
195
- @PrimaryKey()
196
- id: number;
197
-
198
- @Property()
199
- email: string;
200
-
201
- @Property()
202
- isActive: boolean;
203
-
204
- @Property()
205
- status: string;
206
- }
207
- ```
208
-
209
- Note: MySQL does not support partial indexes; using `where` with the MySQL driver will throw.
210
-
211
- #### Property options
212
- | Option | Type | Description |
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 |
213
215
  | ------ | ---- |--------------------------------------------------------------------------------------------|
214
216
  | nullable | boolean | Defines if the property is nullable. |
215
217
  | unique | boolean | Defines if the property is unique. |
@@ -333,6 +335,189 @@ await repo.find({ where: { name: 'John' }, cache: new Date(Date.now() + 60_000)
333
335
  // Infinite/driver-default cache
334
336
  await repo.find({ where: { name: 'John' }, cache: true });
335
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
+
336
521
  Is Required to implement the validate method, that returns a boolean value.
337
522
  To use the Value Object in the Entity, just add the ValueObject type to the property:
338
523
 
@@ -0,0 +1,9 @@
1
+ export type UniqueDefinition = {
2
+ name: string;
3
+ properties: string[];
4
+ };
5
+ type UniqueOptions<T> = {
6
+ properties: (keyof T)[];
7
+ } | (keyof T)[] | undefined;
8
+ export declare function Unique<T>(options?: UniqueOptions<T>): ClassDecorator & PropertyDecorator;
9
+ export {};
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Unique = Unique;
4
+ const core_1 = require("@cheetah.js/core");
5
+ function getCtor(target) {
6
+ return typeof target === "function" ? target : target.constructor;
7
+ }
8
+ function buildFromOptions(options) {
9
+ const props = Array.isArray(options) ? options : options?.properties;
10
+ if (!props || props.length === 0) {
11
+ return null;
12
+ }
13
+ const keys = props;
14
+ return {
15
+ name: `${keys.join('_')}_unique`,
16
+ properties: keys,
17
+ };
18
+ }
19
+ function buildFromProperty(propertyKey) {
20
+ const name = String(propertyKey);
21
+ return {
22
+ name: `${name}_unique`,
23
+ properties: [name],
24
+ };
25
+ }
26
+ function resolveUnique(options, propertyKey) {
27
+ const fromOptions = buildFromOptions(options);
28
+ if (fromOptions) {
29
+ return fromOptions;
30
+ }
31
+ if (!propertyKey) {
32
+ throw new Error("@Unique on class requires properties option");
33
+ }
34
+ return buildFromProperty(propertyKey);
35
+ }
36
+ function Unique(options) {
37
+ return (target, propertyKey) => {
38
+ const ctor = getCtor(target);
39
+ const uniques = [...(core_1.Metadata.get("uniques", ctor) || [])];
40
+ const unique = resolveUnique(options, propertyKey);
41
+ uniques.push(unique);
42
+ core_1.Metadata.set("uniques", uniques, ctor);
43
+ };
44
+ }
@@ -1,5 +1,5 @@
1
1
  import { PropertyOptions } from "../decorators/property.decorator";
2
- import { Relationship, SnapshotIndexInfo, SnapshotTable } from "../driver/driver.interface";
2
+ import { Relationship, SnapshotIndexInfo, SnapshotTable, SnapshotUniqueInfo } from "../driver/driver.interface";
3
3
  export type Property = {
4
4
  options: PropertyOptions;
5
5
  type: Function;
@@ -10,6 +10,7 @@ export type Options = {
10
10
  };
11
11
  hideProperties: string[];
12
12
  indexes?: SnapshotIndexInfo[];
13
+ uniques?: SnapshotUniqueInfo[];
13
14
  relations: Relationship<any>[];
14
15
  tableName: string;
15
16
  hooks?: {
@@ -77,6 +77,31 @@ function buildIndexWhere(where, columnMap) {
77
77
  const builder = new index_condition_builder_1.IndexConditionBuilder(columnMap);
78
78
  return builder.build(where);
79
79
  }
80
+ function mapUniqueDefinitions(uniques, entityName, columnMap) {
81
+ return uniques.map((unique) => toSnapshotUnique(unique, entityName, columnMap));
82
+ }
83
+ function toSnapshotUnique(unique, entityName, columnMap) {
84
+ const columns = resolveUniqueColumns(unique, columnMap);
85
+ const uniqueName = resolveUniqueName(unique.name, entityName, columns);
86
+ return {
87
+ table: entityName,
88
+ uniqueName,
89
+ columnName: columns.join(","),
90
+ };
91
+ }
92
+ function resolveUniqueColumns(unique, columnMap) {
93
+ return unique.properties.map((propName) => resolveUniqueColumn(propName, columnMap));
94
+ }
95
+ function resolveUniqueColumn(propName, columnMap) {
96
+ const mapped = columnMap[propName];
97
+ if (mapped) {
98
+ return mapped;
99
+ }
100
+ return (0, utils_1.toSnakeCase)(propName);
101
+ }
102
+ function resolveUniqueName(name, entityName, columns) {
103
+ return `${columns.join("_")}_unique`;
104
+ }
80
105
  let EntityStorage = EntityStorage_1 = class EntityStorage {
81
106
  constructor() {
82
107
  this.entities = new Map();
@@ -85,6 +110,7 @@ let EntityStorage = EntityStorage_1 = class EntityStorage {
85
110
  add(entity, properties, relations, hooks) {
86
111
  const entityName = entity.options?.tableName || (0, utils_1.toSnakeCase)(entity.target.name);
87
112
  const indexes = core_1.Metadata.get("indexes", entity.target) || [];
113
+ const uniques = core_1.Metadata.get("uniques", entity.target) || [];
88
114
  const columnMap = buildIndexColumnMap(properties, relations);
89
115
  this.entities.set(entity.target, {
90
116
  properties: properties,
@@ -93,6 +119,7 @@ let EntityStorage = EntityStorage_1 = class EntityStorage {
93
119
  .map(([key]) => key),
94
120
  relations,
95
121
  indexes: mapIndexDefinitions(indexes, entityName, columnMap),
122
+ uniques: mapUniqueDefinitions(uniques, entityName, columnMap),
96
123
  hooks,
97
124
  tableName: entityName,
98
125
  ...entity.options,
@@ -116,6 +143,7 @@ let EntityStorage = EntityStorage_1 = class EntityStorage {
116
143
  tableName: values.tableName,
117
144
  schema: values.schema || "public",
118
145
  indexes: values.indexes || [],
146
+ uniques: values.uniques || [],
119
147
  columns: this.snapshotColumns(values),
120
148
  };
121
149
  }
@@ -26,6 +26,13 @@ export declare class BunMysqlDriver extends BunDriverBase implements DriverInter
26
26
  name: string;
27
27
  properties?: string[];
28
28
  }, schema: string | undefined, tableName: string): string;
29
+ getCreateUniqueConstraint(unique: {
30
+ name: string;
31
+ properties?: string[];
32
+ }, schema: string | undefined, tableName: string): string;
33
+ getDropUniqueConstraint(unique: {
34
+ name: string;
35
+ }, schema: string | undefined, tableName: string): string;
29
36
  getAlterTableType(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
30
37
  getAlterTableDefaultInstruction(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
31
38
  getAlterTablePrimaryKeyInstruction(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
@@ -141,6 +141,17 @@ class BunMysqlDriver extends bun_driver_base_1.BunDriverBase {
141
141
  getDropIndex(index, schema, tableName) {
142
142
  return `ALTER TABLE \`${schema}\`.\`${tableName}\` DROP INDEX \`${index.name}\`;`;
143
143
  }
144
+ getCreateUniqueConstraint(unique, schema, tableName) {
145
+ const properties = unique.properties || [];
146
+ if (properties.length === 0) {
147
+ throw new Error("Unique properties are required.");
148
+ }
149
+ const columns = properties.map((prop) => `\`${prop}\``).join(', ');
150
+ return `ALTER TABLE \`${schema}\`.\`${tableName}\` ADD CONSTRAINT \`${unique.name}\` UNIQUE (${columns});`;
151
+ }
152
+ getDropUniqueConstraint(unique, schema, tableName) {
153
+ return `ALTER TABLE \`${schema}\`.\`${tableName}\` DROP INDEX \`${unique.name}\`;`;
154
+ }
144
155
  getAlterTableType(schema, tableName, colName, colDiff) {
145
156
  return `ALTER TABLE \`${schema}\`.\`${tableName}\` MODIFY COLUMN \`${colName}\` ${colDiff.colType}${colDiff.colLength ? `(${colDiff.colLength})` : ''};`;
146
157
  }
@@ -25,6 +25,13 @@ export declare class BunPgDriver extends BunDriverBase implements DriverInterfac
25
25
  name: string;
26
26
  properties?: string[];
27
27
  }, schema: string | undefined, tableName: string): string;
28
+ getCreateUniqueConstraint(unique: {
29
+ name: string;
30
+ properties?: string[];
31
+ }, schema: string | undefined, tableName: string): string;
32
+ getDropUniqueConstraint(unique: {
33
+ name: string;
34
+ }, schema: string | undefined, tableName: string): string;
28
35
  getAlterTableType(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
29
36
  getAlterTableDefaultInstruction(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
30
37
  getAlterTablePrimaryKeyInstruction(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
@@ -114,6 +114,17 @@ class BunPgDriver extends bun_driver_base_1.BunDriverBase {
114
114
  getDropIndex(index, schema, tableName) {
115
115
  return this.getDropConstraint(index, schema, tableName);
116
116
  }
117
+ getCreateUniqueConstraint(unique, schema, tableName) {
118
+ const properties = unique.properties || [];
119
+ if (properties.length === 0) {
120
+ throw new Error("Unique properties are required.");
121
+ }
122
+ const columns = properties.map((prop) => `"${prop}"`).join(', ');
123
+ return `ALTER TABLE "${schema}"."${tableName}" ADD CONSTRAINT "${unique.name}" UNIQUE (${columns});`;
124
+ }
125
+ getDropUniqueConstraint(unique, schema, tableName) {
126
+ return this.getDropConstraint(unique, schema, tableName);
127
+ }
117
128
  getAlterTableType(schema, tableName, colName, colDiff) {
118
129
  return `ALTER TABLE "${schema}"."${tableName}" ALTER COLUMN "${colName}" TYPE ${colDiff.colType}${colDiff.colLength ? `(${colDiff.colLength})` : ''};`;
119
130
  }
@@ -20,6 +20,8 @@ export interface DriverInterface {
20
20
  getAddColumn(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff, colDiffInstructions: string[]): void;
21
21
  getDropColumn(colDiffInstructions: string[], schema: string | undefined, tableName: string, colName: string): void;
22
22
  getDropIndex(index: IndexStatement, schema: string | undefined, tableName: string): string;
23
+ getCreateUniqueConstraint(unique: UniqueStatement, schema: string | undefined, tableName: string): string;
24
+ getDropUniqueConstraint(unique: UniqueStatement, schema: string | undefined, tableName: string): string;
23
25
  getAlterTableType(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
24
26
  getAlterTableDefaultInstruction(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
25
27
  getAlterTablePrimaryKeyInstruction(schema: string | undefined, tableName: string, colName: string, colDiff: ColDiff): string;
@@ -136,6 +138,7 @@ export type SnapshotTable = {
136
138
  schema?: string;
137
139
  columns: ColumnsInfo[];
138
140
  indexes: SnapshotIndexInfo[];
141
+ uniques?: SnapshotUniqueInfo[];
139
142
  foreignKeys?: ForeignKeyInfo[];
140
143
  };
141
144
  export type SnapshotIndexInfo = {
@@ -144,11 +147,20 @@ export type SnapshotIndexInfo = {
144
147
  columnName: string;
145
148
  where?: string;
146
149
  };
150
+ export type SnapshotUniqueInfo = {
151
+ table: string;
152
+ uniqueName: string;
153
+ columnName: string;
154
+ };
147
155
  export type IndexStatement = {
148
156
  name: string;
149
157
  properties?: string[];
150
158
  where?: string;
151
159
  };
160
+ export type UniqueStatement = {
161
+ name: string;
162
+ properties?: string[];
163
+ };
152
164
  export type ForeignKeyInfo = {
153
165
  referencedTableName: string;
154
166
  referencedColumnName: string;
package/dist/entry.js CHANGED
@@ -5,4 +5,9 @@ const core_1 = require("@cheetah.js/core");
5
5
  const orm_1 = require("./orm");
6
6
  const orm_service_1 = require("./orm.service");
7
7
  const entities_1 = require("./domain/entities");
8
- exports.CheetahOrm = new core_1.Cheetah({ exports: [orm_1.Orm, orm_service_1.OrmService, entities_1.EntityStorage] });
8
+ const identity_map_middleware_1 = require("./middleware/identity-map.middleware");
9
+ exports.CheetahOrm = new core_1.Cheetah({
10
+ exports: [orm_1.Orm, orm_service_1.OrmService, entities_1.EntityStorage],
11
+ providers: [identity_map_middleware_1.IdentityMapMiddleware],
12
+ globalMiddlewares: [identity_map_middleware_1.IdentityMapMiddleware],
13
+ });
@@ -0,0 +1,10 @@
1
+ export declare class EntityKeyGenerator {
2
+ private entityStorage;
3
+ constructor();
4
+ generate(entityClass: Function, pk: any): string;
5
+ generateForEntity(entity: any): string;
6
+ extractPrimaryKey(entity: any): any;
7
+ private getPrimaryKeyName;
8
+ private serializePrimaryKey;
9
+ private getClassName;
10
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EntityKeyGenerator = void 0;
4
+ const entities_1 = require("../domain/entities");
5
+ class EntityKeyGenerator {
6
+ constructor() {
7
+ this.entityStorage = entities_1.EntityStorage.getInstance();
8
+ }
9
+ generate(entityClass, pk) {
10
+ const className = this.getClassName(entityClass);
11
+ const keyValue = this.serializePrimaryKey(pk);
12
+ return `${className}:${keyValue}`;
13
+ }
14
+ generateForEntity(entity) {
15
+ const pk = this.extractPrimaryKey(entity);
16
+ return this.generate(entity.constructor, pk);
17
+ }
18
+ extractPrimaryKey(entity) {
19
+ const pkName = this.getPrimaryKeyName(entity.constructor);
20
+ return entity[pkName];
21
+ }
22
+ getPrimaryKeyName(entityClass) {
23
+ const options = this.entityStorage.get(entityClass);
24
+ if (!options) {
25
+ return 'id';
26
+ }
27
+ for (const prop in options.properties) {
28
+ const property = options.properties[prop];
29
+ if (property.options.isPrimary) {
30
+ return prop;
31
+ }
32
+ }
33
+ return 'id';
34
+ }
35
+ serializePrimaryKey(pk) {
36
+ if (Array.isArray(pk)) {
37
+ return pk.join(':');
38
+ }
39
+ return String(pk);
40
+ }
41
+ getClassName(entityClass) {
42
+ return entityClass.name;
43
+ }
44
+ }
45
+ exports.EntityKeyGenerator = EntityKeyGenerator;
@@ -0,0 +1,11 @@
1
+ export declare class EntityRegistry {
2
+ private store;
3
+ private finalizationRegistry;
4
+ constructor();
5
+ private setupFinalizationRegistry;
6
+ get<T>(key: string): T | undefined;
7
+ set<T extends object>(key: string, entity: T): void;
8
+ has(key: string): boolean;
9
+ remove(key: string): void;
10
+ clear(): void;
11
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EntityRegistry = void 0;
4
+ class EntityRegistry {
5
+ constructor() {
6
+ this.store = new Map();
7
+ this.setupFinalizationRegistry();
8
+ }
9
+ setupFinalizationRegistry() {
10
+ this.finalizationRegistry = new FinalizationRegistry((key) => {
11
+ this.store.delete(key);
12
+ });
13
+ }
14
+ get(key) {
15
+ const weakRef = this.store.get(key);
16
+ if (!weakRef) {
17
+ return undefined;
18
+ }
19
+ const entity = weakRef.deref();
20
+ if (!entity) {
21
+ this.store.delete(key);
22
+ return undefined;
23
+ }
24
+ return entity;
25
+ }
26
+ set(key, entity) {
27
+ const weakRef = new WeakRef(entity);
28
+ this.store.set(key, weakRef);
29
+ this.finalizationRegistry.register(entity, key, entity);
30
+ }
31
+ has(key) {
32
+ return this.get(key) !== undefined;
33
+ }
34
+ remove(key) {
35
+ this.store.delete(key);
36
+ }
37
+ clear() {
38
+ this.store.clear();
39
+ }
40
+ }
41
+ exports.EntityRegistry = EntityRegistry;
@@ -0,0 +1,9 @@
1
+ import { IdentityMap } from './identity-map';
2
+ export declare class IdentityMapContext {
3
+ private storage;
4
+ constructor();
5
+ run<T>(callback: () => Promise<T>): Promise<T>;
6
+ getIdentityMap(): IdentityMap | undefined;
7
+ hasContext(): boolean;
8
+ }
9
+ export declare const identityMapContext: IdentityMapContext;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.identityMapContext = exports.IdentityMapContext = void 0;
4
+ const async_hooks_1 = require("async_hooks");
5
+ const identity_map_1 = require("./identity-map");
6
+ class IdentityMapContext {
7
+ constructor() {
8
+ this.storage = new async_hooks_1.AsyncLocalStorage();
9
+ }
10
+ run(callback) {
11
+ const identityMap = new identity_map_1.IdentityMap();
12
+ return this.storage.run(identityMap, callback);
13
+ }
14
+ getIdentityMap() {
15
+ return this.storage.getStore();
16
+ }
17
+ hasContext() {
18
+ return this.storage.getStore() !== undefined;
19
+ }
20
+ }
21
+ exports.IdentityMapContext = IdentityMapContext;
22
+ exports.identityMapContext = new IdentityMapContext();
@@ -0,0 +1,5 @@
1
+ export declare class IdentityMapIntegration {
2
+ static getOrCreateInstance<T>(model: new () => T, primaryKey: any, factory: () => T): T;
3
+ static registerEntity<T extends object>(entity: T): void;
4
+ static getEntity<T>(entityClass: new () => T, primaryKey: any): T | undefined;
5
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IdentityMapIntegration = void 0;
4
+ const identity_map_context_1 = require("./identity-map-context");
5
+ class IdentityMapIntegration {
6
+ static getOrCreateInstance(model, primaryKey, factory) {
7
+ const identityMap = identity_map_context_1.identityMapContext.getIdentityMap();
8
+ if (!identityMap) {
9
+ return factory();
10
+ }
11
+ if (primaryKey === undefined || primaryKey === null) {
12
+ return factory();
13
+ }
14
+ const cached = identityMap.get(model, primaryKey);
15
+ if (cached) {
16
+ return cached;
17
+ }
18
+ const instance = factory();
19
+ return instance;
20
+ }
21
+ static registerEntity(entity) {
22
+ const identityMap = identity_map_context_1.identityMapContext.getIdentityMap();
23
+ if (!identityMap) {
24
+ return;
25
+ }
26
+ identityMap.set(entity);
27
+ }
28
+ static getEntity(entityClass, primaryKey) {
29
+ const identityMap = identity_map_context_1.identityMapContext.getIdentityMap();
30
+ if (!identityMap) {
31
+ return undefined;
32
+ }
33
+ return identityMap.get(entityClass, primaryKey);
34
+ }
35
+ }
36
+ exports.IdentityMapIntegration = IdentityMapIntegration;
@@ -0,0 +1,10 @@
1
+ export declare class IdentityMap {
2
+ private registry;
3
+ private keyGenerator;
4
+ constructor();
5
+ get<T>(entityClass: Function, pk: any): T | undefined;
6
+ set<T extends object>(entity: T): void;
7
+ has(entityClass: Function, pk: any): boolean;
8
+ remove(entityClass: Function, pk: any): void;
9
+ clear(): void;
10
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IdentityMap = void 0;
4
+ const entity_key_generator_1 = require("./entity-key-generator");
5
+ const entity_registry_1 = require("./entity-registry");
6
+ class IdentityMap {
7
+ constructor() {
8
+ this.registry = new entity_registry_1.EntityRegistry();
9
+ this.keyGenerator = new entity_key_generator_1.EntityKeyGenerator();
10
+ }
11
+ get(entityClass, pk) {
12
+ const key = this.keyGenerator.generate(entityClass, pk);
13
+ return this.registry.get(key);
14
+ }
15
+ set(entity) {
16
+ const key = this.keyGenerator.generateForEntity(entity);
17
+ this.registry.set(key, entity);
18
+ }
19
+ has(entityClass, pk) {
20
+ const key = this.keyGenerator.generate(entityClass, pk);
21
+ return this.registry.has(key);
22
+ }
23
+ remove(entityClass, pk) {
24
+ const key = this.keyGenerator.generate(entityClass, pk);
25
+ this.registry.remove(key);
26
+ }
27
+ clear() {
28
+ this.registry.clear();
29
+ }
30
+ }
31
+ exports.IdentityMap = IdentityMap;
@@ -0,0 +1,5 @@
1
+ export { IdentityMap } from './identity-map';
2
+ export { EntityKeyGenerator } from './entity-key-generator';
3
+ export { EntityRegistry } from './entity-registry';
4
+ export { IdentityMapContext, identityMapContext } from './identity-map-context';
5
+ export { IdentityMapIntegration } from './identity-map-integration';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IdentityMapIntegration = exports.identityMapContext = exports.IdentityMapContext = exports.EntityRegistry = exports.EntityKeyGenerator = exports.IdentityMap = void 0;
4
+ var identity_map_1 = require("./identity-map");
5
+ Object.defineProperty(exports, "IdentityMap", { enumerable: true, get: function () { return identity_map_1.IdentityMap; } });
6
+ var entity_key_generator_1 = require("./entity-key-generator");
7
+ Object.defineProperty(exports, "EntityKeyGenerator", { enumerable: true, get: function () { return entity_key_generator_1.EntityKeyGenerator; } });
8
+ var entity_registry_1 = require("./entity-registry");
9
+ Object.defineProperty(exports, "EntityRegistry", { enumerable: true, get: function () { return entity_registry_1.EntityRegistry; } });
10
+ var identity_map_context_1 = require("./identity-map-context");
11
+ Object.defineProperty(exports, "IdentityMapContext", { enumerable: true, get: function () { return identity_map_context_1.IdentityMapContext; } });
12
+ Object.defineProperty(exports, "identityMapContext", { enumerable: true, get: function () { return identity_map_context_1.identityMapContext; } });
13
+ var identity_map_integration_1 = require("./identity-map-integration");
14
+ Object.defineProperty(exports, "IdentityMapIntegration", { enumerable: true, get: function () { return identity_map_integration_1.IdentityMapIntegration; } });
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from './decorators/property.decorator';
3
3
  export * from './decorators/primary-key.decorator';
4
4
  export * from './decorators/one-many.decorator';
5
5
  export * from './decorators/index.decorator';
6
+ export * from './decorators/unique.decorator';
6
7
  export * from './decorators/event-hook.decorator';
7
8
  export * from './decorators/enum.decorator';
8
9
  export * from './decorators/computed.decorator';
@@ -23,3 +24,5 @@ export * from './common/email.vo';
23
24
  export * from './common/uuid';
24
25
  export * from './repository/Repository';
25
26
  export { transactionContext } from './transaction/transaction-context';
27
+ export { IdentityMapMiddleware } from './middleware/identity-map.middleware';
28
+ export { identityMapContext } from './identity-map';
package/dist/index.js CHANGED
@@ -14,12 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.transactionContext = exports.EntityStorage = void 0;
17
+ exports.identityMapContext = exports.IdentityMapMiddleware = exports.transactionContext = exports.EntityStorage = void 0;
18
18
  __exportStar(require("./decorators/entity.decorator"), exports);
19
19
  __exportStar(require("./decorators/property.decorator"), exports);
20
20
  __exportStar(require("./decorators/primary-key.decorator"), exports);
21
21
  __exportStar(require("./decorators/one-many.decorator"), exports);
22
22
  __exportStar(require("./decorators/index.decorator"), exports);
23
+ __exportStar(require("./decorators/unique.decorator"), exports);
23
24
  __exportStar(require("./decorators/event-hook.decorator"), exports);
24
25
  __exportStar(require("./decorators/enum.decorator"), exports);
25
26
  __exportStar(require("./decorators/computed.decorator"), exports);
@@ -41,3 +42,7 @@ __exportStar(require("./common/uuid"), exports);
41
42
  __exportStar(require("./repository/Repository"), exports);
42
43
  var transaction_context_1 = require("./transaction/transaction-context");
43
44
  Object.defineProperty(exports, "transactionContext", { enumerable: true, get: function () { return transaction_context_1.transactionContext; } });
45
+ var identity_map_middleware_1 = require("./middleware/identity-map.middleware");
46
+ Object.defineProperty(exports, "IdentityMapMiddleware", { enumerable: true, get: function () { return identity_map_middleware_1.IdentityMapMiddleware; } });
47
+ var identity_map_1 = require("./identity-map");
48
+ Object.defineProperty(exports, "identityMapContext", { enumerable: true, get: function () { return identity_map_1.identityMapContext; } });
@@ -0,0 +1,4 @@
1
+ import { CheetahMiddleware, Context } from '@cheetah.js/core';
2
+ export declare class IdentityMapMiddleware implements CheetahMiddleware {
3
+ handle(ctx: Context, next: () => void): Promise<void>;
4
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.IdentityMapMiddleware = void 0;
10
+ const core_1 = require("@cheetah.js/core");
11
+ const identity_map_1 = require("../identity-map");
12
+ let IdentityMapMiddleware = class IdentityMapMiddleware {
13
+ async handle(ctx, next) {
14
+ await identity_map_1.identityMapContext.run(async () => {
15
+ await next();
16
+ });
17
+ }
18
+ };
19
+ exports.IdentityMapMiddleware = IdentityMapMiddleware;
20
+ exports.IdentityMapMiddleware = IdentityMapMiddleware = __decorate([
21
+ (0, core_1.Injectable)()
22
+ ], IdentityMapMiddleware);
@@ -4,9 +4,14 @@ export declare class ModelTransformer {
4
4
  private entityStorage;
5
5
  constructor(entityStorage: EntityStorage);
6
6
  transform<T>(model: any, statement: Statement<any>, data: any): T;
7
+ private registerInstancesInIdentityMap;
7
8
  private createInstances;
8
9
  private createInstance;
9
10
  private addJoinedInstances;
11
+ private extractPrimaryKeyFromData;
12
+ private extractPrimaryKeyValue;
13
+ private getPrimaryKeyFromData;
14
+ private findPrimaryKeyProperty;
10
15
  private buildOptionsMap;
11
16
  private populateProperties;
12
17
  private parseColumnKey;
@@ -3,39 +3,77 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ModelTransformer = void 0;
4
4
  const value_object_1 = require("../common/value-object");
5
5
  const utils_1 = require("../utils");
6
+ const identity_map_1 = require("../identity-map");
6
7
  class ModelTransformer {
7
8
  constructor(entityStorage) {
8
9
  this.entityStorage = entityStorage;
9
10
  }
10
11
  transform(model, statement, data) {
11
- const instanceMap = this.createInstances(model, statement);
12
+ const instanceMap = this.createInstances(model, statement, data);
12
13
  const optionsMap = this.buildOptionsMap(instanceMap);
13
14
  this.populateProperties(data, instanceMap, optionsMap);
14
15
  this.linkJoinedEntities(statement, instanceMap, optionsMap);
15
16
  this.resetChangedValues(instanceMap);
17
+ this.registerInstancesInIdentityMap(instanceMap);
16
18
  return instanceMap[statement.alias];
17
19
  }
18
- createInstances(model, statement) {
19
- const instance = this.createInstance(model);
20
+ registerInstancesInIdentityMap(instanceMap) {
21
+ Object.values(instanceMap).forEach(instance => {
22
+ identity_map_1.IdentityMapIntegration.registerEntity(instance);
23
+ });
24
+ }
25
+ createInstances(model, statement, data) {
26
+ const primaryKey = this.extractPrimaryKeyFromData(model, statement.alias, data);
27
+ const instance = this.createInstance(model, primaryKey);
20
28
  const instanceMap = {
21
29
  [statement.alias]: instance,
22
30
  };
23
31
  if (statement.join) {
24
- this.addJoinedInstances(statement, instanceMap);
32
+ this.addJoinedInstances(statement, instanceMap, data);
25
33
  }
26
34
  return instanceMap;
27
35
  }
28
- createInstance(model) {
29
- const instance = new model();
30
- instance.$_isPersisted = true;
31
- return instance;
36
+ createInstance(model, primaryKey) {
37
+ return identity_map_1.IdentityMapIntegration.getOrCreateInstance(model, primaryKey, () => {
38
+ const instance = new model();
39
+ instance.$_isPersisted = true;
40
+ return instance;
41
+ });
32
42
  }
33
- addJoinedInstances(statement, instanceMap) {
43
+ addJoinedInstances(statement, instanceMap, data) {
34
44
  statement.join.forEach(join => {
35
- const joinInstance = this.createInstance(join.joinEntity);
45
+ const primaryKey = this.extractPrimaryKeyFromData(join.joinEntity, join.joinAlias, data);
46
+ const joinInstance = this.createInstance(join.joinEntity, primaryKey);
36
47
  instanceMap[join.joinAlias] = joinInstance;
37
48
  });
38
49
  }
50
+ extractPrimaryKeyFromData(model, alias, data) {
51
+ const options = this.entityStorage.get(model);
52
+ if (!options) {
53
+ return undefined;
54
+ }
55
+ return this.extractPrimaryKeyValue(options, alias, data);
56
+ }
57
+ extractPrimaryKeyValue(options, alias, data) {
58
+ const pkProperty = this.findPrimaryKeyProperty(options);
59
+ if (!pkProperty) {
60
+ return undefined;
61
+ }
62
+ return this.getPrimaryKeyFromData(pkProperty, alias, data);
63
+ }
64
+ getPrimaryKeyFromData(pkProperty, alias, data) {
65
+ const pkColumnName = pkProperty.options.columnName;
66
+ const pkKey = `${alias}_${pkColumnName}`;
67
+ return data[pkKey];
68
+ }
69
+ findPrimaryKeyProperty(options) {
70
+ for (const prop in options.properties) {
71
+ if (options.properties[prop].options.isPrimary) {
72
+ return options.properties[prop];
73
+ }
74
+ }
75
+ return null;
76
+ }
39
77
  buildOptionsMap(instanceMap) {
40
78
  const optionsMap = new Map();
41
79
  for (const [alias, instance] of Object.entries(instanceMap)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cheetah.js/orm",
3
- "version": "0.1.144",
3
+ "version": "0.1.147",
4
4
  "description": "A simple ORM for Cheetah.js.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
@@ -55,5 +55,5 @@
55
55
  "bun",
56
56
  "value-object"
57
57
  ],
58
- "gitHead": "73bccb890079ef872f5dcb9f9df1a1a9dfad5912"
58
+ "gitHead": "9efbea28c5bb4bdc91e8aa6ba03a634f0a4ab00f"
59
59
  }