From 81906b02bcfae2e6eb8dcfcf726e66be373a5483 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Sun, 16 Aug 2020 09:32:22 -0500 Subject: [PATCH] start relations (has one or many; has one; has many) --- TODO.txt | 1 - app/index.ts | 14 ++++++++ app/models/LoginAttempt.model.ts | 20 +++++++++++ app/models/User.model.ts | 30 ++++++++++++++++ app/units.ts | 2 +- orm/src/builder/type/ConnectionExecutable.ts | 20 ++++++++--- orm/src/builder/type/Select.ts | 32 +++++++++++++++++ orm/src/builder/type/result/ResultOperator.ts | 5 ++- orm/src/model/Model.ts | 23 ++++++++++-- orm/src/model/ModelResultOperator.ts | 20 +++++++++++ orm/src/model/query/ModelSelect.ts | 20 +++++++++++ orm/src/model/relation/HasMany.ts | 16 +++++++++ orm/src/model/relation/HasOne.ts | 22 ++++++++++++ orm/src/model/relation/HasOneOrMany.ts | 19 +++++++--- orm/src/model/relation/Relation.ts | 36 +++++++++++++++---- 15 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 app/models/LoginAttempt.model.ts create mode 100644 app/models/User.model.ts create mode 100644 orm/src/model/query/ModelSelect.ts diff --git a/TODO.txt b/TODO.txt index afb5838..07a5f9a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,6 +1,5 @@ static assets middleware -view engine internationalization uploads & universal path CLI - view routes, template generation, start server, directives, output, args diff --git a/app/index.ts b/app/index.ts index 2ed0d40..c628bc9 100755 --- a/app/index.ts +++ b/app/index.ts @@ -18,3 +18,17 @@ await scaffolding.up() */ const app = make(Application, units) await app.run() + +import UserModel from './models/User.model.ts' +import ObjectResultOperator from "../orm/src/builder/type/result/ObjectResultOperator.ts"; +// const users = await UserModel.find_one({ username: 'garrettmills' }) + +const sel = UserModel.select('*').with('login_attempts').where('username', '=', 'garrettmills') +console.log(sel) +console.log(sel.clone()) +const gm = await sel.results().first() + +console.log(gm) +console.log(await gm.login_attempts()) + +// console.log(await las.fetchRelated()) diff --git a/app/models/LoginAttempt.model.ts b/app/models/LoginAttempt.model.ts new file mode 100644 index 0000000..96fc409 --- /dev/null +++ b/app/models/LoginAttempt.model.ts @@ -0,0 +1,20 @@ +import {Model} from '../../orm/src/model/Model.ts' +import {Field} from '../../orm/src/model/Field.ts' +import {Type} from '../../orm/src/db/types.ts' + +export default class LoginAttemptModel extends Model { + protected static table = 'daton_login_attempts' + protected static key = 'daton_login_attempt_id' + + protected static readonly CREATED_AT = 'attempt_date' + protected static readonly UPDATED_AT = null + + @Field(Type.int) + protected daton_login_attempt_id?: number + + @Field(Type.int) + protected user_id!: number + + @Field(Type.timestamp) + protected attempt_date!: Date +} diff --git a/app/models/User.model.ts b/app/models/User.model.ts new file mode 100644 index 0000000..ec0f0b4 --- /dev/null +++ b/app/models/User.model.ts @@ -0,0 +1,30 @@ +import {Model} from '../../orm/src/model/Model.ts' +import {Type} from '../../orm/src/db/types.ts' +import {Field} from '../../orm/src/model/Field.ts' +import LoginAttemptModel from './LoginAttempt.model.ts' +import {Relation} from '../../orm/src/model/relation/decorators.ts' + +export default class UserModel extends Model { + protected static table = 'daton_users' + protected static key = 'user_id' + + protected static readonly CREATED_AT = 'created_at' + protected static readonly UPDATED_AT = 'updated_at' + + @Field(Type.int) + protected user_id?: number + + @Field(Type.varchar) + protected first_name!: String + + @Field(Type.varchar) + protected last_name!: String + + @Field(Type.bool) + protected active!: Boolean + + @Relation() + public login_attempts() { + return this.has_many(LoginAttemptModel) + } +} diff --git a/app/units.ts b/app/units.ts index faec4f2..acbcb16 100644 --- a/app/units.ts +++ b/app/units.ts @@ -23,5 +23,5 @@ export default [ ViewEngineUnit, RoutesUnit, RoutingUnit, - HttpServerUnit, + // HttpServerUnit, ] diff --git a/orm/src/builder/type/ConnectionExecutable.ts b/orm/src/builder/type/ConnectionExecutable.ts index 6db7144..1698048 100644 --- a/orm/src/builder/type/ConnectionExecutable.ts +++ b/orm/src/builder/type/ConnectionExecutable.ts @@ -6,7 +6,7 @@ import {Connection} from '../../db/Connection.ts' import {ResultCollection} from './result/ResultCollection.ts' import {ResultIterable} from './result/ResultIterable.ts' import ResultOperator from './result/ResultOperator.ts' -import {Collection} from '../../../../lib/src/collection/Collection.ts' +import {collect, Collection} from '../../../../lib/src/collection/Collection.ts' import NoTargetOperatorError from '../../error/NoTargetOperatorError.ts' export default abstract class ConnectionExecutable { @@ -21,12 +21,17 @@ export default abstract class ConnectionExecutable { throw new Error('Unable to execute database item: no target connection.') } - if ( !this.__target_operator ) throw new NoTargetOperatorError() - const query = `SELECT * FROM (${this.sql(0)}) AS target_query OFFSET ${i} LIMIT 1` const result = await this.__target_connection.query(query) const row = result.rows.first() - if ( row ) return this.__target_operator.inflate_row(row) + if ( row ) { + if ( !this.__target_operator ) throw new NoTargetOperatorError() + + const inflated = this.__target_operator.inflate_row(row) + await this.__target_operator.process_eager_loads(this, collect([inflated])) + + return inflated + } } async get_range(start: number, end: number): Promise> { @@ -36,10 +41,15 @@ export default abstract class ConnectionExecutable { const query = `SELECT * FROM (${this.sql(0)}) AS target_query OFFSET ${start} LIMIT ${(end - start) + 1}` const result = await this.__target_connection.query(query) - return result.rows.map(row => { + const inflated = result.rows.map(row => { if ( !this.__target_operator ) throw new NoTargetOperatorError() return this.__target_operator.inflate_row(row) }) + + if ( !this.__target_operator ) throw new NoTargetOperatorError() + await this.__target_operator.process_eager_loads(this, inflated) + + return inflated } iterator(): ResultIterable { diff --git a/orm/src/builder/type/Select.ts b/orm/src/builder/type/Select.ts index 4349d3b..9f5e559 100644 --- a/orm/src/builder/type/Select.ts +++ b/orm/src/builder/type/Select.ts @@ -20,6 +20,7 @@ import {FullOuterJoin} from './join/FullOuterJoin.ts' import {HavingBuilder} from './HavingBuilder.ts' import ConnectionExecutable from './ConnectionExecutable.ts' import {Scope} from '../Scope.ts' +import {isInstantiable} from "../../../../di/src/type/Instantiable.ts"; export type WhereBuilderFunction = (group: WhereBuilder) => any export type HavingBuilderFunction = (group: HavingBuilder) => any @@ -78,6 +79,37 @@ export class Select extends ConnectionExecutable { return this } + clear_fields() { + this._fields = [] + return this + } + + clone(): Select { + const constructor = this.constructor as typeof Select + if ( !isInstantiable>(constructor) ) { + throw new TypeError(`Parent constructor is not instantiable.`) + } + + const select = new constructor() + + select._fields = this._fields + select._source = this._source + select._wheres = this._wheres + select._scopes = this._scopes + select._havings = this._havings + select._limit = this._limit + select._offset = this._offset + select._joins = this._joins + select._distinct = this._distinct + select._group_by = this._group_by + select._order = this._order + + select.__target_connection = this.__target_connection + select.__target_operator = this.__target_operator + + return select + } + group_by(...groupings: string[]) { this._group_by = groupings return this diff --git a/orm/src/builder/type/result/ResultOperator.ts b/orm/src/builder/type/result/ResultOperator.ts index 9ad3666..3b7d642 100644 --- a/orm/src/builder/type/result/ResultOperator.ts +++ b/orm/src/builder/type/result/ResultOperator.ts @@ -1,8 +1,11 @@ import {QueryRow} from '../../../db/types.ts' +import ConnectionExecutable from '../ConnectionExecutable.ts' +import {Model} from '../../../model/Model.ts' +import {Collection} from '../../../../../lib/src/collection/Collection.ts' export default abstract class ResultOperator { + public async process_eager_loads(query: ConnectionExecutable, results: Collection): Promise { } abstract inflate_row(row: QueryRow): T abstract deflate_row(item: T): QueryRow - } diff --git a/orm/src/model/Model.ts b/orm/src/model/Model.ts index 3707e58..1d43b8e 100644 --- a/orm/src/model/Model.ts +++ b/orm/src/model/Model.ts @@ -11,10 +11,12 @@ import {logger} from '../../../lib/src/service/logging/global.ts' 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 {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' +import {ModelSelect} from "./query/ModelSelect.ts"; +import {Relation} from "./relation/Relation.ts"; // TODO separate read/write connections // TODO manual dirty flags @@ -246,9 +248,13 @@ abstract class Model> extends Builder implements Rehydrata * @param {...FieldSet} fields */ public select(...fields: FieldSet[]) { + if ( fields.length < 1 ) fields.push(this.qualify_column('*')) + const constructor = (this.constructor as typeof Model) - return super.select(...fields) - .from(constructor.table_name()) + const select = new ModelSelect() + + return select.fields(...fields) + .from(constructor.query_source()) .with_scopes(constructor.global_scopes) .target_connection(constructor.get_connection()) .target_operator(make(ModelResultOperator, constructor)) @@ -972,6 +978,17 @@ abstract class Model> extends Builder implements Rehydrata public has_many(related: Instantiable, foreign_key?: string, local_key?: string) { return new HasMany(this as any, new related() as any, foreign_key, local_key) } + + public get_relation>(name: string): Relation { + // @ts-ignore + const rel: any = this[name]() + + if ( rel instanceof Relation ) { + return rel + } + + throw new TypeError(`Cannot get relation of name: ${name}. Method does not return relation.`) + } } export { Model } diff --git a/orm/src/model/ModelResultOperator.ts b/orm/src/model/ModelResultOperator.ts index e584403..f3abb23 100644 --- a/orm/src/model/ModelResultOperator.ts +++ b/orm/src/model/ModelResultOperator.ts @@ -4,6 +4,9 @@ import {Injectable} from '../../../di/src/decorator/Injection.ts' import {QueryRow} from '../db/types.ts' import Instantiable from '../../../di/src/type/Instantiable.ts' import {make} from '../../../di/src/global.ts' +import ConnectionExecutable from '../builder/type/ConnectionExecutable.ts' +import {Collection} from '../../../lib/src/collection/Collection.ts' +import {ModelSelect} from './query/ModelSelect.ts' @Injectable() export default class ModelResultOperator> extends ResultOperator { @@ -21,4 +24,21 @@ export default class ModelResultOperator> extends ResultOpera return item.to_row() } + public async process_eager_loads(query: ConnectionExecutable, results: Collection) { + if ( query instanceof ModelSelect ) { + const eagers = query.eager_relations + const model = new this.ModelClass() + + for ( const rel_name of eagers ) { + const relation = model.get_relation(rel_name) + const select = await relation.build_eager_query(query, results) + const all_related = await select.results() + results.each(result => { + const result_relation = result.get_relation(rel_name) + const result_related = result_relation.match_results(all_related as any) + result_relation.set_value(result_related as any) + }) + } + } + } } diff --git a/orm/src/model/query/ModelSelect.ts b/orm/src/model/query/ModelSelect.ts new file mode 100644 index 0000000..98f1c21 --- /dev/null +++ b/orm/src/model/query/ModelSelect.ts @@ -0,0 +1,20 @@ +import {Select} from '../../builder/type/Select.ts' + +export class ModelSelect extends Select { + protected _withs: string[] = [] + + public with(related: string) { + this._withs.push(related) + return this + } + + public get eager_relations(): string[] { + return [...this._withs] + } + + clone(): ModelSelect { + const select = super.clone() as ModelSelect + select._withs = this._withs + return select + } +} diff --git a/orm/src/model/relation/HasMany.ts b/orm/src/model/relation/HasMany.ts index 7cd7a71..560c9c7 100644 --- a/orm/src/model/relation/HasMany.ts +++ b/orm/src/model/relation/HasMany.ts @@ -3,7 +3,23 @@ import {HasOneOrMany} from './HasOneOrMany.ts' import {Collection} from '../../../../lib/src/collection/Collection.ts' export class HasMany, T2 extends Model> extends HasOneOrMany { + protected _value?: Collection + protected _loaded = false + public async get(): Promise> { return this.fetch().collect() } + + public set_value(related: Collection) { + this._loaded = true + this._value = related.clone() + } + + public get_value(): Collection | undefined { + return this._value + } + + public is_loaded(): boolean { + return this._loaded + } } diff --git a/orm/src/model/relation/HasOne.ts b/orm/src/model/relation/HasOne.ts index 6239775..b86d44b 100644 --- a/orm/src/model/relation/HasOne.ts +++ b/orm/src/model/relation/HasOne.ts @@ -1,8 +1,30 @@ import {Model} from '../Model.ts' import {HasOneOrMany} from './HasOneOrMany.ts' +import {Collection} from "../../../../lib/src/collection/Collection.ts"; +import {Logging} from "../../../../lib/src/service/logging/Logging.ts"; export class HasOne, T2 extends Model> extends HasOneOrMany { + protected _value?: T2 + protected _loaded = false + public async get(): Promise { return this.fetch().first() } + + public set_value(related: Collection) { + this._value = related.first() + this._loaded = true + + if ( related.length > 1 ) { + this.make(Logging).warn(`HasOne relation result contained more than one value for ${this.qualified_local_key} -> ${this.qualified_local_key}`) + } + } + + public get_value(): T2 | undefined { + return this._value + } + + public is_loaded(): boolean { + return this._loaded + } } diff --git a/orm/src/model/relation/HasOneOrMany.ts b/orm/src/model/relation/HasOneOrMany.ts index bbf3edb..b281615 100644 --- a/orm/src/model/relation/HasOneOrMany.ts +++ b/orm/src/model/relation/HasOneOrMany.ts @@ -1,7 +1,9 @@ -import {Model} from "../Model.ts"; -import {Relation} from "./Relation.ts"; -import ConnectionExecutable from "../../builder/type/ConnectionExecutable.ts"; -import {WhereBuilder} from "../../builder/type/WhereBuilder.ts"; +import {Model} from '../Model.ts' +import {Relation} from './Relation.ts' +import ConnectionExecutable from '../../builder/type/ConnectionExecutable.ts' +import {WhereBuilder} from '../../builder/type/WhereBuilder.ts' +import {ModelSelect} from '../query/ModelSelect.ts' +import {Collection} from '../../../../lib/src/collection/Collection.ts' export abstract class HasOneOrMany, T2 extends Model> extends Relation { constructor( @@ -42,4 +44,13 @@ export abstract class HasOneOrMany, T2 extends Model> ext .whereRaw(this.qualified_foreign_key, 'IS NOT', 'NULL') }) } + + public build_eager_query(parent_query: ModelSelect, result: Collection): ModelSelect { + const keys = result.pluck(this.local_key).toArray() + return this.related.select().whereIn(this.foreign_key, keys) + } + + public match_results(possibly_related: Collection) { + return possibly_related.where(this.foreign_key, '=', this.parent_value) + } } diff --git a/orm/src/model/relation/Relation.ts b/orm/src/model/relation/Relation.ts index 4484db9..7b54cff 100644 --- a/orm/src/model/relation/Relation.ts +++ b/orm/src/model/relation/Relation.ts @@ -6,10 +6,12 @@ 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"; +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' +import ModelResultOperator from '../ModelResultOperator.ts' +import {ModelSelect} from '../query/ModelSelect.ts' export type RelationResult = T | Collection | undefined @@ -25,13 +27,21 @@ export abstract class Relation, T2 extends Model> extends public get_operator() { const related_class = this.related.constructor as typeof Model - return this.make(RelationResultOperator, related_class, this) + return this.make(ModelResultOperator, related_class) } public abstract query(): ConnectionExecutable public abstract scope_query(where: WhereBuilder): void + public abstract build_eager_query(parent_query: ModelSelect, result: Collection): ModelSelect + + public abstract match_results(possibly_related: Collection): Collection + + public abstract set_value(related: Collection): void + public abstract get_value(): Collection | T2 | undefined + public abstract is_loaded(): boolean + public fetch(): AsyncCollection { return this.query().results() } @@ -39,7 +49,21 @@ export abstract class Relation, T2 extends Model> extends public abstract get(): Promise> public then(callback: (result: RelationResult) => any) { - this.get().then(callback) + if ( this.is_loaded() ) { + callback(this.get_value() as RelationResult) + } else { + this.get().then(result => { + if ( result instanceof Collection ) { + this.set_value(result as any) + } + + callback(result) + }) + } + } + + public get value(): RelationResult { + return this.get_value() } public get related_query_source() {