From 912f7e18dcb6b05eb61a6b1cd609609f5ca321fa Mon Sep 17 00:00:00 2001 From: Harminder virk Date: Sun, 6 Oct 2019 23:10:09 +0530 Subject: [PATCH] feat(model): implement update,delete and counter methods to model query builder --- adonis-typings/model.ts | 23 +++++-- adonis-typings/querybuilder.ts | 6 +- src/Database/QueryBuilder/Chainable.ts | 18 ------ src/Database/QueryBuilder/Database.ts | 18 ++++++ src/Orm/QueryBuilder/index.ts | 75 +++++++++++++++++++++++ src/Orm/Relations/Base/QueryBuilder.ts | 2 +- test-helpers/index.ts | 1 + test-helpers/tmp/db.sqlite | Bin 45056 -> 0 bytes test/orm/model-query-builder.spec.ts | 80 +++++++++++++++++++++++++ 9 files changed, 197 insertions(+), 26 deletions(-) delete mode 100644 test-helpers/tmp/db.sqlite diff --git a/adonis-typings/model.ts b/adonis-typings/model.ts index acf562a0..dac45ba3 100644 --- a/adonis-typings/model.ts +++ b/adonis-typings/model.ts @@ -9,7 +9,14 @@ declare module '@ioc:Adonis/Lucid/Model' { import { ProfilerContract, ProfilerRowContract } from '@ioc:Adonis/Core/Profiler' - import { ChainableContract, StrictValues, QueryCallback } from '@ioc:Adonis/Lucid/DatabaseQueryBuilder' + import { + Update, + Counter, + StrictValues, + QueryCallback, + ChainableContract, + } from '@ioc:Adonis/Lucid/DatabaseQueryBuilder' + import { QueryClientContract, TransactionClientContract, @@ -368,9 +375,9 @@ declare module '@ioc:Adonis/Lucid/Model' { /** * Model query builder will have extras methods on top of Database query builder */ - export interface ModelQueryBuilderContract< - Model extends ModelConstructorContract - > extends ChainableContract { + export interface ModelQueryBuilderContract + extends ChainableContract + { model: Model /** @@ -405,6 +412,14 @@ declare module '@ioc:Adonis/Lucid/Model' { */ firstOrFail (): Promise> + /** + * Mutations (update and increment can be one query aswell) + */ + update: Update & ExcutableQueryBuilderContract> + increment: Counter & ExcutableQueryBuilderContract> + decrement: Counter & ExcutableQueryBuilderContract> + del (): ModelQueryBuilderContract & ExcutableQueryBuilderContract + /** * Define relationships to be preloaded */ diff --git a/adonis-typings/querybuilder.ts b/adonis-typings/querybuilder.ts index 88e21b58..15e53632 100644 --- a/adonis-typings/querybuilder.ts +++ b/adonis-typings/querybuilder.ts @@ -602,8 +602,6 @@ declare module '@ioc:Adonis/Lucid/DatabaseQueryBuilder' { export interface DatabaseQueryBuilderContract < Result extends any = Dictionary, > extends ChainableContract { - del (): this - client: QueryClientContract, /** @@ -616,8 +614,10 @@ declare module '@ioc:Adonis/Lucid/DatabaseQueryBuilder' { */ first (): Promise + del (): this + /** - * Mutations + * Mutations (update and increment can be one query aswell) */ update: Update increment: Counter diff --git a/src/Database/QueryBuilder/Chainable.ts b/src/Database/QueryBuilder/Chainable.ts index 5fd8f3c4..984c9335 100644 --- a/src/Database/QueryBuilder/Chainable.ts +++ b/src/Database/QueryBuilder/Chainable.ts @@ -1095,22 +1095,4 @@ export abstract class Chainable implements ChainableContract { this.$knexBuilder.sum(this._normalizeAggregateColumns(columns, alias)) return this } - - /** - * Perform update by incrementing value for a given column. Increments - * can be clubbed with `update` as well - */ - public increment (column: any, counter?: any): this { - this.$knexBuilder.increment(column, counter) - return this - } - - /** - * Perform update by decrementing value for a given column. Decrements - * can be clubbed with `update` as well - */ - public decrement (column: any, counter?: any): this { - this.$knexBuilder.decrement(column, counter) - return this - } } diff --git a/src/Database/QueryBuilder/Database.ts b/src/Database/QueryBuilder/Database.ts index 70ad476e..4b7e0184 100644 --- a/src/Database/QueryBuilder/Database.ts +++ b/src/Database/QueryBuilder/Database.ts @@ -89,6 +89,24 @@ export class DatabaseQueryBuilder extends Chainable implements DatabaseQueryBuil return this } + /** + * Perform update by incrementing value for a given column. Increments + * can be clubbed with `update` as well + */ + public increment (column: any, counter?: any): this { + this.$knexBuilder.increment(column, counter) + return this + } + + /** + * Perform update by decrementing value for a given column. Decrements + * can be clubbed with `update` as well + */ + public decrement (column: any, counter?: any): this { + this.$knexBuilder.decrement(column, counter) + return this + } + /** * Perform update */ diff --git a/src/Orm/QueryBuilder/index.ts b/src/Orm/QueryBuilder/index.ts index 3ce3bede..af2c08b1 100644 --- a/src/Orm/QueryBuilder/index.ts +++ b/src/Orm/QueryBuilder/index.ts @@ -35,6 +35,13 @@ import { Executable, ExecutableConstructor } from '../../Traits/Executable' @trait(Executable) export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderContract { + /** + * A flag to know, if the query being executed is a select query + * or not, since we don't transform return types of non-select + * queries + */ + private _isSelectQuery: boolean = true + /** * Sideloaded attributes that will be passed to the model instances */ @@ -67,6 +74,16 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon builder.table(model.$table) } + /** + * 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') + } + } + /** * Process preloads for a single model instance */ @@ -85,11 +102,24 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon return modelInstances } + /** + * Checks to see that the executed query is update or delete + */ + public async beforeExecute () { + if (['update', 'del'].includes(this.$knexBuilder['_method'])) { + this._isSelectQuery = false + } + } + /** * Wraps the query result to model instances. This method is invoked by the * Executable trait. */ public async afterExecute (rows: any[]): Promise { + if (!this._isSelectQuery) { + return Array.isArray(rows) ? rows : [rows] + } + const modelInstances = this.model.$createMultipleFromAdapterResult( rows, this.$sideloaded, @@ -149,6 +179,14 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon * to running the query */ public getQueryClient () { + /** + * Use write client for updates and deletes + */ + if (['update', 'del'].includes(this.$knexBuilder['_method'])) { + this._ensureCanPerformWrites() + return this.client!.getWriteClient().client + } + return this.client!.getReadClient().client } @@ -166,4 +204,41 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon model: this.model.name, })) } + /** + * Perform update by incrementing value for a given column. Increments + * can be clubbed with `update` as well + */ + public increment (column: any, counter?: any): any { + this._ensureCanPerformWrites() + this.$knexBuilder.increment(column, counter) + return this + } + + /** + * Perform update by decrementing value for a given column. Decrements + * can be clubbed with `update` as well + */ + public decrement (column: any, counter?: any): any { + this._ensureCanPerformWrites() + this.$knexBuilder.decrement(column, counter) + return this + } + + /** + * Perform update + */ + public update (columns: any): any { + this._ensureCanPerformWrites() + this.$knexBuilder.update(columns) + return this + } + + /** + * Delete rows under the current query + */ + public del (): any { + this._ensureCanPerformWrites() + this.$knexBuilder.del() + return this + } } diff --git a/src/Orm/Relations/Base/QueryBuilder.ts b/src/Orm/Relations/Base/QueryBuilder.ts index 363e987c..203c70f2 100644 --- a/src/Orm/Relations/Base/QueryBuilder.ts +++ b/src/Orm/Relations/Base/QueryBuilder.ts @@ -59,7 +59,7 @@ export abstract class BaseRelationQueryBuilder /** * Adds neccessary where clause to the query to perform the select */ - public beforeExecute () { + public async beforeExecute () { this.applyConstraints() } diff --git a/test-helpers/index.ts b/test-helpers/index.ts index 476bf658..64750798 100644 --- a/test-helpers/index.ts +++ b/test-helpers/index.ts @@ -109,6 +109,7 @@ export async function setup () { table.integer('country_id') table.string('username').unique() table.string('email') + table.integer('points').defaultTo(0) table.timestamps() }) } diff --git a/test-helpers/tmp/db.sqlite b/test-helpers/tmp/db.sqlite deleted file mode 100644 index a1ace7a06fd368003f9bcf10543f22b86ea2f0ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45056 zcmeI*%Wu;#9KdmVPr3;TLMTEAVI(A0v2oxbE*yAO0)Y@u2~KTj4TPmhNm4{li1Od? zr*P#6{{UxB+_2qdUBWn!O%s*Ao!lmN9sBoj9`c&1k00I(GOb?5Nzc#JMR8b2DXu9c zgp7m`t0pa)R5R(8Ned?B51B9g{nc#a{3Ug;z9HnHZ{pCm+L7|#-|JF>00IagfB*sr z?2|x!WATf3`m`Lq%KR6h4!SzZf-KPKbZOz{qvrJ|P4(pZjeAYio))y#nptbBAjbZa>&87M4YB88R zyHY5FR41i4x^1yHY&;IL%7&yokHQ zT@J_m8_)CP==pd!OcSYXC#@Ab?(!=2dWTUkdvI9Vi>mV1H|9Ruwmf#gkj4z zIbAwXjAUA{SK*AeSUh0dj@t=z)YaU<8pLy8ei%fOqX|vE3FjMp9gjI;gBc1 zQ>WzU?Br+|L|J0aR-JgVuvF~yR{kFK`yUTpGZPH2c=F@XAnNM(Zntqej|FKBqTua7 zyUfK&ub~2s6Xy!CE)S!3`-}St(eN!0p0V1 za95|JgZ8p|Scv-1eY(EV3<3xsfB*srAbU&HsPY zf7HM4NAt9S00IagfB*srAb0tg_000IagfB*srATVPB%>QR>!00Xl2q1s}0tg_0 z00IagfB*s&1oHWR{bz;BR6zg%1Q0*~0R#|0009ILKmdXNRbW+`U-|QdxNU#`?>F{r Bml6N~ diff --git a/test/orm/model-query-builder.spec.ts b/test/orm/model-query-builder.spec.ts index 4befe456..aa9fc41f 100644 --- a/test/orm/model-query-builder.spec.ts +++ b/test/orm/model-query-builder.spec.ts @@ -146,4 +146,84 @@ test.group('Model query builder', (group) => { assert.deepEqual(users[0].$attributes, { id: 1, username: 'virk' }) assert.deepEqual(users[0].$options!.profiler, profiler) }) + + test('perform update using model query builder', async (assert) => { + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + User.$boot() + await db.insertQuery().table('users').insert([{ username: 'virk' }, { username: 'nikk' }]) + + const rows = await User.query().where('username', 'virk').update({ username: 'hvirk' }) + assert.lengthOf(rows, 1) + assert.deepEqual(rows, [1]) + + const user = await db.from('users').where('username', 'hvirk').first() + assert.equal(user!.username, 'hvirk') + }) + + test('perform increment using model query builder', async (assert) => { + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + User.$boot() + await db.insertQuery().table('users').insert([{ username: 'virk', points: 1 }]) + + const rows = await User.query().where('username', 'virk').increment('points', 1) + assert.lengthOf(rows, 1) + assert.deepEqual(rows, [1]) + + const user = await db.from('users').where('username', 'virk').first() + assert.equal(user!.points, 2) + }) + + test('perform decrement using model query builder', async (assert) => { + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + User.$boot() + await db.insertQuery().table('users').insert([{ username: 'virk', points: 3 }]) + + const rows = await User.query().where('username', 'virk').decrement('points', 1) + assert.lengthOf(rows, 1) + assert.deepEqual(rows, [1]) + + const user = await db.from('users').where('username', 'virk').first() + assert.equal(user!.points, 2) + }) + + test('delete in bulk', async (assert) => { + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + User.$boot() + await db.insertQuery().table('users').insert([{ username: 'virk' }, { username: 'nikk' }]) + + const rows = await User.query().where('username', 'virk').del() + assert.lengthOf(rows, 1) + assert.deepEqual(rows, [1]) + + const user = await db.from('users').where('username', 'virk').first() + assert.isNull(user) + }) })