salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
309 lines (307 loc) • 17 kB
JavaScript
;
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
// Node
const util = require("util");
// 3pp
const BBPromise = require("bluebird");
const _ = require("lodash");
const core_1 = require("@salesforce/core");
const fs_extra_1 = require("fs-extra");
// Local
const source_deploy_retrieve_1 = require("@salesforce/source-deploy-retrieve");
const core_2 = require("@salesforce/core");
const srcDevUtil = require("../core/srcDevUtil");
const Messages = require("../messages");
const almError = require("../core/almError");
const MetadataRegistry = require("./metadataRegistry");
const workspaceFileState_1 = require("./workspaceFileState");
const sourceUtil_1 = require("./sourceUtil");
const messages = Messages();
const metadataTypeFactory_1 = require("./metadataTypeFactory");
const sourceWorkspaceAdapter_1 = require("./sourceWorkspaceAdapter");
const aggregateSourceElements_1 = require("./aggregateSourceElements");
const sourceElementsResolver_1 = require("./sourceElementsResolver");
const ManifestCreateApi = require("./manifestCreateApi");
class SourceConvertApi {
constructor(org, swa) {
this.sourceWorkspaceAdapter = swa;
this.scratchOrg = org;
this.projectDir = this.scratchOrg.config.getProjectPath();
this.forceIgnore = source_deploy_retrieve_1.ForceIgnore.findAndCreate(core_1.SfdxProject.resolveProjectPathSync());
}
/**
* Takes an array of strings, surrounds each string with single quotes, then joins the values.
* Used for building a query condition. E.g., WHERE MemberName IN ('Foo','Bar')
* The server uses '/' file path separators, but on windows we could be passed 'Reports\ReportA' we need to change this
*/
static singleQuoteJoin(arr) {
return arr.map((val) => `'${val.replace('\\', '/')}'`).join();
}
async initWorkspaceAdapter() {
if (!this.sourceWorkspaceAdapter) {
const options = {
org: this.scratchOrg,
metadataRegistryImpl: MetadataRegistry,
defaultPackagePath: this.scratchOrg.config.getAppConfig().defaultPackagePath,
fromConvert: true,
};
this.sourceWorkspaceAdapter = await sourceWorkspaceAdapter_1.SourceWorkspaceAdapter.create(options);
}
}
// Convert files in source format to mdapi format and create a package.xml.
async doConvert(context) {
const { rootDir, manifest, metadata, sourcepath, outputDir, packagename, unsupportedMimeTypes } = context;
await this.initWorkspaceAdapter();
try {
await core_1.fs.access(rootDir, core_1.fs.constants.R_OK);
}
catch (err) {
// Throw a more helpful error when the rootDir is invalid; otherwise rethrow.
if (err.code === 'ENOENT') {
throw new core_2.SfdxError(messages.getMessage('invalidRootDirectory', [rootDir], 'sourceConvertCommand'));
}
throw err;
}
const sourceElementsResolver = new sourceElementsResolver_1.SourceElementsResolver(this.scratchOrg, this.sourceWorkspaceAdapter);
let sourceElements = new aggregateSourceElements_1.AggregateSourceElements();
if (manifest) {
sourceElements = await sourceElementsResolver.getSourceElementsFromManifest(manifest);
}
else if (sourcepath) {
sourceElements = await sourceUtil_1.getSourceElementsFromSourcePath(sourcepath, this.sourceWorkspaceAdapter);
}
else if (metadata) {
sourceElements = await sourceElementsResolver.getSourceElementsFromMetadata(context, new aggregateSourceElements_1.AggregateSourceElements());
}
else {
sourceElements = await this.sourceWorkspaceAdapter.getAggregateSourceElements(false, rootDir);
}
if (sourceElements.isEmpty()) {
throw new Error(messages.getMessage('noSourceInRootDirectory', [rootDir], 'sourceConvertCommand'));
}
return this.convertSourceToMdapi(outputDir, packagename, sourceElements, false, unsupportedMimeTypes);
}
async convertSourceToMdapi(targetPath, packageName, aggregateSourceElementsMap, createDestructiveChangesManifest, unsupportedMimeTypes, isSourceDelete) {
let destructiveChangesTypeNamePairs = [];
let sourceElementsForMdDir;
return this.initWorkspaceAdapter()
.then(() => aggregateSourceElementsMap.getAllSourceElements())
.then((aggregateSourceElements) => {
[destructiveChangesTypeNamePairs, sourceElementsForMdDir] = SourceConvertApi.sortSourceElementsForMdDeploy(aggregateSourceElements, this.sourceWorkspaceAdapter.metadataRegistry);
return SourceConvertApi.populateMdDir(targetPath, sourceElementsForMdDir, unsupportedMimeTypes, this.forceIgnore);
})
.then(() => {
if (createDestructiveChangesManifest && destructiveChangesTypeNamePairs.length) {
// Build a tooling query for all SourceMembers with MemberNames matching the locally deleted names.
const deletedMemberNames = _.map(destructiveChangesTypeNamePairs, 'name');
const conditions = `MemberName IN (${SourceConvertApi.singleQuoteJoin(deletedMemberNames)})`;
const fields = ['MemberType', 'MemberName', 'IsNameObsolete'];
if (isSourceDelete) {
return SourceConvertApi.createPackageManifests(targetPath, packageName, destructiveChangesTypeNamePairs, sourceElementsForMdDir, this.scratchOrg, this.sourceWorkspaceAdapter.metadataRegistry);
}
return this.scratchOrg.force
.toolingFind(this.scratchOrg, 'SourceMember', conditions, fields)
.then((sourceMembers) => {
if (!sourceMembers.length) {
// No members exist on the server (i.e., empty scratch org) so don't try to delete anything.
destructiveChangesTypeNamePairs = [];
}
else {
// Filter destructive changes to only the members found on the server that haven't already been deleted.
destructiveChangesTypeNamePairs = _.filter(destructiveChangesTypeNamePairs, (removal) => _.some(sourceMembers, {
MemberType: removal.type,
MemberName: removal.name,
IsNameObsolete: false,
}));
}
})
.then(() => {
destructiveChangesTypeNamePairs.forEach((destructive) => {
// On windows, the name could be 'Report\\ReportA', so we need to change to match what the server wants
destructive.name.replace('\\\\', '/');
});
})
.then(() => SourceConvertApi.createPackageManifests(targetPath, packageName, destructiveChangesTypeNamePairs, sourceElementsForMdDir, this.scratchOrg, this.sourceWorkspaceAdapter.metadataRegistry));
}
return SourceConvertApi.createPackageManifests(targetPath, packageName, [], sourceElementsForMdDir, this.scratchOrg, this.sourceWorkspaceAdapter.metadataRegistry);
})
.then(() => [sourceElementsForMdDir, destructiveChangesTypeNamePairs]);
}
/**
* Sorts the source elements into those that should be added to the destructiveChangesPost.xml
* and those that should be added to the package.xml
*
* @returns {[[],[]]} - the array of destructive changes and the array of elements to be added to the package.xml
* @private
*/
static sortSourceElementsForMdDeploy(aggregateSourceElements, metadataRegistry) {
const destructiveChangeTypeNamePairs = [];
const updatedSourceElements = [];
aggregateSourceElements.forEach((aggregateSourceElement) => {
if (aggregateSourceElement.isDeleted()) {
if (!aggregateSourceElement.getMetadataType().deleteSupported(aggregateSourceElement.getAggregateFullName())) {
return;
}
// if the whole source element should be deleted, then there's no need to process each pending workspace file
destructiveChangeTypeNamePairs.push({
type: aggregateSourceElement.getMetadataName(),
name: aggregateSourceElement.getAggregateFullName(),
});
}
else {
let aggregateSourceElementWasChanged = false;
const aggregateMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMetadataName(aggregateSourceElement.getMetadataName(), metadataRegistry);
if (!aggregateMetadataType.hasIndividuallyAddressableChildWorkspaceElements()) {
aggregateSourceElementWasChanged = true;
}
else {
aggregateSourceElement.getWorkspaceElements().forEach((workspaceElement) => {
const workspaceElementMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMetadataName(workspaceElement.getMetadataName(), metadataRegistry);
if (workspaceElement.getDeleteSupported() &&
workspaceElement.getState() === workspaceFileState_1.WorkspaceFileState.DELETED &&
workspaceElementMetadataType.isAddressable()) {
destructiveChangeTypeNamePairs.push({
type: workspaceElement.getMetadataName(),
name: workspaceElement.getFullName(),
});
}
else {
aggregateSourceElementWasChanged = true;
}
});
}
if (aggregateSourceElementWasChanged) {
updatedSourceElements.push(aggregateSourceElement);
}
}
});
return [destructiveChangeTypeNamePairs, updatedSourceElements];
}
static async populateMdDir(targetPath, aggregateSourceElements, unsupportedMimeTypes, forceIgnore) {
// Create the metadata deploy root directory
srcDevUtil.ensureDirectoryExistsSync(targetPath);
const decompositionDir = await sourceUtil_1.createOutputDir('decomposition');
const translationsMap = {};
const preDeployHookInfo = {};
return BBPromise.map(aggregateSourceElements, (element) => element
.getFilePathTranslations(targetPath, decompositionDir, unsupportedMimeTypes, forceIgnore)
.then((translations) => BBPromise.map(translations, (translation) => {
// check for duplicates since fs.copyAsync will throw an EEXIST error on duplicate files/dirs
if (util.isNullOrUndefined(translationsMap[translation.mdapiPath])) {
translationsMap[translation.mdapiPath] = translation.sourcePath;
preDeployHookInfo[element.aggregateFullName] = {
workspaceElements: element.workspaceElements.map((workspaceElement) => ({
fullName: workspaceElement.fullName,
metadataName: workspaceElement.metadataName,
sourcePath: workspaceElement.sourcePath,
state: workspaceElement.state,
deleteSupported: workspaceElement.deleteSupported,
})),
mdapiFilePath: translation.mdapiPath,
};
return BBPromise.resolve(translation.sourcePath)
.then((sourcePath) => fs_extra_1.copy(sourcePath, translation.mdapiPath))
.catch((err) => {
if (err.code === 'ENOENT') {
throw almError('MissingContentOrMetadataFile', translation.sourcePath);
}
throw err;
});
}
else {
return BBPromise.resolve();
}
}).catch((err) => {
this.revert = true;
this.err = err;
}))).then(() => {
if (this.revert !== undefined) {
srcDevUtil.deleteDirIfExistsSync(targetPath);
throw this.err;
}
// emit pre deploy event if convert is successful. We do this only on a
// successful convert because consumers will only want to be notified
// if a deploy is about to occur. A deploy won't occur if convert fails.
return core_1.Lifecycle.getInstance()
.emit('predeploy', preDeployHookInfo)
.then(() => {
// MD types like static resources might have a zip file created
// which need to be deleted after conversion to MD format
if (srcDevUtil.getZipDirPath()) {
srcDevUtil.deleteIfExistsSync(srcDevUtil.getZipDirPath());
}
return sourceUtil_1.cleanupOutputDir(decompositionDir);
});
});
}
static createPackageManifests(outputdir, packageName, destructiveChangesTypeNamePairs, updatedAggregateSourceElements, scratchOrg, metadataRegistry) {
const updatedTypeNamePairs = SourceConvertApi.getUpdatedSourceTypeNamePairs(updatedAggregateSourceElements, metadataRegistry);
const configSourceApiVersion = scratchOrg.config.getAppConfig().sourceApiVersion;
const sourceApiVersion = configSourceApiVersion || scratchOrg.config.getApiVersion();
// TODO: This should come from source tracking database
const manifestCreateApi = new ManifestCreateApi(scratchOrg);
// Create the package.xml
return manifestCreateApi
.createManifest({ outputdir, sourceApiVersion }, packageName, updatedTypeNamePairs)
.then(() => {
if (destructiveChangesTypeNamePairs.length > 0) {
// Create the destructiveChangesPost.xml
return manifestCreateApi.createManifest({
outputdir,
sourceApiVersion,
outputfile: 'destructiveChangesPost.xml',
}, packageName, destructiveChangesTypeNamePairs);
}
else {
return BBPromise.resolve();
}
});
}
static getUpdatedSourceTypeNamePairs(updatedAggregateSourceElements, metadataRegistry) {
const keys = new Set();
return updatedAggregateSourceElements
.map((se) => ({
type: se.getMetadataName(),
name: se.getAggregateFullName(),
workspaceElements: se.getWorkspaceElements(),
}))
.reduce((typeNamePairs, typeNamePair) => {
const metadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMetadataName(typeNamePair.type, metadataRegistry);
if (metadataType.hasIndividuallyAddressableChildWorkspaceElements()) {
typeNamePair.workspaceElements.forEach((workspaceElement) => {
const workspaceElementMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMetadataName(workspaceElement.getMetadataName(), metadataRegistry);
if (workspaceElement.getState() !== workspaceFileState_1.WorkspaceFileState.DELETED &&
workspaceElementMetadataType.isAddressable()) {
SourceConvertApi.addNoDupes(typeNamePairs, {
type: workspaceElement.getMetadataName(),
name: workspaceElement.getFullName(),
}, keys);
}
});
}
else {
SourceConvertApi.addNoDupes(typeNamePairs, typeNamePair, keys);
if (metadataType.requiresIndividuallyAddressableMembersInPackage()) {
metadataType.getChildMetadataTypes().forEach((childMetadataType) => {
SourceConvertApi.addNoDupes(typeNamePairs, { type: childMetadataType, name: '*' }, keys);
});
}
}
return typeNamePairs;
}, []);
}
static addNoDupes(typeNamePairs, typeNamePair, keys) {
const key = `${typeNamePair.type}#${typeNamePair.name}`;
if (!keys.has(key)) {
typeNamePairs.push(typeNamePair);
keys.add(key);
}
}
}
module.exports = SourceConvertApi;
//# sourceMappingURL=sourceConvertApi.js.map