From 96b682249bb4872ff4f85d365406361ae9a12e51 Mon Sep 17 00:00:00 2001 From: Gajus Kuizinas Date: Fri, 14 Apr 2017 19:06:43 +0100 Subject: [PATCH] feat: allow custom error constructor --- README.md | 23 +++++++++++++++++++ src/index.js | 47 ++++++++++++++++++++++++--------------- src/types.js | 39 ++++++++++++++++++++++++++++---- test/mightyql/many.js | 25 +++++++++++++++++++-- test/mightyql/maybeOne.js | 6 ++--- test/mightyql/one.js | 27 +++++++++++++++++++--- 6 files changed, 137 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 0a8b37d5..2b4457cd 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ A PostgreSQL client with strict types and assertions. * [`many`](#many) * [`maybeOne`](#maybeone) * [`one`](#one) +* [Overriding Errors](#overriding-errors) * [Error handling](#error-handling) * [Handling `NotFoundError`](#handling-notfounderror) * [Handling `DataIntegrityError`](#handling-dataintengrityerror) @@ -212,6 +213,28 @@ 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. +## Overriding Errors + +Overriding the error constructor used by Mightyql allows you to map database layer errors to your application errors. + +```js +import { + createPool +} from 'mightyql'; + +class NotFoundError extends Error {}; + +createPool('postgres://', { + errors: { + NotFoundError + } +}); +``` + +The following error types can be overridden: + +* `NotFoundError` + ## Error handling ### Handling `NotFoundError` diff --git a/src/index.js b/src/index.js index 3fc28a82..02eeec8d 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ import { normalizeNamedValuePlaceholders } from './utilities'; import type { + ClientConfigurationType, DatabaseConfigurationType, DatabasePoolConnectionType, DatabaseSingleConnectionType, @@ -100,11 +101,13 @@ export const query: InternalQueryType = async (connection, sql, values) => { * @throws NotFoundError If query returns no rows. * @throws DataIntegrityError If query returns multiple rows. */ -export const one: InternalQueryOneType = async (connection, sql, values) => { +export const one: InternalQueryOneType = async (connection, clientConfiguration, sql, values) => { const {rows} = await query(connection, sql, values); if (rows.length === 0) { - throw new NotFoundError(); + const ConfigurableNotFoundError = clientConfiguration.errors && clientConfiguration.errors.NotFoundError ? clientConfiguration.errors.NotFoundError : NotFoundError; + + throw new ConfigurableNotFoundError(); } if (rows.length > 1) { @@ -119,7 +122,7 @@ export const one: InternalQueryOneType = async (connection, sql, values) => { * * @throws DataIntegrityError If query returns multiple rows. */ -export const maybeOne: InternalQueryMaybeOneType = async (connection, sql, values) => { +export const maybeOne: InternalQueryMaybeOneType = async (connection, clientConfiguration, sql, values) => { const { rows } = await query(connection, sql, values); @@ -140,11 +143,13 @@ export const maybeOne: InternalQueryMaybeOneType = async (connection, sql, value * * @throws NotFoundError If query returns no rows. */ -export const many: InternalQueryManyType = async (connection, sql, values) => { +export const many: InternalQueryManyType = async (connection, clientConfiguration, sql, values) => { const {rows} = await query(connection, sql, values); if (rows.length === 0) { - throw new NotFoundError(); + const ConfigurableNotFoundError = clientConfiguration.errors && clientConfiguration.errors.NotFoundError ? clientConfiguration.errors.NotFoundError : NotFoundError; + + throw new ConfigurableNotFoundError(); } return rows; @@ -153,39 +158,45 @@ export const many: InternalQueryManyType = async (connection, sql, values) => { /** * Makes a query and expects any number of results. */ -export const any: InternalQueryAnyType = async (connection, sql, values) => { +export const any: InternalQueryAnyType = async (connection, clientConfiguration, sql, values) => { const {rows} = await query(connection, sql, values); return rows; }; -const createConnection = async (configuration: DatabaseConfigurationType): Promise => { - const pool = new pg.Pool(configuration); +const createConnection = async ( + connectionConfiguration: DatabaseConfigurationType, + clientConfiguration: ClientConfigurationType +): Promise => { + const pool = new pg.Pool(connectionConfiguration); const connection = await pool.connect(); return { - any: any.bind(null, connection), + any: any.bind(null, connection, clientConfiguration), end: async () => { await connection.release(); return pool.end(); }, - many: many.bind(null, connection), - maybeOne: maybeOne.bind(null, connection), - one: one.bind(null, connection), + many: many.bind(null, connection, clientConfiguration), + maybeOne: maybeOne.bind(null, connection, clientConfiguration), + one: one.bind(null, connection, clientConfiguration), query: query.bind(null, connection) }; }; -const createPool = (configuration: DatabaseConfigurationType): DatabasePoolConnectionType => { - const pool = new pg.Pool(typeof configuration === 'string' ? parseConnectionString(configuration) : configuration); +const createPool = ( + connectionConfiguration: DatabaseConfigurationType, + clientConfiguration: ClientConfigurationType +): DatabasePoolConnectionType => { + const pool = new pg.Pool(typeof connectionConfiguration === 'string' ? parseConnectionString(connectionConfiguration) : connectionConfiguration); return { - any: any.bind(null, pool), - many: many.bind(null, pool), - maybeOne: maybeOne.bind(null, pool), - one: one.bind(null, pool), + any: any.bind(null, pool, clientConfiguration), + many: many.bind(null, pool, clientConfiguration), + maybeOne: maybeOne.bind(null, pool, clientConfiguration), + one: one.bind(null, pool, clientConfiguration), query: query.bind(null, pool) }; }; diff --git a/src/types.js b/src/types.js index 4c798a13..5fbd9ef6 100644 --- a/src/types.js +++ b/src/types.js @@ -7,6 +7,14 @@ type InternalDatabaseConnectionType = any; export type DatabaseConnectionUriType = string; +export type ClientErrorsConfigurationType = {| + +NotFoundError?: Class +|}; + +export type ClientConfigurationType = {| + +errors?: ClientErrorsConfigurationType +|}; + export type DatabaseConfigurationType = DatabaseConnectionUriType | {| +database?: string, @@ -60,10 +68,33 @@ export type DatabaseQueryValuesType = AnonymouseValuePlaceholderValuesType | NamedValuePlaceholderValuesType; -export type InternalQueryAnyType = (connection: InternalDatabaseConnectionType, sql: string, values?: DatabaseQueryValuesType) => Promise<$ReadOnlyArray>; -export type InternalQueryManyType = (connection: InternalDatabaseConnectionType, sql: string, values?: DatabaseQueryValuesType) => Promise<$ReadOnlyArray>; -export type InternalQueryMaybeOneType = (connection: InternalDatabaseConnectionType, sql: string, values?: DatabaseQueryValuesType) => Promise; -export type InternalQueryOneType = (connection: InternalDatabaseConnectionType, sql: string, values?: DatabaseQueryValuesType) => Promise; +export type InternalQueryAnyType = ( + connection: InternalDatabaseConnectionType, + clientConfiguration: ClientConfigurationType, + sql: string, + values?: DatabaseQueryValuesType +) => Promise<$ReadOnlyArray>; + +export type InternalQueryManyType = ( + connection: InternalDatabaseConnectionType, + clientConfiguration: ClientConfigurationType, + sql: string, + values?: DatabaseQueryValuesType +) => Promise<$ReadOnlyArray>; + +export type InternalQueryMaybeOneType = ( + connection: InternalDatabaseConnectionType, + clientConfiguration: ClientConfigurationType, + sql: string, + values?: DatabaseQueryValuesType +) => Promise; + +export type InternalQueryOneType = ( + connection: InternalDatabaseConnectionType, + clientConfiguration: ClientConfigurationType, + sql: string, + values?: DatabaseQueryValuesType +) => Promise; // eslint-disable-next-line flowtype/no-weak-types export type InternalQueryType = (connection: InternalDatabaseConnectionType, sql: string, values?: DatabaseQueryValuesType) => Promise; diff --git a/test/mightyql/many.js b/test/mightyql/many.js index a4a8830a..3011e6aa 100644 --- a/test/mightyql/many.js +++ b/test/mightyql/many.js @@ -4,11 +4,14 @@ import test from 'ava'; import sinon from 'sinon'; +import ExtendableError from 'es6-error'; import { many, NotFoundError } from '../../src'; +class TestError extends ExtendableError {} + test('returns the query results rows', async (t) => { const stub = sinon.stub().returns({ rows: [ @@ -25,7 +28,7 @@ test('returns the query results rows', async (t) => { query: stub }; - const result = await many(connection, 'SELECT foo FROM bar'); + const result = await many(connection, {}, 'SELECT foo FROM bar'); t.deepEqual(result, [ { @@ -46,5 +49,23 @@ test('throws an error if no rows are returned', async (t) => { query: stub }; - await t.throws(many(connection, 'SELECT foo FROM bar'), NotFoundError); + await t.throws(many(connection, {}, 'SELECT foo FROM bar'), NotFoundError); +}); + +test('throws an error if no rows are returned (user defined error constructor)', async (t) => { + const stub = sinon.stub().returns({ + rows: [] + }); + + const connection: any = { + query: stub + }; + + const clientConfiguration = { + errors: { + NotFoundError: TestError + } + }; + + await t.throws(many(connection, clientConfiguration, 'SELECT foo FROM bar'), TestError); }); diff --git a/test/mightyql/maybeOne.js b/test/mightyql/maybeOne.js index fe9ca157..ff1a5e89 100644 --- a/test/mightyql/maybeOne.js +++ b/test/mightyql/maybeOne.js @@ -22,7 +22,7 @@ test('returns the first row', async (t) => { query: stub }; - const result = await maybeOne(connection, 'SELECT foo FROM bar'); + const result = await maybeOne(connection, {}, 'SELECT foo FROM bar'); t.deepEqual(result, { foo: 1 @@ -38,7 +38,7 @@ test('returns null if no results', async (t) => { query: stub }; - const result = await maybeOne(connection, 'SELECT foo FROM bar'); + const result = await maybeOne(connection, {}, 'SELECT foo FROM bar'); t.true(result === null); }); @@ -59,5 +59,5 @@ test('throws an error if more than one row is returned', async (t) => { query: stub }; - await t.throws(maybeOne(connection, 'SELECT foo FROM bar'), DataIntegrityError); + await t.throws(maybeOne(connection, {}, 'SELECT foo FROM bar'), DataIntegrityError); }); diff --git a/test/mightyql/one.js b/test/mightyql/one.js index f9cc22e1..7a34bad8 100644 --- a/test/mightyql/one.js +++ b/test/mightyql/one.js @@ -4,12 +4,15 @@ import test from 'ava'; import sinon from 'sinon'; +import ExtendableError from 'es6-error'; import { one, DataIntegrityError, NotFoundError } from '../../src'; +class TestError extends ExtendableError {} + test('returns the first row', async (t) => { const stub = sinon.stub().returns({ rows: [ @@ -23,7 +26,7 @@ test('returns the first row', async (t) => { query: stub }; - const result = await one(connection, 'SELECT foo FROM bar'); + const result = await one(connection, {}, 'SELECT foo FROM bar'); t.deepEqual(result, { foo: 1 @@ -39,7 +42,25 @@ test('throws an error if no rows are returned', async (t) => { query: stub }; - await t.throws(one(connection, 'SELECT foo FROM bar'), NotFoundError); + await t.throws(one(connection, {}, 'SELECT foo FROM bar'), NotFoundError); +}); + +test('throws an error if no rows are returned (user defined error constructor)', async (t) => { + const stub = sinon.stub().returns({ + rows: [] + }); + + const connection: any = { + query: stub + }; + + const clientConfiguration = { + errors: { + NotFoundError: TestError + } + }; + + await t.throws(one(connection, clientConfiguration, 'SELECT foo FROM bar'), TestError); }); test('throws an error if more than one row is returned', async (t) => { @@ -58,5 +79,5 @@ test('throws an error if more than one row is returned', async (t) => { query: stub }; - await t.throws(one(connection, 'SELECT foo FROM bar'), DataIntegrityError); + await t.throws(one(connection, {}, 'SELECT foo FROM bar'), DataIntegrityError); });