diff --git a/adonis-typings/model.ts b/adonis-typings/model.ts index 601da8ff..90c2090f 100644 --- a/adonis-typings/model.ts +++ b/adonis-typings/model.ts @@ -72,6 +72,13 @@ declare module '@ioc:Adonis/Lucid/Model' { profiler?: ProfilerContract | ProfilerRowContract, } + /** + * Adapter and also accept a client directly + */ + export type ModelAdapterOptions = ModelOptions & { + client?: QueryClientContract, + } + type DecoratorFn = (target, property) => void /** @@ -116,10 +123,7 @@ declare module '@ioc:Adonis/Lucid/Model' { > extends ChainableContract { model: Model - /** - * A copy of options based to the query builder - */ - options?: ModelOptions + client: QueryClientContract, /** * A custom set of sideloaded properties defined on the query @@ -189,6 +193,12 @@ declare module '@ioc:Adonis/Lucid/Model' { client: QueryClientContract, ): ReturnType | ReturnType + /** + * Read/write options + */ + $setOptions (options?: ModelAdapterOptions): void + $getOptions (): ModelOptions | undefined + /** * Read/write attributes */ @@ -218,8 +228,7 @@ declare module '@ioc:Adonis/Lucid/Model' { /** * Shape of the model static properties. The `$` prefix is to denote - * special properties from the base model with exception to `create` - * `findAll`, `findAll`. + * special properties from the base model */ export interface ModelConstructorContract { /** @@ -285,7 +294,7 @@ declare module '@ioc:Adonis/Lucid/Model' { this: T, result?: ModelObject, sideloadAttributes?: ModelObject, - options?: any, + options?: ModelOptions, ): null | InstanceType /** @@ -296,7 +305,7 @@ declare module '@ioc:Adonis/Lucid/Model' { this: T, results: ModelObject[], sideloadAttributes?: ModelObject, - options?: any, + options?: ModelOptions, ): InstanceType[] /** @@ -326,7 +335,7 @@ declare module '@ioc:Adonis/Lucid/Model' { create ( this: T, values: ModelObject, - options?: ModelOptions, + options?: ModelAdapterOptions, ): InstanceType /** @@ -335,7 +344,7 @@ declare module '@ioc:Adonis/Lucid/Model' { find ( this: T, value: any, - options?: ModelOptions, + options?: ModelAdapterOptions, ): Promise> /** @@ -344,7 +353,7 @@ declare module '@ioc:Adonis/Lucid/Model' { findOrFail ( this: T, value: any, - options?: ModelOptions, + options?: ModelAdapterOptions, ): Promise> /** @@ -353,7 +362,7 @@ declare module '@ioc:Adonis/Lucid/Model' { findMany ( this: T, value: any[], - options?: ModelOptions, + options?: ModelAdapterOptions, ): Promise[]> /** @@ -363,7 +372,7 @@ declare module '@ioc:Adonis/Lucid/Model' { this: T, search: any, savePayload?: any, - options?: ModelOptions, + options?: ModelAdapterOptions, ): Promise> /** @@ -374,13 +383,16 @@ declare module '@ioc:Adonis/Lucid/Model' { this: T, search: any, savePayload?: any, - options?: ModelOptions, + options?: ModelAdapterOptions, ): Promise> /** * Fetch all rows */ - all (this: T, options?: ModelOptions): Promise[]> + all ( + this: T, + options?: ModelAdapterOptions, + ): Promise[]> /** * Returns the query for fetching a model instance @@ -389,7 +401,7 @@ declare module '@ioc:Adonis/Lucid/Model' { Model extends ModelConstructorContract, > ( this: Model, - options?: ModelOptions, + options?: ModelAdapterOptions, ): ModelQueryBuilderContract & ExcutableQueryBuilderContract[]> new (): ModelContract @@ -419,7 +431,7 @@ declare module '@ioc:Adonis/Lucid/Model' { */ query ( modelConstructor: ModelConstructorContract, - options?: ModelOptions, + options?: ModelAdapterOptions, ): ModelQueryBuilderContract & ExcutableQueryBuilderContract } } diff --git a/adonis-typings/querybuilder.ts b/adonis-typings/querybuilder.ts index d875287d..1f1f6620 100644 --- a/adonis-typings/querybuilder.ts +++ b/adonis-typings/querybuilder.ts @@ -11,6 +11,7 @@ declare module '@ioc:Adonis/Lucid/DatabaseQueryBuilder' { import * as knex from 'knex' import { Dictionary } from 'ts-essentials' import { ProfilerRowContract, ProfilerContract } from '@ioc:Adonis/Core/Profiler' + import { QueryClientContract } from '@ioc:Adonis/Lucid/Database' /** * Get one or many of a generic @@ -817,6 +818,7 @@ declare module '@ioc:Adonis/Lucid/DatabaseQueryBuilder' { * other queries */ interface RawContract { + client: QueryClientContract, wrap (before: string, after: string): this } @@ -830,6 +832,8 @@ declare module '@ioc:Adonis/Lucid/DatabaseQueryBuilder' { > extends ChainableContract { del (): this + client: QueryClientContract, + /** * Clone current query */ @@ -870,6 +874,8 @@ declare module '@ioc:Adonis/Lucid/DatabaseQueryBuilder' { Record extends Dictionary = Dictionary, ReturnColumns extends any = any[] > { + client: QueryClientContract, + /** * Table for the insert query */ diff --git a/src/Orm/Adapter/index.ts b/src/Orm/Adapter/index.ts index 3cd0cefe..a0943887 100644 --- a/src/Orm/Adapter/index.ts +++ b/src/Orm/Adapter/index.ts @@ -11,9 +11,9 @@ import { DatabaseContract } from '@ioc:Adonis/Lucid/Database' import { - ModelOptions, ModelContract, AdapterContract, + ModelAdapterOptions, ModelConstructorContract, } from '@ioc:Adonis/Lucid/Model' @@ -28,7 +28,11 @@ export class Adapter implements AdapterContract { /** * Returns the query client based upon the model instance */ - private _getModelClient (modelConstructor: ModelConstructorContract, options?: ModelOptions) { + private _getModelClient (modelConstructor: ModelConstructorContract, options?: ModelAdapterOptions) { + if (options && options.client) { + return options.client + } + const connection = options && options.connection || modelConstructor.$connection const profiler = options && options.profiler return this._db.connection(connection, { profiler }) @@ -37,7 +41,7 @@ export class Adapter implements AdapterContract { /** * Returns the model query builder instance for a given model */ - public query (modelConstructor: ModelConstructorContract, options?: ModelOptions): any { + public query (modelConstructor: ModelConstructorContract, options?: ModelAdapterOptions): any { const client = this._getModelClient(modelConstructor, options) return client.modelQuery(modelConstructor) } diff --git a/src/Orm/BaseModel/index.ts b/src/Orm/BaseModel/index.ts index b4cbc665..a9ea1817 100644 --- a/src/Orm/BaseModel/index.ts +++ b/src/Orm/BaseModel/index.ts @@ -25,6 +25,7 @@ import { BaseRelationNode, RelationContract, AvailableRelations, + ModelAdapterOptions, ThroughRelationNode, ModelConstructorContract, } from '@ioc:Adonis/Lucid/Model' @@ -131,13 +132,10 @@ export class BaseModel implements ModelContract { const instance = new this() instance.$consumeAdapterResult(adapterResult, sideloadAttributes) instance.$hydrateOriginals() + instance.$setOptions(options) instance.$persisted = true instance.$isLocal = false - if (options) { - instance.$options = options - } - return instance } @@ -152,7 +150,7 @@ export class BaseModel implements ModelContract { this: T, adapterResults: ModelObject[], sideloadAttributes?: ModelObject, - options?: any, + options?: ModelOptions, ): InstanceType[] { if (!Array.isArray(adapterResults)) { return [] @@ -302,7 +300,7 @@ export class BaseModel implements ModelContract { public static async find ( this: T, value: any, - options?: any, + options?: ModelAdapterOptions, ) { return this.query(options).where(this.$primaryKey, value).first() } @@ -313,7 +311,7 @@ export class BaseModel implements ModelContract { public static async findOrFail ( this: T, value: any, - options?: any, + options?: ModelAdapterOptions, ) { return this.query(options).where(this.$primaryKey, value).firstOrFail() } @@ -324,9 +322,13 @@ export class BaseModel implements ModelContract { public static async findMany ( this: T, value: any[], - options?: any, + options?: ModelAdapterOptions, ) { - return this.query(options).whereIn(this.$primaryKey, value).exec() + return this + .query(options) + .whereIn(this.$primaryKey, value) + .orderBy(this.$primaryKey, 'desc') + .exec() } /** @@ -336,7 +338,7 @@ export class BaseModel implements ModelContract { this: T, search: any, savePayload?: any, - options?: ModelOptions, + options?: ModelAdapterOptions, ) { const row = await this.firstOrNew(search, savePayload, options) if (!row.$persisted) { @@ -353,13 +355,14 @@ export class BaseModel implements ModelContract { this: T, search: any, savePayload?: any, - options?: ModelOptions, + options?: ModelAdapterOptions, ) { - let row = await this.query(options).where(search).first() + const query = this.query(options) + let row = await query.where(search).first() if (!row) { row = new this() as InstanceType - row.$options = options + row.$setOptions({ client: query.client }) row.fill(Object.assign({}, search, savePayload)) return row } @@ -371,9 +374,9 @@ export class BaseModel implements ModelContract { */ public static async all ( this: T, - options?: any, + options?: ModelAdapterOptions, ) { - return this.query(options).exec() + return this.query(options).orderBy(this.$primaryKey, 'desc').exec() } constructor () { @@ -552,6 +555,38 @@ export class BaseModel implements ModelContract { return Object.keys(this.$dirty).length > 0 } + /** + * Sets the options on the model instance + */ + public $setOptions (options?: ModelAdapterOptions) { + if (!options) { + return + } + + this.$options = this.$options || {} + + if (options.client) { + this.$options.connection = options.client.connectionName + this.$options.profiler = options.client.profiler + return + } + + if (options.connection) { + this.$options.connection = options.connection + } + + if (options.profiler) { + this.$options.profiler = options.profiler + } + } + + /** + * Returns $options + */ + public $getOptions (): ModelOptions | undefined { + return this.$options + } + /** * Set attribute */ diff --git a/src/Orm/QueryBuilder/index.ts b/src/Orm/QueryBuilder/index.ts index 86e7ae39..b8691eaf 100644 --- a/src/Orm/QueryBuilder/index.ts +++ b/src/Orm/QueryBuilder/index.ts @@ -49,17 +49,25 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon }, } = {} + /** + * Options that must be passed to all new model instances + */ + private _options: ModelOptions = { + connection: this.client.connectionName, + profiler: this.client.profiler, + } + constructor ( builder: knex.QueryBuilder, public model: ModelConstructorContract, public client: QueryClientContract, - public options?: ModelOptions, ) { super(builder, (userFn) => { return (builder) => { - userFn(new ModelQueryBuilder(builder, this.model, this.client, this.options)) + userFn(new ModelQueryBuilder(builder, this.model, this.client)) } }) + builder.table(model.$table) } @@ -92,7 +100,7 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon */ private async _processRelation (models: ModelContract[], name: string) { const relation = this._preloads[name] - const query = relation.relation.getEagerQuery(models, this.options) + const query = relation.relation.getEagerQuery(models, this._options) /** * Pass nested preloads @@ -125,7 +133,7 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon const modelInstances = this.model.$createMultipleFromAdapterResult( rows, this._sideloaded, - this.options, + this._options, ) await Promise.all(Object.keys(this._preloads).map((name) => { @@ -166,7 +174,7 @@ export class ModelQueryBuilder extends Chainable implements ModelQueryBuilderCon public async firstOrFail (): Promise { const result = await this.limit(1)['exec']() if (!result.length) { - throw new Error('Row not found') + throw new Exception('Row not found', 404, 'E_ROW_NOT_FOUND') } return result[0] diff --git a/src/QueryClient/index.ts b/src/QueryClient/index.ts index 420691ed..541cb375 100644 --- a/src/QueryClient/index.ts +++ b/src/QueryClient/index.ts @@ -132,10 +132,7 @@ export class QueryClient implements QueryClientContract { * using the same options */ public modelQuery (model: any): any { - return new ModelQueryBuilder(this.knexQuery(), model, this, { - connection: this.connectionName, - profiler: this.profiler, - }) + return new ModelQueryBuilder(this.knexQuery(), model, this) } /** diff --git a/src/TransactionClient/index.ts b/src/TransactionClient/index.ts index 9edaff39..a2722749 100644 --- a/src/TransactionClient/index.ts +++ b/src/TransactionClient/index.ts @@ -99,10 +99,7 @@ export class TransactionClient implements TransactionClientContract { * using the same options */ public modelQuery (model: any): any { - return new ModelQueryBuilder(this.knexQuery(), model, this, { - connection: this.connectionName, - profiler: this.profiler, - }) + return new ModelQueryBuilder(this.knexQuery(), model, this) } /** diff --git a/test-helpers/index.ts b/test-helpers/index.ts index b3a25ca1..1e30bdbc 100644 --- a/test-helpers/index.ts +++ b/test-helpers/index.ts @@ -17,6 +17,7 @@ import { Profiler } from '@adonisjs/profiler/build/standalone' import { Filesystem } from '@poppinss/dev-utils' import { + DatabaseContract, ConnectionContract, QueryClientContract, ConnectionConfigContract, @@ -98,7 +99,8 @@ export async function setup () { if (!hasUsersTable) { await db.schema.createTable('users', (table) => { table.increments() - table.string('username') + table.string('username').unique() + table.string('email') table.timestamps() }) } @@ -223,7 +225,7 @@ export function getDb () { }, } - return new Database(config, getLogger(), getProfiler()) + return new Database(config, getLogger(), getProfiler()) as DatabaseContract } /** diff --git a/test/orm/base-model-options.spec.ts b/test/orm/base-model-options.spec.ts new file mode 100644 index 00000000..5d562344 --- /dev/null +++ b/test/orm/base-model-options.spec.ts @@ -0,0 +1,572 @@ +/* +* @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 test from 'japa' +import { Profiler } from '@adonisjs/profiler/build/standalone' + +import { column } from '../../src/Orm/Decorators' +import { setup, cleanup, getDb, resetTables, getBaseModel, ormAdapter } from '../../test-helpers' + +test.group('Model options | QueryBuilder', (group) => { + group.before(async () => { + await setup() + }) + + group.after(async () => { + await cleanup() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('query builder set model options from the query client', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const users = await User.query().exec() + assert.lengthOf(users, 1) + + assert.equal(users[0].$options!.connection, 'primary') + assert.instanceOf(users[0].$options!.profiler, Profiler) + }) + + test('query builder set model options when only one row is fetched', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const user = await User.query().first() + + assert.equal(user!.$options!.connection, 'primary') + assert.instanceOf(user!.$options!.profiler, Profiler) + }) +}) + +test.group('Model options | Adapter', (group) => { + group.before(async () => { + await setup() + }) + + group.after(async () => { + await cleanup() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('use correct client when custom connection is defined', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const user = await User.query({ connection: 'secondary' }).first() + assert.equal(user!.$options!.connection, 'secondary') + assert.instanceOf(user!.$options!.profiler, Profiler) + }) + + test('pass profiler to the client when defined explicitly', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const profiler = new Profiler({}) + + const user = await User.query({ profiler }).first() + assert.equal(user!.$options!.connection, 'primary') + assert.deepEqual(user!.$options!.profiler, profiler) + }) + + test('pass custom client to query builder', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const client = db.connection() + + const user = await User.query({ client }).first() + assert.equal(user!.$options!.connection, 'primary') + }) + + test('pass transaction client to query builder', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const trx = await db.connection('secondary').transaction() + const user = await User.query({ client: trx }).first() + await trx.rollback() + + assert.equal(user!.$options!.connection, 'secondary') + }) +}) + +test.group('Model options | Model.find', (group) => { + group.before(async () => { + await setup() + }) + + group.after(async () => { + await cleanup() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('define custom connection', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const user = await User.find(1, { connection: 'secondary' }) + assert.equal(user!.$options!.connection, 'secondary') + assert.instanceOf(user!.$options!.profiler, Profiler) + }) + + test('define custom profiler', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const profiler = new Profiler({}) + + const user = await User.find(1, { profiler }) + assert.deepEqual(user!.$options!.profiler, profiler) + }) + + test('define custom query client', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const client = db.connection() + + const user = await User.find(1, { client }) + assert.deepEqual(user!.$options!.profiler, client.profiler) + assert.deepEqual(user!.$options!.connection, client.connectionName) + }) +}) + +test.group('Model options | Model.findOrFail', (group) => { + group.before(async () => { + await setup() + }) + + group.after(async () => { + await cleanup() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('define custom connection', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const user = await User.findOrFail(1, { connection: 'secondary' }) + assert.equal(user.$options!.connection, 'secondary') + assert.instanceOf(user.$options!.profiler, Profiler) + }) + + test('define custom profiler', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const profiler = new Profiler({}) + + const user = await User.findOrFail(1, { profiler }) + assert.deepEqual(user.$options!.profiler, profiler) + }) + + test('define custom query client', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const client = db.connection('secondary') + + const user = await User.findOrFail(1, { client }) + assert.deepEqual(user.$options!.profiler, client.profiler) + assert.deepEqual(user.$options!.connection, client.connectionName) + }) +}) + +test.group('Model options | Model.findMany', (group) => { + group.before(async () => { + await setup() + }) + + group.after(async () => { + await cleanup() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('define custom connection', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const users = await User.findMany([1], { connection: 'secondary' }) + assert.equal(users[0].$options!.connection, 'secondary') + assert.instanceOf(users[0].$options!.profiler, Profiler) + }) + + test('define custom profiler', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const profiler = new Profiler({}) + + const users = await User.findMany([1], { profiler }) + assert.deepEqual(users[0].$options!.profiler, profiler) + }) + + test('define custom query client', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const client = db.connection('secondary') + + const users = await User.findMany([1], { client }) + assert.deepEqual(users[0].$options!.profiler, client.profiler) + assert.deepEqual(users[0].$options!.connection, client.connectionName) + }) +}) + +test.group('Model options | Model.firstOrSave', (group) => { + group.before(async () => { + await setup() + }) + + group.after(async () => { + await cleanup() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('define custom connection', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const user = await User.firstOrSave({ username: 'virk' }, undefined, { connection: 'secondary' }) + const total = await db.from('users').count('*', 'total') + + assert.equal(total[0].total, 1) + assert.equal(user.$options!.connection, 'secondary') + assert.instanceOf(user.$options!.profiler, Profiler) + }) + + test('define custom connection when search fails', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + + const user = await User.firstOrSave({ username: 'nikk' }, undefined, { connection: 'secondary' }) + const total = await db.from('users').count('*', 'total') + + assert.equal(total[0].total, 2) + assert.equal(user.$options!.connection, 'secondary') + assert.instanceOf(user.$options!.profiler, Profiler) + }) + + test('define custom profiler', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const profiler = new Profiler({}) + + const user = await User.firstOrSave({ username: 'virk' }, undefined, { profiler }) + const total = await db.from('users').count('*', 'total') + + assert.equal(total[0].total, 1) + assert.equal(user.$options!.connection, 'primary') + assert.deepEqual(user.$options!.profiler, profiler) + }) + + test('define custom profiler when search fails', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const profiler = new Profiler({}) + + const user = await User.firstOrSave({ username: 'nikk' }, undefined, { profiler }) + const total = await db.from('users').count('*', 'total') + + assert.equal(total[0].total, 2) + assert.deepEqual(user.$options!.profiler, profiler) + }) + + test('define custom client', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const client = db.connection('secondary') + + const user = await User.firstOrSave({ username: 'virk' }, undefined, { client }) + const total = await db.from('users').count('*', 'total') + + assert.equal(total[0].total, 1) + assert.deepEqual(user.$options!.profiler, client.profiler) + assert.deepEqual(user.$options!.connection, client.connectionName) + }) + + test('define custom client when search fails', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + public static $table = 'users' + + @column({ primary: true }) + public id: number + + @column() + public username: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const client = db.connection('secondary') + + const user = await User.firstOrSave({ username: 'nikk' }, undefined, { client }) + const total = await db.from('users').count('*', 'total') + + assert.equal(total[0].total, 2) + assert.deepEqual(user.$options!.profiler, client.profiler) + assert.deepEqual(user.$options!.connection, client.connectionName) + }) +}) diff --git a/test/orm/base-model.spec.ts b/test/orm/base-model.spec.ts index 48fdbfcf..17309fb4 100644 --- a/test/orm/base-model.spec.ts +++ b/test/orm/base-model.spec.ts @@ -11,7 +11,16 @@ import test from 'japa' import { column, computed, hasOne } from '../../src/Orm/Decorators' -import { FakeAdapter, getBaseModel, ormAdapter, mapToObj } from '../../test-helpers' +import { + getDb, + cleanup, + setup, + mapToObj, + ormAdapter, + resetTables, + FakeAdapter, + getBaseModel, +} from '../../test-helpers' test.group('Base model | boot', () => { test('compute table name from model name', async (assert) => { @@ -501,7 +510,7 @@ test.group('Base Model | persist', () => { }) }) -test.group('Base Model | fetch', () => { +test.group('Base Model | create from adapter results', () => { test('create model instance using $createFromAdapterResult method', async (assert) => { const BaseModel = getBaseModel(ormAdapter()) @@ -1183,3 +1192,197 @@ test.group('Base Model | relations', () => { }) }) }) + +test.group('Base Model | fetch', (group) => { + group.before(async () => { + await setup() + }) + + group.after(async () => { + await cleanup() + }) + + group.afterEach(async () => { + await resetTables() + }) + + test('find using the primary key', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const user = await User.find(1) + + assert.instanceOf(user, User) + assert.equal(user!.$primaryKeyValue, 1) + }) + + test('raise exception when row is not found', async (assert) => { + assert.plan(1) + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + try { + await User.findOrFail(1) + } catch ({ message }) { + assert.equal(message, 'E_ROW_NOT_FOUND: Row not found') + } + }) + + test('find many using the primary key', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + const db = getDb() + await db.insertQuery().table('users').multiInsert([ + { username: 'virk' }, + { username: 'nikk' }, + ]) + + const users = await User.findMany([1, 2]) + assert.lengthOf(users, 2) + assert.equal(users[0].$primaryKeyValue, 2) + assert.equal(users[1].$primaryKeyValue, 1) + }) + + test('return the existing row when search criteria matches', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const user = await User.firstOrSave({ username: 'virk' }) + + const totalUsers = await db.query().from('users').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.isTrue(user.$persisted) + assert.instanceOf(user, User) + assert.equal(user!.$primaryKeyValue, 1) + }) + + test('create new row when search criteria doesn\'t match', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const user = await User.firstOrSave({ username: 'nikk' }, { email: 'nikk@gmail.com' }) + + const totalUsers = await db.query().from('users').count('*', 'total') + + assert.equal(totalUsers[0].total, 2) + assert.instanceOf(user, User) + + assert.equal(user!.$primaryKeyValue, 2) + assert.isTrue(user.$persisted) + assert.equal(user!.email, 'nikk@gmail.com') + assert.equal(user!.username, 'nikk') + }) + + test('return the existing row when search criteria matches using firstOrNew', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const user = await User.firstOrNew({ username: 'virk' }) + + const totalUsers = await db.query().from('users').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.instanceOf(user, User) + assert.isTrue(user.$persisted) + assert.equal(user!.$primaryKeyValue, 1) + }) + + test('instantiate new row when search criteria doesn\'t match using firstOrNew', async (assert) => { + const BaseModel = getBaseModel(ormAdapter()) + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @column() + public username: string + + @column() + public email: string + } + + const db = getDb() + await db.insertQuery().table('users').insert({ username: 'virk' }) + const user = await User.firstOrNew({ username: 'nikk' }, { email: 'nikk@gmail.com' }) + + const totalUsers = await db.query().from('users').count('*', 'total') + + assert.equal(totalUsers[0].total, 1) + assert.instanceOf(user, User) + + assert.isUndefined(user!.$primaryKeyValue) + assert.isFalse(user.$persisted) + assert.equal(user!.email, 'nikk@gmail.com') + assert.equal(user!.username, 'nikk') + }) +})