// deno-lint-ignore-file no-explicit-any
import { MemCache } from "@utils/memcache";
import { Postgres } from "@db/pg";
import { EntityQuery, type QueryCache } from "../modules/db/entity_query.ts";
import { EventEmitter } from "../modules/db/event_emitter.ts";
export interface Entity extends DB.Entity {}
export class Entity extends EventEmitter {
constructor(params: DB.Entity) {
super();
this.id = params.id;
this.created_at = params.created_at;
const t = this as any;
for(const [key, value] of Object.entries(params)) {
if(Object.hasOwn(params, key)) {
if(this.EntityClass.is_json(key) && typeof value === 'string') {
t[key] = JSON.parse(value);
} else {
t[key] = value;
}
}
}
this.__parent_class = this.EntityClass.name;
delete t.deleted_at;
}
get EntityClass() {
return this.constructor as typeof Entity;
}
async save() {
this.emit('save');
if(!this.id) {
const [ id ] = await this.EntityClass.insert(this.__clean());
this.id = id;
this.created_at = new Date();
return this;
}
await this.EntityClass.update({ id: this.id! }, this.__clean());
this.emit('saved');
return this;
}
__clean(keep_id?: boolean) {
const t = {} as Record<string, unknown>;
for(const key of Object.keys(this)) {
if(key.startsWith('__')) continue;
if(key == 'id' && !keep_id) continue;
if(['created_at', 'deleted_at'].includes(key)) {
continue;
}
t[key] = (this as Record<string, unknown>)[key];
if(this.EntityClass.is_json(key)) {
t[key] = JSON.stringify(t[key]);
}
}
return t;
}
// region Cache
private static __QueryCache: Record<string, MemCache<QueryCache<unknown>>> = {};
private static __InstanceCache: Record<string, MemCache<Entity>> = {};
private static __ArrayCache: Record<string, MemCache<Entity[]>> = {};
static QueryCache<T extends Entity>(this: DB.Entity.Constructor<T>) {
return (this.__QueryCache[this.name] as MemCache<QueryCache<T>>) ||= new MemCache<QueryCache<T>>();
}
static InstanceCache<T extends Entity>(this: DB.Entity.Constructor<T>) {
return (this.__InstanceCache[this.name] as MemCache<T>) ||= new MemCache<T>();
}
static ArrayCache<T extends Entity>(this: DB.Entity.Constructor<T>) {
return (this.__ArrayCache[this.name] as MemCache<T[]>) ||= new MemCache<T[]>();
}
// region Static Events
static get table() {
return this.name.toLowerCase();
}
protected get table() {
return this.EntityClass.table;
}
static find<T extends Entity>(this: DB.Entity.Constructor<T>, filter: DB.Filterable<T> = {}): EntityQuery<T> {
return new EntityQuery(this as unknown as typeof Entity, (this as unknown as typeof Entity).table, filter);
}
static async findOne<T extends Entity>(this: DB.Entity.Constructor<T>, filter: DB.Filterable<T> = {}): Promise<T | null> {
return (await this.find(filter))[0];
}
static async count<T extends Entity>(this: DB.Entity.Constructor<T>, filter: DB.Filterable<T> = {}) {
return await this.find(filter).count();
}
static async exists<T extends Entity>(this: DB.Entity.Constructor<T>, filter: DB.Filterable<T> = {}) {
return await this.find(filter).exists();
}
static async delete<T extends Entity>(this: DB.Entity.Constructor<T>, filter: DB.Filterable<T> = {}, permanent?: boolean) {
const changed_row_count = await this.find(filter).delete(permanent);
this.emit('delete', { modified: changed_row_count });
this.emit('rows_changed', { modified: changed_row_count });
return changed_row_count;
}
static async restore<T extends Entity>(this: DB.Entity.Constructor<T>, filter: DB.Filterable<T> = {}) {
const changed_row_count = await this.find(filter).restore();
this.emit('restore', { modified: changed_row_count });
this.emit('rows_changed', { modified: changed_row_count });
return changed_row_count;
}
static async update<T extends Entity>(this: DB.Entity.Constructor<T>, filter: DB.Filterable<T>, values: Partial<DB.Entity.Clean<T>>) {
const changed_row_count = await this.find(filter).update(values);
this.emit('update', { modified: changed_row_count });
this.emit('rows_changed', { modified: changed_row_count });
return changed_row_count;
}
static async insert<T extends Entity>(this: DB.Entity.Constructor<T>, input: Omit<Partial<DB.Entity.Clean<T>>, 'id' | 'created_at'> | Omit<Partial<DB.Entity.Clean<T>>, 'id' | 'created_at'>[]): Promise<number[]> {
const items = [input].flat() as Partial<DB.Entity.Clean<T>>[];
if(items.length == 0) return [];
const keys = Object.entries(items[0]).filter(([k, v]) => !['id', 'created_at'].includes(k) && v !== undefined).map(([k]) => k);
const values: unknown[] = [];
const valueRows = items.map((item, rowIndex) => {
const rowValues = keys.map((key, colIndex) => {
const value = (item as any)[key];
if(value === undefined) return undefined;
values.push(value);
return `$${rowIndex * keys.length + colIndex + 1}`;
}).filter(v => v !== undefined);
return `(${rowValues.join(', ')})`;
});
const sql = `INSERT INTO "${this.table}" (${keys.join(', ')}) VALUES ${valueRows.join(', ')} RETURNING id`;
const { rows } = await Postgres.query<{ id: number }>(sql, values);
let i = 0;
for(const row of rows) {
this.emit('insert', { modified: rows.length, instance: { ...items[i++], id: row.id } as any });
}
this.emit('rows_changed', { modified: rows.length });
return rows.map(row => row.id) || [];
}
static Relationship<T extends Entity, O extends typeof Entity>(this: DB.Entity.Constructor<T>, other: O, relationship_config: Omit<DB.Filtering.RelationshipFilter<T, InstanceType<O>>, 'via'>): DB.Filtering.RelationshipFilter<T, InstanceType<O>> {
return {
via: other,
...relationship_config
}
}
// region JSON Utils
private static __json_fields: Record<string, Set<string | number | symbol>> = {}
static mark_json_field<T extends Entity, F extends keyof DB.Entity.Stripped<T>>(this: DB.Entity.Constructor<T>, field: F) {
if(!this.is_json(field)) this.__json_fields[this.name].add(typeof field === 'string' ? field.toLowerCase() : field);
}
static is_json(field_name: string | number | symbol): boolean {
this.__json_fields[this.name] ||= new Set();
return this.__json_fields[this.name].has(typeof field_name === 'string' ? field_name.toLowerCase() : field_name);
}
}