2021-06-03 03:36:25 +00:00
|
|
|
import {ModelKey, QueryRow, QuerySource} from '../types'
|
2022-09-30 17:02:39 +00:00
|
|
|
import {Container, Instantiable, isInstantiable} from '../../di'
|
2021-06-03 03:36:25 +00:00
|
|
|
import {DatabaseService} from '../DatabaseService'
|
|
|
|
import {ModelBuilder} from './ModelBuilder'
|
|
|
|
import {getFieldsMeta, ModelField} from './Field'
|
2022-09-12 17:36:33 +00:00
|
|
|
import {deepCopy, Collection, uuid4, isKeyof, Pipeline, hasOwnProperty} from '../../util'
|
2022-09-30 16:42:13 +00:00
|
|
|
import {EscapeValueObject, QuerySafeValue} from '../dialect/SQLDialect'
|
2021-06-03 03:36:25 +00:00
|
|
|
import {Connection} from '../connection/Connection'
|
2021-06-04 06:03:31 +00:00
|
|
|
import {ModelRetrievedEvent} from './events/ModelRetrievedEvent'
|
|
|
|
import {ModelSavingEvent} from './events/ModelSavingEvent'
|
|
|
|
import {ModelSavedEvent} from './events/ModelSavedEvent'
|
|
|
|
import {ModelUpdatingEvent} from './events/ModelUpdatingEvent'
|
|
|
|
import {ModelUpdatedEvent} from './events/ModelUpdatedEvent'
|
|
|
|
import {ModelCreatingEvent} from './events/ModelCreatingEvent'
|
|
|
|
import {ModelCreatedEvent} from './events/ModelCreatedEvent'
|
2021-11-11 03:30:59 +00:00
|
|
|
import {Relation, RelationValue} from './relation/Relation'
|
|
|
|
import {HasOne} from './relation/HasOne'
|
|
|
|
import {HasMany} from './relation/HasMany'
|
|
|
|
import {HasOneOrMany} from './relation/HasOneOrMany'
|
2021-11-11 22:42:37 +00:00
|
|
|
import {Scope, ScopeClosure} from './scope/Scope'
|
2022-08-20 21:21:06 +00:00
|
|
|
import {LocalBus} from '../../support/bus/LocalBus' // need the specific import to prevent circular dependencies
|
2022-01-27 01:37:54 +00:00
|
|
|
import {ModelEvent} from './events/ModelEvent'
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Base for classes that are mapped to tables in a database.
|
|
|
|
*/
|
2022-01-27 01:37:54 +00:00
|
|
|
export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>> {
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* The name of the connection this model should run through.
|
|
|
|
* @type string
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
protected static connection = 'default'
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The name of the table this model is stored in.
|
|
|
|
* @type string
|
|
|
|
*/
|
|
|
|
protected static table: string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The name of the column that uniquely identifies this model.
|
|
|
|
* @type string
|
|
|
|
*/
|
|
|
|
protected static key: string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If false (default), the primary key will be excluded from INSERTs.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
protected static populateKeyOnInsert = false
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Optionally, the timestamp field set on creation.
|
|
|
|
* @type string
|
|
|
|
*/
|
|
|
|
protected static readonly CREATED_AT: string | null = 'created_at'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Optionally, the timestamp field set op update.
|
|
|
|
* @type string
|
|
|
|
*/
|
|
|
|
protected static readonly UPDATED_AT: string | null = 'updated_at'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If true, the CREATED_AT and UPDATED_AT columns will be automatically set.
|
|
|
|
* @type boolean
|
|
|
|
*/
|
|
|
|
protected static timestamps = true
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Array of additional fields on the class that should
|
|
|
|
* be included in the object serializations.
|
|
|
|
* @type string[]
|
|
|
|
*/
|
|
|
|
protected static appends: string[] = []
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Array of fields on the class that should be excluded
|
|
|
|
* from the object serializations.
|
|
|
|
* @type string[]
|
|
|
|
*/
|
|
|
|
protected static masks: string[] = []
|
|
|
|
|
2021-11-11 22:42:37 +00:00
|
|
|
/**
|
|
|
|
* Relations that should be eager-loaded by default.
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected with: (keyof T)[] = []
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* The original row fetched from the database.
|
|
|
|
* @protected
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
protected originalSourceRow?: QueryRow
|
2021-06-02 01:59:40 +00:00
|
|
|
|
2022-09-12 17:36:33 +00:00
|
|
|
/**
|
|
|
|
* Database fields that should be run on the next save, even if the
|
|
|
|
* fields are not mapped to members on the model.
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected dirtySourceRow?: QueryRow
|
|
|
|
|
2021-11-11 03:30:59 +00:00
|
|
|
/**
|
|
|
|
* Cache of relation instances by property accessor.
|
|
|
|
* This is used by the `@Relation()` decorator to cache Relation instances.
|
|
|
|
*/
|
|
|
|
public relationCache: Collection<{ accessor: string | symbol, relation: Relation<T, any, any> }> = new Collection<{accessor: string | symbol; relation: Relation<T, any, any>}>()
|
|
|
|
|
2021-11-11 22:42:37 +00:00
|
|
|
protected scopes: Collection<{ accessor: string | Instantiable<Scope>, scope: ScopeClosure }> = new Collection<{accessor: string | Instantiable<Scope>; scope: ScopeClosure}>()
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Get the table name for this model.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public static tableName(): string {
|
2021-06-02 01:59:40 +00:00
|
|
|
return this.table
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the QuerySource object for this model as it should be applied to query builders.
|
|
|
|
*
|
|
|
|
* This sets the alias for the model table equal to the table name itself, so it can be
|
|
|
|
* referenced explicitly in queries if necessary.
|
|
|
|
*/
|
|
|
|
public static querySource(): QuerySource {
|
|
|
|
return {
|
|
|
|
table: this.table,
|
|
|
|
alias: this.table,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the name of the connection where this model's table is found.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public static connectionName(): string {
|
2021-06-02 01:59:40 +00:00
|
|
|
return this.connection
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the database connection instance for this model's connection.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public static getConnection(): Connection {
|
|
|
|
return Container.getContainer().make<DatabaseService>(DatabaseService)
|
|
|
|
.get(this.connectionName())
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a new query builder that yields instances of this model,
|
|
|
|
* pre-configured with this model's QuerySource, connection, and fields.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* const user = await UserModel.query<UserModel>().where('name', 'LIKE', 'John Doe').first()
|
|
|
|
* ```
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public static query<T2 extends Model<T2>>(): ModelBuilder<T2> {
|
2022-08-20 21:21:06 +00:00
|
|
|
const di = Container.getContainer()
|
|
|
|
const builder = <ModelBuilder<T2>> di.make<ModelBuilder<T2>>(ModelBuilder, this)
|
2021-06-02 01:59:40 +00:00
|
|
|
const source: QuerySource = this.querySource()
|
|
|
|
|
|
|
|
builder.connection(this.getConnection())
|
|
|
|
|
2022-09-30 16:42:13 +00:00
|
|
|
if ( typeof source === 'string' || source instanceof QuerySafeValue ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
builder.from(source)
|
|
|
|
} else {
|
|
|
|
builder.from(source.table, source.alias)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
getFieldsMeta(this.prototype).each(field => {
|
|
|
|
builder.field(field.databaseKey)
|
|
|
|
})
|
|
|
|
|
2022-08-20 21:21:06 +00:00
|
|
|
const inst = di.make<T2>(this)
|
|
|
|
if ( Array.isArray(inst.with) ) {
|
2021-11-25 22:39:17 +00:00
|
|
|
// Try to get the eager-loaded relations statically, if possible
|
2022-08-20 21:21:06 +00:00
|
|
|
for (const relation of inst.with) {
|
2021-11-25 22:39:17 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
builder.with(relation)
|
|
|
|
}
|
2021-11-25 23:02:43 +00:00
|
|
|
}
|
|
|
|
|
2022-09-12 17:36:33 +00:00
|
|
|
inst.applyScopes(builder)
|
2021-06-02 01:59:40 +00:00
|
|
|
return builder
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
/**
|
|
|
|
* Pre-fill the model's properties from the given values.
|
|
|
|
* Calls `boot()` under the hood.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
values?: {[key: string]: any},
|
2021-06-02 01:59:40 +00:00
|
|
|
) {
|
|
|
|
super()
|
2021-06-04 06:03:31 +00:00
|
|
|
this.initialize()
|
2021-06-02 01:59:40 +00:00
|
|
|
this.boot(values)
|
|
|
|
}
|
|
|
|
|
2021-06-04 06:03:31 +00:00
|
|
|
/**
|
|
|
|
* Called when the model is instantiated. Use for any setup of events, &c.
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected initialize(): void {} // eslint-disable-line @typescript-eslint/no-empty-function
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Initialize the model's properties from the given values and do any other initial setup.
|
|
|
|
*
|
|
|
|
* `values` can optionally be an object mapping model properties to the values of those
|
|
|
|
* properties. Only properties with `@Field()` annotations will be set.
|
|
|
|
*
|
|
|
|
* @param values
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public boot(values?: {[key: string]: unknown}): void {
|
2021-06-02 01:59:40 +00:00
|
|
|
if ( values ) {
|
|
|
|
getFieldsMeta(this).each(field => {
|
|
|
|
this.setFieldFromObject(field.modelKey, String(field.modelKey), values)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-12 17:36:33 +00:00
|
|
|
public getColumn(key: string): unknown {
|
|
|
|
if ( this.dirtySourceRow && hasOwnProperty(this.dirtySourceRow, key) ) {
|
|
|
|
return this.dirtySourceRow[key]
|
|
|
|
}
|
|
|
|
|
|
|
|
const field = getFieldsMeta(this)
|
|
|
|
.firstWhere('databaseKey', '=', key)
|
|
|
|
|
|
|
|
if ( field ) {
|
|
|
|
return (this as any)[field.modelKey]
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.originalSourceRow?.[key]
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets a value in the override row and (if applicable) associated class property
|
|
|
|
* for the given database column.
|
|
|
|
*
|
|
|
|
* @param key
|
|
|
|
* @param value
|
|
|
|
*/
|
|
|
|
public setColumn(key: string, value: unknown): this {
|
|
|
|
// Set the property on the database result row, if one exists
|
|
|
|
if ( !this.dirtySourceRow ) {
|
|
|
|
this.dirtySourceRow = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.dirtySourceRow[key] = value
|
|
|
|
|
|
|
|
// Set the property on the mapped field on the class, if one exists
|
|
|
|
const field = getFieldsMeta(this)
|
|
|
|
.firstWhere('databaseKey', '=', key)
|
|
|
|
|
|
|
|
if ( field ) {
|
|
|
|
this.setFieldFromObject(field.modelKey, field.databaseKey, {
|
|
|
|
[key]: value,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Given a row from the database, set the properties on this model that correspond to
|
|
|
|
* fields on that database.
|
|
|
|
*
|
|
|
|
* The `row` maps database fields to values, and the values are set for the properties
|
|
|
|
* that they correspond to based on the model's `@Field()` annotations.
|
|
|
|
*
|
|
|
|
* @param row
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public async assumeFromSource(row: QueryRow): Promise<this> {
|
|
|
|
this.originalSourceRow = row
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
getFieldsMeta(this).each(field => {
|
|
|
|
this.setFieldFromObject(field.modelKey, field.databaseKey, row)
|
|
|
|
})
|
|
|
|
|
2022-01-27 01:37:54 +00:00
|
|
|
await this.push(new ModelRetrievedEvent<T>(this as any))
|
2021-06-02 01:59:40 +00:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Similar to assumeFromSource, but instead of mapping database fields to model
|
|
|
|
* properties, this function assumes the `object` contains a mapping of model properties
|
|
|
|
* to the values of those properties.
|
|
|
|
*
|
|
|
|
* Only properties with `@Field()` annotations will be set.
|
|
|
|
*
|
|
|
|
* @param object
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public async assume(object: { [key: string]: any }): Promise<this> {
|
2021-06-02 01:59:40 +00:00
|
|
|
getFieldsMeta(this).each(field => {
|
|
|
|
if ( field.modelKey in object ) {
|
|
|
|
this.setFieldFromObject(field.modelKey, String(field.modelKey), object)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the value of the primary key of this model, if it exists.
|
|
|
|
*/
|
2022-09-12 17:36:33 +00:00
|
|
|
public key(): string|number {
|
2021-06-02 01:59:40 +00:00
|
|
|
const ctor = this.constructor as typeof Model
|
|
|
|
|
|
|
|
const field = getFieldsMeta(this)
|
|
|
|
.firstWhere('databaseKey', '=', ctor.key)
|
|
|
|
|
|
|
|
if ( field ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
return (this as any)[field.modelKey]
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-06-03 03:36:25 +00:00
|
|
|
return (this as any)[ctor.key]
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this instance's record has been persisted into the database.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public exists(): boolean {
|
|
|
|
return Boolean(this.originalSourceRow) && Boolean(this.key())
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get normalized values of the configured CREATED_AT/UPDATED_AT fields for this model.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```
|
|
|
|
* user.timestamps() // => {updated: Date, created: Date}
|
|
|
|
* ```
|
|
|
|
*/
|
|
|
|
public timestamps(): { updated?: Date, created?: Date } {
|
|
|
|
const ctor = this.constructor as typeof Model
|
|
|
|
const timestamps: { updated?: Date, created?: Date } = {}
|
|
|
|
|
|
|
|
if ( ctor.timestamps ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( ctor.CREATED_AT ) {
|
|
|
|
timestamps.created = (this as any)[ctor.CREATED_AT]
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( ctor.UPDATED_AT ) {
|
|
|
|
timestamps.updated = (this as any)[ctor.UPDATED_AT]
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return timestamps
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a new query builder that yields instances of this model,
|
|
|
|
* pre-configured with this model's QuerySource, connection, and fields.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* await user.query()
|
|
|
|
* .where('name', 'LIKE', 'John Doe')
|
|
|
|
* .update({ username: 'jdoe' })
|
|
|
|
* ```
|
|
|
|
*/
|
|
|
|
public query(): ModelBuilder<T> {
|
|
|
|
const ModelClass = this.constructor as typeof Model
|
|
|
|
const builder = <ModelBuilder<T>> this.app().make<ModelBuilder<T>>(ModelBuilder, ModelClass)
|
|
|
|
const source: QuerySource = ModelClass.querySource()
|
|
|
|
|
|
|
|
builder.connection(ModelClass.getConnection())
|
|
|
|
|
2022-09-30 16:42:13 +00:00
|
|
|
if ( typeof source === 'string' || source instanceof QuerySafeValue ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
builder.from(source)
|
|
|
|
} else {
|
|
|
|
builder.from(source.table, source.alias)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
getFieldsMeta(this).each(field => {
|
|
|
|
builder.field(field.databaseKey)
|
|
|
|
})
|
|
|
|
|
2021-11-11 22:42:37 +00:00
|
|
|
for ( const relation of this.with ) {
|
|
|
|
builder.with(relation)
|
|
|
|
}
|
|
|
|
|
2022-09-12 17:36:33 +00:00
|
|
|
this.applyScopes(builder)
|
2021-06-02 01:59:40 +00:00
|
|
|
return builder
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the first instance of this model where the primary key matches `key`.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```
|
|
|
|
* const user = await UserModel.findByKey(45)
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param key
|
|
|
|
*/
|
|
|
|
public static async findByKey<T2 extends Model<T2>>(key: ModelKey): Promise<undefined | T2> {
|
|
|
|
return this.query<T2>()
|
|
|
|
.where(this.qualifyKey(), '=', key)
|
|
|
|
.limit(1)
|
|
|
|
.get()
|
|
|
|
.first()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an array of all instances of this model.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public async all(): Promise<T[]> {
|
|
|
|
return this.query().get()
|
|
|
|
.all()
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Count all instances of this model in the database.
|
|
|
|
*/
|
|
|
|
public async count(): Promise<number> {
|
2021-06-03 03:36:25 +00:00
|
|
|
return this.query().get()
|
|
|
|
.count()
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given the name of a column, return the qualified name of the column as it
|
|
|
|
* could appear in a query.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* modelInstance.qualify('id') // => 'model_table_name.id'
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param column
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public qualify(column: string): string {
|
2021-06-02 01:59:40 +00:00
|
|
|
const ctor = this.constructor as typeof Model
|
|
|
|
return `${ctor.tableName()}.${column}`
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the qualified name of the column corresponding to the model's primary key.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* class A extends Model<A> {
|
|
|
|
* protected static table = 'table_a'
|
|
|
|
* protected static key = 'a_id'
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* const a = new A()
|
|
|
|
* a.qualifyKey() // => 'table_a.a_id'
|
|
|
|
* ```
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public qualifyKey(): string {
|
2021-06-02 01:59:40 +00:00
|
|
|
const ctor = this.constructor as typeof Model
|
|
|
|
return this.qualify(ctor.key)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given the name of a column, return the qualified name of the column as it
|
|
|
|
* could appear in a query.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* SomeModel.qualify('col_name') // => 'model_table_name.col_name'
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param column
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public static qualify(column: string): string {
|
2021-06-02 01:59:40 +00:00
|
|
|
return `${this.tableName()}.${column}`
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the qualified name of the column corresponding to the model's primary key.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* class A extends Model<A> {
|
|
|
|
* protected static table = 'table_a'
|
|
|
|
* protected static key = 'a_id'
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* A.qualifyKey() // => 'table_a.a_id'
|
|
|
|
* ```
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public static qualifyKey(): string {
|
2021-06-02 01:59:40 +00:00
|
|
|
return this.qualify(this.key)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given the name of a property on the model with a `@Field()` annotation,
|
|
|
|
* return the unqualified name of the database column it corresponds to.
|
|
|
|
* @param modelKey
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public static propertyToColumn(modelKey: string): string {
|
2022-09-12 17:36:33 +00:00
|
|
|
console.log('propertyToColumn', modelKey, getFieldsMeta(this), this) // eslint-disable-line no-console
|
2021-06-02 01:59:40 +00:00
|
|
|
return getFieldsMeta(this)
|
|
|
|
.firstWhere('modelKey', '=', modelKey)?.databaseKey || modelKey
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the unqualified name of the column corresponding to the primary key of this model.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public keyName(): string {
|
2021-06-02 01:59:40 +00:00
|
|
|
const ctor = this.constructor as typeof Model
|
|
|
|
return ctor.key
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cast the model to the base QueryRow object. The resultant object maps
|
|
|
|
* DATABASE fields to values, NOT MODEL fields to values.
|
|
|
|
*
|
|
|
|
* Only fields with `@Field()` annotations will be included.
|
|
|
|
*/
|
|
|
|
public toQueryRow(): QueryRow {
|
2021-06-03 03:36:25 +00:00
|
|
|
const row: QueryRow = {}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
getFieldsMeta(this).each(field => {
|
2021-06-03 03:36:25 +00:00
|
|
|
row[field.databaseKey] = (this as any)[field.modelKey]
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return row
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a query row mapping database columns to values for properties on this
|
|
|
|
* model that (1) have `@Field()` annotations and (2) have been modified since
|
|
|
|
* the record was fetched from the database or created.
|
|
|
|
*/
|
|
|
|
public dirtyToQueryRow(): QueryRow {
|
2021-06-03 03:36:25 +00:00
|
|
|
const row: QueryRow = {}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
getFieldsMeta(this)
|
2021-06-03 03:36:25 +00:00
|
|
|
.filter(this.isDirtyCheck)
|
2021-06-02 01:59:40 +00:00
|
|
|
.each(field => {
|
2021-06-03 03:36:25 +00:00
|
|
|
row[field.databaseKey] = (this as any)[field.modelKey]
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
|
2022-09-12 17:36:33 +00:00
|
|
|
return {
|
|
|
|
...row,
|
|
|
|
...(this.dirtySourceRow || {}),
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an object of the database field => value mapping that was originally
|
|
|
|
* fetched from the database. Excludes changes to model properties.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public getOriginalValues(): QueryRow | undefined {
|
|
|
|
return deepCopy(this.originalSourceRow)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an object of only the given properties on this model.
|
2021-06-03 03:36:25 +00:00
|
|
|
*
|
2021-06-02 01:59:40 +00:00
|
|
|
* @example
|
|
|
|
* Assume `a` is an instance of some model `A` with the given fields.
|
|
|
|
* ```typescript
|
|
|
|
* const a = new A({ field1: 'field1 value', field2: 'field2 value', id: 123 })
|
|
|
|
*
|
|
|
|
* a.only('field1', 'id) // => {field1: 'field1 value', id: 123}
|
|
|
|
* ```
|
2021-06-03 03:36:25 +00:00
|
|
|
*
|
2021-06-02 01:59:40 +00:00
|
|
|
* @param fields
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public only(...fields: string[]): QueryRow {
|
|
|
|
const row: QueryRow = {}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
for ( const field of fields ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
row[field] = (this as any)[field]
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return row
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if any of the fields on this model have been modified since they
|
|
|
|
* were fetched from the database (or ones that were never saved to the database).
|
|
|
|
*
|
|
|
|
* Only fields with `@Field()` annotations are checked.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public isDirty(): boolean {
|
|
|
|
return getFieldsMeta(this).some(this.isDirtyCheck)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if none of the fields on this model have been modified since they
|
|
|
|
* were fetched from the database (and all exist in the database).
|
|
|
|
*
|
|
|
|
* Only fields with `@Field()` annotations are checked.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public isClean(): boolean {
|
2021-06-02 01:59:40 +00:00
|
|
|
return !this.isDirty()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the given field has changed since this model was fetched from
|
|
|
|
* the database, or if the given field never existed in the database.
|
|
|
|
* @param field
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public wasChanged(field: string): boolean {
|
|
|
|
return (
|
|
|
|
getFieldsMeta(this)
|
|
|
|
.pluck('modelKey')
|
|
|
|
.includes(field)
|
|
|
|
&& (
|
|
|
|
!this.originalSourceRow
|
|
|
|
|| (this as any)[field] !== this.originalSourceRow[field]
|
|
|
|
)
|
|
|
|
)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an array of MODEL fields that have been modified since this record
|
|
|
|
* was fetched from the database or created.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public getDirtyFields(): string[] {
|
2021-06-02 01:59:40 +00:00
|
|
|
return getFieldsMeta(this)
|
2021-06-03 03:36:25 +00:00
|
|
|
.filter(this.isDirtyCheck)
|
2021-06-02 01:59:40 +00:00
|
|
|
.pluck('modelKey')
|
|
|
|
.toArray()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the timestamps for this model, if they are configured.
|
|
|
|
*
|
|
|
|
* If the model doesn't yet exist, set the CREATED_AT date. Always
|
|
|
|
* sets the UPDATED_AT date.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public touch(): this {
|
2021-06-02 01:59:40 +00:00
|
|
|
const constructor = (this.constructor as typeof Model)
|
|
|
|
if ( constructor.timestamps ) {
|
|
|
|
if ( constructor.UPDATED_AT ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
(this as any)[constructor.UPDATED_AT] = new Date()
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( !this.exists() && constructor.CREATED_AT ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
(this as any)[constructor.CREATED_AT] = new Date()
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Persist the model into the database. If the model already exists, perform an
|
|
|
|
* update on its fields. Otherwise, insert a new row with its fields.
|
|
|
|
*
|
|
|
|
* Passing the `withoutTimestamps` will prevent the configured CREATED_AT/UPDATED_AT
|
|
|
|
* timestamps from being updated.
|
|
|
|
*
|
|
|
|
* @param withoutTimestamps
|
|
|
|
*/
|
|
|
|
public async save({ withoutTimestamps = false } = {}): Promise<Model<T>> {
|
2022-01-27 01:37:54 +00:00
|
|
|
await this.push(new ModelSavingEvent<T>(this as any))
|
2021-06-02 01:59:40 +00:00
|
|
|
const ctor = this.constructor as typeof Model
|
|
|
|
|
|
|
|
if ( this.exists() && this.isDirty() ) {
|
2022-01-27 01:37:54 +00:00
|
|
|
await this.push(new ModelUpdatingEvent<T>(this as any))
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
if ( !withoutTimestamps && ctor.timestamps && ctor.UPDATED_AT ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
(this as any)[ctor.UPDATED_AT] = new Date()
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const result = await this.query()
|
|
|
|
.where(this.qualifyKey(), '=', this.key())
|
|
|
|
.clearFields()
|
|
|
|
.returning(...this.getLoadedDatabaseFields())
|
|
|
|
.update(this.dirtyToQueryRow())
|
|
|
|
|
|
|
|
if ( result.rowCount !== 1 ) {
|
|
|
|
this.logging.warn(`Model update modified ${result.rowCount} rows! Expected 1. (Key: ${this.qualifyKey()})`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = result.rows.firstWhere(this.keyName(), '=', this.key())
|
2022-09-12 17:36:33 +00:00
|
|
|
this.logging.debug({updata: data})
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( data ) {
|
|
|
|
await this.assumeFromSource(data)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
2022-09-12 17:36:33 +00:00
|
|
|
delete this.dirtySourceRow
|
2022-01-27 01:37:54 +00:00
|
|
|
await this.push(new ModelUpdatedEvent<T>(this as any))
|
2021-06-02 01:59:40 +00:00
|
|
|
} else if ( !this.exists() ) {
|
2022-01-27 01:37:54 +00:00
|
|
|
await this.push(new ModelCreatingEvent<T>(this as any))
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
if ( !withoutTimestamps ) {
|
|
|
|
if ( ctor.timestamps && ctor.CREATED_AT ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
(this as any)[ctor.CREATED_AT] = new Date()
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( ctor.timestamps && ctor.UPDATED_AT ) {
|
2021-06-03 03:36:25 +00:00
|
|
|
(this as any)[ctor.UPDATED_AT] = new Date()
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-03 03:36:25 +00:00
|
|
|
const row = this.buildInsertFieldObject()
|
2021-09-22 03:25:51 +00:00
|
|
|
this.logging.debug('Insert field object:')
|
|
|
|
this.logging.debug(row)
|
2021-06-02 01:59:40 +00:00
|
|
|
const returnable = new Collection<string>([this.keyName(), ...Object.keys(row)])
|
|
|
|
|
|
|
|
const result = await this.query()
|
|
|
|
.clearFields()
|
|
|
|
.returning(...returnable.unique().toArray())
|
|
|
|
.insert(row)
|
|
|
|
|
|
|
|
if ( result.rowCount !== 1 ) {
|
|
|
|
this.logging.warn(`Model insert created ${result.rowCount} rows! Expected 1. (Key: ${this.qualifyKey()})`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = result.rows.first()
|
2022-09-12 17:36:33 +00:00
|
|
|
this.logging.debug({inserta: data})
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( data ) {
|
2021-10-20 03:29:15 +00:00
|
|
|
await this.assumeFromSource(data)
|
2021-06-03 03:36:25 +00:00
|
|
|
}
|
2021-06-04 06:03:31 +00:00
|
|
|
|
2022-09-12 17:36:33 +00:00
|
|
|
delete this.dirtySourceRow
|
2022-01-27 01:37:54 +00:00
|
|
|
await this.push(new ModelCreatedEvent<T>(this as any))
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
2022-01-27 01:37:54 +00:00
|
|
|
await this.push(new ModelSavedEvent<T>(this as any))
|
2021-06-02 01:59:40 +00:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-08-24 04:51:53 +00:00
|
|
|
/**
|
|
|
|
* Delete the current model from the database, if it exists.
|
|
|
|
*/
|
|
|
|
async delete(): Promise<void> {
|
|
|
|
if ( !this.exists() ) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.query()
|
|
|
|
.where(this.qualifyKey(), '=', this.key())
|
|
|
|
.delete()
|
|
|
|
|
|
|
|
const ctor = this.constructor as typeof Model
|
|
|
|
const field = getFieldsMeta(this)
|
|
|
|
.firstWhere('databaseKey', '=', ctor.key)
|
|
|
|
|
|
|
|
if ( field ) {
|
|
|
|
delete (this as any)[field.modelKey]
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
delete (this as any)[ctor.key]
|
|
|
|
}
|
|
|
|
|
2021-06-02 01:59:40 +00:00
|
|
|
/**
|
|
|
|
* Cast this model to a simple object mapping model fields to their values.
|
|
|
|
*
|
|
|
|
* Only fields with `@Field()` annotations are included.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public toObject(): QueryRow {
|
2021-06-02 01:59:40 +00:00
|
|
|
const ctor = this.constructor as typeof Model
|
2021-06-03 03:36:25 +00:00
|
|
|
const obj: QueryRow = {}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
getFieldsMeta(this).each(field => {
|
2021-06-03 03:36:25 +00:00
|
|
|
obj[String(field.modelKey)] = (this as any)[field.modelKey]
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
ctor.appends.forEach(field => {
|
2021-06-03 03:36:25 +00:00
|
|
|
obj[field] = (this as any)[field]
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
ctor.masks.forEach(field => {
|
|
|
|
delete obj[field]
|
|
|
|
})
|
|
|
|
|
|
|
|
return obj
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cast the model to an JSON string object.
|
|
|
|
*
|
|
|
|
* Only fields with `@Field()` annotations are included.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public toJSON(): QueryRow {
|
|
|
|
return this.toObject()
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch a fresh instance of this record from the database.
|
|
|
|
*
|
|
|
|
* This returns a NEW instance of the SAME record by matching on
|
|
|
|
* the primary key. It does NOT change the current instance of the record.
|
|
|
|
*/
|
|
|
|
public async fresh(): Promise<Model<T> | undefined> {
|
|
|
|
return this.query()
|
|
|
|
.where(this.qualifyKey(), '=', this.key())
|
|
|
|
.limit(1)
|
|
|
|
.get()
|
|
|
|
.first()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Re-load the currently-loaded database fields from the table.
|
|
|
|
*
|
|
|
|
* Overwrites any un-persisted changes in the current instance.
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
public async refresh(): Promise<void> {
|
2021-06-02 01:59:40 +00:00
|
|
|
const results = this.query()
|
|
|
|
.clearFields()
|
|
|
|
.fields(...this.getLoadedDatabaseFields())
|
|
|
|
.where(this.qualifyKey(), '=', this.key())
|
|
|
|
.limit(1)
|
|
|
|
.get()
|
|
|
|
|
|
|
|
const row = await results.first()
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( row ) {
|
|
|
|
await this.assumeFromSource(row)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Populates an instance of the model with the same database fields that
|
|
|
|
* are set on this model, with the exclusion of the primary key.
|
|
|
|
*
|
|
|
|
* Useful for inserting copies of records.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* Assume a record, `a`, is an instance of some model `A` with the given fields.
|
|
|
|
*
|
|
|
|
* ```typescript
|
|
|
|
* const a = A.find(123) // => A{id: 123, name: 'some_name', other_field: 'a value'}
|
|
|
|
*
|
|
|
|
* const b = a.populate(new A) // => A{name: 'some_name', other_field: 'a value'}
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param model
|
|
|
|
*/
|
|
|
|
public async populate(model: T): Promise<T> {
|
|
|
|
const row = this.toQueryRow()
|
|
|
|
delete row[this.keyName()]
|
|
|
|
await model.assumeFromSource(row)
|
|
|
|
return model
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the `other` model refers to the same database record as this instance.
|
|
|
|
*
|
|
|
|
* This is done by comparing the qualified primary keys.
|
|
|
|
*
|
|
|
|
* @param other
|
|
|
|
*/
|
|
|
|
public is(other: Model<any>): boolean {
|
|
|
|
return this.key() === other.key() && this.qualifyKey() === other.qualifyKey()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Inverse of `is()`.
|
|
|
|
* @param other
|
|
|
|
*/
|
|
|
|
public isNot(other: Model<any>): boolean {
|
|
|
|
return !this.is(other)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a wrapped function that compares whether the given model field
|
|
|
|
* on the current instance differs from the originally fetched value.
|
|
|
|
*
|
|
|
|
* Used to filter for dirty fields.
|
|
|
|
*
|
|
|
|
* @protected
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
protected get isDirtyCheck(): (field: ModelField) => boolean {
|
2021-06-02 01:59:40 +00:00
|
|
|
return (field: ModelField) => {
|
2022-09-12 17:36:33 +00:00
|
|
|
return Boolean(
|
|
|
|
!this.originalSourceRow
|
|
|
|
|| (this as any)[field.modelKey] !== this.originalSourceRow[field.databaseKey]
|
|
|
|
|| (
|
|
|
|
this.dirtySourceRow
|
|
|
|
&& hasOwnProperty(this.dirtySourceRow, field.databaseKey)
|
|
|
|
),
|
|
|
|
)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a list of DATABASE fields that have been loaded for the current instance.
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected getLoadedDatabaseFields(): string[] {
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( !this.originalSourceRow ) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
return Object.keys(this.originalSourceRow).map(String)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build an object mapping database fields to the values that should be inserted for them.
|
|
|
|
* @private
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
private buildInsertFieldObject(): EscapeValueObject {
|
2021-06-02 01:59:40 +00:00
|
|
|
const ctor = this.constructor as typeof Model
|
|
|
|
|
2021-09-22 03:25:51 +00:00
|
|
|
this.logging.debug(`buildInsertFieldObject populateKeyOnInsert? ${ctor.populateKeyOnInsert}; keyName: ${this.keyName()}`)
|
|
|
|
|
2022-09-12 17:36:33 +00:00
|
|
|
const row = Pipeline.id<Collection<ModelField>>()
|
2021-06-02 01:59:40 +00:00
|
|
|
.unless(ctor.populateKeyOnInsert, fields => {
|
2021-09-22 03:25:51 +00:00
|
|
|
return fields.where('databaseKey', '!=', this.keyName())
|
2021-06-02 01:59:40 +00:00
|
|
|
})
|
2022-01-17 21:57:40 +00:00
|
|
|
.apply(getFieldsMeta(this))
|
2021-06-03 03:36:25 +00:00
|
|
|
.keyMap('databaseKey', inst => (this as any)[inst.modelKey])
|
2022-09-12 17:36:33 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
...row,
|
|
|
|
...(this.dirtySourceRow || {}),
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets a property on `this` to the value of a given property in `object`.
|
2021-06-03 03:36:25 +00:00
|
|
|
* @param thisFieldName
|
|
|
|
* @param objectFieldName
|
2021-06-02 01:59:40 +00:00
|
|
|
* @param object
|
|
|
|
* @protected
|
|
|
|
*/
|
2021-06-03 03:36:25 +00:00
|
|
|
protected setFieldFromObject(thisFieldName: string | symbol, objectFieldName: string, object: QueryRow): void {
|
|
|
|
(this as any)[thisFieldName] = object[objectFieldName]
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
2021-06-04 06:03:31 +00:00
|
|
|
|
2021-11-11 03:30:59 +00:00
|
|
|
/**
|
|
|
|
* Create a new one-to-one relation instance. Should be called from a method on the model:
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* class MyModel extends Model<MyModel> {
|
|
|
|
* @Related()
|
|
|
|
* public otherModel() {
|
|
|
|
* return this.hasOne(MyOtherModel)
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param related
|
|
|
|
* @param foreignKeyOverride
|
|
|
|
* @param localKeyOverride
|
|
|
|
*/
|
|
|
|
public hasOne<T2 extends Model<T2>>(related: Instantiable<T2>, foreignKeyOverride?: keyof T & string, localKeyOverride?: keyof T2 & string): HasOne<T, T2> {
|
|
|
|
return new HasOne<T, T2>(this as unknown as T, this.make(related), foreignKeyOverride, localKeyOverride)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new one-to-one relation instance. Should be called from a method on the model:
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* class MyModel extends Model<MyModel> {
|
|
|
|
* @Related()
|
|
|
|
* public otherModels() {
|
|
|
|
* return this.hasMany(MyOtherModel)
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param related
|
|
|
|
* @param foreignKeyOverride
|
|
|
|
* @param localKeyOverride
|
|
|
|
*/
|
|
|
|
public hasMany<T2 extends Model<T2>>(related: Instantiable<T2>, foreignKeyOverride?: keyof T & string, localKeyOverride?: keyof T2 & string): HasMany<T, T2> {
|
|
|
|
return new HasMany<T, T2>(this as unknown as T, this.make(related), foreignKeyOverride, localKeyOverride)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the inverse of a one-to-one relation. Should be called from a method on the model:
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* class MyModel extends Model<MyModel> {
|
|
|
|
* @Related()
|
|
|
|
* public otherModel() {
|
|
|
|
* return this.hasOne(MyOtherModel)
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* class MyOtherModel extends Model<MyOtherModel> {
|
|
|
|
* @Related()
|
|
|
|
* public myModel() {
|
|
|
|
* return this.belongsToOne(MyModel, 'otherModel')
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param related
|
|
|
|
* @param relationName
|
|
|
|
*/
|
2022-09-12 17:36:33 +00:00
|
|
|
public belongsToOne<T2 extends Model<T2>>(related: Instantiable<T2>, relationName: keyof T2): HasOne<T, T2> {
|
2021-11-11 03:30:59 +00:00
|
|
|
const relatedInst = this.make(related) as T2
|
|
|
|
const relation = relatedInst.getRelation(relationName)
|
|
|
|
|
|
|
|
if ( !(relation instanceof HasOneOrMany) ) {
|
|
|
|
throw new TypeError(`Cannot create belongs to one relation. Inverse relation must be HasOneOrMany.`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const localKey = relation.localKey
|
|
|
|
const foreignKey = relation.foreignKey
|
|
|
|
|
|
|
|
if ( !isKeyof(localKey, this as unknown as T) || !isKeyof(foreignKey, relatedInst) ) {
|
|
|
|
throw new TypeError('Local or foreign keys do not exist on the base model.')
|
|
|
|
}
|
|
|
|
|
|
|
|
return new HasOne<T, T2>(this as unknown as T, relatedInst, localKey, foreignKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the inverse of a one-to-many relation. Should be called from a method on the model:
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* class MyModel extends Model<MyModel> {
|
|
|
|
* @Related()
|
|
|
|
* public otherModels() {
|
|
|
|
* return this.hasMany(MyOtherModel)
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* class MyOtherModel extends Model<MyOtherModel> {
|
|
|
|
* @Related()
|
|
|
|
* public myModels() {
|
|
|
|
* return this.belongsToMany(MyModel, 'otherModels')
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param related
|
|
|
|
* @param relationName
|
|
|
|
*/
|
|
|
|
public belongsToMany<T2 extends Model<T2>>(related: Instantiable<T>, relationName: keyof T2): HasMany<T, T2> {
|
|
|
|
const relatedInst = this.make(related) as T2
|
|
|
|
const relation = relatedInst.getRelation(relationName)
|
|
|
|
|
|
|
|
if ( !(relation instanceof HasOneOrMany) ) {
|
|
|
|
throw new TypeError(`Cannot create belongs to one relation. Inverse relation must be HasOneOrMany.`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const localKey = relation.localKey
|
|
|
|
const foreignKey = relation.foreignKey
|
|
|
|
|
|
|
|
if ( !isKeyof(localKey, this as unknown as T) || !isKeyof(foreignKey, relatedInst) ) {
|
|
|
|
throw new TypeError('Local or foreign keys do not exist on the base model.')
|
|
|
|
}
|
|
|
|
|
|
|
|
return new HasMany<T, T2>(this as unknown as T, relatedInst, localKey, foreignKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the relation instance returned by a method on this model.
|
|
|
|
* @param name
|
|
|
|
* @protected
|
|
|
|
*/
|
2021-11-11 22:42:37 +00:00
|
|
|
public getRelation<T2 extends Model<T2>>(name: keyof this): Relation<T, T2, RelationValue<T2>> {
|
2021-11-11 03:30:59 +00:00
|
|
|
const relFn = this[name]
|
|
|
|
|
|
|
|
if ( relFn instanceof Relation ) {
|
|
|
|
return relFn
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( typeof relFn === 'function' ) {
|
2022-08-20 21:21:06 +00:00
|
|
|
const rel = relFn.bind(this)()
|
2021-11-11 03:30:59 +00:00
|
|
|
if ( rel instanceof Relation ) {
|
|
|
|
return rel
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-20 21:21:06 +00:00
|
|
|
throw new TypeError(`Cannot get relation of name: ${String(name)}. Method does not return a Relation.`)
|
2021-11-11 03:30:59 +00:00
|
|
|
}
|
2021-11-11 22:42:37 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Register a scope on the model.
|
|
|
|
* @param scope
|
|
|
|
* @protected
|
|
|
|
*/
|
2022-03-29 06:14:46 +00:00
|
|
|
protected withScope(scope: Instantiable<Scope> | ScopeClosure): this {
|
2021-11-11 22:42:37 +00:00
|
|
|
if ( isInstantiable(scope) ) {
|
|
|
|
if ( !this.hasScope(scope) ) {
|
|
|
|
this.scopes.push({
|
|
|
|
accessor: scope,
|
|
|
|
scope: builder => (this.make<Scope>(scope)).apply(builder),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.scopes.push({
|
|
|
|
accessor: uuid4(),
|
|
|
|
scope,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register a scope on the model with a specific name.
|
|
|
|
* @param name
|
|
|
|
* @param scope
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected namedScope(name: string, scope: Instantiable<Scope> | ScopeClosure): this {
|
|
|
|
if ( isInstantiable(scope) ) {
|
|
|
|
if ( !this.hasScope(scope) ) {
|
|
|
|
this.scopes.push({
|
|
|
|
accessor: name,
|
|
|
|
scope: builder => (this.make<Scope>(scope)).apply(builder),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.scopes.push({
|
|
|
|
accessor: name,
|
|
|
|
scope,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the current model has a scope with the given identifier.
|
|
|
|
* @param name
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected hasScope(name: string | Instantiable<Scope>): boolean {
|
|
|
|
return Boolean(this.scopes.firstWhere('accessor', '=', name))
|
|
|
|
}
|
2022-09-12 17:36:33 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply the default scopes to this model to the given query builder.
|
|
|
|
* @param builder
|
|
|
|
*/
|
|
|
|
public applyScopes(builder: ModelBuilder<T>): void {
|
|
|
|
builder.withScopes(this.scopes)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|