MongoDB aggregate. Create new groups for non-existing items - mongodb

My collection of documents contains information about users, their sessions and CRUD operations they performed during these sessions:
{
user_id: '1',
sessions: [
{
actions: [
{
type: 'create',
created_at: ISODate('2020-01-01T00:00:00'),
},
{
type: 'read',
created_at: ISODate('2022-01-01T00:00:00'),
},
{
type: 'read',
created_at: ISODate('2021-01-01T00:00:00'),
}
],
}
]
}
I need to get a summary for each user, which includes the amount of CRUD operations and the date of the last one:
{
user_id: '1',
actions: [
{
type: 'create',
last: ISODate('2020-01-01T00:00:00'),
count: 1,
},
{
type: 'read',
last: ISODate('2022-01-01T00:00:00'),
count: 2,
},
// Problematic part:
{
type: 'update',
last: null,
count: 0,
},
{
type: 'delete',
last: null,
count: 0,
},
]
}
I came up with this solution:
db.users.aggregate([
{$unwind:'$sessions'},
{$unwind:'$sessions.actions'},
{
$group:{
_id:{user_id:'$user_id', type:'$sessions.actions.type'},
last:{$max:'$sessions.actions.created_at'},
count:{$sum:1},
}
},
{
$group:{
_id:{user_id:'$_id.user_id'},
actions:{$push:{type:'$_id.type', last:'$last', count:'$count'}}
}
},
{
$project:{
_id:0,
user_id: '$_id.user_id',
actions: '$actions'
}
}
])
The problem here is that I cannot figure out, how can I add missing actions, like in 'update' and 'delete' in the example above

Try this,
db.collection.aggregate([
{
$unwind: "$sessions"
},
{
$unwind: "$sessions.actions"
},
{
$group: {
_id: {
user_id: "$user_id",
type: "$sessions.actions.type"
},
last: {
$max: "$sessions.actions.created_at"
},
count: {
$sum: 1
},
}
},
{
$group: {
_id: {
user_id: "$_id.user_id"
},
actions: {
$push: {
type: "$_id.type",
last: "$last",
count: "$count"
}
}
}
},
{
$project: {
_id: 0,
user_id: "$_id.user_id",
actions: {
"$function": {
"body": "function(doc) { const ops = {read:0, delete:0, update: 0, create: 0}; const actions = doc.actions; actions.forEach(action => { ops[action.type] = 1 }); Object.keys(ops).filter(key => ops[key] === 0).forEach(key => actions.push({count: 0, last: null, type: key})); return actions }",
"args": [
"$$ROOT"
],
"lang": "js"
}
},
}
},
])
Here, we use $function and provide a small JS function to populate the missing entries.
Playground link.

Related

total count from multiple group aggregations

I'm new to mongodb and I've been tasked with building an API capable of returning a total count of objects returned by a previously made aggregation pipeline. The pipeline contains multiple groupings and counts, and I'm wondering how I can tweak the current pipeline to return a total count of objects returned.
function projectionPipeline(islifecycle, pipeline) {
if (islifecycle) {
pipeline.push(
{
$project: {
state: '$state',
_id: 0,
lifecycle: {
$filter: {
input: '$classification',
as: 'lifecycle',
cond: {
$or: [
{
$eq: [
'$$lifecycle.name',
'Creation Lifecycle Status',
],
},
],
},
},
},
},
},
{
$project: {
state: '$state',
lifecycle: '$lifecycle.value',
},
},
{
$group: {
_id: {
state: '$state',
lifecycle: '$lifecycle',
},
counts: { $sum: 1 },
},
},
{
$group: {
_id: '$_id.state',
lifecycles: {
$push: {
lifecycle: '$_id.lifecycle',
lifecycleCount: '$counts',
},
},
count: { $sum: '$counts' },
},
},
{
$project: {
count: 1,
state: '$_id',
lifecycles: '$lifecycles',
_id: 0,
},
},
);
} else {
pipeline.push(
{
$group: {
_id: '$state',
counts: {
$sum: 1,
},
},
},
{
$project: {
counts: 1,
value: '$_id',
_id: 0,
},
},
);
}
}

Getting `[ [Object] ]` as nested array of object response on MongoDB Aggregation query

I'm trying to do an aggregation on two collections that has a linkage between them, and I need to access information in an array of objects in one of those collections.
Here are the schemas:
User Schema:
{
_id: ObjectId,
username: String,
password: String,
associatedEvents: [
{
event_id: ObjectId,
isCreator: boolean,
access_level: String,
}
]
}
Event Schema:
{
_id: ObjectId,
title: String,
associated_users: [
{
user_id: ObjectId
}
]
}
I'm attempting to get the users associated to an event for a specific user, and then get their access level information. Here's the aggregation I have:
const eventsJoined = await Event.aggregate([
{
$match: {
$expr: { $in: [id, "$associatedUserIds"] },
},
},
{
$lookup: {
from: "users",
localField: "associatedUserIds",
foreignField: "_id",
as: "user_info",
},
},
{ $unwind: "$user_info" },
{
$unwind: {
path: "$user_info.associatedEvents",
preserveNullAndEmptyArrays: true,
},
},
{
$group: {
_id: "$_id",
title: { $first: "$title" },
description: { $first: "$description" },
startDate: { $first: "$startdate" },
userInfo: { $first: "$user_info" },
usersAssociatedEvents: { $push: "$user_info.associatedEvents" },
},
},
{
$project: {
title: 1,
description: 1,
startDate: 1,
userInfo: 1,
usersAssociatedEvents: "$usersAssociatedEvents",
},
},
]);
And this is the result I'm getting:
[
{
_id: 609d5ad1ef4cdbeb32987739,
title: 'hello',
description: 'desc',
startDate: null,
usersAssociatedEvents: [ [Object] ]
}
]
As you can see, the query is already aggregating the correct data. But the last thing that's tripping me up is the fact that the aggregation is [ [Object] ] for usersAssociatedEvents instead of the actual contents of the object. Any idea on why that would be?

How can i do with MonboDB a vlookup between two subdocuments of different collections?

I have two collections : List and ListSnapshot(taken day by day) .
-List
{
_id: { $oid: "5f0d71fc9037e9858c96f131" },
list: [
{
_id: { $oid: "5f0d71fc9037e9858c96f132" },
name: "Item1",
id: "I2WQDFU7Z8C15E",
link: "https://www.ecomm1.heroku.com",
price: 44.99,
},
{
_id: { $oid: "5f0d71fc9037e9858c96f133" },
name: "Item2",
id: "I194JQPIWR0CY5",
link: "https://www.ecomm1.heroku.com",
price: 30.99,
},
],
creationDate: { $date: "2020-07-14T08:51:01.443Z" },
user: "713cdd45df7a6b979643b0a6f4d5be52bef1ea48e7c26088d88c50ac06c1b155",
id: "366c83ffc8be98f4de043629b5dcacb607d80f560cdd5741927c7d5a80513b0f",
__v: 0,
}
-ListSnapshot
{
_id: { $oid: "5f0d824e7e777e44d8115ddc" },
list: [
{
_id: { $oid: "5f0d824e7e777e44d8115ddd" },
name: "Item1",
id: "I2WQDFU7Z8C15E",
price: 54.99,
},
{
_id: { $oid: "5f0d824e7e777e44d8115dde" },
name: "Item2",
id: "I194JQPIWR0CY5",
price: 10.99,
},
],
date: { $date: "2020-07-14T10:00:42.744Z" },
user: "713cdd45df7a6b979643b0a6f4d5be52bef1ea48e7c26088d88c50ac06c1b155",
id: "366c83ffc8be98f4de043629b5dcacb607d80f560cdd5741927c7d5a80513b0f",
__v: 0,
}
I do an aggregation with a match on "List" id and then i group by list.id to calculate some values likes (avg,max,etc..) for each item. I want to add the link field of "List" to my result.
await collection.aggregate(
[
{ $match: { [filter]: group } },
{ $unwind: "$list" },
{ $sort: { date: 1 } },
{
$group: {
_id: { id: "$list.id", name: "$list.name" },
ossNumb: { $sum: 1 },
avgValue: { $avg: "$list.price" },
maxValue: { $max: "$list.price" },
minValue: { $min: "$list.price" },
startValue: { $first: "$list.price" },
lastValue: { $last: "$list.price" },
dev: { $stdDevPop: "$list.price" },
},
},
],
async function (err, queryRes) {
if (err) {
throw err;
return err;
}
const queryResToArray = await queryRes.toArray();
resolve(queryResToArray);
}
);
This is the current output:
{"_id":{"id":"I1PF32XECEDXKQ","name":"Items1"},"avgValue":29.99,"dev":0,"lastValue":"29.99","maxValue":"29.99","minValue":"29.99","ossNumb":1,"startValue":29.99}
I need to add link from "List"

Querying nested document structure in mongoDb

I am trying to query below document structure
{
_id: '7ja9sjkadja',
parent: {
parentName: 'Super',
firstGenerations: [
{
name: 'First Generation',
secondGenerations: [
{
name: 'Second Generation 1',
value: 'Married'
},
{
name: 'Second Generation 2',
value: 'Single'
},
{
name: 'Second Generation 3',
value: 'Single'
}
]
}
]
}
}
Expected output:
{
firstGenerationName: 'First Generation',
totalCount: 3
values: [
{
categoryName: 'Married (1)',
count: 1,
firstGenerationName: 'First Generation'
},
{
categoryName: 'Single (2)',
count: 2,
firstGenerationName: 'First Generation'
}
]
}
Query tried by me:
db.generations.aggregrate([
{ $project: { 'firstGenerations': '$parent.firstGenerations'} },
{ $unwind: '$firstGenerations'},
{ $unwind: '$firstGenerations.secondGenerations'}
{
$group: {
_id: '$_id',
count: { 'sum': '$secondGenerations.value' },
firstGenerationName: { $first: '$firstGenerations.name'}
}
}
])
I am able to unwind properly but not able to club group functionality by taking one value from parent array and count from second array.
Any help will be appreciated
Basically you need to run $group twice to get inner counts first:
db.collection.aggregate([
{
$unwind: "$parent"
},
{
$unwind: "$parent.firstGenerations"
},
{
$unwind: "$parent.firstGenerations.secondGenerations"
},
{
$group: {
_id: {
fgName: "$parent.firstGenerations.name",
sgValue: "$parent.firstGenerations.secondGenerations.value"
},
count: { $sum: 1 }
}
},
{
$group: {
_id: "$_id.fgName",
values: {
$push: {
categoryName: { $concat: [ "$_id.sgValue", " (", { $toString: "$count" }, ")" ] },
count: "$count",
firstGenerationName: "$_id.fgName"
}
}
}
},
{
$project: {
_id: 0,
firstGenerationName: "$_id",
values: 1,
totalCount: { $sum: "$values.count" }
}
}
])
Mongo Playground

Do an aggregate with a populate

I'm having troubles with the following. I wonder if it's possible to do it with a single query.
So I have the following model :
const Analytics = new Schema({
createdAt: {
type: Date,
default: Moment(new Date()).format('YYYY-MM-DD')
},
loginTrack: [
{
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Users',
}
}
]
}, { collection: 'analytics' });
And the user model :
const UserSchema = new mongoose.Schema(
{
nickname: {
type: String,
required: true,
unique: true
},
instance: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Instances',
default: null
}}, {collection: 'users'});
I want to get the connected users for a specific instance at a specific date.
AnalyticsModel.aggregate([
{
$match: {
createdAt: { "$gte": moment(args.startDate).format('YYYY-MM-DD'), "$lt": moment(args.endDate).format('YYYY-MM-DD')}
}
},
{
"$project": {
users: { $size: "$loginTrack" },
"createdAt": 1,
"_id": 0
}
}, {
"$group": {
"_id": "$createdAt",
"count": { "$sum": "$users" }
}
}
This gets me
[ { _id: '2019-02-11', count: 3 },
{ _id: '2019-02-08', count: 6 },
{ _id: '2019-02-07', count: 19 },
{ _id: '2019-02-06', count: 16 } ]
The results expected will be the same but I want to filter on users that belongs to a specific instance
Is it possible to do it with a single query or I need to do a populate first before the aggregation ?
UPDATE
I did some progress on it, I needed to add a lookup and I think it's ok :
AnalyticsModel.aggregate([
{"$unwind": "$loginTrack"},
{
$lookup:
{
from: 'users',
localField:'loginTrack.user_id',
foreignField: '_id',
as: '_users'
}
},
{
$match: {
createdAt: { "$gte": new Date(args.startDate), "$lt": new Date(args.endDate)}
}
},
{
$project: {
_users: {
$filter: {
input: '$_users',
as: 'item',
cond: {
$and: [
{ $eq: ["$$item.instance", new ObjectId(args.instance_id)] }
]
}
}
},
"createdAt": 1,
"_id": 0
}
},
{
"$group": {
"_id": "$createdAt",
"count": { "$sum": { "$size": "$_users" } }
}
}
Also the dates were in string in the model.
The output is now :
[ { _id: 2019-02-11T00:00:00.000Z, count: 2 } ]