import ModelQuery, {ModelQueryResult, WhereTest} from "./ModelQuery"; import Model from "./Model"; import ModelFactory from "./ModelFactory"; export default abstract class ModelRelation { protected readonly model: S; protected readonly foreignFactory: ModelFactory; private readonly query: ModelQuery; protected readonly queryModifiers: QueryModifier[] = []; protected readonly filters: ModelFilter[] = []; protected cachedModels?: R; protected constructor(model: S, foreignFactory: ModelFactory) { this.model = model; this.foreignFactory = foreignFactory; this.query = this.foreignFactory.select(); } public abstract clone(): ModelRelation; public constraint(queryModifier: QueryModifier): this { this.queryModifiers.push(queryModifier); return this; } public filter(modelFilter: ModelFilter): this { this.filters.push(modelFilter); return this; } protected getFinalQuery(): ModelQuery { 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): Promise; public async get(): Promise { 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[]): Promise>; public abstract async populate(models: ModelQueryResult): Promise; } export class OneModelRelation extends ModelRelation { protected readonly dbProperties: RelationDatabaseProperties; constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { super(model, foreignFactory); this.dbProperties = dbProperties; } public clone(): OneModelRelation { return new OneModelRelation(this.model, this.foreignFactory, this.dbProperties); } public getModelID() { return this.model[this.dbProperties.localKey]; } protected async compute(query: ModelQuery): Promise { this.getFinalQuery().where(this.dbProperties.foreignKey, this.getModelID()); return await query.first(); } public async get(): Promise { 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[]): Promise> { 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): Promise { this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID())[0] || null; } } export class ManyModelRelation extends ModelRelation { protected readonly dbProperties: RelationDatabaseProperties; constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { super(model, foreignFactory); this.dbProperties = dbProperties; } public clone(): ManyModelRelation { return new ManyModelRelation(this.model, this.foreignFactory, this.dbProperties); } public cloneReduceToOne(): OneModelRelation { return new OneModelRelation(this.model, this.foreignFactory, this.dbProperties); } public getModelID(): any { return this.model[this.dbProperties.localKey]; } protected async compute(query: ModelQuery): Promise { this.getFinalQuery().where(this.dbProperties.foreignKey, this.getModelID()); return await query.get(); } public async get(): Promise { 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[]): Promise> { 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): Promise { this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID()); } } export class ManyThroughModelRelation extends ModelRelation { protected readonly dbProperties: PivotRelationDatabaseProperties; constructor(model: S, foreignFactory: ModelFactory, 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 { return new ManyThroughModelRelation(this.model, this.foreignFactory, this.dbProperties); } public getModelID(): any { return this.model[this.dbProperties.localKey]; } protected async compute(query: ModelQuery): Promise { this.getFinalQuery().where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID()); return await query.get(); } public async get(): Promise { 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[]): Promise> { 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): Promise { const ids = models.pivot! .filter(p => p[`pivot.${this.dbProperties.localPivotKey}`] === this.getModelID()) .map(p => p[`pivot.${this.dbProperties.foreignPivotKey}`]); this.cachedModels = models.filter(m => ids.indexOf(m[this.dbProperties.foreignKey]) >= 0); } } export type QueryModifier = (query: ModelQuery) => ModelQuery; export type ModelFilter = (model: O) => boolean | Promise; export type RelationDatabaseProperties = { localKey: string; foreignKey: string; }; export type PivotRelationDatabaseProperties = RelationDatabaseProperties & { pivotTable: string; localPivotKey: string; foreignPivotKey: string; };