feat(api): add inventory API (GET /api/inventory)

This commit is contained in:
Bastian Wagner
2026-08-20 16:50:50 +02:00
parent 9df740e7d4
commit 820c69704f
5 changed files with 196 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ import { DatabaseModule } from './database/database.module';
import { EquipmentModule } from './equipment/equipment.module';
import { HealthModule } from './health/health.module';
import { HuntingModule } from './hunting/hunting.module';
import { InventoryModule } from './inventory/inventory.module';
import { TravelModule } from './travel/travel.module';
import { WorldModule } from './world/world.module';
@@ -18,6 +19,7 @@ import { WorldModule } from './world/world.module';
HuntingModule,
CombatModule,
EquipmentModule,
InventoryModule,
],
})
export class AppModule {}

View File

@@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
import { InventoryResponseDto, InventoryService } from './inventory.service';
@Controller('inventory')
export class InventoryController {
constructor(private readonly inventoryService: InventoryService) {}
@Get()
getInventory(): Promise<InventoryResponseDto> {
return this.inventoryService.getInventory(DEMO_CHARACTER_ID);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CharacterEquipment } from '../equipment/entities/character-equipment.entity';
import { CharacterItem } from '../items/entities/character-item.entity';
import { InventoryController } from './inventory.controller';
import { InventoryService } from './inventory.service';
@Module({
imports: [TypeOrmModule.forFeature([CharacterItem, CharacterEquipment])],
controllers: [InventoryController],
providers: [InventoryService],
})
export class InventoryModule {}

View File

@@ -0,0 +1,97 @@
import { Repository } from 'typeorm';
import { CharacterEquipment } from '../equipment/entities/character-equipment.entity';
import { CharacterItem } from '../items/entities/character-item.entity';
import { EquipmentSlot } from '../items/equipment-slot.enum';
import { ItemRarity } from '../items/item-rarity.enum';
import { ItemType } from '../items/item-type.enum';
import { InventoryService } from './inventory.service';
const CHARACTER_ID = 'character-1';
function characterItem(overrides: Partial<CharacterItem> = {}): CharacterItem {
return {
id: 'item-1',
characterId: CHARACTER_ID,
itemDefinitionId: 'def-1',
quantity: 1,
createdAt: new Date('2026-08-18T09:00:00.000Z'),
updatedAt: new Date('2026-08-18T09:00:00.000Z'),
itemDefinition: {
key: 'worn-short-sword',
name: 'Abgenutztes Kurzschwert',
rarity: ItemRarity.COMMON,
type: ItemType.WEAPON,
equipmentSlot: EquipmentSlot.WEAPON,
requiredLevel: 1,
weaponDamage: 8,
bonusAttack: 0,
bonusHp: 0,
bonusArmor: 0,
iconPath: '/images/items/worn-short-sword.png',
},
...overrides,
} as CharacterItem;
}
describe('InventoryService', () => {
it('returns only the current character\'s items with definition data, quantity, and equipped state', async () => {
const items = [
characterItem({ id: 'item-1', quantity: 1 }),
characterItem({ id: 'item-2', quantity: 3, itemDefinitionId: 'def-2' }),
];
const characterItems = {
find: jest.fn().mockResolvedValue(items),
} as unknown as Repository<CharacterItem>;
const equipment = {
find: jest.fn().mockResolvedValue([
{ characterItemId: 'item-1', slot: EquipmentSlot.WEAPON } as CharacterEquipment,
]),
} as unknown as Repository<CharacterEquipment>;
const service = new InventoryService(characterItems, equipment);
const result = await service.getInventory(CHARACTER_ID);
expect(characterItems.find).toHaveBeenCalledWith({
where: { characterId: CHARACTER_ID },
relations: { itemDefinition: true },
order: { createdAt: 'ASC' },
});
expect(result.items).toEqual([
{
id: 'item-1',
quantity: 1,
equipped: true,
item: {
key: 'worn-short-sword',
name: 'Abgenutztes Kurzschwert',
rarity: 'COMMON',
equipmentSlot: 'WEAPON',
requiredLevel: 1,
weaponDamage: 8,
bonusAttack: 0,
bonusHp: 0,
bonusArmor: 0,
iconPath: '/images/items/worn-short-sword.png',
},
},
{
id: 'item-2',
quantity: 3,
equipped: false,
item: expect.objectContaining({ key: 'worn-short-sword' }),
},
]);
});
it('returns an empty list when the character owns nothing', async () => {
const characterItems = {
find: jest.fn().mockResolvedValue([]),
} as unknown as Repository<CharacterItem>;
const equipment = {
find: jest.fn().mockResolvedValue([]),
} as unknown as Repository<CharacterEquipment>;
const service = new InventoryService(characterItems, equipment);
await expect(service.getInventory(CHARACTER_ID)).resolves.toEqual({ items: [] });
});
});

View File

@@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CharacterEquipment } from '../equipment/entities/character-equipment.entity';
import { CharacterItem } from '../items/entities/character-item.entity';
import { EquipmentSlot } from '../items/equipment-slot.enum';
import { ItemRarity } from '../items/item-rarity.enum';
export interface InventoryItemDto {
id: string;
quantity: number;
equipped: boolean;
item: {
key: string;
name: string;
rarity: ItemRarity;
equipmentSlot: EquipmentSlot | null;
requiredLevel: number;
weaponDamage: number;
bonusAttack: number;
bonusHp: number;
bonusArmor: number;
iconPath: string;
};
}
export interface InventoryResponseDto {
items: InventoryItemDto[];
}
@Injectable()
export class InventoryService {
constructor(
@InjectRepository(CharacterItem)
private readonly characterItems: Repository<CharacterItem>,
@InjectRepository(CharacterEquipment)
private readonly equipment: Repository<CharacterEquipment>,
) {}
async getInventory(characterId: string): Promise<InventoryResponseDto> {
const [items, equipped] = await Promise.all([
this.characterItems.find({
where: { characterId },
relations: { itemDefinition: true },
order: { createdAt: 'ASC' },
}),
this.equipment.find({ where: { characterId } }),
]);
const equippedIds = new Set(equipped.map((row) => row.characterItemId));
return {
items: items.map((characterItem) => ({
id: characterItem.id,
quantity: characterItem.quantity,
equipped: equippedIds.has(characterItem.id),
item: {
key: characterItem.itemDefinition.key,
name: characterItem.itemDefinition.name,
rarity: characterItem.itemDefinition.rarity,
equipmentSlot: characterItem.itemDefinition.equipmentSlot,
requiredLevel: characterItem.itemDefinition.requiredLevel,
weaponDamage: characterItem.itemDefinition.weaponDamage,
bonusAttack: characterItem.itemDefinition.bonusAttack,
bonusHp: characterItem.itemDefinition.bonusHp,
bonusArmor: characterItem.itemDefinition.bonusArmor,
iconPath: characterItem.itemDefinition.iconPath,
},
})),
};
}
}