@citrineos/base
Version:
The base module for OCPP v2.0.1 including all interfaces. This module is not intended to be used directly, but rather as a dependency for other modules.
138 lines • 5.8 kB
JavaScript
// Copyright (c) 2023 S44, LLC
// Copyright Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache 2.0
import { z } from 'zod';
import { systemConfigSchema } from './types';
const args = typeof process !== 'undefined' && process.argv ? process.argv.slice(2) : [];
let dynamicPrefix = 'citrineos_';
for (const arg of args) {
if (arg.startsWith('--env-prefix=')) {
dynamicPrefix = arg.split('=')[1].toLowerCase();
break;
}
}
const CITRINE_ENV_VAR_PREFIX = dynamicPrefix;
/**
* Finds a case-insensitive match for a key in an object.
* @param obj The object to search.
* @param targetKey The target key.
* @returns The matching key or undefined.
*/
function findCaseInsensitiveMatch(obj, targetKey) {
const lowerTargetKey = targetKey.toLowerCase();
return Object.keys(obj).find((key) => key.toLowerCase() === lowerTargetKey);
}
const getZodSchemaKeyMap = (schema) => {
var _a;
if (schema instanceof z.ZodEffects) {
return getZodSchemaKeyMap((_a = schema._def) === null || _a === void 0 ? void 0 : _a.schema);
}
if (schema instanceof z.ZodNullable || schema instanceof z.ZodOptional) {
return getZodSchemaKeyMap(schema.unwrap());
}
if (schema instanceof z.ZodArray) {
return getZodSchemaKeyMap(schema.element);
}
if (schema instanceof z.ZodObject) {
const entries = Object.entries(schema.shape);
return entries.reduce((acc, [key, value]) => {
const nested = getZodSchemaKeyMap(value);
if (Object.keys(nested).length > 0) {
acc[key] = nested;
}
else {
acc[key.toLowerCase()] = key;
}
return acc;
}, {});
}
return {};
};
/**
* Merges configuration from environment variables into the default configuration. Allows any to keep it as generic as possible.
* @param defaultConfig The default configuration.
* @param envVars The environment variables.
* @returns The merged configuration.
*/
function mergeConfigFromEnvVars(defaultConfig, envVars, configKeyMap) {
const config = Object.assign({}, defaultConfig);
const errors = [];
for (const [fullEnvKey, value] of Object.entries(envVars)) {
if (!value) {
continue;
}
const lowercaseEnvKey = fullEnvKey.toLowerCase();
if (lowercaseEnvKey.startsWith(CITRINE_ENV_VAR_PREFIX)) {
const envKeyWithoutPrefix = lowercaseEnvKey.substring(CITRINE_ENV_VAR_PREFIX.length);
const path = envKeyWithoutPrefix.split('_');
let currentConfigPart = config;
let currentConfigKeyMap = configKeyMap;
let validMapping = true;
for (let i = 0; i < path.length - 1; i++) {
const part = path[i];
const matchingKey = findCaseInsensitiveMatch(currentConfigKeyMap, part);
if (!matchingKey) {
errors.push(`Environment variable '${fullEnvKey}' refers to unknown configuration segment '${part}'.`);
validMapping = false;
break;
}
if (currentConfigPart[matchingKey] === undefined) {
currentConfigPart[matchingKey] = {};
}
else if (typeof currentConfigPart[matchingKey] !== 'object' ||
currentConfigPart[matchingKey] === null) {
errors.push(`Environment variable '${fullEnvKey}' refers to configuration segment '${part}', but its current value is not an object.`);
validMapping = false;
break;
}
currentConfigPart = currentConfigPart[matchingKey];
currentConfigKeyMap = currentConfigKeyMap[matchingKey];
}
if (!validMapping) {
continue;
}
const finalPart = path[path.length - 1];
const keyToUse = currentConfigKeyMap[finalPart.toLowerCase()] || finalPart;
try {
currentConfigPart[keyToUse] = JSON.parse(value);
}
catch (_a) {
console.debug(`Mapping '${value}' as string for environment variable '${fullEnvKey}'.`);
currentConfigPart[keyToUse] = value;
}
}
}
if (errors.length > 0) {
errors.forEach((err) => console.error(err));
throw new Error(`Configuration errors: ${errors.join('; ')}`);
}
return config;
}
/**
* Validates the system configuration to ensure required properties are set.
* @param finalConfig The final system configuration.
* @throws Error if required properties are not set.
*/
function validateFinalConfig(finalConfig) {
if (!finalConfig.data.sequelize.username) {
throw new Error('CITRINEOS_DATA_SEQUELIZE_USERNAME must be set if username not provided in config');
}
if (!finalConfig.data.sequelize.password) {
throw new Error('CITRINEOS_DATA_SEQUELIZE_PASSWORD must be set if password not provided in config');
}
}
/**
* Defines the application configuration by merging input configuration which is defined in a file with environment variables.
* Takes environment variables over predefined
* @param inputConfig The file defined input configuration.
* @returns The final system configuration.
* @throws Error if required environment variables are not set or if there are parsing errors.
*/
export function defineConfig(inputConfig) {
const configKeyMap = getZodSchemaKeyMap(systemConfigSchema);
const appConfig = mergeConfigFromEnvVars(inputConfig, process.env, configKeyMap);
validateFinalConfig(appConfig);
return systemConfigSchema.parse(appConfig);
}
//# sourceMappingURL=defineConfig.js.map