UNPKG

@smartimpact-it/json-merge-shopify

Version:
378 lines (335 loc) 10.8 kB
import Comparer, { ArrayElementPosition, ArrayElementResult } from './Comparer'; export interface ArrayMergerOptions { base: any[]; ours: any[]; theirs: any[]; preferred?: SourceType | null; filename?: string | null; path?: string[]; deletionsAllowed?: boolean; } export interface ArrayMergerVariant { seemsValid: boolean; positions: Array<{ unionPosition: number; basePosition: number | null; oursPosition: number | null; theirsPosition: number | null; }>; similarityScore: number; } export interface UnionArrayElementResult extends ArrayElementResult { unionIndex: number; } const sourceNames = ['base', 'ours', 'theirs'] as const; export type SourceType = 'base' | 'ours' | 'theirs'; export class ArrayMerger { ours: any[]; theirs: any[]; base: any[]; union: any[]; preferred: SourceType; filename: string | null; path: string[] | null; variants: ArrayMergerVariant[] | null = null; objectPossiblePositions: UnionArrayElementResult[] | null = null; deletionsAllowed: boolean = true; constructor({ base, ours, theirs, preferred, filename, path, deletionsAllowed, }: ArrayMergerOptions) { this.base = base; this.ours = ours; this.theirs = theirs; this.preferred = preferred || 'theirs'; this.filename = filename || null; this.path = path || []; this.union = this.getUnion(); this.deletionsAllowed = deletionsAllowed ?? true; } private getUnion(): any[] { let union: any[]; if (this.preferred === 'ours') { union = [...new Set([...this.ours, ...this.theirs])]; } else { union = [...new Set([...this.theirs, ...this.ours])]; } return union; } getObjectPositions(): UnionArrayElementResult[] { if (this.objectPossiblePositions) { return this.objectPossiblePositions; } const comparer = new Comparer(); const objectPositions: UnionArrayElementResult[] = []; this.union.forEach((object, index) => { const result = comparer.getArrayElementDiff3(object, { base: this.base, theirs: this.theirs, ours: this.ours, }); objectPositions.push({ unionIndex: index, ...result, }); }); this.objectPossiblePositions = objectPositions; return objectPositions; } private getPositionsAsArray( position: ArrayElementPosition, type: SourceType, currentVariants: ArrayMergerVariant[], ): Array<number | { index: number; similarity: number } | null> { if (position === null) { return [null]; } let positions: Array<{ index: number; similarity: number } | null>; if (Array.isArray(position)) { positions = position; } else { positions = [{ index: position, similarity: 1 }]; } positions = positions.filter((position) => { return !currentVariants.find((variant) => { return variant.positions.find((variantPosition) => { if (position === null) { return false; } switch (type) { case 'base': return position.index === variantPosition.basePosition; case 'ours': return position.index === variantPosition.oursPosition; case 'theirs': return position.index === variantPosition.theirsPosition; } }); }); }); if (positions.length === 0) { return [null]; } return positions; } private getPositionIndex( position: number | { index: number; similarity: number } | null, ): number | null { return typeof position === 'object' && position != null ? position.index : position; } private getPositionScore( position: number | { index: number; similarity: number } | null, ): number | null { return typeof position === 'object' && position != null ? position.similarity : position !== null ? 1 : null; } private getNextVariants( currentVariants: ArrayMergerVariant[], unionIndex: number, ): ArrayMergerVariant[] { if (unionIndex === this.union.length) { return currentVariants; } const objectPositions = this.getObjectPositions(); const object = this.union[unionIndex]; const objectPosition = objectPositions.find( (objectPosition) => objectPosition.unionIndex === unionIndex, ); if (!objectPosition) { return currentVariants; } // Create new variants for each possible position of the current object. const newVariants: ArrayMergerVariant[] = []; const { basePosition, oursPosition, theirsPosition } = objectPosition; const basePositions = this.getPositionsAsArray( basePosition, 'base', currentVariants, ); const oursPositions = this.getPositionsAsArray( oursPosition, 'ours', currentVariants, ); const theirsPositions = this.getPositionsAsArray( theirsPosition, 'theirs', currentVariants, ); basePositions.forEach((basePosition) => { oursPositions.forEach((oursPosition) => { theirsPositions.forEach((theirsPosition) => { const basePositionIndex = this.getPositionIndex(basePosition); const oursPositionIndex = this.getPositionIndex(oursPosition); const theirsPositionIndex = this.getPositionIndex(theirsPosition); const basePositionScore = this.getPositionScore(basePosition); const oursPositionScore = this.getPositionScore(oursPosition); const theirsPositionScore = this.getPositionScore(theirsPosition); let similarityScore = (basePositionScore || 0) + (oursPositionScore || 0) + (theirsPositionScore || 0); // If the object is in the same position in base and ours or base and theirs, // it is more likely to be valid. if (basePosition != null) { if ( basePositionIndex === oursPositionIndex || basePositionIndex === theirsPositionIndex ) { similarityScore *= 1.5; } } const variant = { seemsValid: true, positions: [ { unionPosition: unionIndex, basePosition: basePositionIndex, oursPosition: oursPositionIndex, theirsPosition: theirsPositionIndex, }, ], similarityScore: similarityScore, }; newVariants.push(variant); }); }); }); // If there are no variants yet, go to the next object. if (currentVariants.length === 0) { return this.getNextVariants(newVariants, unionIndex + 1); } // Append new variants to the current variants. const nextVariants: ArrayMergerVariant[] = []; currentVariants.forEach((currentVariant) => { newVariants.forEach((newVariant) => { const variant = { seemsValid: currentVariant.seemsValid && newVariant.seemsValid, positions: [...currentVariant.positions, ...newVariant.positions], similarityScore: currentVariant.similarityScore + newVariant.similarityScore, }; nextVariants.push(variant); }); }); // Go to the next object. return this.getNextVariants(nextVariants, unionIndex + 1); } getVariants(): ArrayMergerVariant[] { if (this.variants) { return this.variants; } this.variants = this.getNextVariants([], 0); return this.variants; } generateFromVariant(variant: ArrayMergerVariant): any[] { const positions = variant.positions; const result: any[] = []; positions.forEach((position) => { // Deleted object. if ( position.basePosition !== null && (position.oursPosition === null || (position.theirsPosition === null && this.deletionsAllowed)) ) { return; } // Added object. if ( position.basePosition === null && (position.oursPosition !== null || position.theirsPosition !== null) ) { const ourObject = position.oursPosition ? this.ours[position.oursPosition] : null; const theirObject = position.theirsPosition ? this.theirs[position.theirsPosition] : null; result.push(this.getMergedObject(theirObject, ourObject)); return; } // Modified object. let object; if (position.theirsPosition !== null && position.oursPosition !== null) { object = this.getMergedObject( this.theirs[position.theirsPosition], this.ours[position.oursPosition], ); } else { object = this.union[position.unionPosition]; } result.push(object); }); // Add objects that are missing from the variant. const sources: Array<'ours' | 'theirs'> = this.preferred === 'ours' ? ['ours', 'theirs'] : ['theirs', 'ours']; sources.forEach((source: 'ours' | 'theirs') => { const handledIndexes: number[] = positions .map((position) => { return position[`${source}Position`]!; }) .filter((position) => position !== null); for (let i = 0; i < this[source].length; i++) { if (!handledIndexes.includes(i)) { result.push(this[source][i]); } } }); return result; } getMergedObject(theirs: any, ours: any): any { const oursIsObject = typeof ours === 'object' || ours == null; const theirsIsObject = typeof theirs === 'object' || theirs == null; // If one of the objects is not an object, return the other one. if ((!oursIsObject && !theirsIsObject) || oursIsObject != theirsIsObject) { return this.preferred === 'ours' ? ours : theirs; } theirs = theirs || {}; ours = ours || {}; // If both objects are objects, merge them. if (this.preferred === 'ours') { return { ...theirs, ...ours, }; } else { return { ...ours, ...theirs, }; } } merge(): any[] { const variants = this.getVariants() .sort((a, b) => { return b.similarityScore - a.similarityScore; }) .map((variant) => { return { ...variant, positions: variant.positions.filter((position) => { return ( position.oursPosition !== null || position.theirsPosition !== null ); }), }; }); const bestVariant = variants[0]; if (!bestVariant) { throw new Error('No valid variant found.'); } const result = this.generateFromVariant(bestVariant); return result; } } export default ArrayMerger;