-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Migrate database infrastructure from Drizzle ORM to Prisma #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Create main schema.prisma with all models - Add individual schema files in organized folders (guild/, events/) - Configure Prisma in package.json with new scripts - Add prisma.config.ts for multi-database configuration
- Move schema files to prisma/schema/ directory - Create separate files for each model (guild, user, channel, events) - Add prisma.config.ts with proper configuration - Update package.json to remove deprecated prisma config - Successfully generate Prisma Client with new structure
- Update types.ts to use PrismaClient instead of Drizzle types - Rewrite factory.ts to create Prisma connections - Update connection.ts to remove schema imports - Remove old Drizzle connection files (sqlite, postgres) - Remove old schema, generators, migrations directories - Remove Drizzle config files - Remove generate-schemas script
- Update all query classes to use Prisma Client - Replace Drizzle ORM queries with Prisma queries and raw SQL - Update MessageQueries, UserQueries, VoiceQueries - Update ChannelQueries, MemberQueries, ReactionQueries - Update db/index.ts to remove schema exports - Update db/interface.ts to use PrismaClient type
- Update MemberProcessor to use Prisma Client - Update ReactionProcessor to use Prisma Client - Update InsightsEngine to use Prisma queries - Remove Drizzle imports from helpers.ts - Update all processor methods to use Prisma syntax
- Update upsertUser to use Prisma upsert - Update upsertGuild to use Prisma upsert - Update upsertChannel to use Prisma upsert - Remove schema imports from base processor
- Update gatherUserData to use Prisma raw SQL - Update gatherChannelData to use Prisma raw SQL - Update gatherMessageData to use Prisma findMany - Update gatherVoiceData to use Prisma findMany - Update gatherMemberData to use Prisma findMany - Remove all Drizzle imports and schema references
- Remove drizzle-orm and drizzle-kit from package.json - Remove unused src/db/utils.ts file - Complete migration to Prisma
- Extract buildPostgresUrl helper function - Extract buildMysqlUrl helper function - Extract buildDatasourceUrl helper function - Fix all formatting issues - Pass all linting checks
- Update MessageProcessor to use Prisma create - Update PresenceProcessor to use Prisma create - Update VoiceProcessor to use Prisma create - Update GuildProcessor to use Prisma upsert - Update GuildHandler to use Prisma upsert/update - Fix mention.ts import to use GuildMember type - Fix aggregator.ts type assertion - All TypeScript compilation errors resolved
- Remove drizzle/ directory with old migrations - Complete cleanup of Drizzle-related files
WalkthroughComplete migration from Drizzle ORM to Prisma as the database layer. Removed all Drizzle configuration, migrations, and schema generation infrastructure. Updated database handlers, processors, analytics, and type system to use Prisma client and raw SQL queries where appropriate. Added new Prisma schema file. Changes
Sequence Diagram(s)sequenceDiagram
participant Old as Old (Drizzle ORM)
participant New as New (Prisma)
rect rgb(200, 220, 255)
Note over Old,New: Database Connection Flow
Old->>Old: Load Drizzle config<br/>(drizzle.*.config.ts)
Old->>Old: Connect via drizzle-orm<br/>getDb(), getSqliteDb(), etc.
Old->>Old: Build query with ORM<br/>db.select().from(schema).where()
Old->>Old: Execute & map results
New->>New: Load Prisma schema<br/>(prisma/schema.prisma)
New->>New: Create PrismaClient<br/>createDatabaseConnection()
New->>New: Use client methods<br/>db.user.findMany()
New->>New: Execute & return typed data
end
rect rgb(200, 255, 220)
Note over Old,New: Schema Generation (Removed)
Old->>Old: Read Zod schemas
Old->>Old: Generate SQL via<br/>SchemaGenerator
Old->>Old: Write migrations
New->>New: Schema defined directly<br/>in prisma/schema.prisma
New->>New: No code generation needed
end
rect rgb(255, 220, 200)
Note over Old,New: Query Execution Changes
Old->>Old: ORM: db.select().from().where()
Old->>Old: Returns typed ORM objects
New->>New: Prisma: db.model.findMany({where})
New->>New: Raw SQL: db.$queryRaw\`SELECT...\`
New->>New: Returns plain objects/bigint
New->>New: Convert bigint to number
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Areas requiring extra attention:
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/processors/base.ts (1)
55-73: Require owner_id for guild upsert; avoid empty-string FKs.Empty ownerId can violate FK constraints and create invalid data. Validate presence and fail fast.
- private validateGuildData(guild: unknown): boolean { + private validateGuildData(guild: unknown): boolean { @@ - return !!(g.id && typeof g.id === 'string'); + return !!( + g.id && typeof g.id === 'string' && + g.owner_id && typeof g.owner_id === 'string' + ); }- ownerId: (guildData.owner_id as string) || '', + ownerId: guildData.owner_id as string,Also applies to: 87-95
package.json (1)
7-18: Ensure Prisma Client is installed and generated during install/CI.Add @prisma/client and prisma, and run prisma generate automatically to fix TS2307 and keep client in sync.
"scripts": { + "postinstall": "prisma generate", @@ - "db:generate": "prisma generate", + "db:generate": "prisma generate", @@ - "db:studio": "prisma studio", + "db:studio": "prisma studio", }, @@ "dependencies": { + "@prisma/client": "^5.19.0", @@ "devDependencies": { + "prisma": "^5.19.0",Optional: if Drizzle-era drivers are no longer used directly, consider removing "better-sqlite3" and "postgres" to slim the install.
src/export/exporter.ts (1)
5-10: Implement XLSX formatter or remove xlsx export option.The xlsx formatter is mapped to JSONFormatter, which returns JSON data with
.jsonfile extension andapplication/jsoncontent-type. Since 'xlsx' is listed as a valid format option in ExportOptions, users can request this format but will receive incorrect output. Either implement a real XLSXFormatter class or remove 'xlsx' from the format options.
🧹 Nitpick comments (28)
src/handlers/guildHandler.ts (2)
42-61: Minor: consider relying on Prisma's @updatedat instead of manual timestamp.If your
Guild.updatedAtis annotated with@updatedAt, Prisma sets it automatically; you can dropupdatedAt: new Date()from the update payload. If not, keep as-is.Please confirm the schema for
Guild.updatedAt.
65-75: Make update resilient to missed create events (use upsert or handle P2025).If the bot restarts and misses a create,
update()throws "Record not found." Preferupserthere or catch P2025 and fallback to create.- await this.database.guild.update({ - where: { id: data.id }, - data: { - name: data.name, - icon: data.icon, - ownerId: data.ownerId, - updatedAt: new Date(), - }, - }); + await this.database.guild.upsert({ + where: { id: data.id }, + create: { + id: data.id, + name: data.name, + icon: data.icon, + ownerId: data.ownerId, + memberCount: (data as any).memberCount ?? 0, + }, + update: { + name: data.name, + icon: data.icon, + ownerId: data.ownerId, + // If using @updatedAt, you can omit this: + updatedAt: new Date(), + }, + });If you intentionally want an exception on missing guilds, keep
update()but add contextual logging.src/processors/message.ts (1)
21-32: Use upsert for idempotency and parallelize pre‑work.Create will fail on duplicates (retries, replays). Upsert avoids P2002 and makes the processor safe to retry. Also run user/channel upserts in parallel.
- await this.upsertUser(message.author); - await this.upsertChannel(message); + await Promise.all([this.upsertUser(message.author), this.upsertChannel(message)]); - - await this.db.messageEvent.create({ - data: { + await this.db.messageEvent.upsert({ + where: { id: message.id }, + create: { id: message.id, guildId: message.guild_id, channelId: message.channel_id, userId: message.author.id, - content: (message.content || '').slice(0, 2000), + content: (message.content ?? '').slice(0, 2000), attachmentCount: message.attachments?.length || 0, embedCount: message.embeds?.length || 0, timestamp: new Date(message.timestamp), - }, + }, + update: { + // Typically immutable; keep empty for true idempotency: + }, });If
MessageEvent.idis not the Discord message ID, adjust thewherefield to your unique constraint.prisma/schema/reaction-event.prisma (2)
5-16: Constrainactionand add query indexes.Use an enum for
actionand add indexes you frequently filter by (guildId/timestamp, messageId, userId). This improves correctness and analytics performance.+enum ReactionAction { + add + remove +} + model ReactionEvent { id String @id @default(uuid()) timestamp DateTime createdAt DateTime @default(now()) guildId String channelId String messageId String userId String emojiId String? emojiName String emojiAnimated Boolean @default(false) - action String + action ReactionAction @@ - @@map("reactionevent") + @@map("reactionevent") + @@index([guildId, timestamp]) + @@index([messageId]) + @@index([guildId, userId, timestamp]) }If you already have equivalent indexes at the DB level, align them here so Prisma can generate/validate them.
17-21: Optional: add relations to Channel/Message models if available.If
ChannelandMessageEventexist, relatingchannelId/messageIdimproves referential integrity and enables cascading deletes.Example:
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade) message MessageEvent @relation(fields: [messageId], references: [id], onDelete: Cascade)Only add if you have corresponding models and constraints.
prisma.config.ts (1)
4-10: Config LGTM; consider resolving schema to an absolute path.Minor robustness tweak for alternate CWDs.
-import path from 'node:path'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ - schema: path.join('prisma', 'schema'), + schema: path.resolve(__dirname, 'prisma', 'schema'), engine: 'classic', datasource: { url: env('DATABASE_URL'), }, });Confirm your Prisma version supports
prisma/configand directory schemas (generation already passing suggests yes).src/db/connection.ts (1)
9-16: Harden singleton against concurrent initialization.Two concurrent
getDb()calls can race and create multiple clients. Guard with a shared promise.-let db: DatabaseInstance; +let db: DatabaseInstance | undefined; +let dbInitPromise: Promise<DatabaseInstance> | null = null; @@ export const getDb = async ( config: DatabaseConfig = { type: 'sqlite', path: './data/stats.db' } ): Promise<DatabaseInstance> => { - if (!db) { - db = await createDatabaseConnection(config); - } - return db; + if (db) return db; + if (!dbInitPromise) { + dbInitPromise = createDatabaseConnection(config).then((instance) => { + db = instance; + return instance; + }); + } + return dbInitPromise; };Optional: expose
export const closeDb = async () => db?.$disconnect();for graceful shutdowns.src/stats/aggregator.ts (1)
76-77: Prefer typed query rows over per-field casts.Define an interface for
getChannelStatsrows and type the query so you can drop theseascasts.Example:
type ChannelStatsRow = { channelId: string; channelName?: string; messageCount: number; uniqueUsers: number }; // In StatsQueries.getChannelStats(): return Promise<ChannelStatsRow[]>Then here:
topChannels: channelStats.map((ch) => ({ channelId: ch.channelId, name: ch.channelName ?? undefined, messageCount: Number(ch.messageCount), uniqueUsers: Number(ch.uniqueUsers), })) ```<!-- review_comment_end --> </blockquote></details> <details> <summary>src/stats/queries/member.ts (1)</summary><blockquote> `13-22`: **Optional: compute joins and leaves in one pass.** A single query with conditional aggregation reduces I/O: ```ts type GrowthRow = { date: string; joins: bigint; leaves: bigint }; const rows = await this.db.$queryRaw<GrowthRow[]>` SELECT date(timestamp) as date, SUM(CASE WHEN action = 'join' THEN 1 ELSE 0 END) as joins, SUM(CASE WHEN action = 'leave' THEN 1 ELSE 0 END) as leaves FROM memberevent WHERE guildId = ${guildId} AND timestamp >= ${since} GROUP BY date(timestamp) ORDER BY date(timestamp) `; return { joins: rows.map(r => ({ date: r.date, joins: Number(r.joins) })), leaves: rows.map(r => ({ date: r.date, leaves: Number(r.leaves) })), };This keeps the same shape with fewer round trips.
Also applies to: 24-33
src/processors/reaction.ts (2)
104-106: Replace find+create with upsert to avoid races.Concurrent events can double-create and fail. Use upsert or reuse BaseProcessor.upsertUser.
Apply:
- const existing = await this.db.user.findUnique({ - where: { id: userId }, - }); - - if (!existing) { - await this.db.user.create({ - data: { - id: userId, - username: 'Unknown', - discriminator: '0000', - }, - }); - } + await this.db.user.upsert({ + where: { id: userId }, + update: {}, + create: { id: userId, username: 'Unknown', discriminator: '0000' }, + });Alternatively, normalize on the shared helper:
- private async ensureUserExists(userId: string): Promise<void> { - const existing = await this.db.user.findUnique({ where: { id: userId } }); - if (!existing) { await this.db.user.create({ data: { id: userId, username: 'Unknown', discriminator: '0000' } }); } - } + private async ensureUserExists(userId: string): Promise<void> { + await this.upsertUser({ id: userId }); + }Also applies to: 108-115
120-122: Same race on channel creation — use upsert or shared helper.Apply:
- const existing = await this.db.channel.findUnique({ - where: { id: channelId }, - }); - - if (!existing) { - await this.db.channel.create({ - data: { - id: channelId, - guildId, - name: 'Unknown', - type: 0, - }, - }); - } + await this.db.channel.upsert({ + where: { id: channelId }, + update: {}, + create: { id: channelId, guildId, name: 'Unknown', type: 0 }, + });Optionally reuse BaseProcessor.upsertChannel for consistency.
Also applies to: 124-131
prisma/schema/channel.prisma (1)
1-16: Add updatedAt and helpful indexes.Common queries filter by guildId/parentId; updatedAt aids auditing/sync.
Apply:
model Channel { id String @id guildId String name String? type Int parentId String? - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@ guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) @@map("channel") + @@index([guildId]) + @@index([parentId]) }prisma/schema/user.prisma (1)
1-23: Consider uniqueness and query indexes.If your domain expects (username, discriminator) uniqueness, encode it; add an index on bot for frequent filters.
Apply if appropriate:
model User { id String @id createdAt DateTime @default(now()) updatedAt DateTime @updatedAt username String discriminator String avatar String? bot Boolean @default(false) @@ @@map("user") + @@unique([username, discriminator]) // if required by business rules + @@index([bot]) }src/processors/voice.ts (1)
45-64: Switch case writes should be atomic.Wrap leave+join writes in a single transaction to avoid partial persistence on failure.
Apply:
- await this.db.voiceEvent.create({ - data: { - guildId: voiceState.guild_id, - channelId: currentSession.channelId, - userId: voiceState.user_id, - action: 'leave', - duration: Math.floor(duration / 1000), - timestamp: new Date(), - }, - }); - - await this.db.voiceEvent.create({ - data: { - guildId: voiceState.guild_id, - channelId: voiceState.channel_id, - userId: voiceState.user_id, - action: 'join', - timestamp: new Date(), - }, - }); + await this.db.$transaction([ + this.db.voiceEvent.create({ + data: { + guildId: voiceState.guild_id, + channelId: currentSession.channelId, + userId: voiceState.user_id, + action: 'leave', + duration: Math.floor(duration / 1000), + timestamp: new Date(), + }, + }), + this.db.voiceEvent.create({ + data: { + guildId: voiceState.guild_id, + channelId: voiceState.channel_id, + userId: voiceState.user_id, + action: 'join', + timestamp: new Date(), + }, + }), + ]);Optional: ensure user/channel exist (reuse BaseProcessor helpers) to prevent FK failures.
src/processors/member.ts (1)
55-64: Optional: log leave only when an active membership existed.You already filter leftAt: null, but updateMany may affect 0 rows. Consider guarding the event on updated count.
Example:
const { count } = await this.db.member.updateMany({ /* ... */ }); if (count > 0) { await this.db.memberEvent.create({ /* leave event */ }); }prisma/schema/message-event.prisma (1)
8-16: Add Channel relation and analytics indexes; review cascade semantics.
- channelId has no FK. Consider linking to Channel to prevent orphans:
channel Channel? @relation(fields: [channelId], references: [id], onDelete: SetNull)- Add indexes used by stats/timeline:
@@index([guildId, timestamp])
@@index([guildId, userId, timestamp])- onDelete: Cascade on User will wipe historical analytics if a user row is removed. Confirm intended retention; SetNull + userId optional may be preferable.
Also applies to: 18-19
prisma/schema/member-event.prisma (1)
5-16: Tighten data types and add indexes for query patterns.
- roles as String containing JSON is brittle. Prefer Json (if provider allows) or document strict encoding/decoding path centrally.
- action should be an enum (e.g., JOIN, LEAVE, UPDATE) to avoid free‑form values.
- Add analytics indexes:
@@index([guildId, timestamp])
@@index([guildId, userId, timestamp])Review onDelete: Cascade for User as it impacts historical analytics.
prisma/schema/voice-event.prisma (1)
8-17: Add Channel relation and performance indexes; validate duration semantics.
- Link channelId to Channel to avoid orphans:
channel Channel? @relation(fields: [channelId], references: [id], onDelete: SetNull)- Add:
@@index([guildId, timestamp])
@@index([guildId, userId, timestamp])- Ensure only 'leave' events carry non-null, non-negative duration; enforce in write path and tests.
prisma/schema/member.prisma (1)
4-18: Add composite unique and indexes; consider Json for roles.
- If Member is the current state, enforce uniqueness:
@@unique([guildId, userId])- Add:
@@index([guildId])
@@index([guildId, userId])- roles as JSON is preferable to serialized String for queryability/validation.
prisma/schema/presence-event.prisma (1)
5-18: Constrain status to an enum and add analytics indexes.
- Define an enum PresenceStatus { ONLINE, IDLE, DND, OFFLINE } and set status to that enum.
- Add:
@@index([guildId, timestamp])
@@index([guildId, userId, timestamp])- Reconfirm onDelete: Cascade on User aligns with retention expectations.
prisma/schema/guild.prisma (1)
5-12: Add supporting indexes for common access patterns.Given frequent filtering/joining by ownerId and createdAt, consider:
- @@index([ownerId])
- @@index([createdAt])
Also applies to: 22-23
src/stats/queries/user.ts (1)
21-23: Quote table name "user" for portability."user" is reserved in some SQL dialects (e.g., Postgres). Quote it to avoid future breakage.
- INNER JOIN user u ON m.userId = u.id + INNER JOIN "user" u ON m.userId = u.idApply similarly to the voice query.
src/processors/base.ts (2)
107-120: Upsert channel should refresh mutable fields.Empty update {} leaves stale name/type/parentId after renames. Update safely.
- update: {}, + update: { + name: (channelData.name as string) || 'Unknown Channel', + type: (channelData.type as number) || 0, + parentId: (channelData.parent_id as string | null) ?? null, + },
24-29: Prefer nullish coalescing over || for defaults.'||' treats '' and 0 as falsy; '??' only defaults for null/undefined.
- username: (userData.username as string) || 'Unknown', - discriminator: (userData.discriminator as string) || '0000', - avatar: userData.avatar as string | null, - bot: (userData.bot as boolean) || false, + username: (userData.username as string) ?? 'Unknown', + discriminator: (userData.discriminator as string) ?? '0000', + avatar: (userData.avatar as string | null) ?? null, + bot: Boolean(userData.bot), @@ - name: (guildData.name as string) || 'Unknown Guild', - icon: guildData.icon as string | null, + name: (guildData.name as string) ?? 'Unknown Guild', + icon: (guildData.icon as string | null) ?? null, @@ - name: (channelData.name as string) || 'Unknown Channel', + name: (channelData.name as string) ?? 'Unknown Channel',Also applies to: 68-73, 113-118
src/export/exporter.ts (2)
172-203: Type the voice map callback and avoid implicit any.Fix TS7006 at Line 195 by typing v.
- const voice = await this.db.voiceEvent.findMany({ + type VoiceRow = { + id: string; + userId: string; + channelId: string | null; + action: string; + duration: number | null; + timestamp: Date; + }; + const voice = await this.db.voiceEvent.findMany({ @@ - data.voice = voice.map((v) => ({ + data.voice = voice.map((v: VoiceRow) => ({ id: v.id, userId: v.userId, channelId: v.channelId || '', action: v.action, duration: v.duration || undefined, timestamp: v.timestamp, }));
150-170: Consider pagination instead of hard take=10000.Hard caps risk truncation for large exports. Use cursor/skip-take batching to stream results.
src/db/factory.ts (2)
23-33: Avoid insecure default MySQL password and encode credentials.Defaulting to 'password' is risky; prefer empty or require explicit config. Also URI‑encode.
- const username = config.username || 'root'; - const password = config.password || 'password'; - return `mysql://${username}:${password}@${host}:${port}/${database}`; + const username = encodeURIComponent(config.username || 'root'); + const password = encodeURIComponent(config.password || ''); + return `mysql://${username}:${password}@${host}:${port}/${database}`;
55-75: Harden connection lifecycle (optional).Wrap $connect in try/catch to surface clearer errors and add optional Prisma logs in dev.
- await prisma.$connect(); + try { + await prisma.$connect(); + } catch (e) { + await prisma.$disconnect().catch(() => {}); + throw new Error(`Failed to connect to database at ${datasourceUrl}: ${String(e)}`); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (7)
bun.lockis excluded by!**/*.locksrc/db/generated/mysql/index.tsis excluded by!**/generated/**src/db/generated/mysql/schema.tsis excluded by!**/generated/**src/db/generated/postgres/index.tsis excluded by!**/generated/**src/db/generated/postgres/schema.tsis excluded by!**/generated/**src/db/generated/sqlite/index.tsis excluded by!**/generated/**src/db/generated/sqlite/schema.tsis excluded by!**/generated/**
📒 Files selected for processing (83)
drizzle.mysql.config.ts(0 hunks)drizzle.postgres.config.ts(0 hunks)drizzle.sqlite.config.ts(0 hunks)drizzle/mysql/0000_short_mercury.sql(0 hunks)drizzle/mysql/0001_giant_dreadnoughts.sql(0 hunks)drizzle/mysql/0002_square_rhino.sql(0 hunks)drizzle/mysql/meta/0000_snapshot.json(0 hunks)drizzle/mysql/meta/0001_snapshot.json(0 hunks)drizzle/mysql/meta/0002_snapshot.json(0 hunks)drizzle/mysql/meta/_journal.json(0 hunks)drizzle/postgres/0000_fast_peter_parker.sql(0 hunks)drizzle/postgres/0001_burly_molly_hayes.sql(0 hunks)drizzle/postgres/0002_oval_pyro.sql(0 hunks)drizzle/postgres/0003_cheerful_blacklash.sql(0 hunks)drizzle/postgres/meta/0000_snapshot.json(0 hunks)drizzle/postgres/meta/0001_snapshot.json(0 hunks)drizzle/postgres/meta/0002_snapshot.json(0 hunks)drizzle/postgres/meta/0003_snapshot.json(0 hunks)drizzle/postgres/meta/_journal.json(0 hunks)drizzle/sqlite/0000_good_purple_man.sql(0 hunks)drizzle/sqlite/0001_tough_tag.sql(0 hunks)drizzle/sqlite/0002_salty_layla_miller.sql(0 hunks)drizzle/sqlite/0003_spooky_roughhouse.sql(0 hunks)drizzle/sqlite/0004_melted_darwin.sql(0 hunks)drizzle/sqlite/meta/0000_snapshot.json(0 hunks)drizzle/sqlite/meta/0001_snapshot.json(0 hunks)drizzle/sqlite/meta/0002_snapshot.json(0 hunks)drizzle/sqlite/meta/0003_snapshot.json(0 hunks)drizzle/sqlite/meta/0004_snapshot.json(0 hunks)drizzle/sqlite/meta/_journal.json(0 hunks)package.json(1 hunks)prisma.config.ts(1 hunks)prisma/schema/base.prisma(1 hunks)prisma/schema/channel.prisma(1 hunks)prisma/schema/guild.prisma(1 hunks)prisma/schema/member-event.prisma(1 hunks)prisma/schema/member.prisma(1 hunks)prisma/schema/message-event.prisma(1 hunks)prisma/schema/presence-event.prisma(1 hunks)prisma/schema/reaction-event.prisma(1 hunks)prisma/schema/user.prisma(1 hunks)prisma/schema/voice-event.prisma(1 hunks)scripts/generate-schemas.ts(0 hunks)src/analytics/insights.ts(2 hunks)src/db/connection.ts(2 hunks)src/db/factory.ts(1 hunks)src/db/index.ts(1 hunks)src/db/interface.ts(1 hunks)src/db/postgres/connection.ts(0 hunks)src/db/schema/index.ts(0 hunks)src/db/sqlite/connection.ts(0 hunks)src/db/types.ts(2 hunks)src/db/utils.ts(0 hunks)src/export/exporter.ts(2 hunks)src/generators/base.ts(0 hunks)src/generators/generator.ts(0 hunks)src/generators/index.ts(0 hunks)src/generators/mysql.ts(0 hunks)src/generators/postgres.ts(0 hunks)src/generators/sqlite.ts(0 hunks)src/handlers/guildHandler.ts(1 hunks)src/migrations/index.ts(0 hunks)src/migrations/manager.ts(0 hunks)src/processors/base.ts(6 hunks)src/processors/guild.ts(2 hunks)src/processors/member.ts(3 hunks)src/processors/message.ts(2 hunks)src/processors/presence.ts(2 hunks)src/processors/reaction.ts(4 hunks)src/processors/voice.ts(4 hunks)src/schemas/base.ts(0 hunks)src/schemas/events.ts(0 hunks)src/schemas/guild.ts(0 hunks)src/schemas/index.ts(0 hunks)src/stats/aggregator.ts(1 hunks)src/stats/helpers.ts(0 hunks)src/stats/queries/channel.ts(1 hunks)src/stats/queries/member.ts(1 hunks)src/stats/queries/message.ts(1 hunks)src/stats/queries/reaction.ts(1 hunks)src/stats/queries/user.ts(1 hunks)src/stats/queries/voice.ts(1 hunks)src/types/api/message/mention.ts(1 hunks)
💤 Files with no reviewable changes (48)
- drizzle/sqlite/0002_salty_layla_miller.sql
- drizzle.mysql.config.ts
- src/schemas/index.ts
- drizzle/sqlite/0001_tough_tag.sql
- src/generators/mysql.ts
- src/schemas/guild.ts
- drizzle.postgres.config.ts
- src/db/postgres/connection.ts
- src/migrations/index.ts
- drizzle/sqlite/0000_good_purple_man.sql
- scripts/generate-schemas.ts
- drizzle/postgres/0001_burly_molly_hayes.sql
- src/generators/sqlite.ts
- src/schemas/events.ts
- src/db/sqlite/connection.ts
- drizzle/postgres/meta/_journal.json
- drizzle/mysql/meta/0002_snapshot.json
- src/generators/base.ts
- drizzle/postgres/0002_oval_pyro.sql
- drizzle/postgres/0003_cheerful_blacklash.sql
- drizzle/mysql/meta/0001_snapshot.json
- drizzle/sqlite/meta/0003_snapshot.json
- drizzle/postgres/meta/0000_snapshot.json
- drizzle/sqlite/0004_melted_darwin.sql
- drizzle/sqlite/meta/_journal.json
- drizzle/sqlite/0003_spooky_roughhouse.sql
- drizzle/sqlite/meta/0004_snapshot.json
- drizzle/sqlite/meta/0000_snapshot.json
- drizzle/postgres/0000_fast_peter_parker.sql
- drizzle/postgres/meta/0002_snapshot.json
- src/schemas/base.ts
- drizzle/sqlite/meta/0001_snapshot.json
- src/db/utils.ts
- drizzle/mysql/0000_short_mercury.sql
- drizzle/sqlite/meta/0002_snapshot.json
- src/generators/postgres.ts
- drizzle/mysql/0001_giant_dreadnoughts.sql
- src/generators/generator.ts
- src/stats/helpers.ts
- src/migrations/manager.ts
- drizzle/mysql/meta/_journal.json
- src/generators/index.ts
- drizzle.sqlite.config.ts
- drizzle/postgres/meta/0003_snapshot.json
- drizzle/mysql/meta/0000_snapshot.json
- src/db/schema/index.ts
- drizzle/postgres/meta/0001_snapshot.json
- drizzle/mysql/0002_square_rhino.sql
🧰 Additional context used
🧬 Code graph analysis (12)
src/db/interface.ts (1)
src/db/index.ts (1)
CommonDatabase(6-6)
src/stats/queries/member.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/stats/queries/channel.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/processors/base.ts (2)
src/db/index.ts (1)
CommonDatabase(6-6)src/db/interface.ts (1)
CommonDatabase(6-6)
src/analytics/insights.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/stats/queries/message.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/db/factory.ts (2)
src/db/types.ts (4)
PostgresConfig(10-22)MysqlConfig(24-33)DatabaseConfig(35-35)DatabaseInstance(49-49)src/db/index.ts (2)
createDatabaseConnection(3-3)createDatabaseConnection(4-4)
src/handlers/guildHandler.ts (2)
src/types/event/index.ts (1)
GuildUpdate(65-65)src/types/event/guilds.ts (1)
GuildUpdate(39-39)
src/export/exporter.ts (1)
src/export/formats.ts (2)
ExportData(15-60)ExportOptions(1-13)
src/stats/queries/user.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/stats/queries/reaction.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/stats/queries/voice.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
🪛 GitHub Actions: TypeScript Check
src/db/interface.ts
[error] 1-1: TS2307: Cannot find module '@prisma/client' or its corresponding type declarations.
src/stats/queries/member.ts
[error] 36-36: TS7006: Parameter 'r' implicitly has an 'any' type.
[error] 37-37: TS7006: Parameter 'r' implicitly has an 'any' type.
src/stats/queries/channel.ts
[error] 34-34: TS7006: Parameter 'r' implicitly has an 'any' type.
src/analytics/insights.ts
[error] 32-32: TS2345: Argument of type '{ hour: number; activity: {}; }[]' is not assignable to parameter of type '{ hour: number; activity: number; }[]'. Type '{}' is not assignable to number.
[error] 33-33: TS2345: Argument of type '{ hour: number; activity: {}; }[]' is not assignable to parameter of type '{ hour: number; activity: number; }[]'. Type '{}' is not assignable to number.
[error] 80-80: TS7006: Parameter 'r' implicitly has an 'any' type.
src/stats/queries/message.ts
[error] 55-55: TS7006: Parameter 'r' implicitly has an 'any' type.
src/db/factory.ts
[error] 1-1: TS2307: Cannot find module '@prisma/client' or its corresponding type declarations.
src/export/exporter.ts
[error] 104-104: TS7006: Parameter 'row' implicitly has an 'any' type.
[error] 138-138: TS7006: Parameter 'c' implicitly has an 'any' type.
[error] 195-195: TS7006: Parameter 'v' implicitly has an 'any' type.
src/stats/queries/user.ts
[error] 30-30: TS7006: Parameter 'r' implicitly has an 'any' type.
[error] 54-54: TS7006: Parameter 'r' implicitly has an 'any' type.
src/stats/queries/reaction.ts
[error] 24-24: TS7006: Parameter 'r' implicitly has an 'any' type.
[error] 58-58: TS7006: Parameter 'r' implicitly has an 'any' type.
[error] 93-93: TS7006: Parameter 'r' implicitly has an 'any' type.
[error] 125-125: TS7006: Parameter 'r' implicitly has an 'any' type.
src/db/types.ts
[error] 1-1: TS2307: Cannot find module '@prisma/client' or its corresponding type declarations.
🔇 Additional comments (19)
src/types/api/message/mention.ts (2)
1-1: LGTM! Import consolidation aligns with the migration.The import statement cleanly consolidates both
APIGuildMemberandGuildMemberfrom a single source, which improves maintainability and aligns with the Prisma migration's type reorganization.
4-6: LGTM! Type definitions correctly updated for the migration.The
Mentiontype has been properly updated to useGuildMemberinstead ofMember, maintaining consistency with the import changes and the broader Prisma migration. Since TypeScript compilation passed, type compatibility is confirmed.prisma/schema/base.prisma (1)
1-8: Base schema looks good.Generator and SQLite datasource setup are correct for a split schema. Ensure
DATABASE_URLpoints to your desired file path in all environments.Confirm your Prisma version supports multi-file schemas with
prisma.config.ts(it should if generation passed).src/handlers/guildHandler.ts (1)
42-61: Remove dead event listener registrations in constructor.The
dispatcher.on()calls (lines 30–39) register handlers that are never invoked. EventDispatcher uses internal routing via switch/processEvent() and never emits events with these listener names. The handlers are unreachable dead code; remove all 11 registrations:export class GuildHandler extends Handler { public constructor(database: CommonDatabase, dispatcher: EventDispatcher) { super(database, [Intents.GUILDS]); - dispatcher.on(Events.GUILD_CREATE, this.handleGuildCreate); - dispatcher.on(Events.GUILD_UPDATE, this.handleGuildUpdate); - dispatcher.on(Events.GUILD_DELETE, this.handleGuildDelete); - dispatcher.on(Events.GUILD_ROLE_CREATE, this.handleGuildRoleCreate); - dispatcher.on(Events.GUILD_ROLE_UPDATE, this.handleGuildRoleUpdate); - dispatcher.on(Events.GUILD_ROLE_DELETE, this.handleGuildRoleDelete); - dispatcher.on(Events.CHANNEL_CREATE, this.handleChannelCreate); - dispatcher.on(Events.CHANNEL_UPDATE, this.handleChannelUpdate); - dispatcher.on(Events.CHANNEL_DELETE, this.handleChannelDelete); - dispatcher.on(Events.CHANNEL_PINS_UPDATE, this.handleChannelPinsUpdate); }The handler methods themselves are not called and should either be removed or refactored into the dispatcher's processor classes if their logic is needed.
Likely an incorrect or invalid review comment.
src/processors/guild.ts (1)
4-6: Docs LGTM. Brief method JSDoc improves readability.src/processors/presence.ts (1)
10-12: Docs LGTM.src/processors/reaction.ts (4)
20-22: Docs LGTM.
32-44: Event write LGTM. Payload shape looks consistent; null-coalesce for emojiId is fine.If ReactionEvent has FKs to user/channel, ensure upstream creation succeeds or use a transaction with the create below.
50-52: Docs LGTM.
62-74: Event write LGTM. Mirrors add-case; consistent fields.src/processors/voice.ts (1)
7-9: Docs LGTM.src/db/index.ts (1)
7-7: Export LGTM. Adding DatabaseManager aligns with the new DB management flow.src/processors/member.ts (1)
4-6: Docs LGTM.src/stats/queries/voice.ts (1)
13-31: LGTM with tiny nits.
- Logic is sound; Prisma aggregate is appropriate.
- Prefer nullish coalescing: result._sum.duration ?? 0.
- Verify action values exactly match stored VoiceEvent.action (e.g., 'leave' vs 'LEFT').
src/stats/queries/message.ts (2)
13-26: Aggregate path looks good; confirm provider-specific SQL below.
- getStats aggregate is fine.
- The timeline SQL uses SQLite strftime(). If you may switch providers (e.g., PostgreSQL), this will break; consider a provider-specific switch or a query builder.
Also applies to: 28-34
55-58: Type annotation already provided via generic parameter—proposed diff is unnecessary.The code does not have a TS7006 issue. The
resultsvariable at line 43 is explicitly typed with a generic parameter:this.db.$queryRaw<Array<{ hour: number; count: bigint }>>. TypeScript infers the parameter typerin the map callback from this generic declaration, making the proposed explicit annotation on the callback parameter redundant.Likely an incorrect or invalid review comment.
prisma/schema/guild.prisma (1)
13-13: Confirm cascade semantics for owner relation.onDelete: Cascade means deleting a User cascades and deletes its Guild(s). Is that intended? Many systems prefer SetNull on ownerId (then reassign) to avoid accidental guild loss. If you want SetNull, make ownerId optional and change the referential action.
src/analytics/insights.ts (1)
151-161: Guard against zero/undefined memberCount and ensure number type.If guild.memberCount is null/undefined, default to 1 to avoid division by zero; ensure numeric.
Please confirm schema has memberCount default or that nulls won’t occur. Current fallback is fine for safety.
src/stats/queries/reaction.ts (1)
1-6: Prisma model names verified—no issues found.The models "ReactionEvent" and "User" both exist in the Prisma schema with the correct identifiers, confirming that raw SQL references match actual table/model names.
- Add bigint types for COUNT and SUM aggregations - Add proper type annotations for map callbacks - Fix type compatibility issues in queries and exports - Resolve all TypeScript compilation errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (7)
src/analytics/insights.ts (1)
66-87: Add explicit types forgetHourlyActivity.TypeScript still infers
{}foractivity, producing the TS7006/assignment error reported in the pipeline. Please spell out the return type, give theMapexplicit generics, and use??so the number stays a number.- private async getHourlyActivity(guildId: string, days: number) { + private async getHourlyActivity( + guildId: string, + days: number + ): Promise<Array<{ hour: number; activity: number }>> { @@ - const activityMap = new Map( - result.map((r: { hour: number; activity: bigint }) => [r.hour, Number(r.activity)]) - ); + const activityMap = new Map<number, number>( + result.map((r: { hour: number; activity: bigint }) => [r.hour, Number(r.activity)]) + ); @@ - activity: activityMap.get(hour) || 0, + activity: activityMap.get(hour) ?? 0,src/stats/queries/member.ts (1)
13-38: Order growth timelines deterministically.The joins/leaves queries still omit
ORDER BY, so result order depends on the engine and can shuffle between runs—breaking charting/analytics. Please add sorting on the date column to both queries so consumers get a stable chronological sequence.WHERE guildId = ${guildId} AND action = 'join' AND timestamp >= ${since} GROUP BY date(timestamp) + ORDER BY date(timestamp) @@ WHERE guildId = ${guildId} AND action = 'leave' AND timestamp >= ${since} GROUP BY date(timestamp) + ORDER BY date(timestamp)src/stats/queries/message.ts (1)
43-53: Eliminate string interpolation in the raw SQL filter.
${userId ? \AND userId = ${userId}` : ''}splices an ad-hoc SQL fragment into$queryRaw, bypassing parameterization and reopening the injection hole the earlier review called out. Please compose the optional clause withPrisma.sql` instead.- const results = await this.db.$queryRaw< - Array<{ hour: number; count: bigint }> - >` - SELECT - CAST(strftime('%H', timestamp) AS INTEGER) as hour, - COUNT(*) as count - FROM messageevent - WHERE guildId = ${guildId} - AND timestamp >= ${since} - ${userId ? `AND userId = ${userId}` : ''} - GROUP BY strftime('%H', timestamp) - ORDER BY strftime('%H', timestamp) - `; + const userClause = userId ? Prisma.sql`AND userId = ${userId}` : Prisma.sql``; + const results = await this.db.$queryRaw<Array<{ hour: number; count: bigint }>>( + Prisma.sql` + SELECT + CAST(strftime('%H', timestamp) AS INTEGER) as hour, + COUNT(*) as count + FROM messageevent + WHERE guildId = ${guildId} + AND timestamp >= ${since} + ${userClause} + GROUP BY strftime('%H', timestamp) + ORDER BY strftime('%H', timestamp) + ` + );import { Prisma } from '@prisma/client';src/stats/queries/reaction.ts (4)
13-22: Fix SQL injection vulnerability and use portable table names.The nested template literal at line 20 bypasses Prisma's parameterization and creates an SQL injection risk. Additionally, the unquoted table name
reactioneventwill fail on Postgres (which expects"ReactionEvent").Apply this diff to use
Prisma.sqlfragments for safe conditional SQL and quoted identifiers:+import { Prisma } from '@prisma/client'; export class ReactionQueries { constructor(private db: CommonDatabase) {} @@ async getReactionStats(guildId: string, userId?: string, days = 30) { const since = createDateSince(days); + const userFilter = userId ? Prisma.sql`AND "userId" = ${userId}` : Prisma.empty; const results = await this.db.$queryRaw<Array<{ action: string; count: bigint }>>` SELECT - action, + "action", COUNT(*) as count - FROM reactionevent - WHERE guildId = ${guildId} - AND timestamp >= ${since} - ${userId ? `AND userId = ${userId}` : ''} - GROUP BY action + FROM "ReactionEvent" + WHERE "guildId" = ${guildId} + AND "timestamp" >= ${since} + ${userFilter} + GROUP BY "action" `;
36-56: Add emojiAnimated to GROUP BY and quote identifiers for Postgres compatibility.The query selects
emojiAnimated(line 47) but omits it from the GROUP BY clause (line 53), which violates SQL standards and will fail on Postgres/MySQL withONLY_FULL_GROUP_BY. Additionally, unquoted identifiers will cause table/column resolution issues on Postgres.Apply this diff:
const results = await this.db.$queryRaw< Array<{ emojiId: string | null; emojiName: string; - emojiAnimated: number; + emojiAnimated: number | boolean; count: bigint; }> >` SELECT - emojiId, - emojiName, - emojiAnimated, + "emojiId", + "emojiName", + "emojiAnimated", COUNT(*) as count - FROM reactionevent - WHERE guildId = ${guildId} - AND action = 'add' - AND timestamp >= ${since} - GROUP BY emojiId, emojiName + FROM "ReactionEvent" + WHERE "guildId" = ${guildId} + AND "action" = 'add' + AND "timestamp" >= ${since} + GROUP BY "emojiId", "emojiName", "emojiAnimated" ORDER BY count DESC LIMIT ${limit} `;
72-91: Add u.username to GROUP BY and quote all identifiers.The query selects
u.username(line 81) but only groups byr.userId(line 88). This violates SQL standards and will fail on Postgres/MySQL withONLY_FULL_GROUP_BYenabled. Additionally, unquoted identifiers cause portability issues.Apply this diff:
const results = await this.db.$queryRaw< Array<{ userId: string; username: string; reactions: bigint; }> >` SELECT - r.userId, - u.username, + r."userId", + u."username", COUNT(*) as reactions - FROM reactionevent r - LEFT JOIN user u ON u.id = r.userId - WHERE r.guildId = ${guildId} - AND r.action = 'add' - AND r.timestamp >= ${since} - GROUP BY r.userId + FROM "ReactionEvent" r + LEFT JOIN "User" u ON u."id" = r."userId" + WHERE r."guildId" = ${guildId} + AND r."action" = 'add' + AND r."timestamp" >= ${since} + GROUP BY r."userId", u."username" ORDER BY reactions DESC LIMIT ${limit} `;
106-123: Replace SQLite-specific date functions with portable solution.The
date()andstrftime()functions (lines 114-115) are SQLite-specific and will fail on Postgres/MySQL. Since the codebase supports multiple database backends throughsrc/db/factory.ts, this breaks portability.Consider computing time buckets in JavaScript to avoid dialect-specific SQL:
async getReactionTimeline(guildId: string, days = 7) { const since = createDateSince(days); - const results = await this.db.$queryRaw< - Array<{ - date: string; - hour: number; - reactions: bigint; - }> - >` - SELECT - date(timestamp) as date, - CAST(strftime('%H', timestamp) AS INTEGER) as hour, - COUNT(*) as reactions - FROM reactionevent - WHERE guildId = ${guildId} - AND action = 'add' - AND timestamp >= ${since} - GROUP BY date(timestamp), strftime('%H', timestamp) - ORDER BY date, hour - `; - - return results.map((r: { date: string; hour: number; reactions: bigint }) => ({ - date: r.date, - hour: r.hour, - reactions: Number(r.reactions), - })); + const rows = await this.db.reactionEvent.findMany({ + where: { guildId, action: 'add', timestamp: { gte: since } }, + select: { timestamp: true }, + }); + + const buckets = new Map<string, number>(); + for (const { timestamp } of rows) { + const d = new Date(timestamp); + const date = d.toISOString().slice(0, 10); + const hour = d.getUTCHours(); + const key = `${date}|${hour}`; + buckets.set(key, (buckets.get(key) ?? 0) + 1); + } + + return Array.from(buckets.entries()) + .map(([k, count]) => { + const [date, hourStr] = k.split('|'); + return { date, hour: Number(hourStr), reactions: count }; + }) + .sort((a, b) => (a.date === b.date ? a.hour - b.hour : a.date < b.date ? -1 : 1)); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/analytics/insights.ts(2 hunks)src/export/exporter.ts(2 hunks)src/stats/queries/channel.ts(1 hunks)src/stats/queries/member.ts(1 hunks)src/stats/queries/message.ts(1 hunks)src/stats/queries/reaction.ts(1 hunks)src/stats/queries/user.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/stats/queries/channel.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/stats/queries/member.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/stats/queries/user.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/export/exporter.ts (1)
src/export/formats.ts (2)
ExportData(15-60)ExportOptions(1-13)
src/stats/queries/reaction.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/stats/queries/message.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
src/analytics/insights.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
🪛 GitHub Actions: Biome Check
src/stats/queries/channel.ts
[error] 32-46: Formatter would have reformatted code in channel.ts. Run the formatter to apply style fixes.
src/stats/queries/member.ts
[error] 34-43: Formatter would have reformatted code in member.ts. Run the formatter to apply style fixes.
src/export/exporter.ts
[error] 102-111: Formatter would have reformatted code in exporter.ts. Run the formatter to apply style fixes.
src/stats/queries/reaction.ts
[error] 56-70: Formatter would have reformatted code in reaction.ts. Run the formatter to apply style fixes.
🪛 GitHub Actions: Format Check
src/stats/queries/channel.ts
[error] 34-46: Formatter would reformat return mapping in channel query. Code would be rewritten by the formatter.
[error] 34-46: TOOLNAME: Formatter detected formatting issues. Run the formatter again to apply fixes.
src/stats/queries/member.ts
[error] 34-43: Formatter would reformat joins/leaves mapping in member query. Code would be rewritten by the formatter.
src/export/exporter.ts
[error] 102-110: Formatter would reformat data.users mapping. Code would be rewritten by the formatter.
[error] 136-147: Formatter would reformat data.channels mapping. Code would be rewritten by the formatter.
[error] 193-215: Formatter would reformat data.voice mapping. Code would be rewritten by the formatter.
[error] 102-110: TOOLNAME: Formatter detected formatting issues. Run the formatter again to apply fixes.
src/stats/queries/reaction.ts
[error] 58-70: Formatter would reformat reaction count mapping. Code would be rewritten by the formatter.
[error] 58-70: TOOLNAME: Formatter detected formatting issues. Run the formatter again to apply fixes.
🪛 GitHub Actions: TypeScript Check
src/analytics/insights.ts
[error] 32-32: Type '({ hour: number; activity: {}; } | { hour: number; activity: {}; }[])' is not assignable to type '{ hour: number; activity: number; }[]'. Property 'activity' has type '{}' which is not assignable to 'number'.
[error] 33-33: Type '{ hour: number; activity: {}; }[]' is not assignable to type '{ hour: number; activity: number; }[]'. Property 'activity' has type '{}' which is not assignable to 'number'.
- Add Promise<Array<{ hour: number; activity: number }>> return type
- Resolve TypeScript type inference issue
- Regenerate Prisma Client
- Add Map<number, number> type annotation to activityMap - Use nullish coalescing operator (??) instead of logical OR - Resolve TypeScript type inference issue with Map.get()
|
beanie update minor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/analytics/insights.ts (2)
72-81: Use createdAt (not timestamp) and address cross‑DB portability.
- Column mismatch: other queries use createdAt; using timestamp will 404 at runtime if the column doesn’t exist. This was previously flagged.
- strftime() is SQLite‑only; this query will break on PostgreSQL/MySQL. Provide provider‑specific SQL or an abstraction.
Apply for SQLite (and align on createdAt):
- SELECT - CAST(strftime('%H', timestamp) AS INTEGER) as hour, + SELECT + CAST(strftime('%H', createdAt) AS INTEGER) as hour, COUNT(*) as activity FROM messageevent WHERE guildId = ${guildId} - AND timestamp >= ${since} - GROUP BY strftime('%H', timestamp) + AND createdAt >= ${since} + GROUP BY strftime('%H', createdAt) ORDER BY hourSuggested approach for portability:
- SQLite: strftime('%H', createdAt)
- PostgreSQL: EXTRACT(HOUR FROM createdAt)
- MySQL: HOUR(createdAt)
Centralize this in a small helper that switches on provider. I can draft it if helpful.
#!/bin/bash # Verify schema/usage consistency rg -nP --glob '!**/node_modules/**' -C2 '\bmessageevent\b.*\b(timestamp|createdAt)\b' src prisma rg -nP --glob '!**/node_modules/**' -C2 '\bmessageevent\b' prisma/schema
138-160: Message stats: createdAt vs timestamp again; portability; remove unused avgDuration; add indexes.
- Use createdAt consistently (duplicate of earlier concern).
- strftime/DATE functions must be provider‑specific (SQLite/Postgres/MySQL). See earlier comment.
- avgDuration is selected but never used; drop it to avoid wasted work.
Apply:
- this.db.$queryRaw<Array<{ totalMessages: bigint; activeUsers: bigint }>>` + this.db.$queryRaw<Array<{ totalMessages: bigint; activeUsers: bigint }>>` SELECT COUNT(*) as totalMessages, COUNT(DISTINCT userId) as activeUsers FROM messageevent WHERE guildId = ${guildId} - AND timestamp >= ${since} + AND createdAt >= ${since} `, - this.db.$queryRaw<Array<{ voiceUsers: bigint; avgDuration: number | null }>>` + this.db.$queryRaw<Array<{ voiceUsers: bigint }>>` SELECT COUNT(DISTINCT userId) as voiceUsers - AVG(duration) as avgDuration FROM voiceevent WHERE guildId = ${guildId} AND createdAt >= ${since} AND action = 'leave' `,Operational advice:
- Ensure composite indexes: messageevent(guildId, createdAt), voiceevent(guildId, createdAt, action) to keep COUNT/filters fast.
🧹 Nitpick comments (1)
src/analytics/insights.ts (1)
162-166: Prefer nullish coalescing with BigInt defaults; guard memberCount explicitly.Avoid falsy pitfalls and make the divide‑by‑zero guard clear.
- const totalMessages = Number(messageStats[0]?.totalMessages || 0); - const activeUsers = Number(messageStats[0]?.activeUsers || 0); - const voiceUsers = Number(voiceStats[0]?.voiceUsers || 0); - const totalMembers = guild?.memberCount || 1; + const totalMessages = Number(messageStats[0]?.totalMessages ?? 0n); + const activeUsers = Number(messageStats[0]?.activeUsers ?? 0n); + const voiceUsers = Number(voiceStats[0]?.voiceUsers ?? 0n); + const totalMembers = Math.max(1, guild?.memberCount ?? 0);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
package.json(2 hunks)src/analytics/insights.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/analytics/insights.ts (1)
src/utils/date.ts (1)
createDateSince(1-2)
🔇 Additional comments (4)
package.json (1)
3-3: Version bump is reasonable.The minor version bump from 2.2.2 to 2.3.0 aligns with the significant infrastructure change (Drizzle → Prisma migration), though this could also justify a patch bump if no user-facing changes were introduced. No concerns here.
src/analytics/insights.ts (3)
66-69: Types for getHourlyActivity — LGTM.Explicit return type and typed raw result resolve prior TS issues.
83-85: Typed Map and nullish coalescing — good.BigInt→number conversion and defaulting via ?? keep types safe.
Also applies to: 89-90
97-119: Review comment is based on incorrect assumption about the action field.The
actionfield in the MemberEvent model is typed asString, not a Prisma enum. Therefore:
- Using
action: 'join'as a string literal is correct and appropriate- There is no MemberAction enum to use (and one shouldn't be added for a string field)
The suggestion to "prefer enum" is incorrect for this schema design.
The optional suggestion about adding an explicit return type to
getMemberGrowthTrendis valid (the function currently has no return type annotation, only inferred typing), but this is a minor code clarity preference, not a correctness issue.Likely an incorrect or invalid review comment.
|
Will address failing type check when I get back to my computer later today. |
…rrors - Create main prisma/schema.prisma file that consolidates all model definitions - Rename split schema files in prisma/schema/ to .prisma.bak to prevent Prisma validation conflicts - This resolves TS2339 errors where CommonDatabase properties were not recognized during CI/CD - Prisma client now generates correctly with all models (User, Guild, Channel, Member, MessageEvent, VoiceEvent, MemberEvent, PresenceEvent, ReactionEvent)
- Add npm install -g @biomejs/biome to all three Biome workflows - Remove bun pm rebuild @biomejs/biome step from format-check and lint-check - Ensures Biome binary is available on all runners (self-hosted and cloud)
- Replace Bun setup with Node.js 20 setup - Use npm install instead of bun install - Use npx biome commands instead of bun run scripts - Remove global biome installation - Simplify workflow configuration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
package.json (1)
27-28: Remove @prisma/internals from dependencies or pin version to match.The critical issue—missing Prisma dependencies—is resolved. However:
@prisma/internals is unused: No explicit imports of this package were found in the codebase. It should be removed from
dependenciesunless it's required by a specific build or CLI tool.Version mismatch: If retained, pin
@prisma/internalsto exact version6.18.0to match@prisma/clientandprisma, preventing potential divergence from caret versioning (^6.18.0).Recommendation: Remove
@prisma/internalsfromdependenciesentirely—it will remain available transitively if genuinely needed by Prisma tooling.Also applies to: 40-40
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (2)
dummy.md(1 hunks)package.json(4 hunks)
✅ Files skipped from review due to trivial changes (1)
- dummy.md
- Update getHourlyActivity query to use createdAt column - Update getEngagementMetrics messageStats query to use createdAt - Ensure consistency with schema and other queries throughout codebase - Prevents runtime errors from missing timestamp column
- Use encodeURIComponent for username and password in buildPostgresUrl - Use encodeURIComponent for username and password in buildMysqlUrl - Safely handle special characters like @, :, %, & in credentials - Prevents DSN parsing errors from special characters in credentials
- Remove updatedAt from user upsert update payload - Remove updatedAt from guild upsert update payload - Let Prisma manage updatedAt automatically via @updatedat directive - Prevents runtime errors from manual timestamp assignment
- Add id field to create block in member upsert operation - Use same composite id format (guildId-userId) as where clause - Prevents duplicate records from being created on subsequent upserts - Ensures created record matches future upsert lookups
- Add id field to member upsert create block using composite key - Prevents duplicate records from being created on subsequent upserts - Tighten validateJoinData to check u.id exists and is a string - Avoid undefined runtime errors when accessing userData.id - Ensures created record matches upsert where clause
- Extract userId and validate it is a non-empty string before DB insert - Skip processing if userId is missing or empty (trim check) - Tighten validatePresence to check u.id exists and is a string - Pass validated userId directly to presenceEvent.create - Prevents storing empty or invalid userId values in database
…into database-to-prisma
- Add ORDER BY date ASC to joinsResults query for deterministic ordering - Add ORDER BY date ASC to leavesResults query for deterministic ordering - Annotate map callback parameters with proper types (bigint) - Ensures stable timelines and satisfies TypeScript type checking - Maintains Number() conversion for bigint to number
- Replace unsafe string interpolation with Prisma.sql fragments - Build userIdFragment using Prisma.sql for proper parameterization - Use empty Prisma.sql fragment when userId is not provided - Ensures all values are passed as parameters, not raw strings - Prevents SQL injection and maintains query safety
- Replace unsafe string interpolation with Prisma.sql for userFilter - Use Prisma.empty for empty conditional fragments - Quote all table and column identifiers (ReactionEvent, User, etc.) - Update all queries to use parameterized conditions - Ensures SQL injection prevention and database portability - Maintains type safety with proper parameter binding
- Include username in GROUP BY clause alongside userId - Ensures SQL compliance when selecting non-aggregated columns - Map callback has explicit type annotation - All identifiers remain properly quoted
…lity
- Remove SQLite-specific date() and strftime() functions from query
- Query now selects raw timestamp without database-specific functions
- Aggregate by date and hour in JavaScript using Map for bucketing
- Extract YYYY-MM-DD date and UTC hour from timestamp in JS
- Sort results by date then hour for consistent ordering
- Works with any database backend (PostgreSQL, MySQL, SQLite, etc.)
- Maintains same output shape: { date, hour, reactions }
- Add WHERE clause to filter users by guild activity - Only include users with MessageEvent or VoiceEvent in target guild - Prevents data leak of all database users in multi-guild deployments - Ensures export contains only guild-relevant user statistics - Maintains accurate guild-scoped analytics
|
Beanie is already updating this PR to start 2.3.x This PR has made me question all of my decisions up to this point in my life. |
madeline-xs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good<3
Drizzle to Prisma Database Migration
This PR completes a comprehensive migration of the database infrastructure from Drizzle ORM to Prisma, implementing a multi-file schema structure and updating all database interactions across the codebase. The migration includes 55 commits addressing schema consolidation, query layer updates, type safety improvements, and critical bug fixes.
Overview
The migration replaces all Drizzle ORM usage with Prisma Client, providing better type safety, improved query performance, and a more maintainable schema structure. All database operations have been rewritten to use Prisma's API, including queries, mutations, and raw SQL where necessary.
Schema Structure
Implemented Prisma schema:
prisma/schema.prismaDatabase Layer Changes
src/db/factory.ts) to create PrismaClient connectionsQuery Layer Migration
All query classes migrated to Prisma Client with database portability improvements:
$queryRaw$queryRawwith INNER JOINs for complex queries$queryRawfor statistics with proper quoting$queryRawwith ORDER BY for deterministic results$queryRawwith Prisma.sql fragments for safe parameterizationQuery Improvements
date()andstrftime()functions with JavaScript aggregation for database portabilityProcessor Updates
All event processors migrated to Prisma operations:
@updatedAtmanagementProcessor Improvements
updatedAtassignments to let Prisma manage timestamps automaticallyHandler Updates
Analytics and Export
createdAtcolumn consistency$queryRawwith guild-scoped filteringType Safety Improvements
Security Improvements
Cleanup
CI/CD Updates
prisma generatestep to TypeScript check workflowCommits Summary
Recent Fixes (Latest 13 commits)
Schema & Build (Commits 14-38)
Type Safety (Commits 39-42)
Core Migration (Commits 43-55)
Breaking Changes
None - This is an internal database layer migration. All public APIs remain unchanged.
Migration Notes
@,:,%,&) are now properly handled