feat(items): add server field and improve item management

refactor(ai): enhance item structure generation with schema validation
feat(ai): add token usage tracking and cost calculation
fix(ui): improve item card number formatting with locale support
docs(items): add documentation for item value separation
test(items): add tests for value field validation
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-11 03:04:49 -03:00
1 parent dc169f9eba
commit d3704602fd
22 files changed
+1317 -95

No files matched your search

+52
View File
@@ -6,6 +6,7 @@
import { Prisma } from "@prisma/client";
import { prisma } from "../app";
import { ensureItemsSeeded } from "../../items/import-from-sqlite";
import { secureLogger } from "../../secure-logger";
export type ItemSort =
| "name"
@@ -193,6 +194,53 @@ function buildMetadata(
return merged;
}
// Campos relacionados a valores/preços que NÃO pertencem ao modelo Item.
// Qualquer presença deles em metadata deve ser rejeitada.
const FORBIDDEN_VALUE_KEYS = new Set<string>([
// Geral
"price",
"prices",
"valor",
"value",
"sell_value",
"sellPrice",
"buy",
"buy_price",
// Chaves usadas em Listings
"price_pix",
"price_tibia_coins",
"price_gold_coins",
"initial_price_pix",
"initial_price_tibia_coins",
"initial_price_gold_coins",
"current_bid_amount",
]);
/**
* Verifica recursivamente se há campos proibidos relacionados a valores/preços.
* Lança erro se encontrar.
*/
export function assertNoValueFieldsInMetadata(meta: Record<string, any> | undefined | null, context?: string): void {
if (!meta || typeof meta !== "object") return;
const stack: Array<{ obj: Record<string, any>; path: string }> = [{ obj: meta as Record<string, any>, path: "metadata" }];
while (stack.length) {
const { obj, path } = stack.pop()!;
for (const key of Object.keys(obj)) {
const nextPath = `${path}.${key}`;
if (FORBIDDEN_VALUE_KEYS.has(key)) {
const msg = `Itens não possuem campos de valor/preço no cadastro. Campo proibido: ${nextPath}. Use listings para preços.`;
secureLogger.warn(msg, { context });
throw new Error(msg);
}
const val = obj[key];
if (val && typeof val === "object" && !Array.isArray(val)) {
stack.push({ obj: val as Record<string, any>, path: nextPath });
}
}
}
}
export async function getItemById(itemId: number): Promise<Item | null> {
const item = await prisma.item.findUnique({ where: { item_id: itemId } });
if (!item) {
@@ -322,6 +370,8 @@ export async function searchItems(params: SearchItemsParams = {}): Promise<{ ite
export async function createAdminItem(input: ItemUpsertInput): Promise<ItemWithMeta> {
const now = Date.now();
// Validação: impedir qualquer campo de valor em metadata (preço etc.)
assertNoValueFieldsInMetadata(input.metadata, "createAdminItem");
const data: Prisma.ItemCreateInput = {
source_table_id: input.source_table_id ?? now,
source_table_name: input.source_table_name ?? "custom",
@@ -353,6 +403,8 @@ export async function updateAdminItem(itemId: number, input: ItemUpsertInput): P
}
const existingMeta = parseMetadata(existing.metadata);
// Validação: impedir qualquer campo de valor em metadata (preço etc.)
assertNoValueFieldsInMetadata(input.metadata, "updateAdminItem");
const metadata = buildMetadata(existingMeta, {
servers: input.servers ?? extractServers(existingMeta),
tags: input.tags ?? extractTags(existingMeta),