Node.js: Developing the user information model

We’ll be storing the user information using a Sequelize-based model in a SQL database. We went through that process in the previous chapter, but we’ll do it a little differently this time. Rather than go for the ultimate flexibility of using any kind of database, we’ll stick with Sequelize since the user information model is very simple and a SQL database is perfectly adequate.

The project will contain two modules. In this section, we’ll create users- sequelize.mjs, which will define the SQUser schema and a couple of utility functions. In the next section, we’ll start on user-server.mjs, which contains the REST server implementation.

First, let’s ponder an architectural preference. Just how much should we separate between the data model code interfacing with the database from the REST server code? In the previous chapter, we went for a clean abstraction with several implementations of the database storage layer. For a simple server such as this, the REST request handler functions could contain all database calls, with no abstraction layer. Which is the best approach? We don’t have a hard rule to follow. For this server, we will have database code more tightly integrated to the router functions, with a few shared functions.

Create a new file named users-sequelize.mjs in users containing the following code:

import Sequelize from “sequelize”;

import { default as jsyaml } from ‘js-yaml’;

import { promises as fs } from ‘fs’;

import * as util from ‘util’;

import DBG from ‘debug’;

const log = DBG(‘users:model-users’);

const error = DBG(‘users:error’);

var sequlz;

export class SQUser extends Sequelize.Model {}

export async function connectDB() {

if (sequlz) return sequlz;

const yamltext = await fs.readFile(process.env.SEQUELIZE_CONNECT,‘utf8’);

const params = await jsyaml.safeLoad(yamltext, ‘utf8’);

if (typeof process.env.SEQUELIZE_DBNAME !== ‘undefined’ && process.env.SEQUELIZE_DBNAME !== ”) {

params.dbname = process.env.SEQUELIZE_DBNAME;

}

if (typeof process.env.SEQUELIZE_DBUSER !== ‘undefined’ && process.env.SEQUELIZE_DBUSER !== ”) {

params.username = process.env.SEQUELIZE_DBUSER;

}

if (typeof process.env.SEQUELIZE_DBPASSWD !== ‘undefined’ && process.env.SEQUELIZE_DBPASSWD !== ”) {

params.password = process.env.SEQUELIZE_DBPASSWD;

}

if (typeof process.env.SEQUELIZE_DBHOST !== ‘undefined’ && process.env.SEQUELIZE_DBHOST !== ”) {

params.params.host = process.env.SEQUELIZE_DBHOST;

}

if (typeof process.env.SEQUELIZE_DBPORT !== ‘undefined’ && process.env.SEQUELIZE_DBPORT !== ”) {

params.params.port = process.env.SEQUELIZE_DBPORT;

}

if (typeof process.env.SEQUELIZE_DBDIALECT !== ‘undefined’ && process.env.SEQUELIZE_DBDIALECT !== ”) {

params.params.dialect = process.env.SEQUELIZE_DBDIALECT;

}

log(‘Sequelize params ‘+ util.inspect(params));

sequlz = new Sequelize(params.dbname, params.username, params.password, params.params);

SQUser.init({

username: { type: Sequelize.STRING, unique: true },

password: Sequelize.STRING,

provider: Sequelize.STRING,

familyName: Sequelize.STRING,

givenName: Sequelize.STRING,

middleName: Sequelize.STRING,

emails: Sequelize.STRING(2048),

photos: Sequelize.STRING(2048)

}, {

sequelize: sequlz,

modelName: ‘SQUser’

});

await SQUser.sync();

}

As with our Sequelize-based model for Notes, we will use a YAML Ain’t Markup Language (YAML) file to store connection configuration. We’re even using the same environment variable, SEQUELIZE_CONNECT, and the same approach to overriding fields of the configuration. The approach is similar, with a connectDB function setting up the connection and initializing the SQUsers table.

With this approach, we can use a base configuration file in the SEQUELIZE_CONNECT variable and then use the other environment variables to override its fields. This will be useful when we start deploying Docker containers.

The Passport project developed this object by harmonizing the user information given by several third-party services into a single object definition. To simplify our code, we’re simply using the schema defined by Passport.

There are several functions to create that will be an API to manage user data. Let’s add them to the bottom of users-sequelize.mjs, starting with the following code:

export function userParams(req) {

return {

username: req.params.username,

password: req.params.password,

provider: req.params.provider,

familyName: req.params.familyName,

givenName: req.params.givenName,

middleName: req.params.middleName,

emails: JSON.stringify(req.params.emails),

photos: JSON.stringify(req.params.photos)

};

}

In Restify, the route handler functions supply the same sort of request and response objects we’ve already seen. We’ll go over the configuration of the REST server in the next section. Suffice to say that REST parameters arrive in the request handlers as the req.params object, as shown in the preceding code block. This function simplifies the gathering of those parameters into a simple object that happens to match the SQUser schema, as shown in the following code block:

export function sanitizedUser(user) {

var ret = {

id: user.username,

username: user.username,

provider: user.provider,

familyName: user.familyName,

givenName: user.givenName,

middleName: user.middleName

};

try {

ret.emails = JSON.parse(user.emails);

} catch(e) { ret.emails = []; } try {

ret.photos = JSON.parse(user.photos);

} catch(e) { ret.photos = []; }

return ret;

}

When we fetch an SQUser object from the database, Sequelize obviously gives us a Sequelize object that has many extra fields and functions used by Sequelize. We don’t want to send that data to our callers. Furthermore, we think it will increase security to not provide the password data beyond the boundary of this server. This function produces a simple, sanitized, anonymous JavaScript object from the SQUser instance. We could have defined a full JavaScript class, but would that have served any purpose? This anonymous JavaScript class is sufficient for this simple server, as illustrated in the following code block:

export async function findOneUser(username) {

let user = await SQUser.findOne({ where: { username: username } });

user = user ? sanitizedUser(user) : undefined;

return user;

}

export async function createUser(req) {

let tocreate = userParams(req);

await SQUser.create(tocreate);

const result = await findOneUser(req.params.username);

return result;

}

The pair of functions shown in the preceding code block provides some database operations that are used several times in the user-server.mjs module.

In findOneUser, we are looking up a single SQUser, and then returning a sanitized copy. In createUser, we gather the user parameters from the request object, create the SQUser object in the database, and then retrieve that newly created object to return it to the caller.

If you refer back to the connectDB function, there is a SEQUELIZE_CONNECT environment variable for the configuration file. Let’s create one for SQLite3 that we can name sequelize-sqlite.yaml, as follows:

dbname: users username:

password:

params:

dialect: sqlite

storage: users-sequelize.sqlite3

This is just like the configuration files we used in the previous chapter.

That’s what we need for the database side of this service. Let’s now move on to creating the REST service.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *