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.
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.
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.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.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.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.
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.Your cohort will never march without supplies again:
clear(), or skip seeding when count() detects existing data,saveMany(100) multiplies it for tests,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.