Start implementation of model relations
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
589cb7d579
commit
d92c8b5409
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue