0.1.6Updated a month ago
import State from "@infinity-beyond/modules/state.ts";
import dayjs from "npm:dayjs";

export class EntityMeta<
  EMC extends Entity.Meta.Config, 
  // deno-lint-ignore ban-types
  PEMC extends Entity.Meta.Config = {},
  ValidField = Extract<keyof EMC | keyof PEMC, string>
> {
  private table: string;
  private schema: EMC & PEMC;

  constructor(table: string, default_schema: EMC, passed_schema?: PEMC) {
    this.table = table;
    this.schema = { ...passed_schema, ...default_schema } as EMC & PEMC;
  }

  async snapshots<
    Field extends ValidField,
    Value = Entity.Meta.InferFrom<EMC, PEMC, ValidField, Field>
  >({ field, range, start, end }: { field: Field, range: 'minute' | 'hour' | 'day', start: Date, end: Date }) {
    const type = `snapshot_${range[0]}`;

    const { rows } = (await State.PostgresClient.query(
      `SELECT timestamp, ${field} FROM ${this.table} WHERE (timestamp IS NULL OR timestamp between $1 and $2) AND (type = $3 OR type = 'live')`,
      [ start, end, type ]
    ));

    return (rows.map(r => {
      const timestamp = r.timestamp || dayjs().startOf(range).add(1, range).toDate();
      let label;
      switch(range) {
        case "minute":
        case "hour": {
          label = dayjs(timestamp).format('MMM D hh:mm')
          break;
        }
        case "day": label = dayjs(timestamp).format('MMM D');
      }

      // TODO: Different labels based on range. ddd:mm hh:mm if minute or hour, 

      return {
        timestamp,
        value: r[field],
        label,
      }
    }) as Chart.DataPoint[]).filter(r => r.timestamp >= start && r.timestamp <= end).sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf());
  }

  async get<Field extends ValidField, Value = Entity.Meta.InferFrom<EMC, PEMC, ValidField, Field>, Out = Field extends keyof EMC ? Value : Value | null>(field: Field): Promise<Out> {

    const { rows: [ row ] } = (await State.PostgresClient.query(
      `SELECT ${field} FROM ${this.table} WHERE type = 'live'`
    ));

    if(!row) return null as Out;

    return row[field] as Out;
  }

  async set<Field extends ValidField>(
    field: Field, 
    value: Entity.Meta.InferFrom<EMC, PEMC, ValidField, Field>
  ): Promise<Entity.Meta.InferFrom<EMC, PEMC, ValidField, Field>> {
    await State.PostgresClient.query(`UPDATE ${this.table} SET value=$2 WHERE key=$1`, [field, (value as Entity.Meta.Types | null)]);

    return value;
  }

  async increment<
    Field extends ExtractNumeric<EMC, PEMC, ValidField>
  >(field: Field, amount: number): Promise<number | null> {
    if (!['int', 'real'].includes(this.schema[field as keyof (EMC & PEMC)])) {
      throw new Error(`Field ${String(field)} is not a number type`);
    }

    const cast = this.schema[field];
    const { rows: [{ value } = {}] } = await State.PostgresClient.query<{ value: string }>(`UPDATE ${this.table} SET value = (value::${cast} + ${amount})::varchar WHERE key='${String(field)}' returning value`);

    if(!value) return null;

    if(cast == 'real') return parseFloat(value);
    return parseInt(value);
  }
}

type ExtractNumeric<EMC, PEMC, ValidField> =
  Extract<ValidField, {[F in keyof EMC]: EMC[F] extends 'int' ? F : EMC[F] extends 'real' ? F : never}[keyof EMC]> |
  Extract<ValidField, {[F in keyof PEMC]: PEMC[F] extends 'int' ? F : PEMC[F] extends 'real' ? F : never}[keyof PEMC]>;