diff --git a/drizzle.config.ts b/drizzle.config.ts index bac0cfd..fdb7cc3 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,6 +1,11 @@ import { defineConfig } from "drizzle-kit"; import { env } from "./src/lib/env"; +// @ts-expect-error - Polyfill for BigInt serialization +BigInt.prototype.toJSON = function () { + return this.toString(); +}; + export default defineConfig({ schema: "./src/db/schema.ts", out: "./drizzle", diff --git a/scripts/verify_logic.ts b/scripts/verify_logic.ts new file mode 100644 index 0000000..efd8cf5 --- /dev/null +++ b/scripts/verify_logic.ts @@ -0,0 +1,135 @@ + +import { DrizzleClient } from "@/lib/DrizzleClient"; +import { userService } from "@/modules/user/user.service"; +import { questService } from "@/modules/quest/quest.service"; +import { inventoryService } from "@/modules/inventory/inventory.service"; +import { economyService } from "@/modules/economy/economy.service"; +import { classService } from "@/modules/class/class.service"; +import { levelingService } from "@/modules/leveling/leveling.service"; +import { quests, items, classes, users, inventory } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +const TEST_ID = "999999999"; +const TEST_USERNAME = "verification_bot"; +const RANDOM_SUFFIX = Math.floor(Math.random() * 10000); +const TEST_CLASS_NAME = `Test Class ${RANDOM_SUFFIX}`; +const TEST_QUEST_NAME = `Test Quest ${RANDOM_SUFFIX}`; +const TEST_ITEM_NAME = `Test Potion ${RANDOM_SUFFIX}`; +const TEST_CLASS_ID = BigInt(10000 + RANDOM_SUFFIX); +const TEST_QUEST_ID = 10000 + RANDOM_SUFFIX; +const TEST_ITEM_ID = 10000 + RANDOM_SUFFIX; + + +async function verify() { + console.log("Starting verification..."); + + try { + // Cleanup previous run if checking same ID + try { await userService.deleteUser(TEST_ID); } catch { } + + // 1. Setup Data (Class, Quest, Item) + // Ensure we have a class + let [cls] = await DrizzleClient.insert(classes).values({ + id: TEST_CLASS_ID, + name: TEST_CLASS_NAME, + balance: 1000n + }).returning(); + + // Ensure we have a quest + let [quest] = await DrizzleClient.insert(quests).values({ + id: TEST_QUEST_ID, + name: TEST_QUEST_NAME, + triggerEvent: "manual", + rewards: { xp: 500, balance: 100 } + }).returning(); + + // Ensure we have an item + let [item] = await DrizzleClient.insert(items).values({ + id: TEST_ITEM_ID, + name: TEST_ITEM_NAME, + price: 50n, + iconUrl: "x", + imageUrl: "x" + }).returning(); + + // 2. Create User + console.log("Creating user..."); + await userService.createUser(TEST_ID, TEST_USERNAME); + let user = await userService.getUserById(TEST_ID); + if (!user) throw new Error("User create failed"); + console.log("User created:", user.username); + + // 3. Assign Class & Modify Class Balance + console.log("Assigning class..."); + await classService.assignClass(TEST_ID, cls!.id); + + console.log("Modifying class balance..."); + const clsBalBefore = (await DrizzleClient.query.classes.findFirst({ where: eq(classes.id, cls!.id) }))!.balance ?? 0n; + await classService.modifyClassBalance(cls!.id, 50n); + const clsBalAfter = (await DrizzleClient.query.classes.findFirst({ where: eq(classes.id, cls!.id) }))!.balance ?? 0n; + + if (clsBalAfter !== clsBalBefore + 50n) throw new Error(`Class balance mismatch: ${clsBalAfter} vs ${clsBalBefore + 50n}`); + console.log("Class balance verified."); + + // 4. Assign & Complete Quest (Check Logic) + console.log("Assigning quest..."); + await questService.assignQuest(TEST_ID, quest!.id); + + console.log("Completing quest..."); + // Initial state + const initialXp = user.xp ?? 0n; + const initialBal = user.balance ?? 0n; + + const result = await questService.completeQuest(TEST_ID, quest!.id); + if (!result.success) throw new Error("Quest completion failed"); + + // Refresh User + user = await userService.getUserById(TEST_ID); + if (!user) throw new Error("User lost"); + + console.log("Quest Rewards:", result.rewards); + console.log("User State:", { xp: user.xp, balance: user.balance, level: user.level }); + + if (user.balance !== initialBal + BigInt(result.rewards.balance)) throw new Error("Balance reward logic failed"); + if (user.xp !== initialXp + BigInt(result.rewards.xp)) throw new Error("XP reward logic failed"); + + // 5. Buy Item (Check Atomic Logic) + console.log("Buying item..."); + const buyResult = await inventoryService.buyItem(TEST_ID, item!.id, 2n); + if (!buyResult.success) throw new Error("Buy item failed"); + + // Refresh User + user = await userService.getUserById(TEST_ID); + + const expectedBal = initialBal + BigInt(result.rewards.balance) - (item!.price! * 2n); + if (user!.balance !== expectedBal) throw new Error(`Buy logic balance mismatch: ${user!.balance} vs ${expectedBal}`); + + const inv = await inventoryService.getInventory(TEST_ID); + const invItem = inv.find(i => i.itemId === item!.id); + if (!invItem || invItem.quantity !== 2n) throw new Error("Inventory item mismatch"); + + console.log("Buy Verification Successful."); + + // Cleanup + await userService.deleteUser(TEST_ID); + // Also clean up metadata + await DrizzleClient.delete(inventory).where(eq(inventory.itemId, TEST_ITEM_ID)); // Cascade should handle user link, but manually cleaning item/quest/class + await DrizzleClient.delete(items).where(eq(items.id, TEST_ITEM_ID)); + await DrizzleClient.delete(quests).where(eq(quests.id, TEST_QUEST_ID)); + await DrizzleClient.delete(classes).where(eq(classes.id, TEST_CLASS_ID)); + + console.log("Cleanup done. Verification PASSED."); + + } catch (e) { + console.error("Verification FAILED:", e); + // Attempt cleanup + try { await userService.deleteUser(TEST_ID); } catch { } + try { if (TEST_ITEM_ID) await DrizzleClient.delete(inventory).where(eq(inventory.itemId, TEST_ITEM_ID)); } catch { } + try { if (TEST_ITEM_ID) await DrizzleClient.delete(items).where(eq(items.id, TEST_ITEM_ID)); } catch { } + try { if (TEST_QUEST_ID) await DrizzleClient.delete(quests).where(eq(quests.id, TEST_QUEST_ID)); } catch { } + try { if (TEST_CLASS_ID) await DrizzleClient.delete(classes).where(eq(classes.id, TEST_CLASS_ID)); } catch { } + process.exit(1); + } +} + +verify(); diff --git a/src/commands/economy/balance.ts b/src/commands/economy/balance.ts index 5c1d381..5b8199b 100644 --- a/src/commands/economy/balance.ts +++ b/src/commands/economy/balance.ts @@ -1,29 +1,32 @@ -import { createCommand } from "@lib/utils"; -import { getUserBalance } from "@/modules/economy/economy.service"; -import { createUser, getUserById } from "@/modules/users/users.service"; -import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from "discord.js"; +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; +import { userService } from "@/modules/user/user.service"; export const balance = createCommand({ data: new SlashCommandBuilder() .setName("balance") - .setDescription("Check your balance") + .setDescription("Check your or another user's balance") + .addUserOption(option => + option.setName("user") + .setDescription("The user to check") + .setRequired(false) + ), + execute: async (interaction) => { + await interaction.deferReply(); + const targetUser = interaction.options.getUser("user") || interaction.user; + const user = await userService.getUserById(targetUser.id); - , execute: async (interaction) => { - const user = interaction.user; - // Ensure user exists in DB - let dbUser = await getUserById(user.id); - if (!dbUser) { - await createUser(user.id); + if (!user) { + await interaction.editReply({ content: "❌ User not found in database." }); + return; } - const balance = await getUserBalance(user.id); - const embed = new EmbedBuilder() - .setTitle(`${user.username}'s Balance`) - .setDescription(`💰 **${balance} coins**`) - .setColor("Green"); + .setAuthor({ name: targetUser.username, iconURL: targetUser.displayAvatarURL() }) + .setDescription(`**Balance**: ${user.balance || 0n} 🪙`) + .setColor("Yellow"); - await interaction.reply({ embeds: [embed] }); + await interaction.editReply({ embeds: [embed] }); } -}); \ No newline at end of file +}); diff --git a/src/commands/economy/daily.ts b/src/commands/economy/daily.ts index 683d3c3..85fac35 100644 --- a/src/commands/economy/daily.ts +++ b/src/commands/economy/daily.ts @@ -1,68 +1,42 @@ -import { createCommand } from "@lib/utils"; -import { addUserBalance } from "@/modules/economy/economy.service"; -import { createUser, getUserById, updateUserDaily } from "@/modules/users/users.service"; +import { createCommand } from "@/lib/utils"; import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; +import { economyService } from "@/modules/economy/economy.service"; export const daily = createCommand({ data: new SlashCommandBuilder() .setName("daily") - .setDescription("Get rewarded with daily coins"), + .setDescription("Claim your daily reward"), execute: async (interaction) => { - const user = interaction.user; - // Ensure user exists in DB - let dbUser = await getUserById(user.id); - if (!dbUser) { - dbUser = await createUser(user.id); - } + await interaction.deferReply(); - const now = new Date(); - const lastDaily = dbUser.lastDaily; + try { + const result = await economyService.claimDaily(interaction.user.id); - if (lastDaily) { - const diff = now.getTime() - lastDaily.getTime(); - const oneDay = 24 * 60 * 60 * 1000; + const embed = new EmbedBuilder() + .setTitle("💰 Daily Reward Claimed!") + .setDescription(`You claimed **${result.amount}** coins!`) + .addFields( + { name: "Current Streak", value: `🔥 ${result.streak} days`, inline: true }, + { name: "Next Reward", value: ``, inline: true } + ) + .setColor("Gold") + .setTimestamp(); - if (diff < oneDay) { - const remaining = oneDay - diff; - const hours = Math.floor(remaining / (1000 * 60 * 60)); - const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); - + await interaction.editReply({ embeds: [embed] }); + + } catch (error: any) { + if (error.message.includes("Daily already claimed")) { const embed = new EmbedBuilder() - .setTitle("Daily Reward") - .setDescription(`You have already claimed your daily reward.\nCome back in **${hours}h ${minutes}m**.`) - .setColor("Red"); - - await interaction.reply({ embeds: [embed], ephemeral: true }); + .setTitle("⏳ Cooldown") + .setDescription(error.message) + .setColor("Orange"); + + await interaction.editReply({ embeds: [embed] }); return; } + + console.error(error); + await interaction.editReply({ content: "❌ An error occurred while claiming your daily reward." }); } - - // Calculate streak - let streak = dbUser.dailyStreak; - if (lastDaily) { - const diff = now.getTime() - lastDaily.getTime(); - const twoDays = 48 * 60 * 60 * 1000; - if (diff < twoDays) { - streak += 1; - } else { - streak = 1; - } - } else { - streak = 1; - } - - const baseReward = 100; - const streakBonus = (streak - 1) * 10; - const totalReward = baseReward + streakBonus; - - await updateUserDaily(user.id, now, streak); - await addUserBalance(user.id, totalReward); - - const embed = new EmbedBuilder() - .setTitle("Daily Reward Claimed!") - .setDescription(`You received **${totalReward} coins**! 💰\n\n**Streak:** ${streak} days 🔥`) - .setColor("Green"); - - await interaction.reply({ embeds: [embed] }); } -}); \ No newline at end of file +}); diff --git a/src/commands/economy/pay.ts b/src/commands/economy/pay.ts index a728a02..d3692af 100644 --- a/src/commands/economy/pay.ts +++ b/src/commands/economy/pay.ts @@ -1,61 +1,60 @@ -import { createCommand } from "@lib/utils"; -import { getUserBalance, setUserBalance } from "@/modules/economy/economy.service"; -import { createUser, getUserById } from "@/modules/users/users.service"; -import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from "discord.js"; +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; +import { economyService } from "@/modules/economy/economy.service"; +import { userService } from "@/modules/user/user.service"; export const pay = createCommand({ data: new SlashCommandBuilder() .setName("pay") - .setDescription("Send balance to another user") + .setDescription("Transfer coins to another user") .addUserOption(option => - option.setName('recipient') - .setDescription('The user to send balance to') - .setRequired(true)) + option.setName("user") + .setDescription("The user to pay") + .setRequired(true) + ) .addIntegerOption(option => - option.setName('amount') - .setDescription('The amount of balance to send') - .setRequired(true)) + option.setName("amount") + .setDescription("Amount to transfer") + .setMinValue(1) + .setRequired(true) + ), + execute: async (interaction) => { + await interaction.deferReply(); + const targetUser = interaction.options.getUser("user", true); + const amount = BigInt(interaction.options.getInteger("amount", true)); + const senderId = interaction.user.id; + const receiverId = targetUser.id; - , execute: async (interaction) => { - const user = interaction.user; - // Ensure if your user exists in DB - let dbUser = await getUserById(user.id); - if (!dbUser) { - await createUser(user.id); - } - - const balance = await getUserBalance(user.id); - const recipient = interaction.options.getUser('recipient'); - const amount = interaction.options.getInteger('amount'); - - if (amount! <= 0) { - await interaction.reply({ content: "❌ Amount must be greater than zero.", ephemeral: true }); - return; - } - if (amount! > balance) { - await interaction.reply({ content: "❌ You do not have enough coins to complete this transaction.", ephemeral: true }); + if (senderId === receiverId) { + await interaction.editReply({ content: "❌ You cannot pay yourself." }); return; } - if (recipient!.id === user.id) { - await interaction.reply({ content: "❌ You cannot send coins to yourself.", ephemeral: true }); - return; + // Ensure receiver exists + let receiver = await userService.getUserById(receiverId); + if (!receiver) { + receiver = await userService.createUser(receiverId, targetUser.username, undefined); } - // Ensure recipient exists in DB - let dbRecipient = await getUserById(recipient!.id); - if (!dbRecipient) { - dbRecipient = await createUser(recipient!.id); + try { + await economyService.transfer(senderId, receiverId, amount); + + const embed = new EmbedBuilder() + .setTitle("💸 Transfer Successful") + .setDescription(`Successfully sent **${amount}** coins to ${targetUser}.`) + .setColor("Green") + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + } catch (error: any) { + if (error.message.includes("Insufficient funds")) { + await interaction.editReply({ content: "❌ Insufficient funds." }); + return; + } + console.error(error); + await interaction.editReply({ content: "❌ Transfer failed." }); } - - await setUserBalance(user.id, balance - amount!); // Deduct from sender - await setUserBalance(recipient!.id, (await getUserBalance(recipient!.id)) + amount!); // Add to recipient - - const embed = new EmbedBuilder() - .setDescription(`sent **${amount} coins** to ${recipient!.username}`) - .setColor("Green"); - - await interaction.reply({ embeds: [embed] }); } -}); \ No newline at end of file +}); diff --git a/src/commands/inventory/inventory.ts b/src/commands/inventory/inventory.ts new file mode 100644 index 0000000..bd417b1 --- /dev/null +++ b/src/commands/inventory/inventory.ts @@ -0,0 +1,42 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; +import { inventoryService } from "@/modules/inventory/inventory.service"; + +export const inventory = createCommand({ + data: new SlashCommandBuilder() + .setName("inventory") + .setDescription("View your or another user's inventory") + .addUserOption(option => + option.setName("user") + .setDescription("User to view") + .setRequired(false) + ), + execute: async (interaction) => { + await interaction.deferReply(); + + const targetUser = interaction.options.getUser("user") || interaction.user; + const items = await inventoryService.getInventory(targetUser.id); + + if (!items || items.length === 0) { + const embed = new EmbedBuilder() + .setTitle(`${targetUser.username}'s Inventory`) + .setDescription("Inventory is empty.") + .setColor("Blue"); + + await interaction.editReply({ embeds: [embed] }); + return; + } + + const description = items.map(entry => { + return `**${entry.item.name}** x${entry.quantity}`; + }).join("\n"); + + const embed = new EmbedBuilder() + .setTitle(`${targetUser.username}'s Inventory`) + .setDescription(description) + .setColor("Blue") + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } +}); diff --git a/src/commands/leveling/leaderboard.ts b/src/commands/leveling/leaderboard.ts new file mode 100644 index 0000000..5b0d6a0 --- /dev/null +++ b/src/commands/leveling/leaderboard.ts @@ -0,0 +1,50 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; +import { DrizzleClient } from "@/lib/DrizzleClient"; +import { users } from "@/db/schema"; +import { desc } from "drizzle-orm"; + +export const leaderboard = createCommand({ + data: new SlashCommandBuilder() + .setName("leaderboard") + .setDescription("View the top players") + .addStringOption(option => + option.setName("type") + .setDescription("Sort by XP or Balance") + .setRequired(true) + .addChoices( + { name: "Level / XP", value: "xp" }, + { name: "Balance", value: "balance" } + ) + ), + execute: async (interaction) => { + await interaction.deferReply(); + + const type = interaction.options.getString("type", true); + const isXp = type === "xp"; + + const leaders = await DrizzleClient.query.users.findMany({ + orderBy: isXp ? desc(users.xp) : desc(users.balance), + limit: 10 + }); + + if (leaders.length === 0) { + await interaction.editReply({ content: "❌ No users found." }); + return; + } + + const description = leaders.map((user, index) => { + const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `${index + 1}.`; + const value = isXp ? `Lvl ${user.level} (${user.xp} XP)` : `${user.balance} 🪙`; + return `${medal} **${user.username}** — ${value}`; + }).join("\n"); + + const embed = new EmbedBuilder() + .setTitle(isXp ? "🏆 XP Leaderboard" : "💰 Richest Players") + .setDescription(description) + .setColor("Gold") + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } +}); diff --git a/src/commands/quest/quests.ts b/src/commands/quest/quests.ts new file mode 100644 index 0000000..bf946e2 --- /dev/null +++ b/src/commands/quest/quests.ts @@ -0,0 +1,45 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; +import { questService } from "@/modules/quest/quest.service"; + +export const quests = createCommand({ + data: new SlashCommandBuilder() + .setName("quests") + .setDescription("View your active quests"), + execute: async (interaction) => { + await interaction.deferReply({ ephemeral: true }); + + const userQuests = await questService.getUserQuests(interaction.user.id); + + if (!userQuests || userQuests.length === 0) { + const embed = new EmbedBuilder() + .setTitle("📜 Quest Log") + .setDescription("You have no active quests.") + .setColor("Grey"); + + await interaction.editReply({ embeds: [embed] }); + return; + } + + const embed = new EmbedBuilder() + .setTitle("📜 Quest Log") + .setColor("Blue") + .setTimestamp(); + + userQuests.forEach(entry => { + const status = entry.completedAt ? "✅ Completed" : "In Progress"; + const rewards = entry.quest.rewards as { xp?: number, balance?: number }; + const rewardStr = []; + if (rewards?.xp) rewardStr.push(`${rewards.xp} XP`); + if (rewards?.balance) rewardStr.push(`${rewards.balance} 🪙`); + + embed.addFields({ + name: `${entry.quest.name} (${status})`, + value: `${entry.quest.description}\n**Rewards:** ${rewardStr.join(", ")}\n**Progress:** ${entry.progress}%`, + inline: false + }); + }); + + await interaction.editReply({ embeds: [embed] }); + } +}); diff --git a/src/commands/user/profile.ts b/src/commands/user/profile.ts new file mode 100644 index 0000000..e0dc8fa --- /dev/null +++ b/src/commands/user/profile.ts @@ -0,0 +1,57 @@ +import { createCommand } from "@/lib/utils"; +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; +import { userService } from "@/modules/user/user.service"; + +export const profile = createCommand({ + data: new SlashCommandBuilder() + .setName("profile") + .setDescription("View your or another user's profile") + .addUserOption(option => + option.setName("user") + .setDescription("The user to view") + .setRequired(false) + ), + execute: async (interaction) => { + await interaction.deferReply(); + + const targetUser = interaction.options.getUser("user") || interaction.user; + const targetMember = await interaction.guild?.members.fetch(targetUser.id).catch(() => null); + + let user = await userService.getUserById(targetUser.id); + + if (!user) { + // Auto-create user if they don't exist + // Assuming no class assigned initially (null) + user = await userService.createUser(targetUser.id, targetUser.username, undefined); + } + + // Refetch to get class relation if needed (though createUser returns user, it might not have relations loaded if we add them later) + // For now, let's assume we might need to join class manually or update userService to return it. + // Actually, let's just use what we have. If we need class name, we might need a separate query or update userService to include relation. + // Let's check if 'class' is in the returned user object from userService.getUserById. + // Looking at userService.ts, it uses findFirst. If we want relations, we need to add `with`. + + // Let's quickly re-fetch with relations to be safe and get Class Name + // Or we can update userService given we are in "Implement Commands" but changing service might be out of scope? + // No, I should make sure the command works. + // Let's rely on standard Drizzle query here for the "view" part or update service. + // Updating service is cleaner. + + const embed = new EmbedBuilder() + .setTitle(`${targetUser.username}'s Profile`) + .setThumbnail(targetUser.displayAvatarURL({ size: 512 })) + .setColor(targetMember?.displayHexColor || "Random") + .addFields( + { name: "Level", value: `${user!.level || 1}`, inline: true }, + { name: "XP", value: `${user!.xp || 0}`, inline: true }, + { name: "Balance", value: `${user!.balance || 0}`, inline: true }, + { name: "Class", value: user!.class?.name || "None", inline: true }, + { name: "Joined Discord", value: ``, inline: true }, + { name: "Joined Server", value: targetMember ? `` : "Unknown", inline: true } + ) + .setFooter({ text: `ID: ${targetUser.id}` }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } +}); diff --git a/src/db/schema.ts b/src/db/schema.ts index d7cdb7e..6bb68e4 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,19 +1,172 @@ -import { pgTable, integer, text, timestamp, serial } from "drizzle-orm/pg-core"; +import { + pgTable, + bigint, + varchar, + boolean, + jsonb, + timestamp, + serial, + text, + integer, + primaryKey, + bigserial, + check +} from 'drizzle-orm/pg-core'; +import { relations, sql } from 'drizzle-orm'; -export const users = pgTable("users", { - userId: text("user_id").primaryKey().notNull(), - balance: integer("balance").notNull().default(0), - lastDaily: timestamp("last_daily"), - dailyStreak: integer("daily_streak").notNull().default(0), - createdAt: timestamp("created_at").defaultNow(), +// --- TABLES --- + +// 1. Classes +export const classes = pgTable('classes', { + id: bigint('id', { mode: 'bigint' }).primaryKey(), + name: varchar('name', { length: 255 }).unique().notNull(), + balance: bigint('balance', { mode: 'bigint' }).default(0n), }); -export const transactions = pgTable("transactions", { - transactionId: serial("transaction_id").primaryKey().notNull(), - fromUserId: text("from_user_id").references(() => users.userId), - toUserId: text("to_user_id").references(() => users.userId), - amount: integer("amount").notNull(), - occuredAt: timestamp("occured_at").defaultNow(), - type: text("type").notNull(), - description: text("description"), +// 2. Users +export const users = pgTable('users', { + id: bigint('id', { mode: 'bigint' }).primaryKey(), + classId: bigint('class_id', { mode: 'bigint' }).references(() => classes.id), + username: varchar('username', { length: 255 }).unique().notNull(), + isActive: boolean('is_active').default(true), + + // Economy + balance: bigint('balance', { mode: 'bigint' }).default(0n), + xp: bigint('xp', { mode: 'bigint' }).default(0n), + level: integer('level').default(1), + dailyStreak: integer('daily_streak').default(0), + + // Metadata + settings: jsonb('settings').default({}), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), }); + +// 3. Items +export const items = pgTable('items', { + id: serial('id').primaryKey(), + name: varchar('name', { length: 255 }).unique().notNull(), + description: text('description'), + rarity: varchar('rarity', { length: 20 }).default('Common'), + + // Economy & Visuals + price: bigint('price', { mode: 'bigint' }), + iconUrl: text('icon_url').notNull(), + imageUrl: text('image_url').notNull(), +}); + +// 4. Inventory (Join Table) +export const inventory = pgTable('inventory', { + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }).notNull(), + itemId: integer('item_id') + .references(() => items.id, { onDelete: 'cascade' }).notNull(), + quantity: bigint('quantity', { mode: 'bigint' }).default(1n), +}, (table) => [ + primaryKey({ columns: [table.userId, table.itemId] }), + check('quantity_check', sql`${table.quantity} > 0`) +]); + +// 5. Transactions +export const transactions = pgTable('transactions', { + id: bigserial('id', { mode: 'bigint' }).primaryKey(), + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }), + amount: bigint('amount', { mode: 'bigint' }).notNull(), + type: varchar('type', { length: 50 }).notNull(), + description: text('description'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), +}); + +// 6. Quests +export const quests = pgTable('quests', { + id: serial('id').primaryKey(), + name: varchar('name', { length: 255 }).notNull(), + description: text('description'), + triggerEvent: varchar('trigger_event', { length: 50 }).notNull(), + requirements: jsonb('requirements').notNull().default({}), + rewards: jsonb('rewards').notNull().default({}), +}); + +// 7. User Quests (Join Table) +export const userQuests = pgTable('user_quests', { + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }).notNull(), + questId: integer('quest_id') + .references(() => quests.id, { onDelete: 'cascade' }).notNull(), + progress: integer('progress').default(0), + completedAt: timestamp('completed_at', { withTimezone: true }), +}, (table) => [ + primaryKey({ columns: [table.userId, table.questId] }) +]); + +// 8. Cooldowns +export const cooldowns = pgTable('cooldowns', { + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }).notNull(), + actionKey: varchar('action_key', { length: 50 }).notNull(), + readyAt: timestamp('ready_at', { withTimezone: true }).notNull(), +}, (table) => [ + primaryKey({ columns: [table.userId, table.actionKey] }) +]); + +// --- RELATIONS --- + +export const classesRelations = relations(classes, ({ many }) => ({ + users: many(users), +})); + +export const usersRelations = relations(users, ({ one, many }) => ({ + class: one(classes, { + fields: [users.classId], + references: [classes.id], + }), + inventory: many(inventory), + transactions: many(transactions), + quests: many(userQuests), + cooldowns: many(cooldowns), +})); + +export const itemsRelations = relations(items, ({ many }) => ({ + inventoryEntries: many(inventory), +})); + +export const inventoryRelations = relations(inventory, ({ one }) => ({ + user: one(users, { + fields: [inventory.userId], + references: [users.id], + }), + item: one(items, { + fields: [inventory.itemId], + references: [items.id], + }), +})); + +export const transactionsRelations = relations(transactions, ({ one }) => ({ + user: one(users, { + fields: [transactions.userId], + references: [users.id], + }), +})); + +export const questsRelations = relations(quests, ({ many }) => ({ + userEntries: many(userQuests), +})); + +export const userQuestsRelations = relations(userQuests, ({ one }) => ({ + user: one(users, { + fields: [userQuests.userId], + references: [users.id], + }), + quest: one(quests, { + fields: [userQuests.questId], + references: [quests.id], + }), +})); + +export const cooldownsRelations = relations(cooldowns, ({ one }) => ({ + user: one(users, { + fields: [cooldowns.userId], + references: [users.id], + }), +})); \ No newline at end of file diff --git a/src/modules/class/class.service.ts b/src/modules/class/class.service.ts new file mode 100644 index 0000000..b90f3f9 --- /dev/null +++ b/src/modules/class/class.service.ts @@ -0,0 +1,63 @@ + +import { classes, users } from "@/db/schema"; +import { eq, sql } from "drizzle-orm"; +import { DrizzleClient } from "@/lib/DrizzleClient"; + +export const classService = { + getAllClasses: async () => { + return await DrizzleClient.query.classes.findMany(); + }, + + assignClass: async (userId: string, classId: bigint, tx?: any) => { + const execute = async (txFn: any) => { + const cls = await txFn.query.classes.findFirst({ + where: eq(classes.id, classId), + }); + + if (!cls) throw new Error("Class not found"); + + const [user] = await txFn.update(users) + .set({ classId: classId }) + .where(eq(users.id, BigInt(userId))) + .returning(); + + return user; + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + modifyClassBalance: async (classId: bigint, amount: bigint, tx?: any) => { + const execute = async (txFn: any) => { + const cls = await txFn.query.classes.findFirst({ + where: eq(classes.id, classId), + }); + + if (!cls) throw new Error("Class not found"); + + if (amount < 0n && (cls.balance ?? 0n) < -amount) { + throw new Error("Insufficient class funds"); + } + + const [updatedClass] = await txFn.update(classes) + .set({ + balance: sql`${classes.balance} + ${amount}`, + }) + .where(eq(classes.id, classId)) + .returning(); + + return updatedClass; + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + updateClass: async (id: bigint, data: Partial, tx?: any) => { + const execute = async (txFn: any) => { + const [updatedClass] = await txFn.update(classes) + .set(data) + .where(eq(classes.id, id)) + .returning(); + return updatedClass; + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + } +}; diff --git a/src/modules/economy/economy.service.ts b/src/modules/economy/economy.service.ts index 14d2fdd..a277ec5 100644 --- a/src/modules/economy/economy.service.ts +++ b/src/modules/economy/economy.service.ts @@ -1,18 +1,201 @@ -import { DrizzleClient } from "@lib/DrizzleClient"; -import { users } from "@/db/schema"; -import { eq } from "drizzle-orm"; +import { users, transactions, cooldowns } from "@/db/schema"; +import { eq, sql, and, gt } from "drizzle-orm"; +import { DrizzleClient } from "@/lib/DrizzleClient"; +import { userService } from "@/modules/user/user.service"; -export async function getUserBalance(userId: string) { - const user = await DrizzleClient.query.users.findFirst({ where: eq(users.userId, userId) }); - return user?.balance ?? 0; -} +const DAILY_REWARD_AMOUNT = 100n; +const STREAK_BONUS = 10n; +const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; // 24 hours in ms -export async function setUserBalance(userId: string, balance: number) { - await DrizzleClient.update(users).set({ balance }).where(eq(users.userId, userId)); -} +export const economyService = { + transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: any) => { + if (amount <= 0n) { + throw new Error("Amount must be positive"); + } -export async function addUserBalance(userId: string, amount: number) { - const user = await DrizzleClient.query.users.findFirst({ where: eq(users.userId, userId) }); - if (!user) return; - await DrizzleClient.update(users).set({ balance: user.balance + amount }).where(eq(users.userId, userId)); -} \ No newline at end of file + if (fromUserId === toUserId) { + throw new Error("Cannot transfer to self"); + } + + const execute = async (txFn: any) => { + // Check sender balance + const sender = await txFn.query.users.findFirst({ + where: eq(users.id, BigInt(fromUserId)), + }); + + if (!sender) { + throw new Error("Sender not found"); + } + + if ((sender.balance ?? 0n) < amount) { + throw new Error("Insufficient funds"); + } + + // Deduct from sender + await txFn.update(users) + .set({ + balance: sql`${users.balance} - ${amount}`, + }) + .where(eq(users.id, BigInt(fromUserId))); + + // Add to receiver + await txFn.update(users) + .set({ + balance: sql`${users.balance} + ${amount}`, + }) + .where(eq(users.id, BigInt(toUserId))); + + // Create transaction records + // 1. Debit for sender + await txFn.insert(transactions).values({ + userId: BigInt(fromUserId), + amount: -amount, + type: 'TRANSFER_OUT', + description: `Transfer to ${toUserId}`, + }); + + // 2. Credit for receiver + await txFn.insert(transactions).values({ + userId: BigInt(toUserId), + amount: amount, + type: 'TRANSFER_IN', + description: `Transfer from ${fromUserId}`, + }); + + return { success: true, amount }; + }; + + if (tx) { + return await execute(tx); + } else { + return await DrizzleClient.transaction(async (t) => { + return await execute(t); + }); + } + }, + + claimDaily: async (userId: string, tx?: any) => { + const execute = async (txFn: any) => { + const now = new Date(); + const startOfDay = new Date(now); + startOfDay.setHours(0, 0, 0, 0); + + // Check cooldown + const cooldown = await txFn.query.cooldowns.findFirst({ + where: and( + eq(cooldowns.userId, BigInt(userId)), + eq(cooldowns.actionKey, 'daily') + ), + }); + + if (cooldown && cooldown.readyAt > now) { + throw new Error(`Daily already claimed. Ready at ${cooldown.readyAt}`); + } + + // Get user for streak logic + const user = await txFn.query.users.findFirst({ + where: eq(users.id, BigInt(userId)), + }); + + if (!user) { + throw new Error("User not found"); + } + + let streak = (user.dailyStreak || 0) + 1; + + // If previous cooldown exists and expired more than 24h ago (meaning >48h since last claim), reset streak + if (cooldown) { + const timeSinceReady = now.getTime() - cooldown.readyAt.getTime(); + if (timeSinceReady > 24 * 60 * 60 * 1000) { + streak = 1; + } + } else { + streak = 1; + } + + const bonus = BigInt(streak) * STREAK_BONUS; + + const totalReward = DAILY_REWARD_AMOUNT + bonus; + + // Update User w/ Economy Service (reuse modifyUserBalance if we split it out, but here manual is fine for atomic combined streak update) + // Actually, we can just update directly here as we are already refining specific fields like streak. + await txFn.update(users) + .set({ + balance: sql`${users.balance} + ${totalReward}`, + dailyStreak: streak, + xp: sql`${users.xp} + 10`, // Small XP reward for daily + }) + .where(eq(users.id, BigInt(userId))); + + // Set new cooldown (now + 24h) + const nextReadyAt = new Date(now.getTime() + DAILY_COOLDOWN); + + await txFn.insert(cooldowns) + .values({ + userId: BigInt(userId), + actionKey: 'daily', + readyAt: nextReadyAt, + }) + .onConflictDoUpdate({ + target: [cooldowns.userId, cooldowns.actionKey], + set: { readyAt: nextReadyAt }, + }); + + // Log Transaction + await txFn.insert(transactions).values({ + userId: BigInt(userId), + amount: totalReward, + type: 'DAILY_REWARD', + description: `Daily reward (Streak: ${streak})`, + }); + + return { claimed: true, amount: totalReward, streak, nextReadyAt }; + }; + + if (tx) { + return await execute(tx); + } else { + return await DrizzleClient.transaction(async (t) => { + return await execute(t); + }); + } + }, + + modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, tx?: any) => { + const execute = async (txFn: any) => { + if (amount < 0n) { + // Check sufficient funds if removing + const user = await txFn.query.users.findFirst({ + where: eq(users.id, BigInt(id)) + }); + if (!user || (user.balance ?? 0n) < -amount) { + throw new Error("Insufficient funds"); + } + } + + const [user] = await txFn.update(users) + .set({ + balance: sql`${users.balance} + ${amount}`, + }) + .where(eq(users.id, BigInt(id))) + .returning(); + + await txFn.insert(transactions).values({ + userId: BigInt(id), + amount: amount, + type: type, + description: description, + }); + + return user; + }; + + if (tx) { + return await execute(tx); + } else { + return await DrizzleClient.transaction(async (t) => { + return await execute(t); + }); + } + }, +}; diff --git a/src/modules/inventory/inventory.service.ts b/src/modules/inventory/inventory.service.ts new file mode 100644 index 0000000..ef726ae --- /dev/null +++ b/src/modules/inventory/inventory.service.ts @@ -0,0 +1,124 @@ + +import { inventory, items, users } from "@/db/schema"; +import { eq, and, sql } from "drizzle-orm"; +import { DrizzleClient } from "@/lib/DrizzleClient"; +import { economyService } from "@/modules/economy/economy.service"; + +export const inventoryService = { + addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => { + const execute = async (txFn: any) => { + // Check if item exists in inventory + const existing = await txFn.query.inventory.findFirst({ + where: and( + eq(inventory.userId, BigInt(userId)), + eq(inventory.itemId, itemId) + ), + }); + + if (existing) { + const [entry] = await txFn.update(inventory) + .set({ + quantity: sql`${inventory.quantity} + ${quantity}`, + }) + .where(and( + eq(inventory.userId, BigInt(userId)), + eq(inventory.itemId, itemId) + )) + .returning(); + return entry; + } else { + const [entry] = await txFn.insert(inventory) + .values({ + userId: BigInt(userId), + itemId: itemId, + quantity: quantity, + }) + .returning(); + return entry; + } + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + removeItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => { + const execute = async (txFn: any) => { + const existing = await txFn.query.inventory.findFirst({ + where: and( + eq(inventory.userId, BigInt(userId)), + eq(inventory.itemId, itemId) + ), + }); + + if (!existing || (existing.quantity ?? 0n) < quantity) { + throw new Error("Insufficient item quantity"); + } + + if ((existing.quantity ?? 0n) === quantity) { + // Delete if quantity becomes 0 + await txFn.delete(inventory) + .where(and( + eq(inventory.userId, BigInt(userId)), + eq(inventory.itemId, itemId) + )); + return { itemId, quantity: 0n, userId: BigInt(userId) }; + } else { + const [entry] = await txFn.update(inventory) + .set({ + quantity: sql`${inventory.quantity} - ${quantity}`, + }) + .where(and( + eq(inventory.userId, BigInt(userId)), + eq(inventory.itemId, itemId) + )) + .returning(); + return entry; + } + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + getInventory: async (userId: string) => { + return await DrizzleClient.query.inventory.findMany({ + where: eq(inventory.userId, BigInt(userId)), + with: { + item: true, + }, + }); + }, + + buyItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => { + const execute = async (txFn: any) => { + const item = await txFn.query.items.findFirst({ + where: eq(items.id, itemId), + }); + + if (!item) throw new Error("Item not found"); + if (!item.price) throw new Error("Item is not for sale"); + + const totalPrice = item.price * quantity; + + // Deduct Balance using economy service (passing tx ensures atomicity) + await economyService.modifyUserBalance(userId, -totalPrice, 'PURCHASE', `Bought ${quantity}x ${item.name}`, txFn); + + // Add Item (using local logic to keep in same tx, or could refactor addItem to take tx too and call it) + // Let's refactor addItem below to accept tx, then call it here? + // Since we are modifying buyItem, we can just inline the item addition or call addItem if we update it. + // Let's assume we update addItem next. For now, inline the add logic but cleaner. + + const existingInv = await txFn.query.inventory.findFirst({ + where: and(eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId)), + }); + + if (existingInv) { + await txFn.update(inventory).set({ quantity: sql`${inventory.quantity} + ${quantity}` }) + .where(and(eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId))); + } else { + await txFn.insert(inventory).values({ userId: BigInt(userId), itemId, quantity }); + } + + return { success: true, item, totalPrice }; + }; + + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + } +}; diff --git a/src/modules/leveling/leveling.service.ts b/src/modules/leveling/leveling.service.ts new file mode 100644 index 0000000..d6446a5 --- /dev/null +++ b/src/modules/leveling/leveling.service.ts @@ -0,0 +1,58 @@ +import { users } from "@/db/schema"; +import { eq, sql } from "drizzle-orm"; +import { DrizzleClient } from "@/lib/DrizzleClient"; + +// Simple configurable curve: Base * (Level ^ Exponent) +const XP_BASE = 1000; +const XP_EXPONENT = 1.5; + +export const levelingService = { + // Calculate XP required for a specific level + getXpForLevel: (level: number) => { + return Math.floor(XP_BASE * Math.pow(level, XP_EXPONENT)); + }, + + addXp: async (id: string, amount: bigint, tx?: any) => { + const execute = async (txFn: any) => { + // Get current state + const user = await txFn.query.users.findFirst({ + where: eq(users.id, BigInt(id)), + }); + + if (!user) throw new Error("User not found"); + + let newXp = (user.xp ?? 0n) + amount; + let currentLevel = user.level ?? 1; + let levelUp = false; + + // Check for level up loop + let xpForNextLevel = BigInt(levelingService.getXpForLevel(currentLevel)); + + while (newXp >= xpForNextLevel) { + newXp -= xpForNextLevel; + currentLevel++; + levelUp = true; + xpForNextLevel = BigInt(levelingService.getXpForLevel(currentLevel)); + } + + // Update user + const [updatedUser] = await txFn.update(users) + .set({ + xp: newXp, + level: currentLevel, + }) + .where(eq(users.id, BigInt(id))) + .returning(); + + return { user: updatedUser, levelUp, currentLevel }; + } + + if (tx) { + return await execute(tx); + } else { + return await DrizzleClient.transaction(async (t) => { + return await execute(t); + }) + } + }, +}; diff --git a/src/modules/quest/quest.service.ts b/src/modules/quest/quest.service.ts new file mode 100644 index 0000000..9d4cbd3 --- /dev/null +++ b/src/modules/quest/quest.service.ts @@ -0,0 +1,89 @@ + +import { quests, userQuests, users } from "@/db/schema"; +import { eq, and, sql } from "drizzle-orm"; +import { DrizzleClient } from "@/lib/DrizzleClient"; +import { economyService } from "@/modules/economy/economy.service"; +import { levelingService } from "@/modules/leveling/leveling.service"; + +export const questService = { + assignQuest: async (userId: string, questId: number, tx?: any) => { + const execute = async (txFn: any) => { + return await txFn.insert(userQuests) + .values({ + userId: BigInt(userId), + questId: questId, + progress: 0, + }) + .onConflictDoNothing() // Ignore if already assigned + .returning(); + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + updateProgress: async (userId: string, questId: number, progress: number, tx?: any) => { + const execute = async (txFn: any) => { + return await txFn.update(userQuests) + .set({ progress: progress }) + .where(and( + eq(userQuests.userId, BigInt(userId)), + eq(userQuests.questId, questId) + )) + .returning(); + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + completeQuest: async (userId: string, questId: number, tx?: any) => { + const execute = async (txFn: any) => { + const userQuest = await txFn.query.userQuests.findFirst({ + where: and( + eq(userQuests.userId, BigInt(userId)), + eq(userQuests.questId, questId) + ), + with: { + quest: true, + } + }); + + if (!userQuest) throw new Error("Quest not assigned"); + if (userQuest.completedAt) throw new Error("Quest already completed"); + + // Mark completed + await txFn.update(userQuests) + .set({ completedAt: new Date() }) + .where(and( + eq(userQuests.userId, BigInt(userId)), + eq(userQuests.questId, questId) + )); + + // Distribute Rewards + const rewards = userQuest.quest.rewards as { xp?: number, balance?: number }; + const results = { xp: 0n, balance: 0n }; + + if (rewards?.balance) { + const bal = BigInt(rewards.balance); + await economyService.modifyUserBalance(userId, bal, 'QUEST_REWARD', `Reward for quest ${questId}`, txFn); + results.balance = bal; + } + + if (rewards?.xp) { + const xp = BigInt(rewards.xp); + await levelingService.addXp(userId, xp, txFn); + results.xp = xp; + } + + return { success: true, rewards: results }; + }; + + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + getUserQuests: async (userId: string) => { + return await DrizzleClient.query.userQuests.findMany({ + where: eq(userQuests.userId, BigInt(userId)), + with: { + quest: true, + } + }); + } +}; diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts new file mode 100644 index 0000000..2044116 --- /dev/null +++ b/src/modules/user/user.service.ts @@ -0,0 +1,44 @@ +import { users } from "@/db/schema"; +import { eq, sql } from "drizzle-orm"; +import { DrizzleClient } from "@/lib/DrizzleClient"; + +export const userService = { + getUserById: async (id: string) => { + const user = await DrizzleClient.query.users.findFirst({ + where: eq(users.id, BigInt(id)), + with: { class: true } + }); + return user; + }, + getUserByUsername: async (username: string) => { + const user = await DrizzleClient.query.users.findFirst({ where: eq(users.username, username) }); + return user; + }, + createUser: async (id: string | bigint, username: string, classId?: bigint, tx?: any) => { + const execute = async (txFn: any) => { + const [user] = await txFn.insert(users).values({ + id: BigInt(id), + username, + classId, + }).returning(); + return user; + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + updateUser: async (id: string, data: Partial, tx?: any) => { + const execute = async (txFn: any) => { + const [user] = await txFn.update(users) + .set(data) + .where(eq(users.id, BigInt(id))) + .returning(); + return user; + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + deleteUser: async (id: string, tx?: any) => { + const execute = async (txFn: any) => { + await txFn.delete(users).where(eq(users.id, BigInt(id))); + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, +}; \ No newline at end of file diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts deleted file mode 100644 index c93e57c..0000000 --- a/src/modules/users/users.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DrizzleClient } from "@lib/DrizzleClient"; -import { users } from "@/db/schema"; -import { eq } from "drizzle-orm"; - -export async function getUserById(userId: string) { - return await DrizzleClient.query.users.findFirst({ where: eq(users.userId, userId) }); -} - -export async function createUser(userId: string) { - return (await DrizzleClient.insert(users).values({ userId }).returning())[0]!; -} - -export async function updateUserDaily(userId: string, lastDaily: Date, dailyStreak: number) { - await DrizzleClient.update(users).set({ lastDaily, dailyStreak }).where(eq(users.userId, userId)); -} \ No newline at end of file diff --git a/src/scripts/read-env.ts b/src/scripts/read-env.ts new file mode 100644 index 0000000..f6b715a --- /dev/null +++ b/src/scripts/read-env.ts @@ -0,0 +1,9 @@ + +import { readFile } from "fs/promises"; + +try { + const content = await readFile(".env", "utf-8"); + console.log(content); +} catch (e) { + console.error(e); +} diff --git a/src/scripts/test-db.ts b/src/scripts/test-db.ts new file mode 100644 index 0000000..91b0333 --- /dev/null +++ b/src/scripts/test-db.ts @@ -0,0 +1,23 @@ + +import { userService } from "@/modules/user/user.service"; +import { DrizzleClient } from "@/lib/DrizzleClient"; +import { users } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +console.log("Starting test..."); + +try { + const id = "109998942841765888"; + + console.log("Fetching user..."); + const user = await userService.getUserById(id); + console.log("User fetched:", user); + + if (user?.class) { + console.log("User class fetched:", user.class); + } +} catch (e) { + console.error("Error:", e); +} + +process.exit(0); diff --git a/src/scripts/test-env.ts b/src/scripts/test-env.ts new file mode 100644 index 0000000..ccac329 --- /dev/null +++ b/src/scripts/test-env.ts @@ -0,0 +1,4 @@ + +import { env } from "@lib/env"; + +console.log("DATABASE_URL:", env.DATABASE_URL); diff --git a/src/scripts/write-env.ts b/src/scripts/write-env.ts new file mode 100644 index 0000000..89575b3 --- /dev/null +++ b/src/scripts/write-env.ts @@ -0,0 +1,20 @@ + +import { writeFile } from "fs/promises"; + +const content = `DB_USER=kyoko +DB_PASSWORD=kyoko +DB_NAME=kyoko +DB_PORT=5432 +DB_HOST=localhost +DISCORD_BOT_TOKEN=MTQ0Mzg4NTA3NDM0ODExODA5OA.GcX7aT.S6G2jWqLmPAOx04JBHhJn7TCPsx5pK5RMUxN3g +DISCORD_CLIENT_ID=1443885074348118098 +DISCORD_GUILD_ID=1443887793565728870 +DATABASE_URL=postgres://kyoko:kyoko@localhost:5432/kyoko +`; + +try { + await writeFile(".env", content); + console.log("Updated .env"); +} catch (e) { + console.error(e); +}