npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@mokuteki/propagated-transactions

v1.2.2

Published

Convenient wrapper to propagate and manage database transactions using AsyncLocalStorage

Downloads

66

Readme

@mokuteki/propagated-transactions

Convenient wrapper to propagate database connection and give you opportunity to manage the state of your transactions, uses AsyncLocalStorage under the hood, Typescript friendly, 0 production dependencies

Main advantages and use cases

  • Package prevents you from polluting method arguments with connection object userService.create(payload, connection)
  • Package prevents you from binding your transaction managing logic to any database, driver, ORM, whatever. In case you need to change one of the mentioned above, you will just need to provide a different implementation of ITransactionRunner

Installation

npm i @mokuteki/propagated-transactions

Usage

  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. 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

Javascript example

const { PropagatedTransaction } = require('@mokuteki/propagated-transactions');

const knex = require('knex')({
  client: 'pg',
  connection: {
    version: '8.10',
    host: '127.0.0.1',
    port: 5432,
    user: 'mokuteki',
    password: 'pass123',
    database: 'propagated-test',
  },
});

// Step 1
const KnexTransactionRunner = {
  start: async () => {
    const trx = await knex.transaction();

    return trx;
  },
  commit: async (trx) => {
    return trx.commit();
  },
  rollback: async (trx) => {
    return trx.rollback();
  },
};

// Step 2
module.exports.ptx = new PropagatedTransaction(KnexTransactionRunner);
async create(payload1, payload2) {
  // Step 3
  const callback = async () => {
    const user = await userService.create(payload1);
    const wallet = await walletService.create(payload2);

    return user;
  };

  const user = await ptx.run(callback);

  return user;
}
class UserService {
  async create(payload) {
    /**
     * Step 4
     * If you run this method in PropagatedTransaction context it will be executed in transaction
     * Otherwise it will be executed as usual query
     */
    const connection = ptx.connection || knex;
    return connection('user').insert(payload);
  }
}
class WalletService {
  async create(payload) {
    // Step 4
    const connection = ptx.connection || knex;
    return connection('wallet').insert(payload);
  }
}

Typescript example + Layers Separation

import { DataSource, QueryRunner } from 'typeorm';
import { PropagatedTransaction, ITransactionRunner } from '@mokuteki/propagated-transactions';

// Step 1
class TypeormTransactionRunner implements ITransactionRunner<QueryRunner> {
  constructor(private readonly dataSource: DataSource) {}

  /**
   * Book and return database connection, run `.start()` method if exists
   */
  public async start(): Promise<QueryRunner> {
    const queryRunner = this.dataSource.createQueryRunner();

    await queryRunner.connect();

    await queryRunner.startTransaction();

    return queryRunner;
  }

  public async commit(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.commitTransaction();

    return queryRunner.release();
  }

  public async rollback(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.rollbackTransaction();

    return queryRunner.release();
  }
}

// Step 2
export const ptx = new PropagatedTransaction(TypeormTransactionRunner);
export class UserService {
  constructor(
    private readonly ptx: PropagatedTransaction, 
    private readonly userRepository: IUserRepository, 
    private readonly walletRepository: IWalletRepository, 
  ) {}

  public async create(
    payload1: ICreateUser, 
    payload2: ICreateWallet
  ): Promise<UserEntity> {
    // Step 3
    const callback = async () => {
      const user = await this.userRepository.create(payload1);
      const wallet = await this.walletRepository.create(payload2);

      return user;
    };

    const user = await this.ptx.run<Promise<UserEntity>>(callback);

    return user;
  }
}
export class UserRepository implements IUserRepository {
  constructor(
    private readonly manager: EntityManager, 
    private readonly ptx: PropagatedTransaction,
  ) {}

  /**
   * Step 4
   * If you run this method in PropagatedTransaction context it will be executed in transaction
   * Otherwise it will be executed as usual query
   */
  public async create(data: ICreateUser): Promise<UserEntity> {
    const manager = this.ptx.connection?.manager || this.manager;

    const user = manager.getRepository(TypeormUserEntity).create(data);

    return manager.save(user)
  }
}
export class WalletRepository implements IWalletRepository {
  constructor(
    private readonly manager: EntityManager, 
    private readonly ptx: PropagatedTransaction,
  ) {}

  /**
   * Step 4
   * If you run this method in PropagatedTransaction context it will be executed in transaction
   * Otherwise it will be executed as usual query
   */
  public async create(data: ICreateWallet): Promise<WalletEntity> {
    const manager = this.ptx.connection?.manager || this.manager;

    const wallet = manager.getRepository(TypeormWalletEntity).create(data);

    return manager.save(wallet)
  }
}

Isolation Level

Package gives you an ability to work with essential isolation levels:

  • READ COMMITTED
  • READ UNCOMMITTED
  • 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

const KnexTransactionRunner = {
  start: async (isolationLevel) => {
    // adapt internal @mokuteki/propagated-transactions isolation level to the one your transaction runner uses
    const adaptedIsolationLevel = adapt(isolationLevel);

    const trx = await knex.transaction({
      isolationLevel: adaptedIsolationLevel,
    });

    return trx;
  },
  commit: async (trx) => {
    return trx.commit();
  },
  rollback: async (trx) => {
    return trx.rollback();
  },
};
const { IsolationLevels } = require('@mokuteki/propagated-transactions')

// some code

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(callback, IsolationLevels.Serializable);

  return user;
}

Nested execution

Since version 1.2.0 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

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(callback);

  return user;
}
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

class UserService {
  async create(payload) {
    return knex('user').insert(payload);
  }
}
class WalletService {
  async create(payload) {
    return knex('wallet').insert(payload);
  }
}

Due to Node.js asynchronous nature in order to guarantee operations' atomicity we have to book a database connection and execute all of the transaction's operations using this specific connection. So we have to do something like this

class UserService {
  async create(payload, trx) {
    return trx('user').insert(payload);
  }
}
class WalletService {
  async create(payload, trx) {
    return trx('wallet').insert(payload);
  }
}
async create(payload1, payload2) {
  const trx = await knex.transaction();

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

    await trx.commit();
  } catch (err) {
    await trx.rollback();
  }
}

The idea of this package is to propagate the connection and give you opportunity to manage the state of your transaction without binding your business logic to any database, ORM, driver, whatever

Tests

Before doing any kind of contribution run tests, here is how

cd db && sudo docker-compose up -d

npm run test

License

Licensed under MIT