How to get the default value of mongoose using aggregate - node.js

I am using mongoose in nodejs.
So I defined a schema as below.
const userSchema = new mongoose.Schema({
name: {
type: String,
default: 'kim'
})
When I use 'findOne', it gives 'name value' as default, even though, there is no field in its' document.
But when I use '$project of aggregate', there is no name field, when there is no value of name.
How can I get default value using aggregate?
I am using 'mongo 3.6' and 'documentDB of AWS'.
Thank you so much for reading it.

Mongoose applies defaults clientside, aggregation runs serverside and it knows nothing about Mongoose schemas or even nodejs.
If you want to assign default values to the missing fields you need to amend your pipeline by prepending your $project with an extra stage to apply default values explicitly using $mergeObjects operator:
db.collection.aggregate([
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
{
"name": "kim"
},
"$$ROOT"
]
}
}
},
{ $project: {...} }
])
You will be responsible to maintain it to remain in sync with the schema if defaults change at some point.

Related

How to add new fields to the last object in an array in MongoDB?

I have an Array of objects in mongoDB as follow
Initially there is only the heartRate field inside the object. Now I want to add new fields to this object along with the existing heartRate field.
Also there can be multiple objects inside the dailyReadings array. Therefore, I need to add new fields only to the last object using nodejs and expressjs
I tried using the $push method but ended up adding new object intead of adding the fields to the last object. Is there a way to achieve this? Thanks in advance!
Why I am doing this (For further understanding):-
I have developed a mobile app to read the heart rate. Initially it will save the heart rate in the database as a new object (As in the image). Then, there are several other data sent through a desktop application which needs to add to the same object (Which is the last object in the dailyReadings array)
There is no straight way to do this, you can try update with aggregation pipeline starting from MongoDB 4.2,
$size to get total elements in dailyReadings array
$subtract to minus 1 from above total elements
$slice to get elements other than the last object element
$slice to get last object element by -1 from dailyReadings
$arrayElemAt to select first object element from array
$mergeObjects to merge existing object fields of the last object and new fields that you want to insert
$concatArrays to concat first slice array and second updated object
db.collection.update(
{}, // put your query condition
[{
$set: {
dailyReadings: {
$concatArrays: [
{
$slice: [
"$dailyReadings",
0,
{ $subtract: [{ $size: "$dailyReadings" }, 1] }
]
},
[
{
$mergeObjects: [
{ $arrayElemAt: [{ $slice: ["$dailyReadings", -1] }, 0] },
{
newField: "1"
}
]
}
]
]
}
}
}]
)
Playground
In order for you to add fields to the last object, the heartRate should be an object with a schema containing the following
Array (for you to add to)
any other necessary data type you'd want the object to have
you must define a complex schema using mongoose, perform the following changes to your file of model
const mongoose = require('mongoose');
const childSchema = mongoose.Schema({
heartRate: {type: Array, required: true}
array: {type: Array, required: false}, //change the required parameter based on your requirement
});
const parentSchema = mongoose.Schema({
dailyReadings: {
type: childSchema,
required: false //change the required parameter based on your requirement
},
});
module.exports = mongoose.model('modelCollection', parentSchema);
So basically you need to define the child schema, and change the type of dailyReadings to that schema and add to the array of different objects.

MongoDB: How to perform a second match using the results (an array of ObjectIds) of the previous match in aggregation pipeline

I have a MongoDB collection called users with documents that look like:
{
_id: ObjectId('123'),
username: "abc",
avatar: "avatar/long-unique-random-string.jpg",
connections: [ObjectId('abc'), ObjectId('xyz'), ObjectId('lmn'), ObjectId('efg')]
}
This document belongs to the users collection.
What I want to do:
First, find one document from the users' collection that matches _id -> '123'.
Project the connections field received from step 1, which is an array of ObjectIds of other users within the same collection.
Find all documents of users from the array field projected in step 2.
Project and return an array of only the username and avatar of all those users from step 3.
While I know that I can do this in two separate queries. First using findOne which returns the friends array. Then, using find with the results of findOne to get all the corresponding usernames and avatars.
But, I would like to do this in one single query, using the aggregation pipeline.
What I want to know, is it even possible to do this in one query using aggregation?
If so, what would the query look like?
What, I currently have:
await usersCollection
.aggregate([
{ $match: { _id: new ObjectId(userId) } },
{ $project: { ids: "$connections" } },
{ $match: { _id: { $in: "ids" } } },
{
$project: {
username: "$username",
avatar: { $ifNull: ["$avatar", "$$REMOVE"] },
},
},
])
.toArray()
I know this is wrong because each aggregation stage receives the results from the previous stage. So, the second match cannot query on the entire users' collection, as far as I know.
I'm using MongoDB drivers for nodeJS. And I would like to avoid $lookup for possible solutions.

mongoose exclude field in array

I have something like:
Schema Subdocument
name: String
data: Mixed
Schema Stuff
documents: [Subdocument]
Now, in my API there are two endpoints, one for the Subdocument and another for Stuff. When I want to get a Subdocument I need to contain the data field, but when I want to get Stuff, I want to show the name of those subdocuments, but I don't want to show the data field because is quite large and it won't be used.
So, to keep things clear, data is not private. It's just that I don't want it to be shown when I get it from Stuff
I tried by doing:
Stuff.findById(id)
.populate("documents")
.populate("-documents.data")
but that doesn't work... I'm getting the Stuffwith the Subdocumentcontaining the name and data. I feel like i'm missing to tell mongoose when I call populate("-documents.data") that documents is an array and I want to exclude the data field for each element in this array.
edit: Sorry the Schema I provided was not for my case. In my case it was not embedded, but a reference, like so:
Schema Subdocument
name: String
data: Mixed
Schema Stuff
documents: [{
type: Schema.Types.ObjectId,
ref: 'Subdocument'
}]
Assuming subDocument is not embedded and using as "ref" as you say populate is working but data part is not included:
Stuff.findById(id).populate( { "path" : "documents", "select" : "-data" })
Your documents have an "embedded" schema, so populate is not used here, it is used only for "referenced" schemas where the other objects are in another collection.
Fortunately with "embedded" there is an easy way using projection:
Stuff.findById(id,{ "documents.name": 1 },function(err,results) {
})
With results like
{ "documents": [{ "name": "this" },{ "name": "that" }] }
Or with .aggregate() and the $map operator:
Stuff.aggregate(
[
{ "$match": { "_id": ObjectID(id) } },
{ "$project": {
"documents": {
"$map": {
"$input": "$documents",
"as": "el",
"in": "$$el.name"
}
}
}}
],function(err,results) {
}
)
That will just tranform into an array of "only" the name "values", which is different to the last form.
{ "documents": ["this", "that"] }
Note, if using .aggregate() you need to properly cast the ObjectId as the autocasting from mongoose schema types does not work in aggregation pipeline stages.

Aggregate and flatten an array field in MongoDB

I have a Schema:
var ProjectSchema = new Schema({
name: {
type: String,
default: ''
},
topics: [{
type: Schema.ObjectId,
ref: 'Topic'
}],
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
What I want to do is get an array with all topics from all projects. I cannot query Topic directly and get a full list because some topics are unassigned and they do not hold a reference back to a Project (for reasons of avoiding two way references). So I need to query Project and aggregate some how. I am doing something like:
Project.aggregate([{$project:{topics:1}}]);
But this is giving me an array of Project objects with the topics field. What I want is an array with topic objects.
How can I do this?
When dealing with arrays you typically want to use $unwind on the array members first and then $group to find the distinct entries:
Project.aggregate(
[
{ "$unwind": "$topics" },
{ "$group": { "_id": "$topics._id" } }
],
function(err,docs) {
}
)
But for this case, it is probably simplier to just use .distinct() which will do the same as above, but with just an array of results rather than documents:
Project.distinct("topics._id",function(err,topics) {
});
But wait there a minute because I know what you are really asking here. It's not the _id values you want but your Topic data has a property on it like "name".
Since your items are "referenced" and in another collection, you cannot do an aggregation pipeline or .distinct() operation on the property of a document in another collection. Put basically "MongoDB does not perform Joins" and mongoose .populate() is not a join, just something that "emulates" that with additional query(ies).
But you can of course just find the "distinct" values from "Project" and then fetch the information from "Topic". As in:
Project.distinct("topics._id",function(err,topics) {
Topic.find({ "_id": { "$in": topics } },function(err,topics) {
});
});
Which is handy because the .distinct() function already returned an array suitable for use with $in.

Mongoose: how to count a number of schema that other schema refers to?

As I wrote above, I have a schema which refers to another different schema. These are:
exports.Policies = new mongoose.Schema({
name: String,
description: String,
exploits: [ {type : mongoose.Schema.ObjectId, ref : 'exploit', required: true} ]
});
exports.exploit = new mongoose.Schema({
name: String,
type: String,
required: [String]
});
What I want to do is just know how many values have 'exploits' array as a field inside of the response, is this possible? In sql I have to write only "count(field)" and making a "group by", how might I do this?
An example:
{
"name" : "mmmmmm",
"description" : "jjjj",
"_id" : ObjectId("533721b91a985b883399cdc2"),
"n_exploits" : 2
}
Thanks in advance.
Actually there is just the $size operator if you have no need to filter array content.
Assuming that you have a model to your schema that is named "policies" then the following is achieved through use of .aggregate():
policies.aggregate([
{ "$unwind": "$exploits" }
{ "$group": {
"_id": "$_id",
"count": { "sum": 1 }
}}
],function(err,doc) {
});
So this works by unwinding the array and then counting the number of elements that are produced when you re-group.
In future releases there is a a new $size operator so you can skip those stages and just use project:
policies.aggregate([
{ "$project": {
"count": { "$size": "$exploits" }
}}
],function(err,doc) {
});
Or otherwise use that with $group and $sum to add up arrays across documents.
But for now you do the $unwind and $sum operations. Working inside the mongodb engine is typically done in "native" code and the JavaScript operators are not available. But also as native code these operators work very fast.
Also see the aggregation operator reference for other things you may wish to do.
Have you tried searching this before posting here? A simple google search with words "mongodb count group by" gives kind of a lot of results, which are not only telling you that you have too look into aggregation framework, but also give you some examples even on SO like this and this.

Resources