diff --git a/src/commands/inventory/use.ts b/src/commands/inventory/use.ts index 59f3d80..2ee1160 100644 --- a/src/commands/inventory/use.ts +++ b/src/commands/inventory/use.ts @@ -4,9 +4,6 @@ import { inventoryService } from "@/modules/inventory/inventory.service"; import { userService } from "@/modules/user/user.service"; import { createErrorEmbed } from "@lib/embeds"; import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view"; -import { inventory, items } from "@/db/schema"; -import { eq, and, like } from "drizzle-orm"; -import { DrizzleClient } from "@/lib/DrizzleClient"; import type { ItemUsageData } from "@/lib/types"; import { UserError } from "@/lib/errors"; import { config } from "@/lib/config"; @@ -75,28 +72,8 @@ export const use = createCommand({ const focusedValue = interaction.options.getFocused(); const userId = interaction.user.id; - // Fetch owned items that match the search query - // We join with items table to filter by name directly in the database - const entries = await DrizzleClient.select({ - quantity: inventory.quantity, - item: items - }) - .from(inventory) - .innerJoin(items, eq(inventory.itemId, items.id)) - .where(and( - eq(inventory.userId, BigInt(userId)), - like(items.name, `%${focusedValue}%`) - )) - .limit(20); // Fetch up to 20 matching items + const results = await inventoryService.getAutocompleteItems(userId, focusedValue); - const filtered = entries.filter(entry => { - const usageData = entry.item.usageData as ItemUsageData | null; - const isUsable = usageData && usageData.effects && usageData.effects.length > 0; - return isUsable; - }); - - await interaction.respond( - filtered.map(entry => ({ name: `${entry.item.name} (${entry.quantity})`, value: entry.item.id })) - ); + await interaction.respond(results); } }); diff --git a/src/modules/inventory/inventory.service.test.ts b/src/modules/inventory/inventory.service.test.ts index e42e8d7..a60d8b3 100644 --- a/src/modules/inventory/inventory.service.test.ts +++ b/src/modules/inventory/inventory.service.test.ts @@ -18,6 +18,8 @@ const mockWhere = mock(); const mockSelect = mock(); const mockFrom = mock(); const mockOnConflictDoUpdate = mock(); +const mockInnerJoin = mock(); +const mockLimit = mock(); // Chain setup mockInsert.mockReturnValue({ values: mockValues }); @@ -34,7 +36,10 @@ mockWhere.mockReturnValue({ returning: mockReturning }); mockDelete.mockReturnValue({ where: mockWhere }); mockSelect.mockReturnValue({ from: mockFrom }); -mockFrom.mockReturnValue({ where: mockWhere }); +mockFrom.mockReturnValue({ where: mockWhere, innerJoin: mockInnerJoin }); +mockInnerJoin.mockReturnValue({ where: mockWhere }); +mockWhere.mockReturnValue({ returning: mockReturning, limit: mockLimit }); +mockLimit.mockResolvedValue([]); // Mock DrizzleClient mock.module("@/lib/DrizzleClient", () => { @@ -239,4 +244,39 @@ describe("inventoryService", () => { expect(mockDelete).toHaveBeenCalledWith(inventory); // Consume }); }); + + describe("getAutocompleteItems", () => { + it("should return formatted autocomplete results with rarity", async () => { + const mockItems = [ + { + item: { id: 1, name: "Common Sword", rarity: "Common", usageData: { effects: [{}] } }, + quantity: 5n + }, + { + item: { id: 2, name: "Epic Shield", rarity: "Epic", usageData: { effects: [{}] } }, + quantity: 1n + } + ]; + + mockLimit.mockResolvedValue(mockItems); + + // Restore mocks that might have been polluted by other tests + mockFrom.mockReturnValue({ where: mockWhere, innerJoin: mockInnerJoin }); + mockWhere.mockReturnValue({ returning: mockReturning, limit: mockLimit }); + + const result = await inventoryService.getAutocompleteItems("1", "Sw"); + + expect(mockSelect).toHaveBeenCalled(); + expect(mockFrom).toHaveBeenCalledWith(inventory); + expect(mockInnerJoin).toHaveBeenCalled(); // checks join + expect(mockWhere).toHaveBeenCalled(); // checks filters + expect(mockLimit).toHaveBeenCalledWith(20); + + expect(result).toHaveLength(2); + expect(result[0].name).toBe("Common Sword (5) [Common]"); + expect(result[0].value).toBe(1); + expect(result[1].name).toBe("Epic Shield (1) [Epic]"); + expect(result[1].value).toBe(2); + }); + }); }); diff --git a/src/modules/inventory/inventory.service.ts b/src/modules/inventory/inventory.service.ts index 5964c58..3eb6e46 100644 --- a/src/modules/inventory/inventory.service.ts +++ b/src/modules/inventory/inventory.service.ts @@ -1,5 +1,5 @@ import { inventory, items, users, userTimers } from "@/db/schema"; -import { eq, and, sql, count } from "drizzle-orm"; +import { eq, and, sql, count, ilike } from "drizzle-orm"; import { DrizzleClient } from "@/lib/DrizzleClient"; import { economyService } from "@/modules/economy/economy.service"; import { levelingService } from "@/modules/leveling/leveling.service"; @@ -181,5 +181,29 @@ export const inventoryService = { return { success: true, results, usageData, item }; }, tx); + }, + + getAutocompleteItems: async (userId: string, query: string) => { + const entries = await DrizzleClient.select({ + quantity: inventory.quantity, + item: items + }) + .from(inventory) + .innerJoin(items, eq(inventory.itemId, items.id)) + .where(and( + eq(inventory.userId, BigInt(userId)), + ilike(items.name, `%${query}%`) + )) + .limit(20); + + const filtered = entries.filter(entry => { + const usageData = entry.item.usageData as ItemUsageData | null; + return usageData && usageData.effects && usageData.effects.length > 0; + }); + + return filtered.map(entry => ({ + name: `${entry.item.name} (${entry.quantity}) [${entry.item.rarity || 'Common'}]`, + value: entry.item.id + })); } };