You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

357 lines
10 KiB

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<ModelField>
*/
public static fields(): Collection<ModelField> {
return get_fields_meta(this.prototype)
}
public field_defs(): Collection<ModelField> {
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<ModelField> fields
*/
public static set_fields(fields: Collection<ModelField>) {
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<Model> {
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}`
}
}