0.0.8Updated a month ago
import { Postgres } from "@db/pg";
import { logger } from "@utils/logger";

const log = logger({
  name: "FeatureFlags",
  glyph: "",
  name_color: "green",
  glyph_color: "white"
})

export class FeatureFlags<T extends Record<string, string>> {
  private constructor(private readonly __shape: T, private readonly pg_connected: boolean, saved_values: Record<string, boolean>) {
    for(const key of Object.keys(__shape)) {
      this.__set(key, saved_values[key] ?? false, true);
    }
  }
  private __values: Record<string, boolean> = {}

  private __get(prop: string) {
    return this.__values[prop];
  }

  private __set(prop: string, value: boolean, skip_pg?: boolean) {
    if(this.pg_connected && !skip_pg && this.__shape[prop]) {
      Postgres.query(`
        INSERT INTO "flags"."feature_flags"
          ("name", "description", "value")
        VALUES
          ($1, $2, $3)
        ON CONFLICT ("name") DO
          UPDATE SET value=$3
      `, [prop, this.__shape[prop], value]);
    }
    return this.__values[prop] = value;
  }

  get All(): Flag[] {
    return Object.keys(this.__shape).map(name => ({ name, description: this.__shape[name], value: this.__get(name) }));
  }

  get Mapped(): Record<keyof T & string, boolean> {
    return Object.assign({}, ...Object.keys(this.__shape).map(name => ({ [name]: this.__get(name) })));
  }

  Has(key: string): key is keyof T & string {
    return !!this.__shape[key];
  }

  Get(key: keyof T & string) {
    return this.__get(key);
  }

  Set<B extends boolean>(key: keyof T & string, value: B): B {
    return this.__set(key, value) as B;
  }

  private static Wrap<T extends Record<string, string>>(instance: FeatureFlags<T>) {
    return new Proxy(instance, {
      get(target, prop, receiver) {
        if (typeof prop === "string" && prop in target.__shape) {
          return target.__get(prop);
        }
        return Reflect.get(target, prop, receiver);
      },
      has(target, prop) {
        return typeof prop === "string" && prop in target.__shape
          ? true
          : Reflect.has(target, prop);
      },
      getOwnPropertyDescriptor(target, prop) {
        if (typeof prop === "string" && prop in target.__shape) {
          return {
            configurable: true,
            enumerable: true,
            writable: true,
            value: target.__get(prop),
          };
        }
        return Reflect.getOwnPropertyDescriptor(target, prop);
      },
      set(target, prop: string, value: boolean) {
        // SET

        target.__set(prop, value);

        return true;
      },
    }) as unknown as FeatureFlagWrapper<T>;
  }

  static async Init<T extends Record<string, string>>(shape: T) {
    const pg_connected = Postgres.IsConfigured;

    const saved_values: Record<string, boolean> = {}

    if(pg_connected) {
      log("PostgreSQL configured. Restoring from database...");

      await Postgres.query(`
        create schema if not exists "flags";
  
        create table if not exists
          "flags"."feature_flags" (
            "id" serial not null,
            "created_at" timestamptz not null default NOW(),
            "name" text not null UNIQUE,
            "description" text not null,
            "value" bool not null default false,
            constraint "__featureflags_pkey" primary key ("id")
          );
      `);

      const { rows } = await Postgres.query<FlagRow>(`SELECT * FROM "flags"."feature_flags"`);

      let found_entries = 0;
      const new_entries: Record<string, true> = Object.assign({}, ...Object.keys(shape).map(k => ({[k]: true})));
      const entries_to_delete: string[] = [];
      for(const { name, value } of rows) {
        if(shape[name]) {
          delete new_entries[name];
          saved_values[name] = value;
          found_entries++;
        } else {
          entries_to_delete.push(name);
        }
      }

      const entries_to_create = Object.keys(new_entries).map(k => ({name: k, description: shape[k]}));
      if(entries_to_create.length > 0) {
        const placeholders = Array(entries_to_create.length).fill('').map((_, i) => `($${i*2+1},$${i*2+2})`).join(', ');
  
        await Postgres.query(`
          INSERT INTO "flags"."feature_flags"
            ("name", "description")
          VALUES
            ${placeholders}
          ON CONFLICT ("name") DO NOTHING
        `, entries_to_create.map(e => [e.name, e.description]).flat());

        found_entries += entries_to_create.length;

        log(`%c${entries_to_create.length} new flags added`, 'color: green');
      }

      if(entries_to_delete.length > 0) {
        await Postgres.query(`DELETE FROM "flags"."feature_flags" WHERE "name" = ANY($1)`, [entries_to_delete]);
        log(`%c${entries_to_delete.length} stale flags deleted`, 'color: orange');
      }

      log(`Flags loaded: ${found_entries}`);
    } else {
      log("%cPostgreSQL not configured!", "color: red");
      log("Running in memory-only mode. All flags defaulted to %cfalse%c.", "color: orange", "");
    }

    return FeatureFlags.Wrap(new FeatureFlags(shape, pg_connected, saved_values));
  }
}

export type FeatureFlagWrapper<T extends Record<string, string>> = {
  All: Flag[]
  Mapped: Record<keyof T & string, boolean>
  Has(key: string): key is keyof T & string
  Get(key: keyof T & string): boolean
  Set<B extends boolean>(key: keyof B & string, value: B): B
} & {
  [K in keyof T & string]: boolean;
}

export interface Flag {
  name: string
  description: string
  value: boolean
}

interface FlagRow {
  id: number
  created_at: Date
  name: string
  description: string
  value: boolean
}