Adding TypeORM

TypeORM is our preferred library for persistance. Coupled with the choice to implement both Active Record or the Data Mapper pattern, it’s a flexible design choice.

Additionaly it supports many popular SQL dialects such as MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases out of the box, along with expermiental access for MongoDB.

Assumptions



Configuration

Install drivers

yarn add typeorm

And the relevant database driver for your project.

Create relevant folders

mkdir src/entities
mkdir migrations

ormconfig.ts

Create an ormconfig.ts in your PROJECT_ROOT.

import dotenv from 'dotenv-safe'

// TODO: Improve imports so it's clear that this is from typeorm
// eg. import { SnakeCaseNamingStrategy } from '@enso/typeorm'
import { SnakeCaseNamingStrategy } from '@ensojs/framework'

dotenv.load()
/**
 * https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md#what-is-connectionoptions
 *
 * @type {ConnectionOptions}
 */
module.exports = {
  type: process.env.DB_TYPE,
  url: process.env.DB_CONNECTION_URL,
  synchronize: false,
  logging: true,
  namingStrategy: new SnakeCaseNamingStrategy(),
  entities: [
    'src/entities/*.ts',
    'src/entities/index.ts'
  ],
  migrations: [
    'migrations/*.ts'
  ],
  cli: {
    entitiesDir: 'src/entities',
    migrationsDir: 'migrations'
  }
}

Adding additional Environment dependencies

Enso only ships with ENVIRONMENT and PORT as part of its default enviroment. So lets create our own custom Environment and add in the required TypeORM attributes.

// file src/config/interfaces.ts
export interface IEnvironmentConfig {
  ENVIRONMENT: string
  PORT: number
  // ++ TypeORM
  DB_TYPE: string
  DB_CONNECTION_URL: string
}

We need to also inform our server that we want to use our custom IEnvironmentConfig instead of the default Enso environment.

// file src/config/env.ts
import dotenv from 'dotenv-safe'

// import { IEnvironmentConfig } from '@ensojs/framework'
import { IEnvironmentConfig } from './interfaces'

dotenv.config({
  allowEmptyValues: false
})

export const env: IEnvironmentConfig = {
  ENVIRONMENT: process.env.ENVIRONMENT!,
  PORT: parseInt(process.env.PORT!),
  DB_TYPE: process.env.DB_TYPE!,
  DB_CONNECTION_URL: process.env.DB_CONNECTION_URL!,
}

Synchronously load the container

Add the connection to your registry and await the createConnection to guarantee the connection is available for injection.

// file: src/config/registry.ts
import { Container } from 'inversify'
import Debug from 'debug'

const debug = Debug('enso:createRegistry')

import { container } from './container'
import { createConnection, Connection } from 'typeorm'
import { IEnvironmentConfig } from './interfaces'
import { $b } from './bindings'
import { env } from './env'

export const createRegistry = async (): Promise<Container> => {
  // postgres
  const connectionOptions = await require('./../../ormconfig')
  const connection = await createConnection(connectionOptions)
  // Bind Application-instance-specific values
  container.bind<IEnvironmentConfig>($b.Environment).toConstantValue(env)
  container.bind<Connection>(Connection).toConstantValue(connection)

  debug('createRegistry() - Done')
  return container
}

Update our server.ts

Use await to resolve a fully container from our registry instead of loading the container asynchronously.

import 'reflect-metadata'

import { App } from './App'
import Debug from 'debug'

const debug = Debug('enso:server')

import { env } from './config/env'
// import { container } from './config/container'
import { createRegistry } from './config/registry'
(async () => {
  try {
    debug('============================================')
    debug('> Starting server...')
    debug('============================================')

    debug('> Creating registry for dependency injection')
    const container = await createRegistry()
    const app = new App(env)
    await app.build(container)
    await app.start()

    debug('')
    debug('✔ [nodejs] %s', process.version)
    debug('')
    debug(
      '✔ API server listening on port %d in [%s] mode',
      env.PORT,
      env.ENVIRONMENT
    )
  } catch (e) {
    debug(e)
    process.exit(1)
  }
})()

👊

Your server should now boot up and connect to a datasource.

# test it with
yarn dev

Entities

https://github.com/typeorm/typeorm#step-by-step-guide

Migrations

TypeORM exposes an excellent CLI.

However we want to be writing our code in Typescript. So we need to transpile our code.

Alternativly; We can port over the commands we want in our package.json and/or expose the CLI..

{
  "scripts": {
    "typeorm": "ts-node --transpile-only -r tsconfig-paths/register ../../node_modules/typeorm/cli.js",
    "migration:create": "ts-node --transpile-only -r tsconfig-paths/register ../../node_modules/typeorm/cli.js migration:create",
    "migration:revert": "ts-node --transpile-only -r tsconfig-paths/register ../../node_modules/typeorm/cli.js migration:revert",
    "migration:run": "ts-node --transpile-only -r tsconfig-paths/register ../../node_modules/typeorm/cli.js migration:run",
    "schema:drop": "ts-node --transpile-only -r tsconfig-paths/register ../../node_modules/typeorm/cli.js schema:drop"
  }
}

We can then use the commands via yarn.

TIP

A working example of this recipe can be found in the packages/koa-typeorm monorepo