Basic support for lazy loaded relations
This commit is contained in:
parent
b5bde7d077
commit
2fd3c6c22b
@ -5,7 +5,7 @@ import {Statement} from './Statement.ts'
|
||||
import {Update} from './type/Update.ts'
|
||||
import {Insert} from './type/Insert.ts'
|
||||
import {Delete} from './type/Delete.ts'
|
||||
import {Truncate} from "./type/Truncate.ts";
|
||||
import {Truncate} from './type/Truncate.ts'
|
||||
|
||||
export function raw(value: string) {
|
||||
return new RawValue(value)
|
||||
|
@ -5,6 +5,7 @@ import {apply_filter_to_where, QueryFilter} from '../../model/filter.ts'
|
||||
import {Scope} from '../Scope.ts'
|
||||
import {FunctionScope, ScopeFunction} from '../scope/FunctionScope.ts'
|
||||
import {make} from '../../../../di/src/global.ts'
|
||||
import RawValue from '../RawValue.ts'
|
||||
|
||||
export class WhereBuilder {
|
||||
protected _wheres: WhereStatement[] = []
|
||||
@ -68,6 +69,16 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
whereRaw(field: string, operator: SQLWhereOperator, operand: string) {
|
||||
this._createWhere('AND', field, operator, new RawValue(operand))
|
||||
return this
|
||||
}
|
||||
|
||||
orWhereRaw(field: string, operator: SQLWhereOperator, operand: string) {
|
||||
this._createWhere('OR', field, operator, new RawValue(operand))
|
||||
return this
|
||||
}
|
||||
|
||||
whereIn(field: string, values: EscapedValue) {
|
||||
this._wheres.push({
|
||||
field,
|
||||
|
@ -10,7 +10,7 @@ export type WherePreOperator = 'AND' | 'OR' | 'AND NOT' | 'OR NOT'
|
||||
export type WhereClause = { field: string, operator: SQLWhereOperator, operand: string, preop: WherePreOperator }
|
||||
export type WhereGroup = { items: WhereStatement[], preop: WherePreOperator }
|
||||
export type WhereStatement = WhereClause | WhereGroup
|
||||
export type SQLWhereOperator = WhereOperator | 'IN' | 'NOT IN' | 'LIKE' | 'BETWEEN' | 'NOT BETWEEN'
|
||||
export type SQLWhereOperator = WhereOperator | 'IN' | 'NOT IN' | 'LIKE' | 'BETWEEN' | 'NOT BETWEEN' | 'IS' | 'IS NOT'
|
||||
export type OrderDirection = 'ASC' | 'DESC'
|
||||
export type OrderStatement = { direction: OrderDirection, field: string }
|
||||
|
||||
|
@ -12,6 +12,9 @@ import {QueryFilter} from './filter.ts'
|
||||
import {BehaviorSubject} from '../../../lib/src/support/BehaviorSubject.ts'
|
||||
import {Scope} from '../builder/Scope.ts'
|
||||
import {JSONState, Rehydratable} from '../../../lib/src/support/Rehydratable.ts'
|
||||
import {HasOne} from './relation/HasOne.ts'
|
||||
import Instantiable from '../../../di/src/type/Instantiable.ts'
|
||||
import {HasMany} from './relation/HasMany.ts'
|
||||
|
||||
// TODO separate read/write connections
|
||||
// TODO manual dirty flags
|
||||
@ -23,11 +26,16 @@ export type ModelJSONState = {
|
||||
ephemeral_values?: string,
|
||||
}
|
||||
|
||||
export type CachedRelation = {
|
||||
accessor_name: string | symbol,
|
||||
relation: HasOne<any, any>, // TODO generalize
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for database models.
|
||||
* @extends Builder
|
||||
*/
|
||||
export abstract class Model<T extends Model<T>> extends Builder<T> implements Rehydratable {
|
||||
abstract class Model<T extends Model<T>> extends Builder<T> implements Rehydratable {
|
||||
/**
|
||||
* The name of the connection this model should run through.
|
||||
* @type string
|
||||
@ -140,6 +148,8 @@ export abstract class Model<T extends Model<T>> extends Builder<T> implements Re
|
||||
*/
|
||||
protected deleted$ = new BehaviorSubject<Model<T>>()
|
||||
|
||||
public readonly relation_cache = new Collection<CachedRelation>()
|
||||
|
||||
/**
|
||||
* Get the model table name.
|
||||
* @return string
|
||||
@ -148,6 +158,17 @@ export abstract class Model<T extends Model<T>> extends Builder<T> implements Re
|
||||
return this.table
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query source for this model.
|
||||
* @return QuerySource
|
||||
*/
|
||||
public static query_source(): QuerySource {
|
||||
return {
|
||||
ref: this.table,
|
||||
alias: this.table,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model connection name.
|
||||
* @return string
|
||||
@ -861,6 +882,25 @@ export abstract class Model<T extends Model<T>> extends Builder<T> implements Re
|
||||
return `${this.table_name()}.${this.key}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a column name, qualify it with this model's table name.
|
||||
* @param {string} name
|
||||
* @return string
|
||||
*/
|
||||
public static qualify_column(name: string) {
|
||||
return `${this.table_name()}.${name}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a column name, qualify it with this model's table name.
|
||||
* @param {string} name
|
||||
* @return string
|
||||
*/
|
||||
public qualify_column(name: string) {
|
||||
const constructor = this.constructor as typeof Model
|
||||
return constructor.qualify_column(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Like to_object, but only the fields that have changed.
|
||||
* @return object
|
||||
@ -912,4 +952,26 @@ export abstract class Model<T extends Model<T>> extends Builder<T> implements Re
|
||||
this.assume(JSON.parse(state.ephemeral_values))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new HasOne relation for this model.
|
||||
* @param {typeof Model} related - related model class
|
||||
* @param {string} [foreign_key] - the key to match on the foreign table
|
||||
* @param {string} [local_key] - the key to match on the local table
|
||||
*/
|
||||
public has_one<T>(related: Instantiable<T>, foreign_key?: string, local_key?: string) {
|
||||
return new HasOne(this as any, new related() as any, foreign_key, local_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new HasMany relation for this model.
|
||||
* @param {typeof Model} related - related model class
|
||||
* @param {string} [foreign_key] - the key to match on the foreign table
|
||||
* @param {string} [local_key] - the key to match on the local table
|
||||
*/
|
||||
public has_many<T>(related: Instantiable<T>, foreign_key?: string, local_key?: string) {
|
||||
return new HasMany(this as any, new related() as any, foreign_key, local_key)
|
||||
}
|
||||
}
|
||||
|
||||
export { Model }
|
||||
|
19
orm/src/model/RelationResultOperator.ts
Normal file
19
orm/src/model/RelationResultOperator.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import ModelResultOperator from "./ModelResultOperator.ts";
|
||||
import {Model} from "./Model.ts";
|
||||
import Instantiable from "../../../di/src/type/Instantiable.ts";
|
||||
import {HasOne} from "./relation/HasOne.ts";
|
||||
import {QueryRow} from "../db/types.ts";
|
||||
|
||||
export default class RelationResultOperator<T extends Model<T>> extends ModelResultOperator<T> {
|
||||
constructor(
|
||||
protected ModelClass: Instantiable<T>,
|
||||
protected relation: HasOne<any, T>,
|
||||
) {
|
||||
super(ModelClass)
|
||||
}
|
||||
|
||||
inflate_row(row: QueryRow): T {
|
||||
const instance = super.inflate_row(row)
|
||||
return instance
|
||||
}
|
||||
}
|
9
orm/src/model/relation/HasMany.ts
Normal file
9
orm/src/model/relation/HasMany.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {Model} from '../Model.ts'
|
||||
import {HasOneOrMany} from './HasOneOrMany.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
|
||||
export class HasMany<T extends Model<T>, T2 extends Model<T2>> extends HasOneOrMany<T, T2> {
|
||||
public async get(): Promise<Collection<T2>> {
|
||||
return this.fetch().collect()
|
||||
}
|
||||
}
|
8
orm/src/model/relation/HasOne.ts
Normal file
8
orm/src/model/relation/HasOne.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {Model} from '../Model.ts'
|
||||
import {HasOneOrMany} from './HasOneOrMany.ts'
|
||||
|
||||
export class HasOne<T extends Model<T>, T2 extends Model<T2>> extends HasOneOrMany<T, T2> {
|
||||
public async get(): Promise<T2 | undefined> {
|
||||
return this.fetch().first()
|
||||
}
|
||||
}
|
45
orm/src/model/relation/HasOneOrMany.ts
Normal file
45
orm/src/model/relation/HasOneOrMany.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {Model} from "../Model.ts";
|
||||
import {Relation} from "./Relation.ts";
|
||||
import ConnectionExecutable from "../../builder/type/ConnectionExecutable.ts";
|
||||
import {WhereBuilder} from "../../builder/type/WhereBuilder.ts";
|
||||
|
||||
export abstract class HasOneOrMany<T extends Model<T>, T2 extends Model<T2>> extends Relation<T, T2> {
|
||||
constructor(
|
||||
protected parent: T,
|
||||
public readonly related: T2,
|
||||
protected foreign_key_spec?: string,
|
||||
protected local_key_spec?: string,
|
||||
) { super(parent, related) }
|
||||
|
||||
public get foreign_key() {
|
||||
return this.foreign_key_spec || this.parent.key_name()
|
||||
}
|
||||
|
||||
public get local_key() {
|
||||
return this.local_key_spec || this.foreign_key
|
||||
}
|
||||
|
||||
public get qualified_foreign_key() {
|
||||
return this.related.qualify_column(this.foreign_key)
|
||||
}
|
||||
|
||||
public get qualified_local_key() {
|
||||
return this.related.qualify_column(this.local_key)
|
||||
}
|
||||
|
||||
protected get parent_value() {
|
||||
// @ts-ignore
|
||||
return this.parent[this.local_key]
|
||||
}
|
||||
|
||||
public query(): ConnectionExecutable<T2> {
|
||||
return this.builder().select('*')
|
||||
}
|
||||
|
||||
public scope_query(where: WhereBuilder) {
|
||||
where.where(where => {
|
||||
where.where(this.qualified_foreign_key, '=', this.parent_value)
|
||||
.whereRaw(this.qualified_foreign_key, 'IS NOT', 'NULL')
|
||||
})
|
||||
}
|
||||
}
|
66
orm/src/model/relation/Relation.ts
Normal file
66
orm/src/model/relation/Relation.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import {Model} from '../Model.ts'
|
||||
import AppClass from '../../../../lib/src/lifecycle/AppClass.ts'
|
||||
import RelationResultOperator from '../RelationResultOperator.ts'
|
||||
import ConnectionExecutable from '../../builder/type/ConnectionExecutable.ts'
|
||||
import {AsyncCollection} from '../../../../lib/src/collection/AsyncCollection.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
import {WhereBuilder} from '../../builder/type/WhereBuilder.ts'
|
||||
import {RelationBuilder} from './RelationBuilder.ts'
|
||||
import {Select} from "../../builder/type/Select.ts";
|
||||
import {FieldSet} from "../../builder/types.ts";
|
||||
import {Update} from "../../builder/type/Update.ts";
|
||||
import {Delete} from "../../builder/type/Delete.ts";
|
||||
|
||||
export type RelationResult<T> = T | Collection<T> | undefined
|
||||
|
||||
export abstract class Relation<T extends Model<T>, T2 extends Model<T2>> extends AppClass {
|
||||
constructor(
|
||||
protected parent: T,
|
||||
public readonly related: T2,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected abstract get parent_value(): any
|
||||
|
||||
public get_operator() {
|
||||
const related_class = this.related.constructor as typeof Model
|
||||
return this.make(RelationResultOperator, related_class, this)
|
||||
}
|
||||
|
||||
public abstract query(): ConnectionExecutable<T2>
|
||||
|
||||
public abstract scope_query(where: WhereBuilder): void
|
||||
|
||||
public fetch(): AsyncCollection<T2> {
|
||||
return this.query().results()
|
||||
}
|
||||
|
||||
public abstract get(): Promise<RelationResult<T2>>
|
||||
|
||||
public then(callback: (result: RelationResult<T2>) => any) {
|
||||
this.get().then(callback)
|
||||
}
|
||||
|
||||
public get related_query_source() {
|
||||
const related_class = this.related.constructor as typeof Model
|
||||
return related_class.query_source()
|
||||
}
|
||||
|
||||
public builder(): RelationBuilder<T2> {
|
||||
return new RelationBuilder(this)
|
||||
}
|
||||
|
||||
public select(...fields: FieldSet[]): Select<T2> {
|
||||
if ( fields.length < 1 ) fields.push(this.related.qualify_column('*'))
|
||||
return this.builder().select(...fields)
|
||||
}
|
||||
|
||||
public update(): Update<T2> {
|
||||
return this.builder().update()
|
||||
}
|
||||
|
||||
public delete(): Delete<T2> {
|
||||
return this.builder().delete()
|
||||
}
|
||||
}
|
40
orm/src/model/relation/RelationBuilder.ts
Normal file
40
orm/src/model/relation/RelationBuilder.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import {Builder} from '../../builder/Builder.ts'
|
||||
import {Select} from '../../builder/type/Select.ts'
|
||||
import {Relation} from './Relation.ts'
|
||||
import {Update} from '../../builder/type/Update.ts'
|
||||
import {FieldSet, QuerySource} from '../../builder/types.ts'
|
||||
import {Delete} from '../../builder/type/Delete.ts'
|
||||
import {Model} from '../Model.ts'
|
||||
|
||||
export class RelationBuilder<T extends Model<T>> extends Builder<T> {
|
||||
constructor(
|
||||
protected relation: Relation<any, T>
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public select(...fields: FieldSet[]): Select<T> {
|
||||
const select = this.relation.related.select(...fields)
|
||||
.from(this.relation.related_query_source)
|
||||
|
||||
select.target_operator(this.relation.get_operator())
|
||||
this.relation.scope_query(select)
|
||||
return select
|
||||
}
|
||||
|
||||
public update(target?: QuerySource, alias?: string): Update<T> {
|
||||
const update = this.relation.related.update(this.relation.related_query_source)
|
||||
|
||||
update.target_operator(this.relation.get_operator())
|
||||
this.relation.scope_query(update)
|
||||
return update
|
||||
}
|
||||
|
||||
public delete(target?: QuerySource, alias?: string): Delete<T> {
|
||||
const delete_query = this.relation.related.delete(this.relation.related_query_source)
|
||||
|
||||
delete_query.target_operator(this.relation.get_operator())
|
||||
this.relation.scope_query(delete_query)
|
||||
return delete_query
|
||||
}
|
||||
}
|
28
orm/src/model/relation/decorators.ts
Normal file
28
orm/src/model/relation/decorators.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {Model} from '../Model.ts'
|
||||
|
||||
export function Relation(): MethodDecorator {
|
||||
return (target: any, propertyKey, descriptor) => {
|
||||
console.log('relation decorator', target, propertyKey, descriptor)
|
||||
// @ts-ignore
|
||||
const original = descriptor.value
|
||||
|
||||
// @ts-ignore
|
||||
descriptor.value = function(...args) {
|
||||
const model = this as Model<any>
|
||||
const cache = model.relation_cache
|
||||
|
||||
const existing_relation = cache.firstWhere('accessor_name', '=', propertyKey)
|
||||
if ( existing_relation ) return existing_relation.relation
|
||||
|
||||
// @ts-ignore
|
||||
const value = original.apply(this, args)
|
||||
|
||||
cache.push({
|
||||
accessor_name: propertyKey,
|
||||
relation: value,
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user