Mongoose document schema and validation - mongodb

I Have a schema like so:
class Schemas
constructor: ->
#mongoose = require 'mongoose'
#schema = #mongoose.Schema
#EmployeeSchema = new #schema
'firstname': { type: String, required: true },
'lastname': { type: String, required: true },
'email': { type: String, required: true, index: { unique: true }, validate: /\b[a-zA-Z0-9._%+-]+#[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}\b/ },
'departmentId': { type: #schema.ObjectId, required: true }
'enddate': String,
'active': { type: Boolean, default: true }
#EmployeeSchemaModel = #mongoose.model 'employees', #EmployeeSchema
#DepartmentSchema = new #schema
'name': { type: String, required: true, index: { unique: true } }
'employees' : [ #EmployeeSchema ]
#DepartmentSchemaModel = #mongoose.model 'departments', #DepartmentSchema
So that my employees live in an array of employee documents inside a department
I have several department documents that have a number of employee documents stored in the employees array.
I then added a new department but it contained no employees. If I then attempt to add another department without employees, Mongoose produces a Duplicate key error for the employee.email field which is a required field. The employee.email field is required and unique, and it needs to be.
Is there anyway round this?

If you enable Mongoose debug logging with the coffeescript equivalent of mongoose.set('debug', true); you can see what's going on:
DEBUG: Mongoose: employees.ensureIndex({ email: 1 }) { safe: true, background: true, unique: true }
DEBUG: Mongoose: departments.ensureIndex({ name: 1 }) { safe: true, background: true, unique: true }
DEBUG: Mongoose: departments.ensureIndex({ 'employees.email': 1 }) { safe: true, background: true, unique: true }
By embedding the full EmployeeSchema in the employees array of DepartmentSchema (rather than just an ObjectId reference to it), you end up creating unique indexes on both employees.email and department.employees.email.
So when you create a new department without any employees you are 'using up' the undefined email case in the department.employees.email index as far a uniqueness. So when you try and do that a second time that unique value is already taken and you get the Duplicate key error.
The best fix for this is probably to change DepartmentSchema.employees to an array of ObjectId references to employees instead of full objects. Then the index stays in the employees collection where it belongs and you're not duplicating data and creating opportunities for inconsistencies.

Check out these references:
http://docs.mongodb.org/manual/core/indexes/#sparse-indexes
mongoDB/mongoose: unique if not null (specifically JohnnyHK's answer)
The short of it is that since Mongo 1.8, you can define what is called a sparse index, which only kicks in the unique check if the value is not null.
In your case, you would want:
#EmployeeSchema = new #schema
'firstname': { type: String, required: true },
'lastname': { type: String, required: true },
'email': { type: String, required: true, index: { unique: true, sparse: true }, validate: /\b[a-zA-Z0-9._%+-]+#[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}\b/ },
'departmentId': { type: #schema.ObjectId, required: true }
'enddate': String,
'active': { type: Boolean, default: true }
Notice the sparse: true added to your index on EmployeeSchema's email attribute.
https://gist.github.com/juanpaco/5124144

It appears that you can't create a unique index on an individual field of a sub-document. Although the db.collection.ensureIndex function in the Mongo shell appears to let you do that, it tests the sub-document as a whole for its uniqueness and not the individual field.
You can create an index on an individual field of a sub-document, you just can't make it unique.

Related

Is there a way to conditionally set 'unique' in Mongoose Schema?

I came across a problem while implementing user deletion functionality. A
Suppose I have a model:
const UserSchema = new mongoose.Schema(
{
email: { type: String, required: true, unique: true },
name: { type: String, required: true },
password: { type: String, required: true },
deleted: {type: Date, default: null}
},
{ timestamps: true }
);
This clearly states that the field email has to be unique. However, I would like to set it unique only in the set filtered for deleted != null.
In other words, I would like to filter out the deleted users' records before checking if it is unique or not.
Are there any best practices regarding this?
Or should I just create a field called del-email and null the email field to avoid over-complication and preserve the data?
You can try,
Partial index with unique constraint:
The partial unique index, you can specify the filter expression condition, if it matches then the unique index will take the role,
UserSchema.index(
{ email: 1 },
{ unique: true, partialFilterExpression: { deleted: { $eq: null } } }
);
Note:
As noted in the query coverage documentation for partial indexes:
To use the partial index, a query must contain the filter expression (or a modified filter expression that specifies a subset of the filter expression) as part of its query condition.
User.find({ email: "something#mail.com", deleted: null });

How to define a unique field in mongoose schema?

My Schema is like below:
const schema = new mongoose.Schema({
a: {
b: { type: String, unique: true },
c: { type: String }
},
aa: {
bb: [{
cc: { type: String, unique: true },
dd: { type: String }
}]
}
})
now I want to 'b' and 'cc' fields be unique.
how can i do this?
I added this code at the end of the top code, but the schema allows duplicate values.
schema.index({'a.b':1}, {unique:true})
schema.index({'aa.bb.cc':1, {unique:true})
Do you have any idea to solve this problem?
try adding dropDups as shown below.
const schema = new mongoose.Schema({
a: {
b: { type: String, unique: true, dropDups: true },
c: { type: String }
},
aa: {
bb: [{
cc: { type: String, unique: true, dropDups: true },
dd: { type: String }
}]
}
})
if it doesn't work try: -
const schema = new mongoose.Schema({
a: {
b: { type: String, index: { unique: true, dropDups: true } },
c: { type: String }
},
aa: {
bb: [{
cc: { type: String, index: { unique: true, dropDups: true } },
dd: { type: String }
}]
}
})
The answer here depends on exactly what you mean by "I want to 'b' and 'cc' fields be unique."
Mongoose implements the unique constraint by creating a MongoDB index on that field with the unique:true option.
MongoDB enforces the unique option by not allowing the same value to be stored twice in the index.
When a document is written to MongoDB, it extracts the field values from the document that are required by the index, deduplicates the list, and then stores the values in the index with a pointer to the document.
This means that only 1 document may contain a specific value, but that document may contain that value many times.
For example:
If there is an index on {i:1}, these sample documents would have the following entries in the index:
Document
Index entries
{i:1}
1=>
{i:[2,3,4]
2=>
3=>
4=>
{i:[5,6,5,6,5]}
5=>
6=>
{i:[2,6]}
2=>
6=>
If the index were unique, and the documents were inserted in exactly that order, the first 3 documents would be perfectly acceptible, and the last would result in a duplicate key error.

How can I use partialFilterExpression on a mongoose model

I have created a mongoose model that has an email field. I want it to be unique if a value is provided by a user but I want it to be empty is a user has not provided any value. I have found a good mongodb reference here: https://docs.mongodb.com/manual/core/index-partial/#partial-index-with-unique-constraints that could work but I don't know how to make it work on mongoose
This is how the field looks like right now
email: {
type: String,
index: true,
unique: true
}
If I leave it the way it is, I cant create multiple documents with an empty/null email field
In the email path level, you can use only:
email: {
type: String
}
And in the schema level use:
SchemaName.index({ email: 1 }, {
unique: true,
partialFilterExpression: {
'email': { $exists: true, $gt: '' }
}
});
This way the unique constraint is applied only if email exists and is not an empty string
You can have something like :
email: {
type: String,
index: {
unique: true,
partialFilterExpression: { email: { $type: 'string' } },
},
default : null
}
but do read below link, before you actually implement it, as defaults seems to work only on new document inserts :-
Mongoose v5.6.9 Documentation
You can use sparse
email: {
type: String,
unique: true,
sparse: true
}
That way if you dont't send the email field at all mongo will not add automatically null value for the field. It will just skip it.

How to specify field which is array of objects, objects of array has one of attribute as unique but optional in Mongoose?

This is my schema for employee personal information
const mongoose = require('mongoose');
const PersonalInformationSchema = mongoose.Schema({
name: {
type: String,
required: true
},
emails: [{
email: {
type: String,
trim: true,
unique: true
},
isPrimary: {
type: Boolean
}
}]
}, {
timestamps: true
});
module.exports = mongoose.model('PersonalInformation', PersonalInformationSchema);
In my case I have array emails which is optional for employee but should be unique. When I insert a record without emails, document saves successfully (with empty array emails: []) but in next try it says
"err": {
"driver": true,
"name": "MongoError",
"index": 0,
"code": 11000,
"errmsg": "E11000 duplicate key error collection: EmployeePersonalInformation.personalinformations index: emails.email_1 dup key: { : null }"
}
You can add sparse: true to your schema, try:
const PersonalInformationSchema = mongoose.Schema({
name: {
type: String,
required: true
},
emails: [{
email: {
type: String,
trim: true,
unique: true,
sparse: true
},
isPrimary: {
type: Boolean
}
}]
}, {
timestamps: true
});
The sparse property of an index ensures that the index only contain entries for documents that have the indexed field. The index skips documents that do not have the indexed field.
So when empty documents will be skipped you won't get an error from unique constraint.

sails.js join tables on mongodb native id

I have two collections in mongodb database and model for each of them
App Model
module.exports = {
tableName: 'app',
attributes: {
_id : {
primaryKey: true,
unique: true,
type: 'string',
},
userId: {
model: 'user'
},
title: {
type: 'string',
required: true,
unique: true,
},
createdDate : 'string'
},
};
and User Model
module.exports = {
tableName: 'user',
attributes: {
id : {
primaryKey: true,
unique: true,
type: 'string',
collection: "app",
via : "userId"
},
password: {
type: 'string',
required: true
},
apps : {
collection: "app",
via : "userId"
}
},
};
When i use numeric values for join this collection, it works fine, but when i try do it with mongodb native id object, i get the empty result
How i call join query
User.find().populate('apps').exec(function(err, result) {});
You need to get rid of both the _id and id attribute definitions in your models. Waterline will handle the primary key fields for you automatically (normalizing them to id), so unless you need to change the field type, they can be safely left out. Also, I'm not sure what your intention was by adding collection and via to the id definition, but the primary key is never going to be an association.
Otherwise, your models look correct. If you get rid of those two attributes, things should work fine.