Skip to content

Commit

Permalink
feat: add support for extending query builders
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 12, 2020
1 parent 329e961 commit 1fb858d
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 4 deletions.
5 changes: 5 additions & 0 deletions adonis-typings/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare module '@ioc:Adonis/Lucid/Database' {
import { Pool } from 'tarn'
import { EventEmitter } from 'events'
import { Dictionary } from 'ts-essentials'
import { MacroableConstructorContract } from 'macroable'
import { ProfilerRowContract, ProfilerContract } from '@ioc:Adonis/Core/Profiler'

import {
Expand Down Expand Up @@ -587,6 +588,10 @@ declare module '@ioc:Adonis/Lucid/Database' {
* database connections
*/
export interface DatabaseContract {
DatabaseQueryBuilder: MacroableConstructorContract<DatabaseQueryBuilderContract>,
InsertQueryBuilder: MacroableConstructorContract<InsertQueryBuilderContract>,
ModelQueryBuilder: MacroableConstructorContract<ModelQueryBuilderContract<any, any>>,

/**
* Name of the primary connection defined inside `config/database.ts`
* file
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"knex": "^0.20.4",
"knex-dynamic-connection": "^1.0.3",
"log-update": "^3.3.0",
"macroable": "^3.0.0",
"pluralize": "^8.0.0",
"snake-case": "^3.0.2",
"ts-essentials": "^4.0.0"
Expand Down
7 changes: 5 additions & 2 deletions src/Database/QueryBuilder/Chainable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/// <reference path="../../../adonis-typings/index.ts" />

import knex from 'knex'
import { Macroable } from 'macroable'
import { ChainableContract, DBQueryCallback } from '@ioc:Adonis/Lucid/DatabaseQueryBuilder'
import { RawQueryBuilder } from './Raw'

Expand All @@ -20,11 +21,13 @@ import { RawQueryBuilder } from './Raw'
* The API internally uses the knex query builder. However, many of methods may have
* different API.
*/
export abstract class Chainable implements ChainableContract {
export abstract class Chainable extends Macroable implements ChainableContract {
constructor (
public $knexBuilder: knex.QueryBuilder, // Needs to be public for Executable trait
private _queryCallback: DBQueryCallback,
) {}
) {
super()
}

/**
* Returns the value pair for the `whereBetween` clause
Expand Down
6 changes: 6 additions & 0 deletions src/Database/QueryBuilder/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export class DatabaseQueryBuilder extends Chainable implements DatabaseQueryBuil
super(builder, queryCallback)
}

/**
* Required by macroable
*/
protected static _macros = {}
protected static _getters = {}

/**
* Ensures that we are not executing `update` or `del` when using read only
* client
Expand Down
10 changes: 9 additions & 1 deletion src/Database/QueryBuilder/Insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/// <reference path="../../../adonis-typings/index.ts" />

import knex from 'knex'
import { Macroable } from 'macroable'
import { trait } from '@poppinss/traits'

import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'
Expand All @@ -21,10 +22,17 @@ import { Executable, ExecutableConstructor } from '../../Traits/Executable'
* Exposes the API for performing SQL inserts
*/
@trait<ExecutableConstructor>(Executable)
export class InsertQueryBuilder implements InsertQueryBuilderContract {
export class InsertQueryBuilder extends Macroable implements InsertQueryBuilderContract {
constructor (public $knexBuilder: knex.QueryBuilder, public client: QueryClientContract) {
super()
}

/**
* Required by macroable
*/
protected static _macros = {}
protected static _getters = {}

/**
* Returns the client to be used for the query. Even though the insert query
* is always using the `write` client, we still go through the process of
Expand Down
11 changes: 11 additions & 0 deletions src/Database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
import { QueryClient } from '../QueryClient'
import { ConnectionManager } from '../Connection/Manager'

import { InsertQueryBuilder } from './QueryBuilder/Insert'
import { DatabaseQueryBuilder } from './QueryBuilder/Database'
import { ModelQueryBuilder } from '../Orm/QueryBuilder'

/**
* Database class exposes the API to manage multiple connections and obtain an instance
* of query/transaction clients.
Expand All @@ -38,6 +42,13 @@ export class Database implements DatabaseContract {
*/
public primaryConnectionName = this._config.connection

/**
* Reference to query builders
*/
public DatabaseQueryBuilder = DatabaseQueryBuilder
public InsertQueryBuilder = InsertQueryBuilder
public ModelQueryBuilder = ModelQueryBuilder

constructor (
private _config: DatabaseConfigContract,
private _logger: LoggerContract,
Expand Down
6 changes: 6 additions & 0 deletions src/Orm/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon
*/
private _preloader = new Preloader(this.model)

/**
* Required by macroable
*/
protected static _macros = {}
protected static _getters = {}

/**
* Options that must be passed to all new model instances
*/
Expand Down
62 changes: 61 additions & 1 deletion test/database/database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import test from 'japa'

import { Database } from '../../src/Database'
import { getConfig, setup, cleanup, getLogger, getProfiler } from '../../test-helpers'
import { getConfig, setup, cleanup, getLogger, getProfiler, getDb } from '../../test-helpers'

test.group('Database', (group) => {
group.before(async () => {
Expand Down Expand Up @@ -220,3 +220,63 @@ test.group('Database', (group) => {
await db.manager.closeAll()
})
})

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

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

test('extend database query builder by adding macros', async (assert) => {
const db = getDb()

db.DatabaseQueryBuilder.macro('whereActive', function whereActive () {
this.where('is_active', true)
return this
})

const knexClient = db.connection().getReadClient()

const { sql, bindings } = db.query().from('users')['whereActive']().toSQL()
const { sql: knexSql, bindings: knexBindings } = knexClient
.from('users')
.where('is_active', true)
.toSQL()

assert.equal(sql, knexSql)
assert.deepEqual(bindings, knexBindings)

await db.manager.closeAll()
})

test('extend insert query builder by adding macros', async (assert) => {
const db = getDb()

db.InsertQueryBuilder.macro('returnId', function whereActive () {
this.returning('id')
return this
})

const knexClient = db.connection().getReadClient()

const { sql, bindings } = db
.insertQuery()
.table('users')['returnId']()
.insert({ id: 1 })
.toSQL()

const { sql: knexSql, bindings: knexBindings } = knexClient
.from('users')
.returning('id')
.insert({ id: 1 })
.toSQL()

assert.equal(sql, knexSql)
assert.deepEqual(bindings, knexBindings)

await db.manager.closeAll()
})
})
74 changes: 74 additions & 0 deletions test/orm/base-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2170,3 +2170,77 @@ test.group('Base Model | hooks', (group) => {
assert.equal(usersCount[0].total, 1)
})
})

test.group('Base model | extend', (group) => {
group.before(async () => {
db = getDb()
BaseModel = getBaseModel(ormAdapter(db))
})

group.after(async () => {
await db.manager.closeAll()
})

test('extend model query builder', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string
}
User.$boot()

db.ModelQueryBuilder.macro('whereActive', function () {
this.where('is_active', true)
return this
})

const knexClient = db.connection().getReadClient()
const { sql, bindings } = User.query()['whereActive']().toSQL()
const { sql: knexSql, bindings: knexBindings } = knexClient
.from('users')
.where('is_active', true)
.toSQL()

assert.equal(sql, knexSql)
assert.deepEqual(bindings, knexBindings)
})

test('extend model insert query builder', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string

public $getQueryFor (_, client) {
return client.insertQuery().table('users').withId()
}
}
User.$boot()

db.InsertQueryBuilder.macro('withId', function () {
this.returning('id')
return this
})

const knexClient = db.connection().getReadClient()
const user = new User()

const { sql, bindings } = user
.$getQueryFor('insert', db.connection())
.insert({ id: 1 })
.toSQL()

const { sql: knexSql, bindings: knexBindings } = knexClient
.from('users')
.returning('id')
.insert({ id: 1 })
.toSQL()

assert.equal(sql, knexSql)
assert.deepEqual(bindings, knexBindings)
})
})

0 comments on commit 1fb858d

Please sign in to comment.