From 8babc2d6dc53a2185a7dad405bd3f350d72bb347 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 31 Mar 2026 16:41:58 +0200 Subject: [PATCH] refactor: reduce code duplication across OCPP services, UI server, and configuration - Centralize AJV instantiation into createAjv() factory in OCPPServiceUtils - Centralize payload validator config creation into createPayloadConfigs() generic factory, simplifying OCPP16/20ServiceUtils from multi-line .map() to one-liners - Move rate limiter creation from module-level duplicates in UIHttpServer and UIMCPServer to a shared instance property on AbstractUIServer - Extract 200-line checkDeprecatedConfigurationKeys() and helpers from Configuration.ts into dedicated ConfigurationMigration.ts module --- .../ocpp/1.6/OCPP16ServiceUtils.ts | 26 +-- .../ocpp/2.0/OCPP20ServiceUtils.ts | 26 +-- .../ocpp/OCPPIncomingRequestService.ts | 16 +- .../ocpp/OCPPRequestService.ts | 15 +- .../ocpp/OCPPResponseService.ts | 21 +- src/charging-station/ocpp/OCPPServiceUtils.ts | 34 ++- .../ui-server/AbstractUIServer.ts | 9 +- .../ui-server/UIHttpServer.ts | 7 +- src/charging-station/ui-server/UIMCPServer.ts | 11 +- src/utils/Configuration.ts | 211 +---------------- src/utils/ConfigurationMigration.ts | 214 ++++++++++++++++++ 11 files changed, 281 insertions(+), 309 deletions(-) create mode 100644 src/utils/ConfigurationMigration.ts diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 230d1c8f..d0fc1e26 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -60,8 +60,8 @@ import { buildEmptyMeterValue, buildMeterValue, buildSampledValue, + createPayloadConfigs, getSampledValueTemplate, - PayloadValidatorConfig, PayloadValidatorOptions, sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' @@ -555,11 +555,7 @@ export class OCPP16ServiceUtils { public static createIncomingRequestPayloadConfigs = (): [ OCPP16IncomingRequestCommand, { schemaPath: string } - ][] => - OCPP16ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ - command, - PayloadValidatorConfig(`${schemaBase}.json`), - ]) + ][] => createPayloadConfigs(OCPP16ServiceUtils.incomingRequestSchemaNames, '.json') /** * OCPP 1.6 Incoming Request Response Service validator configurations @@ -568,11 +564,7 @@ export class OCPP16ServiceUtils { public static createIncomingRequestResponsePayloadConfigs = (): [ OCPP16IncomingRequestCommand, { schemaPath: string } - ][] => - OCPP16ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ - command, - PayloadValidatorConfig(`${schemaBase}Response.json`), - ]) + ][] => createPayloadConfigs(OCPP16ServiceUtils.incomingRequestSchemaNames, 'Response.json') /** * Factory options for OCPP 1.6 payload validators @@ -595,11 +587,7 @@ export class OCPP16ServiceUtils { public static createRequestPayloadConfigs = (): [ OCPP16RequestCommand, { schemaPath: string } - ][] => - OCPP16ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ - command, - PayloadValidatorConfig(`${schemaBase}.json`), - ]) + ][] => createPayloadConfigs(OCPP16ServiceUtils.outgoingRequestSchemaNames, '.json') /** * OCPP 1.6 Response Service validator configurations @@ -608,11 +596,7 @@ export class OCPP16ServiceUtils { public static createResponsePayloadConfigs = (): [ OCPP16RequestCommand, { schemaPath: string } - ][] => - OCPP16ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ - command, - PayloadValidatorConfig(`${schemaBase}Response.json`), - ]) + ][] => createPayloadConfigs(OCPP16ServiceUtils.outgoingRequestSchemaNames, 'Response.json') /** * Checks whether a connector or the charging station has a valid reservation for the given idTag. diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 391f0ba1..de52832c 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -46,7 +46,7 @@ import { import { buildConfigKey, getConfigurationKey } from '../../ConfigurationKeyUtils.js' import { buildMeterValue, - PayloadValidatorConfig, + createPayloadConfigs, PayloadValidatorOptions, sendAndSetConnectorStatus, } from '../OCPPServiceUtils.js' @@ -223,11 +223,7 @@ export class OCPP20ServiceUtils { public static createIncomingRequestPayloadConfigs = (): [ OCPP20IncomingRequestCommand, { schemaPath: string } - ][] => - OCPP20ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ - command, - PayloadValidatorConfig(`${schemaBase}Request.json`), - ]) + ][] => createPayloadConfigs(OCPP20ServiceUtils.incomingRequestSchemaNames, 'Request.json') /** * Configuration for OCPP 2.0 Incoming Request Response validators @@ -236,11 +232,7 @@ export class OCPP20ServiceUtils { public static createIncomingRequestResponsePayloadConfigs = (): [ OCPP20IncomingRequestCommand, { schemaPath: string } - ][] => - OCPP20ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ - command, - PayloadValidatorConfig(`${schemaBase}Response.json`), - ]) + ][] => createPayloadConfigs(OCPP20ServiceUtils.incomingRequestSchemaNames, 'Response.json') /** * Factory options for OCPP 2.0 payload validators @@ -263,11 +255,7 @@ export class OCPP20ServiceUtils { public static createRequestPayloadConfigs = (): [ OCPP20RequestCommand, { schemaPath: string } - ][] => - OCPP20ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ - command, - PayloadValidatorConfig(`${schemaBase}Request.json`), - ]) + ][] => createPayloadConfigs(OCPP20ServiceUtils.outgoingRequestSchemaNames, 'Request.json') /** * OCPP 2.0 Response Service validator configurations @@ -276,11 +264,7 @@ export class OCPP20ServiceUtils { public static createResponsePayloadConfigs = (): [ OCPP20RequestCommand, { schemaPath: string } - ][] => - OCPP20ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ - command, - PayloadValidatorConfig(`${schemaBase}Response.json`), - ]) + ][] => createPayloadConfigs(OCPP20ServiceUtils.outgoingRequestSchemaNames, 'Response.json') /** * Enforce ItemsPerMessage and BytesPerMessage limits on request data. diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts index c3a13f7b..ff9f3c9b 100644 --- a/src/charging-station/ocpp/OCPPIncomingRequestService.ts +++ b/src/charging-station/ocpp/OCPPIncomingRequestService.ts @@ -1,5 +1,6 @@ -import _Ajv, { type ValidateFunction } from 'ajv' -import _ajvFormats from 'ajv-formats' +import type { ValidateFunction } from 'ajv' +import type _Ajv from 'ajv' + import { EventEmitter } from 'node:events' import { type ChargingStation } from '../../charging-station/index.js' @@ -12,12 +13,9 @@ import { type OCPPVersion, } from '../../types/index.js' import { isAsyncFunction, logger } from '../../utils/index.js' -import { ajvErrorsToErrorType } from './OCPPServiceUtils.js' +import { ajvErrorsToErrorType, createAjv } from './OCPPServiceUtils.js' type Ajv = _Ajv.default -// eslint-disable-next-line @typescript-eslint/no-redeclare -const Ajv = _Ajv.default -const ajvFormats = _ajvFormats.default const moduleName = 'OCPPIncomingRequestService' @@ -47,11 +45,7 @@ export abstract class OCPPIncomingRequestService extends EventEmitter { protected constructor (version: OCPPVersion) { super() this.version = version - this.ajv = new Ajv({ - keywords: ['javaType'], - multipleOfPrecision: 2, - }) - ajvFormats(this.ajv) + this.ajv = createAjv() this.incomingRequestHandler = this.incomingRequestHandler.bind(this) this.validateIncomingRequestPayload = this.validateIncomingRequestPayload.bind(this) } diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts index c4cab42d..ca104d39 100644 --- a/src/charging-station/ocpp/OCPPRequestService.ts +++ b/src/charging-station/ocpp/OCPPRequestService.ts @@ -1,5 +1,5 @@ -import _Ajv, { type ValidateFunction } from 'ajv' -import _ajvFormats from 'ajv-formats' +import type { ValidateFunction } from 'ajv' +import type _Ajv from 'ajv' import type { ChargingStation } from '../../charging-station/index.js' import type { OCPPResponseService } from './OCPPResponseService.js' @@ -32,12 +32,9 @@ import { logger, } from '../../utils/index.js' import { OCPPConstants } from './OCPPConstants.js' -import { ajvErrorsToErrorType, convertDateToISOString } from './OCPPServiceUtils.js' +import { ajvErrorsToErrorType, convertDateToISOString, createAjv } from './OCPPServiceUtils.js' type Ajv = _Ajv.default -// eslint-disable-next-line @typescript-eslint/no-redeclare -const Ajv = _Ajv.default -const ajvFormats = _ajvFormats.default const moduleName = 'OCPPRequestService' @@ -60,11 +57,7 @@ export abstract class OCPPRequestService { protected constructor (version: OCPPVersion, ocppResponseService: OCPPResponseService) { this.version = version - this.ajv = new Ajv({ - keywords: ['javaType'], - multipleOfPrecision: 2, - }) - ajvFormats(this.ajv) + this.ajv = createAjv() this.ocppResponseService = ocppResponseService this.requestHandler = this.requestHandler.bind(this) this.sendMessage = this.sendMessage.bind(this) diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index 96d27f29..ad9674f2 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -1,5 +1,5 @@ -import _Ajv, { type ValidateFunction } from 'ajv' -import _ajvFormats from 'ajv-formats' +import type { ValidateFunction } from 'ajv' +import type _Ajv from 'ajv' import type { ChargingStation } from '../../charging-station/index.js' @@ -13,12 +13,9 @@ import { type ResponseHandler, } from '../../types/index.js' import { Constants, isAsyncFunction, logger } from '../../utils/index.js' -import { ajvErrorsToErrorType } from './OCPPServiceUtils.js' +import { ajvErrorsToErrorType, createAjv } from './OCPPServiceUtils.js' type Ajv = _Ajv.default -// eslint-disable-next-line @typescript-eslint/no-redeclare -const Ajv = _Ajv.default -const ajvFormats = _ajvFormats.default const moduleName = 'OCPPResponseService' @@ -41,16 +38,8 @@ export abstract class OCPPResponseService { protected constructor (version: OCPPVersion) { this.version = version - this.ajv = new Ajv({ - keywords: ['javaType'], - multipleOfPrecision: 2, - }) - ajvFormats(this.ajv) - this.ajvIncomingRequest = new Ajv({ - keywords: ['javaType'], - multipleOfPrecision: 2, - }) - ajvFormats(this.ajvIncomingRequest) + this.ajv = createAjv() + this.ajvIncomingRequest = createAjv() this.responseHandler = this.responseHandler.bind(this) this.validateResponsePayload = this.validateResponsePayload.bind(this) } diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index 81f42a01..0b0a858f 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -1,4 +1,5 @@ import _Ajv, { type ErrorObject, type JSONSchemaType, type ValidateFunction } from 'ajv' +import _ajvFormats from 'ajv-formats' import { isDate } from 'date-fns' import { randomInt } from 'node:crypto' import { readFileSync } from 'node:fs' @@ -69,6 +70,16 @@ const moduleName = 'OCPPServiceUtils' type Ajv = _Ajv.default // eslint-disable-next-line @typescript-eslint/no-redeclare const Ajv = _Ajv.default +const ajvFormats = _ajvFormats.default + +export const createAjv = (): Ajv => { + const ajv = new Ajv({ + keywords: ['javaType'], + multipleOfPrecision: 2, + }) + ajvFormats(ajv) + return ajv +} interface MultiPhaseMeasurandData { perPhaseTemplates: MeasurandPerPhaseSampledValueTemplates @@ -2081,16 +2092,27 @@ export function isRequestCommandSupported ( return false } -/** - * Configuration for a single payload validator. - * @param schemaPath - Path to the JSON schema file - * @returns Configuration object for payload validator creation - */ -export const PayloadValidatorConfig = (schemaPath: string) => +const PayloadValidatorConfig = (schemaPath: string) => ({ schemaPath, }) as const +/** + * Maps schema name tuples to payload validator config tuples with the given suffix. + * @param schemaNames - Array of `[command, schemaBase]` tuples + * @param schemaSuffix - File suffix appended to each schema base (e.g. `Request.json`) + * @returns Array of `[command, config]` tuples for payload validator map construction + */ +export function createPayloadConfigs ( + schemaNames: readonly [Command, string][], + schemaSuffix: string +): [Command, { schemaPath: string }][] { + return schemaNames.map(([command, schemaBase]) => [ + command, + PayloadValidatorConfig(`${schemaBase}${schemaSuffix}`), + ]) +} + /** * Options for payload validator creation. * @param ocppVersion - The OCPP version diff --git a/src/charging-station/ui-server/AbstractUIServer.ts b/src/charging-station/ui-server/AbstractUIServer.ts index 4e1c418e..8654901b 100644 --- a/src/charging-station/ui-server/AbstractUIServer.ts +++ b/src/charging-station/ui-server/AbstractUIServer.ts @@ -23,13 +23,19 @@ import { } from '../../types/index.js' import { isEmpty, isNotEmptyString, logger, logPrefix } from '../../utils/index.js' import { UIServiceFactory } from './ui-services/UIServiceFactory.js' -import { isValidCredential } from './UIServerSecurity.js' +import { + createRateLimiter, + DEFAULT_RATE_LIMIT, + DEFAULT_RATE_WINDOW, + isValidCredential, +} from './UIServerSecurity.js' import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js' const moduleName = 'AbstractUIServer' export abstract class AbstractUIServer { protected readonly httpServer: Http2Server | Server + protected readonly rateLimiter: ReturnType protected readonly responseHandlers: Map protected abstract readonly uiServerType: string @@ -62,6 +68,7 @@ export abstract class AbstractUIServer { ) } this.responseHandlers = new Map() + this.rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW) this.uiServices = new Map() } diff --git a/src/charging-station/ui-server/UIHttpServer.ts b/src/charging-station/ui-server/UIHttpServer.ts index a07f51d3..8dce2ca3 100644 --- a/src/charging-station/ui-server/UIHttpServer.ts +++ b/src/charging-station/ui-server/UIHttpServer.ts @@ -23,18 +23,13 @@ import { generateUUID, getErrorMessage, JSONStringify, logger } from '../../util import { AbstractUIServer } from './AbstractUIServer.js' import { createBodySizeLimiter, - createRateLimiter, DEFAULT_COMPRESSION_THRESHOLD, DEFAULT_MAX_PAYLOAD_SIZE, - DEFAULT_RATE_LIMIT, - DEFAULT_RATE_WINDOW, } from './UIServerSecurity.js' import { HttpMethod, isProtocolAndVersionSupported } from './UIServerUtils.js' const moduleName = 'UIHttpServer' -const rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW) - /** * @deprecated Use UIMCPServer (ApplicationProtocol.MCP) instead. Will be removed in a future major version. */ @@ -112,7 +107,7 @@ export class UIHttpServer extends AbstractUIServer { private requestListener (req: IncomingMessage, res: ServerResponse): void { // Rate limiting check const clientIp = req.socket.remoteAddress ?? 'unknown' - if (!rateLimiter(clientIp)) { + if (!this.rateLimiter(clientIp)) { res .writeHead(StatusCodes.TOO_MANY_REQUESTS, { 'Content-Type': 'text/plain', diff --git a/src/charging-station/ui-server/UIMCPServer.ts b/src/charging-station/ui-server/UIMCPServer.ts index 03f6381e..695bb3c3 100644 --- a/src/charging-station/ui-server/UIMCPServer.ts +++ b/src/charging-station/ui-server/UIMCPServer.ts @@ -31,20 +31,13 @@ import { registerMCPResources, registerMCPSchemaResources, } from './mcp/index.js' -import { - createRateLimiter, - DEFAULT_MAX_PAYLOAD_SIZE, - DEFAULT_RATE_LIMIT, - DEFAULT_RATE_WINDOW, -} from './UIServerSecurity.js' +import { DEFAULT_MAX_PAYLOAD_SIZE } from './UIServerSecurity.js' import { HttpMethod } from './UIServerUtils.js' const moduleName = 'UIMCPServer' const MCP_TOOL_TIMEOUT_MS = 30_000 -const rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW) - export class UIMCPServer extends AbstractUIServer { protected override readonly uiServerType = 'UI MCP Server' @@ -122,7 +115,7 @@ export class UIMCPServer extends AbstractUIServer { } const clientIp = req.socket.remoteAddress ?? 'unknown' - if (!rateLimiter(clientIp)) { + if (!this.rateLimiter(clientIp)) { res.writeHead(429, { 'Content-Type': 'text/plain' }).end('429 Too Many Requests') return } diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index a7745520..cc944a2a 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -26,6 +26,7 @@ import { DEFAULT_WORKER_START_DELAY, WorkerProcessType, } from '../worker/index.js' +import { checkDeprecatedConfigurationKeys } from './ConfigurationMigration.js' import { buildPerformanceUriFilePath, checkWorkerElementsPerWorker, @@ -161,9 +162,9 @@ export class Configuration { } public static getStationTemplateUrls (): StationTemplateUrl[] | undefined { - const checkDeprecatedConfigurationKeysOnce = once( - Configuration.checkDeprecatedConfigurationKeys.bind(Configuration) - ) + const checkDeprecatedConfigurationKeysOnce = once(() => { + checkDeprecatedConfigurationKeys(Configuration.getConfigurationData()) + }) checkDeprecatedConfigurationKeysOnce() return Configuration.getConfigurationData()?.stationTemplateUrls } @@ -351,177 +352,6 @@ export class Configuration { } } - private static checkDeprecatedConfigurationKeys (): void { - const deprecatedKeys: [string, ConfigurationSection | undefined, string][] = [ - // connection timeout - [ - 'autoReconnectTimeout', - undefined, - "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead", - ], - [ - 'connectionTimeout', - undefined, - "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead", - ], - // connection retries - ['autoReconnectMaxRetries', undefined, 'Use it in charging station template instead'], - // station template url(s) - ['stationTemplateURLs', undefined, "Use 'stationTemplateUrls' instead"], - // supervision url(s) - ['supervisionURLs', undefined, "Use 'supervisionUrls' instead"], - // supervision urls distribution - ['distributeStationToTenantEqually', undefined, "Use 'supervisionUrlDistribution' instead"], - ['distributeStationsToTenantsEqually', undefined, "Use 'supervisionUrlDistribution' instead"], - // worker section - [ - 'useWorkerPool', - undefined, - `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`, - ], - [ - 'workerProcess', - undefined, - `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`, - ], - [ - 'workerStartDelay', - undefined, - `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`, - ], - [ - 'chargingStationsPerWorker', - undefined, - `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`, - ], - [ - 'elementAddDelay', - undefined, - `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`, - ], - [ - 'workerPoolMinSize', - undefined, - `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`, - ], - [ - 'workerPoolSize', - undefined, - `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`, - ], - [ - 'workerPoolMaxSize', - undefined, - `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`, - ], - [ - 'workerPoolStrategy', - undefined, - `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`, - ], - ['poolStrategy', ConfigurationSection.worker, 'Not publicly exposed to end users'], - ['elementStartDelay', ConfigurationSection.worker, "Use 'elementAddDelay' instead"], - // log section - [ - 'logEnabled', - undefined, - `Use '${ConfigurationSection.log}' section to define the logging enablement instead`, - ], - [ - 'logFile', - undefined, - `Use '${ConfigurationSection.log}' section to define the log file instead`, - ], - [ - 'logErrorFile', - undefined, - `Use '${ConfigurationSection.log}' section to define the log error file instead`, - ], - [ - 'logConsole', - undefined, - `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`, - ], - [ - 'logStatisticsInterval', - undefined, - `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`, - ], - [ - 'logLevel', - undefined, - `Use '${ConfigurationSection.log}' section to define the log level instead`, - ], - [ - 'logFormat', - undefined, - `Use '${ConfigurationSection.log}' section to define the log format instead`, - ], - [ - 'logRotate', - undefined, - `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`, - ], - [ - 'logMaxFiles', - undefined, - `Use '${ConfigurationSection.log}' section to define the log maximum files instead`, - ], - [ - 'logMaxSize', - undefined, - `Use '${ConfigurationSection.log}' section to define the log maximum size instead`, - ], - // performanceStorage section - ['URI', ConfigurationSection.performanceStorage, "Use 'uri' instead"], - ] - for (const [key, section, msg] of deprecatedKeys) { - Configuration.warnDeprecatedConfigurationKey(key, section, msg) - } - // station template url(s) remapping - if ( - Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData] != - null - ) { - const configurationData = Configuration.getConfigurationData() - if (configurationData != null) { - configurationData.stationTemplateUrls = configurationData[ - 'stationTemplateURLs' as keyof ConfigurationData - ] as StationTemplateUrl[] - } - } - Configuration.getConfigurationData()?.stationTemplateUrls.forEach( - (stationTemplateUrl: StationTemplateUrl) => { - if (stationTemplateUrl['numberOfStation' as keyof StationTemplateUrl] != null) { - console.error( - `${chalk.green(logPrefix())} ${chalk.red( - `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead` - )}` - ) - } - } - ) - // worker section: staticPool check - if ( - Configuration.getConfigurationData()?.worker?.processType === - ('staticPool' as WorkerProcessType) - ) { - console.error( - `${chalk.green(logPrefix())} ${chalk.red( - `Deprecated configuration 'staticPool' value usage in worker section 'processType' field. Use '${WorkerProcessType.fixedPool}' value instead` - )}` - ) - } - // uiServer section - if (has('uiWebSocketServer', Configuration.getConfigurationData())) { - console.error( - `${chalk.green(logPrefix())} ${chalk.red( - `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead` - )}` - ) - } - } - private static getConfigurationFileWatcher (): FSWatcher | undefined { if ( Configuration.configurationFile == null || @@ -573,37 +403,4 @@ export class Configuration { private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean { return Configuration.configurationSectionCache.has(sectionName) } - - private static warnDeprecatedConfigurationKey ( - key: string, - configurationSection?: ConfigurationSection, - logMsgToAppend = '' - ): void { - if ( - configurationSection != null && - Configuration.getConfigurationData()?.[configurationSection as keyof ConfigurationData] != - null && - ( - Configuration.getConfigurationData()?.[ - configurationSection as keyof ConfigurationData - ] as Record - )[key] != null - ) { - console.error( - `${chalk.green(logPrefix())} ${chalk.red( - `Deprecated configuration key '${key}' usage in section '${configurationSection}'${ - logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : '' - }` - )}` - ) - } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) { - console.error( - `${chalk.green(logPrefix())} ${chalk.red( - `Deprecated configuration key '${key}' usage${ - logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : '' - }` - )}` - ) - } - } } diff --git a/src/utils/ConfigurationMigration.ts b/src/utils/ConfigurationMigration.ts new file mode 100644 index 00000000..65f34d32 --- /dev/null +++ b/src/utils/ConfigurationMigration.ts @@ -0,0 +1,214 @@ +import chalk from 'chalk' + +import { + type ConfigurationData, + ConfigurationSection, + type StationTemplateUrl, +} from '../types/index.js' +import { WorkerProcessType } from '../worker/index.js' +import { logPrefix } from './ConfigurationUtils.js' +import { has } from './Utils.js' + +/** + * Check and warn about deprecated configuration keys + * @param configurationData - The configuration data to check + */ +export function checkDeprecatedConfigurationKeys ( + configurationData: ConfigurationData | undefined +): void { + const deprecatedKeys: [string, ConfigurationSection | undefined, string][] = [ + // connection timeout + [ + 'autoReconnectTimeout', + undefined, + "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead", + ], + [ + 'connectionTimeout', + undefined, + "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead", + ], + // connection retries + ['autoReconnectMaxRetries', undefined, 'Use it in charging station template instead'], + // station template url(s) + ['stationTemplateURLs', undefined, "Use 'stationTemplateUrls' instead"], + // supervision url(s) + ['supervisionURLs', undefined, "Use 'supervisionUrls' instead"], + // supervision urls distribution + ['distributeStationToTenantEqually', undefined, "Use 'supervisionUrlDistribution' instead"], + ['distributeStationsToTenantsEqually', undefined, "Use 'supervisionUrlDistribution' instead"], + // worker section + [ + 'useWorkerPool', + undefined, + `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`, + ], + [ + 'workerProcess', + undefined, + `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`, + ], + [ + 'workerStartDelay', + undefined, + `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`, + ], + [ + 'chargingStationsPerWorker', + undefined, + `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`, + ], + [ + 'elementAddDelay', + undefined, + `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`, + ], + [ + 'workerPoolMinSize', + undefined, + `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`, + ], + [ + 'workerPoolSize', + undefined, + `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`, + ], + [ + 'workerPoolMaxSize', + undefined, + `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`, + ], + [ + 'workerPoolStrategy', + undefined, + `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`, + ], + ['poolStrategy', ConfigurationSection.worker, 'Not publicly exposed to end users'], + ['elementStartDelay', ConfigurationSection.worker, "Use 'elementAddDelay' instead"], + // log section + [ + 'logEnabled', + undefined, + `Use '${ConfigurationSection.log}' section to define the logging enablement instead`, + ], + [ + 'logFile', + undefined, + `Use '${ConfigurationSection.log}' section to define the log file instead`, + ], + [ + 'logErrorFile', + undefined, + `Use '${ConfigurationSection.log}' section to define the log error file instead`, + ], + [ + 'logConsole', + undefined, + `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`, + ], + [ + 'logStatisticsInterval', + undefined, + `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`, + ], + [ + 'logLevel', + undefined, + `Use '${ConfigurationSection.log}' section to define the log level instead`, + ], + [ + 'logFormat', + undefined, + `Use '${ConfigurationSection.log}' section to define the log format instead`, + ], + [ + 'logRotate', + undefined, + `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`, + ], + [ + 'logMaxFiles', + undefined, + `Use '${ConfigurationSection.log}' section to define the log maximum files instead`, + ], + [ + 'logMaxSize', + undefined, + `Use '${ConfigurationSection.log}' section to define the log maximum size instead`, + ], + // performanceStorage section + ['URI', ConfigurationSection.performanceStorage, "Use 'uri' instead"], + ] + for (const [key, section, msg] of deprecatedKeys) { + warnDeprecatedConfigurationKey(configurationData, key, section, msg) + } + // station template url(s) remapping + if (configurationData?.['stationTemplateURLs' as keyof ConfigurationData] != null) { + configurationData.stationTemplateUrls = configurationData[ + 'stationTemplateURLs' as keyof ConfigurationData + ] as StationTemplateUrl[] + } + configurationData?.stationTemplateUrls.forEach((stationTemplateUrl: StationTemplateUrl) => { + if (stationTemplateUrl['numberOfStation' as keyof StationTemplateUrl] != null) { + console.error( + `${chalk.green(logPrefix())} ${chalk.red( + `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead` + )}` + ) + } + }) + // worker section: staticPool check + if (configurationData?.worker?.processType === ('staticPool' as WorkerProcessType)) { + console.error( + `${chalk.green(logPrefix())} ${chalk.red( + `Deprecated configuration 'staticPool' value usage in worker section 'processType' field. Use '${WorkerProcessType.fixedPool}' value instead` + )}` + ) + } + // uiServer section + if (has('uiWebSocketServer', configurationData)) { + console.error( + `${chalk.green(logPrefix())} ${chalk.red( + `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead` + )}` + ) + } +} + +/** + * Warn about a deprecated configuration key + * @param configurationData - The configuration data to check + * @param key - The deprecated key name + * @param configurationSection - The configuration section containing the key + * @param logMsgToAppend - Additional message to append to the warning + */ +function warnDeprecatedConfigurationKey ( + configurationData: ConfigurationData | undefined, + key: string, + configurationSection?: ConfigurationSection, + logMsgToAppend = '' +): void { + if ( + configurationSection != null && + configurationData?.[configurationSection as keyof ConfigurationData] != null && + (configurationData[configurationSection as keyof ConfigurationData] as Record)[ + key + ] != null + ) { + console.error( + `${chalk.green(logPrefix())} ${chalk.red( + `Deprecated configuration key '${key}' usage in section '${configurationSection}'${ + logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : '' + }` + )}` + ) + } else if (configurationData?.[key as keyof ConfigurationData] != null) { + console.error( + `${chalk.green(logPrefix())} ${chalk.red( + `Deprecated configuration key '${key}' usage${ + logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : '' + }` + )}` + ) + } +} -- 2.43.0