@cqrs-ddd/mikro-orm 0.2.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/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +299 -0
- package/dist/cache/cache-entry.d.ts +43 -0
- package/dist/cache/cache-entry.d.ts.map +1 -0
- package/dist/cache/cache-entry.js +92 -0
- package/dist/cache/cache-entry.js.map +1 -0
- package/dist/cache/mikro-orm.cache.d.ts +82 -0
- package/dist/cache/mikro-orm.cache.d.ts.map +1 -0
- package/dist/cache/mikro-orm.cache.js +209 -0
- package/dist/cache/mikro-orm.cache.js.map +1 -0
- package/dist/concurrency/assert-autocommit.d.ts +21 -0
- package/dist/concurrency/assert-autocommit.d.ts.map +1 -0
- package/dist/concurrency/assert-autocommit.js +27 -0
- package/dist/concurrency/assert-autocommit.js.map +1 -0
- package/dist/concurrency/conditioned-write.d.ts +23 -0
- package/dist/concurrency/conditioned-write.d.ts.map +1 -0
- package/dist/concurrency/conditioned-write.js +26 -0
- package/dist/concurrency/conditioned-write.js.map +1 -0
- package/dist/concurrency/optimistic-delete.d.ts +52 -0
- package/dist/concurrency/optimistic-delete.d.ts.map +1 -0
- package/dist/concurrency/optimistic-delete.js +67 -0
- package/dist/concurrency/optimistic-delete.js.map +1 -0
- package/dist/concurrency/optimistic-update.d.ts +51 -0
- package/dist/concurrency/optimistic-update.d.ts.map +1 -0
- package/dist/concurrency/optimistic-update.js +60 -0
- package/dist/concurrency/optimistic-update.js.map +1 -0
- package/dist/errors/mikro-orm.dialect.d.ts +32 -0
- package/dist/errors/mikro-orm.dialect.d.ts.map +1 -0
- package/dist/errors/mikro-orm.dialect.js +96 -0
- package/dist/errors/mikro-orm.dialect.js.map +1 -0
- package/dist/errors/transient-error.d.ts +51 -0
- package/dist/errors/transient-error.d.ts.map +1 -0
- package/dist/errors/transient-error.js +97 -0
- package/dist/errors/transient-error.js.map +1 -0
- package/dist/helpers/sql-identifier.d.ts +19 -0
- package/dist/helpers/sql-identifier.d.ts.map +1 -0
- package/dist/helpers/sql-identifier.js +23 -0
- package/dist/helpers/sql-identifier.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/entity-manager-source.d.ts +19 -0
- package/dist/interfaces/entity-manager-source.d.ts.map +1 -0
- package/dist/interfaces/entity-manager-source.js +4 -0
- package/dist/interfaces/entity-manager-source.js.map +1 -0
- package/dist/mapping/root-entity.properties.d.ts +63 -0
- package/dist/mapping/root-entity.properties.d.ts.map +1 -0
- package/dist/mapping/root-entity.properties.js +63 -0
- package/dist/mapping/root-entity.properties.js.map +1 -0
- package/dist/mapping/unix-timestamp.type.d.ts +17 -0
- package/dist/mapping/unix-timestamp.type.d.ts.map +1 -0
- package/dist/mapping/unix-timestamp.type.js +55 -0
- package/dist/mapping/unix-timestamp.type.js.map +1 -0
- package/dist/repository/aggregate.repository.d.ts +68 -0
- package/dist/repository/aggregate.repository.d.ts.map +1 -0
- package/dist/repository/aggregate.repository.js +103 -0
- package/dist/repository/aggregate.repository.js.map +1 -0
- package/dist/tenancy/tenant-store.d.ts +63 -0
- package/dist/tenancy/tenant-store.d.ts.map +1 -0
- package/dist/tenancy/tenant-store.js +91 -0
- package/dist/tenancy/tenant-store.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ICache, IWriteSideAggregateRepository } from '@cqrs-ddd/core/application';
|
|
2
|
+
import type { RootEntity, RootEntitySnapshot } from '@cqrs-ddd/core/domain';
|
|
3
|
+
import { CommandRepository } from '@cqrs-ddd/core/persistence';
|
|
4
|
+
import type { EntityName } from '@mikro-orm/core';
|
|
5
|
+
import type { IEntityManagerSource } from '../interfaces/entity-manager-source';
|
|
6
|
+
/**
|
|
7
|
+
* Base class for the command repositories that load and save an existing aggregate.
|
|
8
|
+
*
|
|
9
|
+
* Provides authoritative aggregate hydration via {@link findById}:
|
|
10
|
+
* - Bypasses read-side caches (`@FromCache`) to prevent stale reads on mutation paths.
|
|
11
|
+
* - Forces MikroORM `{ refresh: true }` to avoid returning stale Unit-of-Work identity-map entities.
|
|
12
|
+
* - Translates low-level driver failures through {@link mapPersistenceError} into application-neutral transient signals.
|
|
13
|
+
*
|
|
14
|
+
* Concrete repositories extend this class, inject dependencies into `super(...)`, and provide their
|
|
15
|
+
* decorated `save()` method, usually through `@PersistedWrite`.
|
|
16
|
+
* The `store` is any {@link IEntityManagerSource}, typically the application's own
|
|
17
|
+
* store; its `em` is also what `save()` passes to `optimisticUpdate` or `optimisticDelete`.
|
|
18
|
+
*
|
|
19
|
+
* @typeParam TSnapshot - Snapshot structure of the aggregate.
|
|
20
|
+
* @typeParam TEntity - Domain aggregate class extending {@link RootEntity}.
|
|
21
|
+
* @typeParam TResult - Persisted result type returned by `save()`.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // A NestJS provider; STORE is the application's IEntityManagerSource token.
|
|
26
|
+
* @Injectable()
|
|
27
|
+
* export class UpdateUserCommandRepository extends AggregateRepository<
|
|
28
|
+
* UserSnapshot,
|
|
29
|
+
* User,
|
|
30
|
+
* UserSnapshot
|
|
31
|
+
* > {
|
|
32
|
+
* constructor(
|
|
33
|
+
* @Inject(CACHE_TOKEN) cache: ICache<UserSnapshot>,
|
|
34
|
+
* @Inject(STORE) store: IEntityManagerSource,
|
|
35
|
+
* ) {
|
|
36
|
+
* super(cache, store, User, User.aggregateName, User.fromJSON);
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* @PersistedWrite<User>({
|
|
40
|
+
* unique: { email: (user) => new UniqueEmailException(user) },
|
|
41
|
+
* })
|
|
42
|
+
* async save(user: User): Promise<UserSnapshot> {
|
|
43
|
+
* ...
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare abstract class AggregateRepository<TSnapshot extends Partial<RootEntitySnapshot>, TEntity extends RootEntity<TSnapshot>, TResult = unknown> extends CommandRepository<TEntity, TResult, TSnapshot> implements IWriteSideAggregateRepository<TEntity> {
|
|
49
|
+
protected readonly store: IEntityManagerSource;
|
|
50
|
+
protected readonly entityClass: EntityName<TEntity>;
|
|
51
|
+
protected readonly aggregateName: string;
|
|
52
|
+
protected readonly hydrateFn: (snapshot: TSnapshot) => TEntity;
|
|
53
|
+
constructor(cache: ICache<TSnapshot>, store: IEntityManagerSource, entityClass: EntityName<TEntity>, aggregateName: string, hydrateFn: (snapshot: TSnapshot) => TEntity);
|
|
54
|
+
/**
|
|
55
|
+
* Loads the authoritative aggregate directly from persistence.
|
|
56
|
+
*
|
|
57
|
+
* Enforces `{ refresh: true }` so that if the entity was already loaded in the active
|
|
58
|
+
* EntityManager's identity map, its columns are re-fetched from the database before
|
|
59
|
+
* domain mutations and optimistic concurrency checks take place. The manager comes
|
|
60
|
+
* from `store.em`, read for this call.
|
|
61
|
+
*
|
|
62
|
+
* @param id - The aggregate identifier.
|
|
63
|
+
* @returns Rehydrated aggregate instance, or `null` if not found.
|
|
64
|
+
* @throws {TransientOperationError} If a retryable database connection or driver error occurs.
|
|
65
|
+
*/
|
|
66
|
+
findById(id: string): Promise<TEntity | null>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=aggregate.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate.repository.d.ts","sourceRoot":"","sources":["../../src/repository/aggregate.repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,MAAM,EACN,6BAA6B,EAC9B,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EACL,iBAAiB,EAElB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,iBAAiB,CAAC;AAE/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,8BAAsB,mBAAmB,CACrC,SAAS,SAAS,OAAO,CAAC,kBAAkB,CAAC,EAC7C,OAAO,SAAS,UAAU,CAAC,SAAS,CAAC,EACrC,OAAO,GAAG,OAAO,CAEnB,SAAQ,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,CACrD,YAAW,6BAA6B,CAAC,OAAO,CAAC;IAI/C,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,oBAAoB;IAC9C,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,CAAC;IACnD,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,MAAM;IACxC,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,OAAO;gBAJ9D,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,EACL,KAAK,EAAE,oBAAoB,EAC3B,WAAW,EAAE,UAAU,CAAC,OAAO,CAAC,EAChC,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,OAAO;IAKhE;;;;;;;;;;;OAWG;IAYG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;CAcpD"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
4
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
5
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
6
|
+
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;
|
|
7
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
8
|
+
};
|
|
9
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
10
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.AggregateRepository = void 0;
|
|
14
|
+
const persistence_1 = require("@cqrs-ddd/core/persistence");
|
|
15
|
+
const transient_error_1 = require("../errors/transient-error");
|
|
16
|
+
/**
|
|
17
|
+
* Base class for the command repositories that load and save an existing aggregate.
|
|
18
|
+
*
|
|
19
|
+
* Provides authoritative aggregate hydration via {@link findById}:
|
|
20
|
+
* - Bypasses read-side caches (`@FromCache`) to prevent stale reads on mutation paths.
|
|
21
|
+
* - Forces MikroORM `{ refresh: true }` to avoid returning stale Unit-of-Work identity-map entities.
|
|
22
|
+
* - Translates low-level driver failures through {@link mapPersistenceError} into application-neutral transient signals.
|
|
23
|
+
*
|
|
24
|
+
* Concrete repositories extend this class, inject dependencies into `super(...)`, and provide their
|
|
25
|
+
* decorated `save()` method, usually through `@PersistedWrite`.
|
|
26
|
+
* The `store` is any {@link IEntityManagerSource}, typically the application's own
|
|
27
|
+
* store; its `em` is also what `save()` passes to `optimisticUpdate` or `optimisticDelete`.
|
|
28
|
+
*
|
|
29
|
+
* @typeParam TSnapshot - Snapshot structure of the aggregate.
|
|
30
|
+
* @typeParam TEntity - Domain aggregate class extending {@link RootEntity}.
|
|
31
|
+
* @typeParam TResult - Persisted result type returned by `save()`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // A NestJS provider; STORE is the application's IEntityManagerSource token.
|
|
36
|
+
* @Injectable()
|
|
37
|
+
* export class UpdateUserCommandRepository extends AggregateRepository<
|
|
38
|
+
* UserSnapshot,
|
|
39
|
+
* User,
|
|
40
|
+
* UserSnapshot
|
|
41
|
+
* > {
|
|
42
|
+
* constructor(
|
|
43
|
+
* @Inject(CACHE_TOKEN) cache: ICache<UserSnapshot>,
|
|
44
|
+
* @Inject(STORE) store: IEntityManagerSource,
|
|
45
|
+
* ) {
|
|
46
|
+
* super(cache, store, User, User.aggregateName, User.fromJSON);
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* @PersistedWrite<User>({
|
|
50
|
+
* unique: { email: (user) => new UniqueEmailException(user) },
|
|
51
|
+
* })
|
|
52
|
+
* async save(user: User): Promise<UserSnapshot> {
|
|
53
|
+
* ...
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
class AggregateRepository extends persistence_1.CommandRepository {
|
|
59
|
+
store;
|
|
60
|
+
entityClass;
|
|
61
|
+
aggregateName;
|
|
62
|
+
hydrateFn;
|
|
63
|
+
constructor(cache, store, entityClass, aggregateName, hydrateFn) {
|
|
64
|
+
super(cache);
|
|
65
|
+
this.store = store;
|
|
66
|
+
this.entityClass = entityClass;
|
|
67
|
+
this.aggregateName = aggregateName;
|
|
68
|
+
this.hydrateFn = hydrateFn;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Loads the authoritative aggregate directly from persistence.
|
|
72
|
+
*
|
|
73
|
+
* Enforces `{ refresh: true }` so that if the entity was already loaded in the active
|
|
74
|
+
* EntityManager's identity map, its columns are re-fetched from the database before
|
|
75
|
+
* domain mutations and optimistic concurrency checks take place. The manager comes
|
|
76
|
+
* from `store.em`, read for this call.
|
|
77
|
+
*
|
|
78
|
+
* @param id - The aggregate identifier.
|
|
79
|
+
* @returns Rehydrated aggregate instance, or `null` if not found.
|
|
80
|
+
* @throws {TransientOperationError} If a retryable database connection or driver error occurs.
|
|
81
|
+
*/
|
|
82
|
+
async findById(id) {
|
|
83
|
+
const entity = await this.store.em.findOne(this.entityClass, { id }, { refresh: true });
|
|
84
|
+
if (!entity) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const snapshot = entity.toJSON();
|
|
88
|
+
return this.hydrateFn(snapshot);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.AggregateRepository = AggregateRepository;
|
|
92
|
+
__decorate([
|
|
93
|
+
(0, persistence_1.MapPersistenceErrors)({
|
|
94
|
+
entity: ([id]) => id,
|
|
95
|
+
otherwise(error, id) {
|
|
96
|
+
return (0, transient_error_1.mapPersistenceError)(error, `loading ${this.aggregateName} ${id}`);
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
__metadata("design:type", Function),
|
|
100
|
+
__metadata("design:paramtypes", [String]),
|
|
101
|
+
__metadata("design:returntype", Promise)
|
|
102
|
+
], AggregateRepository.prototype, "findById", null);
|
|
103
|
+
//# sourceMappingURL=aggregate.repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate.repository.js","sourceRoot":"","sources":["../../src/repository/aggregate.repository.ts"],"names":[],"mappings":";AAAA,sEAAsE;;;;;;;;;;;;AAOtE,4DAGoC;AAEpC,+DAAgE;AAGhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAsB,mBAKpB,SAAQ,+BAA8C;IAKjC;IACA;IACA;IACA;IALrB,YACE,KAAwB,EACL,KAA2B,EAC3B,WAAgC,EAChC,aAAqB,EACrB,SAA2C;QAE9D,KAAK,CAAC,KAAK,CAAC,CAAC;QALM,UAAK,GAAL,KAAK,CAAsB;QAC3B,gBAAW,GAAX,WAAW,CAAqB;QAChC,kBAAa,GAAb,aAAa,CAAQ;QACrB,cAAS,GAAT,SAAS,CAAkC;IAGhE,CAAC;IAED;;;;;;;;;;;OAWG;IAYG,AAAN,KAAK,CAAC,QAAQ,CAAC,EAAU;QACvB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CACxC,IAAI,CAAC,WAAW,EAChB,EAAE,EAAE,EAA0B,EAC9B,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAC;QAEF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAe,CAAC;QAC9C,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC;CACF;AAvDD,kDAuDC;AAdO;IAXL,IAAA,kCAAoB,EAKnB;QACA,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE;QACpB,SAAS,CAAC,KAAK,EAAE,EAAE;YACjB,OAAO,IAAA,qCAAmB,EAAC,KAAK,EAAE,WAAW,IAAI,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC,CAAC;QAC3E,CAAC;KACF,CAAC;;;;mDAcD"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { EntityManager, MikroORM } from '@mikro-orm/core';
|
|
2
|
+
import type { IEntityManagerSource } from '../interfaces/entity-manager-source';
|
|
3
|
+
/**
|
|
4
|
+
* How tenants are kept apart: `'database'` — one ORM per tenant, each on its own
|
|
5
|
+
* database; `'schema'` — one ORM, each tenant a schema of its database.
|
|
6
|
+
*/
|
|
7
|
+
export type TenantIsolation = 'database' | 'schema';
|
|
8
|
+
/** Options of {@link TenantStore}. */
|
|
9
|
+
export interface TenantStoreOptions {
|
|
10
|
+
/** The active tenant. Throw when there is none: the store never picks one. */
|
|
11
|
+
tenant(): string;
|
|
12
|
+
/** The initialized ORM that owns `tenant`'s data. Throw for an unknown tenant. */
|
|
13
|
+
orm(tenant: string): MikroORM;
|
|
14
|
+
isolation: TenantIsolation;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The `EntityManager` source of a multi-tenant application: `em` and
|
|
18
|
+
* `transactional()` always act on the active tenant's database or schema, for
|
|
19
|
+
* `AggregateRepository`, `MikroOrmCache` and the application's own repositories.
|
|
20
|
+
*
|
|
21
|
+
* `em` reuses a contextual manager (a MikroORM `RequestContext` or an active
|
|
22
|
+
* transaction) only when it belongs to the tenant's ORM, configuration, driver
|
|
23
|
+
* and schema and no other tenant claimed it first; otherwise it forks one.
|
|
24
|
+
* `transactional()` always runs on a fresh fork. Every manager it hands out is
|
|
25
|
+
* recorded to its tenant outside the manager (a `WeakMap`), so a later request
|
|
26
|
+
* for another tenant cannot reuse it; MikroORM objects are never modified.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const store = new TenantStore({
|
|
31
|
+
* tenant: () => currentTenantId() ?? fail(),
|
|
32
|
+
* orm: (tenant) => orms.get(tenant) ?? fail(),
|
|
33
|
+
* isolation: 'database',
|
|
34
|
+
* });
|
|
35
|
+
* const repository = new UpdateUserRepository(cache, store);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare class TenantStore<TManager extends EntityManager = EntityManager> implements IEntityManagerSource {
|
|
39
|
+
private readonly options;
|
|
40
|
+
private readonly owners;
|
|
41
|
+
constructor(options: TenantStoreOptions);
|
|
42
|
+
/**
|
|
43
|
+
* The active tenant's manager: its contextual one when that may be reused,
|
|
44
|
+
* otherwise a new fork. Read it per operation, never keep it.
|
|
45
|
+
*
|
|
46
|
+
* @throws What `options.tenant` or `options.orm` throws.
|
|
47
|
+
*/
|
|
48
|
+
get em(): TManager;
|
|
49
|
+
/**
|
|
50
|
+
* Runs `work` in a transaction on a fork dedicated to this call, for the
|
|
51
|
+
* active tenant; commits when it resolves and rolls back when it rejects.
|
|
52
|
+
* What `options.tenant` or `options.orm` throws becomes its rejection.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* await store.transactional((em) => em.nativeDelete(CacheEntry, { key }));
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
transactional<T>(work: (em: TManager) => Promise<T>): Promise<T>;
|
|
60
|
+
private fork;
|
|
61
|
+
private canReuse;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=tenant-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-store.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAEhF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEpD,sCAAsC;AACtC,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,MAAM,IAAI,MAAM,CAAC;IACjB,kFAAkF;IAClF,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC9B,SAAS,EAAE,eAAe,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW,CAAC,QAAQ,SAAS,aAAa,GAAG,aAAa,CACrE,YAAW,oBAAoB;IAInB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;gBAE3B,OAAO,EAAE,kBAAkB;IAExD;;;;;OAKG;IACH,IAAI,EAAE,IAAI,QAAQ,CASjB;IAED;;;;;;;;;OASG;IACG,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAMtE,OAAO,CAAC,IAAI;IASZ,OAAO,CAAC,QAAQ;CAcjB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.TenantStore = void 0;
|
|
5
|
+
/**
|
|
6
|
+
* The `EntityManager` source of a multi-tenant application: `em` and
|
|
7
|
+
* `transactional()` always act on the active tenant's database or schema, for
|
|
8
|
+
* `AggregateRepository`, `MikroOrmCache` and the application's own repositories.
|
|
9
|
+
*
|
|
10
|
+
* `em` reuses a contextual manager (a MikroORM `RequestContext` or an active
|
|
11
|
+
* transaction) only when it belongs to the tenant's ORM, configuration, driver
|
|
12
|
+
* and schema and no other tenant claimed it first; otherwise it forks one.
|
|
13
|
+
* `transactional()` always runs on a fresh fork. Every manager it hands out is
|
|
14
|
+
* recorded to its tenant outside the manager (a `WeakMap`), so a later request
|
|
15
|
+
* for another tenant cannot reuse it; MikroORM objects are never modified.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const store = new TenantStore({
|
|
20
|
+
* tenant: () => currentTenantId() ?? fail(),
|
|
21
|
+
* orm: (tenant) => orms.get(tenant) ?? fail(),
|
|
22
|
+
* isolation: 'database',
|
|
23
|
+
* });
|
|
24
|
+
* const repository = new UpdateUserRepository(cache, store);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
class TenantStore {
|
|
28
|
+
options;
|
|
29
|
+
owners = new WeakMap();
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The active tenant's manager: its contextual one when that may be reused,
|
|
35
|
+
* otherwise a new fork. Read it per operation, never keep it.
|
|
36
|
+
*
|
|
37
|
+
* @throws What `options.tenant` or `options.orm` throws.
|
|
38
|
+
*/
|
|
39
|
+
get em() {
|
|
40
|
+
const tenant = this.options.tenant();
|
|
41
|
+
const orm = this.options.orm(tenant);
|
|
42
|
+
const context = contextOf(orm);
|
|
43
|
+
if (context && this.canReuse(context, orm, tenant)) {
|
|
44
|
+
if (!this.owners.has(context))
|
|
45
|
+
this.owners.set(context, tenant);
|
|
46
|
+
return context;
|
|
47
|
+
}
|
|
48
|
+
return this.fork(orm, tenant);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Runs `work` in a transaction on a fork dedicated to this call, for the
|
|
52
|
+
* active tenant; commits when it resolves and rolls back when it rejects.
|
|
53
|
+
* What `options.tenant` or `options.orm` throws becomes its rejection.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* await store.transactional((em) => em.nativeDelete(CacheEntry, { key }));
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
async transactional(work) {
|
|
61
|
+
const tenant = this.options.tenant();
|
|
62
|
+
const fork = this.fork(this.options.orm(tenant), tenant);
|
|
63
|
+
return fork.transactional((em) => work(em));
|
|
64
|
+
}
|
|
65
|
+
fork(orm, tenant) {
|
|
66
|
+
const em = orm.em.fork({
|
|
67
|
+
disableContextResolution: true,
|
|
68
|
+
...(this.options.isolation === 'schema' ? { schema: tenant } : {}),
|
|
69
|
+
});
|
|
70
|
+
this.owners.set(em, tenant);
|
|
71
|
+
return em;
|
|
72
|
+
}
|
|
73
|
+
canReuse(context, orm, tenant) {
|
|
74
|
+
const owner = this.owners.get(context);
|
|
75
|
+
return (context !== orm.em &&
|
|
76
|
+
(owner === undefined || owner === tenant) &&
|
|
77
|
+
(context.schema === undefined || context.schema === tenant) &&
|
|
78
|
+
context.config === orm.config &&
|
|
79
|
+
context.getDriver() === orm.em.getDriver());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.TenantStore = TenantStore;
|
|
83
|
+
function contextOf(orm) {
|
|
84
|
+
try {
|
|
85
|
+
return orm.em.getContext(false);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=tenant-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-store.js","sourceRoot":"","sources":["../../src/tenancy/tenant-store.ts"],"names":[],"mappings":";AAAA,sEAAsE;;;AAoBtE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,WAAW;IAKO;IAFZ,MAAM,GAAG,IAAI,OAAO,EAAkB,CAAC;IAExD,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAE5D;;;;;OAKG;IACH,IAAI,EAAE;QACJ,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;YACnD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAChE,OAAO,OAAmB,CAAC;QAC7B,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAChC,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,aAAa,CAAI,IAAkC;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,EAAc,CAAC,CAAC,CAAC;IAC1D,CAAC;IAEO,IAAI,CAAC,GAAa,EAAE,MAAc;QACxC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;YACrB,wBAAwB,EAAE,IAAI;YAC9B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAa,CAAC;QACf,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAC5B,OAAO,EAAE,CAAC;IACZ,CAAC;IAEO,QAAQ,CACd,OAAsB,EACtB,GAAa,EACb,MAAc;QAEd,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvC,OAAO,CACL,OAAO,KAAK,GAAG,CAAC,EAAE;YAClB,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,CAAC;YACzC,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC;YAC3D,OAAO,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM;YAC7B,OAAO,CAAC,SAAS,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,CAC3C,CAAC;IACJ,CAAC;CACF;AA/DD,kCA+DC;AAED,SAAS,SAAS,CAAC,GAAa;IAC9B,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cqrs-ddd/mikro-orm",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MikroORM persistence adapters for @cqrs-ddd/core: authoritative aggregate loading, version-conditioned writes, a revision-fenced cache and root-entity schema mapping.",
|
|
5
|
+
"author": "Aristotelis",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.0.0"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/aristoteliss/nestjs-pipeline.git",
|
|
13
|
+
"directory": "packages/ddd-mikro-orm"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/aristoteliss/nestjs-pipeline/tree/master/packages/ddd-mikro-orm#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/aristoteliss/nestjs-pipeline/issues"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ddd",
|
|
21
|
+
"cqrs",
|
|
22
|
+
"mikro-orm",
|
|
23
|
+
"repository",
|
|
24
|
+
"optimistic-concurrency",
|
|
25
|
+
"cache"
|
|
26
|
+
],
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"types": "dist/index.d.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"COMMERCIAL_LICENSE.txt"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@mikro-orm/core": "^7.2.1",
|
|
40
|
+
"@cqrs-ddd/core": "^0.2.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@mikro-orm/core": "^7.2.1",
|
|
44
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
45
|
+
"typescript": "^6.0.3",
|
|
46
|
+
"vitest": "^4.1.11",
|
|
47
|
+
"@cqrs-ddd/core": "^0.2.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
51
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
52
|
+
"rebuild": "pnpm run clean && pnpm run build",
|
|
53
|
+
"lint": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest"
|
|
56
|
+
}
|
|
57
|
+
}
|