Is it possible to simplify this code with collection polymorphism? - mongodb

I have to different collections Characters, and Souls, that share a lot of the same attributes and used within the same context. This means that every time I want to read/write to these collections, Ill have to do a "type check", and then duplicate the code twice as seen below. Is there a way to accomplish
Polymorphic.update()..
instead of
(Pseudocode)
if target.is(Character)
same logic..
Character.update(same query/fields)..
else
same logic..
Soul.update(same query/fields)..
Complete code below
#
# Adds a new condition instance to target
#
addCondition: (effect, target) ->
name = effect.name
maxDuration = effect.maxDuration
curDuration = if effect.curDuration then effect.curDuration else maxDuration
maxStack = effect.maxStack
curStack = if effect.curStack then effect.curStack else 1
stackable = if maxStack then true else false
if target.location <--- This is my type check, only the Character collection has a location field
character = Characters.findOne({_id: target._id, 'conditions.name': name}, { fields: {'conditions.$': 1} })
if character then existingCondition = character.conditions[0]
if existingCondition and stackable
# Apply additional stack and refresh duration
if existingCondition.curStack < existingCondition.maxStack
Characters.update({ _id: target._id, 'conditions.name': name }, { $inc: { 'conditions.$.curStack': 1 }, $set: { 'conditions.$.curDuration': maxDuration } })
else
Characters.update({ _id: target._id, 'conditions.name': name }, { $set: { 'conditions.$.curDuration': maxDuration } })
else if existingCondition and !stackable
Characters.update({ _id: target._id, 'conditions.name': name }, { $set: { 'conditions.$.curDuration': maxDuration } })
else
effect = _.extend(effect, {curDuration: curDuration, curStack: curStack})
Characters.update(_id: target._id, {$addToSet: { conditions: effect }})
else
soul = Souls.findOne({_id: target._id, 'conditions.name': name}, { fields: {'conditions.$': 1} })
if soul then existingCondition = soul.conditions[0]
if existingCondition and stackable
# Apply additional stack and refresh duration
if existingCondition.curStack < existingCondition.maxStack
Souls.update({ _id: target._id, 'conditions.name': name }, { $inc: { 'conditions.$.curStack': 1 }, $set: { 'conditions.$.curDuration': maxDuration } })
else
Souls.update({ _id: target._id, 'conditions.name': name }, { $set: { 'conditions.$.curDuration': maxDuration } })
else if existingCondition and !stackable
Souls.update({ _id: target._id, 'conditions.name': name }, { $set: { 'conditions.$.curDuration': maxDuration } })
else
effect = _.extend(effect, {curDuration: curDuration, curStack: curStack})
Souls.update(_id: target._id, {$addToSet: { conditions: effect }})

Just add a type (/class/collection) field to your doc:
Character.prototype.type = ->
Character
Soul.prototype.type = ->
Soul
...
target.type.update ...

Related

mongodb aggregation where document field is less than another field

Using mongoose, I'm trying to make a query that searches for tasks where timeSpent is greater than timeBilled.
Task schema:
const myTaskSchema = new Schema({
date: { type: Date, default: Date.now },
timeSpent: { type: Number },
timeBilled: { type: Number }
})
The query I've tried:
myTaskSchema.aggregate([
{
$match: {
timeSpent: { $gt: '$timeBilled' }
}
}
])
.then(data => {
console.log(data)
})
But I'm getting zero results (I know there should be results)
NOTE: Not every task has a timeSpent or timeBilled.field if that matters.
here is my dirty solution. It'd be nice if I didnt have to add a field but this gets me where I want to be.
myTaskSchema.aggregate([
{
$addFields: {
needToBill: { $gt: ['$timeSpent', '$timeBilled'] }
}
},
{
$match: {
needToBill: true
}
},
{
$project: {
timeSpent: 1,
timeBilled: 1
}
}
])

Update field using previous value (mongodb) [duplicate]

Is it possible, using mongoose middleware, to increment two fields one with a condition and the other without? In this case i want to increment "stats.ratings" by one, if the user inserts an input greater than 0, else increment zero.
"stats.answered" always increments one
See code below
module.exports.updateStats = function (req, res) {
var rating = parseInt(req.body.rating, 10);
var wasRated;
if (rating > 0) {
wasRated = true;
} else wasRated = false
Collection.findOneAndUpdate({
_id: req.body._id
}, {
$cond: {
if: wasRated,
then: {
$inc: {
"stats.answered": 1,
"stats.ratings": 1
}
},
else: {
$inc: {
"stats.answered": 1,
"stats.ratings": 0
}
}
}
},
function (err, doc) {
if (err)
throw err;
res.status(200);
})
}
What you can do is this:
// define the default case
var update = {
$inc: {
"stats.answered": 1
}
};
if(parseInt(req.body.rating, 10) > 0) {
// override default in some cases
update = {
$inc: {
"stats.answered": 1,
"stats.ratings": 1
}
}
}
and then
Collection.findOneAndUpdate({
_id: req.body._id
}, update,
function (err, doc) {
if (err)
throw err;
res.status(200);
})
}

Select a same field from a list of similar nested fields in mongoose

I have a schema.
const placeSchema = new Schema({
description: {
fr: String,
en: String,
},
comment: {
fr: String,
en: String,
},
...
...
});
const Place= mongoose.model('Place', placeSchema);
module.exports = Place;
If I want to get only 'en' value I am currently using
await Place.find({}, '-description.fr -comment.fr ...')
If the number of similar fields increases so does the length of the query. Is there a way to select all the similar fields like maybe $field.fr?
Technically yes there is a way. using $objectToArray and doing some structure manipulation.
It would look something like this:
db.collection.aggregate([
{
$match: {} //match your document.
},
{
$addFields: {
rootArr: {
$objectToArray: "$$ROOT"
}
}
},
{
$unwind: "$rootArr"
},
{
$match: {
"rootArr.v.en": {
$exists: true
}
}
},
{
$group: {
_id: "$_id",
data: {
$push: {
k: "$rootArr.k",
v: "$rootArr.v.en"
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: "$data"
}
}
}
])
Mongo Playground
It's a little "hacky" thought, how strict are your schema needs?
Have you considered building it under the following structure?:
const placeSchema = new Schema({
data: [
{
lang: String,
description: String,
comment: String,
...
}
]
});
The following aggregation will check all the top level fields for a subfield en. If it's truthy (should work if you strictly have string values for the language properties), the subfield will be { field: { en: fieldValue.en } } otherwise it will be { field: fieldValue }
db.collection.aggregate([
{
$replaceRoot: {
newRoot: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$$ROOT" },
in: {
k: "$$this.k",
v: {
$cond: [
"$$this.v.en", // works for string values, otherwise you will have to check more explicitly
{
en: "$$this.v.en"
},
"$$this.v"
]
}
}
}
}
}
}
}
])
Mongo Playground
Both the answers above are exactly what the question was looking for. This might be a more 'hacky' way of doing things.
First create a function that generates the query string '-description.fr -comment.fr ...'
let select = '';
const selectLanguage = (fields, lang) => {
switch (true) {
case lang === 'fr':
fields.forEach(field => {
select= `${select} -${field}.en `;
});
break;
case lang === 'en':
fields.forEach(field => {
select = `${select} -${field}.fr `;
});
break;
default:
break;
}
return select;
}
This generates a string like ' -fieldName1.fr -fieldName2.fr ..' for english and and ' -fieldName1.en ..' for french. Then we can use this statement in the query above.
const select = selectLanguage(['description', 'comment', ..], 'en')
await Place.find({}, select) //await Place.find({}, ' -description.fr -comment.fr ..')

How can I retrieve current week's data on Mongoose?

I have a Model for all requests that I get for my business (real estate).
I want to create an aggregation middleware based on the "createdAt" field which returns only models created between monday at midnight and the present time.
I have looked through all previous questions but could not find anything and the aggregation docs in mongo are so huge I cannot wrap my head around!
Do you have any tips?
I have tried this but it's returning an empty array:
getGestionaleStats = async (req, res) => {
try {
const richieste = await Richiesta.aggregate([
{
$match: {
createdAt: { $lt: new Date() }
}
},
{
$group: {
_id: { $week: '$createdAt' }
}
}
]);
res.status(200).json({
status: 'success',
data: { richieste }
});
} catch (err) {
console.error(err.message);
res.status(500).json({
status: 'error',
data: err.message
});
}
Of course the {$lt: new Date()} is probably where the problem lies.
Any suggestions?
This solution uses Aggregation query and a custom JavaScript function. The function takes a date object and returns the first Monday's date before today. This is used to get all the documents with createdAt date after the calculated date.
// Function returns the date of the "last Monday" from
// the given input date.
function getLastMonday(dt) {
let n = null; // last Monday conversion
switch (dt.getDay()) {
case 0: n = -5; break;
case 1: n = -6; break;
case 2: n = 0; break;
case 3: n = -1; break;
case 4: n = -2; break;
case 5: n = -3; break;
case 6: n = -4; break;
default: "This never happens";
}
let today_date = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate());
let last_monday_date = today_date.setDate(today_date.getDate() + n );
return last_monday_date;
}
var d = ISODate(); // -or- any date like ISODate("2019-11-26T00:00:00Z")
var LAST_MONDAY = getLastMonday(d);
db.test.aggregate( [
{
$addFields: {
last_monday: {
$dateFromParts : {
year: { $year: new Date(LAST_MONDAY) },
month: { $month: new Date(LAST_MONDAY) },
day: { $dayOfMonth: new Date(LAST_MONDAY) }
}
},
created_at: {
$dateFromParts : {
year: { $year: "$createdAt" },
month: { $month: "$createdAt" },
day: { $dayOfMonth: "$createdAt" }
}
}
}
},
{
$match: { $expr: { $gt: [ "$created_at", "$last_monday" ] } }
},
{
$project: { created_at: 0, last_monday: 0 }
}
] )
For a set of input documents like this:
{ _id : 1, createdAt : ISODate("2019-12-03T00:00:00Z") }
{ _id : 2, createdAt : ISODate("2019-11-12T02:00:00Z") }
{ _id : 3, createdAt : ISODate("2019-11-25T05:00:00Z") }
{ _id : 4, createdAt : ISODate("2019-11-26T00:00:00Z") }
{ _id : 9, createdAt : ISODate("2019-12-02T23:45:00Z") }
And, LAST_MONDAY = getLastMonday(ISODate("2019-12-04T05:40:20Z")), the aggregation query returns the document with _id : 1.
I'm using momentJS for this:
const result = Collection.aggregate([
{
$match: {
createdAt: {
$gte: moment().startOf('isoweek').toDate(),
$lt: moment().endOf('isoweek').toDate()
},
}
}
]);
I've found an answer with vanillaJS:
const richieste = await Richiesta.aggregate([
{
$match: {
createdAt: { $gte: getBeginningOfTheWeek(new Date()), $lt: new Date() }
}
},
{
$group: {
_id: null,
count: { $sum: 1 }
}
},
]}
where getBeginningOfTheWeek is as such:
exports.getBeginningOfTheWeek = (now) => {
const days = (now.getDay() + 7 - 1) % 7;
now.setDate(now.getDate() - days);
now.setHours(0, 0, 0, 0);
return now;
};
The latter function is from T.J. Crowder: get current week moday javascript

MongoDB + Mongoose Aggregate w/ Asnyc

I've got the following route in my express file, which takes parameters passed in from a middleware function and queries my backend MongoDB database. But for some reason, it only ever returns an empty array.
I'd like to convert the Mongoose model that allows me to use aggregate functions into async/await to conform with the rest of my code. It's online here.
module.exports = {
search: asyncWrapper(async(req, res, next) => { // Retrieve and return documents from the database.
const {
filterTarget,
filter,
source,
minDate,
maxDate,
skip,
limit,
sortBy,
sortOrder
} = req.search;
try {
const mongoData = await Model.aggregate([
{
$match: {
date: {
$gt: minDate, // Filter out by time frame...
$lt: maxDate
}
}
},
{
$match: {
[filterTarget]: filter // Match search query....
}
},
{
$set: {
[filterTarget]: { $toLower: `$${filterTarget}` } // Necessary to ensure that sort works properly...
}
},
{
$sort: {
[sortBy]: sortOrder // Sort by date...
}
},
{
$group: {
_id: null,
data: { $push: "$$ROOT" }, // Push each document into the data array.
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
count: 1,
data: {
$slice: ["$data", skip, limit]
},
}
}
])
return res.status(200).json({ data: mongoData.data || [], count: mongoData.count || 0 });
} catch (err) {
next(err);
}
})
};
For some reason, the route is only returning an empty array every time. I've double and triple checked my variables, they are not the problem.
How can I use the Mongoose.aggregate() function in an async await route?