Skip to content

Commit

Permalink
Merge pull request #6 from mokuteki225/feat/implicit
Browse files Browse the repository at this point in the history
Implicit flow control
  • Loading branch information
vkondratiuk482 authored Nov 8, 2023
2 parents 8c804b4 + 5d8a0de commit 82c9785
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 114 deletions.
100 changes: 53 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ npm i @mokuteki/propagated-transactions

1. Create an implementation of `ITransactionRunner` interface (provided by the package) for your specific database, driver, ORM, whatever
2. Create an instance of `PropagatedTransaction` and pass implementation from step one into constructor
3. Instantiate and store database connection by starting the transaction with `PropagatedTransaction.start()`
4. Create a callback that executes business logic, use `PropagatedTransaction.commit() / PropagatedTransaction.rollback()` inside of it
5. Run `PropagatedTransaction.run(connection, callback)`, where `connection` is stored connection from step three, `callback` is a callback from step four
6. Obtain connection inside of inner method/abstraction layer and use it to run your query
3. Create a callback that executes business logic, pass it to `PropagatedTransaction.run()`. If the execution of the provided callback fails - the library rollbacks the transaction and rethrows the error. In case of a successful execution we implicitly commit the transaction and return the value from the callback
4. Obtain connection inside of inner method/abstraction layer and use it to run your query

### Examples

Expand Down Expand Up @@ -61,24 +59,14 @@ module.exports.ptx = new PropagatedTransaction(KnexTransactionRunner);
```js
async create(payload1, payload2) {
// Step 3
const connection = await ptx.start();

// Step 4
const callback = async () => {
try {
const user = await userService.create(payload1);
const wallet = await walletService.create(payload2);

await ptx.commit();
const user = await userService.create(payload1);
const wallet = await walletService.create(payload2);

return user;
} catch (err) {
await ptx.rollback();
}
return user;
};

// Step 5
const user = await ptx.run(connection, callback);
const user = await ptx.run(callback);

return user;
}
Expand All @@ -88,7 +76,7 @@ async create(payload1, payload2) {
class UserService {
async create(payload) {
/**
* Step 6
* Step 4
* If you run this method in PropagatedTransaction context it will be executed in transaction
* Otherwise it will be executed as usual query
*/
Expand All @@ -101,7 +89,7 @@ class UserService {
```js
class WalletService {
async create(payload) {
// Step 6
// Step 4
const connection = ptx.connection || knex;
return connection('wallet').insert(payload);
}
Expand Down Expand Up @@ -161,24 +149,14 @@ export class UserService {
payload2: ICreateWallet
): Promise<UserEntity> {
// Step 3
const connection = await this.ptx.start();

// Step 4
const callback = async () => {
try {
const user = await this.userRepository.create(payload1);
const wallet = await this.walletRepository.create(payload2);
const user = await this.userRepository.create(payload1);
const wallet = await this.walletRepository.create(payload2);

await this.ptx.commit();

return user;
} catch (err) {
await this.ptx.rollback();
}
return user;
};

// Step 5
const user = await this.ptx.run<Promise<UserEntity>>(connection, callback);
const user = await this.ptx.run<Promise<UserEntity>>(callback);

return user;
}
Expand All @@ -193,7 +171,7 @@ export class UserRepository implements IUserRepository {
) {}

/**
* Step 6
* Step 4
* If you run this method in PropagatedTransaction context it will be executed in transaction
* Otherwise it will be executed as usual query
*/
Expand All @@ -215,7 +193,7 @@ export class WalletRepository implements IWalletRepository {
) {}

/**
* Step 6
* Step 4
* If you run this method in PropagatedTransaction context it will be executed in transaction
* Otherwise it will be executed as usual query
*/
Expand All @@ -236,6 +214,8 @@ Package gives you an ability to work with essential isolation levels:
* `REPEATABLE READ`
* `SERIALIZABLE`

Just import `IsolationLevels` and pass the desired level as a second argument of `PropagatedTransaction.run()` method

By default we use `READ COMMITTED` isolations level

```js
Expand All @@ -260,28 +240,54 @@ const KnexTransactionRunner = {
```

```js
async create(payload1, payload2) {
const connection = await ptx.start(IsolationLevels.ReadCommitted);
const { IsolationLevels } = require('@mokuteki/propagated-transactions')

// some code

async create(payload1, payload2) {
const callback = async () => {
try {
const user = await userService.create(payload1);
const wallet = await walletService.create(payload2);
const user = await userService.create(payload1);
const wallet = await walletService.create(payload2);

await ptx.commit();
return user;
};

return user;
} catch (err) {
await ptx.rollback();
}
const user = await ptx.run(callback, IsolationLevels.Serializable);

return user;
}
```

#### Nested execution
Since version <b><u>1.2.0</u></b> the library supports execution of nested transactions like in the example below. That means that if we call `updateBalance` from `create`, the `updateBalance` won't start a separate transaction, and will be treated as a part of `create's` transaction. However, if we call `updateBalance` directly, it will start its own transaction


```js
async create(payload1, payload2) {
const callback = async () => {
const user = await userService.create(payload1);
const wallet = await walletService.create(payload2);

return user;
};

const user = await ptx.run(connection, callback);
const user = await ptx.run(callback);

return user;
}
```

```js
async updateBalance(payload2) {
const callback = async () => {
await walletService.updateBalance(payload2);
};

return ptx.run(callback);
}
```


## Motivation

Imagine we need to run `UserService.create()` and `WalletService.create()` in transaction
Expand Down
2 changes: 1 addition & 1 deletion db/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
version: '3.8'
services:
postgres:
propagated_transactions_postgres:
image: postgres:14
restart: always
environment:
Expand Down
30 changes: 25 additions & 5 deletions lib/propagated-transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,48 @@ class PropagatedTransaction {
PropagatedTransaction.#instance = this;
}

async start(isolationLevel = IsolationLevels.ReadCommitted) {
async #start(isolationLevel = IsolationLevels.ReadCommitted) {
return this.connection || this.runner.start(isolationLevel);
}

async commit() {
async #commit() {
if (!this.connection) {
throw TransactionError.NotInContext();
}

return this.runner.commit(this.connection);
}

async rollback() {
async #rollback() {
if (!this.connection) {
throw TransactionError.NotInContext();
}

return this.runner.rollback(this.connection);
}

async run(connection, callback) {
return this.als.run(connection, callback);
async run(callback, isolationLevel = undefined) {
if (this.connection) {
return callback();
}

const connection = await this.#start(isolationLevel);

const wrapped = async () => {
try {
const result = await callback();

await this.#commit();

return result;
} catch (err) {
await this.#rollback();

throw err;
}
};

return this.als.run(connection, wrapped);
}
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mokuteki/propagated-transactions",
"version": "1.1.4",
"version": "1.2.0",
"description": "Convenient wrapper to propagate and manage database transactions using AsyncLocalStorage",
"main": "lib/propagated-transaction.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ const KnexTransactionRunner = {
const data = {
user: {
id: 1,
balance: 0,
name: 'Mykola',
surname: 'Lysenko',
balance: 0,
},
};

Expand Down
Loading

0 comments on commit 82c9785

Please sign in to comment.