0

I'm writing a simple server in vanilla JavaScript and Node.js. I made a database.js file which contains abstractions to interact with my database (redis).

I want to automatically set n_users and n_rooms if they are not yet found in the database. To do so, I have the following lines in database.js:

//set n_users if never set in redis
if (await getKey("n_users") == null){
    setKey("n_users", 0);
}
//set n_rooms if never set in redis
if (await getKey("n_rooms") == null){
    setKey("n_rooms", 0);
}

This code works as expected when I open a node session and then type .load database.js. However, the code does not work when directly calling node database.js from the terminal:

/Users/.../database.js:29
if (await getKey("n_users") == null){
          ^^^^^^

SyntaxError: Unexpected identifier

I expected javascript files to work similarly to python files, in the sense that the entirety of a python file gets interpreted when called (or imported). I can't find any information on how exactly node handles its input files to correct what I'm doing wrong.

Ultimately, I want to be able to automatically execute the above lines of code when running node server.js from a terminal, where server.js requires an object from database.js. What is the best way to acomplish this?

Edit

Here's my database.jsfile:

let User = require('./shared/user').User;
let Room = require('./shared/room').Room;
const md5 = require('./stack_md5').MD5;
const redis = require('redis');
const salt = require('./salt').salt;


var prefix = "JS2023:";
const client = redis.createClient();
client.connect();

//should be map of token to user_id
let TOKENS = {};

async function getKey(key) {
    return await client.get(`${prefix}${key}`);
}


async function setKey(key, value){
    await client.set(`${prefix}${key}`, value);
}

async function delKey(key){
    await client.del(`${prefix}${key}`);
}

//set n_users if never set in redis
if (await getKey("n_users") == null){
    setKey("n_users", 0);
}
//set n_rooms if never set in redis
if (await getKey("n_rooms") == null){
    setKey("n_rooms", 0);
}

//using local objects because mongodb doesnt work dont know why
const DB = {
    //user should have at least username and password
    //automatically md5's the password
    //TODO refactor for clarity
    create_user: async function(user_data){
        //username must be unique
        console.log(`user data: ${user_data}`);
        if (await DB.get_user_id_by_username(user_data.username) != undefined) return Promise.reject("Username is already taken!");

        let user_id = parseInt(await getKey(`n_users`));

        let username = user_data.username;
        let user_salt = salt(16);

        let password = md5(user_data.password + user_salt);

        let user = new User({id: user_id, username: username, password: password, salt: user_salt});

        let user_string = JSON.stringify(user);

        setKey(username, user_id)
            .then(DB.set_user_by_id(user_id, user_string))
            .then(setKey("n_users", user_id + 1));

        return user_id;
    },

    get_room_by_id: async function (id){

    },

    set_room_by_id: async function (id, room_string){

    },

    //TODO this returns undefined rn
    get_user_by_id: async function (id) {
        //access DB
        //clean data
        //call callback with data
        return await getKey(id);
    }, //TODO enforce unique usernames

    set_user_by_id: async function (id, user_string) {
        await setKey(id, user_string);
    }, //id should be unique

    get_user_id_by_username: async function (username) {
        return await getKey(username);
    }, //verify password is correct and return cool access token

    authenticate: async function (user_id, password) {
        let user = JSON.parse(await getKey(user_id));

        if (user.password !== md5(password + user.salt)) return Promise.reject("Wrong password!");

        //is user already is online, he is in tokens. will not be authenticated
        for (token in TOKENS) {
            if (TOKENS[token] === user_id) return Promise.reject("User is already connected!");
        }

        var user_token = await DB.token(32);
        //check that token is unique and if not create a new one and hope it is
        while (user_token in TOKENS) user_token = await DB.token(32);
        TOKENS[user_token] = user_id;

        console.log(`generated token for ${user_id}: ${user_token}`)

        return user_token;

    },

    //update_obj an object with the fields to update {username, password, whatever else except id and salt}
    update_user: async function (id, update_obj) {
        DB.get_user_by_id(id)
            .then(function (user) {
                for (let field in update_obj) {
                    switch (field) {
                        //password stored as its hash
                        case "password":
                            //salt is stored by user
                            user[field] = md5(update_obj[field] + user.salt);
                            break;
                        //id is only given by the server and cannot be modified
                        case "id":
                            continue;
                        case "salt":
                            continue;
                        //update inverse key-value in redis db
                        case "username":
                            delKey(`${user[field]}`)
                                .then(setKey(`${id}`, update_obj[field]));
                            break;
                        default:
                            user[field] = update_obj[field];
                    }
                }
                return user;
            }).then(user => DB.set_user_by_id(id, JSON.stringify(user)));
    },

    //make a random token of given length
    token: function (length) {
        return salt(length);
    },

    // delete token associated with user_id (i.e, user no longer online)
    revoke: async function (user_id) {
        for (token in TOKENS) {
            if (TOKENS[token] == user_id) {
                delete (TOKENS[token]);
                console.log(`${token} revoked, user ${user_id} is no longer online.`);
            }
        }
        //for chaining promises
        return user_id;
    }, // this doesn't need to be async since tokens have to be dynamically maintained

    /**
     * check if token string is in tokens map
     * @param token
     * @returns {boolean}
     */
    present: function (token) {
        for (let stored_token in TOKENS) {
            if (token === stored_token) {
                return true;
            }
        }
        return false;
    },


}

module.exports = {DB, TOKENS};
Pollastre
  • 72
  • 7
  • FYI, checking and setting a database value in two separate database operations like you show is a race condition waiting to happen. – jfriend00 Mar 23 '23 at 21:05
  • I think we would have to seed the structure/code of your database.js file to understand why it won't run on its own. Typically a file is either designed to run on its own or to be a module that someone else loads. It sounds like you're trying to make one file do both which is a bit odd. So, please show the actual code of the file. Nodejs does interpret or run the entire file when it is imported. A file designed to be a reusable module that other files load will typically then initialize itself and export something that others can use. – jfriend00 Mar 23 '23 at 21:06
  • One thing that might be confusing you is that each file in nodejs is a separate scope. Symbols created in one file and not automatically available to other files. You don't show where `getKey()` is supposed to come from, but if `database.js` doesn't define that function itself, then it will have to import something to get access to that function. Again, please show the actual code of `database.js` for us to be able to help. – jfriend00 Mar 23 '23 at 21:11
  • @jfriend00 maybe it is odd, I was mainly relying on how python works to execute those lines when the module got loaded. I can post the full code in an edit if it helps. – Pollastre Mar 23 '23 at 21:11
  • Does this answer your question? [How can I use async/await at the top level?](https://stackoverflow.com/questions/46515764/how-can-i-use-async-await-at-the-top-level) – derpirscher Mar 23 '23 at 21:24
  • @derpirscher not really, node just does not want to run the code unless using the .load command. Thanks for the help though – Pollastre Mar 23 '23 at 21:42
  • 1
    because you have a toplevel await ... just as the answer you accepted told you ... and the linked answer also tells you how to run such a file ... – derpirscher Mar 23 '23 at 22:20
  • @derpirscher you are right, I understood the link you posted answered a different problem. I've tried one of the solutions listed and the code runs now. Thanks! – Pollastre Mar 24 '23 at 11:26

1 Answers1

0

This statement in your file:

//set n_users if never set in redis
if (await getKey("n_users") == null){
    setKey("n_users", 0);
}

is attempting to use what is referred to as "top level await" (an await statement at the top level scope of a module). That is only supported in recent versions of nodejs in ESM module files. Your module is a CommonJS module and thus you can't use top level await. That is the source of the error that is being reported.

There are lots of articles on using ESM module files so I won't attempt to cover all of that here, but nodejs knows whether a file is an ESM module or a CommonJS module either from the module line in package.json or from the file extension and in ESM module files, you use the newer import syntax, not require().


FYI, there is no point to using await in something like this:

return await client.get(`${prefix}${key}`);

That is functionally the same as:

return client.get(`${prefix}${key}`);

Either way, your function is returning a promise that resolves to the eventual value of client.get().

jfriend00
  • 683,504
  • 96
  • 985
  • 979