Thank you for this question. It was enjoyable to work with.
I hope I have explained myself well enough below, but feel free to ask about anything unclear.
Here's a variation without any reduce:
group_by(.item) |
map({
item: first.item,
attributes: (
group_by(.attributes[].type) |
map({
type: first.attributes[].type,
colour: ( map(.attributes[].colour[]) )
})
)
})
I start by grouping the original elements by their item key. This gives us an array that is grouped by item. Each element in that array consists of a sub-array containing all the original objects with the same item.
The first map() brings these groups together by creating one object for each group. The object has item and attributes keys, and the value for the item key is taken arbitrarily from the first element of the group (they are all the same).
A further group_by() and map() creates the value for the attributes key. This time grouping over the type key down in the attributes array of the original objects and, for each created group, collecting the type and colour values from the original objects.
You can also do it with reduce, like so:
group_by(.item) |
map(reduce .[] as $a ({}; .item = $a.item | .attributes += $a.attributes)) |
map(.attributes |= (
group_by(.type) |
map(reduce .[] as $a ({}; .type = $a.type | .colour += $a.colour))
))
This is more or less group_by()+map(reduce) to create the outer structure of the result, i.e., the grouping of the data into parts based on item and organising that outer structure. The values for the attributes array are just passed along.
This pattern (group_by()+map(reduce)) is then repeated for each group's attributes array for grouping and organising it based on the type values.