@tsed/mikro-orm 8.0.1 → 8.0.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.
- package/package.json +13 -12
- package/src/MikroOrmModule.spec.ts +110 -0
- package/src/MikroOrmModule.ts +104 -0
- package/src/constants.ts +2 -0
- package/src/decorators/entityManager.spec.ts +98 -0
- package/src/decorators/entityManager.ts +25 -0
- package/src/decorators/orm.spec.ts +97 -0
- package/src/decorators/orm.ts +19 -0
- package/src/decorators/subscriber.spec.ts +27 -0
- package/src/decorators/subscriber.ts +12 -0
- package/src/decorators/transactional.spec.ts +34 -0
- package/src/decorators/transactional.ts +10 -0
- package/src/filters/OptimisticLockErrorFilter.spec.ts +33 -0
- package/src/filters/OptimisticLockErrorFilter.ts +19 -0
- package/src/index.ts +17 -0
- package/src/interceptors/TransactionalInterceptor.spec.ts +248 -0
- package/src/interceptors/TransactionalInterceptor.ts +130 -0
- package/src/interfaces/AfterTransactionCommit.ts +5 -0
- package/src/interfaces/BeforeTransactionCommit.ts +5 -0
- package/src/interfaces/RetryStrategy.ts +5 -0
- package/src/services/MikroOrmContext.spec.ts +95 -0
- package/src/services/MikroOrmContext.ts +29 -0
- package/src/services/MikroOrmFactory.ts +16 -0
- package/src/services/MikroOrmRegistry.spec.ts +121 -0
- package/src/services/MikroOrmRegistry.ts +62 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {EntityManager, IsolationLevel, MikroORM, OptimisticLockError} from "@mikro-orm/core";
|
|
2
|
+
import {InjectorService, InterceptorContext} from "@tsed/di";
|
|
3
|
+
import {Logger} from "@tsed/logger";
|
|
4
|
+
import {anyFunction, anything, deepEqual, instance, mock, objectContaining, reset, verify, when} from "ts-mockito";
|
|
5
|
+
|
|
6
|
+
import {RetryStrategy} from "../interfaces/RetryStrategy.js";
|
|
7
|
+
import {MikroOrmContext} from "../services/MikroOrmContext.js";
|
|
8
|
+
import {MikroOrmRegistry} from "../services/MikroOrmRegistry.js";
|
|
9
|
+
import {TransactionalInterceptor} from "./TransactionalInterceptor.js";
|
|
10
|
+
|
|
11
|
+
// AHDOC: https://github.com/NagRock/ts-mockito/issues/191
|
|
12
|
+
// TODO: avoid using ts-mockito
|
|
13
|
+
const resolvableInstance = <T extends object>(m: T): T =>
|
|
14
|
+
new Proxy<T>(instance(m), {
|
|
15
|
+
get(target, prop, receiver) {
|
|
16
|
+
if (["Symbol(Symbol.toPrimitive)", "then", "catch"].includes(prop.toString())) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return Reflect.get(target, prop, receiver);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("TransactionalInterceptor", () => {
|
|
25
|
+
const mockedInjectorService = mock<InjectorService>();
|
|
26
|
+
const mockedMikroOrmRegistry = mock<MikroOrmRegistry>();
|
|
27
|
+
const mockedMikroOrm = mock<MikroORM>();
|
|
28
|
+
const mockedEntityManager = mock<EntityManager>();
|
|
29
|
+
const mockedLogger = mock<Logger>();
|
|
30
|
+
const mockedRetryStrategy = mock<RetryStrategy>();
|
|
31
|
+
const mockedMikroOrmContext = mock<MikroOrmContext>();
|
|
32
|
+
const next = vi.fn();
|
|
33
|
+
|
|
34
|
+
let entityManger!: EntityManager;
|
|
35
|
+
let mikroOrm!: MikroORM;
|
|
36
|
+
let transactionalInterceptor!: TransactionalInterceptor;
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
next.mockReset();
|
|
40
|
+
reset<InjectorService | MikroOrmRegistry | MikroORM | EntityManager | Logger | RetryStrategy | MikroOrmContext>(
|
|
41
|
+
mockedInjectorService,
|
|
42
|
+
mockedMikroOrmRegistry,
|
|
43
|
+
mockedMikroOrm,
|
|
44
|
+
mockedEntityManager,
|
|
45
|
+
mockedLogger,
|
|
46
|
+
mockedMikroOrmContext,
|
|
47
|
+
mockedRetryStrategy
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
entityManger = resolvableInstance(mockedEntityManager);
|
|
52
|
+
mikroOrm = resolvableInstance(mockedMikroOrm);
|
|
53
|
+
when(mockedMikroOrm.em).thenReturn(entityManger);
|
|
54
|
+
|
|
55
|
+
transactionalInterceptor = new TransactionalInterceptor(
|
|
56
|
+
instance(mockedInjectorService),
|
|
57
|
+
instance(mockedMikroOrmRegistry),
|
|
58
|
+
instance(mockedMikroOrmContext),
|
|
59
|
+
instance(mockedLogger)
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("intercept", () => {
|
|
64
|
+
it("should run within a existing context", async () => {
|
|
65
|
+
// arrange
|
|
66
|
+
const context = {} as InterceptorContext;
|
|
67
|
+
|
|
68
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
69
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(true);
|
|
70
|
+
when(mockedMikroOrmContext.get(anything())).thenReturn(entityManger);
|
|
71
|
+
|
|
72
|
+
// act
|
|
73
|
+
await transactionalInterceptor.intercept(context, next);
|
|
74
|
+
|
|
75
|
+
// assert
|
|
76
|
+
verify(mockedEntityManager.transactional(anyFunction(), objectContaining({}))).once();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should set an isolation level", async () => {
|
|
80
|
+
// arrange
|
|
81
|
+
const context = {options: {isolationLevel: IsolationLevel.SERIALIZABLE}} as InterceptorContext;
|
|
82
|
+
|
|
83
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
84
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(true);
|
|
85
|
+
when(mockedMikroOrmContext.get(anything())).thenReturn(entityManger);
|
|
86
|
+
|
|
87
|
+
// act
|
|
88
|
+
await transactionalInterceptor.intercept(context, next);
|
|
89
|
+
|
|
90
|
+
// assert
|
|
91
|
+
verify(mockedEntityManager.transactional(anyFunction(), objectContaining({isolationLevel: IsolationLevel.SERIALIZABLE}))).once();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should run within a new context", async () => {
|
|
95
|
+
// arrange
|
|
96
|
+
const context = {} as InterceptorContext;
|
|
97
|
+
|
|
98
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
99
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(false);
|
|
100
|
+
when(mockedMikroOrmContext.get(anything())).thenReturn(entityManger);
|
|
101
|
+
when(mockedMikroOrmContext.run(anything(), anything())).thenCall((_: EntityManager[], func: (...args: unknown[]) => unknown) =>
|
|
102
|
+
func()
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// act
|
|
106
|
+
await transactionalInterceptor.intercept(context, next);
|
|
107
|
+
|
|
108
|
+
// assert
|
|
109
|
+
verify(mockedMikroOrmContext.run(deepEqual([entityManger]), anyFunction())).twice();
|
|
110
|
+
verify(mockedEntityManager.transactional(anyFunction(), objectContaining({}))).once();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should perform a task within a transaction", async () => {
|
|
114
|
+
// arrange
|
|
115
|
+
const context = {} as InterceptorContext;
|
|
116
|
+
|
|
117
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
118
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(true);
|
|
119
|
+
when(mockedMikroOrmContext.get(anything())).thenReturn(entityManger);
|
|
120
|
+
when(mockedEntityManager.transactional(anything(), anything())).thenCall((func: (...args: unknown[]) => unknown) => func());
|
|
121
|
+
|
|
122
|
+
// act
|
|
123
|
+
await transactionalInterceptor.intercept(context, next);
|
|
124
|
+
|
|
125
|
+
// assert
|
|
126
|
+
expect(next).toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should emit events in the right order", async () => {
|
|
130
|
+
// arrange
|
|
131
|
+
const context = {} as InterceptorContext;
|
|
132
|
+
|
|
133
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
134
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(true);
|
|
135
|
+
when(mockedMikroOrmContext.get(anything())).thenReturn(entityManger);
|
|
136
|
+
when(mockedEntityManager.transactional(anything(), anything())).thenCall((func: (...args: unknown[]) => unknown) => func());
|
|
137
|
+
when(mockedMikroOrmContext.run(anything(), anything())).thenCall((_: EntityManager[], func: (...args: unknown[]) => unknown) =>
|
|
138
|
+
func()
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// act
|
|
142
|
+
await transactionalInterceptor.intercept(context, next);
|
|
143
|
+
|
|
144
|
+
// assert
|
|
145
|
+
verify(mockedInjectorService.alterAsync("$beforeTransactionCommit", entityManger)).calledBefore(
|
|
146
|
+
mockedInjectorService.alterAsync("$afterTransactionCommit", entityManger)
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should disable an explicit transaction", async () => {
|
|
151
|
+
// arrange
|
|
152
|
+
const context = {options: {disabled: true}} as InterceptorContext;
|
|
153
|
+
|
|
154
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
155
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(true);
|
|
156
|
+
when(mockedMikroOrmContext.get(anything())).thenReturn(entityManger);
|
|
157
|
+
|
|
158
|
+
// act
|
|
159
|
+
await transactionalInterceptor.intercept(context, next);
|
|
160
|
+
|
|
161
|
+
// assert
|
|
162
|
+
expect(next).toHaveBeenCalled();
|
|
163
|
+
verify(mockedEntityManager.transactional(anything(), anything())).never();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should throw an error if context is lost", async () => {
|
|
167
|
+
// arrange
|
|
168
|
+
const context = {} as InterceptorContext;
|
|
169
|
+
|
|
170
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
171
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(true);
|
|
172
|
+
when(mockedEntityManager.transactional(anything(), anything())).thenCall((func: (...args: unknown[]) => unknown) => func());
|
|
173
|
+
|
|
174
|
+
// act
|
|
175
|
+
const result = transactionalInterceptor.intercept(context, next);
|
|
176
|
+
|
|
177
|
+
// assert
|
|
178
|
+
await expect(result).rejects.toThrow("No such context");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should throw an error if no such context", async () => {
|
|
182
|
+
// arrange
|
|
183
|
+
const context = {} as InterceptorContext;
|
|
184
|
+
|
|
185
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn();
|
|
186
|
+
|
|
187
|
+
// act
|
|
188
|
+
const result = transactionalInterceptor.intercept(context, next);
|
|
189
|
+
|
|
190
|
+
// assert
|
|
191
|
+
await expect(result).rejects.toThrow("No such context");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should throw an optimistic lock exception immediately if no retry strategy", async () => {
|
|
195
|
+
// arrange
|
|
196
|
+
const context = {} as InterceptorContext;
|
|
197
|
+
|
|
198
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(true);
|
|
199
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
200
|
+
when(mockedEntityManager.transactional(anything(), anything())).thenReject(OptimisticLockError.lockFailed("Lock"));
|
|
201
|
+
when(mockedMikroOrmContext.get(anything())).thenReturn(entityManger);
|
|
202
|
+
|
|
203
|
+
// act
|
|
204
|
+
const result = transactionalInterceptor.intercept(context, next);
|
|
205
|
+
|
|
206
|
+
// assert
|
|
207
|
+
await expect(result).rejects.toThrow("Lock");
|
|
208
|
+
verify(mockedEntityManager.transactional(anyFunction(), objectContaining({}))).times(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should apply a retry mechanism if retry is enabled", async () => {
|
|
212
|
+
// arrange
|
|
213
|
+
transactionalInterceptor = new TransactionalInterceptor(
|
|
214
|
+
instance(mockedInjectorService),
|
|
215
|
+
instance(mockedMikroOrmRegistry),
|
|
216
|
+
instance(mockedMikroOrmContext),
|
|
217
|
+
instance(mockedLogger),
|
|
218
|
+
instance(mockedRetryStrategy)
|
|
219
|
+
);
|
|
220
|
+
const context = {options: {retry: true}} as InterceptorContext;
|
|
221
|
+
|
|
222
|
+
when(mockedMikroOrmContext.has(anything())).thenReturn(true);
|
|
223
|
+
when(mockedRetryStrategy.acquire(anything())).thenResolve();
|
|
224
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(mikroOrm);
|
|
225
|
+
when(mockedEntityManager.transactional(anything(), anything())).thenReject(OptimisticLockError.lockFailed("Lock"));
|
|
226
|
+
when(mockedMikroOrmContext.get(anything())).thenReturn(entityManger);
|
|
227
|
+
|
|
228
|
+
// act
|
|
229
|
+
await transactionalInterceptor.intercept(context, next);
|
|
230
|
+
|
|
231
|
+
// assert
|
|
232
|
+
verify(mockedRetryStrategy.acquire(anything())).once();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should log a warning if the retry is enabled, while a retry strategy is not resolved", async () => {
|
|
236
|
+
// arrange
|
|
237
|
+
const context = {options: {retry: true}} as InterceptorContext;
|
|
238
|
+
|
|
239
|
+
when(mockedMikroOrmRegistry.get(anything())).thenReturn(instance(mockedMikroOrm));
|
|
240
|
+
|
|
241
|
+
// act
|
|
242
|
+
await transactionalInterceptor.intercept(context, next);
|
|
243
|
+
|
|
244
|
+
// assert
|
|
245
|
+
verify(mockedLogger.warn(`To retry a transaction you have to implement a "${RetryStrategy.description}" interface`)).once();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {EntityManager, FlushMode, IsolationLevel} from "@mikro-orm/core";
|
|
2
|
+
import {Inject, InjectorService, Interceptor, InterceptorContext, InterceptorMethods, InterceptorNext} from "@tsed/di";
|
|
3
|
+
import {Logger} from "@tsed/logger";
|
|
4
|
+
|
|
5
|
+
import {DEFAULT_CONTEXT_NAME} from "../constants.js";
|
|
6
|
+
import {RetryStrategy} from "../interfaces/RetryStrategy.js";
|
|
7
|
+
import {MikroOrmContext} from "../services/MikroOrmContext.js";
|
|
8
|
+
import {MikroOrmRegistry} from "../services/MikroOrmRegistry.js";
|
|
9
|
+
|
|
10
|
+
export interface TransactionOptions {
|
|
11
|
+
retry?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
isolationLevel?: IsolationLevel;
|
|
14
|
+
flushMode?: FlushMode;
|
|
15
|
+
contextName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type TransactionSettings = Omit<TransactionOptions, "contextName"> & Required<Pick<TransactionOptions, "contextName">>;
|
|
19
|
+
|
|
20
|
+
@Interceptor()
|
|
21
|
+
export class TransactionalInterceptor implements InterceptorMethods {
|
|
22
|
+
constructor(
|
|
23
|
+
@Inject() private readonly injector: InjectorService,
|
|
24
|
+
@Inject() private readonly registry: MikroOrmRegistry,
|
|
25
|
+
@Inject() private readonly context: MikroOrmContext,
|
|
26
|
+
@Inject() private readonly logger: Logger,
|
|
27
|
+
@Inject(RetryStrategy)
|
|
28
|
+
private readonly retryStrategy?: RetryStrategy
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
public intercept(context: InterceptorContext<unknown>, next: InterceptorNext) {
|
|
32
|
+
const options = this.extractContextName(context.options);
|
|
33
|
+
|
|
34
|
+
if (options.retry && !this.retryStrategy) {
|
|
35
|
+
this.logger.warn(`To retry a transaction you have to implement a "${RetryStrategy.description}" interface`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return this.context.has(options.contextName) ? this.runWithinCtx(next, options) : this.runWithinNewCtx(next, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private runWithinCtx(next: InterceptorNext, options: TransactionSettings): Promise<unknown> {
|
|
42
|
+
const callback = () => this.executeInTransaction(next, options);
|
|
43
|
+
|
|
44
|
+
return options.retry && this.retryStrategy ? this.retryStrategy.acquire(callback) : callback();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async runWithinNewCtx(next: InterceptorNext, options: TransactionSettings): Promise<unknown> {
|
|
48
|
+
const em = await this.getGlobalEntityManager(options.contextName);
|
|
49
|
+
|
|
50
|
+
return this.context.run([em], () => this.runWithinCtx(next, options));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private getGlobalEntityManager(contextName: string = DEFAULT_CONTEXT_NAME): EntityManager {
|
|
54
|
+
const orm = this.registry.get(contextName);
|
|
55
|
+
|
|
56
|
+
if (!orm) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`No such context: ${contextName}. Please register a corresponding MikroOrm instance using '@Configuration()' decorator.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return orm.em;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private extractContextName(options: TransactionOptions | string | undefined): TransactionSettings {
|
|
66
|
+
if (typeof options === "string") {
|
|
67
|
+
return {
|
|
68
|
+
contextName: options
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const {flushMode, isolationLevel, contextName = DEFAULT_CONTEXT_NAME, retry = false, disabled = false} = options ?? {};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
contextName,
|
|
76
|
+
retry,
|
|
77
|
+
disabled,
|
|
78
|
+
flushMode,
|
|
79
|
+
isolationLevel
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async executeInTransaction(next: InterceptorNext, options: TransactionSettings): Promise<unknown> {
|
|
84
|
+
const em = this.getEntityManager(options.contextName);
|
|
85
|
+
|
|
86
|
+
if (options.disabled) {
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await em.transactional(() => this.executeHandlerDelegate(next, options), {
|
|
91
|
+
flushMode: options.flushMode,
|
|
92
|
+
isolationLevel: options.isolationLevel
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await this.afterTransactionCommit(options);
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async executeHandlerDelegate(next: InterceptorNext, options: TransactionSettings): Promise<unknown> {
|
|
101
|
+
const result = await next();
|
|
102
|
+
await this.beforeTransactionCommit(options);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private beforeTransactionCommit(options: TransactionSettings): Promise<unknown> {
|
|
107
|
+
return this.injector.alterAsync("$beforeTransactionCommit", this.getEntityManager(options.contextName));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private afterTransactionCommit(options: TransactionSettings): Promise<unknown> {
|
|
111
|
+
const em = this.getGlobalEntityManager(options.contextName);
|
|
112
|
+
|
|
113
|
+
return this.context.run([em], () => {
|
|
114
|
+
const forkedEm = this.getEntityManager(options.contextName);
|
|
115
|
+
forkedEm.resetTransactionContext();
|
|
116
|
+
|
|
117
|
+
return this.injector.alterAsync("$afterTransactionCommit", forkedEm);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private getEntityManager(contextName: string = DEFAULT_CONTEXT_NAME): EntityManager {
|
|
122
|
+
const em = this.context.get(contextName);
|
|
123
|
+
|
|
124
|
+
if (!em) {
|
|
125
|
+
throw new Error(`No such context: ${contextName}. Please check if the async context is lost in one of the asynchronous operations.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return em;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {EntityManager, RequestContext} from "@mikro-orm/core";
|
|
2
|
+
import {deepEqual, instance as originalInstance, mock, reset, spy, verify, when} from "ts-mockito";
|
|
3
|
+
|
|
4
|
+
import {MikroOrmContext} from "./MikroOrmContext.js";
|
|
5
|
+
|
|
6
|
+
const instance = <T extends object>(m: T): T =>
|
|
7
|
+
new Proxy<T>(originalInstance(m), {
|
|
8
|
+
get(target, prop, receiver) {
|
|
9
|
+
if (["Symbol(Symbol.toPrimitive)", "Symbol(Symbol.isConcatSpreadable)", "length"].includes(prop.toString())) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return Reflect.get(target, prop, receiver);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("MikroOrmContext", () => {
|
|
17
|
+
const mockedEntityManager = mock<EntityManager>();
|
|
18
|
+
|
|
19
|
+
let spiedRequestContext: typeof RequestContext;
|
|
20
|
+
let entityManager!: EntityManager;
|
|
21
|
+
let context!: MikroOrmContext;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
spiedRequestContext = spy(RequestContext);
|
|
25
|
+
entityManager = instance(mockedEntityManager);
|
|
26
|
+
context = new MikroOrmContext();
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => reset<typeof RequestContext | EntityManager>(mockedEntityManager, spiedRequestContext));
|
|
29
|
+
|
|
30
|
+
describe("get", () => {
|
|
31
|
+
it("should return undefined if not such context", () => {
|
|
32
|
+
// act
|
|
33
|
+
const result = context.get("unknown");
|
|
34
|
+
|
|
35
|
+
// assert
|
|
36
|
+
expect(result).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return entity manager if context is initialized", () => {
|
|
40
|
+
// arrange
|
|
41
|
+
when(spiedRequestContext.getEntityManager("context1")).thenReturn(entityManager);
|
|
42
|
+
|
|
43
|
+
// act
|
|
44
|
+
const result = context.get("context1");
|
|
45
|
+
|
|
46
|
+
// assert
|
|
47
|
+
expect(result).toBe(entityManager);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("wrap", () => {
|
|
52
|
+
it("should run a function within a mikro-orm context", async () => {
|
|
53
|
+
// arrange
|
|
54
|
+
const task = vi.fn();
|
|
55
|
+
|
|
56
|
+
// act
|
|
57
|
+
await context.run([entityManager], task);
|
|
58
|
+
|
|
59
|
+
// assert
|
|
60
|
+
verify(spiedRequestContext.create(deepEqual([entityManager]), task)).once();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should return a result of function execution", async () => {
|
|
64
|
+
// arrange
|
|
65
|
+
const task = vi.fn().mockResolvedValue("string");
|
|
66
|
+
|
|
67
|
+
// act
|
|
68
|
+
const result = await context.run([entityManager], task);
|
|
69
|
+
|
|
70
|
+
// assert
|
|
71
|
+
expect(result).toEqual("string");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("has", () => {
|
|
76
|
+
it("should return false if not such context", () => {
|
|
77
|
+
// act
|
|
78
|
+
const result = context.has("unknown");
|
|
79
|
+
|
|
80
|
+
// assert
|
|
81
|
+
expect(result).toBeFalsy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should return true if context is initialized", () => {
|
|
85
|
+
// arrange
|
|
86
|
+
when(spiedRequestContext.getEntityManager("context1")).thenReturn(entityManager);
|
|
87
|
+
|
|
88
|
+
// act
|
|
89
|
+
const result = context.has("context1");
|
|
90
|
+
|
|
91
|
+
// assert
|
|
92
|
+
expect(result).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {EntityManager, RequestContext} from "@mikro-orm/core";
|
|
2
|
+
import {isFunction} from "@tsed/core";
|
|
3
|
+
import {Injectable} from "@tsed/di";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @internal Since 2022-06-20.
|
|
7
|
+
*/
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class MikroOrmContext {
|
|
10
|
+
public run(managers: EntityManager[], task: (...args: unknown[]) => unknown): Promise<unknown> {
|
|
11
|
+
// @ts-expect-error `RequestContext.createAsync` has been removed in favour of `RequestContext.create` since mikro-orm v6.0.0
|
|
12
|
+
if (isFunction(RequestContext.createAsync)) {
|
|
13
|
+
// @ts-expect-error see above
|
|
14
|
+
return RequestContext.createAsync(managers, task);
|
|
15
|
+
} else {
|
|
16
|
+
return RequestContext.create(managers, task) as Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public get(contextName?: string): EntityManager | undefined {
|
|
21
|
+
return RequestContext.getEntityManager(contextName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public has(contextName?: string): boolean {
|
|
25
|
+
const manager = this.get(contextName);
|
|
26
|
+
|
|
27
|
+
return !!manager;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {MikroORM, Options} from "@mikro-orm/core";
|
|
2
|
+
import {Inject, Injectable} from "@tsed/di";
|
|
3
|
+
|
|
4
|
+
import {MikroOrmContext} from "./MikroOrmContext.js";
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class MikroOrmFactory {
|
|
8
|
+
constructor(@Inject() private readonly context: MikroOrmContext) {}
|
|
9
|
+
|
|
10
|
+
public create(options: Options): Promise<MikroORM> {
|
|
11
|
+
return MikroORM.init({
|
|
12
|
+
...options,
|
|
13
|
+
context: (name: string) => this.context.get(name)
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {MikroORM, Options} from "@mikro-orm/core";
|
|
2
|
+
import {Logger} from "@tsed/logger";
|
|
3
|
+
import {anything, instance, mock, reset, verify, when} from "ts-mockito";
|
|
4
|
+
|
|
5
|
+
import {MikroOrmFactory} from "./MikroOrmFactory.js";
|
|
6
|
+
import {MikroOrmRegistry} from "./MikroOrmRegistry.js";
|
|
7
|
+
|
|
8
|
+
const fixtures: {mydb2: Options; none: Options; mydb: Options} = {
|
|
9
|
+
mydb: {
|
|
10
|
+
contextName: "mydb",
|
|
11
|
+
type: "postgresql",
|
|
12
|
+
clientUrl: "postgresql://localhost:5432"
|
|
13
|
+
},
|
|
14
|
+
none: {
|
|
15
|
+
type: "postgresql",
|
|
16
|
+
clientUrl: "postgresql://localhost:5432"
|
|
17
|
+
},
|
|
18
|
+
mydb2: {
|
|
19
|
+
contextName: "mydb2",
|
|
20
|
+
type: "postgresql",
|
|
21
|
+
clientUrl: "postgresql://localhost:5432"
|
|
22
|
+
}
|
|
23
|
+
} as never;
|
|
24
|
+
|
|
25
|
+
describe("MikroOrmRegistry", () => {
|
|
26
|
+
const loggerMock = mock<Logger>();
|
|
27
|
+
const mikroOrmFactoryMock = mock<MikroOrmFactory>();
|
|
28
|
+
const mikroOrm = mock(MikroORM);
|
|
29
|
+
|
|
30
|
+
Object.values(fixtures).forEach((options: Options) => when(mikroOrmFactoryMock.create(options)).thenResolve(instance(mikroOrm)));
|
|
31
|
+
|
|
32
|
+
let mikroOrmRegistry!: MikroOrmRegistry;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mikroOrmRegistry = new MikroOrmRegistry(instance(loggerMock), instance(mikroOrmFactoryMock));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => reset<MikroORM | MikroOrmFactory>(mikroOrmFactoryMock, mikroOrm));
|
|
39
|
+
|
|
40
|
+
describe("register", () => {
|
|
41
|
+
it("should register instances", async () => {
|
|
42
|
+
// arrange
|
|
43
|
+
Object.values(fixtures).forEach((options: Options) => when(mikroOrmFactoryMock.create(options)).thenResolve(instance(mikroOrm)));
|
|
44
|
+
|
|
45
|
+
// act
|
|
46
|
+
const result1 = await mikroOrmRegistry.register(fixtures.mydb);
|
|
47
|
+
const result2 = await mikroOrmRegistry.register(fixtures.mydb);
|
|
48
|
+
const result3 = await mikroOrmRegistry.register(fixtures.none);
|
|
49
|
+
const result4 = await mikroOrmRegistry.register(fixtures.mydb2);
|
|
50
|
+
|
|
51
|
+
// assert
|
|
52
|
+
expect(result1).toEqual(mikroOrmRegistry.get("mydb"));
|
|
53
|
+
expect(result2).toEqual(mikroOrmRegistry.get("mydb"));
|
|
54
|
+
expect(result3).toEqual(mikroOrmRegistry.get("default"));
|
|
55
|
+
expect(result4).toEqual(mikroOrmRegistry.get("mydb2"));
|
|
56
|
+
verify(mikroOrmFactoryMock.create(anything())).thrice();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("clear", () => {
|
|
61
|
+
it("should dispose all instances", async () => {
|
|
62
|
+
// arrange
|
|
63
|
+
when(mikroOrm.isConnected()).thenResolve(true);
|
|
64
|
+
Object.values(fixtures).forEach((options: Options) => when(mikroOrmFactoryMock.create(options)).thenResolve(instance(mikroOrm)));
|
|
65
|
+
await mikroOrmRegistry.register(fixtures.none);
|
|
66
|
+
await mikroOrmRegistry.register(fixtures.mydb2);
|
|
67
|
+
|
|
68
|
+
// act
|
|
69
|
+
await mikroOrmRegistry.clear();
|
|
70
|
+
|
|
71
|
+
// assert
|
|
72
|
+
verify(mikroOrm.close(anything())).twice();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("get", () => {
|
|
77
|
+
it("should return corresponded instances", async () => {
|
|
78
|
+
// arrange
|
|
79
|
+
Object.values(fixtures).forEach((options: Options) => when(mikroOrmFactoryMock.create(options)).thenResolve(instance(mikroOrm)));
|
|
80
|
+
const none = await mikroOrmRegistry.register(fixtures.none);
|
|
81
|
+
const mydb = await mikroOrmRegistry.register(fixtures.mydb);
|
|
82
|
+
|
|
83
|
+
// act
|
|
84
|
+
const result1 = mikroOrmRegistry.get();
|
|
85
|
+
const result2 = mikroOrmRegistry.get("default");
|
|
86
|
+
const result3 = mikroOrmRegistry.get("mydb");
|
|
87
|
+
|
|
88
|
+
// assert
|
|
89
|
+
expect(result1).toEqual(none);
|
|
90
|
+
expect(result2).toEqual(none);
|
|
91
|
+
expect(result3).toEqual(mydb);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("has", () => {
|
|
96
|
+
it("should return corresponded instances existence", async () => {
|
|
97
|
+
// arrange
|
|
98
|
+
Object.values(fixtures).forEach((options: Options) => when(mikroOrmFactoryMock.create(options)).thenResolve(instance(mikroOrm)));
|
|
99
|
+
await mikroOrmRegistry.register(fixtures.none);
|
|
100
|
+
await mikroOrmRegistry.register(fixtures.mydb);
|
|
101
|
+
|
|
102
|
+
// act & assert
|
|
103
|
+
expect(mikroOrmRegistry.has()).toBeTruthy();
|
|
104
|
+
expect(mikroOrmRegistry.has("mydb")).toBeTruthy();
|
|
105
|
+
expect(mikroOrmRegistry.has("default")).toBeTruthy();
|
|
106
|
+
expect(mikroOrmRegistry.has("mydb10")).toBeFalsy();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("values", () => {
|
|
111
|
+
it("should return corresponded instances", async () => {
|
|
112
|
+
// arrange
|
|
113
|
+
Object.values(fixtures).forEach((options: Options) => when(mikroOrmFactoryMock.create(options)).thenResolve(instance(mikroOrm)));
|
|
114
|
+
await mikroOrmRegistry.register(fixtures.none);
|
|
115
|
+
await mikroOrmRegistry.register(fixtures.mydb);
|
|
116
|
+
|
|
117
|
+
// act & assert
|
|
118
|
+
expect([...mikroOrmRegistry.values()]).toMatchObject([instance(mikroOrm), instance(mikroOrm)]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|