1

I’m trying to find duplicate usernames in the database when a new user registers before a user is authenticated using Firebase Cloud Functions as I was suggested, as the .queryEqual(toValue) locally won’t work every time. What I can’t get my head around now is how to make use of it in my app. Here’s the cloud function I deployed:

// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');

// The Firebase Admin SDK to access the Firebase Realtime Database. 
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.uniqueUsername = functions.https.onRequest((req, res) => {
    const username = req.query.username
    admin.database().ref('users').orderByChild('username').equalTo(username).once('value').then(snap => {
        // if the child exists, then the username is taken
        if (snap.exists()) {
            res.send('username not available');
        } else {
            res.send('username available');
        }
    }); 
});

The equalTo(username) is a value picked from UITextField and then compared to in the database. The error I get here is:

Error: Query.equalTo failed: First argument contains undefined in property 'users'
    at Error (native)
    at Ae (/user_code/node_modules/firebase-admin/lib/database/database.js:105:67)
    at ze (/user_code/node_modules/firebase-admin/lib/database/database.js:104:400)
    at W.h.Jf (/user_code/node_modules/firebase-admin/lib/database/database.js:142:60)
    at exports.uniqueUsername.functions.https.onRequest (/user_code/index.js:10:60)
    at cloudFunction (/user_code/node_modules/firebase-functions/lib/providers/https.js:26:47)
    at /var/tmp/worker/worker.js:649:7
    at /var/tmp/worker/worker.js:633:9
    at _combinedTickCallback (internal/process/next_tick.js:67:7)
    at process._tickDomainCallback (internal/process/next_tick.js:122:9)

And the error I get when I use the URL in the browser is:

Error: could not handle the request

What am I doing wrong? And what should I do next?

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
waseefakhtar
  • 1,373
  • 2
  • 24
  • 47
  • How are you invoking this Cloud Function? It looks like `username` doesn't have a value. You might want to check that and exit if this happens: `if (!username) return;` – Frank van Puffelen Jul 08 '17 at 15:13
  • @FrankvanPuffelen yes, but how do I take a value from my app via text field and let the function know about it? how can I link username in Cloud Functions with usernameTextField text in my app? – waseefakhtar Jul 08 '17 at 17:03
  • That depends on the code that invoked this Cloud Functions. Please update your question to include that code too. This might turn out to be a pure [how to pass parameters in a HTTP request in Swift](https://stackoverflow.com/questions/27723912/swift-get-request-with-parameters) problem. – Frank van Puffelen Jul 08 '17 at 19:13

1 Answers1

2

From that error message, you are passing a value of undefined into the equalTo function.

Before performing the query (and for any query) you want to use a "fail-fast" methodology. In this case, if req.query.username is falsy, return an error.

exports.uniqueUsername = functions.https.onRequest((req, res) => {
  const username = req.query.username;
  if (!username) {
    res.status(400).json({error: 'Missing username GET parameter'});
    return;
  }
  // rest of your code.
}

A Better Approach

For any key that requires uniqueness, you want to use an index. In your database, it would look similar to:

"/usernames": {
  "bob": "userId1",
  "frank": "userId2",
  "c00lguy": "userId3"
}

In order to check if a username is free, I would recommend getting a user logged in and authenticated first. This makes sure that we have a User ID to associate with the username. Once they log in, check if they haven't set a username yet and if they don't, prompt them for one using some form of dialog/input.

To check the availability of a username and claim it in a 'single' operation, you can call the following code: (internally it's not a single operation)

var desiredUsername = '...'; // from the form.
var userId = '...'; // the current logged in user's ID (normally from user.uid)
var ref = firebase.database().ref('/usernames').child(desiredUsername);
ref.transaction(function (currentData) {
  if (currentData !== null && currentData !== userId) {
    return; // returns undefined. Which aborts the operation because there is data here. (someone owns that username other than the current user).
  }
  // if here, username is available or owned by this user.
  return userId; // returning a value will write it to the database at the given location.
})
.then(function(transactionResult) {
  if (transactionResult.committed) {
    // username is now owned by current user
    // do something
  } else {
    // username already taken.
    // do something
  }
})
.catch(function (error) {
  // if here, an error has occured
  // (probably a permissions error, check database rules)
  // do something
});

This snippet makes use of a Transaction operation which allows you to read a value from the database and then act on it instantly. In your original code, between the time you call the uniqueUsername function and claim that username, someone else could do so. Using a transaction minimises the window of opportunity to do this by claiming the username for the user as soon as it knows it's free.

This method is faster than searching over the database, and allows you to lookup users by username using a simple query.

The below code is an example of such a query and will return null for unclaimed usernames.

function findUserIdByUsername(username) {
  if (!username) {
    return Promise.reject('username is falsy');
  }
  firebase.database().ref('/usernames').child(username).once('value')
  .then(snapshot => {
    if (!snapshot.exists()) {
      return null; // no user with that username
    }
    return snapshot.val(); // the owner's user ID
  });
}

Combined with your original code, it would look like this:

exports.uniqueUsername = functions.https.onRequest((req, res) => {
  const username = req.query.username;
  if (!username) {
    res.status(400).json({error: 'Missing username GET parameter'});
    return;
  }
  var ownerID = findUserIdByUsername(username);
  res.json({taken: (ownerID !== null)}); // Returns {"taken": "true"} or {"taken": "false"}
}
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
  • That means I can do so without the need of Cloud Functions. And what’s the need of a separate username node? Can’t I use a Transaction block on users/username rather than /username? Also, what if I want to match the usernames without authenticating the user first? Is my approach of signing up a user wrong? – waseefakhtar Jul 08 '17 at 17:01