salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
255 lines (253 loc) • 12.6 kB
JavaScript
"use strict";
/*
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MdapiPullApi = void 0;
// 3pp
const util = require("util");
const path = require("path");
const BBPromise = require("bluebird");
const _ = require("lodash");
// Local
const kit_1 = require("@salesforce/kit");
const core_1 = require("@salesforce/core");
const mdapiRetrieveApi_1 = require("../mdapi/mdapiRetrieveApi");
const messagesApi = require("../messages");
const sourceUtil_1 = require("./sourceUtil");
const ManifestCreateApi = require("./manifestCreateApi");
const SourceMetadataMemberRetrieveHelper = require("./sourceMetadataMemberRetrieveHelper");
const syncCommandHelper = require("./syncCommandHelper");
const MetadataRegistry = require("./metadataRegistry");
const bundleMetadataType_1 = require("./metadataTypeImpl/bundleMetadataType");
const pathUtil = require("./sourcePathUtil");
const sourceWorkspaceAdapter_1 = require("./sourceWorkspaceAdapter");
const aggregateSourceElements_1 = require("./aggregateSourceElements");
const srcStatusApi_1 = require("./srcStatusApi");
const remoteSourceTrackingService_1 = require("./remoteSourceTrackingService");
const sourceLocations_1 = require("./sourceLocations");
class MdapiPullApi extends kit_1.AsyncCreatable {
constructor(options) {
super(options);
this.swa = options.adapter;
if (this.swa) {
this.smmHelper = new SourceMetadataMemberRetrieveHelper(this.swa);
}
this.scratchOrg = options.org;
this.force = this.scratchOrg.force;
this.messages = messagesApi(this.force.config.getLocale());
this.obsoleteNames = [];
}
async init() {
this.remoteSourceTrackingService = await remoteSourceTrackingService_1.RemoteSourceTrackingService.getInstance({
username: this.scratchOrg.name,
});
this.logger = await core_1.Logger.child(this.constructor.name);
if (!this.swa) {
const options = {
org: this.scratchOrg,
metadataRegistryImpl: MetadataRegistry,
defaultPackagePath: this.force.getConfig().getAppConfig().defaultPackagePath,
};
this.swa = await sourceWorkspaceAdapter_1.SourceWorkspaceAdapter.create(options);
this.smmHelper = new SourceMetadataMemberRetrieveHelper(this.swa);
}
}
async doPull(options) {
// Remove this when pull has been modified to support the new mdapi wait functionality;
if (isNaN(options.wait)) {
options.wait = this.force.config.getConfigContent().defaultSrcWaitMinutes;
}
await this._checkForConflicts(options);
// if no remote changes were made, quick exit
if (this.statusApi && !this.statusApi.getRemoteChanges().length) {
return [];
}
const packages = await this.smmHelper.getRevisionsAsPackage(this.obsoleteNames);
const results = await BBPromise.mapSeries(Object.keys(packages), async (pkgName) => {
core_1.SfdxProject.getInstance().setActivePackage(pkgName);
const pkg = packages[pkgName];
const opts = Object.assign({}, options);
this.logger.debug('Retrieving', pkgName);
try {
// Create a temp directory
opts.retrievetargetdir = await sourceUtil_1.createOutputDir('pull');
// Create a manifest (package.xml).
const manifestOptions = Object.assign({}, opts, {
outputdir: opts.retrievetargetdir,
});
const manifest = await this._createPackageManifest(manifestOptions, pkg);
this.logger.debug(util.inspect(manifest, { depth: 6 }));
let result;
if (manifest.empty) {
if (this.obsoleteNames.length > 0) {
result = { fileProperties: [], success: true, status: 'Succeeded' };
}
}
else {
// Get default metadata retrieve options
const retrieveOptions = Object.assign(mdapiRetrieveApi_1.MdRetrieveApi.getDefaultOptions(), {
retrievetargetdir: opts.retrievetargetdir,
unpackaged: manifest.file,
wait: opts.wait,
});
// Retrieve the metadata
result = await new mdapiRetrieveApi_1.MdRetrieveApi(this.scratchOrg).retrieve(retrieveOptions).catch((err) => err.result);
}
this.logger.debug('Retrieve result:', result);
// Update local metadata source.
return await this._postRetrieve(result, opts);
}
finally {
// Delete the output dir.
await sourceUtil_1.cleanupOutputDir(opts.retrievetargetdir);
}
});
return results;
}
// eslint-disable-next-line @typescript-eslint/require-await
async _createPackageManifest(options, pkg) {
if (pkg.isEmpty()) {
return BBPromise.resolve({ empty: true });
}
if (_.isNil(options.packageXml) || !options.debug) {
const configSourceApiVersion = this.force.getConfig().getAppConfig().sourceApiVersion;
const sourceApiVersion = !_.isNil(configSourceApiVersion)
? configSourceApiVersion
: this.force.getConfig().getApiVersion();
pkg.setVersion(sourceApiVersion);
return BBPromise.resolve(new ManifestCreateApi(this.force).createManifestForMdapiPackage(options, pkg, this.smmHelper.metadataRegistry));
}
else {
return BBPromise.resolve({ file: options.packageXml });
}
}
static _didRetrieveSucceed(result) {
return (!_.isNil(result) &&
result.success &&
result.status === 'Succeeded' &&
_.isNil(result.messages) &&
!_.isNil(result.fileProperties) &&
Array.isArray(result.fileProperties));
}
async _postRetrieve(result, options) {
let changedSourceElements;
let inboundFiles;
if (MdapiPullApi._didRetrieveSucceed(result)) {
changedSourceElements = await this._syncDownSource(result, options, this.swa);
// NOTE: Even if no updates were made, we need to update source tracking for those elements
// E.g., we pulled metadata but it's the same locally so it's not seen as a change.
inboundFiles = changedSourceElements
.getAllWorkspaceElements()
.map((workspaceElement) => workspaceElement.toObject());
await sourceLocations_1.SourceLocations.nonDecomposedElementsIndex.maybeRefreshIndex(inboundFiles);
await this.remoteSourceTrackingService.sync();
}
return this._processResults(result, inboundFiles);
}
async _syncDownSource(result, options, swa) {
const changedSourceElements = new aggregateSourceElements_1.AggregateSourceElements();
// Each Aura bundle has a definition file that has one of the suffixes: .app, .cmp, .design, .evt, etc.
// In order to associate each sub-component of an aura bundle (e.g. controller, style, etc.) with
// its parent aura definition type, we must find its parent's file properties and pass those along
// to processMdapiFileProperty. Similarly, for other BundleMetadataTypes.
const bundleFileProperties = bundleMetadataType_1.BundleMetadataType.getDefinitionProperties(result.fileProperties, this.swa.metadataRegistry);
const postRetrieveHookInfo = {};
result.fileProperties.forEach((fileProperty) => {
let { fullName, fileName } = fileProperty;
if (fileProperty.type === 'Package') {
return;
}
// After retrieving, switch back to path separators (for Windows)
fileProperty.fullName = fullName = pathUtil.replaceForwardSlashes(fullName);
fileProperty.fileName = fileName = pathUtil.replaceForwardSlashes(fileName);
this.swa.processMdapiFileProperty(changedSourceElements, options.retrievetargetdir, fileProperty, bundleFileProperties);
if (!(typeof postRetrieveHookInfo[fullName] === 'object')) {
postRetrieveHookInfo[fullName] = {
mdapiFilePath: [],
};
}
postRetrieveHookInfo[fullName].mdapiFilePath = postRetrieveHookInfo[fullName].mdapiFilePath.concat(path.join(options.retrievetargetdir, fileName));
});
// emit post retrieve event
await core_1.Lifecycle.getInstance().emit('postretrieve', postRetrieveHookInfo);
this.obsoleteNames.forEach((obsoleteName) => {
this.swa.handleObsoleteSource(changedSourceElements, obsoleteName.fullName, obsoleteName.type);
});
const sourcePromise = swa.updateSource(changedSourceElements, options.manifest, false /** check for duplicates **/, options.unsupportedMimeTypes, options.forceoverwrite);
return sourcePromise
.then((updatedSource) => {
// emit post source update event
const postSourceUpdateHookInfo = {};
updatedSource.forEach((sourceElementMap) => {
sourceElementMap.forEach((sourceElement) => {
const fullName = sourceElement.aggregateFullName;
if (!postSourceUpdateHookInfo[fullName]) {
postSourceUpdateHookInfo[fullName] = {
workspaceElements: [],
};
}
const hookInfo = postSourceUpdateHookInfo[fullName];
const newElements = hookInfo.workspaceElements.concat(sourceElement.workspaceElements.map((we) => we.toObject()));
hookInfo.workspaceElements = [...newElements];
postSourceUpdateHookInfo[fullName] = hookInfo;
});
});
core_1.Lifecycle.getInstance()
.emit('postsourceupdate', postSourceUpdateHookInfo)
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => { });
})
.then(() => sourcePromise);
}
_processResults(result, inboundFiles) {
if (_.isNil(result)) {
return;
}
else if (MdapiPullApi._didRetrieveSucceed(result)) {
return { inboundFiles };
}
else {
const retrieveFailed = new Error(syncCommandHelper.getRetrieveFailureMessage(result, this.messages));
retrieveFailed.name = 'RetrieveFailed';
throw retrieveFailed;
}
}
async _checkForConflicts(options) {
if (options.forceoverwrite) {
// do not check for conflicts when pull --forceoverwrite
return [];
}
this.statusApi = await srcStatusApi_1.SrcStatusApi.create({ org: this.scratchOrg, adapter: this.swa });
return this.statusApi
.doStatus({ local: true, remote: true }) // rely on status so that we centralize the logic
.then(() => this.statusApi.getLocalConflicts())
.catch((err) => {
const sfdxError = core_1.SfdxError.wrap(err);
if (err.errorCode === 'INVALID_TYPE') {
const messages = core_1.Messages.loadMessages('salesforce-alm', 'source_pull');
sfdxError.message = messages.getMessage('NonScratchOrgPull');
}
else if (err.errorCode === 'INVALID_SESSION_ID') {
sfdxError.actions = [this.messages.getMessage('invalidInstanceUrlForAccessTokenAction')];
}
else {
sfdxError.message = err.message;
}
throw sfdxError;
})
.then((conflicts) => {
if (conflicts.length > 0) {
const error = new Error('Conflicts found during sync down');
error['name'] = 'SourceConflict';
error['sourceConflictElements'] = conflicts;
throw error;
}
});
}
}
exports.MdapiPullApi = MdapiPullApi;
//# sourceMappingURL=sourcePullApi.js.map