UNPKG

@smartimpact-it/json-merge-shopify

Version:

The library handles merging JSON objects and arrays.

324 lines (323 loc) 11.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Comparer = void 0; const js_levenshtein_1 = __importDefault(require("js-levenshtein")); class Comparer { /** * Compare two variables, of any type. */ compare(a, b, parentPath = [], filename = null) { // If they are equal, return that they are equal :) if (JSON.stringify(a) === JSON.stringify(b)) { return this.equal(); } // If they are both arrays, compare them as arrays. if (Array.isArray(a) && Array.isArray(b)) { const result = this.compareArrays(a, b, parentPath, filename); if (result) { return result; } } // If they are both objects, compare them as objects. if (typeof a === 'object' && typeof b === 'object' && Array.isArray(a) === Array.isArray(b)) { const result = this.compareObjects(a, b, parentPath, filename); if (result) { return result; } } // If they are both strings, compare them as strings. if (typeof a === 'string' && typeof b === 'string') { return this.compareStrings(a, b, parentPath, filename); } return this.unequal(); } /** * Compare two arrays. */ compareArrays(a, b, parentPath = [], filename = null) { // Check if they are both arrays. const aIsArray = Array.isArray(a); const bIsArray = Array.isArray(b); if (!aIsArray && !bIsArray) { return this.compare(a, b, parentPath, filename); } // If they are equal, return that they are equal :) if (JSON.stringify(a) === JSON.stringify(b)) { return this.equal(); } // If one of them is not an array, return that they are not equal. if ((!aIsArray || !bIsArray) && aIsArray !== bIsArray) { return this.unequal(); } // Now we know that both are arrays. Compare them as arrays. const common = a.filter((element) => { const found = b.find((bElement) => { return this.compare(element, bElement, parentPath, filename)?.equal; }); return !!found; }); if (a.length === b.length && common.length === a.length) { return this.equal(); } if (common.length > 0 && common.length / a.length > 0.5) { return { equal: false, couldBeEqual: true, similarity: common.length / a.length, }; } return this.unequal(); } /** * Compare two objects (not arrays). */ compareObjects(a, b, parentPath = [], filename = null) { // If they are equal, return that they are equal :) if (JSON.stringify(a) === JSON.stringify(b)) { return this.equal(); } // If one of theme is not an object, return that they are not equal. if (typeof a !== 'object' || typeof b !== 'object') { return this.unequal(); } // Check if any of them is an array. const aIsArray = Array.isArray(a); const bIsArray = Array.isArray(b); if (aIsArray || bIsArray) { return this.compareArrays(a, b, parentPath, filename); } const parentPathString = parentPath ? parentPath.join('.') : ''; // Handle setting fields. let result = null; if (a.type && b.type && a.id && b.id) { result = this.compareSettings(a, b, parentPath, filename); if (result) { return result; } } else if (parentPathString.match(/\d+\.settings/)) { result = this.compareSettings(a, b, parentPath, filename); if (result) { return result; } } else if (a.name && b.name && (parentPathString == '' || (a.settings && b.settings && Array.isArray(a.settings) && Array.isArray(b.settings)))) { result = this.compareSettingBlocks(a, b, parentPath, filename); if (result) { return result; } } return this.unequal(); } /** * Compare blocks of settings */ compareSettingBlocks(a, b, parentPath = null, filename = null) { // If the name is different, they are not equal if (a.name && a.name !== b.name) { // Compare the settings. const aSettingIds = (a.settings || []).map((setting) => setting.id); const bSettingIds = (b.settings || []).map((setting) => setting.id); const intersection = aSettingIds.filter((id) => bSettingIds.includes(id)); const union = [...new Set([...aSettingIds, ...bSettingIds])]; // If the intersection is more than half of the settings, they could be equal if (intersection.length > 0 && intersection.length / union.length > 0.5) { return { equal: false, couldBeEqual: true, similarity: (intersection.length / union.length) * 0.85, }; } return this.unequal(); } // Blocks without settings, but with the same name are equal. if (!a.settings && !b.settings) { return { equal: true, couldBeEqual: true, similarity: 0.8, }; } // If the name is the same, but the one doesn't have settings, they could be equal if (!!a.settings !== !!b.settings) { return { equal: false, couldBeEqual: true, similarity: 0.5, }; } // If both have settings, compare the setting IDs const aSettingIds = (a.settings || []).map((setting) => setting?.id); const bSettingIds = (b.settings || []).map((setting) => setting?.id); const intersection = aSettingIds.filter((id) => bSettingIds.includes(id)); const union = [...new Set([...aSettingIds, ...bSettingIds])]; // If the intersection is more than half of the settings, they could be equal if (intersection.length > 0 && intersection.length > Math.max(aSettingIds.length, bSettingIds) / 2 && intersection.length > Math.min(aSettingIds.length, bSettingIds.length)) { return { equal: false, couldBeEqual: true, similarity: intersection.length / union.length, }; } return this.unequal(); } /** * Compare Shopify setting fields */ compareSettings(a, b, parentPath = null, filename = null) { if (a.id && a.id === b.id) { return this.equal(); } // If they are the same type and have the same label, they are equal if (a.type && a.type === b.type && a.label && a.label === b.label) { return this.equal(); } // The ID and type are different, but the label is the same if (a.label && a.label === b.label) { return { equal: false, couldBeEqual: true, similarity: 0.5, }; } // The type is the same and some of the properties are the same if (a.type && a.type === b.type) { const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])]; const intersection = keys.filter((key) => a[key] === b[key]); const similarity = intersection.length / keys.length; return { equal: false, couldBeEqual: similarity > 0.25, similarity, }; } return this.unequal(); } /** * Compare two strings. */ compareStrings(a, b, parentPath = null, filename = null) { if (a === b) { return this.equal(); } // If the strings are similar, they could be equal const score = (0, js_levenshtein_1.default)(a, b); const similarity = 1 - score / Math.max(a.length, b.length); if (similarity > 0.5) { return { equal: false, couldBeEqual: true, similarity, }; } return this.unequal(); } /** * Default values for non-equal comparison results. */ unequal() { return { equal: false, couldBeEqual: false, similarity: 0, }; } /** * Default values for equal comparison results. */ equal() { return { equal: true, couldBeEqual: true, similarity: 1, }; } /** * Normalize the comparison result. */ normalizeComparisonResult(result) { if (result.equal) { return { equal: true, couldBeEqual: true, similarity: 1, }; } return { couldBeEqual: false, similarity: 0, ...result, }; } /** * Get the possible positions of an object in an array. */ getArrayElementPossiblePositions(object, source, path = [], filename = null) { const comparisonResults = []; for (let i = 0; i < source.length; i++) { const sourceObject = source[i]; comparisonResults.push({ index: i, result: this.normalizeComparisonResult(this.compare(object, sourceObject, path, filename)), }); } // If there is only one equal result, we can use it const foundEquals = comparisonResults.filter((result) => result.result.equal); if (foundEquals.length === 1) { return foundEquals[0].index; } // If there's no clear winner, we need to sort the results... const sortedResults = comparisonResults .filter((result) => result.result.couldBeEqual) .sort((a, b) => { if (a.result.similarity > b.result.similarity) { return -1; } else if (a.result.similarity < b.result.similarity) { return 1; } return 0; }); if (sortedResults.length === 0) { return null; } return sortedResults.map((result) => { return { index: result.index, similarity: result.result.similarity, }; }); } /** * Do a 3-way comparison of an object in 3 arrays. */ getArrayElementDiff3(object, sources, path = [], filename = null) { const finalResults = { object, basePosition: null, oursPosition: null, theirsPosition: null, }; const sourceNames = ['base', 'ours', 'theirs']; sourceNames.forEach((key) => { const source = sources[key] || []; finalResults[`${key}Position`] = this.getArrayElementPossiblePositions(object, source, path, filename); }); return finalResults; } } exports.Comparer = Comparer; exports.default = Comparer;