forked from syntaxbullet/AuroraBot-discord
feat(inventory): implement item name autocomplete with rarity and case-insensitive search
This commit is contained in:
@@ -4,9 +4,6 @@ import { inventoryService } from "@/modules/inventory/inventory.service";
|
|||||||
import { userService } from "@/modules/user/user.service";
|
import { userService } from "@/modules/user/user.service";
|
||||||
import { createErrorEmbed } from "@lib/embeds";
|
import { createErrorEmbed } from "@lib/embeds";
|
||||||
import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view";
|
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 type { ItemUsageData } from "@/lib/types";
|
||||||
import { UserError } from "@/lib/errors";
|
import { UserError } from "@/lib/errors";
|
||||||
import { config } from "@/lib/config";
|
import { config } from "@/lib/config";
|
||||||
@@ -75,28 +72,8 @@ export const use = createCommand({
|
|||||||
const focusedValue = interaction.options.getFocused();
|
const focusedValue = interaction.options.getFocused();
|
||||||
const userId = interaction.user.id;
|
const userId = interaction.user.id;
|
||||||
|
|
||||||
// Fetch owned items that match the search query
|
const results = await inventoryService.getAutocompleteItems(userId, focusedValue);
|
||||||
// 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 filtered = entries.filter(entry => {
|
await interaction.respond(results);
|
||||||
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 }))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const mockWhere = mock();
|
|||||||
const mockSelect = mock();
|
const mockSelect = mock();
|
||||||
const mockFrom = mock();
|
const mockFrom = mock();
|
||||||
const mockOnConflictDoUpdate = mock();
|
const mockOnConflictDoUpdate = mock();
|
||||||
|
const mockInnerJoin = mock();
|
||||||
|
const mockLimit = mock();
|
||||||
|
|
||||||
// Chain setup
|
// Chain setup
|
||||||
mockInsert.mockReturnValue({ values: mockValues });
|
mockInsert.mockReturnValue({ values: mockValues });
|
||||||
@@ -34,7 +36,10 @@ mockWhere.mockReturnValue({ returning: mockReturning });
|
|||||||
mockDelete.mockReturnValue({ where: mockWhere });
|
mockDelete.mockReturnValue({ where: mockWhere });
|
||||||
|
|
||||||
mockSelect.mockReturnValue({ from: mockFrom });
|
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 DrizzleClient
|
||||||
mock.module("@/lib/DrizzleClient", () => {
|
mock.module("@/lib/DrizzleClient", () => {
|
||||||
@@ -239,4 +244,39 @@ describe("inventoryService", () => {
|
|||||||
expect(mockDelete).toHaveBeenCalledWith(inventory); // Consume
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { inventory, items, users, userTimers } from "@/db/schema";
|
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 { DrizzleClient } from "@/lib/DrizzleClient";
|
||||||
import { economyService } from "@/modules/economy/economy.service";
|
import { economyService } from "@/modules/economy/economy.service";
|
||||||
import { levelingService } from "@/modules/leveling/leveling.service";
|
import { levelingService } from "@/modules/leveling/leveling.service";
|
||||||
@@ -181,5 +181,29 @@ export const inventoryService = {
|
|||||||
|
|
||||||
return { success: true, results, usageData, item };
|
return { success: true, results, usageData, item };
|
||||||
}, tx);
|
}, 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
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user