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
JavaScript
;
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");