feat: centralized constants and enums for project-wide use

This commit is contained in:
syntaxbullet
2026-01-06 18:15:52 +01:00
parent c807fd4fd0
commit 34347f0c63
17 changed files with 144 additions and 63 deletions

View File

@@ -7,8 +7,9 @@ import { userTimers, users } from "@/db/schema";
import { eq, and, sql } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { config } from "@lib/config";
import { TimerType } from "@/lib/constants";
const EXAM_TIMER_TYPE = 'EXAM_SYSTEM';
const EXAM_TIMER_TYPE = TimerType.EXAM_SYSTEM;
const EXAM_TIMER_KEY = 'default';
interface ExamMetadata {

65
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,65 @@
/**
* Global Constants and Enums
*/
export enum TimerType {
COOLDOWN = 'COOLDOWN',
EFFECT = 'EFFECT',
ACCESS = 'ACCESS',
EXAM_SYSTEM = 'EXAM_SYSTEM',
}
export enum EffectType {
ADD_XP = 'ADD_XP',
ADD_BALANCE = 'ADD_BALANCE',
REPLY_MESSAGE = 'REPLY_MESSAGE',
XP_BOOST = 'XP_BOOST',
TEMP_ROLE = 'TEMP_ROLE',
COLOR_ROLE = 'COLOR_ROLE',
LOOTBOX = 'LOOTBOX',
}
export enum TransactionType {
TRANSFER_IN = 'TRANSFER_IN',
TRANSFER_OUT = 'TRANSFER_OUT',
DAILY_REWARD = 'DAILY_REWARD',
ITEM_USE = 'ITEM_USE',
LOOTBOX = 'LOOTBOX',
EXAM_REWARD = 'EXAM_REWARD',
PURCHASE = 'PURCHASE',
TRADE_IN = 'TRADE_IN',
TRADE_OUT = 'TRADE_OUT',
QUEST_REWARD = 'QUEST_REWARD',
}
export enum ItemTransactionType {
TRADE_IN = 'TRADE_IN',
TRADE_OUT = 'TRADE_OUT',
SHOP_BUY = 'SHOP_BUY',
DROP = 'DROP',
GIVE = 'GIVE',
USE = 'USE',
}
export enum ItemType {
MATERIAL = 'MATERIAL',
CONSUMABLE = 'CONSUMABLE',
EQUIPMENT = 'EQUIPMENT',
QUEST = 'QUEST',
}
export enum CaseType {
WARN = 'warn',
TIMEOUT = 'timeout',
KICK = 'kick',
BAN = 'ban',
NOTE = 'note',
PRUNE = 'prune',
}
export enum LootType {
NOTHING = 'NOTHING',
CURRENCY = 'CURRENCY',
XP = 'XP',
ITEM = 'ITEM',
}

View File

@@ -1,4 +1,6 @@
import type { AutocompleteInteraction, ChatInputCommandInteraction, ClientEvents, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder } from "discord.js";
import { LootType, EffectType } from "./constants";
import { DrizzleClient } from "./DrizzleClient";
export interface Command {
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder;
@@ -14,16 +16,16 @@ export interface Event<K extends keyof ClientEvents> {
}
export type ItemEffect =
| { type: 'ADD_XP'; amount: number }
| { type: 'ADD_BALANCE'; amount: number }
| { type: 'XP_BOOST'; multiplier: number; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
| { type: 'TEMP_ROLE'; roleId: string; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
| { type: 'REPLY_MESSAGE'; message: string }
| { type: 'COLOR_ROLE'; roleId: string }
| { type: 'LOOTBOX'; pool: LootTableItem[] };
| { type: EffectType.ADD_XP; amount: number }
| { type: EffectType.ADD_BALANCE; amount: number }
| { type: EffectType.XP_BOOST; multiplier: number; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
| { type: EffectType.TEMP_ROLE; roleId: string; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
| { type: EffectType.REPLY_MESSAGE; message: string }
| { type: EffectType.COLOR_ROLE; roleId: string }
| { type: EffectType.LOOTBOX; pool: LootTableItem[] };
export interface LootTableItem {
type: 'CURRENCY' | 'ITEM' | 'XP' | 'NOTHING';
type: LootType;
weight: number;
amount?: number; // For CURRENCY, XP
itemId?: number; // For ITEM
@@ -37,7 +39,5 @@ export interface ItemUsageData {
effects: ItemEffect[];
}
import { DrizzleClient } from "./DrizzleClient";
export type DbClient = typeof DrizzleClient;
export type Transaction = Parameters<Parameters<DbClient['transaction']>[0]>[0];

View File

@@ -4,6 +4,7 @@ import { DrizzleClient } from "@/lib/DrizzleClient";
import type { ItemUsageData, ItemEffect } from "@/lib/types";
import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view";
import type { DraftItem } from "./item_wizard.types";
import { ItemType, EffectType } from "@/lib/constants";
// --- Types ---
@@ -23,7 +24,7 @@ export const renderWizard = (userId: string, isDraft = true) => {
name: "New Item",
description: "No description",
rarity: "Common",
type: "MATERIAL",
type: ItemType.MATERIAL,
price: null,
iconUrl: "",
imageUrl: "",
@@ -176,26 +177,26 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
if (type) {
let effect: ItemEffect | null = null;
if (type === "ADD_XP" || type === "ADD_BALANCE") {
if (type === EffectType.ADD_XP || type === EffectType.ADD_BALANCE) {
const amount = parseInt(interaction.fields.getTextInputValue("amount"));
if (!isNaN(amount)) effect = { type: type as any, amount };
}
else if (type === "REPLY_MESSAGE") {
effect = { type: "REPLY_MESSAGE", message: interaction.fields.getTextInputValue("message") };
else if (type === EffectType.REPLY_MESSAGE) {
effect = { type: EffectType.REPLY_MESSAGE, message: interaction.fields.getTextInputValue("message") };
}
else if (type === "XP_BOOST") {
else if (type === EffectType.XP_BOOST) {
const multiplier = parseFloat(interaction.fields.getTextInputValue("multiplier"));
const duration = parseInt(interaction.fields.getTextInputValue("duration"));
if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: "XP_BOOST", multiplier, durationSeconds: duration };
if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: EffectType.XP_BOOST, multiplier, durationSeconds: duration };
}
else if (type === "TEMP_ROLE") {
else if (type === EffectType.TEMP_ROLE) {
const roleId = interaction.fields.getTextInputValue("role_id");
const duration = parseInt(interaction.fields.getTextInputValue("duration"));
if (roleId && !isNaN(duration)) effect = { type: "TEMP_ROLE", roleId: roleId, durationSeconds: duration };
if (roleId && !isNaN(duration)) effect = { type: EffectType.TEMP_ROLE, roleId: roleId, durationSeconds: duration };
}
else if (type === "COLOR_ROLE") {
else if (type === EffectType.COLOR_ROLE) {
const roleId = interaction.fields.getTextInputValue("role_id");
if (roleId) effect = { type: "COLOR_ROLE", roleId: roleId };
if (roleId) effect = { type: EffectType.COLOR_ROLE, roleId: roleId };
}
if (effect) {

View File

@@ -10,12 +10,13 @@ import {
} from "discord.js";
import { createBaseEmbed } from "@lib/embeds";
import type { DraftItem } from "./item_wizard.types";
import { ItemType } from "@/lib/constants";
const getItemTypeOptions = () => [
{ label: "Material", value: "MATERIAL", description: "Used for crafting or trading" },
{ label: "Consumable", value: "CONSUMABLE", description: "Can be used to gain effects" },
{ label: "Equipment", value: "EQUIPMENT", description: "Can be equipped (Not yet implemented)" },
{ label: "Quest Item", value: "QUEST", description: "Required for quests" },
{ label: "Material", value: ItemType.MATERIAL, description: "Used for crafting or trading" },
{ label: "Consumable", value: ItemType.CONSUMABLE, description: "Can be used to gain effects" },
{ label: "Equipment", value: ItemType.EQUIPMENT, description: "Can be equipped (Not yet implemented)" },
{ label: "Quest Item", value: ItemType.QUEST, description: "Required for quests" },
];
const getEffectTypeOptions = () => [

View File

@@ -4,6 +4,7 @@ import { config } from "@/lib/config";
import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types";
import { UserError } from "@/lib/errors";
import { TimerType, TransactionType } from "@/lib/constants";
export const economyService = {
transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => {
@@ -48,7 +49,7 @@ export const economyService = {
await txFn.insert(transactions).values({
userId: BigInt(fromUserId),
amount: -amount,
type: 'TRANSFER_OUT',
type: TransactionType.TRANSFER_OUT,
description: `Transfer to ${toUserId}`,
});
@@ -56,7 +57,7 @@ export const economyService = {
await txFn.insert(transactions).values({
userId: BigInt(toUserId),
amount: amount,
type: 'TRANSFER_IN',
type: TransactionType.TRANSFER_IN,
description: `Transfer from ${fromUserId}`,
});
@@ -74,7 +75,7 @@ export const economyService = {
const cooldown = await txFn.query.userTimers.findFirst({
where: and(
eq(userTimers.userId, BigInt(userId)),
eq(userTimers.type, 'COOLDOWN'),
eq(userTimers.type, TimerType.COOLDOWN),
eq(userTimers.key, 'daily')
),
});
@@ -127,7 +128,7 @@ export const economyService = {
await txFn.insert(userTimers)
.values({
userId: BigInt(userId),
type: 'COOLDOWN',
type: TimerType.COOLDOWN,
key: 'daily',
expiresAt: nextReadyAt,
})
@@ -140,7 +141,7 @@ export const economyService = {
await txFn.insert(transactions).values({
userId: BigInt(userId),
amount: totalReward,
type: 'DAILY_REWARD',
type: TransactionType.DAILY_REWARD,
description: `Daily reward (Streak: ${streak})`,
});

View File

@@ -5,6 +5,7 @@ import type { EffectHandler } from "./types";
import type { LootTableItem } from "@/lib/types";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { inventory, items } from "@/db/schema";
import { TimerType, TransactionType, LootType } from "@/lib/constants";
// Helper to extract duration in seconds
@@ -20,7 +21,7 @@ export const handleAddXp: EffectHandler = async (userId, effect, txFn) => {
};
export const handleAddBalance: EffectHandler = async (userId, effect, txFn) => {
await economyService.modifyUserBalance(userId, BigInt(effect.amount), 'ITEM_USE', `Used Item`, null, txFn);
await economyService.modifyUserBalance(userId, BigInt(effect.amount), TransactionType.ITEM_USE, `Used Item`, null, txFn);
return `Gained ${effect.amount} 🪙`;
};
@@ -33,7 +34,7 @@ export const handleXpBoost: EffectHandler = async (userId, effect, txFn) => {
const expiresAt = new Date(Date.now() + boostDuration * 1000);
await txFn.insert(userTimers).values({
userId: BigInt(userId),
type: 'EFFECT',
type: TimerType.EFFECT,
key: 'xp_boost',
expiresAt: expiresAt,
metadata: { multiplier: effect.multiplier }
@@ -49,7 +50,7 @@ export const handleTempRole: EffectHandler = async (userId, effect, txFn) => {
const roleExpiresAt = new Date(Date.now() + roleDuration * 1000);
await txFn.insert(userTimers).values({
userId: BigInt(userId),
type: 'ACCESS',
type: TimerType.ACCESS,
key: `role_${effect.roleId}`,
expiresAt: roleExpiresAt,
metadata: { roleId: effect.roleId }
@@ -84,22 +85,22 @@ export const handleLootbox: EffectHandler = async (userId, effect, txFn) => {
if (!winner) return "The box is empty..."; // Should not happen
// Process Winner
if (winner.type === 'NOTHING') {
if (winner.type === LootType.NOTHING) {
return winner.message || "You found nothing inside.";
}
if (winner.type === 'CURRENCY') {
if (winner.type === LootType.CURRENCY) {
let amount = winner.amount || 0;
if (winner.minAmount && winner.maxAmount) {
amount = Math.floor(Math.random() * (winner.maxAmount - winner.minAmount + 1)) + winner.minAmount;
}
if (amount > 0) {
await economyService.modifyUserBalance(userId, BigInt(amount), 'LOOTBOX', 'Lootbox Reward', null, txFn);
await economyService.modifyUserBalance(userId, BigInt(amount), TransactionType.LOOTBOX, 'Lootbox Reward', null, txFn);
return winner.message || `You found ${amount} 🪙!`;
}
}
if (winner.type === 'XP') {
if (winner.type === LootType.XP) {
let amount = winner.amount || 0;
if (winner.minAmount && winner.maxAmount) {
amount = Math.floor(Math.random() * (winner.maxAmount - winner.minAmount + 1)) + winner.minAmount;
@@ -110,7 +111,7 @@ export const handleLootbox: EffectHandler = async (userId, effect, txFn) => {
}
}
if (winner.type === 'ITEM') {
if (winner.type === LootType.ITEM) {
if (winner.itemId) {
const quantity = BigInt(winner.amount || 1);

View File

@@ -7,6 +7,7 @@ import { config } from "@/lib/config";
import { UserError } from "@/lib/errors";
import { withTransaction } from "@/lib/db";
import type { Transaction, ItemUsageData } from "@/lib/types";
import { TransactionType } from "@/lib/constants";
@@ -121,7 +122,7 @@ export const inventoryService = {
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}`, null, txFn);
await economyService.modifyUserBalance(userId, -totalPrice, TransactionType.PURCHASE, `Bought ${quantity}x ${item.name}`, null, txFn);
await inventoryService.addItem(userId, itemId, quantity, txFn);

View File

@@ -1,5 +1,6 @@
import { EmbedBuilder } from "discord.js";
import type { ItemUsageData } from "@/lib/types";
import { EffectType } from "@/lib/constants";
/**
* Inventory entry with item details
@@ -34,7 +35,7 @@ export function getItemUseResultEmbed(results: string[], item?: { name: string,
const description = results.map(r => `${r}`).join("\n");
// Check if it was a lootbox
const isLootbox = item?.usageData?.effects?.some((e: any) => e.type === 'LOOTBOX');
const isLootbox = item?.usageData?.effects?.some((e: any) => e.type === EffectType.LOOTBOX);
const embed = new EmbedBuilder()
.setDescription(description)

View File

@@ -3,6 +3,7 @@ import { eq, sql, and } from "drizzle-orm";
import { withTransaction } from "@/lib/db";
import { config } from "@/lib/config";
import type { Transaction } from "@/lib/types";
import { TimerType } from "@/lib/constants";
export const levelingService = {
// Calculate total XP required to REACH a specific level (Cumulative)
@@ -78,7 +79,7 @@ export const levelingService = {
const cooldown = await txFn.query.userTimers.findFirst({
where: and(
eq(userTimers.userId, BigInt(id)),
eq(userTimers.type, 'COOLDOWN'),
eq(userTimers.type, TimerType.COOLDOWN),
eq(userTimers.key, 'chat_xp')
),
});
@@ -95,7 +96,7 @@ export const levelingService = {
const xpBoost = await txFn.query.userTimers.findFirst({
where: and(
eq(userTimers.userId, BigInt(id)),
eq(userTimers.type, 'EFFECT'),
eq(userTimers.type, TimerType.EFFECT),
eq(userTimers.key, 'xp_boost')
)
});
@@ -114,7 +115,7 @@ export const levelingService = {
await txFn.insert(userTimers)
.values({
userId: BigInt(id),
type: 'COOLDOWN',
type: TimerType.COOLDOWN,
key: 'chat_xp',
expiresAt: nextReadyAt,
})

View File

@@ -2,6 +2,7 @@
import { describe, it, expect, mock, beforeEach } from "bun:test";
import { ModerationService } from "./moderation.service";
import { moderationCases } from "@/db/schema";
import { CaseType } from "@/lib/constants";
// Mock Drizzle Functions
const mockFindFirst = mock();
@@ -83,7 +84,7 @@ describe("ModerationService", () => {
it("should issue a warning and attempt to DM the user", async () => {
mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" });
mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]);
mockFindMany.mockResolvedValue([{ type: 'warn', active: true }]); // 1 warning total
mockFindMany.mockResolvedValue([{ type: CaseType.WARN, active: true }]); // 1 warning total
const mockDmTarget = { send: mock() };
@@ -178,7 +179,7 @@ describe("ModerationService", () => {
mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" });
const mockNewCase = {
caseId: "CASE-0002",
type: 'warn',
type: CaseType.WARN,
userId: 123456789n,
username: "testuser",
moderatorId: 987654321n,
@@ -190,7 +191,7 @@ describe("ModerationService", () => {
mockReturning.mockResolvedValue([mockNewCase]);
const result = await ModerationService.createCase({
type: 'warn',
type: CaseType.WARN,
userId: "123456789",
username: "testuser",
moderatorId: "987654321",
@@ -202,7 +203,7 @@ describe("ModerationService", () => {
expect(mockInsert).toHaveBeenCalled();
expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({
caseId: "CASE-0002",
type: 'warn',
type: CaseType.WARN,
userId: 123456789n,
reason: "test reason"
}));
@@ -213,7 +214,7 @@ describe("ModerationService", () => {
mockReturning.mockImplementation((values) => [values]); // Simplified mock
const result = await ModerationService.createCase({
type: 'ban',
type: CaseType.BAN,
userId: "123456789",
username: "testuser",
moderatorId: "987654321",
@@ -273,8 +274,8 @@ describe("ModerationService", () => {
describe("getActiveWarningCount", () => {
it("should return the number of active warnings", async () => {
mockFindMany.mockResolvedValue([
{ id: 1n, type: 'warn', active: true },
{ id: 2n, type: 'warn', active: true }
{ id: 1n, type: CaseType.WARN, active: true },
{ id: 2n, type: CaseType.WARN, active: true }
]);
const count = await ModerationService.getActiveWarningCount("123456789");

View File

@@ -4,6 +4,7 @@ import { DrizzleClient } from "@/lib/DrizzleClient";
import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "./moderation.types";
import { config } from "@/lib/config";
import { getUserWarningEmbed } from "./moderation.view";
import { CaseType } from "@/lib/constants";
export class ModerationService {
/**
@@ -43,7 +44,7 @@ export class ModerationService {
moderatorName: options.moderatorName,
reason: options.reason,
metadata: options.metadata || {},
active: options.type === 'warn' ? true : false, // Only warnings are "active" by default
active: options.type === CaseType.WARN ? true : false, // Only warnings are "active" by default
}).returning();
return newCase;
@@ -63,7 +64,7 @@ export class ModerationService {
timeoutTarget?: { timeout: (duration: number, reason: string) => Promise<any> };
}) {
const moderationCase = await this.createCase({
type: 'warn',
type: CaseType.WARN,
userId: options.userId,
username: options.username,
moderatorId: options.moderatorId,
@@ -105,7 +106,7 @@ export class ModerationService {
// Create a timeout case
await this.createCase({
type: 'timeout',
type: CaseType.TIMEOUT,
userId: options.userId,
username: options.username,
moderatorId: "0", // System/Bot
@@ -154,7 +155,7 @@ export class ModerationService {
return await DrizzleClient.query.moderationCases.findMany({
where: and(
eq(moderationCases.userId, BigInt(userId)),
eq(moderationCases.type, 'warn'),
eq(moderationCases.type, CaseType.WARN),
eq(moderationCases.active, true)
),
orderBy: [desc(moderationCases.createdAt)],
@@ -168,7 +169,7 @@ export class ModerationService {
return await DrizzleClient.query.moderationCases.findMany({
where: and(
eq(moderationCases.userId, BigInt(userId)),
eq(moderationCases.type, 'note')
eq(moderationCases.type, CaseType.NOTE)
),
orderBy: [desc(moderationCases.createdAt)],
});

View File

@@ -1,4 +1,6 @@
export type CaseType = 'warn' | 'timeout' | 'kick' | 'ban' | 'note' | 'prune';
import { CaseType } from "@/lib/constants";
export { CaseType };
export interface CreateCaseOptions {
type: CaseType;

View File

@@ -6,6 +6,7 @@ import { economyService } from "@/modules/economy/economy.service";
import { levelingService } from "@/modules/leveling/leveling.service";
import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types";
import { TransactionType } from "@/lib/constants";
export const questService = {
assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
@@ -62,7 +63,7 @@ export const questService = {
if (rewards?.balance) {
const bal = BigInt(rewards.balance);
await economyService.modifyUserBalance(userId, bal, 'QUEST_REWARD', `Reward for quest ${questId}`, null, txFn);
await economyService.modifyUserBalance(userId, bal, TransactionType.QUEST_REWARD, `Reward for quest ${questId}`, null, txFn);
results.balance = bal;
}

View File

@@ -4,6 +4,7 @@ import { DrizzleClient } from "@/lib/DrizzleClient";
import { AuroraClient } from "@/lib/BotClient";
import { env } from "@/lib/env";
import { config } from "@/lib/config";
import { TimerType } from "@/lib/constants";
export const cleanupService = {
/**
@@ -57,7 +58,7 @@ export const cleanupService = {
// This is migrated from scheduler.ts
const expiredAccess = await DrizzleClient.query.userTimers.findMany({
where: and(
eq(userTimers.type, 'ACCESS'),
eq(userTimers.type, TimerType.ACCESS),
lt(userTimers.expiresAt, now)
)
});

View File

@@ -4,6 +4,7 @@ import { inventoryService } from "@/modules/inventory/inventory.service";
import { itemTransactions } from "@/db/schema";
import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types";
import { TransactionType, ItemTransactionType } from "@/lib/constants";
// Module-level session storage
const sessions = new Map<string, TradeSession>();
@@ -25,7 +26,7 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
await economyService.modifyUserBalance(
from.id,
-from.offer.money,
'TRADE_OUT',
TransactionType.TRADE_OUT,
`Trade with ${to.username} (Thread: ${threadId})`,
to.id,
tx
@@ -33,7 +34,7 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
await economyService.modifyUserBalance(
to.id,
from.offer.money,
'TRADE_IN',
TransactionType.TRADE_IN,
`Trade with ${from.username} (Thread: ${threadId})`,
from.id,
tx
@@ -54,7 +55,7 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
relatedUserId: BigInt(to.id),
itemId: item.id,
quantity: -item.quantity,
type: 'TRADE_OUT',
type: ItemTransactionType.TRADE_OUT,
description: `Traded to ${to.username}`,
});
@@ -64,7 +65,7 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
relatedUserId: BigInt(from.id),
itemId: item.id,
quantity: item.quantity,
type: 'TRADE_IN',
type: ItemTransactionType.TRADE_IN,
description: `Received from ${from.username}`,
});
}

View File

@@ -1,8 +1,9 @@
import { userTimers } from "@/db/schema";
import { eq, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { TimerType } from "@/lib/constants";
export type TimerType = 'COOLDOWN' | 'EFFECT' | 'ACCESS';
export { TimerType };
export const userTimerService = {
/**