UNPKG

object-deep-compare

Version:

A type-safe collection of comparison methods for objects and arrays in TypeScript/JavaScript

200 lines (199 loc) 8.85 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MemoizedCompareValuesWithConflicts = exports.GetCommonStructure = exports.IsSubset = exports.ObjectsAreEqual = exports.CompareValuesWithConflicts = void 0; const errors_1 = require("../core/errors"); const schema_validation_1 = require("../core/schema-validation"); const value_comparison_1 = require("../core/value-comparison"); const memoization_1 = require("../utils/memoization"); /** * Compares the properties of two objects (deep comparison) * Returns an array, each element is the path of a property that is different * * @param firstObject - First object to compare * @param secondObject - Second object to compare * @param pathOfConflict - Starting path for conflict (used in recursion) * @param options - Comparison options * @return Array of conflict paths */ const CompareValuesWithConflicts = (firstObject, secondObject, pathOfConflict = '', options = {}) => { // Perform schema validation if specified if (options.schemaValidation) { (0, schema_validation_1.ValidateObjectsAgainstSchemas)(firstObject, secondObject, options.schemaValidation); } return _CompareValuesWithConflicts(firstObject, secondObject, pathOfConflict, options); }; exports.CompareValuesWithConflicts = CompareValuesWithConflicts; /** * Internal implementation of CompareValuesWithConflicts * This is separated to allow for memoization */ const _CompareValuesWithConflicts = (firstObject, secondObject, pathOfConflict = '', options = {}) => { // Extract options const { circularReferences = 'error', pathFilter, strict = true } = options; // If the objects are the same reference, there are no conflicts if (Object.is(firstObject, secondObject)) { return []; } // Check for obvious circular references in the top-level objects if (circularReferences === 'error') { // Look for direct self-references in both objects for (const key in firstObject) { if (firstObject[key] === firstObject) { throw new errors_1.CircularReferenceError(key); } } for (const key in secondObject) { if (secondObject[key] === secondObject) { throw new errors_1.CircularReferenceError(key); } } } try { // Use the unified depth handling function const conflicts = (0, value_comparison_1.handleDepthComparison)(firstObject, secondObject, pathOfConflict, options, false, false); if (!Array.isArray(conflicts)) { return []; } // Type assertion because we know the conflicts will be strings due to detailed=false parameter const stringConflicts = conflicts; // Post-process the conflicts to maintain backward compatibility // This ensures arrays are reported at their parent level only const processedConflicts = new Set(); for (const conflict of stringConflicts) { // If this path should be filtered out, skip it if (pathFilter && !(0, path_filtering_1.shouldComparePath)(conflict, pathFilter)) { continue; } // If this is an array element conflict (contains [), get the array path if (conflict.includes('[')) { const arrayPath = conflict.substring(0, conflict.indexOf('[')); // If the array path should be filtered, skip this conflict if (pathFilter && !(0, path_filtering_1.shouldComparePath)(arrayPath, pathFilter)) { continue; } processedConflicts.add(arrayPath); } else { processedConflicts.add(conflict); } } return Array.from(processedConflicts); } catch (error) { if (error instanceof errors_1.CircularReferenceError) { if (circularReferences === 'error') { throw error; } // If circularReferences is 'ignore' and we're getting an error, return empty array return []; } throw error; } }; /** * Type guard that checks if two objects are equal * Can be used to narrow types in conditional branches * * @param firstObject - First object to compare * @param secondObject - Second object to compare * @param options - Optional comparison options * @returns Type predicate indicating if the objects are equal */ const ObjectsAreEqual = (firstObject, secondObject, options = {}) => { if (!firstObject || !secondObject) { return firstObject === secondObject; } // For type guard functionality, we need to check if the first object contains all properties // from the second object, this ensures the type narrowing works correctly const firstObjectKeys = Object.keys(firstObject); const secondObjectKeys = Object.keys(secondObject); // Check if all properties from second object exist in first object for (const key of secondObjectKeys) { if (!firstObjectKeys.includes(key)) { return false; } } // For non-strict comparison, we need to check if property values are equal // We specifically only compare the properties that exist in second object const comparisonObject = {}; for (const key of secondObjectKeys) { // @ts-ignore - We know these keys exist based on the check above comparisonObject[key] = firstObject[key]; } // Now compare only the properties that matter for type guard const conflicts = (0, exports.CompareValuesWithConflicts)(comparisonObject, secondObject, '', options); return conflicts.length === 0; }; exports.ObjectsAreEqual = ObjectsAreEqual; /** * Checks if the second object is a subset of the first object * This is useful for checking if an object satisfies a specific interface * * @param firstObject - Object to check against * @param secondObject - Object that should be a subset * @param options - Optional comparison options * @returns Boolean indicating if secondObject is a subset of firstObject */ const IsSubset = (firstObject, secondObject, options = {}) => { if (!firstObject || !secondObject) { return false; } // Create a filtered version of firstObject with only the keys from secondObject const secondObjectKeys = Object.keys(secondObject); const filteredFirstObject = {}; for (const key of secondObjectKeys) { if (key in firstObject) { // @ts-ignore - We know these keys exist based on the check filteredFirstObject[key] = firstObject[key]; } else { return false; // If secondObject has a key that firstObject doesn't, it's not a subset } } // Now compare the filtered first object with the second object const conflicts = (0, exports.CompareValuesWithConflicts)(filteredFirstObject, secondObject, '', options); return conflicts.length === 0; }; exports.IsSubset = IsSubset; /** * Gets the common type structure between two objects * Useful for understanding what properties are shared between objects * * @param firstObject - First object to compare * @param secondObject - Second object to compare * @returns A new object containing only common properties with their types */ const GetCommonStructure = (firstObject, secondObject) => { if (!firstObject || !secondObject) { return {}; } const result = {}; const { common } = (0, compare_properties_1.CompareProperties)(firstObject, secondObject); for (const key of common) { if (key in firstObject && key in secondObject) { // @ts-ignore - We know these keys exist based on the check const firstValue = firstObject[key]; // @ts-ignore const secondValue = secondObject[key]; // If both values are objects, recursively get their common structure if ((0, utils_1.isObject)(firstValue) && (0, utils_1.isObject)(secondValue)) { // @ts-ignore result[key] = (0, exports.GetCommonStructure)(firstValue, secondValue); } else { // @ts-ignore result[key] = firstValue; } } } return result; }; exports.GetCommonStructure = GetCommonStructure; /** * Memoized version of CompareValuesWithConflicts */ exports.MemoizedCompareValuesWithConflicts = (0, memoization_1.Memoize)(exports.CompareValuesWithConflicts, memoization_1.compareValuesWithConflictsKeyFn); // Import missing dependencies after defining functions to avoid circular dependencies const path_filtering_1 = require("../core/path-filtering"); const utils_1 = require("../core/utils"); const compare_properties_1 = require("./compare-properties");