Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Seeders i Fixtures - początkowe wyposażenie kohorty

Witajcie, organizatorzy wypraw! Konsul Caesar.js zauważył, że za każdym razem gdy kohorta wyrusza na kampanię, legion musi od nowa ładować podstawowe zapasy. To strata czasu! Czas nauczyć się tworzenia seedów i fixtures - gotowych zestawów danych, które automatycznie wyposażą naszą kohortę we wszystko, co potrzebne do udanej ekspedycji!

Czym są Seeders i Fixtures?

Seeders to jak doświadczeni kwatermistrzowie, którzy dokładnie wiedzą, czym wyposażyć kohortę przed wymarszem:

  • Podstawowe dane - początkowy legion, kastra, typy tributów
  • Dane testowe - przykładowi legioniści i tributy do testowania
  • Dane konfiguracyjne - ustawienia systemu, role użytkowników
  • Dane demonstracyjne - przykłady dla nowych użytkowników

Fixtures vs Seeders

  • Fixtures - statyczne dane w plikach (JSON, YAML, TypeScript)
  • Seeders - kod wykonujący się automatycznie przy starcie

Seeders w TypeORM

Podstawowy seeder

1// seeds/legionariusze.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder, SeederFactoryManager } from 'typeorm-extension';
4import { Legionary } from '../src/legionariusze/legionariusze.entity';
5import { Centurion } from '../src/centurion/centurion.entity';
6import { Skill } from '../src/skill/skill.entity';
7
8export default class LegionarySeeder implements Seeder {
9 public async run(
10 dataSource: DataSource,
11 factoryManager: SeederFactoryManager
12 ): Promise<any> {
13 const legionariuszeRepository = dataSource.getRepository(Legionary);
14 const centurionRepository = dataSource.getRepository(Centurion);
15 const skillRepository = dataSource.getRepository(Skill);
16
17 // 1. Sprawdź czy dane już istnieją
18 const existingLegionarysCount = await legionariuszeRepository.count();
19 if (existingLegionarysCount > 0) {
20 console.log('Legionarys already seeded, skipping...');
21 return;
22 }
23
24 // 2. Stwórz umiejętności
25 const skills = await skillRepository.save([
26 {
27 name: 'Navigation',
28 description: 'Ability to navigate cohorts across dangerous waters',
29 difficulty: 'Intermediate'
30 },
31 {
32 name: 'Sword Fighting',
33 description: 'Master the art of swordsmanship',
34 difficulty: 'Advanced'
35 },
36 {
37 name: 'Tribute Hunting',
38 description: 'Finding hidden tributes using maps and clues',
39 difficulty: 'Expert'
40 },
41 {
42 name: 'Legion Repair',
43 description: 'Maintaining and repairing cohort components',
44 difficulty: 'Basic'
45 }
46 ]);
47
48 // 3. Stwórz centurionów
49 const centurions = await centurionRepository.save([
50 {
51 name: 'Maximus Decimus',
52 cohortName: 'Legio X Equestris',
53 yearsOfExperience: 15,
54 totalTributeValue: 50000
55 },
56 {
57 name: 'Livia Drusilla',
58 cohortName: 'Legio III Gallica',
59 yearsOfExperience: 8,
60 totalTributeValue: 25000
61 },
62 {
63 name: 'Gnaeus Pompeius',
64 cohortName: 'Legio XII Fulminata',
65 yearsOfExperience: 12,
66 totalTributeValue: 40000
67 }
68 ]);
69
70 // 4. Stwórz legionariuszów
71 const legionariuszes = [
72 {
73 name: 'Marek Antoniusz',
74 rank: 'Speculator',
75 tributeCount: 15,
76 totalTributeValue: 12000,
77 isActive: true,
78 centurion: centurions[0],
79 skills: [skills[0], skills[2]] // Navigation i Tribute Hunting
80 },
81 {
82 name: 'Gajusz Juliusz',
83 rank: 'Blacksmith',
84 tributeCount: 8,
85 totalTributeValue: 6000,
86 isActive: true,
87 centurion: centurions[0],
88 skills: [skills[1], skills[3]] // Sword Fighting i Legion Repair
89 },
90 {
91 name: 'Aurelia Cotta',
92 rank: 'Optio',
93 tributeCount: 20,
94 totalTributeValue: 18000,
95 isActive: true,
96 centurion: centurions[1],
97 skills: [skills[0], skills[1]] // Navigation i Sword Fighting
98 }
99 ];
100
101 await legionariuszeRepository.save(legionariuszes);
102
103 console.log('Legionary legion successfully seeded! ');
104 }
105}

Seeder dla tributów

1// seeds/tribute.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder, SeederFactoryManager } from 'typeorm-extension';
4import { Tribute } from '../src/tribute/tribute.entity';
5import { Legionary } from '../src/legionariusze/legionariusze.entity';
6import { TributeMap } from '../src/tribute-map/tribute-map.entity';
7
8export default class TributeSeeder implements Seeder {
9 public async run(
10 dataSource: DataSource,
11 factoryManager: SeederFactoryManager
12 ): Promise<any> {
13 const tributeRepository = dataSource.getRepository(Tribute);
14 const legionariuszeRepository = dataSource.getRepository(Legionary);
15 const tributeMapRepository = dataSource.getRepository(TributeMap);
16
17 // Sprawdź czy tributy już istnieją
18 const existingTributes = await tributeRepository.count();
19 if (existingTributes > 0) {
20 console.log('Tributes already seeded, skipping...');
21 return;
22 }
23
24 // Pobierz istniejących legionariuszów
25 const legionariuszes = await legionariuszeRepository.find();
26 if (legionariuszes.length === 0) {
27 console.log('No legionariuszes found, please run LegionarySeeder first');
28 return;
29 }
30
31 // Definicje typów tributów
32 const tributeTypes = [
33 {
34 type: 'gold',
35 items: [
36 { name: 'Golden Aureus', baseValue: 100 },
37 { name: 'Golden Chalice', baseValue: 500 },
38 { name: 'Crown of Neptune', baseValue: 5000 }
39 ]
40 },
41 {
42 type: 'silver',
43 items: [
44 { name: 'Silver Coin', baseValue: 50 },
45 { name: 'Silver Sword', baseValue: 300 },
46 { name: 'Silver Compass', baseValue: 800 }
47 ]
48 },
49 {
50 type: 'jewels',
51 items: [
52 { name: 'Ruby Ring', baseValue: 1200 },
53 { name: 'Emerald Necklace', baseValue: 2000 },
54 { name: 'Diamond Tiara', baseValue: 8000 }
55 ]
56 },
57 {
58 type: 'artifacts',
59 items: [
60 { name: 'Ancient Map', baseValue: 1500 },
61 { name: 'Mystical Medallion', baseValue: 3000 },
62 { name: 'Cursed Eagle', baseValue: 666, isCursed: true }
63 ]
64 }
65 ];
66
67 const locations = [
68 'Forum Romanum',
69 'Castra Praetoria',
70 'Via Appia',
71 'Pompeii',
72 'Capua',
73 'Ostia',
74 'Mare Nostrum',
75 'Catacumbae'
76 ];
77
78 const tributesToCreate = [];
79 const tributeMapsToCreate = [];
80
81 tributeTypes.forEach(category => {
82 category.items.forEach(item => {
83 const randomLegionary = legionariuszes[Math.floor(Math.random() * legionariuszes.length)];
84 const randomLocation = locations[Math.floor(Math.random() * locations.length)];
85 const valueVariation = 0.8 + Math.random() * 0.4; // ±20% wartości
86 
87 const tribute = {
88 name: item.name,
89 type: category.type,
90 value: Math.round(item.baseValue * valueVariation),
91 description: `A magnificent ${item.name.toLowerCase()} found in the depths of ${randomLocation}`,
92 location: randomLocation,
93 isCursed: item.isCursed || false,
94 collectedAt: this.getRandomPastDate(),
95 legionariusze: randomLegionary
96 };
97
98 tributesToCreate.push(tribute);
99
100 // Niektóre tributy mają mapy
101 if (Math.random() < 0.3) { // 30% szans na mapę
102 tributeMapsToCreate.push({
103 islandName: randomLocation,
104 latitude: this.getRandomLatitude(),
105 longitude: this.getRandomLongitude(),
106 instructions: this.generateTributeInstructions(),
107 isFound: true, // Skoro mamy tributy, mapa została użyta
108 legionariusze: randomLegionary
109 });
110 }
111 });
112 });
113
114 // Zapisz tributy i mapy
115 await tributeRepository.save(tributesToCreate);
116 await tributeMapRepository.save(tributeMapsToCreate);
117
118 console.log(`Created ${tributesToCreate.length} tributes and ${tributeMapsToCreate.length} tribute maps! `);
119 }
120
121 private getRandomPastDate(): Date {
122 const now = new Date();
123 const pastDays = Math.floor(Math.random() * 365 * 2); // Ostatnie 2 lata
124 return new Date(now.getTime() - pastDays * 24 * 60 * 60 * 1000);
125 }
126
127 private getRandomLatitude(): number {
128 return (Math.random() - 0.5) * 180; // -90 do 90
129 }
130
131 private getRandomLongitude(): number {
132 return (Math.random() - 0.5) * 360; // -180 do 180
133 }
134
135 private generateTributeInstructions(): string {
136 const instructions = [
137 'Walk 50 paces north from the old oak tree',
138 'Find the eagle-shaped rock near the beach',
139 'Look for the X marked on the ancient stone',
140 'Dig beneath the palm tree with the broken trunk',
141 'Search near the cave entrance on the east side',
142 'Follow the stream until you reach the waterfall'
143 ];
144 
145 const chosen = [];
146 const count = 2 + Math.floor(Math.random() * 3); // 2-4 instrukcji
147 
148 for (let i = 0; i < count; i++) {
149 const randomIndex = Math.floor(Math.random() * instructions.length);
150 chosen.push(instructions[randomIndex]);
151 instructions.splice(randomIndex, 1);
152 }
153 
154 return chosen.join('. ') + '.';
155 }
156}

Factory Pattern dla testów

1// factories/legionariusze.factory.ts
2import { define } from 'typeorm-seeding';
3import { Legionary } from '../src/legionariusze/legionariusze.entity';
4import { Faker } from '@faker-js/faker';
5
6define(Legionary, (faker: Faker) => {
7 const legionariusze = new Legionary();
8 
9 legionariusze.name = faker.person.fullName();
10 legionariusze.rank = faker.helpers.arrayElement([
11 'Legionary', 'Ballistarius', 'Speculator', 'Optio', 'Centurion'
12 ]);
13 legionariusze.tributeCount = faker.number.int({ min: 0, max: 50 });
14 legionariusze.totalTributeValue = faker.number.int({ min: 0, max: 100000 });
15 legionariusze.isActive = faker.datatype.boolean();
16 legionariusze.joinedAt = faker.date.past({ years: 5 });
17 legionariusze.lastSeenAt = faker.date.recent({ days: 30 });
18 
19 // Umiejętności jako JSON
20 legionariusze.skills = {
21 navigation: faker.number.int({ min: 1, max: 100 }),
22 combat: faker.number.int({ min: 1, max: 100 }),
23 negotiation: faker.number.int({ min: 1, max: 100 }),
24 leadership: faker.number.int({ min: 1, max: 100 })
25 };
26 
27 legionariusze.biography = faker.lorem.paragraphs(2);
28 
29 return legionariusze;
30});
31
32// factories/tribute.factory.ts
33import { define } from 'typeorm-seeding';
34import { Tribute } from '../src/tribute/tribute.entity';
35import { Faker } from '@faker-js/faker';
36
37define(Tribute, (faker: Faker) => {
38 const tribute = new Tribute();
39 
40 const tributeNames = [
41 'Golden Coin', 'Silver Chalice', 'Ruby Ring', 'Diamond Necklace',
42 'Ancient Map', 'Mystical Orb', 'Cursed Medallion', 'Crystal Eagle'
43 ];
44 
45 tribute.name = faker.helpers.arrayElement(tributeNames);
46 tribute.type = faker.helpers.arrayElement(['gold', 'silver', 'jewels', 'artifacts']);
47 tribute.value = faker.number.int({ min: 100, max: 10000 });
48 tribute.description = faker.lorem.sentence();
49 tribute.location = faker.location.city();
50 tribute.isCursed = faker.datatype.boolean(0.1); // 10% szans na klątwę
51 tribute.collectedAt = faker.date.past({ years: 2 });
52 
53 return tribute;
54});

Seeder używający Factory

1// seeds/factory.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder, SeederFactoryManager } from 'typeorm-extension';
4import { Legionary } from '../src/legionariusze/legionariusze.entity';
5import { Tribute } from '../src/tribute/tribute.entity';
6
7export default class FactorySeeder implements Seeder {
8 public async run(
9 dataSource: DataSource,
10 factoryManager: SeederFactoryManager
11 ): Promise<any> {
12 // Stwórz 50 losowych legionariuszów
13 const legionariuszeFactory = factoryManager.get(Legionary);
14 const legionariuszes = await legionariuszeFactory.saveMany(50);
15 
16 // Stwórz 200 losowych tributów
17 const tributeFactory = factoryManager.get(Tribute);
18 const tributes = await tributeFactory.saveMany(200);
19 
20 // Przypisz tributy do legionariuszów
21 for (const tribute of tributes) {
22 if (Math.random() < 0.8) { // 80% tributów ma właściciela
23 const randomLegionary = legionariuszes[Math.floor(Math.random() * legionariuszes.length)];
24 tribute.legionariusze = randomLegionary;
25 }
26 }
27 
28 await dataSource.getRepository(Tribute).save(tributes);
29 
30 console.log(`Created ${legionariuszes.length} legionariuszes and ${tributes.length} tributes using factories! `);
31 }
32}

Fixtures z plików JSON

1// fixtures/basic-skills.json
2[
3 {
4 "name": "Navigation",
5 "description": "Ability to navigate cohorts across dangerous waters",
6 "difficulty": "Intermediate"
7 },
8 {
9 "name": "Sword Fighting",
10 "description": "Master the art of swordsmanship",
11 "difficulty": "Advanced"
12 },
13 {
14 "name": "Tribute Hunting",
15 "description": "Finding hidden tributes using maps and clues",
16 "difficulty": "Expert"
17 },
18 {
19 "name": "Legion Repair",
20 "description": "Maintaining and repairing cohort components",
21 "difficulty": "Basic"
22 },
23 {
24 "name": "Ballista Operation",
25 "description": "Operating and maintaining cohort ballistae",
26 "difficulty": "Intermediate"
27 },
28 {
29 "name": "Rope Work",
30 "description": "Siege rope and equipment handling skills",
31 "difficulty": "Basic"
32 }
33]
1// seeds/json-fixture.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder, SeederFactoryManager } from 'typeorm-extension';
4import { Skill } from '../src/skill/skill.entity';
5import * as fs from 'fs';
6import * as path from 'path';
7
8export default class JsonFixtureSeeder implements Seeder {
9 public async run(
10 dataSource: DataSource,
11 factoryManager: SeederFactoryManager
12 ): Promise<any> {
13 const skillRepository = dataSource.getRepository(Skill);
14 
15 // Sprawdź czy umiejętności już istnieją
16 const existingSkills = await skillRepository.count();
17 if (existingSkills > 0) {
18 console.log('Skills already seeded, skipping...');
19 return;
20 }
21 
22 // Wczytaj dane z pliku JSON
23 const fixturePath = path.join(__dirname, '../fixtures/basic-skills.json');
24 const skillsData = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
25 
26 // Zapisz umiejętności
27 await skillRepository.save(skillsData);
28 
29 console.log(`Loaded ${skillsData.length} skills from JSON fixture! `);
30 }
31}

Konfiguracja i uruchamianie seedów

1// data-source.ts (aktualizacja)
2import { DataSource, DataSourceOptions } from 'typeorm';
3import { SeederOptions } from 'typeorm-extension';
4
5const options: DataSourceOptions & SeederOptions = {
6 type: 'postgres',
7 host: 'localhost',
8 port: 5432,
9 username: 'centurion',
10 password: 'tribute123',
11 database: 'legionariusze_cohort',
12 entities: ['src/**/*.entity{.ts,.js}'],
13 migrations: ['src/migrations/*{.ts,.js}'],
14 
15 // Konfiguracja seedów
16 seeds: ['src/seeds/*{.ts,.js}'],
17 factories: ['src/factories/*{.ts,.js}'],
18};
19
20export const AppDataSource = new DataSource(options);
1// package.json (dodanie skryptów)
2{
3 "scripts": {
4 "seed:run": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run --dataSource=./data-source.ts",
5 "seed:run:specific": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run --dataSource=./data-source.ts --seed",
6 "db:setup": "npm run migration:run && npm run seed:run",
7 "db:reset": "npm run migration:revert && npm run migration:run && npm run seed:run"
8 }
9}
1# Uruchamianie seedów
2npm run seed:run
3
4# Uruchomienie konkretnego seeda
5npm run seed:run:specific LegionarySeeder
6
7# Pełne resetowanie bazy z seedami
8npm run db:reset

Seedy dla różnych środowisk

1// seeds/development.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder, SeederFactoryManager } from 'typeorm-extension';
4
5export default class DevelopmentSeeder implements Seeder {
6 public async run(
7 dataSource: DataSource,
8 factoryManager: SeederFactoryManager
9 ): Promise<any> {
10 // Tylko w środowisku development
11 if (process.env.NODE_ENV !== 'development') {
12 console.log('Skipping development seeds - not in development environment');
13 return;
14 }
15 
16 // Stwórz dużo danych testowych
17 const legionariuszeFactory = factoryManager.get(Legionary);
18 const tributeFactory = factoryManager.get(Tribute);
19 
20 await legionariuszeFactory.saveMany(100);
21 await tributeFactory.saveMany(500);
22 
23 console.log('Development environment seeded with test data! 🧪');
24 }
25}
26
27// seeds/production.seeder.ts
28export default class ProductionSeeder implements Seeder {
29 public async run(
30 dataSource: DataSource,
31 factoryManager: SeederFactoryManager
32 ): Promise<any> {
33 // Tylko podstawowe dane dla produkcji
34 if (process.env.NODE_ENV !== 'production') {
35 return;
36 }
37 
38 // Tylko niezbędne dane systemowe
39 await this.seedBasicSkills(dataSource);
40 await this.seedSystemSettings(dataSource);
41 
42 console.log('Production environment seeded with essential data only! ');
43 }
44 
45 private async seedBasicSkills(dataSource: DataSource) {
46 // Podstawowe umiejętności potrzebne w systemie
47 const skillRepository = dataSource.getRepository(Skill);
48 // ... implementacja
49 }
50 
51 private async seedSystemSettings(dataSource: DataSource) {
52 // Podstawowe ustawienia systemu
53 // ... implementacja
54 }
55}

Conditional Seeding

1// seeds/conditional.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder, SeederFactoryManager } from 'typeorm-extension';
4
5export default class ConditionalSeeder implements Seeder {
6 public async run(
7 dataSource: DataSource,
8 factoryManager: SeederFactoryManager
9 ): Promise<any> {
10 // Sprawdź flagę środowiskową
11 const shouldSeedDemo = process.env.SEED_DEMO_DATA === 'true';
12 const shouldSeedTest = process.env.SEED_TEST_DATA === 'true';
13 const forceReseed = process.env.FORCE_RESEED === 'true';
14 
15 if (shouldSeedDemo) {
16 await this.seedDemoData(dataSource, factoryManager, forceReseed);
17 }
18 
19 if (shouldSeedTest) {
20 await this.seedTestData(dataSource, factoryManager, forceReseed);
21 }
22 
23 // Sprawdź czy to pierwsze uruchomienie
24 const legionariuszeCount = await dataSource.getRepository(Legionary).count();
25 if (legionariuszeCount === 0 || forceReseed) {
26 await this.seedInitialData(dataSource);
27 }
28 }
29 
30 private async seedDemoData(
31 dataSource: DataSource,
32 factoryManager: SeederFactoryManager,
33 force: boolean
34 ) {
35 console.log('Seeding demo data for presentations...');
36 // Utworz ładne, prezentowalne dane
37 }
38 
39 private async seedTestData(
40 dataSource: DataSource,
41 factoryManager: SeederFactoryManager,
42 force: boolean
43 ) {
44 console.log('Seeding test data for automated tests...');
45 // Utworz przewidywalne dane do testów
46 }
47 
48 private async seedInitialData(dataSource: DataSource) {
49 console.log('Seeding initial required data...');
50 // Dane wymagane do działania aplikacji
51 }
52}

Najlepsze praktyki Seedów

1. Idempotentność

1// DOBRZE - sprawdź czy dane już istnieją
2async run(dataSource: DataSource) {
3 const repository = dataSource.getRepository(Entity);
4 const existingCount = await repository.count();
5 
6 if (existingCount > 0) {
7 console.log('Data already exists, skipping...');
8 return;
9 }
10 
11 // Wykonaj seeding
12}
13
14// ŹLE - zawsze dodawaj nowe dane
15async run(dataSource: DataSource) {
16 // Zawsze tworzy duplikaty!
17 await repository.save(data);
18}

2. Hierarchia zależności

1// Określ kolejność seedów
2// 1. Podstawowe entity (Skills, Ranks)
3// 2. Główne entity (Legionarys, Centurions)
4// 3. Relacyjne entity (Tributes, Maps)
5// 4. Dane testowe/demo

3. Separacja środowisk

1// Różne dane dla różnych środowisk
2if (process.env.NODE_ENV === 'test') {
3 // Przewidywalne dane testowe
4} else if (process.env.NODE_ENV === 'development') {
5 // Dużo różnorodnych danych
6} else {
7 // Tylko podstawowe dane
8}

Podsumowanie

Gratulacje! Teraz jesteś mistrzowskim kwatermistrzem! Umiesz:

Tworzyć seedery dla różnych typów danych ✓ Używać Factory Pattern do generowania testów ✓ Ładować dane z plików JSON jako fixtures ✓ Konfigurować seedery dla różnych środowisk ✓ Implementować conditional seeding z flagami

Teraz Twoja kohorta zawsze będzie gotowa do wymarszu z pełnym wyposażeniem!

Vai a CodeWorlds