UNPKG

@cere/rob-cli

Version:

CLI tool for deploying and managing rafts and data sources

302 lines 13.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.deployDataSourceCommand = void 0; const ora_1 = __importDefault(require("ora")); const api_client_1 = require("../lib/api-client"); const yaml_loader_1 = require("../lib/yaml-loader"); const logger_1 = require("../lib/logger"); const lodash_1 = __importDefault(require("lodash")); const logger = new logger_1.Logger('DataSourceCommand'); /** * Normalize an object for comparison by removing server-generated fields * and ensuring consistent field naming (for example config/configuration) */ function normalizeForComparison(obj) { if (!obj) return obj; // Deep clone the object to avoid mutating the original const normalized = JSON.parse(JSON.stringify(obj)); // Recursively clean up and normalize objects const processObject = (obj) => { if (!obj || typeof obj !== 'object') return; // Remove server-generated and irrelevant fields delete obj.createdAt; delete obj.updatedAt; delete obj.jsCode; delete obj.__v; delete obj.version; // Handle dates for (const key in obj) { // Convert Date objects to ISO strings if (obj[key] instanceof Date) { obj[key] = obj[key].toISOString(); } // Recursively process nested objects else if (obj[key] && typeof obj[key] === 'object') { processObject(obj[key]); } } }; processObject(normalized); // Handle config/configuration fields consistency - make sure we use 'configuration' consistently if (normalized.config && !normalized.configuration) { normalized.configuration = normalized.config; delete normalized.config; } // For data sources, remove sensitive fields and environment variables if (normalized.configuration) { for (const key of Object.keys(normalized.configuration)) { const value = normalized.configuration[key]; // Skip environment variables if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) { delete normalized.configuration[key]; } // Skip sensitive information fields if (key.toLowerCase().includes('password') || key.toLowerCase().includes('secret') || key.toLowerCase().includes('key') || key.toLowerCase().includes('token')) { delete normalized.configuration[key]; } } // Remove configuration entirely if empty after removing sensitive fields if (Object.keys(normalized.configuration).length === 0) { delete normalized.configuration; } } // Convert empty arrays to undefined for consistent comparison for (const key in normalized) { if (Array.isArray(normalized[key]) && normalized[key].length === 0) { delete normalized[key]; } } return normalized; } /** * Deep equality check for objects */ function isEqual(a, b) { // Normalize objects before comparison const normalizedA = normalizeForComparison(a); const normalizedB = normalizeForComparison(b); // Use lodash isEqual for deep comparison return lodash_1.default.isEqual(normalizedA, normalizedB); } /** * Deploy data sources from a YAML file */ async function deployDataSources(options) { try { const filePath = options.file; const spinner = (0, ora_1.default)('Loading data sources file...').start(); const isDebug = options.debug || false; if (isDebug) { logger.info('Debug mode enabled - will show detailed comparison info'); } // Load and parse the YAML file let dataSources = []; let dataServiceId = options.dataServiceId; try { // Try to load as DataSourcesYaml first const yamlData = (0, yaml_loader_1.loadYamlFile)(filePath); logger.debug(`Loaded YAML data: ${JSON.stringify(yamlData, null, 2)}`); if ('dataSources' in yamlData && Array.isArray(yamlData.dataSources)) { dataSources = yamlData.dataSources; } else if (Array.isArray(yamlData)) { // Handle case where the file just contains an array of data sources dataSources = yamlData; } // Use dataServiceId from file if provided and not overridden by command line if (!dataServiceId && yamlData.dataServiceId) { dataServiceId = yamlData.dataServiceId; } spinner.succeed(`Loaded ${dataSources.length} data sources from ${filePath}`); } catch (error) { spinner.fail(`Failed to load data sources file: ${error instanceof Error ? error.message : 'Unknown error'}`); return; } if (!dataServiceId) { spinner.fail('No dataServiceId provided. Please specify it in the YAML file or via --dataServiceId option.'); return; } if (dataSources.length === 0) { spinner.warn('No data sources found in the file.'); return; } logger.info(`Processing ${dataSources.length} data sources for dataServiceId: ${dataServiceId}`); // Dry run check if (options.dryRun) { spinner.info('Dry run mode - no changes will be made'); logger.info(`Would process ${dataSources.length} data sources for data service: ${dataServiceId}`); for (const ds of dataSources) { logger.info(`Would process data source: ${ds.id} (${ds.type})`); // Log more detailed information logger.info(` - Name: ${ds.name || 'Not specified'}`); logger.info(` - Data service ID: ${ds.dataServiceId || dataServiceId}`); if (ds.configuration) { logger.info(' - Configuration:'); Object.entries(ds.configuration).forEach(([key, value]) => { logger.info(` - ${key}: ${JSON.stringify(value)}`); }); } logger.info(' - Would send POST/PUT request to the API server'); } // Display summary for dry run logger.info('Dry run summary:'); logger.info(`Would have created/updated ${dataSources.length} data sources`); return; } // Track changes for summary let added = 0; let changed = 0; let unchanged = 0; const destroyed = 0; // Process each data source for (const dataSource of dataSources) { const { id, ...dataSourceData } = dataSource; // Add dataServiceId if not present if (!dataSourceData.dataServiceId) { dataSourceData.dataServiceId = dataServiceId; } try { // Get existing data source logger.creating('data_source', id); const startTime = Date.now(); let existingDataSource = null; try { existingDataSource = await api_client_1.DataSourceApi.getDataSourceById(id, dataServiceId); logger.debug(`Found data source with id ${id} in data service ${dataServiceId}`); } catch (error) { if (error.response && error.response.status === 404) { logger.debug(`No existing data source found with id ${id} in data service ${dataServiceId}`); } else { logger.error(`Error fetching data source with id ${id} in data service ${dataServiceId}: ${error.message}`); throw error; } } if (existingDataSource) { // Use direct object comparison instead of JSON string comparison if (isEqual(existingDataSource, { ...dataSourceData, id })) { logger.info(`No changes detected for data source ${id}, skipping update`); if (isDebug) { logger.debug(`Verified all fields for data source ${id}`); } unchanged++; } else { // For debugging only, show details about the differences if (isDebug) { logger.debug(`Comparing data source ${id}:`); // Normalize both objects for comparison const normalizedExisting = normalizeForComparison(existingDataSource); const normalizedNew = normalizeForComparison({ ...dataSourceData, id }); // Convert to sorted JSON strings for detailed comparison const existingJson = JSON.stringify(normalizedExisting, Object.keys(normalizedExisting).sort(), 2); const newJson = JSON.stringify(normalizedNew, Object.keys(normalizedNew).sort(), 2); if (existingJson !== newJson) { logger.debug(`Found differences for data source ${id}`); // Show simple diffs for debugging const existingObj = JSON.parse(existingJson); const newObj = JSON.parse(newJson); // Check basic properties for (const key of Object.keys({ ...existingObj, ...newObj })) { if (JSON.stringify(existingObj[key]) !== JSON.stringify(newObj[key])) { logger.debug(`Difference in ${key}:`); logger.debug(` Existing: ${JSON.stringify(existingObj[key])}`); logger.debug(` New: ${JSON.stringify(newObj[key])}`); } } } else { logger.debug(`No differences found in JSON representation, but isEqual returned false`); } } // Update existing data source with dataServiceId parameter await api_client_1.DataSourceApi.updateDataSource(id, dataSourceData, dataServiceId); const timeTaken = ((Date.now() - startTime) / 1000).toFixed(0); logger.updated('data_source', id, parseInt(timeTaken)); changed++; } } else { // Create new data source await api_client_1.DataSourceApi.createDataSource({ ...dataSourceData, id, }); const timeTaken = ((Date.now() - startTime) / 1000).toFixed(0); logger.created('data_source', id, parseInt(timeTaken)); added++; } } catch (error) { spinner.fail(`Failed to process data source: ${id}. Error: ${error.message}`); throw error; } } // Display summary logger.completionSummary(added, changed, destroyed); } catch (error) { if (error instanceof Error) { logger.error(`Failed to deploy data sources: ${error.message}`); } else { logger.error('Failed to deploy data sources due to an unknown error'); } throw error; } } /** * Command module for deploying data sources */ exports.deployDataSourceCommand = { command: 'deploy data-source <file>', describe: 'Deploy data sources from a YAML file', builder: (yargs) => { return yargs .positional('file', { describe: 'Path to the YAML file containing data sources', type: 'string', demandOption: true, }) .option('dataServiceId', { describe: 'ID of the data service to associate with the data sources', type: 'string', }) .option('dryRun', { describe: 'Validate and process the file without making any changes', type: 'boolean', default: false, }) .option('debug', { describe: 'Enable detailed comparison information', type: 'boolean', default: false, }) .example('$0 deploy data-source ./data-sources.yaml', 'Deploy data sources from data-sources.yaml file') .example('$0 deploy data-source ./data-sources.yaml --dataServiceId 123', 'Deploy data sources with specific dataServiceId'); }, handler: async (argv) => { try { await deployDataSources({ file: argv.file, dataServiceId: argv.dataServiceId, dryRun: argv.dryRun, debug: argv.debug, }); } catch (error) { process.exit(1); } }, }; //# sourceMappingURL=data-source.js.map