254 lines
7.4 KiB
TypeScript
254 lines
7.4 KiB
TypeScript
|
|
/**
|
|||
|
|
* Script de Migración: Stackable Items → Instanced Items con Durabilidad
|
|||
|
|
*
|
|||
|
|
* Problema:
|
|||
|
|
* - Items de herramientas/armas en DB tienen stackable=true (error de versión antigua)
|
|||
|
|
* - Inventarios tienen quantity>1 sin state.instances con durabilidad
|
|||
|
|
* - Esto causa que reduceToolDurability decremente quantity en lugar de degradar durabilidad
|
|||
|
|
*
|
|||
|
|
* Solución:
|
|||
|
|
* 1. Actualizar EconomyItem: stackable=false para tools/weapons/armor/capes
|
|||
|
|
* 2. Migrar InventoryEntry: convertir quantity a state.instances[] con durabilidad inicializada
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { PrismaClient, Prisma } from "@prisma/client";
|
|||
|
|
|
|||
|
|
const prisma = new PrismaClient();
|
|||
|
|
|
|||
|
|
type ItemProps = {
|
|||
|
|
breakable?: {
|
|||
|
|
enabled?: boolean;
|
|||
|
|
maxDurability?: number;
|
|||
|
|
durabilityPerUse?: number;
|
|||
|
|
};
|
|||
|
|
tool?: { type: string; tier?: number };
|
|||
|
|
damage?: number;
|
|||
|
|
defense?: number;
|
|||
|
|
[k: string]: unknown;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
type InventoryState = {
|
|||
|
|
instances?: Array<{
|
|||
|
|
durability?: number;
|
|||
|
|
expiresAt?: string;
|
|||
|
|
notes?: string;
|
|||
|
|
mutations?: string[];
|
|||
|
|
}>;
|
|||
|
|
notes?: string;
|
|||
|
|
[k: string]: unknown;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
console.log("🔧 Iniciando migración de items stackable...\n");
|
|||
|
|
|
|||
|
|
// PASO 1: Actualizar definiciones de items
|
|||
|
|
console.log("📝 PASO 1: Actualizando EconomyItem (stackable → false)...");
|
|||
|
|
const itemUpdateResult = await prisma.$executeRaw`
|
|||
|
|
UPDATE "EconomyItem"
|
|||
|
|
SET "stackable" = false
|
|||
|
|
WHERE "key" LIKE 'tool.%'
|
|||
|
|
OR "key" LIKE 'weapon.%'
|
|||
|
|
OR "key" LIKE 'armor.%'
|
|||
|
|
OR "key" LIKE 'cape.%'
|
|||
|
|
`;
|
|||
|
|
console.log(`✅ ${itemUpdateResult} items actualizados\n`);
|
|||
|
|
|
|||
|
|
// PASO 2: Obtener items que ahora son non-stackable
|
|||
|
|
const nonStackableItems = await prisma.economyItem.findMany({
|
|||
|
|
where: {
|
|||
|
|
stackable: false,
|
|||
|
|
OR: [
|
|||
|
|
{ key: { startsWith: "tool." } },
|
|||
|
|
{ key: { startsWith: "weapon." } },
|
|||
|
|
{ key: { startsWith: "armor." } },
|
|||
|
|
{ key: { startsWith: "cape." } },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log(
|
|||
|
|
`📦 ${nonStackableItems.length} items non-stackable identificados\n`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// PASO 3: Migrar inventarios
|
|||
|
|
console.log("🔄 PASO 2: Migrando inventarios...");
|
|||
|
|
|
|||
|
|
let migratedCount = 0;
|
|||
|
|
let skippedCount = 0;
|
|||
|
|
let errorCount = 0;
|
|||
|
|
|
|||
|
|
for (const item of nonStackableItems) {
|
|||
|
|
const props = (item.props as ItemProps | null) ?? {};
|
|||
|
|
const breakable = props.breakable;
|
|||
|
|
const maxDurability =
|
|||
|
|
breakable?.enabled !== false
|
|||
|
|
? breakable?.maxDurability ?? 100
|
|||
|
|
: undefined;
|
|||
|
|
|
|||
|
|
// Encontrar todas las entradas de inventario de este item con quantity>1 o sin instances
|
|||
|
|
const entries = await prisma.inventoryEntry.findMany({
|
|||
|
|
where: { itemId: item.id },
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
for (const entry of entries) {
|
|||
|
|
try {
|
|||
|
|
const currentState = (entry.state as InventoryState | null) ?? {};
|
|||
|
|
const currentInstances = currentState.instances ?? [];
|
|||
|
|
const currentQuantity = entry.quantity ?? 0;
|
|||
|
|
|
|||
|
|
// Caso 1: quantity>1 pero sin instances (inventario corrupto de versión anterior)
|
|||
|
|
if (currentQuantity > 1 && currentInstances.length === 0) {
|
|||
|
|
console.log(
|
|||
|
|
` 🔧 Migrando: ${item.key} (user=${entry.userId.slice(
|
|||
|
|
0,
|
|||
|
|
8
|
|||
|
|
)}, qty=${currentQuantity})`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const newInstances: InventoryState["instances"] = [];
|
|||
|
|
for (let i = 0; i < currentQuantity; i++) {
|
|||
|
|
if (maxDurability && maxDurability > 0) {
|
|||
|
|
newInstances.push({ durability: maxDurability });
|
|||
|
|
} else {
|
|||
|
|
newInstances.push({});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await prisma.inventoryEntry.update({
|
|||
|
|
where: { id: entry.id },
|
|||
|
|
data: {
|
|||
|
|
state: {
|
|||
|
|
...currentState,
|
|||
|
|
instances: newInstances,
|
|||
|
|
} as unknown as Prisma.InputJsonValue,
|
|||
|
|
quantity: newInstances.length,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
migratedCount++;
|
|||
|
|
}
|
|||
|
|
// Caso 2: Instancia única sin durabilidad inicializada
|
|||
|
|
else if (currentQuantity === 1 && currentInstances.length === 0) {
|
|||
|
|
const newInstance =
|
|||
|
|
maxDurability && maxDurability > 0
|
|||
|
|
? { durability: maxDurability }
|
|||
|
|
: {};
|
|||
|
|
|
|||
|
|
await prisma.inventoryEntry.update({
|
|||
|
|
where: { id: entry.id },
|
|||
|
|
data: {
|
|||
|
|
state: {
|
|||
|
|
...currentState,
|
|||
|
|
instances: [newInstance],
|
|||
|
|
} as unknown as Prisma.InputJsonValue,
|
|||
|
|
quantity: 1,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
migratedCount++;
|
|||
|
|
}
|
|||
|
|
// Caso 3: Ya tiene instances pero sin durabilidad inicializada
|
|||
|
|
else if (currentInstances.length > 0 && maxDurability) {
|
|||
|
|
let needsUpdate = false;
|
|||
|
|
const fixedInstances = currentInstances.map((inst) => {
|
|||
|
|
if (inst.durability == null) {
|
|||
|
|
needsUpdate = true;
|
|||
|
|
return { ...inst, durability: maxDurability };
|
|||
|
|
}
|
|||
|
|
return inst;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (needsUpdate) {
|
|||
|
|
console.log(
|
|||
|
|
` 🔧 Reparando durabilidad: ${
|
|||
|
|
item.key
|
|||
|
|
} (user=${entry.userId.slice(0, 8)}, instances=${
|
|||
|
|
fixedInstances.length
|
|||
|
|
})`
|
|||
|
|
);
|
|||
|
|
await prisma.inventoryEntry.update({
|
|||
|
|
where: { id: entry.id },
|
|||
|
|
data: {
|
|||
|
|
state: {
|
|||
|
|
...currentState,
|
|||
|
|
instances: fixedInstances,
|
|||
|
|
} as unknown as Prisma.InputJsonValue,
|
|||
|
|
quantity: fixedInstances.length,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
migratedCount++;
|
|||
|
|
} else {
|
|||
|
|
skippedCount++;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
skippedCount++;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(` ❌ Error migrando entry ${entry.id}:`, error);
|
|||
|
|
errorCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log("\n📊 Resumen de migración:");
|
|||
|
|
console.log(` ✅ Entradas migradas: ${migratedCount}`);
|
|||
|
|
console.log(` ⏭️ Entradas omitidas (ya correctas): ${skippedCount}`);
|
|||
|
|
console.log(` ❌ Errores: ${errorCount}\n`);
|
|||
|
|
|
|||
|
|
// PASO 4: Validación post-migración
|
|||
|
|
console.log("🔍 PASO 3: Validando integridad...");
|
|||
|
|
const inconsistentEntries = await prisma.$queryRaw<
|
|||
|
|
Array<{
|
|||
|
|
id: string;
|
|||
|
|
userId: string;
|
|||
|
|
key: string;
|
|||
|
|
quantity: number;
|
|||
|
|
state: any;
|
|||
|
|
}>
|
|||
|
|
>`
|
|||
|
|
SELECT
|
|||
|
|
ie.id,
|
|||
|
|
ie."userId",
|
|||
|
|
ei.key,
|
|||
|
|
ie.quantity,
|
|||
|
|
ie.state
|
|||
|
|
FROM "InventoryEntry" ie
|
|||
|
|
JOIN "EconomyItem" ei ON ie."itemId" = ei.id
|
|||
|
|
WHERE ei."stackable" = false
|
|||
|
|
AND ie.quantity > 1
|
|||
|
|
AND (
|
|||
|
|
ie.state IS NULL
|
|||
|
|
OR jsonb_array_length(COALESCE((ie.state->>'instances')::jsonb, '[]'::jsonb)) = 0
|
|||
|
|
)
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
if (inconsistentEntries.length > 0) {
|
|||
|
|
console.log(
|
|||
|
|
`\n⚠️ ADVERTENCIA: ${inconsistentEntries.length} entradas inconsistentes detectadas:`
|
|||
|
|
);
|
|||
|
|
inconsistentEntries.forEach((entry) => {
|
|||
|
|
console.log(
|
|||
|
|
` - ${entry.key} (user=${entry.userId.slice(0, 8)}, qty=${
|
|||
|
|
entry.quantity
|
|||
|
|
})`
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
console.log(
|
|||
|
|
"\n❗ Ejecuta el comando admin !reset-inventory para estos usuarios\n"
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
console.log("✅ No se detectaron inconsistencias\n");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log("🎉 Migración completada exitosamente");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main()
|
|||
|
|
.catch((error) => {
|
|||
|
|
console.error("❌ Error fatal durante migración:", error);
|
|||
|
|
process.exit(1);
|
|||
|
|
})
|
|||
|
|
.finally(async () => {
|
|||
|
|
await prisma.$disconnect();
|
|||
|
|
});
|