Related
Hello I am trying to add an element to an array that is inside an object and the object in turn inside an array, below is the structure.
// Schema called "Team" with mongoose
category: [
{
seasson: { type: String, required: true },
categories: [{ type: String, required: true }]
}]
// In code looks like:
[
{
seasson: "The seasson name 1",
categories: ["categoryOne", "categoryTwo"]
}
{
seasson: "The seasson name 2",
categories: ["categoryOne"] // I want to make push in this array the value "categoryTwo"
},
]
// I´m trying something like following code:
const status = await Team.updateOne(
{
_id: mongoose.Types.ObjectId(teamId),
},
{ $addToSet: { "category.$last.categories": "categoryTwo"} }
)
Whenever an array has to be pushed into the object, it will be in the last position of the main array. Honestly, I've been trying to find a way for a while, but I can't think of anything that works.
Thanks in advance.
There is no straight way to update the last element of the array without any identity, you can use update with aggregation pipeline starting from MongoDB 4.2,
$map to iterate loop of category array
$mergeObjects to merge current category object with updated categories field
$last to get the last element value from category.seasson
$cond check condition if above last element's value and current object session matches then do update operation otherwise return existing values
$setUnion to concat new value of categories, if it is present then it will do replace
let category = "categoryTwo";
const status = await Team.updateOne(
{ _id: mongoose.Types.ObjectId(teamId) },
[{
$set: {
category: {
$map: {
input: "$category",
in: {
$mergeObjects: [
"$$this",
{
categories: {
$cond: [
{
$eq: [
{ $last: "$category.seasson" },
"$$this.seasson"
]
},
{ $setUnion: ["$$this.categories", [category]] },
"$$this.categories"
]
}
}
]
}
}
}
}
}]
)
Playground
The bellow query,adds "categoryTwo" in categories,of the last member of array
category.I think this is what you want.
If you can next time give the document in the initial form,describe the query you want,and give the document in the final form,in valid JSON so people can help you easier.
You can try the code here
Its pipeline update,needs MongoDB >= 4.2
Data in(Collection)
[
{
"_id": 1,
"category": [
{
"seasson": "The seasson name 1",
"categories": [
"categoryOne",
"categoryTwo"
]
},
{
"seasson": "The seasson name 2",
"categories": [
"categoryOne"
]
},
]
}
]
Query
db.collection.update({
"_id": {
"$eq": 1
}
},
[
{
"$addFields": {
"category": {
"$let": {
"vars": {
"without_last": {
"$slice": [
"$category",
0,
{
"$subtract": [
{
"$size": "$category"
},
1
]
}
]
},
"last_member": {
"$arrayElemAt": [
{
"$slice": [
"$category",
-1,
1
]
},
0
]
}
},
"in": {
"$concatArrays": [
"$$without_last",
[
{
"$mergeObjects": [
"$$last_member",
{
"categories": {
"$concatArrays": [
"$$last_member.categories",
[
"categoryTwo"
]
]
}
}
]
}
]
]
}
}
}
}
}
])
Results
[
{
"_id": 1,
"category": [
{
"categories": [
"categoryOne",
"categoryTwo"
],
"seasson": "The seasson name 1"
},
{
"categories": [
"categoryOne",
"categoryTwo"
],
"seasson": "The seasson name 2"
}
]
}
]
-room collection
_id: ObjectId("xxx")
bedspaces: Array
0:ObjectId("xx")
1:ObjectId("xx")
***
***
-bedspace collection
_id: ObjectId("xxxx");
number: 1
decks: Array
{
_id: ObjectId("xxx");
number: 1
status: "Vacant"
tenant: ObjectId("5c964ae7f5097e3020d1926c")
dueRent: 11
away: null
},
{
_id: ObjectId("xxx");
number: 2
status: "Vacant"
tenant: null
dueRent: 11
away: null
}
Under the decks array, is my tenant field, that have objectId, and i am going to lookup this object id, in the tenants, collection.
-tenant collection
_id: ObjectId("5c964ae7f5097e3020d1926c");
name: 'John Doe'
-expected output
/*room collection*/
_id: ObjectId("xxx")
bedspaces: [
{
_id: ObjectId("xxx")
number: 1
decks: [
{
_id: ObjectId("xxx")
number: 1
status: "Vacant"
tenant: {
name: 'John Doe'
}
dueRent: 11
away: null
},
{
_id: ObjectId("xxx");
number: 1
status: "Vacant"
tenant: null
dueRent: 11
away: null
}
]
}
]
There is also an instances, that deck array is equal to null.
In below aggregation it will only display the decks, that have tenant with object id, what i want is to display both the decks.
{
from: 'beds',
let: {bedspace: '$bedspaces'},
pipeline:[
{
$match: {
$expr: {
$in: ["$_id", "$$bedspace"]
}
}
},
{
$unwind: "$decks"
},
{
$lookup: {
from: 'tenants',
let: {tenant: "$decks.tenant"},
pipeline: [
{
$match: {
$expr: {
$eq: ["$_id", "$$tenant"]
}
}
}
],
as: "decks.tenant",
}
},
{
$unwind: "$decks.tenant"
},
{ $group: {
_id: "$_id",
decks: { $push: "$decks" },
number: {$first: "$number"}
}}
],
as: "bedspaces"
}
"how can i add condition on my second look up, to execute only if tenant is not null", so that i could retrieve both decks, or any work-around so i could achieved my desired result
Don't really have time for all the explanation right now (sorry),
Explanation
The basic issue here is that usage of $unwind is your problem and you don't need it. Use $map on the produced array content merging with the "decks" array instead. Then you can have nulls.
What you want to do here is have the values from the $lookup from your "tenants" collection transposed into the existing array within your "beds/bedspaces" collection for it's own existing "tenant" values which are the ObjectId references for the foreign collection.
The $lookup stage cannot do this by simply naming the field path within the "as" output where that path is already inside another array, and in fact the output of $lookup is always an array of results obtained from the foreign collection. You want singular values for each actual match, and of course you expect a null to be in place where nothing matches, and of course keeping the original document array of "decks" intact, but just including the foreign details where those were found.
Your code attempt seems partially aware of this point as you are using $unwind on the $lookup result on the ""tenants" collection into a "temporary array" ( but you put in in the existing path and that overwrites content ) and then attempting to "re-group" as an array through $group and $push. But the problem of course is the $lookup result does not apply to every array member within "decks", so you end up with less results than you want.
The real solution is not a "conditional $lookup", but instead to transpose the "temporary array" content from the result into the existing "decks" entries. You do this using $map to process the array members, and $arrayElemAt along with $indexOfArray in order to return the matching elements from the "temporary array" by the matching _id values to "tenant".
{ "$lookup": {
"from": Tenant.collection.name,
"let": { "tenant": "$decks.tenant" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$tenant" ] }
}}
],
"as": "tenant"
}},
{ "$addFields": {
"decks": {
"$map": {
"input": "$decks",
"in": {
"$mergeObjects": [
"$$this",
{
"tenant": {
"$cond": {
"if": {
"$eq": [
{ "$indexOfArray": ["$tenant._id", "$$this.tenant"] },
-1
]
},
"then": null,
"else": {
"$arrayElemAt": [
"$tenant",
{ "$indexOfArray": ["$tenant._id", "$$this.tenant"]}
]
}
}
}
}
Noting there we are using $mergeObjects inside the $map in order to keep the existing content of the "decks" array and only replace ( or "merge" ) an overwritten representation of "tenant" for each array member. You are using the expressive $lookup already and this like $mergeObjects is a MongoDB 3.6 feature.
Just for interest the same thing can be done by just specifying every field within the array. i.e:
"decks": {
"$map": {
"input": "$decks",
"in": {
"_id": "$$this._id",
"number": "$$this.number",
"tenant": {
// same expression
},
"__v": "$$this.__v" // just because it's mongoose
}
}
}
Much the same can be said for the $$REMOVE used in the $addFields which is also another MongoDB 3.6 feature. You can alternately just use $project and simply omit the unwanted fields:
{ "$project": {
"number": "$number",
"decks": {
"$map": { /* same expression */ }
},
"__v": "$__v"
// note we don't use the "tenant" temporary array
}}
But that's basically how it works. By taking the $lookup result and then transposing those results back into the original array within the document.
Example Listing
Also abstracting on your data from previous questions here, which is a bit better than what you posted in the question here. Runnable listing for demonstration:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/hotel';
const opts = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndexes', true);
mongoose.set('debug', true);
const tenantSchema = new Schema({
name: String,
age: Number
});
const deckSchema = new Schema({
number: Number,
tenant: { type: Schema.Types.ObjectId, ref: 'Tenant' }
});
const bedSchema = new Schema({
number: Number,
decks: [deckSchema]
});
const roomSchema = new Schema({
bedspaces: [{ type: Schema.Types.ObjectId, ref: 'Bed' }]
});
const Tenant = mongoose.model('Tenant', tenantSchema);
const Bed = mongoose.model('Bed', bedSchema);
const Room = mongoose.model('Room', roomSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k, m]) => m.deleteMany())
);
// Insert data
let [john, jane, bilbo ] = await Tenant.insertMany([
{
_id: ObjectId("5c964ae7f5097e3020d1926c"),
name: "john doe",
age: 11
},
{
_id: ObjectId("5c964b2531bc162fdce64f15"),
name: "jane doe",
age: 12
},
{
_id: ObjectId("5caa5454494558d863513b24"),
name: "bilbo",
age: 111
}
]);
let bedspaces = await Bed.insertMany([
{
_id: ObjectId("5c98d89c6bd5fc26a4c2851b"),
number: 1,
decks: [
{
number: 1,
tenant: john
},
{
number: 1,
tenant: jane
}
]
},
{
_id: ObjectId("5c98d89f6bd5fc26a4c28522"),
number: 2,
decks: [
{
number: 2,
tenant: bilbo
},
{
number: 3
}
]
}
]);
await Room.create({ bedspaces });
// Aggregate
let results = await Room.aggregate([
{ "$lookup": {
"from": Bed.collection.name,
"let": { "bedspaces": "$bedspaces" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$bedspaces" ] }
}},
{ "$lookup": {
"from": Tenant.collection.name,
"let": { "tenant": "$decks.tenant" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$tenant" ] }
}}
],
"as": "tenant"
}},
{ "$addFields": {
"decks": {
"$map": {
"input": "$decks",
"in": {
"$mergeObjects": [
"$$this",
{
"tenant": {
"$cond": {
"if": {
"$eq": [
{ "$indexOfArray": ["$tenant._id", "$$this.tenant"] },
-1
]
},
"then": null,
"else": {
"$arrayElemAt": [
"$tenant",
{ "$indexOfArray": ["$tenant._id", "$$this.tenant"]}
]
}
}
}
}
]
}
}
},
"tenant": "$$REMOVE"
}}
],
"as": "bedspaces"
}}
]);
log(results);
} catch (e) {
console.error(e)
} finally {
mongoose.disconnect();
}
})()
Returns:
Mongoose: tenants.deleteMany({}, {})
Mongoose: beds.deleteMany({}, {})
Mongoose: rooms.deleteMany({}, {})
Mongoose: tenants.insertMany([ { _id: 5c964ae7f5097e3020d1926c, name: 'john doe', age: 11, __v: 0 }, { _id: 5c964b2531bc162fdce64f15, name: 'jane doe', age: 12, __v: 0 }, { _id: 5caa5454494558d863513b24, name: 'bilbo', age: 111, __v: 0 } ], {})
Mongoose: beds.insertMany([ { _id: 5c98d89c6bd5fc26a4c2851b, number: 1, decks: [ { _id: 5caa5af6ed3dce1c3ed72cef, number: 1, tenant: 5c964ae7f5097e3020d1926c }, { _id: 5caa5af6ed3dce1c3ed72cee, number: 1, tenant: 5c964b2531bc162fdce64f15 } ], __v: 0 }, { _id: 5c98d89f6bd5fc26a4c28522, number: 2, decks: [ { _id: 5caa5af6ed3dce1c3ed72cf2, number: 2, tenant: 5caa5454494558d863513b24 }, { _id: 5caa5af6ed3dce1c3ed72cf1, number: 3 } ], __v: 0 } ], {})
Mongoose: rooms.insertOne({ bedspaces: [ ObjectId("5c98d89c6bd5fc26a4c2851b"), ObjectId("5c98d89f6bd5fc26a4c28522") ], _id: ObjectId("5caa5af6ed3dce1c3ed72cf3"), __v: 0 })
Mongoose: rooms.aggregate([ { '$lookup': { from: 'beds', let: { bedspaces: '$bedspaces' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$bedspaces' ] } } }, { '$lookup': { from: 'tenants', let: { tenant: '$decks.tenant' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$tenant' ] } } } ], as: 'tenant' } }, { '$addFields': { decks: { '$map': { input: '$decks', in: { '$mergeObjects': [ '$$this', { tenant: [Object] } ] } } }, tenant: '$$REMOVE' } } ], as: 'bedspaces' } } ], {})
[
{
"_id": "5caa5af6ed3dce1c3ed72cf3",
"bedspaces": [
{
"_id": "5c98d89c6bd5fc26a4c2851b",
"number": 1,
"decks": [
{
"_id": "5caa5af6ed3dce1c3ed72cef",
"number": 1,
"tenant": {
"_id": "5c964ae7f5097e3020d1926c",
"name": "john doe",
"age": 11,
"__v": 0
}
},
{
"_id": "5caa5af6ed3dce1c3ed72cee",
"number": 1,
"tenant": {
"_id": "5c964b2531bc162fdce64f15",
"name": "jane doe",
"age": 12,
"__v": 0
}
}
],
"__v": 0
},
{
"_id": "5c98d89f6bd5fc26a4c28522",
"number": 2,
"decks": [
{
"_id": "5caa5af6ed3dce1c3ed72cf2",
"number": 2,
"tenant": {
"_id": "5caa5454494558d863513b24",
"name": "bilbo",
"age": 111,
"__v": 0
}
},
{
"_id": "5caa5af6ed3dce1c3ed72cf1",
"number": 3,
"tenant": null
}
],
"__v": 0
}
],
"__v": 0
}
]
Shows the null on the second entry of the second entry in the bedspaces array as expected.
Having trouble Populating my user.
The case:
var User = new mongoose.Schema({
name: {
type: String,
lowercase: true,
unique: true
},
portfolio:[
{
name: String,
formatType: { type: mongoose.Schema.Types.ObjectId, ref: 'FormatType' },
}
]
});
And this is my Mongoose command:
User.findById(req.payload.id)
.populate({
path:'portfolio',
populate:{
path: 'formatType',
model: 'FormatType'
}
})
.then(user => { ...
So what we have here is a model - inside of an Obect - inside of an array - inside of an entity.
Couldn't find an Answer online, would be very thankful~!
What you basically missed here is the "path" to the field you want to populate() is actually 'portfolio.formatType' and not just 'portfolio' as you have typed. Due to that mistake and the structure, you might have a few general misconceptions though.
Populate Correction
The basic correction merely needs the correct path, and you don't need the model argument since this is already implied in the schema:
User.findById(req.params.id).populate('portfolio.formatType');
It is however generally not a great idea to "mix" both "embedded" data and "referenced" data within arrays, and you should really be either embedding everything or simply referencing everything. It's also a little bit of an "anti-pattern" in general to keep an array of references in the document if your intention is referencing, since your reason should be not to cause the document to grow beyond the 16MB BSON limit. And where that limit would never be reached by your data it's generally better to "embed fully". That's really a wider discussion, but something you should be aware of.
The next general point here is populate() itself is somewhat "old hat", and really not the "magical" thing most new users perceive it to be. To be clear populate() is NOT A JOIN, and all it is doing is executing another query to the server in order to return the "related" items, then merge that content into the documents returned from the previous query.
$lookup Alternative
If you are looking for "joins", then really you probably wanted "embedding" as mentioned earlier. This is really the "MongoDB Way" of dealing with "relations" but keeping all "related" data together in the one document. The other means of a "join" where data is in separate collections is via the $lookup operator in modern releases.
This gets a bit more complex due to your "mixed" content array form, but can generally be represented as:
// Aggregation pipeline don't "autocast" from schema
const { Types: { ObjectId } } = require("mongoose");
User.aggregate([
{ "$match": { _id: ObjectId(req.params.id) } },
{ "$lookup": {
"from": FormatType.collection.name,
"localField": "portfolio.formatType",
"foreignField": "_id",
"as": "formats"
}},
{ "$project": {
"name": 1,
"portfolio": {
"$map": {
"input": "$portfolio",
"in": {
"name": "$$this.name",
"formatType": {
"$arrayElemAt": [
"$formats",
{ "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
]
}
}
}
}
}}
]);
Or with the more expressive form of $lookup since MongoDB 3.6:
User.aggregate([
{ "$match": { _id: ObjectId(req.params.id) } },
{ "$lookup": {
"from": FormatType.collection.name,
"let": { "portfolio": "$portfolio" },
"as": "portfolio",
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$portfolio.formatType" ]
}
}},
{ "$project": {
"_id": {
"$arrayElemAt": [
"$$portfolio._id",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"name": {
"$arrayElemAt": [
"$$portfolio.name",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"formatType": "$$ROOT",
}}
]
}}
]);
The two approaches work slightly differently, but both essentially work with the concept of returning the matching "related" entries and then "re-mapping" onto the existing array content in order to merge with the "name" properties "embedded" inside the array. That is actually the main complication that otherwise is a fairly straightforward method of retrieval.
It's pretty much the same process as what populate() actually does on the "client" but executed on the "server". So the comparisons are using the $indexOfArray operator to find where the matching ObjectId values are and then return a property from the array at that matched "index" via the $arrayElemAt operation.
The only difference is that in the MongoDB 3.6 compatible version, we do that "substitution" within the "foreign" content "before" the joined results are returned to the parent. In prior releases we return the whole matching foreign array and then "marry up" the two to form a singular "merged" array using $map.
Whilst these may initially look "more complex", the big advantage here is that these constitute a "single request" to the server with a "single response" and not issuing and receiving "multiple" requests as populate() does. This actually saves a lot of overhead in network traffic and greatly increases response time.
In addition, these are "real joins" so there is a lot more you can do which cannot be achieved with "multiple queries". For instance you can "sort" results on the "join" and only return the top results, where as using populate() needs to pull in "all parents" before it can even look for which "children" to return in result. The same goes for "filtering" conditions on the child "join" as well.
There is some more detail on this on Querying after populate in Mongoose about the general limitations and what you actually can even practically do to "automate" the generation of such "complex" aggregation pipeline statements where needed.
Demonstration
Another common problem with doing these "joins" and understanding referenced schema in general is that people often get the concepts wrong on where and when to store the references and how it all works. Therefore the following listings serve as demonstration of both the storage and retrieval of such data.
In a native Promises implementation for older NodeJS releases:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/usertest';
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const formatTypeSchema = new Schema({
name: String
});
const portfolioSchema = new Schema({
name: String,
formatType: { type: Schema.Types.ObjectId, ref: 'FormatType' }
});
const userSchema = new Schema({
name: String,
portfolio: [portfolioSchema]
});
const FormatType = mongoose.model('FormatType', formatTypeSchema);
const User = mongoose.model('User', userSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(function() {
mongoose.connect(uri).then(conn => {
let db = conn.connections[0].db;
return db.command({ buildInfo: 1 }).then(({ version }) => {
version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
return Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()))
.then(() => FormatType.insertMany(
[ 'A', 'B', 'C' ].map(name => ({ name }))
)
.then(([A, B, C]) => User.insertMany(
[
{
name: 'User 1',
portfolio: [
{ name: 'Port A', formatType: A },
{ name: 'Port B', formatType: B }
]
},
{
name: 'User 2',
portfolio: [
{ name: 'Port C', formatType: C }
]
}
]
))
.then(() => User.find())
.then(users => log({ users }))
.then(() => User.findOne({ name: 'User 1' })
.populate('portfolio.formatType')
)
.then(user1 => log({ user1 }))
.then(() => User.aggregate([
{ "$match": { "name": "User 2" } },
{ "$lookup": {
"from": FormatType.collection.name,
"localField": "portfolio.formatType",
"foreignField": "_id",
"as": "formats"
}},
{ "$project": {
"name": 1,
"portfolio": {
"$map": {
"input": "$portfolio",
"in": {
"name": "$$this.name",
"formatType": {
"$arrayElemAt": [
"$formats",
{ "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
]
}
}
}
}
}}
]))
.then(user2 => log({ user2 }))
.then(() =>
( version >= 3.6 ) ?
User.aggregate([
{ "$lookup": {
"from": FormatType.collection.name,
"let": { "portfolio": "$portfolio" },
"as": "portfolio",
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$portfolio.formatType" ]
}
}},
{ "$project": {
"_id": {
"$arrayElemAt": [
"$$portfolio._id",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"name": {
"$arrayElemAt": [
"$$portfolio.name",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"formatType": "$$ROOT",
}}
]
}}
]).then(users => log({ users })) : ''
);
})
.catch(e => console.error(e))
.then(() => mongoose.disconnect());
})()
And with async/await syntax for newer NodeJS releases, including current LTS v.8.x series:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/usertest';
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const formatTypeSchema = new Schema({
name: String
});
const portfolioSchema = new Schema({
name: String,
formatType: { type: Schema.Types.ObjectId, ref: 'FormatType' }
});
const userSchema = new Schema({
name: String,
portfolio: [portfolioSchema]
});
const FormatType = mongoose.model('FormatType', formatTypeSchema);
const User = mongoose.model('User', userSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
let db = conn.connections[0].db;
let { version } = await db.command({ buildInfo: 1 });
version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
log(version);
// Clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Insert some things
let [ A, B, C ] = await FormatType.insertMany(
[ 'A', 'B', 'C' ].map(name => ({ name }))
);
await User.insertMany(
[
{
name: 'User 1',
portfolio: [
{ name: 'Port A', formatType: A },
{ name: 'Port B', formatType: B }
]
},
{
name: 'User 2',
portfolio: [
{ name: 'Port C', formatType: C }
]
}
]
);
// Show plain users
let users = await User.find();
log({ users });
// Get user with populate
let user1 = await User.findOne({ name: 'User 1' })
.populate('portfolio.formatType');
log({ user1 });
// Get user with $lookup
let user2 = await User.aggregate([
{ "$match": { "name": "User 2" } },
{ "$lookup": {
"from": FormatType.collection.name,
"localField": "portfolio.formatType",
"foreignField": "_id",
"as": "formats"
}},
{ "$project": {
"name": 1,
"portfolio": {
"$map": {
"input": "$portfolio",
"in": {
"name": "$$this.name",
"formatType": {
"$arrayElemAt": [
"$formats",
{ "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
]
}
}
}
}
}}
]);
log({ user2 });
// Expressive $lookup
if ( version >= 3.6 ) {
let users = await User.aggregate([
{ "$lookup": {
"from": FormatType.collection.name,
"let": { "portfolio": "$portfolio" },
"as": "portfolio",
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$portfolio.formatType" ]
}
}},
{ "$project": {
"_id": {
"$arrayElemAt": [
"$$portfolio._id",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"name": {
"$arrayElemAt": [
"$$portfolio.name",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"formatType": "$$ROOT",
}}
]
}}
]);
log({ users })
}
mongoose.disconnect();
} catch(e) {
console.log(e)
} finally {
process.exit()
}
})()
The latter listing if commented on each stage to explain the parts, and you can at least see by comparison how both forms of syntax relate to each other.
Note that the "expressive" $lookup example only runs where the MongoDB server connected to actually supports the syntax.
And the "output" for those who cannot be bothered to run the code themselves:
Mongoose: formattypes.remove({}, {})
Mongoose: users.remove({}, {})
Mongoose: formattypes.insertMany([ { _id: 5b1601d8be9bf225554783f5, name: 'A', __v: 0 }, { _id: 5b1601d8be9bf225554783f6, name: 'B', __v: 0 }, { _id: 5b1601d8be9bf225554783f7, name: 'C', __v: 0 } ], {})
Mongoose: users.insertMany([ { _id: 5b1601d8be9bf225554783f8, name: 'User 1', portfolio: [ { _id: 5b1601d8be9bf225554783fa, name: 'Port A', formatType: 5b1601d8be9bf225554783f5 }, { _id: 5b1601d8be9bf225554783f9, name: 'Port B', formatType: 5b1601d8be9bf225554783f6 } ], __v: 0 }, { _id: 5b1601d8be9bf225554783fb, name: 'User 2', portfolio: [ { _id: 5b1601d8be9bf225554783fc, name: 'Port C', formatType: 5b1601d8be9bf225554783f7 } ], __v: 0 } ], {})
Mongoose: users.find({}, { fields: {} })
{
"users": [
{
"_id": "5b1601d8be9bf225554783f8",
"name": "User 1",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fa",
"name": "Port A",
"formatType": "5b1601d8be9bf225554783f5"
},
{
"_id": "5b1601d8be9bf225554783f9",
"name": "Port B",
"formatType": "5b1601d8be9bf225554783f6"
}
],
"__v": 0
},
{
"_id": "5b1601d8be9bf225554783fb",
"name": "User 2",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fc",
"name": "Port C",
"formatType": "5b1601d8be9bf225554783f7"
}
],
"__v": 0
}
]
}
Mongoose: users.findOne({ name: 'User 1' }, { fields: {} })
Mongoose: formattypes.find({ _id: { '$in': [ ObjectId("5b1601d8be9bf225554783f5"), ObjectId("5b1601d8be9bf225554783f6") ] } }, { fields: {} })
{
"user1": {
"_id": "5b1601d8be9bf225554783f8",
"name": "User 1",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fa",
"name": "Port A",
"formatType": {
"_id": "5b1601d8be9bf225554783f5",
"name": "A",
"__v": 0
}
},
{
"_id": "5b1601d8be9bf225554783f9",
"name": "Port B",
"formatType": {
"_id": "5b1601d8be9bf225554783f6",
"name": "B",
"__v": 0
}
}
],
"__v": 0
}
}
Mongoose: users.aggregate([ { '$match': { name: 'User 2' } }, { '$lookup': { from: 'formattypes', localField: 'portfolio.formatType', foreignField: '_id', as: 'formats' } }, { '$project': { name: 1, portfolio: { '$map': { input: '$portfolio', in: { name: '$$this.name', formatType: { '$arrayElemAt': [ '$formats', { '$indexOfArray': [ '$formats._id', '$$this.formatType' ] } ] } } } } } } ], {})
{
"user2": [
{
"_id": "5b1601d8be9bf225554783fb",
"name": "User 2",
"portfolio": [
{
"name": "Port C",
"formatType": {
"_id": "5b1601d8be9bf225554783f7",
"name": "C",
"__v": 0
}
}
]
}
]
}
Mongoose: users.aggregate([ { '$lookup': { from: 'formattypes', let: { portfolio: '$portfolio' }, as: 'portfolio', pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$portfolio.formatType' ] } } }, { '$project': { _id: { '$arrayElemAt': [ '$$portfolio._id', { '$indexOfArray': [ '$$portfolio.formatType', '$_id' ] } ] }, name: { '$arrayElemAt': [ '$$portfolio.name', { '$indexOfArray': [ '$$portfolio.formatType', '$_id' ] } ] }, formatType: '$$ROOT' } } ] } } ], {})
{
"users": [
{
"_id": "5b1601d8be9bf225554783f8",
"name": "User 1",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fa",
"name": "Port A",
"formatType": {
"_id": "5b1601d8be9bf225554783f5",
"name": "A",
"__v": 0
}
},
{
"_id": "5b1601d8be9bf225554783f9",
"name": "Port B",
"formatType": {
"_id": "5b1601d8be9bf225554783f6",
"name": "B",
"__v": 0
}
}
],
"__v": 0
},
{
"_id": "5b1601d8be9bf225554783fb",
"name": "User 2",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fc",
"name": "Port C",
"formatType": {
"_id": "5b1601d8be9bf225554783f7",
"name": "C",
"__v": 0
}
}
],
"__v": 0
}
]
}
My Collection:
geoGraphicalFilter: {
aCountries: [String],
aCities: [String],
aCoordinates: [{
coordinates: { type: Array }
}]
}
CollectionData
"geoGraphicalFilter": {
"aCoordinates": [
{
"_id": ObjectId("5acb641d93fa0e52557fc6aa"),
"coordinates": [
[
72.42919972527011,
23.0437703991947
],
[
72.45031407464302,
23.045823913521474
],
[
72.43263295281557,
23.030500782775746
],
[
72.42919972527011,
23.0437703991947
]
]
},
{
"_id": ObjectId("5acb641d93fa0e52557fc6ac"),
"coordinates": [
[
72.51520207511979,
23.038241551175616
],
[
72.55399754632015,
23.03934733892872
],
[
72.51812031852671,
23.025129376064214
],
[
72.51520207511979,
23.038241551175616
]
]
},
{
"_id": ObjectId("5acb641d93fa0e52557fc6ad"),
"coordinates": [
[
72.44653752434493,
23.02828905299478
],
[
72.4896245299627,
23.02828905299478
],
[
72.45477727044641,
23.0194417709901
],
[
72.44653752434493,
23.02828905299478
]
]
},
{
"_id": ObjectId("5acb641d93fa0e52557fc6ab"),
"coordinates": [
[
72.47451832878957,
23.045350028380867
],
[
72.50576069939376,
23.04835127278581
],
[
72.47949650871226,
23.031606634051897
],
[
72.47451832878957,
23.045350028380867
]
]
}
],
"aCities": [],
"aCountries": []
}
Remove From Database Snippet
const deleteZones = (req,res,next) => {
var body = _.pick(req.body, ["zones"]);
var zoneList = body.zones;
debug(zoneList)
var promise = function() {
return new Promise(function(resolve, reject) {
zoneList.forEach(itemA => {
console.log(itemA.coordinates)
huntingModel.update(
{ _id: req.body.id },
{ $pull: { 'geoGraphicalFilter.aCoordinates': itemA.id} },
(error, success) => {
if (error) console.log(error);
console.log(success);
}
);
});
resolve();
});
};
promise().then(function() {
return res.status(200).jsonp({
message: adminMessages.succ_zone_removed
});
});
}
Now the scenario is like when I am trying to delete data it shows success message but data does not get deleted.
var object = {
id:this.id,
zones: this.zonesDelete // Contains list of id
};
I am getting object in a requested body and I want to find the document from a collection and delete the particular array element by finding an id in geoGraphicalFilter.aCoordinates and wants to remove it.
As per documentation of $pull operator you can either specify a value or a condition
i.e.
{ $pull: { <field1>: <value|condition>, <field2>: <value|condition>, ... } }
In your scenario you need to either specify complete value of one or more aCoordinates item object or an condition that matches one or more aCoordinates item
Add the condition where you match id of aCoordinates item i.e.
Use following pull condition to solve the issue:
huntingModel.update(
{ _id: req.body.id },
{ $pull: { 'geoGraphicalFilter.aCoordinates': {'_id' : ObjectId(itemA.id)}} },
(error, success) => {
if (error) console.log(error);
console.log(success);
}
);
I am going to handle users search based on email_address, firstname, lastname, type and phone_number.
phone_number search will be exact search with & without country code while others will be containing & case-insensitive search.
So, I wrote the below code.
User.aggregate(
[
{
"$redact": {
"$cond": [
{
"$and": [
{
"$match": {
type: req.body.type,
email_address: new RegExp((req.body.email_address || req.body.any || '').toLowerCase(), "i"),
"firstname": new RegExp((req.body.firstname || req.body.any || '').toLowerCase(), "i") ,
"lastname": new RegExp((req.body.lastname || req.body.any || '').toLowerCase(), "i")
}
},
{
"$or": [
{
"$setIsSubset": [
[
{ "$substr": [ "$phone_number.local_number", 0, -1 ] }
],
[req.body.phone_number, req.body.any]
]
},
{
"$setIsSubset": [
[
{
"$concat": [
{ "$substr": [ "$phone_number.country_code", 0, -1 ] },
{ "$substr": [ "$phone_number.local_number", 0, -1 ] }
]
}
],
[req.body.phone_number, req.body.any]
]
},
{}
]
}
]
},
"$$KEEP",
"$$PRUNE"
]
}
}
],
function(err, users) {
if (err) {
return res.json({ success: false, err: err });
}
res.json({ success: true, users: users });
}
);
But when I run this code, I get "invalid operator '$match'" error.
If I remove $match, it evaluate req.body values as expression instead of value and emit "FieldPath 'abc' doesn't start with $" kind error.
So, I hope to get help how to solve this problem and search by conditions.
Please help me !!!
Move the $match outside $redact as it's an independent pipeline stage, it will provide the initial filter with the regex that can otherwise be invalid within the $redact pipeline:
User.aggregate([
{
"$match": {
"type": req.body.type,
"email_address": new RegExp((req.body.email_address || req.body.any || '').toLowerCase(), "i"),
"firstname": new RegExp((req.body.firstname || req.body.any || '').toLowerCase(), "i") ,
"lastname": new RegExp((req.body.lastname || req.body.any || '').toLowerCase(), "i")
}
},
{
"$redact": {
"$cond": [
{
"$or": [
{
"$setIsSubset": [
[ { "$substr": [ "$phone_number.local_number", 0, -1 ] } ],
[req.body.phone_number, req.body.any]
]
},
{
"$setIsSubset": [
[
{
"$concat": [
{ "$substr": [ "$phone_number.country_code", 0, -1 ] },
{ "$substr": [ "$phone_number.local_number", 0, -1 ] }
]
}
],
[req.body.phone_number, req.body.any]
]
}
]
},
"$$KEEP",
"$$PRUNE"
]
}
}
], function(err, users) {
if (err) {
return res.json({ success: false, err: err });
}
res.json({ success: true, users: users });
}
);