feat: implement user and guild existence checks to prevent foreign key constraint errors

This commit is contained in:
2025-10-06 13:40:05 -05:00
parent b258a83212
commit 04adcf3c64
9 changed files with 403 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ import { prisma } from '../../core/database/prisma';
import { giveRewards, type Reward } from '../rewards/service';
import { getOrCreatePlayerStats } from '../stats/service';
import logger from '../../core/lib/logger';
import { ensureUserAndGuildExist } from '../core/userService';
/**
* Verificar y desbloquear logros según un trigger
@@ -12,6 +13,9 @@ export async function checkAchievements(
trigger: string
): Promise<any[]> {
try {
// Asegurar que User y Guild existan antes de buscar achievements
await ensureUserAndGuildExist(userId, guildId);
// Obtener todos los logros del servidor que no estén desbloqueados
const achievements = await prisma.achievement.findMany({
where: {

View File

@@ -1,5 +1,6 @@
import { prisma } from '../../core/database/prisma';
import type { ItemProps } from '../economy/types';
import { ensureUserAndGuildExist } from '../core/userService';
function parseItemProps(json: unknown): ItemProps {
if (!json || typeof json !== 'object') return {};
@@ -7,6 +8,9 @@ function parseItemProps(json: unknown): ItemProps {
}
export async function ensurePlayerState(userId: string, guildId: string) {
// Asegurar que User y Guild existan antes de crear/buscar state
await ensureUserAndGuildExist(userId, guildId);
return prisma.playerState.upsert({
where: { userId_guildId: { userId, guildId } },
update: {},
@@ -15,6 +19,9 @@ export async function ensurePlayerState(userId: string, guildId: string) {
}
export async function getEquipment(userId: string, guildId: string) {
// Asegurar que User y Guild existan antes de crear/buscar equipment
await ensureUserAndGuildExist(userId, guildId);
const eq = await prisma.playerEquipment.upsert({
where: { userId_guildId: { userId, guildId } },
update: {},
@@ -27,6 +34,9 @@ export async function getEquipment(userId: string, guildId: string) {
}
export async function setEquipmentSlot(userId: string, guildId: string, slot: 'weapon'|'armor'|'cape', itemId: string | null) {
// Asegurar que User y Guild existan antes de crear/actualizar equipment
await ensureUserAndGuildExist(userId, guildId);
const data = slot === 'weapon' ? { weaponItemId: itemId }
: slot === 'armor' ? { armorItemId: itemId }
: { capeItemId: itemId };

View File

@@ -1,10 +1,14 @@
import { prisma } from '../../core/database/prisma';
import { ensureUserAndGuildExist } from '../core/userService';
export async function getCooldown(userId: string, guildId: string, key: string) {
return prisma.actionCooldown.findUnique({ where: { userId_guildId_key: { userId, guildId, key } } });
}
export async function setCooldown(userId: string, guildId: string, key: string, seconds: number) {
// Asegurar que User y Guild existan antes de crear/actualizar cooldown
await ensureUserAndGuildExist(userId, guildId);
const until = new Date(Date.now() + Math.max(0, seconds) * 1000);
return prisma.actionCooldown.upsert({
where: { userId_guildId_key: { userId, guildId, key } },

View File

@@ -0,0 +1,89 @@
import { prisma } from '../../core/database/prisma';
import logger from '../../core/lib/logger';
/**
* Asegura que existan los registros de User y Guild en la base de datos.
*
* **PROBLEMA RESUELTO**: Cuando un usuario nuevo usa comandos de juego (como !inventario, !craftear, etc.),
* las funciones como getOrCreateWallet(), getOrCreatePlayerStats(), etc. intentaban crear registros
* con foreign keys a User y Guild que no existían, causando errores de constraint.
*
* **SOLUCIÓN**: Esta función garantiza que User y Guild existan ANTES de crear cualquier dato relacionado.
*
* @param userId - Discord User ID
* @param guildId - Discord Guild ID
* @param guildName - Nombre del servidor (opcional, para crear Guild si no existe)
* @returns Promise<void>
*/
export async function ensureUserAndGuildExist(
userId: string,
guildId: string,
guildName?: string
): Promise<void> {
try {
// Verificar y crear User si no existe
await prisma.user.upsert({
where: { id: userId },
update: {}, // No actualizamos nada si ya existe
create: { id: userId }
});
// Verificar y crear Guild si no existe
await prisma.guild.upsert({
where: { id: guildId },
update: {}, // No actualizamos nada si ya existe
create: {
id: guildId,
name: guildName || 'Unknown Server',
prefix: '!'
}
});
} catch (error) {
logger.error({ userId, guildId, error }, 'Error ensuring User and Guild exist');
throw error;
}
}
/**
* Asegura que un User exista en la base de datos.
* Útil cuando solo necesitas garantizar que el usuario existe.
*
* @param userId - Discord User ID
* @returns Promise<void>
*/
export async function ensureUserExists(userId: string): Promise<void> {
try {
await prisma.user.upsert({
where: { id: userId },
update: {},
create: { id: userId }
});
} catch (error) {
logger.error({ userId, error }, 'Error ensuring User exists');
throw error;
}
}
/**
* Asegura que un Guild exista en la base de datos.
*
* @param guildId - Discord Guild ID
* @param guildName - Nombre del servidor (opcional)
* @returns Promise<void>
*/
export async function ensureGuildExists(guildId: string, guildName?: string): Promise<void> {
try {
await prisma.guild.upsert({
where: { id: guildId },
update: {},
create: {
id: guildId,
name: guildName || 'Unknown Server',
prefix: '!'
}
});
} catch (error) {
logger.error({ guildId, error }, 'Error ensuring Guild exists');
throw error;
}
}

View File

@@ -1,6 +1,7 @@
import { prisma } from '../../core/database/prisma';
import type { ItemProps, InventoryState, Price, OpenChestResult } from './types';
import type { Prisma } from '@prisma/client';
import { ensureUserAndGuildExist } from '../core/userService';
// Utilidades de tiempo
function now(): Date {
@@ -30,6 +31,9 @@ export async function findItemByKey(guildId: string, key: string) {
}
export async function getOrCreateWallet(userId: string, guildId: string) {
// Asegurar que User y Guild existan antes de crear/buscar wallet
await ensureUserAndGuildExist(userId, guildId);
return prisma.economyWallet.upsert({
where: { userId_guildId: { userId, guildId } },
update: {},

View File

@@ -1,6 +1,7 @@
import { prisma } from '../../core/database/prisma';
import { giveRewards, type Reward } from '../rewards/service';
import logger from '../../core/lib/logger';
import { ensureUserAndGuildExist } from '../core/userService';
/**
* Actualizar progreso de misiones del jugador
@@ -12,6 +13,9 @@ export async function updateQuestProgress(
increment: number = 1
) {
try {
// Asegurar que User y Guild existan antes de crear/buscar quest progress
await ensureUserAndGuildExist(userId, guildId);
// Obtener misiones activas que coincidan con el tipo
const quests = await prisma.quest.findMany({
where: {

View File

@@ -1,11 +1,15 @@
import { prisma } from '../../core/database/prisma';
import type { Prisma } from '@prisma/client';
import logger from '../../core/lib/logger';
import { ensureUserAndGuildExist } from '../core/userService';
/**
* Obtener o crear las estadísticas de un jugador
*/
export async function getOrCreatePlayerStats(userId: string, guildId: string) {
// Asegurar que User y Guild existan antes de crear/buscar stats
await ensureUserAndGuildExist(userId, guildId);
let stats = await prisma.playerStats.findUnique({
where: { userId_guildId: { userId, guildId } }
});

View File

@@ -1,11 +1,15 @@
import { prisma } from '../../core/database/prisma';
import { giveRewards, type Reward } from '../rewards/service';
import logger from '../../core/lib/logger';
import { ensureUserAndGuildExist } from '../core/userService';
/**
* Obtener o crear racha del jugador
*/
export async function getOrCreateStreak(userId: string, guildId: string) {
// Asegurar que User y Guild existan antes de crear/buscar streak
await ensureUserAndGuildExist(userId, guildId);
let streak = await prisma.playerStreak.findUnique({
where: { userId_guildId: { userId, guildId } }
});