swaf/src/db/ModelRelation.ts

248 lines
9.5 KiB
TypeScript
Raw Normal View History

import ModelQuery, {ModelQueryResult, WhereTest} from "./ModelQuery";
import Model, {ModelType} from "./Model";
2020-07-24 12:13:28 +02:00
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 foreignModelType: ModelType<O>;
protected readonly dbProperties: RelationDatabaseProperties;
protected readonly queryModifiers: QueryModifier<O>[] = [];
2020-07-27 10:56:10 +02:00
protected readonly filters: ModelFilter<O>[] = [];
protected cachedModels?: O[];
protected constructor(model: S, foreignModelType: ModelType<O>, dbProperties: RelationDatabaseProperties) {
this.model = model;
this.foreignModelType = foreignModelType;
this.dbProperties = dbProperties;
}
public abstract clone(): ModelRelation<S, O, R>;
public constraint(queryModifier: QueryModifier<O>): this {
this.queryModifiers.push(queryModifier);
return this;
}
2020-07-27 10:56:10 +02:00
public filter(modelFilter: ModelFilter<O>): this {
this.filters.push(modelFilter);
return this;
}
2020-09-04 18:01:29 +02:00
protected makeQuery(): ModelQuery<O> {
const query = ModelFactory.get(this.foreignModelType).select();
2020-09-04 18:01:29 +02:00
for (const modifier of this.queryModifiers) modifier(query);
return query;
}
public getModelID(): any {
return this.model[this.dbProperties.localKey];
}
protected applyRegularConstraints(query: ModelQuery<O>): void {
query.where(this.dbProperties.foreignKey, this.getModelID());
}
2020-09-04 18:01:29 +02:00
public async get(): Promise<R> {
if (this.cachedModels === undefined) {
2020-09-04 18:01:29 +02:00
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);
}
2020-06-27 17:11:31 +02:00
public getOrFail(): R {
if (this.cachedModels === undefined) throw new Error('Models were not fetched');
return this.collectionToOutput(this.cachedModels);
2020-06-27 17:11:31 +02:00
}
protected abstract collectionToOutput(models: O[]): R;
2020-09-07 13:43:02 +02:00
public async eagerLoad(relations: ModelRelation<S, O, R>[], subRelations: string[] = []): Promise<ModelQueryResult<O>> {
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);
2020-09-07 13:43:02 +02:00
query.with(...subRelations);
return await query.get();
}
public async populate(models: ModelQueryResult<O>): Promise<void> {
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<number> {
const models = await this.get();
if (Array.isArray(models)) return models.length;
else return models !== null ? 1 : 0;
}
public async has(model: O): Promise<boolean> {
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<S extends Model, O extends Model> extends ModelRelation<S, O, O | null> {
public constructor(model: S, foreignModelType: ModelType<O>, dbProperties: RelationDatabaseProperties) {
super(model, foreignModelType, dbProperties);
}
public clone(): OneModelRelation<S, O> {
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<void> {
(this.model as Model)[this.dbProperties.localKey] = model[this.dbProperties.foreignKey];
}
public async clear(): Promise<void> {
(this.model as Model)[this.dbProperties.localKey] = undefined;
}
}
export class ManyModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
2020-09-04 18:01:29 +02:00
protected readonly paginatedCache: { [perPage: number]: { [pageNumber: number]: ModelQueryResult<O> } } = {};
constructor(model: S, foreignModelType: ModelType<O>, dbProperties: RelationDatabaseProperties) {
super(model, foreignModelType, dbProperties);
}
public clone(): ManyModelRelation<S, O> {
return new ManyModelRelation<S, O>(this.model, this.foreignModelType, this.dbProperties);
}
public cloneReduceToOne(): OneModelRelation<S, O> {
return new OneModelRelation<S, O>(this.model, this.foreignModelType, this.dbProperties);
}
protected collectionToOutput(models: O[]): O[] {
2020-07-27 10:56:10 +02:00
return models;
}
2020-09-04 18:01:29 +02:00
public async paginate(page: number, perPage: number): Promise<ModelQueryResult<O>> {
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];
}
}
2020-09-04 18:01:29 +02:00
export class ManyThroughModelRelation<S extends Model, O extends Model> extends ManyModelRelation<S, O> {
protected readonly dbProperties: PivotRelationDatabaseProperties;
constructor(model: S, foreignModelType: ModelType<O>, dbProperties: PivotRelationDatabaseProperties) {
super(model, foreignModelType, dbProperties);
this.dbProperties = dbProperties;
this.constraint(query => query
2020-09-05 15:33:51 +02:00
.leftJoin(this.dbProperties.pivotTable, 'pivot')
.on(`pivot.${this.dbProperties.foreignPivotKey}`, `${this.foreignModelType.table}.${this.dbProperties.foreignKey}`)
);
}
public clone(): ManyThroughModelRelation<S, O> {
return new ManyThroughModelRelation<S, O>(this.model, this.foreignModelType, this.dbProperties);
}
2020-09-04 18:01:29 +02:00
public cloneReduceToOne(): OneModelRelation<S, O> {
throw new Error('Cannot reduce many through relation to one model.');
}
protected applyRegularConstraints(query: ModelQuery<O>): void {
query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID());
2020-07-27 10:56:10 +02:00
}
2020-09-07 13:43:02 +02:00
public async eagerLoad(relations: ModelRelation<S, O, O[]>[], subRelations: string[] = []): Promise<ModelQueryResult<O>> {
const ids = relations.map(r => r.getModelID())
.reduce((array: O[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []);
if (ids.length === 0) return [];
2020-09-04 18:01:29 +02:00
const query = this.makeQuery();
query.where(`pivot.${this.dbProperties.localPivotKey}`, ids, WhereTest.IN);
query.pivot(`pivot.${this.dbProperties.localPivotKey}`, `pivot.${this.dbProperties.foreignPivotKey}`);
2020-09-07 13:43:02 +02:00
query.with(...subRelations);
return await query.get();
}
public async populate(models: ModelQueryResult<O>): Promise<void> {
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<M extends Model> extends ManyModelRelation<M, M> {
private readonly reverse: boolean;
public constructor(model: M, foreignModelType: ModelType<M>, dbProperties: RelationDatabaseProperties, reverse: boolean = false) {
super(model, foreignModelType, dbProperties);
this.constraint(query => query.recursive(this.dbProperties, reverse));
this.reverse = reverse;
}
public clone(): RecursiveModelRelation<M> {
return new RecursiveModelRelation(this.model, this.foreignModelType, this.dbProperties);
}
public async populate(models: ModelQueryResult<M>): Promise<void> {
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<M extends Model> = (query: ModelQuery<M>) => ModelQuery<M>;
2020-07-27 10:56:10 +02:00
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;
};