diff --git a/README.md b/README.md index 5063fdd..1952888 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ Typescript wrapper around node-postgres :wrench: chore :notebook: docs -### v8.0.0 +## v8.0.1 +- :bug: sql fragment parser should unwrap complex types (e.g. NonEmptyArray, Option) before + passing it to the driver. + +## v8.0.0 - :bug: fix broken nominal typing on classes by changing `public readonly _T = ""` to `public readonly _: void`. - :boom: as a result of fixing nominal typing, `makeConnectionPool` now returns an error union diff --git a/src/utils/sql.ts b/src/utils/sql.ts index dc60b6f..af366ab 100644 --- a/src/utils/sql.ts +++ b/src/utils/sql.ts @@ -1,5 +1,40 @@ +import { NonEmptyArray } from "fp-ts/lib/NonEmptyArray"; +import { None, Some } from "fp-ts/lib/Option"; import { mixed } from "io-ts"; +import { forOwn, isEqual, isPlainObject } from "lodash"; import * as pg from "pg"; +import { inspect } from "util"; + +const normaliseValue = (value: any): any => { + if (value == null) { + return value; + } + + if (value instanceof NonEmptyArray) { + return normaliseValue(value.toArray()); + } + + if (value instanceof None || value instanceof Some) { + return normaliseValue(value.toUndefined()); + } + + if (Array.isArray(value)) { + return value.map(normaliseValue); + } + + if (isPlainObject(value)) { + const next: any = {}; + + forOwn(value, (v, k) => { + // tslint:disable-next-line:no-expression-statement no-object-mutation + next[k] = normaliseValue(v); + }); + + return next; + } + + return value; +}; type IndexGetter = (value: mixed) => string; @@ -12,14 +47,18 @@ export const SQL = (parts: TemplateStringsArray, ...inValues: any[]): pg.QueryCo const outValues: mixed[] = []; const getValueIndex = (value: mixed): string => { - if (value == null) { + const normalisedValue = normaliseValue(value); + + if (normalisedValue == null) { return `NULL`; } - const found = outValues.indexOf(value); + + const found = outValues.findIndex(o => isEqual(o, normalisedValue)); + if (found > -1) { return `$${found + 1}`; } - outValues.push(value); + outValues.push(normalisedValue); return `$${outValues.length}`; }; diff --git a/tests/integration/queries.test.ts b/tests/integration/queries.test.ts index 175677e..851a66c 100644 --- a/tests/integration/queries.test.ts +++ b/tests/integration/queries.test.ts @@ -1,4 +1,5 @@ import { Either, left, right } from "fp-ts/lib/Either"; +import { NonEmptyArray } from "fp-ts/lib/NonEmptyArray"; import { ask, fromReader, fromTaskEither } from "fp-ts/lib/ReaderTaskEither"; import { TaskEither } from "fp-ts/lib/TaskEither"; import * as t from "io-ts"; @@ -50,9 +51,9 @@ describe("queries", () => { test("queryOne with a query that returns 1 parseable row returns a single parsed type", () => connectionTest( - queryOne(Unit, SQL`SELECT * FROM units WHERE id = 2`) - .mapLeft(fail) - .map(unit => expect(unit).toMatchObject({ id: 2, name: "Bike" })), + queryOne(Unit, SQL`SELECT * FROM units WHERE id = 2`).map(unit => + expect(unit).toMatchObject({ id: 2, name: "Bike" }), + ), )); test("queryOne with a query that returns 1 unparseable row returns a validation error", () => @@ -128,18 +129,15 @@ describe("queries", () => { test("queryOneOrMore with a query that returns 1 parseable row returns an array of a single parsed type", () => connectionTest( - queryOneOrMore(Unit, SQL`SELECT * FROM units WHERE id = 2`) - .mapLeft(fail) - .map(units => { - expect(units.head).toMatchObject({ id: 2, name: "Bike" }); - expect(units.tail).toHaveLength(0); - }), + queryOneOrMore(Unit, SQL`SELECT * FROM units WHERE id = 2`).map(units => { + expect(units.head).toMatchObject({ id: 2, name: "Bike" }); + expect(units.tail).toHaveLength(0); + }), )); test("queryOneOrMore with a query that returns 2 parseable rows returns an array of two parsed types", () => connectionTest( queryOneOrMore(Unit, SQL`SELECT * FROM units WHERE name = 'Car' ORDER BY id`) - .mapLeft(fail) .map(units => units.toArray()) .map(units => { expect(units).toHaveLength(2); @@ -175,32 +173,28 @@ describe("queries", () => { test("queryOneOrNone with a query that returns 1 parseable row returns a Some of a single parsed type", () => connectionTest( - queryOneOrNone(Unit, SQL`SELECT * FROM units WHERE id = 2`) - .mapLeft(fail) - .map(unitO => { - return unitO.foldL( - () => fail(new Error("Query should have returned a Some.")), - unit => { - expect(unit).toMatchObject({ id: 2, name: "Bike" }); - }, - ); - }), + queryOneOrNone(Unit, SQL`SELECT * FROM units WHERE id = 2`).map(unitO => { + return unitO.foldL( + () => fail(new Error("Query should have returned a Some.")), + unit => { + expect(unit).toMatchObject({ id: 2, name: "Bike" }); + }, + ); + }), )); test("queryOneOrNone with a query that returns 0 rows returns a None", () => connectionTest( - queryOneOrNone(Unit, SQL`SELECT * FROM units WHERE id = 0`) - .mapLeft(fail) - .map(unitO => { - return unitO.foldL( - () => { - return; - }, - () => { - fail(new Error("Query should have returned a None.")); - }, - ); - }), + queryOneOrNone(Unit, SQL`SELECT * FROM units WHERE id = 0`).map(unitO => { + return unitO.foldL( + () => { + return; + }, + () => { + fail(new Error("Query should have returned a None.")); + }, + ); + }), )); test("queryOneOrNone with a query that returns 2 rows returns PgRowCountError", () => @@ -230,32 +224,32 @@ describe("queries", () => { test("queryAny with a query that returns 0 rows returns an empty array", () => connectionTest( - camelCasedQueries - .queryAny(Unit, SQL`SELECT * FROM units WHERE id = 0`) - .mapLeft(fail) - .map(units => { - expect(units).toHaveLength(0); - }), + camelCasedQueries.queryAny(Unit, SQL`SELECT * FROM units WHERE id = 0`).map(units => { + expect(units).toHaveLength(0); + }), )); test("queryAny with a query that returns 1 rows returns an array of 1 parsed row", () => connectionTest( - queryAny(Unit, SQL`SELECT * FROM units WHERE id = 1`) - .mapLeft(fail) - .map(units => { - expect(units).toHaveLength(1); - expect(units[0]).toMatchObject({ id: 1, name: "Car" }); - }), + queryAny(Unit, SQL`SELECT * FROM units WHERE id = 1`).map(units => { + expect(units).toHaveLength(1); + expect(units[0]).toMatchObject({ id: 1, name: "Car" }); + }), )); test("queryAny with a query that returns 2 rows returns an array of 2 parsed rows", () => connectionTest( - queryAny(Unit, SQL`SELECT * FROM units WHERE name = 'Car' ORDER BY id`) - .mapLeft(fail) - .map(units => { - expect(units).toHaveLength(2); - expect(units[0]).toMatchObject({ id: 1, name: "Car" }); - expect(units[1]).toMatchObject({ id: 4, name: "Car" }); - }), + queryAny(Unit, SQL`SELECT * FROM units WHERE name = 'Car' ORDER BY id`).map(units => { + expect(units).toHaveLength(2); + expect(units[0]).toMatchObject({ id: 1, name: "Car" }); + expect(units[1]).toMatchObject({ id: 4, name: "Car" }); + }), + )); + + test("queryAny with a json parameter that has a NonEmptyArray inside", () => + connectionTest( + queryAny(t.any, SQL`SELECT ${{ foo: new NonEmptyArray(1, [2]) }}::json`).map(results => { + expect(results).toEqual([{ json: { foo: [1, 2] } }]); + }), )); });