Start implementation of model relations
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-11-10 21:30:59 -06:00
parent 589cb7d579
commit d92c8b5409
15 changed files with 557 additions and 4 deletions

View File

@@ -3,7 +3,7 @@ import {Container, Inject, Instantiable, StaticClass} from '../../di'
import {DatabaseService} from '../DatabaseService'
import {ModelBuilder} from './ModelBuilder'
import {getFieldsMeta, ModelField} from './Field'
import {deepCopy, Pipe, Collection, Awaitable, uuid4} from '../../util'
import {deepCopy, Pipe, Collection, Awaitable, uuid4, isKeyof} from '../../util'
import {EscapeValueObject} from '../dialect/SQLDialect'
import {AppClass} from '../../lifecycle/AppClass'
import {Logging} from '../../service/Logging'
@@ -17,6 +17,10 @@ import {ModelUpdatedEvent} from './events/ModelUpdatedEvent'
import {ModelCreatingEvent} from './events/ModelCreatingEvent'
import {ModelCreatedEvent} from './events/ModelCreatedEvent'
import {EventBus} from '../../event/EventBus'
import {Relation, RelationValue} from './relation/Relation'
import {HasOne} from './relation/HasOne'
import {HasMany} from './relation/HasMany'
import {HasOneOrMany} from './relation/HasOneOrMany'
/**
* Base for classes that are mapped to tables in a database.
@@ -95,6 +99,12 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
*/
protected modelEventBusSubscribers: Collection<EventSubscriberEntry<any>> = new Collection<EventSubscriberEntry<any>>()
/**
* Cache of relation instances by property accessor.
* This is used by the `@Relation()` decorator to cache Relation instances.
*/
public relationCache: Collection<{ accessor: string | symbol, relation: Relation<T, any, any> }> = new Collection<{accessor: string | symbol; relation: Relation<T, any, any>}>()
/**
* Get the table name for this model.
*/
@@ -871,4 +881,152 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
},
}
}
/**
* Create a new one-to-one relation instance. Should be called from a method on the model:
*
* @example
* ```ts
* class MyModel extends Model<MyModel> {
* @Related()
* public otherModel() {
* return this.hasOne(MyOtherModel)
* }
* }
* ```
*
* @param related
* @param foreignKeyOverride
* @param localKeyOverride
*/
public hasOne<T2 extends Model<T2>>(related: Instantiable<T2>, foreignKeyOverride?: keyof T & string, localKeyOverride?: keyof T2 & string): HasOne<T, T2> {
return new HasOne<T, T2>(this as unknown as T, this.make(related), foreignKeyOverride, localKeyOverride)
}
/**
* Create a new one-to-one relation instance. Should be called from a method on the model:
*
* @example
* ```ts
* class MyModel extends Model<MyModel> {
* @Related()
* public otherModels() {
* return this.hasMany(MyOtherModel)
* }
* }
* ```
*
* @param related
* @param foreignKeyOverride
* @param localKeyOverride
*/
public hasMany<T2 extends Model<T2>>(related: Instantiable<T2>, foreignKeyOverride?: keyof T & string, localKeyOverride?: keyof T2 & string): HasMany<T, T2> {
return new HasMany<T, T2>(this as unknown as T, this.make(related), foreignKeyOverride, localKeyOverride)
}
/**
* Create the inverse of a one-to-one relation. Should be called from a method on the model:
*
* @example
* ```ts
* class MyModel extends Model<MyModel> {
* @Related()
* public otherModel() {
* return this.hasOne(MyOtherModel)
* }
* }
*
* class MyOtherModel extends Model<MyOtherModel> {
* @Related()
* public myModel() {
* return this.belongsToOne(MyModel, 'otherModel')
* }
* }
* ```
*
* @param related
* @param relationName
*/
public belongsToOne<T2 extends Model<T2>>(related: Instantiable<T>, relationName: keyof T2): HasOne<T, T2> {
const relatedInst = this.make(related) as T2
const relation = relatedInst.getRelation(relationName)
if ( !(relation instanceof HasOneOrMany) ) {
throw new TypeError(`Cannot create belongs to one relation. Inverse relation must be HasOneOrMany.`)
}
const localKey = relation.localKey
const foreignKey = relation.foreignKey
if ( !isKeyof(localKey, this as unknown as T) || !isKeyof(foreignKey, relatedInst) ) {
throw new TypeError('Local or foreign keys do not exist on the base model.')
}
return new HasOne<T, T2>(this as unknown as T, relatedInst, localKey, foreignKey)
}
/**
* Create the inverse of a one-to-many relation. Should be called from a method on the model:
*
* @example
* ```ts
* class MyModel extends Model<MyModel> {
* @Related()
* public otherModels() {
* return this.hasMany(MyOtherModel)
* }
* }
*
* class MyOtherModel extends Model<MyOtherModel> {
* @Related()
* public myModels() {
* return this.belongsToMany(MyModel, 'otherModels')
* }
* }
* ```
*
* @param related
* @param relationName
*/
public belongsToMany<T2 extends Model<T2>>(related: Instantiable<T>, relationName: keyof T2): HasMany<T, T2> {
const relatedInst = this.make(related) as T2
const relation = relatedInst.getRelation(relationName)
if ( !(relation instanceof HasOneOrMany) ) {
throw new TypeError(`Cannot create belongs to one relation. Inverse relation must be HasOneOrMany.`)
}
const localKey = relation.localKey
const foreignKey = relation.foreignKey
if ( !isKeyof(localKey, this as unknown as T) || !isKeyof(foreignKey, relatedInst) ) {
throw new TypeError('Local or foreign keys do not exist on the base model.')
}
return new HasMany<T, T2>(this as unknown as T, relatedInst, localKey, foreignKey)
}
/**
* Get the relation instance returned by a method on this model.
* @param name
* @protected
*/
protected getRelation<T2 extends Model<T2>>(name: keyof this): Relation<T, T2, RelationValue<T2>> {
const relFn = this[name]
if ( relFn instanceof Relation ) {
return relFn
}
if ( typeof relFn === 'function' ) {
const rel = relFn.apply(relFn, this)
if ( rel instanceof Relation ) {
return rel
}
}
throw new TypeError(`Cannot get relation of name: ${name}. Method does not return a Relation.`)
}
}