docs: add JSDoc to service public methods

One-line JSDoc on 82 methods across 11 service files for quick
scanning without reading full implementations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-04-02 11:36:18 +02:00
parent 5f8819bb46
commit 5bd390b4ee
11 changed files with 82 additions and 10 deletions

View File

@@ -6,10 +6,12 @@ import { withTransaction } from "@/lib/db";
import type { Transaction } from "@shared/lib/types"; import type { Transaction } from "@shared/lib/types";
export const classService = { export const classService = {
/** Retrieve all available classes. */
getAllClasses: async () => { getAllClasses: async () => {
return await DrizzleClient.query.classes.findMany(); return await DrizzleClient.query.classes.findMany();
}, },
/** Assign a class to a user; throws if the class does not exist. */
assignClass: async (userId: string, classId: bigint, tx?: Transaction) => { assignClass: async (userId: string, classId: bigint, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const cls = await txFn.query.classes.findFirst({ const cls = await txFn.query.classes.findFirst({
@@ -26,12 +28,14 @@ export const classService = {
return user; return user;
}, tx); }, tx);
}, },
/** Get the current balance for a class, returning 0 if not found. */
getClassBalance: async (classId: bigint) => { getClassBalance: async (classId: bigint) => {
const cls = await DrizzleClient.query.classes.findFirst({ const cls = await DrizzleClient.query.classes.findFirst({
where: eq(classes.id, classId), where: eq(classes.id, classId),
}); });
return cls?.balance || 0n; return cls?.balance || 0n;
}, },
/** Adjust a class balance by the given amount; throws if funds are insufficient for a deduction. */
modifyClassBalance: async (classId: bigint, amount: bigint, tx?: Transaction) => { modifyClassBalance: async (classId: bigint, amount: bigint, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const cls = await txFn.query.classes.findFirst({ const cls = await txFn.query.classes.findFirst({
@@ -55,6 +59,7 @@ export const classService = {
}, tx); }, tx);
}, },
/** Update a class record with partial data. */
updateClass: async (id: bigint, data: Partial<typeof classes.$inferInsert>, tx?: Transaction) => { updateClass: async (id: bigint, data: Partial<typeof classes.$inferInsert>, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const [updatedClass] = await txFn.update(classes) const [updatedClass] = await txFn.update(classes)
@@ -65,6 +70,7 @@ export const classService = {
}, tx); }, tx);
}, },
/** Create a new class record. */
createClass: async (data: typeof classes.$inferInsert, tx?: Transaction) => { createClass: async (data: typeof classes.$inferInsert, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const [newClass] = await txFn.insert(classes) const [newClass] = await txFn.insert(classes)
@@ -74,6 +80,7 @@ export const classService = {
}, tx); }, tx);
}, },
/** Delete a class by ID. */
deleteClass: async (id: bigint, tx?: Transaction) => { deleteClass: async (id: bigint, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
await txFn.delete(classes).where(eq(classes.id, id)); await txFn.delete(classes).where(eq(classes.id, id));

View File

@@ -8,6 +8,7 @@ import { UserError } from "@shared/lib/errors";
import { TimerKey, TimerType, TransactionType } from "@shared/lib/constants"; import { TimerKey, TimerType, TransactionType } from "@shared/lib/constants";
export const economyService = { export const economyService = {
/** Transfer currency between two users; validates sufficient balance and creates transaction records for both sides. */
transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => { transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => {
if (amount <= 0n) { if (amount <= 0n) {
throw new UserError("Amount must be positive"); throw new UserError("Amount must be positive");
@@ -69,6 +70,7 @@ export const economyService = {
}, tx); }, tx);
}, },
/** Claim the daily reward, applying streak bonuses and weekly bonuses; enforces a UTC-midnight cooldown. */
claimDaily: async (userId: string, tx?: Transaction) => { claimDaily: async (userId: string, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const now = new Date(); const now = new Date();
@@ -164,6 +166,7 @@ export const economyService = {
}, tx); }, tx);
}, },
/** Adjust a user's balance by the given amount and log a transaction; throws if deduction exceeds current balance. */
modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, relatedUserId?: string | null, tx?: Transaction) => { modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, relatedUserId?: string | null, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
if (amount < 0n) { if (amount < 0n) {

View File

@@ -213,12 +213,20 @@ async function clearCaches() {
} }
export const lootdropService = { export const lootdropService = {
/** Delete expired lootdrops from the database; optionally includes already-claimed ones. */
cleanupExpiredLootdrops, cleanupExpiredLootdrops,
/** Record a message in a channel and return whether a lootdrop should spawn based on activity and RNG. */
trackActivity, trackActivity,
/** Calculate a random lootdrop reward amount and currency, with optional overrides. */
calculateReward, calculateReward,
/** Save a spawned lootdrop to the database with a 10-minute expiration. */
persistLootdrop, persistLootdrop,
/** Remove a lootdrop by message ID and return its channel ID for Discord cleanup. */
removeLootdrop, removeLootdrop,
/** Atomically claim a lootdrop for a user; credits reward to their balance. */
tryClaim, tryClaim,
/** Get current lootdrop system state including the most active channel and spawn config. */
getLootdropState, getLootdropState,
/** Clear all in-memory activity tracking and cooldown caches. */
clearCaches, clearCaches,
}; };

View File

@@ -10,6 +10,7 @@ export interface FeatureFlagContext {
} }
export const featureFlagsService = { export const featureFlagsService = {
/** Check whether a feature flag is enabled globally. */
isFlagEnabled: async (flagName: string): Promise<boolean> => { isFlagEnabled: async (flagName: string): Promise<boolean> => {
const flag = await DrizzleClient.query.featureFlags.findFirst({ const flag = await DrizzleClient.query.featureFlags.findFirst({
where: eq(featureFlags.name, flagName), where: eq(featureFlags.name, flagName),
@@ -17,6 +18,7 @@ export const featureFlagsService = {
return flag?.enabled ?? false; return flag?.enabled ?? false;
}, },
/** Check if a guild/user/role has access to a feature flag; returns false if flag is disabled. */
hasAccess: async ( hasAccess: async (
flagName: string, flagName: string,
context: FeatureFlagContext context: FeatureFlagContext
@@ -51,6 +53,7 @@ export const featureFlagsService = {
return false; return false;
}, },
/** Create a new feature flag, disabled by default. */
createFlag: async (name: string, description?: string) => { createFlag: async (name: string, description?: string) => {
const [flag] = await DrizzleClient.insert(featureFlags).values({ const [flag] = await DrizzleClient.insert(featureFlags).values({
name, name,
@@ -60,6 +63,7 @@ export const featureFlagsService = {
return flag; return flag;
}, },
/** Enable or disable a feature flag by name; throws if the flag does not exist. */
setFlagEnabled: async (name: string, enabled: boolean) => { setFlagEnabled: async (name: string, enabled: boolean) => {
const [flag] = await DrizzleClient.update(featureFlags) const [flag] = await DrizzleClient.update(featureFlags)
.set({ enabled, updatedAt: new Date() }) .set({ enabled, updatedAt: new Date() })
@@ -72,6 +76,7 @@ export const featureFlagsService = {
return flag; return flag;
}, },
/** Grant a guild, user, or role access to a feature flag. */
grantAccess: async ( grantAccess: async (
flagName: string, flagName: string,
access: { guildId?: string; userId?: string; roleId?: string } access: { guildId?: string; userId?: string; roleId?: string }
@@ -90,6 +95,7 @@ export const featureFlagsService = {
return accessRecord; return accessRecord;
}, },
/** Revoke a specific access record by ID; throws if not found. */
revokeAccess: async (accessId: number) => { revokeAccess: async (accessId: number) => {
const [access] = await DrizzleClient.delete(featureFlagAccess) const [access] = await DrizzleClient.delete(featureFlagAccess)
.where(eq(featureFlagAccess.id, accessId)) .where(eq(featureFlagAccess.id, accessId))
@@ -101,18 +107,21 @@ export const featureFlagsService = {
return access; return access;
}, },
/** Retrieve a single feature flag by name. */
getFlag: async (name: string) => { getFlag: async (name: string) => {
return await DrizzleClient.query.featureFlags.findFirst({ return await DrizzleClient.query.featureFlags.findFirst({
where: eq(featureFlags.name, name), where: eq(featureFlags.name, name),
}); });
}, },
/** List all feature flags, ordered by name. */
listFlags: async () => { listFlags: async () => {
return await DrizzleClient.query.featureFlags.findMany({ return await DrizzleClient.query.featureFlags.findMany({
orderBy: (flags, { asc }) => [asc(flags.name)], orderBy: (flags, { asc }) => [asc(flags.name)],
}); });
}, },
/** List all access records for a given feature flag. */
listAccess: async (flagName: string) => { listAccess: async (flagName: string) => {
const flag = await DrizzleClient.query.featureFlags.findFirst({ const flag = await DrizzleClient.query.featureFlags.findFirst({
where: eq(featureFlags.name, flagName), where: eq(featureFlags.name, flagName),
@@ -125,6 +134,7 @@ export const featureFlagsService = {
}); });
}, },
/** Delete a feature flag and its associated access records; throws if not found. */
deleteFlag: async (name: string) => { deleteFlag: async (name: string) => {
const [flag] = await DrizzleClient.delete(featureFlags) const [flag] = await DrizzleClient.delete(featureFlags)
.where(eq(featureFlags.name, name)) .where(eq(featureFlags.name, name))

View File

@@ -29,6 +29,7 @@ let cacheTimestamp = 0;
const CACHE_TTL_MS = 30000; const CACHE_TTL_MS = 30000;
export const gameSettingsService = { export const gameSettingsService = {
/** Retrieve game settings, using a 30-second TTL cache by default. */
getSettings: async (useCache = true): Promise<GameSettingsData | null> => { getSettings: async (useCache = true): Promise<GameSettingsData | null> => {
if (useCache && cachedSettings && Date.now() - cacheTimestamp < CACHE_TTL_MS) { if (useCache && cachedSettings && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
return cachedSettings; return cachedSettings;
@@ -56,6 +57,7 @@ export const gameSettingsService = {
return cachedSettings; return cachedSettings;
}, },
/** Create or update game settings, merging with existing values and invalidating cache. */
upsertSettings: async (data: Partial<GameSettingsData>) => { upsertSettings: async (data: Partial<GameSettingsData>) => {
const existing = await gameSettingsService.getSettings(false); const existing = await gameSettingsService.getSettings(false);
@@ -86,6 +88,7 @@ export const gameSettingsService = {
return result; return result;
}, },
/** Update a single configuration section (e.g., "leveling", "economy") and invalidate cache. */
updateSection: async <K extends keyof GameSettingsData>( updateSection: async <K extends keyof GameSettingsData>(
section: K, section: K,
value: GameSettingsData[K] value: GameSettingsData[K]
@@ -102,6 +105,7 @@ export const gameSettingsService = {
gameSettingsService.invalidateCache(); gameSettingsService.invalidateCache();
}, },
/** Enable or disable a specific command in the game settings. */
toggleCommand: async (commandName: string, enabled: boolean) => { toggleCommand: async (commandName: string, enabled: boolean) => {
const settings = await gameSettingsService.getSettings(false); const settings = await gameSettingsService.getSettings(false);
@@ -117,11 +121,13 @@ export const gameSettingsService = {
await gameSettingsService.updateSection("commands", commands); await gameSettingsService.updateSection("commands", commands);
}, },
/** Invalidate the in-memory settings cache, forcing a fresh DB read on next access. */
invalidateCache: () => { invalidateCache: () => {
cachedSettings = null; cachedSettings = null;
cacheTimestamp = 0; cacheTimestamp = 0;
}, },
/** Return default leveling configuration values. */
getDefaultLeveling: (): LevelingConfig => ({ getDefaultLeveling: (): LevelingConfig => ({
base: 100, base: 100,
exponent: 1.5, exponent: 1.5,
@@ -132,6 +138,7 @@ export const gameSettingsService = {
}, },
}), }),
/** Return default economy configuration values. */
getDefaultEconomy: (): EconomyConfig => ({ getDefaultEconomy: (): EconomyConfig => ({
daily: { daily: {
amount: "100", amount: "100",
@@ -149,11 +156,13 @@ export const gameSettingsService = {
}, },
}), }),
/** Return default inventory configuration values. */
getDefaultInventory: (): InventoryConfig => ({ getDefaultInventory: (): InventoryConfig => ({
maxStackSize: "99", maxStackSize: "99",
maxSlots: 20, maxSlots: 20,
}), }),
/** Return default lootdrop configuration values. */
getDefaultLootdrop: (): LootdropConfig => ({ getDefaultLootdrop: (): LootdropConfig => ({
activityWindowMs: 300000, activityWindowMs: 300000,
minMessages: 5, minMessages: 5,
@@ -166,6 +175,7 @@ export const gameSettingsService = {
}, },
}), }),
/** Return default trivia configuration values. */
getDefaultTrivia: (): TriviaConfig => ({ getDefaultTrivia: (): TriviaConfig => ({
entryFee: "50", entryFee: "50",
rewardMultiplier: 1.8, rewardMultiplier: 1.8,
@@ -175,6 +185,7 @@ export const gameSettingsService = {
difficulty: "random", difficulty: "random",
}), }),
/** Return default moderation configuration values. */
getDefaultModeration: (): ModerationConfig => ({ getDefaultModeration: (): ModerationConfig => ({
prune: { prune: {
maxAmount: 100, maxAmount: 100,
@@ -184,10 +195,12 @@ export const gameSettingsService = {
}, },
}), }),
/** Return default quest configuration values. */
getDefaultQuest: (): QuestConfig => ({ getDefaultQuest: (): QuestConfig => ({
maxActiveQuests: 3, maxActiveQuests: 3,
}), }),
/** Return the complete set of default game settings across all sections. */
getDefaults: (): GameSettingsData => ({ getDefaults: (): GameSettingsData => ({
leveling: gameSettingsService.getDefaultLeveling(), leveling: gameSettingsService.getDefaultLeveling(),
economy: gameSettingsService.getDefaultEconomy(), economy: gameSettingsService.getDefaultEconomy(),

View File

@@ -20,6 +20,7 @@ export interface GuildSettingsData {
} }
export const guildSettingsService = { export const guildSettingsService = {
/** Retrieve guild settings by guild ID, or null if none exist. */
getSettings: async (guildId: string): Promise<GuildSettingsData | null> => { getSettings: async (guildId: string): Promise<GuildSettingsData | null> => {
const settings = await DrizzleClient.query.guildSettings.findFirst({ const settings = await DrizzleClient.query.guildSettings.findFirst({
where: eq(guildSettings.guildId, BigInt(guildId)), where: eq(guildSettings.guildId, BigInt(guildId)),
@@ -44,6 +45,7 @@ export const guildSettingsService = {
}; };
}, },
/** Create or fully replace guild settings via upsert. */
upsertSettings: async (data: Partial<GuildSettingsData> & { guildId: string }) => { upsertSettings: async (data: Partial<GuildSettingsData> & { guildId: string }) => {
const values: typeof guildSettings.$inferInsert = { const values: typeof guildSettings.$inferInsert = {
guildId: BigInt(data.guildId), guildId: BigInt(data.guildId),
@@ -73,6 +75,7 @@ export const guildSettingsService = {
return result; return result;
}, },
/** Update a single guild setting by key name; throws if the key is unknown or settings do not exist. */
updateSetting: async ( updateSetting: async (
guildId: string, guildId: string,
key: string, key: string,
@@ -128,6 +131,7 @@ export const guildSettingsService = {
return result; return result;
}, },
/** Delete all settings for a guild. */
deleteSettings: async (guildId: string) => { deleteSettings: async (guildId: string) => {
const [result] = await DrizzleClient.delete(guildSettings) const [result] = await DrizzleClient.delete(guildSettings)
.where(eq(guildSettings.guildId, BigInt(guildId))) .where(eq(guildSettings.guildId, BigInt(guildId)))
@@ -136,6 +140,7 @@ export const guildSettingsService = {
return result; return result;
}, },
/** Add a color role to the guild's allowed list; no-ops if already present. */
addColorRole: async (guildId: string, roleId: string) => { addColorRole: async (guildId: string, roleId: string) => {
const settings = await guildSettingsService.getSettings(guildId); const settings = await guildSettingsService.getSettings(guildId);
const colorRoleIds = settings?.colorRoleIds ?? []; const colorRoleIds = settings?.colorRoleIds ?? [];
@@ -148,6 +153,7 @@ export const guildSettingsService = {
return await guildSettingsService.upsertSettings({ guildId, colorRoleIds }); return await guildSettingsService.upsertSettings({ guildId, colorRoleIds });
}, },
/** Remove a color role from the guild's allowed list. */
removeColorRole: async (guildId: string, roleId: string) => { removeColorRole: async (guildId: string, roleId: string) => {
const settings = await guildSettingsService.getSettings(guildId); const settings = await guildSettingsService.getSettings(guildId);
if (!settings) return null; if (!settings) return null;

View File

@@ -13,6 +13,7 @@ import { TransactionType } from "@shared/lib/constants";
export const inventoryService = { export const inventoryService = {
/** Add items to a user's inventory; enforces max stack size and max slot limits. */
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => { addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
// Check if item exists in inventory // Check if item exists in inventory
@@ -74,6 +75,7 @@ export const inventoryService = {
}, tx); }, tx);
}, },
/** Remove items from a user's inventory; deletes the entry if quantity reaches zero. */
removeItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => { removeItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const existing = await txFn.query.inventory.findFirst({ const existing = await txFn.query.inventory.findFirst({
@@ -110,6 +112,7 @@ export const inventoryService = {
}, tx); }, tx);
}, },
/** Retrieve all inventory entries for a user, including item details. */
getInventory: async (userId: string) => { getInventory: async (userId: string) => {
return await DrizzleClient.query.inventory.findMany({ return await DrizzleClient.query.inventory.findMany({
where: eq(inventory.userId, BigInt(userId)), where: eq(inventory.userId, BigInt(userId)),
@@ -119,6 +122,7 @@ export const inventoryService = {
}); });
}, },
/** Purchase an item from the shop; deducts balance and adds to inventory atomically. */
buyItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => { buyItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const item = await txFn.query.items.findFirst({ const item = await txFn.query.items.findFirst({
@@ -139,12 +143,14 @@ export const inventoryService = {
}, tx); }, tx);
}, },
/** Fetch a single item definition by ID. */
getItem: async (itemId: number) => { getItem: async (itemId: number) => {
return await DrizzleClient.query.items.findFirst({ return await DrizzleClient.query.items.findFirst({
where: eq(items.id, itemId), where: eq(items.id, itemId),
}); });
}, },
/** Use a consumable item, applying its effects and consuming it if configured. */
useItem: async (userId: string, itemId: number, tx?: Transaction) => { useItem: async (userId: string, itemId: number, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
// 1. Check Ownership & Quantity // 1. Check Ownership & Quantity
@@ -189,6 +195,7 @@ export const inventoryService = {
}, tx); }, tx);
}, },
/** Search a user's usable inventory items by name for autocomplete suggestions. */
getAutocompleteItems: async (userId: string, query: string) => { getAutocompleteItems: async (userId: string, query: string) => {
const entries = await DrizzleClient.select({ const entries = await DrizzleClient.select({
quantity: inventory.quantity, quantity: inventory.quantity,

View File

@@ -8,11 +8,7 @@ import { TimerKey, TimerType } from "@shared/lib/constants";
import { UserError } from "@shared/lib/errors"; import { UserError } from "@shared/lib/errors";
export const levelingService = { export const levelingService = {
// Calculate total XP required to REACH a specific level (Cumulative) /** Calculate the cumulative XP required to reach a specific level. */
// Level 1 = 0 XP
// Level 2 = Base * (1^Exp)
// Level 3 = Level 2 + Base * (2^Exp)
// ...
getXpToReachLevel: (level: number) => { getXpToReachLevel: (level: number) => {
let total = 0; let total = 0;
for (let l = 1; l < level; l++) { for (let l = 1; l < level; l++) {
@@ -21,7 +17,7 @@ export const levelingService = {
return total; return total;
}, },
// Calculate level from Total XP /** Derive a user's level from their total accumulated XP. */
getLevelFromXp: (totalXp: bigint) => { getLevelFromXp: (totalXp: bigint) => {
let level = 1; let level = 1;
let xp = Number(totalXp); let xp = Number(totalXp);
@@ -37,13 +33,12 @@ export const levelingService = {
} }
}, },
// Get XP needed to complete the current level (for calculating next level threshold in isolation) /** Get the XP needed to advance from the given level to the next. */
// Used internally or for display
getXpForNextLevel: (currentLevel: number) => { getXpForNextLevel: (currentLevel: number) => {
return Math.floor(config.leveling.base * Math.pow(currentLevel, config.leveling.exponent)); return Math.floor(config.leveling.base * Math.pow(currentLevel, config.leveling.exponent));
}, },
// Cumulative XP addition /** Add XP to a user, recalculating their level and emitting a quest event. */
addXp: async (id: string, amount: bigint, tx?: Transaction) => { addXp: async (id: string, amount: bigint, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
// Get current state // Get current state
@@ -77,7 +72,7 @@ export const levelingService = {
}, tx); }, tx);
}, },
// Handle chat XP with cooldowns /** Award random chat XP if the user is not on cooldown; applies active XP boost multipliers. */
processChatXp: async (id: string, tx?: Transaction) => { processChatXp: async (id: string, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
// check if an xp cooldown is in place // check if an xp cooldown is in place

View File

@@ -11,6 +11,7 @@ import { TransactionType } from "@shared/lib/constants";
import { systemEvents, EVENTS } from "@shared/lib/events"; import { systemEvents, EVENTS } from "@shared/lib/events";
export const questService = { export const questService = {
/** Assign a quest to a user; enforces the maximum active quest limit. */
assignQuest: async (userId: string, questId: number, tx?: Transaction) => { assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
// Check active quest limit // Check active quest limit
@@ -40,6 +41,7 @@ export const questService = {
}, tx); }, tx);
}, },
/** Set the progress value for a user's active quest. */
updateProgress: async (userId: string, questId: number, progress: number, tx?: Transaction) => { updateProgress: async (userId: string, questId: number, progress: number, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
return await txFn.update(userQuests) return await txFn.update(userQuests)
@@ -52,6 +54,7 @@ export const questService = {
}, tx); }, tx);
}, },
/** Process a domain event against active quests, incrementing progress and auto-completing if target is met. */
handleEvent: async (userId: string, eventName: string, weight: number = 1, tx?: Transaction) => { handleEvent: async (userId: string, eventName: string, weight: number = 1, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
// 1. Fetch active user quests for this event // 1. Fetch active user quests for this event
@@ -86,6 +89,7 @@ export const questService = {
}, tx); }, tx);
}, },
/** Mark a quest as completed and distribute its XP and balance rewards. */
completeQuest: async (userId: string, questId: number, tx?: Transaction) => { completeQuest: async (userId: string, questId: number, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const userQuest = await txFn.query.userQuests.findFirst({ const userQuest = await txFn.query.userQuests.findFirst({
@@ -137,6 +141,7 @@ export const questService = {
}, tx); }, tx);
}, },
/** Get all quests assigned to a user, including quest details. */
getUserQuests: async (userId: string) => { getUserQuests: async (userId: string) => {
return await DrizzleClient.query.userQuests.findMany({ return await DrizzleClient.query.userQuests.findMany({
where: eq(userQuests.userId, BigInt(userId)), where: eq(userQuests.userId, BigInt(userId)),
@@ -146,6 +151,7 @@ export const questService = {
}); });
}, },
/** Get quests not yet assigned to a user. */
async getAvailableQuests(userId: string) { async getAvailableQuests(userId: string) {
const userQuestIds = (await DrizzleClient.query.userQuests.findMany({ const userQuestIds = (await DrizzleClient.query.userQuests.findMany({
where: eq(userQuests.userId, BigInt(userId)), where: eq(userQuests.userId, BigInt(userId)),
@@ -161,6 +167,7 @@ export const questService = {
}); });
}, },
/** Create a new quest definition with trigger event, requirements, and rewards. */
async createQuest(data: { async createQuest(data: {
name: string; name: string;
description: string; description: string;
@@ -181,12 +188,14 @@ export const questService = {
}, tx); }, tx);
}, },
/** List all quest definitions, ordered by ID. */
async getAllQuests() { async getAllQuests() {
return await DrizzleClient.query.quests.findMany({ return await DrizzleClient.query.quests.findMany({
orderBy: (quests, { asc }) => [asc(quests.id)], orderBy: (quests, { asc }) => [asc(quests.id)],
}); });
}, },
/** Delete a quest definition by ID. */
async deleteQuest(id: number, tx?: Transaction) { async deleteQuest(id: number, tx?: Transaction) {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
return await txFn.delete(quests) return await txFn.delete(quests)
@@ -195,6 +204,7 @@ export const questService = {
}, tx); }, tx);
}, },
/** Update a quest definition with partial data. */
async updateQuest(id: number, data: { async updateQuest(id: number, data: {
name?: string; name?: string;
description?: string; description?: string;

View File

@@ -101,10 +101,12 @@ export const tradeService = {
return session; return session;
}, },
/** Get an active trade session by thread ID. */
getSession: (threadId: string): TradeSession | undefined => { getSession: (threadId: string): TradeSession | undefined => {
return sessions.get(threadId); return sessions.get(threadId);
}, },
/** Remove a trade session from memory. */
endSession: (threadId: string) => { endSession: (threadId: string) => {
sessions.delete(threadId); sessions.delete(threadId);
}, },
@@ -126,6 +128,7 @@ export const tradeService = {
session.lastInteraction = Date.now(); session.lastInteraction = Date.now();
}, },
/** Add an item to a participant's trade offer; unlocks both sides when the offer changes. */
addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => { addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => {
const session = tradeService.getSession(threadId); const session = tradeService.getSession(threadId);
if (!session) throw new UserError("Session not found"); if (!session) throw new UserError("Session not found");
@@ -145,6 +148,7 @@ export const tradeService = {
session.lastInteraction = Date.now(); session.lastInteraction = Date.now();
}, },
/** Remove an item from a participant's trade offer; unlocks both sides. */
removeItem: (threadId: string, userId: string, itemId: number) => { removeItem: (threadId: string, userId: string, itemId: number) => {
const session = tradeService.getSession(threadId); const session = tradeService.getSession(threadId);
if (!session) throw new UserError("Session not found"); if (!session) throw new UserError("Session not found");
@@ -158,6 +162,7 @@ export const tradeService = {
session.lastInteraction = Date.now(); session.lastInteraction = Date.now();
}, },
/** Toggle a participant's lock status on their offer; returns the new lock state. */
toggleLock: (threadId: string, userId: string): boolean => { toggleLock: (threadId: string, userId: string): boolean => {
const session = tradeService.getSession(threadId); const session = tradeService.getSession(threadId);
if (!session) throw new UserError("Session not found"); if (!session) throw new UserError("Session not found");
@@ -199,6 +204,7 @@ export const tradeService = {
tradeService.endSession(threadId); tradeService.endSession(threadId);
}, },
/** Clear all active trade sessions from memory. */
clearSessions: () => { clearSessions: () => {
sessions.clear(); sessions.clear();
console.log("[TradeService] All active trade sessions cleared."); console.log("[TradeService] All active trade sessions cleared.");

View File

@@ -5,6 +5,7 @@ import { withTransaction } from "@/lib/db";
import type { Transaction } from "@shared/lib/types"; import type { Transaction } from "@shared/lib/types";
export const userService = { export const userService = {
/** Fetch a user by Discord ID, including their class relation. */
getUserById: async (id: string) => { getUserById: async (id: string) => {
const user = await DrizzleClient.query.users.findFirst({ const user = await DrizzleClient.query.users.findFirst({
where: eq(users.id, BigInt(id)), where: eq(users.id, BigInt(id)),
@@ -12,10 +13,12 @@ export const userService = {
}); });
return user; return user;
}, },
/** Fetch a user by their username. */
getUserByUsername: async (username: string) => { getUserByUsername: async (username: string) => {
const user = await DrizzleClient.query.users.findFirst({ where: eq(users.username, username) }); const user = await DrizzleClient.query.users.findFirst({ where: eq(users.username, username) });
return user; return user;
}, },
/** Fetch a user by ID, creating a new record if one does not exist. */
getOrCreateUser: async (id: string, username: string, tx?: Transaction) => { getOrCreateUser: async (id: string, username: string, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
let user = await txFn.query.users.findFirst({ let user = await txFn.query.users.findFirst({
@@ -38,6 +41,7 @@ export const userService = {
return user; return user;
}, tx); }, tx);
}, },
/** Get the class assigned to a user, or undefined if none. */
getUserClass: async (id: string) => { getUserClass: async (id: string) => {
const user = await DrizzleClient.query.users.findFirst({ const user = await DrizzleClient.query.users.findFirst({
where: eq(users.id, BigInt(id)), where: eq(users.id, BigInt(id)),
@@ -45,6 +49,7 @@ export const userService = {
}); });
return user?.class; return user?.class;
}, },
/** Create a new user with an optional initial class assignment. */
createUser: async (id: string | bigint, username: string, classId?: bigint, tx?: Transaction) => { createUser: async (id: string | bigint, username: string, classId?: bigint, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const [user] = await txFn.insert(users).values({ const [user] = await txFn.insert(users).values({
@@ -55,6 +60,7 @@ export const userService = {
return user; return user;
}, tx); }, tx);
}, },
/** Update a user record with partial data. */
updateUser: async (id: string, data: Partial<typeof users.$inferInsert>, tx?: Transaction) => { updateUser: async (id: string, data: Partial<typeof users.$inferInsert>, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
const [user] = await txFn.update(users) const [user] = await txFn.update(users)
@@ -64,6 +70,7 @@ export const userService = {
return user; return user;
}, tx); }, tx);
}, },
/** Delete a user by ID. */
deleteUser: async (id: string, tx?: Transaction) => { deleteUser: async (id: string, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
await txFn.delete(users).where(eq(users.id, BigInt(id))); await txFn.delete(users).where(eq(users.id, BigInt(id)));