Skip to content

Commit

Permalink
refactor: tighten up preloads api
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 12, 2020
1 parent 1332b97 commit 0171d0b
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 56 deletions.
51 changes: 38 additions & 13 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ declare module '@ioc:Adonis/Lucid/Model' {
*/
export interface BaseRelationNode {
relatedModel: (() => ModelConstructorContract),
localKey: string,
foreignKey: string,
serializeAs: string,
localKey?: string,
foreignKey?: string,
serializeAs?: string,
}

/**
Expand Down Expand Up @@ -84,22 +84,47 @@ declare module '@ioc:Adonis/Lucid/Model' {
}

type DecoratorFn = (target, property) => void
type BaseRelationDecoratorNode = Omit<BaseRelationNode, 'relatedModel'>
type ThroughRelationDecoratorNode = Omit<ThroughRelationNode, 'relatedModel'>
type ModelExecuteableQueryBuilder = ModelQueryBuilderContract<any> & ExcutableQueryBuilderContract<any>

/**
* Types for decorators
*/
export type ColumnFn = (column?: Partial<ColumnNode>) => DecoratorFn
export type ComputedFn = (column?: Partial<ComputedNode>) => DecoratorFn
export type HasOneFn = (column?: Partial<BaseRelationNode>) => DecoratorFn
export type HasManyFn = (column?: Partial<BaseRelationNode>) => DecoratorFn
export type BelongsToFn = (column?: Partial<BaseRelationNode>) => DecoratorFn
export type ManyToManyFn = (column?: Partial<BaseRelationNode>) => DecoratorFn
export type HasOneThroughFn = (column?: Partial<ThroughRelationNode>) => DecoratorFn
export type HasManyThroughFn = (column?: Partial<ThroughRelationNode>) => DecoratorFn

export type AvailableRelations = 'hasOne'
export type HasOneFn = (
model: BaseRelationNode['relatedModel'],
column?: BaseRelationDecoratorNode,
) => DecoratorFn

type ModelExecuteableQueryBuilder = ModelQueryBuilderContract<any> & ExcutableQueryBuilderContract<any>
export type HasManyFn = (
model: BaseRelationNode['relatedModel'],
column?: BaseRelationDecoratorNode,
) => DecoratorFn

export type BelongsToFn = (
model: BaseRelationNode['relatedModel'],
column?: BaseRelationDecoratorNode,
) => DecoratorFn

export type ManyToManyFn = (
model: BaseRelationNode['relatedModel'],
column?: BaseRelationDecoratorNode,
) => DecoratorFn

export type HasOneThroughFn = (
model: BaseRelationNode['relatedModel'],
column?: ThroughRelationDecoratorNode,
) => DecoratorFn

export type HasManyThroughFn = (
model: BaseRelationNode['relatedModel'],
column?: ThroughRelationDecoratorNode,
) => DecoratorFn

export type AvailableRelations = 'hasOne'

/**
* Callback accepted by the preload method
Expand All @@ -113,8 +138,8 @@ declare module '@ioc:Adonis/Lucid/Model' {
type: AvailableRelations
serializeAs: string
relatedModel (): ModelConstructorContract
getQuery (model: ModelContract, options?: ModelOptions): ModelExecuteableQueryBuilder
getEagerQuery (models: ModelContract[], options?: ModelOptions): ModelExecuteableQueryBuilder
getQuery (model: ModelContract, client: QueryClientContract): ModelExecuteableQueryBuilder
getEagerQuery (models: ModelContract[], client: QueryClientContract): ModelExecuteableQueryBuilder
setRelated (model: ModelContract, related?: ModelContract | null): void
setRelatedMany (models: ModelContract[], related: ModelContract[]): void
}
Expand Down
2 changes: 1 addition & 1 deletion src/Orm/BaseModel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export class BaseModel implements ModelContract {
public static $addRelation (
name: string,
type: AvailableRelations,
options: Partial<BaseRelationNode | ThroughRelationNode>,
options: BaseRelationNode | ThroughRelationNode,
) {
switch (type) {
case 'hasOne':
Expand Down
24 changes: 12 additions & 12 deletions src/Orm/Decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,65 +50,65 @@ export const computed: ComputedFn = (column) => {
/**
* Define belongsTo relationship
*/
export const belongsTo: BelongsToFn = (relation?) => {
export const belongsTo: BelongsToFn = (relatedModel, relation?) => {
return function decorateAsRelation (target, property: string) {
const Model = target.constructor as ModelConstructorContract
Model.$boot()
Model.$addRelation(property, 'belongsTo', relation || {})
Model.$addRelation(property, 'belongsTo', Object.assign({ relatedModel }, relation))
}
}

/**
* Define hasOne relationship
*/
export const hasOne: HasOneFn = (relation?) => {
export const hasOne: HasOneFn = (relatedModel, relation?) => {
return function decorateAsRelation (target, property: string) {
const Model = target.constructor as ModelConstructorContract
Model.$boot()
Model.$addRelation(property, 'hasOne', relation || {})
Model.$addRelation(property, 'hasOne', Object.assign({ relatedModel }, relation))
}
}

/**
* Define hasMany relationship
*/
export const hasMany: HasManyFn = (relation?) => {
export const hasMany: HasManyFn = (relatedModel, relation?) => {
return function decorateAsRelation (target, property: string) {
const Model = target.constructor as ModelConstructorContract
Model.$boot()
Model.$addRelation(property, 'hasMany', relation || {})
Model.$addRelation(property, 'hasMany', Object.assign({ relatedModel }, relation))
}
}

/**
* Define manyToMany relationship
*/
export const manyToMany: ManyToManyFn = (relation?) => {
export const manyToMany: ManyToManyFn = (relatedModel, relation?) => {
return function decorateAsRelation (target, property: string) {
const Model = target.constructor as ModelConstructorContract
Model.$boot()
Model.$addRelation(property, 'manyToMany', relation || {})
Model.$addRelation(property, 'manyToMany', Object.assign({ relatedModel }, relation))
}
}

/**
* Define hasOneThrough relationship
*/
export const hasOneThrough: HasOneThroughFn = (relation?) => {
export const hasOneThrough: HasOneThroughFn = (relatedModel, relation?) => {
return function decorateAsRelation (target, property: string) {
const Model = target.constructor as ModelConstructorContract
Model.$boot()
Model.$addRelation(property, 'hasOneThrough', relation || {})
Model.$addRelation(property, 'hasOneThrough', Object.assign({ relatedModel }, relation))
}
}

/**
* Define hasManyThrough relationship
*/
export const hasManyThrough: HasManyThroughFn = (relation?) => {
export const hasManyThrough: HasManyThroughFn = (relatedModel, relation?) => {
return function decorateAsRelation (target, property: string) {
const Model = target.constructor as ModelConstructorContract
Model.$boot()
Model.$addRelation(property, 'hasManyThrough', relation || {})
Model.$addRelation(property, 'hasManyThrough', Object.assign({ relatedModel }, relation))
}
}
2 changes: 1 addition & 1 deletion src/Orm/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,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.client)

/**
* Pass nested preloads
Expand Down
19 changes: 10 additions & 9 deletions src/Orm/Relations/HasOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@
/// <reference path="../../../adonis-typings/index.ts" />

import {
ModelOptions,
ModelContract,
RelationContract,
BaseRelationNode,
RelationContract,
ModelConstructorContract,
} from '@ioc:Adonis/Lucid/Model'

import { camelCase, snakeCase, uniq } from 'lodash'
import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'

import { Exception } from '@poppinss/utils'
import { camelCase, snakeCase, uniq } from 'lodash'

export class HasOne implements RelationContract {
/**
Expand Down Expand Up @@ -63,7 +64,7 @@ export class HasOne implements RelationContract {

constructor (
private _relationName: string,
private _options: Partial<BaseRelationNode>,
private _options: BaseRelationNode,
private _model: ModelConstructorContract,
) {
this._validateOptions()
Expand All @@ -77,7 +78,7 @@ export class HasOne implements RelationContract {
private _validateOptions () {
if (!this._options.relatedModel) {
throw new Exception(
'Related model reference is required for construct relationship',
'Related model reference is required to construct the relationship',
500,
'E_MISSING_RELATED_MODEL',
)
Expand Down Expand Up @@ -153,25 +154,25 @@ export class HasOne implements RelationContract {
/**
* Returns query for the relationship with applied constraints
*/
public getQuery (parent: ModelContract, options?: ModelOptions) {
public getQuery (parent: ModelContract, client: QueryClientContract) {
const value = parent[this.localKey]

return this.relatedModel()
.query(options)
.query({ client })
.where(this.foreignAdapterKey, this._ensureValue(value))
}

/**
* Returns query for the relationship with applied constraints for
* eagerloading
*/
public getEagerQuery (parents: ModelContract[], options?: ModelOptions) {
public getEagerQuery (parents: ModelContract[], client: QueryClientContract) {
const values = uniq(parents.map((parentInstance) => {
return this._ensureValue(parentInstance[this.localKey])
}))

return this.relatedModel()
.query(options)
.query({ client })
.whereIn(this.foreignAdapterKey, values)
}

Expand Down
132 changes: 131 additions & 1 deletion test/orm/base-model-options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import test from 'japa'
import { Profiler } from '@adonisjs/profiler/build/standalone'

import { column } from '../../src/Orm/Decorators'
import { column, hasOne } from '../../src/Orm/Decorators'
import { setup, cleanup, getDb, resetTables, getBaseModel, ormAdapter } from '../../test-helpers'

let db: ReturnType<typeof getDb>
Expand Down Expand Up @@ -529,3 +529,133 @@ test.group('Model options | Model.firstOrSave', (group) => {
assert.deepEqual(user.$options!.connection, client.connectionName)
})
})

test.group('Model options | Preloads', (group) => {
group.before(async () => {
db = getDb()
BaseModel = getBaseModel(ormAdapter(db))
await setup()
})

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

group.afterEach(async () => {
await resetTables()
})

test('pass query options to preloaded models', async (assert) => {
class Profile extends BaseModel {
@column({ primary: true })
public id: number

@column()
public userId: number

@column()
public displayName: string
}

class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string

@hasOne(() => Profile)
public profile: Profile
}

await db.insertQuery().table('users').insert({ username: 'virk' })
await db.insertQuery().table('profiles').insert({ user_id: 1, display_name: 'Virk' })

const users = await User.query({ connection: 'secondary' }).preload('profile').exec()
assert.lengthOf(users, 1)

assert.equal(users[0].$options!.connection, 'secondary')
assert.instanceOf(users[0].$options!.profiler, Profiler)

assert.equal(users[0].profile.$options!.connection, 'secondary')
assert.instanceOf(users[0].profile.$options!.profiler, Profiler)
})

test('use transaction client to execute preload queries', async (assert) => {
class Profile extends BaseModel {
@column({ primary: true })
public id: number

@column()
public userId: number

@column()
public displayName: string
}

class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string

@hasOne(() => Profile)
public profile: Profile
}

await db.insertQuery().table('users').insert({ username: 'virk' })
await db.insertQuery().table('profiles').insert({ user_id: 1, display_name: 'Virk' })

const trx = await db.transaction()
const users = await User.query({ client: trx }).preload('profile').exec()
await trx.commit()

assert.lengthOf(users, 1)

assert.equal(users[0].$options!.connection, 'primary')
assert.instanceOf(users[0].$options!.profiler, Profiler)

assert.equal(users[0].profile.$options!.connection, 'primary')
assert.instanceOf(users[0].profile.$options!.profiler, Profiler)
})

test('pass profiler to preload models', async (assert) => {
class Profile extends BaseModel {
@column({ primary: true })
public id: number

@column()
public userId: number

@column()
public displayName: string
}

class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string

@hasOne(() => Profile)
public profile: Profile
}

await db.insertQuery().table('users').insert({ username: 'virk' })
await db.insertQuery().table('profiles').insert({ user_id: 1, display_name: 'Virk' })

const profiler = new Profiler({})
const users = await User.query({ profiler }).preload('profile').exec()

assert.lengthOf(users, 1)

assert.equal(users[0].$options!.connection, 'primary')
assert.deepEqual(users[0].$options!.profiler, profiler)

assert.equal(users[0].profile.$options!.connection, 'primary')
assert.deepEqual(users[0].profile.$options!.profiler, profiler)
})
})
Loading

0 comments on commit 0171d0b

Please sign in to comment.