import State from "@infinity-beyond/modules/state.ts";
import { FeatureFlagRest } from "@infinity-beyond/modules/features/feature_flags.rest.ts";
export class FeatureFlag {
private constructor() {}
private static cache: Record<string, I_FeatureFlag> = {}
static readonly REST = new FeatureFlagRest();
static has(key: string) {
return this.cache[key] !== undefined;
}
static get ADMIN_API() {
return FeatureFlag.get('ADMIN_API', 'Enables `/admin` API routes for all data types');
}
/**
* Gets (and creates, if necessary) a feature flag value.
*
* @param key The name of the feature flag
* @param description Optional. A description for what the feature flag does. Userful for the admin flag management screen.
* @returns boolean
*/
static get(key: string, description: string | null = null) {
if(this.cache[key] !== undefined) {
const entry = this.cache[key];
if(description && entry.description !== description) {
entry.description = description;
State.PostgresClient.query(`INSERT INTO ${this.flag_table} (key, value, description) VALUES ($1, $2, $3) ON CONFLICT(key) DO UPDATE set description=$3`, [entry.key, entry.value, description]);
}
return entry.value;
};
this.cache[key] = {
key,
value: false,
description
};
State.PostgresClient.query<I_FeatureFlag>(`INSERT INTO ${this.flag_table} (key, value, description) VALUES ($1, $2, $3) ON CONFLICT(key) DO NOTHING`, [key, false, description]);
this.trigger_update();
return false;
}
static entries() {
return Object.values({...this.cache});
}
protected static set(key: string, value: boolean, request: Request) {
if(!request.state?.user?.has_permission('SET_FEATURE_FLAGS')) throw new Error('[SYSTEM] access denied');
if(!this.has(key)) return null;
if(this.cache[key].value !== value) {
this.cache[key].value = value;
this.persist(key, value, request);
}
return value;
}
private static async persist(key: string, value: boolean, request: Request) {
if(!this.has(key)) return null;
const flag = this.cache[key];
if(!flag) return;
if(!flag.id) {
const { rows: [{ id: id }] } = await State.PostgresClient.query<{ id: number }>(`INSERT INTO ${this.flag_table} (key, value, description) VALUES ($1, $2, $3) ON CONFLICT(key) DO NOTHING RETURNING id`, [flag.key, flag.value, flag.description]);
flag.id = id;
}
await State.PostgresClient.query<I_FeatureFlagChange>(`INSERT INTO ${this.changes_table} (flag_id, new_value, draco_user_id) VALUES ($1, $2, $3)`, [flag.id, value, request.state.user!.id!]);
const { rows: [ row ] } = await State.PostgresClient.query<{ updated_value: boolean }>(`INSERT INTO ${this.flag_table} (key, value, description) VALUES ($1, $2, $3) ON CONFLICT(key) DO UPDATE SET value=$2 returning $2 as updated_value`, [key, value, null]);
this.cache[key].value = row.updated_value;
}
private static get flag_table() {
return `_feature_flags`;
}
private static get changes_table() {
return `_feature_flag_changes`;
}
static async Setup() {
return await (await this.init()).trigger_update();
}
private static async init() {
const flag_table = this.flag_table;
await State.PostgresClient.CreateTable<I_FeatureFlag>(this.flag_table, {
id: "SERIAL PRIMARY KEY",
key: "varchar(24) NOT NULL",
description: "varchar(128)",
value: "boolean NOT NULL default false",
}, {
async onCreate() {
await State.PostgresClient.query(`
CREATE UNIQUE INDEX feature_flags_key_idx ON ${flag_table} (key);
`)
}
});
await State.PostgresClient.CreateTable<I_FeatureFlagChange>(this.changes_table, {
id: "SERIAL PRIMARY KEY",
flag_id: "int4 NOT NULL",
new_value: "boolean NOT NULL",
draco_user_id: "int4 NOT NULL",
timestamp: "timestamptz NOT NULL DEFAULT NOW()",
});
return this;
}
private static async trigger_update() {
const { rows: [...flags]} = await State.PostgresClient.query<I_FeatureFlag>(`SELECT * FROM ${this.flag_table}`);
for(const flag of flags) {
this.cache[flag.key] = flag;
}
return this;
}
}
if(!State.IS_BUILDING) await FeatureFlag.Setup();
export interface I_FeatureFlag {
id?: number
key: string
description: string | null
value: boolean
}
interface I_FeatureFlagChange {
id?: number
flag_id: number
new_value: boolean
draco_user_id: number
timestamp: Date
}