Files
pagodera/lib/prisma/queries/items.ts
T
Evandro Fonseca Junior 62ab5b2608 feat(admin): centralize subscriptions under /admin?tab=subscriptions
Replaced the dedicated /admin/subscriptions page with a redirect to the central admin panel’s subscriptions tab. Removed session checks, imports, and the AdminSubscriptionDashboard since routing and access are now handled centrally. This streamlines admin UX and keeps access control unified.
2025-11-09 01:50:50 -03:00

380 lines
10 KiB
TypeScript

/**
* Queries de itens usando Prisma
* Busca itens da tabela unificada items (PostgreSQL)
*/
import { Prisma } from "@prisma/client";
import { prisma } from "../app";
import { ensureItemsSeeded } from "../../items/import-from-sqlite";
export type ItemSort =
| "name"
| "level-asc"
| "level-desc"
| "weight-asc"
| "weight-desc"
| "category"
| "updated-desc";
export interface Item {
item_id: number;
source_table_id: number;
source_table_name: string;
item_name: string;
category: string;
image_local_path: string | null;
image_url: string | null;
rarity: string | null;
level: number | null;
weight: number | null;
slots: number | null;
skill: string | null;
metadata: Record<string, any>;
created_at: Date | string;
updated_at: Date | string;
}
export interface ItemWithMeta extends Item {
servers: string[];
tags: string[];
description?: string | null;
}
export interface SearchItemsParams {
q?: string;
category?: string;
rarity?: string;
server?: string;
sort?: ItemSort;
page?: number;
pageSize?: number;
}
export interface AdminSearchParams extends SearchItemsParams {
tags?: string[];
}
export interface AdminItemsResult {
items: ItemWithMeta[];
total: number;
page: number;
pageSize: number;
servers: string[];
categories: string[];
}
export interface ItemUpsertInput {
item_name: string;
category: string;
rarity?: string | null;
image_url?: string | null;
image_local_path?: string | null;
level?: number | null;
weight?: number | null;
slots?: number | null;
skill?: string | null;
servers?: string[];
tags?: string[];
description?: string | null;
source_table_name?: string;
source_table_id?: number;
metadata?: Record<string, any>;
}
function buildOrder(sort: ItemSort): Prisma.ItemOrderByWithRelationInput[] {
switch (sort) {
case "level-asc":
return [{ level: "asc" }, { item_name: "asc" }];
case "level-desc":
return [{ level: "desc" }, { item_name: "asc" }];
case "weight-asc":
return [{ weight: "asc" }, { item_name: "asc" }];
case "weight-desc":
return [{ weight: "desc" }, { item_name: "asc" }];
case "category":
return [{ category: "asc" }, { item_name: "asc" }];
case "name":
return [{ item_name: "asc" }];
case "updated-desc":
default:
return [{ updated_at: "desc" }];
}
}
function parseMetadata(metadata: Prisma.JsonValue | null | undefined): Record<string, any> {
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
return {};
}
return metadata as Record<string, any>;
}
function extractServers(meta: Record<string, any>): string[] {
const servers = meta?.servers;
if (Array.isArray(servers)) {
return servers
.map((server) => (typeof server === "string" ? server.trim() : ""))
.filter(Boolean);
}
if (typeof servers === "string") {
return servers
.split(",")
.map((server) => server.trim())
.filter(Boolean);
}
return [];
}
function extractTags(meta: Record<string, any>): string[] {
const tags = meta?.tags;
if (Array.isArray(tags)) {
return tags
.map((tag) => (typeof tag === "string" ? tag.trim() : ""))
.filter(Boolean);
}
if (typeof tags === "string") {
return tags
.split(",")
.map((tag) => tag.trim())
.filter(Boolean);
}
return [];
}
function buildItem(raw: any): Item {
const createdAt = raw.created_at instanceof Date ? raw.created_at.toISOString() : raw.created_at;
const updatedAt = raw.updated_at instanceof Date ? raw.updated_at.toISOString() : raw.updated_at;
return {
...raw,
weight: raw.weight ? Number(raw.weight) : null,
metadata: parseMetadata(raw.metadata),
created_at: createdAt,
updated_at: updatedAt,
} as Item;
}
function buildItemWithMeta(raw: any): ItemWithMeta {
const base = buildItem(raw);
const servers = extractServers(base.metadata);
const tags = extractTags(base.metadata);
const description = typeof base.metadata.description === "string" ? base.metadata.description : null;
return {
...base,
servers,
tags,
description,
};
}
function toDecimal(value: number | null | undefined): Prisma.Decimal | null {
if (typeof value !== "number" || Number.isNaN(value)) {
return null;
}
return new Prisma.Decimal(value);
}
function buildMetadata(
existing: Record<string, any>,
overrides: { servers?: string[]; tags?: string[]; description?: string | null; metadata?: Record<string, any> }
): Record<string, any> {
const merged = { ...existing, ...(overrides.metadata || {}) };
if (overrides.servers) {
merged.servers = overrides.servers;
}
if (overrides.tags) {
merged.tags = overrides.tags;
}
if (overrides.description !== undefined) {
merged.description = overrides.description;
}
return merged;
}
export async function getItemById(itemId: number): Promise<Item | null> {
const item = await prisma.item.findUnique({ where: { item_id: itemId } });
if (!item) {
return null;
}
return buildItem(item);
}
export async function getItemByName(itemName: string): Promise<Item | null> {
const item = await prisma.item.findFirst({
where: {
item_name: {
equals: itemName,
mode: "insensitive",
},
},
});
if (!item) {
return null;
}
return buildItem(item);
}
export async function getItemBySource(sourceTableName: string, sourceTableId: number): Promise<Item | null> {
const item = await prisma.item.findUnique({
where: {
source_table_name_source_table_id: {
source_table_name: sourceTableName,
source_table_id: sourceTableId,
},
},
});
if (!item) {
return null;
}
return buildItem(item);
}
export async function searchAdminItems(params: AdminSearchParams = {}): Promise<AdminItemsResult> {
const {
q = "",
category,
rarity,
server,
page = 1,
pageSize = 50,
} = params;
await ensureItemsSeeded();
const safePage = Math.max(1, page);
const safePageSize = Math.min(Math.max(1, pageSize), 200);
const where: Prisma.ItemWhereInput = {};
if (q) {
where.item_name = {
contains: q,
mode: "insensitive",
};
}
if (category && category !== "all") {
where.category = category;
}
if (rarity && rarity !== "all") {
where.rarity = rarity;
}
if (server && server !== "all") {
const existingAnd = Array.isArray(where.AND)
? [...where.AND]
: where.AND
? [where.AND]
: [];
existingAnd.push({
metadata: {
path: ["servers"],
array_contains: [server],
},
});
where.AND = existingAnd;
}
const [items, total, categoryRows] = await Promise.all([
prisma.item.findMany({
where,
orderBy: buildOrder(params.sort || "updated-desc"),
skip: (safePage - 1) * safePageSize,
take: safePageSize,
}),
prisma.item.count({ where }),
prisma.item.findMany({
distinct: ["category"],
select: { category: true },
orderBy: { category: "asc" },
}),
]);
const mapped = items.map(buildItemWithMeta);
const serversSet = new Set<string>();
mapped.forEach((item) => {
item.servers.forEach((srv) => serversSet.add(srv));
});
return {
items: mapped,
total,
page: safePage,
pageSize: safePageSize,
servers: Array.from(serversSet).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })),
categories: categoryRows
.map((row) => row.category)
.filter((c): c is string => Boolean(c))
.sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })),
};
}
export async function searchItems(params: SearchItemsParams = {}): Promise<{ items: ItemWithMeta[]; total: number }> {
const { items, total } = await searchAdminItems(params);
return { items, total };
}
export async function createAdminItem(input: ItemUpsertInput): Promise<ItemWithMeta> {
const now = Date.now();
const data: Prisma.ItemCreateInput = {
source_table_id: input.source_table_id ?? now,
source_table_name: input.source_table_name ?? "custom",
item_name: input.item_name,
category: input.category,
image_local_path: input.image_local_path ?? null,
image_url: input.image_url ?? null,
rarity: input.rarity ?? null,
level: input.level ?? null,
weight: toDecimal(input.weight),
slots: input.slots ?? null,
skill: input.skill ?? null,
metadata: buildMetadata({}, {
servers: input.servers ?? [],
tags: input.tags ?? [],
description: input.description ?? null,
metadata: input.metadata ?? {},
}),
};
const created = await prisma.item.create({ data });
return buildItemWithMeta(created);
}
export async function updateAdminItem(itemId: number, input: ItemUpsertInput): Promise<ItemWithMeta> {
const existing = await prisma.item.findUnique({ where: { item_id: itemId } });
if (!existing) {
throw new Error("Item não encontrado");
}
const existingMeta = parseMetadata(existing.metadata);
const metadata = buildMetadata(existingMeta, {
servers: input.servers ?? extractServers(existingMeta),
tags: input.tags ?? extractTags(existingMeta),
description: input.description !== undefined ? input.description : existingMeta.description,
metadata: input.metadata,
});
const data: Prisma.ItemUpdateInput = {
item_name: input.item_name ?? existing.item_name,
category: input.category ?? existing.category,
image_local_path: input.image_local_path !== undefined ? input.image_local_path : existing.image_local_path,
image_url: input.image_url !== undefined ? input.image_url : existing.image_url,
rarity: input.rarity !== undefined ? input.rarity : existing.rarity,
level: input.level !== undefined ? input.level : existing.level,
weight: input.weight !== undefined ? toDecimal(input.weight) : existing.weight,
slots: input.slots !== undefined ? input.slots : existing.slots,
skill: input.skill !== undefined ? input.skill : existing.skill,
metadata,
};
const updated = await prisma.item.update({ where: { item_id: itemId }, data });
return buildItemWithMeta(updated);
}