NodeJS + MongoDB mapping relational data in toJSON() - node.js

I have three models (collections): Project, User and Category.
The Project Schema looks like this:
var ProjectSchema = new mongoose.Schema({
title: { // title of the project
type: String
},
_category : {
type: mongoose.Schema.Types.ObjectId
},
participants: [
{
_user: {
type: mongoose.Schema.Types.ObjectId
},
invitedDate: {
type: Date
}
]
}
The User model has the usual properties: name, email, (hashed) password, address etc.
The Category model has the properties: name, description.
What is an efficient way to retrieve certain properties from User and Category inside the toJSON method of the Project model?
So, for example, when I GET the projects, I don't want the response to return the ObjectID but rather the name and email address of the User, and the name and description of the Category.

You can override toJson method and delete properties which you dont want to return.
Lets take this example. If you dont want to return userId then delete it from definition of toJson method
UserSchema.methods.toJSON = function() {
var obj = this.toObject()
delete obj.userId
return obj
}
Just like this, you can change for any schema.

Related

How to model a collection in nodejs+mongodb

Hello I am new to nodejs and mongodb.
I have 3 models:
"user" with fields "name phone"
"Shop" with fields "name, address"
"Member" with fields "shop user status". (shop and user hold the "id" of respective collections).
Now when I create "shops" api to fetch all shop, then I need to add extra field "isShopJoined" which is not part of the model. This extra field will true if user who see that shop is joined it otherwise it will be false.
The problem happens when I share my model with frontend developers like Android/iOS and others, They will not aware of that extra field until they see the API response.
So is it ok if I add extra field in shops listing which is not part of the model? Or do I need to add that extra field in model?
Important note
All the code below has NOT been tested (yet, I'll do it when I can setup a minimal environment) and should be adapted to your project. Keep in mind that I'm no expert when it comes to aggregation with MongoDB, let alone with Mongoose, the code is only here to grasp the general idea and algorithm.
If I understood correctly, you don't have to do anything since the info is stored in the Member collection. But it forces the front-end to do an extra-request (or many extra-requests) to have both the list of Shops and to check (one by one) if the current logged user is a Member of the shop.
Keep in mind that the front-end in general is driven by the data (and so, the API/back-end), not the contrary. The front-end will have to adapt to what you give it.
If you're happy with what you have, you can just keep it that way and it will work, but that might not be very effective.
Assuming this:
import mongoose from "mongoose";
const MemberSchema = new mongoose.Schema({
shopId: {
type: ObjectId,
ref: 'ShopSchema',
required: true
},
userId: {
type: ObjectId,
ref: 'UserSchema',
required: true
},
status: {
type: String,
required: true
}
});
const ShopSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
address: {
//your address model
}
});
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
phone: {
type: String,
required: true,
},
// Add something like this
shopsJoined: {
type: Array,
default: [],
required: true
}
});
You could tackle this problem via 2 ways:
MongoDB Aggregates
When retrieving (back-end side) the list of shops, if you know the user that made the request, instead of simply returning the list of Shops, you could return an aggregate of Shops and Members resulting in an hybrid document containing both the info of Shops and Models. That way, the front-end have all the info it needs with one back-end request.
Important note
The following code might not work as-is and you'll have to adapt it, I currently have nothing to test it against. Keep in mind I'm not very familiar with aggregates, let alone with Mongoose, but you'll get the general idea by looking the code and comments.
const aggregateShops = async (req, res, next) => {
try {
// $lookup will merge the "Model" and "Shop" documents into one
// $match will return only the results matching the condition
const aggreg = await Model.aggregate({$lookup: {
from: 'members', //the name of the mongodb collection
localField: '_id', //the "Shop" field to match with foreign collection
foreignField: 'shopId', //the "Member" field to match with local collection
as: 'memberInfo' //the field name in which to store the "Member" fields;
}, {
$match: {memberInfo: {userId: myUserId}}
}});
// the result should be an array of object looking like this:
/*{
_id: SHOP_OBJECT_ID,
name: SHOP_NAME,
address: SHOP_ADDRESS,
memberInfo: {
shopId: SHOP_OBJECT_ID,
userId: USER_OBJECT_ID,
status: STATUS_JOINED_OR_NOT
}
}*/
// send back the aggregated result to front-end
} catch (e) {
return next(e);
}
}
Drop the Members collection and store the info elsewhere
Instinctively, I would've gone this way. The idea is to either store an array field shopsJoined in the User model, or a membersJoined array field in the Shops model. That way, the info is retrieved no matter what, since you still have to retrieve the Shops and you already have your User.
// Your PATCH route should look like this
const patchUser = async (req, res, next) => {
try {
// How you chose to proceed here is up to you
// I tend to facilitate front-end work, so get them to send you (via req.body) the shopId to join OR "un-join"
// They should already know what shops are joined or not as they have the User
// For example, req.body.shopId = "+ID" if it's a join, or req.body.shopId = "-ID" if it's an un-join
if (req.body.shopId.startsWith("+")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $push: { shopsJoined: req.body.shopId } }
);
} else if (req.body.shopId.startsWith("-")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $pull: { shopsJoined: req.body.shopId } }
);
} else {
// not formatted correctly, return error
}
// return OK here depending on the framework you use
} catch (e) {
return next(e);
}
};
Of course, the above code is for the User model, but you can do the same thing for the Shop model.
Useful links:
MongoDB aggregation pipelines
Mongoose aggregates
MongoDB $push operator
MongoDB $pull operator
Yes you have to add the field to the model because adding it to the response will be only be a temporary display of the key but what if you need that in the future or in some list filters, so its good to add it to the model.
If you are thinking that front-end will have to be informed so just go it, and also you can set some default values to the "isShopJoined" key let it be flase for the time.

Save entity with id created in code, mongoose

How can I do to pass the id of a document from outside and not that mongoose generates it?
I need the objects to be stored with the id I tell them, but mongoose overwrites it and saves it with the one he wants. I tried several ways now and nothing happens.
It is an entity that I am distributing through events in several databases, so I need it to be stored with the id I want to maintain the integrity of the data.
Now I have this and it says "document must have an _id before saving", but the id I have already put it, does not recognize it.
The scheme is like this:
const schema = new Schema({
_id: { type: String },
name : { type: String },
});
I also tried with this, and the error is the same:
const schema = new Schema({
_id: { type : String },
name : { type: String },
},
{
_id: false
});
I am passing the object like this:
Item.create({ _id: 'my uuid here', name: 'something' });
but when it is saved it remains with the id generated by mongoose replacing mine, that is, it changes it to me with a _id: '5twt563e3j5i34knrwnt43w'
Your syntax should work, but sometimes mongoose acts weird.
You can try this syntax (works on my project) :
const item = new Item({ name: 'something' });
item._id = 'my uuid here';
await item.save();
Instead of using a random uuid, you need to use a mongoDB objectID. Mongoose can also create that,
var mongoose = require('mongoose');
var id = mongoose.Types.ObjectId();
Store this id in the collection,
Item.create({ _id: id, name: 'something' });

Mongoose findOneAndUpdate cast error with custom _id

I have my Person schema like this :
const schema = new mongoose.Schema({
_id: Number,
name: String,
birthday: Date,
sex: String
});
schema.pre('findOneAndUpdate', async function (next) {
try {
let counter = await Counters.findByIdAndUpdate('person',
{
$inc: {
value: 1
}
},
{ new: true}
);
this._update._id = counter.value;
next();
}
catch (err) {
next(err);
}
});
The problem is when I try to add some new persons with findOneAndUpdate and upsert: true, it generates a CastError: Cast to ObjectId failed for value "18" at path "person".
My _id is defined as a Number so I don't understand why it's trying to cast it to an ObjectId ?
Update :
I found my problem, the Person model is referenced in some other model but I forgot to change the ref type in the other model...
person: {
type: Number, //HERE
ref: 'person',
required: true
}
You can change the type of the_id property although ins't a good approach, but actually you can't change the value since it's immutable and represents the primary key of the document. Keep in mind that _id is very important for MongoDB life cycle, like indexing. If you aim to change an Entity key, you can create other property, something like person_id.
_id is an auto generated property for MongoDB. If you want to add try a different name for the Id attribute like "personId" or you can use the auto generated Id by MongoDB without creating a seperate Id.

Can't populate when referencing a SubDocument in mongoose

I have a database with three relevant models, here are the mongoose schemas
// models/company.js
let companySchema = new Schema({
rut : String,
name : String,
owner : { type: Schema.Types.ObjectId, ref : 'User' }
})
export default mongoose.model('Company', companySchema)
// models/account.js
let accountSchema = new Schema({
code : String,
name : String,
type : {
id : Number,
name : String
}
})
// I have two different exports, because sometimes I just need the schema,
// and others I need the model
export default accountSchema
export const Account = mongoose.model('Account', accountSchema)
// models/mayor.js
let mayorSchema = new Schema({
account : { type: Schema.Types.ObjectId, ref : 'Account' },
company : { type: Schema.Types.ObjectId, ref : 'Company' },
month : Number,
year : Number,
data : [{
date : Date,
detail : String
}]
})
export default mongoose.model('Mayor', mayorSchema)
I want to query my Mayor model from mayorSchema, my query is like this
import {Account} from '../models/account'
import Company from '../models/company'
import Mayor from '../models/mayor'
Mayor
.find({ 'company' : companyId, 'month' : month, 'year' : year })
.populate('company')
.populate('account')
.exec((err, mayores) => {
if (err) {
console.log(JSON.stringify(err))
res.status(500)
res.json({
code: "001",
message: "Some error with the server. Please try later"
})
} else {
res.status(200)
res.json(mayores)
}
})
The result of that query is an array, but every object in the array has the account property set to null. I don't know why is that happening, if I query the data directly on mongo console, all is fine, all the documents has correctly set their account property. Some things that might be useful to know:
I'm using express.js, part of the notation is from express.
My account property is correctly populated
The account schema is a subdocument (embedded) in another model, but the (account) model is correctly registered in mongoose.
The documents in mayors collection where previously inserted with Mayor.insertMany method, and after that insert, if I inspect the just created documents, they have the account field correctly set.
So why is this happening? why can't I populate my account field. I guess that it is something related to referencing on sub documents, but I would have expected that a failed population would return just an ObjectId, not null.

How can i use an if statement in a mongoose query?

i have a mongodb collection named Feed and it has an attribute named "type". according to that string, i want to send a changeable fields with json. For example if type is "photo" i want to do somethig like that
schema.find({number: "123456"},"body number",
function(err, data) {
but if the string is story, instead of photo; İn the same 'schema.find' query,it should create a json with "body url" instead of "body number". and they all should be passed with the same json.
res.json(data);
For a clear example, i want my json to be like this. as you se the fields change according to "type". but they are all actually in the same collection.
[
{
type: 'photo',
number: 123456,
url: 'asd.jpg',
},
{
type: 'story',
body: 'hello',
number: 123456,
}
]
So basically you want to return certain documents fields from the Feed collection, which are specified in a variable like e.g. "firstName pic points photos".
Are there Feed documents with the story field?
The Model.find() does not create any schema.
Maybe edit with further code so we can understand the command.
For document-specific JSON formatting like this, you can override the default toJSON method of your Feed model as shown in this gist.
UPDATE
If you want this sort of flexibility in your documents then it's even easier. Just define your schema to include all possible fields and then only set the fields that apply to given document for its type. The fields that you don't use won't appear in the document (or in the JSON response). So your schema would look like:
var feedSchema = new Schema({
type: { type: 'String' },
venue: Number,
url: String,
body: String
});
Take a look to mongoose-schema-extend. Using the 'Discriminator Key' feature, you can instruct .find() to create the proper model in each individual case.
Your code should look like this (not tested):
var feedSchema = new Schema({
venue: Number,
}, {discriminatorKey : 'type' }});
var photoSchema = feedSchema.extend({
url: String
});
var storySchema = feedSchema.extend({
body: String
});
var Feed= mongoose.model('feed', feedSchema );
var Photo= mongoose.model('photo', photoSchema );
var Story= mongoose.model('story', storySchema );
//'photo' and 'story' will be the values for 'type' key
Feed.find({venue: "123456"}, function(err, models) {
console.log(models[0] instanceof Photo); // true
console.log(models[0] instanceof Story); // false
console.log(models[1] instanceof Photo); // false
console.log(models[1] instanceof Story); // true
});

Resources