dchester / jsonpath

Query and manipulate JavaScript objects with JSONPath expressions. Robust JSONPath engine for Node.js.
MIT License
1.33k stars 215 forks source link

Struggling with "update or create in array" functionality #170

Open pepijnolivier opened 2 years ago

pepijnolivier commented 2 years ago

So basically I have a javascript data object that might or might not contain an array entry for the jsonpath. I would like to use jp.value(data, path, value) and have it update the value in the array if it exists - or create it if it doesn't.

Updating it this way works fine, but something odd is happening during create.

Proof of concept:

import jp from 'jsonpath';

const path = `$.items[?(@.type=='Food')]`;
const value = { "title": "Lasagna", "type": "Food" };

// example one: this works fine
const dataWithExistingFoodType = {
    items: [
        { "title": "Water", type: 'Drinks' },
        { "title": "Spaghetti", type: 'Food' }
    ]
};

jp.value(dataWithExistingFoodType, path, value);
console.log('dataWithExistingFoodType', dataWithExistingFoodType);

// example two: this doesn't work. See output, the jsonpath somehow ends up as as an object key
const dataWithoutExistingFoodType = {
    items: [
        { "title": "Water", type: 'Drinks' }
    ]
};
jp.value(dataWithoutExistingFoodType, path, value);
console.log('dataWithoutExistingFoodType', dataWithoutExistingFoodType);

Output

dataWithExistingFoodType {
  items: [
    { title: 'Water', type: 'Drinks' },
    { title: 'Lasagna', type: 'Food' }
  ]
}
dataWithoutExistingFoodType {
  items: [
    { title: 'Water', type: 'Drinks' },
    "?(@.type=='Food')": { title: 'Lasagna', type: 'Food' }
  ]
}

☝️ You'll see that in the second example, the data is created inside the array... But not exactly with the right jsonpath. Somehow the jsonpath ends up as an object key. Is there any way I can make it so that the data is correctly appended to the array as I would expect it to ?

Thanks for any directions

dsdavis4 commented 2 years ago

I am running into the same issue.

pepijnolivier commented 2 years ago

I ended up working around the issue by defining the field with just the root array json path (eg $.items rather than $.items[?(@.type=='Food')]) and using a wrapper component to handle the create or update functionality.

It's a workaround, but gets the job done. Just FYI for anyone else struggling with this (@dsdavis4)

drc-nloftsgard commented 2 years ago

I'm having a similar issue trying to set a property in an object which doesn't already exist in the object. So I wrote a function as a workaround. Essentially I initialize each property that doesn't exist in the object before setting the final value. This probably doesn't handle all the edge cases but I think a similar approach could be taken for arrays where you push a new item onto the array.

function setValueOnNewProperty(theObject, newValue) {
    let paths = jp.paths(theObject, path);

    const parsed = jp.parse(path);
    const queue = [];
    let shorterPath = '';

    // find a path that does exist on the object
    while (paths.length === 0) {
        queue.push(parsed.pop());
        shorterPath = jp.stringify(parsed);
        paths = jp.paths(theObject, shorterPath);
    }

    // build up the object to set the value
    while (queue.length > 0) {

        const subPath = queue.pop();
        const newPath = subPath.expression.value;

        // need to initialize every property that doesn't already exist on the object
        const currentValue = jp.value(theObject, shorterPath);
        currentValue[newPath] = {}
        jp.value(theObject, shorterPath, currentValue);

        parsed.push(subPath);
        shorterPath = jp.stringify(parsed);

        // at the end of reconstructing the path -> set the final/new value
        if (queue.length === 0) {
            jp.value(theObject, shorterPath, newValue);
        }
    }
}
skyreginag commented 1 year ago

If it may be of any use to someone: this is a further work on @drc-nloftsgard workaround that:

` const JSON_PATH_ROOT = "$"; function setValueOnNewProperty(theObject, path, newValue) { let paths = jp.paths(theObject, path);

    const parsed = jp.parse(path);
    const queue = [];
    let shorterPath = '';

    // find a path that does exist on the object
    while (paths.length === 0) {
        queue.push(parsed.pop());
        shorterPath = jp.stringify(parsed);
        paths = jp.paths(theObject, shorterPath);
    }

    // build up the object to set the value
    while (queue.length > 0) {

        const subPath = queue.pop();
        const newPath = subPath.expression.value;

        // need to initialize every property that doesn't already exist on the object
        if (shorterPath === JSON_PATH_ROOT)
        {
            // root, see https://github.com/dchester/jsonpath/issues/72
            jp.value(theObject, "$")[newPath] = {}
        }
        else 
        {
            jp.apply(theObject, shorterPath, function(elem) {
                elem[newPath] = {}
                return elem
            })
        }

        parsed.push(subPath);
        shorterPath = jp.stringify(parsed);

        // at the end of reconstructing the path -> set the final/new value
        if (queue.length === 0) {
            jp.apply(theObject, shorterPath, x => newValue);
            return
        }
    }

    // When property exists
    jp.apply(theObject, path, x => newValue);
}`