From 013da0aa3d579ce3d8594e9f740f854507b1239d Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sun, 22 Oct 2023 17:14:32 +0200 Subject: [PATCH] refactor(server): streamline get config, enable the use of arrays (#4562) --- .../system-config/system-config.core.ts | 82 ++++++++++++------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 9a9ea969ea..66c72fc92e 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -13,11 +13,10 @@ import { VideoCodec, } from '@app/infra/entities'; import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common'; -import { plainToClass } from 'class-transformer'; +import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; import * as _ from 'lodash'; import { Subject } from 'rxjs'; -import { DeepPartial } from 'typeorm'; import { QueueName } from '../job/job.constants'; import { ISystemConfigRepository } from '../repositories'; import { SystemConfigDto } from './dto'; @@ -140,7 +139,7 @@ let instance: SystemConfigCore | null; export class SystemConfigCore { private logger = new Logger(SystemConfigCore.name); private validators: SystemConfigValidator[] = []; - private configCache: SystemConfig | null = null; + private configCache: SystemConfigEntity[] | null = null; public config$ = new Subject(); @@ -218,9 +217,28 @@ export class SystemConfigCore { this.validators.push(validator); } - public getConfig(force = false): Promise { + public async getConfig(force = false): Promise { const configFilePath = process.env.IMMICH_CONFIG_FILE; - return configFilePath ? this.loadFromFile(configFilePath, force) : this.loadFromDatabase(); + const config = _.cloneDeep(defaults); + const overrides = configFilePath ? await this.loadFromFile(configFilePath, force) : await this.repository.load(); + + for (const { key, value } of overrides) { + // set via dot notation + _.set(config, key, value); + } + + const errors = await validate(plainToInstance(SystemConfigDto, config), { + forbidNonWhitelisted: true, + forbidUnknownValues: true, + }); + if (errors.length > 0) { + this.logger.error('Validation error', errors); + if (configFilePath) { + throw new Error(`Invalid value(s) in file: ${errors}`); + } + } + + return config; } public async updateConfig(config: SystemConfig): Promise { @@ -246,7 +264,13 @@ export class SystemConfigCore { const defaultValue = _.get(defaults, key); const isMissing = !_.has(config, key); - if (isMissing || item.value === null || item.value === '' || item.value === defaultValue) { + if ( + isMissing || + item.value === null || + item.value === '' || + item.value === defaultValue || + _.isEqual(item.value, defaultValue) + ) { deletes.push(item); continue; } @@ -275,34 +299,25 @@ export class SystemConfigCore { this.config$.next(newConfig); } - private async loadFromDatabase() { - const config: DeepPartial = {}; - const overrides = await this.repository.load(); - for (const { key, value } of overrides) { - // set via dot notation - _.set(config, key, value); - } - - return plainToClass(SystemConfigDto, _.defaultsDeep(config, defaults)); - } - private async loadFromFile(filepath: string, force = false) { if (force || !this.configCache) { try { - const overrides = JSON.parse((await this.repository.readFile(filepath)).toString()); - const config = plainToClass(SystemConfigDto, _.defaultsDeep(overrides, defaults)); + const file = JSON.parse((await this.repository.readFile(filepath)).toString()); + const overrides: SystemConfigEntity[] = []; - const errors = await validate(config, { - whitelist: true, - forbidNonWhitelisted: true, - forbidUnknownValues: true, - }); - if (errors.length > 0) { - this.logger.error('Validation error', errors); - throw new Error(`Invalid value(s) in file: ${errors}`); + for (const key of Object.values(SystemConfigKey)) { + const value = _.get(file, key); + this.unsetDeep(file, key); + if (value !== undefined) { + overrides.push({ key, value }); + } } - this.configCache = config; + if (!_.isEmpty(file)) { + throw new Error(`Unknown keys found: ${file}`); + } + + this.configCache = overrides; } catch (error: Error | any) { this.logger.error(`Unable to load configuration file: ${filepath} due to ${error}`, error?.stack); throw new Error('Invalid configuration file'); @@ -311,4 +326,15 @@ export class SystemConfigCore { return this.configCache; } + + private unsetDeep(object: object, key: string) { + _.unset(object, key); + const path = key.split('.'); + while (path.pop()) { + if (!_.isEmpty(_.get(object, path))) { + return; + } + _.unset(object, path); + } + } }