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";
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));

View File

@@ -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) {

View File

@@ -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,
};

View File

@@ -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))

View File

@@ -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(),

View File

@@ -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;

View File

@@ -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,

View File

@@ -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

View File

@@ -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;

View File

@@ -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.");

View File

@@ -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)));