4.0.0-beta.1 • Published 1 year ago

@concepta/typeorm-seeding v4.0.0-beta.1

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

Table of Contents

Introduction

Creating sample data for your TypeORM project entities is exhausting 🥵

The TypeORM seeding module's goal is to make this task fun and rewarding.

How does it work? Just create your entity factories and/or seeders, and run the CLI command!

Entities

@Entity()
class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column()
  name: string

  @Column()
  lastname: string
}

Factories

class UserFactory extends Factory<User> {
  protected async entity(): Promise<User> {
    const user = new User()
    user.name = 'John'
    user.lastname = 'Doe'
    return user
  }
}

Seeders

export class UserSeeder extends Seeder {
  async run() {
    await this.factory(UserFactory).create()
  }
}

Run It!

typeorm-seeding seed -c seeding-source.js

Installation

This module requires TypeORM 0.3.0 and higher.

Please read the TypeORM Getting Started documentation. This explains how to setup a TypeORM project.

Install the package with npm or yarn. Add the development flag if you are not using seeders and/or factories in production code.

The Faker package was previously a dependency of the project, but now it is optional. If you want to use faker, you will need to install and import it.

npm i [-D] @concepta/typeorm-seeding @faker-js/faker
yarn add [-D] @concepta/typeorm-seeding @faker-js/faker

Configuration

The heart of the module is the SeedingSource class.

You must create an instance and provide it to the CLI via configuration file, or pass it directly to your Seeder and Factory constructors.

Seeding Source Class

class SeedingSource {
  constructor(options: SeedingSourceOptions)
}

Seeding Source Options

The SeedingSource instance is configured with a TypeORM DataSource instance (or options to create an instance), a list of Seeder classes, and optionally a list of default Seeder classes.

interface SeedingSourceOptions {
  // data source instance, or options for creating a data source instance
  dataSource: DataSource | DataSourceOptions
  // all of your seeder classes, REQUIRED for CLI
  seeders?: ClassConstructor<Seeder>[]
  // default seeders (if provided, only these will be run by the CLI)
  defaultSeeders?: ClassConstructor<Seeder>[]
}

Data Source Instance

The DataSource config can be defined directly in the seeding source module, or imported from a different module. Since most developers have this in it's own file, our example will follow this standard.

const { DataSource } = require('typeorm')
const { User, Pet } = require('./my-module')

module.exports = new DataSource({
  type: 'sqlite',
  database: ':memory:',
  entities: [User, Pet],
})

Seeding Source Instance

We import the above DataSource and our seeders from my-module.

const { SeedingSource } = require('@concepta/typeorm-seeding')
const { AppSeeder, UserSeeder, PetSeeder, dataSource } = require('./my-module')

module.exports = new SeedingSource({
  dataSource, // overridden if provided by CLI arg
  seeders: [UserSeeder, PetSeeder],
})

Seeding Source Initialization

You must initialize your SeedingSource instance or manually set an already intialized instance of DataSource before you make any queries.

When you are seeding via the CLI command, these steps are done for you automatically at runtime.

Initialize DataSource passed via options

// get your seeding source instance
const { seedingSource } = require('./seeding-source')

// initialize the Data Source that was passed via options
await seedingSource.initialize()

Manually initialize DataSource and set on SeedingSource

const { dataSource } = require('./data-source')
const { seedingSource } = require('./seeding-source')

// initialize the Data Source manually
await dataSource.initialize()

// set on the Seeding Source
seedingSource.dataSource = dataSource

Factory Class

Factory is how we provide a way to simplify entities creation, implementing a factory creational pattern. It is defined as an abstract class with generic typing, so you have to extend it.

Note: It is possible to create more than one Factory related to the same entity class.

class UserFactory extends Factory<User> {
  // required
  protected async entity(): Promise<User> {
    const user = new User()
    user.name = 'John'
    user.lastname = 'Doe'
    return user
  }

  // optional
  protected async finalize(user: User): Promise<void> {
    // last chance to mutate the entity at end of factory lifecycle
  }
}

entity

This method must be overridden to define how the entity is generated. It is called to instantiate the entity and the result will be used for the rest of factory lifecycle.

Basic Implementation

In the most basic implementation, the factory is responsible for creating the new entity instance.

class UserFactory extends Factory<User> {
  protected async entity(): Promise<User> {
    const user = new User()
    user.name = 'John'
    user.lastname = 'Doe'
    return user
  }
}

Advanced Implementation

If you configure the entity option, then the method receives a new instance of that entity class.

This factory is now eligible for entity overrides.

class UserFactory extends Factory<User> {
  protected options = {
    entity: User,
  }

  protected async entity(user: User): Promise<User> {
    user.name = 'John'
    user.lastname = 'Doe'
    return user
  }
}

finalize

This method can be overridden to customize how the entity is finalized. It is called at the end of the entity generation lifecycle, giving one last opportunity to set defaults or perform data validation, etc.

protected async finalize(pet: Pet): Promise<void> {
  if (!pet.owner) {
    pet.owner = await this.factory(UserFactory).create()
  }
}

Factory Usage

make

The make() method executes the factory lifecycle and returns a new instance of the given entity.

The instance is filled with the generated values from the factory lifecycle, but not saved in the database.

Important: You must pass the entity to the factory's save() method to persist the entity if desired.

make(overrideParams: Partial<Entity> = {}): Promise<Entity>
export class UserSeeder extends Seeder {
  async run() {
    // get a user factory
    const userFactory = this.factory(UserFactory)
    // using defaults
    const user = userFactory.make()
    // with email override
    const user2 = userFactory.make({ email: 'other@mail.com' })
    // persist to database (optional)
    await userFactory.save([user, user2])
  }
}

makeMany

The makeMany() method executes the factory lifecycle and returns many new instances of the given entity.

Each instance is filled with the generated values from the factory lifecycle, but not saved in the database.

Important: You must pass the entities to the factory's save() method to persist the entities if desired.

makeMany(amount: number, overrideParams: Partial<Entity> = {}): Promise<Entity>
export class UserSeeder extends Seeder {
  async run() {
    // get a user factory
    const userFactory = this.factory(UserFactory)
    // using defaults
    const users = userFactory.makeMany(10)
    // with email override
    const users2 = userFactory.makeMany(10, { email: 'other@mail.com' })
    // persist to database (optional)
    await userFactory.save(users)
    await userFactory.save(users2)
  }
}

create

The create() method is similar to the make() method, but at the end the generated entity instance gets persisted in the database using the TypeORM entity manager.

create(overrideParams: Partial<Entity> = {}, saveOptions?: SaveOptions): Promise<Entity>
export class UserSeeder extends Seeder {
  async run() {
    // get a user factory
    const userFactory = this.factory(UserFactory)
    // using default
    await userFactory.create()
    // override the email
    await userFactory.create({ email: 'other@mail.com' })
    // using save options
    await userFactory.create({ email: 'other@mail.com' }, { listeners: false })
  }
}

createMany

The createMany() methods is similar to the makeMany() method, but at the end the generated entity instances gets persisted in the database using the TypeORM entity manager.

createMany(amount: number, overrideParams: Partial<Entity> = {}, saveOptions?: SaveOptions): Promise<Entity>
export class UserSeeder extends Seeder {
  async run() {
    // get a user factory
    const userFactory = this.factory(UserFactory)
    // using default
    await userFactory.createMany(10)
    // override the email
    await userFactory.createMany(10, { email: 'other@mail.com' })
    // using save options
    await userFactory.createMany(10, { email: 'other@mail.com' }, { listeners: false })
  }
}

map

Use the .map() function to alter the generated value for each entity.

This is especially useful for setting different data for each generated entity with makeMany() and createMany(), compared to overrideParams which sets the same data for all generated entities.

map(mapFunction: (entity: Entity) => void): Factory
import { faker } from '@faker-js/faker'

export class UserSeeder extends Seeder {
  async run() {
    await this.factory(UserFactory)
      .map((user) => {
        user.favoriteColor = faker.color.human()
      })
      .createMany(10)
  }
}

factory

Use the .factory() utility method to get an instance of a Factory class dependency.

This is the recommended way to obtain a factory instance, as it automatically sets the seeding source, as well as supports the factory overrides api.

The seeding source of the calling Factory is automatically set on the new factory.

factory<T>(factory: ClassConstructor<ExtractFactory<T>>): ExtractFactory<T>
class UserFactory extends Factory<User> {
  protected options = {
    entity: User,
  }

  protected async entity(user: User): Promise<User> {
    user.name = 'John'
    user.lastname = 'Doe'
    user.pet = await this.factory(PetFactory).create()
    return user
  }
}

save

Use the .save() utility method to persist entities to the database, that were not automatically persisted.

async save(entity: Entity, saveOptions?: SaveOptions): Promise<Entity>
async save(entities: Entity[], saveOptions?: SaveOptions): Promise<Entity[]>
export class UserSeeder extends Seeder {
  async run() {
    const userFactory = this.factory(UserFactory)
    const user = await userFactory.make()
    await userFactory.save(user)
  }
}

Lifecycle Steps

The complete factory lifecycle is explained in the following table.

PriorityStep
1map()Entity is passed to map function (if provided)
2paramOverridesParam overrides passed to make(), makeMany(),create(), createMany() are applied
3PromisesEntity attributes which are promises are resolved
4FactoriesEntity attributes which are factory instances are executed (.make() or .create()).
5finalize()Entity is passed to finalize. No further processing is done after this.

Seeder Class

The Seeder class provides a way to orchestrate the use of factories to insert data into the database.

It is an abstract class with one method to be implemented: run().

Note: Seeder classes are required for seeding from the CLI. However, you can create your own seeding scripts using only Factory classes.

class UserSeeder extends Seeder {
  async run() {
    // use factories to generate and persist entities
    await this.factory(UserFactory).createMany(10)
  }
}

run

This function must be defined when extending the class.

run(): Promise<void>
async run() {
    await this.factory(UserFactory).createMany(10)
}

Seeder Usage

factory

Use the .factory() utility method to get an instance of a Factory class.

This is the recommended way to obtain a factory instance, as it automatically sets the seeding source, as well as supports the factory overrides api.

The seeding source of the calling Seeder is automatically set on the returned factory.

factory<T>(factory: ClassConstructor<ExtractFactory<T>>): ExtractFactory<T>
export class UserSeeder extends Seeder {
  async run() {
    const userFactory = this.factory(UserFactory)
    await userFactory.create()
  }
}

call

This method enables you to create a tree structure of seeders.

protected async call(seeders?: SeederInstanceOrClass[]): Promise<void>

Call one or more seeders explicitly.

export class UserSeeder extends Seeder {
  async run() {
    await this.factory(UserFactory).createMany(10)
    await this.call([PetSeeder])
  }
}

Call one or more seeders via options.

This seeder is now eligible for seeder overrides.

export class UserSeeder extends Seeder {
  protected options: {
    seeders: [PetSeeder]
  }

  async run() {
    await this.factory(UserFactory).createMany(10)
    await this.call()
  }
}

If your seeders use a tree structure, you can use the defaultSeeders option to determine the entry point(s) of your seeder tree(s)

CLI

There are two possible commands to execute, one to see the current configuration and one to run seeders.

Add the following scripts to your package.json file to configure them.

"scripts": {
  "seed:config": "typeorm-seeding config -r ./dist -c seeding-source.js",
  "seed:run": "typeorm-seeding seed -r ./dist -c seeding-source.js",
}

config

This command prints the seeder configuration.

typeorm-seeding config

Example result

{
  seeders: [ [class User], [class Pet] ],
  defaultSeeders: [ [class AppSeeder] ],
  dataSource: [ [class DataSource] ]
}
Options
OptionDefaultDescription
--root or -rprocess.cwd()Path to the project root
--seedingSource or -cRelative path to the seeding config from root.

seed

This command executes your seeder(s).

typeorm-seeding seed
Options
OptionDefaultDescription
--root or -rprocess.cwd()Path to the project root
--seedingSource or -cRelative path to the seeding config from root.
--dataSource or -dRelative path to TypeORM data source config from root.
--seed or -sOne or more specific seeder class(es) to run individually.

Unit Testing

The unit testing features allow you to use Factories and Seeders completely independently of the CLI.

SeedingSources and DataSources can be instanitated at run time and directly injected into Factories and Seeders.

Additionally, you can override Factory and Seeder dependencies via class and constructor options.

Runner Class

To seed your unit tests with Seeders, a Runner class instance is provided on SeedingSource.

To run one or more seeders with one command, use the SeedingSource.run instance.

SeedingSource.run.all

Execute all seeders.

SeedingSource.run.all(): Promise<void>

SeedingSource.run.one

Execute one seeder.

SeedingSource.run.one(
  seeder: SeederInstanceOrClass,
): Promise<void>

SeedingSource.run.many

Execute many seeders.

SeedingSource.run.many(
  seeders: SeederInstanceOrClass[],
): Promise<void>

SeedingSource.run.defaults

Execute all default seeders.

SeedingSource.run.defaults(): Promise<void>

Factories

Factories can be used stand alone if you explicitly pass a SeedingSource instance option to the constructor.

import { seedingSource } from './my-module'

const factory = new UserFactory({ seedingSource })
const user = await factory.create()

If a factory gets a Factory dependency with the factory() method, you can override it using the overrides api.

import { faker } from '@faker-js/faker'
import { seedingSource } from './my-module'

class UserFactory extends Factory<User> {
  protected options = {
    entity: User,
  }

  protected async entity(user: User): Promise<User> {
    user.name = 'John'
    user.lastname = 'Doe'
    user.pet = await this.factory(PetFactory).create()
    return user
  }
}

class PetFactory2 extends Factory<Pet> {
  protected options = {
    entity: Pet,
    override: PetFactory,
  }

  async entity(pet: Pet) {
    pet.color = faker.color.human()
    return pet
  }
}

const factory = new UserFactory({
  seedingSource,
  factories: [new PetFactory2()],
})

// PetFactory2 is used to generate Pet entities
const user = await factory.create()

All Factory overrides are supported via the constructor for just-in-time operations.

constructor(optionOverrides?: FactoryOptionsOverrides<Entity>);
interface FactoryOptionsOverrides<T, SF = any> {
  entity?: ClassConstructor<T>
  factories?: ExtractFactory<SF>[]
  override?: ClassConstructor<Factory<any>>
  seedingSource?: SeedingSource
}

Seeders

Seeders can be used stand alone if you explicitly pass a SeedingSource instance option to the constructor.

import { seedingSource } from './my-module'

const userSeeder = new UserSeeder({ seedingSource })
await userSeeder.run()

If a seeder gets a Factory dependency with the factory() method, you can override it using the overrides api.

import { faker } from '@faker-js/faker'
import { seedingSource } from './my-module'

export class UserSeeder extends Seeder {
  async run() {
    await this.factory(UserFactory).createMany(10)
  }
}

class UserFactory2 extends Factory<User> {
  protected options = {
    entity: User,
    override: UserFactory,
  }

  async entity(user: User) {
    user.favoriteColor = faker.color.human()
    return user
  }
}

const userSeeder = new UserSeeder({
  seedingSource,
  factories: [new UserFactory2()],
})

// UserFactory2 is used to generate User entities
await userSeeder.run()

If a seeder has the seeders options set, you can override them using the overrides api.

Important: When you override seeders, the entire list of seeders options is replaced, NOT merged.

import { faker } from '@faker-js/faker'
import { seedingSource } from './my-module'

export class UserSeeder extends Seeder {
  protected options: {
    seeders: [PetSeeder, FoodSeeder]
  }

  async run() {
    await this.factory(UserFactory).createMany(10)
    await this.call()
  }
}

export class PetSeederOverride extends Seeder {
  async run() {
    await this.factory(PetFactory)
      .map((pet) => {
        pet.color = faker.color.human()
      })
      .createMany(20)
  }
}

const userSeeder = new UserSeeder({
  seedingSource,
  seeders: [PetSeederOverride],
})

// UserSeeder and PetSeederOverride are executed, FoodSeeder is NOT
await userSeeder.run()

All Seeder overrides are supported via the constructor for just-in-time operations.

constructor(optionOverrides?: SeederOptionsOverrides);
interface SeederOptionsOverrides<SF = any> {
  seeders?: SeederInstanceOrClass[]
  factories?: ExtractFactory<SF>[]
  seedingSource?: SeedingSource
}