UNPKG

salesforce-alm

Version:

This package contains tools, and APIs, for an improved salesforce.com developer experience.

321 lines (319 loc) 13.6 kB
"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