Skip to content

Commit

Permalink
feat(seeder): add seeder package
Browse files Browse the repository at this point in the history
  • Loading branch information
Langstra committed Feb 23, 2021
1 parent 8038b46 commit 0fcc64f
Show file tree
Hide file tree
Showing 23 changed files with 845 additions and 49 deletions.
188 changes: 188 additions & 0 deletions docs/docs/seeding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
title: Seeding
---

When initializing your application or testing it can be exhausting to create sample data for your database. The solution is to use seeding. Create factories for your entities and use them in the seed script or combine multiple seed scripts.

## Seeders
A seeder class contains one method `run`. This method is called when you use the command `mikro-orm db:seed`. In the `run` method you define how and what data you want to insert into the database. You can create entities using the [EntityManager](http://mikro-orm.io/docs/entity-manager) or you can use [Factories](#using-entity-factories).

As an example we will look at a very basic seeder.
```typescript
import { EntityManager } from '@mikro-orm/core';
import { Seeder } from '@mikro-orm/seeder';
import { Author } from './author'

class DatabaseSeeder extends Seeder {

async run(em: EntityManager): Promise<void> {
const author = em.create(Author, {
name: 'John Snow',
email: 'snow@wall.st'
});
await em.persistAndFlush(author);
em.clear();
}
}
```

### Using entity factories
Instead of specifying all the attributes for every entity, you can also use [entity factories](#entity-factories). These can be used to generate large amounts of database records. Please read the [documentation on how to define factories](#entity-factories) to learn how to define your factories.

As an example we will generate 10 authors.
```typescript
import { EntityManager } from '@mikro-orm/core';
import { Seeder } from '@mikro-orm/seeder';
import { AuthorFactory } from '../factories/author.factory'

class DatabaseSeeder extends Seeder {

async run(em: EntityManager): Promise<void> {
await (new AuthorFactory(em)).count(10).create()
em.clear();
}
}
```

### Calling additional seeders
Inside the `run` method you can specify other seeder classes. You can use the `call` method to breakup the database seeder into multiple files to prevent a seeder file from becoming too large. The `call` method accepts an array of seeder classes.
```typescript
import { EntityManager } from '@mikro-orm/core';
import { Seeder } from '@mikro-orm/seeder';
import { AuthorSeeder, BookSeeder } from '../seeders'

class DatabaseSeeder extends Seeder {

run(em: EntityManager): Promise<void> {
return this.call(em, [
AuthorSeeder,
BookSeeder
]);
}
}
```

## Entity factories
When testing you may insert entities in the database before starting a test. Instead of specifying every attribute of every entity by hand, you could also use a `Factory` to define a set of default attributes for an entity using entity factories.

Lets have a look at an example factory for an [Author entity](http://mikro-orm.io/docs/defining-entities).
```typescript
import { Factory } from '@mikro-orm/seeder';
import * as Faker from 'faker';
import { Author } from './entities/author.entity';

export class AuthorFactory extends Factory<Author> {
model = Author;

definition(faker: typeof Faker): Partial<Author> {
return {
name: faker.person.findName(),
email: faker.internet.email(),
age: faker.random.number(18, 99)
};
}
}
```
Basically you extend the base `Factory` class, define a `model` property and a `definition` method. The `model` defines for which entity the factory generates entity instances. The `definition` method returns the default set of attribute values that should be applied when creating an entity using the factory.

Via the faker property, factories have access to the [Faker library](https://www.npmjs.com/package/faker), which allows you to conveniently generate various kinds of random data for testing.

### Creating entities using factories
Once you defined your factories you can use them to generate entities. Simply import the factory, instantiate it and call the `make` or `create` method.
```typescript
const author = await (new AuthorFactory(orm.em)).make() as Author;
```
The `make` method returns the type `Author | Author[]`, so be sure to cast it to the correct type.

#### Generate multiple entities
Generate multiple entities by chaining the `count` and `make` method. The parameter of the `count` method is the number of entities you generate.
```typescript
// Generate 5 authors
const authors = await (new AuthorFactory(orm.em)).count(5).make() as Author[];
```
The `make` method returns the type `Author | Author[]`, since we chained the `count` method here with `5` we can be sure it returns `Author[]`.

#### Overriding attributes
If you would like to override some of the default values of your factories, you may pass an object to the make method. Only the specified attributes will be replaced while the rest of the attributes remain set to their default values as specified by the factory.
```typescript
const author = await (new AuthorFactory(orm.em)).make({
name: 'John Snow'
}) as Author;
```

### Persisting entities
The `create` method instantiates entities and persists them to the database using the `persistAndFlush` method of the EntityManager.
```typescript
// Make and persist a single author
const author = await (new AuthorFactory(orm.em)).make() as Author;

// Make and persist 5 authors
const authors = await (new AuthorFactory(orm.em)).count(5).create() as Author[];
```
You can override the default values of your factories by passing an object to the `create` method.
```typescript
// Make and persist a single author
const author = await (new AuthorFactory(orm.em)).create({
name: 'John Snow'
}) as Author;
```
### Factory relationships
It is nice to create large quantities of data for one entity, but most of the time we want to create data for multiple entities and also have relations between these. For this we can use the `map` method which can be chained on a factory. The `map` method can be called with a function that maps the output entity from the factory before returning it. Lets look at some examples for the different relations.

#### ManyToOne and OneToOne relations
```typescript
const book = await (new BookFactory(orm.em)).map(async (book) => {
book.author = await (new AuthorFactory(orm.em)).make() as Author;
return book;
}) as Book;
```

#### OneToMany and ManyToMany
```typescript
const book = await (new BookFactory(orm.em)).map(async (book) => {
book.owners = new Collection(book, await (new OwnerFactory(orm.em)).make() as Owner[]);
return book;
}) as Book;
```

## Use with CLI
You may execute the `db:seed` Mikro-ORM CLI command to seed your database. By default, the db:seed command runs the DatabaseSeeder class, which may in turn invoke other seed classes. However, you may use the --class option to specify a specific seeder class to run individually:
```shell script
mikro-orm database:seed

mikro-orm database:seed --class=BookSeeder
```

You may also seed your database using the `migrate:fresh` or `schema:fresh` command in combination with the --seed option, which will drop all tables and re-run all of your migrations or generate the database based on the current entities. This command is useful for completely re-building your database:
```shell script
mikro-orm migration:fresh --seed

mikro-orm schema:fresh --seed
```

## Use in tests
Now we know how to create seeders and factories, but how can we effectively use them in tests. We will show an example how it can be used.

```typescript
let seeder: MikroOrmSeeder;

beforeAll(async () => {
// Initialize seeder with config
seeder = await MikroOrmSeeder.init(mikroOrmConfig);

// Refresh the database to start clean
await seeder.refreshDatabase();

// Seed using a seeder defined by you
await seeder.seed(DatabaseSeeder);
})

test(() => {
// Do tests
});

afterAll(() => {
// Close connection
seeder.closeConnection();
})
```
13 changes: 8 additions & 5 deletions packages/cli/src/CLIConfigurator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import yargs, { Argv } from 'yargs';

import { ConfigurationLoader, Utils } from '@mikro-orm/core';
import yargs, { Argv } from 'yargs';
import { ClearCacheCommand } from './commands/ClearCacheCommand';
import { GenerateEntitiesCommand } from './commands/GenerateEntitiesCommand';
import { SchemaCommandFactory } from './commands/SchemaCommandFactory';
import { MigrationCommandFactory } from './commands/MigrationCommandFactory';
import { DatabaseSeedCommand } from './commands/DatabaseSeedCommand';
import { DebugCommand } from './commands/DebugCommand';
import { GenerateCacheCommand } from './commands/GenerateCacheCommand';
import { GenerateEntitiesCommand } from './commands/GenerateEntitiesCommand';
import { ImportCommand } from './commands/ImportCommand';
import { MigrationCommandFactory } from './commands/MigrationCommandFactory';
import { SchemaCommandFactory } from './commands/SchemaCommandFactory';

export class CLIConfigurator {

Expand All @@ -30,14 +30,17 @@ export class CLIConfigurator {
.command(new GenerateCacheCommand())
.command(new GenerateEntitiesCommand())
.command(new ImportCommand())
.command(new DatabaseSeedCommand())
.command(SchemaCommandFactory.create('create'))
.command(SchemaCommandFactory.create('drop'))
.command(SchemaCommandFactory.create('update'))
.command(SchemaCommandFactory.create('fresh'))
.command(MigrationCommandFactory.create('create'))
.command(MigrationCommandFactory.create('up'))
.command(MigrationCommandFactory.create('down'))
.command(MigrationCommandFactory.create('list'))
.command(MigrationCommandFactory.create('pending'))
.command(MigrationCommandFactory.create('fresh'))
.command(new DebugCommand())
.recommendCommands()
.strict();
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/commands/DatabaseSeedCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { MikroOrmSeeder } from '@mikro-orm/seeder';
import c from 'ansi-colors';
import { Arguments, Argv, CommandModule } from 'yargs';
import { CLIHelper } from '../CLIHelper';

export class DatabaseSeedCommand<T> implements CommandModule<T, {class: string}> {

command = 'database:seed';
describe = 'Seed the database using the seeder class';
builder = (args: Argv) => {
args.option('c', {
alias: 'class',
type: 'string',
desc: 'Seeder class to run',
default: 'DatabaseSeeder',
});
return args as Argv<{class: string}>;
};

/**
* @inheritdoc
*/
async handler(args: Arguments<{class: string}>) {
const seeder = await MikroOrmSeeder.init(await CLIHelper.getConfiguration());
await seeder.seedString(args.class);
CLIHelper.dump(c.green(`Seeder ${args.class} successfully seeded`));
await seeder.closeConnection(true);
}

}
4 changes: 2 additions & 2 deletions packages/cli/src/commands/ImportCommand.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Arguments, CommandModule } from 'yargs';
import c from 'ansi-colors';
import { MikroORM } from '@mikro-orm/core';
import { AbstractSqlDriver } from '@mikro-orm/knex';
import c from 'ansi-colors';
import { Arguments, CommandModule } from 'yargs';
import { CLIHelper } from '../CLIHelper';

export class ImportCommand implements CommandModule {
Expand Down
84 changes: 58 additions & 26 deletions packages/cli/src/commands/MigrationCommandFactory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Configuration, MikroORM, Utils } from '@mikro-orm/core';
import { AbstractSqlDriver, SchemaGenerator } from '@mikro-orm/knex';
import { MigrateOptions, Migrator } from '@mikro-orm/migrations';
import { MikroOrmSeeder } from '@mikro-orm/seeder';
import c from 'ansi-colors';
import { Arguments, Argv, CommandModule } from 'yargs';
import { MigrateOptions, Migrator } from '@mikro-orm/migrations';
import { Configuration, MikroORM, Utils } from '@mikro-orm/core';
import { AbstractSqlDriver } from '@mikro-orm/knex';
import { CLIHelper } from '../CLIHelper';

export class MigrationCommandFactory {
Expand All @@ -13,6 +14,7 @@ export class MigrationCommandFactory {
down: 'Migrate one step down',
list: 'List all executed migrations',
pending: 'List all pending migrations',
fresh: 'Clear the database and rerun all migrations',
};

static create<U extends Options = Options>(command: MigratorMethod): CommandModule<unknown, U> & { builder: (args: Argv) => Argv<U>; handler: (args: Arguments<U>) => Promise<void> } {
Expand All @@ -33,9 +35,39 @@ export class MigrationCommandFactory {
this.configureUpDownCommand(args, method);
}

if (method === 'fresh') {
this.configureFreshCommand(args);
}

return args;
}

static async handleMigrationCommand(args: Arguments<Options>, method: MigratorMethod): Promise<void> {
const options = { pool: { min: 1, max: 1 } } as Partial<Configuration>;
const orm = await CLIHelper.getORM(undefined, options) as MikroORM<AbstractSqlDriver>;
const migrator = new Migrator(orm.em);

switch (method) {
case 'create':
await this.handleCreateCommand(migrator, args, orm.config);
break;
case 'list':
await this.handleListCommand(migrator);
break;
case 'pending':
await this.handlePendingCommand(migrator);
break;
case 'up':
case 'down':
await this.handleUpDownCommand(args, migrator, method);
break;
case 'fresh':
await this.handleFreshCommand(args, migrator, orm);
}

await orm.close(true);
}

private static configureUpDownCommand(args: Argv, method: MigratorMethod) {
args.option('t', {
alias: 'to',
Expand Down Expand Up @@ -77,27 +109,12 @@ export class MigrationCommandFactory {
});
}

static async handleMigrationCommand(args: Arguments<Options>, method: MigratorMethod): Promise<void> {
const options = { pool: { min: 1, max: 1 } } as Partial<Configuration>;
const orm = await CLIHelper.getORM(undefined, options) as MikroORM<AbstractSqlDriver>;
const migrator = new Migrator(orm.em);

switch (method) {
case 'create':
await this.handleCreateCommand(migrator, args, orm.config);
break;
case 'list':
await this.handleListCommand(migrator);
break;
case 'pending':
await this.handlePendingCommand(migrator);
break;
case 'up':
case 'down':
await this.handleUpDownCommand(args, migrator, method);
}

await orm.close(true);
private static configureFreshCommand(args: Argv) {
args.option('seed', {
type: 'boolean',
desc: 'Allows to seed the database after dropping it and rerunning all migrations',
default: false,
});
}

private static async handleUpDownCommand(args: Arguments<Options>, migrator: Migrator, method: MigratorMethod) {
Expand Down Expand Up @@ -141,6 +158,21 @@ export class MigrationCommandFactory {
CLIHelper.dump(c.green(`${ret.fileName} successfully created`));
}

private static async handleFreshCommand(args: Arguments<Options>, migrator: Migrator, orm: MikroORM<AbstractSqlDriver>) {
const generator = new SchemaGenerator(orm.em);
await generator.dropSchema();
CLIHelper.dump(c.green('Dropped schema successfully'));
const opts = MigrationCommandFactory.getUpDownOptions(args);
await migrator.up(opts as string[]);
const message = this.getUpDownSuccessMessage('up', opts);
CLIHelper.dump(c.green(message));
if (args.seed) {
const seeder = await MikroOrmSeeder.init(orm);
await seeder.seedString('DatabaseSeeder');
CLIHelper.dump(c.green('Database seeded successfully'));
}
}

private static getUpDownOptions(flags: CliUpDownOptions): MigrateOptions {
if (!flags.to && !flags.from && flags.only) {
return { migrations: flags.only.split(/[, ]+/) };
Expand Down Expand Up @@ -182,7 +214,7 @@ export class MigrationCommandFactory {

}

type MigratorMethod = 'create' | 'up' | 'down' | 'list' | 'pending';
type MigratorMethod = 'create' | 'up' | 'down' | 'list' | 'pending' | 'fresh';
type CliUpDownOptions = { to?: string | number; from?: string | number; only?: string };
type GenerateOptions = { dump?: boolean; blank?: boolean; initial?: boolean; path?: string; disableFkChecks?: boolean };
type GenerateOptions = { dump?: boolean; blank?: boolean; initial?: boolean; path?: string; disableFkChecks?: boolean; seed: boolean };
type Options = GenerateOptions & CliUpDownOptions;
Loading

0 comments on commit 0fcc64f

Please sign in to comment.