Create index in Mongoose for array of objects - node.js

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.

Related

How to sync the schema change for old document collection in mongodb with default values

How can I apply the schema changes to sync with default value to all the old data in mongodb
import mongoose from "mongoose";
interface ITodo {
title: string;
description: string;
by: string;
}
interface todoModelInterface extends mongoose.Model<TodoDoc> {
build(attr: ITodo): TodoDoc;
}
interface TodoDoc extends mongoose.Document {
title: string;
description: string;
by: string;
}
const todoSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
by: {
type: String,
required: true,
},
});
todoSchema.statics.build = (attr: ITodo) => {
return new Todo(attr);
};
const Todo = mongoose.model<TodoDoc, todoModelInterface>("Todo", todoSchema);
Todo.build({
title: "some title",
description: "some description",
by: "special",
});
Todo.collection.dropIndexes(function () {
Todo.collection.reIndex(function (finished) {
console.log("finished re indexing");
});
});
Todo.collection
.getIndexes()
.then((indexes: any) => {
console.log("indexes:", indexes);
})
.catch(console.error);
export { Todo };
Db:
[{
"_id": {
"$oid": "62cee1eea60e181e412cb0a2"
},
"title": "one",
"description": "one desc"
},{
"_id": {
"$oid": "62cee2bd44026b1f85464d41"
},
"title": "one",
"description": "one desc",
"by": "alphs"
},{
"_id": {
"$oid": "62cee3c8cf1592205dacda3e"
},
"title": "one",
"description": "one desc",
"by": "alphs"
}]
Here the old data still missing the "by" key, similarly if there is nested schema change it may impact the old users, how can we define the default collection for old data in mongodb at runtime without using update query migration?
Have you tried setting the default value for "by". By giving a default value, if the old data is missing a value then the default will kick in and return the default value provided. Read about Mongoose Default: Here. I don't know if this is a good practice but we also use this method when there is change in schema and don't want to run update query.

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"
}
]

Mongoose self referenced schema not creating ObjectId for all sub documents

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: []
}]
});

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"
}
]
}
]

Mongoose Mixed type field not getting populated

I have a json doc that has embedded document which is variable (but in json format). So I use Mixed Schema type.
When I save, everything works fine except the mixed type object doesn't get populated and won't save.
What am I doing wrong here ?
Updating --> What I mean is - everything works as expected except the data node (which is suppose to be of mixed type)
My Document Example:
{
"data": {
"user_name": "username",
"cart_items": [
{
"sku": "ABCD",
"msrp": 1250.25,
"discount": 10,
"final_price": 112.22
},
{
"sku": "PQRSDF",
"msrp": 12.25,
"discount": 10,
"final_price": 1.2
}
]
},
"template_id": "1",
"from": "x#gmail.com",
"send_status": 0,
"priority": 99,
"app_id": "app3",
"_id": "532a54aa1c76fba0874c48ea",
"bcc": [],
"cc": [],
"to": [
{
"name": "acv",
"email": "x#outlook.com"
},
{
"name": "pem",
"email": "y#gmail.com"
}
],
"call_details": {
"data_id": "01234",
"event_id": 25
}
}
code to insert:
Schema definition:
app_id : { type: String, trim: true },
priority: { type: Number},
send_status: { type: Number},
call_details : {
event_id : { type: Number},
data_id : { type: String, trim: true },
id : false
},
from : { type: String, trim: true },
to : [addressSchema],
cc : [addressSchema],
bcc : [addressSchema],
template_id : { type: String, trim: true },
data: { any: {} }
Code:
r.app_id = req.body.app_id;
r.priority= req.body.priority;
r.send_status= req.body.send_status;
r.call_details.event_id= req.body.call_details.event_id;
r.call_details.data_id= req.body.call_details.data_id;
r.from= req.body.from;
r.to = populate_address(req.body.to);
r.cc = populate_address(req.body.cc);
r.bcc = populate_address(req.body.bcc);
r.template_id= req.body.template_id;
r.data =req.body.data);
r.markModified('data');
r.save(function (err){
console.log("add");
res.send ("added");
});
As you currently define your schema, it will only save the any field within data.
Remove the any embedded field from the definition for data in your schema.
So instead of:
data: { any: {} }
Use:
data: {}
As mongoose does not handle the embedded document save automatically. You need to save the embedded document first and assign the object ID to the parent schema as reference.

Resources