sfdx-git-delta
Version:
Generate the sfdx content in source format and destructive change from two git commits
205 lines • 7.5 kB
JavaScript
'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