Dynamic Mongoose Field Type - node.js

I am developing an app where a user could store his model on a database using mongoDB and mongoose. Taken from mongoose tutorial the type of the field has to be defined. For example here we have to define that the name is a string.
const personSchema = new mongoose.Schema({
name: String
});
const Person = mongoose.model('Person', personSchema);
Is there any way to make it dynamic to user's input. I want to create a form where a user will enter a field name and select one of the field types that Mongoose offers [String,Number,Date etc], but I cannot figure any way to implement it. To be honest I don't know even if this is a good approach. An alternative would be to pass everything as a String and serialise the input in order to store it. I want to achieve something like that:
const {fieldName,fieldType} = userInput;
const customSchema = new mongoose.Schema({
fieldName: fieldType
});
const CustomModel = mongoose.model('CustomSchema', customSchema);
Is this possible or should I implement another approach? An alternative would be to pass everything as a String and serialise the input in order to store it.
Thank you in advance!

If I understand you correctly it should work like that:
User defines the model to store
Schema is created using the data provided by the user
User can pass the data to store using the previously created model which will validate the user's input later
In fact, I'm working on a project that has the same functionality. Here is how we did it.
A user sends the model and we store it as a string since we need to have the ability to create the model once again.
When the user passes new data to store using the created model we get the string from mongo and parse it to create the schema. This operation is relatively easy (but depends on what you want to achieve as it can get tricky if you want to have some advanced validation) as you have to just create an object with correct values from mongoose. Something like this for every field that the user has defined.
export const fieldConverter = ({name, type}) => {
switch (type) {
case 'String':
return { [name]: String };
case 'Number':
return { [name]: Number };
...
}
When you have your object ready then you can create a model out of it.
The line with accessing your model from mongoose.models is important as the mongoose will cache the model and throw an error if you try to create it once again.
const DatasetModel =
mongoose.models["your-model-name"] ??
mongoose.model("your-model-name", new mongoose.Schema(schema));
Now when you have the model the rest is just like with the normally created one.
This approach worked for us so I'm adding this as inspiration maybe it will help you. If you have any specific questions about the implementation feel free to ask I will be happy to help.
There is also a Mixed type in mongoose if you don't need the validation later. You can check it here: https://mongoosejs.com/docs/schematypes.html#mixed

You can use Schema.Types.Mixed, An "anything goes" SchemaType. Mongoose will not do any casting on mixed paths.
let customSchema = new Schema({custom: Schema.Types.Mixed})
Read more about it here

After some research I figure at that mongoose type can also be strings. For example
const personSchema = new mongoose.Schema({
name: "String"
});
const Person = mongoose.model('Person', personSchema);
Mongoose will handle it

Related

Using mongoose, how to add object propety to an object stored in mongodb that is not included in the schema?

I want to be able to add a new object property to an object in a document stored in a mongodb database where the model schema does not include that property. I've seen all sorts of examples on google and none of them seem to work so I want to ask this question myself and get some direct answers.
For example take this scheme
const MapSchema = new mongoose.Schema({
name:{
type: String
}
})
const ListMap = mongoose.model("listmap", MapSchema);
Now I want to add a new property to it using its id
model.ListMap.findByIdAndUpdate(_id, {newprop:"whatever");
model.ListMap.findByIdAndUpdate(_id, {name:"test");
Using those two commands, I can add name:"test" to it but I can not add newprop:"whatever" to it. What I want to do is be able to add a new property without having to declare it in the schema first. I know this seems like it has been asked before but I've googled it all and I don't believe anyone has answered it. They either didn't understand the question or their code doesn't actually work.
Also bonus question, why does mongodb always add an s to collection names? like the above would show up in collection "listmaps", assuming I used .create() to add the first object.
For your first question, you can not add a property to your schema without declaring it first.
you can define a generic property like this:
const MapSchema = new mongoose.Schema({
property:{
title: String,
value: String
}
})
const ListMap = mongoose.model("listmap", MapSchema);
and for your second question you can refer to this answer:
Why does mongoose always add an s to the end of my collection name
-------SOLVED-------------
I am adding this here for others because it was extremely difficult for me to google this. This answer is from another stackexchange link
How to add a new key:value pair to existing sub object of a mongoDB document
based on this, this is how to do what I proposed earlier
const commentMapSchema = new mongoose.Schema({
List:{}
})
const CommentsMap = mongoose.model("commentmap", commentMapSchema);
const update = {$set: {["List."+"test2"] : "data2"}};
CommentsMap.findByIdAndUpdate(id, update);
First you need to declare the property in the schema. This property can hold more properties exactly like how it works with javascript objects. Then use the update method with mongoose with the query that combines the property name with a new property via dot notation. However when adding a property this way, the key must be a string. This works exactly like it does in javascript. You can just create a new name and it'll add to the object properties.
I hope this helps anyone else who landed here.

Models and Schemas, why can't I set properties on my Model [duplicate]

The two types of objects seem to be so close to one another that having both feels redundant. What is the point of having both schemas and models?
EDIT: Although this has been useful for many people, as mentioned in the comments it answers the "how" rather than the why. Thankfully, the why of the question has been answered elsewhere also, with this answer to another question. This has been linked in the comments for some time but I realise that many may not get that far when reading.
Often the easiest way to answer this type of question is with an example. In this case, someone has already done it for me :)
Take a look here:
http://rawberg.com/blog/nodejs/mongoose-orm-nested-models/
EDIT: The original post (as mentioned in the comments) seems to no longer exist, so I am reproducing it below. Should it ever return, or if it has just moved, please let me know.
It gives a decent description of using schemas within models in mongoose and why you would want to do it, and also shows you how to push tasks via the model while the schema is all about the structure etc.
Original Post:
Let’s start with a simple example of embedding a schema inside a model.
var TaskSchema = new Schema({
name: String,
priority: Number
});
TaskSchema.virtual('nameandpriority')
.get( function () {
return this.name + '(' + this.priority + ')';
});
TaskSchema.method('isHighPriority', function() {
if(this.priority === 1) {
return true;
} else {
return false;
}
});
var ListSchema = new Schema({
name: String,
tasks: [TaskSchema]
});
mongoose.model('List', ListSchema);
var List = mongoose.model('List');
var sampleList = new List({name:'Sample List'});
I created a new TaskSchema object with basic info a task might have. A Mongoose virtual attribute is setup to conveniently combine the name and priority of the Task. I only specified a getter here but virtual setters are supported as well.
I also defined a simple task method called isHighPriority to demonstrate how methods work with this setup.
In the ListSchema definition you’ll notice how the tasks key is configured to hold an array of TaskSchema objects. The task key will become an instance of DocumentArray which provides special methods for dealing with embedded Mongo documents.
For now I only passed the ListSchema object into mongoose.model and left the TaskSchema out. Technically it's not necessary to turn the TaskSchema into a formal model since we won’t be saving it in it’s own collection. Later on I’ll show you how it doesn’t harm anything if you do and it can help to organize all your models in the same way especially when they start spanning multiple files.
With the List model setup let’s add a couple tasks to it and save them to Mongo.
var List = mongoose.model('List');
var sampleList = new List({name:'Sample List'});
sampleList.tasks.push(
{name:'task one', priority:1},
{name:'task two', priority:5}
);
sampleList.save(function(err) {
if (err) {
console.log('error adding new list');
console.log(err);
} else {
console.log('new list successfully saved');
}
});
The tasks attribute on the instance of our List model (sampleList) works like a regular JavaScript array and we can add new tasks to it using push. The important thing to notice is the tasks are added as regular JavaScript objects. It’s a subtle distinction that may not be immediately intuitive.
You can verify from the Mongo shell that the new list and tasks were saved to mongo.
db.lists.find()
{ "tasks" : [
{
"_id" : ObjectId("4dd1cbeed77909f507000002"),
"priority" : 1,
"name" : "task one"
},
{
"_id" : ObjectId("4dd1cbeed77909f507000003"),
"priority" : 5,
"name" : "task two"
}
], "_id" : ObjectId("4dd1cbeed77909f507000001"), "name" : "Sample List" }
Now we can use the ObjectId to pull up the Sample List and iterate through its tasks.
List.findById('4dd1cbeed77909f507000001', function(err, list) {
console.log(list.name + ' retrieved');
list.tasks.forEach(function(task, index, array) {
console.log(task.name);
console.log(task.nameandpriority);
console.log(task.isHighPriority());
});
});
If you run that last bit of code you’ll get an error saying the embedded document doesn’t have a method isHighPriority. In the current version of Mongoose you can’t access methods on embedded schemas directly. There’s an open ticket to fix it and after posing the question to the Mongoose Google Group, manimal45 posted a helpful work-around to use for now.
List.findById('4dd1cbeed77909f507000001', function(err, list) {
console.log(list.name + ' retrieved');
list.tasks.forEach(function(task, index, array) {
console.log(task.name);
console.log(task.nameandpriority);
console.log(task._schema.methods.isHighPriority.apply(task));
});
});
If you run that code you should see the following output on the command line.
Sample List retrieved
task one
task one (1)
true
task two
task two (5)
false
With that work-around in mind let’s turn the TaskSchema into a Mongoose model.
mongoose.model('Task', TaskSchema);
var Task = mongoose.model('Task');
var ListSchema = new Schema({
name: String,
tasks: [Task.schema]
});
mongoose.model('List', ListSchema);
var List = mongoose.model('List');
The TaskSchema definition is the same as before so I left it out. Once its turned into a model we can still access it’s underlying Schema object using dot notation.
Let’s create a new list and embed two Task model instances within it.
var demoList = new List({name:'Demo List'});
var taskThree = new Task({name:'task three', priority:10});
var taskFour = new Task({name:'task four', priority:11});
demoList.tasks.push(taskThree.toObject(), taskFour.toObject());
demoList.save(function(err) {
if (err) {
console.log('error adding new list');
console.log(err);
} else {
console.log('new list successfully saved');
}
});
As we’re embedding the Task model instances into the List we’re calling toObject on them to convert their data into plain JavaScript objects that the List.tasks DocumentArray is expecting. When you save model instances this way your embedded documents will contain ObjectIds.
The complete code example is available as a gist. Hopefully these work-arounds help smooth things over as Mongoose continues to develop. I’m still pretty new to Mongoose and MongoDB so please feel free to share better solutions and tips in the comments. Happy data modeling!
Schema is an object that defines the structure of any documents that will be stored in your MongoDB collection; it enables you to define types and validators for all of your data items.
Model is an object that gives you easy access to a named collection, allowing you to query the collection and use the Schema to validate any documents you save to that collection. It is created by combining a Schema, a Connection, and a collection name.
Originally phrased by Valeri Karpov, MongoDB Blog
I don't think the accepted answer actually answers the question that was posed. The answer doesn't explain why Mongoose has decided to require a developer to provide both a Schema and a Model variable. An example of a framework where they have eliminated the need for the developer to define the data schema is django--a developer writes up their models in the models.py file, and leaves it to the framework to manage the schema. The first reason that comes to mind for why they do this, given my experience with django, is ease-of-use. Perhaps more importantly is the DRY (don't repeat yourself) principle--you don't have to remember to update the schema when you change the model--django will do it for you! Rails also manages the schema of the data for you--a developer doesn't edit the schema directly, but changes it by defining migrations that manipulate the schema.
One reason I could understand that Mongoose would separate the schema and the model is instances where you would want to build a model from two schemas. Such a scenario might introduce more complexity than is worth managing--if you have two schemas that are managed by one model, why aren't they one schema?
Perhaps the original question is more a relic of the traditional relational database system. In world NoSQL/Mongo world, perhaps the schema is a little more flexible than MySQL/PostgreSQL, and thus changing the schema is more common practice.
To understand why? you have to understand what actually is Mongoose?
Well, the mongoose is an object data modeling library for MongoDB and Node JS, providing a higher level of abstraction. So it's a bit like the relationship between Express and Node, so Express is a layer of abstraction over regular Node, while Mongoose is a layer of abstraction over the regular MongoDB driver.
An object data modeling library is just a way for us to write Javascript code that will then interact with a database. So we could just use a regular MongoDB driver to access our database, it would work just fine.
But instead we use Mongoose because it gives us a lot more functionality out of the box, allowing for faster and simpler development of our applications.
So, some of the features Mongoose gives us schemas to model our data and relationship, easy data validation, a simple query API, middleware, and much more.
In Mongoose, a schema is where we model our data, where we describe the structure of the data, default values, and validation, then we take that schema and create a model out of it, a model is basically a wrapper around the schema, which allows us to actually interface with the database in order to create, delete, update, and read documents.
Let's create a model from a schema.
const tourSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'A tour must have a name'],
unique: true,
},
rating: {
type: Number,
default: 4.5,
},
price: {
type: Number,
required: [true, 'A tour must have a price'],
},
});
//tour model
const Tour = mongoose.model('Tour', tourSchema);
According to convetion first letter of a model name must be capitalized.
Let's create instance of our model that we created using mongoose and schema. also, interact with our database.
const testTour = new Tour({ // instance of our model
name: 'The Forest Hiker',
rating: 4.7,
price: 497,
});
// saving testTour document into database
testTour
.save()
.then((doc) => {
console.log(doc);
})
.catch((err) => {
console.log(err);
});
So having both schama and modle mongoose makes our life easier.
Think of Model as a wrapper to schemas. Schemas define the structure of your document , what kind of properties can you expect and what will be their data type (String,Number etc.). Models provide a kind of interface to perform CRUD on schema. See this post on FCC.
Schema basically models your data (where you provide datatypes for your fields) and can do some validations on your data. It mainly deals with the structure of your collection.
Whereas the model is a wrapper around your schema to provide you with CRUD methods on collections. It mainly deals with adding/querying the database.
Having both schema and model could appear redundant when compared to other frameworks like Django (which provides only a Model) or SQL (where we create only Schemas and write SQL queries and there is no concept of model). But, this is just the way Mongoose implements it.

Creating Mongoose Schemas with or without 'new' keyword?

Most of the examples I have seen online do something like...
var UserSchema = new mongoose.Schema({
name: String,
age: String
});
However, recently I found a book do the above... but without the new keyword.
var UserSchema = mongoose.Schema({
name: String,
age: String
});
I am now confused. Do we use the new keyword for creating the schema or not.. and what happens in both cases?
Both are valid and returns a new instance of the Mongoose.Schema class. What this means is that both does exactly the same. This line checks whether you already have an instance of the Schema class, if not, it returns one for you.
To summarize, if you call
var schema = new mongoose.Schema({})
you initialize an instance yourself, while if you call
var schema = mongoose.Schema({})
mongoose initializes one for you, with this:
function Schema(obj, options) {
if (!(this instanceof Schema)) {
return new Schema(obj, options);
}
...
Yes, I agree with the above answer. But here I would like to add some more important points to it. Looking at the Mongoose documentation, I feel like it should always be called with the new keyword since Schema is a constructor.
Both will work and are valid, but my suggestion is to only use the new mongoose.Schema() since it's the correct way. Following standard conventions will make your code easier to read; not only for others but for you as well when you go back to it in 6 months.

Unable to get the value of particular key in mongoose if that key is not present in Schema

UserEventsInfo = new mongoose.Schema({
name: String,
username: String,
event_movie:[String],
event_tour:[String],
event_restaurant:[String],
event_lifetimeevents:[String]
},{strict : false});
I am able to insert new key-value pair other than defined in the schema
but when I try to read the value of that key. I can't. I am using the following code.
UserEventsDetails.find({username:username},function(err,docs){
if(!docs.length)
{
res.send('datanotavailable');
}
else{
res.send(docs[0][eventname]);
}
});
Here eventname is a variable.
When I add that key in the schema it returns the value i.e. work's fine.
Otherwise it is not returning any value.
Looks like there was an issue submitted like this to mongoose. Here is there response:
The benefit we see in a schemaless database is the ability for our data model to evolve as fast as our features require it, without a linear impact on performance and slower deployment cycles with needless migrations.
If you don't want your data to be normalized and validated prior to saving, then you don't need a tool like Mongoose, you can use the driver directly.
After a little digging there is a way to do this, but you will need to have a field with type Schema.Types.Mixed. So it would look like this:
var schema = new Schema({
mixed: Schema.Types.Mixed,
});
var Thing = mongoose.model('Thing', schema);
var m = new Thing;
m.mixed = { any: { thing: 'i want' } };
m.save(callback);
To do a find on a mixed this SO question answers that.
****EDIT
forgot to link the documentation of mixed types

mongoose custom schema types

I read from mongoose documentation that it is possible to create custom schema types to be added to the ones already existing.
As suggested I tried to look into the mongoose-long example: https://github.com/aheckmann/mongoose-long
I need this in an application I am developing in which I have a profile that is a mongoose model. The profile has several fields such as name, surname and so on that are MultiValueField. A MultiValueField is on object with the following structure:
{
current : <string>
providers : <Array>
values : {
provider1 : value1,
provider2 : value2
}
}
The structure above has a list of allowed providers (ordered by priorities) from which the field's value can be retrieved. The current property keeps track of which provider is currently picked to use as value of the field. Finally the object values contains the values for each provider.
I have defined an object in node.js, whose constructor takes the structure above as argument and provides a lot of useful methods to set a new providers, adding values and so on.
Each field of my profile should be initialized with a different list of providers, but I haven't found a way yet to do so.
If I define MultiValueField as an Embedded Schema, I can do so only having each field as Array. Also I cannot initialize every field at the moment a profile is created with the list of providers.
I think the best solution is to define a SchemaType MultiValueFieldType, that has a cast function that returns a MultiValueField object. However, how can I define such a custom schema type? And how can I use custom options when defining it in the schema of the profile?
I already posted a question on mongoose Google group to ask how to create Custom Schema Types: https://groups.google.com/forum/?fromgroups#!topic/mongoose-orm/fCpxtKSXFKc
I was able to create custom schema types only as arrays too. You will be able to initialize the model, but this aproach has drawbacks:
You will have arrays even if you don't need them
If you use ParameterSchema on other model, you will need to replicate it
Custom Schema Type Code:
// Location has inputs and outputs, which are Parameters
// file at /models/Location.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ParameterSchema = new Schema({
name: String,
type: String,
value: String,
});
var LocationSchema = new Schema({
inputs: [ParameterSchema],
outputs: [ParameterSchema],
});
module.exports = mongoose.model('Location', LocationSchema);
To initialize the model:
// file at /routes/locations.js
var Location = require('../models/Location');
var location = { ... } // javascript object usual initialization
location = new Location(location);
location.save();
When this code runs, it will save the initialized location with its parameters.
I didn't understand all topics of your question(s), but I hope that this answer helps you.

Resources