How to create a query in Mongoose/Mongodb to obtain this json? - javascript

I'm trying to obtain an object like this from my mongodb, counting the OS per month:
{January: {Android: 30, iOS: 10, winPhone: 5}, February: {Android: 4, iOS: 40}, etc}.
And here is my mongoose schema:
var MySchema = new Schema({
date: {type: Date, default: Date.now},
os: String
});
Can give me someone an idea? Is there a way to create a single query to return me the entire object or should I build it piece by piece, unifying multiple queries?
Thank you!

You're probably better off using an output structure that's more natural for MongoDB where the keys are static and the values contain the data. Then you can use an aggregation pipeline to do this as:
MyModel.aggregate([
// Group the docs by month & os
{$group: {_id: {month: {$month: '$date'}, os: '$os'}, count: {$sum: 1}}},
// Group again by just month
{$group: {_id: '$_id.month', counts: {$push: {os: '$_id.os', count: '$count'}}}},
// Rename _id to month
{$project: {month: '$_id', counts: 1, _id: 0}}
], callback);
Which generates output like:
{
"result" : [
{
"counts" : [
{
"os" : "winPhone",
"count" : 2
},
{
"os" : "iOS",
"count" : 1
},
{
"os" : "Android",
"count" : 2
}
],
"month" : 1
},
{
"counts" : [
{
"os" : "iOS",
"count" : 2
},
{
"os" : "Android",
"count" : 1
}
],
"month" : 2
}
],
"ok" : 1
}
If you really want the original format you can then post-process the result to reshape it however you want.

Since you want to project values as keys in the output, you can't achieve this using the aggregation pipeline. None of the pipeline operators accept values as keys. You need to write map reduce functions. The map function that groups the records based on the year. So the year is emitted as the key. The value being each os name and 'month' in which it was sold.
The map function:
var map = function(){
emit(this.date.getFullYear(),
{"month":this.date.getMonth(),"os":this.os});
}
The reduce function for each group now aggregates the count of different types of os sold in a month for that year.
var reduce = function(id,osArr){
var result = {};
var osCount = {};
var monthNames = [ "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December" ];
for(var i=0;i<osArr.length;i++)
{
var mon = monthNames[(osArr[i].month)-1];
if(!osCount.hasOwnProperty(mon))
{
osCount[mon] = {};
}
if(osCount[mon].hasOwnProperty(osArr[i].os))
{
osCount[mon][osArr[i].os]++;
}
else
{
osCount[mon][osArr[i].os] = 1;
}
}
result[id] = osCount;
return result;
}
Call the map reduce function on the collection and dump it to a new collection named 'temp'.
var o = {};
o.map = map;
o.reduce = reduce;
o.out = { replace: 'temp' }
o.verbose = true;
ModelName.mapReduce(o, function (err, results) {
console.log(results)
})
This yields the following Sample results:
> db.temp.find({},{"value":1,"_id":0})
{ "value" : { "2013" : { "November" : { "Android" : 2, "ios" : 2, "blackberry" :
1 } } } }
{ "value" : { "2014" : { "October" : { "Android" : 3, "ios" : 2 } } } }

Related

MongoDB get ArrayFilters reference to update value

I recently started using MongoDB and I'm trying to update a few properties from a document but not being able to get the object reference to update a value.
Please consider the following data:
const data = {
weekplanId: 'someid',
days: [
{label: 'Monday', cost: 20, comment: "some comment" },
{label: 'Tuesday', cost: 40, comment: "..." }
]
}
const update = await weekplan.updateOne(
{
_id: new ObjectId(data.weekplanId),
},
{
$set: {
"weekdays.$[i].cost": data.days.$[i].cost,
"weekdays.$[i].comment": data.days.$[i].comment,
"weekdays.$[i].date": new Date(),
"weekdays.$[i].someproperty": "something",
}
},
{
arrayFilters: [
{
"i.label": {
$in: data.days.map(p => p.label),
},
}]
}
);
How can I reference the array object to set the property value?
I know data.days.$[i].cost and data.days.$[i].comment are wrong, they are just an example of what I'm trying to achieve.
Setting the date and someproperty works as expected, since the values are not dependent on the source data.
I would like to try to do this without using JS.
Is arrayFilters even appropriate for this? I'd appreciate some guidance as to where to look at.
Thanks in advance!
###EDIT:
Expected output:
"_id": {"someid"},
"weekdays": [
{
"day": "Monday",
"cost": 20,
"comment": "some comment",
"date": 2021-08-01T19:51:45.057Z
"someproperty": "something"
},
{
"day": "Tuesday",
"cost": 40,
"comment": "...",
"date": 2021-08-01T19:51:45.057Z
"someproperty": "something"
},
...
...
...
]
The rest of the week days (Wednesday, Thursday, Friday) would remain untouched on this update.
In that example code data.days.$[i].cost is evaluated on the client side, before the query is submitted, so whatever value (or lack thereof) that is has will be assigned to the corresponding field of the $set object when the server receives it.
The data object will not be sent to the server with the query, so even if it were able to do array lookup on input values, the input value would not be there.
The way to accomplish this is to iterate the array on the client side, and programmatically build the update query. Perhaps something like:
let labelChar = 105;
let setOps = {};
let filters = {};
data.days.forEach( function(day) {
let char = String.fromCharCode(labelChar++);
setOps['weekdays.$[' + char + '].cost'] = day.cost;
setOps['weekdays.$[' + char + '].comment'] = day.comment;
setOps['weekdays.$[' + char + '].date'] = new Date();
setOps['weekdays.$[' + char + '].someproperty'] = "something";
let filterObj = {};
filterObj[char + '.label'] = day.label;
filters.push(filterObj);
});
const update = await weekplan.updateOne(
{
_id: new ObjectId(data.weekplanId),
},
{
$set: setOps
},
{
arrayFilters: filters
}
);
For the provided sample input, this will give the update:
.updateOne(
{
_id: new ObjectId(data.weekplanId),
},
{
$set: {
'weekdays.$[i].cost': 20,
'weekdays.$[i].comment': 'some comment',
'weekdays.$[i].date': ISODate(),
'weekdays.$[i].someproperty': 'something',
'weekdays.$[j].cost': 40,
'weekdays.$[j].comment': '...',
'weekdays.$[j].date': ISODate(),
'weekdays.$[j].someproperty': 'something'
}
},
{
arrayFilters: [
{'i.label': 'Monday'},
{'j.label': 'Tuesday'}
]
}
);

JavaScript Object retrieve value dynamically

Sorry if it sounds like a stupid question
I have this following JavaScript object:
data = {
"Max" : 100
}
This same object can also have the following form:
data = {
"January": {"Max" : 100}
}
I want to use one same function to retrieve the value of Max if given the two forms
And it made me wonder if it is possible to write a conditional expressions directly in the [] when you write the values you want to retrieve? Is this following expression allowed in JavaScript? Something like this:
data[monthly ? 'month' : '']
Of course I tried it and it doesn't work. But is there another way to do such a thing in a line? monthly is a boolean
You can use the following script to do that, I have added some comments to make it clear
var data1 = {
"Max" : 100
}
var data2 = {
"January": {"Max" : 100}
}
// storing the months in an array to loop over and see if the current key is a month
var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
function getMax(data) {
// if the key `Max` exists in data then just get the value of `data.Max` else loop over the months, and see if one of the months is a key and stores a truthy value, in this case it stores an object, so that's what we want and then get it's .Max value
return "Max" in data ? data.Max : data[months.filter(m => data[m] ? m : "")[0]].Max;
}
console.log(getMax(data1));
console.log(getMax(data2));
You can make use of Object.values
let data = {
"Max": 100
};
const getMax = (data) => {
//check if "Max" is available in the `data`, if so return `data.Max` else
//get values of the keys in the object using `Object.values` and
//retreive `Max` from the array
return data.Max ? data.Max : Object.values(data)[0].Max
}
console.log(getMax(data));
data = {
"January": {
"Max": 200
}
}
console.log(getMax(data));
There's yet another way of achieving this using Array.find and Optional Chaining.
let data = {
Max: 100,
};
const getMax = (data = {}) => {
return data.Max ? data.Max : Object.values(data).find(({ Max }) => Max)?.Max;
};
console.log(getMax(data));
data = {
January: { Max: 200 },
};
console.log(getMax(data));

VueJS Reducing JSON return data by category and sum associated values

I have raw data coming from a mongodb/nestjs API via axios with the format of:
created_at: "date"
quantity: "number"
month: "string"
organization: "string"
state: "string"
category: "string"
year: "date"
_id: "numeber"
By using axios and getting the data I can generate a manageable output:
.get(`${server.baseURL}/quantity/quantities`)
.then(response => {
this.rawData = response.data
console.log(this.rawData);
this.rawDataB = this.rawData.map(( {category, quantities} ) => ({category, quantities}))
console.log(this.rawDataB)
})
This console log of rawDataB produces the following output array:
[
{category: "string",
quantities: "number}
]
Because I have monthly data for each category, I have an array of about 100 objects with this format, one for each month.
So I have...:
[{category: "string", quantities: "number"}]
for each month starting from January to September. So each category has about 8-9 objects associated with the same category, different months, and different quantities.
I want to reduce this to the following format, but can't seem to figure it out:
[
{ category: "string", quantities: "number }
]
for each category -- essentially removing the category name duplicates by way of this external function:
export function removeDuplicate (a, b) {
if (a.indexOf(b) < 0) {
a.push(b)
}
return a
}
But when do this, the date keys get lost and only provides me with arrays within array:
[
[ "category", number],["category", number]...
]
for each category. Notice, the number was changed to an integer and not a string anymore
Any thoughts on how I might be able to achieve the data format of?
[
{category: "string", totalQuantity: number}
]
Without a library... take note of Object.values()
const rawData = [
{ category: "january", quantities: "2" },
{ category: "february", quantities: "2" },
{ category: "january", quantities: "2" },
{ category: "february", quantities: "2" },
{ category: "january", quantities: "2" },
{ category: "february", quantities: "2" },
{ category: "january", quantities: "2" },
{ category: "february", quantities: "2" }
];
const formattedData = rawData.reduce((previousValue, { category, quantities }) => {
if (!previousValue[category]) {
previousValue[category] = { category, totalQuantity: 0 };
}
previousValue[category].totalQuantity += +quantities;
return previousValue;
}, {});
console.log(Object.values(formattedData));

Mongodb find query optimization

I'm writing a migration script to update some fields of a collection, say collection2.
In our collections, we store Japanese dates with following format in each document:
"date" : { "era" : 4, "year" : 25, "month" : 11, "day" : 25 }// i.e `25-11-2014`
Now I'm looking for an easy way to get all the documents of the collection with date > 1-10-2014 i.e
date > { "era" : 4, "year" : 25, "month" : 10, "day" : 1 }
Code works well, but I'm feeling like it can be optimised but don't know how.
iterating collection1 using forEach and extracting its date
check date collection1.date > 1-10-2014
copy some document fields from collection2 and update them
db.col1.find({ name: x123 }).forEach(function(doc){
if(hasValidDate(doc.date1)){
db.col2.find({col1_id:doc._id}).forEach(function(doc2){
var copyobj = {doc2.x1, doc2.x2, ...};
db.col2.update({col1_id:doc._id}, copyobj);
});
}
});
function hasValidDate(date){
return (date.era == 4 && date.year >= 26 &&
(date.month >= 10 && date.day >= 1))?true:false;
}
You could try including the actual date filtering within your find() query:
db.col1.find(
{
"name": "x123",
"date.era": 4,
"date.year": { "$gte": 26 },
"date.month": { "$gte": 10 },
"date.day": { "$gte": 1 },
}
).forEach(function(doc){
var copyobj = { "$set": {"x1": "1", "x2": "3", ...} };
db.col2.update({_id: doc._id}, copyobj);
});

Using angular foreach loop to rearrange JSON

I have an ng-repeat which returns arrays of objects like the following:
[{"day":"10","title":"day","summary":"summary","description":"ok","_id":"53f25185bffedb83d8348b22"}]
[{"day":"3","title":"day","summary":"summary","description":"ok","_id":"53f25185bffedb83d8348b22"}]
I'd like to have pull out the objects and push them into another array so they are formatted like this:
[
{"day":"10","title":"day","summary":"summary","description":"ok","_id":"53f25185bffedb83d8348b22"},
{"day":"3","title":"day","summary":"summary","description":"ok","_id":"53f25185bffedb83d8348b22"
}]
The goal is to use an orderBy on the array. Is it possible to restructure the JSON into this format and then access the data?
Here is my view for reference:
<div class="calDynamic" data-ng-repeat="n in [] | range:100">
<div ng-repeat="cal in calendar[n].year | filterKey:month">
<p>{{cal}}</p>
</div>
</div>
My JSON format:
{
"_id" : ObjectId("53f252537d343a9ad862866c"),
"year" : {
"December" : [],
"November" : [],
"October" : [],
"September" : [],
"August" : [],
"July" : [
{
"day" : "21",
"title" : "u",
"summary" : "u",
"description" : "ok",
"_id" : ObjectId("53f252537d343a9ad862866d")
}
],
"June" : [],
"May" : [],
"April" : [],
"March" : [],
"February" : [],
"January" : []
},
"__v" : 0
},
{
"_id" : ObjectId("53f252537d343a9ad862866c"),
"year" : {
"December" : [],
"November" : [],
"October" : [],
"September" : [],
"August" : [],
"July" : [
{
"day" : "3",
"title" : "u",
"summary" : "u",
"description" : "ok",
"_id" : ObjectId("53f252537d343a9ad862866d")
}
],
"June" : [],
"May" : [],
"April" : [],
"March" : [],
"February" : [],
"January" : []
},
"__v" : 0
}
Just elaborating my comment to answer:-
You can do this way to merge the arrays scattered across various month to 1 array.
//obj has the the array result that is the input
var temp = [];
var result = temp.concat.apply(temp,obj.map(function(itm){ //Get each object in the source array that hold the year.
return temp.concat.apply(temp, Object.keys(itm.year).map(function(key){ //Get Month for each yeah and flatten it out
return itm.year[key]; //Get day array for each of the month in each year
}));
}));
Object.keys(obj.year) ==> Will give you the months in your property Year to an array
Array.prototype.map ==> You pass in the months array and get back 2D array of days from all of months.
[].concat ==> returns array of concatenated arrays. It can take multiple arrays are argument, so we use function.apply conveniently to convert 2D to 1D.
Bin
Other simple and performance effective way always is loop through and add upon.
Array.prototype.concat allows to join two arrays into one. However, it is just javascript and has nothing to do with Angular, the way to do it is like:
var a = [{"day":"10","title":"day","summary":"summary","description":"ok","_id":"53f25185bffedb83d8348b22"}];
var b = [{"day":"3","title":"day","summary":"summary","description":"ok","_id":"53f25185bffedb83d8348b22"}];
var c = a.concat(b); // returns the concatenated array.

Categories