From b2189a8c5f98669af7f4ccdf0b0ed9fc25001aef Mon Sep 17 00:00:00 2001 From: liuyi Date: Thu, 5 Jun 2025 22:04:45 +0800 Subject: [PATCH] add config module --- src/modules/config/ConfigStorage.ts | 53 ++++++++++ src/modules/config/config.module.ts | 20 ++++ src/modules/config/configure.ts | 144 ++++++++++++++++++++++++++++ src/modules/config/types.ts | 24 +++++ src/modules/core/helpers/utils.ts | 7 ++ 5 files changed, 248 insertions(+) create mode 100644 src/modules/config/ConfigStorage.ts create mode 100644 src/modules/config/config.module.ts create mode 100644 src/modules/config/configure.ts create mode 100644 src/modules/config/types.ts diff --git a/src/modules/config/ConfigStorage.ts b/src/modules/config/ConfigStorage.ts new file mode 100644 index 0000000..73f3d35 --- /dev/null +++ b/src/modules/config/ConfigStorage.ts @@ -0,0 +1,53 @@ +import { resolve } from 'path'; + +import { ensureFileSync, readFileSync, writeFileSync } from 'fs-extra'; +import { has, isNil, omit, set } from 'lodash'; +import { parse } from 'yaml'; + +export class ConfigStorage { + protected _enabled = false; + + protected _path = resolve(__dirname, '../../..', 'config.yaml'); + + protected _config: RecordAny = {}; + + get enabled() { + return this._enabled; + } + + get path() { + return this._path; + } + + get config() { + return this._config; + } + + constructor(enabled?: boolean, filePath?: string) { + if (!isNil(enabled)) { + this._enabled = enabled; + } + if (this._enabled) { + if (!isNil(filePath)) { + this._path = filePath; + } + ensureFileSync(this._path); + const config = parse(readFileSync(this._path, 'utf-8')); + this._config = isNil(config) ? {} : config; + } + } + + set(key: string, value: T) { + ensureFileSync(this.path); + set(this._config, key, value); + writeFileSync(this.path, JSON.stringify(this._config, null, 4)); + } + + remove(key: string) { + this._config = omit(this._config, [key]); + if (has(this._config, key)) { + omit(this._config, [key]); + } + writeFileSync(this.path, JSON.stringify(this._config, null, 4)); + } +} diff --git a/src/modules/config/config.module.ts b/src/modules/config/config.module.ts new file mode 100644 index 0000000..e26933b --- /dev/null +++ b/src/modules/config/config.module.ts @@ -0,0 +1,20 @@ +import { DynamicModule, Module } from '@nestjs/common'; + +import { Configure } from './configure'; + +@Module({}) +export class ConfigModule { + static forRoot(configure: Configure): DynamicModule { + return { + global: true, + module: ConfigModule, + providers: [ + { + provide: Configure, + useValue: configure, + }, + ], + exports: [Configure], + }; + } +} diff --git a/src/modules/config/configure.ts b/src/modules/config/configure.ts new file mode 100644 index 0000000..9be8611 --- /dev/null +++ b/src/modules/config/configure.ts @@ -0,0 +1,144 @@ +import { get, has, isArray, isFunction, isNil, isObject, omit, set } from 'lodash'; + +import { deepMerge, isAsyncFunction } from '../core/helpers'; + +import { ConfigStorage } from './ConfigStorage'; +import { Env } from './env'; +import { ConfigStorageOption, ConfigureFactory, ConfigureRegister } from './types'; + +interface SetStorageOption { + enabled?: boolean; + change?: boolean; +} + +export class Configure { + protected inited = false; + + protected factories: Record> = {}; + + protected config: RecordAny = {}; + + protected _env: Env; + + protected storage: ConfigStorage; + + get env() { + return this._env; + } + + all() { + return this.config; + } + + has(key: string) { + return has(this.config, key); + } + + async initialize(configs: RecordAny = {}, options: ConfigStorageOption = {}) { + if (this.inited) { + return this; + } + this._env = new Env(); + await this._env.load(); + const { enable, filePath } = options; + this.storage = new ConfigStorage(enable, filePath); + for (const key of Object.keys(configs)) { + this.add(key, configs[key]); + } + await this.sync(); + this.inited = true; + return this; + } + + async get(key: string, defaultValue?: T): Promise { + if (!has(this.config, key) && defaultValue === undefined && has(this.factories, key)) { + await this.syncFactory(key); + return this.get(key, defaultValue); + } + return get(this.config, key, defaultValue); + } + + set(key: string, value: T, storage: SetStorageOption | boolean = false, append = false) { + const storageEnable = typeof storage === 'boolean' ? storage : !!storage.enabled; + const storageChange = typeof storage === 'boolean' ? false : !!storage.change; + if (storageEnable && this.storage.enabled) { + this.changeStorageValue(key, value, storageChange, append); + } else { + set(this.config, key, value); + } + return this; + } + + async sync(name?: string) { + if (isNil(name)) { + for (const key in this.factories) { + await this.syncFactory(key); + } + } else { + await this.syncFactory(name); + } + } + + protected async syncFactory(key: string) { + if (has(this.config, key) || !has(this.factories, key)) { + return this; + } + const { register, defaultRegister, storage, hook, append } = this.factories[key]; + let defaultValue = {}; + let value = isAsyncFunction(register) ? await register(this) : register(this); + if (!isNil(defaultRegister)) { + defaultValue = isAsyncFunction(defaultRegister) + ? await defaultRegister(this) + : defaultRegister(this); + value = deepMerge(defaultValue, value, 'replace'); + } + + if (!isNil(hook)) { + value = isAsyncFunction(hook) ? await hook(this, value) : hook(this, value); + } + if (this.storage.enabled) { + value = deepMerge(value, get(this.storage.config, key, isArray(value) ? [] : {})); + } + this.set(key, value, storage && isNil(await this.get(key, null)), append); + return this; + } + + add(key: string, register: ConfigureFactory | ConfigureRegister) { + if (!isFunction(register) && 'register' in register) { + this.factories[key] = register as any; + } else if (isFunction(register)) { + this.factories[key] = { register }; + } + return this; + } + + remove(key: string) { + if (this.storage.enabled && has(this.storage.config, key)) { + this.storage.remove(key); + this.config = deepMerge(this.config, this.storage.config, 'replace'); + } else if (has(this.config, key)) { + this.config = omit(this.config, [key]); + } + return this; + } + + async store(key: string, change = false, append = false) { + if (!this.storage.enabled) { + throw new Error('Must enable storage first'); + } + this.changeStorageValue(key, await this.get(key, null), change, append); + return this; + } + + protected changeStorageValue(key: string, value: T, change = false, append = false) { + if (change || !has(this.storage.config, key)) { + this.storage.set(key, value); + } else if (isObject(get(this.storage.config, key))) { + this.storage.set( + key, + deepMerge(value, get(this.storage.config, key), append ? 'merge' : 'replace'), + ); + } + this.config = deepMerge(this.config, this.storage.config, append ? 'merge' : 'replace'); + } +} diff --git a/src/modules/config/types.ts b/src/modules/config/types.ts new file mode 100644 index 0000000..90245a8 --- /dev/null +++ b/src/modules/config/types.ts @@ -0,0 +1,24 @@ +import { Configure } from './configure'; + +export interface ConfigStorageOption { + enable?: boolean; + filePath?: string; +} + +export type ConfigureRegister = (configure: Configure) => T | Promise; + +export interface ConfigureFactory { + register: ConfigureRegister>; + + defaultRegister?: ConfigureRegister; + + storage?: boolean; + + hook?: (configure: Configure, value: T) => P | Promise

; + + append?: boolean; +} + +export type ConnectionOption = { name?: string } & T; + +export type ConnectionRst = Array<{ name?: string } & T>; diff --git a/src/modules/core/helpers/utils.ts b/src/modules/core/helpers/utils.ts index 0723492..f4c3633 100644 --- a/src/modules/core/helpers/utils.ts +++ b/src/modules/core/helpers/utils.ts @@ -32,3 +32,10 @@ export const deepMerge = ( } return deepmerge(x, y, options) as P extends T ? T : T & P; }; + +export function isAsyncFunction>( + callback: (...args: P) => T | Promise, +): callback is (...args: P) => Promise { + const AsyncFunction = (async () => {}).constructor; + return callback instanceof AsyncFunction === true; +}