mavensmate
Version:
Core APIs that drive MavensMate IDEs for Salesforce1/Force.com
499 lines (463 loc) • 15.2 kB
JavaScript
/**
* @file Represents a package.xml file
* @author Joseph Ferraro <@joeferraro>
*/
;
var Promise = require('bluebird');
var _ = require('lodash');
var swig = require('swig');
var fs = require('fs-extra');
var path = require('path');
var logger = require('winston');
var config = require('../config');
var xmldoc = require('xmldoc');
var sax = require('sax');
/**
* Represents a collection of metadata
* @param {Object} opts
* @param {String} opts.path - path to package.xml
* @param {Array} opts.metadata - Array of Metadata
* @param {Array} opts.metadataTypeXmlNames - Array of metadata type xml names, e.g. ['ApexClass', 'ApexPage']
*/
// can take array of metadata as constructor argument, turns into object representation
// { "ApexClass" : [ "thisclass", "thatclass" ], "ApexPage" : "*" }
function Package(opts) {
this.path = opts.path;
this.files = opts.files;
this.metadataTypeXmlNames = opts.metadataTypeXmlNames;
this.subscription = opts.subscription;
this.project = opts.project;
}
Package.prototype._path = null;
Package.prototype._files = null;
Package.prototype._subscription = null;
Package.prototype.init = function() {
var self = this;
return new Promise(function(resolve, reject) {
if (self.subscription) {
logger.debug('initing package instance with subscription: ');
logger.debug(self.subscription);
self._ensureCustomObjectSubscriptionIsValid()
.then(function() {
resolve();
})
.catch(function(e) {
reject(e);
});
} else {
logger.debug('initing package instance');
if (self.files) {
self._getSubscriptionFromFiles()
.then(function(obj) {
logger.debug('setting package subscription: ');
logger.debug(obj);
self.subscription = obj;
resolve();
})
.catch(function(err) {
reject(new Error('Could not initiate Package instance by metadata list: '+err.message));
});
} else if (self.metadataTypeXmlNames) {
self._getSubscriptionFromMetadataTypeXmlNames()
.then(function(obj) {
logger.debug('setting package subscription: ');
logger.debug(obj);
self.subscription = obj;
resolve();
})
.catch(function(err) {
reject(new Error('Could not initiate Package instance by metadata list: '+err.message));
});
} else if (self.path) {
self._deserialize()
.then(function(obj) {
logger.debug('setting package subscription: ');
logger.debug(obj);
self.subscription = obj;
self._ensureCustomObjectSubscriptionIsValid()
.then(function() {
resolve();
})
.catch(function(e) {
reject(e);
});
})
.catch(function(err) {
reject(new Error('Could not initiate Package instance by path: '+err.message));
});
}
}
});
};
/**
* Unstructured array of Metadata elements included in this package
*/
Object.defineProperty(Package.prototype, 'files', {
get: function() {
return this._files;
},
set: function(value) {
this._files = value;
}
});
/**
* Structured Metadata subscription ( { "ApexClass" : [ "thisclass", "thatclass" ], "ApexPage" : "*" } )
*/
Object.defineProperty(Package.prototype, 'subscription', {
get: function() {
return this._subscription;
},
set: function(value) {
this._subscription = value;
}
});
/**
* File path of this package
*/
Object.defineProperty(Package.prototype, 'path', {
get: function() {
return this._path;
},
set: function(value) {
this._path = value;
}
});
Package.prototype._ensureCustomObjectSubscriptionIsValid = function() {
var self = this;
return new Promise(function(resolve, reject) {
try {
logger.debug('_ensureCustomObjectSubscriptionIsValid');
if (self.project && self.subscription.CustomObject && self.subscription.CustomObject === '*') {
var customObjectSub = [];
var projectMetadata = self.project.getOrgMetadataIndex()
.then(function(index) {
var customObjectIndex = _.find(index, function(item){ return item.id === 'CustomObject' });
_.each(customObjectIndex.children, function(customObjectChild) {
customObjectSub.push(customObjectChild.fullName);
});
self.subscription.CustomObject = customObjectSub;
resolve();
})
.catch(function(err) {
logger.error('could not subscribe to all custom objects', err);
resolve();
});
} else {
resolve();
}
} catch(err) {
logger.error('could not subscribe to all custom objects', err);
resolve(); // we resolve so we dont break a user's retrieve
}
});
};
// todo: some types dont support '*' subscription
Package.prototype._getSubscriptionFromMetadataTypeXmlNames = function() {
var self = this;
return new Promise(function(resolve, reject) {
try {
var sub = {};
_.each(self.metadataTypeXmlNames, function(typeXmlName) {
sub[typeXmlName] = '*';
});
resolve(sub);
} catch(err) {
reject(new Error('Could not get package dictionary: '+err.message));
}
});
};
Package.prototype._getSubscriptionFromFiles = function() {
var self = this;
return new Promise(function(resolve, reject) {
try {
var pkg = {};
_.each(self.files, function(f) {
var metadataTypeXmlName = f.type.xmlName;
if (!_.has(pkg, metadataTypeXmlName)) {
pkg[metadataTypeXmlName] = [f.name];
} else {
var value = pkg[metadataTypeXmlName];
value.push(f.name);
}
});
return resolve(pkg);
} catch(err) {
reject(new Error('Could not get package dictionary: '+err.message));
}
});
};
Package.prototype.writeFile = function() {
var self = this;
return new Promise(function(resolve, reject) {
if (!self.path) {
return reject(new Error('Could not write to disk. Please specify package path'));
}
logger.debug('writing package to path: '+self.path);
var xmlBody = self._serialize();
fs.outputFile(self.path, xmlBody, function(err) {
if (err) {
reject(new Error('Could not write package to disk: '+err.message));
} else {
resolve();
}
});
});
};
Package.prototype.writeFileSync = function() {
var xmlBody = this._serialize();
logger.debug('writing package to path: '+this.path);
logger.debug(xmlBody);
fs.outputFileSync(this.path, xmlBody);
};
/**
* Inserts metadata to package subscription
* @param {Array of type Metadata} metadata
* @return {None}
*/
Package.prototype.subscribe = function(files) {
var self = this;
if (!_.isArray(files)) {
files = [files];
}
_.each(files, function(f) {
// logger.debug('metadata type: ');
// logger.debug(f.type);
var metadataTypeXmlName = f.type.xmlName;
if (_.has(self.subscription, metadataTypeXmlName)) {
if (self.subscription[metadataTypeXmlName] === '*') {
return false; // nothing to do here
} else {
if (self.subscription[metadataTypeXmlName].indexOf(f.subscriptionName) === -1) {
self.subscription[metadataTypeXmlName].push(f.subscriptionName);
}
}
} else {
self.subscription[metadataTypeXmlName] = [f.subscriptionName];
}
});
};
/**
* Removes metadata from package subscription
* @param {Array of type Metadata} metadata
* @return {[type]}
*/
Package.prototype.unsubscribe = function(files) {
var self = this;
if (!_.isArray(files)) {
files = [files];
}
_.each(files, function(f) {
logger.debug('unsubscribing: '+f.name);
logger.debug('type: '+f.type);
var metadataTypeXmlName = f.type.xmlName;
if (_.has(self.subscription, metadataTypeXmlName)) {
if (self.subscription[metadataTypeXmlName] === '*') {
return false; // nothing to do here
} else {
var members = self.subscription[metadataTypeXmlName];
var newMembers = [];
_.each(members, function(member) {
if (member !== f.subscriptionName) {
newMembers.push(member);
}
});
self.subscription[metadataTypeXmlName] = newMembers;
}
} else {
self.subscription[metadataTypeXmlName] = f.subscriptionName;
}
});
};
/**
* Take JS object representation of package.xml, serializes to XML
* @param {Object} packageXmlObject
* @return {String}
*/
Package.prototype._serialize = function() {
var self = this;
logger.debug('serializing package:');
logger.debug(self.subscription);
var serialized = swig.renderFile(path.join(__dirname, 'templates', 'package.xml'), {
obj: self.subscription,
apiVersion: config.get('mm_api_version')
});
return serialized;
};
/**
* Parses package.xml to JS object
* @param {String} path - disk path of package.xml
* @return {Promise} - resolves to JavaScript object
*/
Package.prototype._deserialize = function() {
var self = this;
return new Promise(function(resolve, reject) {
var pkg = {};
logger.debug('deserializing: '+self.path);
if (!self.path) {
reject(new Error('Please set package.xml path'));
} else {
fs.readFile(self.path, function(err, data) {
if (err) {
reject(err);
} else {
try {
var parser = sax.parser(true);
var isValidPackage = true;
parser.onerror = function (e) {
logger.debug('Parse error: package.xml --> '+e);
isValidPackage = false;
parser.resume();
};
parser.onend = function () {
if (!isValidPackage) {
reject(new Error('Could not parse package.xml, invalid XML'));
} else {
var doc = new xmldoc.XmlDocument(data);
_.each(doc.children, function(type) {
var metadataType;
var val = [];
if (type.name !== 'types') {
return;
}
_.each(type.children, function(node) {
if (node.name === 'name' && node.val !== undefined) {
metadataType = node.val;
return false;
}
});
_.each(type.children, function(node) {
if (node.name === 'members') {
if (node.val === '*') {
val = '*';
return false;
} else {
val.push(node.val);
}
}
});
pkg[metadataType] = val;
});
logger.debug('parsed package.xml to -->'+JSON.stringify(pkg));
resolve(pkg);
}
};
parser.write(data.toString().trim()).close();
} catch(e) {
reject('Could not deserialize package: '+e.message);
}
}
});
}
});
};
/**
* Member providers are overrides when creating a package for retrieval by salesforce
* they are necessary because certain metadata types don't behave "normally"
*/
function MemberProvider(sfdcClient) {
this.sfdcClient = sfdcClient;
}
MemberProvider.prototype = {
getMembers : function(){}
};
function ListProvider(sfdcClient) {
this.sfdcClient = sfdcClient;
}
ListProvider.prototype = {
getList : function(){}
};
function SharingRulesMemberProvider(sfdcClient) {
MemberProvider.call(this, sfdcClient);
}
SharingRulesMemberProvider.prototype = Object.create(MemberProvider.prototype, {
getMembers : {
value : function() {
var self = this;
return new Promise(function(resolve, reject) {
self.sfdcClient.list('CustomObject')
.then(function(res) {
var customObjectNames = [];
_.each(res.CustomObject, function(r) {
customObjectNames.push(r.fullName);
});
resolve(customObjectNames);
})
.catch(function(err) {
reject(err);
});
});
}
}
});
SharingRulesMemberProvider.prototype.constructor = SharingRulesMemberProvider;
function SharingRulesListProvider(sfdcClient) {
ListProvider.call(this, sfdcClient);
}
SharingRulesListProvider.prototype = Object.create(ListProvider.prototype, {
getList: {
value: function() {
var self = this;
return new Promise(function(resolve, reject) {
var memberProvider = new SharingRulesMemberProvider(self.sfdcClient);
memberProvider.getMembers()
.then(function(members) {
return self.sfdcClient.retrieveUnpackaged({
SharingRules : members
});
})
.then(function(retrieveResult) {
var res = {};
res.SharingRules = _.filter(retrieveResult.fileProperties, function(fileProperty) {
return fileProperty.type === 'SharingRules';
});
resolve(res);
})
.catch(function(err) {
reject(err);
});
});
}
}
});
SharingRulesListProvider.prototype.constructor = SharingRulesListProvider;
function CustomObjectMemberProvider(sfdcClient) {
MemberProvider.call(this, sfdcClient);
}
function WorkflowListProvider(sfdcClient) {
ListProvider.call(this, sfdcClient);
}
WorkflowListProvider.prototype = Object.create(ListProvider.prototype, {
getList: {
value: function() {
var self = this;
return new Promise(function(resolve, reject) {
Promise.all([
// retrieve CaseComment workflow file properties separately, since CaseComment isn't returned
// with list workflow call, see http://salesforce.stackexchange.com/q/100517/594
self.sfdcClient.retrieveUnpackaged({
Workflow : ['CaseComment']
}),
self.sfdcClient.list('Workflow')
])
.then(function(results) {
var retrieveResults = results[0];
var listResults = results[1];
var output = listResults;
var caseCommentWorkflows = _.filter(retrieveResults.fileProperties, function(fileProperty) {
return fileProperty.type === 'Workflow';
})
output.Workflow = output.Workflow.concat(caseCommentWorkflows);
resolve(output);
})
.catch(reject).done();
});
}
}
});
WorkflowListProvider.prototype.constructor = WorkflowListProvider;
module.exports.Package = Package;
module.exports.MemberProvider = MemberProvider;
module.exports.ListProvider = ListProvider;
module.exports.SharingRulesMemberProvider = SharingRulesMemberProvider;
module.exports.SharingRulesListProvider = SharingRulesListProvider;
module.exports.WorkflowListProvider = WorkflowListProvider;