Skip one nested level of subdocument in Mongoose - node.js

I have a parent schema with a subdocument. The subdocument has a property with an array of embedded objects:
Child schema
var snippetSchema = new Schema({
snippet: [
{
language: String,
text: String,
_id: false
}
]
});
Parent schema
var itemSchema = new Schema({
lsin: Number,
identifier: {
isbn13: Number,
},
title: snippetSchema,
});
Which upon Item.find returns an object like so:
[
{
_id: (...),
lsin: 676765,
identifier: {
isbn13: 8797734598763
},
title: {
_id: (...),
snippet: [
{
language: 'se',
text: 'Pippi Långstrump'
}
]
}
}
]
I would like to skip one nested level of the subdocument when the object is returned to the client:
[
{
_id: (...),
lsin: 676765,
identifier: {
isbn13: 8797734598763
},
title: {
language: 'se',
text: 'Pippi Långstrump'
}
}
]
So far I have tried:
#1 using a getter
function getter() {
return this.title.snippet[0];
}
var itemSchema = new Schema({
...
title: { type: snippetSchema, get: getter }
});
But it creates an infinite loop resulting in RangeError: Maximum call stack size exceeded.
#2 using a virtual attribute
var itemSchema = new Schema({
..., {
toObject: {
virtuals: true
}
});
itemSchema
.virtual('title2')
.get(function () {
return this.title.snippet[0];
});
Which will generate the desired nested level but under a new attribute, which is not acceptable. To my knowledge there is no way of overriding an attribute with an virtual attribute.
The question is: is there any other way to go about in getting the desired output? There will be several references to the snippetSchema across the application and a DRY method is preferred.
I am new to MongoDB and Mongoose.

You'll need to use the $project within an mongodb aggregation pipeline.
Within my database I have the following:
> db.items.find().pretty()
{
"_id" : 123,
"lsin" : 676765,
"identifier" : {
"isbn13" : 8797734598763
},
"title" : {
"_id" : 456,
"snippet" : [
{
"language" : "se",
"text" : "Pippi Långstrump"
}
]
}
}
Then we just need to create a simple aggregation query:
db.items.aggregate([
{$project: { lsin: 1, identifier: 1, title: { $arrayElemAt: [ '$title.snippet', 0 ] }}}
])
This just uses a $project (https://docs.mongodb.com/v3.2/reference/operator/aggregation/project/) and a $arrayElemAt (https://docs.mongodb.com/v3.2/reference/operator/aggregation/arrayElemAt/) to project the first item out of the array. If we execute that we will get the following:
{
"_id" : 123,
"lsin" : 676765,
"identifier" : {
"isbn13" : 8797734598763
},
"title" : {
"language" : "se",
"text" : "Pippi Långstrump"
}
}

Related

MongoDB update multiple items in an array of objects with corresponding data

I have an array in my MongoDB document as shown below:
{
...otherDocFields,
groupID: "group-id",
users: [
{id: "uid1", name: "User1"},
{id: "uid2", name: "User2"},
{id: "uid3", name: "User3"},
{id: "uid4", name: "User4"}
]
}
I'm trying to write a function that will update users' names based on that ID.
I tried something like:
async function updateNames(groupID: string, data: Array<{id: string, name: string}>) {
try {
// MongoDB Aggregation
await mongoDB.collection("users").aggregate([
{$match: {groupID}},
{$unwind: {
path: '$users',
includeArrayIndex: 'users.id',
preserveNullAndEmptyArrays: true
}
}
//....
])
} catch (e) {
console.log(e)
}
}
I'm stuck at the part to update the relevant names from the data param in the function.
A sample data would be:
[
{id: "uid1", name: "newName1"},
{id: "uid3", name: "newName3"}
]
I can for sure read, manually process it and update the document but I'm looking for a way to do it in single go using aggregation.
You can do this with an update statement using array filters (https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/#---identifier--)
First, if we just declare some tests data which would be passed into your method:
const data = [
{id: "uid2", name: "ChangeName1"},
{id: "uid4", name: "ChangedName2"}
];
We can then create an update statement which updates all the users within that list within a map and a reduce:
const sets = data.map((element, index) => ({ [`users.$[f${index}].name`]: element.name })).reduce((accumulator, currentValue) => (Object.assign(currentValue, accumulator)), { });
const update = { $set: sets };
This will give us the following update statement:
{
"$set" : {
"users.$[f1].name" : "ChangedName2",
"users.$[f0].name" : "ChangeName1"
}
}
We can create a bunch of array filters with that data:
const arrayFilters = data.map((element, index) => ({ [`f${index}.id`]: element.id }));
This will give us the following which we can pass into the options of an update.
[ { "f0.id" : "uid2" }, { "f1.id" : "uid4" } ]
Last of all we can execute the following update command:
db.users.updateOne(
filter,
update,
{ arrayFilters }
);
Now if we check the output we'll get the following results
> db.users.find().pretty()
{
"_id" : ObjectId("60e20841351156603932c526"),
"groupID" : "123",
"users" : [
{
"id" : "uid1",
"name" : "User1"
},
{
"id" : "uid2",
"name" : "ChangeName1"
},
{
"id" : "uid3",
"name" : "User3"
},
{
"id" : "uid4",
"name" : "ChangedName2"
}
]
}

How to update object array within another object array with mongoose?

I have a problem regarding MongoDB, Mongoose, and Node.js.
I have this schema using Mongoose:
const EmoteSchema = new mongoose.Schema({
guild_id: String,
users: [{
user_id: String,
emotes: [{
emote: String,
channels: [String],
cooldown: Number,
all_channels: { type: Boolean, default: true },
global_cooldown: { type: Boolean, default: true },
lock: { type: Boolean, default: false }
}]
}]
});
What it looks like MongoDB:
{
"_id" : ObjectId("5ee6776b9277898982b0a25a"),
"guild_id" : "714931095929618443",
"users" : [
{
"_id" : ObjectId("5ee6776b9277898982b0a25b"),
"user_id" : "160230784257622016",
"emotes" : [
{
"channels" : [ ],
"all_channels" : true,
"global_cooldown" : true,
"lock" : false,
"_id" : ObjectId("5ee6776b9277898982b0a25c"),
"emote" : "🔤"
}
]
}
],
"__v" : 0
}
What I'm trying to achieve is to update the database, based on three conditions:
When sent the information of the guild_id, user_id, and emote:
If the guild_id, user_id, and emote exist in the database do nothing
If the guild_id, user_id (and emote does NOT exist), update the emote array with new emote
If the guild_id, (and user_id does not exist), update the users array with new user_id and update with the emote
If guild_id does not exit: create new entry with new guild_id, users array and emotes array
This is what I have attempted to do:
let filter = {
guild_id: guild_inp,
"users.user_id": user_inp
};
let update = {
$push: { "users.$[i].emotes": { emote: emote_inp } }
};
let options = {
arrayFilters: [
{ "i.users.user_id": user_inp }
],
rawResult: true
};
Emote.findOneAndUpdate(filter, update, options, fn);
If the guild_inp: 714931095929618443, user_inp: 160230784257622016, and emote:
The callback functions gets this result:
RES: {
lastErrorObject: { n: 1, updatedExisting: true },
value: {
_id: 5ee6776b9277898982b0a25a,
guild_id: '714931095929618443',
users: [ [Object] ],
__v: 0
},
ok: 1
}
It says that it has been updated? But I see no changes in MongoDB!
Any help is appreciated if there is an easier way to achieve what I am doing such as a new schema please share it!

How to improve mongoDb query performance?

I have a collection named Codes. This is how the Schema is defined:
import mongoose from 'mongoose'
import autoIncrement from 'mongoose-auto-increment';
const Schema = mongoose.Schema;
const CodesSchema = mongoose.Schema(
{
configId: { type: Number },
campaignId: { type: Number },
permissions: {
all: { type: Boolean, default: false },
users: { type: [Number], default: [] }
}
)
autoIncrement.initialize(mongoose.connection);
CodesSchema.plugin(autoIncrement.plugin, { model: 'Codes', field: 'campaignId' });
export default mongoose.model('Codes', CodesSchema)
There is one query that looks like this:
const query = {
$and:[
{$or: [
{'permissions.all': true},
{'permissions.users': 12}
]},
{configId: 3}
]
};
Codes.find(query, (err, res) => {
// do something with the result
})
This works fine, but if there is a huge number of documents in the database then this query is really slow.
Is there anything I can do to improve the performance of this specific query? I'm thinking that createIndex would help, but I'm not sure if that can be applied since there are $and and $or conditions.
UPDATE
I've added indexes this way:
CodesSchema.index({configId: 1, 'permissions.all': 1, 'permissions.users': 1});
But running the query with .explain('executionStats') option returns:
{
"executionSuccess" : true,
"nReturned" : 6,
"executionTimeMillis" : 0,
"totalKeysExamined" : 10,
"totalDocsExamined" : 10,
}
Which doesn't seems right because the number of docs examined is greater than the number of docs returned.
The index itself is correct.
It must be CodesSchema.index, not Code.index.
Ensure you call Code.syncIndexes to update indexes dbside.
The "explain" part - you should check winningPlan.
If no indexes are used by the query, it should be something like
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
When the index is being used it changes to
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "OR",
"inputStages" : [
{
"stage" : "IXSCAN",
"keyPattern" : {

Update existing items in nested array using mongoose

There is a schema as mentioned below. I'm trying to update the existing todo.task.
The problem is, I am storing the path as var done = 'todos.'+todoIndex+'.tasks.'+taskIndex+'.done' and it does not work. I was looking to update like todos.0.tasks.0.done:req.body.done, but it doen't work at all.
(todoIndex and taskIndex are in string which stores the index values)
What is the correct way of doing this?
var mongoose = require('mongoose');
var todoSchema = {
authorId : mongoose.Schema.Types.ObjectId,
date:{
type:Date
},
title : {
type : String
},
description : {
type : String
},
todos : [ {
created : {
type : Date
},
updated : {
type : Date
},
title : {
type : String
},
description : {
type : String
},
done:{
type:Boolean
},
deadline:{
type:Date
},
tasks : [ {
done : Boolean,
task : String
} ]
} ]
}
module.exports = new mongoose.Schema(todoSchema);
module.exports.todoSchema = todoSchema;
I was trying to build the Api like this:
api.put('/notebooks/todo/update/:pid',wagner.invoke(function(Todo,User){
return function(req,res){
var taskIndex=req.body.taskIndex;
var todoIndex=req.body.todoIndex;
var done = 'todos.'+todoIndex+'.tasks.'+taskIndex+'.done';
console.log(done);
Todo.update({_id:req.params.pid},{$set:{
done : req.body.done,
}}, function(err,done){
console.log( done);
})
}}));
If you're using a recent Node version, you can use a computed property name:
Todo.update({ _id : req.params.pid }, { $set : { [ done ] : req.body.done } }, ...
Otherwise, you need to use an intermediate object:
var done = 'todos.'+todoIndex+'.tasks.'+taskIndex+'.done';
var obj = {};
obj[done] = req.body.done;
Todo.update({ _id : req.params.pid }, { $set : obj }, ...

mongodb mongoose nodejs express4 why insert a _id in Object Array field automatically?

It might be conceptual question about _id in mongodb.
I understand mongodb will insert a _id field automatically if you don't set key field in document.In my case, I defined a field as Object Array, I don't know why it always create a _id in each Object in Array of this field.
I do appreciate if someone could clarify it for me.
Mongoose Model Scheme definition:
module.exports = mongoose.model("Application", {
Name: String,
Description: String,
Dependency: [
{
App_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Application'
},
Priority: Number
}
]
});
This is an Update operation, request data is:
{ _id: '571953e33f33c919d03381b5',
Name: 'A Test Utility (cmd)',
Description: 'A Test Utility (cmd)'
Dependency:
[ { App_id: '571953e33f33c919d03381b6', Priority: true },
{ App_id: '571953e33f33c919d03383da', Priority: 0 } ]
}
I use this code to update it
var id = req.body._id;
Application.findOneAndUpdate({ _id: id }, req.body, function (err, app) {
if (err)
res.send(err);
res.json(app);
});
The update is successful.But the document in mongodb is:
{
"_id" : ObjectId("571953e33f33c919d03381b5"),
"Name" : "A Test Utility (cmd)",
"Description" : "A Test Utility (cmd)",
"Dependency" : [
{
"Priority" : 1,
"App_id" : ObjectId("571953e33f33c919d03381b6"),
"_id" : ObjectId("571a7f552985372426509acb")
},
{
"Priority" : 0,
"App_id" : ObjectId("571953e33f33c919d03383da"),
"_id" : ObjectId("571a7f552985372426509aca")
}
]
}
I just don't understand how come the _id in the "Dependency" Array?
Thanks.
When you use [{..}] that means inside it act as a sub schema and you know that MongoDB insert a _id field automatically if you don't set key field in document. So you need to force to insert document without _id field.
Need use {_id:false} for your Dependency array schema to insert without _id
var ApplicationSchema = new mongoose.Schema({
Name: String,
Description: String,
Dependency: [
{
App_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Application'
},
Priority: Number,
_id: false
}
]
});
module.exports = mongoose.model("Application", ApplicationSchema);

Resources