We use cookies to enhance your experience on the site
CodeWorlds

Repository Pattern - treasury organization

Experienced administrators! Consul Caesar.js noticed that our legion is getting better and better at managing data relations. Now it's time to learn the Repository Pattern - best practices for organizing access to our Roman treasury!

What is the Repository Pattern?

The Repository Pattern is like hiring the best administrator in the application - a person who knows exactly where every tribute lies, how to safely retrieve it, and where to place it. Instead of directly rummaging through the treasury (database), all legion members ask the administrator for what they need.

Advantages of the Repository Pattern:

  • Centralized access logic - all tribute-related code in one place
  • Easier testing - you can simulate the administrator without the real treasury
  • Code readability - clear business methods instead of raw SQL queries
  • Database changeability - the administrator can switch to a different type of treasury

Basic Repository in TypeORM

1// tribute.repository.ts
2import { Repository, EntityRepository } from 'typeorm';
3import { Tribute } from './tribute.entity';
4
5@EntityRepository(Tribute)
6export class TributeRepository extends Repository<Tribute> {
7 // Find tributes by type
8 async findByType(type: string): Promise<Tribute[]> {
9 return this.find({
10 where: { type },
11 order: { collectedAt: 'DESC' }
12 });
13 }
14
15 // Find most valuable tributes
16 async findMostValuable(limit: number = 10): Promise<Tribute[]> {
17 return this.find({
18 order: { value: 'DESC' },
19 take: limit
20 });
21 }
22
23 // Find tributes at a specific island
24 async findByLocation(location: string): Promise<Tribute[]> {
25 return this.createQueryBuilder('tribute')
26 .where('tribute.location ILIKE :location', { location: `%${location}%` })
27 .orderBy('tribute.value', 'DESC')
28 .getMany();
29 }
30
31 // Calculate total tribute value
32 async getTotalValue(): Promise<number> {
33 const result = await this.createQueryBuilder('tribute')
34 .select('SUM(tribute.value)', 'total')
35 .getRawOne();
36
37 return parseFloat(result.total) || 0;
38 }
39
40 // Find tributes in a specific value range
41 async findByValueRange(minValue: number, maxValue: number): Promise<Tribute[]> {
42 return this.createQueryBuilder('tribute')
43 .where('tribute.value BETWEEN :minValue AND :maxValue', {
44 minValue,
45 maxValue
46 })
47 .orderBy('tribute.value', 'ASC')
48 .getMany();
49 }
50}

Custom Repository for Legionaries

1// legionary.repository.ts
2import { Repository, EntityRepository, SelectQueryBuilder } from 'typeorm';
3import { Legionary } from './legionary.entity';
4import { Centurion } from './centurion.entity';
5
6@EntityRepository(Legionary)
7export class LegionaryRepository extends Repository<Legionary> {
8 // Find legionaries by rank
9 async findByRank(rank: string): Promise<Legionary[]> {
10 return this.find({
11 where: { rank },
12 relations: ['centurion', 'tributeMap'],
13 order: { tributeCount: 'DESC' }
14 });
15 }
16
17 // Find legionaries with a specific skill
18 async findBySkill(skillName: string, minLevel: number = 1): Promise<Legionary[]> {
19 return this.createQueryBuilder('legionary')
20 .leftJoinAndSelect('legionary.legionarySkills', 'legionarySkill')
21 .leftJoinAndSelect('legionarySkill.skill', 'skill')
22 .leftJoinAndSelect('legionary.centurion', 'centurion')
23 .where('skill.name = :skillName', { skillName })
24 .andWhere('legionarySkill.level >= :minLevel', { minLevel })
25 .orderBy('legionarySkill.level', 'DESC')
26 .getMany();
27 }
28
29 // Find active legionaries without a centurion
30 async findActiveWithoutCenturion(): Promise<Legionary[]> {
31 return this.find({
32 where: {
33 isActive: true,
34 centurion: null
35 },
36 order: { tributeCount: 'DESC' }
37 });
38 }
39
40 // Advanced search
41 async searchLegionarys(searchTerm: string): Promise<Legionary[]> {
42 return this.createQueryBuilder('legionary')
43 .leftJoinAndSelect('legionary.centurion', 'centurion')
44 .where('legionary.name ILIKE :search', { search: `%${searchTerm}%` })
45 .orWhere('legionary.cohort ILIKE :search', { search: `%${searchTerm}%` })
46 .orWhere('centurion.name ILIKE :search', { search: `%${searchTerm}%` })
47 .orderBy('legionary.name', 'ASC')
48 .getMany();
49 }
50
51 // Legionary statistics
52 async getLegionaryStatistics() {
53 const stats = await this.createQueryBuilder('legionary')
54 .select([
55 'COUNT(*) as totalLegionaries',
56 'AVG(legionary.tributeCount) as avgTributes',
57 'SUM(legionary.totalTributeValue) as totalValue',
58 'MAX(legionary.tributeCount) as maxTributes',
59 'COUNT(CASE WHEN legionary.isActive = true THEN 1 END) as activeLegionaries'
60 ])
61 .getRawOne();
62
63 return {
64 totalLegionaries: parseInt(stats.totallegionarys),
65 avgTributes: parseFloat(stats.avgtributes) || 0,
66 totalValue: parseFloat(stats.totalvalue) || 0,
67 maxTributes: parseInt(stats.maxtributes) || 0,
68 activeLegionaries: parseInt(stats.activelegionarys)
69 };
70 }
71
72 // Find legionaries not seen for a specified time
73 async findInactive(daysAgo: number): Promise<Legionary[]> {
74 const cutoffDate = new Date();
75 cutoffDate.setDate(cutoffDate.getDate() - daysAgo);
76
77 return this.createQueryBuilder('legionary')
78 .where('legionary.lastSeenAt < :cutoffDate', { cutoffDate })
79 .orWhere('legionary.lastSeenAt IS NULL')
80 .orderBy('legionary.lastSeenAt', 'ASC')
81 .getMany();
82 }
83}

Repository for Centurions

1// centurion.repository.ts
2import { Repository, EntityRepository } from 'typeorm';
3import { Centurion } from './centurion.entity';
4
5@EntityRepository(Centurion)
6export class CenturionRepository extends Repository<Centurion> {
7 // Find centurions with the largest legion
8 async findByLegionSize(minLegionSize: number = 5): Promise<Centurion[]> {
9 return this.createQueryBuilder('centurion')
10 .leftJoinAndSelect('centurion.cohort', 'legionary')
11 .having('COUNT(legionary.id) >= :minLegionSize', { minLegionSize })
12 .groupBy('centurion.id')
13 .orderBy('COUNT(legionary.id)', 'DESC')
14 .getMany();
15 }
16
17 // Centurion ranking by tribute value
18 async getRankingByTributeValue(): Promise<any[]> {
19 return this.createQueryBuilder('centurion')
20 .leftJoin('centurion.cohort', 'legionary')
21 .select([
22 'centurion.id as centurionId',
23 'centurion.name as centurionName',
24 'centurion.cohortName as cohortName',
25 'COUNT(legionary.id) as cohortSize',
26 'SUM(legionary.totalTributeValue) as totalLegionValue',
27 'centurion.totalTributeValue as centurionValue',
28 '(centurion.totalTributeValue + COALESCE(SUM(legionary.totalTributeValue), 0)) as combinedValue'
29 ])
30 .groupBy('centurion.id')
31 .orderBy('combinedValue', 'DESC')
32 .getRawMany();
33 }
34
35 // Find centurions with experience in a specified range
36 asyncfindByExperience(minYears: number, maxYears: number): Promise<Centurion[]> {
37 return this.find({
38 where: {
39 yearsOfExperience: MoreThanOrEqual(minYears) && LessThanOrEqual(maxYears)
40 },
41 relations: ['cohort'],
42 order: { yearsOfExperience: 'DESC' }
43 });
44 }
45
46 // Find centurions without a legion
47 async findWithoutLegion(): Promise<Centurion[]> {
48 return this.createQueryBuilder('centurion')
49 .leftJoin('centurion.cohort', 'legionary')
50 .where('legionary.id IS NULL')
51 .orderBy('centurion.yearsOfExperience', 'DESC')
52 .getMany();
53 }
54}

Abstract Base Repository

1// base.repository.ts
2import { Repository, FindOptionsWhere, FindManyOptions } from 'typeorm';
3
4export abstract class BaseRepository<T> extends Repository<T> {
5 // Standard methods for all repositories
6
7 async findByIds(ids: number[]): Promise<T[]> {
8 if (ids.length === 0) return [];
9
10 return this.createQueryBuilder('entity')
11 .where('entity.id IN (:...ids)', { ids })
12 .getMany();
13 }
14
15 async existsById(id: number): Promise<boolean> {
16 const count = await this.count({ where: { id } as FindOptionsWhere<T> });
17 return count > 0;
18 }
19
20 async findWithPagination(
21 page: number = 1,
22 limit: number = 10,
23 options?: FindManyOptions<T>
24 ) {
25 const [items, total] = await this.findAndCount({
26 ...options,
27 skip: (page - 1) * limit,
28 take: limit
29 });
30
31 return {
32 items,
33 pagination: {
34 page,
35 limit,
36 total,
37 totalPages: Math.ceil(total / limit),
38 hasNext: page * limit < total,
39 hasPrev: page > 1
40 }
41 };
42 }
43
44 async batchInsert(entities: Partial<T>[], batchSize: number = 100): Promise<T[]> {
45 const results: T[] = [];
46
47 for (let i = 0; i < entities.length; i += batchSize) {
48 const batch = entities.slice(i, i + batchSize);
49 const inserted = await this.save(batch as any);
50 results.push(...inserted);
51 }
52
53 return results;
54 }
55}

Repository with custom business logic

1// advanced-tribute.repository.ts
2import { BaseRepository } from './base.repository';
3import { Tribute } from './tribute.entity';
4import { EntityRepository } from 'typeorm';
5
6@EntityRepository(Tribute)
7export class AdvancedTributeRepository extends BaseRepository<Tribute> {
8 // Business logic: find tributes suitable for sale
9 async findSellableTributes(minValue: number = 100): Promise<Tribute[]> {
10 return this.createQueryBuilder('tribute')
11 .where('tribute.value >= :minValue', { minValue })
12 .andWhere('tribute.isCursed = false') // We don't sell cursed ones!
13 .andWhere('tribute.location != :forbiddenLocation', {
14 forbiddenLocation: 'Tartarus'
15 })
16 .orderBy('tribute.value', 'DESC')
17 .getMany();
18 }
19
20 // Tribute market analysis
21 async getMarketAnalysis() {
22 const analysis = await this.createQueryBuilder('tribute')
23 .select([
24 'tribute.type',
25 'COUNT(*) as count',
26 'AVG(tribute.value) as avgValue',
27 'MIN(tribute.value) as minValue',
28 'MAX(tribute.value) as maxValue',
29 'SUM(tribute.value) as totalValue'
30 ])
31 .where('tribute.isCursed = false')
32 .groupBy('tribute.type')
33 .orderBy('totalValue', 'DESC')
34 .getRawMany();
35
36 return analysis.map(item => ({
37 type: item.tribute_type,
38 count: parseInt(item.count),
39 avgValue: parseFloat(item.avgvalue),
40 minValue: parseFloat(item.minvalue),
41 maxValue: parseFloat(item.maxvalue),
42 totalValue: parseFloat(item.totalvalue)
43 }));
44 }
45
46 // Find tributes by expert criteria
47 async findByExpertCriteria(criteria: {
48 minValue?: number;
49 maxValue?: number;
50 types?: string[];
51 locations?: string[];
52 excludeCursed?: boolean;
53 discoveredAfter?: Date;
54 }): Promise<Tribute[]> {
55 let query = this.createQueryBuilder('tribute');
56
57 if (criteria.minValue) {
58 query = query.andWhere('tribute.value >= :minValue', { minValue: criteria.minValue });
59 }
60
61 if (criteria.maxValue) {
62 query = query.andWhere('tribute.value <= :maxValue', { maxValue: criteria.maxValue });
63 }
64
65 if (criteria.types && criteria.types.length > 0) {
66 query = query.andWhere('tribute.type IN (:...types)', { types: criteria.types });
67 }
68
69 if (criteria.locations && criteria.locations.length > 0) {
70 query = query.andWhere('tribute.location IN (:...locations)', { locations: criteria.locations });
71 }
72
73 if (criteria.excludeCursed) {
74 query = query.andWhere('tribute.isCursed = false');
75 }
76
77 if (criteria.discoveredAfter) {
78 query = query.andWhere('tribute.collectedAt >= :discoveredAfter', {
79 discoveredAfter: criteria.discoveredAfter
80 });
81 }
82
83 return query
84 .orderBy('tribute.value', 'DESC')
85 .getMany();
86 }
87
88 // Tribute recommendations for a specific legionary
89 async getRecommendationsForLegionary(legionaryId: number): Promise<Tribute[]> {
90 // Complex business logic
91 return this.createQueryBuilder('tribute')
92 .leftJoin('tribute.legionary', 'currentOwner')
93 .leftJoin('legionarys', 'legionary', 'legionary.id = :legionaryId', { legionaryId })
94 .where('currentOwner.id IS NULL') // Tributes without an owner
95 .andWhere('tribute.isCursed = false')
96 .andWhere(
97 '(tribute.value BETWEEN legionary.totalTributeValue * 0.1 AND legionary.totalTributeValue * 2)'
98 )
99 .orderBy('tribute.value', 'DESC')
100 .limit(5)
101 .getMany();
102 }
103}

Repository configuration in Module

1// tribute.module.ts
2import { Module } from '@nestjs/common';
3import { TypeOrmModule } from '@nestjs/typeorm';
4import { TributeRepository } from './tribute.repository';
5import { AdvancedTributeRepository } from './advanced-tribute.repository';
6import { TributeService } from './tribute.service';
7import { TributeController } from './tribute.controller';
8import { Tribute } from './tribute.entity';
9
10@Module({
11 imports: [TypeOrmModule.forFeature([Tribute, TributeRepository, AdvancedTributeRepository])],
12 controllers: [TributeController],
13 providers: [TributeService],
14 exports: [TributeService, TributeRepository]
15})
16export class TributeModule {}

Using Repository in Service

1// tribute.service.ts
2import { Injectable } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { TributeRepository } from './tribute.repository';
5import { AdvancedTributeRepository } from './advanced-tribute.repository';
6import { CreateTributeDto } from './dto/create-tribute.dto';
7
8@Injectable()
9export class TributeService {
10 constructor(
11 @InjectRepository(TributeRepository)
12 private tributeRepository: TributeRepository,
13 @InjectRepository(AdvancedTributeRepository)
14 private advancedTributeRepository: AdvancedTributeRepository,
15 ) {}
16
17 async findTributesByType(type: string) {
18 return this.tributeRepository.findByType(type);
19 }
20
21 async getMostValuable(limit: number = 10) {
22 return this.tributeRepository.findMostValuable(limit);
23 }
24
25 async getTributeMarketAnalysis() {
26 return this.advancedTributeRepository.getMarketAnalysis();
27 }
28
29 async searchTributes(criteria: any) {
30 return this.advancedTributeRepository.findByExpertCriteria(criteria);
31 }
32
33 async getRecommendations(legionaryId: number) {
34 return this.advancedTributeRepository.getRecommendationsForLegionary(legionaryId);
35 }
36
37 async createTribute(createTributeDto: CreateTributeDto) {
38 const tribute = this.tributeRepository.create(createTributeDto);
39 return this.tributeRepository.save(tribute);
40 }
41
42 async getTotalTributeValue(): Promise<number> {
43 return this.tributeRepository.getTotalValue();
44 }
45}

Testing Repository

1// tribute.repository.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { getRepositoryToken } from '@nestjs/typeorm';
4import { TributeRepository } from './tribute.repository';
5import { Tribute } from './tribute.entity';
6
7describe('TributeRepository', () => {
8 let repository: TributeRepository;
9
10 const mockTributeRepository = {
11 find: jest.fn(),
12 findOne: jest.fn(),
13 save: jest.fn(),
14 create: jest.fn(),
15 createQueryBuilder: jest.fn(() => ({
16 where: jest.fn().mockReturnThis(),
17 orderBy: jest.fn().mockReturnThis(),
18 getMany: jest.fn(),
19 getRawOne: jest.fn()
20 }))
21 };
22
23 beforeEach(async () => {
24 const module: TestingModule = await Test.createTestingModule({
25 providers: [
26 {
27 provide: getRepositoryToken(TributeRepository),
28 useValue: mockTributeRepository,
29 },
30 ],
31 }).compile();
32
33 repository = module.get<TributeRepository>(getRepositoryToken(TributeRepository));
34 });
35
36 describe('findByType', () => {
37 it('should find tributes by type', async () => {
38 const mockTributes = [
39 { id: 1, name: 'Golden Coin', type: 'gold', value: 100 },
40 { id: 2, name: 'Gold Bar', type: 'gold', value: 500 }
41 ];
42
43 mockTributeRepository.find.mockResolvedValue(mockTributes);
44
45 const result = await repository.findByType('gold');
46
47 expect(mockTributeRepository.find).toHaveBeenCalledWith({
48 where: { type: 'gold' },
49 order: { collectedAt: 'DESC' }
50 });
51 expect(result).toEqual(mockTributes);
52 });
53 });
54
55 describe('getTotalValue', () => {
56 it('should calculate total tribute value', async () => {
57 const mockQueryBuilder = {
58 select: jest.fn().mockReturnThis(),
59 getRawOne: jest.fn().mockResolvedValue({ total: '1500.50' })
60 };
61
62 mockTributeRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
63
64 const result = await repository.getTotalValue();
65
66 expect(result).toBe(1500.50);
67 });
68 });
69});

Repository Pattern best practices

1. Readable method names

1// GOOD
2findActiveBySkillLevel(skillName: string, minLevel: number)
3getTributeStatisticsByType(type: string)
4findLegionarysWithoutCenturion()
5
6// BAD
7findLegionarys(criteria: any)
8getData(params: any)
9query(sql: string)

2. Return appropriate types

1// GOOD
2async findById(id: number): Promise<Tribute | null>
3async getTotalCount(): Promise<number>
4async findWithPagination(): Promise<PaginatedResult<Tribute>>
5
6// BAD
7async findById(id: number): Promise<any>
8async getStats(): Promise<any>

3. Error handling

1async findTributeById(id: number): Promise<Tribute> {
2 if (id <= 0) {
3 throw new BadRequestException('ID must be positive');
4 }
5
6 const tribute = await this.findOne({ where: { id } });
7
8 if (!tribute) {
9 throw new NotFoundException(`Tribute with ID ${id} not found`);
10 }
11
12 return tribute;
13}

Summary

Congratulations! Now you're a master of treasury organization! You can:

Create custom repositories with business logic ✓ Use advanced queries in repositories ✓ Implement abstract bases for shared functionality ✓ Test repositories with mocks ✓ Organize code according to best practices

The Repository Pattern is the key to maintaining order in data access - like the best administrator in the application!

Go to CodeWorlds