|
|
|
@ -2,26 +2,43 @@ 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 {QueryRow, ModelKey} 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'
|
|
|
|
|
import {QueryFilter} from './filter.ts'
|
|
|
|
|
import {BehaviorSubject} from '../../../lib/src/support/BehaviorSubject.ts'
|
|
|
|
|
import {Scope} from '../builder/Scope.ts'
|
|
|
|
|
|
|
|
|
|
// TODO separate read/write connections
|
|
|
|
|
// TODO manual dirty flags
|
|
|
|
|
export abstract class Model extends Builder {
|
|
|
|
|
export abstract class Model<T extends Model<T>> extends Builder<T> {
|
|
|
|
|
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 static timestamps = true
|
|
|
|
|
|
|
|
|
|
protected static global_scopes: Scope[] = []
|
|
|
|
|
protected static appends: string[] = []
|
|
|
|
|
protected static masks: string[] = []
|
|
|
|
|
|
|
|
|
|
protected _original?: QueryRow
|
|
|
|
|
|
|
|
|
|
protected retrieved$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
protected saving$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
protected saved$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
protected updating$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
protected updated$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
protected creating$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
protected created$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
protected deleting$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
protected deleted$ = new BehaviorSubject<Model<T>>()
|
|
|
|
|
|
|
|
|
|
public static table_name() {
|
|
|
|
|
return this.table
|
|
|
|
|
}
|
|
|
|
@ -46,10 +63,19 @@ export abstract class Model extends Builder {
|
|
|
|
|
return this.prototype.insert()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static delete() {
|
|
|
|
|
return this.prototype.delete()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static truncate() {
|
|
|
|
|
return this.prototype.truncate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public update(target?: QuerySource, alias?: string) {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
return super.update()
|
|
|
|
|
.to(constructor.table_name())
|
|
|
|
|
.with_scopes(constructor.global_scopes)
|
|
|
|
|
.target_connection(constructor.get_connection())
|
|
|
|
|
.target_operator(make(ModelResultOperator, constructor))
|
|
|
|
|
}
|
|
|
|
@ -58,6 +84,7 @@ export abstract class Model extends Builder {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
return super.select(...fields)
|
|
|
|
|
.from(constructor.table_name())
|
|
|
|
|
.with_scopes(constructor.global_scopes)
|
|
|
|
|
.target_connection(constructor.get_connection())
|
|
|
|
|
.target_operator(make(ModelResultOperator, constructor))
|
|
|
|
|
}
|
|
|
|
@ -70,6 +97,23 @@ export abstract class Model extends Builder {
|
|
|
|
|
.target_operator(make(ModelResultOperator, constructor))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public delete(target?: QuerySource, alias?: string) {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
return super.delete()
|
|
|
|
|
.from(constructor.table_name())
|
|
|
|
|
.with_scopes(constructor.global_scopes)
|
|
|
|
|
.target_connection(constructor.get_connection())
|
|
|
|
|
.target_operator(make(ModelResultOperator, constructor))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public truncate(target?: QuerySource, alias?: string) {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
return super.truncate()
|
|
|
|
|
.table(constructor.table_name())
|
|
|
|
|
.target_connection(constructor.get_connection())
|
|
|
|
|
.target_operator(make(ObjectResultOperator))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
values?: any
|
|
|
|
|
) {
|
|
|
|
@ -94,22 +138,52 @@ export abstract class Model extends Builder {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
this[field_def.model_key] = row[field_def.database_key]
|
|
|
|
|
})
|
|
|
|
|
this.retrieved$.next(this)
|
|
|
|
|
return this
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// changes
|
|
|
|
|
// date format
|
|
|
|
|
// appends
|
|
|
|
|
// caching
|
|
|
|
|
// relations
|
|
|
|
|
// timestamps
|
|
|
|
|
// hidden
|
|
|
|
|
// fillable
|
|
|
|
|
// guarded
|
|
|
|
|
// key type
|
|
|
|
|
// with
|
|
|
|
|
// per page
|
|
|
|
|
// exists
|
|
|
|
|
public timestamps(): { updated_at?: Date, created_at?: Date } {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
const timestamps: { updated_at?: Date, created_at?: Date } = {}
|
|
|
|
|
if ( constructor.timestamps ) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
if ( constructor.CREATED_AT ) timestamps.created_at = this[constructor.CREATED_AT]
|
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
if ( constructor.UPDATED_AT ) timestamps.updated_at = this[constructor.UPDATED_AT]
|
|
|
|
|
}
|
|
|
|
|
return timestamps
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static model_select(...other_fields: FieldSet[]) {
|
|
|
|
|
const fields = get_fields_meta(this.prototype).pluck('database_key').toArray()
|
|
|
|
|
return this.select(...[...fields, ...other_fields])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async find(filter: QueryFilter = {}) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
return await this.model_select().filter(filter).results()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async find_one(filter: QueryFilter = {}) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const result = await this.model_select().filter(filter).limit(1).results()
|
|
|
|
|
return result.first()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async find_by_key(key: ModelKey): Promise<undefined | typeof Model> {
|
|
|
|
|
const result = await this.model_select()
|
|
|
|
|
.where(this.qualified_key_name(), '=', key)
|
|
|
|
|
.limit(1)
|
|
|
|
|
.results()
|
|
|
|
|
|
|
|
|
|
return result.first()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async count(): Promise<number> {
|
|
|
|
|
const result = this.model_select().results()
|
|
|
|
|
return await result.count()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected get _is_dirty() {
|
|
|
|
|
return (field_def: ModelField) => {
|
|
|
|
@ -118,18 +192,16 @@ export abstract class Model extends Builder {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// to_row
|
|
|
|
|
public to_row(): QueryRow {
|
|
|
|
|
const data = {}
|
|
|
|
|
const meta = (this.constructor as typeof Model).fields()
|
|
|
|
|
meta.each(field => {
|
|
|
|
|
|
|
|
|
|
const row = {}
|
|
|
|
|
this.field_defs()
|
|
|
|
|
.each(field_def => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
row[field_def.database_key] = this[field_def.model_key]
|
|
|
|
|
})
|
|
|
|
|
return {}
|
|
|
|
|
return row
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// relations_to_row
|
|
|
|
|
// dirty_to_row
|
|
|
|
|
public dirty_to_row(): QueryRow {
|
|
|
|
|
const row = {}
|
|
|
|
|
this.field_defs()
|
|
|
|
@ -142,8 +214,6 @@ export abstract class Model extends Builder {
|
|
|
|
|
return row
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// attributes
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a collection of field definitions that contains information
|
|
|
|
|
* on which database fields correspond to which model fields, and
|
|
|
|
@ -229,10 +299,10 @@ export abstract class Model extends Builder {
|
|
|
|
|
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
|
|
|
|
|
public was_changed(field: string) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
return this.field_defs().pluck('model_key').includes(field) && this[field] !== this._original[field]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// has one
|
|
|
|
|
// morph one
|
|
|
|
@ -244,28 +314,48 @@ export abstract class Model extends Builder {
|
|
|
|
|
// 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
|
|
|
|
|
// relations
|
|
|
|
|
// with
|
|
|
|
|
// relations_to_row
|
|
|
|
|
|
|
|
|
|
// update - bulk
|
|
|
|
|
// push - update single
|
|
|
|
|
// support for soft deletes!
|
|
|
|
|
// force delete - for soft deleted models
|
|
|
|
|
|
|
|
|
|
// save - update or create instance
|
|
|
|
|
public async save(): Promise<Model> {
|
|
|
|
|
public touch() {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
if ( constructor.timestamps ) {
|
|
|
|
|
if ( this.exists() && constructor.UPDATED_AT ) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
this[constructor.UPDATED_AT] = new Date()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( constructor.CREATED_AT ) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
this[constructor.CREATED_AT] = new Date()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return this
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static all() {
|
|
|
|
|
return this.model_select().results()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async save({ without_timestamps = false }): Promise<Model<T>> {
|
|
|
|
|
await this.saving$.next(this)
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
|
|
|
|
|
// TODO timestamps
|
|
|
|
|
if ( this.exists() && this.is_dirty() ) { // We're updating an existing record
|
|
|
|
|
await this.updating$.next(this)
|
|
|
|
|
|
|
|
|
|
if ( !without_timestamps && constructor.timestamps && constructor.UPDATED_AT ) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
this[constructor.UPDATED_AT] = new Date()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mutable = this.update()
|
|
|
|
|
.data(this.dirty_to_row())
|
|
|
|
|
.where(constructor.qualified_key_name(), '=', this.key())
|
|
|
|
@ -280,8 +370,22 @@ export abstract class Model extends Builder {
|
|
|
|
|
logger.warn(`Model update modified ${modified_rows} rows! (Key: ${constructor.qualified_key_name()})`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.assume_from_source(result.firstWhere(this.key_name(), '=', this.key()))
|
|
|
|
|
const model = result.firstWhere(this.key_name(), '=', this.key())
|
|
|
|
|
if ( model ) this.assume_from_source(model)
|
|
|
|
|
await this.updated$.next(this)
|
|
|
|
|
} else if ( !this.exists() ) { // We're inserting a new record
|
|
|
|
|
await this.creating$.next(this)
|
|
|
|
|
|
|
|
|
|
if ( !without_timestamps && constructor.timestamps && constructor.CREATED_AT ) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
this[constructor.CREATED_AT] = new Date()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( constructor.timestamps && constructor.UPDATED_AT ) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
this[constructor.UPDATED_AT] = new Date()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const insert_object: FieldValueObject = this._build_insert_field_object()
|
|
|
|
|
const mutable = this.insert()
|
|
|
|
|
.row(insert_object)
|
|
|
|
@ -296,18 +400,21 @@ export abstract class Model extends Builder {
|
|
|
|
|
logger.warn(`Model insert created ${inserted_rows} rows! (Key: ${constructor.qualified_key_name()})`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.assume_from_source(result.first())
|
|
|
|
|
const model = result.first()
|
|
|
|
|
if ( model ) this.assume_from_source(model)
|
|
|
|
|
await this.created$.next(this)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.saved$.next(this)
|
|
|
|
|
return this
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected _build_insert_field_object(): FieldValueObject {
|
|
|
|
|
const fields = this.field_defs()
|
|
|
|
|
const fields = this.field_defs().whereNot('model_key', '=', this.key_name())
|
|
|
|
|
const values = {}
|
|
|
|
|
fields.each(field_def => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
values[field_def.database_key] = this[field_def.model_key]
|
|
|
|
|
values[field_def.database_key] = this[field_def.model_key] ?? null
|
|
|
|
|
})
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
@ -317,20 +424,112 @@ export abstract class Model extends Builder {
|
|
|
|
|
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
|
|
|
|
|
public static async destroy(id_or_ids: ModelKey | ModelKey[]) {
|
|
|
|
|
const ids = Array.isArray(id_or_ids) ? id_or_ids : [id_or_ids]
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
const mutable = this.delete()
|
|
|
|
|
.whereIn(constructor.qualified_key_name(), ids)
|
|
|
|
|
.target_operator(make(ObjectResultOperator))
|
|
|
|
|
.results()
|
|
|
|
|
|
|
|
|
|
const result = await mutable
|
|
|
|
|
|
|
|
|
|
const modified_rows = await mutable.count()
|
|
|
|
|
if ( modified_rows !== ids.length ) {
|
|
|
|
|
logger.warn(`Model bulk destroy modified ${modified_rows} when ${ids.length} keys were provided. (Key: ${constructor.qualified_key_name()})`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async destroy(): Promise<void> {
|
|
|
|
|
await this.deleting$.next(this)
|
|
|
|
|
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
const mutable = this.delete()
|
|
|
|
|
.where(constructor.qualified_key_name, '=', this.key())
|
|
|
|
|
.target_operator(make(ObjectResultOperator))
|
|
|
|
|
.results()
|
|
|
|
|
|
|
|
|
|
const result = await mutable
|
|
|
|
|
|
|
|
|
|
const modified_rows = await mutable.count()
|
|
|
|
|
if ( modified_rows !== 1 ) {
|
|
|
|
|
logger.warn(`Model delete modified ${modified_rows} rows! (Key: ${constructor.qualified_key_name()})`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.deleted$.next(this)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public to_object(): { [key: string]: any } {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
const obj = {}
|
|
|
|
|
this.field_defs()
|
|
|
|
|
.each(field_def => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
obj[field_def.model_key] = this[field_def.model_key]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
constructor.appends.forEach(appended_field => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
obj[appended_field] = this[appended_field]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
constructor.masks.forEach(masked_field => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
delete obj[masked_field]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public to_json(): string {
|
|
|
|
|
return JSON.stringify(this.to_object())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async fresh(): Promise<Model<T>> {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
const fields = this.field_defs()
|
|
|
|
|
.whereNot('model_key', '=', this.key_name())
|
|
|
|
|
.pluck('database_key')
|
|
|
|
|
.toArray()
|
|
|
|
|
|
|
|
|
|
const result = await constructor.select(...fields)
|
|
|
|
|
.where(constructor.qualified_key_name(), '=', this.key())
|
|
|
|
|
.limit(1)
|
|
|
|
|
.results()
|
|
|
|
|
return result.first()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async refresh() {
|
|
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
|
const values = await this.select(this._loaded_database_fields())
|
|
|
|
|
.where(constructor.qualified_key_name(), '=', this.key())
|
|
|
|
|
.limit(1)
|
|
|
|
|
.target_operator(make(ObjectResultOperator))
|
|
|
|
|
.results()
|
|
|
|
|
|
|
|
|
|
const row = values.first()
|
|
|
|
|
if ( row ) this.assume_from_source(row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public populate(other: Model<T>): Model<T> {
|
|
|
|
|
const row = this.to_row()
|
|
|
|
|
delete row[this.key_name()]
|
|
|
|
|
other.assume_from_source(row)
|
|
|
|
|
return other
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public is(other_instance: Model<any>): boolean {
|
|
|
|
|
const this_constructor = (this.constructor as typeof Model)
|
|
|
|
|
const other_constructor = (other_instance.constructor as typeof Model)
|
|
|
|
|
return (
|
|
|
|
|
other_instance.key() === this.key()
|
|
|
|
|
&& this_constructor.qualified_key_name() === other_constructor.qualified_key_name()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public is_not(other_instance: Model<any>): boolean {
|
|
|
|
|
return !this.is(other_instance)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the field name of the primary key for this model.
|
|
|
|
@ -343,7 +542,7 @@ export abstract class Model extends Builder {
|
|
|
|
|
/**
|
|
|
|
|
* If defined, returns the value of the primary key for this model.
|
|
|
|
|
*/
|
|
|
|
|
public key() {
|
|
|
|
|
public key(): ModelKey {
|
|
|
|
|
return this?._original?.[(this.constructor as typeof Model).key]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|