actaeon/src/config.ts
2024-04-01 07:25:26 -04:00

145 lines
4.2 KiB
TypeScript

import { revalidatePath } from 'next/cache';
import { db } from './db';
export type GlobalConfig = {
chuni_allow_equip_unearned: number,
allow_user_add_card: boolean,
user_max_card: number | null
};
type ConfigEntry<T extends keyof GlobalConfig> = {
defaultValue: GlobalConfig[T],
validate: (val: any) => ({ error: true, message: string; } | { error?: false, value?: GlobalConfig[T] } | undefined | void)
};
const CONFIG_ENTRIES: { [K in keyof GlobalConfig]: ConfigEntry<K> } = {
chuni_allow_equip_unearned: {
validate: val => {
if (!Number.isInteger(val))
return { error: true, message: 'Invalid permission mask' };
},
defaultValue: 0
},
allow_user_add_card: {
validate: val => {
if (![0, 1, true, false].includes(val))
return { error: true, message: 'Invalid boolean value' };
return { value: !!val };
},
defaultValue: false
},
user_max_card: {
validate: val => {
if (val === null)
return;
if (!Number.isInteger(val) || val < 1)
return { error: true, message: 'Invalid max card count' };
},
defaultValue: 4
}
} as const;
let CONFIG = {} as GlobalConfig;
if ((globalThis as any).CONFIG) CONFIG = (globalThis as any).CONFIG;
type GetConfig = {
<T extends keyof GlobalConfig>(key: T): GlobalConfig[T],
(): GlobalConfig
};
export const getGlobalConfig: GetConfig = <T extends keyof GlobalConfig>(key?: T) => key ? CONFIG[key] : CONFIG;
export const setGlobalConfig = async (update: Partial<GlobalConfig>) => {
for (const [key, value] of Object.entries(update)) {
if (!Object.hasOwn(CONFIG, key))
return { error: true, message: `Unknown key ${key}` };
const res = CONFIG_ENTRIES[key as keyof typeof CONFIG].validate(value);
if (res?.error)
return res;
const val = res?.value ?? value;
if (val === (CONFIG as any)[key])
delete update[key as keyof typeof update];
else
(CONFIG as any)[key] = res?.value ?? value;
}
if ('chuni_allow_equip_unearned' in update)
revalidatePath('/chuni/userbox', 'page');
await db.transaction().execute(async trx => {
for (const [key, value] of Object.entries(update)) {
await trx.updateTable('actaeon_global_config')
.where('key', '=', key)
.set({ value: JSON.stringify((CONFIG as any)[key]) })
.executeTakeFirst();
}
});
};
export const loadConfig = async () => {
const entries = await db.selectFrom('actaeon_global_config')
.selectAll()
.execute();
const updates: { key: string, value: string }[] = [];
const inserts: { key: string, value: string; }[] = [];
if (!entries.length) {
console.log('[INFO] first startup detected, loading global config default values');
CONFIG = Object.fromEntries(Object.entries(CONFIG_ENTRIES).map(([k, { defaultValue }]) => {
inserts.push({ key: k, value: JSON.stringify(defaultValue) });
return [k, defaultValue];
})) as GlobalConfig;
} else {
CONFIG = Object.fromEntries(Object.entries(CONFIG_ENTRIES).map(([k, { defaultValue, validate }]) => {
const index = entries.findIndex(({ key }) => key === k);
if (index === -1) {
console.log(`[INFO] config key ${k} not found, loading default`);
inserts.push({ key: k, value: JSON.stringify(defaultValue) });
return [k, defaultValue];
}
const { value } = entries.splice(index, 1)[0];
let parsed: any;
try {
parsed = JSON.parse(value);
} catch {
console.warn(`[WARN] failed to parse config value for ${k}, falling back to default`);
updates.push({ key: k, value: JSON.stringify(defaultValue) });
return [k, defaultValue];
}
const res = validate(parsed);
if (res?.error) {
console.warn(`[WARN] failed to parse config value for ${k}: ${res.message ?? 'unknown error'}; falling back to default`);
updates.push({ key: k, value: JSON.stringify(defaultValue) });
return [k, defaultValue];
}
return [k, res?.value ?? parsed];
})) as GlobalConfig;
}
await db.transaction().execute(async trx => {
if (inserts.length)
await trx.insertInto('actaeon_global_config')
.values(inserts)
.execute();
for (const update of updates) {
await trx.updateTable('actaeon_global_config')
.where('key', '=', update.key)
.set({ value: update.value })
.executeTakeFirst();
}
});
(globalThis as any).CONFIG = CONFIG;
};