We use cookies to enhance your experience on the site
CodeWorlds

Seeders and Fixtures - the cohort's starting equipment

Consul Caesar.js is tired of a certain ritual. Every time a new developer joins the treasury project, or a fresh database is set up for tests, somebody types in the same data by hand: a few centurions, a couple of legionaries, the basic skills. A fresh database is an empty castra - and a cohort without equipment will not march on campaign. Time to hire a quartermaster who does this for us: a seeder.

Seeder vs fixtures - the code and the supplies

These two terms love to get mixed up, so let's separate them right away. A seeder is code - a class which, when run, fills the database with starting data. Fixtures are the data itself - static sets stored in files (JSON, YAML), waiting for someone to load them. In Roman terms: fixtures are the supply list on parchment, the seeder is the quartermaster who loads the wagons according to that list. A seeder can have its data written directly in code, or read it precisely from a fixtures file.

Your first seeder

We will use the

typeorm-extension
package, which adds seeding support to TypeORM. A seeder is a class implementing the
Seeder
interface with a single
run
method - it receives the database connection (
dataSource
), from which we take the repositories we already know:

1// seeds/centurion.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder } from 'typeorm-extension';
4import { Centurion } from '../src/centurion/centurion.entity';
5
6export default class CenturionSeeder implements Seeder {
7  public async run(dataSource: DataSource): Promise<void> {
8    const centurionRepository = dataSource.getRepository(Centurion);
9
10    await centurionRepository.save([
11      { name: 'Maximus Decimus', cohortName: 'Legio X Equestris', yearsOfExperience: 15 },
12      { name: 'Livia Drusilla', cohortName: 'Legio III Gallica', yearsOfExperience: 8 },
13    ]);
14  }
15}

Nothing beyond what you already know: we take a repository and call

save
with an array of objects. All the novelty is in the packaging: a class with a
run
method can be executed with a single command - on a fresh database, on every developer's machine, in every environment - and it will load exactly the same equipment every time.

Cleaning and idempotency - the seeder run twice

What happens when someone runs the seeder again? With the code above - a second batch of the same centurions and a database full of duplicates. The quartermaster must be prepared for this, and there are two strategies:

1public async run(dataSource: DataSource): Promise<void> {
2  const centurionRepository = dataSource.getRepository(Centurion);
3
4  // Strategy 1: clear the table before seeding
5  await centurionRepository.clear();
6
7  // Strategy 2: if the data is already there, do nothing
8  const existing = await centurionRepository.count();
9  if (existing > 0) {
10    return;
11  }
12
13  await centurionRepository.save([ /* ... */ ]);
14}

In practice you pick one of them.

clear()
empties the table and seeds from scratch - the result is always predictable, so it is a good choice for development and test databases. The
count()
check leaves existing data untouched - safer wherever the database may already hold something valuable. Both versions share the property this is all about: the seeder can be run any number of times and the database will not turn into a pile of duplicates. This resistance to repeated runs is called idempotency - and I recommend treating it as an iron requirement of every seeder, @name.

Order matters - relations between tables

A legionary in our system belongs to a centurion. That means we cannot seed legionaries into an empty database - their records point at centurions, which must exist first. The quartermaster loads the wagons in a fixed order:

1// 1. Independent entities first
2const centurions = await centurionRepository.save([
3  { name: 'Maximus Decimus', cohortName: 'Legio X Equestris' },
4]);
5
6// 2. Then the entities that reference them
7await legionaryRepository.save([
8  { name: 'Marcus Antonius', rank: 'Speculator', centurion: centurions[0] },
9  { name: 'Gaius Julius', rank: 'Optio', centurion: centurions[0] },
10]);

Notice that

save
returns the saved objects - already carrying the identifiers the database assigned. That is what lets us pass
centurions[0]
as the relation's owner in the second step. The general rule reads: first the tables others point to (skills, ranks, centurions), then the dependent tables (legionaries), and finally the data that ties one to the other (tributes with an assigned owner). Cleaning runs in the reverse order - dependents first, independents last.

A data factory - a hundred legionaries for tests

Starting data we type by hand, because there is little of it. But for performance tests or pagination work you need hundreds of records - and nobody is going to invent them. That is what a factory is for: a recipe for one random object, which the

@faker-js/faker
library fills with made-up data:

1// factories/legionary.factory.ts
2import { setSeederFactory } from 'typeorm-extension';
3import { Legionary } from '../src/legionariusze/legionariusze.entity';
4
5export default setSeederFactory(Legionary, (faker) => {
6  const legionary = new Legionary();
7  legionary.name = faker.person.fullName();
8  legionary.rank = faker.helpers.arrayElement(['Legionary', 'Speculator', 'Optio']);
9  legionary.tributeCount = faker.number.int({ min: 0, max: 50 });
10  legionary.isActive = faker.datatype.boolean();
11  return legionary;
12});

The factory describes what one random legionary looks like: a real-sounding name, a rank drawn from a list, a tribute counter within a given range. Multiplying is a single line in the seeder:

1const legionaryFactory = factoryManager.get(Legionary);
2await legionaryFactory.saveMany(100);

And the castra is full: a hundred different legionaries, each with different data, zero manual work. Note the division of roles - the factory defines the shape of a single record, while the seeder decides how many are created and when.

Running it - wiring everything together

Finally we tell TypeORM where to look for seeds and factories. We extend the data source configuration with two fields:

1// data-source.ts
2const options: DataSourceOptions & SeederOptions = {
3  type: 'postgres',
4  database: 'legionariusze_cohort',
5  entities: ['src/**/*.entity{.ts,.js}'],
6  seeds: ['src/seeds/*{.ts,.js}'],
7  factories: ['src/factories/*{.ts,.js}'],
8};

The

seeds
field points at the seeder classes,
factories
at the factory recipes. From now on a single terminal command,
npm run seed:run
, walks through all the seeds and equips the database. A fresh castra becomes a ready camp in seconds.

Summary

Your cohort will never march without supplies again:

  • a seeder is code that loads starting data, fixtures are static data in files - the supply list and the quartermaster who fulfils it,
  • a seeder must be idempotent: clear the table with
    clear()
    , or skip seeding when
    count()
    detects existing data,
  • with relations the order is sacred: independent entities first, dependent ones after - and cleaning in reverse,
  • a factory with faker describes one random record, and
    saveMany(100)
    multiplies it for tests,
  • the
    seeds
    and
    factories
    fields in the configuration tie it all into one
    seed:run
    command.

In the next lesson we will take on data validation - since the treasury can now equip itself, it is time to make sure nothing spoiled gets inside. For now remember: a seeder is the cohort's quartermaster - thanks to it, every fresh database receives the same complete equipment with a single command.

Go to CodeWorlds