JS: $addToSet or $pull depending on existing/missing value - javascript

I need to add or remove an ID from an array (target), depending if it is already existing. This is how I am doing this:
var isExisting = Articles.findOne({ _id }).target.indexOf(mID) > -1
if (isExisting === false) {
Articles.update(
{ _id },
{ $addToSet: { target: mID } }
)
} else if (isExisting === true) {
Articles.update(
{ _id },
{ $pull: { target: mID } }
)
}
Is it possible to do this in a better way - without doing if/else and min. two db operations?

Mongoose operations are asynchronous, so you need to wait for its callback to get the document.
// find the article by its ID
Articles.findById(_id, function (err, article) {
// make appropriate change depending on whether mID exist in the article's target
if (article.target.indexOf(mID) > -1)
article.target.pull(mID)
else
article.target.push(mID)
// commit the change
article.save(function (err) {
});
})
Although you are doing if/else, you are doing 2 operations.

here is my suggestion
let isExisting = Articles.findOne({ _id: _id, target : mID}) //mongo can search for mID in array of [mIDs]
let query = { _id : _id };
let update = isExisting ? { $pull: { target: mID } } : { $addToSet: { target: mID } };
Articles.update(query, update);
is it better and clearer now?

Related

How to keep decreasing value in mongo until is 0

singleObj = await Objects.findByIdAndUpdate({ _id: req.body.id, }, { $inc: { 'total_obj': -1, 'total_stuff': 1 }, }, { new: true })
The user clicks a button and the value of 'total_obj' gets decreased by one. The value doesn't have to be less than 0.
I have tried to do this:
singleObj = await Objects.findByIdAndUpdate(
{ _id: req.body.id, "total_obj": { "$lt": 0 } },
{ "$set": { "total_obj": 0 } }
);
But this messes up every time I load the page and I have the values set to 0.
I also added on the definition on the schema:
total_obj: {
type: Number,
required: true,
min: 0
},
I assume you meant that you don't want your value to be lesser than 0.
You would need to use $gt operator and while you used $inc properly in the first findByIdAndUpdate you didn't use it in the second one.
Also, we are not looking only for id so we should use findOneAndUpdate instead.
singleObj = await Objects.findOneAndUpdate(
{ _id: req.body.id, "total_obj": { "$gt": 0 } },
{ $inc: { "total_obj": -1 } }
);
Try to fetch the Objects instance first and update the value only if > 0:
const singleObj = await Objects.findById(req.body.id)
if (!singleObj) // Error, obj not found
if (singleObj.total_obj > 0) {
singleObj.total_obj = singleObj.total_obj-1
await singleObj.save()
} else {
// `total_obj` is already zero
}

Construct MongoDB query from GraphQL request

Let's say we query the server with this request, we only want to get the following user's Email, My current implementation requests the whole User object from the MongoDB, which I can imagine is extremely inefficient.
GQL
{
user(id:"34567345637456") {
email
}
}
How would you go about creating a MongoDB filter that would only return those Specified Fields? E.g,
JS object
{
"email": 1
}
My current server is running Node.js, Fastify and Mercurius
which I can imagine is extremely inefficient.
Doing this task is an advanced feature with many pitfalls. I would suggest starting building a simple extraction that read all the fields. This solution works and does not return any additional field to the client.
The pitfalls are:
nested queries
complex object composition
aliasing
multiple queries into one request
Here an example that does what you are looking for.
It manages aliasing and multiple queries.
const Fastify = require('fastify')
const mercurius = require('mercurius')
const app = Fastify({ logger: true })
const schema = `
type Query {
select: Foo
}
type Foo {
a: String
b: String
}
`
const resolvers = {
Query: {
select: async (parent, args, context, info) => {
const currentQueryName = info.path.key
// search the input query AST node
const selection = info.operation.selectionSet.selections.find(
(selection) => {
return (
selection.name.value === currentQueryName ||
selection.alias.value === currentQueryName
)
}
)
// grab the fields requested by the user
const project = selection.selectionSet.selections.map((selection) => {
return selection.name.value
})
// do the query using the projection
const result = {}
project.forEach((fieldName) => {
result[fieldName] = fieldName
})
return result
},
},
}
app.register(mercurius, {
schema,
resolvers,
graphiql: true,
})
app.listen(3000)
Call it using:
query {
one: select {
a
}
two: select {
a
aliasMe:b
}
}
Returns
{
"data": {
"one": {
"a": "a"
},
"two": {
"a": "a",
"aliasMe": "b"
}
}
}
Expanding from #Manuel Spigolon original answer, where he stated that one of the pitfalls of his implementation is that it doesn't work on nested queries and 'multiple queries into one request' which this implementation seeks to fix.
function formFilter(context:any) {
let filter:any = {};
let getValues = (selection:any, parentObj?:string[]) => {
//selection = labelSelection(selection);
selection.map((selection:any) => {
// Check if the parentObj is defined
if(parentObj)
// Merge the two objects
_.merge(filter, [...parentObj, null].reduceRight((obj, next) => {
if(next === null) return ({[selection.name?.value]: 1});
return ({[next]: obj});
}, {}));
// Check for a nested selection set
if(selection.selectionSet?.selections !== undefined){
// If the selection has a selection set, then we need to recurse
if(!parentObj) getValues(selection.selectionSet?.selections, [selection.name.value]);
// If the selection is nested
else getValues(selection.selectionSet?.selections, [...parentObj, selection.name.value]);
}
});
}
// Start the recursive function
getValues(context.operation.selectionSet.selections);
return filter;
}
Input
{
role(id: "61f1ccc79623d445bd2f677f") {
name
users {
user_name
_id
permissions {
roles
}
}
permissions
}
}
Output (JSON.stringify)
{
"role":{
"name":1,
"users":{
"user_name":1,
"_id":1,
"permissions":{
"roles":1
}
},
"permissions":1
}
}

Compare 2 values from same doc in mongo

I want to compare 2 values in same doc id
For example
{
"_id" : ObjectId("5f180441ad1cd40008dc6a5a"),
"fcount" : 5,
"key" : "27b6e581-796c-4f3a-882b-0e0c2a0a8a64",
"student_id" : "5f0ffbcdd67d70c1a3b143aa",
"__v" : 0,
"dcount" : 5
}
I have this doc, Now If I want to check fcount and dcount is same then return true. OR false
How can I do this with mongoose query?
update:
const result = await execute_reports_model.find({ _id: _id}).lean().cursor({batchSize: 10}).eachAsync(async ({fcount, dcount}) => {
if (fcount === dcount) {
// true Logic
} else {
// false logic
}
}, { parallel: 10})
So here I will be passing the ID in find. and then compare values that I found from that ID. values are fcount and dcount
Using Mongoose's Model.aggregate you can compare a document's two fields and return a boolean:
const result = await model.aggregate([
{ $match: { _id: id } },
{ $project: { _id: 0, isMatch: { $eq: [ "$fcount", "$dcount" ] } } }
])
result value: { "isMatch" : true }
This precursor code will help you to solve your problem. It will allow you to find the necessary documents and then deal with all true and false cases.
{batchSize: 10} and {parallel:10} will help you to do your task at parallel. You could easily modify them.
Don't forget to add callback or handle all cases somehow, as you need it to.
await collection_name.find({ your_query: here}).lean().cursor({batchSize: 10}).eachAsync(async ({fcount, dcount}) => {
if (fcount === dcount) {
//true case
} else {
//false case
}
}, { parallel: 10})

Retrieve n random document with certain filters MongoDB

I need to retrieve from a collection of random docs based on a limit given.
If some filters are provided they should be added to filter the results of the response. I'm able to build the match and size based on the fields provided but even tho I have 20 documents that meet the filter when I make the call I receive only 2 or 3 docs back and I can't seem to figure it out. If I set only the limit it does give me back N random docs based on the limit but if I add a filter it won't give me the wanted results.
This is what I do now
const limit = Number(req.query.limit || 1);
const difficulty = req.query.difficulty;
const category = req.query.category;
const settings = [
{
$sample: {
size: limit
}
}
];
if (difficulty && category) {
settings.push({
$match: {
difficulty: difficulty,
category: category
}
});
} else if (difficulty && category == null) {
settings.push({
$match: {
difficulty
}
});
}
if (difficulty == null && category) {
settings.push({
$match: {
category
}
});
}
console.log(settings);
Question.aggregate(settings)
.then(docs => {
const response = {
count: docs.length,
difficulty: difficulty ? difficulty : "random",
questions:
docs.length > 0
? docs.map(question => {
return {
_id: question._id,
question: question.question,
answers: question.answers,
difficulty: question.difficulty,
category: question.category,
request: {
type: "GET",
url:
req.protocol +
"://" +
req.get("host") +
"/questions/" +
question._id
}
};
})
: {
message: "No results found"
}
};
res.status(200).json(response);
})
.catch(err => {
res.status(500).json({
error: err
});
});
Order of the stages matters here. You are pushing the $match stage after the $sample stage which first put the $size to whole the documents and then applies the $match stage on the $sampled documents documents.
So finally you need to push the $sample stage after the $match stage. The order should be
const limit = Number(req.query.limit || 1);
const difficulty = req.query.difficulty;
const category = req.query.category;
const settings = []
if (difficulty && category) {
settings.push({
$match: {
difficulty: difficulty,
category: category
}
})
} else if (difficulty && category == null) {
settings.push({
$match: {
difficulty
}
})
}
if (difficulty == null && category) {
settings.push({
$match: {
category
}
})
}
setting.push({
$sample: {
size: limit
}
})
console.log(settings);
Question.aggregate(settings)

How to Tally Up Numerical Values and Produce One Value for all Documents in Mongo/Node

I am trying to do what should be a rather simple operation in my mongoDB/Node environment. Every document in the collection I'm targeting has a field "openBalance", which is a number value. All I want to do is find the totalOpenBalance by adding all of those together.
So far, in reviewing the MongoDB documentation, both $add and $sum seem to be used to perform an operation on the individual documents within the collection, rather than on the collection itself.
This leads me to wonder, is there a different way I should approach this? I've tried numerous constructions, but none work. Here is my function in full:
exports.getClientData = async function (req, res, next) {
let MongoClient = await require('../config/database')();
let db = MongoClient.connection.db;
let search, skip, pagesize, page, ioOnly = false, client;
let docs = [];
let records = 0;
if (_.isUndefined(req.params)) {
skip = parseInt(req.skip) || 0;
search = JSON.parse(req.search);
pagesize = parseInt(req.pagesize) || 0;
page = parseInt(req.page) || 0;
client = req.client || '';
ioOnly = true;
}
else {
skip = parseInt(req.query.skip) || 0;
search = req.body;
pagesize = parseInt(req.query.pagesize) || 0;
page = parseInt(req.query.page) || 0;
client = req.query.client || '';
}
search = {};
if (skip === 0) {
skip = page * pagesize;
}
if (client) {
let arrClient = [];
arrClient = client.split(",");
if (arrClient) {
// convert each ID to a mongo ID
let mongoArrClient = arrClient.map(client => new mongo.ObjectID(client));
if (mongoArrClient) {
search['client._id'] = { $in: mongoArrClient };
}
}
}
console.log(search);
let counter = 0;
let count = await db.collection('view_client_data').find(search).count();
let totalClients = await db.collection('view_client_data').find(search).count({ $sum: "client._id" });
console.log('totalClients', totalClients);
let totalOpenBalance = await db.collection('view_client_data').find(search).count({ $sum: { "$add" : "openBalance" } });
console.log('totalOpenBalance', totalOpenBalance);
db.collection('view_client_data').find(search).skip(skip).limit(pagesize).forEach(function (doc) {
counter ++; {
console.log(doc);
docs.push(doc);
}
}, function (err) {
if (err) {
if (!ioOnly) {
return next(err);
} else {
return res(err);
}
}
if (ioOnly) {
res({ sessionId: sessID, count: count, data: docs, totalClients: totalClients, totalOpenBalance: totalOpenBalance });
}
else {
res.send({ count: count, data: docs, totalClients: totalClients, totalOpenBalance: totalOpenBalance });
}
});
}
As you can see in the above code, I am getting the total number of clients with this code:
let totalClients = await db.collection('view_client_data').find(search).count({ $sum: "client._id" });
console.log('totalClients', totalClients);
That works perfectly, adding up the instances of a client and giving me the total.
Again, to be crystal clear, where I'm running into a problem is in summing up the numerical value for all of the openBalance values. Each document has a field, openBalance. All I want to do is add those up and output them in a variable titled totalOpenBalance and pass that along in the response I send, just like I do for totalClients. I have tried numerous options, including this:
let totalOpenBalance = await db.collection('view_client_data').find(search).count({ $sum: { "$add" : "openBalance" } });
console.log('totalOpenBalance', totalOpenBalance);
and this:
let totalOpenBalance = await db.collection('view_client_data').find(search).aggregate({ $sum: { "$add" : "openBalance" } });
console.log('totalOpenBalance', totalOpenBalance);
... but as I say, none work. Sometimes I get a circular reference error, sometimes an aggregate is not a function error, other times different errors. I've been wracking my brain trying to figure this out -- and I assume it shouldn't be that complicated once I understand the required syntax. How can I get my totalOpenBalance here?
By the way, the documents I'm targeting look something like this:
{
"_id": "3hu40890sf131d361f1ad908",
"client": {
"_id": "4ft9d366121j04563be0b01d6",
"name": {
"first": "John",
"last": "Smith"
}
},
"openBalance": 128,
"lastPurchaseDate": "2018-01-19T00:00:00.000Z"
},
$sum is an accumulator operator that must appear within a $group or $project aggregate pipeline stage. To also incorporate your search filter, you can include a $match stage in your pipeline.
let result = await db.collection('view_client_data').aggregate([
{$match: search},
{$group: {_id: null, totalOpenBalance: {$sum: '$openBalance'}}}
]).next();
console.log(result.totalOpenBalance);
I think $group is what you're looking for.
So for example to calculate all the openBalance fields
db.view_client_data.aggregate(
[
{
$group: {
_id : null
totalOpenBalance: { $sum: "$openBalance" },
}
}
]
)
this should give you an object back like {totalOpenBalance: 900}
Here is the mongodb documentation for some more examples
https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group

Categories

Resources