Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions backend/src/vaults/vault.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Vault } from './entities/vault.entity';

@Injectable()
export class VaultRepository {
constructor(
@InjectRepository(Vault)
private readonly repository: Repository<Vault>,
) {}

async findAll(): Promise<Vault[]> {
return this.repository.find({
order: { createdAt: 'DESC' },
});
}

async findById(id: string): Promise<Vault | null> {
return this.repository.findOne({ where: { id } });
}

async save(vault: Vault): Promise<Vault> {
return this.repository.save(vault);
}

async findLeaderboard(): Promise<Vault[]> {
return this.repository.find({
order: { tvlAtHighWatermark: 'DESC' },
});
}
}
3 changes: 2 additions & 1 deletion backend/src/vaults/vaults.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Vault } from './entities/vault.entity';
import { VaultsService } from './vaults.service';
import { VaultsController } from './vaults.controller';
import { VaultRepository } from './vault.repository';

@Module({
imports: [TypeOrmModule.forFeature([Vault])],
controllers: [VaultsController],
providers: [VaultsService],
providers: [VaultsService, VaultRepository],
exports: [VaultsService],
})
export class VaultsModule {}
37 changes: 19 additions & 18 deletions backend/src/vaults/vaults.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { NotFoundException } from '@nestjs/common';
import { VaultsService } from './vaults.service';
import { Vault } from './entities/vault.entity';
import { VaultRepository } from './vault.repository';

// ---------------------------------------------------------------------------
// Mock repository factory
Expand All @@ -22,9 +22,10 @@ const mockVault = (overrides: Partial<Vault> = {}): Vault => ({
});

const mockRepository = () => ({
find: jest.fn(),
findOne: jest.fn(),
findAll: jest.fn(),
findById: jest.fn(),
save: jest.fn(),
findLeaderboard: jest.fn(),
});

// ---------------------------------------------------------------------------
Expand All @@ -39,12 +40,12 @@ describe('VaultsService', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
VaultsService,
{ provide: getRepositoryToken(Vault), useFactory: mockRepository },
{ provide: VaultRepository, useFactory: mockRepository },
],
}).compile();

service = module.get<VaultsService>(VaultsService);
repo = module.get(getRepositoryToken(Vault));
repo = module.get(VaultRepository);
});

afterEach(() => jest.clearAllMocks());
Expand All @@ -54,7 +55,7 @@ describe('VaultsService', () => {
describe('findAll()', () => {
it('returns all vaults as response DTOs', async () => {
const vaults = [mockVault(), mockVault({ id: 'vault-uuid-2', name: 'Vault 2' })];
repo.find.mockResolvedValue(vaults);
repo.findAll.mockResolvedValue(vaults);

const result = await service.findAll();

Expand All @@ -69,7 +70,7 @@ describe('VaultsService', () => {
describe('findOne()', () => {
it('returns a single vault with watermark fields', async () => {
const vault = mockVault();
repo.findOne.mockResolvedValue(vault);
repo.findById.mockResolvedValue(vault);

const result = await service.findOne('vault-uuid-1');

Expand All @@ -79,7 +80,7 @@ describe('VaultsService', () => {
});

it('throws NotFoundException when vault does not exist', async () => {
repo.findOne.mockResolvedValue(null);
repo.findById.mockResolvedValue(null);

await expect(service.findOne('non-existent')).rejects.toThrow(NotFoundException);
});
Expand All @@ -89,14 +90,14 @@ describe('VaultsService', () => {

describe('deposit()', () => {
it('throws NotFoundException when vault does not exist', async () => {
repo.findOne.mockResolvedValue(null);
repo.findById.mockResolvedValue(null);

await expect(service.deposit('non-existent', '100')).rejects.toThrow(NotFoundException);
});

it('updates totalAssets after deposit', async () => {
const vault = mockVault({ totalAssets: '1000.000000000000000000', tvlAtHighWatermark: '1000.000000000000000000' });
repo.findOne.mockResolvedValue(vault);
repo.findById.mockResolvedValue(vault);
repo.save.mockImplementation(async (v) => v);

const result = await service.deposit('vault-uuid-1', '500');
Expand All @@ -109,7 +110,7 @@ describe('VaultsService', () => {
totalAssets: '1000.000000000000000000',
tvlAtHighWatermark: '1000.000000000000000000',
});
repo.findOne.mockResolvedValue(vault);
repo.findById.mockResolvedValue(vault);
repo.save.mockImplementation(async (v) => v);

const before = new Date();
Expand All @@ -128,7 +129,7 @@ describe('VaultsService', () => {
tvlAtHighWatermark: '2000.000000000000000000',
watermarkAchievedAt: new Date('2024-01-01T00:00:00.000Z'),
});
repo.findOne.mockResolvedValue(vault);
repo.findById.mockResolvedValue(vault);
repo.save.mockImplementation(async (v) => v);

const result = await service.deposit('vault-uuid-1', '100');
Expand All @@ -144,7 +145,7 @@ describe('VaultsService', () => {
tvlAtHighWatermark: '1000.000000000000000000',
watermarkAchievedAt: new Date('2024-01-01T00:00:00.000Z'),
});
repo.findOne.mockResolvedValue(vault);
repo.findById.mockResolvedValue(vault);
repo.save.mockImplementation(async (v) => v);

const result = await service.deposit('vault-uuid-1', '100');
Expand All @@ -160,7 +161,7 @@ describe('VaultsService', () => {
watermarkAchievedAt: null,
});

repo.findOne.mockImplementation(async () => currentVault);
repo.findById.mockImplementation(async () => currentVault);
repo.save.mockImplementation(async (v) => {
currentVault = { ...v };
return currentVault;
Expand All @@ -186,7 +187,7 @@ describe('VaultsService', () => {
tvlAtHighWatermark: '0.000000000000000000',
watermarkAchievedAt: null,
});
repo.findOne.mockResolvedValue(vault);
repo.findById.mockResolvedValue(vault);
repo.save.mockImplementation(async (v) => v);

const result = await service.deposit('vault-uuid-1', '250');
Expand All @@ -205,7 +206,7 @@ describe('VaultsService', () => {
mockVault({ id: '2', name: 'Mid Vault', tvlAtHighWatermark: '2000.000000000000000000' }),
mockVault({ id: '3', name: 'Low Vault', tvlAtHighWatermark: '500.000000000000000000' }),
];
repo.find.mockResolvedValue(vaults);
repo.findLeaderboard.mockResolvedValue(vaults);

const result = await service.getLeaderboard();

Expand All @@ -217,7 +218,7 @@ describe('VaultsService', () => {
});

it('each entry includes rank, id, name, tvlAtHighWatermark, watermarkAchievedAt, totalAssets', async () => {
repo.find.mockResolvedValue([mockVault()]);
repo.findLeaderboard.mockResolvedValue([mockVault()]);

const result = await service.getLeaderboard();

Expand All @@ -231,7 +232,7 @@ describe('VaultsService', () => {
});

it('returns empty array when no vaults exist', async () => {
repo.find.mockResolvedValue([]);
repo.findLeaderboard.mockResolvedValue([]);

const result = await service.getLeaderboard();

Expand Down
18 changes: 6 additions & 12 deletions backend/src/vaults/vaults.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,23 @@ import {
NotFoundException,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Vault } from './entities/vault.entity';
import { VaultLeaderboardEntryDto, VaultResponseDto } from './dto/vault-response.dto';
import { VaultRepository } from './vault.repository';

@Injectable()
export class VaultsService {
private readonly logger = new Logger(VaultsService.name);

constructor(
@InjectRepository(Vault)
private readonly vaultRepository: Repository<Vault>,
private readonly vaultRepository: VaultRepository,
) {}

/**
* Retrieve all vaults including their TVL watermark fields.
*/
async findAll(): Promise<VaultResponseDto[]> {
const vaults = await this.vaultRepository.find({
order: { createdAt: 'DESC' },
});
const vaults = await this.vaultRepository.findAll();
return vaults.map(this.toResponseDto);
}

Expand All @@ -33,7 +29,7 @@ export class VaultsService {
* @throws NotFoundException if the vault does not exist
*/
async findOne(id: string): Promise<VaultResponseDto> {
const vault = await this.vaultRepository.findOne({ where: { id } });
const vault = await this.vaultRepository.findById(id);
if (!vault) {
throw new NotFoundException(`Vault with id ${id} not found`);
}
Expand All @@ -56,7 +52,7 @@ export class VaultsService {
* @throws NotFoundException if the vault does not exist
*/
async deposit(vaultId: string, amount: string): Promise<VaultResponseDto> {
const vault = await this.vaultRepository.findOne({ where: { id: vaultId } });
const vault = await this.vaultRepository.findById(vaultId);
if (!vault) {
throw new NotFoundException(`Vault with id ${vaultId} not found`);
}
Expand Down Expand Up @@ -93,9 +89,7 @@ export class VaultsService {
* @returns Ranked list of vaults with watermark data
*/
async getLeaderboard(): Promise<VaultLeaderboardEntryDto[]> {
const vaults = await this.vaultRepository.find({
order: { tvlAtHighWatermark: 'DESC' },
});
const vaults = await this.vaultRepository.findLeaderboard();

return vaults.map((vault, index) => ({
rank: index + 1,
Expand Down