Skip to content

Commit

Permalink
Merge pull request #6 from JakeGinnivan/fix-consumption-issues
Browse files Browse the repository at this point in the history
Fix consumption issues
  • Loading branch information
JakeGinnivan authored Jan 3, 2019
2 parents 9c2b130 + bd3cea3 commit 7d86674
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 24 deletions.
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug tests: current file",
"program": "${workspaceRoot}/node_modules/.bin/jest",
"args": ["${relativeFile}", "--runInBand"],
"cwd": "${workspaceRoot}",
"sourceMaps": true
}
]
}
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,25 @@ const exampleQuery = queryExecutor.createQuery(async function exampleQuery<
const queryResult = await queryExecutor.execute(exampleQuery).withArgs({})
```

### Wrapping database queries

Sometimes you may want to instrument knex queries (for benchmarking, debugging etc), the query executor makes this really easy.

```ts
const queryExecutor = new ReadQueryExecutor(knex, {}, tables, {
queryBuilderWrapper: (query: Knex.QueryBuilder) => {
// Do what you want here

return query
},
rawQueryWrapper: (query: Knex.Raw) => {
// Do what you want here

return query
}
})
```

### Testing

```ts
Expand Down Expand Up @@ -125,6 +144,39 @@ queryExecutor
})
```

## Simplifying types

Because the QueryExecutor types are generic, it often is verbose writing `QueryExecutor<typeof keyof tableNames, YourQueryServices>`, it is suggested you export your own closed generic types to make them easy to pass around.

```ts
import * as KnexQueryExecutor from 'node-knex-query-executor'

interface YourQueryServices {
log: Logger
}

export type Query<QueryArguments, QueryResult> = KnexQueryExecutor.Query<
QueryArguments,
QueryResult,
keyof typeof tableNames,
YourQueryServices
>
export type QueryExecutor = KnexQueryExecutor.QueryExecutor<
keyof typeof tableNames,
YourQueryServices
>
export type ReadQueryExecutor = KnexQueryExecutor.ReadQueryExecutor<
keyof typeof tableNames,
YourQueryServices
>
export type UnitOfWorkQueryExecutor = KnexQueryExecutor.UnitOfWorkQueryExecutor<
keyof typeof tableNames,
YourQueryServices
>
export type TableNames = KnexQueryExecutor.TableNames<keyof typeof tableNames>
export type Tables = KnexQueryExecutor.Tables<keyof typeof tableNames>
```
## Further reading
This library is inspired by a few object oriented patterns, and a want to move away from repositories.
Expand Down
90 changes: 88 additions & 2 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ReadQueryExecutor } from '.'
import { createMockedKnex } from './test-helpers/knex'

const tables = {
const testTables = {
tableOne: 'table-one'
}

it('can initialise read query executor', async () => {
const knex = createMockedKnex(query => query.response([]))
const queryExecutor = new ReadQueryExecutor(knex, {}, tables)
const queryExecutor = new ReadQueryExecutor(knex, {}, testTables)
const tableNameQuery = queryExecutor.createQuery(
async ({ tableNames }) => tableNames.tableOne
)
Expand All @@ -16,3 +16,89 @@ it('can initialise read query executor', async () => {

expect(tableName).toBe('table-one')
})

it('can wrap queryBuilder queries', async () => {
let executedQuery: any

const knex = createMockedKnex(query => query.response([]))
const queryExecutor = new ReadQueryExecutor(knex, {}, testTables, {
queryBuilderWrapper: query => {
executedQuery = query.toString()

return query
}
})
const tableNameQuery = queryExecutor.createQuery(async ({ tables }) =>
tables.tableOne()
)

await queryExecutor.execute(tableNameQuery).withArgs({})

expect(executedQuery).toEqual('select * from `table-one`')
})

it('can wrap raw queries', async () => {
let executedQuery: any

const knex = createMockedKnex(query => query.response([]))
const queryExecutor = new ReadQueryExecutor(knex, {}, testTables, {
rawQueryWrapper: query => {
executedQuery = query.toString()

return query
}
})
const testQuery = queryExecutor.createQuery(async ({ query }) =>
query(db => db.raw('select 1'))
)

await queryExecutor.execute(testQuery).withArgs({})

expect(executedQuery).toEqual('select 1')
})

it('combined wrap API can wrap builder queries', async () => {
let executedQuery: any

const knex = createMockedKnex(query => query.response([]))
const queryExecutor = new ReadQueryExecutor(
knex,
{},
testTables,
(query: any) => {
executedQuery = query.toString()

return query
}
)
const testQuery = queryExecutor.createQuery(async ({ tables }) =>
tables.tableOne()
)

await queryExecutor.execute(testQuery).withArgs({})

expect(executedQuery).toEqual('select * from `table-one`')
})

it('combined wrap API can wrap raw queries', async () => {
let executedQuery: any

const knex = createMockedKnex(query => query.response([]))
const queryExecutor = new ReadQueryExecutor(
knex,
{},
testTables,
(query: any) => {
executedQuery = query.toString()

return query
}
)
const testQuery = queryExecutor.createQuery(async ({ query }) =>
query(db => db.raw('select 1'))
)

await queryExecutor.execute(testQuery).withArgs({})

expect(executedQuery).toEqual('select 1')
})
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ export interface ExecuteResult<Args, Result> {
withArgs: (args: Args) => Promise<Result>
}

export interface QueryWrapper {
export interface QueryWrapperFn {
(builder: Knex.QueryBuilder): Knex.QueryBuilder
(builder: Knex.Raw): Knex.Raw
}

export type QueryWrapper =
| QueryWrapperFn
| {
rawQueryWrapper?: (builder: Knex.Raw) => Knex.Raw
queryBuilderWrapper?: (
builder: Knex.QueryBuilder
) => Knex.QueryBuilder
}
18 changes: 8 additions & 10 deletions src/mock-query-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,24 @@ export type Matcher<Args, Result> = (args: Args) => typeof NoMatch | Result
* return NoMatch
* })
*/
export class MockQueryExecutor<
TTableNames extends string,
Services extends {}
> extends ReadQueryExecutor<TTableNames, Services> {
export class MockQueryExecutor extends ReadQueryExecutor<any, any> {
// Making kind `any` on the executor means it's compatible with all QueryExecutors
kind: any

constructor() {
// The real query executor should not be called so this is fine
super(undefined as any, undefined as any, {} as any)
}
private mocks: Array<{
query: Query<any, any, TTableNames, Services>
query: Query<any, any, any, any>
matcher: Matcher<any, any>
}> = []

clear() {
this.mocks = []
}

mock<Args, Result>(query: Query<Args, Result, TTableNames, Services>) {
mock<Args, Result>(query: Query<Args, Result, any, any>) {
const mocker = {
match: (getResult: Matcher<Args, Result>) => {
this.mocks.push({
Expand All @@ -58,7 +58,7 @@ export class MockQueryExecutor<
}

execute<Args, Result>(
query: Query<Args, Result, TTableNames, Services>
query: Query<Args, Result, any, any>
): ExecuteResult<Args, Result> {
return {
withArgs: (args: Args) => {
Expand Down Expand Up @@ -90,9 +90,7 @@ export class MockQueryExecutor<
* @example executor.unitOfWork(unit => unit.executeQuery(insertBlah, blah))
*/
unitOfWork<T>(
work: (
executor: UnitOfWorkQueryExecutor<TTableNames, Services>
) => Promise<T>
work: (executor: UnitOfWorkQueryExecutor<any, any>) => Promise<T>
): PromiseLike<any> {
return work(this as any)
}
Expand Down
52 changes: 42 additions & 10 deletions src/query-executor.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import * as Knex from 'knex'
import Knex from 'knex'
import { ExecuteResult, Query, QueryWrapper, TableNames, Tables } from '.'

export class QueryExecutor<
TTableNames extends string,
Services extends object
> {
protected tables: Tables<TTableNames>
protected wrap: QueryWrapper

constructor(
public kind: 'read-query-executor' | 'unit-of-work-query-executor',
protected knex: Knex | Knex.Transaction,
protected services: Services,
protected tableNames: TableNames<TTableNames>,
wrapQuery?: QueryWrapper
protected wrapQuery?: QueryWrapper
) {
this.wrap = wrapQuery || ((b: any) => b)

this.tables = Object.keys(tableNames).reduce<any>((acc, tableName) => {
acc[tableName] = () => this.wrap(knex(tableName))
acc[tableName] = () => {
return performWrap(
knex(tableNames[tableName as TTableNames]),
this.wrapQuery
)
}

return acc
}, {})
Expand All @@ -37,11 +39,10 @@ export class QueryExecutor<
return {
withArgs: async args =>
query({
query: createQuery =>
this.wrap(createQuery(this.knex) as any),
query: getQuery => {
return performWrap(getQuery(this.knex), this.wrapQuery)
},
queryExecutor: this,
wrapQuery: (builder: Knex.QueryBuilder) =>
this.wrap(builder),
tables: this.tables,
args,
tableNames: this.tableNames,
Expand All @@ -50,3 +51,34 @@ export class QueryExecutor<
}
}
}

function performWrap(
queryToWrap: Knex.QueryBuilder | Knex.Raw,
wrapper: QueryWrapper | undefined
) {
if (!wrapper) {
return queryToWrap
}

if (typeof wrapper === 'function') {
return wrapper(queryToWrap as any)
}

if (isRawQuery(queryToWrap)) {
if (!wrapper.rawQueryWrapper) {
return queryToWrap
}

return wrapper.rawQueryWrapper(queryToWrap)
}

if (wrapper.queryBuilderWrapper) {
return wrapper.queryBuilderWrapper(queryToWrap)
}

return queryToWrap
}

function isRawQuery(query: Knex.Raw | Knex.QueryBuilder): query is Knex.Raw {
return 'sql' in query
}
2 changes: 1 addition & 1 deletion src/read-query-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class ReadQueryExecutor<
trx,
this.services,
this.tableNames,
this.wrap
this.wrapQuery
)
)
})
Expand Down

0 comments on commit 7d86674

Please sign in to comment.