Skip to content

Commit

Permalink
fix: unwrap complex types before passing to driver
Browse files Browse the repository at this point in the history
  • Loading branch information
leemhenson committed Aug 23, 2018
1 parent 987ba2e commit 9267e5e
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 55 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<class name>"`
to `public readonly _<class name>: void`.
- :boom: as a result of fixing nominal typing, `makeConnectionPool` now returns an error union
Expand Down
45 changes: 42 additions & 3 deletions src/utils/sql.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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}`;
};

Expand Down
96 changes: 45 additions & 51 deletions tests/integration/queries.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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", () =>
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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", () =>
Expand Down Expand Up @@ -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] } }]);
}),
));
});

0 comments on commit 9267e5e

Please sign in to comment.