/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
/* eslint-disable */
import {
DataSource,
FindOneOptions,
EntityNotFoundError,
FindOptionsWhere,
} from "typeorm";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { BaseClassWithId } from "../entities/BaseClass";
import { Config, getDatabase } from "../util";
import { CacheManager } from "./Cache";
function getObjectKeysAsArray(obj?: Record) {
if (!obj) return [];
if (Array.isArray(obj)) return obj;
return Object.keys(obj);
}
export type ThisType = {
new (): T;
} & typeof BaseEntityCache;
interface BaseEntityCache {
constructor: typeof BaseEntityCache;
}
// @ts-ignore
class BaseEntityCache extends BaseClassWithId {
static cache: CacheManager;
static cacheEnabled: boolean;
public get metadata() {
return getDatabase()?.getMetadata(this.constructor)!;
}
static useDataSource(dataSource: DataSource | null) {
super.useDataSource(dataSource);
this.cacheEnabled = Config.get().cache.enabled ?? true;
if (Config.get().cache.redis) return; // TODO: Redis cache
if (!this.cacheEnabled) return;
this.cache = new CacheManager();
}
static async findOne(
this: ThisType,
options: FindOneOptions,
) {
// @ts-ignore
if (!this.cacheEnabled) return super.findOne(options);
let select = getObjectKeysAsArray(options.select);
if (!select.length) {
// get all columns that are marked as select
getDatabase()
?.getMetadata(this)
.columns.forEach((x) => {
if (!x.isSelect) return;
select.push(x.propertyName);
});
}
if (options.relations) {
select.push(...getObjectKeysAsArray(options.relations));
}
const cacheResult = this.cache.find(options.where as never, select);
if (cacheResult) {
const hasAllProps = select.every((key) => {
if (key.includes(".")) return true; // @ts-ignore
return cacheResult[key] !== undefined;
});
// console.log(`[Cache] get ${cacheResult.id} from ${cacheResult.constructor.name}`,);
if (hasAllProps) return cacheResult;
}
// @ts-ignore
const result = await super.findOne(options);
if (!result) return null;
this.cache.insert(result as any);
return result;
}
static async findOneOrFail(
this: ThisType,
options: FindOneOptions,
) {
const result = await this.findOne(options);
if (!result) throw new EntityNotFoundError(this, options);
return result;
}
save() {
if (this.constructor.cacheEnabled) this.constructor.cache.insert(this);
return super.save();
}
remove() {
if (this.constructor.cacheEnabled)
this.constructor.cache.delete(this.id);
return super.remove();
}
static async update(
this: ThisType,
criteria: FindOptionsWhere,
partialEntity: QueryDeepPartialEntity,
) {
// @ts-ignore
const result = super.update(criteria, partialEntity);
if (!this.cacheEnabled) return result;
const entities = this.cache.filter(criteria as never);
for (const entity of entities) {
// @ts-ignore
partialEntity.id = entity.id;
this.cache.insert(partialEntity as never);
}
return result;
}
static async delete(
this: ThisType,
criteria: FindOptionsWhere,
) {
// @ts-ignore
const result = super.delete(criteria);
if (!this.cacheEnabled) return result;
const entities = this.cache.filter(criteria as never);
for (const entity of entities) {
this.cache.delete(entity.id);
}
return result;
}
}
// needed, because typescript can't infer the type of the static methods with generics
const EntityCache = BaseEntityCache as unknown as typeof BaseClassWithId;
export { EntityCache };