2021-05-03 19:29:22 +02:00
|
|
|
import Model, {ModelType} from "./Model.js";
|
|
|
|
import ModelFactory from "./ModelFactory.js";
|
|
|
|
import ModelQuery, {ModelFieldData, ModelQueryResult, WhereTest} from "./ModelQuery.js";
|
2020-06-27 14:36:50 +02:00
|
|
|
|
|
|
|
export default abstract class ModelRelation<S extends Model, O extends Model, R extends O | O[] | null> {
|
2021-05-31 11:16:46 +02:00
|
|
|
protected readonly _model: S;
|
2020-09-07 14:28:18 +02:00
|
|
|
protected readonly foreignModelType: ModelType<O>;
|
2020-09-05 14:55:19 +02:00
|
|
|
protected readonly dbProperties: RelationDatabaseProperties;
|
2020-08-26 14:41:40 +02:00
|
|
|
protected readonly queryModifiers: QueryModifier<O>[] = [];
|
2020-07-27 10:56:10 +02:00
|
|
|
protected readonly filters: ModelFilter<O>[] = [];
|
2020-09-05 14:55:19 +02:00
|
|
|
protected cachedModels?: O[];
|
2020-06-27 14:36:50 +02:00
|
|
|
|
2020-09-07 14:28:18 +02:00
|
|
|
protected constructor(model: S, foreignModelType: ModelType<O>, dbProperties: RelationDatabaseProperties) {
|
2021-05-31 11:16:46 +02:00
|
|
|
this._model = model;
|
2020-09-07 14:28:18 +02:00
|
|
|
this.foreignModelType = foreignModelType;
|
2020-09-05 14:55:19 +02:00
|
|
|
this.dbProperties = dbProperties;
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public abstract clone(): ModelRelation<S, O, R>;
|
|
|
|
|
|
|
|
public constraint(queryModifier: QueryModifier<O>): this {
|
2020-08-26 14:41:40 +02:00
|
|
|
this.queryModifiers.push(queryModifier);
|
2020-06-27 14:36:50 +02:00
|
|
|
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> {
|
2020-09-07 14:28:18 +02:00
|
|
|
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;
|
2020-08-26 14:41:40 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public getModelId(): ModelFieldData {
|
2021-05-31 11:16:46 +02:00
|
|
|
return this._model[this.dbProperties.localKey];
|
2020-09-05 14:55:19 +02:00
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
|
2020-09-05 14:55:19 +02:00
|
|
|
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
2020-09-25 23:42:15 +02:00
|
|
|
query.where(this.dbProperties.foreignKey, this.getModelId());
|
2020-09-05 14:55:19 +02:00
|
|
|
}
|
2020-09-04 18:01:29 +02:00
|
|
|
|
2020-06-27 14:36:50 +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);
|
2020-09-05 14:55:19 +02:00
|
|
|
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;
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
2020-09-05 14:55:19 +02:00
|
|
|
return this.collectionToOutput(models);
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 17:11:31 +02:00
|
|
|
public getOrFail(): R {
|
2020-07-28 15:02:40 +02:00
|
|
|
if (this.cachedModels === undefined) throw new Error('Models were not fetched');
|
2020-09-05 14:55:19 +02:00
|
|
|
return this.collectionToOutput(this.cachedModels);
|
2020-06-27 17:11:31 +02:00
|
|
|
}
|
|
|
|
|
2020-09-05 14:55:19 +02:00
|
|
|
protected abstract collectionToOutput(models: O[]): R;
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public async eagerLoad(
|
|
|
|
relations: ModelRelation<S, O, R>[],
|
|
|
|
subRelations: string[] = [],
|
|
|
|
): Promise<ModelQueryResult<O>> {
|
|
|
|
const ids = relations.map(r => r.getModelId())
|
2020-09-07 14:02:43 +02:00
|
|
|
.filter(id => id !== null && id !== undefined)
|
2020-09-25 23:42:15 +02:00
|
|
|
.reduce((array: ModelFieldData[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []);
|
2020-09-05 14:55:19 +02:00
|
|
|
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);
|
2020-09-05 14:55:19 +02:00
|
|
|
return await query.get();
|
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
|
2020-09-05 14:55:19 +02:00
|
|
|
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
2020-09-25 23:42:15 +02:00
|
|
|
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelId())
|
2020-09-07 14:02:43 +02:00
|
|
|
.reduce((array: O[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []);
|
2020-09-05 14:55:19 +02:00
|
|
|
}
|
2020-09-02 14:08:35 +02:00
|
|
|
|
|
|
|
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();
|
2020-12-04 14:42:09 +01:00
|
|
|
if (models instanceof Array) {
|
|
|
|
return models.find(m => m.equals(model)) !== undefined;
|
|
|
|
} else {
|
|
|
|
return models !== null && models.equals(model);
|
|
|
|
}
|
2020-09-02 14:08:35 +02:00
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export class OneModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O | null> {
|
2020-09-07 14:28:18 +02:00
|
|
|
public constructor(model: S, foreignModelType: ModelType<O>, dbProperties: RelationDatabaseProperties) {
|
|
|
|
super(model, foreignModelType, dbProperties);
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public clone(): OneModelRelation<S, O> {
|
2021-05-31 11:16:46 +02:00
|
|
|
return new OneModelRelation(this._model, this.foreignModelType, this.dbProperties);
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
2020-09-05 14:55:19 +02:00
|
|
|
protected collectionToOutput(models: O[]): O | null {
|
|
|
|
return models[0] || null;
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
2020-09-02 14:08:49 +02:00
|
|
|
public async set(model: O): Promise<void> {
|
2021-05-31 11:16:46 +02:00
|
|
|
(this._model as Model)[this.dbProperties.localKey] = model[this.dbProperties.foreignKey];
|
2020-09-02 14:08:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async clear(): Promise<void> {
|
2021-05-31 11:16:46 +02:00
|
|
|
(this._model as Model)[this.dbProperties.localKey] = undefined;
|
2020-09-02 14:08:49 +02:00
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export class ManyModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
|
2020-09-25 23:42:15 +02:00
|
|
|
protected readonly paginatedCache: {
|
|
|
|
[perPage: number]: {
|
|
|
|
[pageNumber: number]: ModelQueryResult<O> | undefined
|
|
|
|
} | undefined
|
|
|
|
} = {};
|
2020-06-27 14:36:50 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor(model: S, foreignModelType: ModelType<O>, dbProperties: RelationDatabaseProperties) {
|
2020-09-07 14:28:18 +02:00
|
|
|
super(model, foreignModelType, dbProperties);
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public clone(): ManyModelRelation<S, O> {
|
2021-05-31 11:16:46 +02:00
|
|
|
return new ManyModelRelation<S, O>(this._model, this.foreignModelType, this.dbProperties);
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 14:58:39 +02:00
|
|
|
public cloneReduceToOne(): OneModelRelation<S, O> {
|
2021-05-31 11:16:46 +02:00
|
|
|
return new OneModelRelation<S, O>(this._model, this.foreignModelType, this.dbProperties);
|
2020-06-27 14:58:39 +02:00
|
|
|
}
|
|
|
|
|
2020-09-05 14:55:19 +02:00
|
|
|
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>> {
|
2020-09-25 23:42:15 +02:00
|
|
|
let cache = this.paginatedCache[perPage];
|
|
|
|
if (!cache) cache = this.paginatedCache[perPage] = {};
|
2020-09-04 18:01:29 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
let cachePage = cache[page];
|
|
|
|
if (!cachePage) {
|
2020-09-04 18:01:29 +02:00
|
|
|
const query = this.makeQuery();
|
|
|
|
this.applyRegularConstraints(query);
|
2020-09-25 23:42:15 +02:00
|
|
|
cachePage = cache[page] = await query.paginate(page, perPage);
|
2020-09-04 18:01:29 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
return cachePage;
|
2020-09-04 18:01:29 +02:00
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
2020-09-04 18:01:29 +02:00
|
|
|
export class ManyThroughModelRelation<S extends Model, O extends Model> extends ManyModelRelation<S, O> {
|
2020-06-27 14:36:50 +02:00
|
|
|
protected readonly dbProperties: PivotRelationDatabaseProperties;
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor(model: S, foreignModelType: ModelType<O>, dbProperties: PivotRelationDatabaseProperties) {
|
2020-09-07 14:28:18 +02:00
|
|
|
super(model, foreignModelType, dbProperties);
|
2020-06-27 14:36:50 +02:00
|
|
|
this.dbProperties = dbProperties;
|
2020-08-26 14:41:40 +02:00
|
|
|
this.constraint(query => query
|
2020-09-05 15:33:51 +02:00
|
|
|
.leftJoin(this.dbProperties.pivotTable, 'pivot')
|
2020-09-25 23:42:15 +02:00
|
|
|
.on(`pivot.${this.dbProperties.foreignPivotKey}`, `${this.foreignModelType.table}.${this.dbProperties.foreignKey}`),
|
2020-08-26 14:41:40 +02:00
|
|
|
);
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public clone(): ManyThroughModelRelation<S, O> {
|
2021-05-31 11:16:46 +02:00
|
|
|
return new ManyThroughModelRelation<S, O>(this._model, this.foreignModelType, this.dbProperties);
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
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 {
|
2020-09-25 23:42:15 +02:00
|
|
|
query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelId());
|
2020-07-27 10:56:10 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public async eagerLoad(
|
|
|
|
relations: ModelRelation<S, O, O[]>[],
|
|
|
|
subRelations: string[] = [],
|
|
|
|
): Promise<ModelQueryResult<O>> {
|
|
|
|
const ids = relations.map(r => r.getModelId())
|
|
|
|
.reduce((array: ModelFieldData[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []);
|
2020-07-29 16:17:30 +02:00
|
|
|
if (ids.length === 0) return [];
|
|
|
|
|
2020-09-04 18:01:29 +02:00
|
|
|
const query = this.makeQuery();
|
2020-08-26 14:41:40 +02:00
|
|
|
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);
|
2020-08-26 14:41:40 +02:00
|
|
|
return await query.get();
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
2020-09-25 23:42:15 +02:00
|
|
|
if (!models.pivot) throw new Error('ModelQueryResult.pivot not loaded.');
|
|
|
|
const ids = models.pivot
|
|
|
|
.filter(p => p[`pivot.${this.dbProperties.localPivotKey}`] === this.getModelId())
|
2020-08-30 18:56:27 +02:00
|
|
|
.map(p => p[`pivot.${this.dbProperties.foreignPivotKey}`]);
|
2020-09-07 14:02:43 +02:00
|
|
|
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], []);
|
2020-06-27 14:36:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-05 14:55:19 +02:00
|
|
|
export class RecursiveModelRelation<M extends Model> extends ManyModelRelation<M, M> {
|
2020-09-08 18:12:39 +02:00
|
|
|
private readonly reverse: boolean;
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor(
|
|
|
|
model: M,
|
|
|
|
foreignModelType: ModelType<M>,
|
|
|
|
dbProperties: RelationDatabaseProperties,
|
|
|
|
reverse: boolean = false,
|
|
|
|
) {
|
2020-09-07 14:28:18 +02:00
|
|
|
super(model, foreignModelType, dbProperties);
|
2020-09-08 18:12:39 +02:00
|
|
|
this.constraint(query => query.recursive(this.dbProperties, reverse));
|
|
|
|
this.reverse = reverse;
|
2020-09-05 14:55:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public clone(): RecursiveModelRelation<M> {
|
2021-05-31 11:16:46 +02:00
|
|
|
return new RecursiveModelRelation(this._model, this.foreignModelType, this.dbProperties);
|
2020-09-05 14:55:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async populate(models: ModelQueryResult<M>): Promise<void> {
|
|
|
|
await super.populate(models);
|
2020-09-25 23:42:15 +02:00
|
|
|
const cachedModels = this.cachedModels;
|
|
|
|
if (cachedModels) {
|
2020-09-05 14:55:19 +02:00
|
|
|
let count;
|
|
|
|
do {
|
2020-09-25 23:42:15 +02:00
|
|
|
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];
|
|
|
|
}),
|
2020-09-08 19:24:05 +02:00
|
|
|
).reduce((array: M[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []));
|
2020-09-25 23:42:15 +02:00
|
|
|
} while (count !== cachedModels.length);
|
2020-09-08 18:12:39 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
if (this.reverse) cachedModels.reverse();
|
2020-09-05 14:55:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-07-27 10:52:39 +02:00
|
|
|
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>;
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
export type RelationDatabaseProperties = {
|
|
|
|
localKey: string;
|
|
|
|
foreignKey: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type PivotRelationDatabaseProperties = RelationDatabaseProperties & {
|
|
|
|
pivotTable: string;
|
|
|
|
localPivotKey: string;
|
|
|
|
foreignPivotKey: string;
|
|
|
|
};
|