Tutorial: Database & ORM

Database & ORM

Flitter uses a lightweight ORM to provide MongoDB models (flitter-orm). Model schemas are defined in files and loaded when Flitter initializes. MongoDB connection info is specified in the environment configuration. The schema for these models provides a rich way of defining structure in MongoDB.

Defining a Model

We can use the ./flitter command to generate a new model. Let's create a basic model to keep track of a particular user's blog:

./flitter new model blog:Blog

This will create a new model file, app/models/blog/Blog.model.js with the following template:

const Model = require('flitter-orm/src/model/Model')

/*
 * Blog Model
 * -------------------------------------------------------------
 * Put some description here!
 */
class Blog extends Model {
    static get schema() {
        // Return a schema here.
        return {
            some_field: String,
        }
    }
    
    // Static and instance methods can go here
}

module.exports = exports = Blog

The static getter for schema tells Flitter's ORM how to format the data on save. There are a few primitive types, and more advanced embedded types you can use. See the flitter-orm schema page for more info, but let's fill in the Blog model's schema:

const Model = require('flitter-orm/src/model/Model')
const { ObjectId } = require('mongodb')

/*
 * Blog Model
 * -------------------------------------------------------------
 * Put some description here!
 */
class Blog extends Model {
    static get schema() {
        // Return a schema here.
        return {
            name: String,
            owner_user_id: ObjectId,
            active: { type: Boolean, default: true },
            follower_user_ids: [ObjectId],
        }
    }
    
    // Static and instance methods can go here
}

module.exports = exports = Blog

Here, we defined a few useful fields. The name field will coerce to a string, the owner_user_id refers to the ObjectId of the user model record that owns the Blog, the active flag tells us if the Blog is enabled (it defaults to true), and an array of ObjectId's of the user model records of users that follow that blog.

This is a cursory overview of the format for schemata. You can find more information about it on the flitter-orm doc pages.

Relationships

There is an implicit relationship here between a Blog and a User (as defined by the owner_user_id field). We can make this relationship explicit (and therefore accessible directly) using the Model class' built-in relationship operators:

const Model = require('flitter-orm/src/model/Model')
const { ObjectId } = require('mongodb')

// Require the user model for this application.
// This is usually provided by the flitter-auth package.
const User = require('../auth/User.model')

/*
 * Blog Model
 * -------------------------------------------------------------
 * Put some description here!
 */
class Blog extends Model {
    static get schema() {
        // Return a schema here.
        return {
            name: String,
            owner_user_id: ObjectId,
            active: { type: Boolean, default: true },
            follower_user_ids: [ObjectId],
        }
    }
    
    // Static and instance methods can go here
    async owner() {
        // Define the relationship to the User model where Blog.owner_user_id corresponds to User._id (the ObjectId).
        return this.belongs_to_one(User, 'owner_user_id', '_id')
    }
}

module.exports = exports = Blog

We defined a method, owner, that defines the relationship of the Blog to the User model and retrieves a single User record by matching the owner_user_id field on the Blog model. The relationships are covered in more detail in the flitter-orm docs.

Using Models

When Flitter starts, it loads each of the schemata and injects dependencies into each of the models. These models can then be accessed by requiring the model file, or (preferrably) through the global models service using their Flitter canonical names. Here's an example of querying a Blog instance from the Flitter shell:

(flitter) ➤ Blog = _services.models.get('blog:Blog')
(flitter) ➤ User = _services.models.get('auth:User')
(flitter) ➤ glmdev_user = await User.findOne({uid: 'glmdev'})
User {
  uid: 'glmdev',
  provider: 'flitter',
  data: '{}',
  uuid: 'cf699085-9806-4ca3-bcf6-e804ecaf2343',
  roles: [],
  permissions: [],
  _id: 5e3312c12685b72f9b61935e
}
(flitter) ➤ new_blog = new Blog({name: "Garrett's Blog", owner_user_id: glmdev_user._id})
(flitter) ➤ await new_blog.save()
Blog {
  name: "Garrett's Blog",
  owner_user_id: 5e3312c12685b72f9b61935e,
  follower_user_ids: [],
  _id: 5e3313302685b72f9b61935f,
  active: true
}
(flitter) ➤ await new_blog.owner()
User {
  uid: 'glmdev',
  provider: 'flitter',
  data: '{}',
  uuid: 'cf699085-9806-4ca3-bcf6-e804ecaf2343',
  roles: [],
  permissions: [],
  _id: 5e3312c12685b72f9b61935e
}
(flitter) ➤ .exit

Here, we queried an instance of the User model, and created a new Blog instance associated with the user and saved it. Then, we were able to use the owner relationship we created to fetch the associated user.

By the way: that bit there was the Flitter shell. It's like the standard Node REPL, except it is started after your application has started. It allows you to interact with your application's services, models, controllers, and configuration from the command line and is a very valuable development tool. You can start it using the built-in ./flitter command:

./flitter shell

Or, if you want to be able to use async/await from the shell (which makes working with Flitter nicer), you can start it with await support:

node --experimental-repl-await flitter shell

Next: Controllers