Logo
Published on

How to Enforce Unique Field Values in Cloud Firestore

Introduction

The Cloud Firestore is an awesome platform for quickly prototyping your app ideas into reality, however, it certainly comes with some limitations.

Currently, there is no way for you to enable uniqueness in field values in the whole database. There is a quick workaround, though with queries that you can use.

Table of Contents

The Problem

You want to enforce uniqueness for a field value in your Firestore database for your app.

Use Case

An app where users have their profiles and a unique username assigned to them.

Natively, the Cloud Firestore provides us with no uniqueness check from either Rules or the SDK. But, of course, there is a workaround.

The Workaround

You can use Firestore queries to get the document ID which corresponds to the field you want to keep unique. If the query returns null, that would mean that the database doesn't contain that value, ergo, the username, in this case, is available or vice versa.

Of course, you'd like to see some code sample, so let's get to that.

Consider a sample usernames collection with and a few child documents. Our database is set up such that each document corresponds to a single user profile data.

The document schema is as follows:

/usernames/
    {johndoe}/
        name: "John Doe"
        email: "[email protected]"
    {janedoe}/
        name: "Jane Doe"
        email: "[email protected]"
    ...
/...

To keep things simple, we will simply focus on keeping the username field unique in our database.

The simplest approach for this is this:

  1. Create a collection usernames containing a unique doc for each user. The doc-id of the document should be the <username> itself.

  2. When a user tries to sign up, check the usernames collection to see if a document with the id equal to the username that the user entered is available or not.

  3. If the doc exists, the username is taken. Else, allow signup.

Here's the code for the Javascript web SDK:

'use strict'

/** firebase init start */
firebase.initializeApp({
  apiKey: '####',
  authDomain: '####',
  projectId: '####',
})

const db = firebase.firestore()
/** firebase init end */

const createUserInFirestore = (username) => {
  db.collection('usernames')
    .get(inputUsername())
    .then((doc) => {
      if (doc.exists) {
        return usernameUnavailable()
      }

      return createAccount()
    })
    .catch((error) => handleError(error))
}

/*
 * make sure that you trim() and use toString() methods (if required)
 * to ensure that the inputUsername only contains the exact
 * string that you have stored in the Firestore
 * with no starting or ending spaces.
 */
const inputUsername = getUsernameFromInput()

createUserInFirestore(inputUsername())

Looks very simple, right?

Well… There is a problem with this approach.

Since you have to get a doc, you need to give the clients access to the users collection.

If you don't set up the Firestore rules correctly, chances are that someone will be able to query your database to get the list of all usernames.

Creating User With Cloud Functions

The approach we will follow here is this:

  1. Get the username from the user when they click signup after filling out their details.

  2. Send a GET request to the server with the username in the query parameter. const url = https://us-central1-your-project-name.cloudfunctions.net/checkUsernames?username=${getUsernameFromInput()};

  3. In the usernames collection, check for a document with the id that is the username from the query parameter.

  4. If the doc exists, the username is taken, else, create the account.

Seems pretty simple.

Let's see the Cloud Function code.

// check-usernames.js
const admin = require('firebase-admin')

admin.initializeApp()

const db = admin.firestore()

const checkUsernames = (req, res) => {
  if (req.method !== 'GET') {
    return res.status(405).send(`${req.method} is not allowed. Use GET.`)
  }

  if (!req.query.hasOwnProperty('username')) {
    return res.status(400).send('No username provided.')
  }

  // Source: https://stackoverflow.com/a/52850529/2758318
  const isValidDocId = (id) => id && /^(?!\.\.?$)(?!.*__.*__)([^/]{1,1500})$/.test(id)

  // Document Ids should be non-empty strings
  if (!isValidDocId(req.query.username)) {
    return res.status(400).send('Invalid username string.')
  }

  db.collection('usernames')
    .doc(req.query.username)
    .get()
    .then((doc) => {
      /** If doc exists, the username is unavailable */
      return res.status(200).send(!doc.exists)
    })
    .catch((error) => handleError(req, res))
}

module.exports = checkUsernames
// index.js
const functions = require('firebase-functions')

const checkUsernames = require('./src/check-usernames')

module.exports = {
  checkUsernames: functions.https.onRequest(checkUsernames),
}

As you can see, this code is pretty simple. It checks the document with the id that is the username that you want to check.

If the doc exists, the username is taken.

On your client, you need to check what the response for this request is. And, based on that, you can decide whether to sign up the user.