How to specify schema attributes for mongoose subdocument? - node.js

I'm building a plugin and need to specify attributes for one of the subdocument collection fields. Someone mentioned that statics are the official way of doing this.
So, instead of...
var familySchema = mongoose.Schema({
surName : { type: String },
members : { type: [ personSchema ], minLength: 1, maxLength: 10 }
});
... it would be this ...
var familySchema = mongoose.Schema({
surName : { type: String },
members : [ personSchema ]
});
familySchema.static('minLength', 1);
familySchema.static('maxLength', 10);
I realize there is no minLength or maxLength. The plugin would handle this.
I've been looking through the docs and can't find anything that mentions the official way. Is there a benefit of one versus the other?

You're right, there is no such thing as minLength for ref array. You have to use custom validation
var validateFamilyLength = function (members) {
return members.length > 1 && members.length < 10;
};
var familySchema = mongoose.Schema({
surName : { type: String },
members : {
type: [personSchema],
validate: [validateFamilyLength, 'Not valid members count']
}
});
But i strongly recommend to use refs instead
var familySchema = mongoose.Schema({
surName : { type: String },
members : [{
type: mongoose.Schema.ObjectId,
ref: 'Person',
validate: [validateFamilyLength, 'Not valid members count']
}]
});

Related

mongoDB returning with $numberDecimal in the response of a query

Below is my mongoose schema
const mongoose = require('mongoose');
const AccountingCostsSchema = mongoose.Schema(
{
// other properties goes here
accountCosts: {
type: mongoose.Decimal128,
set: (v) =>
mongoose.Types.Decimal128.fromString(parseFloat(v).toFixed(4)),
required: true
}
},
{
collection: 'accountingCosts'
}
);
export = mongoose.model('AccountingCosts', AccountingCostsSchema);
Data in MongoDB
accountingCosts collection
Data in Text mode view
{
"_id" : ObjectId("4e1eb38c2bdea2fb81f5e771"),
"accountId" : ObjectId("4e8c1180d85de1704ce12110"),
"accountCosts" : NumberDecimal("200.00"),
}
Data in Text mode view
My query
db.getCollection('accountingCosts').find({'accountId': '4e8c1180d85de1704ce12110'})
Result from query
"accountCosts": {
"$numberDecimal": "123.00"
}
I tried writing a getter function on schema like i have a setter function. But it is not working
get: function(value) {
return value.toString();
}
My expected output is just a plain property with name and value like below
"accountCosts": "123.00"
I am sure you found a solution, but i will also write it here so who will land here would find a solution.
Your idea to using a getter was right, but maybe you forgot to enable it in the schema, so let' s see how using your code.
You have this schema:
var AccountingCostsSchema = new Schema({
accountId: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0
}
});
So when you retrieve it you will get:
{
"_id" : "12345",
"accountId" : "123456",
"accountCosts" : {
"$numberDecimal": "123.00"
}
}
Taking in example that you would get accountCosts as number, so something like that:
{
"_id" : "12345",
"accountId" : "123456",
"accountCosts" : 123
}
We would need a getter as you said, so we will need to add a function of this getter in our model file, let' s say after your schema. This could be your getter function:
function getCosts(value) {
if (typeof value !== 'undefined') {
return parseFloat(value.toString());
}
return value;
};
Now, let' s declare this getter in our schema that will become:
var AccountingCostsSchema = new Schema({
accountId: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
}
});
Now mongoose will uderstand how you want that value is returned, but we have to specify that we want it uses the getter. So we have to enable it when we want to retieve the value in json format. We can simply add it as this:
var AccountingCostsSchema = new Schema({
accountId: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
}
}, {toJSON: {getters: true}});
And so, when you will retrieve it, you will get this:
{
"_id" : "12345",
"accountId" : "123456",
"accountCosts" : 123,
"id" : "12345"
}
But, whoa (this is not my batman glass!) what is that "id" : "12345"?
According to Mongoose documents:
Mongoose assigns each of your schemas an id virtual getter by default which returns the document's _id field cast to a string, or in the case of ObjectIds, its hexString. If you don't want an id getter added to your schema, you may disable it by passing this option at schema construction time.
How we can avoid it? We can just add id: false in our schema that now will be:
var AccountingCostsSchema = new Schema({
accountId: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
},
id: false
}, {toJSON: {getters: true}});
And now you will not see it anymore.
Just a little last tip (no, i have not finished):
What happens if you have an array of objects in your schema like this?
var AccountingCostsSchema = new Schema({
accountId: String,
usersAndCosts: [{
username: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
}
],
id: false
}, {toJSON: {getters: true}});
Bad news now: you can not keep on using this schema. Good news now: you can fix it easly. You need to create a new schema for usersAndCosts and then reference it in the parent schema. Let' s see it:
var usersAndCostsSchema = new Schema({
username: String,
accountCosts: {
type: Schema.Types.Decimal128,
default: 0,
get: getCosts
},
id: false
}, {toJSON: {getters: true}});
var AccountingCostsSchema = new Schema({
accountId: String,
usersAndCosts: [usersAndCostsSchema],
id: false
}, {toJSON: {getters: true}});
The result will be the same, your accountCosts will be shown as number, no double ids.
Remember that of course this is to enable getters for toJSON but you could need to enable them for toObject too, as you could need to do the same for setters. But we here talked about getters for JSON.

Mongoose populate a field without ObjectId?

I have a schema:
var Schema = mongoose.Schema;
var TicketSchema = new Schema({
externalId: String,
name: String,
items: [{
externalId: String,
price: Number,
quantity: {
type: Number,
default: 1
},
comment: {
type: String,
default: '',
trim: true
},
entity: {
type: String,
ref: 'Entity'
}
}],
tableId: String
});
mongoose.model('Ticket', TicketSchema);
And I want to populate entity field with an unique field other than ObjectId.
How can I achieve that?
Though late answer. Please check Populate Virtuals for Mongoose 4.5.0
Click the link below
http://mongoosejs.com/docs/populate.html
And scroll down or search for Populate Virtuals you will see it does exactly what you want.
I found Views as one useful approach, though not sure it is the most efficient! For example, in movielens database, I wanted to refer 'movieId' in ratings collection to 'movieId' in the movies collection using 'movieId' as foreign key.
db.createView('rating-movie-view','ratings',[{$lookup:{from:"movies",localField:"movieId",foreignField:"movieId",as:"ratings_movie"}},{ $project:{'userId':1,'movieId':1,'rating':1,'timestamp':1,'ratings_movie.title':1,'ratings_movie.genres':1 } }])
New view "rating-movie-view" thus created has the required fields 'title and 'genres'.
db["rating-movie-view"].findOne()
{
"_id" : ObjectId("598747c28198f78eef1de7a3"),
"userId" : 1,
"movieId" : 1129,
"rating" : 2,
"timestamp" : 1260759185,
"ratings_movie" : [
{
"title" : "Escape from New York (1981)",
"genres" : "Action|Adventure|Sci-Fi|Thriller"
}
]
}
Hope this useful!
Those who are not familiar with movielens data here are the schema
var MovieSchema = new mongoose.Schema({
movieId: Number,
title: String,
genres: String,
});
var RatingSchema = new mongoose.Schema({
userid: Number,
movieId:Number,
rating: Number,
timestamp:Number,
});
//View schema
var RatingViewSchema = new mongoose.Schema({
userid: Number,
movieId:Number,
rating: Number,
timestamp:Number,
rating_movie:{title:String,genres:String}
});
const blogs = this.blogModel
.find(find)
.populate('blogCategory', 'name -_id');
Note -_id will exclude the object _id
I'm not sure if I understood your question correctly.
In Mongoose model, in case we do not specify a primary key, it automatically adds in an extra field called ObjectId and assigns a unique value for each object.
In case we need to specify our own key, we can do it by specifying the key property.
For example:
mongoose.model('Todo', {
todoID: {
type: String,
key: true
},
text: {
type: 'text'
},
done: {
type: Boolean,
default: false,
},
date: {
type: Date,
},
items: [{
entity: {
type: String,
ref: 'Entity'
}
}]
});
I hope, this is what you meant.
If you are asking about fetching objects based on Items -> entity's property,
Todo.find({'items.entity.type':required_type}, function(err, foundTodos){
// ---
});
Thanks,
Use crypto to hash something unique like the objectId , and then save it to your entities.
Var hash = crypto.createHmac('sha256', ticket.objectId).digest('hex');
Ticket.entities= hash;

Mongoose - Save array of strings

I can't save an array of strings into my DB using Mongoose.
(Note all code below is simplified for ease of writing here)
So i declare a variable of a person schema I have:
var newPerson = new Person ({
tags: req.body.tags
});
The schema itself looks like:
var personSchema = new mongoose.Schema({
tags: Array
});
And when it comes to saving its just a simple:
newPerson.save(function(err) {
//basic return of json
});
So using Postman I send in an array in the body - however everytime I check the DB, it just shows one entry with the array as a whole i.e. how I sent it:
Any ideas what extra I'm supposed to do?
Write up from my comment:
The way to specify an array of strings in mongoose is like so:
var personSchema = new mongoose.Schema({
tags: [{
type: String
}]
However, the problem here is most-likely to do with Postman as it is sending the 'array' as a string. You can check this by checking the type of req.body.tags like so:
console.log(typeof req.body.tags)
If this returns a String, make sure to set the content-type in Postman to JSON as seen in this screenshot rather than the default 'form-data' option.
var schema = new Schema({
name: String,
binary: Buffer,
living: Boolean,
updated: { type: Date, default: Date.now },
age: { type: Number, min: 18, max: 65 },
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
decimal: Schema.Types.Decimal128,
array: [],
ofString: [String],
ofNumber: [Number],
ofDates: [Date],
ofBuffer: [Buffer],
ofBoolean: [Boolean],
ofMixed: [Schema.Types.Mixed],
ofObjectId: [Schema.Types.ObjectId],
ofArrays: [[]],
ofArrayOfNumbers: [[Number]],
nested: {
stuff: { type: String, lowercase: true, trim: true }
},
map: Map,
mapOfString: {
type: Map,
of: String
}
})
// example use
var Thing = mongoose.model('Thing', schema);
var m = new Thing;
m.name = 'Statue of Liberty';
m.age = 125;
m.updated = new Date;
m.binary = Buffer.alloc(0);
m.living = false;
m.mixed = { any: { thing: 'i want' } };
m.markModified('mixed');
m._someId = new mongoose.Types.ObjectId;
m.array.push(1);
m.ofString.push("strings!");
m.ofNumber.unshift(1,2,3,4);
m.ofDates.addToSet(new Date);
m.ofBuffer.pop();
m.ofMixed = [1, [], 'three', { four: 5 }];
m.nested.stuff = 'good';
m.map = new Map([['key', 'value']]);
m.save(callback);
On Schema:
( Since you have mentioned in the problem that it is an array of strings )
var personSchema = new mongoose.Schema({
tags:{
type:[String],
required: true
}
});
On Postman:
{
"tags": ["css", "javascript", "mongoose", "node"]
}
On MongoDB
{
"tags":["css", "javascript", "mongoose", "node"]
}
Similarly, you can create other types of primitive arrays and document arrays in the mongoose schema as:
({
toys: [ToySchema],
buffers: [Buffer],
strings: [String],
numbers: [Number]
// ... etc
});
Try changing the schema to
var personSchema = new mongoose.Schema({
tags: [{type: String}]
});
or you can use Mixed type
var personSchema = new mongoose.Schema({
tags: mongoose.Schema.Types.Mixed
});
EDIT
i think the problem is with assignment. Use:
person.tags.push("string to push");
On Schema
techs: Array
On Postman
"techs": ["express","rect","html","css","scss"]
On DB (MongoDB)
"techs" : [
"epxress",
"rect",
"html",
"css",
"scss"
]
var personSchema = new mongoose.Schema({
tags: [{type: String}]
});
Use this in the schema.
Saving the Array:
var etc = new modename({yourprimaryid: primaryid});
for (var i = 0; i < tag.length; i++) {
etc.tag.push(tag[i]);
}
etc.save(function(err) {
//whatever you want here
}
Define a Schema:
const schema = new Schema({
name: { type: String, required: true },
tags: [String]
});
In postman add each element separately using the array syntax below
name:Thing
tags[]:task
tags[]:other
tags[]:thing
Return Data:
{
"__v": 0,
"name": "Thing",
"_id": "5a96e8d0c7b7d1323c677b33",
"tags": [
"task",
"other",
"thing"
]
}
this will also work
var personSchema = new mongoose.Schema({
tags: {
type: [String], default: []
}
});
Firstly, as many people have noted, the schema needs to change to indicate that the tags field is intended to hold an array of strings, and not just a single one. So that needs to change to:
var personSchema = new mongoose.Schema({
tags: [String]
});
The other thing you need to keep in mind (and which caused me a lot of trouble), is that when saving, make sure to use a fresh array for the tags field. For example, this won't work:
person.tags[0] = "new tag";
person.save();
Instead, you need to do something like:
person.tags = person.tags.slice(); // Clone the tags array
person.tags[0] = "new tag";
person.save();
Hope this helps.
I had a simialr problem,
In the model, do this :
tags : {[String], default: undefined}
So that it defaults to undefined unstead of an empty array,
and instead of this:
const person = new Person({
tags : req.body.tags
});
Do this :
const person = new Person();
person.tags = req.body.tags;
My requirement;
ingredients: Type Array of Strings
Solution:
ingredients: {
type: [String],
},
const productSchema = new mongoose.Schema(
{
name: {
type: String,
},
description: {
type: String,
},
price: {
type: String,
},
categoryId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Category",
},
sellerId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Seller",
},
discount: {
type: String,
},
status: {
type: String,
default: "active",
enum: ["active", "inactive", "deleted"],
},
images: {
type: Array,
required: true,
},
},
{ timestamps: true }
);

Mongoose data saving without _id

I am using mongoose with node.js application. I don't want _id field in record.
I am using this code to save my record without _id field. But it is giving error
document must have an _id before saving
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var PlayerSchema = new Schema({
player_id : { type: Number },
player_name : { type: String },
player_age : { type: Number },
player_country : { type: String }
}
, { _id: false }
);
var Player = mongoose.model('Player', PlayerSchema );
var athlete = new Player();
athlete.player_id = 1;
athlete.player_name = "Vicks";
athlete.player_age = 20;
athlete.player_country = "UK";
athlete.save(function(err) {
if (err){
console.log("Error saving in PlayerSchema"+ err);
}
});
I am using mongoose version 3.8.14
Unfortunately, You can not skip having a primary key for the document but you can override the primary key content, you can define your own primary key for each document.
Try the following schema for the same.
var PlayerSchema = new mongoose.Schema({
_id : { type: Number },
player_name : { type: String },
player_age : { type: Number },
player_country : { type: String },
}
);
I have replaced your player_id with _id. Now you have control over the primary key of the document and the system won't generate the key for you.
There are some plugins which can also do the autoincremet for your primary key. https://github.com/chevex-archived/mongoose-auto-increment. You might try these as well.
Also, about the error you are getting :
Any document is an object and should be wrapped inside the curly brackets you can not define two independent object in the same document. So you are getting this error.
Copying an answer given on a similar Github issue comment:
The _id option exists to prevent one being created and assigned to
every sub-document within your document.
So, it's not for this:
var Schema = mongoose.Schema,
ThingSchema = new Schema({
name: String,
categories: [{
label: {
type: String,
uppercase: true
},
value: String
}]
}, {_id: false});
// ^^^^^^^^^
// this won't work
It's for this:
var Schema = mongoose.Schema,
ThingSchema = new Schema({
name: String,
categories: [{
label: {
type: String,
uppercase: true
},
value: String,
_id: false // <--- this works
}]
});
Took me a while but, after running into the same problem, I found the
documentation here.
instade of writing this
{
player_id : { type: Number },
player_name : { type: String },
...}
write this:-
{
_id : { type: Number },
_name : { type: String },
_age : { type: Number },
_country : { type: String }
}
hence _id will be players_id
then further write code like this:-
var athlete = new Player();
athlete._id = 1;
athlete._name = "Vicks";
athlete._age = 20;
athlete._country = "UK";
athlete.save(function(err) {
if (err){
console.log("Error saving in PlayerSchema"+ err);
}
});

Mongoose Relationship Populate Doesn't Return results

var SecuritySchema = new Mongoose.Schema({
_bids: [{
type: Mongoose.Schema.Types.ObjectId,
ref: 'BuyOrder'
}],
_asks: [{
type: Mongoose.Schema.Types.ObjectId,
ref: 'SellOrder'
}]
});
var OrdersSchema = new Mongoose.Schema({
_security: {
type: Mongoose.Schema.Types.ObjectId,
ref: 'Security'
},
price: {
type: Number,
required: true
},
quantity: {
type: Number,
required: true
}
});
// declare seat covers here too
var models = {
Security: Mongoose.model('Security', SecuritySchema),
BuyOrder: Mongoose.model('BuyOrder', OrdersSchema),
SellOrder: Mongoose.model('SellOrder', OrdersSchema)
};
return models;
And than when I save a new BuyOrder for example:
// I put the 'id' of the security: order.__security = security._id on the client-side
var order = new models.BuyOrder(req.body.order);
order.save(function(err) {
if (err) return console.log(err);
});
And attempt to re-retrieve the associated security:
models.Security.findById(req.params.id).populate({
path: '_bids'
}).exec(function(err, security) {
// the '_bids' array is empty.
});
I think this is some sort of naming issue, but I'm not sure, I've seen examples here and on the moongoose website that use Number as the Id type: http://mongoosejs.com/docs/populate.html
The ref field should use the singular model name
Also, just do:
models.Security.findById(req.params.id).populate('_bids').exec(...
My main suspicion given your snippet at the moment is your req.body.order has _security as a string instead of an array containing a string.
Also, you don't need an id property. Mongodb itself will automatically do the _id as a real BSON ObjectId, and mongoose will add id as a string representation of the same value, so don't worry about that.
While I don't understand your schema (and the circular nature of it?), this code works:
var order = new models.BuyOrder({ price: 100, quantity: 5});
order.save(function(err, orderDoc) {
var security = new models.Security();
security._bids.push(orderDoc);
security.save(function(err, doc) {
models.Security.findById({ _id: doc._id })
.populate("_bids").exec(function(err, security) {
console.log(security);
});
});
});
It:
creates a BuyOrder
saves it
creates a Security
adds to the array of _bids the new orderDoc's _id
saves it
searches for the match and populates
Note that there's not an automatic method for adding the document to the array of _bids, so I've done that manually.
Results:
{ _id: 5224e73af7c90a2017000002,
__v: 0,
_asks: [],
_bids: [ { price: 100,
quantity: 5,
_id: 5224e72ef7c90a2017000001, __v: 0 } ] }

Resources