@cere/rob-cli
Version:
CLI tool for deploying and managing rafts and data sources
431 lines • 18 kB
JavaScript
;
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