Mongoose Validation Error occurs on OpenShift but not local version - node.js

I am migrating my Node.js server with Mongoose to OpenShift and an error occurs on the live server that I cannot reproduce on my local WebStorm built-in server.
I get the error message:
undefined: {
properties: {
message: "Cannot read property 'options' of undefined"
type: "cast"
}-
message: "Cannot read property 'options' of undefined"
name: "ValidatorError"
kind: "cast"
}
This occurs when I try to push an element into the items array and save, for the following schema:
var listSchema = new mongoose.Schema({
owner: {type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true},
name: {type: String, required: true},
items: [
{
name:{
type: String,
required:true
},
quantity:Number,
check: Boolean
}
]
});
The local version that works, and the OpenShift version use the exact same code. The code that adds the new element is:
var listId = req.params["id"];
if (sessions.verifyToken(userId, token)) {
var data = req.body;
var query = List.findOne({
owner: userId,
"_id": listId
});
query.exec(function(err, list) {
...
//handle error and null (omitted for brevity)
...
list.items.push({ // error thrown here
name: req.body["name"],
quantity: req.body["quantity"],
check: false
});
list.save(function(err, list) {
if (err) {
var message = "Unable save appended list to persistent memory";
console.log(message, err);
res.setHeader("content-type", "application/json");
res.send(JSON.stringify({success: false, message: message, error: err}));
return;
}
res.setHeader("content-type", "application/json");
res.send(JSON.stringify(list));
});
});
I thought that maybe an earlier version of the schema had added a constraint, so I dropped the lists collection, but the problem did not go away.
What might be different on the OpenShift PaaS that could be causing the error?
[Edit]
Just for fun, I removed all required fields from items and now the error message is this:
"undefined": {
"properties": {
"message": "Cannot read property 'options' of undefined",
"type": "cast"
},
"message": "Cannot read property 'options' of undefined",
"name": "ValidatorError",
"kind": "cast"
},
"owner": {
"properties": {
"type": "required",
"message": "Path `{PATH}` is required.",
"path": "owner"
},
"message": "Path `owner` is required.",
"name": "ValidatorError",
"kind": "required",
"path": "owner"
}
This seems to suggest that the Model loses its owner field somewhere between finding the list and saving it again.
[/Edit]

On OpenShift, when you find or findOne a model that has a required reference to another entity, that field will not be automatically filled in. Thus, when save is called, the field will be missing. Change
var query = List.findOne({
owner: userId,
"_id": listId
});
to
var query = List.findOne({
owner: userId,
"_id": listId
}).populate("owner");
For some reason, this does not work the same in every environment. For some, either it does automatically populate the reference field, or it assumed it unchanged when saving. I'm not sure which.

Related

Is there a way to validate a UUID inserted in a mongoDB using mongo's validator?

I am using migrate-mongo for managing my database migration and I am trying to create a new migration that create a collection with a validator and insert values in it. I want to use a UUID for the _id property and I am using the uuid-mongodb library to generate it. My problem is that I am not able to set the bsonType of my _id in the validator without causing the data insertion failure. Is there any way to make sure that the id of the documents inserted in the collection is a UUID? I know that mongoose could help me to solve this issue, but I would like the validation to be done at the database level. Also, when I do not specify the _id's bsonType in the validator, the insertion works, it fails validation only when I specify it.
Here is my migration code
const MUUID = require("uuid-mongodb");
module.exports = {
async up(db) {
//Will use https://docs.mongodb.com/manual/tutorial/model-tree-structures-with-materialized-paths/
await db.createCollection("itemCategories", {
validator: {
$jsonSchema: {
required: ["name"],
bsonType: "object",
properties: {
_id: {"object"}, //I also tried with binData
name: {
bsonType: "string",
maxLength: 50,
},
path: {
bsonType: ["string", "null"],
pattern: "^,([^,]+,)+$"
}
},
additionalProperties: false,
}
},
});
await db.collection("itemCategories").createIndex({"name": 1}, {unique: true});
await db.collection("itemCategories").insertMany([
{_id: MUUID.v4(), name: "Sport", path: null},
{_id: MUUID.v4(), name: "Tool", path: null},
{_id: MUUID.v4(), name: "Entertainment", path: null}
]);
},
async down(db) {
await db.dropCollection("itemCategories");
}
};
And here is the error I get when running it
ERROR: Could not migrate up 20210627041314-create-categories.js: Document failed validation BulkWriteError: Document failed validation
at OrderedBulkOperation.handleWriteError (C:\Users\username\projectDirectory\node_modules\mongodb\lib\bulk\common.js:1352:9)
at resultHandler (C:\Users\username\projectDirectory\node_modules\mongodb\lib\bulk\common.js:579:23)
at handler (C:\Users\username\projectDirectory\node_modules\mongodb\lib\core\sdam\topology.js:943:24)
at C:\Users\username\projectDirectory\node_modules\mongodb\lib\cmap\connection_pool.js:350:13
at handleOperationResult (C:\Users\username\projectDirectory\node_modules\mongodb\lib\core\sdam\server.js:558:5)
at MessageStream.messageHandler (C:\Users\username\projectDirectory\node_modules\mongodb\lib\cmap\connection.js:281:5)
at MessageStream.emit (events.js:321:20)
at processIncomingData (C:\Users\username\projectDirectory\node_modules\mongodb\lib\cmap\message_stream.js:144:12)
at MessageStream._write (C:\Users\username\projectDirectory\node_modules\mongodb\lib\cmap\message_stream.js:42:5)
at doWrite (_stream_writable.js:441:12)
Assuming collection name user_demo and having 2 fields only ( _id, name )
Create collection Schema Validator
db.createCollection("user_demo", {
validator: {
$jsonSchema: {
bsonType: "object",
title: "User Object Validation",
required: [ "_id","name"],
properties: {
_id: {
bsonType: "binData",
description: "Unique identifier,I am using it instead of objectId for portibility",
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
},
name: {
bsonType: "string",
description: "'name' must be a string and is required",
maxLength: 50,
minLength: 1
}
}
}
}
} )
Insert data in collection
a) If you already have a uuid4
db.user_demo.insertOne({_id: UUID("a5750db3-1616-45a4-bf92-6a44c3e67342"), name:"shiva"})
b) If you want random uuid4
db.user_demo.insertOne({_id: UUID(), name:"explore"})
Tested with mongo version 6.0.3

Mongoose Find not filtering for object property inside array (MongoDB 4.4.1)

I've been trying to make a chat app and I use the following schema for messages:
const messageObject = {
sender: {type: mongoose.Schema.Types.ObjectId, ref: 'User', require: true},
message: {type: String, require: true, min: 1, max: global.messageMaxLength},
time: Number,
seen: Boolean
}
const MessageSchema = mongoose.Schema({
_id: {type: mongoose.Schema.Types.ObjectId, ref: 'User', require: true},
messages: [messageObject]
}, {versionKey: false}) ;
module.exports = mongoose.model('Messages', MessageSchema);
It takes in entries successfully. Example of entries:
"_id": "5fb3d971e6092d2da001bbad",
"messages": [
{
"_id": "5fc58bfe0e0ffb313c27fa0a",
"message": "Hello user",
"time": 1606781949959,
"seen": false
},
{
"_id": "5fc58c010e0ffb313c27fa0b",
"message": "Hello user",
"time": 1606781953442,
"seen": false
},
{
"_id": "5fc58c020e0ffb313c27fa0c",
"message": "Hello user",
"time": 1606781954137,
"seen": false
}
]
}
I want only the seen:false messages now, but when I try to code that in find, or aggregate, I get all the data, ie, the above records:
MessageModel.find({$and: [{_id: req.userData.userid}, {'messages.seen': false}]}).then(result => {
res.status(200).json({
message: "passed",
result,
});
})
It however works fine and returns [] if I give {"messages.seen": null}}, and 1 entry with seen: null will return an entire array.
Ive seen all forums there's no place where anybody has encountered this kind of error. Please help.
Thanks
Your message objects are nested inside a document and Mongo will only return either the full document or specific top level fields you have projected. Mongo will not filter out objects within a nested array (as is the case with your schema). So if any of the message objects within the array match the selector, the whole document itself passes the match and returns as a result.
Your options are to either:
Filter out the true/false messages within your code
Change your db structure so that you have a separate messages collection where each document is a single message. So in your example, the collection would look like:
{
"_id": "5fc58bfe0e0ffb313c27fa0a",
"parentId": "5fb3d971e6092d2da001bbad",
"message": "Hello user",
"time": 1606781949959,
"seen": false
},
{
"_id": "5fc58c010e0ffb313c27fa0b",
"parentId": "5fb3d971e6092d2da001bbad",
"message": "Hello user",
"time": 1606781953442,
"seen": false
},
{
"_id": "5fc58c020e0ffb313c27fa0c",
"parentId": "5fb3d971e6092d2da001bbad",
"message": "Hello user",
"time": 1606781954137,
"seen": false
}
And then you can query that collection with:
{ "parentId": "5fb3d971e6092d2da001bbad","seen": false}

Check if user id is in mongoose document - if yes, change property value to true?

I have an expressjs router that looks in MongoDB collection using the mongoose findById method. It returns an object where inside there exist an userHasSelected array with users id. I dont want to return users id, but just check if current users (the one who made the request) exist in the array. If he is then return true instead of returning the user id.
The verifytoken middleware in the router adds a user id property to the request.That user id is available in the get router message - can i somehow pass that to the Mongoose schema ???
//My router
router.get('/challenge/:challengeId', verifyToken ,function (req, res){
//+ Return one challenge and its options
//- Check if userId is set to options and winner properties
let userId = req.userId;
console.log(userId);
let challengeId = req.params.challengeId;
Challenge.findById(challengeId, (err, suc)=>{
if(err){
res.status(304).send(err);
}
Challenge.
res.status(200).send(suc);
});
})
// And the mongoose Schema
const mongoose = require('mongoose');
var Schema = mongoose.Schema;
//Optionsschema is use in the ChallengeSchema
var OptionSchema = new Schema({
name: { type: String},
isCorrect : { type: Boolean },
description: { type: String },
image : { type : String },
userHasSelected : { type : Object, get : returnUserChallengeStatus}
})
OptionSchema.set('toObject', { getters: true });
OptionSchema.set('toJSON', { getters: true });
var ChallengeSchema = new Schema({
shortEventId : String,
organization: String,
name: String,
winner: String,
options : [OptionSchema]
});
ChallengeSchema.set('toObject', { getters: true });
ChallengeSchema.set('toJSON', { getters: true });
ChallengeSchema.virtual('born').get(function(value) {
return this.name + "plus andet"
});
module.exports = mongoose.model('challenge', ChallengeSchema);
So again - I dont want to return the user id from the userHasSelected array - just check if he is there and if yes, use a getter or a method to set value to true.
Updated explanation
The findById returns this object / document
{
"_id":"5b86dc5bfb6fc03893e55001",
"shortEventId": "d2d3",
"organization": "Braedstrup",
"name": "1. december",
"winner": "5b864cbaa9ce291b148ddd6d",
"options": [
{
"name": "Matas",
"isCorrect": "true",
"description": "Matas er byens førende indenfor pleje og Matas er byens førende indenfor pleje og omsorg Matas er byens førende indenfor pleje og omsorg",
"image": "https://cityxpstorage.blob.core.windows.net/images/Matas.png",
"userHasSelected": [
{
"userId": "5b864cbaa9ce291b148ddd6d"
}
]
},
{
"name": "Føtex",
"isCorrect": "false",
"description": "Føtex er en dejlig butik",
"image": "https://cityxpstorage.blob.core.windows.net/images/Føtex.png"
},
{
"name": "Kvickly",
"isCorrect": "false",
"description": "Kvickly er en dejlig butik",
"image": "https://cityxpstorage.blob.core.windows.net/images/Matas.png"
},
{
"name": "MC Jørgensen",
"isCorrect": "false",
"description": "MC Jørgensen er en dejlig butik",
"image": "https://cityxpstorage.blob.core.windows.net/images/Matas.png"
}
],
"startDate": "2018-10-06T00:00:00.000Z",
"endDate": "2018-10-06T23:59:00.000Z"
}
So the nested array 'userHasSelected' contains information about the users id. I do not want to send that - instead I would like to a {userId : true}.
I have read that getters a able to handle outgoing data.
Posible Solution
I could make the check inside the router get method before returning the object to the client like this
// If user is in array set user to true. I would like to move this responsibility to the schema / document.
suc.options.forEach(option => {
if(Array.isArray(option.userHasSelected))
option.userHasSelected = {userId : true}
});
But I would really like schema to be responsible for that - Is that possible?
I had similar issue and found a workaround. Simply create an optional field on your responsible modal schema, let call it "status". On your controller, check if your array includes requested user's id and write to that field. For example;
on your schema;
caseStatus: {
type: Boolean,
required: false
},
voters: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
}],
then on your controller;
let theCase = await Case.find({speciality: res.locals.user_speciality }).exec();
let status = theCase.voters.includes(res.locals.user_id);
caseItem.caseStatus = status;

Best way to wrap mongoose validation error

We know that mongoose provides us an easy way to do validation. But suppose you are using express+mongoose to building a microservice; and some clients (could be web-app, mobile app etc.) needs to consume it.
Usually, I prefer to response JSON back with simple error code and message. In most cases, the clients who can create their own messages depending on which language they are showing to users.
By default, if we catch the error from mongoose, we can get JSON response such as:
JSON Response
{
"errors": {
"price": {
"message": "Path `price` (-1) is less than minimum allowed value (0).",
"name": "ValidatorError",
"properties": {
"min": 0,
"type": "min",
"message": "Path `{PATH}` ({VALUE}) is less than minimum allowed value (0).",
"path": "price",
"value": -1
},
"kind": "min",
"path": "price",
"value": -1,
"$isValidatorError": true
},
"code": {
"message": "Product with given code already exists",
"name": "ValidatorError",
"properties": {
"type": "user defined",
"message": "Product with given code already exists",
"path": "code",
"value": "p-1000"
},
"kind": "user defined",
"path": "code",
"value": "p-1000",
"$isValidatorError": true
}
},
"_message": "Product validation failed",
"message": "Product validation failed: price: Path `price` (-1) is less than minimum allowed value (0)., code: Product with given code already exists",
"name": "ValidationError"
}
Restful Api Controller
exports.createOne = async(function* list(req, res) {
try {
const product = new Product(req.body)
const newProduct = yield product.save()
res.json(newProduct)
} catch (err) {
res.status(400).json(err)
}
})
Model Product.js
const mongoose = require('mongoose')
const Schama = mongoose.Schema
const minlength = [5, 'The value of `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH}).'];
const ProductSchema = new Schama({
code: { type: String, required: true, minlength, index: true, unique: true, trim: true, lowercase: true },
name: { type: String, required: true, trim: true },
price: { type: Number, required: true, min: 0, max: 100000 },
categories: [String],
})
ProductSchema.path('code').validate(function uniqueEmail(code, fn) {
const Product = mongoose.model('Product')
// Check only when it is a new Product or when code field is modified
if (this.isNew || this.isModified('code')) {
Product.find({ code }).exec((err, products) => {
fn(!err && products.length === 0)
})
} else fn(true)
}, 'Product with given code already exists')
ProductSchema.statics = {
/**
* List products
*
* #param {Object} options
* #api private
*/
pageList: function pageList(conditions, index, size) {
const criteria = conditions || {}
const page = index || 0
const limit = size || 30
return this.find(criteria)
.populate('user', 'name username')
.sort({ createdAt: -1 })
.limit(limit)
.skip(limit * page)
.exec()
},
}
mongoose.model('Product', ProductSchema)
What I expect
I am trying to wrap the error message to make it simple to consumer.
It could be like:
{
"errors": [
{
"message": "Path `price` (-1) is less than minimum allowed value (0).",
"code": "100020"
},
{
"message": "Product with given code already exists",
"code": "100021"
}
],
"success": false
}
The code and the corresponding message will be maintained on api documents. The message is usualy useful for consumer to understand the code and consumer (such as web client) could create their own message such as French messages according to the code and show to end users.
How can I leverage mongoose's valiation to accomplish this? Maybe I could loop erros's properties and combine an code using ${path}-${kind}.
I know that in most case, Client side should do the validation before calling apis. But there must be some cases that errors have to be thrown by APIs.
Any idea for this?

DynamoDB: Dynamoose update ($PUT default behaviour) not working as expected?

I'm trying to implement a PUT (update) API endpoint, passing the primary key of an element as a parameter in the path and the attributes I want to update as parameter in the body. But is not working as expected, instead of updating the attributes of an existing element, is creating a new element in the database with wrong attributes.
As far as I understand from the Dynamoose API documentation, Model.update(key, update, options, callback) updates and existing item in the table. For example, if we have a Dog model where age is one of the attributes, then this code
Dog.update({ownerId: 4, name: 'Odie'}, {age: 1}, function (err) {
if(err) { return console.log(err); }
console.log('Just a puppy');
})
would update the age of a Dog called 'Odie' with ownerId: 4
Now, I tried to do a similar update for my own API. I have a data model called InvoiceConfig where the primary key corresponds to a unique name/id (string) attribute and contains another attribute called providerVariants (an array of objects)
This is my API definition in swagger:
put:
description: Updates the invoice config matching the id (providerName)
operationId: updateInvoiceConfigByName
consumes:
- application/json
produces:
- application/json
parameters:
- name: id
in: path
required: true
type: string
- name: providerVariants
description: array of JSON objects
in: body
required: true
schema:
$ref: "#/definitions/ProviderVariantsDataList"
responses:
"200":
description: A confirmation message
schema:
$ref: "#/definitions/ResponseMessage"
"500":
description: Error message
schema:
$ref: "#/definitions/ErrorResponse"
And this is the function that implements the dynamoose update in my code:
updateInvoiceConfigByName: function(req, res) {
var name = req.swagger.params.id.value;
var providerVariants = req.body;
console.log("UPDATE /invoice-config");
InvoiceConfig.update({provideName: name}, providerVariants, function(err) {
if (err) {
console.log("Update InvoiceConfig error");
throw err;
}
res.status(200).send({
providerName: `${name}`,
message: 'provider invoice cfg updated'
});
});
}
I have an object in the database:
{
"providerName": "ID01",
"providerVariants": [
{
"displayName": "My 1st Provider ",
"phone": "915698471",
"availableTemplates": [
"default"
]
}
]
}
and I try to update it from swagger-ui passing the following paramenters:
ID01 in the endpoint path itself and a modified providerVariants array in the body:
[
{
"displayName": "My new name",
"phone": "913333333",
"availableTemplates": [
"default"
]
}
]
But as I said at the beginning, if I check the contents of my table, I see that the item with providerName "ID01" has not changed, and there is a new item created:
{
"providerName": {
"S": "[object Object]"
}
}
I suspect that in this new object the providerName (primary key) was populated with the providerVariants array, which is totally wrong. Any hints about how to fix this are welcome, I don't know how to proceed with the update. Other endpoints (get, delete, post) in my API are working fine, but I'm blocked with the update/put
There is a typo in your update.
InvoiceConfig.update({provideName: name}, providerVariants, function(err)
You're missing the 'r' in providerName :)

Resources