Skip to content

Commit

Permalink
feat: add oneFirst and maybeOneFirst; remove firstColumn helper
Browse files Browse the repository at this point in the history
BREAKING CHANGE: removed firstColumn helper
  • Loading branch information
gajus committed Sep 22, 2017
1 parent 2de1c3a commit ad6a8c5
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 32 deletions.
53 changes: 35 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ A PostgreSQL client with strict types and assertions.
* [`insert`](#insert)
* [`many`](#many)
* [`maybeOne`](#maybeone)
* [`maybeOneFirst`](#maybeonefirst)
* [`one`](#one)
* [`oneFirst`](#onefirst)
* [`query`](#query)
* [`transaction`](#transaction)
* [Overriding Error Constructor](#overriding-error-constructor)
Expand Down Expand Up @@ -262,12 +264,28 @@ Selects the first row from the result.
Example:

```js
const row = await connection.one('SELECT foo');
const row = await connection.maybeOne('SELECT foo');

// row.foo is the result of the `foo` column value of the first row.

```

### `maybeOneFirst`

Returns value of the first column from the first row.

* Returns `null` if row is not found.
* Throws `DataIntegrityError` if query returns multiple rows.

Example:

```js
const foo = await connection.maybeOneFirst('SELECT foo');

// foo is the result of the `foo` column value of the first row.

```

### `one`

Selects the first row from the result.
Expand All @@ -291,6 +309,22 @@ const row = await connection.one('SELECT foo');
> `knex` is a query builder; it does not assert the value of the result.
> Mightyql `one` adds assertions about the result of the query.
### `oneFirst`

Returns value of the first column from the first row.

* Throws `NotFoundError` if query returns no rows.
* Throws `DataIntegrityError` if query returns multiple rows.

Example:

```js
const foo = await connection.oneFirst('SELECT foo');

// foo is the result of the `foo` column value of the first row.

```

### `query`

API and the result shape are equivalent to [`pg#query`](https://github.com/brianc/node-postgres).
Expand Down Expand Up @@ -389,23 +423,6 @@ try {

`UniqueViolationError` is thrown when Postgres responds with [`unique_violation`](https://www.postgresql.org/docs/9.4/static/errcodes-appendix.html) (`23505`) error.

## Utilities

### `firstColumn`

`firstColumn` is used to extract value of the first column from a result set, e.g.

```js
import {
firstColumn
} from 'mightyql';

const rows = await connection.any('SELECT id, name FROM person');

const personIds = firstColumn(rows);

```

## Types

This package is using [Flow](https://flow.org/) types.
Expand Down
60 changes: 47 additions & 13 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ import type {
DatabaseSingleConnectionType,
InternalQueryAnyType,
InternalQueryManyType,
InternalQueryMaybeOneFirstType,
InternalQueryMaybeOneType,
InternalQueryOneFirstType,
InternalQueryOneType,
InternalQueryType,
InternalTransactionType,
QueryResultRowType,
TaggledTemplateLiteralInvocationType
} from './types';

Expand All @@ -52,18 +53,6 @@ types.setTypeParser(20, (value) => {
return parseInt(value, 10);
});

export const firstColumn = (rows: $ReadOnlyArray<QueryResultRowType>) => {
if (rows.length === 0) {
return [];
}

const columnName = Object.keys(rows[0])[0];

return rows.map((row) => {
return row[columnName];
});
};

const debug = createDebug('mightyql');

export const query: InternalQueryType<*> = async (connection, rawSql, values) => {
Expand Down Expand Up @@ -161,6 +150,45 @@ export const maybeOne: InternalQueryMaybeOneType = async (connection, clientConf
return rows[0];
};

/**
* Makes a query and expects exactly one result.
* Returns value of the first column.
*
* @throws NotFoundError If query returns no rows.
* @throws DataIntegrityError If query returns multiple rows.
*/
export const oneFirst: InternalQueryOneFirstType = async (connection, clientConfiguration, rawSql, values) => {
const row = await one(connection, clientConfiguration, rawSql, values);

// eslint-disable-next-line guard-for-in
for (const key in row) {
return row[key];
}

throw new Error('Unexpected state.');
};

/**
* Makes a query and expects exactly one result.
* Returns value of the first column.
*
* @throws DataIntegrityError If query returns multiple rows.
*/
export const maybeOneFirst: InternalQueryMaybeOneFirstType = async (connection, clientConfiguration, rawSql, values) => {
const row = await maybeOne(connection, clientConfiguration, rawSql, values);

if (!row) {
return null;
}

// eslint-disable-next-line guard-for-in
for (const key in row) {
return row[key];
}

throw new Error('Unexpected state.');
};

/**
* Makes a query and expects at least 1 result.
*
Expand Down Expand Up @@ -239,7 +267,9 @@ const createConnection = async (
},
many: mapTaggedTemplateLiteralInvocation(many.bind(null, connection, clientConfiguration)),
maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne.bind(null, connection, clientConfiguration)),
maybeOneFirst: mapTaggedTemplateLiteralInvocation(maybeOneFirst.bind(null, connection, clientConfiguration)),
one: mapTaggedTemplateLiteralInvocation(one.bind(null, connection, clientConfiguration)),
oneFirst: mapTaggedTemplateLiteralInvocation(oneFirst.bind(null, connection, clientConfiguration)),
query: mapTaggedTemplateLiteralInvocation(query.bind(null, connection)),
transaction: (handler) => {
return transaction(bindConnection, handler);
Expand All @@ -262,7 +292,9 @@ const createPool = (
any: mapTaggedTemplateLiteralInvocation(any.bind(null, connection, clientConfiguration)),
many: mapTaggedTemplateLiteralInvocation(many.bind(null, connection, clientConfiguration)),
maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne.bind(null, connection, clientConfiguration)),
maybeOneFirst: mapTaggedTemplateLiteralInvocation(maybeOneFirst.bind(null, connection, clientConfiguration)),
one: mapTaggedTemplateLiteralInvocation(one.bind(null, connection, clientConfiguration)),
oneFirst: mapTaggedTemplateLiteralInvocation(oneFirst.bind(null, connection, clientConfiguration)),
query: mapTaggedTemplateLiteralInvocation(query.bind(null, connection)),
release: connection.release.bind(connection),
transaction: (handler) => {
Expand All @@ -278,7 +310,9 @@ const createPool = (
connect,
many: mapTaggedTemplateLiteralInvocation(many.bind(null, pool, clientConfiguration)),
maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne.bind(null, pool, clientConfiguration)),
maybeOneFirst: mapTaggedTemplateLiteralInvocation(maybeOneFirst.bind(null, pool, clientConfiguration)),
one: mapTaggedTemplateLiteralInvocation(one.bind(null, pool, clientConfiguration)),
oneFirst: mapTaggedTemplateLiteralInvocation(oneFirst.bind(null, pool, clientConfiguration)),
query: mapTaggedTemplateLiteralInvocation(query.bind(null, pool)),
transaction: async (handler) => {
debug('allocating a new connection to execute the transaction');
Expand Down
22 changes: 21 additions & 1 deletion src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export type DatabaseConnectionType = {
+any: QueryAnyType<*>,
+many: QueryManyType<*>,
+maybeOne: QueryMaybeOneType<*>,
+maybeOneFirst: QueryMaybeOneFirstType<*>,
+one: QueryOneType<*>,
+oneFirst: QueryOneFirstType<*>,
+query: QueryType<*>,
+transaction: TransactionType
};
Expand All @@ -67,8 +69,10 @@ export type DatabasePoolType = DatabaseConnectionType & {
+connect: () => Promise<DatabasePoolConnectionType>
};

type QueryResultRowColumnType = string | number;

export type QueryResultRowType = {
[key: string]: string | number
[key: string]: QueryResultRowColumnType
};

export type NormalizedQueryType = {|
Expand Down Expand Up @@ -114,13 +118,27 @@ export type InternalQueryManyType = (
values?: DatabaseQueryValuesType
) => Promise<$ReadOnlyArray<QueryResultRowType>>;

export type InternalQueryMaybeOneFirstType = (
connection: InternalDatabaseConnectionType,
clientConfiguration: ClientConfigurationType,
sql: string,
values?: DatabaseQueryValuesType
) => Promise<QueryResultRowColumnType | null>;

export type InternalQueryMaybeOneType = (
connection: InternalDatabaseConnectionType,
clientConfiguration: ClientConfigurationType,
sql: string,
values?: DatabaseQueryValuesType
) => Promise<QueryResultRowType | null>;

export type InternalQueryOneFirstType = (
connection: InternalDatabaseConnectionType,
clientConfiguration: ClientConfigurationType,
sql: string,
values?: DatabaseQueryValuesType
) => Promise<QueryResultRowColumnType>;

export type InternalQueryOneType = (
connection: InternalDatabaseConnectionType,
clientConfiguration: ClientConfigurationType,
Expand All @@ -136,7 +154,9 @@ export type InternalQueryType<T: QueryResultRowType> = (connection: InternalData

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 QueryMaybeOneFirstType<T: QueryResultRowColumnType> = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<T>;
export type QueryMaybeOneType<T: QueryResultRowType | null> = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<T>;
export type QueryOneFirstType<T: QueryResultRowColumnType> = (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<*>;
4 changes: 4 additions & 0 deletions src/utilities/mapTaggedTemplateLiteralInvocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export default (targetMethod: *) => {
if (typeof maybeQuery === 'string') {
return targetMethod(maybeQuery, values);
} else {
if (!Array.isArray(values)) {
throw new TypeError('Unexpected state.');
}

return targetMethod(maybeQuery.sql, maybeQuery.values.concat(values));
}
};
Expand Down
61 changes: 61 additions & 0 deletions test/mightyql/maybeOneFirst.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// @flow

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

import test from 'ava';
import sinon from 'sinon';
import {
maybeOneFirst,
DataIntegrityError
} from '../../src';

test('returns the first row', async (t) => {
const stub = sinon.stub().returns({
rows: [
{
foo: 1
}
]
});

const connection: any = {
query: stub
};

const result = await maybeOneFirst(connection, {}, 'SELECT foo FROM bar');

t.deepEqual(result, 1);
});

test('returns null if no results', async (t) => {
const stub = sinon.stub().returns({
rows: []
});

const connection: any = {
query: stub
};

const result = await maybeOneFirst(connection, {}, 'SELECT foo FROM bar');

t.true(result === null);
});

test('throws an error if more than one row is returned', async (t) => {
const stub = sinon.stub().returns({
rows: [
{
foo: 1
},
{
foo: 2
}
]
});

const connection: any = {
query: stub
};

await t.throws(maybeOneFirst(connection, {}, 'SELECT foo FROM bar'), DataIntegrityError);
});
Loading

0 comments on commit ad6a8c5

Please sign in to comment.