diff --git a/packages/salesforce/src/retrieveDeltaStrategy.ts b/packages/salesforce/src/retrieveDeltaStrategy.ts index affeeed5..1181402c 100644 --- a/packages/salesforce/src/retrieveDeltaStrategy.ts +++ b/packages/salesforce/src/retrieveDeltaStrategy.ts @@ -4,6 +4,7 @@ import { RetrieveManifestOptions, SalesforceDeployService } from "./salesforceDe import { SalesforcePackage, SalesforcePackageComponent } from "./deploymentPackage"; import { MetadataRegistry, MetadataType } from "./metadataRegistry"; import { RetrieveResultComponent } from "./deploy"; +import { outputFile } from "fs-extra"; /** * Interface for a strategy to determine if two objects are equal. Used in the delta strategy to determine if a component has changed. @@ -13,6 +14,9 @@ interface CompareStrategy { (a: Buffer | string, b: Buffer | string): boolean } +type CompareXmlStrategyType = 'xmlStrict' | 'xmlStrictOrder' | 'xml' | 'metaXml'; +type CompareStrategyType = CompareXmlStrategyType | 'binary' | 'default'; + /** * Interface for a strategy to determine which components in the packages have changed and need to be deployed. * Returns a list of components that have changed which can be used to create a new deployment package. @@ -20,15 +24,34 @@ interface CompareStrategy { @injectable({ lifecycle: LifecyclePolicy.transient }) export class RetrieveDeltaStrategy { - private readonly compareStrategies : Record = { - 'xmlStrictOrder': (a, b) => this.isXmlEqual(a, b, { strictOrder: true }), + private readonly compareStrategies: Record = { + 'xmlStrict': (a, b) => this.isXmlEqual(a, b, { strictOrder: true }), + 'xmlStrictOrder': (a, b) => this.isXmlEqual(a, b, { strictOrder: true, ignoreExtra: true }), 'xml': (a, b) => this.isXmlEqual(a, b, { strictOrder: false, ignoreExtra: true }), 'metaXml': (a, b) => this.isMetaXmlEqual(a, b), 'binary': this.isBinaryEqual.bind(this), 'default': this.isStringEqual.bind(this), - // Custom comparers - 'InstalledPackage': (a, b) => !this.isPackageNewer(a, b), - } + 'InstalledPackage': (a, b) => !this.isPackageNewer(a, b) + }; + + /** + * Represents the metadata strategy for different types of metadata. + */ + public static readonly metadataStrategy : Record = { + 'FlexiPage': 'xmlStrict', + 'Layout': 'xmlStrict', + 'Flow;': 'xmlStrict', + 'StaticResource': 'binary', + 'ContentAsset': 'binary', + 'Document': 'binary', + }; + + /** + * Determines the default strategy for strict XML parsing. + * This strategy is used when no specific strategy is defined for a metadata type. + * This strategy is used for XML files that are not metadata types. + */ + public static readonly defaultXmlStrategy: CompareXmlStrategyType = 'xml'; constructor( private readonly deployService: SalesforceDeployService, @@ -104,8 +127,8 @@ export class RetrieveDeltaStrategy { packagePath: string, localData: Buffer | string | undefined, orgData: Buffer | string | undefined, - type: MetadataType): boolean - { + type: MetadataType + ): boolean { if (Buffer.isBuffer(localData) && Buffer.isBuffer(orgData) && localData.compare(orgData) === 0) { // If both are buffers first do a quick buffer comparison return false; @@ -120,9 +143,9 @@ export class RetrieveDeltaStrategy { } try { - return !this.getComparer(packagePath, localData, type)(localData, orgData); + return !this.getComparer(packagePath, localData, type)(orgData, localData); } catch { - return !this.compareStrategies.default(localData, orgData); + return !this.compareStrategies.default(orgData, localData); } } @@ -131,11 +154,6 @@ export class RetrieveDeltaStrategy { return this.compareStrategies[type.name]; } - if (type.name === 'FlexiPage' || - type.name === 'Layout') { - return this.compareStrategies.xmlStrictOrder; - } - if (/\.([a-z]+)-meta\.xml$/i.test(packagePath)) { return this.compareStrategies.metaXml; } @@ -146,10 +164,20 @@ export class RetrieveDeltaStrategy { return this.compareStrategies.binary; } - if (/\.xml$/i.test(packagePath) || XML.isXml(data)) { - return this.compareStrategies.xml; + const strategyName = RetrieveDeltaStrategy.metadataStrategy[type.name]; + if (strategyName) { + if (typeof strategyName === 'string') { + if (!(strategyName in this.compareStrategies)) { + throw new Error(`Specified strategy for metadata type ${type.name} does not exist: ${strategyName}`); + } + return this.compareStrategies[strategyName]; + } + return strategyName; } + if (/\.xml$/i.test(packagePath) || XML.isXml(data)) { + return this.compareStrategies[RetrieveDeltaStrategy.defaultXmlStrategy]; + } return this.compareStrategies.default; } @@ -170,7 +198,8 @@ export class RetrieveDeltaStrategy { return deepCompare(parsedA, parsedB, { primitiveCompare: this.primitiveCompare, ignoreArrayOrder: !options?.strictOrder, - ignoreExtraProperties: !!options?.ignoreExtra + ignoreExtraProperties: !!options?.ignoreExtra, + ignoreExtraElements: !!options?.ignoreExtra }); } diff --git a/packages/util/src/object.ts b/packages/util/src/object.ts index c950ee21..2224c731 100644 --- a/packages/util/src/object.ts +++ b/packages/util/src/object.ts @@ -420,6 +420,12 @@ export interface ObjectEqualsOptions { * the objects are considered equal even though object `b` has an extra property */ ignoreExtraProperties?: boolean; + /** + * Ignore extra elements in an array when comparing arrays for equality. + * For example when array `a` looks like `[1, 2, 3]` and array `b` looks like `[1, 2, 3, 4]` + * the arrays are considered equal even though array `b` has an extra element. + */ + ignoreExtraElements?: boolean; } /** @@ -450,6 +456,26 @@ export function objectEquals( return true; } + const objectEqualityFn: typeof objectEquals = options?.objectCompare ?? objectEquals; + + if (Array.isArray(a) && Array.isArray(b)) { + if (!options?.ignoreExtraElements && a.length !== b.length) { + return false; + } + + if (options?.ignoreArrayOrder) { + const bElements = [...b]; + for (const element of a) { + const index = bElements.findIndex(otherElement => objectEqualityFn(element, otherElement, options)); + if (index === -1) { + return false; + } + bElements.splice(index, 1); + } + return options?.ignoreExtraElements === true || bElements.length === 0; + } + } + const missingKeys = Object.keys(a).filter(key => !(key in b)); if (missingKeys.length && !options?.ignoreMissingProperties) { return false; @@ -460,22 +486,6 @@ export function objectEquals( return false; } - const objectEqualityFn: typeof objectEquals = options?.objectCompare ?? objectEquals; - - // If both A and B are arrays and the ignoreArrayOrder option is set, then check if all elements of A are in B - // but ignore the order of the elements in B - if (options?.ignoreArrayOrder && Array.isArray(a) && Array.isArray(b)) { - const validElements = [...b]; - for (const element of a) { - const index = validElements.findIndex(otherElement => objectEqualityFn(otherElement, element, options)); - if (index === -1) { - return false; - } - validElements.splice(index, 1); - } - return options?.ignoreExtraProperties === true || validElements.length === 0; - } - // Check if all keys of A are equal to the keys in B for (const key of Object.keys(a).filter(key => key in b)) { if (!objectEqualityFn(a[key], b[key], options)) {