finish TreeModel implementation and updateMany builder method
This commit is contained in:
		
							parent
							
								
									f63891ef99
								
							
						
					
					
						commit
						c966904418
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,3 +1,5 @@
 | 
			
		||||
.undodir
 | 
			
		||||
 | 
			
		||||
# ---> JetBrains
 | 
			
		||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
 | 
			
		||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@extollo/lib",
 | 
			
		||||
  "version": "0.14.0",
 | 
			
		||||
  "version": "0.14.1",
 | 
			
		||||
  "description": "The framework library that lifts up your code.",
 | 
			
		||||
  "main": "lib/index.js",
 | 
			
		||||
  "types": "lib/index.d.ts",
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,8 @@ import {
 | 
			
		||||
    SpecifiedField,
 | 
			
		||||
} from '../types'
 | 
			
		||||
import {Connection} from '../connection/Connection'
 | 
			
		||||
import {deepCopy, ErrorWithContext, Maybe} from '../../util'
 | 
			
		||||
import {EscapeValue, QuerySafeValue, raw} from '../dialect/SQLDialect'
 | 
			
		||||
import {Collectable, deepCopy, ErrorWithContext, Maybe} from '../../util'
 | 
			
		||||
import {EscapeValue, QuerySafeValue, raw, ScalarEscapeValue, VectorEscapeValue} from '../dialect/SQLDialect'
 | 
			
		||||
import {ResultCollection} from './result/ResultCollection'
 | 
			
		||||
import {AbstractResultIterable} from './result/AbstractResultIterable'
 | 
			
		||||
import {AppClass} from '../../lifecycle/AppClass'
 | 
			
		||||
@ -310,7 +310,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
 | 
			
		||||
     * @param field
 | 
			
		||||
     * @param values
 | 
			
		||||
     */
 | 
			
		||||
    whereIn(field: string, values: EscapeValue): this {
 | 
			
		||||
    whereIn<TConstraint extends ScalarEscapeValue>(field: string, values: VectorEscapeValue<TConstraint>): this {
 | 
			
		||||
        this.constraints.push({
 | 
			
		||||
            field,
 | 
			
		||||
            operator: 'IN',
 | 
			
		||||
@ -325,7 +325,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
 | 
			
		||||
     * @param field
 | 
			
		||||
     * @param values
 | 
			
		||||
     */
 | 
			
		||||
    whereNotIn(field: string, values: EscapeValue): this {
 | 
			
		||||
    whereNotIn<TConstraint extends ScalarEscapeValue>(field: string, values: VectorEscapeValue<TConstraint>): this {
 | 
			
		||||
        this.constraints.push({
 | 
			
		||||
            field,
 | 
			
		||||
            operator: 'NOT IN',
 | 
			
		||||
@ -340,7 +340,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
 | 
			
		||||
     * @param field
 | 
			
		||||
     * @param values
 | 
			
		||||
     */
 | 
			
		||||
    orWhereIn(field: string, values: EscapeValue): this {
 | 
			
		||||
    orWhereIn<TConstraint extends ScalarEscapeValue>(field: string, values: VectorEscapeValue<TConstraint>): this {
 | 
			
		||||
        this.constraints.push({
 | 
			
		||||
            field,
 | 
			
		||||
            operator: 'IN',
 | 
			
		||||
@ -355,7 +355,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
 | 
			
		||||
     * @param field
 | 
			
		||||
     * @param values
 | 
			
		||||
     */
 | 
			
		||||
    orWhereNotIn(field: string, values: EscapeValue): this {
 | 
			
		||||
    orWhereNotIn<TConstraint extends ScalarEscapeValue>(field: string, values: VectorEscapeValue<TConstraint>): this {
 | 
			
		||||
        this.constraints.push({
 | 
			
		||||
            field,
 | 
			
		||||
            operator: 'NOT IN',
 | 
			
		||||
@ -528,6 +528,35 @@ export abstract class AbstractBuilder<T> extends AppClass {
 | 
			
		||||
        return this.registeredConnection.query(query)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Run a batch update on all rows matched by this query, setting the values for discrete
 | 
			
		||||
     * rows based on some key.
 | 
			
		||||
     *
 | 
			
		||||
     * This is a more efficient way of combining discrete update queries.
 | 
			
		||||
     *
 | 
			
		||||
     * @example
 | 
			
		||||
     * ```ts
 | 
			
		||||
     * query.table('my_table')
 | 
			
		||||
     *  .updateMany('id_col', [
 | 
			
		||||
     *      {id_col: 1, val1_col: 'a'},
 | 
			
		||||
     *      {id_col: 2, val2_col: 'b'},
 | 
			
		||||
     *  ])
 | 
			
		||||
     * ```
 | 
			
		||||
     *
 | 
			
		||||
     * This will set the `val1_col` to `a` for rows where `id_col` is `1` and so on.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key
 | 
			
		||||
     * @param rows
 | 
			
		||||
     */
 | 
			
		||||
    async updateMany(key: string, rows: Collectable<{[key: string]: EscapeValue}>): Promise<QueryResult> {
 | 
			
		||||
        if ( !this.registeredConnection ) {
 | 
			
		||||
            throw new ErrorWithContext(`No connection specified to execute update query.`)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const query = this.registeredConnection.dialect().renderBatchUpdate(this, key, rows)
 | 
			
		||||
        return this.registeredConnection.query(query)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute a DELETE based on this query.
 | 
			
		||||
     *
 | 
			
		||||
@ -600,6 +629,15 @@ export abstract class AbstractBuilder<T> extends AppClass {
 | 
			
		||||
        return Boolean(result.rows.first())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Render the query as a string. */
 | 
			
		||||
    toString(): string {
 | 
			
		||||
        if ( !this.registeredConnection ) {
 | 
			
		||||
            throw new ErrorWithContext('No connection specified to render query.')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.registeredConnection.dialect().renderSelect(this.finalize())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the query manually. Overrides any builder methods.
 | 
			
		||||
     * @example
 | 
			
		||||
@ -615,6 +653,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
 | 
			
		||||
        return this
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Pass this instance into a callback, then return this instance for chaining. */
 | 
			
		||||
    tap(callback: (inst: this) => unknown): this {
 | 
			
		||||
        callback(this)
 | 
			
		||||
        return this
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adds a constraint to this query. This is used internally by the various `where`, `whereIn`, `orWhereNot`, &c.
 | 
			
		||||
     * @param preop
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect'
 | 
			
		||||
import {Constraint, inverseFieldType, isConstraintGroup, isConstraintItem, SpecifiedField} from '../types'
 | 
			
		||||
import {AbstractBuilder} from '../builder/AbstractBuilder'
 | 
			
		||||
import {ColumnBuilder, ConstraintBuilder, ConstraintType, IndexBuilder, TableBuilder} from '../schema/TableBuilder'
 | 
			
		||||
import {ErrorWithContext, Maybe} from '../../util'
 | 
			
		||||
import {collect, Collectable, Collection, ErrorWithContext, hasOwnProperty, Maybe} from '../../util'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An implementation of the SQLDialect specific to PostgreSQL.
 | 
			
		||||
@ -14,7 +14,7 @@ export class PostgreSQLDialect extends SQLDialect {
 | 
			
		||||
    public escape(value: EscapeValue): QuerySafeValue {
 | 
			
		||||
        if ( value instanceof QuerySafeValue ) {
 | 
			
		||||
            return value
 | 
			
		||||
        } else if ( Array.isArray(value) ) {
 | 
			
		||||
        } else if ( Array.isArray(value) || value instanceof Collection ) {
 | 
			
		||||
            return new QuerySafeValue(value, `(${value.map(v => this.escape(v)).join(',')})`)
 | 
			
		||||
        } else if ( String(value).toLowerCase() === 'true' || value === true ) {
 | 
			
		||||
            return new QuerySafeValue(value, 'TRUE')
 | 
			
		||||
@ -34,10 +34,10 @@ export class PostgreSQLDialect extends SQLDialect {
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
            return new QuerySafeValue(value, `'${y}-${m}-${d} ${h}:${i}:${s}'`)
 | 
			
		||||
        } else if ( !isNaN(Number(value)) ) {
 | 
			
		||||
            return new QuerySafeValue(value, String(Number(value)))
 | 
			
		||||
        } else if ( value === null || typeof value === 'undefined' ) {
 | 
			
		||||
            return new QuerySafeValue(value, 'NULL')
 | 
			
		||||
        } else if ( !isNaN(Number(value)) ) {
 | 
			
		||||
            return new QuerySafeValue(value, String(Number(value)))
 | 
			
		||||
        } else {
 | 
			
		||||
            const escaped = value.replace(/'/g, '\'\'')  // .replace(/"/g, '\\"').replace(/`/g, '\\`')
 | 
			
		||||
            return new QuerySafeValue(value, `'${escaped}'`)
 | 
			
		||||
@ -154,6 +154,78 @@ export class PostgreSQLDialect extends SQLDialect {
 | 
			
		||||
        return queryLines.join('\n')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public renderBatchUpdate(builder: AbstractBuilder<any>, primaryKey: string, dataRows: Collectable<{[key: string]: EscapeValue}>): string {
 | 
			
		||||
        const rows = Collection.normalize(dataRows)
 | 
			
		||||
        const rawSql = builder.appliedRawSql
 | 
			
		||||
        if ( rawSql ) {
 | 
			
		||||
            return rawSql
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const queryLines: string[] = []
 | 
			
		||||
 | 
			
		||||
        // Add table source
 | 
			
		||||
        const source = builder.querySource
 | 
			
		||||
        let sourceAlias = 'extollo_update_source'
 | 
			
		||||
        if ( !source ) {
 | 
			
		||||
            throw new ErrorWithContext('No table specified for update query')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tableString = typeof source === 'string' ? source : source.table
 | 
			
		||||
        const table: string = tableString.split('.')
 | 
			
		||||
            .map(x => `"${x}"`)
 | 
			
		||||
            .join('.')
 | 
			
		||||
 | 
			
		||||
        if ( typeof source !== 'string' && source.alias ) {
 | 
			
		||||
            sourceAlias = source.alias
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        queryLines.push('UPDATE ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
 | 
			
		||||
 | 
			
		||||
        queryLines.push('SET')
 | 
			
		||||
        const updateFields = this.getAllFieldsFromUpdateRows(rows)
 | 
			
		||||
 | 
			
		||||
        const updateTuples = rows.map(row => {
 | 
			
		||||
            return updateFields.map(field => {
 | 
			
		||||
                if ( hasOwnProperty(row, field) ) {
 | 
			
		||||
                    return this.escape(row[field])
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // FIXME: This is fairly inefficient. Probably a better way with a FROM ... SELECT
 | 
			
		||||
                // return raw(`"${sourceAlias}"."${field}"`)
 | 
			
		||||
                return raw(`(SELECT "${field}" FROM ${table} WHERE "${primaryKey}" = ${this.escape(row[primaryKey])})`)
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        queryLines.push(updateFields.map(field => `  "${field}" = "extollo_update_tuple"."${field}"`).join(',\n'))
 | 
			
		||||
 | 
			
		||||
        queryLines.push('FROM (VALUES')
 | 
			
		||||
 | 
			
		||||
        queryLines.push(
 | 
			
		||||
            updateTuples.map(tuple => `  (${tuple.implode(', ')})`).join(',\n'),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        queryLines.push(`) as extollo_update_tuple(${updateFields.map(x => `"${x}"`).join(', ')})`)
 | 
			
		||||
 | 
			
		||||
        queryLines.push(`WHERE "extollo_update_tuple"."${primaryKey}" = "${sourceAlias}"."${primaryKey}" AND (`)
 | 
			
		||||
 | 
			
		||||
        queryLines.push(this.renderConstraints(builder.appliedConstraints, 2))
 | 
			
		||||
 | 
			
		||||
        queryLines.push(`)`)
 | 
			
		||||
 | 
			
		||||
        return queryLines.join('\n')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private getAllFieldsFromUpdateRows(rows: Collection<{[key: string]: EscapeValue}>): Collection<string> {
 | 
			
		||||
        return rows.reduce((fields: Collection<string>, row) => {
 | 
			
		||||
            Object.keys(row).forEach(key => {
 | 
			
		||||
                if ( !fields.includes(key) ) {
 | 
			
		||||
                    fields.push(key)
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            return fields
 | 
			
		||||
        }, collect<string>())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO support FROM, RETURNING
 | 
			
		||||
    public renderUpdate(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}): string {
 | 
			
		||||
        const rawSql = builder.appliedRawSql
 | 
			
		||||
@ -181,6 +253,14 @@ export class PostgreSQLDialect extends SQLDialect {
 | 
			
		||||
            queryLines.push(wheres)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const fields = this.renderFields(builder).map(x => `  ${x}`)
 | 
			
		||||
            .join(',\n')
 | 
			
		||||
 | 
			
		||||
        if ( fields ) {
 | 
			
		||||
            queryLines.push('RETURNING')
 | 
			
		||||
            queryLines.push(fields)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return queryLines.join('\n')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -298,8 +378,8 @@ export class PostgreSQLDialect extends SQLDialect {
 | 
			
		||||
        return queryLines.join('\n')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public renderConstraints(allConstraints: Constraint[]): string {
 | 
			
		||||
        const constraintsToSql = (constraints: Constraint[], level = 1): string => {
 | 
			
		||||
    public renderConstraints(allConstraints: Constraint[], startingLevel = 1): string {
 | 
			
		||||
        const constraintsToSql = (constraints: Constraint[], level = startingLevel): string => {
 | 
			
		||||
            const indent = Array(level * 2).fill(' ')
 | 
			
		||||
                .join('')
 | 
			
		||||
            const statements = []
 | 
			
		||||
 | 
			
		||||
@ -2,15 +2,18 @@ import {Constraint} from '../types'
 | 
			
		||||
import {AbstractBuilder} from '../builder/AbstractBuilder'
 | 
			
		||||
import {AppClass} from '../../lifecycle/AppClass'
 | 
			
		||||
import {ColumnBuilder, IndexBuilder, TableBuilder} from '../schema/TableBuilder'
 | 
			
		||||
import {Collectable, Collection} from '../../util'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A value which can be escaped to be interpolated into an SQL query.
 | 
			
		||||
 */
 | 
			
		||||
export type EscapeValue = null | undefined | string | number | boolean | Date | QuerySafeValue | EscapeValue[] // FIXME | Select<any>
 | 
			
		||||
/** A scalar value which can be interpolated safely into an SQL query. */
 | 
			
		||||
export type ScalarEscapeValue = null | undefined | string | number | boolean | Date | QuerySafeValue;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Object mapping string field names to EscapeValue items.
 | 
			
		||||
 */
 | 
			
		||||
/** A list of scalar escape values. */
 | 
			
		||||
export type VectorEscapeValue<T extends ScalarEscapeValue> = T[] | Collection<T>
 | 
			
		||||
 | 
			
		||||
/** All possible escaped query values. */
 | 
			
		||||
export type EscapeValue<T extends ScalarEscapeValue = ScalarEscapeValue> = T | VectorEscapeValue<T> // FIXME | Select<any>
 | 
			
		||||
 | 
			
		||||
/** Object mapping string field names to EscapeValue items. */
 | 
			
		||||
export type EscapeValueObject = { [field: string]: EscapeValue }
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -70,6 +73,15 @@ export abstract class SQLDialect extends AppClass {
 | 
			
		||||
     */
 | 
			
		||||
    public abstract renderUpdate(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render the given query builder as an "UPDATE ..." query string, setting column values
 | 
			
		||||
     * for multiple distinct records based on their primary key.
 | 
			
		||||
     * @param builder
 | 
			
		||||
     * @param primaryKey
 | 
			
		||||
     * @param dataRows
 | 
			
		||||
     */
 | 
			
		||||
    public abstract renderBatchUpdate(builder: AbstractBuilder<any>, primaryKey: string, dataRows: Collectable<{[key: string]: EscapeValue}>): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render the given query builder as a "DELETE ..." query string.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ export * from './model/relation/HasOneOrMany'
 | 
			
		||||
export * from './model/relation/HasOne'
 | 
			
		||||
export * from './model/relation/HasMany'
 | 
			
		||||
export * from './model/relation/HasSubtree'
 | 
			
		||||
export * from './model/relation/HasTreeParent'
 | 
			
		||||
export * from './model/relation/decorators'
 | 
			
		||||
 | 
			
		||||
export * from './model/scope/Scope'
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import {Container, Inject, Instantiable, isInstantiable} from '../../di'
 | 
			
		||||
import {DatabaseService} from '../DatabaseService'
 | 
			
		||||
import {ModelBuilder} from './ModelBuilder'
 | 
			
		||||
import {getFieldsMeta, ModelField} from './Field'
 | 
			
		||||
import {deepCopy, Collection, uuid4, isKeyof, Pipeline} from '../../util'
 | 
			
		||||
import {deepCopy, Collection, uuid4, isKeyof, Pipeline, hasOwnProperty} from '../../util'
 | 
			
		||||
import {EscapeValueObject} from '../dialect/SQLDialect'
 | 
			
		||||
import {Logging} from '../../service/Logging'
 | 
			
		||||
import {Connection} from '../connection/Connection'
 | 
			
		||||
@ -96,6 +96,13 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
     */
 | 
			
		||||
    protected originalSourceRow?: QueryRow
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Database fields that should be run on the next save, even if the
 | 
			
		||||
     * fields are not mapped to members on the model.
 | 
			
		||||
     * @protected
 | 
			
		||||
     */
 | 
			
		||||
    protected dirtySourceRow?: QueryRow
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Cache of relation instances by property accessor.
 | 
			
		||||
     * This is used by the `@Relation()` decorator to cache Relation instances.
 | 
			
		||||
@ -175,8 +182,7 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        builder.withScopes(inst.scopes)
 | 
			
		||||
 | 
			
		||||
        inst.applyScopes(builder)
 | 
			
		||||
        return builder
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -214,6 +220,49 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public getColumn(key: string): unknown {
 | 
			
		||||
        if ( this.dirtySourceRow && hasOwnProperty(this.dirtySourceRow, key) ) {
 | 
			
		||||
            return this.dirtySourceRow[key]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const field = getFieldsMeta(this)
 | 
			
		||||
            .firstWhere('databaseKey', '=', key)
 | 
			
		||||
 | 
			
		||||
        if ( field ) {
 | 
			
		||||
            return (this as any)[field.modelKey]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.originalSourceRow?.[key]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets a value in the override row and (if applicable) associated class property
 | 
			
		||||
     * for the given database column.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key
 | 
			
		||||
     * @param value
 | 
			
		||||
     */
 | 
			
		||||
    public setColumn(key: string, value: unknown): this {
 | 
			
		||||
        // Set the property on the database result row, if one exists
 | 
			
		||||
        if ( !this.dirtySourceRow ) {
 | 
			
		||||
            this.dirtySourceRow = {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dirtySourceRow[key] = value
 | 
			
		||||
 | 
			
		||||
        // Set the property on the mapped field on the class, if one exists
 | 
			
		||||
        const field = getFieldsMeta(this)
 | 
			
		||||
            .firstWhere('databaseKey', '=', key)
 | 
			
		||||
 | 
			
		||||
        if ( field ) {
 | 
			
		||||
            this.setFieldFromObject(field.modelKey, field.databaseKey, {
 | 
			
		||||
                [key]: value,
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a row from the database, set the properties on this model that correspond to
 | 
			
		||||
     * fields on that database.
 | 
			
		||||
@ -256,7 +305,7 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the value of the primary key of this model, if it exists.
 | 
			
		||||
     */
 | 
			
		||||
    public key(): string {
 | 
			
		||||
    public key(): string|number {
 | 
			
		||||
        const ctor = this.constructor as typeof Model
 | 
			
		||||
 | 
			
		||||
        const field = getFieldsMeta(this)
 | 
			
		||||
@ -333,8 +382,7 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
            builder.with(relation)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        builder.withScopes(this.scopes)
 | 
			
		||||
 | 
			
		||||
        this.applyScopes(builder)
 | 
			
		||||
        return builder
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -445,6 +493,7 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
     * @param modelKey
 | 
			
		||||
     */
 | 
			
		||||
    public static propertyToColumn(modelKey: string): string {
 | 
			
		||||
        console.log('propertyToColumn', modelKey, getFieldsMeta(this), this)  // eslint-disable-line no-console
 | 
			
		||||
        return getFieldsMeta(this)
 | 
			
		||||
            .firstWhere('modelKey', '=', modelKey)?.databaseKey || modelKey
 | 
			
		||||
    }
 | 
			
		||||
@ -487,7 +536,10 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
                row[field.databaseKey] = (this as any)[field.modelKey]
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        return row
 | 
			
		||||
        return {
 | 
			
		||||
            ...row,
 | 
			
		||||
            ...(this.dirtySourceRow || {}),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -621,10 +673,12 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const data = result.rows.firstWhere(this.keyName(), '=', this.key())
 | 
			
		||||
            this.logging.debug({updata: data})
 | 
			
		||||
            if ( data ) {
 | 
			
		||||
                await this.assumeFromSource(data)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            delete this.dirtySourceRow
 | 
			
		||||
            await this.push(new ModelUpdatedEvent<T>(this as any))
 | 
			
		||||
        } else if ( !this.exists() ) {
 | 
			
		||||
            await this.push(new ModelCreatingEvent<T>(this as any))
 | 
			
		||||
@ -654,10 +708,12 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const data = result.rows.first()
 | 
			
		||||
            this.logging.debug({inserta: data})
 | 
			
		||||
            if ( data ) {
 | 
			
		||||
                await this.assumeFromSource(data)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            delete this.dirtySourceRow
 | 
			
		||||
            await this.push(new ModelCreatedEvent<T>(this as any))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -808,7 +864,14 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
     */
 | 
			
		||||
    protected get isDirtyCheck(): (field: ModelField) => boolean {
 | 
			
		||||
        return (field: ModelField) => {
 | 
			
		||||
            return !this.originalSourceRow || (this as any)[field.modelKey] !== this.originalSourceRow[field.databaseKey]
 | 
			
		||||
            return Boolean(
 | 
			
		||||
                !this.originalSourceRow
 | 
			
		||||
                || (this as any)[field.modelKey] !== this.originalSourceRow[field.databaseKey]
 | 
			
		||||
                || (
 | 
			
		||||
                    this.dirtySourceRow
 | 
			
		||||
                    && hasOwnProperty(this.dirtySourceRow, field.databaseKey)
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -833,12 +896,17 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
 | 
			
		||||
        this.logging.debug(`buildInsertFieldObject populateKeyOnInsert? ${ctor.populateKeyOnInsert}; keyName: ${this.keyName()}`)
 | 
			
		||||
 | 
			
		||||
        return Pipeline.id<Collection<ModelField>>()
 | 
			
		||||
        const row = Pipeline.id<Collection<ModelField>>()
 | 
			
		||||
            .unless(ctor.populateKeyOnInsert, fields => {
 | 
			
		||||
                return fields.where('databaseKey', '!=', this.keyName())
 | 
			
		||||
            })
 | 
			
		||||
            .apply(getFieldsMeta(this))
 | 
			
		||||
            .keyMap('databaseKey', inst => (this as any)[inst.modelKey])
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            ...row,
 | 
			
		||||
            ...(this.dirtySourceRow || {}),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -918,7 +986,7 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
     * @param related
 | 
			
		||||
     * @param relationName
 | 
			
		||||
     */
 | 
			
		||||
    public belongsToOne<T2 extends Model<T2>>(related: Instantiable<T>, relationName: keyof T2): HasOne<T, T2> {
 | 
			
		||||
    public belongsToOne<T2 extends Model<T2>>(related: Instantiable<T2>, relationName: keyof T2): HasOne<T, T2> {
 | 
			
		||||
        const relatedInst = this.make(related) as T2
 | 
			
		||||
        const relation = relatedInst.getRelation(relationName)
 | 
			
		||||
 | 
			
		||||
@ -1055,4 +1123,12 @@ export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>>
 | 
			
		||||
    protected hasScope(name: string | Instantiable<Scope>): boolean {
 | 
			
		||||
        return Boolean(this.scopes.firstWhere('accessor', '=', name))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Apply the default scopes to this model to the given query builder.
 | 
			
		||||
     * @param builder
 | 
			
		||||
     */
 | 
			
		||||
    public applyScopes(builder: ModelBuilder<T>): void {
 | 
			
		||||
        builder.withScopes(this.scopes)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Collection } from '../util'
 | 
			
		||||
import {EscapeValue, QuerySafeValue} from './dialect/SQLDialect'
 | 
			
		||||
import {QuerySafeValue, VectorEscapeValue} from './dialect/SQLDialect'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A single query row, as an object.
 | 
			
		||||
@ -40,7 +40,7 @@ export type ConstraintOperator = '&' | '>' | '>=' | '<' | '<=' | '!=' | '<=>' |
 | 
			
		||||
export interface ConstraintItem {
 | 
			
		||||
    field: string,
 | 
			
		||||
    operator: ConstraintOperator,
 | 
			
		||||
    operand: EscapeValue,
 | 
			
		||||
    operand: VectorEscapeValue<any>,
 | 
			
		||||
    preop: ConstraintConnectionOperator,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ type DeterminesEquality<T> = (item: CollectionItem<T>, other: any) => boolean
 | 
			
		||||
type CollectionIndex = number
 | 
			
		||||
type MaybeCollectionIndex = CollectionIndex | undefined
 | 
			
		||||
type ComparisonFunction<T> = (item: CollectionItem<T>, otherItem: CollectionItem<T>) => number
 | 
			
		||||
type Collectable<T> = CollectionItem<T>[] | Collection<T>
 | 
			
		||||
 | 
			
		||||
import { WhereOperator, applyWhere, whereMatch } from './where'
 | 
			
		||||
import {Awaitable, Awaited, Either, isLeft, Maybe, MethodsOf, MethodType, right, unright} from '../support/types'
 | 
			
		||||
@ -39,6 +40,7 @@ export {
 | 
			
		||||
    CollectionIndex,
 | 
			
		||||
    MaybeCollectionIndex,
 | 
			
		||||
    ComparisonFunction,
 | 
			
		||||
    Collectable,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -61,13 +63,16 @@ class Collection<T> {
 | 
			
		||||
     * Filters out undefined items.
 | 
			
		||||
     * @param itemOrItems
 | 
			
		||||
     */
 | 
			
		||||
    public static normalize<T2>(itemOrItems: (CollectionItem<T2> | undefined)[] | CollectionItem<T2> | undefined): Collection<T2> {
 | 
			
		||||
    public static normalize<T2>(itemOrItems: Collection<T2> | (CollectionItem<T2>)[] | CollectionItem<T2>): Collection<T2> {
 | 
			
		||||
        if ( itemOrItems instanceof Collection ) {
 | 
			
		||||
            return itemOrItems
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ( !Array.isArray(itemOrItems) ) {
 | 
			
		||||
            itemOrItems = [itemOrItems]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const items = itemOrItems.filter(x => typeof x !== 'undefined') as CollectionItem<T2>[]
 | 
			
		||||
        return new Collection<T2>(items)
 | 
			
		||||
        return new Collection<T2>(itemOrItems)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1041,6 +1046,8 @@ class Collection<T> {
 | 
			
		||||
     * @param reducer
 | 
			
		||||
     * @param initialValue
 | 
			
		||||
     */
 | 
			
		||||
    reduce<T2>(reducer: KeyReducerFunction<T, T2>, initialValue: T2): T2
 | 
			
		||||
 | 
			
		||||
    reduce<T2>(reducer: KeyReducerFunction<T, T2>, initialValue?: T2): T2 | undefined {
 | 
			
		||||
        let currentValue = initialValue
 | 
			
		||||
        this.storedItems.forEach((item, index) => {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user