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:
@@ -6,10 +6,12 @@ import { withTransaction } from "@/lib/db";
|
||||
import type { Transaction } from "@shared/lib/types";
|
||||
|
||||
export const classService = {
|
||||
/** Retrieve all available classes. */
|
||||
getAllClasses: async () => {
|
||||
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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const cls = await txFn.query.classes.findFirst({
|
||||
@@ -26,12 +28,14 @@ export const classService = {
|
||||
return user;
|
||||
}, tx);
|
||||
},
|
||||
/** Get the current balance for a class, returning 0 if not found. */
|
||||
getClassBalance: async (classId: bigint) => {
|
||||
const cls = await DrizzleClient.query.classes.findFirst({
|
||||
where: eq(classes.id, classId),
|
||||
});
|
||||
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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const cls = await txFn.query.classes.findFirst({
|
||||
@@ -55,6 +59,7 @@ export const classService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Update a class record with partial data. */
|
||||
updateClass: async (id: bigint, data: Partial<typeof classes.$inferInsert>, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const [updatedClass] = await txFn.update(classes)
|
||||
@@ -65,6 +70,7 @@ export const classService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Create a new class record. */
|
||||
createClass: async (data: typeof classes.$inferInsert, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const [newClass] = await txFn.insert(classes)
|
||||
@@ -74,6 +80,7 @@ export const classService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Delete a class by ID. */
|
||||
deleteClass: async (id: bigint, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
await txFn.delete(classes).where(eq(classes.id, id));
|
||||
|
||||
@@ -8,6 +8,7 @@ import { UserError } from "@shared/lib/errors";
|
||||
import { TimerKey, TimerType, TransactionType } from "@shared/lib/constants";
|
||||
|
||||
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) => {
|
||||
if (amount <= 0n) {
|
||||
throw new UserError("Amount must be positive");
|
||||
@@ -69,6 +70,7 @@ export const economyService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Claim the daily reward, applying streak bonuses and weekly bonuses; enforces a UTC-midnight cooldown. */
|
||||
claimDaily: async (userId: string, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const now = new Date();
|
||||
@@ -164,6 +166,7 @@ export const economyService = {
|
||||
}, 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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
if (amount < 0n) {
|
||||
|
||||
@@ -213,12 +213,20 @@ async function clearCaches() {
|
||||
}
|
||||
|
||||
export const lootdropService = {
|
||||
/** Delete expired lootdrops from the database; optionally includes already-claimed ones. */
|
||||
cleanupExpiredLootdrops,
|
||||
/** Record a message in a channel and return whether a lootdrop should spawn based on activity and RNG. */
|
||||
trackActivity,
|
||||
/** Calculate a random lootdrop reward amount and currency, with optional overrides. */
|
||||
calculateReward,
|
||||
/** Save a spawned lootdrop to the database with a 10-minute expiration. */
|
||||
persistLootdrop,
|
||||
/** Remove a lootdrop by message ID and return its channel ID for Discord cleanup. */
|
||||
removeLootdrop,
|
||||
/** Atomically claim a lootdrop for a user; credits reward to their balance. */
|
||||
tryClaim,
|
||||
/** Get current lootdrop system state including the most active channel and spawn config. */
|
||||
getLootdropState,
|
||||
/** Clear all in-memory activity tracking and cooldown caches. */
|
||||
clearCaches,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface FeatureFlagContext {
|
||||
}
|
||||
|
||||
export const featureFlagsService = {
|
||||
/** Check whether a feature flag is enabled globally. */
|
||||
isFlagEnabled: async (flagName: string): Promise<boolean> => {
|
||||
const flag = await DrizzleClient.query.featureFlags.findFirst({
|
||||
where: eq(featureFlags.name, flagName),
|
||||
@@ -17,6 +18,7 @@ export const featureFlagsService = {
|
||||
return flag?.enabled ?? false;
|
||||
},
|
||||
|
||||
/** Check if a guild/user/role has access to a feature flag; returns false if flag is disabled. */
|
||||
hasAccess: async (
|
||||
flagName: string,
|
||||
context: FeatureFlagContext
|
||||
@@ -51,6 +53,7 @@ export const featureFlagsService = {
|
||||
return false;
|
||||
},
|
||||
|
||||
/** Create a new feature flag, disabled by default. */
|
||||
createFlag: async (name: string, description?: string) => {
|
||||
const [flag] = await DrizzleClient.insert(featureFlags).values({
|
||||
name,
|
||||
@@ -60,6 +63,7 @@ export const featureFlagsService = {
|
||||
return flag;
|
||||
},
|
||||
|
||||
/** Enable or disable a feature flag by name; throws if the flag does not exist. */
|
||||
setFlagEnabled: async (name: string, enabled: boolean) => {
|
||||
const [flag] = await DrizzleClient.update(featureFlags)
|
||||
.set({ enabled, updatedAt: new Date() })
|
||||
@@ -72,6 +76,7 @@ export const featureFlagsService = {
|
||||
return flag;
|
||||
},
|
||||
|
||||
/** Grant a guild, user, or role access to a feature flag. */
|
||||
grantAccess: async (
|
||||
flagName: string,
|
||||
access: { guildId?: string; userId?: string; roleId?: string }
|
||||
@@ -90,6 +95,7 @@ export const featureFlagsService = {
|
||||
return accessRecord;
|
||||
},
|
||||
|
||||
/** Revoke a specific access record by ID; throws if not found. */
|
||||
revokeAccess: async (accessId: number) => {
|
||||
const [access] = await DrizzleClient.delete(featureFlagAccess)
|
||||
.where(eq(featureFlagAccess.id, accessId))
|
||||
@@ -101,18 +107,21 @@ export const featureFlagsService = {
|
||||
return access;
|
||||
},
|
||||
|
||||
/** Retrieve a single feature flag by name. */
|
||||
getFlag: async (name: string) => {
|
||||
return await DrizzleClient.query.featureFlags.findFirst({
|
||||
where: eq(featureFlags.name, name),
|
||||
});
|
||||
},
|
||||
|
||||
/** List all feature flags, ordered by name. */
|
||||
listFlags: async () => {
|
||||
return await DrizzleClient.query.featureFlags.findMany({
|
||||
orderBy: (flags, { asc }) => [asc(flags.name)],
|
||||
});
|
||||
},
|
||||
|
||||
/** List all access records for a given feature flag. */
|
||||
listAccess: async (flagName: string) => {
|
||||
const flag = await DrizzleClient.query.featureFlags.findFirst({
|
||||
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) => {
|
||||
const [flag] = await DrizzleClient.delete(featureFlags)
|
||||
.where(eq(featureFlags.name, name))
|
||||
|
||||
@@ -29,6 +29,7 @@ let cacheTimestamp = 0;
|
||||
const CACHE_TTL_MS = 30000;
|
||||
|
||||
export const gameSettingsService = {
|
||||
/** Retrieve game settings, using a 30-second TTL cache by default. */
|
||||
getSettings: async (useCache = true): Promise<GameSettingsData | null> => {
|
||||
if (useCache && cachedSettings && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
|
||||
return cachedSettings;
|
||||
@@ -56,6 +57,7 @@ export const gameSettingsService = {
|
||||
return cachedSettings;
|
||||
},
|
||||
|
||||
/** Create or update game settings, merging with existing values and invalidating cache. */
|
||||
upsertSettings: async (data: Partial<GameSettingsData>) => {
|
||||
const existing = await gameSettingsService.getSettings(false);
|
||||
|
||||
@@ -86,6 +88,7 @@ export const gameSettingsService = {
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Update a single configuration section (e.g., "leveling", "economy") and invalidate cache. */
|
||||
updateSection: async <K extends keyof GameSettingsData>(
|
||||
section: K,
|
||||
value: GameSettingsData[K]
|
||||
@@ -102,6 +105,7 @@ export const gameSettingsService = {
|
||||
gameSettingsService.invalidateCache();
|
||||
},
|
||||
|
||||
/** Enable or disable a specific command in the game settings. */
|
||||
toggleCommand: async (commandName: string, enabled: boolean) => {
|
||||
const settings = await gameSettingsService.getSettings(false);
|
||||
|
||||
@@ -117,11 +121,13 @@ export const gameSettingsService = {
|
||||
await gameSettingsService.updateSection("commands", commands);
|
||||
},
|
||||
|
||||
/** Invalidate the in-memory settings cache, forcing a fresh DB read on next access. */
|
||||
invalidateCache: () => {
|
||||
cachedSettings = null;
|
||||
cacheTimestamp = 0;
|
||||
},
|
||||
|
||||
/** Return default leveling configuration values. */
|
||||
getDefaultLeveling: (): LevelingConfig => ({
|
||||
base: 100,
|
||||
exponent: 1.5,
|
||||
@@ -132,6 +138,7 @@ export const gameSettingsService = {
|
||||
},
|
||||
}),
|
||||
|
||||
/** Return default economy configuration values. */
|
||||
getDefaultEconomy: (): EconomyConfig => ({
|
||||
daily: {
|
||||
amount: "100",
|
||||
@@ -149,11 +156,13 @@ export const gameSettingsService = {
|
||||
},
|
||||
}),
|
||||
|
||||
/** Return default inventory configuration values. */
|
||||
getDefaultInventory: (): InventoryConfig => ({
|
||||
maxStackSize: "99",
|
||||
maxSlots: 20,
|
||||
}),
|
||||
|
||||
/** Return default lootdrop configuration values. */
|
||||
getDefaultLootdrop: (): LootdropConfig => ({
|
||||
activityWindowMs: 300000,
|
||||
minMessages: 5,
|
||||
@@ -166,6 +175,7 @@ export const gameSettingsService = {
|
||||
},
|
||||
}),
|
||||
|
||||
/** Return default trivia configuration values. */
|
||||
getDefaultTrivia: (): TriviaConfig => ({
|
||||
entryFee: "50",
|
||||
rewardMultiplier: 1.8,
|
||||
@@ -175,6 +185,7 @@ export const gameSettingsService = {
|
||||
difficulty: "random",
|
||||
}),
|
||||
|
||||
/** Return default moderation configuration values. */
|
||||
getDefaultModeration: (): ModerationConfig => ({
|
||||
prune: {
|
||||
maxAmount: 100,
|
||||
@@ -184,10 +195,12 @@ export const gameSettingsService = {
|
||||
},
|
||||
}),
|
||||
|
||||
/** Return default quest configuration values. */
|
||||
getDefaultQuest: (): QuestConfig => ({
|
||||
maxActiveQuests: 3,
|
||||
}),
|
||||
|
||||
/** Return the complete set of default game settings across all sections. */
|
||||
getDefaults: (): GameSettingsData => ({
|
||||
leveling: gameSettingsService.getDefaultLeveling(),
|
||||
economy: gameSettingsService.getDefaultEconomy(),
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface GuildSettingsData {
|
||||
}
|
||||
|
||||
export const guildSettingsService = {
|
||||
/** Retrieve guild settings by guild ID, or null if none exist. */
|
||||
getSettings: async (guildId: string): Promise<GuildSettingsData | null> => {
|
||||
const settings = await DrizzleClient.query.guildSettings.findFirst({
|
||||
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 }) => {
|
||||
const values: typeof guildSettings.$inferInsert = {
|
||||
guildId: BigInt(data.guildId),
|
||||
@@ -73,6 +75,7 @@ export const guildSettingsService = {
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Update a single guild setting by key name; throws if the key is unknown or settings do not exist. */
|
||||
updateSetting: async (
|
||||
guildId: string,
|
||||
key: string,
|
||||
@@ -128,6 +131,7 @@ export const guildSettingsService = {
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Delete all settings for a guild. */
|
||||
deleteSettings: async (guildId: string) => {
|
||||
const [result] = await DrizzleClient.delete(guildSettings)
|
||||
.where(eq(guildSettings.guildId, BigInt(guildId)))
|
||||
@@ -136,6 +140,7 @@ export const guildSettingsService = {
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Add a color role to the guild's allowed list; no-ops if already present. */
|
||||
addColorRole: async (guildId: string, roleId: string) => {
|
||||
const settings = await guildSettingsService.getSettings(guildId);
|
||||
const colorRoleIds = settings?.colorRoleIds ?? [];
|
||||
@@ -148,6 +153,7 @@ export const guildSettingsService = {
|
||||
return await guildSettingsService.upsertSettings({ guildId, colorRoleIds });
|
||||
},
|
||||
|
||||
/** Remove a color role from the guild's allowed list. */
|
||||
removeColorRole: async (guildId: string, roleId: string) => {
|
||||
const settings = await guildSettingsService.getSettings(guildId);
|
||||
if (!settings) return null;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TransactionType } from "@shared/lib/constants";
|
||||
|
||||
|
||||
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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
// Check if item exists in inventory
|
||||
@@ -74,6 +75,7 @@ export const inventoryService = {
|
||||
}, 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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const existing = await txFn.query.inventory.findFirst({
|
||||
@@ -110,6 +112,7 @@ export const inventoryService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Retrieve all inventory entries for a user, including item details. */
|
||||
getInventory: async (userId: string) => {
|
||||
return await DrizzleClient.query.inventory.findMany({
|
||||
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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const item = await txFn.query.items.findFirst({
|
||||
@@ -139,12 +143,14 @@ export const inventoryService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Fetch a single item definition by ID. */
|
||||
getItem: async (itemId: number) => {
|
||||
return await DrizzleClient.query.items.findFirst({
|
||||
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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
// 1. Check Ownership & Quantity
|
||||
@@ -189,6 +195,7 @@ export const inventoryService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Search a user's usable inventory items by name for autocomplete suggestions. */
|
||||
getAutocompleteItems: async (userId: string, query: string) => {
|
||||
const entries = await DrizzleClient.select({
|
||||
quantity: inventory.quantity,
|
||||
|
||||
@@ -8,11 +8,7 @@ import { TimerKey, TimerType } from "@shared/lib/constants";
|
||||
import { UserError } from "@shared/lib/errors";
|
||||
|
||||
export const levelingService = {
|
||||
// Calculate total XP required to REACH a specific level (Cumulative)
|
||||
// Level 1 = 0 XP
|
||||
// Level 2 = Base * (1^Exp)
|
||||
// Level 3 = Level 2 + Base * (2^Exp)
|
||||
// ...
|
||||
/** Calculate the cumulative XP required to reach a specific level. */
|
||||
getXpToReachLevel: (level: number) => {
|
||||
let total = 0;
|
||||
for (let l = 1; l < level; l++) {
|
||||
@@ -21,7 +17,7 @@ export const levelingService = {
|
||||
return total;
|
||||
},
|
||||
|
||||
// Calculate level from Total XP
|
||||
/** Derive a user's level from their total accumulated XP. */
|
||||
getLevelFromXp: (totalXp: bigint) => {
|
||||
let level = 1;
|
||||
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)
|
||||
// Used internally or for display
|
||||
/** Get the XP needed to advance from the given level to the next. */
|
||||
getXpForNextLevel: (currentLevel: number) => {
|
||||
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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
// Get current state
|
||||
@@ -77,7 +72,7 @@ export const levelingService = {
|
||||
}, 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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
// check if an xp cooldown is in place
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TransactionType } from "@shared/lib/constants";
|
||||
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||
|
||||
export const questService = {
|
||||
/** Assign a quest to a user; enforces the maximum active quest limit. */
|
||||
assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
// Check active quest limit
|
||||
@@ -40,6 +41,7 @@ export const questService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Set the progress value for a user's active quest. */
|
||||
updateProgress: async (userId: string, questId: number, progress: number, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
return await txFn.update(userQuests)
|
||||
@@ -52,6 +54,7 @@ export const questService = {
|
||||
}, 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) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
// 1. Fetch active user quests for this event
|
||||
@@ -86,6 +89,7 @@ export const questService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Mark a quest as completed and distribute its XP and balance rewards. */
|
||||
completeQuest: async (userId: string, questId: number, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const userQuest = await txFn.query.userQuests.findFirst({
|
||||
@@ -137,6 +141,7 @@ export const questService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Get all quests assigned to a user, including quest details. */
|
||||
getUserQuests: async (userId: string) => {
|
||||
return await DrizzleClient.query.userQuests.findMany({
|
||||
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) {
|
||||
const userQuestIds = (await DrizzleClient.query.userQuests.findMany({
|
||||
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: {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -181,12 +188,14 @@ export const questService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** List all quest definitions, ordered by ID. */
|
||||
async getAllQuests() {
|
||||
return await DrizzleClient.query.quests.findMany({
|
||||
orderBy: (quests, { asc }) => [asc(quests.id)],
|
||||
});
|
||||
},
|
||||
|
||||
/** Delete a quest definition by ID. */
|
||||
async deleteQuest(id: number, tx?: Transaction) {
|
||||
return await withTransaction(async (txFn) => {
|
||||
return await txFn.delete(quests)
|
||||
@@ -195,6 +204,7 @@ export const questService = {
|
||||
}, tx);
|
||||
},
|
||||
|
||||
/** Update a quest definition with partial data. */
|
||||
async updateQuest(id: number, data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
|
||||
@@ -101,10 +101,12 @@ export const tradeService = {
|
||||
return session;
|
||||
},
|
||||
|
||||
/** Get an active trade session by thread ID. */
|
||||
getSession: (threadId: string): TradeSession | undefined => {
|
||||
return sessions.get(threadId);
|
||||
},
|
||||
|
||||
/** Remove a trade session from memory. */
|
||||
endSession: (threadId: string) => {
|
||||
sessions.delete(threadId);
|
||||
},
|
||||
@@ -126,6 +128,7 @@ export const tradeService = {
|
||||
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) => {
|
||||
const session = tradeService.getSession(threadId);
|
||||
if (!session) throw new UserError("Session not found");
|
||||
@@ -145,6 +148,7 @@ export const tradeService = {
|
||||
session.lastInteraction = Date.now();
|
||||
},
|
||||
|
||||
/** Remove an item from a participant's trade offer; unlocks both sides. */
|
||||
removeItem: (threadId: string, userId: string, itemId: number) => {
|
||||
const session = tradeService.getSession(threadId);
|
||||
if (!session) throw new UserError("Session not found");
|
||||
@@ -158,6 +162,7 @@ export const tradeService = {
|
||||
session.lastInteraction = Date.now();
|
||||
},
|
||||
|
||||
/** Toggle a participant's lock status on their offer; returns the new lock state. */
|
||||
toggleLock: (threadId: string, userId: string): boolean => {
|
||||
const session = tradeService.getSession(threadId);
|
||||
if (!session) throw new UserError("Session not found");
|
||||
@@ -199,6 +204,7 @@ export const tradeService = {
|
||||
tradeService.endSession(threadId);
|
||||
},
|
||||
|
||||
/** Clear all active trade sessions from memory. */
|
||||
clearSessions: () => {
|
||||
sessions.clear();
|
||||
console.log("[TradeService] All active trade sessions cleared.");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { withTransaction } from "@/lib/db";
|
||||
import type { Transaction } from "@shared/lib/types";
|
||||
|
||||
export const userService = {
|
||||
/** Fetch a user by Discord ID, including their class relation. */
|
||||
getUserById: async (id: string) => {
|
||||
const user = await DrizzleClient.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(id)),
|
||||
@@ -12,10 +13,12 @@ export const userService = {
|
||||
});
|
||||
return user;
|
||||
},
|
||||
/** Fetch a user by their username. */
|
||||
getUserByUsername: async (username: string) => {
|
||||
const user = await DrizzleClient.query.users.findFirst({ where: eq(users.username, username) });
|
||||
return user;
|
||||
},
|
||||
/** Fetch a user by ID, creating a new record if one does not exist. */
|
||||
getOrCreateUser: async (id: string, username: string, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
let user = await txFn.query.users.findFirst({
|
||||
@@ -38,6 +41,7 @@ export const userService = {
|
||||
return user;
|
||||
}, tx);
|
||||
},
|
||||
/** Get the class assigned to a user, or undefined if none. */
|
||||
getUserClass: async (id: string) => {
|
||||
const user = await DrizzleClient.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(id)),
|
||||
@@ -45,6 +49,7 @@ export const userService = {
|
||||
});
|
||||
return user?.class;
|
||||
},
|
||||
/** Create a new user with an optional initial class assignment. */
|
||||
createUser: async (id: string | bigint, username: string, classId?: bigint, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const [user] = await txFn.insert(users).values({
|
||||
@@ -55,6 +60,7 @@ export const userService = {
|
||||
return user;
|
||||
}, tx);
|
||||
},
|
||||
/** Update a user record with partial data. */
|
||||
updateUser: async (id: string, data: Partial<typeof users.$inferInsert>, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
const [user] = await txFn.update(users)
|
||||
@@ -64,6 +70,7 @@ export const userService = {
|
||||
return user;
|
||||
}, tx);
|
||||
},
|
||||
/** Delete a user by ID. */
|
||||
deleteUser: async (id: string, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
await txFn.delete(users).where(eq(users.id, BigInt(id)));
|
||||
|
||||
Reference in New Issue
Block a user