dmc
Version:
Cross-platform, developer cli tool for Salesforce
601 lines (482 loc) • 15.2 kB
JavaScript
var fs = require('fs-extra');
var user = require('../lib/user');
var cliUtil = require('../lib/cli-util');
var sfClient = require('../lib/sf-client');
var index = require('../lib/index');
var config = require('../lib/config');
var metaMap = require('../lib/metadata-map');
var getFiles = require('../lib/get-files');
var Promise = require('bluebird');
var path = require('path');
var async = require('async');
var _ = require('lodash');
var archiver = require('archiver');
var logger = require('../lib/logger');
var dmcignore = require('../lib/dmcignore');
var resolve = require('../lib/resolve');
var hl = logger.highlight;
function createStubFiles(map, client) {
var keys = map.index.getMemberTypeNames();
function iterator(obj, cb) {
logger.create(obj.type + '::' + hl(obj.object.name));
client.tooling.insert(obj, function(err, res) {
if(err) return cb(err);
map.setMetaId(obj.type, obj.object.name, res.id);
cb(null, res.id);
});
}
return new Promise(function(resolve, reject) {
var stubs = [];
_.each(keys, function(k) {
_(map.meta[k])
.filter(function(o) {
return (!o.id || o.id === null || o.id === '');
})
.each(function(o) {
stubs.push(metaMap.getStub(k, o.name, o.object));
})
.value();
});
if(!stubs || !stubs.length) return resolve([]);
async.mapLimit(stubs, 5, iterator, function(err, results) {
if(err) {
logger.error(err.message);
return reject(new Error('unable to create stub files'));
} else {
return resolve(results);
}
});
});
}
function createStaticResources(map, client) {
function iterator(obj, cb) {
fs.readFile(obj.path, { encoding: 'base64' }, function(err, body) {
if(err) return cb(err);
var opts = {
id: obj.id,
type: 'StaticResource',
object: {
name: obj.name,
contenttype: 'application/zip', // hard-coding this for now
body: body
}
};
var method = (obj.id) ? 'update' : 'insert';
logger.log('executing StaticResource ' + method);
client.tooling[method](opts, function(err, res) {
if(err) return cb(err);
logger[(method === 'insert') ? 'create' : 'update']('StaticResource::' + obj.name);
cb(null, res);
});
});
}
return new Promise(function(resolve, reject) {
async.mapLimit(map.meta.StaticResource, 5, iterator, function(err, srs) {
if(err) {
logger.error('StaticResource deploy error');
return reject(err);
}
resolve(srs);
});
});
}
function createContainer(client) {
logger.log('creating container');
var name = 'dmc:' + (new Date()).getTime();
return new Promise(function(resolve, reject) {
client.tooling.createContainer({ name: name }, function(err, container) {
if(err) return reject(err);
logger.create('metadata container: ' + hl(container.id));
resolve(container.id);
});
});
}
function createDeployArtifacts(map, containerId, client) {
var iterator = function(m, cb2) {
if(map.index.getMemberTypeNames().indexOf(m.type) === -1) {
return cb2(null);
}
fs.readFile(m.path, { encoding: 'utf8' }, function(err, data) {
if(err) return cb2(err);
var artifact = client.tooling.createDeployArtifact(m.type + 'Member', {
body: data,
contentEntityId: m.id
});
if(!artifact) {
return cb2(new Error('couldn\'t create artifact: ' + m.name));
}
var opts = {
id: containerId,
artifact: artifact
};
client.tooling.addContainerArtifact(opts, function(err, resp) {
if(err) {
logger.error('problem creating container artifact: ' + m.type + '::' + m.name);
return cb2(err);
}
logger.create('container member: ' + m.type + '::' + m.name);
return cb2(null, resp);
});
});
};
return new Promise(function(resolve, reject) {
var files = _(map.meta)
.values()
.flatten()
.remove(function(m) {
return (m.id && m.id !== '');
})
.value();
async.mapLimit(files, 5, iterator, function(err, res) {
if(err) reject(err);
else resolve(res);
});
});
}
function deployContainer(containerId, client) {
return new Promise(function(resolve, reject) {
var asyncContainerId;
var opts = {
id: containerId,
isCheckOnly: false
};
function logStatus(status) {
logger.list('deploy status: ' + hl(status));
}
function poll() {
var pollOpts = {
id: asyncContainerId
};
client.tooling.getContainerDeployStatus(pollOpts, function(err, resp) {
if(err) return reject(err);
logStatus(resp.State);
if(resp.State === 'Completed') {
logger.success('deployment successful');
return resolve(resp);
} else if(resp.State === 'Failed') {
if(resp.ErrorMsg) logger.error(resp.ErrorMsg);
if(resp.CompilerErrors) {
_.each(resp.CompilerErrors, function(e) {
logger.error('=> ' + e.extent[0] + ': ' + e.name[0]);
logger.error(' Line ' + e.line[0] + ' - ' + e.problem[0]);
});
}
if(resp.DeployDetails) {
logDetails(resp.DeployDetails);
}
return reject(new Error('Compiler Errors'));
} else if(resp.State === 'Errored') {
if(resp.ErrorMsg) logger.error(res.ErrorMsg);
return reject(new Error(res.ErrorMsg || 'an unknown error occcurred'));
} else {
setTimeout(function() {
poll();
}, 1000);
}
});
}
client.tooling.deployContainer(opts, function(err, asyncContainer) {
if(err) return cb(err);
logger.log('Deploying...');
asyncContainerId = asyncContainer.id;
poll();
});
});
}
function deleteContainer(containerId, client) {
var opts = {
type: 'MetadataContainer',
id: containerId
};
return new Promise(function(resolve, reject) {
client.tooling.delete(opts, function(err, res) {
if(err) return reject(err);
logger.destroy('metadata container: ' + hl(containerId));
resolve(containerId);
});
});
}
function runToolingDeploy(map, client) {
var containerId;
return Promise.resolve()
.then(function() {
logger.log('loading related metadata ids');
return map.fetchIds().then(function(results) {
logger.log('loaded ' + hl(results.length) + ' ids');
});
})
// create stub files if necessary
.then(function() {
logger.log('creating stub files');
return createStubFiles(map, client).then(function(stubs) {
logger.log('created ' + hl(stubs.length) + ' stub files');
});
})
// create static resources
.then(function() {
logger.log('creating static resources');
return createStaticResources(map, client).then(function(srs) {
logger.log('deployed ' + hl(srs.length) + ' static resources');
});
})
.then(function(){
return createContainer(client);
})
.then(function(id) {
containerId = id;
return createDeployArtifacts(map, containerId, client);
})
.then(function(){
return deployContainer(containerId, client);
})
.finally(function() {
if(containerId) {
return deleteContainer(containerId, client);
}
});
}
function logDetails(details, opts) {
if(!details) return;
var cType;
var method;
if(details.componentSuccesses) {
logger.success('component successes [' + details.componentSuccesses.length + '] ====>');
_(details.componentSuccesses)
.map(function(e) {
e.cType = (_.isString(e.componentType) && e.componentType.length) ?
(e.componentType + ': ') :
'';
e.method =
(e.created) ? 'create' :
(e.changed) ? 'update' :
(e.deleted) ? 'destroy' :
'noChange';
return e;
})
.sortBy(function(e) {
return e.cType + e.fullName;
})
.each(function(e) {
logger[e.method](e.cType + e.fullName);
})
.value();
}
if(details.componentFailures) {
logger.error('component failures [' + details.componentFailures.length + '] ====>');
_(details.componentFailures)
.map(function(e) {
e.cType = _.isString(e.componentType) ?
(e.componentType + ': ') :
'';
return e;
})
.sortBy(function(e) {
return e.cType + e.fullName;
})
.each(function(e) {
logger.listError(
'[' + e.cType + e.fullName + '] ' +
e.problemType + ' at ' +
'l:' + (e.lineNumber || '0') + '/' +
'c:' + (e.columnNumber || '0') + ' ' +
'=> ' +
e.problem
);
})
.value();
}
if(details.runTestResult) {
if(details.runTestResult.numFailures) {
logger.error('test results ====>');
} else {
logger.success('test results ====>');
}
// console.error(details.runTestResult.codeCoverage);
// console.error(details.runTestResult.codeCoverageWarnings);
logger.list('tests run: ' + details.runTestResult.numTestsRun);
logger.list('failures: ' + details.runTestResult.numFailures);
logger.list('total time: ' + details.runTestResult.totalTime);
var cc = details.runTestResult.codeCoverage;
if(opts.coverage && cc && cc.length) {
logger.success('code coverage results ====>');
_(cc)
.map(function(c) {
var locations = c.numLocations;
var notCovered = c.numLocationsNotCovered;
var covered = locations - notCovered;
var coverage = ((covered / locations) * 100);
if(locations === 0) {
coverage = 100;
}
return {
type: c.type,
name: c.name,
locations: locations,
notCovered: notCovered,
covered: covered,
coverage: coverage
};
})
.sortBy(function(c) {
return c.coverage * -1;
})
.each(function(c) {
logger.list(c.coverage.toFixed(2) + '% => ' + c.type + ':' + c.name + ' (' + c.covered + '/' + c.locations + ')');
})
.value();
}
var ccw = details.runTestResult.codeCoverageWarnings;
if(ccw && ccw.length) {
logger.error('code coverage warnings ====>');
_.each(ccw, function(w) {
logger.list(w.message);
});
}
_.each(details.runTestResult.failures, function (f) {
logger.error(f);
});
}
}
function runMetadataDeploy(map, client, opts) {
return new Promise(function(resolve, reject) {
var archive = archiver('zip');
var promise = client.meta.deployAndPoll({
zipFile: archive,
includeDetails: true,
deployOptions: {
rollbackOnError: true
}
});
// write the package.xml to the zip
archive.append(
new Buffer(map.createPackageXML(client.apiVersion)
), { name: 'src/package.xml' });
promise.poller.on('poll', function(res) {
logger.log('deploy status: ' + hl(res.status));
});
promise.then(function(results){
logDetails(results.details, opts);
resolve();
}).catch(function(err) {
if(err.details) {
logDetails(err.details, opts);
}
if(err.message) {
logger.error(err.message);
}
if(err.errorMessage) {
logger.error(err.errorMessage);
}
reject(new Error('metatadata api deployment failed'));
});
// iterator for adding files
// checks for existence and adds
function iterator(p, cb) {
var exists;
fs.existsAsync(p).then(function(e) {
exists = e;
if(!exists) {
throw new Error('missing file: ' + p);
}
return fs.lstatAsync(p);
}).then(function(stat) {
if(stat.isDirectory()) {
archive.directory(p);
} else {
archive.file(p);
}
logger.list(p);
cb(null, {
file: p,
exists: exists
});
});
}
logger.log('adding metadata');
// map over files to add and add if they exist
async.mapLimit(map.getFilePathsForDeploy(), 5, iterator, function(err, res) {
if(err) {
reject(err);
} else {
var hasErrors = false;
_.each(res, function(r) {
if(!r.exists) {
logger.error('missing file: ' + r.file);
hasErrors = true;
return;
}
});
if(hasErrors) {
return reject(new Error('cannot deploy - missing files'));
}
logger.log('starting deploy');
archive.finalize();
}
});
});
}
var run = module.exports.run = function(opts, cb) {
return resolve(cb, function(){
var containerId;
var client;
var oauth = opts.oauth;
var globs = (opts.globs && opts.globs.length > 0) ?
opts.globs:
['src/**/*'];
var map = metaMap.createMap({
oauth: opts.oauth,
org: opts.org
});
var ignores = null;
return Promise.resolve()
.then(config.loadAll)
.then(function(){
return dmcignore.load().then(function(lines) {
ignores = lines;
});
})
.then(function(){
return sfClient.getClient(opts.oauth);
})
// load the index for the org
.then(function(sfdcClient) {
client = sfdcClient;
return map.autoLoad();
})
// search src/ for file matches
.then(function() {
logger.log('searching for local metadata');
return getFiles({ globs: globs, ignores: ignores })
.then(function(files) {
if(!files || files.length < 1) {
throw new Error('no files for deployment found');
}
logger.log('deploying ' + hl(files.length) + ' metadata files');
map.addFiles(files);
});
})
.then(function() {
var deployMode = config.get('deploy_mode') || 'dynamic';
logger.log('deploy mode: ' + deployMode);
if(!map.requiresMetadataDeploy() && !opts.meta && deployMode !== 'metadata') {
logger.info('deploy api: ' + hl('tooling'));
return runToolingDeploy(map, client);
} else {
logger.info('deploy api: ' + hl('metadata'));
return runMetadataDeploy(map, client, opts);
}
});
});
};
module.exports.cli = function(program) {
program.command('deploy [globs...]')
.description('deploy metadata to target org')
.option('-o, --org <org>', 'the Salesforce organization to use')
.option('--coverage', 'show code coverage for tests run')
.option('--meta', 'force deploy with metadata api')
.action(function(globs, opts) {
opts.globs = globs;
opts._loadOrg = true;
return cliUtil.executeRun(run)(opts);
});
};