Reference to schema (not a particular document) in mongoose - mongodb

Please, note that this is not a duplicate of this, nor this, nor this, since what I need is not a reference to a document from another collection, but a reference to the collection itself.
I'm using mongoose-schema-extend to create a hierarchic structure for contents.
Let's say I have this:
/**
* Base class for content
*/
var ContentSchema = new Schema({
URI: {type: String, trim: true, unique: true, required: true },
auth: {type: [Schema.Types.ObjectId], ref: 'User'},
timestamps: {
creation: {type: Date, default: Date.now},
lastModified: {type: Date, default: Date.now}
}
}, {collection: 'content'}); // The plural form of content is content
/**
* Pages are a content containing a body and a title
*/
var PageSchema = ContentSchema.extend({
title: {type: String, trim: true, unique: true, required: true },
format: {type: String, trim: true, required: true, validate: /^(md|html)$/, default: 'html' },
body: {type: String, trim: true, required: true}
});
/**
* Articles are pages with a reference to its author and a list of tags
* articles may have a summary
*/
var ArticleSchema = PageSchema.extend({
author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
summary: { type: String },
tags: { type: [String] }
});
Now, I want to create another schema, which is a subtype of content, but which represents a set of contents, like so:
/**
* Content sets are groups of content intended to be displayed by views
*/
var ContentSetSchema = ContentSchema.extend({
name: {type: String, trim: true, unique: true, required: true },
description: {type: String },
content: [{
source: { type: [OTHER_SCHEMA] }, // <- HERE
filter: {type: String, trim: true },
order: {type: String, trim: true }
}]
})
So, the content attribute of this schema should be a reference to any of the other schemas.
Is it possible?

The best I came up whit, was using a string, a discriminator key, and a validator:
var ContentSchema = new Schema({
// ...
}, {collection: 'content', discriminatorKey : '_type'});
var ContentSetSchema = ContentSchema.extend({
// ...
content: [{
source: { type: [String], validate: doesItExist }
}]
});
function doesItExist(type, result) {
ContentModel.distinct('_type').exec()
.then((contentTypes) =>
respond(contentTypes.some((validType) => validType === type)));
}
But with this solution (good enough an this moment) I can only create ContentSets for types of content that have already some register in the database.

Related

How to make sure that only one of two fields are populated in mongoDB

I am using mongoose as an ODM and trying to model an animal/pet. In the model I have 2 fields, parent and shelter. I want to make sure that pet belongs to a person or a shelter but not both. What constraints would allow me to do this.
My model in JS:-
const petSchema = mongoose.Schema({
name: {
type: String,
required: [true, "Pet must have a name."],
trim: true
},
species: {
type: String,
required: [true, "Pet must have a species."]
},
parent: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
shelter: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Shelter'
}
}
I am new to databases and their jargons, correct me if any error in question.
Thank you.
You can use the required function to determine this as follows:
const petSchema = mongoose.Schema({
name: {
type: String,
required: [true, "Pet must have a name."],
trim: true
},
species: {
type: String,
required: [true, "Pet must have a species."]
},
parent: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: function() {
return !this.shelter;
}
},
shelter: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Shelter',
required: function() {
return !this.parent;
}
}
}
I ended up using pre validate middleware in mongoose:-
petSchema.pre('validate', function (next) {
if((this.parent && this.shelter) || (!this.parent && !this.shelter))
return next(new Error("At least and Only one field(parent, shelter) should be populated"))
next()
})

How to update an object for a document in MongoDB using React and axios?

I am able to update: deviceName, macAddress, and blacklisted, but any values in the connection object will not update (ie: active, connectionURL & configuration). The purpose of the if statements are to not modify the values in the database if the user left them blank.
Schema:
const deviceSchema = new Schema({
deviceName: { type: String, required: true, trim: true },
macAddress: { type: String, required: true, unique: true, trim: true },
blacklisted: {type: Boolean, required: true},
connection: { type: Object, required: true,
active: { type: Boolean, required: true, trim: true },
connectionUrl: { type: String, required: true, trim: true},
configuration: { type: String, required: true, trim: true},
},
}, {
timestamps: true,
});
Updating Database:
router.route('/update/config/:id').post((req, res) => {
Device.findById(req.params.id)
.then(device => {
device.macAddress = req.body.macAddress;
device.connection.active = req.body.active;
if (req.body.connectionUrl != '' && req.body.connectionUrl != "Choose...") {
device.connection.connectionUrl = req.body.connectionUrl;
}
if (req.body.configuration != '') {
device.connection.configuration = req.body.configuration;
}
device.save()
.then(() => res.json('device configuration(s) updated!'))
.catch(err => res.status(400).json('Error: ' + err));
})
.catch(err => res.status(400).json('Error: ' + err));
});
In Mongoose, if you're assigning a Schema the type of 'Object', it will become a Mixed schema, which means a couple things, but the most important of which in your case is that mongoose cannot automatically detect changes to that part of the document.
What you're likely looking for here is either a 'Nested Path' or a 'SubDocument'. See here for more information on the difference between the two.
The gist is, you need to change how you're defining the schema. There's a few options here:
1. You know exactly what members the connection object has, and always want to be able to assign to them, even if you've never explicitely created the connection.
In this case, your best bet is a 'nested path', which would be defined like so:
const deviceSchema = new Schema({
deviceName: { type: String, required: true, trim: true },
macAddress: { type: String, required: true, unique: true, trim: true },
blacklisted: {type: Boolean, required: true},
connection: {
active: { type: Boolean, required: true, trim: true },
connectionUrl: { type: String, required: true, trim: true},
configuration: { type: String, required: true, trim: true},
},
}, {
timestamps: true,
});
2. You know exactly what members the connection object has, but only want to be able to assign to specific members if the entire connection has already been created.
In this case, you're going to want to use a SubDocument
const connectionSchema = new Schema({
active: { type: Boolean, required: true, trim: true },
connectionUrl: { type: String, required: true, trim: true },
configuration: { type: String, required: true, trim: true },
}, { timestamps: false });
const deviceSchema = new Schema({
deviceName: { type: String, required: true, trim: true },
macAddress: { type: String, required: true, unique: true, trim: true },
blacklisted: {type: Boolean, required: true},
connection: { type: connectionSchema, required: true },
}, {
timestamps: true,
});
EDIT: In reference to a question posted in a comment below: If you want to be able to reference the same connectionSchema from multiple 'deviceSchema's, you would need to change to something along the lines of this:
const deviceSchema = new Schema({
deviceName: { type: String, required: true, trim: true },
macAddress: { type: String, required: true, unique: true, trim: true },
blacklisted: {type: Boolean, required: true},
connection: { type: Schema.Types.ObjectId, required: true, ref: "Connection" },
}, {
timestamps: true,
});
In this situation, you would also need to be certain that you're explicitly creating both models (a Device model, and a Connection model), and to ensure the connection model is loaded with the device model, you would then run:
DeviceModel.findOne(/*whatever*/).populate('connection') rather than your normal 'find' method. This is getting a bit far afoot from the original question though.
3. You don't know the exact structure of the connection object
Here, you can use the built in 'Mixed' type as you were already, but it comes with the caveat that you can't define any further children, and mongoose won't automatically detect changes to that portion of the document, meaning device.save() won't update anything in your connection object automatically.
const deviceSchema = new Schema({
deviceName: { type: String, required: true, trim: true },
macAddress: { type: String, required: true, unique: true, trim: true },
blacklisted: {type: Boolean, required: true},
connection: { type: Object, required: true },
}, {
timestamps: true,
});
In order to tell mongoose that the connection field has been updated, we would have to call device.markModified('connection') before device.save()

Populate a property of a mongoose schema with all the data in another collection

I have a model with articles, and would like to populate an array of data with all the documents in a collection.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ArticlesSchema = new mongoose.Schema({
path: {
type: String,
required: true,
unique: true,
},
base_headline: {
type: String,
required: true,
},
intro: {
type: String,
required: true,
},
featured_image: {
type: String,
required: true,
},
author: {
type: String,
required: true,
},
platform_filter: {
type: String,
},
free_filter: {
type: String,
},
content_type: {
type: String,
required: true,
},
data: [{ type: Schema.Types.ObjectId, ref: 'DesignProducts' }],
date: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Articles', ArticlesSchema);
The data property should be populated with all documents in the DesignProducts collection.
I tried running this but the data array is still empty:
Article.findOne({ path: slug }).populate('data').exec();
Here is what the designProducts model looks like:
const mongoose = require('mongoose');
const DesignProductsSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true,
},
intro: {
type: String,
required: true,
},
website: {
type: String,
required: true,
},
date: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('DesignProducts', DesignProductsSchema);
This array should be populated with all the documents in the DesignProducts collection:

Mongoose populate method on query returns empty array

I am having trouble querying my model, and using the .populate method to grab referenced documents of my object. Here are my schemas:
var userSchema = new Schema({
firstname: { type: String, required: true, unique: false },
lastname: { type: String, required: true, unique: false },
...
children: [{type: mongoose.Schema.Types.ObjectId, ref: 'Child'}],
});
var childSchema = new Schema({
firstname: { type: String, required: true, unique: false },
lastname: { type: String, required: true, unique: false },
...
legal_guardian_id: [{type: mongoose.Schema.Types.ObjectId, ref: 'User'}],
});
And here is how i'm trying to run my query:
User.findOne({ _id: '5b9d30083e33585cc0b8c710' })
.populate('children').exec((err, doc) => {
if (err) { return console.error(err); }
res.send(doc);
})
This results in "children": []
When I just use the findOne method and return the user, I get "children":["5b9d3f23d1408c5f4e2624f3"].
What am I doing wrong?

Products with different variants schema

I am trying to create an e-commerce website using MongoDB. I have created a Product and variant model, my question is how can I search the product with variant, for example for "Size" user can add variant value as "S" or "Small". How can I search the product which has for example small product in this case as a product have many variants, how can I list eg. all products with small size. Here is my variant model.
var variantSchema = Schema({
name: {
type: String,
required: true,
unique: true
},
count: {type: Number, default : 0}
});
And my Product Schema is:
var productSchema = Schema({
sku: {
type: String,
lowercase: true
}, //, required: true, unique: true
name: {
type: String,
lowercase: true,
max: 65,
required: true
},
slug: {
type: String,
lowercase: true,
unique: true,
index: true,
slug: "name",
slug_padding_size: 3
},
status: Boolean,
listPrice: Number,
description: {
short: {
type: String,
trim: true,
lowercase: true
},
long: {
type: String,
trim: true,
lowercase: true
}
},
images: [],
categoryId: {
type: Schema.Types.ObjectId,
ref: 'Category'
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
},
isActive: Boolean,
vars: [
{
varId : {
type: Schema.Types.ObjectId,
ref: 'Variants'
},
values: [
{
value : String,
image:[]
}
]
}
]
});
Based on your comment.
You can distinguish "Small" and "small" by ignoring case sensitive.
UserModel.findOne({
email: {'$regex': "^"+ email +"$", $options:'i'}
}, function (err, data) {
callback(err, data)
});
But you can not match S with Small.
Approach 1:
You need to maintain the possible words that you want to consider as Small. Maybe by inserting in Variant Schema an array ["S", "Small"] like this. But in this scenario. You must have to caution about S. S can be anything. (I am not recommended this approach)
Approach 2:
I would like to suggest making one schema (SizeSchema) that can present the size. for e.g. Small, Large, Extra small, Extra Large etc... And reference that SizeSchema to VariantSchema and ProductSchema to VariantSchema. (Triple relationship). And this would be fixed for the end user. No one will have an option like "S".
Hope this may help you.