mongoose exclude field in array - node.js

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.

Related

find a product inside a specific category using mongodb and mongoose

I am new to mongodb. I'm trying to query for a particular product in a specific category (for example if I'm in the books category then I would like to query for a particular book by its name i.e., productName) but I'm unable to do so successfully. Could anybody help me with this. I'm attaching the schema below for reference.
const categorySchema = {
name: String,
description: String,
products: [
{
productName: String,
price: String,
thumbnail: String,
description: String
}
]
};
To search by an object into an array you need to use the dot notation and is very simple.
You need a query similar to this:
db.collection.find({
"_id": 0,
"products.productName": "productName"
})
Note that the find has two conditions.
The first one is to look for in the document you want using its _id (if you want all documents which has 'productName' into the array this condition is not neccesary, but for query a single document it does). This is for query into a specific category document.
The second condition is to get those documents which has the value productName for the key productName into the array products.
Check an example here
Also, if you want to return only the subdocument instead of the entire document, you need this query:
db.collection.find({
"_id": 0
},
{
"products": {
"$elemMatch": {
"productName": "productName"
}
}
})
Example here
Using $elementMatch only the subdocument is returned.
Also, using mongoose the query is the same. Something like this.
var find = await model.find({
"_id": 0,
"products.productName": "productName"
})

How to get the default value of mongoose using aggregate

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.

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.

How should I find a Mongoose subdocument given the parent document and a value in the subdocument?

I'm just trying to do a basic find similar to .findOne, but on a subdocument array in Mongoose. Should I simply loop through the array looking for the right subdocument?
I have a schema like so:
var RegionSchema = new Schema({
"metadata": {
"regionType": String,
"name": String,
"children": [{
"name": String,
"childType": String
}],
"parent": Schema.ObjectId
},
"data": [DataContainer]
});
I know that I want to find a DataContainer in data with dataYear equal to "2014". I'm new to MongoDB, so I don't know many advanced commands. What would be the most efficient way?
EDIT: dataYear is guaranteed to be unique in that array.
Use the aggregation framework for this. Your aggregation pipeline would consist of a $match operation stage which matches documents in the collection where the data array element object has a field dataYear with value equal to "2014". The next pipeline stage would be the $unwind operation on the data array object and a further $match pipeline to filter the arrays. Final stage would be to $project the deconstructed array. For example, suppose you have a model Region that uses the RegionSchema, your mongoose code would be:
Region.aggregate([
// similar query object you use in findOne() can also be applied to the $match
{
"$match": {
"data.dataYear": "2014",
"metadata.name": "example text"
}
},
{
"$unwind": "$data"
},
{
"$match": {
"data.dataYear": "2014"
}
},
{
"$project": {
"_id": 0,
"dataYear": "$data.dataYear",
"population": "$data.population"
}
}
], function(err, docs){
});
You could add an $elemMatch to your query.
Assuming Region is the model:
Region.findOne(...).select({data: {$elemMatch: {dataYear: 2014}});
This will return the Region document with the data array containing only the matching item.
UPDATE: If you already have a document with an array of subdocuments you can use something like lodash's find method to select the subdocument. NOTE: this will convert the subdocument to a POJO.
_.find(region.data.toObject(), {dataYear: 2014});

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