|
| 1 | +# Node query executor |
| 2 | + |
| 3 | +[](https://travis-ci.com/sevenwestmedia-labs/node-knex-query-executor)  |
| 4 | + |
| 5 | +A simple library which enables encapsulation of knex queries inside functions. It enables inversion of control for database queries, making it easy to mock the database layer entirely while making database queries themselves easy to test. |
| 6 | + |
| 7 | +## Why |
| 8 | + |
| 9 | +Using knex directly in code means often it is hard to create re-usable database queries, to avoid this the queries are put into functions and the `knex` instance passed into those function. This approach is hard to test and often results in many queries being written inline in places query code should not be written directly. |
| 10 | + |
| 11 | +By forcing all queries to be encapsulated it encourages reuse of queries and building a collection of well tested queries. |
| 12 | + |
| 13 | +## Usage |
| 14 | + |
| 15 | +This library extends this concept to introduce a `QueryExecutor` which can be instructed to execute queries. There are 3 variations, the `ReadQueryExecutor` which is the entry point, when the `unitOfWork` function is called a `UnitOfWorkQueryExecutor` is passed in the callback, everything inside this callback will be executed inside a transaction. If the promise rejects the transaction will be rolled back. |
| 16 | + |
| 17 | +### Constructor |
| 18 | + |
| 19 | +The query executor is a class, to start using it you need to create an instance of the `ReadQueryExecutor`. |
| 20 | + |
| 21 | +```ts |
| 22 | +const queryExecutor = new ReadQueryExecutor( |
| 23 | + // The knex instance |
| 24 | + knex, |
| 25 | + // The services object is available to all queries, ie a logger |
| 26 | + { |
| 27 | + logger |
| 28 | + }, |
| 29 | + // Table names is an object with the tables you would like to access, |
| 30 | + // mapping from the JS name to the database table name |
| 31 | + { |
| 32 | + tableOne: 'table-one' |
| 33 | + }, |
| 34 | + // Optional, you can wrap every query before execution, allowing you to hook in logs or |
| 35 | + // some other manipulation |
| 36 | + query => query |
| 37 | +) |
| 38 | +``` |
| 39 | + |
| 40 | +#### Query Executor types |
| 41 | + |
| 42 | +There are 3 query executor classes, you should only need to construct the `ReadQueryExecutor` as above. |
| 43 | + |
| 44 | +- `ReadQueryExecutor`: Entry point, represents a query executor not in a transaction |
| 45 | +- `UnitOfWorkQueryExecutor`: Type used when function wants to execute a query inside a transaction |
| 46 | +- `QueryExecutor`: Type when code does not care if the query is executed inside or outside a transaction |
| 47 | + |
| 48 | +### Executing a query |
| 49 | + |
| 50 | +```ts |
| 51 | +// If using TypeScript it is advised to use the create query helper |
| 52 | +// which can infer all the types from usage |
| 53 | +interface QueryArgs { |
| 54 | + someArg: string |
| 55 | +} |
| 56 | + |
| 57 | +interface QueryResult { |
| 58 | + col1: string |
| 59 | +} |
| 60 | + |
| 61 | +// NOTE: Name your functions here if possible, it makes the error messages when using |
| 62 | +// the mock query executor better |
| 63 | +const exampleQuery = queryExecutor.createQuery(async function exampleQuery< |
| 64 | + QueryArgs, |
| 65 | + QueryResult |
| 66 | +>({ args, tables, tableNames, query }) { |
| 67 | + // You can access the query arguments through `args` |
| 68 | + const { someArg } = args |
| 69 | + |
| 70 | + // Use tables to get Knex.QueryBuilder's for each table |
| 71 | + const result = await tables.tableOne().where(...).select('col1') |
| 72 | + |
| 73 | + // Use tableNames if you need to access a table name directly (for joins etc) |
| 74 | + // Use query() to access knex directly (it is a callback for wrapping purposes) |
| 75 | + const result = await query(knex => knex(tableNames.tableOne).select('col1')) |
| 76 | + |
| 77 | + // It is the queries responsibility to ensure the type is correct |
| 78 | + return result |
| 79 | +}) |
| 80 | + |
| 81 | +// Then execute the query |
| 82 | +const queryResult = await queryExecutor.execute(exampleQuery).withArgs({}) |
| 83 | +``` |
| 84 | + |
| 85 | +### Wrapping database queries |
| 86 | + |
| 87 | +Sometimes you may want to instrument knex queries (for benchmarking, debugging etc), the query executor makes this really easy. |
| 88 | + |
| 89 | +```ts |
| 90 | +const queryExecutor = new ReadQueryExecutor(knex, {}, tables, { |
| 91 | + queryBuilderWrapper: (query: Knex.QueryBuilder) => { |
| 92 | + // Do what you want here |
| 93 | + |
| 94 | + return query |
| 95 | + }, |
| 96 | + rawQueryWrapper: (query: Knex.Raw) => { |
| 97 | + // Do what you want here |
| 98 | + |
| 99 | + return query |
| 100 | + } |
| 101 | +}) |
| 102 | +``` |
| 103 | + |
| 104 | +### Testing |
| 105 | + |
| 106 | +```ts |
| 107 | +import { NoMatch, MockQueryExecutor } from 'node-query-executor' |
| 108 | + |
| 109 | +const queryExecutor = new MockQueryExecutor() |
| 110 | + |
| 111 | +const exampleQuery = queryExecutor.createQuery<{}, number>(async ({}) => { |
| 112 | + // real query here |
| 113 | +}) |
| 114 | +const exampleQuery2 = queryExecutor.createQuery<{ input: number }, number>( |
| 115 | + async ({}) => { |
| 116 | + // real query here |
| 117 | + } |
| 118 | +) |
| 119 | + |
| 120 | +// Setup the mock in the query executor, returning the same value no matter the args |
| 121 | +queryExecutor.mock(exampleQuery).match(() => { |
| 122 | + return 1 |
| 123 | +}) |
| 124 | + |
| 125 | +// You can also chain matches, inspecting the query arguments |
| 126 | +queryExecutor |
| 127 | + .mock(exampleQuery2) |
| 128 | + .match(({ input }) => { |
| 129 | + // Return 1 if even |
| 130 | + if (input % 2 === 0) { |
| 131 | + return 1 |
| 132 | + } |
| 133 | + |
| 134 | + // Use the NoMatch symbol otherwise |
| 135 | + return NoMatch |
| 136 | + }) |
| 137 | + .match(({ input }) => { |
| 138 | + // Return 0 if odd |
| 139 | + if (input % 2 === 1) { |
| 140 | + return 0 |
| 141 | + } |
| 142 | + |
| 143 | + return NoMatch |
| 144 | + }) |
| 145 | +``` |
| 146 | + |
| 147 | +## Simplifying types |
| 148 | + |
| 149 | +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. |
| 150 | + |
| 151 | +```ts |
| 152 | +import * as KnexQueryExecutor from 'node-knex-query-executor' |
| 153 | + |
| 154 | +interface YourQueryServices { |
| 155 | + log: Logger |
| 156 | +} |
| 157 | + |
| 158 | +export type Query<QueryArguments, QueryResult> = KnexQueryExecutor.Query< |
| 159 | + QueryArguments, |
| 160 | + QueryResult, |
| 161 | + keyof typeof tableNames, |
| 162 | + YourQueryServices |
| 163 | +> |
| 164 | +export type QueryExecutor = KnexQueryExecutor.QueryExecutor< |
| 165 | + keyof typeof tableNames, |
| 166 | + YourQueryServices |
| 167 | +> |
| 168 | +export type ReadQueryExecutor = KnexQueryExecutor.ReadQueryExecutor< |
| 169 | + keyof typeof tableNames, |
| 170 | + YourQueryServices |
| 171 | +> |
| 172 | +export type UnitOfWorkQueryExecutor = KnexQueryExecutor.UnitOfWorkQueryExecutor< |
| 173 | + keyof typeof tableNames, |
| 174 | + YourQueryServices |
| 175 | +> |
| 176 | +export type TableNames = KnexQueryExecutor.TableNames<keyof typeof tableNames> |
| 177 | +export type Tables = KnexQueryExecutor.Tables<keyof typeof tableNames> |
| 178 | +``` |
| 179 | +
|
| 180 | +## Further reading |
| 181 | +
|
| 182 | +This library is inspired by a few object oriented patterns, and a want to move away from repositories. |
| 183 | +
|
| 184 | +https://en.wikipedia.org/wiki/Specification_pattern |
| 185 | +https://martinfowler.com/eaaCatalog/queryObject.html |
| 186 | +https://lostechies.com/chadmyers/2008/08/02/query-objects-with-the-repository-pattern/ |
| 187 | +https://codeopinion.com/query-objects-instead-of-repositories/ |
0 commit comments