20

I have a userSchema like so:

var userSchema = new Schema({
    name: {
      type: String
    , required: true
    , validate: [validators.notEmpty, 'Name is empty']
    }
  , username: {
      type: String
    , required: true
    , unique: true
    , validate: [validators.notEmpty, 'Username is empty']
    }
});

The username field is supposed to be unique. Mongoose will throw an error if this username already exists in the database. However, it is not case insensitive, which I need it to be.

Am I right in thinking that the only way to achieve a case insensitive unique check is to write my own validation rule, which will perform a query on the collection? Is it OK to write validation checks like this, creating more connections to the collection? I will need to do something similar for email, too.

9 Answers9

19

What about using:

{ type: String, lowercase: true, trim: true }

to achieve your purpose?

sqreept
  • 5,236
  • 3
  • 21
  • 26
  • 3
    Won't that force the username to be lowercase? I would like the username check to be case insensitive without forcing the username to have a lowercase username. –  Dec 21 '12 at 14:14
  • 6
    That trick will spare you from additional queries to the DB. But to also keep the username in the original casing you could have a separate field that you don't use for uniqueness. – sqreept Dec 21 '12 at 14:17
  • Good suggestion but it seems a bit hacky. Is it expensive to do more additional queries? Bearing in mind I will need to do similar checks for other fields. –  Dec 21 '12 at 14:20
  • 1
    This is the best solution I've been able to find so far: http://fabianosoriani.wordpress.com/2012/03/22/mongoose-validate-unique-field-insensitive/. –  Dec 21 '12 at 14:54
  • Additional queries are more expensive and the gravity of the situation greatly depends on your DB size, structure & network layout. If app server is on the same machine as MongoDB, additional queries are not that expensive. If they're far away, you'll be able to drink a sip of coffee for each query. – sqreept Dec 21 '12 at 14:59
  • BTW, which is the situation where an username needs to preserve case? I haven't seen this in practice yet. – sqreept Dec 21 '12 at 15:03
  • Ha, good way to put it. What do you mean? Twitter, for example, preserves case of usernames. –  Dec 21 '12 at 15:04
  • I guess I can either change the design of my model so that username does not preserve case, or have the overhead of an extra connection to the DB. That's my answer! While we're on the subject of Mongoose schemas and validation, would you mind looking at this [similar issue of mine](http://stackoverflow.com/questions/13982159/validating-password-confirm-password-with-mongoose-schema/13982399)? –  Dec 21 '12 at 15:06
  • For the record, similar issue and method discussed here: https://groups.google.com/forum/#!msg/mongoose-orm/BX7kz0BwLjk/iedDQ7CLmOEJ –  Dec 21 '12 at 15:15
  • 1
    There is really no way around it (if you're going to use MongoDB). If you want uniqueness to be case insensitive, but you want users to be able to use WhaTeVEr casing they want, you'll need two fields. I do this for usernames, email addresses, URLs. – danmactough Dec 21 '12 at 15:40
13

A collation with strength: 2 on the index solves this issue.

index: {
  unique: true,
  collation: {
    locale: 'en',
    strength: 2
  }
}

Place this in your Schema creation code as follows:

var userSchema = new Schema({
  ...
  username: {
    type: String,
    required: true,
    index: {
      unique: true,
      collation: { locale: 'en', strength: 2 }
    }
});

Note: make sure the index on the model gets updated - you might need to do this manually.

Andrew Philips
  • 1,950
  • 18
  • 23
Kristianmitk
  • 4,528
  • 5
  • 26
  • 46
  • 1
    This is the proper method if you're running Mongoose with MongoDB 3.4+. – Andrew Philips Apr 06 '19 at 02:49
  • 1
    Could you please edit your answer to explain what is 'collation' and 'index' in the mongoose model? –  Jul 18 '19 at 09:04
  • @ArturTagisow The collation is a feature in MongoDB which allows for some universally accepted ways to treat different characters as the same character. For example e and é. The strength is a number out of 5 which determines the "strictness" of comparison. Any number over 3 is for rare cases in specific languages. The locale is the rule-set which defines which characters are considered to be the same. You can learn more here: http://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations – Anthony Yershov Jun 24 '20 at 05:48
6

... with mongoose on NodeJS that query:

const countryName = req.params.country;

{ 'country': new RegExp(`^${countryName}$`, 'i') };

or

const countryName = req.params.country;

{ 'country': { $regex: new RegExp(`^${countryName}$`), $options: 'i' } };

// ^australia$

or

const countryName = req.params.country;

{ 'country': { $regex: new RegExp(`^${countryName}$`, 'i') } };

// ^turkey$

A full code example in Javascript, NodeJS with Mongoose ORM on MongoDB

// get all customers that given country name
app.get('/customers/country/:countryName', (req, res) => {
    //res.send(`Got a GET request at /customer/country/${req.params.countryName}`);

    const countryName = req.params.countryName;

    // using Regular Expression (case intensitive and equal): ^australia$

    // const query = { 'country': new RegExp(`^${countryName}$`, 'i') };
    // const query = { 'country': { $regex: new RegExp(`^${countryName}$`, 'i') } };
    const query = { 'country': { $regex: new RegExp(`^${countryName}$`), $options: 'i' } };

    Customer.find(query).sort({ name: 'asc' })
        .then(customers => {
            res.json(customers);
        })
        .catch(error => {
            // error..
            res.send(error.message);
        });
});
aygunyilmaz
  • 399
  • 4
  • 5
  • just wanted to say that regex queries are not recommended by anyone, including mongodb themselves. – tubbo Nov 18 '21 at 15:27
2

I don't know if you are doing this in node. But you can use a npm like this: https://github.com/blakehaswell/mongoose-unique-validator to check unique validation on the fields of collections. Other way could be checking in the collection every time a new requests come. http://timstermatic.github.io/blog/2013/08/06/async-unique-validation-with-expressjs-and-mongoose/ You can refer the material here and use it the way suitable for your case.

Avinash
  • 419
  • 2
  • 11
  • While the links you provided may answer the question, it is best to put the essential parts of your solution directly in your Stack Overflow answer in case the links expire in the future. – Kmeixner Mar 03 '16 at 19:39
  • Both `mongoose-unique-validator` and the technique in the timstermatic blog are fundamentally flawed because they do a separate query to check for uniqueness before the insert/update, so if another insert/update runs after the validator runs, both will get inserted. See https://github.com/blakehaswell/mongoose-unique-validator#caveats. The only way to guarantee uniqueness is with a unique index in the database. – ZachB Jun 08 '17 at 22:03
1

The best way is to use already existing npm packages as shared below. https://www.npmjs.com/package/mongoose-unique-validator

To make it case sensitive you can follow uniqueCaseInsensitive in the same page.

No need to write your own validation logic when there is already a package available for this(follow Avinash's post too).

Community
  • 1
  • 1
1

I use mongoose-unique-validator

Example :

const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const { Schema } = mongoose;

const UserSchema = new Schema({
  name: {
    type: String,
    required: true,
    unique: true,
    index: true,
    maxlength: 100,
    trim: true,
    uniqueCaseInsensitive: true
  },
  username: {
    type: String,
    required: true,
    unique: true,
    index: true,
    maxlength: 100,
    trim: true,
    uniqueCaseInsensitive: true
  }
});

UserSchema.plugin(uniqueValidator, {
  message: 'Error, expected {PATH} to be unique.'
});

module.exports = mongoose.model('User', UserSchema);

vanduc1102
  • 5,769
  • 1
  • 46
  • 43
0

Very simple solution

username : {
        trim:true,
        //lowercase:true,

        type:String,
        required:[true, '{PATH} is required.'],
        match : [
            new RegExp('^[a-z0-9_.-]+$', 'i'),
            '{PATH} \'{VALUE}\' is not valid. Use only letters, numbers, underscore or dot.'
        ],
        minlength:5,
        maxlength:30,
        //unique:true

        validate : [
            function(un, cb){
                console.log(v);
                student.findOne({username:/^un$/i}, function(err, doc){
                    if(err) return console.log(err);
                    if(!_.isEmpty(doc)) return cb(false);
                    return cb(true);
                });
            },
            'Username already exists.'
        ]
    },

Here, I am using async validation and checking in my model student if same field exist. Use can obviously use regex if you want.

But I would not recommend this method, it just doesn't fit in my head.

Instead stick with { type: String, lowercase: true, trim: true, unique:true } approach and copy the original username to some other field in case you need it.

Uday Hiwarale
  • 4,028
  • 6
  • 45
  • 48
0

I had the same issue while working on a project.I simply make it simple with two lines of code. converting all incoming value to small letters .

let getUsername = req.body.username;
let username = getUsername.toLowerCase();
Léa Gris
  • 17,497
  • 4
  • 32
  • 41
-2

How about using a regular expression?

var pattern = [ /some pattern/, "{VALUE} is not a valid user name!" ];

{ type: String, match: pattern }

For further reference: http://mongoosejs.com/docs/api.html#schematype_SchemaType-required

Sahil
  • 504
  • 6
  • 9