Salve once again, experienced Roman programmer! Now we will delve into the heart of our Empire - we will learn about services and providers. They do the real work, while controllers merely pass on orders!
Services are classes containing the business logic of the application. They are like experienced specialists in a Roman legion - each has their field and knows how to carry out the work entrusted to them.
1import { Injectable, NotFoundException } from '@nestjs/common';
2
3@Injectable()
4export class LegionService {
5 private legionaries: Legionary[] = [
6 { id: 1, name: 'Marcus Aurelius', rank: 'Centurion', experience: 10 },
7 { id: 2, name: 'Julia Domna', rank: 'Engineer', experience: 8 },
8 { id: 3, name: 'Titus Flavius', rank: 'Ballista-Operator', experience: 6 },
9 ];
10
11 // Find all legionaries
12 findAll(): Legionary[] {
13 return this.legionaries;
14 }
15
16 // Find a legionary by ID
17 findById(id: number): Legionary {
18 const legionary = this.legionaries.find(l => l.id === id);
19 if (!legionary) {
20 throw new NotFoundException(\`Legionary with ID \${id} not found in legion!\`);
21 }
22 return legionary;
23 }
24
25 // Recruit a new legionary
26 recruit(legionaryData: CreateLegionaryDto): Legionary {
27 const newLegionary = {
28 id: this.getNextId(),
29 ...legionaryData,
30 joinDate: new Date(),
31 };
32
33 this.legionaries.push(newLegionary);
34 return newLegionary;
35 }
36
37 // Promote a legionary
38 promote(id: number, newRank: string): Legionary {
39 const legionary = this.findById(id);
40 legionary.rank = newRank;
41 legionary.experience += 1;
42 return legionary;
43 }
44
45 // Remove a legionary from the legion
46 dismiss(id: number): boolean {
47 const index = this.legionaries.findIndex(l => l.id === id);
48 if (index === -1) {
49 throw new NotFoundException(\`Cannot dismiss - legionary with ID \${id} not found!\`);
50 }
51
52 this.legionaries.splice(index, 1);
53 return true;
54 }
55
56 private getNextId(): number {
57 return Math.max(...this.legionaries.map(l => l.id), 0) + 1;
58 }
59}Services often need help from other specialists. Here's how we organize collaboration:
1@Injectable()
2export class TributesService {
3 constructor(
4 private readonly legionService: LegionService,
5 private readonly mapService: MapService,
6 private readonly weatherService: WeatherService,
7 ) {}
8
9 async organizeTributeCollection(provinceId: string): Promise<TributeCollectionResult> {
10 // Check legion availability
11 const availableLegionaries = this.legionService.getAvailableLegionaries();
12 if (availableLegionaries.length < 3) {
13 throw new BadRequestException('Need at least 3 legionaries for tribute collection!');
14 }
15
16 // Get province map
17 const provinceMap = await this.mapService.getProvinceLocation(provinceId);
18
19 // Check weather
20 const weather = await this.weatherService.getWeatherReport(provinceMap.coordinates);
21 if (weather.condition === 'storm') {
22 throw new BadRequestException('Too dangerous! Wait for better weather.');
23 }
24
25 // Organize tribute collection
26 const campaign = {
27 tribute: provinceMap,
28 legion: this.selectBestLegion(availableLegionaries, provinceMap.difficulty),
29 estimatedDuration: this.calculateDuration(provinceMap),
30 startTime: new Date(),
31 };
32
33 return this.launchCampaign(campaign);
34 }
35
36 private selectBestLegion(legionaries: Legionary[], difficulty: number): Legionary[] {
37 return legionaries
38 .filter(l => l.experience >= difficulty - 2)
39 .sort((a, b) => b.experience - a.experience)
40 .slice(0, Math.min(5, legionaries.length));
41 }
42}1@Injectable()
2export class LegionaryRepository {
3 constructor(@Inject('DATABASE_CONNECTION') private db: Database) {}
4
5 async findAll(): Promise<Legionary[]> {
6 return this.db.query('SELECT * FROM legionaries ORDER BY experience DESC');
7 }
8
9 async findByRank(rank: string): Promise<Legionary[]> {
10 return this.db.query('SELECT * FROM legionaries WHERE rank = ?', [rank]);
11 }
12
13 async save(legionary: Legionary): Promise<Legionary> {
14 if (legionary.id) {
15 await this.db.query(
16 'UPDATE legionaries SET name = ?, rank = ?, experience = ? WHERE id = ?',
17 [legionary.name, legionary.rank, legionary.experience, legionary.id]
18 );
19 } else {
20 const result = await this.db.query(
21 'INSERT INTO legionaries (name, rank, experience) VALUES (?, ?, ?)',
22 [legionary.name, legionary.rank, legionary.experience]
23 );
24 legionary.id = result.insertId;
25 }
26 return legionary;
27 }
28
29 async delete(id: number): Promise<boolean> {
30 const result = await this.db.query('DELETE FROM legionaries WHERE id = ?', [id]);
31 return result.affectedRows > 0;
32 }
33}
34
35// Service using the repository
36@Injectable()
37export class LegionManagementService {
38 constructor(private readonly legionaryRepository: LegionaryRepository) {}
39
40 async getAllLegionaries(): Promise<Legionary[]> {
41 return this.legionaryRepository.findAll();
42 }
43
44 async getCenturions(): Promise<Legionary[]> {
45 return this.legionaryRepository.findByRank('Centurion');
46 }
47
48 async promoteLegionary(id: number, newRank: string): Promise<Legionary> {
49 const legionary = await this.legionaryRepository.findById(id);
50 legionary.rank = newRank;
51 legionary.experience += 1;
52 return this.legionaryRepository.save(legionary);
53 }
54}1// Interface for different weapon types
2interface Weapon {
3 name: string;
4 damage: number;
5 range: number;
6 reload(): void;
7 fire(): AttackResult;
8}
9
10// Implementations of different weapons
11class Ballista implements Weapon {
12 constructor(
13 public name: string,
14 public damage: number,
15 public range: number,
16 private ammunitionCount: number,
17 ) {}
18
19 reload(): void {
20 console.log(\`Reloading \${this.name}...\`);
21 this.ammunitionCount = Math.min(this.ammunitionCount + 1, 10);
22 }
23
24 fire(): AttackResult {
25 if (this.ammunitionCount === 0) {
26 return { hit: false, message: 'No ammunition!' };
27 }
28
29 this.ammunitionCount--;
30 const hit = Math.random() > 0.3; // 70% chance to hit
31
32 return {
33 hit,
34 damage: hit ? this.damage : 0,
35 message: hit ? 'Direct hit!' : 'Missed target!',
36 };
37 }
38}
39
40class Pilum implements Weapon {
41 constructor(
42 public name: string,
43 public damage: number,
44 public range: number,
45 private loaded: boolean = false,
46 ) {}
47
48 reload(): void {
49 this.loaded = true;
50 }
51
52 fire(): AttackResult {
53 if (!this.loaded) {
54 return { hit: false, message: 'Pilum not loaded!' };
55 }
56
57 this.loaded = false;
58 const hit = Math.random() > 0.2; // 80% chance to hit (more accurate)
59
60 return {
61 hit,
62 damage: hit ? this.damage : 0,
63 message: hit ? 'Precise shot!' : 'Shot went wide!',
64 };
65 }
66}
67
68// Factory service
69@Injectable()
70export class ArmoryFactory {
71 private weaponConfigs = {
72 ballista: { damage: 50, range: 500 },
73 pilum: { damage: 25, range: 200 },
74 scorpio: { damage: 35, range: 300 },
75 };
76
77 createWeapon(type: string, name: string): Weapon {
78 const config = this.weaponConfigs[type];
79 if (!config) {
80 throw new Error(\`Unknown weapon type: \${type}\`);
81 }
82
83 switch (type) {
84 case 'ballista':
85 return new Ballista(name, config.damage, config.range, 5);
86 case 'pilum':
87 return new Pilum(name, config.damage, config.range);
88 case 'scorpio':
89 return new Ballista(name, config.damage, config.range, 8); // Different ammo count
90 default:
91 throw new Error(\`Cannot create weapon of type: \${type}\`);
92 }
93 }
94
95 createWeaponSet(unitType: string): Weapon[] {
96 const weaponSets = {
97 legion: [
98 { type: 'ballista', name: 'Left Ballista 1' },
99 { type: 'ballista', name: 'Left Ballista 2' },
100 { type: 'ballista', name: 'Right Ballista 1' },
101 { type: 'ballista', name: 'Right Ballista 2' },
102 { type: 'scorpio', name: 'Front Scorpio' },
103 ],
104 cohort: [
105 { type: 'ballista', name: 'Main Ballista' },
106 { type: 'pilum', name: 'Legionary Pilum 1' },
107 { type: 'pilum', name: 'Legionary Pilum 2' },
108 ],
109 };
110
111 const weaponList = weaponSets[unitType] || weaponSets.cohort;
112 return weaponList.map(w => this.createWeapon(w.type, w.name));
113 }
114}1// Legion configuration
2const LEGION_CONFIG = {
3 name: 'Legio X Equestris',
4 maxSpeed: 15,
5 legionaryCapacity: 50,
6 supplyCapacity: 1000,
7 ballistae: 32,
8};
9
10@Module({
11 providers: [
12 {
13 provide: 'LEGION_CONFIG',
14 useValue: LEGION_CONFIG,
15 },
16 LegionService,
17 ],
18})
19export class LegionModule {}
20
21// Usage in the service
22@Injectable()
23export class LegionService {
24 constructor(@Inject('LEGION_CONFIG') private legionConfig: any) {}
25
26 getLegionSpecs() {
27 return {
28 ...this.legionConfig,
29 currentStatus: this.getCurrentStatus(),
30 };
31 }
32}1const databaseConnectionFactory = {
2 provide: 'DATABASE_CONNECTION',
3 useFactory: async (configService: ConfigService): Promise<Database> => {
4 const config = {
5 host: configService.get('DB_HOST'),
6 port: configService.get('DB_PORT'),
7 username: configService.get('DB_USERNAME'),
8 password: configService.get('DB_PASSWORD'),
9 database: configService.get('DB_NAME'),
10 };
11
12 const connection = new Database(config);
13 await connection.connect();
14
15 // Initialize tables if they don't exist
16 await connection.query(\`
17 CREATE TABLE IF NOT EXISTS legionaries (
18 id INT AUTO_INCREMENT PRIMARY KEY,
19 name VARCHAR(255) NOT NULL,
20 rank VARCHAR(100) NOT NULL,
21 experience INT DEFAULT 1
22 )
23 \`);
24
25 return connection;
26 },
27 inject: [ConfigService],
28};
29
30@Module({
31 providers: [databaseConnectionFactory],
32 exports: ['DATABASE_CONNECTION'],
33})
34export class DatabaseModule {}1// Interface for the logging system
2interface Logger {
3 log(message: string): void;
4 error(message: string): void;
5 warn(message: string): void;
6}
7
8// Implementation for development
9@Injectable()
10class ConsoleLogger implements Logger {
11 log(message: string): void {
12 console.log(\` [LOG]: \${message}\`);
13 }
14
15 error(message: string): void {
16 console.error(\`[ERROR]: \${message}\`);
17 }
18
19 warn(message: string): void {
20 console.warn(\` [WARN]: \${message}\`);
21 }
22}
23
24// Implementation for production
25@Injectable()
26class FileLogger implements Logger {
27 log(message: string): void {
28 this.writeToFile('log', message);
29 }
30
31 error(message: string): void {
32 this.writeToFile('error', message);
33 }
34
35 warn(message: string): void {
36 this.writeToFile('warn', message);
37 }
38
39 private writeToFile(level: string, message: string): void {
40 // File writing implementation
41 }
42}
43
44// Dynamic implementation selection
45@Module({
46 providers: [
47 {
48 provide: 'Logger',
49 useClass: process.env.NODE_ENV === 'production' ? FileLogger : ConsoleLogger,
50 },
51 ],
52})
53export class LoggingModule {}1@Injectable()
2export class CampaignService {
3 async planCampaign(destination: string): Promise<CampaignPlan> {
4 // Parallel information retrieval
5 const [weatherData, mapData, suppliesNeeded] = await Promise.all([
6 this.weatherService.getForecast(destination),
7 this.mapService.getRouteInfo(destination),
8 this.suppliesService.calculateNeeds(destination),
9 ]);
10
11 // Risk analysis
12 const riskAssessment = await this.analyzeRisks({
13 weather: weatherData,
14 route: mapData,
15 supplies: suppliesNeeded,
16 });
17
18 return {
19 destination,
20 estimatedDuration: mapData.travelTime,
21 weatherForecast: weatherData,
22 requiredSupplies: suppliesNeeded,
23 riskLevel: riskAssessment.level,
24 recommendations: riskAssessment.recommendations,
25 };
26 }
27
28 async executeCampaign(plan: CampaignPlan): Promise<CampaignResult> {
29 try {
30 // Preparation
31 await this.prepareLegion(plan.requiredSupplies);
32 await this.briefLegionaries(plan);
33
34 // Campaign execution (simulation)
35 const progress = await this.simulateMarch(plan);
36
37 return {
38 success: true,
39 tributesCollected: progress.discoveries,
40 legionStatus: progress.legionCondition,
41 timeElapsed: progress.duration,
42 };
43
44 } catch (error) {
45 return {
46 success: false,
47 error: error.message,
48 partialResults: error.partialData,
49 };
50 }
51 }
52
53 private async simulateMarch(plan: CampaignPlan): Promise<MarchProgress> {
54 const stages = ['departure', 'march', 'conquest', 'return'];
55 const progress = { discoveries: [], legionCondition: 'good', duration: 0 };
56
57 for (const stage of stages) {
58 await this.delay(1000); // Time simulation
59 const stageResult = await this.executeStage(stage, plan);
60
61 progress.discoveries.push(...stageResult.discoveries);
62 progress.duration += stageResult.timeSpent;
63
64 if (stageResult.legionCondition !== 'good') {
65 progress.legionCondition = stageResult.legionCondition;
66 }
67 }
68
69 return progress;
70 }
71
72 private delay(ms: number): Promise<void> {
73 return new Promise(resolve => setTimeout(resolve, ms));
74 }
75}Services are the true strength of our Roman Empire - they do all the work while controllers only issue orders. Well-designed services are the foundation of a scalable and maintainable application!
In the next module we will learn about Guards and Interceptors - the security and monitoring systems of our forum!