Find document based on field value on single subdocument in array - javascript

I have a Mongoose collection called WorkoutUser with the following simplified schema:
const WorkoutUserSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
exercises: [{
exercise: {
type: Schema.Types.ObjectId,
ref: 'Exercise',
},
sets: [// not important],
date: Date, // <- important
}],
});
For reasons I won't get into here, I'd like to query the WorkoutUser collection to find all instances of a workout tied to the current user that is prior to the current workout. The date is not defined on the WorkoutUser for other reasons that aren't relevant here, but the date on each exercise will be the same.
I'd like to do something like the following:
const workouts = await db.WorkoutUser
.find({
_id: { $ne: workout._id },
user: workout.user,
'exercises.exercise': exercise.exercise,
'exercises.exercise.date': { $lt: userWorkout.date }, // <- relevant line
});
This does not work currently. Is there a way to basically search on the date for exercises.exercise[0].date?

Yes, actually exercise and date both are at the same level so you can modify your query like this
const workouts = await db.WorkoutUser
.find({
_id: { $ne: workout._id },
user: workout.user,
'exercises.exercise': exercise.exercise,
'exercises.date': { $lt: userWorkout.date }, // <- relevant line
});

Related

Creating a GET request that displays all reviews based on the item value of what they reviewed

I am currently working on making a GET request on my backend that maps out all reviews based on the item number attached to the review. I've made a few dummy reviews that share the same item number (but have different _id values) and am trying to figure out how to create this GET request.
I can get all reviews to map out in general but not by item number
My current code block is this and I'm getting the error
TypeError: Cannot read properties of undefined (reading 'get')
router.get("/:id", async (req, res) => {
const populateQuery = [
{
path: "user",
select: ["username"],
},
{
path: "reviews",
select: ["item"],
populate: { path: "user", select: ["username"] },
},
]
const reviews = await Review.find({})
response.json(reviews.map((review) => review.toJSON()))
})
An example review would be:
_id:615323ee97dc7d1960350e33
text:"This could've been worse but I'm glad they actually tried this time"
item:168
_id:615b95dfd6a4fc3814139ad4
text:"Delete this review and this game"
item:168
_id:615b968e9dafdbead0de3fde
text:"Witcher is amazing"
item:4062
Basically, I want the function to show the ones with item:168 mapped out and separate from the Witcher review.
My model schema for reviews is this
import mongoose from "mongoose"
const { ObjectId } = mongoose.Schema.Types
const reviewSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
maxlength: 500,
},
author: {
type: ObjectId,
ref: "User",
},
created: {
type: Date,
default: Date.now,
},
item: {
type: Number,
required: true,
}
},
{ timestamps: true }
)
const Review = mongoose.model("Review", reviewSchema)
export default Review

Updating embedded documents in mongoose

I have a user and a premium schema. The premium one is embedded in user.
const premiumSchema = new mongoose.Schema(
{
subscriptionID: {
type: String,
required: true,
},
guild: {
type: Object,
default: {},
},
payment_id: {
type: String,
required: true,
},
expiry: {
type: Date,
require: true,
},
reminded: {
type: Boolean,
default: false,
},
},
{ collection: "premium" }
);
const userSchema = new mongoose.Schema({
discordId: {
type: String,
required: true,
},
accessToken: {
type: String,
required: true,
},
premium: {
type: [premiumSchema],
required: false,
},
});
I am trying to update a value of my premium collection by:
const user = await User.findOne({
"premium._id": new ObjectId(req.body.premiumId),
});
const premium = user.premium.id(new ObjectId(req.body.premiumId));
premium.reminded = true;
await user.save();
The Problem is that it updates the reminded attribute in my user.premium array but doesn't update it in the premium collection itself. Is there anything I am doing wrong?
You can use findOneAndUpdate method and positional operator to directly update your user.
Positional operator $ allows you to iterate through the array and update only the corresponding subdocument
const savedUser = await User.findOneAndUpdate(
{_id: new ObjectId(req.body.premiumId)},
{$set: {'premium.$.reminded': true}}
);
See doc: https://docs.mongodb.com/manual/reference/operator/update/positional/
This way you can update the subdocument within the User collection, but the value in the Premium collection will not be updated.
This is because when you define the premium property within the Users collection you are telling mongoose (and mongodb) to save the object within the user collection itself.
In this case there are no references between the two collections: the data will be saved only in the user collection, intended as subdocuments.
You are just defining schema for child won't create a separate collection for the children in the database instead you will embed the whole child document in the parent (user).
Example data:
user1: {
discordId: { 'abc' },
accessToken: { 'token' },
premium: {
subscriptionID: { 'idSubscription' }
guild: { },
payment_id: { 'idPayment' },
expiry: { '2021-07-31T20:41:19.618+00:00' },
reminded: { true }
},
});
The two User and Premium collections remain completely separate.
If you want to upgrade the Premium collection as well, you can do it using two different calls, one for User and one for Premium.

Mongoose deleteMany subdocuments and related subdocuments

I have a document Project with an array of subdocuments, with a schema Tasks. Tasks has an array of subdocuments with a schema Comments.
const projectSchema = new Schema({
_id: Schema.Types.ObjectId,
name: { type: String, required: true, unique: true },
description: { type: String, default: '' },
tasks: [{ type: Schema.Types.ObjectId, ref: 'Threads' }]
});
module.exports = mongoose.model('Project', projectSchema);
const tasksSchema = new Schema({
projectId: { type: Schema.Types.ObjectId },
_id: Schema.Types.ObjectId,
title: { type: String, required: true },
text: { type: String, required: true },
comments: [{ type: Schema.Types.ObjectId, ref: 'Replies' }]
})
module.exports = mongoose.model('Tasks', tasksSchema);
const commentSchema = new Schema({
taskId: { type: Schema.Types.ObjectId },
_id: Schema.Types.ObjectId,
text: { type: String, required: true }
})
module.exports = mongoose.model('Comment', commentSchema);
When I delete the Project document I want to delete every Task and every Comment relate to that project.
To delete the Project I use findOneAndDelete so I set up a post middleware to delete all the Tasks
projectSchema.post('findOneAndDelete', function(doc, next) {
mongoose.model('Tasks').deleteMany({ projectId: doc._id }).exec();
next();
})
But now I don’t know how to delete every comment, because deletemany returns an object with the result of the operation.
Should I map the array of Tasks and call findOneAndDelete every time and then delete every single comment? It looks very inefficient for a lot of tasks.
How about embedding comments in post? since its one to many(not huge) relation. So in your code where you delete a project, you first delete all posts, which contain all the comments, only after it succeeds you delete the project. It will also benefit your read performance significantly because you just have to return a single post document instead of multiple(1post + many comment) documents.
Embedding post to project could also be possible, but depending on the size and number of possible posts, its probably better to keep it as a separate document.
In this case you need some logic to ensure consistency.
Here you could use mongodb's new feature, transaction. But I think for this case a transaction is not necessary.(Also I find it quite unstable for now) You could go with the "eventual consistency" method.
Basically you just delete all the posts related to a project and then delete a project. And then you run batches to check for any inconsistency.(check if there are any posts where its project doesnt exist. If it doestnt then delete the posts)

Mongoose find inside nested schema

In mongoose we are deeply searching inside a nested schema, without much success. Every time we run this function we always get an empty array returned.
function findAlarms(lastUpdate = new Date(0), record = Record) {
// For docs on find http://mongoosejs.com/docs/queries.html
return record
.find({
// Date due must be less than "now"
'documents.alarm.date_due': {
$lte: Date.now(),
},
// Must be greater than the last update and less than "now"
'documents.alarm.date_reminder.reminder': {
$gte: lastUpdate,
$lte: Date.now(),
},
})
.populate('documents')
.exec();
}
Our schemas, greatly summarized, look like this:
const RecordSchema = new mongoose.Schema({
documents: [
{
type: Schema.Types.ObjectId,
ref: 'Document',
},
],
});
And our documents schema, similarly summarized looks like this:
const DocumentSchema = new mongoose.Schema({
alarm: {
date_due: { type: Date },
date_reminder: [
{
reminder: { type: Date },
},
],
},
});
This search returns no matching elements, even though we know there are documents that match. If we modify our findAlarms method to use the documents schema:
function findAlarms(lastUpdate = new Date(0), document = Document) {
// For docs on find http://mongoosejs.com/docs/queries.html
return document
.find({
// Date due must be less than "now"
'alarm.date_due': {
$lte: Date.now(),
},
// Must be greater than the last update and less than "now"
'alarm.date_reminder.reminder': {
$gte: lastUpdate,
$lte: Date.now(),
},
})
.exec();
}
It will return all of our matching documents. However, having records is essential for our needs. Now, I could use a hack and then find records using the array of document._ids that return.
Nonetheless, I would love to know if there's an approach where we can find using the records directly, since adding that extra step feels really hacky, and this operation runs every 5 minutes so I'd love to be more efficient wherever posible.

Relational database design to mongoDB/mongoose design

I have recently started using mongoDB and mongoose for my new node.js application. Having only used relational databases before I am struggling to adapt to the mongoDB/noSQL way of thinking such as denormalization and lack of foreign key relationships. I have this relational database design:
**Users Table**
user_id
username
email
password
**Games Table**
game_id
game_name
**Lobbies Table**
lobby_id
game_id
lobby_name
**Scores Table**
user_id
game_id
score
So, each lobby belongs to a game, and multiple lobbies can belong to the same game. Users also have different scores for different games. So far for my user schema I have the following:
var UserSchema = new mongoose.Schema({
username: {
type: String,
index: true,
required: true,
unique: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
}
});
So my question is, how would I go about structing the relational design into mongoDB/mongoose schemas? Thanks!
EDIT 1
I have now tried to create all the schemas but I have no idea if this is the right approach or not.
var UserSchema = new mongoose.Schema({
_id: Number,
username: {
type: String,
index: true,
required: true,
unique: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
scores: [{ type: Schema.Types.ObjectId, ref: 'Score' }]
});
var GameSchema = new mongoose.Schema({
_id: Number,
name: String
});
var LobbySchema = new mongoose.Schema({
_id: Number,
_game: { type: Number, ref: 'Game' },
name: String
});
var ScoreSchema = new mongoose.Schema({
_user : { type: Number, ref: 'User' },
_game : { type: Number, ref: 'Game' },
score: Number
});
Mongoose is designed in such a way that you can model your tables relationally with relative ease and populate relational data based on the ref you defined in the schema. The gotcha is that you need to be careful with populating. If you populate too much or nest your populations you will run into performance bottle necks.
Your approach in Edit 1 is largely correct however you usually don't want to populate a remote ref based on a Number or set the _id of a model to a Number since mongo uses it's own hashing mechanism for managing the _id, this would usually be an ObjectId with _id implied. Example as shown below:
var ScoreSchema = new mongoose.Schema({
user : { type: Schema.Types.ObjectId, ref: 'User' },
game : { type: Schema.Types.ObjectId, ref: 'Game' },
score: Number
});
If for some reason you need to maintain a number id for your records consider calling it uid or something that won't conflict with mongo / mongoose internals. Good luck!
First of all, you are hitting on some good points here. The beauty of Mongoose is that you can easily connect and bind schemas to a single collection and reference them in other collections, thus getting the best of both relational and non-relational DBs.
Also, you wouldn't have _id as one of you properties, Mongo will add it for you.
I've made some changes to your schemas using the mongoose.Schema.Types.ObjectId type.
var UserSchema = new mongoose.Schema({
username: {
type: String,
index: true,
required: true,
unique: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
scores: [{ type: Schema.Types.ObjectId, ref: 'Score' }]
});
var GameSchema = new mongoose.Schema({
name: String
});
var LobbySchema = new mongoose.Schema({
_game: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Game'
},
name: String
});
var ScoreSchema = new mongoose.Schema({
_user : {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
_game : {
type: mongoose.Schema.Types.ObjectId,
ref: 'Game'
},
score: Number
});
This will allow you to query your database and populate any referenced collections and objects.
For example:
ScoreSchema.find({_id:##userIdHere##})
.populate('_user')
.populate('_game')
.exec(function(err, foundScore){
if(err){
res.send(err)
} else {
res.send(foundScore)
}
}
This will populate the related user and game properties.
As you edited the post, I think it would be good. At least not bad :)
Check Mongoose Query Population. It's very useful to get related data.
var mongoose = require('mongoose'),
ObjectId = mongoose.Schema.Types.ObjectId
// code, code, code
function something(req, res) {
var id = req.params.id
// test id
return Lobby.findOne({_id: new ObjectId(id)})
.populate('_game')
.exec(function(error, lobby) {
console.log(lobby._game.name);
});
}
Two ways (that I know of). You store an id (that is indexed) and once you query the first table, you then query the second table to grab info from that, as there are no joins. This means that if you grab say, user id's from one table, you will then need to make multiple queries to the user table to get the user's data.
The other way is to store it all in one table, even if it's repetitive. If all you need to store is for example, a user's screen name with something else, then just store it with the other data, even if it's already in the user table. I'm sure others will know of better/different ways.

Categories