Skip to content
This repository has been archived by the owner on Oct 23, 2022. It is now read-only.

Commit

Permalink
improve handling of certain FileProperties
Browse files Browse the repository at this point in the history
* refactor fetching and parsing describe information from remotes
* determine active Flow version by running a SOQL query (fixes #21)
* identify PersonAccount RecordTypes listed as Account RecordTypes by running a SOQL query
* retrieve api versions from remote
  • Loading branch information
amtrack committed Aug 8, 2016
1 parent 90650bf commit fbe6ba1
Show file tree
Hide file tree
Showing 14 changed files with 776 additions and 405 deletions.
3 changes: 1 addition & 2 deletions README.md
Expand Up @@ -43,8 +43,7 @@ https://mynamespace.my.salesforce.com/secur/frontdoor.jsp?sid=REDACTED
```console
$ force-dev-tool fetch
Fetching from remote mydev
Created config/mydev-describe-metadata-result.json
Created config/mydev-manifest.json
Created config/mydev-fetch-result.json
Fetching remotes finished.
$ force-dev-tool package -a
Created src/package.xml
Expand Down
30 changes: 9 additions & 21 deletions lib/cli/fetch.js
Expand Up @@ -65,27 +65,15 @@ SubCommand.prototype.process = function(proc, callback) {
if (err) {
return remoteCallback(prefix + err);
}
var manifestDir = path.dirname(self.project.getManifestPath(remoteName));
if (!fs.existsSync(manifestDir)) {
fs.mkdirSync(manifestDir);
}
if (result.warnings && result.warnings.length > 0) {
console.log(chalk.yellow(result.warnings.join('\n')));
}
if (result.describeMetadataResult) {
var describeMetadataResultPath = self.project.getConfigPath(remoteName) + '-describe-metadata-result.json';
fs.writeFileSync(describeMetadataResultPath, JSON.stringify(result.describeMetadataResult));
console.log("Created " + chalk.cyan(path.relative(proc.cwd, describeMetadataResultPath)));
}
if (result.manifest) {
var manifestPath = self.project.getManifestPath(remoteName);
fs.writeFile(manifestPath, JSON.stringify(result.manifest), function(writeErr) {
if (writeErr) {
return remoteCallback(prefix + writeErr);
}
console.log("Created " + chalk.cyan(path.relative(proc.cwd, manifestPath)));
return remoteCallback(null);
});
if (result) {
var storagePath = self.project.storage.getConfigPath()
if (!fs.existsSync(storagePath)) {
fs.mkdirSync(storagePath);
}
var fetchResultPath = self.project.getFetchResultPath(remoteName);
fs.writeFileSync(fetchResultPath, JSON.stringify(result, null, ' '));
console.log("Created " + chalk.cyan(path.relative(proc.cwd, fetchResultPath)));
return remoteCallback(null);
} else {
return remoteCallback(prefix + "Fetching failed.");
}
Expand Down
18 changes: 10 additions & 8 deletions lib/cli/package.js
Expand Up @@ -3,9 +3,10 @@
var Command = require('./command');
var config = new(require('../config'))();
var Manifest = require('../manifest');
var FetchResultParser = require('../fetch-result-parser');
var CliUtils = require('./utils');
var path = require("path");
var _ = require("underscore");
var chalk = require('chalk');

var doc = "Usage:\n" +
" force-dev-tool package fmt [options]\n" +
Expand Down Expand Up @@ -38,14 +39,15 @@ SubCommand.prototype.process = function(proc, callback) {
self.currentPackageXml = Manifest.fromPackageXml(CliUtils.readFileSafe(templatePath));
return CliUtils.writePackageXml(self.currentPackageXml, targetPath, false, callback);
}
var manifest = self.project.getManifest(self.opts['<remote>']);
// filter installed metadata components
var unpackagedManifestJSON = _.filter(manifest.manifest(), function(item) {
return item.manageableState !== 'installed' && item.type !== 'InstalledPackage';
});
manifest = new Manifest({
manifestJSON: unpackagedManifestJSON
var fetchResult = new FetchResultParser(CliUtils.readJsonFile(self.project.getFetchResultPath(self.opts['<remote>'])));
var manifest = new Manifest({
manifestJSON: fetchResult.getComponents()
});
var warnings = fetchResult.getWarnings();
if (warnings && warnings.length > 0) {
console.log(chalk.yellow(warnings.join('\n')));
}

try {
self.currentPackageXml = Manifest.fromPackageXml(CliUtils.readFileSafe(templatePath));
} catch (parseErr) {
Expand Down
157 changes: 100 additions & 57 deletions lib/describe-remote.js
Expand Up @@ -59,83 +59,126 @@ DescribeRemote.prototype.listMetadata = function(queries, callback) {
);
};

/**
* Retrieve FileProperties[] for all MetadataComponents in two steps.
* First list folders, then list all MetadataComponents including those in folders.
* @return {FileProperties[]}
*/
DescribeRemote.prototype.fetch = function(callback) {
var self = this;
self._conn.metadata.pollTimeout = config.pollTimeout;
var types = [];
var manifest = [];

// intermediate results
var describeMetadataResult = {};
var warnings = [];

async.series({
apiVersions: function(cb) {
self._conn.request({
method: 'get',
url: '/services/data',
headers: {
'Content-Type': 'application/json'
}
}, {}, function(err, response) {
if (err) {
warnings.push('Error retrieving api versions from /services/data: ' + err);
return cb(null, []);
}
return cb(null, response);
});
},
describeMetadataResult: function(cb) {
self._conn.metadata.describe(function(describeErr, result) {
if (describeErr) {
return cb(describeErr);
// DescribeMetadataResult
// https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_describemeta_result.htm
self._conn.metadata.describe(function(err, result) {
if (err) {
return cb(err);
}
types = result;
// store this to be able to reuse later
describeMetadataResult = result;
cb(null, result);
});
},
manifest: function(cb) {
var metadataTypeNames = _.pluck(types.metadataObjects, 'xmlName');
var childXmlNames = _.pluck(_.filter(types.metadataObjects, function(type) {
return type.childXmlNames;
}), 'childXmlNames');
metadataTypeNames = _.flatten([].concat(metadataTypeNames, childXmlNames));
var queries = metadataTypeNames.map(function(item) {
return {
type: item
};
});
var folderBasedMetadataMap = config.get('folderBasedMetadataMap');
var folderBasedMetadataTypes = Object.keys(folderBasedMetadataMap);
var inFolderQueries = folderBasedMetadataTypes.map(function(item) {
return {
type: item
};
});
if (self.opts.ignorePatterns) {
inFolderQueries = DescribeRemote.getNotIgnoredMatches(inFolderQueries, self.opts.ignorePatterns);
}
self.listMetadata(inFolderQueries, function(inFolderErr, foldersManifestJSON) {
if (inFolderErr) {
return cb(inFolderErr);
}
if (foldersManifestJSON) {
var folderBasedQueries = foldersManifestJSON.map(function(item) {
fileProperties: function(cbFileProperties) {
// FileProperties[]
var folderBasedQueries = [];
var filePropertyQueries = [];
async.series([
function(cbFolder) {
// folders
var metadataTypeNames = _.pluck(describeMetadataResult.metadataObjects, 'xmlName');
var childXmlNames = _.pluck(_.filter(describeMetadataResult.metadataObjects, function(type) {
return type.childXmlNames;
}), 'childXmlNames');
metadataTypeNames = _.flatten([].concat(metadataTypeNames, childXmlNames));
filePropertyQueries = metadataTypeNames.map(function(item) {
return {
type: item
};
});
var folderBasedMetadataMap = config.get('folderBasedMetadataMap');
var folderBasedMetadataTypes = Object.keys(folderBasedMetadataMap);
var queries = folderBasedMetadataTypes.map(function(item) {
return {
type: folderBasedMetadataMap[item.type],
folder: item.fullName
type: item
};
});
queries = [].concat(queries, folderBasedQueries);
if (self.opts.ignorePatterns) {
queries = DescribeRemote.getNotIgnoredMatches(queries, self.opts.ignorePatterns);
}
self.listMetadata(queries, function(err, result) {
if (err) {
return cbFolder(err);
}
// store this to be able to reuse later
folderBasedQueries = result.map(function(item) {
return {
type: folderBasedMetadataMap[item.type],
folder: item.fullName
};
});
return cbFolder(null, result);
});
},
function(cbComponents) {
// components
var queries = [].concat(folderBasedQueries, filePropertyQueries); // from previous step
if (self.opts.ignorePatterns) {
queries = DescribeRemote.getNotIgnoredMatches(queries, self.opts.ignorePatterns);
}
self.listMetadata(queries, function(err, result) {
if (err) {
return cbComponents(err);
}
cbComponents(null, result);
});
}
if (self.opts.ignorePatterns) {
queries = DescribeRemote.getNotIgnoredMatches(queries, self.opts.ignorePatterns);
], function(err, results) {
if (err) {
return cbFileProperties(err);
}
self.listMetadata(queries, function(err, manifestJSON) {
if (err) {
return cb(err);
}
manifest = [].concat(foldersManifestJSON, manifestJSON);
cb(null, manifest);
});
return cbFileProperties(null, _.flatten(results));
});
},
warnings: function(cb) {
manifest.forEach(function(component) {
if (component.type === 'QuickAction' && new RegExp('^09D26.*').test(component.id)) {
warnings.push('Warning: Found non-global QuickAction: ' + component.fullName);
personAccountRecordTypes: function(cb) {
self._conn.query("SELECT Name, SobjectType, IsPersonType FROM RecordType WHERE SobjectType='Account' AND IsPersonType=True", function(err, result) {
// ignore errors here since the query only succeeds when PersonAccounts are enabled
if (err || !result.records) {
return cb(null, []);
}
if (component.type === 'Flow' && !new RegExp('^.*-[0-9]+$').test(component.fullName)) {
warnings.push('Warning: Found non-versioned Flow: ' + component.fullName);
return cb(null, result.records);
});
},
flowDefinitions: function(cb) {
self._conn.tooling.query("SELECT DeveloperName, ActiveVersion.VersionNumber FROM FlowDefinition", function(err, result) {
// don't fail on error
if (err) {
warnings.push('Warning: ' + err);
return cb(null, []);
} else if (!result.records) {
return cb(null, []);
}
return cb(null, result.records);
});
cb(null, warnings);
},
warnings: function(cb) {
return cb(null, warnings);
}
},
callback
Expand Down
127 changes: 127 additions & 0 deletions lib/fetch-result-parser.js
@@ -0,0 +1,127 @@
"use strict";

var _ = require('underscore');
var config = new(require('./config'))();
var MetadataComponent = require('./metadata-component');

var folderBasedMetadataMap = config.get('folderBasedMetadataMap');

var getFolderTypes = function() {
var folderTypes = [];
Object.keys(folderBasedMetadataMap).forEach(function(key) {
folderTypes.push(key);
folderTypes.push(folderBasedMetadataMap[key]);
});
return folderTypes;
}

var FetchResultParser = module.exports = function(resultJSON) {
var self = this;
resultJSON = JSON.parse(JSON.stringify(resultJSON));
self.apiVersions = resultJSON.apiVersions || [];
self.describeMetadataResult = resultJSON.describeMetadataResult || {};
self.fileProperties = resultJSON.fileProperties || [];
self.personAccountRecordTypes = resultJSON.personAccountRecordTypes || [];
self.flowDefinitions = resultJSON.flowDefinitions || [];
self.warnings = resultJSON.warnings || [];
};

FetchResultParser.prototype.getApiVersion = function() {
var self = this;
var versions = self.apiVersions;
if (Array.isArray(versions) && versions.length) {
return versions[versions.length - 1];
}
return null;
};

FetchResultParser.prototype.getComponents = function(opts) {
var self = this;
opts = opts ? opts : {};
opts.filterManaged = opts.filterManaged !== undefined ? opts.filterManaged : true;
var components = [];
if (opts.filterManaged) {
self.filterManaged();
}
self.transform();
self.filterInvalid();
self.fileProperties.forEach(function(fileProperty) {
components.push(
new MetadataComponent({
type: fileProperty.type,
fullName: fileProperty.fullName,
fileName: fileProperty.fileName
})
);
});
return components;
};

FetchResultParser.prototype.getWarnings = function() {
return this.warnings;
}

FetchResultParser.prototype.filterManaged = function() {
var self = this;
// filter installed metadata components
self.fileProperties = _.filter(self.fileProperties, function(fileProperty) {
return fileProperty.manageableState !== 'installed' && fileProperty.type !== 'InstalledPackage';
});
return self;
}

FetchResultParser.prototype.transform = function() {
var self = this;
self.fileProperties = self.fileProperties.map(function(fileProperty) {
if (Object.keys(folderBasedMetadataMap).indexOf(fileProperty.type) > -1) {
// DocumentFolder has to be listed as Document
fileProperty.type = folderBasedMetadataMap[fileProperty.type];
} else if (fileProperty.type === 'Flow' && !new RegExp('^.*-[0-9]+$').test(fileProperty.fullName) && self.flowDefinitions) {
// determine the active version number using the FlowDefinition Metadata
var flowDefinitionMatch = _.findWhere(self.flowDefinitions, {
DeveloperName: fileProperty.fullName
});
if (flowDefinitionMatch && flowDefinitionMatch.ActiveVersion && flowDefinitionMatch.ActiveVersion.VersionNumber) {
fileProperty.fullName = fileProperty.fullName + '-' + flowDefinitionMatch.ActiveVersion.VersionNumber;
}
} else if (fileProperty.type === 'RecordType' && self.personAccountRecordTypes) {
// PersonAccount RecordTypes are being listed incorrectly as Account RecordTypes
var fullNameParts = fileProperty.fullName.split('.');
var itemType = fullNameParts[0];
var itemName = fullNameParts[1];
var personAccountRecordTypeMatch = _.findWhere(self.personAccountRecordTypes, {
Name: itemName,
SobjectType: itemType,
IsPersonType: true
});
if (personAccountRecordTypeMatch) {
fileProperty.fullName = 'PersonAccount.' + itemName;
}
}
return fileProperty;
});
return self;
};

FetchResultParser.prototype.filterInvalid = function() {
var self = this;
var folderTypes = getFolderTypes();
self.fileProperties = _.filter(self.fileProperties, function(fileProperty) {
if (folderTypes.indexOf(fileProperty.type) > -1 && fileProperty.fullName === 'unfiled$public') {
// we don't consider this as a warning
// self.warnings.push('Warning: Skipped standard ' + fileProperty.type + ': ' + fileProperty.fullName);
return false;
} else if (fileProperty.type === 'QuickAction' && new RegExp('^09D26.*').test(fileProperty.id)) {
self.warnings.push('Warning: Skipped non-global QuickAction: ' + fileProperty.fullName);
return false;
} else if (fileProperty.type === 'Flow' && !new RegExp('^.*-[0-9]+$').test(fileProperty.fullName)) {
self.warnings.push('Warning: Skipped non-versioned Flow: ' + fileProperty.fullName);
return false;
} else if (fileProperty.type === 'CustomObject' && fileProperty.fullName === 'PersonAccount') {
// self.warnings.push('Warning: Skipped CustomObject: ' + fileProperty.fullName + ', though children of this custom object will remain.');
return false;
}
return true;
});
return self;
};

0 comments on commit fbe6ba1

Please sign in to comment.