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.assign({}, ...Object.keys(this.__shape).map(name => ({ name, description: this.__shape[name], value: this.__get(name) })));
}
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[]
} & {
[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
}