/**
* Configuration File Service Layer
*
* Handles business logic for configuration file management.
* This service manages YAML configuration files for different applications
* in the CommTool system, including validation, storage, and real-time updates.
*
* @module ConfigService
*/
import _ from 'lodash';
import { busboyUpload } from '../../utils/uploadBusboy.js';
import { s3, myMinioClient } from '../../utils/s3Client.js';
import { configs, readConfig, remergeConfig } from '../../utils/compileTemplates.js';
import { publishEvent } from '../../utils/events.js';
import { updateConfig } from '../../server.ws.js';
// Configuration constants
const BUCKET = process.env.bucket || 'kpe20';
/**
* Generates the S3 key for uploaded config files
* Files are initially uploaded to 'confignew' for validation
*
* @param {Object} params - Upload parameters
* @returns {string} S3 key path
*/
const generateConfigUploadKey = ({ fields, filename, extension, params }) => {
return `confignew/${params.app}/${filename.filename}`;
};
/**
* Processes config file after successful upload and validation
*
* @param {string} uploadedKey - S3 key of uploaded file
* @param {string} app - Application name
* @param {string} configUID - Configuration UID from parsed YAML
* @returns {Promise<Object>} Processing result
*/
const processValidatedConfig = async (uploadedKey, app, configUID) => {
try {
const finalKey = `config/${app}/${configUID}.yaml`;
// Remove existing config file (if any)
try {
await myMinioClient.removeObject(BUCKET, finalKey);
console.log(`Removed old config file: ${finalKey}`);
} catch (removeError) {
// File might not exist, continue processing
console.log(`No existing config file to remove: ${finalKey}`);
}
// Copy validated file to final location
await myMinioClient.copyObject(
BUCKET,
finalKey,
`/${BUCKET}/${uploadedKey}`
);
// Clean up temporary upload
await myMinioClient.removeObject(BUCKET, uploadedKey);
return { success: true };
} catch (error) {
console.error('Error processing validated config:', error);
throw new Error(`Failed to process config file: ${error.message}`);
}
};
/**
* Triggers config updates across applications in the ui
*
* @param {string} app - Application that was updated
* @param {string} configUID - Configuration UID
*/
export const triggerConfigUpdates = (app, configUID) => {
try {
if (app === 'admin') {
// Admin configs affect all other apps
const currentApps = process.env.apps
.split('|')
.filter(appName => appName !== 'admin');
currentApps.forEach(appName => {
remergeConfig(appName, configUID);
updateConfig(appName, configUID);
publishEvent(`/config/${appName}/${configUID}`, { organization: configUID, data: { UID: configUID } });
});
updateConfig('admin', configUID);
publishEvent(`/config/admin/${configUID}`, { organization: configUID, data: { UID: configUID } });
} else {
// Regular app config update
remergeConfig(app, configUID);
updateConfig(app, configUID);
publishEvent(`/config/${app}/${configUID}`, { organization: configUID, data: { UID: configUID } });
}
} catch (error) {
console.error('Error triggering config updates:', error);
throw error;
}
};
/**
* Configuration File Service
*/
export const configService = {
/**
* Upload and validate a configuration file
*
* @param {Object} req - Express request object
* @param {string} app - Application name
* @returns {Promise<Object>} Upload result
*/
async uploadAndValidateConfig(req, app) {
try {
const uploadedFileData = await busboyUpload(req, {
s3,
uploadUrlGen: generateConfigUploadKey,
Bucket: BUCKET
});
if (uploadedFileData.length === 0) {
return {
success: false,
message: 'No file uploaded'
};
}
const filename = uploadedFileData[0].key;
const result = await readConfig(filename, app);
if (!result.success) {
console.log('Error in config file upload or YAML parsing:', result);
// Clean up invalid file
try {
await myMinioClient.removeObject(BUCKET, filename);
} catch (cleanupError) {
console.error('Error cleaning up invalid file:', cleanupError);
}
return result;
}
console.log('Config file upload and YAML parsing successful');
const configUID = result.config.UID;
await processValidatedConfig(filename, app, configUID);
triggerConfigUpdates(app, configUID);
const config = result.config;
// publish config change event
const timestamp = Math.floor(Date.now() / 1000);
return result;
} catch (error) {
console.error('Error in uploadAndValidateConfig:', error);
throw error;
}
},
/**
* Get a configuration file for download
*
* @param {string} app - Application name
* @param {string} UID - Configuration UID
* @returns {Promise<Object>} File stream result
*/
async getConfigFile(app, UID) {
return new Promise((resolve, reject) => {
const configPath = `config/${app}/${UID}.yaml`;
myMinioClient.getObject(BUCKET, configPath, (err, data) => {
if (err) {
console.error('Error retrieving config file:', err);
resolve({
success: false,
message: 'Configuration file not found'
});
} else {
resolve({
success: true,
stream: data
});
}
});
});
},
/**
* Get list of configuration files for an application
*
* @param {string} app - Application name
* @returns {Promise<Object>} List of configuration files
*/
async getConfigList(app) {
try {
const myConfigs = configs[app];
if (!myConfigs || Object.keys(myConfigs).length === 0) {
return {
success: false,
files: [],
message: 'No configs found. Did you supply a valid app? Have there been already uploaded configs for this app?'
};
}
const configFiles = Object.entries(myConfigs)
.filter(([key, value]) => value !== null)
.map(([key, value]) => _.merge({}, configs['db'][key], value));
return {
success: true,
files: configFiles
};
} catch (error) {
console.error('Error in getConfigList:', error);
throw error;
}
},
/**
* Delete a configuration file
*
* @param {string} app - Application name
* @param {string} UID - Configuration UID
* @returns {Promise<Object>} Deletion result
*/
async deleteConfigFile(app, UID) {
try {
const configPath = `config/${app}/${UID}.yaml`;
const result = await myMinioClient.removeObject(BUCKET, configPath);
// Trigger config updates after deletion
triggerConfigUpdates(app, UID);
return {
success: true,
result,
message: `Configuration file ${UID} deleted successfully`
};
} catch (error) {
console.error('Error in deleteConfigFile:', error);
throw error;
}
}
};