fix: use lodash merge to deep merge objects
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import { type FSWatcher, readFileSync, watch } from 'node:fs'
2 import { dirname, join } from 'node:path'
3 import { env } from 'node:process'
4 import { fileURLToPath } from 'node:url'
5
6 import chalk from 'chalk'
7 import { merge } from 'lodash-es'
8
9 import {
10 buildPerformanceUriFilePath,
11 checkWorkerElementsPerWorker,
12 checkWorkerProcessType,
13 getDefaultPerformanceStorageUri,
14 handleFileException,
15 logPrefix
16 } from './ConfigurationUtils.js'
17 import { Constants } from './Constants.js'
18 import { hasOwnProp, isCFEnvironment, once } from './Utils.js'
19 import {
20 ApplicationProtocol,
21 type ConfigurationData,
22 ConfigurationSection,
23 FileType,
24 type LogConfiguration,
25 type StationTemplateUrl,
26 type StorageConfiguration,
27 StorageType,
28 SupervisionUrlDistribution,
29 type UIServerConfiguration,
30 type WorkerConfiguration
31 } from '../types/index.js'
32 import {
33 DEFAULT_ELEMENT_START_DELAY,
34 DEFAULT_POOL_MAX_SIZE,
35 DEFAULT_POOL_MIN_SIZE,
36 DEFAULT_WORKER_START_DELAY,
37 WorkerProcessType
38 } from '../worker/index.js'
39
40 type ConfigurationSectionType =
41 | LogConfiguration
42 | StorageConfiguration
43 | WorkerConfiguration
44 | UIServerConfiguration
45
46 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
47 export class Configuration {
48 public static configurationChangeCallback?: () => Promise<void>
49
50 private static readonly configurationFile = join(
51 dirname(fileURLToPath(import.meta.url)),
52 'assets',
53 'config.json'
54 )
55
56 private static configurationFileReloading = false
57 private static configurationData?: ConfigurationData
58 private static configurationFileWatcher?: FSWatcher
59 private static readonly configurationSectionCache = new Map<
60 ConfigurationSection,
61 ConfigurationSectionType
62 >([
63 [ConfigurationSection.log, Configuration.buildLogSection()],
64 [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
65 [ConfigurationSection.worker, Configuration.buildWorkerSection()],
66 [ConfigurationSection.uiServer, Configuration.buildUIServerSection()]
67 ])
68
69 private constructor () {
70 // This is intentional
71 }
72
73 public static getConfigurationSection<T extends ConfigurationSectionType>(
74 sectionName: ConfigurationSection
75 ): T {
76 if (!Configuration.isConfigurationSectionCached(sectionName)) {
77 Configuration.cacheConfigurationSection(sectionName)
78 }
79 return Configuration.configurationSectionCache.get(sectionName) as T
80 }
81
82 public static getStationTemplateUrls (): StationTemplateUrl[] | undefined {
83 const checkDeprecatedConfigurationKeysOnce = once(
84 Configuration.checkDeprecatedConfigurationKeys.bind(Configuration),
85 Configuration
86 )
87 checkDeprecatedConfigurationKeysOnce()
88 return Configuration.getConfigurationData()?.stationTemplateUrls
89 }
90
91 public static getSupervisionUrls (): string | string[] | undefined {
92 if (
93 Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData] != null
94 ) {
95 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
96 Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
97 'supervisionURLs' as keyof ConfigurationData
98 ] as string | string[]
99 }
100 return Configuration.getConfigurationData()?.supervisionUrls
101 }
102
103 public static getSupervisionUrlDistribution (): SupervisionUrlDistribution | undefined {
104 return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
105 ? Configuration.getConfigurationData()?.supervisionUrlDistribution
106 : SupervisionUrlDistribution.ROUND_ROBIN
107 }
108
109 public static workerPoolInUse (): boolean {
110 return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes(
111 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
112 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
113 .processType!
114 )
115 }
116
117 public static workerDynamicPoolInUse (): boolean {
118 return (
119 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
120 .processType === WorkerProcessType.dynamicPool
121 )
122 }
123
124 private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean {
125 return Configuration.configurationSectionCache.has(sectionName)
126 }
127
128 private static cacheConfigurationSection (sectionName: ConfigurationSection): void {
129 switch (sectionName) {
130 case ConfigurationSection.log:
131 Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection())
132 break
133 case ConfigurationSection.performanceStorage:
134 Configuration.configurationSectionCache.set(
135 sectionName,
136 Configuration.buildPerformanceStorageSection()
137 )
138 break
139 case ConfigurationSection.worker:
140 Configuration.configurationSectionCache.set(sectionName, Configuration.buildWorkerSection())
141 break
142 case ConfigurationSection.uiServer:
143 Configuration.configurationSectionCache.set(
144 sectionName,
145 Configuration.buildUIServerSection()
146 )
147 break
148 default:
149 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
150 throw new Error(`Unknown configuration section '${sectionName}'`)
151 }
152 }
153
154 private static buildUIServerSection (): UIServerConfiguration {
155 let uiServerConfiguration: UIServerConfiguration = {
156 enabled: false,
157 type: ApplicationProtocol.WS,
158 options: {
159 host: Constants.DEFAULT_UI_SERVER_HOST,
160 port: Constants.DEFAULT_UI_SERVER_PORT
161 }
162 }
163 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
164 uiServerConfiguration = merge(
165 uiServerConfiguration,
166 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
167 Configuration.getConfigurationData()!.uiServer!
168 )
169 }
170 if (isCFEnvironment()) {
171 delete uiServerConfiguration.options?.host
172 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
173 uiServerConfiguration.options!.port = parseInt(env.PORT!)
174 }
175 return uiServerConfiguration
176 }
177
178 private static buildPerformanceStorageSection (): StorageConfiguration {
179 let storageConfiguration: StorageConfiguration
180 switch (Configuration.getConfigurationData()?.performanceStorage?.type) {
181 case StorageType.SQLITE:
182 storageConfiguration = {
183 enabled: false,
184 type: StorageType.SQLITE,
185 uri: getDefaultPerformanceStorageUri(StorageType.SQLITE)
186 }
187 break
188 case StorageType.JSON_FILE:
189 storageConfiguration = {
190 enabled: false,
191 type: StorageType.JSON_FILE,
192 uri: getDefaultPerformanceStorageUri(StorageType.JSON_FILE)
193 }
194 break
195 case StorageType.NONE:
196 default:
197 storageConfiguration = {
198 enabled: true,
199 type: StorageType.NONE
200 }
201 break
202 }
203 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) {
204 storageConfiguration = {
205 ...storageConfiguration,
206 ...Configuration.getConfigurationData()?.performanceStorage,
207 ...((Configuration.getConfigurationData()?.performanceStorage?.type ===
208 StorageType.JSON_FILE ||
209 Configuration.getConfigurationData()?.performanceStorage?.type === StorageType.SQLITE) &&
210 Configuration.getConfigurationData()?.performanceStorage?.uri != null && {
211 uri: buildPerformanceUriFilePath(
212 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
213 new URL(Configuration.getConfigurationData()!.performanceStorage!.uri!).pathname
214 )
215 })
216 }
217 }
218 return storageConfiguration
219 }
220
221 private static buildLogSection (): LogConfiguration {
222 const defaultLogConfiguration: LogConfiguration = {
223 enabled: true,
224 file: 'logs/combined.log',
225 errorFile: 'logs/error.log',
226 statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
227 level: 'info',
228 format: 'simple',
229 rotate: true
230 }
231 const deprecatedLogConfiguration: LogConfiguration = {
232 ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
233 enabled: Configuration.getConfigurationData()?.logEnabled
234 }),
235 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
236 file: Configuration.getConfigurationData()?.logFile
237 }),
238 ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
239 errorFile: Configuration.getConfigurationData()?.logErrorFile
240 }),
241 ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
242 statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval
243 }),
244 ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
245 level: Configuration.getConfigurationData()?.logLevel
246 }),
247 ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
248 console: Configuration.getConfigurationData()?.logConsole
249 }),
250 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
251 format: Configuration.getConfigurationData()?.logFormat
252 }),
253 ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
254 rotate: Configuration.getConfigurationData()?.logRotate
255 }),
256 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
257 maxFiles: Configuration.getConfigurationData()?.logMaxFiles
258 }),
259 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
260 maxSize: Configuration.getConfigurationData()?.logMaxSize
261 })
262 }
263 const logConfiguration: LogConfiguration = {
264 ...defaultLogConfiguration,
265 ...deprecatedLogConfiguration,
266 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
267 Configuration.getConfigurationData()?.log)
268 }
269 return logConfiguration
270 }
271
272 private static buildWorkerSection (): WorkerConfiguration {
273 const defaultWorkerConfiguration: WorkerConfiguration = {
274 processType: WorkerProcessType.workerSet,
275 startDelay: DEFAULT_WORKER_START_DELAY,
276 elementsPerWorker: 'auto',
277 elementStartDelay: DEFAULT_ELEMENT_START_DELAY,
278 poolMinSize: DEFAULT_POOL_MIN_SIZE,
279 poolMaxSize: DEFAULT_POOL_MAX_SIZE
280 }
281 const deprecatedWorkerConfiguration: WorkerConfiguration = {
282 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerProcess') && {
283 processType: Configuration.getConfigurationData()?.workerProcess
284 }),
285 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerStartDelay') && {
286 startDelay: Configuration.getConfigurationData()?.workerStartDelay
287 }),
288 ...(hasOwnProp(Configuration.getConfigurationData(), 'chargingStationsPerWorker') && {
289 elementsPerWorker: Configuration.getConfigurationData()?.chargingStationsPerWorker
290 }),
291 ...(hasOwnProp(Configuration.getConfigurationData(), 'elementStartDelay') && {
292 elementStartDelay: Configuration.getConfigurationData()?.elementStartDelay
293 }),
294 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMinSize') && {
295 poolMinSize: Configuration.getConfigurationData()?.workerPoolMinSize
296 }),
297 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMaxSize') && {
298 poolMaxSize: Configuration.getConfigurationData()?.workerPoolMaxSize
299 })
300 }
301 hasOwnProp(Configuration.getConfigurationData(), 'workerPoolStrategy') &&
302 delete Configuration.getConfigurationData()?.workerPoolStrategy
303 const workerConfiguration: WorkerConfiguration = {
304 ...defaultWorkerConfiguration,
305 ...deprecatedWorkerConfiguration,
306 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.worker) &&
307 Configuration.getConfigurationData()?.worker)
308 }
309 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
310 checkWorkerProcessType(workerConfiguration.processType!)
311 checkWorkerElementsPerWorker(workerConfiguration.elementsPerWorker)
312 return workerConfiguration
313 }
314
315 private static checkDeprecatedConfigurationKeys (): void {
316 // connection timeout
317 Configuration.warnDeprecatedConfigurationKey(
318 'autoReconnectTimeout',
319 undefined,
320 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
321 )
322 Configuration.warnDeprecatedConfigurationKey(
323 'connectionTimeout',
324 undefined,
325 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
326 )
327 // connection retries
328 Configuration.warnDeprecatedConfigurationKey(
329 'autoReconnectMaxRetries',
330 undefined,
331 'Use it in charging station template instead'
332 )
333 // station template url(s)
334 Configuration.warnDeprecatedConfigurationKey(
335 'stationTemplateURLs',
336 undefined,
337 "Use 'stationTemplateUrls' instead"
338 )
339 Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData] !=
340 null &&
341 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
342 (Configuration.getConfigurationData()!.stationTemplateUrls =
343 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
344 Configuration.getConfigurationData()![
345 'stationTemplateURLs' as keyof ConfigurationData
346 ] as StationTemplateUrl[])
347 Configuration.getConfigurationData()?.stationTemplateUrls.forEach(
348 (stationTemplateUrl: StationTemplateUrl) => {
349 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
350 if (stationTemplateUrl['numberOfStation' as keyof StationTemplateUrl] != null) {
351 console.error(
352 `${chalk.green(logPrefix())} ${chalk.red(
353 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
354 )}`
355 )
356 }
357 }
358 )
359 // supervision url(s)
360 Configuration.warnDeprecatedConfigurationKey(
361 'supervisionURLs',
362 undefined,
363 "Use 'supervisionUrls' instead"
364 )
365 // supervision urls distribution
366 Configuration.warnDeprecatedConfigurationKey(
367 'distributeStationToTenantEqually',
368 undefined,
369 "Use 'supervisionUrlDistribution' instead"
370 )
371 Configuration.warnDeprecatedConfigurationKey(
372 'distributeStationsToTenantsEqually',
373 undefined,
374 "Use 'supervisionUrlDistribution' instead"
375 )
376 // worker section
377 Configuration.warnDeprecatedConfigurationKey(
378 'useWorkerPool',
379 undefined,
380 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
381 )
382 Configuration.warnDeprecatedConfigurationKey(
383 'workerProcess',
384 undefined,
385 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
386 )
387 Configuration.warnDeprecatedConfigurationKey(
388 'workerStartDelay',
389 undefined,
390 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`
391 )
392 Configuration.warnDeprecatedConfigurationKey(
393 'chargingStationsPerWorker',
394 undefined,
395 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
396 )
397 Configuration.warnDeprecatedConfigurationKey(
398 'elementStartDelay',
399 undefined,
400 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`
401 )
402 Configuration.warnDeprecatedConfigurationKey(
403 'workerPoolMinSize',
404 undefined,
405 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
406 )
407 Configuration.warnDeprecatedConfigurationKey(
408 'workerPoolSize',
409 undefined,
410 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
411 )
412 Configuration.warnDeprecatedConfigurationKey(
413 'workerPoolMaxSize',
414 undefined,
415 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
416 )
417 Configuration.warnDeprecatedConfigurationKey(
418 'workerPoolStrategy',
419 undefined,
420 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
421 )
422 Configuration.warnDeprecatedConfigurationKey(
423 'poolStrategy',
424 ConfigurationSection.worker,
425 'Not publicly exposed to end users'
426 )
427 if (
428 Configuration.getConfigurationData()?.worker?.processType ===
429 ('staticPool' as WorkerProcessType)
430 ) {
431 console.error(
432 `${chalk.green(logPrefix())} ${chalk.red(
433 `Deprecated configuration 'staticPool' value usage in worker section 'processType' field. Use '${WorkerProcessType.fixedPool}' value instead`
434 )}`
435 )
436 }
437 // log section
438 Configuration.warnDeprecatedConfigurationKey(
439 'logEnabled',
440 undefined,
441 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
442 )
443 Configuration.warnDeprecatedConfigurationKey(
444 'logFile',
445 undefined,
446 `Use '${ConfigurationSection.log}' section to define the log file instead`
447 )
448 Configuration.warnDeprecatedConfigurationKey(
449 'logErrorFile',
450 undefined,
451 `Use '${ConfigurationSection.log}' section to define the log error file instead`
452 )
453 Configuration.warnDeprecatedConfigurationKey(
454 'logConsole',
455 undefined,
456 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
457 )
458 Configuration.warnDeprecatedConfigurationKey(
459 'logStatisticsInterval',
460 undefined,
461 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
462 )
463 Configuration.warnDeprecatedConfigurationKey(
464 'logLevel',
465 undefined,
466 `Use '${ConfigurationSection.log}' section to define the log level instead`
467 )
468 Configuration.warnDeprecatedConfigurationKey(
469 'logFormat',
470 undefined,
471 `Use '${ConfigurationSection.log}' section to define the log format instead`
472 )
473 Configuration.warnDeprecatedConfigurationKey(
474 'logRotate',
475 undefined,
476 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
477 )
478 Configuration.warnDeprecatedConfigurationKey(
479 'logMaxFiles',
480 undefined,
481 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
482 )
483 Configuration.warnDeprecatedConfigurationKey(
484 'logMaxSize',
485 undefined,
486 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
487 )
488 // performanceStorage section
489 Configuration.warnDeprecatedConfigurationKey(
490 'URI',
491 ConfigurationSection.performanceStorage,
492 "Use 'uri' instead"
493 )
494 // uiServer section
495 if (hasOwnProp(Configuration.getConfigurationData(), 'uiWebSocketServer')) {
496 console.error(
497 `${chalk.green(logPrefix())} ${chalk.red(
498 `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`
499 )}`
500 )
501 }
502 }
503
504 private static warnDeprecatedConfigurationKey (
505 key: string,
506 sectionName?: string,
507 logMsgToAppend = ''
508 ): void {
509 if (
510 sectionName != null &&
511 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] != null &&
512 (
513 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] as Record<
514 string,
515 unknown
516 >
517 )[key] != null
518 ) {
519 console.error(
520 `${chalk.green(logPrefix())} ${chalk.red(
521 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
522 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
523 }`
524 )}`
525 )
526 } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) {
527 console.error(
528 `${chalk.green(logPrefix())} ${chalk.red(
529 `Deprecated configuration key '${key}' usage${
530 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
531 }`
532 )}`
533 )
534 }
535 }
536
537 private static getConfigurationData (): ConfigurationData | undefined {
538 if (Configuration.configurationData == null) {
539 try {
540 Configuration.configurationData = JSON.parse(
541 readFileSync(Configuration.configurationFile, 'utf8')
542 ) as ConfigurationData
543 if (Configuration.configurationFileWatcher == null) {
544 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher()
545 }
546 } catch (error) {
547 handleFileException(
548 Configuration.configurationFile,
549 FileType.Configuration,
550 error as NodeJS.ErrnoException,
551 logPrefix()
552 )
553 }
554 }
555 return Configuration.configurationData
556 }
557
558 private static getConfigurationFileWatcher (): FSWatcher | undefined {
559 try {
560 return watch(Configuration.configurationFile, (event, filename): void => {
561 if (
562 !Configuration.configurationFileReloading &&
563 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
564 filename!.trim().length > 0 &&
565 event === 'change'
566 ) {
567 Configuration.configurationFileReloading = true
568 const consoleWarnOnce = once(console.warn, this)
569 consoleWarnOnce(
570 `${chalk.green(logPrefix())} ${chalk.yellow(
571 `${FileType.Configuration} ${this.configurationFile} file have changed, reload`
572 )}`
573 )
574 delete Configuration.configurationData
575 Configuration.configurationSectionCache.clear()
576 if (Configuration.configurationChangeCallback != null) {
577 Configuration.configurationChangeCallback()
578 .catch(error => {
579 throw typeof error === 'string' ? new Error(error) : error
580 })
581 .finally(() => {
582 Configuration.configurationFileReloading = false
583 })
584 } else {
585 Configuration.configurationFileReloading = false
586 }
587 }
588 })
589 } catch (error) {
590 handleFileException(
591 Configuration.configurationFile,
592 FileType.Configuration,
593 error as NodeJS.ErrnoException,
594 logPrefix()
595 )
596 }
597 }
598 }