UNPKG

sfdx-git-delta

Version:

Generate the sfdx content in source format and destructive change from two git commits

205 lines 7.5 kB
'use strict'; import { deepEqual } from 'fast-equals'; import { differenceWith, isUndefined } from 'lodash-es'; import { XML_HEADER_ATTRIBUTE_KEY, convertJsonToXml, parseXmlFileToJson, } from './fxpHelper.js'; import { ATTRIBUTE_PREFIX } from './fxpHelper.js'; import { fillPackageWithParameter } from './packageHelper.js'; const ARRAY_SPECIAL_KEY = '<array>'; const isEmpty = (arr) => arr.length === 0; export default class MetadataDiff { config; toContent; fromContent; extractor; constructor(config, attributes) { this.config = config; this.extractor = new MetadataExtractor(attributes); } async compare(path) { const [toContent, fromContent] = await Promise.all([ parseXmlFileToJson({ path, oid: this.config.to }, this.config), parseXmlFileToJson({ path, oid: this.config.from }, this.config), ]); this.toContent = toContent; this.fromContent = fromContent; const comparator = new MetadataComparator(this.extractor, this.fromContent, this.toContent); const added = comparator.getChanges(); const deleted = comparator.getDeletion(); return { added, deleted }; } prune() { const transformer = new JsonTransformer(this.extractor); const { prunedContent, isEmpty } = transformer.generatePartialJson(this.fromContent, this.toContent); return { xmlContent: convertJsonToXml(prunedContent), isEmpty, }; } } class MetadataExtractor { attributes; keySelectorCache = new Map(); constructor(attributes) { this.attributes = attributes; } getSubTypes(root) { return Object.keys(root).filter(tag => this.attributes.has(tag)); } getSubKeys(root) { return Object.keys(root); } isTypePackageable(subType) { return !this.attributes.get(subType)?.excluded; } getXmlName(subType) { return this.attributes.get(subType)?.xmlName; } getKeyValueSelector(subType) { if (!this.keySelectorCache.has(subType)) { const metadataKey = this.getKeyFieldDefinition(subType); this.keySelectorCache.set(subType, elem => elem[metadataKey]); } return this.keySelectorCache.get(subType); } getKeyFieldDefinition(subType) { return this.attributes.get(subType)?.key; } extractForSubType(root, subType) { const content = root[subType]; // Only cast to array if it's not already an array return Array.isArray(content) ? content : content ? [content] : []; } extractRootElement(fileContent) { const rootKey = this.extractRootKey(fileContent); return fileContent[rootKey] ?? {}; } extractRootKey(fileContent) { return (Object.keys(fileContent).find(key => key !== XML_HEADER_ATTRIBUTE_KEY) ?? ''); } } class MetadataComparator { extractor; fromContent; toContent; constructor(extractor, fromContent, toContent) { this.extractor = extractor; this.fromContent = fromContent; this.toContent = toContent; } getChanges() { return this.compare(this.toContent, this.fromContent, this.compareAdded); } getDeletion() { return this.compare(this.fromContent, this.toContent, this.compareDeleted); } compare(baseContent, targetContent, elementMatcher) { const base = this.extractor.extractRootElement(baseContent); const target = this.extractor.extractRootElement(targetContent); const manifest = new Map(); // Get all subtypes once const subTypes = this.extractor.getSubTypes(base); for (const subType of subTypes) { if (!this.extractor.isTypePackageable(subType)) continue; const baseMeta = this.extractor.extractForSubType(base, subType); if (isEmpty(baseMeta)) continue; const targetMeta = this.extractor.extractForSubType(target, subType); const keySelector = this.extractor.getKeyValueSelector(subType); const xmlName = this.extractor.getXmlName(subType); for (const elem of baseMeta) { if (elementMatcher(targetMeta, keySelector, elem)) { fillPackageWithParameter({ store: manifest, type: xmlName, member: keySelector(elem), }); } } } return manifest; } compareAdded = (meta, keySelector, elem) => { const elemKey = keySelector(elem); const match = meta.find(el => keySelector(el) === elemKey); return !match || !deepEqual(match, elem); }; compareDeleted = (meta, keySelector, elem) => { const elemKey = keySelector(elem); return !meta.some(el => keySelector(el) === elemKey); }; } class JsonTransformer { extractor; isEmpty = true; constructor(extractor) { this.extractor = extractor; } generatePartialJson(fromContent, toContent) { const from = this.extractor.extractRootElement(fromContent); const to = this.extractor.extractRootElement(toContent); const base = {}; if (XML_HEADER_ATTRIBUTE_KEY in toContent) { base[XML_HEADER_ATTRIBUTE_KEY] = toContent[XML_HEADER_ATTRIBUTE_KEY]; } const rootKey = this.extractor.extractRootKey(toContent); base[rootKey] = {}; const root = base[rootKey]; const subKeys = this.extractor.getSubKeys(to); for (const key of subKeys) { if (key.startsWith(ATTRIBUTE_PREFIX)) { root[key] = to[key]; continue; } const fromMeta = this.extractor.extractForSubType(from, key); const toMeta = this.extractor.extractForSubType(to, key); const keyField = this.extractor.getKeyFieldDefinition(key); const partialContent = this.getPartialContent(fromMeta, toMeta, keyField); if (!isEmpty(partialContent)) { root[key] = partialContent; } } return { prunedContent: base, isEmpty: this.isEmpty }; } getPartialContent(fromMeta, toMeta, keyField) { // Early return for empty arrays if (isEmpty(toMeta)) { return []; } if (isEmpty(fromMeta)) { this.isEmpty = false; return toMeta; } if (isUndefined(keyField)) { return this.getPartialContentWithoutKey(fromMeta, toMeta); } else if (keyField === ARRAY_SPECIAL_KEY) { return this.getPartialContentForArray(fromMeta, toMeta); } else { return this.getPartialContentWithKey(fromMeta, toMeta); } } getPartialContentWithoutKey(fromMeta, toMeta) { if (!deepEqual(fromMeta, toMeta)) { this.isEmpty = false; } return toMeta; } getPartialContentForArray(fromMeta, toMeta) { if (!deepEqual(fromMeta, toMeta)) { this.isEmpty = false; return toMeta; } return []; } getPartialContentWithKey(fromMeta, toMeta) { const diff = differenceWith(toMeta, fromMeta, deepEqual); if (!isEmpty(diff)) { this.isEmpty = false; } return diff; } } //# sourceMappingURL=metadataDiff.js.map