Skip to content

Commit

Permalink
feat: add support for modes in query clients
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Aug 16, 2019
1 parent 5919bd3 commit 069c5aa
Show file tree
Hide file tree
Showing 13 changed files with 453 additions and 86 deletions.
17 changes: 16 additions & 1 deletion adonis-typings/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,21 @@ declare module '@ioc:Adonis/Addons/Database' {
* Returns an instance of a given client. A sticky client
* always uses the write connection for all queries
*/
getClient (sticky?: boolean): QueryClientContract,
getClient (mode?: 'write' | 'read'): QueryClientContract,
}

/**
* Database contract serves as the main API to interact with multiple
* database connections
*/
export interface DatabaseContract {
primaryConnectionName: string,
getRawConnection: ConnectionManagerContract['get']

connection (connectionName: string): QueryClientContract
query: QueryClientContract['query']
insertQuery: QueryClientContract['insertQuery']
from: QueryClientContract['from']
table: QueryClientContract['table']
}
}
5 changes: 5 additions & 0 deletions adonis-typings/querybuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,11 @@ declare module '@ioc:Adonis/Addons/DatabaseQueryBuilder' {
*/
dialect: string

/**
* The client mode in which it is execute queries
*/
mode: 'dual' | 'write' | 'read'

/**
* Returns the read and write clients
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Connection/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export class ConnectionManager extends EventEmitter implements ConnectionManager
const connection = this.connections.get(connectionName)
if (!connection) {
throw new Exception(
`Cannot connect to unregisted connection ${connectionName}`,
`Cannot connect to unregistered connection ${connectionName}`,
500,
'E_UNMANAGED_DB_CONNECTION',
)
Expand Down
12 changes: 10 additions & 2 deletions src/Connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,16 @@ export class Connection extends EventEmitter implements ConnectionContract {
* Returns an instance for a query client that using this connection. Setting
* `sticky=true` will use the write connection for reads
*/
public getClient (sticky = false) {
public getClient (mode?: 'read' | 'write') {
this._ensureClients()
return sticky ? new QueryClient(this.client!) : new QueryClient(this.client!, this.readClient!)
if (!mode) {
return new QueryClient('dual', this.client!, this.readClient!)
}

if (mode === 'read') {
return new QueryClient('read', undefined, this.readClient!)
}

return new QueryClient('write', this.client!)
}
}
26 changes: 26 additions & 0 deletions src/QueryBuilder/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
/// <reference path="../../adonis-typings/database.ts" />

import * as knex from 'knex'
import { Exception } from '@poppinss/utils'

import {
DatabaseQueryBuilderContract,
QueryCallback,
Expand Down Expand Up @@ -40,12 +42,34 @@ export class DatabaseQueryBuilder extends Chainable implements DatabaseQueryBuil
super(builder, queryCallback)
}

/**
* Ensures that we are not executing `update` or `del` when using read only
* client
*/
private _ensureCanPerformWrites () {
if (this._client && this._client.mode === 'read') {
throw new Exception('Updates and deletes cannot be performed in read mode')
}
}

/**
* Returns the client to be used for the query. This method relies on the
* query method and will choose the read or write connection whenever
* required.
*/
private _getQueryClient () {
/**
* Do not use custom client when knex builder is using transaction
* client
*/
if (this.$knexBuilder['client']['transacting']) {
return
}

/**
* Return undefined when no parent client is defined or dialect
* is sqlite
*/
if (!this._client || this._client.dialect === 'sqlite3') {
return
}
Expand Down Expand Up @@ -165,6 +189,7 @@ export class DatabaseQueryBuilder extends Chainable implements DatabaseQueryBuil
* Delete rows under the current query
*/
public del (): this {
this._ensureCanPerformWrites()
this.$knexBuilder.del()
return this
}
Expand All @@ -188,6 +213,7 @@ export class DatabaseQueryBuilder extends Chainable implements DatabaseQueryBuil
* Perform update
*/
public update (columns: any): this {
this._ensureCanPerformWrites()
this.$knexBuilder.update(columns)
return this
}
Expand Down
12 changes: 12 additions & 0 deletions src/QueryBuilder/Insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ export class InsertQueryBuilder implements InsertQueryBuilderContract {
* this process.
*/
private _getQueryClient () {
/**
* Do not use custom client when knex builder is using transaction
* client
*/
if (this.$knexBuilder['client']['transacting']) {
return
}

/**
* Return undefined when no parent client is defined or dialect
* is sqlite
*/
if (!this._client || this._client.dialect === 'sqlite3') {
return
}
Expand Down
69 changes: 59 additions & 10 deletions src/QueryClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
InsertQueryBuilderContract,
DatabaseQueryBuilderContract,
} from '@ioc:Adonis/Addons/DatabaseQueryBuilder'

import { Exception } from '@poppinss/utils'
import { resolveClientNameWithAliases } from 'knex/lib/helpers'

import { TransactionClient } from '../TransactionClient'
Expand All @@ -37,38 +39,85 @@ export class QueryClient implements QueryClientContract {
/**
* The name of the dialect in use
*/
public dialect = resolveClientNameWithAliases(this._client.client.config)
public dialect: string

constructor (
public mode: 'dual' | 'write' | 'read',
private _client?: knex,
private _readClient?: knex,
) {
this._validateMode()
this.dialect = resolveClientNameWithAliases(this._getAvailableClient().client.config)
}

constructor (private _client: knex, private _readClient?: knex) {
/**
* Returns any of the available clients, giving preference to the
* write client.
*
* One client will always exists, otherwise instantiation of this
* class will fail.
*/
private _getAvailableClient () {
return (this._client || this._readClient)!
}

/**
* Validates the modes against the provided clients to ensure that class is
* constructed as expected.
*/
private _validateMode () {
if (this.mode === 'dual' && (!this._client || !this._readClient)) {
throw new Exception('Read and write both clients are required in dual mode')
}

if (this.mode === 'write' && (!this._client || this._readClient)) {
throw new Exception('Write client is required in write mode, without the read client')
}

if (this.mode === 'read' && (this._client || !this._readClient)) {
throw new Exception('Read client is required in read mode, without the write client')
}
}

/**
* Returns the read client. The readClient is optional, since we can get
* an instance of [[QueryClient]] with a sticky write client.
*/
public getReadClient () {
return this._readClient || this._client
if (this.mode === 'read' || this.mode === 'dual') {
return this._readClient!
}

return this._client!
}

/**
* Returns the write client
*/
public getWriteClient () {
return this._client
if (this.mode === 'write' || this.mode === 'dual') {
return this._client!
}

throw new Exception(
'Write client is not available for query client instantiated in read mode',
500,
'E_RUNTIME_EXCEPTION',
)
}

/**
* Truncate table
*/
public async truncate (table: string): Promise<void> {
await this._client.select(table).truncate()
await this.getWriteClient().select(table).truncate()
}

/**
* Get information for a table columns
*/
public async columnsInfo (table: string, column?: string): Promise<any> {
const query = this._client.select(table)
const query = this.getWriteClient().select(table)
const result = await (column ? query.columnInfo(column) : query.columnInfo())
return result
}
Expand All @@ -78,7 +127,7 @@ export class QueryClient implements QueryClientContract {
* query and hold a single connection for all queries.
*/
public async transaction (): Promise<TransactionClientContract> {
const trx = await this._client.transaction()
const trx = await this.getWriteClient().transaction()
return new TransactionClient(trx, this.dialect)
}

Expand All @@ -87,21 +136,21 @@ export class QueryClient implements QueryClientContract {
* or deleting rows
*/
public query (): DatabaseQueryBuilderContract {
return new DatabaseQueryBuilder(this._client.queryBuilder(), this)
return new DatabaseQueryBuilder(this._getAvailableClient().queryBuilder(), this)
}

/**
* Returns instance of a query builder for inserting rows
*/
public insertQuery (): InsertQueryBuilderContract {
return new InsertQueryBuilder(this._client.queryBuilder(), this)
return new InsertQueryBuilder(this.getWriteClient().queryBuilder(), this)
}

/**
* Returns instance of raw query builder
*/
public raw (sql: any, bindings?: any): RawContract {
return new RawQueryBuilder(this._client.raw(sql, bindings))
return new RawQueryBuilder(this._getAvailableClient().raw(sql, bindings))
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/TransactionClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export class TransactionClient implements TransactionClientContract {
*/
public isTransaction: true = true

/**
* Transactions are always in write mode, since they always needs
* the primary connection
*/
public mode: 'dual' = 'dual'

constructor (public knexClient: knex.Transaction, public dialect: string) {
}

Expand Down
6 changes: 4 additions & 2 deletions test-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,10 @@ export function getRawQueryBuilder (connection: ConnectionContract, sql: string,
/**
* Returns query builder instance for a given connection
*/
export function getInsertBuilder (connection: ConnectionContract) {
return new InsertQueryBuilder(connection.client!.queryBuilder()) as unknown as InsertQueryBuilderContract
export function getInsertBuilder (client: QueryClientContract) {
return new InsertQueryBuilder(
client.getWriteClient().queryBuilder(),
) as unknown as InsertQueryBuilderContract
}

/**
Expand Down
66 changes: 0 additions & 66 deletions test/connection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,69 +149,3 @@ if (process.env.DB === 'mysql') {
})
})
}

test.group('Connection | queryClient', (group) => {
group.before(async () => {
await setup()
})

group.after(async () => {
await cleanup()
})

test('get query builder instance to perform select queries', async (assert) => {
const connection = new Connection('primary', getConfig(), getLogger())
connection.connect()

const results = await connection.getClient().query().from('users')
assert.isArray(results)
assert.lengthOf(results, 0)

await connection.disconnect()
})

test('get insert query builder instance', async (assert) => {
const connection = new Connection('primary', getConfig(), getLogger())
connection.connect()

await connection.getClient().insertQuery().table('users').insert({ username: 'virk' })

const results = await connection.getClient().query().from('users')
assert.isArray(results)
assert.lengthOf(results, 1)
assert.equal(results[0].username, 'virk')

await connection.disconnect()
})

test('perform raw queries', async (assert) => {
const connection = new Connection('primary', getConfig(), getLogger())
connection.connect()

const command = process.env.DB === 'sqlite' ? `DELETE FROM users;` : 'TRUNCATE users;'

await connection.getClient().insertQuery().table('users').insert({ username: 'virk' })
await connection.getClient().raw(command).exec()
const results = await connection.getClient().query().from('users')
assert.isArray(results)
assert.lengthOf(results, 0)

await connection.disconnect()
})

test('perform queries inside a transaction', async (assert) => {
const connection = new Connection('primary', getConfig(), getLogger())
connection.connect()

const trx = await connection.getClient().transaction()
await trx.insertQuery().table('users').insert({ username: 'virk' })
await trx.rollback()

const results = await connection.getClient().query().from('users')

assert.isArray(results)
assert.lengthOf(results, 0)

await connection.disconnect()
})
})
6 changes: 3 additions & 3 deletions test/insert-query-builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test.group('Query Builder | from', (group) => {
const connection = new Connection('primary', getConfig(), getLogger())
connection.connect()

const db = getInsertBuilder(connection)
const db = getInsertBuilder(connection.getClient())
const { sql, bindings } = db.table('users').insert({ username: 'virk' }).toSQL()

const { sql: knexSql, bindings: knexBindings } = connection.client!
Expand All @@ -43,7 +43,7 @@ test.group('Query Builder | from', (group) => {
const connection = new Connection('primary', getConfig(), getLogger())
connection.connect()

const db = getInsertBuilder(connection)
const db = getInsertBuilder(connection.getClient())
const { sql, bindings } = db
.table('users')
.multiInsert([{ username: 'virk' }, { username: 'nikk' }])
Expand All @@ -62,7 +62,7 @@ test.group('Query Builder | from', (group) => {
const connection = new Connection('primary', getConfig(), getLogger())
connection.connect()

const db = getInsertBuilder(connection)
const db = getInsertBuilder(connection.getClient())
const { sql, bindings } = db
.table('users')
.returning(['id', 'username'])
Expand Down

0 comments on commit 069c5aa

Please sign in to comment.