$in requires an array as a second argument, found: missing - arrays

can anybody please tell me what am i doing wrong?
db document structure:
{
"_id" : "module_settings",
"moduleChildren" : [
{
"_id" : "module_settings_general",
"name" : "General",
},
{
"_id" : "module_settings_users",
"name" : "Users",
},
{
"_id" : "module_settings_emails",
"name" : "Emails",
}
],
“permissions” : [
"module_settings_general",
"module_settings_emails"
]
}
pipeline stage:
{ $project: {
filteredChildren: {
$filter: {
input: "$moduleChildren",
as: "moduleChild",
cond: { $in : ["$$moduleChild._id", "$permissions"] }
}
},
}}
I need to filter "moduleChildren" array to show only modules which ids are in "permissions" array. Ive tried "$$ROOT.permissions" and "$$CURRENT.permissions" but none of them is working. I always get an error that $in is missing array as argument. It works when i hardcode the array like this: cond: { $in : ["$$moduleChild._id", [“module_settings_general", "module_settings_emails”]] } so it seems the problem is in passing of the array.
Thanks for any advices!

First option --> Use aggregation
Because your some of the documents in your collection may or may not contain permissions field or is type not equal to array that's why you are getting this error.
You can find the $type of the field and if it is not an array or not exists in your document than you can add it as an array with $addFields and $cond aggregation
db.collection.aggregate([
{ "$addFields": {
"permissions": {
"$cond": {
"if": {
"$ne": [ { "$type": "$permissions" }, "array" ]
},
"then": [],
"else": "$permissions"
}
}
}},
{ "$project": {
"filteredChildren": {
"$filter": {
"input": "$moduleChildren",
"as": "moduleChild",
"cond": {
"$in": [ "$$moduleChild._id", "$permissions" ]
}
}
}
}}
])
Second option -->
Go to your mongo shell or robomongo on any GUI you are using and run
this command
db.collection.update(
{ "permissions": { "$ne": { "$type": "array" } } },
{ "$set": { "permissions": [] } },
{ "multi": true }
)

Related

MongoDB 4.4: input to $filter must be an array not long

I've implemented the following aggregation in MongoDB 4.4.2 and it works fine:
[{
$match: {
"$expr": {
"$and": [{
"$not": "$history_to"
}, ],
},
}
}, {
$unwind: {
"path": "$used",
"preserveNullAndEmptyArrays": true,
}
}, {
$project: {
"ticket": "$$ROOT",
"status": {
"$map": {
"input": {
"$filter": {
"input": "$status",
"as": "cstatus",
"cond": {
"$not": "$$cstatus.history_to",
},
},
},
"as": "status",
"in": "$$status.value",
},
},
}
}]
But when I try it in MongoDB 4.4.4 I encounter input to $filter must be an array not long.
If it's any help, I figured that the cause of this error has something to do with:
"cond": {
"$not": "$$cstatus.history_to",
},
Since when I comment $not it works fine; At first I thought that maybe $not is not supported anymore but it is supported so I'm out of ideas.
Some example documents
{
"_id" : ObjectId("6083ca1ce151beea45602e5d"),
"created_at" : ISODate("2021-04-24T07:34:52.947Z"),
"ticket_id" : ObjectId("6083ca1c68badcedddd875ba"),
"expire_at" : ISODate("2021-04-24T19:30:00Z"),
"history_from" : ISODate("2021-04-24T07:34:52.992Z"),
"created_by" : ObjectId("604df7857d58ab06685ed02e"),
"serial" : "116627138",
"status" : [
{
"history_from" : ISODate("2021-04-24T07:34:52.985Z"),
"history_created_by" : ObjectId("604df7857d58ab06685ed02e"),
"value" : NumberLong(1)
}
],
"history_created_by" : ObjectId("604df7857d58ab06685ed02e")
},
{
"_id" : ObjectId("60713b0fe151beea45602e56"),
"created_by" : ObjectId("604df7857d58ab06685ed02e"),
"ticket_id" : ObjectId("60713b0f68badcedddd875b8"),
"created_at" : ISODate("2021-04-10T05:43:43.228Z"),
"history_created_by" : ObjectId("604df7857d58ab06685ed02e"),
"status" : [
{
"history_created_by" : ObjectId("604df7857d58ab06685ed02e"),
"value" : NumberLong(1),
"history_from" : ISODate("2021-04-10T05:43:43.277Z")
}
],
"serial" : "538142578",
"expire_at" : ISODate("2021-04-10T19:30:00Z"),
"history_from" : ISODate("2021-04-10T05:43:43.281Z"),}
Your query looks good as per your example documents you can check playground,
But when I try it in MongoDB 4.4.4, I encounter input to $filter must be an array not long.
It is not any MongoDB version specific issue, The error says provided field status as input in $filter is not array type it is long type, and $filter input should be an array type.
Definitely there are/is some document(s) having non array value in status field.
If you want to check you can match condition before $filter operation,
{ $match: { status: { $type: "array" } } } // 4 = "array"
This will filter documents by status, it should be an array type.

How to query on embedded documents

{
"_id" : ObjectId("5fa919a49bbe481d117506c9"),
"isDeleted" : 0,
"productId" : 31,
"references" : [
{
"_id" : ObjectId("5fa919a49bbe481d117506ca"),
"languageCode" : "en",
"languageId" : 1,
"productId" : ObjectId("5fa919a49bbe481d117506ba")
},
{
"_id" : ObjectId("5fa91cc7d7d52f1e389dee1f"),
"languageCode" : "ar",
"languageId" : 2,
"productId" : ObjectId("5fa91cc7d7d52f1e389dee1e")
}
],
"createdAt" : ISODate("2020-11-09T10:27:48.859Z"),
"updatedAt" : ISODate("2020-11-09T10:27:48.859Z"),
"__v" : 0
},
{
"_id" : ObjectId("5f9aab1d8e475489270ebe3a"),
"isDeleted" : 0,
"productId" : 21,
"references" : [
{
"_id" : ObjectId("5f9aab1d8e475489270ebe3b"),
"languageCode" : "en",
"languageId" : 1,
"productId" : ObjectId("5f9aab1c8e475489270ebe2d")
}
],
"createdAt" : ISODate("2020-10-29T11:44:29.852Z"),
"updatedAt" : ISODate("2020-10-29T11:44:29.852Z"),
"__v" : 0
}
This is my mongoDB collection in which i store the multilingual references to product collection. In productId are the references to product Collection. Now If we have ar in our request, then we will only have the productId of ar languageCode. If that languageCode does not exist then we will have en langCode productId.
For Example if the user pass ar then the query should return
"productId" : ObjectId("5fa91cc7d7d52f1e389dee1e")
"productId" : ObjectId("5f9aab1c8e475489270ebe2d")
I have tried using $or with $elemMatch but I am not able to get the desired result. Also i am thinking of using $cond. can anyone help me construct the query.
We can acheive
$facet helps to categorized the incoming documents
In the arArray, we get all documents which has"references.languageCode": "ar" (This document may or may not have en), then de-structure the references array, then selecting the "references.languageCode": "ar" only using $match. $group helps to get all productIds which belong to "references.languageCode": "ar"
In the enArray, we only get documents which have only "references.languageCode": "en". Others are same like arArray.
$concatArrays helps to concept both arArray,enArray arrays
$unwind helps to de-structure the array.
$replaceRoot helps to make the Object goes to root
Here is the mongo script.
db.collection.aggregate([
{
$facet: {
arAarray: [
{
$match: {
"references.languageCode": "ar"
}
},
{
$unwind: "$references"
},
{
$match: {
"references.languageCode": "ar"
}
},
{
$group: {
_id: "$_id",
productId: {
$addToSet: "$references.productId"
}
}
}
],
enArray: [
{
$match: {
$and: [
{
"references.languageCode": "en"
},
{
"references.languageCode": {
$ne: "ar"
}
}
]
}
},
{
$unwind: "$references"
},
{
$group: {
_id: "$_id",
productId: {
$addToSet: "$references.productId"
}
}
}
]
}
},
{
$project: {
combined: {
"$concatArrays": [
"$arAarray",
"$enArray"
]
}
}
},
{
$unwind: "$combined"
},
{
"$replaceRoot": {
"newRoot": "$combined"
}
}
])
Working Mongo playground
You can test this solution to see if it is useful for you question:
db.collection.aggregate([
{
$addFields: {
foundResults:
{
$cond: {
if: { $in: ["ar", "$references.languageCode"] }, then:
{
$filter: {
input: "$references",
as: "item",
cond: {
$and: [{ $eq: ["$$item.languageCode", 'ar'] },
]
}
}
}
, else:
{
$filter: {
input: "$references",
as: "item",
cond: {
$and: [{ $eq: ["$$item.languageCode", 'en'] },
]
}
}
}
}
}
}
},
{ $unwind: "$foundResults" },
{ $replaceRoot: { newRoot: { $mergeObjects: ["$foundResults"] } } },
{ $project: { _id: 0, "productId": 1 } }
])

Error: "an object representing an expression must have exactly one field" while using expr in find()

My output for the db.dummy.find(query).count() should be 2 but I am getting the following error.
Please note the "mark" field is in an array and has "string value" therefore used $map because using $convert for array throws conversion error.
db.dummy.find({
$expr: {
$project: {
adjustedGrades: {
$map: {
input: "$data.mark",
as: "grade",
in: {
$lte: [
{
$toInt: "$$grade"
},
5
]
}
}
}
},
$match: {
adjustedGrades: {
$eq: true
}
}
}
}).count()
Error:
error: {
"ok" : 0,
"errmsg" : "An object representing an expression must have exactly one field: { $project: { adjustedGrades: { $map: { input: \"$data.mark\", as: \"grade\", in: { $lte: [ { $toInt: \"$$grade\" }, 5.0 ] } } } }, $match: { adjustedGrades: { $eq: true } } }",
"code" : 15983,
"codeName" : "Location15983"}
Collection:
{ "_id" : "1_0", "data" : [ { "Class" : "DUMMY1", "mark" : "5" } ] }
{ "_id" : "2_0", "data" : [ { "Class" : "DUMMY2", "mark" : "3" } ] }
{ "_id" : "3_0", "data" : [ { "Class" : "DUMMY3", "mark" : "9" } ] }
With help from mongoDB community, I was able to get the solution for conversion and comparison of values in an array that can be used inside db.collection.find() rather than aggregate().
>db.testcol.find({
"$expr": {
"$allElementsTrue": {
"$map": {
"input": "$data",
"as": "d",
"in": {
"$gt": [
{
"$toDouble": "$$d.userDL"
},
5
]
}
}
}
}
})
Output: 2
This should help
db.getCollection("dummy").find({ "data.0. mark": { $lte: "5" } }, {})
From MongoDB Documentation,
$expr:
Allows the use of aggregation expressions within the query language.
$expr has the following syntax:
{ $expr: { <expression> } }
The arguments can be any valid aggregation expression. For more information, see Expressions.
$project cannot be used inside $expr
In order to achieve your use case, you can use aggregation framework:
db.dummy.aggregate([
{
$project: {
adjustedGrades: {
$map: {
input: "$data",
as: "grade",
in: {
$lte: [
{
$toInt: "$$grade.mark"
},
5
]
}
}
}
}
},
{
$match: {
adjustedGrades: {
$eq: true
}
}
},
{
"$count": "count"
}
])
MongoDB Playground:Using aggregate()
If you don't want to use aggregation framework, try the following method:
First do the conversion of string to Int using parseInt() :
db.dummy.find().forEach(function(doc) {
doc.data.forEach(function(d) {
d.mark=parseInt(d.mark); })
db.dummy.save(doc);
})
And then run the find() query:
db.dummy.find({
"data.mark": {
$lte: 5
}
}).count()

mongodb, how can I project or push root field on nested array using aggregate?

I have the following example document with four nested arrays
{
"_id" : ObjectId("5ed6bd9596908c36f4200980"),
"attr2" : "hello",
"attr3" : 1234,
"attr1" : {
"firstArray" : [
{
"secondArray" : [
{
"secondAttr1" : "world",
"secondAttr2" : 456,
"secondAttr3" : [
{
"finalArray" : [
{
"finalAttr1" : "alex",
"finalAttr2" : 9876
}
]
}
]
}
]
}
]
}
}
and this aggregate query that finds through nested arrays, unwinding and creating new root for each array processed, returning elements from "finalArray"
db.AssetTest.aggregate(
[
{'$match':{'$and':[{'attr2': {'$eq':'hello'}}]}},
{'$project': {'values': '$attr1.firstArray'}},
{'$unwind':'$values'},
{'$replaceRoot':{'newRoot':'$values'}},
{'$project': {'values': {'$filter': {'input': '$secondArray','cond': {'$and':[{'$eq':['$$this.secondAttr1', 'world']}]}}}}},
{'$unwind':'$values'},
{'$replaceRoot':{'newRoot':'$values'}},
{'$project': {'values': '$secondAttr3'}},
{'$unwind':'$values'},
{'$replaceRoot':{'newRoot':'$values'}},
{'$project': {'values': {'$filter': {'input': '$finalArray','cond': {'$and':[{'$eq':['$$this.finalAttr1', 'alex']}]}}}}},
{'$unwind':'$values'},
{'$replaceRoot':{'newRoot':'$values'}}
]
)
the result for this aggregate is
{
"finalAttr1" : "alex",
"finalAttr2" : 9876
}
my question is, how can I project root field "attr2" through all aggregation stages so it will be on result array?
{
"attr2" : "hello",
"finalAttr1" : "alex",
"finalAttr2" : 9876
}
I'm new to mongo and I'm completely lost on this so any help will be very appreciated.
By the way I'm using mongo 3.4.15.
Many Thanks!!
In your case all you need is $addFields which got introduced in MongoDB version 3.4 :
I've cut down few stages & operators which doesn't seems to be useful, Check this latest aggregation pipeline :
db.collection.aggregate([
{
"$match": { "attr2": "hello" }
},
/** This stage adds new field to the place where you want */
{
$addFields: { "attr1.firstArray.secondArray.secondAttr3.finalArray.attr2": "$attr2" }
},
{
"$unwind": "$attr1.firstArray"
},
{
"$replaceRoot": { "newRoot": "$attr1.firstArray" }
},
{
"$project": {
"secondArray": {
"$filter": { "input": "$secondArray", "cond": { "$eq": [ "$$this.secondAttr1", "world" ] } }
}
}
},
{
"$unwind": "$secondArray"
},
{
"$replaceRoot": { "newRoot": "$secondArray" }
},
{
"$unwind": "$secondAttr3"
},
{
"$replaceRoot": { "newRoot": "$secondAttr3" }
},
{
"$project": {
"finalArray": { "$filter": { "input": "$finalArray", "cond": { "$eq": [ "$$this.finalAttr1", "alex" ] } } }
}
},
{
"$unwind": "$finalArray"
},
{
"$replaceRoot": { "newRoot": "$finalArray"}
}
])
Test : mongoplayground
Ref : $addFields
Note :
You don't need to use $and on single conditions (Refer your 1st stage).
You don't need to multiple $project stages in the middle for conversion (Refer your 2nd stage).

Sort by deep document field in MongoDb

I have a collection called Visitor which has an array of chats and each array has a document called user.
I need to find some documents on this collection and sort them by if they have some specific user in their chats first.
The path for the user id is:
chats.user._id
where:
chats // array
user // document
_id // ObjectId
The below script does sort the documents correctly, however, it expands the chats array and multiplies the document for each chat in the array.
I only need the sorting, so can I sort and not use the unwind pipeline or make it somehow not multiply the documents?
db.getCollection('Visitor').aggregate([
{$unwind: "$chats"},
{ $match: {'event._id':ObjectId('5c942a3591deb389bfd92579'), 'chats.enabled': {$exists: true}}},
{
"$project": {
"_id": 1,
"chats.user._id": 1,
"weight": {
"$cond": [
{ "$eq": [ "$chats.user._id", ObjectId("5c942a3591deb389bfd92579") ] },
10,
0
]
}
}
},
{ "$sort": { "weight": -1 } },
])
EDIT: I don't need to sort the inner array, but sort the find command by checking if a specific user is in the chats array.
Some sample of Visitor collection:
[
{
"_id" : ObjectId("5c9a3a1bd86e0ba64106e90e"),
"event" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"chats" : [
{
"enabled" : false,
"user" : {
"_id" : ObjectId("5c81232f09a923b559763418")
},
"_id" : ObjectId("5c9a3a1bd86e0ba64106e915")
}
]
},
{
"_id" : ObjectId("5c9a3a35d86e0ba64106e950"),
"event" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"chats" : [
{
"enabled" : true,
"user" : {
"_id" : ObjectId("5c81232f09a923b559763418")
},
"_id" : ObjectId("5c9a3a35d86e0ba64106e957")
},
{
"enabled" : true,
"user" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"_id" : ObjectId("5c9a3a34d86e0ba64106e91d")
}
]
}
]
In the above sample, I need to make the second document to be sorted first because it has the user with the _id ObjectId("5c942a3591deb389bfd92579").
The problem here is that using $unwind you modify initial structure of your documents (you will get one document per chats. I would suggest using $map to get an array of weights based on specified userId and then you can use $max to get final weight
db.col.aggregate([
{ $match: {'event._id':ObjectId('5c942a3591deb389bfd92579'), 'chats.enabled': {$exists: true}}},
{
"$project": {
"_id": 1,
"chats.user._id": 1,
"weight": {
$max: { $map: { input: "$chats", in: { $cond: [ { $eq: [ "$$this.user._id", ObjectId("5c942a3591deb389bfd92579") ] }, 10, 0 ] } } }
}
}
},
{ "$sort": { "weight": -1 } },
])

Resources