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 = Maybe | 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, T2 extends Model, V extends RelationValue> 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 /** Limit the results of the builder to only this relation's rows. */ public abstract applyScope(where: AbstractBuilder): void /** Create a relation query that will eager-load the result of this relation for a set of models. */ public abstract buildEagerQuery(parentQuery: ModelBuilder, result: Collection): ModelBuilder /** Given a set of possibly-related instances, filter out the ones that are relevant to the parent. */ public abstract matchResults(possiblyRelated: Collection): Collection /** 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 { return this.query() .where(where => this.applyScope(where)) .get() } /** Resolve the result of this relation. */ public abstract get(): Promise /** * 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 { const relatedCtor = this.related.constructor as typeof Model return this.make>(RelationBuilder, this) .connection(relatedCtor.getConnection()) .from(relatedCtor.tableName()) } }