Sapan Diwakar

Software developer

Follow me on Twitter Check out my code on GitHub View some of my designs on Dribbble Take a look at my Linked In profile

Rails like console for Node JS (and Node TS)

If you have ever used Rails and then gone over to a Node.js app, one thing that you will surely find missing is the ability to evaluate arbitrary code in the rails console. It's great to run one-off tasks like creating some rows or debugging issues etc.

REPL Server

While there isn't a native polished solution out there that includes all features like Rails, the Node API already contains features that allow us to build things like that through the REPL Server. The repl module provides a Read-Eval-Print-Loop (REPL) implementation that is available both as a standalone program or includible in other applications. It can be accessed using:

const repl = require('repl');  

ORM

The next thing that we need for the console is an ORM (Object–relational mapping) that provides us mappings to encapsulate the database queries into a nice object based interface like Rails' ActiveModel. As you will see later that the implementation of the console can be made to work to any ORM of choice, but since I used Sequelize, it deserves a mention. Sequelize is a promise-based Node.js ORM for Postgres, MySQL, MariaDB, SQLite and Microsoft SQL Server. It features solid transaction support, relations, eager and lazy loading, read replication and more.

With Sequelize, you usually have models like (models/client.ts):

export default class Client extends Model {  
  public id!: number;
  public name!: string;

  // Association to the Config model
  public config?: Config;

  // Auto generated timesetamps
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  public static associations: {
    config: Association<Config, Client>
  };

  public static setup(sequelize: Sequelize): void {
    Client.init({
      id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true,
      },
      name: {
        type: new DataTypes.STRING(128),
        allowNull: false,
      },
    }, {
      tableName: 'Clients',
      sequelize,
    });
  }

  public static associate(): void {
    Client.hasOne(Config, { onDelete: 'CASCADE', foreignKey: 'clientId', as: 'config' });
  }
}

And an initialization of those models like this (models/index.ts):

export const sequelize: Sequelize = initSequelize(config);

// Set up Models
[Client, Config].forEach((model) => {
  model.setup(sequelize);
  db[model.name] = model;
});

// Set up model associations
Object.keys(db).forEach((modelName) => {  
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

Console

So, now to the interesting part, the console. Create a new file, console.ts at the root of your project with the following content:

import repl from 'repl';  
import * as models from './models';

Object.keys(models).forEach((modelName) => {  
  global[modelName] = models[modelName];
});

const replServer = repl.start({  
  prompt: 'app > ',
});

replServer.context.db = models;  

Now you can run the console with the following command:

$ node --require ts-node/register/transpile-only --experimental-repl-await console

I prefer adding it to my package.json inside "scripts" so that I can quickly access the console like yarn console (or if you want to access the console over Heroku, you can do heroku run yarn console [-a app-name]).

Once you enter the console, you could do things like:

app > clients = await Client.findAll()  
Executing (default): SELECT "id", "name", "createdAt", "updatedAt" FROM "Clients" AS "Client";  
[
  Client {
    ...
  },
  Client {
    ...
  }
]
# Or more complex queries too
app > client = await Client.findOne({ where: { name: 'Client 1' }, include: [Client.associations.config] })  
Executing (default): SELECT "Client"."id", "Client"."name", "Client"."domain", "Client"."createdAt", "Client"."updatedAt", "config"."id" AS "config.id", "config"."token" AS "config.token", "config"."adminToken" AS "config.adminToken", "config"."appId" AS "config.appId", "config"."spaceId" AS "config.spaceId", "config"."teamId" AS "config.teamId", "config"."adminTeamId" AS "config.adminTeamId", "config"."schemaVersion" AS "config.schemaVersion", "config"."createdAt" AS "config.createdAt", "config"."updatedAt" AS "config.updatedAt", "config"."clientId" AS "config.clientId" FROM "Clients" AS "Client" LEFT OUTER JOIN "TypeflowConfigs" AS "config" ON "Client"."id" = "config"."clientId" WHERE "Client"."name" = 'Client 1' LIMIT 1;  
Client {  
  ...
  config: Config {
    ...
  }
}