From 5fb81e1e3c5f8fc24972b9258ca4f627c85ed4cd Mon Sep 17 00:00:00 2001 From: Daniel Schaffer Date: Mon, 11 Mar 2019 18:13:23 -0700 Subject: [PATCH] 9 --- .../data-pg/src/pg-db-queryable.spec.ts | 153 +++++++++++++++++- .../data-pg/src/pg-db-queryable.ts | 10 +- .../testing/src/pg-db-pool-client.fixture.ts | 15 +- 3 files changed, 168 insertions(+), 10 deletions(-) diff --git a/packages/dandi-contrib/data-pg/src/pg-db-queryable.spec.ts b/packages/dandi-contrib/data-pg/src/pg-db-queryable.spec.ts index 4ab719b3..56065e63 100644 --- a/packages/dandi-contrib/data-pg/src/pg-db-queryable.spec.ts +++ b/packages/dandi-contrib/data-pg/src/pg-db-queryable.spec.ts @@ -1,11 +1,12 @@ -import { PgDbPoolClient } from '@dandi-contrib/data-pg' +import { PgDbMultipleResultsError, PgDbPoolClient, PgDbQueryError } from '@dandi-contrib/data-pg' import { PgDbPoolClientFixture, PgDbQueryableBase, PgDbQueryableClient } from '@dandi-contrib/data-pg/testing' +import { Url, Uuid } from '@dandi/common' import { stubHarness } from '@dandi/core/testing' +import { Property } from '@dandi/model' import { ModelBuilder } from '@dandi/model-builder' import { ModelBuilderFixture } from '@dandi/model-builder/testing' import { expect } from 'chai' -import { stub } from 'sinon' describe('PgDbQueryableBase', function() { @@ -29,6 +30,46 @@ describe('PgDbQueryableBase', function() { expect(this.client.query).to.have.been.calledWithExactly(cmd, [args]) }) + + it('correctly formats Uuid instances', async function() { + const cmd = 'SELECT foo FROM bar WHERE id = $1' + const id = Uuid.create() + + await this.queryable.query(cmd, id) + + expect(this.client.query).to.have.been.calledWithExactly(cmd, [`{${id}}`]) + + }) + + it('correctly formats Url instances', async function() { + const cmd = 'SELECT foo FROM bar WHERE url = $1' + const url = new Url('https://google.com') + + await this.queryable.query(cmd, url) + + expect(this.client.query).to.have.been.calledWithExactly(cmd, [url.toString()]) + }) + + it('correctly formats values of nested arrays', async function() { + const cmd = 'SELECT foo FROM bar WHERE id in ($1, $2, $3)' + const id1 = Uuid.create() + const id2 = Uuid.create() + const id3 = Uuid.create() + + await this.queryable.query(cmd, [id1, id2, id3]) + + expect(this.client.query).to.have.been.calledWithExactly(cmd, [[`{${id1}}`, `{${id2}}`, `{${id3}}`]]) + }) + + it('wraps any query errors in PgDbQueryError', async function() { + + const err = new Error('Your llama is lloose!') + PgDbPoolClientFixture.throws(err) + + const rethrownErr = await expect(this.queryable.query('SELECT foo FROM bar')).to.be.rejectedWith(PgDbQueryError) + expect(rethrownErr.innerError).to.equal(err) + + }) }) describe('queryModel', function() { @@ -41,6 +82,15 @@ describe('PgDbQueryableBase', function() { this.modelBuilder = await harness.injectStub(ModelBuilder) }) + it('returns an empty array if the underlying query did not return any rows', async function() { + PgDbPoolClientFixture.result({ rows: [] }) + + const result = await this.queryable.queryModel(TestModel, 'Select foo FROM bar') + + expect(result).to.be.an.instanceof(Array) + expect(result).to.be.empty + }) + it('converts each of the returned rows using the modelBuilder instance', async function() { await this.queryable.queryModel(TestModel, 'SELECT foo FROM bar') @@ -51,5 +101,104 @@ describe('PgDbQueryableBase', function() { .calledWithExactly(TestModel, { id: 'b' }, undefined) }) + + describe('queryModelSingle', function() { + + it('returns undefined if queryModel returns no value or an empty array', async function() { + + PgDbPoolClientFixture.result({}) + expect(await this.queryable.queryModelSingle(TestModel, 'SELECT foo FROM bar')).to.be.undefined + + PgDbPoolClientFixture.result({ rows: [] }) + expect(await this.queryable.queryModelSingle(TestModel, 'SELECT foo FROM bar')).to.be.undefined + + }) + + it('throws a PgDbMultipleResultsError if the query returns more than one result', async function() { + await expect(this.queryable.queryModelSingle(TestModel, 'SELECT foo FROM bar')).to.be.rejectedWith(PgDbMultipleResultsError) + }) + + it('returns the only result if there is one', async function() { + + const result = { id: 'c' } + PgDbPoolClientFixture.result({ rows: [result] }) + this.modelBuilder.constructModel.returnsArg(1) + + expect(await this.queryable.queryModelSingle(TestModel, 'SELECT foo FROM bar')).to.equal(result) + + }) + + }) + + }) + + describe('replaceSelectList', function() { + + class AnotherModel { + @Property(Uuid) + public anotherId: Uuid + } + + class SomeModel { + @Property(Uuid) + public someId: Uuid + + @Property(Uuid) + public anotherId: Uuid + } + + class TestModel { + @Property(SomeModel) + public someProperty: SomeModel + + @Property(AnotherModel) + public anotherProperty: AnotherModel + } + + it('ignores queries that are not SELECT queries and returns them unchanged', function() { + const cmd = 'INSERT (foo) VALUES (bar) INTO table' + expect(this.queryable.replaceSelectList(TestModel, cmd)).to.equal(cmd) + }) + + it('returns the original command if none of the select arguments match the aliased table names', function() { + const cmd = 'SELECT id FROM bar bar' + expect(this.queryable.replaceSelectList(TestModel, cmd)).to.equal(cmd) + }) + + it('returns the original command if the model does not have any decorated properties matching the select arguments', function() { + const cmd = 'SELECT non_existent_thing FROM non_existent_stuff non_existent_thing' + expect(this.queryable.replaceSelectList(TestModel, cmd)).to.equal(cmd) + }) + + it('expands column selection based on decorated properties', function() { + + const sourceCmd = +`SELECT + some_property, + another_property +FROM some_stuff some_property +LEFT JOIN another_stuff another_property + ON some_property.another_id = another_property.another_id` + + const expectedExpandedCmd = +`select + some_property.some_id as "some_property.some_id", + some_property.another_id as "some_property.another_id", + another_property.another_id as "another_property.another_id" +from some_stuff some_property +LEFT JOIN another_stuff another_property + ON some_property.another_id = another_property.another_id`.replace(/\s+/g, ' ') + .trim() + .toLocaleLowerCase() + + const result = this.queryable.replaceSelectList(TestModel, sourceCmd) + .trim() + .replace(/\s+/g, ' ') + .toLocaleLowerCase() + + expect(result).to.equal(expectedExpandedCmd) + + }) + }) }) diff --git a/packages/dandi-contrib/data-pg/src/pg-db-queryable.ts b/packages/dandi-contrib/data-pg/src/pg-db-queryable.ts index 3bee847c..97a5905c 100644 --- a/packages/dandi-contrib/data-pg/src/pg-db-queryable.ts +++ b/packages/dandi-contrib/data-pg/src/pg-db-queryable.ts @@ -30,7 +30,7 @@ export class PgDbQueryableBase implements D cmd = this.replaceSelectList(model, cmd) const result = await this.queryInternal(cmd, args) if (!result || !result.length) { - return result + return [] } return result.map((item) => this.modelBuilder.constructModel(model, item, this.modelBuilderOptions)) } @@ -38,7 +38,7 @@ export class PgDbQueryableBase implements D public async queryModelSingle(model: Constructor, cmd: string, ...args: any[]): Promise { const result = await this.queryModel(model, cmd, ...args) if (!result || !result.length) { - return null + return undefined } if (result.length > 1) { throw new PgDbMultipleResultsError(cmd) @@ -52,10 +52,10 @@ export class PgDbQueryableBase implements D return cmd } const ogSelect = ogSelectMatch[1].split(',').map((field) => field.trim()) - const tableMatch = cmd.match(/from\s+[\w._]+\s+(?:as\s+)?(\w+)/) + const tableMatch = cmd.match(/from\s+[\w._]+\s+(?:as\s+)?(\w+)/i) const table = tableMatch ? tableMatch[1] : null - const joinMatches = cmd.match(/join\s+[\w._]+\s+(?:as\s+)?(\w+)\s+on/g) - const joins = joinMatches ? joinMatches.map((join) => join.match(/join\s+[\w._]+\s+(?:as\s+)?(\w+)/)[1]) : [] + const joinMatches = cmd.match(/join\s+[\w._]+\s+(?:as\s+)?(\w+)\s+on/gi) + const joins = joinMatches ? joinMatches.map((join) => join.match(/join\s+[\w._]+\s+(?:as\s+)?(\w+)/i)[1]) : [] const aliases = (table ? [table] : []).concat(joins) if (!aliases.length) { return cmd diff --git a/packages/dandi-contrib/data-pg/testing/src/pg-db-pool-client.fixture.ts b/packages/dandi-contrib/data-pg/testing/src/pg-db-pool-client.fixture.ts index bcfd43d5..4887b4a8 100644 --- a/packages/dandi-contrib/data-pg/testing/src/pg-db-pool-client.fixture.ts +++ b/packages/dandi-contrib/data-pg/testing/src/pg-db-pool-client.fixture.ts @@ -6,10 +6,14 @@ import { stub } from 'sinon' export class PgDbPoolClientFixture implements PgDbPoolClient { - public static result(result: Options): void { + public static result(result: Options | Error): void { this.currentResult = result } + public static throws(err: Error): void { + this.currentResult = err + } + public static get factory(): Provider { beforeEach(() => { this.result({ rows: [] }) @@ -20,10 +24,15 @@ export class PgDbPoolClientFixture implements PgDbPoolClient { } } - private static currentResult: Options + private static currentResult: Options | Error constructor() { - stub(this, 'query').resolves(PgDbPoolClientFixture.currentResult) + stub(this, 'query').callsFake(() => { + if (PgDbPoolClientFixture.currentResult instanceof Error) { + return Promise.reject(PgDbPoolClientFixture.currentResult) + } + return Promise.resolve(PgDbPoolClientFixture.currentResult) + }) stub(this, 'dispose') }