optimize settings helper
This commit is contained in:
parent
730c25a9a1
commit
a8edc19f34
11 changed files with 343 additions and 119 deletions
|
@ -1,135 +1,242 @@
|
|||
import { SettingString, settingsType } from "../type/settingTypes";
|
||||
import ms from "ms";
|
||||
import { SettingString, settingsType, SettingTopic, SettingTypeAtom, SettingValueMapping } from "../type/settingTypes";
|
||||
import { CodingHelper } from "./codingHelper";
|
||||
import SettingCommandHandler from "../command/settingCommandHandler";
|
||||
import SettingService from "../service/settingService";
|
||||
import { APPLICATION_SECRET } from "../env.defaults";
|
||||
import {
|
||||
BooleanConverter,
|
||||
LongStringConverter,
|
||||
MsConverter,
|
||||
NumberConverter,
|
||||
StringConverter,
|
||||
TypeConverter,
|
||||
UrlConverter,
|
||||
} from "./convertHelper";
|
||||
|
||||
export default abstract class SettingHelper {
|
||||
private static settings: { [key in SettingString]?: string } = {};
|
||||
|
||||
public static getSetting(key: SettingString): string | number | boolean | ms.StringValue {
|
||||
let settingType = settingsType[key];
|
||||
let setting = this.settings[key] ?? String(settingType.default ?? "");
|
||||
private static listeners: Map<SettingString, Array<(newValue: any, oldValue: any) => void>> = new Map();
|
||||
private static topicListeners: Map<SettingTopic, Array<() => void>> = new Map();
|
||||
|
||||
private static readonly converters: Record<SettingTypeAtom, TypeConverter<any>> = {
|
||||
longstring: new LongStringConverter(),
|
||||
string: new StringConverter(),
|
||||
url: new UrlConverter(),
|
||||
number: new NumberConverter(),
|
||||
boolean: new BooleanConverter(),
|
||||
ms: new MsConverter(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the value of a setting with the correct type based on the key
|
||||
* @param key The key of the setting
|
||||
* @returns The typed value of the setting
|
||||
*/
|
||||
public static getSetting<K extends SettingString>(key: K): SettingValueMapping[K] {
|
||||
const settingType = settingsType[key];
|
||||
const rawValue = this.settings[key] ?? String(settingType.default ?? "");
|
||||
|
||||
if (Array.isArray(settingType.type)) {
|
||||
return setting;
|
||||
return rawValue as unknown as SettingValueMapping[K];
|
||||
}
|
||||
|
||||
if (settingType.type.includes("/crypt")) {
|
||||
setting = CodingHelper.decrypt(APPLICATION_SECRET, String(setting));
|
||||
let processedValue = rawValue;
|
||||
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
|
||||
processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue);
|
||||
}
|
||||
|
||||
if (settingType.type.startsWith("string")) {
|
||||
return setting;
|
||||
}
|
||||
if (settingType.type.startsWith("ms")) {
|
||||
return setting as ms.StringValue;
|
||||
}
|
||||
if (settingType.type.startsWith("number")) {
|
||||
return Number(setting);
|
||||
}
|
||||
if (settingType.type.startsWith("boolean")) {
|
||||
return setting == "true";
|
||||
}
|
||||
return setting;
|
||||
const baseType =
|
||||
typeof settingType.type === "string"
|
||||
? (settingType.type.split("/")[0] as SettingTypeAtom)
|
||||
: (settingType.type as SettingTypeAtom);
|
||||
|
||||
return this.converters[baseType].fromString(processedValue) as unknown as SettingValueMapping[K];
|
||||
}
|
||||
|
||||
public static async setSetting(key: SettingString, value: string) {
|
||||
if (value == undefined || value == null) return;
|
||||
let settingType = settingsType[key];
|
||||
/**
|
||||
* Sets a setting
|
||||
* @param key The key of the setting
|
||||
* @param value The value to set
|
||||
*/
|
||||
public static async setSetting(key: SettingString, value: string): Promise<void> {
|
||||
if (value === undefined || value === null) return;
|
||||
|
||||
let result = value;
|
||||
const settingType = settingsType[key];
|
||||
this.validateSetting(key, value);
|
||||
|
||||
this.checkSettings(key, result);
|
||||
const oldValue = this.getSetting(key);
|
||||
let finalValue = value;
|
||||
|
||||
if (!Array.isArray(settingType.type) && settingType.type.includes("/crypt")) {
|
||||
result = CodingHelper.encrypt(APPLICATION_SECRET, value);
|
||||
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
|
||||
finalValue = CodingHelper.encrypt(APPLICATION_SECRET, value);
|
||||
}
|
||||
|
||||
this.settings[key] = finalValue;
|
||||
const [topic, settingKey] = key.split(".") as [SettingTopic, string];
|
||||
|
||||
await SettingCommandHandler.create({
|
||||
topic: key.split(".")[0],
|
||||
key: key.split(".")[1],
|
||||
value: result,
|
||||
topic,
|
||||
key: settingKey,
|
||||
value: finalValue,
|
||||
});
|
||||
|
||||
const newValue = this.getSetting(key);
|
||||
this.notifyListeners(key, newValue, oldValue);
|
||||
}
|
||||
|
||||
public static async resetSetting(key: SettingString) {
|
||||
let settingType = settingsType[key];
|
||||
/**
|
||||
* Resets a setting to its default value
|
||||
* @param key The key of the setting
|
||||
*/
|
||||
public static async resetSetting(key: SettingString): Promise<void> {
|
||||
const oldValue = this.getSetting(key);
|
||||
|
||||
const settingType = settingsType[key];
|
||||
this.settings[key] = String(settingType.default ?? "");
|
||||
|
||||
const [topic, settingKey] = key.split(".") as [SettingTopic, string];
|
||||
await SettingCommandHandler.delete({
|
||||
topic: key.split(".")[0],
|
||||
key: key.split(".")[1],
|
||||
topic,
|
||||
key: settingKey,
|
||||
});
|
||||
|
||||
const newValue = this.getSetting(key);
|
||||
this.notifyListeners(key, newValue, oldValue);
|
||||
}
|
||||
|
||||
public static async configure() {
|
||||
console.log("Configured Settings");
|
||||
let settings = await SettingService.getSettings();
|
||||
public static async configure(): Promise<void> {
|
||||
console.log("Configuring Settings");
|
||||
const settings = await SettingService.getSettings();
|
||||
|
||||
for (const element of settings) {
|
||||
let ref = `${element.topic}.${element.key}` as SettingString;
|
||||
const ref = `${element.topic}.${element.key}` as SettingString;
|
||||
this.settings[ref] = element.value;
|
||||
this.checkSettings(ref);
|
||||
}
|
||||
}
|
||||
|
||||
private static checkSettings(key: SettingString, value?: string) {
|
||||
let settingType = settingsType[key];
|
||||
|
||||
if (!value) {
|
||||
value = this.getSetting(key).toString();
|
||||
}
|
||||
|
||||
if (
|
||||
!Array.isArray(settingType.type) &&
|
||||
settingType.type.startsWith("string") &&
|
||||
this.checkIfEmptyOrNotString(value)
|
||||
) {
|
||||
throw new Error(`set valid value to ${key}`);
|
||||
}
|
||||
if (
|
||||
!Array.isArray(settingType.type) &&
|
||||
settingType.type.startsWith("ms") &&
|
||||
this.checkIfNotMS(value as ms.StringValue)
|
||||
) {
|
||||
throw new Error(`set valid ms value to ${key} -> [0-9]*(y|d|h|m|s)`);
|
||||
}
|
||||
if (!Array.isArray(settingType.type) && settingType.type.startsWith("number") && isNaN(Number(value))) {
|
||||
throw new Error(`set valid numeric value to ${key}`);
|
||||
}
|
||||
if (
|
||||
!Array.isArray(settingType.type) &&
|
||||
settingType.type.startsWith("number") &&
|
||||
settingType.min &&
|
||||
Number(value) < settingType.min
|
||||
) {
|
||||
throw new Error(`${key} has to be at least ${settingType.min}`);
|
||||
}
|
||||
if (
|
||||
!Array.isArray(settingType.type) &&
|
||||
settingType.type.startsWith("boolean") &&
|
||||
value != "true" &&
|
||||
value != "false"
|
||||
) {
|
||||
throw new Error(`"set 'true' or 'false' to ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static checkIfEmptyOrNotString(val: any) {
|
||||
return typeof val != "string" || val == "";
|
||||
}
|
||||
|
||||
private static checkIfNotMS(input: ms.StringValue): boolean {
|
||||
try {
|
||||
const result = ms(input);
|
||||
if (result === undefined) {
|
||||
return true;
|
||||
try {
|
||||
this.validateSetting(ref);
|
||||
} catch (error) {
|
||||
console.warn(`Invalid setting ${ref}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a setting
|
||||
* @param key The key of the setting
|
||||
* @param value Optional value to validate
|
||||
*/
|
||||
private static validateSetting(key: SettingString, value?: string): void {
|
||||
const settingType = settingsType[key];
|
||||
const valueToCheck = value ?? this.settings[key] ?? String(settingType.default ?? "");
|
||||
|
||||
if (Array.isArray(settingType.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let processedValue = valueToCheck;
|
||||
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
|
||||
try {
|
||||
processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue);
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to decrypt value for ${key}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const baseType =
|
||||
typeof settingType.type === "string"
|
||||
? (settingType.type.split("/")[0] as SettingTypeAtom)
|
||||
: (settingType.type as SettingTypeAtom);
|
||||
|
||||
if (!this.converters[baseType].validate(processedValue)) {
|
||||
throw new Error(`Invalid value for ${key} of type ${baseType}`);
|
||||
}
|
||||
|
||||
if (baseType === "number" && settingType.min !== undefined) {
|
||||
const numValue = Number(processedValue);
|
||||
if (numValue < settingType.min) {
|
||||
throw new Error(`${key} must be at least ${settingType.min}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener for changes to a specific setting
|
||||
* @param key The setting to monitor
|
||||
* @param callback Function to be called when changes occur
|
||||
*/
|
||||
public static onSettingChanged<K extends SettingString>(
|
||||
key: K,
|
||||
callback: (newValue: SettingValueMapping[K], oldValue: SettingValueMapping[K]) => void
|
||||
): void {
|
||||
if (!this.listeners.has(key)) {
|
||||
this.listeners.set(key, []);
|
||||
}
|
||||
|
||||
this.listeners.get(key)!.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener for changes to a specific setting
|
||||
* @param key The setting to monitor
|
||||
* @param callback Function to be called when changes occur
|
||||
*/
|
||||
public static onSettingTopicChanged<K extends SettingTopic>(key: K, callback: () => void): void {
|
||||
if (!this.topicListeners.has(key)) {
|
||||
this.topicListeners.set(key, []);
|
||||
}
|
||||
|
||||
this.topicListeners.get(key)!.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a registered listener
|
||||
* @param key The setting
|
||||
* @param callback The callback to remove
|
||||
*/
|
||||
public static removeSettingListener<K extends SettingString>(
|
||||
key: K,
|
||||
callback: (newValue: SettingValueMapping[K], oldValue: SettingValueMapping[K]) => void
|
||||
): void {
|
||||
if (!this.listeners.has(key)) return;
|
||||
|
||||
const callbacks = this.listeners.get(key)!;
|
||||
const index = callbacks.indexOf(callback);
|
||||
|
||||
if (index !== -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
if (callbacks.length === 0) {
|
||||
this.listeners.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all registered listeners about changes
|
||||
* @param key The changed setting
|
||||
* @param newValue The new value
|
||||
* @param oldValue The old value
|
||||
*/
|
||||
private static notifyListeners(key: SettingString, newValue: any, oldValue: any): void {
|
||||
if (!this.listeners.has(key)) return;
|
||||
|
||||
const callbacks = this.listeners.get(key)!;
|
||||
for (const callback of callbacks) {
|
||||
try {
|
||||
callback(newValue, oldValue);
|
||||
} catch (error) {
|
||||
console.error(`Error in setting listener for ${key}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const topicCallbacks = this.topicListeners.get(key.split(".")[0] as SettingTopic)!;
|
||||
for (const callback of topicCallbacks) {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error(`Error in setting listener for ${key.split(".")[0]}:`, error);
|
||||
}
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue