How to find records in nested documents? - node.js

So I'm learning mongoose and I've implemented a Customer model like this:
let CustomerSchema = new Schema({
stripe_id: {
type: String,
required: true
},
telegram_id: {
type: Number
},
email: {
type: String,
required: true
},
subscriptions: [SubscriptionSchema],
created_at: {
type: Date,
default: Date.now,
required: true
}
});
essentially I would like to return all the subscriptions of a customer, but how can I search in nested document, in this case subscriptions?
This is the subscription model:
let SubscriptionSchema = new Schema({
status: {
type: String,
required: true
},
plan_id: {
type: String,
required: true
}
});
I would like to return only the subscriptions which have as status active, at the moment I'm able to search for customer as:
let customer = await CustomerModel.findOne({telegram_id: ctx.chat.id});

You can use the filter aggregation to filter in a nested array.
Playground
Sample express route with mongoose:
router.get("/customers/:id", async (req, res) => {
let result = await Customer.aggregate([
{
$match: {
telegram_id: 1 //todo: req.params.id
}
},
{
$project: {
_id: "$_id",
stripe_id: "$stripe_id",
telegram_id: "$telegram_id",
email: "$email",
subscriptions: {
$filter: {
input: "$subscriptions",
as: "item",
cond: {
$eq: ["$$item.status", "active"]
}
}
}
}
}
]);
//todo: result will be an array, you can return the result[0] if you want to return as object
res.send(result);
});
Let'a say we have the following document:
{
"_id" : ObjectId("5e09eaa1c22a8850c01dff77"),
"stripe_id" : "stripe_id 1",
"telegram_id" : 1,
"email" : "email#gmail.com",
"subscriptions" : [
{
"_id" : ObjectId("5e09eaa1c22a8850c01dff7a"),
"status" : "active",
"plan_id" : "plan 1"
},
{
"_id" : ObjectId("5e09eaa1c22a8850c01dff79"),
"status" : "passive",
"plan_id" : "plan 2"
},
{
"_id" : ObjectId("5e09eaa1c22a8850c01dff78"),
"status" : "active",
"plan_id" : "plan 3"
}
],
"created_at" : ISODate("2019-12-30T15:16:33.967+03:00"),
"__v" : 0
}
The result will be like this:
[
{
"_id": "5e09eaa1c22a8850c01dff77",
"stripe_id": "stripe_id 1",
"telegram_id": 1,
"email": "email#gmail.com",
"subscriptions": [
{
"_id": "5e09eaa1c22a8850c01dff7a",
"status": "active",
"plan_id": "plan 1"
},
{
"_id": "5e09eaa1c22a8850c01dff78",
"status": "active",
"plan_id": "plan 3"
}
]
}
]
If you don't want to project the items one by one, you can use addFields aggregation like this:
router.get("/customers/:id", async (req, res) => {
let result = await Customer.aggregate([
{
$match: {
telegram_id: 1
}
},
{
$addFields: {
subscriptions: {
$filter: {
input: "$subscriptions",
as: "item",
cond: {
$eq: ["$$item.status", "active"]
}
}
}
}
}
]);
res.send(result);
});

You can do something like this.
await CustomerModel.findOne({telegram_id: ctx.chat.id})
.populate({
path: 'subscriptions',
match: {status: 'active'}
});
here path is used to join the next model and match is used to query inside that model.

Related

MongoDB : Push or merge one lookup's result in other lookup's result

I have 3 collection namely constants, activities, applications with mentioned properties.
Now, querying constants collection with activities and activities with applications with matching Id's. I am getting correct results. But now activity_types are shown at per data level.
But expecting the output should be at per item level inside data whichever is matching with item. Because activities are matching for Item and it should be shown in per item level not at data level. I tried with $push and $group but not getting expected results.
Constants
{
_id: id
value : {
categories: [
{
id: 001,
title: "test 1"
},
{
id: 002,
title: "test 2"
},
{
id: 003,
title: "test 3"
}
]
}
}
Activity
{
propert1: "",
propert2: "",
config: {
agenda_item_category_ids: [ObjectId(001), ObjectId(002)]
},
activity_type_id: ObjectId(123)
}
{
propert1: "",
propert2: "",
activity_type_id: ObjectId(456)
config: {
agenda_item_category_ids: [ObjectId(002)]
}
}
applications
{
_id: ObjectId(123),
prop1: "",
prop2: ""
}
{
_id: ObjectId(456),
prop1: "",
prop2: ""
}
Current query
const results = await Constants.aggregate([
{
$match: query,
},
{
$unwind: {
path: '$value.categories',
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: 'activity',
localField: 'value.categories.id',
foreignField: 'config.agenda_item_category_ids',
as: 'data',
},
},
{
$lookup: {
from: 'applications',
localField: 'items.activity_type_id',
foreignField: '_id',
as: 'activity_type',
},
},
{
$project: {
_id: 0,
category_id: '$value.categories.id',
title: '$value.categories.title',
description: '$value.categories.description',
icon_src: '$value.categories.icon_src',
data: 1,
activity_type: 1,
},
},
]);
Current output
[
{
data: [
{item1},
{item2}
],
activity_type,
title
_id
},
{
data: [
{item1},
{item2}
],
activity_type,
title
_id
}
]
Expected output
[
{
data: [
{
item1,
activity_type
},
{
item2,
activity_type
}
],
title
_id
},
]
Tried method
{
"_id": "$_id",
"activity_type": {
"$push": "$activity_type"
}
}

mongodb insert data into the array of objects and update it

I need to make a vote, it looks like an array of objects, look like the user’s ID and the value that he set.
If the user has already voted, but changed his value, you need to change the value of the rate in the array of objects for this user.
I need to make an array of objects into which data will be inserted like this {rate: 3, user: "asdr2r24f2f42f24"} and if the user has already voted in this array, then you need to change the value rate of the given user
I already tried to do something, but it seems to me you can write something better, can you help?
JSON https://jsoneditoronline.org/?id=442f1dae0b2d4997ac69d44614e55aa6
router.post('/rating', (req, res) => {
console.log(req.body)
// { id: 'f58482b1-ae3a-4d8a-b53b-ede80fe1e225',
// rating: 5,
// user: '5e094d988ddbe02020e13879' }
Habalka.find({
_id: req.body.id
})
.then(habalka => {
// here I need to check whether the user has already voted or not, and from this whether to add an object with it or update the number
Habalka.updateOne(
{_id: req.body.id},
{$push: {rating: {rate: req.body.rating, user: req.body.user}}}
)
.then(e => {
console.log(e)
})
});
});
Schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const HabalkaSchema = new Schema({
_id: {
type: String
},
bio: {
firstname: String,
lastname: String,
middlename: String,
company: String
},
rating: [
],
files: [
{
_id: {
type: String
},
destination: {
type: String
},
filename: {
type: String
},
path: {
type: String
},
folder: {
type: String
},
info: {
size: {
type: Number
},
mimetype: {
type: String
},
encoding: {
type: String
},
originalname: {
type: String
},
fieldname: {
type: String
},
},
date: {
type: Date,
default: Date.now
},
bio: {
type: Object
},
userId: String,
guessId: {},
}
],
date: {
type: Date,
default: Date.now
}
});
module.exports = Habalka = mongoose.model('habalka', HabalkaSchema);
This is an aggregation query which inserts a new user or updates the rating of existing user in the rating array:
The req.body.id, req.body.user and req.body.rating are set as follows for the example code:
var ID = 1, INPUT_USER = "new user", INPUT_RATE = 5;
const matchStage = { $match: { _id: ID } };
const facetStage = {
$facet: {
new_user: [
{ $match: { "rating.user": { $not: { $eq: INPUT_USER } } } },
{ $addFields: { rating: { $concatArrays: [ "$rating", [ { user: "new user", rate: INPUT_RATE } ] ] } } },
],
user: [
{ $match: { "rating.user": INPUT_USER } },
{ $addFields: {
rating: {
$map: {
input: "$rating",
as: "r",
in: {
$cond: [ { $eq: [ "$$r.user", INPUT_USER ] },
{ user: "$$r.user", rate: { $add: [ "$$r.rate", INPUT_RATE ] } },
"$$r"
]
}
}
}
} }
]
}
};
const projectStage = {
$project: {
result: { $arrayElemAt: [ { $concatArrays: [ "$user", "$new_user" ] }, 0 ] }
}
};
const queryPipeline = [
matchStage,
facetStage,
projectStage
];
// Run the aggregation query and get the modified document
// after applying the user and rate data in the rating array.
// The result of the aggregation is used to update the collection.
col.aggregate(queryPipeline).toArray( ( err, docs ) => {
console.log("Aggregation output:");
console.log( JSON.stringify( docs[0] ) );
// Update the aggregate result to the collection.
col.updateOne( { _id: docs[0].result._id },
{ $set: { rating: docs[0].result.rating } },
( err, updateResult ) => {
console.log( 'Updated count: ', updateResult.matchedCount );
}
);
callback(docs);
} );
Example collection document:
{ "_id" : 1, "rating" : [ { "user" : "user1", "rate" : 2 } ] }
If the input is var ID = 1, INPUT_USER = "new user", INPUT_RATE = 5; the updated document will be:
{ "_id" : 1, "rating" : [ { "user" : "user1", "rate" : 2 }, { "user" : "new user", "rate" : 5 } ] }
If the input is var ID = 1, INPUT_USER = "user1", INPUT_RATE = 5; the updated document will be:
{ "_id" : 1, "rating" : [ { "user" : "user1", "rate" : 7 } ] }

Why Mongoose createdAt / updatedAt output a wrong time(only return current time not the time in db)

The Data stored in my db is:
{
"_id" : ObjectId("58da135cfc80bc44f7653fd4"),
"updatedAt" : ISODate("2017-03-28T08:00:59.541Z"),
"createdAt" : ISODate("2017-03-28T07:40:12.742Z"),
"name" : "hello",
"delete" : false,
"enabledPlugins" : [
ObjectId("58c24f65b363502f907738f9")
],
"__v" : 0
}
My Schema Like:
const mongoose = require('./db');
const { Schema } = mongoose;
const templateSchema = new Schema({
name: { type: String, index: true, unique: true },
enabledPlugins: [
{ type: Schema.Types.ObjectId }
],
delete: { type: Boolean, default: false }
}, {
timestamps: true
});
const Template = mongoose.model('Template', templateSchema);
module.exports = Template;
But When I want to get templates, I get the wrong timestamp:
exports.getAllTemplates = async function() {
return await Template.aggregate(
{ $match: { delete: false } },
{ $project: { id: '$_id', _id: 0, name: 1, enabledPlugins: 1, createdAt: 1 } }
);
};
The result like :
[
{
"createdAt": "2017-03-28T17:04:30.502+08:00",
"name": "hello",
"enabledPlugins": [
"58c24f65b363502f907738f9"
],
"id": "58da135cfc80bc44f7653fd4"
}
]
And I found before toJSON, the output has been wrong. I don't use any plugins. All the date type has the same problem.
Thanks, the problem is that I rewrite Date.prototype.toISOString

Find object id in object ids array returns empty array using Mongoose

I have two Mongoose schemas:
var EmployeeSchema = new Schema({
name: String,
servicesProvided: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Service'
}]
});
var ServiceSchema = new Schema({
name: String
});
I'm trying to find employees who provide a specified service with the service ID I send into the http request. This is my code:
Employee
.find({
servicesProvided: req.params.service_id
})
.exec(function(err, employees) {
if (err) {
console.log(err);
res.send(err);
} else {
res.json(employees);
}
});
The problem is that this code returns an empty array and I don't know why. I've tried a lot of things like casting the service id to mongoose.Schema.Types.ObjectId but it doesn't work.
Any idea? I'm using Mongoose 3.8.39. Thanks!
In your EmployeeSchema, servicesProvided is an array, to filter employees by that field you should use $in operator:
var services = [req.params.service_id];
Employee.find({
servicesProvided: {
$in: services
}
}, ...
I think you need $elemMatch! From docs:
{ _id: 1, results: [ { product: "abc", score: 10 }, { product: "xyz", score: 5 } ] },
{ _id: 2, results: [ { product: "abc", score: 8 }, { product: "xyz", score: 7 } ] },
{ _id: 3, results: [ { product: "abc", score: 7 }, { product: "xyz", score: 8 } ] }
Search like:
db.survey.find({ results: { $elemMatch: { product: "xyz", score: { $gte: 8 } } } })
Results in:
{ "_id" : 3, "results" : [ { "product" : "abc", "score" : 7 }, { "product" : "xyz", "score" : 8 } ] }
But since you're doing a single query condition (look at the docs again) you can replace
db.survey.find(
{ results: { $elemMatch: { product: "xyz" } } }
)
with
db.survey.find(
{ "results.product": "xyz" }
)
So in your case it should be something like:
find({
'servicesProvided': ObjectId(req.params.service_id)
})

Mongoose - accessing nested object with .populate

Schema Definitions
Team.js
var TeamSchema = new Schema({
// Team Name.
name: String,
lead: String,
students :type: [{
block : Number,
status : String,
student : {
type: Schema.ObjectId,
ref: 'Student'
}]
});
Student.js
var StudentSchema = new Schema({
name: String,
rollNo : Number,
class : Number
});
How I can populate "student" to get output, as below:
team
{
"__v": 1,
"_id": "5252875356f64d6d28000001",
"students": [
{
"__v": 1,
"_id": "5252875a56f64d6d28000002",
block : 1,
status : joined,
"student": {
"name": Sumeeth
"rollNo" : 2
"class" : 5
}
},
{
"__v": 1,
"_id": "5252875a56f64d6d28000003",
block : 1,
status : joined,
"student": {
"name": Sabari
"rollNo" : 3
"class" : 4
}
}
],
"lead": "Ratha",
}
This is JS I use to get the document using Mongoose:
Team.findOne({
_id: req.team._id
})
.populate('students')
.select('students')
.exec(function(err, team) {
console.log(team);
var options = {
path: 'students.student',
model: 'Student'
};
Student.populate(team.students,options,function(err, students) {
console.log(students);
if (err) {
console.log(students);
res.send(500, {
message: 'Unable to query the team!'
});
} else {
res.send(200, students);
}
});
});
In my console output I get the following:
{ _id: 53aa434858f760900b3f2246,
students
[ { block : 1
status: 'joined'
_id: 53aa436b58f760900b3f2249 },
{ block : 1
status: 'joined'
_id: 53aa436b58f760900b3f2250 }]
}
And the expected output is:
{ _id: 53aa434858f760900b3f2246,
students
[ { block : 1
status: 'joined'
student :{
"name": Sumeeth
"rollNo" : 2
"class" : 5
}
},
{ block : 1
status: 'joined'
student :{
"name": Sabari
"rollNo" : 3
"class" : 4
}
}
]
}
Some one please help me where I am wrong. How should I make use of .populate, so that , I can get the entire student object and not only its id.
Reference :
Populate nested array in mongoose
I have been facing same issue. I have use this code for my rescue :
Team.findOne({_id: req.team._id})
.populate({ path: "students.student"})
.exec(function(err, team) {
console.log(team);
});
Here is a simplified version of what you want.
Basic data to set up, first the "students":
{
"_id" : ObjectId("53aa90c83ad07196636e175f"),
"name" : "Bill",
"rollNo" : 1,
"class" : 12
},
{
"_id" : ObjectId("53aa90e93ad07196636e1761"),
"name" : "Ted",
"rollNo" : 2,
"class" : 12
}
And then the "teams" collection:
{
"_id" : ObjectId("53aa91b63ad07196636e1762"),
"name" : "team1",
"lead" : "me",
"students" : [
{
"block" : 1,
"status" : "Y",
"student" : ObjectId("53aa90c83ad07196636e175f")
},
{
"block" : 2,
"status" : "N",
"student" : ObjectId("53aa90e93ad07196636e1761")
}
]
}
This is how you do it:
var async = require('async'),
mongoose = require('mongoose');
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/team');
var teamSchema = new Schema({
name: String,
lead: String,
students: [{
block: Number,
status: String,
student: {
type: Schema.ObjectId, ref: 'Student'
}
}]
});
var studentSchema = new Schema({
name: String,
rollNo: Number,
class: Number
});
var Team = mongoose.model( "Team", teamSchema );
var Student = mongoose.model( "Student", studentSchema );
Team.findById("53aa91b63ad07196636e1762")
.select('students')
.exec(function(err, team) {
console.log( team );
async.forEach(team.students, function(student,callback) {
Student.populate(
student,
{ "path": "student" },
function(err,output) {
if (err) throw err;
callback();
}
);
},function(err) {
console.log( JSON.stringify( team, undefined, 4 ) );
});
});
And it gives you the results:
{
"_id": "53aa91b63ad07196636e1762",
"students": [
{
"block": 1,
"status": "Y",
"student": {
"_id": "53aa90c83ad07196636e175f",
"name": "Bill",
"rollNo": 1,
"class": 12
}
},
{
"block": 2,
"status": "N",
"student": {
"_id": "53aa90e93ad07196636e1761",
"name": "Ted",
"rollNo": 2,
"class": 12
}
}
]
}
You really do not need the "async" module, but I am just "in the habit" as it were. It doesn't "block" so therefore I consider it better.
So as you can see, you initial .populate() call does not do anything as it expects to "key" off of an _id value in the foreign collection from an array input which this "strictly speaking" is not so as the "key" is on "student" containing the "foreign key".
I really did cover this in a recent answer here, maybe not exactly specific to your situation. It seems that your search did not turn up the correct "same answer" ( though not exactly ) for you to draw reference from.
You are overthinking it. Let Mongoose do the work for you.
Team.findOne({
_id: req.team._id
})
.populate({path:'students'})
.exec(function(err, team) {
console.log(team);
});
This will return students as documents rather than just the ids.
TL DR
const team = await Team.findById(req.team._id)
.populate("students");
team.students = await Student.populate(team.students, {path: "student"});
Context
Reading from all the answers I went testing everything and just Neil Lun's answer worked for me. The problem is it was on the path to a cb hell. So I cracked my head a little and 'refactored' to an elegant one-liner.
const foundPost = await Post.findById(req.params.id)
.populate("comments")
.populate("author");
foundPost.comments = await User.populate(foundPost.comments, {path: "author"});
My initial problem:
{
title: "Hello World",
description: "lorem",
author: {/* populated */},
comments: [ // populated
{text: "hi", author: {/* not populated */ }}
]
};
How my models basically are:
User = {
author,
password
};
Post = {
title,
description,
author: {}, //ref User
comments: [] // ref Comment
};
Comment = {
text,
author: {} // ref User
};
The output after problem solved:
{
comments: [
{
_id: "5dfe3dada7f3570b60dd977f",
text: "hi",
author: {_id: "5df2f84d4d9fcb228cd1df42", username: "jo", password: "123"}
}
],
_id: "5da3cfff50cf094c68aa2a37",
title: "Hello World",
description: "lorem",
author: {
_id: "5df2f84d4d9fcb228cd1aef6",
username: "la",
password: "abc"
}
};

Resources