add config module

This commit is contained in:
liuyi 2025-06-05 22:04:45 +08:00
parent 7b6a5ca24e
commit b2189a8c5f
5 changed files with 248 additions and 0 deletions

View File

@ -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<T>(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));
}
}

View File

@ -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],
};
}
}

View File

@ -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<string, ConfigureFactory<RecordAny>> = {};
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<T>(key: string, defaultValue?: T): Promise<T> {
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<T>(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<T extends RecordAny>(key: string, register: ConfigureFactory<T> | ConfigureRegister<T>) {
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<T>(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');
}
}

View File

@ -0,0 +1,24 @@
import { Configure } from './configure';
export interface ConfigStorageOption {
enable?: boolean;
filePath?: string;
}
export type ConfigureRegister<T extends RecordAny> = (configure: Configure) => T | Promise<T>;
export interface ConfigureFactory<T extends RecordAny, P extends RecordAny = T> {
register: ConfigureRegister<RePartial<T>>;
defaultRegister?: ConfigureRegister<T>;
storage?: boolean;
hook?: (configure: Configure, value: T) => P | Promise<P>;
append?: boolean;
}
export type ConnectionOption<T extends RecordAny> = { name?: string } & T;
export type ConnectionRst<T extends RecordAny> = Array<{ name?: string } & T>;

View File

@ -32,3 +32,10 @@ export const deepMerge = <T, P>(
}
return deepmerge(x, y, options) as P extends T ? T : T & P;
};
export function isAsyncFunction<T, P extends Array<any>>(
callback: (...args: P) => T | Promise<T>,
): callback is (...args: P) => Promise<T> {
const AsyncFunction = (async () => {}).constructor;
return callback instanceof AsyncFunction === true;
}