Getting first element of embedded array in mongoDB using Node JS driver - node.js

Let's say I have the following document stored in a mongoDB collection 'people':
{
_id: 489324,
name: "Ryan Jones"
skills: [ "fishing", "programming" ]
}
I am trying to retrieve Ryan Jones's first skill in the array (skills[0]).
This seems like a dead simple operation but I can't seem to do it using the Node JS driver. I can retrieve just the skills array easily:
db.collection('people').findOne({ name:"Ryan Jones"},{ projection: { skills:1 }})
...but I don't want to transfer the whole array over the wire. I just want to get "fishing".
I have tried using slice and arrayElemAt within projection but I get a MongoError. How can I achieve this using the NodeJS driver? Does it require a more complex aggregation operation instead of findOne?

You can achieve that with aggregation , with $arrayElemAt something like this
db.collection('people').aggregate([
{
$match: {
name: "Ryan Jones"
}
},
{
$project: {
name: 1,
skills: {
$arrayElemAt: [
"$skills",
0
]
},
}
}
])
See demo here

Try this one:
db.collection('people').findOne({
name: "Ryan Jones"
},
{
skills: {
$slice: 1
}
})
MongoTemplate with .find

Related

mongoose: sort and paginating the field inside $project

$project: {
_id: 1,
edited: 1,
game: {
gta: {
totalUserNumber: {
$reduce: {
input: "$gta.users",
initialValue: 0,
in: { $add: [{ $size: "$$this" }, "$$value"] },
},
},
userList: "$gta.users", <----- paginating this
},
DOTA2: {
totalUserNumber: {
$reduce: {
input: "$dota2.users",
initialValue: 0,
in: { $add: [{ $size: "$$this" }, "$$value"] },
},
},
userList: "$dota2.users", <------ paginating this
},
},
.... More Games
},
I have this $project. I have paginated the list of games by using $facet,$sort, $skip and $limit after $project.
I am trying also trying to paginate each game's userList. I have done to get the total value in order to calculate the page number and more.
But, I am struggling to apply $sort and $limit inside the $project. So far, I have just returned the document and then paginated with the return value. However, I don't think this is very efficient and wondering if there is any way that I can paginate the field inside the $project.
Is there any way that I can apply $sort and $limit inside the $project, in order to apply pagination to the fields and return?
------ Edit ------
this is for paginating the field. Because, I am already paginating the document (game list), I could not find any way that I can paginate the field, because I could not find any way that I can apply $facet to the field.
e.g. document
[
gta: {
userID: ['aa', 'bb', 'cc' ......],
},
dota: {
userID: ['aa', 'bb', 'cc' ......],
}
....
]
I am using $facet to paginate the list of games (dota, gta, lol and more). However, I did not want to return all the userID. I had to return the entire document and then paginate the userID to replace the json doc.
Now, I can paginate the field inside the aggregate pipeline by using $function.
thanks to Mongodb sort inner array !
const _function = function (e) {
e // <---- will return userID array. You can do what you want to do.
return {
};
};
game
.collection("game")
.aggregate([
{},
{
$set: {
"game": {
$function: {
body: _function,
args: ["$userID"],
lang: "js",
},
},
},
},
])
.toArray();
By using $function multiple time, you will be able to paginate the field. I don' really know if this is faster or not tho. Plus, make sure you can use $function. I read that you can't use this if you are on the free tier at Atlas.
What you are looking for is the $slice Operator.
It requires three parameters.
"$slice": [<Array>, <start-N>, <No-Of.elements to fetch>]
userList: {"$slice": ["$dota2.users", 20, 10]} // <-- Will ignore first 20 elements in array and gets the next 10

How to return field based on other fields with mongoose

I have mongoose schema that looks something like this:
{
_id: someId,
name: 'mike',
keys: {
apiKey: 'fsddsfdsfdsffds',
secretKey: 'sddfsfdsfdsfdsds'
}
}
I don't want to send back to the front the keys of course, but I want some indication, like:
{
_id: someId,
name: 'mike',
hasKeys: true
}
There is built in way to create 'field' on the way based on other fields, or do I need every time fetch the whole document, check if keys is not empty and set object property based on that?
For Mongo version 4.2+ What you're looking for is called pipelined updates, it let's you use a (restricted) aggregate pipeline as your update allowing the usage of existing field values.
Here is a toy example with your data:
db.collection.updateOne(
{ _id: someId },
[
{
"$set": {
"hasKeys": {
$cond: [
{
$ifNull: [
"$keys",
false
]
},
true,
false
]
}
}
},
])
Mongo Playground
For older Mongo versions you have to do it in code.
If you don't want to update the actual document but just populate this field when you fetch it you can use the same aggregation to fetch the document
you can use $project in mongoose aggregation like this.
$project: { hasKeys: { $cond: [{ $eq: ['$keys', null] }, false, true]}}

How can I mix a populated ObjectId with a string

Actually, in the database I got a job that I request with a GET route:
So when I populate candidates I got this response format :
My problem here is I don't need that "id" object, I just need a "selected_candidates" array with users inside as objects. Actually it's an object, in another object that is in an Array.
Here the code from my controller (the populate is in the jobsService):
If I change the data format of the job like that way:
...It is working great (with a path: "candidates_selected") like expected BUT I don't have that "status" string (Normal because I don't have it anymore in the DataBase. Because of ObjectId):
I would like a solution to have them both, but maybe it's the limit of noSQL?
A solution without populate but with a Loop (I don't think it's a good idea):
I think there is no convenience way to achieve it. However you may try the aggregate framework from the native MongoDB driver.
Let your Mongoose schemas be ASchema and BSchema
const result = await ASchema.aggregate([
{$addFields: {org_doc: '$$ROOT'}}, // save original document to retrieve later
{$unwind: '$candidates_selected'},
{
$lookup: {
from: BSchema.collection.name,
let: {
selected_id: '$candidates_selected.id',
status: '$candidates_selected.status',
},
pipeline: [
{
$match: {$expr: {$eq: ['$$selected_id', '$_id']}}, // find candidate by id
},
{
$addFields: {status: '$$status'} // attach status
}
],
as: 'found_candidate'
}
},
{
$group: { // regroup the very first $unwind stage
_id: '$_id',
org_doc: {$first: '$org_doc'},
found_candidates: {
$push: {$arrayElemAt: ['$found_candidate', 0]} // result of $lookup is an array, concat them to reform the original array
}
}
},
{
$addFields: {'org_doc.candidates_selected': '$found_candidates'} // attach found_candidates to the saved original document
},
{
$replaceRoot: {newRoot: '$org_doc'} // recover the original document
}
])

MongoDB/Mongoose: Search based on value within a given document without first returning that document

I'd like to do a search in MongoDB using either Mongo or Mongoose based on the value of a field in a document.
Let's say I had three MongoDB documents that looked like this:
{
name: "Michael",
mentored: ["Dwight", "Ryan", "Jim"]
},
{
name: "Jim",
mentored: ["Toby", "Roy", "Darryl"]
},
{
name: "Stanley",
mentored: ["Pam", "Meredith", "Angela"]
}
Let's further say I want to do a search for anyone who Michael has not mentored, which in this case would be Stanley (let's assume that the people in the arrays don't necessarily have their own records). I know I can do a search like this in Mongoose to get the result I want:
User.findOne({ name: "Michael" })
.then((person) => {
const mentored = person.mentored
return User.find({ name: { $nin: mentored } })
)
.then((person2) => {
console.log(person2); // Stanley
})
However, is there any way to do this without first returning the array from the database and then doing a second search? Something like this:
User.findOne({ name: { $nin: { "Michael's mentored people array" } } })
Ultimately I'm looking to see if there's any way to make more efficient such a situation in which arrays can get 10s of thousands of values long. Many thanks.
I think what you are doing is already efficient for large arrays.
But you could try the following, as suggested by #D.SM. but using aggregation the intermediate results will have to be loaded in memory, which does not seem efficient to me.
User.aggregate([
{
$match: {
name: "Michael"
}
},
{
$lookup: {
from: "collection",
as: "notMentored",
let: {
mentored: "$mentored",
/*
You probably want to remove "Michael" from the result, one way is to add him to the mentored array
mentored: { $concatArrays: ["$mentored", ["$name"]]}
*/
},
pipeline: [{
$match: {
$expr: { $not: { $in: ["$name", "$$mentored"] } }
}
}]
}
},
{
$unwind: "$notMentored"
},
{
$replaceRoot: {
newRoot: "$notMentored"
}
}
])

MongoDB remove nested objects

I have a collection like this:
_id: {name: 'name', family: 'family'}
i want to remove some objects by _id using by $in, how i can do this?
for example my query should be something like:
db.persons.remove({_id: {$in: [ { name: 'name1', family: 'family1' }
, { name: 'name2', family: 'family2' }
]
}
})
You can also do this with an $or query and dot notation if your fields are not always in the same order:
db.persons.remove({
"$or": [
{ "_id.name": "name1", "_id.family": "family1" },
{ "_id.name": "name2", "_id.family": "family2" },
}
})
Not the mongoose syntax, but you get the idea. It is logically the same thing but the field order is not dependent as it is with the full object you are specifying to $in.

Resources