salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
321 lines (319 loc) • 13.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NonDecomposedElementsIndex = void 0;
/*
* 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
*/
const path_1 = require("path");
const core_1 = require("@salesforce/core");
const ts_types_1 = require("@salesforce/ts-types");
const remoteSourceTrackingService_1 = require("./remoteSourceTrackingService");
const MetadataRegistry = require("./metadataRegistry");
const XmlParser = require('fast-xml-parser');
const NON_DECOMPOSED_CONFIGS = {
CustomLabels: [
{
childType: 'CustomLabel',
xmlTag: 'CustomLabels[0].labels',
namePath: 'fullName',
},
],
};
/**
* NonDecomposedElementsIndex maintains an index of non-decomposed elements (e.g. CustomLabel) at
* <project_dir>/.sfdx/orgs/<username>/nonDecomposedElementsIndex.json.
*
* The purpose of this is to be able to figure out which elements belong to which file. So for example,
* if we have CustomLabels files in two separate packages, we can use this index to determine which
* labels to put into the package.xml when executing a retrieve or a pull.
*
* We use the NON_DECOMPOSED_CONFIGS to determine which metadata types need to be read and stored into the index.
* - The keys (e.g. CustomLabels) are the aggregate metadata types. This tells us which meta files we need to read.
* - childType refers to the metadata type of the elements inside the meta file
* - xmlTag tells us where to find the elements inside the xml
* - namePath tells us where to find the name of the element
*/
// eslint-disable-next-line no-redeclare
class NonDecomposedElementsIndex extends core_1.ConfigFile {
constructor() {
super(...arguments);
this.includedFiles = new Set();
this.hasChanges = false;
}
static async getInstance(options) {
if (!this._instances[options.username]) {
this._instances[options.username] = await NonDecomposedElementsIndex.create(options);
}
return this._instances[options.username];
}
static getFileName() {
return 'nonDecomposedElementsIndex.json';
}
async init() {
this.options.filePath = path_1.join('orgs', this.options.username);
this.options.filename = NonDecomposedElementsIndex.getFileName();
this.logger = await core_1.Logger.child(this.constructor.name);
this.metadataRegistry = this.options.metadataRegistry;
this.remoteSourceTrackingService = await remoteSourceTrackingService_1.RemoteSourceTrackingService.getInstance({
username: this.options.username,
});
await super.init();
this.populateIncludedFiles();
}
populateIncludedFiles() {
this.values().forEach((v) => this.includedFiles.add(v.metadataFilePath));
}
// eslint-disable-next-line @typescript-eslint/require-await
async addElement(metadataName, fullName, sourcePath) {
const key = MetadataRegistry.getMetadataKey(metadataName, fullName);
const value = {
fullName,
type: metadataName,
metadataFilePath: sourcePath,
};
if (!this.has(key)) {
this.set(key, value);
}
}
getMetadataFilePath(key) {
const value = this.get(key);
return value ? value.metadataFilePath : null;
}
/**
* Returns true if the metadata type contains non-decomposed elements
* that we want to put into the index.
*/
isNonDecomposedElement(metadataName) {
return NonDecomposedElementsIndex.isSupported(metadataName);
}
static isSupported(metadataName) {
return NON_DECOMPOSED_CONFIGS.hasOwnProperty(metadataName);
}
/**
* Returns true if the provided sourcePath is in this.includedFiles.
* If a file is in this.includedFiles, that means that the index has
* already read that file
*/
isIncludedFile(sourcePath) {
return this.includedFiles.has(sourcePath);
}
/**
* Returns true if the file has NOT changed or is NOT new
*/
shouldSkip(sourcePathInfo) {
return !(sourcePathInfo.isChanged() || sourcePathInfo.isNew()) && this.isIncludedFile(sourcePathInfo.sourcePath);
}
/**
* Adds the non-decomposed elements within a sourcePath to the index
*
* If the given sourcePath is supported, then we:
* - read the xml
* - parse the xml for the non-decomposed elements
* - add all those elements to the index
*
* We skip this process if:
* - the sourcePath belongs to a metadata type that doesn't have non-decomposed elements
* - OR the sourcePath hasn't changed since the last time we read it
*
* Set the refresh flag to true if you want to force update the index
*/
async handleDecomposedElements(sourcePathInfo, refresh = false) {
if (!refresh && this.shouldSkip(sourcePathInfo)) {
return;
}
const metadataType = this.metadataRegistry.getTypeDefinitionByFileName(sourcePathInfo.sourcePath);
const configs = NON_DECOMPOSED_CONFIGS[metadataType.metadataName];
const contents = await this.readXmlAsJson(sourcePathInfo.sourcePath);
for (const config of configs) {
const elements = ts_types_1.get(contents, config.xmlTag, []);
for (const element of elements) {
const fullName = ts_types_1.get(element, config.namePath);
if (fullName) {
await this.addElement(metadataType.metadataName, fullName, sourcePathInfo.sourcePath);
}
}
}
this.write();
}
/**
* Unsets elements with a metadataFilePath that matches the provided sourcePath
*/
clearElements(sourcePath) {
const matchingElements = this.getElementsByMetadataFilePath(sourcePath);
matchingElements.forEach((element) => {
const key = MetadataRegistry.getMetadataKey(element.type, element.fullName);
this.unset(key);
});
}
/**
* Returns JSON representation of an xml file
*/
async readXmlAsJson(sourcePath) {
const contents = await core_1.fs.readFile(sourcePath, 'utf-8');
try {
return XmlParser.parse(contents, { arrayMode: true });
}
catch (err) {
throw core_1.SfdxError.create('salesforce-alm', 'source', 'XmlParsingError', [`; ${err.message}`]);
}
}
/**
* Synchronously read a source file and look for a specific metadata key contained within it,
* returning `true` if found. If the metadata key is a type unknown to this index, or if there
* is a problem reading/parsing the source file, an error will be logged.
*
* @param sourcePath The path to the source file.
* @param mdKey The metadata key to search within the source file. E.g., CustomLabels__MyLabelName
*/
static belongsTo(sourcePath, mdKey) {
let belongs = false;
try {
const [mdType, mdName] = mdKey.split('__');
const configs = NON_DECOMPOSED_CONFIGS[mdType];
if (!configs) {
throw new Error(`Unsupported NonDecomposedIndex type: ${mdType}`);
}
const contents = core_1.fs.readFileSync(sourcePath, 'utf-8');
const jsonContents = XmlParser.parse(contents, { arrayMode: true });
for (const config of configs) {
const elements = ts_types_1.get(jsonContents, config.xmlTag, []);
for (const element of elements) {
const fullName = ts_types_1.get(element, config.namePath);
if (fullName === mdName) {
belongs = true;
break;
}
}
}
}
catch (err) {
const logger = core_1.Logger.childFromRoot(this.constructor.name);
logger.debug(`Encountered an error reading/parsing source path: ${sourcePath} for ${mdKey} due to:\n${err.stack}`);
}
return belongs;
}
/**
* Given an array of ChangeElements, find all changeElements that live in the same file location.
* For example, given a custom label this will return all custom labels that live in the same CustomLabels
* meta file.
*/
getRelatedNonDecomposedElements(changeElements) {
const elements = [];
const seen = new Set();
const contents = this.values();
const isRelatedElement = function (existingElement, comparisonElement) {
return (existingElement.metadataFilePath === comparisonElement.metadataFilePath &&
existingElement.fullName !== comparisonElement.fullName &&
!seen.has(comparisonElement.fullName));
};
for (const changeElement of changeElements) {
const metadataType = this.metadataRegistry.getTypeDefinitionByMetadataName(changeElement.type);
if (metadataType && NonDecomposedElementsIndex.isSupported(metadataType.metadataName)) {
const key = MetadataRegistry.getMetadataKey(metadataType.metadataName, changeElement.name);
const element = this.get(key);
contents.forEach((item) => {
const shouldAdd = this.has(key) ? isRelatedElement(element, item) : this.elementBelongsToDefaultPackage(item);
if (shouldAdd) {
seen.add(item.fullName);
const trackedElement = this.remoteSourceTrackingService.getTrackedElement(key);
const isNameObsolete = trackedElement ? trackedElement.deleted : false;
elements.push({
type: changeElement.type,
name: item.fullName,
deleted: isNameObsolete,
});
}
});
}
}
return elements;
}
/**
* Returns all elements in the index that have a given metadataFilePath
*/
getElementsByMetadataFilePath(metadataFilePath) {
if (!this.isIncludedFile(metadataFilePath)) {
return [];
}
const elements = [...this.values()];
return elements.filter((element) => element.metadataFilePath === metadataFilePath);
}
/**
* Refreshes the index IF the inboundFiles contain any paths that have
* been previously added to the index.
*/
async maybeRefreshIndex(inboundFiles) {
const results = inboundFiles.filter((c) => !c.fullName.includes('xml'));
const supportedTypes = results.filter((r) => NonDecomposedElementsIndex.isSupported(decodeURIComponent(r.fullName)));
if (supportedTypes.length) {
const sourcePaths = supportedTypes.map((r) => r.filePath);
return this.refreshIndex(sourcePaths);
}
}
/**
* Refreshes the index using the provided sourcePaths. If no sourcePaths
* are provided then it will default to refreshing files that have already
* been indexed (this.includedFiles)
*/
async refreshIndex(sourcePaths) {
const paths = sourcePaths || this.includedFiles;
for (const sourcePath of paths) {
if (await core_1.fs.fileExists(sourcePath)) {
this.clearElements(sourcePath);
await this.handleDecomposedElements({ sourcePath }, true);
}
else {
this.deleteEntryBySourcePath(sourcePath);
}
}
}
/**
* Returns true if the given nonDecomposedElements belongs to the default package
*/
elementBelongsToDefaultPackage(nonDecomposedElement) {
const defaultPackage = core_1.SfdxProject.getInstance().getDefaultPackage().name;
const elementPackage = core_1.SfdxProject.getInstance().getPackageNameFromPath(nonDecomposedElement.metadataFilePath);
return defaultPackage === elementPackage;
}
deleteEntryBySourcePath(path) {
try {
const elements = this.getElementsByMetadataFilePath(path);
elements.forEach((element) => {
this.unset(`${element.type}__${element.fullName}`);
});
}
catch (e) {
// if it's already been deleted, don't throw an error when trying to delete it again
// but if it's a different error, throw it!
if (e.message !== 'Cannot convert undefined or null to object') {
this.logger.debug(`An error occured when trying to delete ${path} from the nonDecomposedElementsIndex`);
throw core_1.SfdxError.wrap(e);
}
}
}
async write() {
if (!this.hasChanges) {
return;
}
this.hasChanges = false;
return super.write();
}
get(key) {
return super.get(key);
}
set(key, value) {
super.set(key, value);
this.includedFiles.add(value.metadataFilePath);
this.hasChanges = true;
return this.getContents();
}
values() {
return super.values();
}
}
exports.NonDecomposedElementsIndex = NonDecomposedElementsIndex;
NonDecomposedElementsIndex._instances = {};
//# sourceMappingURL=nonDecomposedElementsIndex.js.map