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; protected readonly dbProperties: RelationDatabaseProperties; protected readonly queryModifiers: QueryModifier[] = []; protected readonly filters: ModelFilter[] = []; protected cachedModels?: O[]; protected constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { this.model = model; this.foreignFactory = foreignFactory; this.dbProperties = dbProperties; } 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 makeQuery(): ModelQuery { const query = this.foreignFactory.select(); for (const modifier of this.queryModifiers) modifier(query); return query; } public getModelID(): any { return this.model[this.dbProperties.localKey]; } protected applyRegularConstraints(query: ModelQuery): void { query.where(this.dbProperties.foreignKey, this.getModelID()); } public async get(): Promise { if (this.cachedModels === undefined) { const query = this.makeQuery(); this.applyRegularConstraints(query); this.cachedModels = await query.get(); } let models = this.cachedModels; for (const filter of this.filters) { const newModels = []; for (const model of models) { if (await filter(model)) { newModels.push(model); } } models = newModels; } return this.collectionToOutput(models); } public getOrFail(): R { if (this.cachedModels === undefined) throw new Error('Models were not fetched'); return this.collectionToOutput(this.cachedModels); } protected abstract collectionToOutput(models: O[]): R; 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.makeQuery(); 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()); } public async count(): Promise { const models = await this.get(); if (Array.isArray(models)) return models.length; else return models !== null ? 1 : 0; } public async has(model: O): Promise { const models = await this.get(); if (Array.isArray(models)) return models.find(m => m.equals(model)) !== undefined; else return models && models.equals(model); } } export class OneModelRelation extends ModelRelation { public constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { super(model, foreignFactory, dbProperties); } public clone(): OneModelRelation { return new OneModelRelation(this.model, this.foreignFactory, this.dbProperties); } protected collectionToOutput(models: O[]): O | null { return models[0] || null; } public async set(model: O): Promise { (this.model as Model)[this.dbProperties.localKey] = model[this.dbProperties.foreignKey]; } public async clear(): Promise { (this.model as Model)[this.dbProperties.localKey] = undefined; } } export class ManyModelRelation extends ModelRelation { protected readonly paginatedCache: { [perPage: number]: { [pageNumber: number]: ModelQueryResult } } = {}; constructor(model: S, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { super(model, foreignFactory, 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); } protected collectionToOutput(models: O[]): O[] { return models; } public async paginate(page: number, perPage: number): Promise> { if (!this.paginatedCache[perPage]) this.paginatedCache[perPage] = {}; const cache = this.paginatedCache[perPage]; if (!cache[page]) { const query = this.makeQuery(); this.applyRegularConstraints(query); cache[page] = await query.paginate(page, perPage); } return cache[page]; } } export class ManyThroughModelRelation extends ManyModelRelation { protected readonly dbProperties: PivotRelationDatabaseProperties; constructor(model: S, foreignFactory: ModelFactory, dbProperties: PivotRelationDatabaseProperties) { super(model, foreignFactory, dbProperties); 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 cloneReduceToOne(): OneModelRelation { throw new Error('Cannot reduce many through relation to one model.'); } protected applyRegularConstraints(query: ModelQuery): void { query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID()); } public async eagerLoad(relations: ModelRelation[]): Promise> { const ids = relations.map(r => r.getModelID()); if (ids.length === 0) return []; const query = this.makeQuery(); 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 class RecursiveModelRelation extends ManyModelRelation { public constructor(model: M, foreignFactory: ModelFactory, dbProperties: RelationDatabaseProperties) { super(model, foreignFactory, dbProperties); this.constraint(query => query.recursive(this.dbProperties)); } public clone(): RecursiveModelRelation { return new RecursiveModelRelation(this.model, this.foreignFactory, this.dbProperties); } public async populate(models: ModelQueryResult): Promise { await super.populate(models); if (this.cachedModels) { let count; do { count = this.cachedModels.length; this.cachedModels.push(...models.filter(model => !this.cachedModels!.find(cached => cached.equals(model)) && this.cachedModels!.find(cached => cached[this.dbProperties.localKey] === model[this.dbProperties.foreignKey]) )); } while (count !== this.cachedModels.length); } } } 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; };