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