Group array by nested array key, and duplicate items in result - javascript

This is a bit tricky to find the right words, so hopefully showing some code will help.
I have the following (simplified) array of people and their departments. This comes from CMS data which allows a person to be added to multiple departments (hence why departments is an array).
[
{
id: '12345',
name: 'Person 1',
jobTitle: 'Engineering director',
departments: ['Engineering', 'Leadership']
},
{
id: '54321',
name: 'Person 2',
jobTitle: 'Junior engineer',
departments: ['Engineering']
},
{
id: '00001',
name: 'Person 3',
jobTitle: 'Founder',
departments: ['Leadership']
},
{
id: '00099',
name: 'Person 4',
jobTitle: 'No department',
departments: []
}
]
The result I'm after is to get the unique values of departments and create arrays for each, with the appropriate users inside it, so something like:
{
'Engineering': [
{
id: '12345',
name: 'Person 1',
jobTitle: 'Engineering director',
departments: ['Engineering', 'Leadership']
},
{
id: '54321',
name: 'Person 2',
jobTitle: 'Junior engineer',
departments: ['Engineering']
}
],
'Leadership': [
{
id: '12345',
name: 'Person 1',
jobTitle: 'Engineering director',
departments: ['Engineering', 'Leadership']
},
{
id: '00001',
name: 'Person 3',
jobTitle: 'Founder',
departments: ['Leadership']
}
]
}
I've got a groupBy function in my code already, but it doesn't quite do what I want (because it's not expecting an array as the property value), so if a person has multiple departments, I get an array with a concatenated name of both departments, but I want the same person to appear in multiple arrays instead.
This is my current groupBy function, but it's now distracting me and reduce is a concept my brain just really struggles with...!
function groupBy(objectArray, property) {
return objectArray.reduce(function (acc, obj) {
var key = obj[property];
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj);
return acc;
}, {});
}
// which I can use like:
const groupedPeople = Object.entries(groupBy(people, "departments"));
// and it'll return
/*
{
'Engineering,Leadership': [
{
id: '12345',
name: 'Person 1',
jobTitle: 'Engineering director',
departments: ['Engineering', 'Leadership']
}
],
'Engineering': [
{
id: '54321',
name: 'Person 2',
jobTitle: 'Junior engineer',
departments: ['Engineering']
}
],
'Leadership': [
{
id: '00001',
name: 'Person 3',
jobTitle: 'Founder',
departments: ['Leadership']
}
]
}
*/
I feel like I'm close, but can't get my brain to engage!
const people = [{
id: '12345',
name: 'Person 1',
jobTitle: 'Engineering director',
departments: ['Engineering', 'Leadership']
},
{
id: '54321',
name: 'Person 2',
jobTitle: 'Junior engineer',
departments: ['Engineering']
},
{
id: '00001',
name: 'Person 3',
jobTitle: 'Founder',
departments: ['Leadership']
},
{
id: '00099',
name: 'Person 4',
jobTitle: 'No department',
departments: []
}
]
function groupBy(objectArray, property) {
return objectArray.reduce(function(acc, obj) {
var key = obj[property];
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj);
return acc;
}, {});
}
// which I can use like:
const groupedPeople = Object.entries(groupBy(people, "departments"));
console.log("Grouped:", groupedPeople);

var key = obj[property];
on this line in your code, the key variable represents the array of deparments, which you then use as acc[key]. What JS does it that in converts the array into string to be used as a key of the acc object and the process for that is to just join the array by commas. What you need is to loop over the array instead:
function groupBy(objectArray, property) {
return objectArray.reduce(function (acc, obj) {
var keys = obj[property];
keys.forEach(key => {
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj);
})
return acc;
}, {});
}
Such change will make it work for your use case, the groupBy function will no longer work if the key is not an array, so use with caution or make it support both strings and arrays.

I was getting a recursion error in my solution and had to go with making a deep copy of an object (JSON.parse(JSON.stringify(obj))) before pushing it into 2 different arrays. Also, considering your property might or might not be an array, I normalized that part with:
let keys = Array.isArray(obj[property]) ? obj[property] : [obj[property]];
let people = [{
id: '12345',
name: 'Person 1',
jobTitle: 'Engineering director',
departments: ['Engineering', 'Leadership']
},
{
id: '54321',
name: 'Person 2',
jobTitle: 'Junior engineer',
departments: ['Engineering']
},
{
id: '00001',
name: 'Person 3',
jobTitle: 'Founder',
departments: ['Leadership']
},
{
id: '00099',
name: 'Person 4',
jobTitle: 'No department',
departments: []
}
];
function groupBy(objectArray, property) {
return objectArray.reduce((acc, obj) => {
let keys = Array.isArray(obj[property]) ? obj[property] : [obj[property]];
keys.forEach(k => {
acc[k] = acc[k] || [];
acc[k].push(JSON.parse(JSON.stringify(obj)))
})
return acc;
}, {});
}
console.log(groupBy(people, "departments"));
console.log(groupBy(people, "jobTitle"));

I have only changed 3 lines of your snippet, converting everything to an array first, then looping through that array and creating groups makes the code easier. You could also check if it's an array and write similar code with an if statement.
const people = [{
id: '12345',
name: 'Person 1',
jobTitle: 'Engineering director',
departments: ['Engineering', 'Leadership']
},
{
id: '54321',
name: 'Person 2',
jobTitle: 'Junior engineer',
departments: ['Engineering']
},
{
id: '00001',
name: 'Person 3',
jobTitle: 'Founder',
departments: ['Leadership']
},
{
id: '00099',
name: 'Person 4',
jobTitle: 'No department',
departments: []
}
]
function groupBy(objectArray, property) {
return objectArray.reduce(function(acc, obj) {
if (obj[property] === null || obj[property] === undefined) return acc; // if obj[property] is not defined or it's null don't add it to groupts
if (obj[property].constructor !== Array) obj[property] = [obj[property]] // obj[property] is always an array now
obj[property].forEach(key => {
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj);
})
return acc;
}, {});
}
// which I can use like:
const groupedPeople = Object.entries(groupBy(people, "departments"));
console.log("Grouped:", groupedPeople);

Related

How do I create an array of objects with a nested array based on a similar key?

I have an array that looks something like this
const example = [
{ id: '1', name: 'Person 1', organization: { id: '11', name: 'Organization A' } },
{ id: '2', name: 'Person 2', organization: { id: '12', name: 'Organization A' } },
{ id: '3', name: 'Person 3', organization: { id: '13', name: 'Organization B' } },
];
As you can see, the organization name is something I want to key off of and create a data structure like this:
const output = [
// data.value will be their ID
{
organizationName: 'Organization A',
data: [
{ label: 'Person 1', value: '1' },
{ label: 'Person 2', value: '2' },
],
},
{
organizationName: 'Organization B',
data: [
{ label: 'Person 3', value: '3' },
],
},
]
What I've tried
I know I want to use reduce for something like this, but I feel like I'm off:
const providerOptions = externalPeople.data.reduce((acc, currentValue) => {
const {
organization: { name: organizationName },
} = currentValue;
if (organizationName) {
acc.push({ organization: organizationName, data: [] });
} else {
const { name: externalPersonName, id } = currentValue;
acc[acc.length - 1].data.push({ name: externalPersonName, value: id });
}
return acc;
}, [] as any);
However the output comes out to something like this:
[
{organizationName: 'Organization A', data: []},
{organizationName: 'Organization A', data: []},
{organizationName: 'Organization B', data: []},
];
data doesn't seem to get anything pushed inside the array in this reduce function, and the organization name get duplicated... what am I doing wrong?
Easiest way is to use an Map/Set/or object to keep track of orgs you create. This way you are not searching in the array to see if the organization was found already. After you are done, you can create the array you want from the object.
const externalPeople = {
data : [
{ id: '1', name: 'Person 1', organization: { id: '11', name: 'Organization A' } },
{ id: '2', name: 'Person 2', organization: { id: '12', name: 'Organization A' } },
{ id: '3', name: 'Person 3', organization: { id: '13', name: 'Organization B' } },
],
};
const providerOptions = Object.values(externalPeople.data.reduce((acc, currentValue) => {
const {
organization: { name: organizationName },
name: externalPersonName,
id
} = currentValue;
// Is the org new? Yes, create an entry for it
if (!acc[organizationName]) {
acc[organizationName] = { organization: organizationName, data: [] };
}
// push the person to the organization
acc[organizationName].data.push({ name: externalPersonName, value: id });
return acc;
}, {}));
console.log(providerOptions)
Here is another solution
const example = [
{ id: '1', name: 'Person 1', organization: { id: '11', name: 'Organization A' } },
{ id: '2', name: 'Person 2', organization: { id: '12', name: 'Organization A' } },
{ id: '3', name: 'Person 3', organization: { id: '13', name: 'Organization B' } },
];
const result = example.reduce((res, entry) => {
const recordIndex = res.findIndex(rec => rec.organizationName === entry.organization.name);
if(recordIndex >= 0) {
res[recordIndex].data.push({ label: entry.name, value: entry.id});
} else {
const record = {
organizationName: entry.organization.name,
data: [{ label: entry.name, value: entry.id }]
};
res.push(record);
}
return res;
}, []);
console.log(result);
You are not checking if the value is already present in your accumulation acc
You can check it with a simple find in the if statement since it's an array
const providerOptions = externalPeople.data.reduce((acc, currentValue) => {
const {
organization: { name: organizationName },
} = currentValue;
//Check if organization is not present already
if (!acc.find(a => a.organization === organizationName)) {
//Add also the data of the element your are processing
acc.push({ organization: organizationName, data: [{label: currentValue.name, value: currentValue.id}] });
} else {
const { name: externalPersonName, id } = currentValue;
acc[acc.length - 1].data.push({ label: externalPersonName, value: id });
}
return acc;
}, [] as any);
I also added the data of the first element of the group you create when adding the organization.
The result should be as your expected output:
[
{
organization: 'Organization A',
data: [
{ label: 'Person 1', value: '1' },
{ label: 'Person 2', value: '2' }
]
},
{
organization: 'Organization B',
data: [
{ label: 'Person 3', value: '3' }
]
}
]
Hope it helps!
Compare this solution (using Lodash) with other solutions. Which one emphasises your intentions at most? This is why we use Lodash in our company - to maintain code as declarative as we can, because code readability, with minimum cognitive overload, is most important goal during coding.
const persons = [
{ id: '1', name: 'Person 1', organization: { id: '11', name: 'Organization A' } },
{ id: '2', name: 'Person 2', organization: { id: '12', name: 'Organization A' } },
{ id: '3', name: 'Person 3', organization: { id: '13', name: 'Organization B' } },
];
const personsByOrganizations = _.groupBy(persons, 'organization.name')
const output = _.map(personsByOrganizations, (persons, organizationName) => ({
organizationName,
data: _.map(persons, ({ name, id }) => ({
label: name,
value: id
}))
}))
Something like that with using a Set?
result = [...new Set(example.map(d => d.organization.name))].map(label => {
return {
organizationName: label,
data: example.filter(d => d.organization.name === label).map(d => {
return {label: d.name, value: d.id}
})
}
})
`

Merge array of objects preserving some key-values php

Consider the following two arrays:
[
{
id: jhz,
name: 'John',
eyes: 'Green',
description: 'Cool guy',
},
{
id: mbe,
name: 'Mary',
brand: 'M&B',
text: 'Something',
}
]
[
{
id: jhz,
name: 'John',
eyes: '',
},
{
id: mbe,
name: 'Mary',
},
{
id: 'beh',
name: 'Bernard',
}
]
First array may have any kind of key value pairs, but it will always have the key id and name. I want to merge the two arrays by taking id and name into account and preserving them, while merging everything else and replacing them with data from the first array if any keys duplicate.
Also tricky part - the merged array needs to follow the order of the second array.
So in this example the result I'm looking for is:
[
{
id: jhz,
name: 'John',
eyes: 'Green',
description: 'Cool guy',
},
{
id: mbe,
name: 'Mary',
brand: 'M&B',
text: 'Something',
},
{
id: 'beh',
name: 'Bernard',
}
]
you can do something like this using Array.map
const data1 = [{
id: 'jhz',
name: 'John',
eyes: 'Green',
description: 'Cool guy',
},
{
id: 'mbe',
name: 'Mary',
brand: 'M&B',
text: 'Something',
}
]
const data2 = [{
id: 'jhz',
name: 'John',
eyes: '',
},
{
id: 'mbe',
name: 'Mary',
},
{
id: 'beh',
name: 'Bernard',
}
]
const result = data2.map(d => ({...d, ...(data1.find(d1 => d1.id === d.id && d1.name === d.name) || {})}))
console.log(result)

Lodash - Group Children using N (repeated) keys for parent

I have a select from database that basically joins a master entity and a child entity, like the example below (Cars vs Parts) as snippet
And I'd like to group by all the keys for the Car part, and have an array of the parts, but including all the keys for the car and the parts. For the groupBy examples I could find, generally it uses groupBy, but it only groups one key only. I was able to achieve using a lot of workarounds, but I'm sure it is manageable (and achieve more performance) to do the same using either es6 or lodash.
Could someone help me in this matter? I've tried multiple groupBy and reduce combinations, but was not able to chain those correctly.
var data = [{id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId1',
partName: 'partName1'},
{id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId2',
partName: 'partName2'},
{id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId3',
partName: 'partName3'},
{id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId4',
partName: 'partName4'}
];
var dictionary = {};
data.forEach(function(item, index, array)
{
var masterDocument = null;
if (typeof dictionary[item.id] === 'undefined')
{
masterDocument = {
id: item.id,
name: item.name,
description: item.description,
parts: []
};
dictionary[item.id] = masterDocument;
}
else {
var masterDocument = dictionary[item.id];
}
masterDocument.parts.push({
partId: item.partId,
partName: item.partName
})
})
var asList = [];
Object.keys(dictionary).forEach((item) => {
asList.push(dictionary[item])
});
console.log(asList);
.as-console-wrapper {
min-height: 100%;
top: 0;
}
Here's the snippet with just the result I want to achieve.
[
{
"id": "car1",
"name": "name for car 1",
"description": "description for car1",
"parts": [
{
"partId": "partId1",
"partName": "partName1"
},
{
"partId": "partId2",
"partName": "partName2"
}
]
},
{
"id": "car2",
"name": "name for car 2",
"description": "description for car2",
"parts": [
{
"partId": "partId3",
"partName": "partName3"
},
{
"partId": "partId4",
"partName": "partName4"
}
]
}
]
The code below should solve your problem using Lodash. Basically what you want to do is:
Group the cars by id
Once you have the cars grouped by their IDs, iterate over that top-level array with a map call, and grab the id, name, and description from the first entry (since you know these are all the same for all cars in this group). Save these for later for your return object
Then, while still in this top-level map iteration, also iterate over the individual cars in each carGrouping (a nested map) to get their partId and partName, and put those into a parts array
Finally, get all of your object attributes, put them all into a return object in your top-level map call, and return them all back
Don't forget to call valueOf() at the end of your chain to get the Lodash sequence to fire
let data = [{id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId1',
partName: 'partName1'},
{id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId2',
partName: 'partName2'},
{id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId3',
partName: 'partName3'},
{id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId4',
partName: 'partName4'}
];
const carsInfo = _(data)
.groupBy('id')
.map(carGrouping => {
// all cars in this array have the same id, name, description, so just grab them from the first one
const firstCarInGroup = _.first(carGrouping);
const id = firstCarInGroup.id;
const name = firstCarInGroup.name;
const description = firstCarInGroup.description;
// do a nested map call to iterate over each car in the carGrouping, and grab their partId and partName, and return it in an object
const parts = _.map(carGrouping, car => {
return {
partId: car.partId,
partName: car.partName
}
});
return {
id,
name,
description,
parts
}
})
.valueOf();
console.log(carsInfo);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
This one uses no dependencies. Just plain ES6+.
const data = [{
id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId1',
partName: 'partName1'
},
{
id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId2',
partName: 'partName2'
},
{
id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId3',
partName: 'partName3'
},
{
id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId4',
partName: 'partName4'
}
];
const nested = data.reduce((acc, part) => {
let index = acc.findIndex(car => car.id === part.id)
const { partId, partName, ...car } = part
if (index === -1) {
acc.push({
...car,
parts: [],
})
index = acc.length - 1
}
acc[index].parts.push({
partId,
partName,
})
return acc
}, [])
console.log(JSON.stringify(nested, null, ' '));

How to iterate through an array of object properties, in an array of objects

I have an array of objects, that looks like this:
data = [
{
title: 'John Doe',
departments: [
{ name: 'Marketing', slug: 'marketing'},
{ name: 'Sales', slug: 'sales'},
{ name: 'Administration', slug: 'administration'},
]
},
{
title: 'John Doe Junior',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Sales', slug: 'sales'},
]
},
{
title: 'Rick Stone',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Marketing', slug: 'marketin'},
]
},
]
How can I iterate over each object's departments array and create new arrays where I would have employees sorted by departments, so that the end result would like this:
operations = [
{
title: 'John Doe Junior',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Sales', slug: 'sales'},
]
},
{
title: 'Rick Stone',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Marketing', slug: 'marketin'},
]
},
]
marketing = [
{
title: 'John Doe',
departments: [
{ name: 'Marketing', slug: 'marketing'},
{ name: 'Sales', slug: 'sales'},
{ name: 'Administration', slug: 'administration'},
]
},
{
title: 'Rick Stone',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Marketing', slug: 'marketin'},
]
},
]
What would be the way to create dynamically this kind of arrays?
Update
I have tried to come up with a solution using the suggestion from the answer, where I would dynamically create an array with department objects that would have an array of employees:
const isInDepartment = departmentToCheck => employer => employer.departments.find(department => department.slug == departmentToCheck);
var departments = [];
function check(departments, name) {
return departments.some(object => name === object.department);
}
employees.forEach((employee) => {
employee.departments.forEach((department) => {
let found = check(departments, department.slug);
if (!found) {
departments.push({ department: department.slug });
}
});
});
departments.forEach((department) => {
// push an array of employees to each department
//employees.filter(isInDepartment(department));
});
But, I don't know how can I push the array of employees to the object in the array that I am looping at the end?
This is the fiddle.
How about this? I use Array.protoype.filter operation, and I use a higher-order function (in this case a function that returns a function) to create the predicate (function that returns a boolean) that will check whether an employee is in a specific department. I added some (hopefully) clarifying comments in the code.
Edit: with the new code and context you provided this JSFiddle demo shows how it would work together.
const employees = [
{
title: 'John Doe',
departments: [
{ name: 'Marketing', slug: 'marketing'},
{ name: 'Sales', slug: 'sales'},
{ name: 'Administration', slug: 'administration'}
]
},
{
title: 'John Doe Junior',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Sales', slug: 'sales'}
]
},
{
title: 'Rick Stone',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Marketing', slug: 'marketin'}
]
}
];
// given a department, this returns a function that checks
// whether an employee is in the specified department
// NOTE: the "find" returns the found object (truthy)
// or undefined (falsy) if no match was found.
const isInDepartment =
departmentToCheck =>
employee => employee.departments.find(dep => dep.name == departmentToCheck);
const employeesInMarketing = employees.filter(isInDepartment('Marketing'));
const employeesInOperations = employees.filter(isInDepartment('Operations'));
console.log('Employees in marketing', employeesInMarketing);
console.log('Employees in operations', employeesInOperations);

Merge & Group Two Javascript array of objects and Group

I have two arrays of objects. One array contains list of items, another array contains list of categories. I want to create a new array based on categoryIds. I tried using lodash. But, couldn't get the correct solution.
I can do this using looping. But, I am looking for more clean approach.
var items = [
{
id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1'
},
{
id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
},
{
id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1'
},
{
id: '004',
name: 'item4',
description: 'description of item4'
}
];
var categories = [
{
id: 'cat1',
name: 'Category1'
},
{
id: 'cat2',
name: 'Category2'
}
];
Expected output
[
{
categoryId: 'cat1',
name: 'Category1',
items: [
{
id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1'
},
{
id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1'
}
]
},
{
categoryId: 'cat2',
name: 'Category2',
items: [
{
id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
}
]
},
{
categoryId: '',
name: '',
items: [
{
id: '004',
name: 'item4',
description: 'description of item4'
}
]
}
]
https://jsfiddle.net/sfpd3ppn/
Thanks for the help
The following does the trick:
var items = [{ id: '001', name: 'item1', description: 'description of item1', categoryId: 'cat1' }, { id: '002', name: 'item2', description: 'description of item2', categoryId: 'cat2' }, { id: '003', name: 'item3', description: 'description of item3', categoryId: 'cat1' }, { id: '004', name: 'item4', description: 'description of item4' } ];
var categories = [ { id: 'cat1', name: 'Category1' }, { id: 'cat2', name: 'Category2' } ];
var output = categories.concat([{id:'',name:''}]).map(function(v) {
return {
categoryId: v.id,
name: v.name,
items: items.filter(function(o) {
return o.categoryId === v.id || !o.categoryId && !v.id;
})
};
});
console.log(output);
I start by using .concat() to create a new categories array that holds the original categories items plus an "empty" category. Then I .map() that array to return category objects with your desired output structure, each of which has an items array that is produced by .filter()ing the original items array.
(Note that the items arrays within the output contain references to the same objects that were in the original items input, not copies of them. If you wanted copies you could add another .map() after the .filter().)
You can accomplish the desired result using a reduce. We are going to start with the original categories array and reduce the items array into it.
var items = [
{ id: '001', name: 'item1', description: 'description of item1', categoryId: 'cat1' },
{ id: '002', name: 'item2', description: 'description of item2', categoryId: 'cat2' },
{ id: '003', name: 'item3', description: 'description of item3', categoryId: 'cat1' },
{ id: '004', name: 'item4', description: 'description of item4' }
];
var categories = [
{ id: 'cat1', name: 'Category1' },
{ id: 'cat2', name: 'Category2' }
];
// Lets add the empty category at the beginning. This simplifies the logic.
categories.push({ id: '', name: '' });
// This is a function that will return a function to be used as a filter later on
function createFilter (category) {
return function (item) {
return item.id === category;
};
}
var mergedSet = items.reduce(function (previous, current) {
// Get the category ID of the current item, if it doesn't exist set to empty string
var categoryId = current.categoryId || '';
// Find the cateogry that matches the category ID
var category = previous.find(createFilter(categoryId));
// If the items property doesn't exists (we don't have any items), create an empty array
if (!category.items) { category.items = []; }
// Add the item the category
category.items.push(current);
// Return the current value that will be used in the next iteration.
// Note, the initial value of previous will be the intial value of categories.
return previous;
}, categories);
console.log(mergedSet);
/* Output
[
{ id: 'cat1',
name: 'Category1',
items:
[ { id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1' },
{ id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1' }
]
},
{ id: 'cat2',
name: 'Category2',
items:
[ { id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
}
]
},
{ id: '',
name: '',
items:
[ { id: '004',
name: 'item4',
description: 'description of item4' } ] }
]
*/
Assuming the variables categories and items are assigned as you defined above:
const keyedCategories = _(categories)
.concat({ id: '', name: '' })
.keyBy('id')
.value();
const groupedItems = _.groupBy(items, (item) => _.get(item, 'categoryId', ''));
const result = _.reduce(groupedItems, (acc, value, key) => {
const { id: categoryId, name } = keyedCategories[key];
return _.concat(acc, { categoryId, name, items: value });
}, []);

Categories