2
\$\begingroup\$

The goal for this code is to provide a semi-secure and privileged method of user registration via a secret code that is stored as a key in the Firebase database.

Subscribers to a mailing list were all sent a 'secret' registration code. Upon completion of the registration form, RegisterFactory.register is called from the controller. One of my biggest concerns is that I have to make 5 async calls all of which return a promise and results in a long chain of promises.

The app has been in use for a month without any major issue. Before the next session starts, I would like to improve things a bit.

Please ignore the code where I un-authenticate the user as this issue needs to be fixed elsewhere first. Once fixed, I should be able to return the last Firebase promise to the controller.

The relevant Firebase schema is:

{
    "secretKey": true,

    "clientList": {
        "paid": {...}
    },

    "users": {
        "$userID": {
            "profile": {...}
        }
    }

RegisterFactory:

app.factory('RegisterFactory', ['AuthRefService', 'APP', '$q', 'appFunctions',
            function(AuthRefService, APP, $q, appFunctions){

    /**
     *  Precondition: A data structure of { 'secretKey': 'secretValue' },
     *  Postcondition: Returns a promise with the data at '/code' resolving with
     *      'secretValue' or null.
    */
    var checkCode = function(code){
        code = code.trim();
        var ref = new Firebase(APP.BASEURL);
        return ref.child(code).once('value');
    };

    /**
     *  Precondition: checkCode(code) resolves with true.
     *  Postcondition: A new user is created in Firebase and returns a 
     *      promise resolving with userData (that only contains userData.uid and does not auth user) or error.
    */
    var createUser = function(credentials) {
        return AuthRefService.$createUser({
            email: credentials.email,
            password: credentials.password
        });
    };

    /**
     *  Precondition: checkCode(code) resolves with true.
     *  Postcondition: User is authenticated is and a promise resovles with authData or error 
    */
    var authUser = function(credentials) {
        return AuthRefService.$authWithPassword({
            email: credentials.email,
            password: credentials.password
        });  
    };

    /**
    *   Precondition: User is authenticated.
    *   Postcondition: Sets data at: /user/uid/profile and returns a
    *       promise resoloving w/o data if success, otherwise with error
    */
    var setProfile = function(authData, profile){
        var ref = new Firebase(APP.BASEURL + "users/" + authData.uid);
        return ref.child("profile").set(profile);
    };

    /**
    *   Precondition: User is authenticated.
    *   Postcondition: Sets data at: /clientList/paid and returns a
    *       promise resoloving w/o data if success, otherwise with error
    */
    var addToClientList = function(data, uid) {
        var now = appFunctions.dateToNum(new Date());  // yyyyMMdd
        var info = {
            first: data.profile.first,
            last: data.profile.last,
            email: data.credentials.email,
            dateJoined: now
        };

        var ref = new Firebase(APP.BASEURL + "clientList/paid");

        return ref.child(uid).set(info);
    };    

    return {
        /**
        *   Precondition: user input, data, is complete
        *   Postcondition: A new user is created in database
        */
        register: function(data) {
            return $q(function(resolve, reject) {
                checkCode(data.credentials.code)
                .then(function(snapshot){
                    if(snapshot) {
                        createUser(data.credentials)
                        .then(function(userData) {
                            console.log("User create with id:", userData.uid);
                            authUser(data.credentials)
                            .then(function(authData) {
                                setProfile(authData, data.profile)
                                .then(function(resp) {

                                    // store new user in clientList index
                                    addToClientList(data, authData.uid)
                                    .then(function(resp) {
                                        // TODO: deal with this better
                                        // logout user, b/c I couldn't figure out a solution to smoothly get the new
                                        //   user to '/app' b/c of how app inits user upon login.
                                        AuthRefService.$unauth();
                                        resolve(true);
                                    }, function(err){
                                        if (typeof err !== "string") {
                                            err = "Error";
                                        }
                                        reject(err);
                                    });

                                }, function(err) {
                                    if (typeof err !== "string") {
                                        err = "Error";
                                    }
                                    reject(err);
                                });

                            }, function(err) {
                                if (typeof err !== "string") {
                                    err = "Error";
                                }
                                reject(err);
                            });

                        }, function(err) {
                            if (typeof err !== "string") {
                                err = "Error";
                            }
                            reject(err);
                        });
                    } else {
                        reject("Error");
                    }


                }, function(err){
                    reject("Invalid Registration Code");
                });
            });
        }
    };
}]);
\$\endgroup\$
1
\$\begingroup\$
return {
    /**
    *   Precondition: user input, data, is complete
    *   Postcondition: A new user is created in database
    */
    register: function(data) {
        return $q(function(resolve, reject) {
            checkCode(data.credentials.code)
            .then(function(snapshot){
                if(snapshot) {
                    createUser(data.credentials)
                    .then(function(userData) {
                        console.log("User create with id:", userData.uid);
                        authUser(data.credentials)
                        .then(function(authData) {
                            setProfile(authData, data.profile)
                            .then(function(resp) {

                                // store new user in clientList index
                                addToClientList(data, authData.uid)
                                .then(function(resp) {
                                    // TODO: deal with this better
                                    // logout user, b/c I couldn't figure out a solution to smoothly get the new
                                    //   user to '/app' b/c of how app inits user upon login.
                                    AuthRefService.$unauth();
                                    resolve(true);
                                }, function(err){
                                    if (typeof err !== "string") {
                                        err = "Error";
                                    }
                                    reject(err);
                                });

                            }, function(err) {
                                if (typeof err !== "string") {
                                    err = "Error";
                                }
                                reject(err);
                            });

                        }, function(err) {
                            if (typeof err !== "string") {
                                err = "Error";
                            }
                            reject(err);
                        });

                    }, function(err) {
                        if (typeof err !== "string") {
                            err = "Error";
                        }
                        reject(err);
                    });
                } else {
                    reject("Error");
                }


            }, function(err){
                reject("Invalid Registration Code");
            });
        });
    }
};

Wwwoooaaahhh! :D

Anyways, you're using promises wrong here. Promises were designed to prevent this thing from happening. Straight to the point, how promises prevent this is something similar to the following:

asyncOperation1().then(() => {

  // asyncOperation1 done
  return asyncOperation2();

}).then(() => {

  // asyncOperation2 done
  return asyncOperation3();

}).then(() => {

  // asyncOperation3 done

});

When you return a promise inside a then (asyncOperation2), the return of then is a promise that resolves when asyncOperation2 resolves. You end up writing code that's fairly linear-looking.

Also, there's also the catch method. Any unhandled rejections in promise chain will be caught by a catch similar to how a try-catch would do it. But note that adding a catch will allow the promise to recover, this succeeding thens will resolve unless catch rethrows the error.

Anyways, your entire operation could be simplified to:

function (data){
    return checkCode(data.credentials.code)
    .then( snapshot => {
        // Note that we can forcefully reject by returning a rejected promise.
        // We can do that with `Promise.reject`.
        return !snapshot ? Promise.reject('Error') : createUser(data.credentials);
    })
    .then( userData => {
        return authUser(data.credentials);
    })
    .then( authData => {
        // We need to pass through `authData` to the next then so `addToClientList`
        // can use it. We attach a `then` to `setProfile` that resolves with
        // `authData` and `resp` so that the next then receives it as `profile`
        return setProfile(authData, data.profile).then( resp => { authData, resp });
    })
    .then( profile => {
        return addToClientList(data, profile.authData.uid);
    })
    .then( resp => {
        AuthRefService.$unauth();
    });

    // No need for catch. Should any errors happen, the entire thing will be a
    // return a rejected promise which the caller can handle instead.
}
\$\endgroup\$
  • \$\begingroup\$ I wish I had posted here a few months ago! I've wasted soooo much time 'researching' on my own. Thanks a bunch for explaining this! \$\endgroup\$ – Mox Mar 2 '16 at 1:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.