finish TreeModel implementation and updateMany builder method
This commit is contained in:
@@ -33,14 +33,24 @@ export abstract class HasOneOrMany<T extends Model<T>, T2 extends Model<T2>, V e
|
||||
return this.localKeyOverride || this.foreignKey
|
||||
}
|
||||
|
||||
public get foreignColumn(): string {
|
||||
const ctor = this.related.constructor as typeof Model
|
||||
return ctor.propertyToColumn(this.foreignKey)
|
||||
}
|
||||
|
||||
public get localColumn(): string {
|
||||
const ctor = this.related.constructor as typeof Model
|
||||
return ctor.propertyToColumn(this.localKey)
|
||||
}
|
||||
|
||||
/** Get the fully-qualified name of the foreign key. */
|
||||
public get qualifiedForeignKey(): string {
|
||||
return this.related.qualify(this.foreignKey)
|
||||
return this.related.qualify(this.foreignColumn)
|
||||
}
|
||||
|
||||
/** Get the fully-qualified name of the local key. */
|
||||
public get qualifiedLocalKey(): string {
|
||||
return this.related.qualify(this.localKey)
|
||||
return this.related.qualify(this.localColumn)
|
||||
}
|
||||
|
||||
/** Get the value of the pivot for this relation from the parent model. */
|
||||
@@ -70,11 +80,11 @@ export abstract class HasOneOrMany<T extends Model<T>, T2 extends Model<T2>, V e
|
||||
.all()
|
||||
|
||||
return this.related.query()
|
||||
.whereIn(this.foreignKey, keys)
|
||||
.whereIn(this.foreignColumn, 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)
|
||||
return possiblyRelated.where(this.foreignColumn as keyof T, '=', this.parentValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,19 @@ import {RelationBuilder} from './RelationBuilder'
|
||||
import {raw} from '../../dialect/SQLDialect'
|
||||
import {AbstractBuilder} from '../../builder/AbstractBuilder'
|
||||
import {ModelBuilder} from '../ModelBuilder'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
|
||||
/**
|
||||
* A relation that recursively loads the subtree of a model using
|
||||
* modified preorder traversal.
|
||||
*/
|
||||
@Injectable()
|
||||
export class HasSubtree<T extends TreeModel<T>> extends Relation<T, T, Collection<T>> {
|
||||
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
/**
|
||||
* When the relation is loaded, the immediate children of the node.
|
||||
* @protected
|
||||
@@ -25,12 +31,24 @@ export class HasSubtree<T extends TreeModel<T>> extends Relation<T, T, Collectio
|
||||
super(model, model)
|
||||
}
|
||||
|
||||
public flatten(): Collection<T> {
|
||||
const children = this.getValue()
|
||||
const subtrees = children.reduce((subtree, child) => subtree.concat(child.subtree().flatten()), collect())
|
||||
return children.concat(subtrees)
|
||||
}
|
||||
|
||||
/** Manually load the subtree. */
|
||||
public async load(): Promise<void> {
|
||||
this.setValue(await this.get())
|
||||
}
|
||||
|
||||
protected get parentValue(): any {
|
||||
return this.model.key()
|
||||
}
|
||||
|
||||
public query(): RelationBuilder<T> {
|
||||
return this.builder()
|
||||
.tap(b => this.model.applyScopes(b))
|
||||
.select(raw('*'))
|
||||
.orderByAscending(this.leftTreeField)
|
||||
}
|
||||
@@ -50,6 +68,9 @@ export class HasSubtree<T extends TreeModel<T>> extends Relation<T, T, Collectio
|
||||
public buildEagerQuery(parentQuery: ModelBuilder<T>, result: Collection<T>): ModelBuilder<T> {
|
||||
const query = this.model.query().without('subtree')
|
||||
|
||||
this.logging.debug(`Building eager query for parent: ${parentQuery}`)
|
||||
this.logging.debug(result)
|
||||
|
||||
if ( result.isEmpty() ) {
|
||||
return query.whereMatchNone()
|
||||
}
|
||||
@@ -61,16 +82,20 @@ export class HasSubtree<T extends TreeModel<T>> extends Relation<T, T, Collectio
|
||||
return
|
||||
}
|
||||
|
||||
query.where(where => {
|
||||
query.orWhere(where => {
|
||||
where.where(this.leftTreeField, '>', left)
|
||||
.where(this.leftTreeField, '<', right)
|
||||
})
|
||||
})
|
||||
|
||||
this.logging.debug(`Built eager query: ${query}`)
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
public matchResults(possiblyRelated: Collection<T>): Collection<T> {
|
||||
this.logging.debug('Matching possibly related: ' + possiblyRelated.length)
|
||||
this.logging.verbose(possiblyRelated)
|
||||
const modelLeft = this.model.leftTreeNum()
|
||||
const modelRight = this.model.rightTreeNum()
|
||||
if ( !modelLeft || !modelRight ) {
|
||||
@@ -83,6 +108,22 @@ export class HasSubtree<T extends TreeModel<T>> extends Relation<T, T, Collectio
|
||||
})
|
||||
}
|
||||
|
||||
public appendSubtree(idx: number, subtree: T): void {
|
||||
if ( !this.instances ) {
|
||||
throw new RelationNotLoadedError()
|
||||
}
|
||||
|
||||
this.instances = this.instances.put(idx, subtree)
|
||||
}
|
||||
|
||||
public removeSubtree(subtree: TreeModel<T>): void {
|
||||
if ( !this.instances ) {
|
||||
throw new RelationNotLoadedError()
|
||||
}
|
||||
|
||||
this.instances = this.instances.filter(x => x.isNot(subtree))
|
||||
}
|
||||
|
||||
public setValue(related: Collection<T>): void {
|
||||
// `related` contains a flat collection of the subtree nodes, ordered by left key ascending
|
||||
// We will loop through the related nodes and recursively call `setValue` for our immediate
|
||||
@@ -126,6 +167,9 @@ export class HasSubtree<T extends TreeModel<T>> extends Relation<T, T, Collectio
|
||||
children.push(finalState.currentChild)
|
||||
}
|
||||
|
||||
// Set the parent relation on the immediate children we identified
|
||||
children.each(child => child.parentNode().setValue(this.model))
|
||||
|
||||
this.instances = children.sortBy(inst => inst.getOriginalValues()?.[this.leftTreeField])
|
||||
}
|
||||
|
||||
|
||||
75
src/orm/model/relation/HasTreeParent.ts
Normal file
75
src/orm/model/relation/HasTreeParent.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {Relation, RelationNotLoadedError} from './Relation'
|
||||
import {TreeModel} from '../TreeModel'
|
||||
import {RelationBuilder} from './RelationBuilder'
|
||||
import {raw} from '../../dialect/SQLDialect'
|
||||
import {AbstractBuilder} from '../../builder/AbstractBuilder'
|
||||
import {ModelBuilder} from '../ModelBuilder'
|
||||
import {Collection, Maybe} from '../../../util'
|
||||
|
||||
export class HasTreeParent<T extends TreeModel<T>> extends Relation<T, T, Maybe<T>> {
|
||||
|
||||
protected parentInstance?: T
|
||||
|
||||
protected loaded = false
|
||||
|
||||
protected constructor(
|
||||
protected model: T,
|
||||
protected readonly leftTreeField: string,
|
||||
protected readonly parentIdField: string,
|
||||
) {
|
||||
super(model, model)
|
||||
}
|
||||
|
||||
protected get parentValue(): any {
|
||||
return this.model.key()
|
||||
}
|
||||
|
||||
public query(): RelationBuilder<T> {
|
||||
return this.builder()
|
||||
.tap(b => this.model.applyScopes(b))
|
||||
.select(raw('*'))
|
||||
.orderByAscending(this.leftTreeField)
|
||||
}
|
||||
|
||||
public applyScope(where: AbstractBuilder<T>): void {
|
||||
const parentId = this.model.parentId()
|
||||
if ( !parentId ) {
|
||||
where.whereMatchNone()
|
||||
return
|
||||
}
|
||||
|
||||
where.where(this.parentIdField, '=', parentId)
|
||||
}
|
||||
|
||||
public buildEagerQuery(parentQuery: ModelBuilder<T>, result: Collection<T>): ModelBuilder<T> {
|
||||
const parentIds = result.map(model => model.parentId()).whereDefined()
|
||||
return this.model.query()
|
||||
.without('subtree')
|
||||
.whereIn(this.parentIdField, parentIds)
|
||||
}
|
||||
|
||||
public matchResults(possiblyRelated: Collection<T>): Collection<T> {
|
||||
return possiblyRelated.filter(related => related.key() === this.model.parentId())
|
||||
}
|
||||
|
||||
public setValue(related: Maybe<T>): void {
|
||||
this.loaded = true
|
||||
this.parentInstance = related
|
||||
}
|
||||
|
||||
public getValue(): Maybe<T> {
|
||||
if ( !this.loaded && this.model.parentId() ) {
|
||||
throw new RelationNotLoadedError()
|
||||
}
|
||||
|
||||
return this.parentInstance
|
||||
}
|
||||
|
||||
public isLoaded(): boolean {
|
||||
return this.loaded || !this.model.parentId()
|
||||
}
|
||||
|
||||
public async get(): Promise<Maybe<T>> {
|
||||
return this.fetch().first()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user