jsonata-js / jsonata

JSONata query and transformation language - http://jsonata.org
MIT License
2.06k stars 220 forks source link

Q: Using transform operator to delete variable keys #493

Open fab-mindflow opened 3 years ago

fab-mindflow commented 3 years ago

Hi,

I'm struggling to use the transform operator to delete a key without knowing its name (the string can't be static in my use case). Is this possible? Can I use a @ context variable binding with transform operator in order to delete the matching element? Are there other alternative?

Here is my example where I'd want to delete any key/value object at any level in the hierarchy (the example is just one level) whose readOnly property is true.

image

Thanks, Fabrice

markmelville commented 3 years ago

I think you simply need to wrap the $key in square brackets. so | { "i":"am here"}, [$key] | . I can't say for sure with your example as it's a screenshot. FYI, the first icon on the right side of the header lets you create a sharable URL of your example.

lepinsk commented 3 years ago

I'm playing around with a similar problem, and I've shared an example here: https://try.jsonata.org/EA_jo9zN7 (in this one, I hard-code the key value to be deleted).

I've been unable to resolve it, as [$key] doesn't evaluate to a string, and I can't figure out how to get it to spit out the name of $key. Here it is with [$key]: https://try.jsonata.org/aki1aZlvG

In my case, I want to actually delete $key2, so I've got three problems:

andrew-coleman commented 3 years ago

@lepinsk please could you paste the resultant JSON that you are trying to generate from this input data? That will help us understand what you are trying to achieve. Thanks.

@mindflow-aws, as Mark suggested, please could you paste a link to the jsonata exerciser rather than a screenshot and also the result you are trying to achieve. Thanks.

lepinsk commented 3 years ago

@andrew-coleman Sorry, that should have been included in my initial message. 🤦🏼‍♂️

Very broadly, I'm trying to find keys of a specific name (lets say targetKey) at arbitrary depths and move their values up to their parent level, so for example:

{
    "someKey": {
        "targetKey": "someval1"
    },
    "otherKey": {
        "test": "yep"
    },
    "anotherKey": {
        "anotherInner": {
            "targetKey": "anotherval"
        }
    }
}

Would be transformed to:

{
    "someKey": "someval1",
    "otherKey": {
        "test": "yep"
    },
    "anotherKey": {
        "anotherInner": "anotherval"
    }
}

I've tried things like: $ ~> | ** [$lookup('targetKey')] | {"this should be parent": $lookup('targetKey')}, ['targetKey'] | (which I've saved here). This does find and identify targetKey and its values, and delete targetKey as expected, however I'm unable to walk one level up to insert a new value.

I'm aware of the % parent operator, but it's not clear to me whether there's a way to use that in the transform's update parameter in order to walk up a level. (Conversely, if the head parameter is operating up a level, I'm unsure how to pass a deletion that walks down one level and deletes a child, as it looks like deletion expects a string that matches a key at the same depth that the transform is operating.)

andrew-coleman commented 3 years ago

The transform operator doesn't work recursively. Probably best write a recursive function that does what you need, such as this:

(
  $promote := function($obj) {
    $each($obj, function($v,$k){
      {$k: ($v.targetKey ? $v.targetKey : $v[*] ? $promote($v) : $v)}
    }) ~> $merge()
  };
  $promote($)
)

It uses the $each() ~> $merge() pattern to work on name/values pairs individually. If the value contains a targetKey property, then it uses the value of that, otherwise if it's another object, it processes that recursively, and if it's not an object, it just uses that value.

See https://try.jsonata.org/CoWnaJ5Yd

lepinsk commented 3 years ago

@andrew-coleman Incredible — thanks so much for this. (And for the great work on JSONata!)

lepinsk commented 3 years ago

@andrew-coleman I found one edge case that I wanted to address here, in the event that someone else stumbles on this thread.

This will fail on any data structure that contains arrays within it, I believe because $each presumes it's always operating on objects (Argument 1 of function "each" does not match function signature).

I've handled that case (I think) with the following modification (available to test here):

(
  $promote := function($obj) {
    $type($obj) = "array" ? (
        [$map($obj, $promote)]
    ) : (
        $each($obj, function($v,$k){
            {$k: ($v.targetKey ? $v.targetKey : $v[*] ? $promote($v) : $v)}
        }) ~> $merge()
    )
  };
  $promote($)
)

I'm using a $type conditional test to decide whether to iterate through the values using $map (in the case of an array), or proceed to $each as usual. (I'm enclosing the return from $map in square brackets to ensure that it doesn't convert single-element arrays to objects.)