Implement scopes on models and support interacting with them via ModelBuilder
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-11-11 16:42:37 -06:00
parent d92c8b5409
commit 0a9dd30909
9 changed files with 229 additions and 13 deletions

View File

@@ -1,5 +1,5 @@
import {ModelKey, QueryRow, QuerySource} from '../types'
import {Container, Inject, Instantiable, StaticClass} from '../../di'
import {Container, Inject, Instantiable, isInstantiable, StaticClass} from '../../di'
import {DatabaseService} from '../DatabaseService'
import {ModelBuilder} from './ModelBuilder'
import {getFieldsMeta, ModelField} from './Field'
@@ -21,6 +21,7 @@ import {Relation, RelationValue} from './relation/Relation'
import {HasOne} from './relation/HasOne'
import {HasMany} from './relation/HasMany'
import {HasOneOrMany} from './relation/HasOneOrMany'
import {Scope, ScopeClosure} from './scope/Scope'
/**
* Base for classes that are mapped to tables in a database.
@@ -87,6 +88,12 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
*/
protected static masks: string[] = []
/**
* Relations that should be eager-loaded by default.
* @protected
*/
protected with: (keyof T)[] = []
/**
* The original row fetched from the database.
* @protected
@@ -105,6 +112,8 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
*/
public relationCache: Collection<{ accessor: string | symbol, relation: Relation<T, any, any> }> = new Collection<{accessor: string | symbol; relation: Relation<T, any, any>}>()
protected scopes: Collection<{ accessor: string | Instantiable<Scope>, scope: ScopeClosure }> = new Collection<{accessor: string | Instantiable<Scope>; scope: ScopeClosure}>()
/**
* Get the table name for this model.
*/
@@ -165,6 +174,14 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
builder.field(field.databaseKey)
})
for ( const relation of this.prototype.with ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
builder.with(relation)
}
builder.withScopes(this.prototype.scopes)
return builder
}
@@ -317,6 +334,12 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
builder.field(field.databaseKey)
})
for ( const relation of this.with ) {
builder.with(relation)
}
builder.withScopes(this.scopes)
return builder
}
@@ -1013,7 +1036,7 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
* @param name
* @protected
*/
protected getRelation<T2 extends Model<T2>>(name: keyof this): Relation<T, T2, RelationValue<T2>> {
public getRelation<T2 extends Model<T2>>(name: keyof this): Relation<T, T2, RelationValue<T2>> {
const relFn = this[name]
if ( relFn instanceof Relation ) {
@@ -1029,4 +1052,60 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
throw new TypeError(`Cannot get relation of name: ${name}. Method does not return a Relation.`)
}
/**
* Register a scope on the model.
* @param scope
* @protected
*/
protected scope(scope: Instantiable<Scope> | ScopeClosure): this {
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))
}
}