import {
ObjectData ,
ObjectDiff ,
STATUS ,
SubProperties ,
ObjectOptions ,
ObjectDiffStatus ,
} from "./model" ;
import { isObject , isEqual } from "./utils" ;
function getLeanDiff (
diff : ObjectDiff [ "diff" ] ,
showOnly : ObjectOptions [ "showOnly" ] = { statuses : [ ] , granularity : "basic" }
) : ObjectDiff [ "diff" ] {
const { statuses , granularity } = showOnly ;
return diff . reduce ( ( acc , value ) = > {
if ( statuses . includes ( value . status ) ) {
return [ . . . acc , value ] ;
}
if ( granularity === "deep" && value . subPropertiesDiff ) {
const cleanSubPropertiesDiff = getLeanDiff (
value . subPropertiesDiff ,
showOnly
) ;
if ( cleanSubPropertiesDiff . length > 0 ) {
return [
. . . acc ,
{ . . . value , subPropertiesDiff : cleanSubPropertiesDiff } ,
] ;
}
}
// @ts-ignore
if ( granularity === "deep" && value . subDiff ) {
// @ts-ignore
const cleanSubDiff = getLeanDiff ( value . subDiff , showOnly ) ;
if ( cleanSubDiff . length > 0 ) {
return [ . . . acc , { . . . value , subDiff : cleanSubDiff } ] ;
}
}
return acc ;
} , [ ] as ObjectDiff [ "diff" ] ) ;
}
function getObjectStatus ( diff : ObjectDiff [ "diff" ] ) : ObjectDiffStatus {
return diff . some ( ( property ) = > property . status !== STATUS . EQUAL )
? STATUS . UPDATED
: STATUS . EQUAL ;
}
function formatSingleObjectDiff (
data : ObjectData ,
status : ObjectDiffStatus
) : ObjectDiff {
if ( ! data ) {
return {
type : "object" ,
status : STATUS.EQUAL ,
diff : [ ] ,
} ;
}
const diff : ObjectDiff [ "diff" ] = [ ] ;
Object . entries ( data ) . forEach ( ( [ property , value ] ) = > {
if ( isObject ( value ) ) {
const subPropertiesDiff : SubProperties [ ] = [ ] ;
Object . entries ( value ) . forEach ( ( [ subProperty , subValue ] ) = > {
subPropertiesDiff . push ( {
property : subProperty ,
previousValue : status === STATUS . ADDED ? undefined : subValue ,
currentValue : status === STATUS . ADDED ? subValue : undefined ,
status ,
} ) ;
} ) ;
return diff . push ( {
property : property ,
previousValue : status === STATUS . ADDED ? undefined : data [ property ] ,
currentValue : status === STATUS . ADDED ? value : undefined ,
status ,
subPropertiesDiff ,
} ) ;
}
return diff . push ( {
property ,
previousValue : status === STATUS . ADDED ? undefined : data [ property ] ,
currentValue : status === STATUS . ADDED ? value : undefined ,
status ,
} ) ;
} ) ;
return {
type : "object" ,
status ,
diff ,
} ;
}
function getPreviousMatch (
previousValue : any | undefined ,
nextSubProperty : any ,
options? : ObjectOptions
) : any | undefined {
if ( ! previousValue ) {
return undefined ;
}
const previousMatch = Object . entries ( previousValue ) . find ( ( [ subPreviousKey ] ) = >
isEqual ( subPreviousKey , nextSubProperty , options )
) ;
return previousMatch ? previousMatch [ 1 ] : undefined ;
}
function getValueStatus (
previousValue : any ,
nextValue : any ,
options? : ObjectOptions
) : ObjectDiffStatus {
if ( isEqual ( previousValue , nextValue , options ) ) {
return STATUS . EQUAL ;
}
return STATUS . UPDATED ;
}
function getPropertyStatus (
subPropertiesDiff : SubProperties [ ]
) : ObjectDiffStatus {
return subPropertiesDiff . some ( ( property ) = > property . status !== STATUS . EQUAL )
? STATUS . UPDATED
: STATUS . EQUAL ;
}
function getDeletedProperties (
previousValue : Record < string , any > | undefined ,
nextValue : Record < string , any >
) : { property : string ; value : any } [ ] | undefined {
if ( ! previousValue ) return undefined ;
const prevKeys = Object . keys ( previousValue ) ;
const nextKeys = Object . keys ( nextValue ) ;
const deletedKeys = prevKeys . filter ( ( prevKey ) = > ! nextKeys . includes ( prevKey ) ) ;
if ( deletedKeys . length > 0 ) {
return deletedKeys . map ( ( deletedKey ) = > ( {
property : deletedKey ,
value : previousValue [ deletedKey ] ,
} ) ) ;
}
return undefined ;
}
function getSubPropertiesDiff (
previousValue : Record < string , any > | undefined ,
nextValue : Record < string , any > ,
options? : ObjectOptions
) : SubProperties [ ] {
const subPropertiesDiff : SubProperties [ ] = [ ] ;
let subDiff : SubProperties [ ] ;
const deletedMainSubProperties = getDeletedProperties (
previousValue ,
nextValue
) ;
if ( deletedMainSubProperties ) {
deletedMainSubProperties . forEach ( ( deletedProperty ) = > {
subPropertiesDiff . push ( {
property : deletedProperty.property ,
previousValue : deletedProperty.value ,
currentValue : undefined ,
status : STATUS.DELETED ,
} ) ;
} ) ;
}
Object . entries ( nextValue ) . forEach ( ( [ nextSubProperty , nextSubValue ] ) = > {
const previousMatch = getPreviousMatch (
previousValue ,
nextSubProperty ,
options
) ;
if ( ! ! ! previousMatch ) {
return subPropertiesDiff . push ( {
property : nextSubProperty ,
previousValue : previousMatch ,
currentValue : nextSubValue ,
status :
! previousValue || ! ( nextSubProperty in previousValue )
? STATUS . ADDED
: previousMatch === nextSubValue
? STATUS . EQUAL
: STATUS . UPDATED ,
} ) ;
}
if ( isObject ( nextSubValue ) ) {
const data : SubProperties [ ] = getSubPropertiesDiff (
previousMatch ,
nextSubValue ,
options
) ;
if ( data && data . length > 0 ) {
subDiff = data ;
}
}
if ( previousMatch ) {
subPropertiesDiff . push ( {
property : nextSubProperty ,
previousValue : previousMatch ,
currentValue : nextSubValue ,
status : getValueStatus ( previousMatch , nextSubValue , options ) ,
. . . ( ! ! subDiff && { subDiff } ) ,
} ) ;
}
} ) ;
return subPropertiesDiff ;
}
export function getObjectDiff (
prevData : ObjectData ,
nextData : ObjectData ,
options : ObjectOptions = {
ignoreArrayOrder : false ,
showOnly : { statuses : [ ] , granularity : "basic" } ,
}
) : ObjectDiff {
if ( ! prevData && ! nextData ) {
return {
type : "object" ,
status : STATUS.EQUAL ,
diff : [ ] ,
} ;
}
if ( ! prevData ) {
return formatSingleObjectDiff ( nextData , STATUS . ADDED ) ;
}
if ( ! nextData ) {
return formatSingleObjectDiff ( prevData , STATUS . DELETED ) ;
}
const diff : ObjectDiff [ "diff" ] = [ ] ;
Object . entries ( nextData ) . forEach ( ( [ nextProperty , nextValue ] ) = > {
const previousValue = prevData [ nextProperty ] ;
if ( ! ! ! previousValue ) {
return diff . push ( {
property : nextProperty ,
previousValue ,
currentValue : nextValue ,
status : ! ( nextProperty in prevData )
? STATUS . ADDED
: previousValue === nextValue
? STATUS . EQUAL
: STATUS . UPDATED ,
} ) ;
}
if ( isObject ( nextValue ) ) {
const subPropertiesDiff : SubProperties [ ] = getSubPropertiesDiff (
previousValue ,
nextValue ,
options
) ;
const subPropertyStatus = getPropertyStatus ( subPropertiesDiff ) ;
return diff . push ( {
property : nextProperty ,
previousValue ,
currentValue : nextValue ,
status : subPropertyStatus ,
. . . ( subPropertyStatus !== STATUS . EQUAL && { subPropertiesDiff } ) ,
} ) ;
}
return diff . push ( {
property : nextProperty ,
previousValue ,
currentValue : nextValue ,
status : getValueStatus ( previousValue , nextValue , options ) ,
} ) ;
} ) ;
const deletedProperties = getDeletedProperties ( prevData , nextData ) ;
if ( deletedProperties ) {
deletedProperties . forEach ( ( deletedProperty ) = > {
diff . push ( {
property : deletedProperty.property ,
previousValue : deletedProperty.value ,
currentValue : undefined ,
status : STATUS.DELETED ,
} ) ;
} ) ;
}
if ( options . showOnly && options . showOnly . statuses . length > 0 ) {
return {
type : "object" ,
status : getObjectStatus ( diff ) ,
diff : getLeanDiff ( diff , options . showOnly ) ,
} ;
}
return {
type : "object" ,
status : getObjectStatus ( diff ) ,
diff ,
} ;
}