Start implementation of model relations
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
589cb7d579
commit
d92c8b5409
37
src/di/InjectionAware.ts
Normal file
37
src/di/InjectionAware.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {Container} from './Container'
|
||||||
|
import {TypedDependencyKey} from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for Injection-aware classes that automatically
|
||||||
|
* pass along their configured container to instances created
|
||||||
|
* via their `make` method.
|
||||||
|
*/
|
||||||
|
export class InjectionAware {
|
||||||
|
private ci: Container
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.ci = Container.getContainer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the container for this instance. */
|
||||||
|
public setContainer(ci: Container): this {
|
||||||
|
this.ci = ci
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the container for this instance. */
|
||||||
|
public getContainer(): Container {
|
||||||
|
return this.ci
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Instantiate a new injectable using the container. */
|
||||||
|
public make<T>(target: TypedDependencyKey<T>, ...parameters: any[]): T {
|
||||||
|
const inst = this.ci.make<T>(target, ...parameters)
|
||||||
|
|
||||||
|
if ( inst instanceof InjectionAware ) {
|
||||||
|
inst.setContainer(this.ci)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inst
|
||||||
|
}
|
||||||
|
}
|
@ -13,3 +13,4 @@ export * from './ScopedContainer'
|
|||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
export * from './decorator/injection'
|
export * from './decorator/injection'
|
||||||
|
export * from './InjectionAware'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Application} from './Application'
|
import {Application} from './Application'
|
||||||
import {Container, DependencyKey, Injectable} from '../di'
|
import {Container, Injectable, TypedDependencyKey} from '../di'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base type for a class that supports binding methods by string.
|
* Base type for a class that supports binding methods by string.
|
||||||
@ -43,7 +43,7 @@ export class AppClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Call the `make()` method on the global container. */
|
/** Call the `make()` method on the global container. */
|
||||||
protected make<T>(target: DependencyKey, ...parameters: any[]): T {
|
protected make<T>(target: TypedDependencyKey<T>, ...parameters: any[]): T {
|
||||||
return this.container().make<T>(target, ...parameters)
|
return this.container().make<T>(target, ...parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,13 @@ export * from './model/ModelResultIterable'
|
|||||||
export * from './model/events'
|
export * from './model/events'
|
||||||
export * from './model/Model'
|
export * from './model/Model'
|
||||||
|
|
||||||
|
export * from './model/relation/RelationBuilder'
|
||||||
|
export * from './model/relation/Relation'
|
||||||
|
export * from './model/relation/HasOneOrMany'
|
||||||
|
export * from './model/relation/HasOne'
|
||||||
|
export * from './model/relation/HasMany'
|
||||||
|
export * from './model/relation/decorators'
|
||||||
|
|
||||||
export * from './support/SessionModel'
|
export * from './support/SessionModel'
|
||||||
export * from './support/ORMSession'
|
export * from './support/ORMSession'
|
||||||
export * from './support/CacheModel'
|
export * from './support/CacheModel'
|
||||||
|
@ -3,7 +3,7 @@ import {Container, Inject, Instantiable, StaticClass} from '../../di'
|
|||||||
import {DatabaseService} from '../DatabaseService'
|
import {DatabaseService} from '../DatabaseService'
|
||||||
import {ModelBuilder} from './ModelBuilder'
|
import {ModelBuilder} from './ModelBuilder'
|
||||||
import {getFieldsMeta, ModelField} from './Field'
|
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 {EscapeValueObject} from '../dialect/SQLDialect'
|
||||||
import {AppClass} from '../../lifecycle/AppClass'
|
import {AppClass} from '../../lifecycle/AppClass'
|
||||||
import {Logging} from '../../service/Logging'
|
import {Logging} from '../../service/Logging'
|
||||||
@ -17,6 +17,10 @@ import {ModelUpdatedEvent} from './events/ModelUpdatedEvent'
|
|||||||
import {ModelCreatingEvent} from './events/ModelCreatingEvent'
|
import {ModelCreatingEvent} from './events/ModelCreatingEvent'
|
||||||
import {ModelCreatedEvent} from './events/ModelCreatedEvent'
|
import {ModelCreatedEvent} from './events/ModelCreatedEvent'
|
||||||
import {EventBus} from '../../event/EventBus'
|
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.
|
* 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>>()
|
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.
|
* 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.`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export class ModelBuilder<T extends Model<T>> extends AbstractBuilder<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getNewInstance(): AbstractBuilder<T> {
|
public getNewInstance(): AbstractBuilder<T> {
|
||||||
return this.app().make<ModelBuilder<T>>(ModelBuilder)
|
return this.app().make<ModelBuilder<T>>(ModelBuilder, this.ModelClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
public getResultIterable(): AbstractResultIterable<T> {
|
public getResultIterable(): AbstractResultIterable<T> {
|
||||||
|
47
src/orm/model/relation/HasMany.ts
Normal file
47
src/orm/model/relation/HasMany.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {Model} from '../Model'
|
||||||
|
import {HasOneOrMany} from './HasOneOrMany'
|
||||||
|
import {Collection} from '../../../util'
|
||||||
|
import {RelationNotLoadedError} from './Relation'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-to-many relation implementation.
|
||||||
|
*/
|
||||||
|
export class HasMany<T extends Model<T>, T2 extends Model<T2>> extends HasOneOrMany<T, T2, Collection<T2>> {
|
||||||
|
protected cachedValue?: Collection<T2>
|
||||||
|
|
||||||
|
protected cachedLoaded = false
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
parent: T,
|
||||||
|
related: T2,
|
||||||
|
foreignKeyOverride?: keyof T & string,
|
||||||
|
localKeyOverride?: keyof T2 & string,
|
||||||
|
) {
|
||||||
|
super(parent, related, foreignKeyOverride, localKeyOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the result of this relation. */
|
||||||
|
public get(): Promise<Collection<T2>> {
|
||||||
|
return this.fetch().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the value of this relation. */
|
||||||
|
public setValue(related: Collection<T2>): void {
|
||||||
|
this.cachedValue = related.clone()
|
||||||
|
this.cachedLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the value of this relation. */
|
||||||
|
public getValue(): Collection<T2> {
|
||||||
|
if ( !this.cachedValue ) {
|
||||||
|
throw new RelationNotLoadedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cachedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the relation has been loaded. */
|
||||||
|
public isLoaded(): boolean {
|
||||||
|
return this.cachedLoaded
|
||||||
|
}
|
||||||
|
}
|
47
src/orm/model/relation/HasOne.ts
Normal file
47
src/orm/model/relation/HasOne.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {Model} from '../Model'
|
||||||
|
import {HasOneOrMany} from './HasOneOrMany'
|
||||||
|
import {RelationNotLoadedError} from './Relation'
|
||||||
|
import {Maybe} from '../../../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-to-one relation implementation.
|
||||||
|
*/
|
||||||
|
export class HasOne<T extends Model<T>, T2 extends Model<T2>> extends HasOneOrMany<T, T2, Maybe<T2>> {
|
||||||
|
protected cachedValue?: T2
|
||||||
|
|
||||||
|
protected cachedLoaded = false
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
parent: T,
|
||||||
|
related: T2,
|
||||||
|
foreignKeyOverride?: keyof T & string,
|
||||||
|
localKeyOverride?: keyof T2 & string,
|
||||||
|
) {
|
||||||
|
super(parent, related, foreignKeyOverride, localKeyOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the result of this relation. */
|
||||||
|
async get(): Promise<Maybe<T2>> {
|
||||||
|
return this.fetch().first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the value of this relation. */
|
||||||
|
public setValue(related: T2): void {
|
||||||
|
this.cachedValue = related
|
||||||
|
this.cachedLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the value of this relation. */
|
||||||
|
public getValue(): Maybe<T2> {
|
||||||
|
if ( !this.cachedLoaded ) {
|
||||||
|
throw new RelationNotLoadedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cachedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the relation has been loaded. */
|
||||||
|
public isLoaded(): boolean {
|
||||||
|
return this.cachedLoaded
|
||||||
|
}
|
||||||
|
}
|
80
src/orm/model/relation/HasOneOrMany.ts
Normal file
80
src/orm/model/relation/HasOneOrMany.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import {Model} from '../Model'
|
||||||
|
import {Relation, RelationValue} from './Relation'
|
||||||
|
import {RelationBuilder} from './RelationBuilder'
|
||||||
|
import {raw} from '../../dialect/SQLDialect'
|
||||||
|
import {AbstractBuilder} from '../../builder/AbstractBuilder'
|
||||||
|
import {ModelBuilder} from '../ModelBuilder'
|
||||||
|
import {Collection, toString} from '../../../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for 1:1 and 1:M relations.
|
||||||
|
*/
|
||||||
|
export abstract class HasOneOrMany<T extends Model<T>, T2 extends Model<T2>, V extends RelationValue<T2>> extends Relation<T, T2, V> {
|
||||||
|
protected constructor(
|
||||||
|
parent: T,
|
||||||
|
related: T2,
|
||||||
|
|
||||||
|
/** Override the foreign key property. */
|
||||||
|
protected foreignKeyOverride?: keyof T & string,
|
||||||
|
|
||||||
|
/** Override the local key property. */
|
||||||
|
protected localKeyOverride?: keyof T2 & string,
|
||||||
|
) {
|
||||||
|
super(parent, related)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the name of the foreign key for this relation. */
|
||||||
|
public get foreignKey(): string {
|
||||||
|
return this.foreignKeyOverride || this.parent.keyName()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the name of the local key for this relation. */
|
||||||
|
public get localKey(): string {
|
||||||
|
return this.localKeyOverride || this.foreignKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the fully-qualified name of the foreign key. */
|
||||||
|
public get qualifiedForeignKey(): string {
|
||||||
|
return this.related.qualify(this.foreignKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the fully-qualified name of the local key. */
|
||||||
|
public get qualifiedLocalKey(): string {
|
||||||
|
return this.related.qualify(this.localKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the value of the pivot for this relation from the parent model. */
|
||||||
|
public get parentValue(): any {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
return this.parent[this.localKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new query for this relation. */
|
||||||
|
public query(): RelationBuilder<T2> {
|
||||||
|
return this.builder().select(raw('*'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply the relation's constraints on a model query. */
|
||||||
|
public applyScope(where: AbstractBuilder<T2>): void {
|
||||||
|
where.where(subq => {
|
||||||
|
subq.where(this.qualifiedForeignKey, '=', this.parentValue)
|
||||||
|
.whereRaw(this.qualifiedForeignKey, 'IS NOT', 'NULL')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create an eager-load query matching this relation's models. */
|
||||||
|
public buildEagerQuery(parentQuery: ModelBuilder<T>, result: Collection<T>): ModelBuilder<T2> {
|
||||||
|
const keys = result.pluck(this.localKey as keyof T)
|
||||||
|
.map(toString)
|
||||||
|
.all()
|
||||||
|
|
||||||
|
return this.related.query()
|
||||||
|
.whereIn(this.foreignKey, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Given a collection of results, filter out those that are relevant to this relation. */
|
||||||
|
public matchResults(possiblyRelated: Collection<T>): Collection<T> {
|
||||||
|
return possiblyRelated.where(this.foreignKey as keyof T, '=', this.parentValue)
|
||||||
|
}
|
||||||
|
}
|
111
src/orm/model/relation/Relation.ts
Normal file
111
src/orm/model/relation/Relation.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import {Model} from '../Model'
|
||||||
|
import {ModelBuilder} from '../ModelBuilder'
|
||||||
|
import {AbstractBuilder} from '../../builder/AbstractBuilder'
|
||||||
|
import {ResultCollection} from '../../builder/result/ResultCollection'
|
||||||
|
import {Collection, ErrorWithContext, Maybe} from '../../../util'
|
||||||
|
import {QuerySource} from '../../types'
|
||||||
|
import {RelationBuilder} from './RelationBuilder'
|
||||||
|
import {InjectionAware} from '../../../di'
|
||||||
|
|
||||||
|
/** Type alias for possible values of a relation. */
|
||||||
|
export type RelationValue<T2> = Maybe<Collection<T2> | T2>
|
||||||
|
|
||||||
|
/** Error thrown when a relation result is accessed synchronously before it is loaded. */
|
||||||
|
export class RelationNotLoadedError extends ErrorWithContext {
|
||||||
|
constructor(
|
||||||
|
context: {[key: string]: any} = {},
|
||||||
|
) {
|
||||||
|
super('Attempted to get value of relation that has not yet been loaded.', context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for inter-model relation implementations.
|
||||||
|
*/
|
||||||
|
export abstract class Relation<T extends Model<T>, T2 extends Model<T2>, V extends RelationValue<T2>> extends InjectionAware {
|
||||||
|
protected constructor(
|
||||||
|
/** The model related from. */
|
||||||
|
protected parent: T,
|
||||||
|
|
||||||
|
/** The model related to. */
|
||||||
|
public readonly related: T2,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the value of the key field from the parent model. */
|
||||||
|
protected abstract get parentValue(): any
|
||||||
|
|
||||||
|
/** Create a new relation builder query for this relation instance. */
|
||||||
|
public abstract query(): RelationBuilder<T2>
|
||||||
|
|
||||||
|
/** Limit the results of the builder to only this relation's rows. */
|
||||||
|
public abstract applyScope(where: AbstractBuilder<T2>): void
|
||||||
|
|
||||||
|
/** Create a relation query that will eager-load the result of this relation for a set of models. */
|
||||||
|
public abstract buildEagerQuery(parentQuery: ModelBuilder<T>, result: Collection<T>): ModelBuilder<T2>
|
||||||
|
|
||||||
|
/** Given a set of possibly-related instances, filter out the ones that are relevant to the parent. */
|
||||||
|
public abstract matchResults(possiblyRelated: Collection<T>): Collection<T>
|
||||||
|
|
||||||
|
/** Set the value of the relation. */
|
||||||
|
public abstract setValue(related: V): void
|
||||||
|
|
||||||
|
/** Get the value of the relation. */
|
||||||
|
public abstract getValue(): V
|
||||||
|
|
||||||
|
/** Returns true if the relation has been loaded. */
|
||||||
|
public abstract isLoaded(): boolean
|
||||||
|
|
||||||
|
/** Get a collection of the results of this relation. */
|
||||||
|
public fetch(): ResultCollection<T2> {
|
||||||
|
return this.query().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the result of this relation. */
|
||||||
|
public abstract get(): Promise<V>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the relation "thenable" so relation methods on models can be awaited
|
||||||
|
* to yield the result of the relation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const rows = await myModelInstance.myHasManyRelation() -- rows is a Collection
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param resolve
|
||||||
|
* @param reject
|
||||||
|
*/
|
||||||
|
public then(resolve: (result: V) => unknown, reject: (e: Error) => unknown): void {
|
||||||
|
if ( this.isLoaded() ) {
|
||||||
|
resolve(this.getValue())
|
||||||
|
} else {
|
||||||
|
this.get()
|
||||||
|
.then(result => {
|
||||||
|
if ( result instanceof Collection ) {
|
||||||
|
this.setValue(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result)
|
||||||
|
})
|
||||||
|
.catch(reject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the value of this relation. */
|
||||||
|
public get value(): V {
|
||||||
|
return this.getValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the query source for the related model in this relation. */
|
||||||
|
public get relatedQuerySource(): QuerySource {
|
||||||
|
const related = this.related.constructor as typeof Model
|
||||||
|
return related.querySource()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a new builder instance for this relation. */
|
||||||
|
public builder(): RelationBuilder<T2> {
|
||||||
|
return this.make(RelationBuilder, this)
|
||||||
|
}
|
||||||
|
}
|
14
src/orm/model/relation/RelationBuilder.ts
Normal file
14
src/orm/model/relation/RelationBuilder.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import {Model} from '../Model'
|
||||||
|
import {ModelBuilder} from '../ModelBuilder'
|
||||||
|
import {Relation} from './Relation'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModelBuilder instance that queries the related model in a relation.
|
||||||
|
*/
|
||||||
|
export class RelationBuilder<T extends Model<T>> extends ModelBuilder<T> {
|
||||||
|
constructor(
|
||||||
|
protected relation: Relation<any, T, any>,
|
||||||
|
) {
|
||||||
|
super(relation.related.constructor as any)
|
||||||
|
}
|
||||||
|
}
|
37
src/orm/model/relation/decorators.ts
Normal file
37
src/orm/model/relation/decorators.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {Model} from '../Model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorator for relation methods on a Model implementation.
|
||||||
|
* Caches the relation instances between uses for the life of the model.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export function Related(): MethodDecorator {
|
||||||
|
return (target, propertyKey, descriptor) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const original = descriptor.value
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
descriptor.value = function(...args) {
|
||||||
|
const model = this as Model<any>
|
||||||
|
const cache = model.relationCache
|
||||||
|
|
||||||
|
const existing = cache.firstWhere('accessor', '=', propertyKey)
|
||||||
|
if ( existing ) {
|
||||||
|
return existing.relation
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const value = original.apply(this, args)
|
||||||
|
|
||||||
|
cache.push({
|
||||||
|
accessor: propertyKey,
|
||||||
|
relation: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -131,6 +131,7 @@ export enum FieldType {
|
|||||||
json = 'json',
|
json = 'json',
|
||||||
line = 'line',
|
line = 'line',
|
||||||
lseg = 'lseg',
|
lseg = 'lseg',
|
||||||
|
ltree = 'ltree',
|
||||||
macaddr = 'macaddr',
|
macaddr = 'macaddr',
|
||||||
money = 'money',
|
money = 'money',
|
||||||
numeric = 'numeric',
|
numeric = 'numeric',
|
||||||
@ -189,6 +190,7 @@ export function inverseFieldType(type: FieldType): string {
|
|||||||
json: 'json',
|
json: 'json',
|
||||||
line: 'line',
|
line: 'line',
|
||||||
lseg: 'lseg',
|
lseg: 'lseg',
|
||||||
|
ltree: 'ltree',
|
||||||
macaddr: 'macaddr',
|
macaddr: 'macaddr',
|
||||||
money: 'money',
|
money: 'money',
|
||||||
numeric: 'numeric',
|
numeric: 'numeric',
|
||||||
|
@ -16,8 +16,11 @@ type ComparisonFunction<T> = (item: CollectionItem<T>, otherItem: CollectionItem
|
|||||||
import { WhereOperator, applyWhere, whereMatch } from './where'
|
import { WhereOperator, applyWhere, whereMatch } from './where'
|
||||||
|
|
||||||
const collect = <T>(items: CollectionItem<T>[]): Collection<T> => Collection.collect(items)
|
const collect = <T>(items: CollectionItem<T>[]): Collection<T> => Collection.collect(items)
|
||||||
|
const toString = (item: unknown): string => String(item)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
collect,
|
collect,
|
||||||
|
toString,
|
||||||
Collection,
|
Collection,
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
@ -9,3 +9,12 @@ export type ParameterizedCallback<T> = ((arg: T) => any)
|
|||||||
|
|
||||||
/** A key-value form of a given type. */
|
/** A key-value form of a given type. */
|
||||||
export type KeyValue<T> = {key: string, value: T}
|
export type KeyValue<T> = {key: string, value: T}
|
||||||
|
|
||||||
|
/** Simple helper method to verify that a key is a keyof some object. */
|
||||||
|
export function isKeyof<T>(key: unknown, obj: T): key is keyof T {
|
||||||
|
if ( typeof key !== 'string' && typeof key !== 'symbol' ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return key in obj
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user