swaf/src/db/ModelRelation.ts

233 lines
8.1 KiB
TypeScript

import ModelQuery, {ModelQueryResult, WhereTest} from "./ModelQuery";
import Model from "./Model";
import ModelFactory from "./ModelFactory";
export default abstract class ModelRelation<S extends Model, O extends Model, R extends O | O[] | null> {
protected readonly model: S;
protected readonly foreignFactory: ModelFactory<O>;
private readonly query: ModelQuery<O>;
protected readonly queryModifiers: QueryModifier<O>[] = [];
protected readonly filters: ModelFilter<O>[] = [];
protected cachedModels?: R;
protected constructor(model: S, foreignFactory: ModelFactory<O>) {
this.model = model;
this.foreignFactory = foreignFactory;
this.query = this.foreignFactory.select();
}
public abstract clone(): ModelRelation<S, O, R>;
public constraint(queryModifier: QueryModifier<O>): this {
this.queryModifiers.push(queryModifier);
return this;
}
public filter(modelFilter: ModelFilter<O>): this {
this.filters.push(modelFilter);
return this;
}
protected getFinalQuery(): ModelQuery<O> {
for (const modifier of this.queryModifiers) modifier(this.query);
this.queryModifiers.splice(0, this.queryModifiers.length); // Empty modifier now that they were applied
return this.query;
}
public abstract getModelID(): any;
protected abstract async compute(query: ModelQuery<O>): Promise<R>;
public async get(): Promise<R> {
if (this.cachedModels === undefined) {
this.cachedModels = await this.compute(this.getFinalQuery());
}
return this.cachedModels;
}
public getOrFail(): R {
if (this.cachedModels === undefined) throw new Error('Models were not fetched');
return this.cachedModels;
}
public abstract async eagerLoad(relations: ModelRelation<S, O, R>[]): Promise<ModelQueryResult<O>>;
public abstract async populate(models: ModelQueryResult<O>): Promise<void>;
}
export class OneModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O | null> {
protected readonly dbProperties: RelationDatabaseProperties;
constructor(model: S, foreignFactory: ModelFactory<O>, dbProperties: RelationDatabaseProperties) {
super(model, foreignFactory);
this.dbProperties = dbProperties;
}
public clone(): OneModelRelation<S, O> {
return new OneModelRelation(this.model, this.foreignFactory, this.dbProperties);
}
public getModelID() {
return this.model[this.dbProperties.localKey];
}
protected async compute(query: ModelQuery<O>): Promise<O | null> {
this.getFinalQuery().where(this.dbProperties.foreignKey, this.getModelID());
return await query.first();
}
public async get(): Promise<O | null> {
const model = await super.get();
if (model) {
for (const filter of this.filters) {
if (!(await filter(model))) {
return null;
}
}
}
return model;
}
public async eagerLoad(relations: ModelRelation<S, O, O | null>[]): Promise<ModelQueryResult<O>> {
const ids = relations.map(r => r.getModelID()).filter(id => id !== null && id !== undefined);
if (ids.length === 0) return [];
const query = this.getFinalQuery();
query.where(this.dbProperties.foreignKey, ids, WhereTest.IN);
return await query.get();
}
public async populate(models: ModelQueryResult<O>): Promise<void> {
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID())[0] || null;
}
}
export class ManyModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
protected readonly dbProperties: RelationDatabaseProperties;
constructor(model: S, foreignFactory: ModelFactory<O>, dbProperties: RelationDatabaseProperties) {
super(model, foreignFactory);
this.dbProperties = dbProperties;
}
public clone(): ManyModelRelation<S, O> {
return new ManyModelRelation<S, O>(this.model, this.foreignFactory, this.dbProperties);
}
public cloneReduceToOne(): OneModelRelation<S, O> {
return new OneModelRelation<S, O>(this.model, this.foreignFactory, this.dbProperties);
}
public getModelID(): any {
return this.model[this.dbProperties.localKey];
}
protected async compute(query: ModelQuery<O>): Promise<O[]> {
this.getFinalQuery().where(this.dbProperties.foreignKey, this.getModelID());
return await query.get();
}
public async get(): Promise<O[]> {
let models = await super.get();
for (const filter of this.filters) {
const newModels = [];
for (const model of models) {
if (await filter(model)) {
newModels.push(model);
}
}
models = newModels;
}
return models;
}
public async eagerLoad(relations: ModelRelation<S, O, O[]>[]): Promise<ModelQueryResult<O>> {
const ids = relations.map(r => r.getModelID()).filter(id => id !== null && id !== undefined);
if (ids.length === 0) return [];
const query = this.getFinalQuery();
query.where(this.dbProperties.foreignKey, ids, WhereTest.IN);
return await query.get();
}
public async populate(models: ModelQueryResult<O>): Promise<void> {
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID());
}
}
export class ManyThroughModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
protected readonly dbProperties: PivotRelationDatabaseProperties;
constructor(model: S, foreignFactory: ModelFactory<O>, dbProperties: PivotRelationDatabaseProperties) {
super(model, foreignFactory);
this.dbProperties = dbProperties;
this.constraint(query => query
.leftJoin(`${this.dbProperties.pivotTable} as pivot`)
.on(`pivot.${this.dbProperties.foreignPivotKey}`, `${this.foreignFactory.table}.${this.dbProperties.foreignKey}`)
);
}
public clone(): ManyThroughModelRelation<S, O> {
return new ManyThroughModelRelation<S, O>(this.model, this.foreignFactory, this.dbProperties);
}
public getModelID(): any {
return this.model[this.dbProperties.localKey];
}
protected async compute(query: ModelQuery<O>): Promise<O[]> {
this.getFinalQuery().where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID());
return await query.get();
}
public async get(): Promise<O[]> {
let models = await super.get();
for (const filter of this.filters) {
const newModels = [];
for (const model of models) {
if (await filter(model)) {
newModels.push(model);
}
}
models = newModels;
}
return models;
}
public async eagerLoad(relations: ModelRelation<S, O, O[]>[]): Promise<ModelQueryResult<O>> {
const ids = relations.map(r => r.getModelID());
if (ids.length === 0) return [];
const query = this.getFinalQuery();
query.where(`pivot.${this.dbProperties.localPivotKey}`, ids, WhereTest.IN);
query.pivot(`pivot.${this.dbProperties.localPivotKey}`, `pivot.${this.dbProperties.foreignPivotKey}`);
return await query.get();
}
public async populate(models: ModelQueryResult<O>): Promise<void> {
const ids = models.pivot!
.filter(p => p[this.dbProperties.localPivotKey] === this.getModelID())
.map(p => p[this.dbProperties.foreignPivotKey]);
this.cachedModels = models.filter(m => ids.indexOf(m[this.dbProperties.foreignKey]) >= 0);
}
}
export type QueryModifier<M extends Model> = (query: ModelQuery<M>) => ModelQuery<M>;
export type ModelFilter<O extends Model> = (model: O) => boolean | Promise<boolean>;
export type RelationDatabaseProperties = {
localKey: string;
foreignKey: string;
};
export type PivotRelationDatabaseProperties = RelationDatabaseProperties & {
pivotTable: string;
localPivotKey: string;
foreignPivotKey: string;
};