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 {Update} from './type/Update.ts'
|
||||||
import {Insert} from './type/Insert.ts'
|
import {Insert} from './type/Insert.ts'
|
||||||
import {Delete} from './type/Delete.ts'
|
import {Delete} from './type/Delete.ts'
|
||||||
import {Truncate} from "./type/Truncate.ts";
|
import {Truncate} from './type/Truncate.ts'
|
||||||
|
|
||||||
export function raw(value: string) {
|
export function raw(value: string) {
|
||||||
return new RawValue(value)
|
return new RawValue(value)
|
||||||
|
@ -5,6 +5,7 @@ import {apply_filter_to_where, QueryFilter} from '../../model/filter.ts'
|
|||||||
import {Scope} from '../Scope.ts'
|
import {Scope} from '../Scope.ts'
|
||||||
import {FunctionScope, ScopeFunction} from '../scope/FunctionScope.ts'
|
import {FunctionScope, ScopeFunction} from '../scope/FunctionScope.ts'
|
||||||
import {make} from '../../../../di/src/global.ts'
|
import {make} from '../../../../di/src/global.ts'
|
||||||
|
import RawValue from '../RawValue.ts'
|
||||||
|
|
||||||
export class WhereBuilder {
|
export class WhereBuilder {
|
||||||
protected _wheres: WhereStatement[] = []
|
protected _wheres: WhereStatement[] = []
|
||||||
@ -68,6 +69,16 @@ export class WhereBuilder {
|
|||||||
return this
|
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) {
|
whereIn(field: string, values: EscapedValue) {
|
||||||
this._wheres.push({
|
this._wheres.push({
|
||||||
field,
|
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 WhereClause = { field: string, operator: SQLWhereOperator, operand: string, preop: WherePreOperator }
|
||||||
export type WhereGroup = { items: WhereStatement[], preop: WherePreOperator }
|
export type WhereGroup = { items: WhereStatement[], preop: WherePreOperator }
|
||||||
export type WhereStatement = WhereClause | WhereGroup
|
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 OrderDirection = 'ASC' | 'DESC'
|
||||||
export type OrderStatement = { direction: OrderDirection, field: string }
|
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 {BehaviorSubject} from '../../../lib/src/support/BehaviorSubject.ts'
|
||||||
import {Scope} from '../builder/Scope.ts'
|
import {Scope} from '../builder/Scope.ts'
|
||||||
import {JSONState, Rehydratable} from '../../../lib/src/support/Rehydratable.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 separate read/write connections
|
||||||
// TODO manual dirty flags
|
// TODO manual dirty flags
|
||||||
@ -23,11 +26,16 @@ export type ModelJSONState = {
|
|||||||
ephemeral_values?: string,
|
ephemeral_values?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CachedRelation = {
|
||||||
|
accessor_name: string | symbol,
|
||||||
|
relation: HasOne<any, any>, // TODO generalize
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for database models.
|
* Base class for database models.
|
||||||
* @extends Builder
|
* @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.
|
* The name of the connection this model should run through.
|
||||||
* @type string
|
* @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>>()
|
protected deleted$ = new BehaviorSubject<Model<T>>()
|
||||||
|
|
||||||
|
public readonly relation_cache = new Collection<CachedRelation>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the model table name.
|
* Get the model table name.
|
||||||
* @return string
|
* @return string
|
||||||
@ -148,6 +158,17 @@ export abstract class Model<T extends Model<T>> extends Builder<T> implements Re
|
|||||||
return this.table
|
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.
|
* Get the model connection name.
|
||||||
* @return string
|
* @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}`
|
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.
|
* Like to_object, but only the fields that have changed.
|
||||||
* @return object
|
* @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))
|
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