├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── BaseRepository.ts ├── BaseTreeRepository.ts ├── DebugLog.ts ├── IsolationLevel.ts ├── Propagation.ts ├── Transactional.ts ├── TransactionalError.ts ├── common.ts ├── hook.ts ├── index.ts ├── patch-typeorm-repository.ts ├── runInTransaction.ts └── wrapInTransaction.ts ├── tests ├── __tests__ │ ├── test_nestjs.ts │ └── test_simple.ts ├── docker-compose.yaml ├── entity │ └── Post.ts ├── nestjs │ └── app.service.ts └── simple │ └── simple.service.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push] 3 | jobs: 4 | main: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | 9 | - name: Check If Tag 10 | id: check-tag 11 | run: |- 12 | if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 13 | echo ::set-output name=match::true 14 | fi 15 | 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '12.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Install 22 | run: npm install 23 | 24 | - name: Test 25 | run: npm test 26 | 27 | - name: Build 28 | run: npm run build 29 | 30 | - name: Publish 31 | if: steps.check-tag.outputs.match == 'true' 32 | run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 35 | 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | *.tgz 11 | docs 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | .editorconfig 4 | tsconfig.json 5 | .prettierrc 6 | docker-compose.yaml 7 | jest.config.js 8 | tslint.json 9 | *.tgz 10 | docs 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.21 2 | * add runInTransaction and wrapInTransaction [#94](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/94) 3 | 4 | # 0.1.20 5 | * Feature request: make NAMESPACE_NAME exported [#82](https://github.com/odavid/typeorm-transactional-cls-hooked/issues/82) 6 | 7 | ## 0.1.19 8 | * Introduced TRANSACTIONAL_CONSOLE_DEBUG environment variable to control debug logging when connection logger is not available [#76](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/76) 9 | 10 | ## 0.1.18 11 | * Error when using repository methods outside of a request context [#73](https://github.com/odavid/typeorm-transactional-cls-hooked/issues/73) 12 | 13 | ## 0.1.17 14 | * Added Transactional Debug Logs - Using Typeorm connection logger [#69](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/69) 15 | 16 | ## 0.1.16 17 | * Re-patch MongoRepisotry manager property to be configurable/writable [#68](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/68) 18 | 19 | ## 0.1.15 20 | * Patching mongo repository [#67](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/67) 21 | 22 | ## 0.1.14 23 | * Using patchRepositoryManager instead of overriding the manager() [#66](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/66) 24 | 25 | 26 | ## 0.1.13 27 | * Fix partial rollback issue [#61](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/61) 28 | * Using github actions for CI 29 | * Using npm instead of yarn during development 30 | * Simple tests 31 | 32 | ## 0.1.12 33 | * Using connectionName as string | ()=>string [#44](https://github.com/odavid/typeorm-transactional-cls-hooked/issues/44) 34 | 35 | ## 0.1.11 36 | * Move @types/cls-hooked to dependencies [#42](https://github.com/odavid/typeorm-transactional-cls-hooked/issues/42) 37 | 38 | ## 0.1.10 39 | * dist folder missing in package version 0.1.9 [#33](https://github.com/odavid/typeorm-transactional-cls-hooked/issues/33) 40 | 41 | ## 0.1.9 (Use 0.1.10 instead) 42 | * feat: adds BaseTreeRepository [#32](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/32) 43 | 44 | ## 0.1.8 45 | * Preserve method name on @Transactional method overwrite [#16](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/16) 46 | 47 | ## 0.1.7 48 | * Added types declaration in package.json [#20](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/20) 49 | 50 | ## 0.1.6 51 | * cls-hooked should be a dependency [#17](https://github.com/odavid/typeorm-transactional-cls-hooked/issues/17) 52 | 53 | ## 0.1.5 54 | * Removed reflect-metadata import and added a comment within the readme [#12](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/12) 55 | * Export getEntityManagerOrTransactionManager for usage in custom repositories [#13](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/13) 56 | * Added patchTypeORMRepositoryWithBaseRepository [#14](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/14) 57 | 58 | 59 | ## 0.1.4 60 | * add basic support for transation lifecycle hooks [#7](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/7) 61 | 62 | ## 0.1.3 63 | * feature: add ability to specify isolation level of transactions [#5](https://github.com/odavid/typeorm-transactional-cls-hooked/pull/5) 64 | 65 | 66 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017-, Ohad David 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typeorm-transactional-cls-hooked 2 | [![npm version](http://img.shields.io/npm/v/typeorm-transactional-cls-hooked.svg?style=flat)](https://npmjs.org/package/typeorm-transactional-cls-hooked "View this project on npm") 3 | 4 | 5 | A `Transactional` Method Decorator for [typeorm](http://typeorm.io/) that uses [cls-hooked](https://www.npmjs.com/package/cls-hooked) to handle and propagate transactions between different repositories and service methods. 6 | 7 | Inspired by [Spring Transactional](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html) Annotation and [Sequelize CLS](http://docs.sequelizejs.com/manual/tutorial/transactions.html) 8 | 9 | See [Changelog](CHANGELOG.md) 10 | 11 | ## Installation 12 | 13 | ```shell 14 | npm install --save typeorm-transactional-cls-hooked 15 | ## Needed dependencies 16 | npm install --save typeorm reflect-metadata 17 | ``` 18 | 19 | Or 20 | 21 | ```shell 22 | yarn add typeorm-transactional-cls-hooked 23 | ## Needed dependencies 24 | yarn add typeorm reflect-metadata 25 | ``` 26 | 27 | > **Note**: You will need to import `reflect-metadata` somewhere in the global place of your app - https://github.com/typeorm/typeorm#installation 28 | 29 | ## Initialization 30 | 31 | In order to use it, you will first need to initialize the cls-hooked namespace before your application is started 32 | 33 | ```typescript 34 | import { initializeTransactionalContext } from 'typeorm-transactional-cls-hooked'; 35 | 36 | initializeTransactionalContext() // Initialize cls-hooked 37 | ... 38 | app = express() 39 | ... 40 | ``` 41 | 42 | ## BaseRepository 43 | 44 | Since this is an external library, all your typeorm repositories will need to be a [custom repository](https://github.com/typeorm/typeorm/blob/master/docs/custom-repository.md) extending either the `BaseRepository` (when using TypeORM's [`Entity`](https://github.com/typeorm/typeorm/blob/master/docs/entities.md)) or the `BaseTreeRepository` class (when using TypeORM's [`TreeEntity`](https://github.com/typeorm/typeorm/blob/master/docs/tree-entities.md)). 45 | 46 | ```typescript 47 | // Post.entity.ts 48 | @Entity() 49 | export class Post{ 50 | @PrimaryGeneratedColumn() 51 | id: number 52 | 53 | @Column 54 | message: string 55 | ... 56 | } 57 | 58 | // Post.repository.ts 59 | import { EntityRepository } from 'typeorm'; 60 | import { BaseRepository } from 'typeorm-transactional-cls-hooked'; 61 | 62 | @EntityRepository(Post) 63 | export class PostRepository extends BaseRepository {} 64 | ``` 65 | 66 | The only purpose of the `BaseRepository` class is to make sure the `manager` property of the repository will always be the right one. In cases where inheritance is not possible, you can always [Patch the Repository/TreeRepository](#patching-typeorm-repository) to enable the same functionality as the `BaseRepository` 67 | 68 | 69 | ### Patching TypeORM Repository 70 | Sometimes there is a need to keep using the [TypeORM Repository](https://github.com/typeorm/typeorm/blob/master/src/repository/Repository.ts) instead of using the `BaseRepository`. 71 | For this cases, you will need to *"mixin/patch"* the original `Repository` with the `BaseRepository`. 72 | By doing so, you will be able to use the original `Repository` and not change the code or use `BaseRepository`. 73 | > This method was taken from https://gist.github.com/Diluka/87efbd9169cae96a012a43d1e5695667 (Thanks @Diluka) 74 | 75 | In order to do that, the following should be done during initialization: 76 | 77 | ```typescript 78 | import { initializeTransactionalContext, patchTypeORMRepositoryWithBaseRepository } from 'typeorm-transactional-cls-hooked'; 79 | 80 | initializeTransactionalContext() // Initialize cls-hooked 81 | patchTypeORMRepositoryWithBaseRepository() // patch Repository with BaseRepository. 82 | ``` 83 | 84 | If there is a need to keep using the TypeORM [`TreeRepository`](https://github.com/typeorm/typeorm/blob/master/docs/tree-entities.md#working-with-tree-entities) instead of using `BaseTreeRepository`, use `patchTypeORMTreeRepositoryWithBaseTreeRepository`. 85 | 86 | 87 | --- 88 | **IMPORTANT NOTE** 89 | 90 | Calling [initializeTransactionalContext](#initialization) and [patchTypeORMRepositoryWithBaseRepository](#patching-typeorm-repository) must happen BEFORE any application context is initialized! 91 | 92 | --- 93 | 94 | 95 | 96 | ## Using Transactional Decorator 97 | 98 | - Every service method that needs to be transactional, need to use the `@Transactional()` decorator 99 | - The decorator can take a `connectionName` as argument (by default it is `default`) 100 | - In some cases, where the connectionName should be dynamically evaluated, the value of connectionName can be a function that returns a string. 101 | - The decorator can take an optional `propagation` as argument to define the [propagation behaviour](#transaction-propagation) 102 | - The decorator can take an optional `isolationLevel` as argument to define the [isolation level](#isolation-levels) (by default it will use your database driver's default isolation level.) 103 | 104 | ```typescript 105 | export class PostService { 106 | constructor(readonly repository: PostRepository) 107 | 108 | @Transactional() // Will open a transaction if one doesn't already exist 109 | async createPost(id, message): Promise { 110 | const post = this.repository.create({ id, message }) 111 | return this.repository.save(post) 112 | } 113 | } 114 | ``` 115 | 116 | ## Transaction Propagation 117 | 118 | The following propagation options can be specified: 119 | 120 | - `MANDATORY` - Support a current transaction, throw an exception if none exists. 121 | - `NESTED` - Execute within a nested transaction if a current transaction exists, behave like `REQUIRED` else. 122 | - `NEVER` - Execute non-transactionally, throw an exception if a transaction exists. 123 | - `NOT_SUPPORTED` - Execute non-transactionally, suspend the current transaction if one exists. 124 | - `REQUIRED` (default behaviour) - Support a current transaction, create a new one if none exists. 125 | - `REQUIRES_NEW` - Create a new transaction, and suspend the current transaction if one exists. 126 | - `SUPPORTS` - Support a current transaction, execute non-transactionally if none exists. 127 | 128 | ## Isolation Levels 129 | 130 | The following isolation level options can be specified: 131 | 132 | - `READ_UNCOMMITTED` - A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur. 133 | - `READ_COMMITTED` - A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur. 134 | - `REPEATABLE_READ` - A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur. 135 | - `SERIALIZABLE` = A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented. 136 | 137 | **NOTE**: If a transaction already exist and a method is decorated with `@Transactional` and `propagation` *does not equal* to `REQUIRES_NEW`, then the declared `isolationLevel` value will *not* be taken into account. 138 | 139 | ## Hooks 140 | 141 | Because you hand over control of the transaction creation to this library, there is no way for you to know whether or not the current transaction was sucessfully persisted to the database. 142 | 143 | To circumvent that, we expose three helper methods that allow you to hook into the transaction lifecycle and take appropriate action after a commit/rollback. 144 | 145 | - `runOnTransactionCommit(cb)` takes a callback to be executed after the current transaction was sucessfully committed 146 | - `runOnTransactionRollback(cb)` takes a callback to be executed after the current transaction rolls back. The callback gets the error that initiated the roolback as a parameter. 147 | - `runOnTransactionComplete(cb)` takes a callback to be executed at the completion of the current transactional context. If there was an error, it gets passed as an argument. 148 | 149 | 150 | 151 | ```typescript 152 | export class PostService { 153 | constructor(readonly repository: PostRepository, readonly events: EventService) {} 154 | 155 | @Transactional() 156 | async createPost(id, message): Promise { 157 | const post = this.repository.create({ id, message }) 158 | const result = await this.repository.save(post) 159 | runOnTransactionCommit(() => this.events.emit('post created')) 160 | return result 161 | } 162 | } 163 | ``` 164 | 165 | ## Unit Test Mocking 166 | `@Transactional` and `BaseRepository` can be mocked to prevent running any of the transactional code in unit tests. 167 | 168 | This can be accomplished in Jest with: 169 | 170 | ```typescript 171 | jest.mock('typeorm-transactional-cls-hooked', () => ({ 172 | Transactional: () => () => ({}), 173 | BaseRepository: class {}, 174 | })); 175 | ``` 176 | 177 | Repositories, services, etc. can be mocked as usual. 178 | 179 | ## Logging / Debug 180 | The `Transactional` uses the [Typeorm Connection logger](https://github.com/typeorm/typeorm/blob/master/docs/logging.md) to emit [`log` messages](https://github.com/typeorm/typeorm/blob/master/docs/logging.md#logging-options). 181 | 182 | In order to enable logs, you should set `logging: ["log"]` or `logging: ["all"]` to your typeorm logging configuration. 183 | 184 | The Transactional log message structure looks as follows: 185 | 186 | ``` 187 | Transactional@UNIQ_ID|CONNECTION_NAME|METHOD_NAME|ISOLATION|PROPAGATION - MESSAGE 188 | ``` 189 | * UNIQ_ID - a timestamp taken at the begining of the Transactional call 190 | * CONNECTION_NAME - The typeorm connection name passed to the Transactional decorator 191 | * METHOD_NAME - The decorated method in action 192 | * ISOLATION - The [Isolation Level](#isolation-levels) passed to the Transactional decorator 193 | * PROPAGATION - The [Propagation](#transaction-propagation) value passed to the Transactional decorator 194 | 195 | During [initialization](#initialization) and [patching repositories](#patching-typeorm-repository), the [Typeorm Connection logger](https://github.com/typeorm/typeorm/blob/master/docs/logging.md) is not available yet. 196 | For this reason, the `console.log()` is being used, but only if `TRANSACTIONAL_CONSOLE_DEBUG` environment variable is defined. 197 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeorm-transactional-cls-hooked", 3 | "version": "0.1.21", 4 | "description": "A Transactional Method Decorator for typeorm that uses cls-hooked to handle and propagate transactions between different repositories and service methods. Inpired by Spring Trasnactional Annotation and Sequelize CLS", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "author": { 8 | "name": "Ohad David", 9 | "email": "ohad.david@gmail.com" 10 | }, 11 | "readmeFilename": "README.md", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/odavid/typeorm-transactional-cls-hooked" 15 | }, 16 | "tags": [ 17 | "typescript", 18 | "typescript-orm", 19 | "typeorm", 20 | "orm", 21 | "cls-hooked", 22 | "transaction", 23 | "isolation", 24 | "decorator" 25 | ], 26 | "license": "MIT", 27 | "scripts": { 28 | "lint": "tslint -p tsconfig.json -c tslint.json ./src/**/*.ts", 29 | "clean": "rm -rf ./dist", 30 | "build": "npm run clean && tsc", 31 | "typedoc": "typedoc --out ./docs src/**.ts", 32 | "setup-test-db": "npm run teardown-test-db; docker-compose -f tests/docker-compose.yaml up -d && sleep 3", 33 | "teardown-test-db": "docker-compose -f tests/docker-compose.yaml down --remove-orphans -v", 34 | "test": "npm run setup-test-db && TRANSACTIONAL_CONSOLE_DEBUG=true jest" 35 | }, 36 | "engines": { 37 | "node": ">=8.0.0" 38 | }, 39 | "dependencies": { 40 | "@types/cls-hooked": "^4.2.1", 41 | "cls-hooked": "^4.2.2" 42 | }, 43 | "devDependencies": { 44 | "@nestjs/common": "^7.5.5", 45 | "@nestjs/core": "^7.5.5", 46 | "@nestjs/testing": "^7.5.5", 47 | "@nestjs/typeorm": "^7.1.5", 48 | "@types/jest": "^26.0.15", 49 | "delay": "^5.0.0", 50 | "jest": "^26.6.3", 51 | "pg": "^8.5.1", 52 | "prettier": "^1.14.3", 53 | "reflect-metadata": "^0.1.12", 54 | "rxjs": "^6.6.3", 55 | "ts-jest": "^26.4.4", 56 | "ts-node": "^9.0.0", 57 | "tslint": "^6.1.0", 58 | "tslint-config-prettier": "^1.15.0", 59 | "typedoc": "^0.17.1", 60 | "typeorm": "^0.2.29", 61 | "typescript": "^3.0.3" 62 | }, 63 | "peerDependencies": { 64 | "reflect-metadata": ">= 0.1.12", 65 | "typeorm": ">= 0.2.8" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/BaseRepository.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLiteral, Repository } from 'typeorm' 2 | import { patchRepositoryManager } from './patch-typeorm-repository' 3 | 4 | export class BaseRepository extends Repository {} 5 | 6 | patchRepositoryManager(BaseRepository.prototype) 7 | -------------------------------------------------------------------------------- /src/BaseTreeRepository.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLiteral, TreeRepository } from 'typeorm' 2 | import { patchRepositoryManager } from './patch-typeorm-repository' 3 | 4 | export class BaseTreeRepository extends TreeRepository {} 5 | 6 | patchRepositoryManager(BaseTreeRepository.prototype) 7 | -------------------------------------------------------------------------------- /src/DebugLog.ts: -------------------------------------------------------------------------------- 1 | 2 | const TRANSACTIONAL_CONSOLE_DEBUG = process.env.TRANSACTIONAL_CONSOLE_DEBUG 3 | 4 | export const debugLog = (message?: any, ...optionalParams: any[]): void => { 5 | if(TRANSACTIONAL_CONSOLE_DEBUG){ 6 | // tslint:disable-next-line: no-console 7 | console.log(message, ...optionalParams) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/IsolationLevel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enumeration that represents transaction isolation levels for use with the {@link Transactional} annotation 3 | */ 4 | export enum IsolationLevel { 5 | /** 6 | * A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur. 7 | */ 8 | READ_UNCOMMITTED = 'READ UNCOMMITTED', 9 | /** 10 | * A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur. 11 | */ 12 | READ_COMMITTED = 'READ COMMITTED', 13 | /** 14 | * A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur. 15 | */ 16 | REPEATABLE_READ = 'REPEATABLE READ', 17 | /** 18 | * A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented. 19 | */ 20 | SERIALIZABLE = 'SERIALIZABLE', 21 | } 22 | -------------------------------------------------------------------------------- /src/Propagation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enumeration that represents transaction propagation behaviors for use with the see {@link Transactional} annotation 3 | */ 4 | export enum Propagation { 5 | /** 6 | * Support a current transaction, throw an exception if none exists. 7 | */ 8 | MANDATORY = 'MANDATORY', 9 | /** 10 | * Execute within a nested transaction if a current transaction exists, behave like `REQUIRED` else. 11 | */ 12 | NESTED = 'NESTED', 13 | /** 14 | * Execute non-transactionally, throw an exception if a transaction exists. 15 | */ 16 | NEVER = 'NEVER', 17 | /** 18 | * Execute non-transactionally, suspend the current transaction if one exists. 19 | */ 20 | NOT_SUPPORTED = 'NOT_SUPPORTED', 21 | /** 22 | * Support a current transaction, create a new one if none exists. 23 | */ 24 | REQUIRED = 'REQUIRED', 25 | /** 26 | * Create a new transaction, and suspend the current transaction if one exists. 27 | */ 28 | REQUIRES_NEW = 'REQUIRES_NEW', 29 | /** 30 | * Support a current transaction, execute non-transactionally if none exists. 31 | */ 32 | SUPPORTS = 'SUPPORTS', 33 | } 34 | -------------------------------------------------------------------------------- /src/Transactional.ts: -------------------------------------------------------------------------------- 1 | import { Options, wrapInTransaction } from './wrapInTransaction' 2 | 3 | /** 4 | * Used to declare a Transaction operation. In order to use it, you must use {@link BaseRepository} custom repository in order to use the Transactional decorator 5 | * @param connectionName - the typeorm connection name. 'default' by default 6 | * @param propagation - The transaction propagation type. see {@link Propagation} 7 | * @param isolationLevel - The transaction isolation level. see {@link IsolationLevel} 8 | */ 9 | export function Transactional(options?: Options): MethodDecorator { 10 | return (target: any, methodName: string | symbol, descriptor: TypedPropertyDescriptor) => { 11 | const originalMethod = descriptor.value 12 | descriptor.value = wrapInTransaction(originalMethod, { ...options, name: methodName }) 13 | 14 | Reflect.getMetadataKeys(originalMethod).forEach(previousMetadataKey => { 15 | const previousMetadata = Reflect.getMetadata(previousMetadataKey, originalMethod) 16 | Reflect.defineMetadata(previousMetadataKey, previousMetadata, descriptor.value) 17 | }) 18 | 19 | Object.defineProperty(descriptor.value, 'name', { value: originalMethod.name, writable: false }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/TransactionalError.ts: -------------------------------------------------------------------------------- 1 | export class TransactionalError extends Error { 2 | public name = 'TransactionalError' 3 | constructor(message: string) { 4 | super(message) 5 | Object.setPrototypeOf(this, TransactionalError.prototype) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import { createNamespace, getNamespace, Namespace } from 'cls-hooked' 2 | import { EventEmitter } from 'events' 3 | import { EntityManager, getManager } from 'typeorm' 4 | import { debugLog } from './DebugLog' 5 | 6 | export const NAMESPACE_NAME = '__typeOrm___cls_hooked_tx_namespace' 7 | 8 | const TYPE_ORM_KEY_PREFIX = '__typeOrm__transactionalEntityManager_' 9 | const TYPE_ORM_HOOK_KEY = '__typeOrm__transactionalCommitHooks' 10 | 11 | export const initializeTransactionalContext = () => { 12 | debugLog(`Transactional@initializeTransactionalContext`) 13 | return getNamespace(NAMESPACE_NAME) || createNamespace(NAMESPACE_NAME) 14 | } 15 | 16 | export const getEntityManagerOrTransactionManager = ( 17 | connectionName: string, 18 | entityManager: EntityManager | undefined 19 | ): EntityManager => { 20 | const context = getNamespace(NAMESPACE_NAME) 21 | 22 | if (context && context.active) { 23 | return getEntityManagerForConnection(connectionName, context) || entityManager 24 | } 25 | return entityManager || getManager(connectionName) 26 | } 27 | 28 | export const getEntityManagerForConnection = ( 29 | connectionName: string, 30 | context: Namespace 31 | ): EntityManager => { 32 | return context.get(`${TYPE_ORM_KEY_PREFIX}${connectionName}`) 33 | } 34 | 35 | export const setEntityManagerForConnection = ( 36 | connectionName: string, 37 | context: Namespace, 38 | entityManager: EntityManager | null 39 | ) => context.set(`${TYPE_ORM_KEY_PREFIX}${connectionName}`, entityManager) 40 | 41 | export const getHookInContext = (context: Namespace | undefined): EventEmitter | null => { 42 | return context?.get(TYPE_ORM_HOOK_KEY) 43 | } 44 | 45 | export const setHookInContext = (context: Namespace, emitter: EventEmitter | null) => { 46 | return context.set(TYPE_ORM_HOOK_KEY, emitter) 47 | } 48 | -------------------------------------------------------------------------------- /src/hook.ts: -------------------------------------------------------------------------------- 1 | import { getNamespace, Namespace } from 'cls-hooked' 2 | import { EventEmitter } from 'events' 3 | import { getHookInContext, NAMESPACE_NAME, setHookInContext } from './common' 4 | 5 | export const getTransactionalContextHook = () => { 6 | const ctx = getNamespace(NAMESPACE_NAME) 7 | const emitter = getHookInContext(ctx) 8 | if (!emitter) { 9 | throw new Error('No hook manager found in context. Are you using @Transactional()?') 10 | } 11 | return emitter 12 | } 13 | 14 | export const createEmitterInNewContext = (context: Namespace) => 15 | context.runAndReturn(_subctx => { 16 | const emitter = new EventEmitter() 17 | context.bindEmitter(emitter) 18 | return emitter 19 | }) 20 | 21 | export const runAndTriggerHooks = async (hook: EventEmitter, cb: () => any) => { 22 | try { 23 | const res = await cb() 24 | setImmediate(() => { 25 | hook.emit('commit') 26 | hook.emit('end', undefined) 27 | hook.removeAllListeners() 28 | }) 29 | return res 30 | } catch (err) { 31 | setImmediate(() => { 32 | hook.emit('rollback', err) 33 | hook.emit('end', err) 34 | hook.removeAllListeners() 35 | }) 36 | throw err 37 | } 38 | } 39 | 40 | export const runInNewHookContext = async (context: Namespace, cb: () => any) => { 41 | const hook = createEmitterInNewContext(context) 42 | return await context.runAndReturn(() => { 43 | setHookInContext(context, hook) 44 | return runAndTriggerHooks(hook, cb) 45 | }) 46 | } 47 | 48 | export const runOnTransactionCommit = (cb: () => void) => { 49 | getTransactionalContextHook().once('commit', cb) 50 | } 51 | 52 | export const runOnTransactionRollback = (cb: (e: Error) => void) => { 53 | getTransactionalContextHook().once('rollback', cb) 54 | } 55 | 56 | export const runOnTransactionComplete = (cb: (e: Error | undefined) => void) => { 57 | getTransactionalContextHook().once('end', cb) 58 | } 59 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseRepository } from './BaseRepository' 2 | export { BaseTreeRepository } from './BaseTreeRepository' 3 | export { initializeTransactionalContext, getEntityManagerOrTransactionManager, NAMESPACE_NAME } from './common' 4 | export { runOnTransactionCommit, runOnTransactionComplete, runOnTransactionRollback } from './hook' 5 | export { 6 | patchTypeORMRepositoryWithBaseRepository, 7 | patchTypeORMTreeRepositoryWithBaseTreeRepository, 8 | patchRepositoryManager, 9 | } from './patch-typeorm-repository' 10 | export { Propagation } from './Propagation' 11 | export { IsolationLevel } from './IsolationLevel' 12 | export { Transactional } from './Transactional' 13 | export { runInTransaction } from './runInTransaction'; 14 | export { wrapInTransaction } from './wrapInTransaction'; 15 | export * from './TransactionalError' 16 | -------------------------------------------------------------------------------- /src/patch-typeorm-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager, Repository, TreeRepository, MongoRepository } from 'typeorm' 2 | import { getEntityManagerOrTransactionManager } from './common' 3 | import { debugLog } from './DebugLog' 4 | 5 | export const patchRepositoryManager = (repositoryType: any) => { 6 | debugLog( 7 | `Transactional@patchRepositoryManager repositoryType: ${repositoryType?.constructor?.name}` 8 | ) 9 | Object.defineProperty(repositoryType, 'manager', { 10 | get() { 11 | return getEntityManagerOrTransactionManager(this._connectionName, this._manager) 12 | }, 13 | set(manager: EntityManager | undefined) { 14 | this._manager = manager 15 | this._connectionName = manager?.connection?.name 16 | }, 17 | }) 18 | } 19 | 20 | export const patchTypeORMRepositoryWithBaseRepository = () => { 21 | patchRepositoryManager(Repository.prototype) 22 | // Since MongoRepository inherits from Repository, but does declare the manager, we re-patch it 23 | // See #64 and #65 24 | Object.defineProperty(MongoRepository.prototype, 'manager', { 25 | configurable: true, 26 | writable: true, 27 | }) 28 | } 29 | 30 | export const patchTypeORMTreeRepositoryWithBaseTreeRepository = () => { 31 | patchRepositoryManager(TreeRepository.prototype) 32 | } 33 | -------------------------------------------------------------------------------- /src/runInTransaction.ts: -------------------------------------------------------------------------------- 1 | import { Options, wrapInTransaction } from './wrapInTransaction'; 2 | 3 | export function runInTransaction ReturnType>( 4 | fn: Func, 5 | options?: Options, 6 | ) { 7 | const wrapper = wrapInTransaction(fn, options); 8 | return wrapper(); 9 | } 10 | -------------------------------------------------------------------------------- /src/wrapInTransaction.ts: -------------------------------------------------------------------------------- 1 | import { getNamespace } from 'cls-hooked' 2 | import { EntityManager, getConnection, getManager } from 'typeorm' 3 | import { 4 | getEntityManagerForConnection, 5 | NAMESPACE_NAME, 6 | setEntityManagerForConnection, 7 | } from './common' 8 | import { runInNewHookContext } from './hook' 9 | import { IsolationLevel } from './IsolationLevel' 10 | import { Propagation } from './Propagation' 11 | import { TransactionalError } from './TransactionalError' 12 | 13 | export type Options = { 14 | connectionName?: string | (() => string | undefined) 15 | propagation?: Propagation 16 | isolationLevel?: IsolationLevel 17 | }; 18 | 19 | export function wrapInTransaction ReturnType>( 20 | fn: Func, 21 | options?: Options & { name?: string | symbol } 22 | ) { 23 | function wrapped(this: unknown, ...newArgs: unknown[]): ReturnType { 24 | const context = getNamespace(NAMESPACE_NAME) 25 | if (!context) { 26 | throw new Error( 27 | 'No CLS namespace defined in your app ... please call initializeTransactionalContext() before application start.' 28 | ) 29 | } 30 | let tempConnectionName = 31 | options && options.connectionName ? options.connectionName : 'default' 32 | if (typeof tempConnectionName !== 'string') { 33 | tempConnectionName = tempConnectionName() || 'default' 34 | } 35 | const connectionName: string = tempConnectionName 36 | const methodNameStr = String(options?.name) 37 | 38 | const propagation: Propagation = 39 | options && options.propagation ? options.propagation : Propagation.REQUIRED 40 | const isolationLevel: IsolationLevel | undefined = options && options.isolationLevel 41 | const isCurrentTransactionActive = getManager(connectionName)?.queryRunner 42 | ?.isTransactionActive 43 | 44 | const operationId = String(new Date().getTime()) 45 | const logger = getConnection(connectionName).logger 46 | const log = (message: string) => 47 | logger.log( 48 | 'log', 49 | `Transactional@${operationId}|${connectionName}|${methodNameStr}|${isolationLevel}|${propagation} - ${message}` 50 | ) 51 | 52 | log(`Before starting: isCurrentTransactionActive = ${isCurrentTransactionActive}`) 53 | 54 | const runOriginal = async () => fn.apply(this, [...newArgs]) 55 | const runWithNewHook = async () => runInNewHookContext(context, runOriginal) 56 | 57 | const runWithNewTransaction = async () => { 58 | const transactionCallback = async (entityManager: EntityManager) => { 59 | log( 60 | `runWithNewTransaction - set entityManager in context: isCurrentTransactionActive: ${entityManager?.queryRunner?.isTransactionActive}` 61 | ) 62 | setEntityManagerForConnection(connectionName, context, entityManager) 63 | try { 64 | const result = await fn.apply(this, [...newArgs]) 65 | log(`runWithNewTransaction - Success`) 66 | return result 67 | } catch (e) { 68 | log(`runWithNewTransaction - ERROR|${e}`) 69 | throw e 70 | } finally { 71 | log(`runWithNewTransaction - reset entityManager in context`) 72 | setEntityManagerForConnection(connectionName, context, null) 73 | } 74 | } 75 | 76 | if (isolationLevel) { 77 | return await runInNewHookContext(context, () => 78 | getManager(connectionName).transaction(isolationLevel, transactionCallback) 79 | ) 80 | } else { 81 | return await runInNewHookContext(context, () => 82 | getManager(connectionName).transaction(transactionCallback) 83 | ) 84 | } 85 | } 86 | 87 | return context.runAndReturn(async () => { 88 | const currentTransaction = getEntityManagerForConnection(connectionName, context) 89 | 90 | switch (propagation) { 91 | case Propagation.MANDATORY: 92 | if (!currentTransaction) { 93 | throw new TransactionalError( 94 | "No existing transaction found for transaction marked with propagation 'MANDATORY'" 95 | ) 96 | } 97 | return runOriginal() 98 | case Propagation.NESTED: 99 | return runWithNewTransaction() 100 | case Propagation.NEVER: 101 | if (currentTransaction) { 102 | throw new TransactionalError( 103 | "Found an existing transaction, transaction marked with propagation 'NEVER'" 104 | ) 105 | } 106 | return runWithNewHook() 107 | case Propagation.NOT_SUPPORTED: 108 | if (currentTransaction) { 109 | setEntityManagerForConnection(connectionName, context, null) 110 | const result = await runWithNewHook() 111 | setEntityManagerForConnection(connectionName, context, currentTransaction) 112 | return result 113 | } 114 | return runOriginal() 115 | case Propagation.REQUIRED: 116 | if (currentTransaction) { 117 | return runOriginal() 118 | } 119 | return runWithNewTransaction() 120 | case Propagation.REQUIRES_NEW: 121 | return runWithNewTransaction() 122 | case Propagation.SUPPORTS: 123 | if (currentTransaction) { 124 | return runOriginal() 125 | } else { 126 | return runWithNewHook() 127 | } 128 | } 129 | }) as unknown as ReturnType; 130 | } 131 | 132 | return wrapped as Func; 133 | } 134 | -------------------------------------------------------------------------------- /tests/__tests__/test_nestjs.ts: -------------------------------------------------------------------------------- 1 | import { initializeTransactionalContext, patchTypeORMRepositoryWithBaseRepository } from '../../src' 2 | import { Post } from '../entity/Post' 3 | import { AppService } from '../nestjs/app.service' 4 | import { Test, TestingModule } from '@nestjs/testing' 5 | import { TypeOrmModule } from '@nestjs/typeorm' 6 | 7 | describe('NestJS', () => { 8 | let app: TestingModule 9 | let service: AppService 10 | beforeAll(async () => { 11 | initializeTransactionalContext() 12 | patchTypeORMRepositoryWithBaseRepository() 13 | app = await Test.createTestingModule({ 14 | imports: [ 15 | TypeOrmModule.forRoot({ 16 | type: 'postgres', 17 | host: 'localhost', 18 | port: 5432, 19 | username: 'postgres', 20 | password: 'postgres', 21 | entities: [Post], 22 | synchronize: true, 23 | logging: 'all', 24 | }), 25 | TypeOrmModule.forFeature([Post]), 26 | ], 27 | exports: [], 28 | providers: [AppService], 29 | }).compile() 30 | service = app.get(AppService) 31 | }) 32 | 33 | afterAll(async () => await app.close()) 34 | 35 | it('Creates a post using service', async done => { 36 | const message = 'NestJS - A successful post' 37 | const post = await service.createPost(message) 38 | expect(post.id).toBeGreaterThan(0) 39 | setTimeout(async () => { 40 | expect(service.success).toEqual('true') 41 | const dbPost = await service.getPostByMessage(message) 42 | console.log(`dbPost: ${dbPost}`) 43 | expect(dbPost).toBeTruthy() 44 | done() 45 | }, 1000) 46 | }) 47 | 48 | it('Fails creating a post using service', async done => { 49 | const message = 'NestJS - An unsuccessful post' 50 | try { 51 | const post = await service.createPost(message, true) 52 | } catch (e) { 53 | setTimeout(async () => { 54 | expect(service.success).toEqual('false') 55 | const dbPost = await service.getPostByMessage(message) 56 | console.log(`dbPost: ${dbPost}`) 57 | expect(dbPost).toBeFalsy() 58 | done() 59 | }, 1000) 60 | } 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /tests/__tests__/test_simple.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | 3 | import { initializeTransactionalContext, patchTypeORMRepositoryWithBaseRepository, runInTransaction, runOnTransactionCommit, runOnTransactionRollback, wrapInTransaction } from '../../src' 4 | import { createConnection, getConnection } from 'typeorm' 5 | import delay from 'delay'; 6 | import { Post } from '../entity/Post' 7 | import { SimpleService } from '../simple/simple.service' 8 | 9 | describe('Simple', () => { 10 | beforeAll(async () => { 11 | initializeTransactionalContext() 12 | patchTypeORMRepositoryWithBaseRepository() 13 | const conn = await createConnection({ 14 | type: 'postgres', 15 | host: 'localhost', 16 | port: 5432, 17 | username: 'postgres', 18 | password: 'postgres', 19 | entities: [Post], 20 | synchronize: true, 21 | logging: 'all', 22 | }) 23 | }) 24 | 25 | afterAll(async () => await getConnection().close()) 26 | 27 | it('Creates a post using service', async () => { 28 | const repository = getConnection().getRepository(Post) 29 | const service = new SimpleService(repository) 30 | const message = 'simple - A successful post' 31 | const post = await service.createPost(message) 32 | 33 | await delay(100) 34 | 35 | expect(post.id).toBeGreaterThan(0) 36 | expect(service.success).toEqual('true') 37 | const dbPost = await service.getPostByMessage(message) 38 | // tslint:disable-next-line: no-console 39 | console.log(`dbPost: ${dbPost}`) 40 | expect(dbPost).toBeTruthy() 41 | }) 42 | 43 | it('Fails creating a post using service', async () => { 44 | const repository = getConnection().getRepository(Post) 45 | const service = new SimpleService(repository) 46 | const message = 'simple - An unsuccessful post' 47 | expect(service.createPost(message, true)).rejects.toThrow() 48 | 49 | await delay(100) 50 | 51 | expect(service.success).toEqual('false') 52 | const dbPost = await service.getPostByMessage(message) 53 | // tslint:disable-next-line: no-console 54 | console.log(`dbPost: ${dbPost}`) 55 | expect(dbPost).toBeFalsy() 56 | }) 57 | 58 | it('Create a post using wrapInTransaction', async () => { 59 | const repository = getConnection().getRepository(Post) 60 | const post = new Post(); 61 | const message = 'simple - An successful post using wrapInTransaction' 62 | post.message = message; 63 | let commitHookCalled = false; 64 | 65 | const result = await (wrapInTransaction(async () => { 66 | const createdPost = await repository.save(post); 67 | runOnTransactionCommit(() => { 68 | commitHookCalled = true 69 | }); 70 | return createdPost; 71 | }))() 72 | 73 | await delay(10); 74 | 75 | expect(post.id).toBeGreaterThan(0) 76 | expect(commitHookCalled).toBeTruthy(); 77 | expect(repository.findOne({ message })).resolves.toBeTruthy(); 78 | }); 79 | 80 | it('Fails creating a post using using wrapInTransaction', async () => { 81 | const repository = getConnection().getRepository(Post) 82 | const post = new Post(); 83 | const message = 'simple - An failed post using wrapInTransaction' 84 | post.message = message; 85 | let rollbackHookCalled = false; 86 | 87 | expect( 88 | wrapInTransaction(async () => { 89 | const createdPost = await repository.save(post); 90 | runOnTransactionRollback(() => { 91 | rollbackHookCalled = true 92 | }); 93 | throw new Error('failing') 94 | })() 95 | ).rejects.toThrow(); 96 | 97 | await delay(100); 98 | 99 | expect(rollbackHookCalled).toBeTruthy(); 100 | expect(repository.findOne({ message })).resolves.toBeUndefined(); 101 | }); 102 | 103 | it('Create a post using runInTransaction', async () => { 104 | const repository = getConnection().getRepository(Post) 105 | const post = new Post(); 106 | const message = 'simple - An successful post using runInTransaction' 107 | post.message = message; 108 | let commitHookCalled = false; 109 | 110 | const result = await runInTransaction(async () => { 111 | const createdPost = await repository.save(post); 112 | runOnTransactionCommit(() => { 113 | commitHookCalled = true 114 | }); 115 | return createdPost; 116 | }) 117 | 118 | await delay(100) 119 | 120 | expect(post.id).toBeGreaterThan(0) 121 | expect(commitHookCalled).toBeTruthy(); 122 | expect(repository.findOne({ message })).resolves.toBeTruthy(); 123 | }); 124 | 125 | it('Fails creating a post using using runInTransaction', async () => { 126 | const repository = getConnection().getRepository(Post) 127 | const post = new Post(); 128 | const message = 'simple - An failed post using runInTransaction' 129 | post.message = message; 130 | let rollbackHookCalled = false; 131 | 132 | expect( 133 | runInTransaction(async () => { 134 | const createdPost = await repository.save(post); 135 | runOnTransactionRollback(() => { 136 | rollbackHookCalled = true 137 | }); 138 | throw new Error('failing') 139 | }) 140 | ).rejects.toThrow(); 141 | 142 | await delay(100); 143 | 144 | expect(rollbackHookCalled).toBeTruthy(); 145 | expect(repository.findOne({ message })).resolves.toBeUndefined(); 146 | }); 147 | }) 148 | -------------------------------------------------------------------------------- /tests/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: postgres:alpine 5 | ports: 6 | - '5432:5432' 7 | environment: 8 | POSTGRES_PASSWORD: postgres 9 | POSTGRES_USER: postgres 10 | POSTGRES_DB: test 11 | -------------------------------------------------------------------------------- /tests/entity/Post.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm' 2 | 3 | @Entity() 4 | export class Post { 5 | @PrimaryGeneratedColumn() 6 | id: number 7 | 8 | @Column() 9 | message: string 10 | } 11 | -------------------------------------------------------------------------------- /tests/nestjs/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectRepository } from '@nestjs/typeorm' 3 | import { Repository } from 'typeorm' 4 | import { Post } from '../entity/Post' 5 | import { SimpleService } from '../simple/simple.service' 6 | 7 | @Injectable() 8 | export class AppService extends SimpleService { 9 | constructor( 10 | @InjectRepository(Post) 11 | readonly repository: Repository 12 | ) { 13 | super(repository) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/simple/simple.service.ts: -------------------------------------------------------------------------------- 1 | import { Transactional, runOnTransactionCommit, runOnTransactionRollback } from '../../src' 2 | import { Post } from '../entity/Post' 3 | import { Repository } from 'typeorm' 4 | 5 | export class SimpleService { 6 | constructor(readonly repository: Repository) {} 7 | private _success = '' 8 | get success(): string { 9 | return this._success 10 | } 11 | set success(value: string) { 12 | this._success = value 13 | } 14 | 15 | @Transactional() 16 | async createPost(message: string, fail: boolean = false): Promise { 17 | const post = new Post() 18 | post.message = message 19 | await this.repository.save(post) 20 | runOnTransactionCommit(() => (this.success = 'true')) 21 | runOnTransactionRollback(() => (this.success = 'false')) 22 | if (fail) { 23 | throw Error('fail = true, so failing') 24 | } 25 | return post 26 | } 27 | 28 | async getPostByMessage(message: string): Promise { 29 | return this.repository.findOne({ message }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "lib": ["es5", "es6", "dom"], 6 | "declaration": true, 7 | "noImplicitAny": true, 8 | "removeComments": true, 9 | "noLib": false, 10 | "strictNullChecks": true, 11 | "allowSyntheticDefaultImports": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es5", 15 | "sourceMap": true, 16 | "outDir": "dist", 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "test/**/*.ts", "dist", "temp"] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "variable-name": [false] 9 | }, 10 | "rulesDirectory": [] 11 | } 12 | --------------------------------------------------------------------------------