MongoDB Aggregate not working as expected - javascript

I am trying to find all votes associated with a comment after a graphlookup but it isn't working.
I'm trying to get the comments/votes recursively of a thread.
I have 3 schemas:
comments
- _id
- points
- content
- userId
- parentId
- parentThreadId
threads
- _id
- upvotes
- downvotes
- title
- content
- userName
votes
- _id
- vote
- commentId
- userId
The problem is that a comment has two keys. One being 'parentId' and the other one 'parentThreadId'. parentId is only set if the comment is a child of another comment, and parentThreadId is set whenever it is a top-level comment under a thread.
Using this code, I only receive the votes associated with top-level comment (thus with a parentThreadId instead of the votes of all comments).
const threadId = req.params.id;
const ObjectId = mongoose.Types.ObjectId;
Thread.aggregate([
{
$match: {_id : ObjectId(threadId)}
},
{
$lookup:
{
from: 'comments',
as: 'comments',
pipeline: [
{
$match: {parentThreadId : ObjectId(threadId)}
},
{
$graphLookup: {
from: "comments",
startWith: "$_id",
connectFromField: "_id",
connectToField: "parentId",
as: "children",
depthField: "level",
}
},
{
$lookup :
{
from: 'votes',
localField: '_id',
foreignField: 'commentId',
as: 'votes'
}
},
]
}
}
]).
Does anybody have a clue as to how to achieve this?

I get into similiar problem not a long ago, you have here recursive relationship between documents in one collection, and sadly, only way to get all tree of comments is recursievly make requests for each child level of comments.
Correct way to handle such stuff is to save all tree in topmost document, so you have thread, you need to save there in one property whole tree of comments, this way moving responsibility to manage it to js (where it is way easier to walk over such tree object, update it etc.)

Related

Sequelize and defaultScope with Models associated

I am using Sequelize with Express, and Node js and I am trying to define defaultScope for Models.
Card and Tag have a Many To Many association.
Here are Models definitions and addScope
// Models Associations
// ONE TO MANY
List.hasMany(Card, {
as: "cards",
});
Card.belongsTo(List, {
as: "list",
});
// MANY TO MANY
Card.belongsToMany(Tag, {
as: "tags",
through: "card_has_tag",
updatedAt: false,
});
Tag.belongsToMany(Card, {
as: "cards",
through: "card_has_tag",
updatedAt: false,
});
// SCOPES
Card.addScope("defaultScope", {
include: {
association: "tags",
},
});
List.addScope("defaultScope", {
include: {
association: "cards",
include: "tags",
},
});
// What I would like to implement
// If I comment lines below => List and Card queries are working
Tag.addScope("defaultScope", {
include: {
association: "cards",
},
});
I would like to print by default all related infos with associated relations.
I want to get this info when I execute a sequelize query for each model.
LISTS with associated :
cards
tags
CARDS with associated:
tags
TAGS with associated :
cards
I manage to get 1 & 2, but when I add Tag.addScopenothing is working anymore.
When I change defaultScope by another string by defining a scope all (for example) , and when I use model.scope("all").findAll(), this is working, but it is not what I would like to do becaue I want to use defaultScope to have a default behavior so I don't have to specify scope in queries command like (findAll...)
Is there a way I can do that ?
The way you are trying to set it up results in an endless recursion, you simply can't have it like that.
If you set it up like that and query Card it will include Tag which will include Card which will include Tag and so on until you get Maximum call stack size exceeded.
There is a workaround you can use, which is to add another scope which includes nothing, then specify that scope for the model in the defaultScope.
Tag.addScope("noInclude", {});
Card.addScope("noInclude", {});
Tag.addScope("defaultScope", {
include: [
{
model: Card.scope("noInclude"),
as: "cards"
}
]
});
Card.addScope("defaultScope", {
include: [
{
model: Tag.scope("noInclude"),
as: "cards"
}
]
});
This should give you the desired behaviour.

Sequelize - Can't limit properly query with includes, using hasMany and belongsToMany associations

Issue explanation
I want to do a query with pagination that limits to 12 lines each query, to be more specific, that limits to 12 Batches each query. Actually the amount of lines get smaller because a belongsToMany association with a join table i got in this query. The join order to this query is: Offer > hasMany > Batch > belongsToMany > BatchFile > belongsTo > File. The problem is, when i have many registries in the File as 'gallery' association, it brings me duplicated registries of batch.
The query i'm trying to do
const { id } = req.params;
const { page = 1 } = req.query;
const offer = await Offer.findAndCountAll({
attributes: [ 'id', 'name', 'canceled_at'],
where: { id, canceled_at: null },
order: [['offer_batches', 'name', 'ASC']],
include: [
{
/* hasMany association */
model: Batch,
as: 'offer_batches',
attributes: [ 'id', 'name'],
include: [
{ /* Other includes... */ },
{
/* belongsToMany association */
model: File,
as: 'gallery',
attributes: ['id', 'path', 'url'],
},
],
},
],
subQuery: false,
limit: 12,
offset: (page - 1) * 12,
});
Model associations
Offer model
this.hasMany(Batch, { foreignKey: 'offer_id', as: 'offer_batches' });
Batch model
this.belongsToMany(File, { through: models.BatchFile, as: 'gallery' });
BatchFile model (join table)
this.belongsTo(Batch, { foreignKey: 'batch_id', as: 'batch' });
this.belongsTo(File, { foreignKey: 'file_id', as: 'file' });
What i already tried
Giving duplicating: false option to any included Model doesn't worked;
Giving separate: true to the Batch model doesn't worked too;
Giving required: true option to any included model doesn't worked too;
If i remove subQuery: false it doesn't respect the setted limit of lines, and i already tried with all of the above combinations;
I thought sequelize would deal with this situation without problems, maybe i'm doint something wrong.
If helps, here's the raw generated SQL:
SELECT
"Offer"."id",
"Offer"."name",
"Offer"."canceled_at",
"offer_batches"."id"
AS "offer_batches.id", "offer_batches"."name"
AS "offer_batches.name", "offer_batches->gallery"."id"
AS "offer_batches.gallery.id", "offer_batches->gallery"."path"
AS "offer_batches.gallery.path", "offer_batches->gallery->BatchFile"."created_at"
AS "offer_batches.gallery.BatchFile.createdAt", "offer_batches->gallery->BatchFile"."updated_at"
AS "offer_batches.gallery.BatchFile.updatedAt", "offer_batches->gallery->BatchFile"."file_id"
AS "offer_batches.gallery.BatchFile.FileId", "offer_batches->gallery->BatchFile"."batch_id"
AS "offer_batches.gallery.BatchFile.BatchId", "offer_batches->gallery->BatchFile"."batch_id"
AS "offer_batches.gallery.BatchFile.batch_id", "offer_batches->gallery->BatchFile"."file_id"
AS "offer_batches.gallery.BatchFile.file_id"
FROM "offer" AS "Offer"
LEFT OUTER JOIN "batch" AS "offer_batches" ON "Offer"."id" = "offer_batches"."offer_id"
LEFT OUTER JOIN (
"batch_file" AS "offer_batches->gallery->BatchFile"
INNER JOIN "file" AS "offer_batches->gallery"
ON "offer_batches->gallery"."id" = "offer_batches->gallery->BatchFile"."file_id"
)
ON "offer_batches"."id" = "offer_batches->gallery->BatchFile"."batch_id"
WHERE "Offer"."id" = '1' AND "Offer"."canceled_at" IS NULL
ORDER BY "offer_batches"."name"
ASC LIMIT 12 OFFSET 0;
Environment
Node: v14.18.0
package.json dependencies
pg: 8.7.1
pg-hstore: 2.3.4
sequelize: 6.9.0
Trying to find any solution on GitHub issues or stackoverflow, nothing solved this problem. Maybe i'm doing this query wrong, any help would be grateful and welcome :)
Well, i didn't found a quite solution for this, so i resolved to use Lazy loading for this situation instead of Eager loading, as mentioned on Sequelize Docs.
I splitted the query into two new queries.
First one, on the "master" model Offer, with a simple findOne():
const offer = await Offer.findOne({
[/* My attributes */],
where: { /* My conditions */ }
});
And a second one, selecting from model Batch, without subQuery: false option, because is not needed anymore.
const batches = await Batch.findAndCountAll({
attributes: [
'id',
'name',
],
where: { offer_id: offer.id },
order: [/* My ordenation */],
include: [
{
model: File,
as: 'gallery',
attributes: ['id', 'path', 'url'],
},
],
limit: 12,
offset: (page - 1) * 12,
});

How can i translate query to sequelize?

select reservation_datetime
from LectureReservation
Inner Join Lecture
On LectureReservation.lecture_id = Lecture.id
Where Lecture.mentor_id = 1
This is my query and I want to change it to sequelize like
if (req.params.id) {
LectureReservation
.findAll({
include: [{
model: Lecture,
where: { mentor_id: req.params.id },
}],
attributes: ['reservation_datetime'],
where: {
lecture_id: Lecture.id,
},
this.. I tried it so hard but can't find solution and my postman keep showing me
"name": "SequelizeEagerLoadingError"
this err..
plz help me to translate query to sequelize..!
Sequelize will do _outer join without required = true.
The errors you have received usually is from association problem.
Try set logging :console.log and check the raw query.

MongoDB - Comment Upvoting/Downvoting with Aggregation Pipeline

I'm trying to implement an upvote/downvote mechanism for comments (similar to the upvoting/downvoting mechanism found on reddit). I have a separate collection called commentReputation and the documents inside can look like this:
{
"_id" : ObjectId("5e5acb6d6034a879655c8819"),
"commentId" : ObjectId("5e5983102328a83d1a4b541f"),
"creationDate" : ISODate("2020-02-29T20:37:01.509Z"),
"upvotes" : [
ObjectId("5e5983102328a83d1a4b53e7"),
ObjectId("5e5983102328a83d1a4b53e4")
],
"downvotes" : [
ObjectId("5e5983102328a83d1a4b53e5")
]
}
In short: every comment will eventually have it's own CommentReputation document (the CommentReputation document should be created as soon as someone upvotes/downvotes a comment)
There are 2 case scenarios:
The collection is empty meaning that I need to create my very first CommentReputation document with a given commentId x. In some other part of the project I was using $setOnInsert with { upsert: true } but it seems (looking at the documentation) that the aggregation pipeline does not support $setOnInsert as for now. Is there another way to deal with this problem?
The document is there and the actuall upvoting should occur.
a) Both upvotes and downvotes arrays do not contain the userId that is trying to upvote thus it gets added to the upvotes array without any further actions
b) The upvotes array contains the userId that is trying to upvote the comment as a result the userId should be REMOVED from the upvotes array. (the user already had this comment upvoted and clicked a second time the upvote button which cancels out the upvote)
c) The downvotes array contains the userId. In this case the userId should be removed from downvotes and added to upvotes
I'm trying to accomplish the above logic with the updateOne method and a aggreagtion pipeline however I'm not sure if this is even possible.
What I currently have is returning a "Unrecognized pipeline stage name: '$cond'"
const updateUpvotes = {
$cond: {
if: { $elemMatch: { upvotes: ObjectID(userId) } },
then: { $pull: { upvotes: ObjectID(userId) } },
else: { $addToSet: { upvotes: ObjectID(userId) } }
}
};
db.collection(collectionName).updateOne({
commentId: ObjectID('5e5983102328a83d1a4b541f')
}, [updateUpvotes])
Am I overthinking the whole feature? I guess the 1. problem can be solved by simply creating a CommentReputation document (with empty upvotes and downvotes at the same time the Comment document is being created.
Is there a better way of doing this? I would love to have it working inside a single query request. Maybe someone of You guys implemented a similar feature and can give me some hints on this one.
you can do it with the following pipeline update but it requires that the upvotes and downvotes arrays exist. even if it's just empty.
var comment_id = ObjectId("5e5983102328a83d1a4b541f");
var user_id = ObjectId("5e5983102328a83d1a4b53e5");
db.commentReputation.update(
{
commentId: comment_id
},
[
{
$set: {
upvotes: {
$cond: [
{ $in: [user_id, '$upvotes'] },
{ $setDifference: ['$upvotes', [user_id]] },
{ $setUnion: ['$upvotes', [user_id]] }
]
}
}
},
{
$set: {
downvotes: {
$cond: [
{ $in: [user_id, '$downvotes'] },
{ $setDifference: ['$downvotes', [user_id]] },
'$downvotes'
]
}
}
}
]
);

Sequelize order by id for root table when we are using join method

this below Sequelize work fine for me without using order, i'm wondering why i can't use order for root table as posts model? when i use this below code i get this error:
Unhandled rejection Error: 'posts' in order / group clause is not valid association
but that work fine on other models such as channelVideoContainer
models.posts.findAll({
where: {
channelId: 1
},
include: [
{
model: models.channelVideoContainer,
include: [models.fileServerSetting]
}, {
model: models.channelMusicContainer,
include: [models.fileServerSetting]
}, {
model: models.channelImageWithTextContainer,
include: [models.fileServerSetting]
}, {
model: models.channelFilesContainer,
include: [models.fileServerSetting]
},
models.channelPlainTextContainer
], order: [
[{model: models.posts}, 'id', 'DESC'],
], limit: 5
}).then(function (result) {
console.log(JSON.stringify(result));
});
You are getting this error because you are querying the posts table/model, and then sorting by a column on the posts model, however you are specifying a "joined" table in your order. This works for your other models because they are in fact joined (using the include option). Since you are querying the posts model you just need to pass in the name of the column you want to order by. See some of the ORDER examples in the documentation.
// just specify the 'id' column, 'post' is assumed because it is the queried Model
order: [['id', 'DESC']],
As a side note, you may want to specify required: false on your include'd models to perform a LEFT JOIN so that rows come back even if there are no matches in the joined table. If you know that rows will be returned (or they are actually required) then leave it as is.
{
model: models.channelFilesContainer,
include: [models.fileServerSetting],
required: false, // LEFT JOIN the channelFilesContainer model
},

Categories

Resources