From 6b67cad88e4e4a3fedc06b7515de0d5457e7da07 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 6 May 2021 13:29:20 +0530 Subject: [PATCH] feat: add support for serializing models directly from paginator --- adonis-typings/model.ts | 24 +++++++- adonis-typings/orm.ts | 21 ++++++- example/index.ts | 1 - providers/DatabaseProvider.ts | 2 + src/Orm/Paginator/index.ts | 29 +++++++++ src/Orm/QueryBuilder/index.ts | 26 ++++++-- test/database-provider.spec.ts | 8 ++- test/orm/base-model.spec.ts | 108 +++++++++++++++++++++++++++++++++ 8 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 src/Orm/Paginator/index.ts diff --git a/adonis-typings/model.ts b/adonis-typings/model.ts index 928c526c..cbf3caf4 100644 --- a/adonis-typings/model.ts +++ b/adonis-typings/model.ts @@ -307,6 +307,15 @@ declare module '@ioc:Adonis/Lucid/Model' { (callback: (preloader: PreloaderContract) => void): Promise } + /** + * An extension of the simple paginator with support for serializing models + */ + export interface ModelPaginatorContract + extends Omit, 'toJSON'> { + serialize(cherryPick?: CherryPick): { meta: any; data: ModelObject[] } + toJSON(): { meta: any; data: ModelObject[] } + } + /** * ------------------------------------------------------ * Model Query Builder @@ -382,10 +391,21 @@ declare module '@ioc:Adonis/Lucid/Model' { del(): ModelQueryBuilderContract delete(): ModelQueryBuilderContract + /** + * A shorthand to define limit and offset based upon the + * current page + */ + forPage(page: number, perPage?: number): this + /** * Execute query with pagination */ - paginate(page: number, perPage?: number): Promise> + paginate( + page: number, + perPage?: number + ): Promise< + Result extends LucidRow ? ModelPaginatorContract : SimplePaginatorContract + > /** * Mutations (update and increment can be one query aswell) @@ -816,7 +836,7 @@ declare module '@ioc:Adonis/Lucid/Model' { after( this: Model, event: 'paginate', - handler: HooksHandler>, 'paginate'> + handler: HooksHandler>, 'paginate'> ): void after( this: Model, diff --git a/adonis-typings/orm.ts b/adonis-typings/orm.ts index f1444a93..2894a18d 100644 --- a/adonis-typings/orm.ts +++ b/adonis-typings/orm.ts @@ -10,14 +10,18 @@ declare module '@ioc:Adonis/Lucid/Orm' { import { ScopeFn, + LucidRow, LucidModel, HooksDecorator, ColumnDecorator, ComputedDecorator, DateColumnDecorator, + ModelPaginatorContract, DateTimeColumnDecorator, } from '@ioc:Adonis/Lucid/Model' + import { SimplePaginatorMetaKeys } from '@ioc:Adonis/Lucid/Database' + import { HasOneDecorator, HasManyDecorator, @@ -33,7 +37,11 @@ declare module '@ioc:Adonis/Lucid/Orm' { ManyToMany, HasManyThrough, } from '@ioc:Adonis/Lucid/Relations' - export { NamingStrategyContract, ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Model' + export { + NamingStrategyContract, + ModelQueryBuilderContract, + ModelPaginatorContract, + } from '@ioc:Adonis/Lucid/Model' export const scope: ScopeFn export const BaseModel: LucidModel @@ -64,6 +72,17 @@ declare module '@ioc:Adonis/Lucid/Orm' { export const afterFetch: HooksDecorator export const beforePaginate: HooksDecorator export const afterPaginate: HooksDecorator + export const ModelPaginator: { + namingStrategy: { + paginationMetaKeys(): SimplePaginatorMetaKeys + } + new ( + rows: Row[], + total: number, + perPage: number, + currentPage: number + ): ModelPaginatorContract + } /** * Columns and computed diff --git a/example/index.ts b/example/index.ts index 79344c2d..aad63a74 100644 --- a/example/index.ts +++ b/example/index.ts @@ -39,7 +39,6 @@ export class User extends BaseModel { } User.query().apply((scopes) => scopes.active().country('India')) - User.create({ id: '1', username: 'a' }) User.fetchOrCreateMany('id', [{ id: '1', username: 'virk' }]) User.create({ id: '1', username: 'virk' }) diff --git a/providers/DatabaseProvider.ts b/providers/DatabaseProvider.ts index 535ce1d7..58680c40 100644 --- a/providers/DatabaseProvider.ts +++ b/providers/DatabaseProvider.ts @@ -40,6 +40,7 @@ export default class DatabaseServiceProvider { const { scope } = require('../src/Helpers/scope') const decorators = require('../src/Orm/Decorators') const { BaseModel } = require('../src/Orm/BaseModel') + const { ModelPaginator } = require('../src/Orm/Paginator') /** * Attaching adapter to the base model. Each model is allowed to define @@ -50,6 +51,7 @@ export default class DatabaseServiceProvider { return { BaseModel, + ModelPaginator, scope, ...decorators, } diff --git a/src/Orm/Paginator/index.ts b/src/Orm/Paginator/index.ts new file mode 100644 index 00000000..66bec62d --- /dev/null +++ b/src/Orm/Paginator/index.ts @@ -0,0 +1,29 @@ +/* + * @adonisjs/lucid + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// + +import { ModelPaginatorContract, CherryPick } from '@ioc:Adonis/Lucid/Model' +import { SimplePaginator } from '../../Database/Paginator/SimplePaginator' + +/** + * Model paginator extends the simple paginator and adds support for + * serializing models as well + */ +export class ModelPaginator extends SimplePaginator implements ModelPaginatorContract { + /** + * Serialize models + */ + public serialize(cherryPick?: CherryPick) { + return { + meta: this.getMeta(), + data: this.all().map((row) => row.serialize(cherryPick)), + } + } +} diff --git a/src/Orm/QueryBuilder/index.ts b/src/Orm/QueryBuilder/index.ts index 825d1ad9..bde97a7d 100644 --- a/src/Orm/QueryBuilder/index.ts +++ b/src/Orm/QueryBuilder/index.ts @@ -35,6 +35,7 @@ import { import { isObject } from '../../utils' import { Preloader } from '../Preloader' +import { ModelPaginator } from '../Paginator' import { QueryRunner } from '../../QueryRunner' import { Chainable } from '../../Database/QueryBuilder/Chainable' import { SimplePaginator } from '../../Database/Paginator/SimplePaginator' @@ -714,7 +715,9 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon /** * Paginate through rows inside a given table */ - public async paginate(page: number, perPage: number = 20) { + public async paginate(page: number, perPage: number = 20): Promise { + const isFetchCall = this.wrapResultsToModelInstances && this.knexQuery['_method'] === 'select' + /** * Cast to number */ @@ -733,18 +736,29 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon * We pass both the counts query and the main query to the * paginate hook */ - await this.model.$hooks.exec('before', 'paginate', [countQuery, this]) - await this.model.$hooks.exec('before', 'fetch', this) + if (isFetchCall) { + await this.model.$hooks.exec('before', 'paginate', [countQuery, this]) + await this.model.$hooks.exec('before', 'fetch', this) + } const aggregateResult = await countQuery.exec() const total = this.hasGroupBy ? aggregateResult.length : aggregateResult[0].total const results = total > 0 ? await this.forPage(page, perPage).execQuery() : [] - const paginator = new SimplePaginator(results, total, perPage, page) + + /** + * Choose paginator + */ + const paginator = this.wrapResultsToModelInstances + ? new ModelPaginator(results, total, perPage, page) + : new SimplePaginator(results, total, perPage, page) + paginator.namingStrategy = this.model.namingStrategy - await this.model.$hooks.exec('after', 'paginate', paginator) - await this.model.$hooks.exec('after', 'fetch', results) + if (isFetchCall) { + await this.model.$hooks.exec('after', 'paginate', paginator) + await this.model.$hooks.exec('after', 'fetch', results) + } return paginator } diff --git a/test/database-provider.spec.ts b/test/database-provider.spec.ts index a9e222af..d8cd9c72 100644 --- a/test/database-provider.spec.ts +++ b/test/database-provider.spec.ts @@ -14,6 +14,7 @@ import { scope } from '../src/Helpers/scope' import { BaseSeeder } from '../src/BaseSeeder' import { FactoryManager } from '../src/Factory' import { BaseModel } from '../src/Orm/BaseModel' +import { ModelPaginator } from '../src/Orm/Paginator' import * as decorators from '../src/Orm/Decorators' import { setupApplication, fs } from '../test-helpers' @@ -35,7 +36,12 @@ test.group('Database Provider', (group) => { ) assert.instanceOf(app.container.use('Adonis/Lucid/Database'), Database) - assert.deepEqual(app.container.use('Adonis/Lucid/Orm'), { BaseModel, scope, ...decorators }) + assert.deepEqual(app.container.use('Adonis/Lucid/Orm'), { + BaseModel, + ModelPaginator, + scope, + ...decorators, + }) assert.isTrue(app.container.hasBinding('Adonis/Lucid/Schema')) assert.instanceOf(app.container.use('Adonis/Lucid/Factory'), FactoryManager) assert.deepEqual(app.container.use('Adonis/Lucid/Seeder'), BaseSeeder) diff --git a/test/orm/base-model.spec.ts b/test/orm/base-model.spec.ts index a14b8e73..e762e1f4 100644 --- a/test/orm/base-model.spec.ts +++ b/test/orm/base-model.spec.ts @@ -51,6 +51,7 @@ import { getBaseModel, setupApplication, } from '../../test-helpers' +import { ModelPaginator } from '../../src/Orm/Paginator' import { SimplePaginator } from '../../src/Database/Paginator/SimplePaginator' import { SnakeCaseNamingStrategy } from '../../src/Orm/NamingStrategies/SnakeCase' @@ -4336,6 +4337,32 @@ test.group('Base Model | hooks', (group) => { await db.insertQuery().table('users').insert({ username: 'virk' }) await User.query().paginate(1) }) + + test('do not invoke before and after paginate hooks when using pojo', async () => { + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + + @beforePaginate() + public static beforePaginateHook() { + throw new Error('Never expected to reached here') + } + + @afterPaginate() + public static afterPaginateHook() { + throw new Error('Never expected to reached here') + } + } + + await db.insertQuery().table('users').insert({ username: 'virk' }) + await User.query().pojo().paginate(1) + }) }) test.group('Base model | extend', (group) => { @@ -5312,6 +5339,8 @@ test.group('Base Model | paginate', (group) => { const users = await User.query().paginate(1, 5) users.baseUrl('/users') + assert.instanceOf(users, ModelPaginator) + assert.lengthOf(users.all(), 5) assert.instanceOf(users.all()[0], User) assert.equal(users.perPage, 5) @@ -5335,6 +5364,85 @@ test.group('Base Model | paginate', (group) => { }) }) + test('serialize from model paginator', async (assert) => { + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + await db.insertQuery().table('users').multiInsert(getUsers(18)) + const users = await User.query().paginate(1, 5) + users.baseUrl('/users') + + assert.instanceOf(users, ModelPaginator) + const { meta, data } = users.serialize({ + fields: ['username'], + }) + + data.forEach((row) => { + assert.notProperty(row, 'email') + assert.notProperty(row, 'id') + }) + assert.deepEqual(meta, { + total: 18, + per_page: 5, + current_page: 1, + last_page: 4, + first_page: 1, + first_page_url: '/users?page=1', + last_page_url: '/users?page=4', + next_page_url: '/users?page=2', + previous_page_url: null, + }) + }) + + test('return simple paginator instance when using pojo', async (assert) => { + class User extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + await db.insertQuery().table('users').multiInsert(getUsers(18)) + const users = await User.query().pojo().paginate(1, 5) + users.baseUrl('/users') + + assert.instanceOf(users, SimplePaginator) + + assert.lengthOf(users.all(), 5) + assert.notInstanceOf(users.all()[0], User) + assert.equal(users.perPage, 5) + assert.equal(users.currentPage, 1) + assert.equal(users.lastPage, 4) + assert.isTrue(users.hasPages) + assert.isTrue(users.hasMorePages) + assert.isFalse(users.isEmpty) + assert.equal(users.total, 18) + assert.isTrue(users.hasTotal) + assert.deepEqual(users.getMeta(), { + total: 18, + per_page: 5, + current_page: 1, + last_page: 4, + first_page: 1, + first_page_url: '/users?page=1', + last_page_url: '/users?page=4', + next_page_url: '/users?page=2', + previous_page_url: null, + }) + }) + test('use model naming strategy for pagination properties', async (assert) => { class User extends BaseModel { @column({ isPrimary: true })