All files / owid-grapher/clientUtils/persistable Persistable.ts

92.96% Statements 66/71
94.44% Branches 17/18
100% Functions 3/3
92.96% Lines 66/71

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 811x 1x 1x 1x 1x 1x 1x 1x 1x 1x 20x 20x 20x 20x 20x 20x 20x 720x 720x 720x 720x 720x 255x 255x 255x 465x 465x 465x 440x 440x 440x   385x 385x 720x 20x 20x 1x 1x 1x 820x 820x 820x 28020x 953x 953x 953x 953x 852x 953x 953x 28020x 378x 1x 1x 1x 86x 86x 86x 86x 86x 86x 86x 86x 475x 475x         475x                    
import { toJS } from "mobx"
import { isEqual } from "../Util"
 
// Any classes that the user can edit, save, and then rehydrate should implement this interface
export interface Persistable {
    toObject(): any // This should dehydrate any runtime instances to a plain object ready to be JSON stringified
    updateFromObject(obj: any): any // This should parse an incoming object, extend the current instance, and create new instances for any non native class types
}
 
// Todo: see if there's a better way to do this with Mobx
export function objectWithPersistablesToObject<T>(
    objWithPersistables: T,
    keysToSerialize: string[] = []
): T {
    const obj = toJS(objWithPersistables) as any
    const keysSet = new Set(keysToSerialize)
    Object.keys(obj).forEach((key) => {
        const val = (objWithPersistables as any)[key]
        const valIsPersistable = val && val.toObject
 
        // Delete any keys we don't want to serialize, if a keep list is provided
        if (keysToSerialize.length && !keysSet.has(key)) {
            delete obj[key]
            return
        }
 
        // Val is persistable, call toObject
        if (valIsPersistable) obj[key] = val.toObject()
        else if (Array.isArray(val))
            // Scan array for persistables and seriazile.
            obj[key] = val.map((item) =>
                item?.toObject ? item.toObject() : item
            )
        else obj[key] = val
    })
    return obj as T
}
 
// Basically does an Object.assign, except if the target is a Persistable, will call updateFromObject on
// that Persistable. It does not recurse. Will only update top level Persistables.
export function updatePersistables(target: any, obj: any): void {
    if (obj === undefined) return
    for (const key in target) {
        if (key in obj) {
            const currentVal = target[key]
            const currentValIsPersistableObject = currentVal?.updateFromObject
            const newVal = obj[key]
            if (currentValIsPersistableObject)
                currentVal.updateFromObject(newVal)
            else target[key] = newVal
        }
    }
}
 
// Don't persist properties that haven't changed from the defaults, and don't
// keep properties not on the comparable class
export function deleteRuntimeAndUnchangedProps<T>(
    changedObj: T,
    defaultObject: T
): T {
    const obj = changedObj as any
    const defaultObj = defaultObject as any
    const defaultKeys = new Set(Object.keys(defaultObj))
    Object.keys(obj).forEach((prop) => {
        const key = prop as any
        if (!defaultKeys.has(key)) {
            // Don't persist any runtime props not in the persistable instance
            delete obj[key]
            return
        }
 
        const currentValue = obj[key]
        const defaultValue = defaultObj[key]
        if (isEqual(currentValue, defaultValue)) {
            // Don't persist any values that weren't changed from the default
            delete obj[key]
        }
    })
    return obj
}