diff --git a/README.md b/README.md index 25cf9187..6b24a0fa 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,10 @@ A PostgreSQL client with strict types and assertions. * [Guarding against accidental unescaped input](#guarding-against-accidental-unescaped-input) * [Query methods](#query-methods) * [`any`](#any) + * [`anyFirst`](#anyfirst) * [`insert`](#insert) * [`many`](#many) + * [`manyFirst`](#manyfirst) * [`maybeOne`](#maybeone) * [`maybeOneFirst`](#maybeonefirst) * [`one`](#one) @@ -225,6 +227,19 @@ const rows = await connection.any('SELECT foo'); ``` +### `anyFirst` + +Returns value of the first column of every row in the result set. + +* Throws `DataIntegrityError` if query returns multiple rows. + +Example: + +```js +const fooValues = await connection.any('SELECT foo'); + +``` + ### `insert` Designed to use when inserting 1 row. @@ -254,6 +269,20 @@ const rows = await connection.many('SELECT foo'); ``` +### `manyFirst` + +Returns value of the first column of every row in the result set. + +* Throws `NotFoundError` if query returns no rows. +* Throws `DataIntegrityError` if query returns multiple rows. + +Example: + +```js +const fooValues = await connection.many('SELECT foo'); + +``` + ### `maybeOne` Selects the first row from the result. diff --git a/src/index.js b/src/index.js index b93e6bf2..d92118a4 100644 --- a/src/index.js +++ b/src/index.js @@ -24,7 +24,9 @@ import type { DatabaseConfigurationType, DatabasePoolType, DatabaseSingleConnectionType, + InternalQueryAnyFirstType, InternalQueryAnyType, + InternalQueryManyFirstType, InternalQueryManyType, InternalQueryMaybeOneFirstType, InternalQueryMaybeOneType, @@ -213,6 +215,30 @@ export const many: InternalQueryManyType = async (connection, clientConfiguratio return rows; }; +export const manyFirst: InternalQueryManyFirstType = async (connection, clientConfigurationType, rawSql, values) => { + const rows = await many(connection, clientConfigurationType, rawSql, values); + + if (rows.length === 0) { + throw new DataIntegrityError(); + } + + const keys = Object.keys(rows[0]); + + if (keys.length !== 1) { + throw new DataIntegrityError(); + } + + const firstColumnName = keys[0]; + + if (typeof firstColumnName !== 'string') { + throw new DataIntegrityError(); + } + + return rows.map((row) => { + return row[firstColumnName]; + }); +}; + /** * Makes a query and expects any number of results. */ @@ -224,6 +250,30 @@ export const any: InternalQueryAnyType = async (connection, clientConfiguration, return rows; }; +export const anyFirst: InternalQueryAnyFirstType = async (connection, clientConfigurationType, rawSql, values) => { + const rows = await any(connection, clientConfigurationType, rawSql, values); + + if (rows.length === 0) { + return []; + } + + const keys = Object.keys(rows[0]); + + if (keys.length !== 1) { + throw new DataIntegrityError(); + } + + const firstColumnName = keys[0]; + + if (typeof firstColumnName !== 'string') { + throw new DataIntegrityError(); + } + + return rows.map((row) => { + return row[firstColumnName]; + }); +}; + export const transaction: InternalTransactionType = async (connection, handler) => { await connection.query('START TRANSACTION'); @@ -259,6 +309,7 @@ const createConnection = async ( const bindConnection = { any: mapTaggedTemplateLiteralInvocation(any.bind(null, connection, clientConfiguration)), + anyFirst: mapTaggedTemplateLiteralInvocation(anyFirst.bind(null, connection, clientConfiguration)), end: async () => { if (ended) { return ended; @@ -271,6 +322,7 @@ const createConnection = async ( return ended; }, many: mapTaggedTemplateLiteralInvocation(many.bind(null, connection, clientConfiguration)), + manyFirst: mapTaggedTemplateLiteralInvocation(manyFirst.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)), @@ -295,7 +347,9 @@ const createPool = ( const bindConnection = { any: mapTaggedTemplateLiteralInvocation(any.bind(null, connection, clientConfiguration)), + anyFirst: mapTaggedTemplateLiteralInvocation(anyFirst.bind(null, connection, clientConfiguration)), many: mapTaggedTemplateLiteralInvocation(many.bind(null, connection, clientConfiguration)), + manyFirst: mapTaggedTemplateLiteralInvocation(manyFirst.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)), @@ -312,8 +366,10 @@ const createPool = ( return { any: mapTaggedTemplateLiteralInvocation(any.bind(null, pool, clientConfiguration)), + anyFirst: mapTaggedTemplateLiteralInvocation(anyFirst.bind(null, pool, clientConfiguration)), connect, many: mapTaggedTemplateLiteralInvocation(many.bind(null, pool, clientConfiguration)), + manyFirst: mapTaggedTemplateLiteralInvocation(manyFirst.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)), diff --git a/src/types.js b/src/types.js index 3152adac..76dbd8b8 100644 --- a/src/types.js +++ b/src/types.js @@ -48,7 +48,9 @@ export type DatabaseConfigurationType = export type DatabaseConnectionType = { +any: QueryAnyType<*>, + +anyFirst: QueryAnyFirstType<*>, +many: QueryManyType<*>, + +manyFirst: QueryManyFirstType<*>, +maybeOne: QueryMaybeOneType<*>, +maybeOneFirst: QueryMaybeOneFirstType<*>, +one: QueryOneType<*>, @@ -111,6 +113,13 @@ export type InternalQueryAnyType = ( values?: DatabaseQueryValuesType ) => Promise<$ReadOnlyArray>; +export type InternalQueryAnyFirstType = ( + connection: InternalDatabaseConnectionType, + clientConfiguration: ClientConfigurationType, + sql: string, + values?: DatabaseQueryValuesType +) => Promise<$ReadOnlyArray>; + export type InternalQueryManyType = ( connection: InternalDatabaseConnectionType, clientConfiguration: ClientConfigurationType, @@ -118,6 +127,13 @@ export type InternalQueryManyType = ( values?: DatabaseQueryValuesType ) => Promise<$ReadOnlyArray>; +export type InternalQueryManyFirstType = ( + connection: InternalDatabaseConnectionType, + clientConfiguration: ClientConfigurationType, + sql: string, + values?: DatabaseQueryValuesType +) => Promise<$ReadOnlyArray>; + export type InternalQueryMaybeOneFirstType = ( connection: InternalDatabaseConnectionType, clientConfiguration: ClientConfigurationType, @@ -152,7 +168,9 @@ export type InternalTransactionType = (connection: InternalDatabaseConnectionTyp export type InternalQueryType = (connection: InternalDatabaseConnectionType, sql: string, values?: DatabaseQueryValuesType) => Promise>; +export type QueryAnyFirstType = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<$ReadOnlyArray>; export type QueryAnyType = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<$ReadOnlyArray>; +export type QueryManyFirstType = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<$ReadOnlyArray>; export type QueryManyType = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise<$ReadOnlyArray>; export type QueryMaybeOneFirstType = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise; export type QueryMaybeOneType = (sql: string | TaggledTemplateLiteralInvocationType, values?: DatabaseQueryValuesType) => Promise; diff --git a/test/mightyql/anyFirst.js b/test/mightyql/anyFirst.js new file mode 100644 index 00000000..82fc7730 --- /dev/null +++ b/test/mightyql/anyFirst.js @@ -0,0 +1,51 @@ +// @flow + +/* eslint-disable flowtype/no-weak-types */ + +import test from 'ava'; +import sinon from 'sinon'; +import { + anyFirst, + DataIntegrityError +} from '../../src'; + +test('returns values of the query result rows', async (t) => { + const stub = sinon.stub().returns({ + rows: [ + { + foo: 1 + }, + { + foo: 2 + } + ] + }); + + const connection: any = { + query: stub + }; + + const result = await anyFirst(connection, {}, ''); + + t.deepEqual(result, [ + 1, + 2 + ]); +}); + +test('throws an error if more than one column is returned', async (t) => { + const stub = sinon.stub().returns({ + rows: [ + { + bar: 1, + foo: 1 + } + ] + }); + + const connection: any = { + query: stub + }; + + await t.throws(anyFirst(connection, {}, ''), DataIntegrityError); +}); diff --git a/test/mightyql/manyFirst.js b/test/mightyql/manyFirst.js new file mode 100644 index 00000000..fb828433 --- /dev/null +++ b/test/mightyql/manyFirst.js @@ -0,0 +1,51 @@ +// @flow + +/* eslint-disable flowtype/no-weak-types */ + +import test from 'ava'; +import sinon from 'sinon'; +import { + DataIntegrityError, + manyFirst +} from '../../src'; + +test('returns values of the query result rows', async (t) => { + const stub = sinon.stub().returns({ + rows: [ + { + foo: 1 + }, + { + foo: 2 + } + ] + }); + + const connection: any = { + query: stub + }; + + const result = await manyFirst(connection, {}, ''); + + t.deepEqual(result, [ + 1, + 2 + ]); +}); + +test('throws an error if more than one column is returned', async (t) => { + const stub = sinon.stub().returns({ + rows: [ + { + bar: 1, + foo: 1 + } + ] + }); + + const connection: any = { + query: stub + }; + + await t.throws(manyFirst(connection, {}, ''), DataIntegrityError); +});