@tsed/mikro-orm 8.11.1 → 8.11.2-rc.1
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/lib/esm/MikroOrmModule.js +0 -1
- package/lib/esm/constants.js +0 -1
- package/lib/esm/decorators/entityManager.js +0 -1
- package/lib/esm/decorators/orm.js +0 -1
- package/lib/esm/decorators/subscriber.js +0 -1
- package/lib/esm/decorators/transactional.js +0 -1
- package/lib/esm/filters/OptimisticLockErrorFilter.js +0 -1
- package/lib/esm/index.js +0 -1
- package/lib/esm/interceptors/TransactionalInterceptor.js +0 -1
- package/lib/esm/interfaces/AfterTransactionCommit.js +0 -1
- package/lib/esm/interfaces/BeforeTransactionCommit.js +0 -1
- package/lib/esm/interfaces/RetryStrategy.js +0 -1
- package/lib/esm/services/MikroOrmContext.js +0 -1
- package/lib/esm/services/MikroOrmFactory.js +0 -1
- package/lib/esm/services/MikroOrmRegistry.js +0 -1
- package/package.json +13 -10
- package/lib/esm/MikroOrmModule.js.map +0 -1
- package/lib/esm/constants.js.map +0 -1
- package/lib/esm/decorators/entityManager.js.map +0 -1
- package/lib/esm/decorators/orm.js.map +0 -1
- package/lib/esm/decorators/subscriber.js.map +0 -1
- package/lib/esm/decorators/transactional.js.map +0 -1
- package/lib/esm/filters/OptimisticLockErrorFilter.js.map +0 -1
- package/lib/esm/index.js.map +0 -1
- package/lib/esm/interceptors/TransactionalInterceptor.js.map +0 -1
- package/lib/esm/interfaces/AfterTransactionCommit.js.map +0 -1
- package/lib/esm/interfaces/BeforeTransactionCommit.js.map +0 -1
- package/lib/esm/interfaces/RetryStrategy.js.map +0 -1
- package/lib/esm/services/MikroOrmContext.js.map +0 -1
- package/lib/esm/services/MikroOrmFactory.js.map +0 -1
- package/lib/esm/services/MikroOrmRegistry.js.map +0 -1
- package/src/MikroOrmModule.spec.ts +0 -110
- package/src/MikroOrmModule.ts +0 -104
- package/src/constants.ts +0 -2
- package/src/decorators/entityManager.spec.ts +0 -98
- package/src/decorators/entityManager.ts +0 -25
- package/src/decorators/orm.spec.ts +0 -97
- package/src/decorators/orm.ts +0 -19
- package/src/decorators/subscriber.spec.ts +0 -27
- package/src/decorators/subscriber.ts +0 -12
- package/src/decorators/transactional.spec.ts +0 -34
- package/src/decorators/transactional.ts +0 -10
- package/src/filters/OptimisticLockErrorFilter.spec.ts +0 -33
- package/src/filters/OptimisticLockErrorFilter.ts +0 -19
- package/src/index.ts +0 -17
- package/src/interceptors/TransactionalInterceptor.spec.ts +0 -248
- package/src/interceptors/TransactionalInterceptor.ts +0 -130
- package/src/interfaces/AfterTransactionCommit.ts +0 -5
- package/src/interfaces/BeforeTransactionCommit.ts +0 -5
- package/src/interfaces/RetryStrategy.ts +0 -5
- package/src/services/MikroOrmContext.spec.ts +0 -95
- package/src/services/MikroOrmContext.ts +0 -29
- package/src/services/MikroOrmFactory.ts +0 -16
- package/src/services/MikroOrmRegistry.spec.ts +0 -121
- package/src/services/MikroOrmRegistry.ts +0 -62
- package/vitest.config.mts +0 -25
package/src/decorators/orm.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import {MikroORM} from "@mikro-orm/core";
|
|
2
|
-
import {Inject, inject} from "@tsed/di";
|
|
3
|
-
|
|
4
|
-
import {MikroOrmRegistry} from "../services/MikroOrmRegistry.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Get the ORM for the given context name using new inject() function.
|
|
8
|
-
* @param contextName
|
|
9
|
-
*/
|
|
10
|
-
export function orm(contextName?: string): MikroORM | undefined {
|
|
11
|
-
return inject(MikroOrmRegistry).get(contextName);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Get the ORM for the given context name.
|
|
16
|
-
* @param {String} contextName
|
|
17
|
-
*/
|
|
18
|
-
export const Orm = (contextName?: string): PropertyDecorator =>
|
|
19
|
-
Inject(MikroOrmRegistry, {transform: (registry: MikroOrmRegistry) => registry.get(contextName)}) as PropertyDecorator;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import {EventSubscriber} from "@mikro-orm/core";
|
|
2
|
-
import {Store} from "@tsed/core";
|
|
3
|
-
|
|
4
|
-
import {DEFAULT_CONTEXT_NAME, SUBSCRIBER_INJECTION_TYPE} from "../constants.js";
|
|
5
|
-
import {Subscriber} from "./subscriber.js";
|
|
6
|
-
|
|
7
|
-
@Subscriber()
|
|
8
|
-
export class Subscriber1 implements EventSubscriber {}
|
|
9
|
-
|
|
10
|
-
@Subscriber({contextName: "non-default"})
|
|
11
|
-
export class Subscriber2 implements EventSubscriber {}
|
|
12
|
-
|
|
13
|
-
describe("@Subscriber", () => {
|
|
14
|
-
it("should decorate the class", () => {
|
|
15
|
-
// act
|
|
16
|
-
const result = Store.from(Subscriber1).get(SUBSCRIBER_INJECTION_TYPE);
|
|
17
|
-
// assert
|
|
18
|
-
expect(result).toEqual({contextName: DEFAULT_CONTEXT_NAME});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("should decorate the class tying a context name to it", () => {
|
|
22
|
-
// act
|
|
23
|
-
const result = Store.from(Subscriber2).get(SUBSCRIBER_INJECTION_TYPE);
|
|
24
|
-
// assert
|
|
25
|
-
expect(result).toEqual({contextName: "non-default"});
|
|
26
|
-
});
|
|
27
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import {StoreSet, useDecorators} from "@tsed/core";
|
|
2
|
-
import {Injectable} from "@tsed/di";
|
|
3
|
-
|
|
4
|
-
import {DEFAULT_CONTEXT_NAME, SUBSCRIBER_INJECTION_TYPE} from "../constants.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Register a new subscriber for the given context name.
|
|
8
|
-
* @decorator
|
|
9
|
-
* @mikroOrm
|
|
10
|
-
*/
|
|
11
|
-
export const Subscriber = (options: {contextName: string} = {contextName: DEFAULT_CONTEXT_NAME}) =>
|
|
12
|
-
useDecorators(Injectable({type: SUBSCRIBER_INJECTION_TYPE}), StoreSet(SUBSCRIBER_INJECTION_TYPE, options));
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import {Controller, DITest} from "@tsed/di";
|
|
2
|
-
import {Post} from "@tsed/schema";
|
|
3
|
-
import {afterEach, beforeEach} from "vitest";
|
|
4
|
-
|
|
5
|
-
import {TransactionalInterceptor} from "../interceptors/TransactionalInterceptor.js";
|
|
6
|
-
import {Transactional} from "./transactional.js";
|
|
7
|
-
|
|
8
|
-
@Controller("/users")
|
|
9
|
-
export class UsersCtrl {
|
|
10
|
-
@Post("/")
|
|
11
|
-
@Transactional()
|
|
12
|
-
create(): any {}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe("@Transactional", () => {
|
|
16
|
-
beforeEach(() => DITest.create());
|
|
17
|
-
afterEach(() => DITest.reset());
|
|
18
|
-
it("should decorate method", async () => {
|
|
19
|
-
const interceptor = {
|
|
20
|
-
intercept: vi.fn().mockResolvedValue({})
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const usersService = await DITest.invoke<UsersCtrl>(UsersCtrl, [
|
|
24
|
-
{
|
|
25
|
-
token: TransactionalInterceptor,
|
|
26
|
-
use: interceptor
|
|
27
|
-
}
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
const result = await usersService.create();
|
|
31
|
-
|
|
32
|
-
expect(result).toEqual({});
|
|
33
|
-
});
|
|
34
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import {Intercept} from "@tsed/di";
|
|
2
|
-
|
|
3
|
-
import {TransactionalInterceptor, TransactionOptions} from "../interceptors/TransactionalInterceptor.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Register a new request context for your method and execute it inside the context.
|
|
7
|
-
* @param {String | TransactionOptions} contextNameOrOptions
|
|
8
|
-
*/
|
|
9
|
-
export const Transactional = (contextNameOrOptions?: string | TransactionOptions): MethodDecorator =>
|
|
10
|
-
Intercept(TransactionalInterceptor, contextNameOrOptions);
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import {OptimisticLockError} from "@mikro-orm/core";
|
|
2
|
-
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
3
|
-
|
|
4
|
-
import {OptimisticLockErrorFilter} from "./OptimisticLockErrorFilter.js";
|
|
5
|
-
|
|
6
|
-
describe("OptimisticLockErrorFilter", () => {
|
|
7
|
-
beforeEach(() => PlatformTest.create());
|
|
8
|
-
afterEach(() => PlatformTest.reset());
|
|
9
|
-
|
|
10
|
-
describe("catch", () => {
|
|
11
|
-
it("should set HTTP status to 409", async () => {
|
|
12
|
-
const optimisticLockErrorFilter = await PlatformTest.invoke(OptimisticLockErrorFilter);
|
|
13
|
-
|
|
14
|
-
// arrange
|
|
15
|
-
const exception = OptimisticLockError.lockFailed("entity");
|
|
16
|
-
const context = PlatformTest.createRequestContext();
|
|
17
|
-
|
|
18
|
-
vi.spyOn(context.logger, "error").mockReturnThis();
|
|
19
|
-
|
|
20
|
-
// act
|
|
21
|
-
optimisticLockErrorFilter.catch(exception, context);
|
|
22
|
-
|
|
23
|
-
expect(context.logger.error).toHaveBeenCalledWith({
|
|
24
|
-
event: "MIKRO_ORM_OPTIMISTIC_LOCK_ERROR",
|
|
25
|
-
error_name: exception.name,
|
|
26
|
-
error_message: exception.message,
|
|
27
|
-
stack: exception.stack
|
|
28
|
-
});
|
|
29
|
-
expect(context.response.getBody()).toEqual("Update refused. The resource has changed on the server. Please try again later");
|
|
30
|
-
expect(context.response.statusCode).toEqual(409);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import {OptimisticLockError} from "@mikro-orm/core";
|
|
2
|
-
import {Catch, type ExceptionFilterMethods} from "@tsed/platform-exceptions";
|
|
3
|
-
import {PlatformContext} from "@tsed/platform-http";
|
|
4
|
-
|
|
5
|
-
@Catch(OptimisticLockError)
|
|
6
|
-
export class OptimisticLockErrorFilter implements ExceptionFilterMethods<OptimisticLockError> {
|
|
7
|
-
public catch(exception: OptimisticLockError, ctx: PlatformContext): void {
|
|
8
|
-
const {response, logger} = ctx;
|
|
9
|
-
|
|
10
|
-
logger.error({
|
|
11
|
-
event: "MIKRO_ORM_OPTIMISTIC_LOCK_ERROR",
|
|
12
|
-
error_name: exception.name,
|
|
13
|
-
error_message: exception.message,
|
|
14
|
-
stack: exception.stack
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
response.status(409).body(`Update refused. The resource has changed on the server. Please try again later`);
|
|
18
|
-
}
|
|
19
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Automatically generated by @tsed/barrels.
|
|
3
|
-
*/
|
|
4
|
-
export * from "./constants.js";
|
|
5
|
-
export * from "./decorators/entityManager.js";
|
|
6
|
-
export * from "./decorators/orm.js";
|
|
7
|
-
export * from "./decorators/subscriber.js";
|
|
8
|
-
export * from "./decorators/transactional.js";
|
|
9
|
-
export * from "./filters/OptimisticLockErrorFilter.js";
|
|
10
|
-
export * from "./interceptors/TransactionalInterceptor.js";
|
|
11
|
-
export * from "./interfaces/AfterTransactionCommit.js";
|
|
12
|
-
export * from "./interfaces/BeforeTransactionCommit.js";
|
|
13
|
-
export * from "./interfaces/RetryStrategy.js";
|
|
14
|
-
export * from "./MikroOrmModule.js";
|
|
15
|
-
export * from "./services/MikroOrmContext.js";
|
|
16
|
-
export * from "./services/MikroOrmFactory.js";
|
|
17
|
-
export * from "./services/MikroOrmRegistry.js";
|
|
@@ -1,248 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
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
|
-
});
|