I have a Pet schema defined as follows:
export default {
animal: {
type: Schema.Types.ObjectId,
validate: {
validator(v) {
if (
(!this.animal && !this.crossbreed_animal) ||
(this.animal && this.crossbreed_animal)
)
return false;
return true;
},
message:
"You must provide one of 'animal' or 'crossbreed_animal' ObjectId",
},
},
crossbreed_animal: { type: Schema.Types.ObjectId, required: false },
user: { type: Schema.Types.ObjectId, required: true },
name: { type: String, required: true },
birth_date: { type: Date, required: true },
weight: { type: Number, required: false },
...
}
The issue is the validator not being triggered at all. (I ensured this by putting console.log() in various places in the validator)
However, it works flawlessly when the type field is not Schema.Types.ObjectId. I tried with the weight and name fields by copy/pasting the exact same validator code and it works as expected.
I google'd this issue but it seems like no-one has the same error.
What's the problem here?
Node version: v14.15.0,
Mongoose version: v.5.10.9
I would advise you to not put a validator that performs a validation of multiple fields on some fields like animal.
The best way to handle this is to define a pre('save') hook that will perform complex validation of the whole object. Though this does not actually answer why it does not work with ObjectID I would also advise you to submit an issue in the Mongoose repository if this is crucial for you.
Related
I am trying to do validation within the schema to validate the length of an array based on another documents length of its array. Suppose:
const Level = mongoose.Schema({
level: {
type: Array,
required: true,
items: {
exercise: {
type: mongoose.Schema.Types.ObjectId,
ref: "Exercise",
required: true,
},
accuracyThreshold: { type: Number, required: true },
timeThreshold: { type: Number, required: true },
},
},
});
const UserLevelProgress = mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
level: {
type: mongoose.Schema.Types.ObjectId,
ref: "Level",
required: true,
},
progress: {
type: Array,
required: true,
validate: {
validator: (progress) => {
return progress.length === level.level.length; // level.level.length is not valid
},
message: () => "level progress legnth is invalid!",
},
items: {
accuracyScore: { type: Number, required: true },
timeScore: { type: Number, required: true },
},
},
});
module.exports = mongoose.model("UserLevelProgress", UserLevelProgress);
module.exports = mongoose.model("Level", Level);
I have a validator function in UserLevelProgress, but obviously level is not valid object. How do I get access to this object?
The short answer is: You could implement this validation logic, but is it really a good idea to do this?
Downsights
validation is taking place in the pre('save') hook. This means that once you initially save of update a document, the validation will run.
since Level documents are stored in a different collection, the values (including length of the array) can change without UserLevelProgress documents noticing. This way your validator would not rerun (as said before it's run in pre('save') hook and your documents would technically be not valid anymore.
Better approach
Check if Level schema can be included as a nested document, instead of a referenced one. This way validation would be much easier and data will not be able to become invalid unnoticed.
Implementation if you still want to follow your intended approach
validate: {
validator: async function(progress) {
const level = await Level.findById(this.level);
return progress.length === level.level.length; // level.level.length is not valid
},
message: () => "level progress legnth is invalid!",
},
I'm working on an e-commerce project in Express and MongoDB. I'm confused with architecture on how to make relationship between two models such that if I delete one element from a table, all of it's associations should be deleted. CASCADE delete, if I'm using the correct term.
I'm not a database designer, just learning Express and MongoDB. So apologies if my schema is not that good.
I have two tables (or documents in MongoDB). One is Order with schema below:
const orderSchema = new mongoose.Schema({
shippingInfo : {
type: mongoose.Types.ObjectId,
ref: 'Address'
},
user : {
type: mongoose.Types.ObjectId,
ref: 'User',
required: true
},
orderItems: [
{
type: mongoose.Types.ObjectId,
ref:'OrderItem'
}
],
totalPrice: {
type: Number,
required: true,
default: 0.0
},
status: {
type: String,
enum: ['processing', 'shipped', 'delivered','cancelled'],
default: 'processing'
},
deliveredAt: {
type: Date,
}
})
and OrderItems
const orderItemSchema = new mongoose.Schema({
product: {
type: mongoose.Types.ObjectId,
ref: 'Product'
},
name: {
type: String,
required: true
},
quantity: {
type: Number,
required: true
},
price: {
type: Number,
required: true
},
image: {
type: String,
required: true
},
})
I want if I delete an Order, all of its OrderItems should be deleted right away (using remove middleware in Order).
I know that Django has something called on_delete=model.CASCADE when we create relationships, but I'm unaware of such thing in Mongoose.
I don't want to explicitly make another API request to search for and delete all OrderItems that are referenced in orderItems array in an Order, once it is deleted. There has to be a better approach for this.
Another post on Stack Overflow suggested that in remove middleware of Order I should do something like
OrderItem.find({ order_id: this._id }).remove().exec();
That would require me to refer order_id in OrderItem right?
And this would create circular dependency since OrderItem would require Order to be created first and vice versa.
What should I do here? Should I change the schema for both tables i.e. remove orderItems entry from Order and instead add order_id in OrderItem? Or is there a Mongoose way to overcome this situation?
I am setting up a comment model where users can post comments reference and can also reply. the complication comes with the reply part. I want users to be able to reply to comments or others' replies, and I am lost on how to set up my model for that.
How should I set up my model to be able to capture that data in my reply?
also, any other suggestion would be appreciated
Here is the model I am currently setting up
const mongoose = require('mongoose')
const commentSchema = new mongoose.Schema({
owner: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
reference: {
type: mongoose.Schema.Types.ObjectId,
required: false,
ref: 'Project' || null,
default: false
},
body: {
type: String,
required: true,
trim: true
},
reply: {
owner: {
type: mongoose.Schema.Types.ObjectId,
required: false,
ref: 'User'
},
body: {
type: String,
required: true
}
}
}, {
timestamps: true
})
const Comment = mongoose.model('Comment', commentSchema)
module.exports = Comment
If you are thinking about a model where we have
some post
>commentA
>replyA-a
>replyA-a-a
>replyA-a-a-a
>replyA-b
>commentB
>commentC
I would aggregate everything for the corresponding entity
Comment {
user,
body,
replies: [Comment] // pattern composite
}
EntityComment { // only persist this one
reference: { id, type: post|topic|whatever },
comment: [Comment]
}
Props are:
an entityComment can grow big (is this problematic?)
no need for multiple fetch, everything's there
easy to "hide" some comments and just show its count (array length)
If record entityComment becomes too big (the max record length seems to be 16MB so likely not be the limit, but maybe the payload is slow to load), then
we can think of saving each comment (using replies: [{ ref: Comment, type: ObjectId)}])
but maybe a better idea is to use a reference for body (body: [ref: CommentBody, type: ObjectId])
The reason is body is likely the culprit (datasize wise), and this would allow to
keep everything nested in entityComment
delay the fetch of the bodies we are interested in (not the whole hierarchy)
There are tradeoffs:
is fine for read
is simpler for writes (just update/delete a singular comment)
I have the following Mongoose schema definition in my project:
export const ProductSchema: SchemaDefinition = {
type: { type: String, enum: constants.productTypes, required: true },
name: { type: String, required: false },
espData: {
type: { type: String, required: true },
text: { type: String, required: true }
},
error: {
type: {
message: { type: String, required: true },
statusCode: { type: Number, default: null }
},
required: false
}
// Some more definitions...
};
What's important from here is that I have collection of products where each product has its own type (which is a required string property that can have values defined in constants.productTypes), a non-required name field and so on. Also, there's espData field that has its own mandatory type property which is completely different from the top level type. And, there's error property that does not exist at all times, but when it does, it must have message property and optional statusCode property.
I now have to modify this schema so that espData becomes optional field since I may now have products that don't have this property. How do I do that? I tried couple of things, but none of them worked:
First, I modified espData so that it looks the same as error:
espData: {
type: {
type: { type: String, required: true },
text: { type: String, required: true }
},
required: false
},
But, this is not working, most probably because there's so many nested type properties. Funny thing is that it perfectly works for the error property which has the same structure as espData, but without nested type property. The code I used is
const model = new this.factories.product.model();
model.type = 'hi-iq';
// model.espData purposely left undefined
await model.save();
The error I'm getting is Product validation failed: espData.type.text: Path 'espData.type.text' is required., espData.type.type: Path 'espData.type.type' is required. This indicates that model created from schema is created as espData.type.type which is not what I wanted (I wanted espData.type).
Second, I have tried the same from above, just instead of required field, I wrote: default: null which gave me an error TypeError: Invalid value for schema path 'espData.default', got value "null".
So, how do I define espData as an optional field, which must have type and text properties when it exists?
Is this what you want. Create a new Document Schema with all the validations and nest it in another Schema with required: false (its default to false anyway)
const EspDataSchema = new Schema({
type: { type: String, required: true },
text: { type: String, required: true },
},
{
_id: false,
}
);
example
export const ProductSchema = new Schema({
...
espData: {
type: EspDataSchema,
required: false
},
...
})
I'm just getting started with Mongoose (v3.8.1) and am experimenting with sub-documents and validation. As far as I understand (from the bottom of this page: http://mongoosejs.com/docs/subdocs.html), the following is the correct way to set the schema up:
var ParentSchema = new Schema({
name: { type: String, required: true },
children: [{
name: { type: String, required: true }
}]
});
Then I can do the following to create the document / sub-document:
ParentModel.create({
name: "Parent 1",
children: [
{ name: "Child 1" },
{ name: "Child 2" },
]
}, callback);
This works perfectly and the validation fails if I omit any of the child names. However, if I completely omit the children key, validation passes and an empty array is inserted.
Therefore, is there a way of triggering a validation error if the children key is omitted or am I going about this in the wrong way?
After some more fiddling I think I've got it! Using the type key to specify the schema allows me to also set required: true. Seems to work ok now.
Updated schema:
var ParentSchema = new Schema({
name: { type: String, required: true },
children: {
type: [{
name: { type: String, required: true }
}],
required: true
}
});