Mongoose self referenced schema not creating ObjectId for all sub documents - node.js

I have a Schema in mongoose that have a self referenced field, like this:
var mongoose = require('mongoose');
var CollectPointSchema = new mongoose.Schema({
name: {type: String},
collectPoints: [ this ]
});
when a CollectPoint object is inserted:
{
"name": "Level 1"
}
it's ok, the result is like expected:
{
"_id": "58b36c83b7134680367b2547",
"name": "Level 1",
"collectPoints": []
}
But when I insert self referenced sub documents,
{
"name": "Level 1",
"collectPoints": [{
"name": "Level 1.1"
}]
}
It gives me this:
{
"_id": "58b36c83b7134680367b2547",
"name": "Level 1",
"collectPoints": [{
"name": "Level 1.1"
}]
}
Where is the _id of the child CollectPointSchema? I need this _id.

You should build a new object when declaring your embedded CollectPoint items :
var data = new CollectPoint({
name: "Level 1",
collectPoints: [
new CollectPoint({
name: "Level 1.1",
collectPoints: []
})
]
});
This way the _id and collectPoints will be created by the instanciation of CollectPoint otherwise, you are just creating a plain JSONObject.
To avoid those kind of issues, build a validator for your array that will trigger an error if its items have wrong type :
var CollectPointSchema = new mongoose.Schema({
name: { type: String },
collectPoints: {
type: [this],
validate: {
validator: function(v) {
if (!Array.isArray(v)) return false
for (var i = 0; i < v.length; i++) {
if (!(v[i] instanceof CollectPoint)) {
return false;
}
}
return true;
},
message: 'bad collect point format'
}
}
});
This way the following will trigger an error :
var data = new CollectPoint({
name: "Level 1",
collectPoints: [{
name: "Level 1.1",
collectPoints: []
}]
});

Related

Mongoose NodeJS Express - How to Push Data To a Specific Sub Document Object Array

thank you in advance for any help.
My problem is essentially to add data to a specific sub document.
I have the following models in my NodeJS server:
MODELS
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const dataSchema = new Schema({
time: Date,
value: String
});
const nodeSchema = new Schema({
name: String,
description: String,
number: Number,
status: String,
lastSeen: Date,
data: [dataSchema]
});
const siteSchema = new Schema({
code: String,
name: String,
description: String,
totalNodes: Number,
nodes: [nodeSchema]
});
const Site = mongoose.model('site',siteSchema);
module.exports = Site;
Which basically looks like this:
Example
{
"_id": "5fa169473a394829bc485069",
"code": "xfx3090",
"name": "Name of this site",
"description": "Some description",
"totalNodes": 2,
"__v": 0,
"nodes": [
{
"_id": "5fa1af361e085b516066d7e2",
"name": "device name",
"description": "device description",
"number": 1,
"status": "Offline",
"lastSeen": "2020-11-03T19:27:50.062Z",
"data": [
{
"Date": "2019-01-01T00:00:00.000Z",
"value": "12"
},
{
"Date": "2019-01-01T00:00:00.000Z",
"Value": "146"
}
]
},
{
"_id": "5fa1b10f4f24051520f85a58",
"name": "device name",
"description": "device description",
"number": 2,
"status": "Offline",
"lastSeen": "2020-11-03T19:35:43.409Z",
"data": [
{
"Date": "2019-01-01T00:00:00.000Z",
"Value": "555"
}
]
}
]
}
]
As you can see I have created two dummy nodes with some random data.
My question now is, say I want to add some data to Node 1. How will this code look?
I've tried many variations and attempted many different things without any luck. I know this would be easier by using the Object Id's, but I was hoping there is a way around this.
My Best result so far was with this code, but unfortunately it doesn't add any data.
addNodeData: async (req,res,next) => {
const {siteCode} = xfx3090; //req.params
const { nodeNumber } = 1; //req. params - just to show example
const nodeData = await Site.findOneAndUpdate({'code': siteCode, 'node.number': nodeNumber}, {$push: {'data':{'time': Date.now(), 'value':1223}}});
res.status(200).json({message:'success'});
}
Thank you in advance!
You need the positional operator $.
The query you want is something like this:
db.collection.update({
"_id": "5fa169473a394829bc485069",
"nodes._id": "5fa1af361e085b516066d7e2"
},
{
"$push": {
"nodes.$.data": {
"Date": "newDate",
"value": "newValue"
}
}
})
The first part is to find the document. I'm assuming nodes._id is not unique so I match _id too.
Then, with the pointer in the document you want to add the new data, you use $push into nodes.$.data. So, in the filed data there will be a new object.
A mongo plauground example is here

What is the best approach for referencing objectIds plus additional parameters in a schema?

I am creating a schema for an Order model that will track the items ordered along with the quantity purchased. I want to keep the itemId references and the quantity tied together as an array in one parameter.
I have created an Array that includes a reference to the ObjectId plus an additional Number type. I am currently unable to populate the product information using a .populate() query.
Order Schema
const mongoose = require("mongoose");
const { Schema } = mongoose;
const orderSchema = new Schema({
orderNumber: String,
_itemsOrdered: [
{
itemId: {
type: mongoose.Schema.Types.ObjectId,
ref: "menuItems"
},
quantity: Number
}
]
});
mongoose.model("orders", orderSchema);
MenuItem Schema
const mongoose = require("mongoose");
const { Schema } = mongoose;
const MenuItemSchema = new Schema({
imageURL: String,
name_en: String,
name_es: String,
type_en: String,
type_es: String,
description_en: String,
description_es: String,
dietaryCallouts: [String],
price: Number
});
mongoose.model("menuItems", MenuItemSchema);
module.export = MenuItemSchema;
I am able to save the record but cannot populate the MenuItem information with the following query:
Order Controller
async show(req, res, next) {
try {
const orderId = req.params.id;
let order = await Order.findById({ _id: orderId }).populate(
"_itemsOrdered.itemId"
);
res.send(order);
} catch (err) {
res.status(402).send(err);
}
}
Here it the order object that is being saved to the DB.
Order Object
{
"_id": "5dc93b9c0085b8045e0c8aa3",
"orderNumber": "Order 3",
"_itemsOrdered": [
{
"_id": "5dc93b9c0085b8045e0c8aa5",
"itemId": "5dc7f814a2679b47319a79a4",
"quantity": 1
},
{
"_id": "5dc93b9c0085b8045e0c8aa4",
"itemId": "5dc7e5c7de590744c46f93da",
"quantity": 2
}
],
"__v": 0
}
Your order schema must be like this:
const orderSchema = new Schema({
orderNumber: String,
_itemsOrdered: [
{
itemId: { type: mongoose.Schema.Types.ObjectId, ref: "menuItems" },
quantity: Number
}
]
});
And you can use the following route to create an order document.
router.post("/order", async (req, res, next) => {
try {
const { orderNumber, _itemsOrdered } = req.body;
let order = new Order({ orderNumber, _itemsOrdered });
order = await order.save();
res.status(201).send(order);
} catch (err) {
console.log(err);
res.status(500).send(err);
}
});
Sample body: (you need to change ids according to yours)
{
"orderNumber": "Order 1",
"_itemsOrdered": [
{"itemId": "5dc90346222b892434e4675a", "quantity" : 1 },
{"itemId": "5dc90359222b892434e4675b", "quantity" : 2 }
]
}
To get the order and its items you can use populate like this:
router.get("/orders/:id", async (req, res) => {
try {
const orderAndItems = await Order.findById(req.params.id).populate(
"_itemsOrdered.itemId"
);
res.send(orderAndItems);
} catch (err) {
console.log(err);
res.status(500).send(err);
}
});
This will give you a result like this:
{
"_id": "5dc904db8407a217b4dfe6f4",
"orderNumber": "Order 1",
"_itemsOrdered": [
{
"_id": "5dc904db8407a217b4dfe6f6",
"itemId": {
"_id": "5dc90346222b892434e4675a",
"name_en": "item 1",
"price": 1,
"__v": 0
},
"quantity": 1
},
{
"_id": "5dc904db8407a217b4dfe6f5",
"itemId": {
"_id": "5dc90359222b892434e4675b",
"name_en": "item 2",
"price": 2,
"__v": 0
},
"quantity": 2
}
],
"__v": 0
}

How to use different types of References and populate

So i have two schemas, Article and Event
Both have an image field.
For Article,
featured_image: {
type: String,
default: '',
}
For Event,
featured_image: {
type: Schema.ObjectId,
ref: 'Medium'
}
I have another schema, Card, like this
type: {
type: String,
enum: ['Article', 'Event']
},
data: {
type: Schema.ObjectId,
refPath: 'type'
}
I am trying to populate the cards, like this
Card
.find(query)
.populate({
path: 'data',
populate: [{
path: 'featured_image',
model: 'Medium',
select: 'source type'
}]
};)
However, it keeps giving me a cast error, because when card is of type Event, it populates fine, but when it's of type 'Article', featured_image field is of string type and hence cannot be populated.
How do i populate featured_image field only if card is of type Event or it's a reference id, instead of string.
Instead of what you are attempting to do you should be using "discriminators", which is in fact the correct way to handle a relationship where the object types vary in the reference given.
You use discriminators by the different way in which you define the model, which instead constructs from a "base model" and schema as in:
const contentSchema = new Schema({
name: String
});
const articleSchema = new Schema({
image: String,
});
const eventSchema = new Schema({
image: { type: Schema.Types.ObjectId, ref: 'Medium' }
});
const cardSchema = new Schema({
name: String,
data: { type: Schema.Types.ObjectId, ref: 'Content' }
});
const Medium = mongoose.model('Medium', mediumSchema);
const Card = mongoose.model('Card', cardSchema )
const Content = mongoose.model('Content', contentSchema);
const Article = Content.discriminator('Article', articleSchema);
const Event = Content.discriminator('Event', eventSchema);
So instead you define a "base model" such as Content here which you actually point the references to within Event.
The next part is that the differing schema are actually registered to this model via the .discriminator() method from the base model, as opposed to the .model() method. This registers the schema with the general Content model in such a way that when you refer to any model instance defined with .discriminator() that a special __t field is implied to exist in that data, using the registered model name.
Aside from enabling mongoose to .populate() on different types, this also has the advantage of being a "full schema" attached to the different types of items. So you have have different validation and other methods as well if you like. It is indeed "polymorphism" at work in a database context, with helpful schema objects attached.
Therefore we can demonstrate both the varied "joins" that are done, as well as that you can now both use the individual models for Article and Event which would deal with only those items in all queries and operations. And not only can you use "individually", but since the mechanism for this actually stores the data in the same collection, there is also a Content model which gives access to both these types. Which is in essence how the main relation works in the definition to the Event schema.
As a full listing
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.set('debug',true);
mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost/cards');
const mediumSchema = new Schema({
title: String
});
const contentSchema = new Schema({
name: String
});
const articleSchema = new Schema({
image: String,
});
const eventSchema = new Schema({
image: { type: Schema.Types.ObjectId, ref: 'Medium' }
});
const cardSchema = new Schema({
name: String,
data: { type: Schema.Types.ObjectId, ref: 'Content' }
});
const Medium = mongoose.model('Medium', mediumSchema);
const Card = mongoose.model('Card', cardSchema )
const Content = mongoose.model('Content', contentSchema);
const Article = Content.discriminator('Article', articleSchema);
const Event = Content.discriminator('Event', eventSchema);
function log(data) {
console.log(JSON.stringify(data, undefined, 2))
}
async.series(
[
// Clean data
(callback) =>
async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
// Insert some data
(callback) =>
async.waterfall(
[
(callback) =>
Medium.create({ title: 'An Image' },callback),
(medium,callback) =>
Content.create(
[
{ name: "An Event", image: medium, __t: 'Event' },
{ name: "An Article", image: "A String", __t: 'Article' }
],
callback
),
(content,callback) =>
Card.create(
[
{ name: 'Card 1', data: content[0] },
{ name: 'Card 2', data: content[1] }
],
callback
)
],
callback
),
// Query and populate
(callback) =>
Card.find()
.populate({
path: 'data',
populate: [{
path: 'image'
}]
})
.exec((err,cards) => {
if (err) callback(err);
log(cards);
callback();
}),
// Query on the model for the discriminator
(callback) =>
Article.findOne({},(err,article) => {
if (err) callback(err);
log(article);
callback();
}),
// Query on the general Content model
(callback) =>
Content.find({},(err,contents) => {
if (err) callback(err);
log(contents);
callback();
}),
],
(err) => {
if (err) throw err;
mongoose.disconnect();
}
);
And the sample output for different queries
Mongoose: cards.find({}, { fields: {} })
Mongoose: contents.find({ _id: { '$in': [ ObjectId("595ef117175f6850dcf657d7"), ObjectId("595ef117175f6850dcf657d6") ] } }, { fields: {} })
Mongoose: media.find({ _id: { '$in': [ ObjectId("595ef117175f6850dcf657d5") ] } }, { fields: {} })
[
{
"_id": "595ef117175f6850dcf657d9",
"name": "Card 2",
"data": {
"_id": "595ef117175f6850dcf657d7",
"name": "An Article",
"image": "A String",
"__v": 0,
"__t": "Article"
},
"__v": 0
},
{
"_id": "595ef117175f6850dcf657d8",
"name": "Card 1",
"data": {
"_id": "595ef117175f6850dcf657d6",
"name": "An Event",
"image": {
"_id": "595ef117175f6850dcf657d5",
"title": "An Image",
"__v": 0
},
"__v": 0,
"__t": "Event"
},
"__v": 0
}
]
Mongoose: contents.findOne({ __t: 'Article' }, { fields: {} })
{
"_id": "595ef117175f6850dcf657d7",
"name": "An Article",
"image": "A String",
"__v": 0,
"__t": "Article"
}
Mongoose: contents.find({}, { fields: {} })
[
{
"_id": "595ef117175f6850dcf657d6",
"name": "An Event",
"image": "595ef117175f6850dcf657d5",
"__v": 0,
"__t": "Event"
},
{
"_id": "595ef117175f6850dcf657d7",
"name": "An Article",
"image": "A String",
"__v": 0,
"__t": "Article"
}
]

Create index in Mongoose for array of objects

I have two models setup:
ShopsModel
var ShopsSchema = new mongoose.Schema({
name: {
type: String,
required: true
}
});
var ShopsModel = mongoose.model("Shops", ShopsSchema);
FieldGroupsModel
var FieldGroupsSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
fields: [{
label: {
type: String,
required: true
},
handle: {
type: String,
required: true
}
}],
shop: {
type: mongoose.Schema.Types.ObjectId,
ref: "Shops"
}
});
var FieldGroupsModel = mongoose.model("FieldGroups", FieldGroupsSchema)
Each FieldGroups instance has a ShopsModel associated with it.
I need to create an index for the FieldGroupsModel fields[i].handle value, this index needs two rules; it needs to be unique to each FieldGroupsModel instance, so this data would be invalid:
{
title: "Some field group title here",
fields: [
{
label: "Some label 1"
handle: "some-label-1"
},
{
label: "Some label 1"
handle: "some-label-1" // Error: `some-label-1` already exists as a value of `fields[i].handle`.
}
],
shop: {
"$oid": "1"
}
}
The second rule is that the first rule should only be in place for FieldGroupsModel instances which share the same shop value. So this data would be invalid:
// First bit of data
{
title: "Some field group title here",
fields: [
{
label: "Some label 1"
handle: "some-label-1"
}
],
shop: {
"$oid": "1"
}
}
// Second bit of data
{
title: "Another field group title here",
fields: [
{
label: "Some label 1"
handle: "some-label-1" // Error: `some-label-1` already exists as a value of `fields[i].handle` of a document which shares the same `shop` value.
}
],
shop: {
"$oid": "1"
}
}
However, this would be valid:
// First bit of data
{
title: "Some field group title here",
fields: [
{
label: "Some label 1"
handle: "some-label-1"
}
],
shop: {
"$oid": "1"
}
}
// Second bit of data
{
title: "Another field group title here",
fields: [
{
label: "Some label 1"
handle: "some-label-1" // This is valid because there's no other documents with the same `shop` value with the same `fields[i].handle` value.
}
],
shop: {
"$oid": "2"
}
}
I'm quite new to Mongo and Mongoose so any help here would be greatly appreciated! :)
You call the index method on your Schema object to do that as shown here. For your case it would be something like:
FieldGroupsSchema.index({"shop": 1, "fields.handle": 1}, {unique: true});
Please read the MongoDB documentation about Compound Indexes for more detail.

Referance multiple documents Mongoose

I'm trying to connect 3 different documents using mongoose (mainly to learn how to use it), I've set up 3 different schemas as follows:
(all of them are in there own files)
const Books = new Schema({
title: { type: String, required: true, unique: true },
description: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Authors' },
stores: [{
available: Number,
store: { type: mongoose.Schema.Types.ObjectId, ref: 'Stores' }
}]
});
exports.Books = mongoose.model('Books', Books);
const Authors = new Schema({
name: { type: String, required: true },
description: String,
born: Date,
died: { type: Date, default: null }
});
exports.Authors = mongoose.model('Authors', Authors);
const Stores = new Schema({
name: { type: String, required: true, unique: true },
description: String,
address: String
});
exports.Stores = mongoose.model('Stores', Stores);
and then I add the following data:
author.name = 'Jonathan Swift';
author.description = 'old satiric bloke';
author.born = new Date(1667, 10, 30);
author.died = new Date(1745, 9, 19);
author.save()
.catch((err) => console.log(err))
.then(() => console.log('author saved'));
store.name = 'Book shop 1';
store.description = 'This is where the cool kids buy all there books!';
store.address = 'La La Land, 123 Combaja street';
store.save()
.catch((err) => console.log(err))
.then(() => console.log('store saved'));
book.title = 'gullivers travels';
book.author = '58a79345711f2350999e0911'; // _id from the author save
book.description = 'A book about a big guy in a small world';
book.stores = [{
available: 8,
store: '58a79345711f2350999e0912' // _id from the store save
}];
book.save()
.catch((err) => console.log(err))
.then(() => console.log('store saved'));
The problem I find is that when I run book.find() it returns:
[
{
"_id": "58a795b8298b50527412815c",
"description": "A book about a big guy in a small world",
"author": {
"_id" : "58a79345711f2350999e0911",
"born" : -9532947600000,
"died" : -7075130400000,
"description" : "old satiric bloke",
"name" : "Jonathan Swift",
"__v" : 0
},
"title": "gullivers travels",
"__v": 0,
"stores": [
{
"available": 8,
"store": "58a79345711f2350999e0912",
"_id": "58a795b8298b50527412815d"
}
]
}
]
what I was expecting/hoping for was to get the entire store as well the same way as with the author, have I missed something or how should I go about this to achieve the expected result?
I've tried populate but without success
In your Books model author & stores.store are reference, you shouldn't have author field populated if you don't use populate().
If you specify directly the _id of your store & author you have just created :
var author = new Authors({
name: 'Jonathan Swift',
description: 'old satiric bloke',
born: new Date(1667, 10, 30),
died: new Date(1745, 9, 19)
});
author.save();
var store = new Stores({
name: 'Book shop 1',
description: 'This is where the cool kids buy all there books!',
address: 'La La Land, 123 Combaja street'
});
store.save();
var book = new Books({
title: 'gullivers travels',
author: author._id, // _id from the author save
description: 'A book about a big guy in a small world',
stores: [{
available: 8,
store: store._id // _id from the store save
}]
})
book.save();
Books.find() gives the expected result :
[
{
"_id": "58a7a2529a8b894656a42e00",
"title": "gullivers travels",
"author": "58a7a2529a8b894656a42dfe",
"description": "A book about a big guy in a small world",
"__v": 0,
"stores": [
{
"available": 8,
"store": "58a7a2529a8b894656a42dff",
"_id": "58a7a2529a8b894656a42e01"
}
]
}
]
If you want stores.store & author populated, use :
Books.find().populate([{path:'author'},{path:'stores.store'}]).exec(function(err, res) {
console.log(JSON.stringify(res, null, 2));
});
It gives as expected author & stores.store both populated :
[
{
"_id": "58a7a2529a8b894656a42e00",
"title": "gullivers travels",
"author": {
"_id": "58a7a2529a8b894656a42dfe",
"name": "Jonathan Swift",
"description": "old satiric bloke",
"born": "1667-11-29T23:00:00.000Z",
"__v": 0,
"died": "1745-10-18T22:00:00.000Z"
},
"description": "A book about a big guy in a small world",
"__v": 0,
"stores": [
{
"available": 8,
"store": {
"_id": "58a7a2529a8b894656a42dff",
"name": "Book shop 1",
"description": "This is where the cool kids buy all there books!",
"address": "La La Land, 123 Combaja street",
"__v": 0
},
"_id": "58a7a2529a8b894656a42e01"
}
]
}
]

Resources