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