finish TreeModel implementation and updateMany builder method
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import {Model} from './Model'
|
||||
import {Collection, Maybe} from '../../util'
|
||||
import {collect, Collection, ErrorWithContext, Maybe} from '../../util'
|
||||
import {HasSubtree} from './relation/HasSubtree'
|
||||
import {Related} from './relation/decorators'
|
||||
import {HasTreeParent} from './relation/HasTreeParent'
|
||||
|
||||
/**
|
||||
* Model implementation with helpers for querying tree-structured data.
|
||||
@@ -41,22 +42,32 @@ export abstract class TreeModel<T extends TreeModel<T>> extends Model<T> {
|
||||
/** The table column where the right tree number is stored. */
|
||||
public static readonly rightTreeField = 'right_num'
|
||||
|
||||
public static readonly parentIdField = 'parent_id'
|
||||
|
||||
/**
|
||||
* @override to eager-load the subtree by default
|
||||
* @protected
|
||||
*/
|
||||
protected with: (keyof T)[] = ['subtree']
|
||||
|
||||
protected removedChildren: Collection<TreeModel<T>> = collect()
|
||||
|
||||
/** Get the left tree number for this model. */
|
||||
public leftTreeNum(): Maybe<number> {
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
return this.originalSourceRow?.[ctor.leftTreeField]
|
||||
return this.getColumn(ctor.leftTreeField) as number
|
||||
}
|
||||
|
||||
/** Get the right tree number for this model. */
|
||||
public rightTreeNum(): Maybe<number> {
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
return this.originalSourceRow?.[ctor.rightTreeField]
|
||||
return this.getColumn(ctor.rightTreeField) as number
|
||||
}
|
||||
|
||||
/** Get the ID of this node's parent, if one exists. */
|
||||
public parentId(): Maybe<number> {
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
return this.getColumn(ctor.parentIdField) as number
|
||||
}
|
||||
|
||||
/** Returns true if this node has no children. */
|
||||
@@ -67,7 +78,7 @@ export abstract class TreeModel<T extends TreeModel<T>> extends Model<T> {
|
||||
}
|
||||
|
||||
/** Returns true if the given `node` exists within the subtree of this node. */
|
||||
public contains(node: this): boolean {
|
||||
public contains(node: TreeModel<T>): boolean {
|
||||
const num = node.leftTreeNum()
|
||||
const left = this.leftTreeNum()
|
||||
const right = this.rightTreeNum()
|
||||
@@ -81,8 +92,182 @@ export abstract class TreeModel<T extends TreeModel<T>> extends Model<T> {
|
||||
return this.make<HasSubtree<T>>(HasSubtree, this, ctor.leftTreeField)
|
||||
}
|
||||
|
||||
/** The parent node of this model, if one exists. */
|
||||
@Related()
|
||||
public parentNode(): HasTreeParent<T> {
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
return this.make<HasTreeParent<T>>(HasTreeParent, this, ctor.leftTreeField, ctor.parentIdField)
|
||||
}
|
||||
|
||||
/** Get the immediate children of this model. */
|
||||
public children(): Collection<T> {
|
||||
return this.subtree().getValue()
|
||||
}
|
||||
|
||||
/** Get the parent of this model, if one exists. */
|
||||
public parent(): Maybe<T> {
|
||||
return this.parentNode().getValue()
|
||||
}
|
||||
|
||||
public root(): Maybe<TreeModel<T>> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
let parent: Maybe<TreeModel<T>> = this
|
||||
while ( parent?.parent() ) {
|
||||
parent = parent?.parent()
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
public rootOrFail(): TreeModel<T> {
|
||||
const root = this.root()
|
||||
if ( !root ) {
|
||||
throw new ErrorWithContext('Unable to determine tree root', {
|
||||
node: this,
|
||||
})
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
/** Get the nearest node in the parental line that is an ancestor of both this node and the `other` node. */
|
||||
public commonAncestorWith(other: TreeModel<T>): Maybe<TreeModel<T>> {
|
||||
if ( this.contains(other) ) {
|
||||
// Handle the special case when this node is an ancestor of the other.
|
||||
return this
|
||||
}
|
||||
|
||||
if ( other.contains(this) ) {
|
||||
// Handle the special case when this node is a descendant of the other.
|
||||
return other
|
||||
}
|
||||
|
||||
// Otherwise, walk up this node's ancestral line and try to find a shared ancestor.
|
||||
// It's getting too anthropological up in here.
|
||||
let parent = this.parent()
|
||||
while ( parent ) {
|
||||
if ( parent.contains(other) ) {
|
||||
return parent
|
||||
}
|
||||
|
||||
parent = parent.parent()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the preorder traversal numbering for this node and its subtree.
|
||||
* After renumbering, you probably also want to save the subtree nodes to persist
|
||||
* the tree into the database.
|
||||
* @example
|
||||
* ```ts
|
||||
* await model.renumber().saveSubtree()
|
||||
* ```
|
||||
*/
|
||||
public renumber(): this {
|
||||
// Assume our leftTreeNum is -correct-.
|
||||
// Given that, renumber the children recursively, then set our rightTreeNum
|
||||
const myLeftNum = this.leftTreeNum()
|
||||
if ( !myLeftNum ) {
|
||||
return this
|
||||
}
|
||||
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
let nextChildLeftNum = myLeftNum + 1
|
||||
let myRightNum = myLeftNum + 1
|
||||
|
||||
this.children()
|
||||
.each(child => {
|
||||
child.setColumn(ctor.leftTreeField, nextChildLeftNum)
|
||||
child.setColumn(ctor.parentIdField, this.key())
|
||||
child.parentNode().setValue(this as unknown as T)
|
||||
child.renumber()
|
||||
myRightNum = (child.rightTreeNum() ?? 0) + 1
|
||||
nextChildLeftNum = (child.rightTreeNum() ?? 0) + 1
|
||||
})
|
||||
|
||||
this.setColumn(ctor.rightTreeField, myRightNum)
|
||||
return this
|
||||
}
|
||||
|
||||
/** Get a flat collection of this node & its subtree nodes, in pre-order. */
|
||||
public flatten(): Collection<TreeModel<T>> {
|
||||
return this.flattenLevel()
|
||||
}
|
||||
|
||||
/** Recursive helper for `flatten()`. */
|
||||
private flattenLevel(topLevel = true): Collection<TreeModel<T>> {
|
||||
const flat = collect<TreeModel<T>>(topLevel ? [this] : [])
|
||||
this.children().each(child => {
|
||||
flat.push(child)
|
||||
flat.concat(child.flattenLevel(false))
|
||||
})
|
||||
return flat
|
||||
}
|
||||
|
||||
/** Call the `save()` method on all nodes in this node & its subtree. */
|
||||
public async saveSubtree(): Promise<void> {
|
||||
await this.save()
|
||||
await this.removedChildren.awaitMapCall('saveSubtree')
|
||||
this.removedChildren = collect()
|
||||
await this.children()
|
||||
.map(child => child.saveSubtree())
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the `other` node as a child of this node.
|
||||
* Returns the common ancestor from which point the tree was renumbered.
|
||||
* You should save the subtree after this.
|
||||
* @example
|
||||
* ```ts
|
||||
* await nodeA.appendChild(nodeB).saveSubtree()
|
||||
* ```
|
||||
* @param other
|
||||
* @param before - if provided, `other` will be inserted before the `before` child of this node. Otherwise, it will be added as the last child.
|
||||
*/
|
||||
public appendChild(other: T, before: Maybe<T> = undefined): TreeModel<T> {
|
||||
// Determine the index where we should insert the node in our children
|
||||
const idx = (before ? this.children().search(before) : undefined) ?? this.children().length
|
||||
|
||||
// Insert the child at that index
|
||||
this.subtree().appendSubtree(idx, other)
|
||||
|
||||
// If the child has a parent:
|
||||
const parent = other.parent()
|
||||
if ( parent ) {
|
||||
// Remove the child from the parent's children
|
||||
parent.subtree().removeSubtree(other)
|
||||
|
||||
// Get the common ancestor of the child's parent and this node
|
||||
const ancestor = this.commonAncestorWith(other)
|
||||
if ( ancestor ) {
|
||||
// Renumber from that ancestor
|
||||
return ancestor.renumber()
|
||||
}
|
||||
}
|
||||
|
||||
// If the child has no parent or a common ancestor could not be found,
|
||||
// renumber the entire tree since the total number of nodes has changed
|
||||
return this.rootOrFail().renumber()
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this node from the tree structure.
|
||||
* If the node existed in the tree structure and was removed, this method
|
||||
* returns the tree node that was renumbered. You should save the subtree after this.
|
||||
* @example
|
||||
* ```ts
|
||||
* await nodeA.removeFromTree().saveSubtree()
|
||||
* ```
|
||||
*/
|
||||
public removeFromTree(): Maybe<TreeModel<T>> {
|
||||
const parent = this.parent()
|
||||
if ( parent ) {
|
||||
const ctor = this.constructor as typeof TreeModel
|
||||
parent.subtree().removeSubtree(this)
|
||||
parent.removedChildren.push(this)
|
||||
this.setColumn(ctor.leftTreeField, null)
|
||||
this.setColumn(ctor.rightTreeField, null)
|
||||
this.setColumn(ctor.parentIdField, null)
|
||||
return this.rootOrFail().renumber()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user