feat(api): add inventory API (GET /api/inventory)
This commit is contained in:
@@ -5,6 +5,7 @@ import { DatabaseModule } from './database/database.module';
|
|||||||
import { EquipmentModule } from './equipment/equipment.module';
|
import { EquipmentModule } from './equipment/equipment.module';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
import { HuntingModule } from './hunting/hunting.module';
|
import { HuntingModule } from './hunting/hunting.module';
|
||||||
|
import { InventoryModule } from './inventory/inventory.module';
|
||||||
import { TravelModule } from './travel/travel.module';
|
import { TravelModule } from './travel/travel.module';
|
||||||
import { WorldModule } from './world/world.module';
|
import { WorldModule } from './world/world.module';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ import { WorldModule } from './world/world.module';
|
|||||||
HuntingModule,
|
HuntingModule,
|
||||||
CombatModule,
|
CombatModule,
|
||||||
EquipmentModule,
|
EquipmentModule,
|
||||||
|
InventoryModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
13
apps/api/src/inventory/inventory.controller.ts
Normal file
13
apps/api/src/inventory/inventory.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/api/src/inventory/inventory.module.ts
Normal file
13
apps/api/src/inventory/inventory.module.ts
Normal 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 {}
|
||||||
97
apps/api/src/inventory/inventory.service.spec.ts
Normal file
97
apps/api/src/inventory/inventory.service.spec.ts
Normal 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: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
71
apps/api/src/inventory/inventory.service.ts
Normal file
71
apps/api/src/inventory/inventory.service.ts
Normal 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,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user