Virtual populate mongoose - javascript

I've tried to use virtual populate between two models I created:
in this case to get all the reviews with the tour id and show them with the tour.
(when using query findById() to show only this tour)
my virtual is set to true in the Schema (I've tried to set them to true after using the virtual populate but it doesn't work - by this soultion)
after checking the mongoose documentation its seems to be right but it doesn't work.
my tourSchema:
const tourSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'A tour must have a name'], //validator
unique: true,
trim: true,
maxlength: [40, 'A tour name must have less or equal then 40 characters'],
minlength: [10, 'A tour name must have at least 10 character']
//validate: [validator.isAlpha, 'A tour name must have only alphabetic characters']
},
etc...
etc...
etc...
//guides: Array --- array of user id's || embedding
guides: [
//Reference to the user data model without saving the guides in the tour data model
//Child referencing
{
type: mongoose.Schema.ObjectId,
ref: 'User'
}
]
},
{
//passing options, getting the virual properties to the document/object
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
//Define virtual properties
tourSchema.virtual('durationWeeks').get(function () {
console.log('Virtual 1');
//using function declaration => using this keyword
return this.duration / 7;
});
//Virtual populate
tourSchema.virtual('reviews', {
ref: 'review',
localField: '_id', // Find tour where `localField`
foreignField: 'tour', // is equal to `foreignField`
//look for the _id of the tour in the tour field in review
});
my reviewSchema:
** I used in the review schema in for the tour and user populate for the tour id **
const reviewSchema = new mongoose.Schema({
review: {
type: String,
required: [true, 'Review can not be empty!']
},
rating: {
type: Number,
min: 1,
max: 5
},
createdAt: {
type: Date,
default: Date.now(),
},
tour: [
{
type: mongoose.Schema.ObjectId,
ref: 'Tour',
required: [true, 'Review must be belong to a tour.']
}
],
user: [
{
type: mongoose.Schema.ObjectId,
ref: 'User',
required: [true, 'Review must be belong to a user.']
}
]
},
{
//passing options, getting the virual properties to the document/object
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
);
//Query middleware
reviewSchema.pre(/^find/, function (next) {
this.populate({
path: 'tour',
select: 'name'
})
.populate({
path: 'user',
select: 'name'
});
next();
});
My output:
get all reviews (review model data):
{
"status": "success",
"data": {
"review": [
{
"_id": "5f6ba5b45624454efca7e0b1",
"review": "What an amzing tour",
"tour": {
"guides": [],
"_id": "5c88fa8cf4afda39709c2955",
"name": "The Sea Explorer",
"durationWeeks": null,
"id": "5c88fa8cf4afda39709c2955"
},
"user": {
"_id": "5f69f736e6eb324decbc3a52",
"name": "Liav"
},
"createdAt": "2020-09-23T19:44:52.519Z",
"id": "5f6ba5b45624454efca7e0b1"
}
]
}
}
and the get tour by id:
{
"status": "success",
"data": {
"tour": {
"startLocation": {
"type": "Point",
"coordinates": [
-80.185942,
25.774772
],
"description": "Miami, USA",
"address": "301 Biscayne Blvd, Miami, FL 33132, USA"
},
"ratingsAverage": 4.8,
"ratingsQuantaity": 0,
"images": [
"tour-2-1.jpg",
"tour-2-2.jpg",
"tour-2-3.jpg"
],
"startDates": [
"2021-06-19T09:00:00.000Z",
"2021-07-20T09:00:00.000Z",
"2021-08-18T09:00:00.000Z"
],
"secretTour": false,
"guides": [],
"_id": "5c88fa8cf4afda39709c2955",
.
.
.
.
"slug": "the-sea-explorer",
"__v": 0,
"durationWeeks": 1,
"id": "5c88fa8cf4afda39709c2955"
}
}
}
as you can see the review has the tour as an arr and the id is inside the arr of the tour is there an option that the populate is not targeting the right field?

You need an option virtuals: true passed into the schema creation:
const tourSchema = new mongoose.Schema({
...
}, {
virtuals: true
}
In addition, we use the mongoose-lean-virtuals module to help with .lean and virtuals. e.g.
const mongooseLeanVirtuals = require('mongoose-lean-virtuals');
...
tourSchema.plugin(mongooseLeanVirtuals);
tourSchema.set('toJSON', { virtuals: true });
tourSchema.set('toObject', { virtuals: true });
though I'm guessing that's not strictly necessary.

So I figure it out.
First i asked in github - mongoose repo and got answerd:
reviewSchema.pre(/^find/, function (next) {
this.populate({
path: 'tour',
options: { select: 'name' } // <-- wrap `select` in `options` here...
}).populate({
path: 'user',
options: { select: 'name photo' } // <-- and here
});
next();
});
We should improve this: nested options are very confusing and it's
hard to remember whether something should be options.select or select
The second issue was to add populate after using the FindById method in the tour controller, using the populate without using the wrap 'select' didn't work for me.
exports.getTour = catchAsync(async (req, res, next) => { //parameter => :id || optinal parameter => :id?
//populate reference to the guides in the user data model
const tour = await Tour.findById(req.params.id).populate('reviews');
if (!tour) {
return next(new AppError('No tour found with that id', 404));
}
res.status(200).json({
status: 'success',
data: {
tour
}
});
})
and in the tour model, I changed the foreign key from "tour_id" (as I saw in other questions to "tour").
//Virtual populate
tourSchema.virtual('reviews', {
ref: 'Review',
localField: '_id', // Find tour where `localField`
foreignField: 'tour' // is equal to `foreignField`
//look for the _id of the tour in the tour field in review
});
Now i do have reviews in my tour data and it does virtual populate to the tour by id

Related

mongoose array saved in wrong order or got error 'Cast to string failed for value' if schema for array is defined

i work on a productivity app with a feature that people can select several items from a list of predefined items (tasks) to build a set of personal daily tasks and optionally fill one input field to modify the duration of that selected task.
in my 'user' mongoose schema i have 'tasks' array and when the user completes his/her tasks selection, list of his/her tasks will be saved under his user details and the default list will not be modified. my problem is that when a user selects some tasks and click 'save today's tasks', array 'tasks' with selected objects is saved in the database but instead key/values pairs are saved for each object, my app saves array with keys in one object and values in other object. here is sample of saved data:
// if defined in userSchema:
// tasks: [] // just empty array
// what i get now:
"tasks": [
{
"title": [
"Title 01",
"Title 02",
],
"duration": [
10,
20,
]
}
]
// what i need:
"tasks": [
{
"title": "Title 01",
"duration": 10
},{
"title": "Title 02",
"duration": 20
}
]
// if defined in userSchema:
// tasks: [tasksSchema] // array of predefinied objects
// i got just error message
"error": {
"message": "Cast to string failed for value \"[ 'Title 01', 'Title 02' ]\" at path \"title\"",
"name": "CastError",
"stringValue": "\"[ 'Title 01', 'Title 02' ]\"",
"value": [
"Title 01",
"Title 02"
],
"path": "title",
"reason": null
}
and here is rest of my code (model, controller, view):
// my model
const tasksSchema = new mongoose.Schema({
_id: {
type: mongoose.Schema.ObjectId,
ref: 'Tasks',
},
title: {
type: String,
},
duration: {
type: Number,
},
});
const userSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
required: true,
},
tasks: [tasksSchema],
}
{
timestamps: true,
});
module.exports = mongoose.model('User', userSchema);
// my controller
exports.updateUser = async (req, res, next) => {
try {
const user = await User.findOne({ _id: req.params.id });
await User.findOneAndUpdate(
{ _id: req.params.id },
req.body,
{ new: true, runValidators: true },
).exec();
if (!user) {
return res.status(404).json({
success: false,
error: 'no User found',
});
}
return res.redirect('back');
} catch (err) {
if (err.name === 'ValidationError') {
const messages = Object.values(err.errors).map((val) => val.message);
return res.status(400).json({
success: false,
error: messages,
});
}
return res.status(500).json({
success: false,
error: err,
});
}
};
// my view (pug/jade)
each item in user.tasksList || [] // this list is generated from a model 'Tasks'
li.sortableItem.ui-state-default.list-group-item(value=item._id id=`id_${item._id}` name="tasks")
span
input#title(type='text' name="tasks[title]" value=item.title)
span
input#duration.col-sm-2.input-sm.text-gray(type='number', name="tasks[duration]" value=item.duration)
what am I doing wrong?
many thnx!
If the framework doesn't have a simple way to return these inputs as an array of objects, you can use map to create an array of objects from the separate arrays before instantiating the user:
tasks = tasks[0].title.map((e,i) => ({"title":e,"duration":tasks[0].duration[i]}))

Mongoose + MongoDB - Cast to ObjectID failed for value after try save with POSTMAN

I'm trying to save an object that references other 2 objects in mongoDB, but I'm not getting it. Whenever I try, I get this message.
For this, this using POSTMAN to test the API that I am creating.
{
"message": "Order validation failed: payments.0.credit_card.card: Cast to ObjectID failed for value \"{ number: '4898308633754712',\n holder_name: 'Test test',\n exp_month: 1,\n exp_year: 2022,\n cvv: '1234' }\" at path \"credit_card.card\", customer: Cast to ObjectID failed for value \"{ name: 'Test Test', email: 'test#gmail.com' }\" at path \"customer\""
}
The json object I'm trying to save in mongoDB:
{
"items": [{
"name": "Plano s",
"amount": 12345,
"description": "Descrição do Plano Sanarflix",
"quantity": 1
}],
"customer": {
"name": "Test Test",
"email": "test#gmail.com"
},
"payments": [{
"payment_method": "credit_card",
"credit_card": {
"installments":1,
"capture": true,
"card": {
"number": "4898308633754712",
"holder_name": "Test Test",
"exp_month": 1,
"exp_year": 2022,
"cvv": "1234"
}
}
}]
}
This is the model order I defined:
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Model for order
const schema = new Schema({
customer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Customer',
required: function(){
return this.customer_id;
}
},
items: [{
amount: {
type: Number,
required: true
},
description: {
type: String,
trim: true
},
quantity: {
type: Number,
required: true
}
}],
payments: [{
payment_method: {
type: String,
required: true,
trim: true
},
credit_card: {
installments: {
type: Number,
default: 1
},
capture: {
type: Boolean,
default: true
},
card: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Card'
}
},
}]
});
module.exports = mongoose.model('Order', schema);
What is wrong with it? Because when I import the same json to MongoDB with Studio3 Mongo Manager, I can save it and see the object in the correct way.
an object that references other 2 objects, yes, ObjectId does references, yet you're passing the whole objects.
Three options:
pass the id of customer and card objects (if they exist already)
insert these new data in their respective collections then insert your order using the created IDs (likely the solution)
rewrite your schema to have only the Order collection that would hold everything (not really great).
Also, having card numbers transiting through your server is really dangerous (legally speaking), especially if you're to store them... check out how the payment service you're implementing handles transactions: on any decent service the server never sees banking data, discharging your company of any liability.

How to search through mongoose ObjectId in feathers?

I have two feathers services, one for profiles and the other one for labels.
A profile can have array of ObjectId labels from other collections.
Now I have an input for search and a user types "linux"
The profile foo should be returned because it contains the id "594ceeff6905e622425f523b" in the labels array.
This kind of search query through ObjectId between objects is possible through feathers?
Profiles
 Mongoose model
{
name: { type: String, trim: true, required: true },
labels: [{ type: ObjectId, ref: 'Labels' }],
}
Feathers api get response to profiles
get http://localhost:3030/profiles
{
"name" : "foo",
"labels" : [
"594ceeff6905e622425f523b",
"594ceeff6905e622425f523c",
"594ceeff6905e622425f523d"
],
}
{
"name" : "bar",
"labels" : [
"594ceeff6905e622425f523e",
"594ceeff6905e622425f523d"
],
}
Labels
 Mongoose model
{
name: { type: String, trim: true, unique: true, required: true },
}
Feathers api get response to labels
get http://localhost:3030/labels
{
"_id": "594ceeff6905e622425f523b",
"name": "linux"
},
{
"_id": "594ceeff6905e622425f523c",
"name": "systemd"
},
{
"_id": "594ceeff6905e622425f523d",
"name": "mongodb"
},
{
"_id": "594ceeff6905e622425f523e",
"name": "javascript"
}
Now I have to populate all the labels on the profiles response, send all the profiles and then filter them on the front with that value of the input for search.
As the database grows this is going to be very inefficient, it has to exist a better way of doing this right?
You can try code like this
Profile.find({}).populate({
path: 'labels',
match: {
name: {
$regex: new RegExp(searchText, 'i');
//searchText: passed from the front end.
}
}
}).then(function(profiles){
var filteredProfiles = profiles.forEach(function(profile){
return profile.labels; //will be null for items don't match the
//searching regex.
//resolve the filtered profiles to the front end.
})
},function(error){
//Error handling
})
Feathers does not restrict you on anything that you can do with Mongoose itself and for what you would like to do you can use the Mongoose query population.
The feathers-mongoose adapter supports this through the $populate query parameter so querying
http://localhost:3030/labels?$populate=labels
Should do what you are looking for.
I the end I just two calls to the api like this:
computed: {
...mapState('profiles', { profiles: 'keyedById' }),
...mapState('labels', { labels: 'keyedById' }),
},
methods: {
...mapActions('profiles', { findProfiles: 'find' }),
async fetch() {
const labels = this.labels
const search = this.search_input.toLowerCase()
// Generates an array of matched labels per profile
const labels_id = Object.keys(labels).filter(label => {
const name = labels[label].name.toLowerCase()
return name.includes(search)
})
// Searches profiles by name or labels
this.findProfiles({
query: {
$or: [
{
name: { $regex: search, $options: 'igm' },
},
{ labels: { $in: labels_id } },
],
$populate: ['user'],
$sort: { updatedAt: -1 },
},
})
},
},

How to Avoid Sequelize JS Associations Including both the Foreign Key and the Included Object

How can I avoid showing both the foreignKey that sequelize creates and the eagerly fetched object through includes?
I have the following model structure:
FormEntry:
owner: User
createdBy: User
modifiedBy: User
formEntryData: [FormEntryData]
I modeled it after reading through SequelizeJS docs and came up with the following:
const User = sequelize.define('user', {
id: {
type: Sequelize.BIGINT(20),
field: 'user_id',
primaryKey: true
},
emailAddress: {
type: Sequelize.STRING(256),
field: 'email_address'
}
}, {
tableName: 'users',
timestamps: false
});
const FormEntryData = sequelize.define('formEntryData', {
id: {
type: Sequelize.BIGINT(20),
field: 'id',
primaryKey: true
},
entryId: {
type: Sequelize.BIGINT(20),
field: 'entry_id'
},
...
}, {
tableName: 'formEntryData',
timestamps: false
});
const FormEntry = sequelize.define('formEntry', {
id: {
type: Sequelize.BIGINT(20),
field: 'entry_id',
primaryKey: true
},
...
}, {
tableName: 'formEntries',
timestamps: false
});
I then need to create the associations to tie the models together and after a lot of trial and error I came up with the following:
FormEntry.hasMany(FormEntryData, {foreignKey: 'entry_id', as: 'FormEntryData'});
FormEntry.belongsTo(User, {foreignKey: 'created_by', as: 'CreatedBy'});
FormEntry.belongsTo(User, {foreignKey: 'modified_by', as: 'ModifiedBy'});
FormEntry.belongsTo(User, {foreignKey: 'owner', as: 'Owner'});
I then was able to query the data by doing the following:
FormEntry.findByPrimary(1472280, {
include: [
{
model: FormEntryData,
as: "FormEntryData"
},
{
model: User,
as: "CreatedBy"
},
{
model: User,
as: "Owner"
},
{
model: User,
as: "ModifiedBy"
}
]
})
Unfortunately, my results seem kind of repetitive as it seems to be including both the foreign key and the object that is eagerly fetched.
{
"id": 1472280,
...
"created_by": 26508, <-- repetitive (don't want this)
"modified_by": 26508, <-- repetitive (don't want this)
"owner": null, <-- repetitive (don't want this)
"FormEntryData": [
{
"id": 27164476,
"entryId": 1472280, <-- repetitive (but I want this one)
...
"entry_id": 1472280 <-- repetitive (don't want this)
},
...
],
"CreatedBy": { <-- repetitive (but I want this one)
"id": 26508,
"emailAddress": "swaraj.kler#greywallsoftware.com"
},
"Owner": null, <-- repetitive (but I want this one)
"ModifiedBy": { <-- repetitive (but I want this one)
"id": 26508,
"emailAddress": "swaraj.kler#greywallsoftware.com"
}
}
You need to exclude specified fields from the query
FormEntry.findByPrimary(1472280, {
include: [
{
model: FormEntryData,
as: "FormEntryData",
attributes: { exclude: ['entry_id'] }
},
{
model: User,
as: "CreatedBy"
},
{
model: User,
as: "Owner"
},
{
model: User,
as: "ModifiedBy"
}
],
attributes: { exclude: ['owner', 'created_by', 'modified_by'] }
})

Validations for associative models sailsJs

I am trying to build a REST API on sailsjs v0.11.0, I am looking for a way to validate all POST requests. Validation works for simple models.
Simple model example:
category.js
module.exports = {
attributes: {
name: {
type: 'string',
required: true, // this works, the POST data is validated, if this field is missing, sails responds with an error json
unique: true
}
}
}
Associative one to many model example where validation doesnt work:
Chapter.js
module.exports = {
attributes: {
name: 'string',
categoryId: 'integer',
pages: {
type: 'string',
required: true // Sails saves the record to DB even if this field is missing.
},
owner: {
model: 'Upload'
}
}
};
Upload.js
module.exports = {
attributes: {
draftId: 'integer',
chapters: {
collection: 'Chapter',
via: 'owner'
}
}
};
EDIT:
I got it to work with the following updated Chapter.js model, but if the associated model fails validation, the sails server responds with a 500 status and a error json as shown below, While this is not an error, It should have sent a 400 status.
Updated Chapter.js:
module.exports = {
attributes: {
name: {
type: 'string',
required: true
},
categoryId: {
type: 'integer',
required: true
},
pages: {
type: 'string',
required: true
},
owner: {
model: 'Upload'
}
}
};
The Error with 500 status:
{
"error": "E_UNKNOWN",
"status": 500,
"summary": "Encountered an unexpected error",
"raw": [
{
"type": "insert",
"collection": "chapter",
"values": {
"name": "chapeterOne",
"pages": "2,3,4,5",
"owner": 12
},
"err": {
"error": "E_VALIDATION",
"status": 400,
"summary": "1 attribute is invalid",
"model": "Chapter",
"invalidAttributes": {
"categoryId": [
{
"rule": "integer",
"message": "`undefined` should be a integer (instead of \"null\", which is a object)"
},
{
"rule": "required",
"message": "\"required\" validation rule failed for input: null"
}
]
}
}
}
]
}
Is there a way to make the error message more sensible?

Categories