UNPKG

@cere/rob-cli

Version:

CLI tool for deploying and managing rafts and data sources

431 lines 18 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.deployRaftCommand = void 0; const path_1 = __importDefault(require("path")); 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 script_handler_1 = require("../lib/script-handler"); const chalk_1 = __importDefault(require("chalk")); const lodash_1 = __importDefault(require("lodash")); const logger = new logger_1.Logger('RaftCommand'); // Add utility functions /** * Check if chalk should be used (always returns true in this implementation) */ function useChalk() { return true; } /** * 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); } /** * Normalize an object for comparison by removing server-generated fields */ function normalizeForComparison(obj) { if (!obj) return obj; // Deep clone the object to avoid mutating the original const normalized = JSON.parse(JSON.stringify(obj)); // Recursively process and normalize objects const processObject = (obj) => { if (!obj || typeof obj !== 'object') return; // Always ignore these fields for comparison delete obj.createdAt; delete obj.updatedAt; delete obj.jsCode; delete obj.__v; delete obj.version; // For script objects, ignore their id as they're often regenerated if (obj.tsCode !== undefined || obj.name !== undefined) { delete obj.id; // Ignore script IDs } // Process nested arrays if (Array.isArray(obj)) { obj.forEach(item => processObject(item)); return; } // Process nested objects for (const key in obj) { if (obj[key] && typeof obj[key] === 'object') { processObject(obj[key]); } } }; processObject(normalized); // For raft-specific normalization if (normalized.indexingScript) { delete normalized.indexingScript.id; } if (normalized.queryOperations && Array.isArray(normalized.queryOperations)) { normalized.queryOperations.forEach((op) => { if (op.script) { delete op.script.id; } }); } // 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; } /** * Deploy rafts from a YAML file */ async function deployRafts(options) { var _a; try { const filePath = options.file; const spinner = (0, ora_1.default)('Loading rafts file...').start(); const isDebug = options.debug || false; if (isDebug) { logger.info('Debug mode enabled - will show detailed comparison info'); } // Get directory of file for relative path resolution const fileDir = path_1.default.dirname(path_1.default.resolve(process.cwd(), filePath)); // Load and parse the YAML file let rafts = []; let dataServiceId = options.dataServiceId; try { // Try to load as RaftsYaml first const yamlData = (0, yaml_loader_1.loadYamlFile)(filePath); if ('rafts' in yamlData && Array.isArray(yamlData.rafts)) { rafts = yamlData.rafts; } else if (Array.isArray(yamlData)) { // Handle case where the file just contains an array of rafts rafts = yamlData; } // Use dataServiceId from file if provided and not overridden by command line if (!dataServiceId && yamlData.dataServiceId) { dataServiceId = yamlData.dataServiceId; } spinner.succeed(`Loaded ${rafts.length} rafts from ${filePath}`); } catch (error) { spinner.fail(`Failed to load rafts 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 (rafts.length === 0) { spinner.warn('No rafts found in the file.'); return; } logger.info(`Processing ${rafts.length} rafts for dataServiceId: ${dataServiceId}`); // Process script files spinner.start('Processing script files...'); for (let i = 0; i < rafts.length; i++) { try { // Ensure dataServiceId is set for each raft if (!rafts[i].dataServiceId) { rafts[i].dataServiceId = dataServiceId; } // Process scripts rafts[i] = (0, script_handler_1.processRaftScripts)(rafts[i], fileDir); } catch (error) { spinner.fail(`Failed to process script for raft ${rafts[i].id}: ${error instanceof Error ? error.message : 'Unknown error'}`); return; } } spinner.succeed('Processed all script files'); // Dry run check if (options.dryRun) { spinner.info('Dry run mode - no changes will be made'); logger.info(`Would process ${rafts.length} rafts for data service: ${dataServiceId}`); for (const raft of rafts) { logger.info(`Would process raft: ${raft.id} (${raft.name})`); // Log more detailed information logger.info(` - Name: ${raft.name || 'Not specified'}`); logger.info(` - Data service ID: ${raft.dataServiceId || dataServiceId}`); logger.info(` - Triggers: ${raft.triggers.length}`); raft.triggers.forEach((trigger, index) => { logger.info(` ${index + 1}. Event pattern: ${trigger.eventPattern}, Enabled: ${trigger.enabled || 'true'}`); }); logger.info(` - Data sources: ${raft.dataSourcesIds.length}`); raft.dataSourcesIds.forEach((id, index) => { logger.info(` ${index + 1}. ID: ${id}`); }); if (raft.queryOperations && raft.queryOperations.length > 0) { logger.info(` - Queries: ${raft.queryOperations.length}`); raft.queryOperations.forEach((op, index) => { logger.info(` ${index + 1}. Name: ${op.name}, Alias: ${op.alias}`); }); } 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 ${rafts.length} rafts`); return; } // Track changes for summary let added = 0; let changed = 0; let unchanged = 0; const destroyed = 0; // Process each raft for (const raft of rafts) { const { id, ...raftData } = raft; // Add dataServiceId if not present if (!raftData.dataServiceId) { raftData.dataServiceId = dataServiceId; } let existingRaft = null; if (id) { // Get existing raft logger.creating('raft', id); const startTime = Date.now(); try { existingRaft = await api_client_1.RaftApi.getRaftById(id, raftData.dataServiceId); logger.debug(`Found raft with id ${id} in data service ${raftData.dataServiceId}`); } catch (error) { if (error.response && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { logger.debug(`No existing raft found with id ${id} in data service ${raftData.dataServiceId}. Proceeding to create a new one.`); } else { logger.error(`Error fetching raft with id ${id} in data service ${raftData.dataServiceId}: ${error.message}`); throw error; } } if (existingRaft) { // Enhanced debugging for raft comparison // Normalize both objects for comparison const normalizedExisting = normalizeForComparison(existingRaft); const normalizedNew = normalizeForComparison({ ...raftData, id }); if (isDebug) { logger.debug('Normalized existing raft:'); console.log(JSON.stringify(normalizedExisting, null, 2)); logger.debug('Normalized new raft:'); console.log(JSON.stringify(normalizedNew, null, 2)); } if (useChalk()) { console.log(chalk_1.default.dim('Comparing normalized objects:')); // Find and log specific differences const differences = findDifferences(normalizedExisting, normalizedNew); if (differences.length > 0) { console.log(chalk_1.default.yellow('Detected differences:')); differences.forEach(diff => { console.log(chalk_1.default.yellow(`- Path: ${diff.path}`)); console.log(chalk_1.default.red(` Existing: ${formatValue(diff.existing)}`)); console.log(chalk_1.default.green(` New: ${formatValue(diff.new)}`)); }); } else { console.log(chalk_1.default.dim('No structural differences found in normalized objects')); } // Compare JSON strings to find first difference const existingJson = JSON.stringify(normalizedExisting, null, 2); const newJson = JSON.stringify(normalizedNew, null, 2); if (existingJson !== newJson) { console.log(chalk_1.default.yellow('String representation differs:')); findFirstStringDifference(existingJson, newJson, console.log); } } // Original comparison logic - directly use isEqual helper if (isEqual(existingRaft, { ...raftData, id })) { logger.info(`No changes detected for raft ${id}, skipping update`); if (isDebug) { logger.debug(`Verified all fields for raft ${id}`); } unchanged++; } else { // Update existing raft with dataServiceId parameter await api_client_1.RaftApi.updateRaft(id, raftData, raftData.dataServiceId); const timeTaken = ((Date.now() - startTime) / 1000).toFixed(0); logger.updated('raft', id, parseInt(timeTaken)); changed++; } } else { // Create new raft try { await api_client_1.RaftApi.createRaft({ ...raftData, id, }); const timeTaken = ((Date.now() - startTime) / 1000).toFixed(0); logger.created('raft', id, parseInt(timeTaken)); added++; } catch (error) { logger.error(`Failed to create raft: ${id}. Error: ${error.message}`); throw error; } } } } // Display summary logger.completionSummary(added, changed, destroyed); } catch (error) { if (error instanceof Error) { logger.error(`Failed to deploy rafts: ${error.message}`); } else { logger.error('Failed to deploy rafts due to an unknown error'); } throw error; } } /** * Command module for deploying rafts */ exports.deployRaftCommand = { command: 'deploy raft <file>', describe: 'Deploy rafts from a YAML file', builder: (yargs) => { return yargs .positional('file', { describe: 'Path to the YAML file containing rafts', type: 'string', demandOption: true, }) .option('dataServiceId', { describe: 'ID of the data service to associate with the rafts', 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 raft ./rafts.yaml', 'Deploy rafts from rafts.yaml file') .example('$0 deploy raft ./rafts.yaml --dataServiceId 123', 'Deploy rafts with specific dataServiceId'); }, handler: async (argv) => { try { await deployRafts({ file: argv.file, dataServiceId: argv.dataServiceId, dryRun: argv.dryRun, debug: argv.debug, }); } catch (error) { process.exit(1); } }, }; /** * Find all differences between two objects and return them as an array */ function findDifferences(obj1, obj2, path = '') { const differences = []; // Handle null/undefined cases if (obj1 === null && obj2 !== null) { return [{ path, existing: null, new: obj2 }]; } if (obj1 !== null && obj2 === null) { return [{ path, existing: obj1, new: null }]; } // Handle type mismatches if (typeof obj1 !== typeof obj2) { return [{ path, existing: obj1, new: obj2 }]; } // Handle primitive types if (typeof obj1 !== 'object' || obj1 === null) { if (obj1 !== obj2) { return [{ path, existing: obj1, new: obj2 }]; } return []; } // Handle arrays if (Array.isArray(obj1) && Array.isArray(obj2)) { if (obj1.length !== obj2.length) { return [{ path, existing: `Array(${obj1.length})`, new: `Array(${obj2.length})` }]; } // Compare array items for (let i = 0; i < obj1.length; i++) { const nestedPath = path ? `${path}[${i}]` : `[${i}]`; differences.push(...findDifferences(obj1[i], obj2[i], nestedPath)); } return differences; } // Handle objects const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]); for (const key of allKeys) { const nestedPath = path ? `${path}.${key}` : key; // Check for keys that exist in one object but not the other if (!(key in obj1)) { differences.push({ path: nestedPath, existing: 'undefined', new: obj2[key] }); continue; } if (!(key in obj2)) { differences.push({ path: nestedPath, existing: obj1[key], new: 'undefined' }); continue; } // Recursively compare nested objects differences.push(...findDifferences(obj1[key], obj2[key], nestedPath)); } return differences; } /** * Format a value for display in logs */ function formatValue(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'object') { try { return JSON.stringify(value); } catch (e) { return typeof value; } } return String(value); } /** * Find and display the first difference between two JSON strings */ function findFirstStringDifference(str1, str2, logFn) { const minLength = Math.min(str1.length, str2.length); for (let i = 0; i < minLength; i++) { if (str1[i] !== str2[i]) { // Get context around the difference (up to 20 chars before and after) const start = Math.max(0, i - 20); const end1 = Math.min(str1.length, i + 20); const end2 = Math.min(str2.length, i + 20); const context1 = str1.substring(start, end1); const context2 = str2.substring(start, end2); logFn(chalk_1.default.yellow(`Strings differ at position ${i}:`)); logFn(chalk_1.default.red(`Existing: ${context1}`)); logFn(chalk_1.default.green(`New: ${context2}`)); logFn(chalk_1.default.yellow(`Character difference: '${str1[i] || ''}' vs '${str2[i] || ''}'`)); return; } } if (str1.length !== str2.length) { logFn(chalk_1.default.yellow(`Strings have different lengths: ${str1.length} vs ${str2.length}`)); logFn(chalk_1.default.yellow(`One string is a substring of the other`)); } } //# sourceMappingURL=raft.js.map