Skip to content

Commit

Permalink
feat: add connection.transaction method
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Jun 6, 2017
1 parent edf6ae4 commit e532ae0
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 15 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ A PostgreSQL client with strict types and assertions.
* [`maybeOne`](#maybeone)
* [`one`](#one)
* [`query`](#query)
* [`transaction`](#transaction)
* [Overriding Error Constructor](#overriding-error-constructor)
* [Error handling](#error-handling)
* [Handling `NotFoundError`](#handling-notfounderror)
Expand Down Expand Up @@ -293,6 +294,20 @@ The following error types can be overridden:

* `NotFoundError`

### `transaction`

`transaction` method is used wrap execution of queries in `START TRANSACTION` and `COMMIT` or `ROLLBACK`. `COMMIT` is called if the transaction handler returns a promise that resolves; `ROLLBACK` is called otherwise.

`transaction` method can be used together with `createPool` method. When used to create a transaction from an instance of a pool, a new connection is allocated for the duration of the transaction.

```js
await connection.transaction(async (transactionConnection) => {
transactionConnection.query(`INSERT INTO foo (bar) VALUES ('baz')`);
transactionConnection.query(`INSERT INTO qux (quux) VALUES ('quuz')`);
});

```

## Error handling

### Handling `NotFoundError`
Expand Down
59 changes: 45 additions & 14 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
InternalQueryMaybeOneType,
InternalQueryOneType,
InternalQueryType,
InternalTransactionType,
QueryResultRowType,
TaggledTemplateLiteralInvocationType
} from './types';
Expand Down Expand Up @@ -188,6 +189,20 @@ export const any: InternalQueryAnyType = async (connection, clientConfiguration,
return rows;
};

export const transaction: InternalTransactionType = async (connection, handler) => {
await query(connection, 'START TRANSACTION');

try {
await handler(connection);

await connection.query('COMMIT');
} catch (error) {
await connection.query('ROLLBACK');

throw error;
}
};

const sql = (parts: $ReadOnlyArray<string>, ...values: AnonymouseValuePlaceholderValuesType): TaggledTemplateLiteralInvocationType => {
return {
sql: parts.join('?'),
Expand Down Expand Up @@ -221,7 +236,8 @@ const createConnection = async (
many: mapTaggedTemplateLiteralInvocation(many.bind(null, connection, clientConfiguration)),
maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne.bind(null, connection, clientConfiguration)),
one: mapTaggedTemplateLiteralInvocation(one.bind(null, connection, clientConfiguration)),
query: mapTaggedTemplateLiteralInvocation(query.bind(null, connection))
query: mapTaggedTemplateLiteralInvocation(query.bind(null, connection)),
transaction: transaction.bind(null, connection)
};
};

Expand All @@ -231,24 +247,39 @@ const createPool = (
): DatabasePoolType => {
const pool = new pg.Pool(typeof connectionConfiguration === 'string' ? parseConnectionString(connectionConfiguration) : connectionConfiguration);

const connect = async () => {
const connection = await pool.connect();

return {
any: mapTaggedTemplateLiteralInvocation(any.bind(null, connection, clientConfiguration)),
many: mapTaggedTemplateLiteralInvocation(many.bind(null, connection, clientConfiguration)),
maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne.bind(null, connection, clientConfiguration)),
one: mapTaggedTemplateLiteralInvocation(one.bind(null, connection, clientConfiguration)),
query: mapTaggedTemplateLiteralInvocation(query.bind(null, connection)),
release: connection.release.bind(connection),
transaction: transaction.bind(null, connection)
};
};

return {
any: mapTaggedTemplateLiteralInvocation(any.bind(null, pool, clientConfiguration)),
connect: async () => {
const connection = await pool.connect();

return {
any: mapTaggedTemplateLiteralInvocation(any.bind(null, connection, clientConfiguration)),
many: mapTaggedTemplateLiteralInvocation(many.bind(null, connection, clientConfiguration)),
maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne.bind(null, connection, clientConfiguration)),
one: mapTaggedTemplateLiteralInvocation(one.bind(null, connection, clientConfiguration)),
query: mapTaggedTemplateLiteralInvocation(query.bind(null, connection)),
release: connection.release.bind(connection)
};
},
connect,
many: mapTaggedTemplateLiteralInvocation(many.bind(null, pool, clientConfiguration)),
maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne.bind(null, pool, clientConfiguration)),
one: mapTaggedTemplateLiteralInvocation(one.bind(null, pool, clientConfiguration)),
query: mapTaggedTemplateLiteralInvocation(query.bind(null, pool))
query: mapTaggedTemplateLiteralInvocation(query.bind(null, pool)),
transaction: async (handler) => {
debug('allocating a new connection to execute the transaction');

const connection = await connect();

try {
await connection.transaction(handler);
} finally {
debug('releasing the connection that was earlier secured to execute the transaction');
await connection.release();
}
}
};
};

Expand Down
8 changes: 7 additions & 1 deletion src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export type DatabaseConnectionType = {
+many: QueryManyType<*>,
+maybeOne: QueryMaybeOneType<*>,
+one: QueryOneType<*>,
+query: QueryType<*>
+query: QueryType<*>,
+transaction: TransactionType
};

export type DatabasePoolConnectionType = DatabaseConnectionType & {
Expand Down Expand Up @@ -126,10 +127,15 @@ export type InternalQueryOneType = (
values?: DatabaseQueryValuesType
) => Promise<QueryResultRowType>;

export type TransactionHandlerType = (connection: DatabaseConnectionType) => Promise<void>;

export type InternalTransactionType = (connection: InternalDatabaseConnectionType, handler: TransactionHandlerType) => Promise<void>;

export type InternalQueryType<T: QueryResultRowType> = (connection: InternalDatabaseConnectionType, sql: string, values?: DatabaseQueryValuesType) => Promise<QueryResultType<T>>;

export type QueryAnyType<T: QueryResultRowType> = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<$ReadOnlyArray<T>>;
export type QueryManyType<T: QueryResultRowType> = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<$ReadOnlyArray<T>>;
export type QueryMaybeOneType<T: QueryResultRowType | null> = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<T>;
export type QueryOneType<T: QueryResultRowType> = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<T>;
export type QueryType<T: QueryResultRowType> = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<QueryResultType<T>>;
export type TransactionType = (handler: TransactionHandlerType) => Promise<void>;
59 changes: 59 additions & 0 deletions test/mightyql/transaction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// @flow

/* eslint-disable flowtype/no-weak-types */

import test from 'ava';
import sinon from 'sinon';
import {
transaction
} from '../../src';

test('commits successful transaction', async (t) => {
const query = sinon.stub();

query.returns({});

const connection = {
query
};

await transaction(connection, async () => {
await query('FOO');
});

t.true(query.args[0].length === 1);
t.true(query.args[0][0] === 'START TRANSACTION');

t.true(query.args[1].length === 1);
t.true(query.args[1][0] === 'FOO');

t.true(query.args[2].length === 1);
t.true(query.args[2][0] === 'COMMIT');
});

test('rollbacks unsuccessful transaction', async (t) => {
const query = sinon.stub();

query.returns({});

const connection = {
query
};

const transactionExecution = transaction(connection, async () => {
await query('FOO');

throw new Error();
});

await t.throws(transactionExecution);

t.true(query.args[0].length === 1);
t.true(query.args[0][0] === 'START TRANSACTION');

t.true(query.args[1].length === 1);
t.true(query.args[1][0] === 'FOO');

t.true(query.args[2].length === 1);
t.true(query.args[2][0] === 'ROLLBACK');
});

0 comments on commit e532ae0

Please sign in to comment.