objection-generator

Generates knex migrations and objection.js models in Typescript from a YAML spec

Usage no npm install needed!

<script type="module">
  import objectionGenerator from 'https://cdn.skypack.dev/objection-generator';
</script>

README

objection-generator

NPM version CircleCI built with typescript JavaScript Style Guide

Generates objection.js models in Typescript from a YAML specification.

  • Generate your initial set of objection.js models from a YAML file
  • Supports $ref for re-using common definitions
  • Can also generate a basic knex migration file based on the YAML file

Installation

Install the CLI utility

$ npm i objection-generator -g

Install knex + objection (if you do not have it installed)

$ npm i knex objection --save

Usage

Objection.js model generation

$ objection-generator generate <specFile> <outDir>

objection-generator generate <specFile> <outDir>

Generates objection.js models from a YAML file

Positionals:
  specFile  The YAML file to use to generate models.                                         [string] [required]
  outDir    The directory to output the models.                                              [string] [required]                                                [string] [required]

Sample output

Using the sample.yaml spec in this project:

$ objection-generator generate sample.yaml /tmp/lib

Will generate the following folder structure:

/tmp/lib/
├── models/
│   ├── BaseModel.ts
│   ├── <model>.ts

Will generate models that look like this:

import { Model } from 'objection'
import path from 'path'

import { BaseModel } from './BaseModel'

export enum PersonGenderEnum {
  MALE = 'Male',
  FEMALE = 'Female',
  OTHER = 'Other'
}
export enum PersonFavFoodEnum {
  PINE_APPLE = 'pine-apple',
  BLUE_BERRY = 'blueBerry',
  CHEESE_PIZZA = 'cheese_pizza'
}

export class PersonModel extends BaseModel {
  id: string
  name: string
  age: number | null
  gender: PersonGenderEnum
  favFood: PersonFavFoodEnum
  username: string
  created: string

  static tableName = 'persons'

  static get jsonSchema () {
    return {
      type: 'object',
      required: ['name', 'username'],
      properties: {
        id: {
          type: 'string'
        },
        name: {
          type: 'string',
          minLength: 1,
          maxLength: 100
        },
        age: {
          type: ['number', 'null']
        },
        gender: {
          type: 'string',
          enum: ['Male', 'Female', 'Other'],
          default: 'Female'
        },
        favFood: {
          type: 'string',
          enum: ['pine-apple', 'blueBerry', 'cheese_pizza']
        },
        username: {
          type: 'string',
          minLength: 1,
          maxLength: 25,
          default: 'default-user'
        },
        created: {
          type: 'string',
          format: 'date-time'
        }
      }
    }
  }

  static get relationMappings () {
    return {
      movies: {
        relation: Model.ManyToManyRelation,
        modelClass: path.join(__dirname, 'MovieModel'),
        join: {
          from: 'persons.id',
          through: {
            from: 'persons_movies.personId',
            to: 'persons_movies.movieId'
          },
          to: 'movies.id'
        }
      },
      reviews: {
        relation: Model.HasManyRelation,
        modelClass: path.join(__dirname, 'ReviewModel'),
        join: {
          from: 'persons.id',
          to: 'review.authorId'
        }
      }
    }
  }
}

Knex configuration

You must use knexSnakeCaseMappers in your knex configuration.

import { knexSnakeCaseMappers } from 'objection'
import knex from 'knex'

const db = knex({
  client: 'postgres',

  connection: {
    host: '127.0.0.1',
    user: 'objection',
    database: 'objection_test'
  },
  // allows usage of camel cased names in the model
  // and snake cased fields in the database
  ...knexSnakeCaseMappers()
})

Knex migration generation

The YAML can also be used to generate a basic migration file. This can be used as a good starting base for building a desired migration.

Limitations

There are many limitations to the generation since there is not an exact mapping between JSON schema types / information in the objection models to an exact database specification.

Some limitations include:

  • No foreign keys are generated (PRs welcomed - make use of the relations please)
  • No through tables are generated

PRs are welcomed for improvements!

Usage

$ objection-generator knex <specFile> <outDir>

objection-generator knex <specFile> <outDir>

Generates a basic knex migration from a YAML file

Positionals:
  specFile  The YAML file to use to generate models.                                         [string] [required]
  outDir    The directory to output the models.                                              [string] [required]                                      [string] [required]

Sample output

Using the sample.yaml spec in this project:

$ objection-generator knex sample.yaml /tmp/lib

Will generate the following folder structure:

/tmp/lib/
├── migrations/
│   └── 000-init.js
└── migrate.js

Example migration output:

async function up (knex) {
  await knex.schema.createTable('persons', table => {
    table.string('id')
    table.string('name', 100).notNullable()
    table.integer('age')
    table.enu('gender', ['Male', 'Female', 'Other']).defaultTo('Female')
    table
      .string('username', 25)
      .notNullable()
      .defaultTo('default-user')
    table.datetime('created')

    table.primary(['id'])

    table.unique(['username'], 'uniq_username')

    table.index(['age', 'name'], 'name_age_index')
  })
  await knex.schema.createTable('movies', table => {
    table.string('id')
    table.string('name', 255).notNullable()

    table.primary(['id'])
  })
  await knex.schema.createTable('reviews', table => {
    table.string('review_id')
    table.string('author_id').notNullable()
    table.string('movie_id').notNullable()
    table.string('content')

    table.primary(['review_id'])
  })
}

async function down (knex) {
  await knex.schema.dropTable('persons')
  await knex.schema.dropTable('movies')
  await knex.schema.dropTable('reviews')
}

module.exports = {
  up,
  down
}

Run the migration

The output includes a sample migration script that uses sqlite3 as the database driver for quick prototyping.

$ npm i sqlite3 --save-dev

$ node <outputDir>/migrate.js

Modify this file to your liking to work with your own database.

YAML spec

See sample.yaml for an example spec.

config:
  model:
    # Adds a prefix to the class names of the generated objection.js models
    classNamePrefix:
    # Adds a postfix to the class names of the generated objection.js models
    classNamePostfix: Model

# Objection models to generate
models:
  # Defines an objection model named Person (actually PersonModel with the postfix)
  Person:
    # database table name
    tableName: persons
    # maps to Model#jsonSchema()
    # https://json-schema.org/understanding-json-schema/reference/type.html
    # https://vincit.github.io/objection.js/guide/models.html#examples
    jsonSchema:
      required: ['name', 'username']
      properties:
        id:
          type: string
        name:
          type: string
          minLength: 1
          maxLength: 100
        age:
          # You can define a re-usable set of properties and reference them via $ref
          $ref: '#/components/fieldProperties/age'
        gender:
          type: string
          enum: ['Male', 'Female', 'Other']
          default: 'Female'
        favFood:
          type: string
          enum: ['pine-apple', 'blueBerry', 'cheese_pizza']
        childrenCount:
          type: number
          default: 0
        username:
          allOf:
            # combine a ref and a non-ref, see json schema spec for more info
            - $ref: '#/components/fieldProperties/username'
            - default: 'default-user'
        someOtherField:
          type: string
        created:
          type: string
          format: date-time
    # Define relations - maps to Model#relationMappings()
    # https://vincit.github.io/objection.js/guide/relations.html#examples
    relations:
      movies:
        relation: Model.ManyToManyRelation
        modelClass: Movie
        join:
          from: persons.id
          through:
            from: persons_movies.personId
            to: persons_movies.movieId
          to: movies.id
      reviews:
        relation: Model.HasManyRelation
        modelClass: Review
        join:
          from: persons.id
          to: review.authorId
    # Section for knex-specific generation
    database:
      # define unique indices
      unique:
        # made-up name for the unique index
        uniq_username:
          # columns to add to unique index
          # values will always be converted to snake case
          columns: ['username']
      # Define indices
      index:
        # made-up name for the index
        name_age_index:
          # columns to index
          # values will always be converted to snake case
          columns: ['age', 'name']
      exclude:
        # exclude these fields from being generated in the migration file
        # this is if you want to have a field defined in the model
        # but not in the database
        columns: ['someOtherField']
  Movie:
    tableName: movies
    jsonSchema:
      required: ['name']
      properties:
        id:
          type: string
        name:
          type: string
          minLength: 1
          maxLength: 255
    relations:
      reviews:
        relation: Model.HasManyRelation
        modelClass: Review
        join:
          from: movie.id
          to: review.movieId
  Review:
    tableName: reviews
    # If you want to use a primary key that's not called "id"
    idColumn: reviewId
    jsonSchema:
      required: ['authorId', 'movieId']
      properties:
        reviewId:
          type: string
        authorId:
          type: string
        movieId:
          type: string
        content:
          type: string
    relations:
      author:
        relation: Model.HasOneRelation
        modelClass: Person
        join:
          from: reviews.authorId
          to: persons.id

# components are re-usable elements that can be
# referenced in the model via $ref
components:
  # This is a made up section used for
  # defining common field properties
  fieldProperties:
    age:
      type: ['number', 'null']
    username:
      type: string
      minLength: 1
      maxLength: 25