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

@dumpstate/dbaction

v0.2.7

Published

reader monad for database connection

Downloads

8

Readme

dbaction

Reader monad for database connection.

Install

Install package:

npm install @dumpstate/dbaction --save

Import

There's either a generic DBAction available:

import { DBAction, Transactor, sequence, pure } from "@dumpstate/dbaction"

or PostgreSQL (node-postgres-based) specific:

import {
	DBAction,
	Transactor,
	sequence,
	pure,
} from "@dumpstate/dbaction/lib/PG"

PostgreSQL specific, requires node-postgres driver installed:

npm install pg --save

Usage

import { Transactor, query } from "@dumpstate/dbaction/lib/PG"
import { Pool } from "pg"

// 1. Create connection pool.
const pool = new Pool({...})
// 2. Create transactor.
const tr = new Transactor(pool)

// 3. Build up some logic by chaining dbactions.
const action = query("SELECT COUNT(*) AS num FROM FOO")
    .map(res => res.rows[0].num)

// 4. Execute.
const res = await action.run(tr)

Motivation

In a node.js, database-based applications one's usually maintains a connection pool. With a connection pool, when the logic wants to access the database, one's requests a connection from the pool, performs the operation then releases connection back to the pool so it's available for another routines.

Because of the nature of the databases, in order to achieve atomicity guarantees, some operations need to be performed on the same connection.

For example, let's say we have two repositories, each backed by a single table in a relational database.

class FooRepository {
    fetchById(id: string): Promise<Foo> { ... }
    create(foo: Foo): Promise<Foo> { ... }
}

class BarRepository {
    fetchById(id: string): Promise<Bar> { ... }
    create(bar: Bar): Promise<Bar> { ... }
}

and a logic, that wants to create Foo and Bar atomically:

const fooRepo = new FooRepository(...)
const barRepo = new BarRepo(...)

await fooRepo.create(new Foo(...))
await barRepo.create(new Bar(...))

from the connection pool perspective, one could bundle the pool into the repositories, like:

class FooRepository {
    private pool: Pool

    constructor(pool: Pool) { this.pool = pool }

    ...
}

and then, the logic may request the connection when it's needed. Two immediate problems:

  1. it is not possible to create Foo and Bar atomically, as the operations may be performed on different connections,
  2. as the connection pool is a finite resource, in some scenarios the application may end up in a deadlock state.

to fix the above, one could change the signature of all the repository methods, to accept the connection as an argument:

class FooRepository {
	fetchById(conn: PoolClient, id: string): Promise<Foo>
	create(conn: PoolClient, foo: Foo): Promise<Foo>
}

now, Foo and Bar could be created as:

const fooRepo = new FooRepository()
const barRepo = new BarRepository()

const pool = new Pool(...)
const conn = await pool.connect()

await conn.query("BEGIN")
await fooRepo.create(conn, new Foo(...))
await barRepo.create(conn, new Bar(...))
await conn.query("COMMIT")

where both Foo and Bar insert queries are executed on the same database connection, wrapped with BEGIN / COMMIT clause.

All is correct, except the code is polluted with the database connection as an argument.

Here comes the DBAction - a reader-like monad for a database connection as an environment. The logic of an application can now be written as a transformation of a monad, and then at the final step the owner of a reference to the monad may decide whether to just execute the program on a database connection or to wrap the monad with BEGIN / COMMIT transaction clause.

import { DBAction, Transactor, sequence } from "@dumpstate/dbaction/lib/PG"

class FooRepository {
    fetchById(id: string): DBAction<Foo>
    create(foo: Foo): DBAction<Foo>
}

class BarRepository {
    fetchById(id: string): DBAction<Bar>
    create(bar: Bar): DBAction<Bar>
}

const fooRepo = new FooRepository()
const barRepo = new BarRepository()

const pool = new Pool(...)
const tr = new Transactor(pool)

const [foo, bar] = await sequence(
    fooRepo.save(new Foo(...)),
    barRepo.save(new Bar(...)),
).transact(tr)

The sequence is just a utility to execute all operations contained within concurrently. Without sequence, one could simply chain the actions with flatMap.

await fooRepo.save(new Foo(...))
    .flatMap(() => barRepo.create(new Bar(...)))
    .transact(tr)

DBAction is a cold set of instructions, thus a statement like:

fooRepo.save(new Foo(...))
    .flatMap(() => barRepo.create(new Bar(...)))

does not trigger any side effects, until either run or transact is called.

Both run and transact require an instance of a Transactor - i.e., a db-driver specific object that knows how to manage the lifecycle of a database connection.

Documentation

The default DBAction<A, B> (imported from @dumpstate/dbaction), requires two generic parameters:

  • A - the connection type,
  • B - the value type.

For a driver specific DBAction<B> (imported from @dumpstate/dbaction/lib/PG - for postgres), the value type is the only parameter.

DBAction

  1. .map<K>(fn: (item: T) => K) - transforms value of type T into type K given a function fn.
  2. .flatMap<K>(fn: (item: T) => DBAction<K>) - transforms value of type T into type K given a function fn, where a function returns a DBAction<K>; useful for chaining database operations, e.g., a result from the database query is an input for another database query.
  3. .run(tr: Transactor): Promise<T> - requests connection from the pool, executes the query, releases connection, returns value.
  4. .transact(tr: Transactor): Promise<T> - same as run, but wrapped with BEGIN / COMMIT / ROLLBACK.

Utilities

  1. flatten - given an array of DBActions, returns a single DBAction with a value being an array of items.
  2. pure - wraps either a scalar, a promise or a function returning promise as a DBAction, so a database operation can be composed with another, non-db, async operation.
  3. chain - builds a sequential chain of DBActions, where a result of the previous is an input to the latter; returns the value of the last operation in the chain.
  4. sequence - executes operations concurrently; returns a single DBAction being an array of values.
  5. query - returns DBAction given the query string and query arguments.