feat: implement comprehensive item management system with admin UI, API, and asset handling utilities.
All checks were successful
Deploy to Production / test (push) Successful in 44s
All checks were successful
Deploy to Production / test (push) Successful in 44s
This commit is contained in:
215
shared/modules/items/items.service.ts
Normal file
215
shared/modules/items/items.service.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Items Service
|
||||
* Handles CRUD operations for game items.
|
||||
* Used by both bot commands and web dashboard.
|
||||
*/
|
||||
|
||||
import { items } from "@db/schema";
|
||||
import { eq, ilike, and, or, count, sql } from "drizzle-orm";
|
||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||
import { withTransaction } from "@/lib/db";
|
||||
import type { Transaction, ItemUsageData } from "@shared/lib/types";
|
||||
import type { ItemType } from "@shared/lib/constants";
|
||||
|
||||
// --- DTOs ---
|
||||
|
||||
export interface CreateItemDTO {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
rarity?: 'Common' | 'Uncommon' | 'Rare' | 'Epic' | 'Legendary';
|
||||
type: 'MATERIAL' | 'CONSUMABLE' | 'EQUIPMENT' | 'QUEST';
|
||||
price?: bigint | null;
|
||||
iconUrl: string;
|
||||
imageUrl: string;
|
||||
usageData?: ItemUsageData | null;
|
||||
}
|
||||
|
||||
export interface UpdateItemDTO {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
rarity?: 'Common' | 'Uncommon' | 'Rare' | 'Epic' | 'Legendary';
|
||||
type?: 'MATERIAL' | 'CONSUMABLE' | 'EQUIPMENT' | 'QUEST';
|
||||
price?: bigint | null;
|
||||
iconUrl?: string;
|
||||
imageUrl?: string;
|
||||
usageData?: ItemUsageData | null;
|
||||
}
|
||||
|
||||
export interface ItemFilters {
|
||||
search?: string;
|
||||
type?: string;
|
||||
rarity?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// --- Service ---
|
||||
|
||||
export const itemsService = {
|
||||
/**
|
||||
* Get all items with optional filtering and pagination.
|
||||
*/
|
||||
async getAllItems(filters: ItemFilters = {}) {
|
||||
const { search, type, rarity, limit = 100, offset = 0 } = filters;
|
||||
|
||||
// Build conditions array
|
||||
const conditions = [];
|
||||
|
||||
if (search) {
|
||||
conditions.push(
|
||||
or(
|
||||
ilike(items.name, `%${search}%`),
|
||||
ilike(items.description, `%${search}%`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
conditions.push(eq(items.type, type));
|
||||
}
|
||||
|
||||
if (rarity) {
|
||||
conditions.push(eq(items.rarity, rarity));
|
||||
}
|
||||
|
||||
// Execute query with conditions
|
||||
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const [itemsList, totalResult] = await Promise.all([
|
||||
DrizzleClient
|
||||
.select()
|
||||
.from(items)
|
||||
.where(whereClause)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.orderBy(items.id),
|
||||
DrizzleClient
|
||||
.select({ count: count() })
|
||||
.from(items)
|
||||
.where(whereClause)
|
||||
]);
|
||||
|
||||
return {
|
||||
items: itemsList,
|
||||
total: totalResult[0]?.count ?? 0
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single item by ID.
|
||||
*/
|
||||
async getItemById(id: number) {
|
||||
return await DrizzleClient.query.items.findFirst({
|
||||
where: eq(items.id, id)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get item by name (for uniqueness checks).
|
||||
*/
|
||||
async getItemByName(name: string) {
|
||||
return await DrizzleClient.query.items.findFirst({
|
||||
where: eq(items.name, name)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new item.
|
||||
*/
|
||||
async createItem(data: CreateItemDTO, tx?: Transaction) {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const [item] = await txFn.insert(items)
|
||||
.values({
|
||||
name: data.name,
|
||||
description: data.description ?? null,
|
||||
rarity: data.rarity ?? 'Common',
|
||||
type: data.type,
|
||||
price: data.price ?? null,
|
||||
iconUrl: data.iconUrl,
|
||||
imageUrl: data.imageUrl,
|
||||
usageData: data.usageData ?? {},
|
||||
})
|
||||
.returning();
|
||||
|
||||
return item;
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing item.
|
||||
*/
|
||||
async updateItem(id: number, data: UpdateItemDTO, tx?: Transaction) {
|
||||
return await withTransaction(async (txFn) => {
|
||||
// Build update object dynamically to support partial updates
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.rarity !== undefined) updateData.rarity = data.rarity;
|
||||
if (data.type !== undefined) updateData.type = data.type;
|
||||
if (data.price !== undefined) updateData.price = data.price;
|
||||
if (data.iconUrl !== undefined) updateData.iconUrl = data.iconUrl;
|
||||
if (data.imageUrl !== undefined) updateData.imageUrl = data.imageUrl;
|
||||
if (data.usageData !== undefined) updateData.usageData = data.usageData;
|
||||
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
// Nothing to update, just return the existing item
|
||||
return await txFn.query.items.findFirst({
|
||||
where: eq(items.id, id)
|
||||
});
|
||||
}
|
||||
|
||||
const [updatedItem] = await txFn
|
||||
.update(items)
|
||||
.set(updateData)
|
||||
.where(eq(items.id, id))
|
||||
.returning();
|
||||
|
||||
return updatedItem;
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an item by ID.
|
||||
*/
|
||||
async deleteItem(id: number, tx?: Transaction) {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const [deletedItem] = await txFn
|
||||
.delete(items)
|
||||
.where(eq(items.id, id))
|
||||
.returning();
|
||||
|
||||
return deletedItem;
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an item name is already taken (for validation).
|
||||
* Optionally exclude a specific ID (for updates).
|
||||
*/
|
||||
async isNameTaken(name: string, excludeId?: number) {
|
||||
const existing = await DrizzleClient.query.items.findFirst({
|
||||
where: excludeId
|
||||
? and(eq(items.name, name), sql`${items.id} != ${excludeId}`)
|
||||
: eq(items.name, name)
|
||||
});
|
||||
|
||||
return !!existing;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get items for autocomplete (search by name, limited results).
|
||||
*/
|
||||
async getItemsAutocomplete(query: string, limit: number = 25) {
|
||||
return await DrizzleClient
|
||||
.select({
|
||||
id: items.id,
|
||||
name: items.name,
|
||||
rarity: items.rarity,
|
||||
iconUrl: items.iconUrl,
|
||||
})
|
||||
.from(items)
|
||||
.where(ilike(items.name, `%${query}%`))
|
||||
.limit(limit);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user