mavensmate
Version:
Core APIs that drive MavensMate IDEs for Salesforce1/Force.com
1,396 lines (1,323 loc) • 47.2 kB
JavaScript
/**
* @file Represents a local MavensMate project
* @author Joseph Ferraro <@joeferraro>
*/
;
var Promise = require('bluebird');
var temp = require('temp');
var _ = require('lodash');
var fs = require('fs-extra-promise');
var gracefulFs = require('graceful-fs');
var path = require('path');
var find = require('findit');
var util = require('./util');
var uuid = require('node-uuid');
var inherits = require('inherits');
var events = require('events');
var moment = require('moment');
var SalesforceClient = require('./sfdc-client');
var MetadataHelper = require('./metadata').MetadataHelper;
var config = require('../config');
var logger = require('winston');
var normalize = require('./utilities/normalize-object');
var IndexService = require('./services/index');
var Package = require('./package').Package;
var SymbolService = require('./services/symbol');
var LogService = require('./services/log');
var LightningService = require('./services/lightning');
var KeychainService = require('./services/keychain');
/**
* Represents a MavensMate project
*
* @constructor
* @param {Object} [opts] - Options used in deployment
* @param {String} [opts.name] - For new projects, sets the name of the project
* @param {String} [opts.subscription] - (optional) Specifies list of Metadata types that the project should subscribe to
* @param {String} [opts.workspace] - (optional) For new projects, sets the workspace
* @param {String} [opts.path] - (optional) Explicitly sets path of the project (defaults to current working directory)
* @param {String} [opts.origin] - (optional) When creating a MavensMate project from an existing directory, pass the existing path as "origin"
*/
var Project = function(opts) {
this.name = opts.name;
this.path = opts.path;
this.workspace = opts.workspace;
this.subscription = opts.subscription;
this.origin = opts.origin;
this.username = opts.username;
this.password = opts.password;
this.accessToken = opts.accessToken;
this.refreshToken = opts.refreshToken;
this.instanceUrl = opts.instanceUrl;
this.package = opts.package;
this.orgType = opts.orgType;
this.sfdcClient = opts.sfdcClient;
this.requiresAuthentication = true;
this.settings = {};
this.packageXml = null;
this.orgMetadata = null;
this.lightningIndex = null;
this.metadataHelper = null;
this.keychainService = new KeychainService();
this.logService = new LogService(this);
this.symbolService = new SymbolService(this);
this.lightningService = new LightningService(this);
};
inherits(Project, events.EventEmitter);
/**
* Initializes project instance based on whether this is a new or existing project
* @param {Boolean} isNewProject
* @return {Promise}
*/
Project.prototype.initialize = function(isNewProject, isExistingDirectory) {
var self = this;
return new Promise(function(resolve, reject) {
isNewProject = isNewProject || false;
if (!isNewProject) {
self._initExisting()
.then(function() {
resolve(self);
})
.catch(function(error) {
logger.error('Could not initiate existing Project instance: '+error.message);
reject(error);
})
.done();
} else if (isNewProject) {
var initPromise = isExistingDirectory ? self._initNewProjectFromExistingDirectory() : self._initNew();
initPromise
.then(function() {
resolve(self);
})
.catch(function(error) {
logger.error('Could not initiate new Project instance: '+error.message);
reject(error);
})
.done();
}
});
};
/**
* Initiates an existing (on disk) MavensMate project instance
* @return {Promise}
*/
Project.prototype._initNewProjectFromExistingDirectory = function() {
var self = this;
return new Promise(function(resolve, reject) {
var pkg, fileProperties;
if (!self.workspace) {
throw new Error('Please select a workspace for this project');
}
if (!self.origin) {
throw new Error('Please select an origin for this project');
}
if (!fs.existsSync(path.join(self.origin, 'src'))) {
return reject(new Error('Project must have a top-level src directory'));
}
if (!fs.existsSync(path.join(self.origin, 'src', 'package.xml'))) {
return reject(new Error('Project must have a valid package.xml file located in the src directory'));
}
self.name = path.basename(self.origin); // set name of the project to the basename of the project's origin path
// if they're moving the project into a workspace
if (self.workspace !== path.dirname(self.origin)) {
if (fs.existsSync(path.join(self.workspace, self.name))) {
return reject(new Error('Project with this name already exists in the selected workspace'));
} else {
// copy non-mavensmate project to selected workspace
fs.ensureDirSync(path.join(self.workspace, self.name));
fs.copySync(self.origin, path.join(self.workspace, self.name));
}
}
self.path = path.join(self.workspace, self.name);
fs.ensureDirSync(path.join(self.path, 'config'));
self.sfdcClient.describe()
.then(function(describe) {
self.metadataHelper = new MetadataHelper({ sfdcClient: self.sfdcClient });
return self.setDescribe(describe);
})
.then(function() {
pkg = new Package({ project: self, path: path.join(self.path, 'src', 'package.xml') });
return pkg.init();
})
.then(function() {
return self.sfdcClient.retrieveUnpackaged(pkg.subscription, true, self.path);
})
.then(function(retrieveResult) {
logger.debug('retrieve result: ');
logger.debug(retrieveResult);
fileProperties = retrieveResult.fileProperties;
if (fs.existsSync(path.join(self.path, 'unpackaged'))) {
fs.removeSync(path.join(self.path, 'unpackaged'));
}
self.id = uuid.v1();
return self._initConfig();
})
.then(function() {
logger.debug('initiating local store');
logger.silly(fileProperties);
return self._writeLocalStore(fileProperties);
})
.then(function() {
resolve();
})
.catch(function(error) {
// remove directory from workspace if we encounter an exception along the way
logger.error('Could not retrieve and write project to file system: '+error.message);
logger.error(error.stack);
if (self.origin !== path.join(self.workspace, self.name) && fs.existsSync(path.join(self.workspace, self.name))) {
fs.removeSync(path.join(self.workspace, self.name));
}
reject(error);
})
.done();
});
};
/**
* Initiates an existing (on disk) MavensMate project instance
* @return {Promise}
*/
Project.prototype._initExisting = function() {
logger.debug('itializing existing project on the disk...');
var self = this;
return new Promise(function(resolve, reject) {
if (!self._hasValidStructure()) {
return reject(new Error('This does not seem to be a valid MavensMate project directory.'));
}
if (self.path) {
self.workspace = path.dirname(self.path);
self.name = path.basename(self.path);
} else if (self.workspace && self.name) {
self.path = path.join(self.workspace, self.name);
} else {
self.path = process.cwd();
self.workspace = path.dirname(self.path);
self.name = path.basename(self.path);
}
if (!fs.existsSync(self.path)) {
return reject(new Error('Project path does not exist.'));
}
logger.debug('project name', self.name);
logger.debug('project path', self.path);
self.settings = self._readSettings();
var creds = self._readCredentials();
if (!creds.password && !creds.accessToken && !creds.refreshToken) {
throw new Error('Could not retrieve credentials for project '+self.name);
}
if (!creds.password && creds.accessToken && !creds.refreshToken) {
throw new Error('Project ('+self.name+') is using Oauth for authentication but no refresh token was found');
}
self.packageXml = new Package({
project: self,
path: path.join(self.path, 'src', 'package.xml')
});
self.packageXml.init()
.then(function() {
if (!self.sfdcClient) {
logger.debug('Creating new sfdc client', self.settings, creds);
if (creds.refreshToken) {
self.sfdcClient = new SalesforceClient({
username: self.settings.username,
accessToken: creds.accessToken,
refreshToken: creds.refreshToken,
instanceUrl: self.settings.instanceUrl,
loginUrl: self.settings.loginUrl,
orgType: self.settings.orgType
});
} else {
self.sfdcClient = new SalesforceClient({
username: self.settings.username,
password: creds.password,
loginUrl: self.settings.loginUrl,
orgType: self.settings.orgType
});
}
self._listenForTokenUpdates();
}
return self.sfdcClient.initialize();
})
.then(function(res) {
self.metadataHelper = new MetadataHelper({ sfdcClient: self.sfdcClient });
self.getLocalStore();
return self.getOrgMetadataIndexWithSelections();
})
.then(function() {
return self._refreshDescribeFromServer();
})
.then(function() {
self.sfdcClient.on('sfdcclient-new-log', function(message) {
if (message.sobject && message.sobject.Id) {
self.logService.downloadLog(message.sobject.Id)
.then(function(filePath) {
self.emit('new-log', filePath);
})
.catch(function(error) {
logger.debug('Could not download log: '+error.message);
})
.done();
}
});
return self.sfdcClient.startSystemStreamingListener();
})
.then(function() {
if (!self.getDebugSettingsSync().debugLevelName) {
return self._writeDebug();
} else {
return Promise.resolve();
}
})
.then(function() {
self.requiresAuthentication = false;
resolve();
})
.catch(function(error) {
logger.error(error);
if (util.isCredentialsError(error)) {
logger.debug('project has expired access/refresh token, marking as invalid');
self.requiresAuthentication = true;
}
reject(error);
})
.done();
});
};
/**
* Initiates a new (not yet on disk) MavensMate project instance
* @return {Promise}
*/
Project.prototype._initNew = function() {
var self = this;
return new Promise(function(resolve, reject) {
if (!self.workspace) {
var workspace;
var workspaceSetting = config.get('mm_workspace');
logger.debug('Workspace not specified, retrieving base workspace: ');
logger.debug(workspaceSetting);
if (_.isArray(workspaceSetting)) {
workspace = workspaceSetting[0];
} else if (_.isString(workspaceSetting)) {
workspace = workspaceSetting;
}
if (workspace && !fs.existsSync(workspace)) {
fs.mkdirSync(workspace);
}
self.workspace = workspace;
logger.debug('workspace set to: '+self.workspace);
} else if (!fs.existsSync(self.workspace)) {
fs.mkdirSync(self.workspace);
}
if (!self.workspace) {
throw new Error('Could not set workspace for new project');
}
self.path = path.join(self.workspace, self.name);
if (fs.existsSync(self.path)) {
reject(new Error('Directory already exists!'));
} else {
self.id = uuid.v1();
resolve(self.id);
}
});
};
/**
* Whether this project has a valid MavensMate project structure
* @return {Boolean}
*/
Project.prototype._hasValidStructure = function() {
if (this.path) {
return fs.existsSync(path.join(this.path, 'config', '.settings'));
} else if (this.workspace && this.name) {
return fs.existsSync(path.join(this.workspace, this.name, 'config', '.settings'));
} else {
return fs.existsSync(path.join(process.cwd(),'config', '.settings'));
}
};
Project.prototype.replaceLocalFiles = function(remotePath, replacePackageXml) {
var self = this;
return new Promise(function(resolve, reject) {
var finder = find(remotePath);
finder.on('file', function (file) {
var fileBasename = path.basename(file);
// file => /foo/bar/myproject/unpackaged/classes/myclass.cls
logger.debug('refreshing file: '+file);
var directory = path.dirname(file); //=> /foo/bar/myproject/unpackaged/classes
var destinationDirectory = directory.replace(remotePath, path.join(self.workspace, self.name, 'src')); //=> /foo/bar/myproject/src/classes
// make directory if it doesnt exist (parent dirs included)
if (!fs.existsSync(destinationDirectory)) {
fs.mkdirpSync(destinationDirectory);
}
// remove project metadata, replace with recently retrieved
if (replacePackageXml && fileBasename === 'package.xml') {
fs.removeSync(path.join(destinationDirectory, fileBasename));
fs.copySync(file, path.join(destinationDirectory, fileBasename));
} else if (fileBasename !== 'package.xml') {
fs.removeSync(path.join(destinationDirectory, fileBasename));
fs.copySync(file, path.join(destinationDirectory, fileBasename));
}
});
finder.on('end', function () {
// remove retrieved
// TODO: package support
if (fs.existsSync(remotePath)) {
fs.removeAsync(remotePath)
.then(function() {
resolve();
})
.catch(function(err) {
reject(err);
});
} else {
resolve();
}
});
finder.on('error', function (err) {
logger.debug('Could not process retrieved metadata: '+err.message);
reject(err);
});
});
};
/**
* Performs a Salesforce.com retrieve based on the type of project being requested,
* create necessary /config, places on the disk in the correct workspace
* @return {Promise}
*/
Project.prototype.retrieveAndWriteToDisk = function() {
var self = this;
return new Promise(function(resolve, reject) {
var fileProperties;
if (fs.existsSync(self.path)) {
reject(new Error('Project with this name already exists in the specified workspace.'));
} else {
if (!self.package) {
// if user has not specified package, add standard developer objects to package
self.package = [
'ApexClass', 'ApexComponent', 'ApexPage', 'ApexTrigger', 'StaticResource'
];
}
self.sfdcClient.describe()
.then(function(describe) {
return self.setDescribe(describe);
})
.then(function() {
self.path = path.join(self.workspace, self.name);
fs.mkdirSync(self.path);
fs.mkdirSync(path.join(self.path, 'config'));
return self.sfdcClient.retrieveUnpackaged(self.package, true, self.path);
})
.then(function(retrieveResult) {
fileProperties = retrieveResult.fileProperties;
if (fs.existsSync(path.join(self.path, 'unpackaged'))) {
gracefulFs.rename(path.join(self.path, 'unpackaged'), path.join(self.path, 'src'), function(err, res) {
if (err) {
return reject(err);
} else {
return self._initConfig();
}
});
}
})
.then(function() {
logger.debug('initiating local store');
logger.silly(fileProperties);
return self._writeLocalStore(fileProperties);
})
.then(function() {
resolve();
})
.catch(function(error) {
// remove directory from workspace if we encounter an exception along the way
logger.error('Could not retrieve and write project to file system: '+error.message);
logger.error(error.stack);
if (fs.existsSync(self.path)) {
fs.removeSync(self.path);
}
reject(error);
})
.done();
}
});
};
/**
* Writes config/ files
* @return {Promise}
*/
Project.prototype._initConfig = function() {
var self = this;
return new Promise(function(resolve, reject) {
var settings = {
projectName: self.name,
username: self.sfdcClient.getUsername(),
id: self.id,
namespace: self.sfdcClient.getNamespace() || '',
orgType: self.sfdcClient.getOrgType(),
loginUrl: self.sfdcClient.getLoginUrl(),
instanceUrl: self.sfdcClient.getInstanceUrl(),
workspace: self.workspace,
subscription: self.subscription || config.get('mm_default_subscription')
};
self.writeSettings(settings);
self._writeCredentials();
var promises = [
self._writeDebug(),
self._writeEditorSettings(),
self._refreshDescribeFromServer(),
self.indexLightning()
];
Promise.all(promises)
.then(function() {
resolve();
})
.catch(function(err) {
logger.error('Could not initiate project config directory -->'+err.message);
reject(err);
})
.done();
});
};
/**
* Reverts a project to server state based on package.xml
* TODO: handle packages!
* @return {Promise}
*/
Project.prototype.refreshFromServer = function() {
// TODO: implement stash!
var self = this;
return new Promise(function(resolve, reject) {
logger.debug('refreshing project from server...');
var retrieveResult;
var retrievePath = temp.mkdirSync({ prefix: 'mm_' });
self.packageXml = new Package({ project: self, path: path.join(self.path, 'src', 'package.xml') });
self.packageXml.init()
.then(function() {
return self.sfdcClient.retrieveUnpackaged(self.packageXml.subscription, true, retrievePath);
})
.then(function(res) {
retrieveResult = res;
util.emptyDirectoryRecursiveSync(path.join(self.path, 'src'));
return self.replaceLocalFiles(path.join(retrievePath, 'unpackaged'), true);
})
.then(function() {
return self._writeLocalStore(retrieveResult.fileProperties);
})
.then(function() {
return self.indexLightning();
})
.then(function() {
util.removeEmptyDirectoriesRecursiveSync(path.join(self.path, 'src'));
resolve();
})
.catch(function(err) {
logger.error('Error refreshing project from server -->'+err.message);
reject(err);
})
.done();
});
};
/**
* Reverts a project to server state based on package.xml, also updates local metadata index and describe index
* TODO: handle packages!
* @return {Promise}
*/
Project.prototype.clean = function() {
// TODO: implement stash!
var self = this;
return new Promise(function(resolve, reject) {
self.refreshFromServer()
.then(function() {
return self._refreshDescribeFromServer();
})
.then(function() {
return self.indexMetadata();
})
.then(function() {
resolve();
})
.catch(function(err) {
logger.error('Error cleaning project -->'+err.message);
reject(err);
})
.done();
});
};
/**
* Compiles projects based on package.xml
* @return {Promise}
*/
Project.prototype.compile = function() {
var self = this;
return new Promise(function(resolve, reject) {
// writes temp directory, puts zip file inside
var newPath = temp.mkdirSync({ prefix: 'mm_' });
fs.copy(path.join(self.path, 'src'), path.join(newPath, 'unpackaged'), function(err) {
if (err) {
return reject(err);
} else {
var deployResult;
util.zipDirectory(path.join(newPath, 'unpackaged'), newPath)
.then(function() {
var zipStream = fs.createReadStream(path.join(newPath, 'unpackaged.zip'));
return self.sfdcClient.deploy(zipStream, { rollbackOnError : true, performRetrieve: true });
})
.then(function(result) {
logger.debug('Compile result: ');
logger.debug(result);
deployResult = result;
if (deployResult.details.retrieveResult) {
return self.updateLocalStore(deployResult.details.retrieveResult.fileProperties);
} else {
return new Promise(function(resolve) {
resolve();
});
}
})
.then(function() {
resolve(deployResult);
})
.catch(function(error) {
reject(error);
})
.done();
}
});
});
};
/**
* Edits project based on provided payload (should be a JSON package)
* @param {Object} payload
* @return {Promise}
*/
Project.prototype.edit = function(pkg) {
// TODO: implement stash!
var self = this;
return new Promise(function(resolve, reject) {
var newPackage;
logger.debug('editing project, requested package is: ', pkg);
var retrievePath = temp.mkdirSync({ prefix: 'mm_' });
self.sfdcClient.retrieveUnpackaged(pkg, true, retrievePath)
.then(function(retrieveResult) {
return self._writeLocalStore(retrieveResult.fileProperties);
})
.then(function() {
// todo: in conversation with sean he noted that it would be nice to not obliterate working
// copies of server metadata on edit-project, which this does
// in that case, we may give the user an option of overwriting all local files or only new ones
util.emptyDirectoryRecursiveSync(path.join(self.path, 'src'));
return self.replaceLocalFiles(path.join(retrievePath, 'unpackaged'), true);
})
.then(function() {
newPackage = new Package({ project: self, path: path.join(self.path, 'src', 'package.xml') });
return newPackage.init();
})
.then(function() {
self.packageXml = newPackage;
util.removeEmptyDirectoriesRecursiveSync(path.join(self.path, 'src'));
resolve();
})
.catch(function(error) {
reject(error);
})
.done();
});
};
/**
* Retrieves config/.settings from the disk
* @return {[type]} [description]
*/
Project.prototype._readSettings = function() {
try {
return fs.readJsonSync(path.join(this.path, 'config', '.settings'));
} catch(err) {
logger.error('Error reading settings -->', err);
throw new Error('Could not read settings: '+err.message);
}
};
/**
* Writes settings to disk, updates local settings store
*/
Project.prototype.writeSettings = function(settings) {
try {
for (var key in settings) {
this.settings[key] = settings[key];
}
fs.writeFileSync(path.join(this.path, 'config', '.settings'), JSON.stringify(this.settings, null, 4));
} catch(err) {
logger.error('Could not write settings', err);
throw new Error('Could not write settings', err);
}
};
// retrieves local_store from config/.local_store
Project.prototype.getLocalStore = function() {
var localStore;
try {
localStore = fs.readJsonSync(path.join(this.path, 'config', '.local_store'));
} catch(e) {
if (e.message.indexOf('Unexpected end of input') >= 0) {
localStore = {};
} else {
throw e;
}
}
return localStore;
};
Project.prototype.getDebugSettingsSync = function() {
var debugSettings;
try {
debugSettings = fs.readJsonSync(path.join(this.path, 'config', '.debug'));
} catch(e) {
if (e.message.indexOf('Unexpected end of input') >= 0) {
debugSettings = {};
} else {
throw e;
}
}
return debugSettings;
};
Project.prototype.setLightningIndex = function(index) {
var self = this;
return new Promise(function(resolve, reject) {
try {
fs.outputFileSync(path.join(self.path, 'config', '.lightning'), JSON.stringify(index, null, 4));
self.lightningIndex = index;
resolve();
} catch(err) {
logger.error('Could not write lightning index file -->'+err.message);
reject(err);
}
});
};
Project.prototype.getLightningIndexSync = function() {
var lightningIndex;
try {
lightningIndex = fs.readJsonSync(path.join(this.path, 'config', '.lightning'));
} catch(e) {
if (e.message.indexOf('Unexpected end of input') >= 0) {
lightningIndex = [];
} else {
throw e;
}
}
return lightningIndex;
};
Project.prototype.getLightningIndex = function() {
var self = this;
return new Promise(function(resolve, reject) {
try {
var lightningIndex = fs.readJsonSync(path.join(self.path, 'config', '.lightning'));
return resolve(lightningIndex);
} catch(err) {
logger.debug('could not get index the first time');
logger.debug(err);
// if err is empty/missing file, index it
self.indexLightning()
.then(function() {
logger.debug('done indexing lightning, now go get it');
var lightningIndex = fs.readJsonSync(path.join(self.path, 'config', '.lightning'));
return resolve(lightningIndex);
})
.catch(function(err) {
logger.error('Could not get lightning index -->'+err.message);
reject(err);
});
}
});
};
// retrieves describe from config/.describe
Project.prototype.getDescribe = function() {
return this._describe;
};
Project.prototype.setDescribe = function(describe) {
var self = this;
return new Promise(function(resolve, reject) {
var describePath = path.join(self.path, 'config', '.describe');
if (fs.existsSync(path.join(self.path, 'config'))) {
fs.outputFile(describePath, JSON.stringify(describe, null, 4), function(err) {
if (err) {
return reject(err);
} else {
self._describe = describe;
resolve();
}
});
} else {
self._describe = describe;
resolve();
}
});
};
Project.prototype._refreshDescribeFromServer = function() {
var self = this;
return new Promise(function(resolve, reject) {
self.sfdcClient.describe()
.then(function(res) {
return self.setDescribe(res);
})
.then(function() {
resolve();
})
.catch(function(error) {
reject(error);
})
.done();
});
};
Project.prototype.indexLightning = function() {
var self = this;
logger.debug('indexing lightning to config/.lightning');
return new Promise(function(resolve, reject) {
self.lightningService.getAll()
.then(function(res) {
return self.setLightningIndex(res);
})
.then(function() {
return resolve();
})
.catch(function(err) {
if (err.message.indexOf('sObject type \'AuraDefinition\' is not supported') >= 0 || err.message.indexOf('requested resource does not exist') >= 0) {
resolve();
} else {
logger.error('Could not index lightning -->'+err.message);
reject(err);
}
})
.done();
});
};
/**
* Indexes Apex symbols
* @return {Promise}
*/
Project.prototype.indexSymbols = function(apexClassName) {
var self = this;
return new Promise(function(resolve, reject) {
if (!fs.existsSync(path.join(self.path, 'config', '.symbols'))) {
fs.mkdirpSync(path.join(self.path, 'config', '.symbols'));
}
// todo: stash existing
var symbolPromise = apexClassName ? self.symbolService.indexApexClass(apexClassName) : self.symbolService.index();
symbolPromise
.then(function() {
logger.debug('done indexing symbols!');
resolve();
})
.catch(function(err) {
logger.error('Could not index apex symbols: '+err.message);
reject(err);
})
.done();
});
};
/**
* Populates project's config/.org_metadata with server metadata based on the projects subscription
* @return {Promise}
*/
Project.prototype.indexMetadata = function() {
var self = this;
return new Promise(function(resolve, reject) {
// todo: stash existing
if (!self.indexService) {
self.indexService = new IndexService({ project: self });
}
self.indexService.indexServerProperties(self.settings.subscription)
.then(function(res) {
fs.outputFile(path.join(self.path, 'config', '.org_metadata'), JSON.stringify(res, null, 4), function(err) {
if (err) {
reject(err);
} else {
resolve();
}
});
})
.catch(function(err) {
logger.error('Could not index metadataHelper: '+err.message);
reject(err);
})
.done();
});
};
Project.prototype.getOrgMetadataIndex = function() {
var self = this;
return new Promise(function(resolve, reject) {
fs.readJson(path.join(self.path, 'config', '.org_metadata'), function(err, orgMetadata) {
if (err) {
logger.debug('Could not return org metadata: '+err.message);
resolve([]);
} else {
resolve(orgMetadata);
}
});
});
};
Project.prototype.getOrgMetadataIndexWithSelections = function(keyword, ids, packageXmlPath) {
var self = this;
return new Promise(function(resolve, reject) {
logger.debug('getOrgMetadataIndexWithSelections, package location: ', packageXmlPath);
if (fs.existsSync(path.join(self.path, 'config', '.org_metadata'))) {
try {
fs.readJson(path.join(self.path, 'config', '.org_metadata'), function(err, orgMetadata) {
if (err) {
reject(err);
} else {
self.orgMetadata = orgMetadata;
var promise;
var customPackage;
if (packageXmlPath) {
customPackage = new Package({ path: packageXmlPath });
promise = customPackage.init();
} else {
promise = Promise.resolve();
}
promise
.then(function() {
if (!ids) {
ids = [];
var pkg = packageXmlPath ? customPackage : self.packageXml;
_.forOwn(pkg.subscription, function(packageMembers, metadataTypeXmlName) {
var metadataType = self.metadataHelper.getTypeByXmlName(metadataTypeXmlName); //inFolder, childXmlNames
if (!metadataType) {
return reject(new Error('Unrecognized package.xml metadata type: '+metadataTypeXmlName));
}
if (_.has(metadataType, 'parentXmlName')) {
var parentMetadataType = self.metadataHelper.getTypeByXmlName(metadataType.parentXmlName);
}
if (packageMembers === '*') {
ids.push(metadataTypeXmlName);
var indexedType = _.find(orgMetadata, { 'xmlName': metadataTypeXmlName });
if (_.has(indexedType, 'children')) {
_.each(indexedType.children, function(child) {
child.select = true;
});
}
} else {
_.each(packageMembers, function(member) {
if (metadataType.inFolder) {
// id : Document.FolderName.FileName.txt
ids.push([metadataTypeXmlName, member.replace(/\//, '.')].join('.'));
} else if (parentMetadataType) {
// id : CustomObject.Object_Name__c.fields.Field_Name__c
var id = [ parentMetadataType.xmlName, member.split('.')[0], metadataType.tagName, member.split('.')[1] ].join('.');
ids.push(id);
} else if (_.has(metadataType, 'childXmlNames')) {
var indexedType = _.find(orgMetadata, { 'xmlName': metadataTypeXmlName });
if (indexedType) {
var indexedNode = _.find(indexedType.children, { 'id': [metadataTypeXmlName, member].join('.')});
if (_.has(indexedNode, 'children')) {
_.each(indexedNode.children, function(child) {
child.select = true;
if (_.has(child, 'children')) {
_.each(child.children, function(grandChild) {
grandChild.select = true;
});
}
});
}
ids.push([metadataTypeXmlName, member].join('.'));
}
} else {
// id: ApexClass.MyClassName
ids.push([metadataTypeXmlName, member].join('.'));
}
});
}
});
}
if (!self.indexService) {
self.indexService = new IndexService({ project: self });
}
self.indexService.setChecked(orgMetadata, ids);
self.indexService.ensureParentsAreCheckedIfNecessary(orgMetadata);
if (keyword) {
self.indexService.setVisibility(orgMetadata, keyword);
}
resolve(orgMetadata);
});
}
});
} catch(err) {
logger.debug('Could not getOrgMetadataIndexWithSelections: '+err.message);
resolve([]);
}
} else {
logger.debug('org_metadata not found, returning empty array');
resolve([]);
}
});
};
Project.prototype.hasIndexedMetadata = function() {
return _.isArray(this.orgMetadata) && this.orgMetadata.length > 0;
};
Project.prototype.updateLocalStore = function(fileProperties) {
var self = this;
return new Promise(function(resolve, reject) {
Promise.resolve(fileProperties).then(function (properties) {
if (!_.isArray(properties)) {
properties = [properties];
}
try {
var store = self.getLocalStore();
_.each(properties, function(fp) {
if (fp.attributes) {
fp = normalize(fp);
}
var metadataType;
if (fp.type) {
metadataType = self.metadataHelper.getTypeByXmlName(fp.type);
} else if (fp.attributes && fp.attributes.type) {
metadataType = self.metadataHelper.getTypeByXmlName(fp.attributes.type);
fp.fullName = fp.name;
fp.fileName = ['unpackaged', metadataType.directoryName, fp.name +'.'+metadataType.suffix].join('/');
fp.createdByName = fp.createdBy.name;
fp.lastModifiedByName = fp.lastModifiedBy.name;
fp.manageableState = !fp.namespacePrefix ? 'unmanaged' : 'managed';
fp.namespacePrefix = fp.namespacePrefix;
fp.type = metadataType.xmlName;
delete fp.createdBy;
delete fp.lastModifiedBy;
delete fp.attributes;
} else {
metadataType = self.metadataHelper.getTypeByPath(fp.fileName.split('.')[1]);
}
logger.debug(metadataType);
if (metadataType && fp.attributes) {
var key = fp.name+'.'+metadataType.suffix;
var value = fp;
value.mmState = 'clean';
store[key] = value;
} else if (metadataType && fp.fullName.indexOf('package.xml') === -1) {
var key = fp.fullName+'.'+metadataType.suffix;
var value = fp;
value.mmState = 'clean';
store[key] = value;
} else {
if (fp.fullName.indexOf('package.xml') === -1) {
logger.debug('Could not determine metadata type for: '+JSON.stringify(fp));
}
}
});
var filePath = path.join(self.path, 'config', '.local_store');
fs.outputFile(filePath, JSON.stringify(store, null, 4), function(err) {
if (err) {
logger.error('Could not write local store: '+err.message);
reject(err);
} else {
resolve();
}
});
} catch(err) {
logger.error('Could not update local store -->'+err.message);
reject(err);
}
});
});
};
Project.prototype._writeLocalStore = function(fileProperties) {
var self = this;
return new Promise(function(resolve, reject) {
logger.debug('writing to local store');
Promise.resolve(fileProperties)
.then(function (properties) {
try {
if (!_.isArray(properties)) {
properties = [properties];
}
logger.debug('writing local store...');
logger.silly(properties);
var store = {};
_.each(properties, function(fp) {
if (!self.metadataHelper) {
self.metadataHelper = new MetadataHelper({ sfdcClient: self.sfdcClient });
}
var metadataType = self.metadataHelper.getTypeByPath(fp.fileName);
logger.silly(metadataType);
if (metadataType !== undefined && fp.fullName.indexOf('package.xml') === -1) {
var key = fp.fullName+'.'+metadataType.suffix;
var value = fp;
value.mmState = 'clean';
store[key] = value;
} else {
if (fp.fullName.indexOf('package.xml') === -1) {
logger.warn('Could not determine metadata type for: '+JSON.stringify(fp));
}
}
});
var filePath = path.join(self.path, 'config', '.local_store');
fs.outputFile(filePath, JSON.stringify(store, null, 4), function(err) {
if (err) {
reject(new Error('Could not write local store: '+err.message));
} else {
resolve();
}
});
} catch(err) {
logger.error('Could not initiate local store', err);
reject(err);
}
})
.catch(function(err) {
logger.error('fileproperties promise rejected', err);
reject(err);
});
});
};
/**
* Updates project debug settings
* @param {String} key - setting key you'd like to override
* @param {Object} value - value to override
* @return {Promise} [description]
*/
Project.prototype._updateDebug = function(key, value) {
var self = this;
return new Promise(function(resolve, reject) {
logger.debug('updating debug setting ['+key+']');
logger.debug(value);
var debug;
try {
debug = fs.readJsonSync(path.join(self.path, 'config', '.debug'));
} catch(err) {
reject(new Error('Could not read project .debug file: '+err.message));
}
debug[key] = value;
logger.debug('Updating project debug: ');
logger.debug(debug);
try {
fs.writeFileSync(path.join(self.path, 'config', '.debug'), JSON.stringify(debug, null, 4));
resolve();
} catch(err) {
logger.error('Could not write project .debug file -->'+err.message);
reject(err);
}
});
};
/**
* Writes config/.debug to the project on creation
* @return {Promise}
*/
Project.prototype._writeDebug = function() {
var self = this;
return new Promise(function(resolve, reject) {
var debug = {
users: [self.sfdcClient.getUserId()],
logType: 'USER_DEBUG',
debugLevelName: 'MAVENSMATE',
levels: {
Workflow: 'INFO',
Callout: 'INFO',
System: 'DEBUG',
Database: 'INFO',
ApexCode: 'DEBUG',
ApexProfiling: 'INFO',
Validation: 'INFO',
Visualforce: 'DEBUG'
},
expiration: 480
};
var filePath = path.join(self.path, 'config', '.debug');
fs.outputFile(filePath, JSON.stringify(debug, null, 4), function(err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
/**
* Writes editor-specific config to the project root
* @return {Promise}
*/
Project.prototype._writeEditorSettings = function() {
var self = this;
return new Promise(function(resolve, reject) {
// TODO: right now these are written to every project root, regardless of editor
/*jshint camelcase: false */
var sublimeSettings = {
folders : [
{
"folder_exclude_patterns": [
"config/.symbols"
],
path : '.'
}
],
settings : {
auto_complete_triggers : [
{
characters: '.',
selector: 'source - comment'
},
{
characters: ':',
selector: 'text.html - comment'
},
{
characters: '<',
selector: 'text.html - comment'
},
{
characters: ' ',
selector: 'text.html - comment'
}
]
}
};
/*jshint camelcase: true */
var filePath = path.join( self.path, [ self.name, 'sublime-project' ].join('.') );
fs.outputFile(filePath, JSON.stringify(sublimeSettings, null, 4), function(err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
/**
* Attaches listener to sfdcClient that updates the local token store on refresh
* @return {Nothing}
*/
Project.prototype._listenForTokenUpdates = function() {
var self = this;
if (self.sfdcClient.listeners('token-refresh').length === 0) {
self.sfdcClient.on('token-refresh', function() {
logger.debug('handling sfdcClient:token-refresh');
try {
self._writeCredentials(true);
} catch(err) {
logger.error('Could not store updated credentials', err);
throw err;
}
});
}
};
/**
* Updates local credentials (in .settings and .credentials)
* @param {Object} creds
* @return {Promise}
*/
Project.prototype.updateCredentials = function(creds) {
var self = this;
logger.debug('updating project creds', creds);
return new Promise(function(resolve, reject) {
var username = creds.username || self.settings.username;
var password = creds.password;
var accessToken = creds.accessToken;
var refreshToken = creds.refreshToken;
var orgType = creds.orgType || self.settings.environment || self.settings.orgType;
var loginUrl = creds.loginUrl || self.settings.loginUrl;
var instanceUrl = creds.instanceUrl;
if (username && password) {
self.sfdcClient = new SalesforceClient({
username: username,
password: password,
orgType: orgType,
loginUrl: loginUrl,
instanceUrl: instanceUrl
});
} else {
self.sfdcClient = new SalesforceClient({
username: username,
accessToken: accessToken,
refreshToken: refreshToken,
orgType: orgType,
loginUrl: loginUrl,
instanceUrl: instanceUrl
});
}
self.sfdcClient.initialize()
.then(function() {
self._writeCredentials(true);
self.writeSettings({
username: username,
orgType: orgType,
loginUrl: loginUrl,
instanceUrl: instanceUrl
});
return self._updateDebug('users', [self.sfdcClient.getUserId()])
})
.then(function() {
if (self.requiresAuthentication) {
logger.debug('project required authentication, running init again');
return self._initExisting();
} else {
return new Promise(function(res) { res(); });
}
})
.then(function() {
self._listenForTokenUpdates();
resolve();
})
.catch(function(err) {
logger.error('Could not update credentials -->'+err.message);
reject(err);
})
.done();
});
};
/**
* Writes accessToken/refreshToken to either the disk or the project's config/.credentials file
* @param {[type]} pw [description]
* @param {[type]} replace [description]
* @return {[type]} [description]
*/
Project.prototype._writeCredentials = function(replace) {
var self = this;
try {
logger.debug('_writeCredentials');
if (self.keychainService.useSystemKeychain()) {
logger.debug('storing credentials in system keychain');
var keychainAction = replace ? 'replacePassword' : 'storePassword';
if (self.sfdcClient.password) {
self.keychainService[keychainAction](self.id || self.settings.id, self.sfdcClient.password, 'password');
} else {
self.keychainService[keychainAction](self.id || self.settings.id, self.sfdcClient.accessToken, 'accessToken');
self.keychainService[keychainAction](self.id || self.settings.id, self.sfdcClient.refreshToken, 'refreshToken');
}
logger.debug('removing local .credentials store if it exists');
if (fs.existsSync(path.join(self.path, 'config', '.credentials'))) {
fs.removeSync(path.join(self.path, 'config', '.credentials'));
}
} else {
logger.debug('storing credentials in config/.credentials');
if (self.sfdcClient.password) {
fs.writeFileSync(path.join(self.path, 'config', '.credentials'), JSON.stringify({
password: self.sfdcClient.password
}, null, 4));
} else {
fs.writeFileSync(path.join(self.path, 'config', '.credentials'), JSON.stringify({
accessToken: self.sfdcClient.accessToken,
refreshToken: self.sfdcClient.refreshToken
}, null, 4));
}
}
} catch(err) {
logger.error('Error writing credentials -->', err);
throw new Error('Could not write credentials: '+err.message);
}
};
/**
* Retrieves access/refresh credentials from config/.credentials or the keychain
* @return {Promise}
*/
Project.prototype._readCredentials = function() {
try {
if (fs.existsSync(path.join(this.path, 'config', '.credentials'))) {
return fs.readJsonSync(path.join(this.path, 'config', '.credentials'));
} else {
return {
accessToken: this.keychainService.getPassword(this.settings.id, 'accessToken', true),
refreshToken: this.keychainService.getPassword(this.settings.id, 'refreshToken', true),
password: this.keychainService.getPassword(this.settings.id, 'password', true)
}
}
} catch(err) {
logger.error('Error reading credentials -->', err);
throw new Error('Could not read credentials for project '+this.name+': '+err.message);
}
};
/**
* Writes result of a SOQL query to the project's soql/ directory
* @return {None}
*/
Project.prototype.writeSoqlResult = function(res) {
var soqlFileName = [moment().format('YYYY-MM-DD HH-mm-ss'), 'json'].join('.');
var filePath = path.join(this.path, 'soql', soqlFileName);
fs.outputFileSync(filePath, JSON.stringify(res, null, 4));
return filePath;
};
module.exports = Project;