import Model, {ModelType} from "./Model.js"; import ModelFactory from "./ModelFactory.js"; import ModelQuery, {ModelFieldData, ModelQueryResult, WhereTest} from "./ModelQuery.js"; 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(): ModelFieldData { 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: ModelFieldData[], 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 (models instanceof Array) { return models.find(m => m.equals(model)) !== undefined; } else { return models !== null && 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 | undefined } | undefined } = {}; public 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> { let cache = this.paginatedCache[perPage]; if (!cache) cache = this.paginatedCache[perPage] = {}; let cachePage = cache[page]; if (!cachePage) { const query = this.makeQuery(); this.applyRegularConstraints(query); cachePage = cache[page] = await query.paginate(page, perPage); } return cachePage; } } export class ManyThroughModelRelation extends ManyModelRelation { protected readonly dbProperties: PivotRelationDatabaseProperties; public 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: ModelFieldData[], 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 { if (!models.pivot) throw new Error('ModelQueryResult.pivot not loaded.'); 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); const cachedModels = this.cachedModels; if (cachedModels) { let count; do { count = cachedModels.length; cachedModels.push(...models.filter(model => !cachedModels.find(cached => cached.equals(model)) && cachedModels.find(cached => { return cached[this.dbProperties.localKey] === model[this.dbProperties.foreignKey]; }), ).reduce((array: M[], val) => array.find(v => v.equals(val)) ? array : [...array, val], [])); } while (count !== cachedModels.length); if (this.reverse) 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; };