Skip to content

Commit

Permalink
Merge pull request #11 from Tombre/execute-runs-query-immediately
Browse files Browse the repository at this point in the history
Run execute query immediately with type safety in args
  • Loading branch information
JakeGinnivan authored Jan 24, 2019
2 parents 9f475f3 + 9d26436 commit 6547fa8
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 68 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ interface QueryResult {
const exampleQuery = queryExecutor.createQuery(async function exampleQuery<
QueryArgs,
QueryResult
>({ args, tables, tableNames, query }) {
>({ tables, tableNames, query }, args) {
// You can access the query arguments through `args`
const { someArg } = args

Expand All @@ -79,7 +79,7 @@ const exampleQuery = queryExecutor.createQuery(async function exampleQuery<
})

// Then execute the query
const queryResult = await queryExecutor.execute(exampleQuery).withArgs({})
const queryResult = await queryExecutor.execute(exampleQuery, { someArg: 'pass the args as the second parameter' })
```

### Wrapping database queries
Expand Down
12 changes: 10 additions & 2 deletions src/__snapshots__/type-safety.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Typescript: Typescript expected failures 1`] = `
"src/type-safety-fixtures/create-query-helper.ts(17,20): error TS2339: Property 'wrongTable' does not exist on type 'TableNames<\\"testTable\\">'.
"src/type-safety-fixtures/args-respected.ts(37,27): error TS7006: Parameter '_' implicitly has an 'any' type.
src/type-safety-fixtures/args-respected.ts(45,41): error TS2345: Argument of type '1' is not assignable to parameter of type 'object'.
src/type-safety-fixtures/args-respected.ts(46,41): error TS2345: Argument of type '1' is not assignable to parameter of type 'string'.
src/type-safety-fixtures/args-respected.ts(47,41): error TS2345: Argument of type '{}' is not assignable to parameter of type '{ id: number; }'.
Property 'id' is missing in type '{}' but required in type '{ id: number; }'.
src/type-safety-fixtures/args-respected.ts(48,41): error TS2345: Argument of type '{}' is not assignable to parameter of type '{ id: number; }'.
Property 'id' is missing in type '{}' but required in type '{ id: number; }'.
src/type-safety-fixtures/args-respected.ts(50,11): error TS2554: Expected 2 arguments, but got 1.
src/type-safety-fixtures/create-query-helper.ts(17,20): error TS2339: Property 'wrongTable' does not exist on type 'TableNames<\\"testTable\\">'.
src/type-safety-fixtures/create-query-helper.ts(18,16): error TS2339: Property 'wrongTable' does not exist on type 'Tables<\\"testTable\\">'.
src/type-safety-fixtures/create-query-helper.ts(20,21): error TS2339: Property 'foo' does not exist on type '{ testArg: string; }'.
src/type-safety-fixtures/query-arguments.ts(22,46): error TS2345: Argument of type '{}' is not assignable to parameter of type '{ testArg: string; }'.
src/type-safety-fixtures/query-arguments.ts(22,37): error TS2345: Argument of type '{}' is not assignable to parameter of type '{ testArg: string; }'.
Property 'testArg' is missing in type '{}' but required in type '{ testArg: string; }'.
"
`;
10 changes: 5 additions & 5 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ it('can initialise read query executor', async () => {
async ({ tableNames }) => tableNames.tableOne
)

const tableName = await queryExecutor.execute(tableNameQuery).withArgs({})
const tableName = await queryExecutor.execute(tableNameQuery, {})

expect(tableName).toBe('table-one')
})
Expand All @@ -32,7 +32,7 @@ it('can wrap queryBuilder queries', async () => {
tables.tableOne()
)

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

expect(executedQuery).toEqual('select * from `table-one`')
})
Expand All @@ -52,7 +52,7 @@ it('can wrap raw queries', async () => {
query(db => db.raw('select 1'))
)

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

expect(executedQuery).toEqual('select 1')
})
Expand All @@ -75,7 +75,7 @@ it('combined wrap API can wrap builder queries', async () => {
tables.tableOne()
)

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

expect(executedQuery).toEqual('select * from `table-one`')
})
Expand All @@ -98,7 +98,7 @@ it('combined wrap API can wrap raw queries', async () => {
query(db => db.raw('select 1'))
)

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

expect(executedQuery).toEqual('select 1')
})
19 changes: 12 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,9 @@ export type TableNames<TTableNames extends string> = {
}

type QueryOptions<
QueryArguments,
TTableNames extends string,
Services extends {}
> = Services & {
args: QueryArguments

/**
* Gives raw access to knex, while still allowing the query to be
* tracked/benchmarked
Expand All @@ -38,18 +35,18 @@ type QueryOptions<
tableNames: TableNames<TTableNames>
queryExecutor: QueryExecutor<TTableNames, Services>
}

export type Query<
QueryArguments,
QueryResult,
TTableNames extends string,
Services extends object
> = (
options: QueryOptions<QueryArguments, TTableNames, Services>
options: QueryOptions<TTableNames, Services>,
args: QueryArguments
) => PromiseLike<QueryResult>

export interface ExecuteResult<Args, Result> {
withArgs: (args: Args) => Promise<Result>
}
export type ExecuteResult<Result> = PromiseLike<Result>

export interface QueryWrapperFn {
(builder: Knex.QueryBuilder): Knex.QueryBuilder
Expand All @@ -64,3 +61,11 @@ export type QueryWrapper =
builder: Knex.QueryBuilder
) => Knex.QueryBuilder
}

export type GetQueryArgs<T> = T extends Query<infer P, any, any, any>
? P
: never

export type GetQueryResult<T> = T extends Query<any, infer P, any, any>
? P
: never
12 changes: 5 additions & 7 deletions src/mock-query-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ it('can mock query', async () => {
})

// Execute the query as normal
const result = await queryExecutor.execute(exampleQuery).withArgs({})
const result = await queryExecutor.execute(exampleQuery, {})

expect(result).toEqual([1])
})
Expand All @@ -38,12 +38,10 @@ it('can match specific query args', async () => {
})

// Execute the query as normal
const result = await queryExecutor
.execute(exampleQuery)
.withArgs({ param: 'first' })
const result2 = await queryExecutor
.execute(exampleQuery)
.withArgs({ param: 'other' })
const result = await queryExecutor.execute(exampleQuery, { param: 'first' })
const result2 = await queryExecutor.execute(exampleQuery, {
param: 'other'
})

expect(result).toEqual(1)
expect(result2).toEqual(2)
Expand Down
40 changes: 18 additions & 22 deletions src/mock-query-executor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExecuteResult, Query, TableNames } from '.'
import { ExecuteResult, GetQueryArgs, GetQueryResult, Query } from '.'
import { ReadQueryExecutor } from './read-query-executor'
import { UnitOfWorkQueryExecutor } from './unit-of-work-query-executor'
export const NoMatch = Symbol('no match')
Expand Down Expand Up @@ -57,30 +57,26 @@ export class MockQueryExecutor extends ReadQueryExecutor<any, any> {
return mocker
}

execute<Args, Result>(
query: Query<Args, Result, any, any>
): ExecuteResult<Args, Result> {
return {
withArgs: (args: Args) => {
for (const mock of this.mocks) {
if (mock.query === query) {
const matcherResult = mock.matcher(args)
if (matcherResult !== NoMatch) {
return new Promise(resolve => {
// Using setTimeout so this is not synchronous
setTimeout(() => {
resolve(matcherResult)
}, 0)
})
}
}
execute<Q extends Query<any, any, any, any>>(
query: Q,
args: GetQueryArgs<Q>
): ExecuteResult<GetQueryResult<Q>> {
for (const mock of this.mocks) {
if (mock.query === query) {
const matcherResult = mock.matcher(args)
if (matcherResult !== NoMatch) {
return new Promise(resolve => {
// Using setTimeout so this is not synchronous
setTimeout(() => {
resolve(matcherResult)
}, 0)
})
}

throw new Error(
`No matcher for query ${query.name || 'unnamed function'}`
)
}
}
throw new Error(
`No matcher for query ${query.name || 'unnamed function'}`
)
}

/**
Expand Down
47 changes: 28 additions & 19 deletions src/query-executor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import Knex from 'knex'
import { ExecuteResult, Query, QueryWrapper, TableNames, Tables } from '.'
import {
ExecuteResult,
GetQueryArgs,
GetQueryResult,
TableNames,
Tables,
Query,
QueryWrapper
} from '.'

export class QueryExecutor<
TTableNames extends string,
Expand Down Expand Up @@ -27,28 +35,29 @@ export class QueryExecutor<
}

/** Helper to create type safe queries */
createQuery<QueryArguments, QueryResult>(
createQuery<QueryArguments = object, QueryResult = any>(
query: Query<QueryArguments, QueryResult, TTableNames, Services>
): Query<QueryArguments, QueryResult, TTableNames, Services> {
) {
return query
}

execute<Args, Result>(
query: Query<Args, Result, TTableNames, Services>
): ExecuteResult<Args, Result> {
return {
withArgs: async args =>
query({
query: getQuery => {
return performWrap(getQuery(this.knex), this.wrapQuery)
},
queryExecutor: this,
tables: this.tables,
args,
tableNames: this.tableNames,
...this.services
})
}
execute<Q extends Query<any, any, TTableNames, Services>>(
query: Q,
args: GetQueryArgs<Q>
): ExecuteResult<GetQueryResult<Q>> {
return query(
{
query: getQuery => {
return performWrap(getQuery(this.knex), this.wrapQuery)
},
queryExecutor: this,
tables: this.tables,
args,
tableNames: this.tableNames,
...this.services
},
args
)
}
}

Expand Down
55 changes: 55 additions & 0 deletions src/type-safety-fixtures/args-respected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ReadQueryExecutor } from '../'
import { createMockedKnex } from '../test-helpers/knex'

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

export async function dontComplainAboutUnused() {
let executedQuery: any

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

const query2 = async (_: any, args: string) => {
return args
}

const query1 = queryExecutor.createQuery(async ({ tables }, args) => {
;(() => args)()
tables.tableOne()
return {}
})

const query3 = queryExecutor.createQuery<{ id: number }, {}>(
async ({ tables }, args) => {
;(() => args)()
tables.tableOne()
return {}
}
)

const query4 = async (_, args: { id: number }) => {
return {}
}

const query5 = async () => {
return {}
}

await queryExecutor.execute(query1, 1)
await queryExecutor.execute(query2, 1)
await queryExecutor.execute(query3, {})
await queryExecutor.execute(query4, {})
await queryExecutor.execute(query5, {})
await queryExecutor.execute(query5)

await queryExecutor.execute(async () => 'x', {})

return executedQuery
}
2 changes: 1 addition & 1 deletion src/type-safety-fixtures/create-query-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const queryExecutor = new ReadQueryExecutor(

const exampleQuery = queryExecutor.createQuery<{ testArg: string }, string>(
// tslint:disable-next-line:no-shadowed-variable
async function exampleQuery({ args, tableNames, tables }) {
async function exampleQuery({ tableNames, tables }, args) {
tableNames.wrongTable
tables.wrongTable()

Expand Down
4 changes: 2 additions & 2 deletions src/type-safety-fixtures/query-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ const exampleQuery: Query<
string,
keyof typeof tableNames,
{}
> = async function exampleQuery({ args }) {
> = async function exampleQuery(_, args) {
return args.testArg
}

// Should fail to compile with missing testArg
queryExecutor.execute(exampleQuery).withArgs({})
queryExecutor.execute(exampleQuery, {})
3 changes: 2 additions & 1 deletion tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"object-literal-sort-keys": false,
"member-access": false,
"interface-name": false,
"no-implicit-dependencies": false
"no-implicit-dependencies": false,
"ordered-imports": false
}
}

0 comments on commit 6547fa8

Please sign in to comment.