Part 3: User Roles and Management Datastore - MongoDB

rachel_cheuk

Rachel

Posted on September 15, 2020

Part 3: User Roles and Management Datastore - MongoDB

The Datastore

Part 3 of this series focuses on the datastore for the user roles and management backend. MongoDB is great for storing documents. When the end-goal is moving data from a spreadsheet to a queryable database, Mongo was an easy choice for this use-case. It also supports GeoQuerying, which I knew I would need as well.

I also used Mongoose for object modeling, and Joi as a validation library, which provides a few more out-of-the-box validations compared to the built-in Mongoose schema validations.

Note: I was looking for a quick and easy way to get this up and running for prototyping purposes. Worth noting is that you could also solely use Mongoose and write your own custom validation methods, which may be better from a dependency management standpoint (or omit Mongoose and use the native MongoDB Driver with a validation library like Joi).

MongoDB Connection String

To define where the MongoDB datastore is hosted, this information is stored in a configuration file (though it can also be passed into the application through an environment variable like in any Node application). FeatherJS has fairly straightforward configuration management based on the NODE_ENV variable. On project creation, default.json file is generated in the /config directory, which is pre-loaded with some common variables you can adjust for your project. If your NODE_ENV variable is set to NODE_ENV=test, then it will merge the default.json with test.json. The FeathersJS app then retrieves the value by accessing the key using something like app.get('key');

To add a MongoDB connection, add "mongodb": "mongodb://localhost:27017/api", as a key-value pair to the json config file like so:

{
  "host": "localhost",
  "port": 3030,
  // ...
  "mongodb": "mongodb://localhost:27017/api"
}
Enter fullscreen mode Exit fullscreen mode

For more information on MongoDB Connection strings, visit the MongoDB docs. The exact format of your string may vary based on your needs.

Connecting to FeathersJS

FeatherJS supports a handful of popular database adapters, including In-Memory Storage, Local Storage, popular relational databases (MySQL, Postgresql, etc.), and Elasticsearch.

The service-oriented database adapter configuration makes it easy to connect to multiple datastores to retrieve data. The CLI generator also makes it easy to scaffold new services for different databases.

For this starter, a Users service is defined to store user information. This can be done as part of the initial app creation. It conveniently asks you what you need for the user service, including the database adapter used. The generator then creates the necessary files to get you started. For creating new services, you can use feathers generate service.

In this application, because I'm using Mongoose, a model file is created in /server/src/models/. Within this folder, the user.model.js file defines the schema that correlates to how the data will be entered into MongoDB:

const schema = new mongooseClient.Schema(
    {
      email: { type: String, unique: true, lowercase: true },
      password: { type: String },
      firstname: { type: String },
      lastname: { type: String },
      company: { type: String },
      department: { type: String },
      title: { type: String },
      city: { type: String },
      permissions: { type: Array, default: ['guest'] },
      phone: { type: String },
      passwordReset: { type: String },
      passwordResetToken: { type: String },
      lastLoggedIn: { type: Date },
      team: { type: 'ObjectId', ref: 'Teams' },
      googleId: { type: String },
      isVerified: { type: Boolean },
      verifyToken: { type: String },
      verifyShortToken: { type: String },
      verifyLongToken: { type: String },
      verifyExpires: { type: Date },
      verifyChanges: { type: Object },
      resetToken: { type: String },
      resetExpires: { type: Date },
    },
    {
      timestamps: true,
    }
  );
Enter fullscreen mode Exit fullscreen mode

As mentioned in Part 2, the User Service is made of a class, hooks, and service file, which are brought together in the User Service (user.service.js). The User Class extends the Mongoose Database provider and provides access to .create(), .update(), .patch(), .remove() methods for MongoDB, which then get used by the service when a user action is triggered.

User Class

const { Service } = require('feathers-mongoose');
exports.Users = class Users extends Service {};
Enter fullscreen mode Exit fullscreen mode

User Service

const { Users } = require('./users.class');
const createModel = require('../../models/users.model');

module.exports = function (app) {
  const options = {
    Model: createModel(app),
    paginate: app.get('paginate')
  };

  // Initialize our service with any options it requires
  app.use('/users', new Users(options, app));

  // Get our initialized service so that we can register hooks
  const service = app.service('users');
};
Enter fullscreen mode Exit fullscreen mode

Model Validation

I handled the user model validation in the feather hooks (users.hooks.js) layer using the Joi library. After defining the acceptable values, I used the validate.mongoose(updateSchema, joiOptions) hook, defining updateSchema as the allowed formats for the few fields a user would be permitted to pass to the backend for changes. If it didn't meet the criteria, the request would fail and return an error.

const firstname = Joi.string()
  .trim()
  .min(2)
  .max(30)
  .pattern(new RegExp('^[a-zA-Z0-9 ]{2,30}$'))

// ... omitted for brevity, view source code 
// for complete validation code

const updateSchema = Joi.object().keys({
  firstname: firstname,
  lastname: lastname,
  city: city,
  company: company,
});
Enter fullscreen mode Exit fullscreen mode

The update schema that is checked depends on the user role, as admins have broader permissions to update additional user fields than regular users.

Mongoose vs MongoDB Native Driver

When I first started this project, I weighed the benefits of sticking with the original MongoDB adapter or using an object modeling library like Mongoose. After reading several analyses linked below, I ultimately decided to stick with Mongoose. Curious to hear about others' experiences with Mongoose vs. MongoDB adapters. Leave a comment and share your thoughts!

Wrapping up

That wraps up this project in its current state. I may update it at a later date when I incorporate elasticsearch into this backend. Looking forward to adding powerful search capabilities to this app. I may also add more testing. There is none at this point in time.

Leave a comment, question, or suggestion! Let me know what you think.

💖 💪 🙅 🚩
rachel_cheuk
Rachel

Posted on September 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related