mavensmate
Version:
Core APIs that drive MavensMate IDEs for Salesforce1/Force.com
446 lines (405 loc) • 14.7 kB
JavaScript
/**
* @file Responsible for compilation of Salesforce metadata (Custom Objects, Apex Classes, Lightning files, etc.)
* @author Joseph Ferraro <@joeferraro>
*/
;
var Promise = require('bluebird');
var _ = require('lodash');
var logger = require('winston');
var temp = require('temp');
var config = require('../../config');
var LightningService = require('./lightning');
var MetadataHelper = require('../metadata').MetadataHelper;
var Deploy = require('./deploy');
var fs = require('fs-extra');
var mavensMateFile = require('../file');
var config = require('../../config');
var moment = require('moment');
var Package = require('../package').Package;
/**
* Responsible for compiling local copies of files/directories
* @param {Project} project - project instance (required)
* @param {Array} paths - array of path strings [ 'foo/bar/src/classes', 'foo/bar/src/pages/foo.page' ]
*/
var CompileDelegate = function(project, paths, force) {
if (!project || !paths) {
throw new Error('CompileDelegate requires a valid project instance and an array of paths to compile.');
}
this.project = project;
this.paths = paths;
this.force = force;
this.metadataHelper = new MetadataHelper({ sfdcClient : this.project.sfdcClient });
};
// we standardize the result output to match that of the metadata api compilation/deploy response
//
// { checkOnly: false,
// completedDate: '2015-01-19T23:55:03.000Z',
// createdBy: '005o0000000TB1i',
// createdByName: 'Joseph Ferraro',
// createdDate: '2015-01-19T23:55:03.000Z',
// details:
// { componentSuccesses: [ [Object], [Object], [Object] ],
// runTestResult: { numFailures: '0', numTestsRun: '0', totalTime: '0.0' },
// componentFailures: [] },
// done: true,
// id: '0Afo000000Ft2NbCAJ',
// ignoreWarnings: false,
// lastModifiedDate: '2015-01-19T23:55:03.000Z',
// numberComponentErrors: 0,
// numberComponentsDeployed: 2,
// numberComponentsTotal: 2,
// numberTestErrors: 0,
// numberTestsCompleted: 0,
// numberTestsTotal: 0,
// rollbackOnError: true,
// runTestsEnabled: 'false',
// startDate: '2015-01-19T23:55:03.000Z',
// status: 'Succeeded',
// success: true }
/**
* Executes local and server compile for all delegate paths
* @return {Promise}
*/
CompileDelegate.prototype.execute = function() {
// TODO: implement stash
var self = this;
return new Promise(function(resolve, reject) {
self._performCompile()
.then(function(compileResults) {
resolve(self._flattenResults(compileResults));
})
.catch(function(err) {
reject(err);
});
});
};
CompileDelegate.prototype._flattenResults = function(compileResults) {
var result = {
checkOnly: false,
completedDate: '',
createdBy: '',
createdByName: '',
createdDate: '',
details:
{ componentSuccesses: [],
runTestResult: { numFailures: '0', numTestsRun: '0', totalTime: '0.0' },
componentFailures: []
},
done: false,
id: '',
ignoreWarnings: false,
lastModifiedDate: '',
numberComponentErrors: 0,
numberComponentsDeployed: 0,
numberComponentsTotal: 0,
numberTestErrors: 0,
numberTestsCompleted: 0,
numberTestsTotal: 0,
rollbackOnError: false,
runTestsEnabled: 'false',
startDate: '',
status: '',
success: true };
logger.debug('flattening results: ');
_.each(compileResults, function(res) {
logger.debug('compile result:');
logger.debug(res);
if (_.has(res, 'runTestsEnabled')) {
// this is metadata api result
result = res;
} else if (_.has(res, 'hasConflict') && !res.success) {
result.details.conflicts = res.conflicts;
result.status = 'Conflict';
result.success = false;
result.done = true;
} else if (_.isArray(res)) {
if (!result.details.componentFailures) {
result.details.componentFailures = [];
}
_.each(res, function(r) {
if (_.has(r, 'State')) {
// tooling result
if (r.State === 'Completed') {
result.numberComponentsDeployed++;
result.details.componentSuccesses.push(r);
} else {
result.numberComponentErrors++;
result.details.componentFailures.push(r);
result.success = false;
}
}
});
}
});
logger.debug('compile results: ');
logger.debug(JSON.stringify(result));
return result;
};
CompileDelegate.prototype._performCompile = function() {
var self = this;
return new Promise(function(resolve, reject) {
if (self.paths.length === 0) {
return resolve();
}
logger.debug('compiling paths: ');
logger.debug(self.paths);
var compileWithToolingApi = config.get('mm_compile_with_tooling_api');
var files = mavensMateFile.createFileInstances(self.paths, self.project);
var lightningBundleItemFiles = mavensMateFile.getLightningBundleItemFiles(files);
var toolingFiles = mavensMateFile.getToolingFiles(files, !compileWithToolingApi);
var metadataApiFiles = mavensMateFile.getMetadataApiFiles(files, compileWithToolingApi);
var compileSubscription = mavensMateFile.createPackageSubscription(files, self.project.packageXml, compileWithToolingApi);
var compilePromises = [];
if (metadataApiFiles.length > 0) {
compilePromises.push(self._compileGenericMetadata(metadataApiFiles, compileSubscription));
}
if (toolingFiles.length > 0) {
if (config.get('mm_legacy_compile')) {
compilePromises.push(self._compileToolingFilesLegacy(toolingFiles));
} else {
compilePromises.push(self._compileToolingFiles(toolingFiles));
}
}
if (lightningBundleItemFiles.length > 0) {
compilePromises.push(self._compileLightningBundleItemFiles(lightningBundleItemFiles));
}
Promise.all(compilePromises)
.then(function(res) {
resolve(res);
})
.catch(function(err) {
// TODO: revert via stash
reject(err);
});
});
};
CompileDelegate.prototype._compileToolingFilesLegacy = function(toolingFiles) {
var self = this;
return new Promise(function(resolve, reject) {
logger.debug('compiling via tooling api, using legacy method (metadata api retrieve upon successul compilation)');
var toolingFilesPayload = [];
_.each(toolingFiles, function(tf) {
if (tf.isDirectory) {
// get directory contents
toolingFilesPayload = toolingFilesPayload.concat(tf.localMembers);
} else {
toolingFilesPayload.push(tf);
}
});
var compileResult;
var retrievePackage;
self._checkConflicts(toolingFilesPayload)
.then(function(conflictCheckResult) {
if (conflictCheckResult.hasConflict) {
return resolve(conflictCheckResult);
} else {
self.project.sfdcClient.compileWithToolingApi(toolingFilesPayload, self.project)
.then(function(result) {
logger.debug('compile via tooling api result: ');
logger.debug(result);
compileResult = result;
retrievePackage = new Package({ subscription: mavensMateFile.createPackageSubscription(toolingFilesPayload) });
return retrievePackage.init();
})
.then(function() {
return self.project.sfdcClient.retrieveUnpackaged(retrievePackage.subscription);
})
.then(function(retrieveResult) {
return self.project.updateLocalStore(retrieveResult.fileProperties);
})
.then(function() {
resolve(compileResult);
})
.catch(function(error) {
reject(error);
})
.done();
}
})
.catch(function(error) {
reject(error);
})
.done();
});
};
CompileDelegate.prototype._compileToolingFiles = function(toolingFiles) {
var self = this;
return new Promise(function(resolve, reject) {
logger.debug('compiling via tooling api');
var toolingFilesPayload = [];
_.each(toolingFiles, function(tf) {
if (tf.isDirectory) {
// get directory contents
toolingFilesPayload = toolingFilesPayload.concat(tf.localMembers);
} else {
toolingFilesPayload.push(tf);
}
});
var compileResult;
var retrievePackage;
self._checkConflicts(toolingFilesPayload)
.then(function(conflictCheckResult) {
if (conflictCheckResult.hasConflict) {
return resolve(conflictCheckResult);
} else {
self.project.sfdcClient.compileWithToolingApi(toolingFilesPayload, self.project)
.then(function(result) {
logger.debug('compile via tooling api result: ');
logger.debug(result);
compileResult = result;
return self.project.sfdcClient.getServerProperties(toolingFiles);
})
.then(function(serverPropertiesResult) {
logger.silly(serverPropertiesResult);
return self.project.updateLocalStore(serverPropertiesResult);
})
.then(function() {
resolve(compileResult);
})
.catch(function(error) {
reject(error);
})
.done();
}
})
.catch(function(error) {
reject(error);
})
.done();
});
};
/**
* Compiles metadata, will use metadata API or tooling API based on the metadata payload requested
* @param {Array} type Metadata - metadata to be compiled (must already exist in salesforce)
* @return {Promise}
*/
CompileDelegate.prototype._compileGenericMetadata = function(metadataApiFiles, compileSubscription) {
var self = this;
return new Promise(function(resolve, reject) {
logger.debug('compiling via metadata api');
logger.debug(compileSubscription);
var metadataApiFilesPayload = [];
_.each(metadataApiFiles, function(f) {
if (f.isDirectory) {
// get directory contents
metadataApiFilesPayload = metadataApiFilesPayload.concat(f.localMembers);
} else {
metadataApiFilesPayload.push(f);
}
});
self._checkConflicts(metadataApiFilesPayload)
.then(function(conflictCheckResult) {
if (conflictCheckResult.hasConflict) {
return resolve(conflictCheckResult);
} else {
var deploy = new Deploy({ project: self.project });
deploy.compileWithMetadataApi(metadataApiFilesPayload, compileSubscription)
.then(function(result) {
logger.debug('compile via metadata api result: ');
logger.debug(result);
resolve(result);
})
.catch(function(error) {
reject(error);
})
.done();
}
})
.catch(function(error) {
reject(error);
})
.done();
});
};
/**
* Compiles metadata, will use metadata API or tooling API based on the metadata payload requested
* @param {Array} type Metadata - metadata to be compiled (must already exist in salesforce)
* @return {Promise}
*/
CompileDelegate.prototype._compileLightningBundleItemFiles = function(files) {
var self = this;
return new Promise(function(resolve, reject) {
var lightningService = new LightningService(self.project);
lightningService.update(files)
.then(function(result) {
logger.debug('compile lightning result: ');
logger.debug(result);
resolve(result);
})
.catch(function(error) {
reject(error);
})
.done();
});
};
/**
* Checks for conflict between local copy and server copy
* @param {Array} files - array of MavensMateFiles
* @return {Promise}
*/
CompileDelegate.prototype._checkConflicts = function(files) {
var self = this;
return new Promise(function(resolve, reject) {
try {
if (!config.get('mm_compile_check_conflicts') || self.force) {
return resolve({ hasConflict: false });
}
logger.debug('checking for conflicts');
var result = { hasConflict: false };
var conflicts = {};
var serverCopyPromises = [];
_.each(files, function(f) {
if (f.isToolingType) {
serverCopyPromises.push( f.serverCopy );
}
});
if (serverCopyPromises.length === 0) {
return resolve({ hasConflict: false });
}
Promise.all(serverCopyPromises)
.then(function(serverCopyResults) {
_.each(files, function(f, i) {
// logger.debug('local copy:');
// logger.debug(f.localStoreEntry);
// logger.debug('remote copy:');
// logger.debug(serverCopyResults[i]);
if (!f.localStoreEntry) {
return reject(new Error('No local index found for '+f.name+'. This can happen if you fetch your project from a remote git repository. Please ensure your project subscription includes the metadata type: "'+f.type.xmlName+'", then run the "Clean Project" command to update your local index of metadata.'))
return false;
}
var localLastModified = moment(f.localStoreEntry.lastModifiedDate);
var remoteLastModified = moment(serverCopyResults[i].LastModifiedDate);
if (remoteLastModified.isAfter(localLastModified)) {
logger.debug('conflict detected between: ');
logger.debug(f.localStoreEntry);
logger.debug(serverCopyResults[i]);
var tempFile = temp.openSync({ prefix: 'mm_', suffix: ' [SERVER COPY].'+self.metadataHelper.getTypeByXmlName(f.localStoreEntry.type).suffix });
fs.writeSync(tempFile.fd, serverCopyResults[i].Body);
serverCopyResults[i].tempPath = tempFile.path;
conflicts[f.basename] = {
local: f.localStoreEntry,
remote: serverCopyResults[i]
};
}
});
if (Object.keys(conflicts).length > 0) {
result.hasConflict = true;
result.success = false;
result.conflicts = conflicts;
}
logger.debug('conflict check result:');
logger.debug(result);
return resolve(result);
})
.catch(function(err) {
logger.error('error checking conflicts', err);
reject(err);
});
} catch(e) {
logger.error('Could not check conflicts: ');
logger.error(e);
reject(e);
}
});
};
module.exports = CompileDelegate;