From b0c3baf5b7134e59659fdc1c570320713f0bf5dd Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 12 Feb 2026 12:14:15 +0100 Subject: [PATCH] refactor(db): split schema into domain modules Split the 276-line schema.ts into focused domain modules: - users.ts: classes, users, userTimers (core identity) - inventory.ts: items, inventory (item system) - economy.ts: transactions, itemTransactions (currency flow) - quests.ts: quests, userQuests (quest system) - moderation.ts: moderationCases, lootdrops (moderation) Original schema.ts now re-exports from schema/index.ts for backward compatibility. All existing imports continue to work. --- shared/db/schema.ts | 279 +-------------------------------- shared/db/schema/economy.ts | 69 ++++++++ shared/db/schema/index.ts | 6 + shared/db/schema/inventory.ts | 57 +++++++ shared/db/schema/moderation.ts | 65 ++++++++ shared/db/schema/quests.ts | 54 +++++++ shared/db/schema/users.ts | 80 ++++++++++ 7 files changed, 334 insertions(+), 276 deletions(-) create mode 100644 shared/db/schema/economy.ts create mode 100644 shared/db/schema/index.ts create mode 100644 shared/db/schema/inventory.ts create mode 100644 shared/db/schema/moderation.ts create mode 100644 shared/db/schema/quests.ts create mode 100644 shared/db/schema/users.ts diff --git a/shared/db/schema.ts b/shared/db/schema.ts index 80b1902..4c0bd65 100644 --- a/shared/db/schema.ts +++ b/shared/db/schema.ts @@ -1,276 +1,3 @@ -import { - pgTable, - bigint, - varchar, - boolean, - jsonb, - timestamp, - serial, - text, - integer, - primaryKey, - index, - bigserial, - check -} from 'drizzle-orm/pg-core'; -import { relations, sql, type InferSelectModel } from 'drizzle-orm'; - -export type User = InferSelectModel; -export type Class = InferSelectModel; -export type Transaction = InferSelectModel; -export type ItemTransaction = InferSelectModel; -export type ModerationCase = InferSelectModel; -export type Item = InferSelectModel; -export type Inventory = InferSelectModel; -export type Quest = InferSelectModel; -export type UserQuest = InferSelectModel; -export type UserTimer = InferSelectModel; -export type Lootdrop = InferSelectModel; - -// --- TABLES --- - -// 1. Classes -export const classes = pgTable('classes', { - id: bigint('id', { mode: 'bigint' }).primaryKey(), - name: varchar('name', { length: 255 }).unique().notNull(), - balance: bigint('balance', { mode: 'bigint' }).default(0n), - roleId: varchar('role_id', { length: 255 }), -}); - -// 2. Users -export const users = pgTable('users', { - id: bigint('id', { mode: 'bigint' }).primaryKey(), - classId: bigint('class_id', { mode: 'bigint' }).references(() => classes.id), - username: varchar('username', { length: 255 }).unique().notNull(), - isActive: boolean('is_active').default(true), - - // Economy - balance: bigint('balance', { mode: 'bigint' }).default(0n), - xp: bigint('xp', { mode: 'bigint' }).default(0n), - level: integer('level').default(1), - dailyStreak: integer('daily_streak').default(0), - - // Metadata - settings: jsonb('settings').default({}), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), -}, (table) => [ - index('users_username_idx').on(table.username), - index('users_balance_idx').on(table.balance), - index('users_level_xp_idx').on(table.level, table.xp), -]); - -// 3. Items -export const items = pgTable('items', { - id: serial('id').primaryKey(), - name: varchar('name', { length: 255 }).unique().notNull(), - description: text('description'), - rarity: varchar('rarity', { length: 20 }).default('C'), - - // Economy & Visuals - type: varchar('type', { length: 50 }).notNull().default('MATERIAL'), - // Examples: 'CONSUMABLE', 'EQUIPMENT', 'MATERIAL' - usageData: jsonb('usage_data').default({}), - price: bigint('price', { mode: 'bigint' }), - iconUrl: text('icon_url').notNull(), - imageUrl: text('image_url').notNull(), -}); - -// 4. Inventory (Join Table) -export const inventory = pgTable('inventory', { - userId: bigint('user_id', { mode: 'bigint' }) - .references(() => users.id, { onDelete: 'cascade' }).notNull(), - itemId: integer('item_id') - .references(() => items.id, { onDelete: 'cascade' }).notNull(), - quantity: bigint('quantity', { mode: 'bigint' }).default(1n), -}, (table) => [ - primaryKey({ columns: [table.userId, table.itemId] }), - check('quantity_check', sql`${table.quantity} > 0`) -]); - -// 5. Transactions -export const transactions = pgTable('transactions', { - id: bigserial('id', { mode: 'bigint' }).primaryKey(), - userId: bigint('user_id', { mode: 'bigint' }) - .references(() => users.id, { onDelete: 'cascade' }), - relatedUserId: bigint('related_user_id', { mode: 'bigint' }) - .references(() => users.id, { onDelete: 'set null' }), - amount: bigint('amount', { mode: 'bigint' }).notNull(), - type: varchar('type', { length: 50 }).notNull(), - description: text('description'), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), -}, (table) => [ - index('transactions_created_at_idx').on(table.createdAt), -]); - -export const itemTransactions = pgTable('item_transactions', { - id: bigserial('id', { mode: 'bigint' }).primaryKey(), - userId: bigint('user_id', { mode: 'bigint' }) - .references(() => users.id, { onDelete: 'cascade' }).notNull(), - relatedUserId: bigint('related_user_id', { mode: 'bigint' }) - .references(() => users.id, { onDelete: 'set null' }), // who they got it from/gave it to - itemId: integer('item_id') - .references(() => items.id, { onDelete: 'cascade' }).notNull(), - quantity: bigint('quantity', { mode: 'bigint' }).notNull(), // positive = gain, negative = loss - type: varchar('type', { length: 50 }).notNull(), // e.g., 'TRADE', 'SHOP_BUY', 'DROP' - description: text('description'), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), -}); - -// 6. Quests -export const quests = pgTable('quests', { - id: serial('id').primaryKey(), - name: varchar('name', { length: 255 }).notNull(), - description: text('description'), - triggerEvent: varchar('trigger_event', { length: 50 }).notNull(), - requirements: jsonb('requirements').notNull().default({}), - rewards: jsonb('rewards').notNull().default({}), -}); - -// 7. User Quests (Join Table) -export const userQuests = pgTable('user_quests', { - userId: bigint('user_id', { mode: 'bigint' }) - .references(() => users.id, { onDelete: 'cascade' }).notNull(), - questId: integer('quest_id') - .references(() => quests.id, { onDelete: 'cascade' }).notNull(), - progress: integer('progress').default(0), - completedAt: timestamp('completed_at', { withTimezone: true }), -}, (table) => [ - primaryKey({ columns: [table.userId, table.questId] }) -]); - -// 8. User Timers (Generic: Cooldowns, Effects, Access) -export const userTimers = pgTable('user_timers', { - userId: bigint('user_id', { mode: 'bigint' }) - .references(() => users.id, { onDelete: 'cascade' }).notNull(), - type: varchar('type', { length: 50 }).notNull(), // 'COOLDOWN', 'EFFECT', 'ACCESS' - key: varchar('key', { length: 100 }).notNull(), // 'daily', 'chn_12345', 'xp_boost' - expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), - metadata: jsonb('metadata').default({}), // Store channelId, specific buff amounts, etc. -}, (table) => [ - primaryKey({ columns: [table.userId, table.type, table.key] }), - index('user_timers_expires_at_idx').on(table.expiresAt), - index('user_timers_lookup_idx').on(table.userId, table.type, table.key), -]); -// 9. Lootdrops -export const lootdrops = pgTable('lootdrops', { - messageId: varchar('message_id', { length: 255 }).primaryKey(), - channelId: varchar('channel_id', { length: 255 }).notNull(), - rewardAmount: integer('reward_amount').notNull(), - currency: varchar('currency', { length: 50 }).notNull(), - claimedBy: bigint('claimed_by', { mode: 'bigint' }).references(() => users.id, { onDelete: 'set null' }), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - expiresAt: timestamp('expires_at', { withTimezone: true }), -}); - -// 10. Moderation Cases -export const moderationCases = pgTable('moderation_cases', { - id: bigserial('id', { mode: 'bigint' }).primaryKey(), - caseId: varchar('case_id', { length: 50 }).unique().notNull(), - type: varchar('type', { length: 20 }).notNull(), // 'warn', 'timeout', 'kick', 'ban', 'note', 'prune' - userId: bigint('user_id', { mode: 'bigint' }).notNull(), - username: varchar('username', { length: 255 }).notNull(), - moderatorId: bigint('moderator_id', { mode: 'bigint' }).notNull(), - moderatorName: varchar('moderator_name', { length: 255 }).notNull(), - reason: text('reason').notNull(), - metadata: jsonb('metadata').default({}), - active: boolean('active').default(true).notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - resolvedAt: timestamp('resolved_at', { withTimezone: true }), - resolvedBy: bigint('resolved_by', { mode: 'bigint' }), - resolvedReason: text('resolved_reason'), -}, (table) => [ - index('moderation_cases_user_id_idx').on(table.userId), - index('moderation_cases_case_id_idx').on(table.caseId), -]); - - - -export const classesRelations = relations(classes, ({ many }) => ({ - users: many(users), -})); - -export const usersRelations = relations(users, ({ one, many }) => ({ - class: one(classes, { - fields: [users.classId], - references: [classes.id], - }), - inventory: many(inventory), - transactions: many(transactions), - quests: many(userQuests), - timers: many(userTimers), -})); - -export const itemsRelations = relations(items, ({ many }) => ({ - inventoryEntries: many(inventory), -})); - -export const inventoryRelations = relations(inventory, ({ one }) => ({ - user: one(users, { - fields: [inventory.userId], - references: [users.id], - }), - item: one(items, { - fields: [inventory.itemId], - references: [items.id], - }), -})); - -export const transactionsRelations = relations(transactions, ({ one }) => ({ - user: one(users, { - fields: [transactions.userId], - references: [users.id], - }), -})); - -export const questsRelations = relations(quests, ({ many }) => ({ - userEntries: many(userQuests), -})); - -export const userQuestsRelations = relations(userQuests, ({ one }) => ({ - user: one(users, { - fields: [userQuests.userId], - references: [users.id], - }), - quest: one(quests, { - fields: [userQuests.questId], - references: [quests.id], - }), -})); - -export const userTimersRelations = relations(userTimers, ({ one }) => ({ - user: one(users, { - fields: [userTimers.userId], - references: [users.id], - }), -})); - -export const itemTransactionsRelations = relations(itemTransactions, ({ one }) => ({ - user: one(users, { - fields: [itemTransactions.userId], - references: [users.id], - }), - relatedUser: one(users, { - fields: [itemTransactions.relatedUserId], - references: [users.id], - }), - item: one(items, { - fields: [itemTransactions.itemId], - references: [items.id], - }), -})); - -export const moderationCasesRelations = relations(moderationCases, ({ one }) => ({ - user: one(users, { - fields: [moderationCases.userId], - references: [users.id], - }), - moderator: one(users, { - fields: [moderationCases.moderatorId], - references: [users.id], - }), - resolver: one(users, { - fields: [moderationCases.resolvedBy], - references: [users.id], - }), -})); \ No newline at end of file +// Re-export all schema definitions from domain modules +// This file is kept for backward compatibility +export * from './schema/index'; diff --git a/shared/db/schema/economy.ts b/shared/db/schema/economy.ts new file mode 100644 index 0000000..70a310f --- /dev/null +++ b/shared/db/schema/economy.ts @@ -0,0 +1,69 @@ +import { + pgTable, + bigint, + varchar, + text, + timestamp, + bigserial, + index, + integer, +} from 'drizzle-orm/pg-core'; +import { relations, type InferSelectModel } from 'drizzle-orm'; +import { users } from './users'; +import { items } from './inventory'; + +// --- TYPES --- +export type Transaction = InferSelectModel; +export type ItemTransaction = InferSelectModel; + +// --- TABLES --- +export const transactions = pgTable('transactions', { + id: bigserial('id', { mode: 'bigint' }).primaryKey(), + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }), + relatedUserId: bigint('related_user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'set null' }), + amount: bigint('amount', { mode: 'bigint' }).notNull(), + type: varchar('type', { length: 50 }).notNull(), + description: text('description'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), +}, (table) => [ + index('transactions_created_at_idx').on(table.createdAt), +]); + +export const itemTransactions = pgTable('item_transactions', { + id: bigserial('id', { mode: 'bigint' }).primaryKey(), + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }).notNull(), + relatedUserId: bigint('related_user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'set null' }), + itemId: integer('item_id') + .references(() => items.id, { onDelete: 'cascade' }).notNull(), + quantity: bigint('quantity', { mode: 'bigint' }).notNull(), + type: varchar('type', { length: 50 }).notNull(), // e.g., 'TRADE', 'SHOP_BUY', 'DROP' + description: text('description'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), +}); + +// --- RELATIONS --- +export const transactionsRelations = relations(transactions, ({ one }) => ({ + user: one(users, { + fields: [transactions.userId], + references: [users.id], + }), +})); + +export const itemTransactionsRelations = relations(itemTransactions, ({ one }) => ({ + user: one(users, { + fields: [itemTransactions.userId], + references: [users.id], + }), + relatedUser: one(users, { + fields: [itemTransactions.relatedUserId], + references: [users.id], + }), + item: one(items, { + fields: [itemTransactions.itemId], + references: [items.id], + }), +})); diff --git a/shared/db/schema/index.ts b/shared/db/schema/index.ts new file mode 100644 index 0000000..90eaf10 --- /dev/null +++ b/shared/db/schema/index.ts @@ -0,0 +1,6 @@ +// Domain modules +export * from './users'; +export * from './inventory'; +export * from './economy'; +export * from './quests'; +export * from './moderation'; diff --git a/shared/db/schema/inventory.ts b/shared/db/schema/inventory.ts new file mode 100644 index 0000000..4923c00 --- /dev/null +++ b/shared/db/schema/inventory.ts @@ -0,0 +1,57 @@ +import { + pgTable, + bigint, + varchar, + serial, + text, + integer, + jsonb, + primaryKey, + check, +} from 'drizzle-orm/pg-core'; +import { relations, sql, type InferSelectModel } from 'drizzle-orm'; +import { users } from './users'; + +// --- TYPES --- +export type Item = InferSelectModel; +export type Inventory = InferSelectModel; + +// --- TABLES --- +export const items = pgTable('items', { + id: serial('id').primaryKey(), + name: varchar('name', { length: 255 }).unique().notNull(), + description: text('description'), + rarity: varchar('rarity', { length: 20 }).default('C'), + type: varchar('type', { length: 50 }).notNull().default('MATERIAL'), + usageData: jsonb('usage_data').default({}), + price: bigint('price', { mode: 'bigint' }), + iconUrl: text('icon_url').notNull(), + imageUrl: text('image_url').notNull(), +}); + +export const inventory = pgTable('inventory', { + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }).notNull(), + itemId: integer('item_id') + .references(() => items.id, { onDelete: 'cascade' }).notNull(), + quantity: bigint('quantity', { mode: 'bigint' }).default(1n), +}, (table) => [ + primaryKey({ columns: [table.userId, table.itemId] }), + check('quantity_check', sql`${table.quantity} > 0`) +]); + +// --- RELATIONS --- +export const itemsRelations = relations(items, ({ many }) => ({ + inventoryEntries: many(inventory), +})); + +export const inventoryRelations = relations(inventory, ({ one }) => ({ + user: one(users, { + fields: [inventory.userId], + references: [users.id], + }), + item: one(items, { + fields: [inventory.itemId], + references: [items.id], + }), +})); diff --git a/shared/db/schema/moderation.ts b/shared/db/schema/moderation.ts new file mode 100644 index 0000000..a2c329a --- /dev/null +++ b/shared/db/schema/moderation.ts @@ -0,0 +1,65 @@ +import { + pgTable, + bigint, + varchar, + text, + jsonb, + timestamp, + boolean, + bigserial, + integer, + index, +} from 'drizzle-orm/pg-core'; +import { relations, type InferSelectModel } from 'drizzle-orm'; +import { users } from './users'; + +// --- TYPES --- +export type ModerationCase = InferSelectModel; +export type Lootdrop = InferSelectModel; + +// --- TABLES --- +export const moderationCases = pgTable('moderation_cases', { + id: bigserial('id', { mode: 'bigint' }).primaryKey(), + caseId: varchar('case_id', { length: 50 }).unique().notNull(), + type: varchar('type', { length: 20 }).notNull(), // 'warn', 'timeout', 'kick', 'ban', 'note', 'prune' + userId: bigint('user_id', { mode: 'bigint' }).notNull(), + username: varchar('username', { length: 255 }).notNull(), + moderatorId: bigint('moderator_id', { mode: 'bigint' }).notNull(), + moderatorName: varchar('moderator_name', { length: 255 }).notNull(), + reason: text('reason').notNull(), + metadata: jsonb('metadata').default({}), + active: boolean('active').default(true).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + resolvedAt: timestamp('resolved_at', { withTimezone: true }), + resolvedBy: bigint('resolved_by', { mode: 'bigint' }), + resolvedReason: text('resolved_reason'), +}, (table) => [ + index('moderation_cases_user_id_idx').on(table.userId), + index('moderation_cases_case_id_idx').on(table.caseId), +]); + +export const lootdrops = pgTable('lootdrops', { + messageId: varchar('message_id', { length: 255 }).primaryKey(), + channelId: varchar('channel_id', { length: 255 }).notNull(), + rewardAmount: integer('reward_amount').notNull(), + currency: varchar('currency', { length: 50 }).notNull(), + claimedBy: bigint('claimed_by', { mode: 'bigint' }).references(() => users.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + expiresAt: timestamp('expires_at', { withTimezone: true }), +}); + +// --- RELATIONS --- +export const moderationCasesRelations = relations(moderationCases, ({ one }) => ({ + user: one(users, { + fields: [moderationCases.userId], + references: [users.id], + }), + moderator: one(users, { + fields: [moderationCases.moderatorId], + references: [users.id], + }), + resolver: one(users, { + fields: [moderationCases.resolvedBy], + references: [users.id], + }), +})); diff --git a/shared/db/schema/quests.ts b/shared/db/schema/quests.ts new file mode 100644 index 0000000..b1f2e5f --- /dev/null +++ b/shared/db/schema/quests.ts @@ -0,0 +1,54 @@ +import { + pgTable, + bigint, + varchar, + serial, + text, + jsonb, + timestamp, + integer, + primaryKey, +} from 'drizzle-orm/pg-core'; +import { relations, type InferSelectModel } from 'drizzle-orm'; +import { users } from './users'; + +// --- TYPES --- +export type Quest = InferSelectModel; +export type UserQuest = InferSelectModel; + +// --- TABLES --- +export const quests = pgTable('quests', { + id: serial('id').primaryKey(), + name: varchar('name', { length: 255 }).notNull(), + description: text('description'), + triggerEvent: varchar('trigger_event', { length: 50 }).notNull(), + requirements: jsonb('requirements').notNull().default({}), + rewards: jsonb('rewards').notNull().default({}), +}); + +export const userQuests = pgTable('user_quests', { + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }).notNull(), + questId: integer('quest_id') + .references(() => quests.id, { onDelete: 'cascade' }).notNull(), + progress: integer('progress').default(0), + completedAt: timestamp('completed_at', { withTimezone: true }), +}, (table) => [ + primaryKey({ columns: [table.userId, table.questId] }) +]); + +// --- RELATIONS --- +export const questsRelations = relations(quests, ({ many }) => ({ + userEntries: many(userQuests), +})); + +export const userQuestsRelations = relations(userQuests, ({ one }) => ({ + user: one(users, { + fields: [userQuests.userId], + references: [users.id], + }), + quest: one(quests, { + fields: [userQuests.questId], + references: [quests.id], + }), +})); diff --git a/shared/db/schema/users.ts b/shared/db/schema/users.ts new file mode 100644 index 0000000..0b68988 --- /dev/null +++ b/shared/db/schema/users.ts @@ -0,0 +1,80 @@ +import { + pgTable, + bigint, + varchar, + boolean, + jsonb, + timestamp, + integer, + primaryKey, + index, +} from 'drizzle-orm/pg-core'; +import { relations, type InferSelectModel } from 'drizzle-orm'; + +// --- TYPES --- +export type Class = InferSelectModel; +export type User = InferSelectModel; +export type UserTimer = InferSelectModel; + +// --- TABLES --- +export const classes = pgTable('classes', { + id: bigint('id', { mode: 'bigint' }).primaryKey(), + name: varchar('name', { length: 255 }).unique().notNull(), + balance: bigint('balance', { mode: 'bigint' }).default(0n), + roleId: varchar('role_id', { length: 255 }), +}); + +export const users = pgTable('users', { + id: bigint('id', { mode: 'bigint' }).primaryKey(), + classId: bigint('class_id', { mode: 'bigint' }).references(() => classes.id), + username: varchar('username', { length: 255 }).unique().notNull(), + isActive: boolean('is_active').default(true), + + // Economy + balance: bigint('balance', { mode: 'bigint' }).default(0n), + xp: bigint('xp', { mode: 'bigint' }).default(0n), + level: integer('level').default(1), + dailyStreak: integer('daily_streak').default(0), + + // Metadata + settings: jsonb('settings').default({}), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), +}, (table) => [ + index('users_username_idx').on(table.username), + index('users_balance_idx').on(table.balance), + index('users_level_xp_idx').on(table.level, table.xp), +]); + +export const userTimers = pgTable('user_timers', { + userId: bigint('user_id', { mode: 'bigint' }) + .references(() => users.id, { onDelete: 'cascade' }).notNull(), + type: varchar('type', { length: 50 }).notNull(), // 'COOLDOWN', 'EFFECT', 'ACCESS' + key: varchar('key', { length: 100 }).notNull(), // 'daily', 'chn_12345', 'xp_boost' + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + metadata: jsonb('metadata').default({}), +}, (table) => [ + primaryKey({ columns: [table.userId, table.type, table.key] }), + index('user_timers_expires_at_idx').on(table.expiresAt), + index('user_timers_lookup_idx').on(table.userId, table.type, table.key), +]); + +// --- RELATIONS --- +export const classesRelations = relations(classes, ({ many }) => ({ + users: many(users), +})); + +export const usersRelations = relations(users, ({ one, many }) => ({ + class: one(classes, { + fields: [users.classId], + references: [classes.id], + }), + timers: many(userTimers), +})); + +export const userTimersRelations = relations(userTimers, ({ one }) => ({ + user: one(users, { + fields: [userTimers.userId], + references: [users.id], + }), +}));