Skip to content

Commit

Permalink
9
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Schaffer committed Mar 12, 2019
1 parent 5f59dc0 commit 5fb81e1
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 10 deletions.
153 changes: 151 additions & 2 deletions 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() {

Expand All @@ -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() {
Expand All @@ -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')
Expand All @@ -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)

})

})
})
10 changes: 5 additions & 5 deletions packages/dandi-contrib/data-pg/src/pg-db-queryable.ts
Expand Up @@ -30,15 +30,15 @@ export class PgDbQueryableBase<TClient extends PgDbQueryableClient> 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))
}

public async queryModelSingle<T>(model: Constructor<T>, cmd: string, ...args: any[]): Promise<T> {
const result = await this.queryModel(model, cmd, ...args)
if (!result || !result.length) {
return null
return undefined
}
if (result.length > 1) {
throw new PgDbMultipleResultsError(cmd)
Expand All @@ -52,10 +52,10 @@ export class PgDbQueryableBase<TClient extends PgDbQueryableClient> 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
Expand Down
Expand Up @@ -6,10 +6,14 @@ import { stub } from 'sinon'

export class PgDbPoolClientFixture implements PgDbPoolClient {

public static result(result: Options<QueryResult>): void {
public static result(result: Options<QueryResult> | Error): void {
this.currentResult = result
}

public static throws(err: Error): void {
this.currentResult = err
}

public static get factory(): Provider<PgDbPoolClient> {
beforeEach(() => {
this.result({ rows: [] })
Expand All @@ -20,10 +24,15 @@ export class PgDbPoolClientFixture implements PgDbPoolClient {
}
}

private static currentResult: Options<QueryResult>
private static currentResult: Options<QueryResult> | 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')
}

Expand Down

0 comments on commit 5fb81e1

Please sign in to comment.