import { Builder } from '../builder/Builder.ts' import {FieldSet, FieldValueObject, QuerySource} from '../builder/types.ts' import {make} from '../../../di/src/global.ts' import Database from '../service/Database.ts' import {QueryRow} from '../db/types.ts' import ModelResultOperator from './ModelResultOperator.ts' import {get_fields_meta, ModelField, set_model_fields_meta} from './Field.ts' import {Collection} from '../../../lib/src/collection/Collection.ts' import {logger} from '../../../lib/src/service/logging/global.ts' import ObjectResultOperator from '../builder/type/result/ObjectResultOperator.ts' // TODO separate read/write connections // TODO manual dirty flags export abstract class Model extends Builder { protected static connection: string protected static table: string protected static key: string protected static readonly CREATED_AT = 'created_at' protected static readonly UPDATED_AT = 'updated_at' protected static timestamps = false protected _original?: QueryRow public static table_name() { return this.table } public static connection_name() { return this.connection } public static get_connection() { return make(Database).connection(this.connection_name()) } public static select(...fields: FieldSet[]) { return this.prototype.select(...fields) } public static update() { return this.prototype.update() } public static insert() { return this.prototype.insert() } public update(target?: QuerySource, alias?: string) { const constructor = (this.constructor as typeof Model) return super.update() .to(constructor.table_name()) .target_connection(constructor.get_connection()) .target_operator(make(ModelResultOperator, constructor)) } public select(...fields: FieldSet[]) { const constructor = (this.constructor as typeof Model) return super.select(...fields) .from(constructor.table_name()) .target_connection(constructor.get_connection()) .target_operator(make(ModelResultOperator, constructor)) } public insert(target?: QuerySource, alias?: string) { const constructor = (this.constructor as typeof Model) return super.insert() .into(constructor.table_name()) .target_connection(constructor.get_connection()) .target_operator(make(ModelResultOperator, constructor)) } constructor( values?: any ) { super() this.boot(values) } public boot(values?: any) { if ( values ) { get_fields_meta(this).each(field_def => { // TODO special type casting // @ts-ignore this[field_def.model_key] = values[field_def.model_key] }) } } public assume_from_source(row: QueryRow) { this._original = row get_fields_meta(this).each(field_def => { // TODO special type casting // @ts-ignore this[field_def.model_key] = row[field_def.database_key] }) return this } // changes // date format // appends // caching // relations // timestamps // hidden // fillable // guarded // key type // with // per page // exists protected get _is_dirty() { return (field_def: ModelField) => { // @ts-ignore return this[field_def.model_key] !== this._original[field_def.database_key] } } // to_row public to_row(): QueryRow { const data = {} const meta = (this.constructor as typeof Model).fields() meta.each(field => { }) return {} } // relations_to_row // dirty_to_row public dirty_to_row(): QueryRow { const row = {} this.field_defs() .filter(this._is_dirty) .each(field_def => { // TODO additional casting and serializing logic here // @ts-ignore row[field_def.database_key] = this[field_def.model_key] }) return row } // attributes /** * Get a collection of field definitions that contains information * on which database fields correspond to which model fields, and * their types. * @return Collection */ public static fields(): Collection { return get_fields_meta(this.prototype) } public field_defs(): Collection { return (this.constructor as typeof Model).fields() } /** * Sets the model field metadata to the specified collection of * model field definitions. You should rarely need to use this. * @param Collection fields */ public static set_fields(fields: Collection) { set_model_fields_meta(this.prototype, fields) } /** * Get the original values of the model as they were retrieved from * the database. These are never updated when the model is modified. * @return QueryRow */ public get_original_values() { return this._original } /** * Get an object with only the fields specified as arguments. * Note that this is NOT a QueryRow. * @param {...string} fields */ public only(...fields: string[]) { const row = {} for ( const field of fields ) { // @ts-ignore row[field] = this[field] } return row } /** * Returns true if any of the defined fields have been modified from * the values that were originally fetched from the database. * @return boolean */ public is_dirty() { return this.field_defs().some(this._is_dirty) } /** * Get an array of model field names that have been modified from * the values that were originally fetched from the database. * @return string[] */ public dirty_fields() { return this.field_defs() .filter(this._is_dirty) .pluck('model_key') .toArray() } /** * Returns true if the model has an ID from, and therefore exists in, * the database backend. * @return boolean */ public exists(): boolean { return !!this._original && !!this.key() } /** * Returns true if none of the defined fields have been modified from * the values that were originally fetched from the database. * @return boolean */ public is_clean() { return !this.is_dirty() } // was changed - pass attribute(s) // observe/observers - retrieved, saving, saved, updating, updated, creating, created, deleting, deleted // global scopes // non-global scopes // has one // morph one // belongs to // morph to // has many // has many through // belongs to many // morph to many // morphed by many // is relation loaded // touch - update update timestamp, created if necessary // touch created - update update and created timestamp // set created at/set updated at // is fillable // is guarded // without touching // all // load relations // load missing relations // increment column // decrement column // update - bulk // push - update single // save - update or create instance public async save(): Promise { const constructor = (this.constructor as typeof Model) // TODO timestamps if ( this.exists() && this.is_dirty() ) { // We're updating an existing record const mutable = this.update() .data(this.dirty_to_row()) .where(constructor.qualified_key_name(), '=', this.key()) .returning(...this._loaded_database_fields()) .target_operator(make(ObjectResultOperator)) .results() const result = await mutable const modified_rows = await mutable.count() if ( modified_rows !== 1 ) { logger.warn(`Model update modified ${modified_rows} rows! (Key: ${constructor.qualified_key_name()})`) } this.assume_from_source(result.firstWhere(this.key_name(), '=', this.key())) } else if ( !this.exists() ) { // We're inserting a new record const insert_object: FieldValueObject = this._build_insert_field_object() const mutable = this.insert() .row(insert_object) .returning(this.key_name(), ...Object.keys(insert_object)) .target_operator(make(ObjectResultOperator)) .results() const result = await mutable const inserted_rows = await mutable.count() if ( inserted_rows !== 1 ) { logger.warn(`Model insert created ${inserted_rows} rows! (Key: ${constructor.qualified_key_name()})`) } this.assume_from_source(result.first()) } return this } protected _build_insert_field_object(): FieldValueObject { const fields = this.field_defs() const values = {} fields.each(field_def => { // @ts-ignore values[field_def.database_key] = this[field_def.model_key] }) return values } protected _loaded_database_fields(): string[] { if ( typeof this._original === 'undefined' ) return [] return Object.keys(this._original).map(String) } // destroy - bulk // delete single // force delete - for soft deleted models // without scope // without global scope // without global scopes // to object // to json // fresh - get new instance of this model // refresh - reload this instance // replicate to new instance // is - check if two models are the same // isNot /** * Returns the field name of the primary key for this model. * @return string */ public key_name() { return (this.constructor as typeof Model).key } /** * If defined, returns the value of the primary key for this model. */ public key() { return this?._original?.[(this.constructor as typeof Model).key] } /** * Returns the table-qualified field name of the primary key for this model. */ public static qualified_key_name() { return `${this.table_name()}.${this.key}` } }