I have two collections. Lets call one baskets and the other one fruits.
In baskets we have the following document:
[{
basket_name: "John's Basket",
items_in_basket: [
{
fruit_id: 1,
comment: "Delicious!"
},
{
fruit_id: 2,
comment: "I did not like this"
}
]
}]
And in fruits we have the following documents:
[{
_id: 1,
fruit_name: "Strawberry",
color: "Red"
},
{
_id: 2,
fruit_name: "Watermelon",
color: "Green"
}]
How do I get information on each fruit in John's Basket?
The result should look like this:
[{
fruit_id: 1,
comment: "Delicious!",
fruit_name: "Strawberry",
color: "Red"
},
{
fruit_id: 2,
comment: "I did not like this",
fruit_name: "Watermelon",
color: "Green"
}]
There's no "join" in MongoDB. You either could:
consider using a MapReduce function to create a new structure that contains the merged data
write the code necessary to fetch each fruit instance on demand and merge it in your client code with a basket document.
denormalize the data and include the details for each fruit in the basket document. This poses it's own set of issues as data is duplicated and updates to a particular fruit would then need to be made to every usage in the collection.
Both have their pros and cons.
You might find this Q/A helpful, and also this documentation for MongoDB.
this is no longer true.
Since version 3.2, MongoDB added the $lookup command.
https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/
db.orders.insert([
{ "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 },
{ "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 },
{ "_id" : 3 }
])
db.inventory.insert([
{ "_id" : 1, "sku" : "almonds", description: "product 1", "instock" : 120 },
{ "_id" : 2, "sku" : "bread", description: "product 2", "instock" : 80 },
{ "_id" : 3, "sku" : "cashews", description: "product 3", "instock" : 60 },
{ "_id" : 4, "sku" : "pecans", description: "product 4", "instock" : 70 },
{ "_id" : 5, "sku" : null, description: "Incomplete" },
{ "_id" : 6 }
])
db.orders.aggregate([
{
$lookup:
{
from: "inventory",
localField: "item",
foreignField: "sku",
as: "inventory_docs"
}
}
])
returns:
{
"_id" : 1,
"item" : "almonds",
"price" : 12,
"quantity" : 2,
"inventory_docs" : [
{ "_id" : 1, "sku" : "almonds", "description" : "product 1", "instock" : 120 }
]
}
{
"_id" : 2,
"item" : "pecans",
"price" : 20,
"quantity" : 1,
"inventory_docs" : [
{ "_id" : 4, "sku" : "pecans", "description" : "product 4", "instock" : 70 }
]
}
{
"_id" : 3,
"inventory_docs" : [
{ "_id" : 5, "sku" : null, "description" : "Incomplete" },
{ "_id" : 6 }
]
}
Related
FULL DISCLOSURE: I'm a MongoDB noob
I'm dealing with a legacy DB structure. A part of my MongoDB looks like this currently:
Events (_id, name (string), ...)
Orders (_id, eventId (as string), products (array of {prodIdentifier (string), quantity (number)}), customer_ID (string), signee (string), sign_time (date), ...)
Products (_id, prodIdentifier (string), price (number), sku (string), ...)
The relations are as follows:
Event 1..N Orders (via eventId)
Orders 1..N Products (via products array)
I need to query in a way that given an eventId, I return
Order ID Customer Name (can be a cascade request / premeditated
by frontend), Product SKU, Product Name, Quantity,
Value (quantity * price), Signee Name, Sign time
Mind that, my interface requires filters and sorts on all of the above fields along with limit and offset for pagination, to reduce query time, fast UI, etc.
I could use populate on orders, but how am I supposed to honor the limit and offset via mongoose then. I'm wondering if I should make a view, in which case how should I flatten it to send/receive a list that honors the limit and offset.
Or will it have to be a very manual, step-by-step build of the resulting list?
UPDATE:
Sample data in the DB:
Event Object:
{
"_id" : ObjectId("6218b9266487367ba1c20258"),
"name" : "XYZ",
"createdAt" : ISODate("2022-02-03T13:25:43.814+0000"),
"updatedAt" : ISODate("2022-02-14T09:34:47.819+0000"),
...
}
Order(s):
[
{
"_id" : ObjectId("613ae653d0112f6b49fdd437"),
"orderItems" : [
{
"quantity" : NumberInt(2),
"productCode" : "VEO001",
},
{
"quantity" : NumberInt(2),
"productCode" : "VEO002",
},
{
"quantity" : NumberInt(1),
"productCode" : "VEO003",
}
],
"orderCode" : "1000",
"customerCode" : "Customer 1",
"createdAt" : ISODate("2021-09-10T05:00:03.496+0000"),
"updatedAt" : ISODate("2022-02-08T10:06:42.255+0000"),
"eventId" : "6218b9266487367ba1c20258"
}
]
Products:
[
{
"_id" : ObjectId("604206685f25b8560a1cd48d"),
"Product name" : "ABC",
"createdAt" : ISODate("2021-03-05T10:22:32.085+0000"),
"tag" : "VEO001",
"updatedAt" : ISODate("2022-03-28T07:29:21.939+0000"),
"Product Price" : NumberInt(0),
"photo" : {
"_id" : ObjectId("6042071a5f25b8560a1cd4a9"),
"key" : "e8c9a085-4e8d-4ac4-84e9-bb0a83a59145",
"name" : "Screenshot 2021-03-05 at 11.24.50.png"
},
"name" : "ABC",
"_costprice" : NumberInt(12),
"_sku" : "SKUVEO001",
},
{
"_id" : ObjectId("604206685f25b8560a1cd48a"),
"Product name" : "DEF",
"createdAt" : ISODate("2021-03-05T10:22:32.085+0000"),
"tag" : "VEO002",
"updatedAt" : ISODate("2022-03-28T07:29:21.939+0000"),
"Product Price" : NumberInt(0),
"photo" : {
"_id" : ObjectId("6042071a5f25b8560a1cd4a9"),
"key" : "e8c9a085-4e8d-4ac4-84e9-bb0a83a59145",
"name" : "Screenshot 2021-03-05 at 11.24.50.png"
},
"name" : "DEF",
"_costprice" : NumberInt(13),
"_sku" : "SKUVEO002",
},
{
"_id" : ObjectId("604206685f25b8560a1cd48a"),
"Product name" : "GHI",
"createdAt" : ISODate("2021-03-05T10:22:32.085+0000"),
"tag" : "VEO003",
"updatedAt" : ISODate("2022-03-28T07:29:21.939+0000"),
"Product Price" : NumberInt(0),
"photo" : {
"_id" : ObjectId("6042071a5f25b8560a1cd4a9"),
"key" : "e8c9a085-4e8d-4ac4-84e9-bb0a83a59145",
"name" : "Screenshot 2021-03-05 at 11.24.50.png"
},
"name" : "GHI",
"_costprice" : NumberInt(13),
"_sku" : "SKUVEO003",
},
]
Expected output:
You can do something like:
db.orders.aggregate([
{$match: {eventId: "6218b9266487367ba1c20258"}},
{
$lookup: {
from: "products",
localField: "orderItems.productCode",
foreignField: "tag",
as: "orderItemsB"
}
},
{
"$addFields": {
"orderItems": {
"$map": {
"input": "$orderItemsB",
"in": {
"$mergeObjects": [
"$$this",
{
"$arrayElemAt": [
"$orderItems",
{"$indexOfArray": ["$orderItems.productCode", "$$this.tag"]}
]
}
]
}
}
},
orderItemsB: 0
}
},
{
$unset: "orderItemsB"
},
{
$lookup: {
from: "events",
let: {eventId: "$eventId"},
pipeline: [
{
$match: {$expr: {$eq: [{$toString: "$_id"}, "$$eventId"]}}
}
],
as: "event"
}
},
{
$set: {event: {"$arrayElemAt": ["$event", 0]}}
},
{$unwind: "$orderItems"}
])
As you can see on this playground example. This will give you a document for each product of the order with all the data.
I tried have bulkWrite query for update multiple and different documents in a single time, customer order multiple products at the time updating the product quantity details. I found bulkWrite updateOne query. this bulk array updates at a single time.
Code :
while upding the key value update: { $set: { 'colorName' : null} } working fine. but using nested array key value update: { $set: { 'sizes.$.qty' : data.qty} } not working.
poductModel.bulkWrite(arrayValue.map((data) => ({
updateOne: {
filter: { _id: data.productQtyDetailsId, 'sizes.name' : data.sizeName },
update: { $set: { 'sizes.$.qty' : data.qty} }
//working fine
//update: { $set: { 'colorName' : null} }
}
}))).then(err,result => {
})
JSON data:
[
{
"qty": 8,
"productId": "5d31567ea23d120f087a9aaf",
"productQtyDetailsId": "5d316373b356873504e78be7",
"sizeName": "4",
"colorName": "green",
},
{
"qty": 5,
"productId": "5d31567ea23d120f087a9aaf",
"productQtyDetailsId": "5d31567ea23d120f087a9ab1",
"sizeName": "4",
"colorName": "blue",
}
]
Mongo DB data :
[
{
"_id" : ObjectId("5d316373b356873504e78be7"),
"colorName" : "green",
"productId" : ObjectId("5d31567ea23d120f087a9aaf"),
"sizes" : [
{
"name" : "4",
"qty" : 5.0,
"price" : 1500.0
},
{
"name" : "5",
"qty" : 6.0,
"price" : 1600.0
},
{
"name" : "6",
"qty" : 7.0,
"price" : 1700.0
}
]
}
{
"_id" : ObjectId("5d31567ea23d120f087a9ab1"),
"colorName" : "blue",
"productId" : ObjectId("5d31567ea23d120f087a9aaf"),
"sizes" : [
{
"name" : "4",
"qty" : 5.0,
"price" : 1500.0
},
{
"name" : "5",
"qty" : 6.0,
"price" : 1600.0
},
{
"name" : "6",
"qty" : 7.0,
"price" : 1700.0
}
]
},
...
]
I had the same error trying to update a field not present in model schema.
The issue can be solved by using <model>.collection.bulkWrite or by adding the field to schema (at least both these actions did work in my case).
I've a row, which contains data like:
{
"_id" : ObjectId("5bcef76b0c9a4c194cf6d0a7"),
"userid" : ObjectId("5bc5ae4355418805b8caabb3"),
"transactionid" : "ch_1DON67EzCa9AoDtY51kialzs",
"adminid" : [
"5b5af339bc69c511816e8a2f",
"5b87948d97b752099c086708"
],
"amount" : 3220,
"ispaid" : true,
"products" : [
{
"productid" : ObjectId("5bceba35003c87043997a1d4"),
"quantity" : 2,
"price" : 200,
"type" : "product",
"isCanceled" : false,
"isDeliverd" : false,
"createdby" : "schooladmin",
"userid" : ObjectId("5b87948d97b752099c086708"),
"isReadyToPickup" : false,
"name" : "The Second Product",
"description" : " "
},
{
"productid" : ObjectId("5bc5b2df55418805b8caabbd"),
"quantity" : 2,
"price" : 100,
"type" : "product",
"isCanceled" : false,
"isDeliverd" : false,
"createdby" : "superadmin",
"userid" : ObjectId("5b5af339bc69c511816e8a2f")
},
{
"productid" : ObjectId("5bc5bc5fe84c3d028aaa269c"),
"quantity" : 2,
"price" : 100,
"type" : "product",
"isCanceled" : false,
"isDeliverd" : false,
"createdby" : "superadmin",
"userid" : ObjectId("5b5af339bc69c511816e8a2f")
}
],
"paymentUsing" : "card",
"cardBrand" : "Visa",
"country" : "US",
"paymentDate" : "2018-10-23T10:26:51.856Z"
}
I want to perform search on products object's all element. And if any of them match, that entire object will be store in array variable.
Suppose, I try to search The Second Product string from name object
of products array. then it would be give me the all element. like
[
{
"productid" : ObjectId("5bceba35003c87043997a1d4"),
"quantity" : 2,
"price" : 200,
"type" : "product",
"isCanceled" : false,
"isDeliverd" : false,
"createdby" : "schooladmin",
"userid" : ObjectId("5b87948d97b752099c086708"),
"isReadyToPickup" : false,
"name" : "The Second Product",
"description" : " "
}
]
And if two or more elements founds then it will return it array accordingly.
Please suggest me the solution.
Thank You in Advance. :)
Something like this:
db.collection.find({products: {$elemMatch: {name:'The Second Product'}}})
Using aggregation pipeline we can get the desired result.
Approach 1:
A simpler approach if we don't have nested arrays
$filter is used within $project to get the desired result
db.collection_name.aggregate([
{
$project: {
products: {
$filter: {
input: "$products",
as: "product",
cond: {
$eq: [
"$$product.name",
"The Second Product"
]
}
}
}
}
}
])
Approach 2:
$unwind to unwind the products array
$match to retrieve the matching documents
$project to project only the required output elements
db.collection_name.aggregate([
{ $unwind: "$products" },
{ $match:{"products.name":"The Second Product"} },
{ $project:{products:1}}
])
Output of the above queries(Approach 1 or Approach 2)
{
"_id" : ObjectId("5bcef76b0c9a4c194cf6d0a7"),
"products" : {
"productid" : ObjectId("5bceba35003c87043997a1d4"),
"quantity" : 2,
"price" : 200,
"type" : "product",
"isCanceled" : false,
"isDeliverd" : false,
"createdby" : "schooladmin",
"userid" : ObjectId("5b87948d97b752099c086708"),
"isReadyToPickup" : false,
"name" : "The Second Product",
"description" : " "
}
}
Can anyone help I am using $lookup for join in MongoDB to get all orders where the date is between date "X" and date "Y" and chef_id is "P". something is wrong in "date" part which is not giving data on the dates. but when I use it in single Find query it works fine and gives data between the dates. but it does not give data when I applied with $lookup for join.
Here is my query
Order.aggregate([{
"$lookup": {
"localField": "user_id",
"from": "users",
"foreignField": "_id",
"as": "order_data"
}
},
{
"$match": {
"$and": [
{ "chef_id": mongoose.Types.ObjectId(req.body.chef_id) },
{ "booking_datetime": { $gte: start_time,
$lte: end_time } }
]
}
}
], function(err, gettt) {
if (err) {
res.json({ 'message': "Error", 'status': false, 'data': err });
return false;
} else {
if (gettt.length != 0) {
res.json({ 'message': "Orders Data", 'status': true, 'data': gettt });
} else {
res.json({ 'message': "No Orders for this date", 'status': false, 'data': gettt });
}
}
});
Can anyone help me out.
my collection is here "Order" collection
{
"_id" : ObjectId("5a4256cc3f76bc45065021fc"),
"order_status" : 2,
"total_order_amount" : "160",
"booking_datetime" : ISODate("2017-12-29T23:24:00.000Z"),
"customer_address" : "121/161, South Extension part",
"user_id" : ObjectId("5a3cb4a8a188f2074714f1de"),
"chef_id" : ObjectId("5a390b07f0b3563db59cb3ca"),
"updated_at" : ISODate("2017-12-26T14:03:56.742Z"),
"created_at" : ISODate("2017-12-26T14:03:56.342Z"),
"products" : [
{
"product_id" : "5a3a50fcefc0c972377c3012",
"product_name" : "sweet corn",
"quantity" : "12",
"_id" : ObjectId("5a4256cc3f76bc45065021fd"),
"updated_at" : ISODate("2017-12-26T14:03:56.736Z"),
"created_at" : ISODate("2017-12-26T14:03:56.736Z")
},
{
"product_id" : "5a3a5119efc0c972377c3013",
"product_name" : "chilly paneer",
"quantity" : "10",
"_id" : ObjectId("5a4256cc3f76bc45065021fe"),
"updated_at" : ISODate("2017-12-26T14:03:56.736Z"),
"created_at" : ISODate("2017-12-26T14:03:56.736Z")
},
{
"product_id" : "5a3a512cefc0c972377c3014",
"product_name" : "Gulab jamun",
"quantity" : "20",
"_id" : ObjectId("5a4256cc3f76bc45065021ff"),
"updated_at" : ISODate("2017-12-26T14:03:56.736Z"),
"created_at" : ISODate("2017-12-26T14:03:56.736Z")
},
{
"product_id" : "5a3a50fcefc0c972377c3012",
"product_name" : "ali baba",
"quantity" : "56",
"_id" : ObjectId("5a4256cc3f76bc4506502200"),
"updated_at" : ISODate("2017-12-26T14:03:56.736Z"),
"created_at" : ISODate("2017-12-26T14:03:56.736Z")
}
],
"__v" : 1
and here is my User Collection
{
"_id" : ObjectId("5a623f67eaa08537fe0dba02"),
"salt" : "73824ba53291740e15d26c300c997ce1436ac678299101171af74f4980433285",
"hash" : "8f78291ac737dac15f59f5438033a61de75282a3c671a8d0231406a8374adec140b4cb2dd30b852f05241c6f9900443906fafec22ad58c983dacaed8f9ef4f9039e72b748d9c63d924239aa40372923d824a9cc796079556c8bc5eb0b0f6b17e7fd4c35b8780c870d1b4b819e641e56ce2f88fb0a7fdfbfd91d15921e9b7441a7051523903b43b930f56057852e41ffdbdc044cc09b14ebaac77940576b483d58ff1e18c381d40a143abcd1a180ca208aac6a13eb5c819b97e7e5753bd6fc40fcc1e19b55cb816879b3fedbf187110e84149bad0918672bd2de49bc323a32f04dd0e55aded9a0157fd5eea7db645303eb4cf461e47ca905e1f196618814b88421a3cab9463dac01d5bf6aebcace6e4b1215c3cf07aaae1cac07c94dc28432d223407778f4c6b12b089e09d56a59b1f00084c727f06247c1799c1a8616c74693e2d7057a5026e3c02b9ef73bf867873508575a33fc1e956bd3c704c54e6cc38ffb22e7a04ade70db134ec87e9ed3f43a7273db115127470f8ca5d8def49ba47fe7852cdf0cbd3140b19d5fe358d29eb84519365eea6353fa34c7a6757fbd9ec2ba93eca802f21944da58cd72b5d0d7000f9fd6f231f0668b7e621117a18fcedf977515e181325a9210380e01892891fcc420a67cb5246688eb6e577fccb6d41e719b426fa20c4689af9a9485d0ae0cf026845de8b4f12c7277b9cc506b5e29224",
"email" : "eduardo.llano#geocampo.co",
"firstname" : "Pedro",
"lastname" : "Peez",
"dob" : "1980-01-19",
"phone" : "3185311158",
"gender" : "male",
"latitude" : "4.6936225",
"longitude" : "-74.0730777",
"address" : "Bogota",
"divice_token" : "dcd8cf3ceefc39b8",
"prossing_form" : "1",
"status" : true,
"role" : "chef",
"updated_at" : ISODate("2018-02-01T16:09:27.465Z"),
"created_at" : ISODate("2018-01-19T18:56:39.070Z"),
"products" : [
{
"product_name" : "Producto 1",
"product_price" : "100",
"discount" : "10",
"product_ingredients" : "Pepper",
"product_description" : "Nsjdjd jsjdjdjx",
"minimum_order" : "2",
"tags" : "Indian food",
"status" : "1",
"product_image0" : "https://s3-us-west-2.amazonaws.com/rafahoproject/a47df980-7221-4fde-97cd-977cb3dd1dcf.jpg",
"product_image1" : "https://s3-us-west-2.amazonaws.com/rafahoproject/3a282cae-028c-4d4a-9a22-4c2c43d440f1.jpg",
"product_image2" : "https://s3-us-west-2.amazonaws.com/rafahoproject/1ab4373e-ec63-43fe-9b94-7b85d289c7f9.jpg",
"_id" : ObjectId("5a6602aa135fae732d5ce3d7")
},
{
"product_name" : "Bandeja Paisa",
"product_price" : 100,
"discount" : 20,
"cuisine" : "Continental",
"minimum_order" : 5,
"cooking_time_at_chef_place" : 8,
"cooking_time_at_user_home" : 10,
"tags" : "Bandeja-Paisa,Bandeja,Paisa",
"status" : "1",
"product_image0" : "https://s3-us-west-2.amazonaws.com/rafahoproject/0252161e-0e2a-4682-b7ab-0dce355be794.jpg",
"_id" : ObjectId("5a71d268947536411def9b11")
},
{
"product_name" : "Fritanga",
"product_price" : 200,
"discount" : 30,
"cuisine" : "continental",
"minimum_order" : 6,
"cooking_time_at_chef_place" : 3,
"cooking_time_at_user_home" : 5,
"tags" : "Fritanga,platter-of-grilled-meats",
"status" : "1",
"product_image0" : "https://s3-us-west-2.amazonaws.com/rafahoproject/14c39e2f-4d02-4cd1-8aa7-2f7179b5ea0c.jpg",
"_id" : ObjectId("5a71d3c6947536411def9b12")
},
{
"product_name" : "Dominican Sancocho",
"product_price" : 300,
"discount" : 50,
"cuisine" : "continental",
"minimum_order" : 5,
"cooking_time_at_chef_place" : 1,
"cooking_time_at_user_home" : 2,
"tags" : "Dominican-Sancocho,Dominican,Sancocho",
"status" : "1",
"product_image0" : "https://s3-us-west-2.amazonaws.com/rafahoproject/40a701ff-f868-492d-bee1-d65004fff024.jpg",
"_id" : ObjectId("5a71d4da947536411def9b13")
},
{
"product_name" : "Sudado de Pollo",
"product_price" : 60,
"discount" : 10,
"cuisine" : "Continental",
"minimum_order" : 3,
"cooking_time_at_chef_place" : 3,
"cooking_time_at_user_home" : 2,
"tags" : "Sudado-de-Pollo,Sudado,de-Pollo",
"status" : "1",
"product_image0" : "https://s3-us-west-2.amazonaws.com/rafahoproject/544c2ceb-839b-4263-b482-e2262c228948.jpg",
"_id" : ObjectId("5a71d6e9947536411def9b14")
}
],
"loc" : {
"coordinates" : [
-74.0730777,
4.6936225
],
"type" : "Point"
},
"__v" : 9,
}
Your syntax looks good. I still suspect there is some data type mismatch or erroneous format. I have a similar aggregate which matches a date range, and I use new Date() to convert my strings to date.
if (typeof dateBeginning === "string" && dateBeginning != "" && typeof dateEnding === "string" && dateEnding != "" && dateEnding >= dateBeginning) {
query.date = { $gte: new Date(dateBeginning), $lte: new Date(dateEnding) };
}
Here is an SO question: Date query with ISODate in mongodb doesn't seem to work
which was resolved this way.
Also, Veeram is correct; you should put your $match first. That way, MongoDb will use any indexes on the match fields that are available.
/* 0 */
{
"_id" : ObjectId("5380c9e097632cee5b000007"),
"month" : "5",
"userid" : "53806aac12c75f4b51000001",
"__v" : 7,
"posts" : [{
"postid" : ObjectId("538185cae0c6b8666e000008"),
"ts" : ISODate("2014-05-25T05:55:22.976Z"),
"userid" : "53806aac12c75f4b51000001",
"name" : "BBB",
"text" : "b1",
}]
}
/* 1 */
{
"_id" : ObjectId("5380c80e97632cee5b000001"),
"month" : "5",
"userid" : "5380629ea3b31f864f000001",
"__v" : 24,
"posts" : [{
"postid" : ObjectId("538185b2e0c6b8666e000004"),
"ts" : ISODate("2014-05-25T05:54:58.703Z"),
"userid" : "5380629ea3b31f864f000001",
"name" : "AAA",
"text" : "a1",
}, {
"postid" : ObjectId("538185b7e0c6b8666e000006"),
"ts" : ISODate("2014-05-25T05:55:03.474Z"),
"userid" : "5380629ea3b31f864f000001",
"name" : "AAA",
"text" : "a2",
}, {
"postid" : ObjectId("538185d6e0c6b8666e00000a"),
"ts" : ISODate("2014-05-25T05:55:34.231Z"),
"userid" : "5380629ea3b31f864f000001",
"name" : "AAA",
"text" : "a3",
}]
}
This is my DATA.
I want to Sort This Data for 'Ts' ( Data ).
I want That Sorted List by 'posts.Ts' Like this..
name : AAA, text = a3
name : BBB, text = b1
name : AAA, text = a2
name : AAA, text = a1
but i Don't know How to query this. Please Talk To ME
This is my code in Node and mongoose.
db.collection('walls', function(err, collection) {
collection.find(function(err, data) {
collection.aggregate(
{$match: {userid:userid}},
{$project: {posts: 1,_id:0}},
{$sort:{'posts.ts':1}},
{$unwind: "$posts"}
)}
...
You are onto the right principles but you have pipeline stages the wrong way around. You need to $unwind the arrays before you sort:
db.collection.aggregate([
{ "$match": { "userId": userId" } },
{ "$project": { "_id": 0, "posts": 1 } },
{ "$unwind": "$posts" },
{ "$sort": { "posts.ts": 1, "posts.name": 1 } }
])