From d340b4e4cde922a5a97274b6e980cb381f6a5a2d Mon Sep 17 00:00:00 2001 From: Jake Ginnivan Date: Wed, 2 Jan 2019 16:32:16 +0800 Subject: [PATCH 1/5] chore: Added test debugging configuration --- .vscode/launch.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..66b877f --- /dev/null +++ b/.vscode/launch.json @@ -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 + } + ] +} From 231d81d40f8366ece70121b9f7d9a235060baa3c Mon Sep 17 00:00:00 2001 From: Jake Ginnivan Date: Wed, 2 Jan 2019 17:02:29 +0800 Subject: [PATCH 2/5] feature: Enabled typescript compatible way to wrap all queries It has been added in a backwards compatible way. Fixes #3. --- README.md | 19 ++++++++ src/index.test.ts | 90 +++++++++++++++++++++++++++++++++++++- src/index.ts | 11 ++++- src/query-executor.ts | 49 ++++++++++++++++----- src/read-query-executor.ts | 2 +- 5 files changed, 157 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c8184ff..f7dca65 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/index.test.ts b/src/index.test.ts index efb13ab..da17ba8 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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 ) @@ -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 `tableOne`') +}) + +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 `tableOne`') +}) + +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') +}) diff --git a/src/index.ts b/src/index.ts index b22cde2..21cfb6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,7 +51,16 @@ export interface ExecuteResult { withArgs: (args: Args) => Promise } -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 + } diff --git a/src/query-executor.ts b/src/query-executor.ts index 0e70db7..f0270a1 100644 --- a/src/query-executor.ts +++ b/src/query-executor.ts @@ -1,4 +1,4 @@ -import * as Knex from 'knex' +import Knex from 'knex' import { ExecuteResult, Query, QueryWrapper, TableNames, Tables } from '.' export class QueryExecutor< @@ -6,19 +6,18 @@ export class QueryExecutor< Services extends object > { protected tables: Tables - 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, - wrapQuery?: QueryWrapper + protected wrapQuery?: QueryWrapper ) { - this.wrap = wrapQuery || ((b: any) => b) - this.tables = Object.keys(tableNames).reduce((acc, tableName) => { - acc[tableName] = () => this.wrap(knex(tableName)) + acc[tableName] = () => { + return performWrap(knex(tableName), this.wrapQuery) + } return acc }, {}) @@ -37,11 +36,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, @@ -50,3 +48,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 +} diff --git a/src/read-query-executor.ts b/src/read-query-executor.ts index fa6cbd9..886d29c 100644 --- a/src/read-query-executor.ts +++ b/src/read-query-executor.ts @@ -37,7 +37,7 @@ export class ReadQueryExecutor< trx, this.services, this.tableNames, - this.wrap + this.wrapQuery ) ) }) From 14acd078f53024918ea86ed07845820376683306 Mon Sep 17 00:00:00 2001 From: Jake Ginnivan Date: Wed, 2 Jan 2019 17:15:05 +0800 Subject: [PATCH 3/5] docs: Added guidance for closing the generic types This makes it easier to reference query executor types in consuming code. Fixes #4 --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index f7dca65..dd51af4 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,39 @@ queryExecutor }) ``` +## Simplifying types + +Because the QueryExecutor types are generic, it often is verbose writing `QueryExecutor`, 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 = 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 +export type Tables = KnexQueryExecutor.Tables +``` + ## Further reading This library is inspired by a few object oriented patterns, and a want to move away from repositories. From 03bd340d9742b5b3a4fe70ecb07774c7ada66356 Mon Sep 17 00:00:00 2001 From: Jake Ginnivan Date: Wed, 2 Jan 2019 17:38:48 +0800 Subject: [PATCH 4/5] fix: Removed generic params from mock-query-executor Fixes #5 --- src/mock-query-executor.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/mock-query-executor.ts b/src/mock-query-executor.ts index a99a48c..ff56f3a 100644 --- a/src/mock-query-executor.ts +++ b/src/mock-query-executor.ts @@ -27,16 +27,16 @@ export type Matcher = (args: Args) => typeof NoMatch | Result * return NoMatch * }) */ -export class MockQueryExecutor< - TTableNames extends string, - Services extends {} -> extends ReadQueryExecutor { +export class MockQueryExecutor extends ReadQueryExecutor { + // 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 + query: Query matcher: Matcher }> = [] @@ -44,7 +44,7 @@ export class MockQueryExecutor< this.mocks = [] } - mock(query: Query) { + mock(query: Query) { const mocker = { match: (getResult: Matcher) => { this.mocks.push({ @@ -58,7 +58,7 @@ export class MockQueryExecutor< } execute( - query: Query + query: Query ): ExecuteResult { return { withArgs: (args: Args) => { @@ -90,9 +90,7 @@ export class MockQueryExecutor< * @example executor.unitOfWork(unit => unit.executeQuery(insertBlah, blah)) */ unitOfWork( - work: ( - executor: UnitOfWorkQueryExecutor - ) => Promise + work: (executor: UnitOfWorkQueryExecutor) => Promise ): PromiseLike { return work(this as any) } From bd3cea304c529515c6ee03abc74d575b4b8fe159 Mon Sep 17 00:00:00 2001 From: Jake Ginnivan Date: Wed, 2 Jan 2019 21:21:09 +0800 Subject: [PATCH 5/5] fix: Table name not looked being up from tableNames map Fixed #7 --- src/index.test.ts | 4 ++-- src/query-executor.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index da17ba8..c9487dd 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -34,7 +34,7 @@ it('can wrap queryBuilder queries', async () => { await queryExecutor.execute(tableNameQuery).withArgs({}) - expect(executedQuery).toEqual('select * from `tableOne`') + expect(executedQuery).toEqual('select * from `table-one`') }) it('can wrap raw queries', async () => { @@ -77,7 +77,7 @@ it('combined wrap API can wrap builder queries', async () => { await queryExecutor.execute(testQuery).withArgs({}) - expect(executedQuery).toEqual('select * from `tableOne`') + expect(executedQuery).toEqual('select * from `table-one`') }) it('combined wrap API can wrap raw queries', async () => { diff --git a/src/query-executor.ts b/src/query-executor.ts index f0270a1..f5d816a 100644 --- a/src/query-executor.ts +++ b/src/query-executor.ts @@ -16,7 +16,10 @@ export class QueryExecutor< ) { this.tables = Object.keys(tableNames).reduce((acc, tableName) => { acc[tableName] = () => { - return performWrap(knex(tableName), this.wrapQuery) + return performWrap( + knex(tableNames[tableName as TTableNames]), + this.wrapQuery + ) } return acc