Find By ID and Remove From MongoDB array - node.js

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);
}
);

Related

Sequelize COUNT inside INCLUDE Associations return error aggregate function or the GROUP BY clause

I'm using Sequelize in my ExpressJS with SQL Server. I've two tables called PersonalInfo and ProductBuy it has relation PersonalInfo hasMany ProductBuy.
Here is the associations my model:
PersonalInfo.js
PersonalInfo.hasMany(models.ProductBuy, {
foreignKey: 'member_id',
sourceKey: 'id'
});
ProductBuy.js
ProductBuy.belongsTo(models.PersonalInfo, {
foreignKey: 'member_id',
targetKey: 'id'
});
I wanna get ProductBuy count of each user.
Expected Result:
{
"data": [
{
"id": 1,
"ProductBuys": [
{
"total_active": 3
}
]
},
{
"id": 2,
"ProductBuys": [
{
"total_active": 1
}
]
}
]
}
Here is my function in controller:
getAll: function (req, res) {
PersonalInfo.findAll({
attributes: ['id'],
include: [
{
model: ProductBuy,
attributes: [
[Sequelize.fn('COUNT', Sequelize.col('member_id')), 'total_active']
],
separate:true,
where: {
status: ['Aktif', 'Expired']
},
group:['member_id', 'id']
}
],
}).then(value => {
responseUtil.success(res, value)
})
}
but, this is what I got
{
"data": [
{
"id": 1,
"ProductBuys": [
{
"total_active": 1
},
{
"total_active": 1
},
{
"total_active": 1
}
]
},
{
"id": 2,
"ProductBuys": [
{
"total_active": 1
}
]
}
]
}
I think that's happen because I GROUP BY it by member_id and id, but if I remove the id it will cause error like this
Column 'ProductBuy.id' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
How do I can achive this. Thankyou

mongoose invalid operator $match

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 });
}
);

mongoose user bulk search by phone numbers

I have UserSchema which contains PhoneNumberSchema as below.
var PhoneNumberSchema = new Schema({
country: {
type: String
},
country_code: {
type: Number
},
local_number: {
type: Number
}
});
And here is sample json format of phone_number.
"phone_number": {
"country": "US",
"country_code": "1",
"local_number": "04152341"
}
What I want to do is to search users by phone numbers with / without country code.
Well, if request is
"phone_numbers": [ "104152341", "124254364" ]
then I want to get users who has exactly matched phone number which belongs in the request phone numbers array with/without country code.
So, I tried as below, but got error "invalid operator '$in'".
User.aggregate(
[
{ "$redact": {
"$cond": [
{
"$in": [ { "$concat": [ "$phone_number.country_code", "$phone_number.local_number" ] }, req.body.phone_numbers]
},
"$$KEEP",
"$$PRUNE"
]
}}
],
function(err, users) {
// Do something
if (err) {
return res.json({ success: false, err: err });
}
res.json({ success: true, users: users });
}
)
I hope to know how to handle my issue.
Please help me !!
Use $setIsSubset as your condition expression:
{ "$redact": {
"$cond": [
{
"$setIsSubset": [
[
{
"$concat": [
"$phone_number.country_code",
"$phone_number.local_number"
]
}
],
req.body.phone_numbers
]
},
"$$KEEP",
"$$PRUNE"
]
}}

Mongoose update document with $or condition

I would like to search for all activity which has the same action_object.reply.id or action_target.reply.id. Something like this:
Activity
.find({ $or: [
{ 'action_object.reply.id': replyId },
{ 'action_target.reply.id': replyId }
]});
But i also only want to update the removed attribute like this:
Activity
.update({ 'action_object.reply.id': replyId }, {
'action_object.reply.removed': true }, { multi: true });
Activity
.update({ 'action_target.reply.id': replyId }, {
'action_target.reply.removed': true }, { multi: true });
Is it possible to somehow combine these two queries? I want to update action_target.reply.removed where action_target.reply.id or action_object.reply.removed where action_object.reply.id.
Or i must write two different queries for this like i did above.
The first argument to the update call is the query object, so you can simply use the same $or query. Mongo will update all documents retrieve by the query.
Activity
.update({ $or: [
{ 'action_object.reply.id': replyId },
{ 'action_target.reply.id': replyId }
]}, {'action_object.reply.removed': true }, { multi: true });
With 4.2, you can use $cond
// Configuration
[
{
"action_object": {
"reply": {
"id": "bar",
"removed": false
}
}
},
{
"action_target": {
"reply": {
"id": "foo",
"removed": false
}
}
}
]
// Query
db.collection.update({
$or: [
{
"action_object.reply.id": "foo"
},
{
"action_target.reply.id": "foo"
}
]
},
[
{
$set: {
"action_object.reply.removed": {
$cond: [
{
$eq: [
"foo",
"$action_object.reply.id"
]
},
true,
"$$REMOVE"
]
},
"action_target.reply.removed": {
$cond: [
{
$eq: [
"foo",
"$action_target.reply.id"
]
},
true,
"$$REMOVE"
]
}
}
}
],
{
multi: true
})
https://mongoplayground.net/p/tOLh5YKRVX1

How to pull one instance of an item in an array in MongoDB?

According to the documents:
The $pull operator removes from an existing array all instances of a value or values that match a specified condition.
Is there an option to remove only the first instance of a value? For example:
var array = ["bird","tiger","bird","horse"]
How can the first "bird" be removed directly in an update call?
So you are correct in that the $pull operator does exactly what the documentation says in that it's arguments are in fact a "query" used to match the elements that are to be removed.
If your array content happened to always have the element in the "first" position as you show then the $pop operator does in fact remove that first element.
With the basic node driver:
collection.findOneAndUpdate(
{ "array.0": "bird" }, // "array.0" is matching the value of the "first" element
{ "$pop": { "array": -1 } },
{ "returnOriginal": false },
function(err,doc) {
}
);
With mongoose the argument to return the modified document is different:
MyModel.findOneAndUpdate(
{ "array.0": "bird" },
{ "$pop": { "array": -1 } },
{ "new": true },
function(err,doc) {
}
);
But neither are of much use if the array position of the "first" item to remove is not known.
For the general approach here you need "two" updates, being one to match the first item and replace it with something unique to be removed, and the second to actually remove that modified item.
This is a lot more simple if applying simple updates and not asking for the returned document, and can also be done in bulk across documents. It also helps to use something like async.series in order to avoid nesting your calls:
async.series(
[
function(callback) {
collection.update(
{ "array": "bird" },
{ "$unset": { "array.$": "" } },
{ "multi": true }
callback
);
},
function(callback) {
collection.update(
{ "array": null },
{ "$pull": { "array": null } },
{ "multi": true }
callback
);
}
],
function(err) {
// comes here when finished or on error
}
);
So using the $unset here with the positional $ operator allows the "first" item to be changed to null. Then the subsequent query with $pull just removes any null entry from the array.
That is how you remove the "first" occurance of a value safely from an array. To determine whether that array contains more than one value that is the same though is another question.
It's worth noting that whilst the other answer here is indeed correct that the general approach here would be to $unset the matched array element in order to create a null value and then $pull just the null values from the array, there are better ways to implement this in modern MongoDB versions.
Using bulkWrite()
As an alternate case to submitting two operations to update in sequence as separate requests, modern MongoDB release support bulk operations via the recommended bulkWrite() method which allows those multiple updates to be submitted as a single request with a single response:
collection.bulkWrite(
[
{ "updateOne": {
"filter": { "array": "bird" },
"update": {
"$unset": { "array.$": "" }
}
}},
{ "updateOne": {
"filter": { "array": null },
"update": {
"$pull": { "array": null }
}
}}
]
);
Does the same thing as the answer showing that as two requests, but this time it's just one. This can save a lot of overhead in server communication, so it's generally the better approach.
Using Aggregation Expressions
With the release of MongoDB 4.2, aggregation expressions are now allowed in the various "update" operations of MongoDB. This is a single pipeline stage of either $addFields, $set ( which is an alias of $addFields meant to make these "update" statements read more logically ), $project or $replaceRoot and it's own alias $replaceWith. The $redact pipeline stage also applies here to some degree. Basically any pipeline stage which returns a "reshaped" document is allowed.
collection.updateOne(
{ "array": "horse" },
[
{ "$set": {
"array": {
"$concatArrays": [
{ "$slice": [ "$array", 0, { "$indexOfArray": [ "$array", "horse" ] }] },
{ "$slice": [
"$array",
{ "$add": [{ "$indexOfArray": [ "$array", "horse" ] }, 1] },
{ "$size": "$array" }
]}
]
}
}}
]
);
In this case the manipulation used is to implement the $slice and $indexOfArray operators to essentially piece together a new array which "skips" over the first matched array element. Theses pieces are joined via the $concatArrays operator, returning a new array absent of the first matched element.
This is now probably more effective since the operation which is still a single request is now also a single operation and would incur a little less server overhead.
Of course the only catch is that this is not supported in any release of MongoDB prior to 4.2. The bulkWrite() on the other hand may be a newer API implementation, but the actual underlying calls to the server would apply back to MongoDB 2.6 implementing actual "Bulk API" calls, and even regresses back to earlier versions by the way all core drivers actually implement this method.
Demonstration
As a demonstration, here is a listing of both approaches:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true, useUnifiedTopology: true };
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useCreateIndex', true);
mongoose.set('useFindAndModify', false);
const arrayTestSchema = new Schema({
array: [String]
});
const ArrayTest = mongoose.model('ArrayTest', arrayTestSchema);
const array = ["bird", "tiger", "horse", "bird", "horse"];
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
await Promise.all(
Object.values(conn.models).map(m => m.deleteMany())
);
await ArrayTest.create({ array });
// Use bulkWrite update
await ArrayTest.bulkWrite(
[
{ "updateOne": {
"filter": { "array": "bird" },
"update": {
"$unset": { "array.$": "" }
}
}},
{ "updateOne": {
"filter": { "array": null },
"update": {
"$pull": { "array": null }
}
}}
]
);
log({ bulkWriteResult: (await ArrayTest.findOne()) });
// Use agggregation expression
await ArrayTest.collection.updateOne(
{ "array": "horse" },
[
{ "$set": {
"array": {
"$concatArrays": [
{ "$slice": [ "$array", 0, { "$indexOfArray": [ "$array", "horse" ] }] },
{ "$slice": [
"$array",
{ "$add": [{ "$indexOfArray": [ "$array", "horse" ] }, 1] },
{ "$size": "$array" }
]}
]
}
}}
]
);
log({ aggregateWriteResult: (await ArrayTest.findOne()) });
} catch (e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
And the output:
Mongoose: arraytests.deleteMany({}, {})
Mongoose: arraytests.insertOne({ array: [ 'bird', 'tiger', 'horse', 'bird', 'horse' ], _id: ObjectId("5d8f509114b61a30519e81ab"), __v: 0 }, { session: null })
Mongoose: arraytests.bulkWrite([ { updateOne: { filter: { array: 'bird' }, update: { '$unset': { 'array.$': '' } } } }, { updateOne: { filter: { array: null }, update: { '$pull': { array: null } } } } ], {})
Mongoose: arraytests.findOne({}, { projection: {} })
{
"bulkWriteResult": {
"array": [
"tiger",
"horse",
"bird",
"horse"
],
"_id": "5d8f509114b61a30519e81ab",
"__v": 0
}
}
Mongoose: arraytests.updateOne({ array: 'horse' }, [ { '$set': { array: { '$concatArrays': [ { '$slice': [ '$array', 0, { '$indexOfArray': [ '$array', 'horse' ] } ] }, { '$slice': [ '$array', { '$add': [ { '$indexOfArray': [ '$array', 'horse' ] }, 1 ] }, { '$size': '$array' } ] } ] } } } ])
Mongoose: arraytests.findOne({}, { projection: {} })
{
"aggregateWriteResult": {
"array": [
"tiger",
"bird",
"horse"
],
"_id": "5d8f509114b61a30519e81ab",
"__v": 0
}
}
NOTE : The example listing is using mongoose, partly because it was referenced in the other answer given and partly to also demonstrate an important point with the aggregate syntax example. Note the code uses ArrayTest.collection.updateOne() since at the present release of Mongoose ( 5.7.1 at time of writing ) the aggregation pipeline syntax to such updates is being removed by the standard mongoose Model methods.
As such the .collection accessor can be used in order to get the underlying Collection object from the core MongoDB Node driver. This would be required until a fix is made to mongoose which allows this expression to be included.
As mentioned in this Jira this feature will never exist properly.
The approach I recommend using would be via the aggregation pipeline update syntax as proposed in a different answer, however that answer has some edge cases where it fails - for example if the element does not exist in the array, here is a working version for all edge cases.
ArrayTest.updateOne({},
[
{
"$set": {
"array": {
"$concatArrays": [
{
$cond: [
{
$gt: [
{
"$indexOfArray": [
"$array",
"horse"
]
},
0
]
},
{
"$slice": [
"$array",
0,
{
"$indexOfArray": [
"$array",
"horse"
]
}
]
},
[]
]
},
{
"$slice": [
"$array",
{
"$add": [
{
"$indexOfArray": [
"$array",
"horse"
]
},
1
]
},
{
"$size": "$array"
}
]
}
]
}
}
}
])
Mongo Playground

Resources