@rsdk/db.typeorm 2.5.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.
Files changed (62) hide show
  1. package/.env +2 -0
  2. package/CHANGELOG.md +155 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +12 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/internal/context.d.ts +46 -0
  7. package/dist/internal/context.js +123 -0
  8. package/dist/internal/context.js.map +1 -0
  9. package/dist/internal/exceptions.d.ts +67 -0
  10. package/dist/internal/exceptions.js +99 -0
  11. package/dist/internal/exceptions.js.map +1 -0
  12. package/dist/internal/initializer.d.ts +15 -0
  13. package/dist/internal/initializer.js +158 -0
  14. package/dist/internal/initializer.js.map +1 -0
  15. package/dist/internal/strategy.d.ts +55 -0
  16. package/dist/internal/strategy.js +109 -0
  17. package/dist/internal/strategy.js.map +1 -0
  18. package/dist/internal/test/strategy.test.e2e.d.ts +5 -0
  19. package/dist/internal/test/strategy.test.e2e.js +779 -0
  20. package/dist/internal/test/strategy.test.e2e.js.map +1 -0
  21. package/dist/internal/test/util.d.ts +22 -0
  22. package/dist/internal/test/util.js +72 -0
  23. package/dist/internal/test/util.js.map +1 -0
  24. package/dist/providers/index.d.ts +3 -0
  25. package/dist/providers/index.js +20 -0
  26. package/dist/providers/index.js.map +1 -0
  27. package/dist/providers/typeorm-logger.adapter.d.ts +13 -0
  28. package/dist/providers/typeorm-logger.adapter.js +61 -0
  29. package/dist/providers/typeorm-logger.adapter.js.map +1 -0
  30. package/dist/providers/typeorm.config.d.ts +10 -0
  31. package/dist/providers/typeorm.config.js +79 -0
  32. package/dist/providers/typeorm.config.js.map +1 -0
  33. package/dist/providers/typeorm.healthcheck.d.ts +8 -0
  34. package/dist/providers/typeorm.healthcheck.js +36 -0
  35. package/dist/providers/typeorm.healthcheck.js.map +1 -0
  36. package/dist/typeorm.errors-transformer.d.ts +5 -0
  37. package/dist/typeorm.errors-transformer.js +32 -0
  38. package/dist/typeorm.errors-transformer.js.map +1 -0
  39. package/dist/typeorm.plugin.d.ts +15 -0
  40. package/dist/typeorm.plugin.js +67 -0
  41. package/dist/typeorm.plugin.js.map +1 -0
  42. package/dist/types.d.ts +14 -0
  43. package/dist/types.js +18 -0
  44. package/dist/types.js.map +1 -0
  45. package/jest.config.js +7 -0
  46. package/package.json +26 -0
  47. package/src/index.ts +9 -0
  48. package/src/internal/context.ts +164 -0
  49. package/src/internal/exceptions.ts +96 -0
  50. package/src/internal/initializer.ts +213 -0
  51. package/src/internal/strategy.ts +149 -0
  52. package/src/internal/test/strategy.test.e2e.ts +1050 -0
  53. package/src/internal/test/util.ts +51 -0
  54. package/src/providers/index.ts +3 -0
  55. package/src/providers/typeorm-logger.adapter.ts +59 -0
  56. package/src/providers/typeorm.config.ts +61 -0
  57. package/src/providers/typeorm.healthcheck.ts +20 -0
  58. package/src/typeorm.errors-transformer.ts +35 -0
  59. package/src/typeorm.plugin.ts +83 -0
  60. package/src/types.ts +23 -0
  61. package/tsconfig.build.json +9 -0
  62. package/tsconfig.json +9 -0
@@ -0,0 +1,164 @@
1
+ import { BaseContext } from '@rsdk/db';
2
+ import type { QueryRunner } from 'typeorm';
3
+
4
+ import type { IsolationLevel } from '../types';
5
+ import { DEFAULT_ISOLATION_LEVEL, isolationLevels } from '../types';
6
+
7
+ import {
8
+ AlreadyAttached,
9
+ MustReleased,
10
+ QueryRunnerIsReleased,
11
+ QueryRunnerIsTransactionActive,
12
+ QueryRunnerMustBeAttached,
13
+ UnexpectedRelease,
14
+ } from './exceptions';
15
+
16
+ export class TypeOrmContext extends BaseContext<IsolationLevel> {
17
+ readonly isolationLevels = isolationLevels;
18
+
19
+ /**
20
+ * TypeOrm queryRunner который будет использоваться для выполнения запросов и инициализации транзакции
21
+ * @private
22
+ */
23
+ private _queryRunner?: QueryRunner;
24
+
25
+ /**
26
+ * Текущая глубина на которую должна быть проинициализирована транзакция
27
+ * @private
28
+ */
29
+ private transactionInitializedDepth = 0;
30
+ /**
31
+ * Глубина вложенности
32
+ * Не путать с индексом вложенности
33
+ * По сути является необходимым количеством проинициализированных слоев
34
+ * @private
35
+ */
36
+ private nestDepth = 1;
37
+
38
+ /**
39
+ * Завершена ли работа с этим контекстом
40
+ * Необходимо для контроля за тем чтобы не было переиспользований этого контекста
41
+ * @private
42
+ */
43
+ private _isReleased = false;
44
+
45
+ private constructor(isolationLevel?: IsolationLevel);
46
+
47
+ private constructor(suspended: true);
48
+
49
+ private constructor(arg?: true | IsolationLevel) {
50
+ super(arg ?? DEFAULT_ISOLATION_LEVEL);
51
+ }
52
+
53
+ get isReleased(): boolean {
54
+ return this._isReleased;
55
+ }
56
+
57
+ get queryRunner(): QueryRunner | undefined {
58
+ return this._queryRunner;
59
+ }
60
+
61
+ static create(isolationLevel: IsolationLevel): TypeOrmContext {
62
+ return new TypeOrmContext(isolationLevel);
63
+ }
64
+
65
+ static createSuspended(): TypeOrmContext {
66
+ return new TypeOrmContext(true);
67
+ }
68
+
69
+ /**
70
+ * Устанавливаем queryRunner который будет использоваться для инициализации транзакции
71
+ * @param queryRunner
72
+ */
73
+ attach(queryRunner: QueryRunner): void {
74
+ if (this._queryRunner) {
75
+ throw new AlreadyAttached();
76
+ }
77
+
78
+ if (queryRunner.isTransactionActive) {
79
+ throw new QueryRunnerIsTransactionActive();
80
+ }
81
+
82
+ if (queryRunner.isReleased) {
83
+ throw new QueryRunnerIsReleased();
84
+ }
85
+
86
+ this._queryRunner = queryRunner;
87
+ }
88
+
89
+ async initialize(): Promise<void> {
90
+ if (this.suspended) {
91
+ return;
92
+ }
93
+
94
+ if (!this._queryRunner) {
95
+ throw new QueryRunnerMustBeAttached();
96
+ }
97
+
98
+ /**
99
+ * Инициализируем нужное количество `savepoint`
100
+ */
101
+ for (
102
+ this.transactionInitializedDepth;
103
+ this.transactionInitializedDepth < this.nestDepth;
104
+ this.transactionInitializedDepth++
105
+ ) {
106
+ await this._queryRunner.startTransaction();
107
+ }
108
+ }
109
+
110
+ isInitialized(): boolean {
111
+ return this.nestDepth === this.transactionInitializedDepth;
112
+ }
113
+
114
+ addNestLayer(): void {
115
+ if (this.suspended) {
116
+ throw new Error('unsupported nesting muted ');
117
+ }
118
+
119
+ this.nestDepth += 1;
120
+ }
121
+
122
+ async rollbackTransaction(): Promise<void> {
123
+ if (!this._queryRunner) {
124
+ return;
125
+ }
126
+
127
+ if (this.isInitialized()) {
128
+ await this._queryRunner.rollbackTransaction();
129
+ }
130
+ }
131
+
132
+ async commitTransaction(): Promise<void> {
133
+ if (!this._queryRunner) {
134
+ return;
135
+ }
136
+
137
+ if (this.isInitialized()) {
138
+ await this._queryRunner.commitTransaction();
139
+ }
140
+ }
141
+
142
+ async finalize(mustRelease: boolean): Promise<void> {
143
+ this.nestDepth -= 1;
144
+ this.transactionInitializedDepth = this.nestDepth;
145
+
146
+ const isReleased = this.nestDepth === 0;
147
+ /**
148
+ * В обоих случаях в идеале завершать работу приложения так как часть операция может быть не выполнена, хотя должна была и наоборот
149
+ * И в качестве бонуса получим висящее соединение к СУБД которое нельзя использовать
150
+ */
151
+ if (!isReleased && mustRelease) {
152
+ throw new MustReleased();
153
+ }
154
+
155
+ if (isReleased && !mustRelease) {
156
+ throw new UnexpectedRelease();
157
+ }
158
+
159
+ if (isReleased) {
160
+ this._isReleased = true;
161
+ await this._queryRunner?.release();
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,96 @@
1
+ import { InternalException } from '@rsdk/core';
2
+
3
+ export class TypeormdbException extends InternalException {}
4
+
5
+ export class ProxiedMethodIsNotFunction extends TypeormdbException {
6
+ constructor() {
7
+ super('Proxied method non function value');
8
+ }
9
+ }
10
+
11
+ export class AlreadyAttached extends TypeormdbException {
12
+ constructor() {
13
+ super('Already attached');
14
+ }
15
+ }
16
+
17
+ export class QueryRunnerMustBeAttached extends TypeormdbException {
18
+ constructor() {
19
+ super('QueryRunner must be attached');
20
+ }
21
+ }
22
+
23
+ export class ContextMustBeProvided extends TypeormdbException {
24
+ constructor() {
25
+ super('Incorrect call, context must be provided');
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Неожиданный релиз (закрытие/возвращение в пул) соединения
31
+ */
32
+ export class UnexpectedRelease extends TypeormdbException {
33
+ constructor() {
34
+ super('Unexpected release connection');
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Ожидалось завершение работы с транзакцией, но не удовлетворены условия завершения (например транзакция не может быть закрыта из-за наличия незакрытых `savepoint`)
40
+ */
41
+ export class MustReleased extends TypeormdbException {
42
+ constructor() {
43
+ super('Must released but conditions not satisfied');
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Попытка использовать контекст когда работа с ним была завершена
49
+ * Например если не дождались завершения промиса использующего соединение
50
+ * @example
51
+ * runInContext(()=> {
52
+ * Promise.resolve().then(() => manager.query('SELECT 1'))
53
+ * })
54
+ *
55
+ * @example
56
+ * class UserController {
57
+ * constructor(
58
+ * private registerAttemptService: { registerAttemptToDb(): Promise<void> },
59
+ * private userService: { fetchUser(userId: UserId): Promise<User | null> }
60
+ * ) {}
61
+ *
62
+ * // @Get('/user')
63
+ * // @Transactional()
64
+ * async sum(@Query() query: { userId: UserId }): Promise<void> {
65
+ * this.registerAttemptService.registerAttemptToDb();
66
+ * return this.userService.fetchUser(query.userId);
67
+ * }
68
+ * }
69
+ */
70
+ export class ContextIsReleased extends InternalException {
71
+ constructor() {
72
+ super(
73
+ "Trying to use a context when it's already released (the function started in the context has been ended)",
74
+ );
75
+ }
76
+ }
77
+
78
+ /**
79
+ * QueryRunner переданный для аттача уже в состоянии isReleased
80
+ * Такой queryRunner не может быть использован для работы контекста
81
+ */
82
+ export class QueryRunnerIsReleased extends InternalException {
83
+ constructor() {
84
+ super('queryRunner.isReleased must be false for attach');
85
+ }
86
+ }
87
+
88
+ /**
89
+ * QueryRunner переданный для аттача уже внутри транзакции
90
+ * Такой queryRunner не может быть использован для работы контекста
91
+ */
92
+ export class QueryRunnerIsTransactionActive extends InternalException {
93
+ constructor() {
94
+ super('queryRunner.isTransactionActive must be false for attach');
95
+ }
96
+ }
@@ -0,0 +1,213 @@
1
+ import { ContextStorage } from '@rsdk/db';
2
+ import type { QueryRunner } from 'typeorm';
3
+ import { DataSource } from 'typeorm';
4
+
5
+ import type { TypeOrmContext } from './context';
6
+ import { ContextIsReleased, ProxiedMethodIsNotFunction } from './exceptions';
7
+
8
+ const originalCreateQueryRunner = DataSource.prototype.createQueryRunner;
9
+
10
+ export class Initializer {
11
+ static readonly storage = new ContextStorage<TypeOrmContext>();
12
+
13
+ /**
14
+ * Патчинг всех необходимых методов из typeorm
15
+ */
16
+ static initialize(): void {
17
+ Object.defineProperty(DataSource.prototype, 'createQueryRunner', {
18
+ value: Initializer.wrappedCreateQueryRunner,
19
+ });
20
+ }
21
+
22
+ private static wrappedCreateQueryRunner(
23
+ this: DataSource,
24
+ ...args: any[]
25
+ ): QueryRunner {
26
+ const originalQueryRunner: QueryRunner = originalCreateQueryRunner.call(
27
+ this,
28
+ ...args,
29
+ );
30
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
31
+ const dataSource = this;
32
+ const proxiesQueryRunner = new Proxy(originalQueryRunner, {
33
+ get(target: QueryRunner, propertyKey: keyof QueryRunner): any {
34
+ const originalQueryRunnerProp = target[propertyKey];
35
+
36
+ /**
37
+ * Мы сами контролируем то когда будет освобожден queryRunner
38
+ * Даже сейчас есть проблема с тем что в пул может попасть соединение в котором не была завершена транзакция
39
+ */
40
+ if (propertyKey === 'release') {
41
+ return Initializer.getWrappedRelease(target);
42
+ }
43
+
44
+ /**
45
+ * ATTENTION: Очень важна последовательность, потому что ряд драйверов (postgres например) делает так
46
+ * @see https://github.com/typeorm/typeorm/blob/f5b93c14b5efa1a55aed0211a4757af1b3d6e66b/src/driver/postgres/PostgresQueryRunner.ts#L148
47
+ * Ничего сверх критичного не происходит, или по крайней мере я не нашёл этого
48
+ * Единственное, уходит в бесконечную проверку коннекты к бд не висят, но `DataSource.destroy` уходит в бесконечный цикл
49
+ * @see https://github.com/typeorm/typeorm/blob/f5b93c14b5efa1a55aed0211a4757af1b3d6e66b/src/driver/postgres/PostgresDriver.ts#L1532
50
+ */
51
+ /**
52
+ * Защита от дурака
53
+ */
54
+ /**
55
+ * class A {
56
+ * queryRunner: QueryRunner
57
+ *
58
+ * constructor(private dataSource: DataSource) {
59
+ * this.queryRunner = this.dataSource.createQueryRunner()
60
+ * }
61
+ *
62
+ * @Transactional()
63
+ * transactionDo() {
64
+ * // wtf -_-
65
+ * this.queryRunner.query('drop database')
66
+ * }
67
+ * }
68
+ */
69
+ if (typeof originalQueryRunnerProp === 'function') {
70
+ return Initializer.getProxiesMethod(
71
+ dataSource,
72
+ originalQueryRunnerProp,
73
+ target,
74
+ );
75
+ }
76
+ const context = Initializer.storage.getContext();
77
+ if (!context) {
78
+ return originalQueryRunnerProp;
79
+ }
80
+ if (context.isReleased) {
81
+ throw new ContextIsReleased();
82
+ }
83
+ /**
84
+ * Если кто-то обратился к любому queryRunner во время исполнения функции контекста, а в контексте ещё нет queryRunner
85
+ * Этот queryRunner добавляем в контекст
86
+ */
87
+ if (
88
+ !context.queryRunner &&
89
+ /**
90
+ * Дополнительно проверяем не является ли этот queryRunner релизнутым/с активной транзакцией
91
+ */
92
+ target.isTransactionActive &&
93
+ !target.isReleased
94
+ ) {
95
+ context.attach(originalQueryRunner);
96
+ }
97
+
98
+ if (propertyKey === 'manager') {
99
+ /**
100
+ * Из-за того что у manager должен быть линк на queryRunner
101
+ */
102
+ return dataSource.createEntityManager(proxiesQueryRunner);
103
+ }
104
+
105
+ return originalQueryRunnerProp;
106
+ },
107
+ });
108
+
109
+ return proxiesQueryRunner;
110
+ }
111
+
112
+ private static getValidQueryRunner(
113
+ dataSource: DataSource,
114
+ originalQueryRunner: QueryRunner,
115
+ ): QueryRunner {
116
+ /**
117
+ * Теоретически может быть ситуация в которой оригинальный
118
+ * queryRunner уже кто-то использует в транзакции или его уже релизнули
119
+ */
120
+ if (
121
+ originalQueryRunner.isTransactionActive ||
122
+ originalQueryRunner.isReleased
123
+ ) {
124
+ /**
125
+ * Так как это нежелательное поведение показываем варнинг
126
+ */
127
+ Initializer.warnOnBusyQueryRunner(originalQueryRunner);
128
+
129
+ return originalCreateQueryRunner.call(
130
+ dataSource,
131
+ originalQueryRunner.getReplicationMode(),
132
+ );
133
+ }
134
+
135
+ return originalQueryRunner;
136
+ }
137
+
138
+ private static getWrappedRelease(originalQueryRunner: QueryRunner) {
139
+ return async function (this: QueryRunner): Promise<void> {
140
+ const ctx = Initializer.storage.getContext();
141
+ const contextQueryRunner = ctx?.queryRunner;
142
+
143
+ if (ctx && contextQueryRunner === originalQueryRunner) {
144
+ return;
145
+ }
146
+
147
+ return originalQueryRunner.release.call(originalQueryRunner);
148
+ };
149
+ }
150
+
151
+ private static getProxiesMethod<K extends keyof QueryRunner>(
152
+ dataSource: DataSource,
153
+ originalQueryRunnerProp: QueryRunner[K],
154
+ originalQueryRunner: QueryRunner,
155
+ ): QueryRunner[K] {
156
+ // Чтобы ts не нервничал
157
+ if (typeof originalQueryRunnerProp !== 'function') {
158
+ throw new ProxiedMethodIsNotFunction();
159
+ }
160
+
161
+ const originalQueryRunnerFunc = originalQueryRunnerProp as (
162
+ ...args: any
163
+ ) => any;
164
+
165
+ return async function (this: QueryRunner, ...args: any[]): Promise<any> {
166
+ const context = Initializer.storage.getContext();
167
+ if (!context) {
168
+ /**
169
+ * Если мы не в контексте просто даём делать свою работу
170
+ */
171
+ return originalQueryRunnerFunc.call(originalQueryRunner, ...args);
172
+ }
173
+
174
+ /**
175
+ * Если у нас уже есть проинициализированный queryRunner делегируем работу ему
176
+ */
177
+ if (context.isInitialized()) {
178
+ return originalQueryRunnerFunc.call(context.queryRunner, ...args);
179
+ }
180
+
181
+ /**
182
+ * Если в контексте нет queryRunner его надо установить перед тем как инициализировать транзакцию
183
+ */
184
+ if (!context.queryRunner) {
185
+ const resultedQueryRunner = Initializer.getValidQueryRunner(
186
+ dataSource,
187
+ originalQueryRunner,
188
+ );
189
+
190
+ context.attach(resultedQueryRunner);
191
+ }
192
+
193
+ await context.initialize();
194
+
195
+ return await originalQueryRunnerFunc.call(context.queryRunner, ...args);
196
+ } as QueryRunner[K];
197
+ }
198
+
199
+ private static warnOnBusyQueryRunner(
200
+ potentialQueryRunner: QueryRunner,
201
+ ): void {
202
+ const warnMessage = Initializer.getWarnMessage(potentialQueryRunner);
203
+
204
+ console.warn(warnMessage);
205
+ }
206
+
207
+ private static getWarnMessage(potentialQueryRunner: QueryRunner): string {
208
+ return `[Initializer -> getProxiesMethod] originalQueryRunner already - ${[
209
+ potentialQueryRunner.isTransactionActive && 'used in transaction',
210
+ potentialQueryRunner.isReleased && 'released',
211
+ ].join(' and ')}`;
212
+ }
213
+ }
@@ -0,0 +1,149 @@
1
+ import type { CallBack, TransactionalStrategy } from '@rsdk/db';
2
+
3
+ import type { IsolationLevel } from '../types';
4
+ import { DEFAULT_ISOLATION_LEVEL } from '../types';
5
+
6
+ import { TypeOrmContext } from './context';
7
+ import { ContextMustBeProvided } from './exceptions';
8
+ import { Initializer } from './initializer';
9
+
10
+ export class TypeormTransactionalStrategy
11
+ implements TransactionalStrategy<IsolationLevel, TypeOrmContext>
12
+ {
13
+ /**
14
+ * Возвращает текущий контекст если вызвано внутри него
15
+ */
16
+ getContext(): TypeOrmContext | undefined {
17
+ return Initializer.storage.getContext();
18
+ }
19
+
20
+ /**
21
+ * Возвращает обернутую в контекст функцию
22
+ * @param fn
23
+ * @param propagationLevel
24
+ * @param options
25
+ */
26
+ bindContext<TResult, TArgs extends any[]>(
27
+ fn: CallBack<TArgs, TResult>,
28
+ options: IsolationLevel = DEFAULT_ISOLATION_LEVEL,
29
+ ): CallBack<TArgs, Promise<TResult>> {
30
+ return (...args: TArgs) => this.runInContext(options, fn, ...args);
31
+ }
32
+
33
+ /**
34
+ * Находимся ли мы в контексте и не является ли он suspend
35
+ */
36
+ isRunning(): boolean {
37
+ const context = Initializer.storage.getContext();
38
+ if (!context) {
39
+ return false;
40
+ }
41
+
42
+ return Boolean(context.suspended);
43
+ }
44
+
45
+ /**
46
+ * Вызывает функцию, обернутую внутри контекста с заданными параметрами и корректной обработкой транзакции
47
+ * @param isolation
48
+ * @param fn
49
+ * @param args
50
+ */
51
+ async runInContext<TResult, TArgs extends any[]>(
52
+ isolation: IsolationLevel,
53
+ fn: CallBack<TArgs, TResult>,
54
+ ...args: TArgs
55
+ ): Promise<TResult> {
56
+ const currentContext = Initializer.storage.getContext();
57
+ if (!currentContext) {
58
+ return await this.runFromNewContext(isolation, fn, ...args);
59
+ }
60
+
61
+ return await this.nestedExecute(currentContext, isolation, fn, ...args);
62
+ }
63
+
64
+ /**
65
+ * Вызывает функцию в новом контексте, игнорируя внешний если он существует
66
+ * @param isolationLevel
67
+ * @param fn
68
+ * @param args
69
+ */
70
+ async runFromNewContext<TResult, TArgs extends any[]>(
71
+ isolationLevel: IsolationLevel,
72
+ fn: CallBack<TArgs, TResult>,
73
+ ...args: TArgs
74
+ ): Promise<TResult> {
75
+ const typeormContext = TypeOrmContext.create(isolationLevel);
76
+ const wrapped = this.wrapInTransactionContext(true, fn);
77
+
78
+ return Initializer.storage.run(typeormContext, wrapped, ...args);
79
+ }
80
+
81
+ /**
82
+ * Создаёт новый контекст внутри которого не будет создаваться/использоваться транзакция и игнорироваться внешняя
83
+ * @param fn
84
+ * @param args
85
+ */
86
+ async runOutTransactionalContext<TResult, TArgs extends any[]>(
87
+ fn: CallBack<TArgs, TResult>,
88
+ ...args: TArgs
89
+ ): Promise<TResult> {
90
+ const typeormContext = TypeOrmContext.createSuspended();
91
+ const wrapped = this.wrapInTransactionContext(true, fn);
92
+
93
+ return Initializer.storage.run(typeormContext, wrapped, ...args);
94
+ }
95
+
96
+ /**
97
+ * Вызывает функцию во вложенном уровне контекста
98
+ * @param context
99
+ * @param isolationLevel
100
+ * @param fn
101
+ * @param args
102
+ */
103
+ async nestedExecute<TResult, TArgs extends any[]>(
104
+ context: TypeOrmContext,
105
+ isolationLevel: IsolationLevel,
106
+ fn: CallBack<TArgs, TResult>,
107
+ ...args: TArgs
108
+ ): Promise<TResult> {
109
+ context.assertIsolationCompatibility(isolationLevel);
110
+
111
+ const wrapped = this.wrapInTransactionContext(false, fn);
112
+
113
+ context.addNestLayer();
114
+
115
+ return Initializer.storage.run(context, wrapped, ...args);
116
+ }
117
+
118
+ /**
119
+ * Оборачивает функцию для корректного завершения транзакции, если она была проинициализирована внутри функции
120
+ * Важно - не оборачивает в вызов с контекстом
121
+ * @param mustReleased
122
+ * @param fn
123
+ */
124
+ private wrapInTransactionContext<TResult, TArgs extends any[]>(
125
+ mustReleased: boolean,
126
+ fn: CallBack<TArgs, TResult>,
127
+ ): CallBack<TArgs, Promise<TResult>> {
128
+ return async (...args) => {
129
+ const context = Initializer.storage.getContext();
130
+ if (!context) {
131
+ throw new ContextMustBeProvided();
132
+ }
133
+
134
+ try {
135
+ const result = await fn(...args);
136
+
137
+ await context.commitTransaction();
138
+
139
+ return result;
140
+ } catch (error) {
141
+ await context.rollbackTransaction();
142
+
143
+ throw error;
144
+ } finally {
145
+ await context.finalize(mustReleased);
146
+ }
147
+ };
148
+ }
149
+ }