Skip to content

Commit

Permalink
feat: add query client to all relationships
Browse files Browse the repository at this point in the history
The query client provides shortcut fetch methods and persistance
methods for relationships
  • Loading branch information
thetutlage committed Dec 29, 2019
1 parent 6da25dd commit 13e03c8
Show file tree
Hide file tree
Showing 16 changed files with 2,056 additions and 3,999 deletions.
14 changes: 14 additions & 0 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,15 @@ declare module '@ioc:Adonis/Lucid/Model' {
options?: ModelAdapterOptions,
): Promise<InstanceType<T>>

/**
* Creating many of model instance
*/
createMany<T extends ModelConstructorContract> (
this: T,
values: ModelObject[],
options?: ModelAdapterOptions,
): Promise<InstanceType<T>[]>

/**
* Find one using the primary key
*/
Expand Down Expand Up @@ -596,6 +605,11 @@ declare module '@ioc:Adonis/Lucid/Model' {
*/
modelClient (instance: ModelContract): QueryClientContract

modelConstructorClient (
modelConstructor: ModelConstructorContract,
options?: ModelAdapterOptions,
): QueryClientContract

/**
* Delete model instance
*/
Expand Down
58 changes: 55 additions & 3 deletions adonis-typings/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ declare module '@ioc:Adonis/Lucid/Relations' {
client (
model: InstanceType<Model> | InstanceType<Model>[],
client: QueryClientContract,
): RelationBaseQueryClientContract<Model, RelatedModel>
): HasOneClientContract<Model, RelatedModel>
}

/**
Expand Down Expand Up @@ -282,7 +282,7 @@ declare module '@ioc:Adonis/Lucid/Relations' {
client (
model: InstanceType<Model> | InstanceType<Model>[],
client: QueryClientContract,
): RelationBaseQueryClientContract<Model, RelatedModel>
): HasManyClientContract<Model, RelatedModel>
}

/**
Expand Down Expand Up @@ -315,7 +315,7 @@ declare module '@ioc:Adonis/Lucid/Relations' {
client (
model: InstanceType<Model> | InstanceType<Model>[],
client: QueryClientContract,
): RelationBaseQueryClientContract<Model, RelatedModel>
): BelongsToClientContract<Model, RelatedModel>
}

/**
Expand Down Expand Up @@ -414,6 +414,47 @@ declare module '@ioc:Adonis/Lucid/Relations' {
& ExcutableQueryBuilderContract<InstanceType<RelatedModel>[]>
}

/**
* Query client for has one relationship
*/
export interface HasOneClientContract<
Model extends ModelConstructorContract,
RelatedModel extends ModelConstructorContract
> extends RelationBaseQueryClientContract<Model, RelatedModel> {
save (related: InstanceType<RelatedModel>): Promise<void>
create (values: ModelObject): Promise<InstanceType<RelatedModel>>

firstOrCreate (search: ModelObject, savePayload?: ModelObject): Promise<InstanceType<RelatedModel>>
updateOrCreate (search: ModelObject, updatePayload: ModelObject): Promise<InstanceType<RelatedModel>>
}

/**
* Query client for has many relationship. Extends hasOne and
* adds support for saving many relations
*/
export interface HasManyClientContract<
Model extends ModelConstructorContract,
RelatedModel extends ModelConstructorContract
> extends HasOneClientContract<Model, RelatedModel> {
saveMany (related: InstanceType<RelatedModel>[]): Promise<void>
createMany (values: ModelObject[]): Promise<InstanceType<RelatedModel>[]>
}

/**
* Query client for belongs to relationship. Uses `associate` and
* `dissociate` over save.
*/
export interface BelongsToClientContract<
Model extends ModelConstructorContract,
RelatedModel extends ModelConstructorContract
> extends RelationBaseQueryClientContract<Model, RelatedModel> {
associate (related: InstanceType<RelatedModel>): Promise<void>
dissociate (): Promise<void>
}

/**
* Query client for many to many relationship.
*/
export interface ManyToManyClientContract<
Model extends ModelConstructorContract,
RelatedModel extends ModelConstructorContract
Expand All @@ -427,6 +468,17 @@ declare module '@ioc:Adonis/Lucid/Relations' {
*/
eagerQuery (): ManyToManyQueryBuilderContract<RelatedModel, InstanceType<RelatedModel>>
& ExcutableQueryBuilderContract<InstanceType<RelatedModel>[]>

save (related: InstanceType<RelatedModel>): Promise<void>
create (values: ModelObject): Promise<InstanceType<RelatedModel>>

saveMany (related: InstanceType<RelatedModel>[]): Promise<void>
createMany (values: ModelObject[]): Promise<InstanceType<RelatedModel>[]>

attach (ids: (string | number)[] | { [key: string]: ModelObject }): Promise<void>
detach (ids: (string | number)[]): Promise<void>

sync (ids: (string | number)[] | { [key: string]: ModelObject }, checkExisting?: boolean): Promise<void>
}

/**
Expand Down
2 changes: 1 addition & 1 deletion example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ user.preload((preloader) => {

// user.preload('profile')

const profile = (User.$getRelation('profile').$relatedModel() as typeof Profile)
// const profile = (User.$getRelation('profile').$relatedModel() as typeof Profile)

// const profile = User.$getRelation('profile')!

Expand Down
9 changes: 6 additions & 3 deletions src/Orm/Adapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export class Adapter implements AdapterContract {
/**
* Returns the query client based upon the model instance
*/
private _getModelClient (modelConstructor: ModelConstructorContract, options?: ModelAdapterOptions) {
public modelConstructorClient (
modelConstructor: ModelConstructorContract,
options?: ModelAdapterOptions,
) {
if (options && options.client) {
return options.client
}
Expand All @@ -42,7 +45,7 @@ export class Adapter implements AdapterContract {
* Returns the model query builder instance for a given model
*/
public query (modelConstructor: ModelConstructorContract, options?: ModelAdapterOptions): any {
const client = this._getModelClient(modelConstructor, options)
const client = this.modelConstructorClient(modelConstructor, options)
return client.modelQuery(modelConstructor)
}

Expand All @@ -51,7 +54,7 @@ export class Adapter implements AdapterContract {
*/
public modelClient (instance: ModelContract): any {
const modelConstructor = instance.constructor as unknown as ModelConstructorContract
return instance.$trx ? instance.$trx : this._getModelClient(modelConstructor, instance.$options)
return instance.$trx ? instance.$trx : this.modelConstructorClient(modelConstructor, instance.$options)
}

/**
Expand Down
38 changes: 36 additions & 2 deletions src/Orm/BaseModel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,8 @@ export class BaseModel implements ModelContract {
}

/**
* Returns a fresh instance of model by applying attributes
* to the model instance
* Returns a fresh persisted instance of model by applying
* attributes to the model instance
*/
public static async create<T extends ModelConstructorContract> (
this: T,
Expand All @@ -387,6 +387,39 @@ export class BaseModel implements ModelContract {
return instance as InstanceType<T>
}

/**
* Same as [[BaseModel.create]], but persists multiple instances. The create
* calls are wrapped inside a transaction.
*/
public static async createMany<T extends ModelConstructorContract> (
this: T,
values: ModelObject[],
options?: ModelAdapterOptions,
): Promise<InstanceType<T>[]> {
const trx = await this.$adapter.modelConstructorClient(this, options).transaction()
const collection: InstanceType<T>[] = []

try {
for (let row of values) {
const instance = new this() as InstanceType<T>

instance.fill(row)
instance.$trx = trx
instance.$options = options

await instance.save()
collection.push(instance)
}

await trx.commit()
} catch (error) {
await trx.rollback()
throw error
}

return collection
}

/**
* Find model instance using a key/value pair
*/
Expand Down Expand Up @@ -1137,6 +1170,7 @@ export class BaseModel implements ModelContract {
await Model.hooks.execute('before', 'save', this)

await Model.$adapter.insert(this, this.$prepareForAdapter(this.$attributes))

this.$hydrateOriginals()
this.$isPersisted = true

Expand Down
89 changes: 89 additions & 0 deletions src/Orm/Relations/Base/QueryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { QueryClientContract, TransactionClientContract } from '@ioc:Adonis/Lucid/Database'
import { RelationBaseQueryClientContract, RelationshipsContract } from '@ioc:Adonis/Lucid/Relations'
import {
ModelObject,
ModelConstructorContract,
ModelContract,
ModelAdapterOptions,
} from '@ioc:Adonis/Lucid/Model'

/**
* Query client for executing queries in scope to the defined
* relationship
*/
export abstract class BaseQueryClient implements RelationBaseQueryClientContract<
ModelConstructorContract,
ModelConstructorContract
> {
constructor (
protected $client: QueryClientContract,
protected $relation: RelationshipsContract,
) {
}

/**
* Client options to be used when persisting related
* models.
*/
protected $clientOptions: ModelAdapterOptions = {
client: this.$client,
connection: this.$client.connectionName,
profiler: this.$client.profiler,
}

/**
* Perisist a model instance
*/
protected async $persist (
related: ModelContract,
trx?: TransactionClientContract,
) {
if (!trx) {
related.$setOptionsAndTrx(this.$clientOptions)
await related.save()
}

related.$trx = trx
await related.save()
}

/**
* Creates a persists a single model instance
*/
protected async $createAndPersist (
values: ModelObject,
trx?: TransactionClientContract,
): Promise<ModelContract> {
if (!trx) {
return this.$relation.$relatedModel().create(values, this.$clientOptions)
}
return this.$relation.$relatedModel().create(values, { client: trx })
}

/**
* Creates a persists many of the model instances
*/
protected async $createAndPersistMany (values: ModelObject[]): Promise<ModelContract[]> {
return this.$relation.$relatedModel().createMany(values, this.$clientOptions)
}

/**
* Returns instance of query builder
*/
public abstract query (): any

/**
* Returns instance of query builder with `eager=true`
*/
public abstract eagerQuery (): any
}

63 changes: 57 additions & 6 deletions src/Orm/Relations/BelongsTo/QueryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,84 @@
* file that was distributed with this source code.
*/

import { Exception } from '@poppinss/utils'
import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'
import { BelongsToClientContract } from '@ioc:Adonis/Lucid/Relations'
import { ModelConstructorContract, ModelContract } from '@ioc:Adonis/Lucid/Model'
import { RelationBaseQueryClientContract } from '@ioc:Adonis/Lucid/Relations'

import { BelongsTo } from './index'
import { getValue } from '../../../utils'
import { BaseQueryClient } from '../Base/QueryClient'
import { BelongsToQueryBuilder } from './QueryBuilder'

/**
* Query client for executing queries in scope to the defined
* relationship
*/
export class BelongsToQueryClient implements RelationBaseQueryClientContract<
export class BelongsToQueryClient extends BaseQueryClient implements BelongsToClientContract<
ModelConstructorContract,
ModelConstructorContract
> {
constructor (
private parent: ModelContract | ModelContract[],
private client: QueryClientContract,
private relation: BelongsTo,
protected $client: QueryClientContract,
protected $relation: BelongsTo,
) {
super($client, $relation)
}

/**
* Ensures that persistance is invoked on a single parent instance
*/
private ensureSingleParent (parent: ModelContract | ModelContract[]): asserts parent is ModelContract {
if (Array.isArray(parent)) {
throw new Exception('Cannot associate related models with multiple parent instances')
}
}

/**
* Returns value for the foreign key from the related model
*/
private getForeignKeyValue (related: ModelContract, action: string) {
return getValue(related, this.$relation.$localKey, this.$relation, action)
}

/**
* Returns instance of query builder
*/
public query (): any {
return new BelongsToQueryBuilder(this.client.knexQuery(), this.client, this.parent, this.relation)
return new BelongsToQueryBuilder(
this.$client.knexQuery(),
this.$client,
this.parent,
this.$relation,
)
}

/**
* Returns instance of query builder with `eager=true`
*/
public eagerQuery (): any {
return new BelongsToQueryBuilder(this.client.knexQuery(), this.client, this.parent, this.relation, true)
return new BelongsToQueryBuilder(
this.$client.knexQuery(),
this.$client,
this.parent,
this.$relation,
true,
)
}

public async associate (related: ModelContract) {
this.ensureSingleParent(this.parent)
await related.save()

this.parent[this.$relation.$foreignKey] = this.getForeignKeyValue(related, 'associate')
await this.parent.save()
}

public async dissociate () {
this.ensureSingleParent(this.parent)
this.parent[this.$relation.$foreignKey] = null
await this.parent.save()
}
}
Loading

0 comments on commit 13e03c8

Please sign in to comment.