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