import ModelQuery, {ModelQueryResult, WhereTest} from "./ModelQuery"; import Model, {ModelType} from "./Model"; import ModelFactory from "./ModelFactory"; export default abstract class ModelRelation { protected readonly model: S; protected readonly foreignModelType: ModelType; protected readonly dbProperties: RelationDatabaseProperties; protected readonly queryModifiers: QueryModifier[] = []; protected readonly filters: ModelFilter[] = []; protected cachedModels?: O[]; protected constructor(model: S, foreignModelType: ModelType, dbProperties: RelationDatabaseProperties) { this.model = model; this.foreignModelType = foreignModelType; 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 = ModelFactory.get(this.foreignModelType).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[], subRelations: string[] = []): Promise> { const ids = relations.map(r => r.getModelID()) .filter(id => id !== null && id !== undefined) .reduce((array: O[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []); if (ids.length === 0) return []; const query = this.makeQuery(); query.where(this.dbProperties.foreignKey, ids, WhereTest.IN); query.with(...subRelations); return await query.get(); } public async populate(models: ModelQueryResult): Promise { this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID()) .reduce((array: O[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []); } 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, foreignModelType: ModelType, dbProperties: RelationDatabaseProperties) { super(model, foreignModelType, dbProperties); } public clone(): OneModelRelation { return new OneModelRelation(this.model, this.foreignModelType, 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, foreignModelType: ModelType, dbProperties: RelationDatabaseProperties) { super(model, foreignModelType, dbProperties); } public clone(): ManyModelRelation { return new ManyModelRelation(this.model, this.foreignModelType, this.dbProperties); } public cloneReduceToOne(): OneModelRelation { return new OneModelRelation(this.model, this.foreignModelType, 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, foreignModelType: ModelType, dbProperties: PivotRelationDatabaseProperties) { super(model, foreignModelType, dbProperties); this.dbProperties = dbProperties; this.constraint(query => query .leftJoin(this.dbProperties.pivotTable, 'pivot') .on(`pivot.${this.dbProperties.foreignPivotKey}`, `${this.foreignModelType.table}.${this.dbProperties.foreignKey}`) ); } public clone(): ManyThroughModelRelation { return new ManyThroughModelRelation(this.model, this.foreignModelType, 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[], subRelations: string[] = []): Promise> { const ids = relations.map(r => r.getModelID()) .reduce((array: O[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []); 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}`); query.with(...subRelations); 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) .reduce((array: O[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []); } } export class RecursiveModelRelation extends ManyModelRelation { private readonly reverse: boolean; public constructor(model: M, foreignModelType: ModelType, dbProperties: RelationDatabaseProperties, reverse: boolean = false) { super(model, foreignModelType, dbProperties); this.constraint(query => query.recursive(this.dbProperties, reverse)); this.reverse = reverse; } public clone(): RecursiveModelRelation { return new RecursiveModelRelation(this.model, this.foreignModelType, 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]) ).reduce((array: M[], val) => array.find(v => v.equals(val)) ? array : [...array, val], [])); } while (count !== this.cachedModels.length); if (this.reverse) this.cachedModels!.reverse(); } } } 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; };