@@ -36,11 +35,6 @@ export const TranslatableMarkdown = dynamic(
)
// Admin components
-export const CustomFieldList = dynamic(
- () => import('@/app/admin/emulators/[emulatorId]/custom-fields/components/CustomFieldList'),
- { loading: LoadingFallback },
-)
-
export const RolePermissionMatrix = dynamic(
() => import('@/app/admin/permissions/components/RolePermissionMatrix'),
{ loading: LoadingFallback },
diff --git a/src/server/api/root.ts b/src/server/api/root.ts
index 923774dbe..a9949f3e7 100644
--- a/src/server/api/root.ts
+++ b/src/server/api/root.ts
@@ -33,7 +33,6 @@ import { rawgRouter } from './routers/rawg'
import { releasesRouter } from './routers/releases'
import { socialRouter } from './routers/social'
import { socsRouter } from './routers/socs'
-import { systemRouter } from './routers/system'
import { systemsRouter } from './routers/systems'
import { tgdbRouter } from './routers/tgdb'
import { trustRouter } from './routers/trust'
@@ -55,7 +54,6 @@ export const appRouter = createTRPCRouter({
deviceBrands: deviceBrandsRouter,
socs: socsRouter,
games: gamesRouter,
- system: systemRouter,
systems: systemsRouter,
emulators: emulatorsRouter,
users: usersRouter,
diff --git a/src/server/api/routers/listingReports.ts b/src/server/api/routers/listingReports.ts
index 1b1526fd5..04a079e5b 100644
--- a/src/server/api/routers/listingReports.ts
+++ b/src/server/api/routers/listingReports.ts
@@ -17,14 +17,13 @@ import {
} from '@/server/api/trpc'
import { getAuthorReportCounts } from '@/server/services/report-stats.service'
import { paginate } from '@/server/utils/pagination'
-import { batchQueries } from '@/server/utils/query-performance'
import { validateEnum, sanitizeInput, validatePagination } from '@/server/utils/security-validation'
import { PERMISSIONS } from '@/utils/permission-system'
import { ApprovalStatus, type Prisma, ReportStatus, TrustAction, ReportReason } from '@orm/client'
export const listingReportsRouter = createTRPCRouter({
stats: permissionProcedure(PERMISSIONS.VIEW_STATISTICS).query(async ({ ctx }) => {
- const [pending, underReview, resolved, dismissed] = await batchQueries([
+ const [pending, underReview, resolved, dismissed] = await Promise.all([
ctx.prisma.listingReport.count({ where: { status: ReportStatus.PENDING } }),
ctx.prisma.listingReport.count({ where: { status: ReportStatus.UNDER_REVIEW } }),
ctx.prisma.listingReport.count({ where: { status: ReportStatus.RESOLVED } }),
@@ -77,7 +76,7 @@ export const listingReportsRouter = createTRPCRouter({
const orderBy: Prisma.ListingReportOrderByWithRelationInput = {}
if (sortField && sortDirection) orderBy[sortField] = sortDirection
- const [reports, total] = await batchQueries([
+ const [reports, total] = await Promise.all([
ctx.prisma.listingReport.findMany({
where,
orderBy,
@@ -303,7 +302,7 @@ export const listingReportsRouter = createTRPCRouter({
...(status ? { status } : {}),
}
- const [handheldReports, pcReports, handheldCount, pcCount] = await batchQueries([
+ const [handheldReports, pcReports, handheldCount, pcCount] = await Promise.all([
ctx.prisma.listingReport.findMany({
where: handheldWhere,
orderBy: { createdAt: 'desc' },
diff --git a/src/server/api/routers/mobile/admin.ts b/src/server/api/routers/mobile/admin.ts
index 5f7c8021d..1d9d28374 100644
--- a/src/server/api/routers/mobile/admin.ts
+++ b/src/server/api/routers/mobile/admin.ts
@@ -26,7 +26,6 @@ import { logAudit } from '@/server/services/audit.service'
import { listingStatsCache } from '@/server/utils/cache/instances'
import { paginate } from '@/server/utils/pagination'
import { buildSearchFilter } from '@/server/utils/query-builders'
-import { createCountQuery } from '@/server/utils/query-performance'
import { canBanUser } from '@/utils/permission-system'
import {
ApprovalStatus,
@@ -59,26 +58,16 @@ export const mobileAdminRouter = createMobileTRPCRouter({
activeUsers,
bannedUsers,
] = await Promise.all([
- createCountQuery(ctx.prisma.listing, {
- status: ApprovalStatus.PENDING,
- }),
- createCountQuery(ctx.prisma.listing, {
- status: ApprovalStatus.APPROVED,
- }),
- createCountQuery(ctx.prisma.listing, {
- status: ApprovalStatus.REJECTED,
- }),
- createCountQuery(ctx.prisma.game, { status: ApprovalStatus.PENDING }),
- createCountQuery(ctx.prisma.game, { status: ApprovalStatus.APPROVED }),
- createCountQuery(ctx.prisma.game, { status: ApprovalStatus.REJECTED }),
- createCountQuery(ctx.prisma.listingReport, {
- status: ReportStatus.PENDING,
- }),
- createCountQuery(ctx.prisma.listingReport, {
- status: ReportStatus.RESOLVED,
- }),
- createCountQuery(ctx.prisma.user, {}),
- createCountQuery(ctx.prisma.userBan, { isActive: true }),
+ ctx.prisma.listing.count({ where: { status: ApprovalStatus.PENDING } }),
+ ctx.prisma.listing.count({ where: { status: ApprovalStatus.APPROVED } }),
+ ctx.prisma.listing.count({ where: { status: ApprovalStatus.REJECTED } }),
+ ctx.prisma.game.count({ where: { status: ApprovalStatus.PENDING } }),
+ ctx.prisma.game.count({ where: { status: ApprovalStatus.APPROVED } }),
+ ctx.prisma.game.count({ where: { status: ApprovalStatus.REJECTED } }),
+ ctx.prisma.listingReport.count({ where: { status: ReportStatus.PENDING } }),
+ ctx.prisma.listingReport.count({ where: { status: ReportStatus.RESOLVED } }),
+ ctx.prisma.user.count(),
+ ctx.prisma.userBan.count({ where: { isActive: true } }),
])
return {
@@ -291,7 +280,7 @@ export const mobileAdminRouter = createMobileTRPCRouter({
if (searchConditions) where.OR = searchConditions
const [total, games] = await Promise.all([
- createCountQuery(ctx.prisma.game, where),
+ ctx.prisma.game.count({ where }),
ctx.prisma.game.findMany({
where,
select: {
@@ -387,7 +376,7 @@ export const mobileAdminRouter = createMobileTRPCRouter({
const where: Prisma.ListingReportWhereInput = { ...(status && { status }) }
const [total, reports] = await Promise.all([
- createCountQuery(ctx.prisma.listingReport, where),
+ ctx.prisma.listingReport.count({ where }),
ctx.prisma.listingReport.findMany({
where,
include: {
@@ -459,7 +448,7 @@ export const mobileAdminRouter = createMobileTRPCRouter({
}
const [total, bans] = await Promise.all([
- createCountQuery(ctx.prisma.userBan, where),
+ ctx.prisma.userBan.count({ where }),
ctx.prisma.userBan.findMany({
where,
include: {
diff --git a/src/server/api/routers/socs.ts b/src/server/api/routers/socs.ts
index e1a415249..749f840a6 100644
--- a/src/server/api/routers/socs.ts
+++ b/src/server/api/routers/socs.ts
@@ -15,7 +15,6 @@ import {
} from '@/server/api/trpc'
import { SoCsRepository } from '@/server/repositories/socs.repository'
import { paginate } from '@/server/utils/pagination'
-import { batchQueries } from '@/server/utils/query-performance'
export const socsRouter = createTRPCRouter({
get: publicProcedure.input(GetSoCsSchema).query(async ({ ctx, input }) => {
@@ -76,7 +75,7 @@ export const socsRouter = createTRPCRouter({
}),
stats: viewStatisticsProcedure.query(async ({ ctx }) => {
- const [withDevices, withoutDevices] = await batchQueries([
+ const [withDevices, withoutDevices] = await Promise.all([
ctx.prisma.soC.count({ where: { devices: { some: {} } } }),
ctx.prisma.soC.count({ where: { devices: { none: {} } } }),
])
diff --git a/src/server/api/routers/system.ts b/src/server/api/routers/system.ts
deleted file mode 100644
index 85ef6a1de..000000000
--- a/src/server/api/routers/system.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { adminProcedure, createTRPCRouter } from '@/server/api/trpc'
-import { ConnectionMonitor } from '@/server/utils/connection-monitor'
-
-/**
- * System monitoring and diagnostics router
- * Admin-only endpoints for database and application health monitoring
- */
-export const systemRouter = createTRPCRouter({
- /**
- * Get current database connection statistics
- */
- getConnectionStats: adminProcedure.query(async () => {
- return await ConnectionMonitor.getConnectionStats()
- }),
-
- /**
- * Get detailed information about active database connections
- */
- getActiveConnections: adminProcedure.query(async () => {
- return await ConnectionMonitor.getActiveConnections()
- }),
-
- /**
- * Get long-running queries (> 5 seconds)
- */
- getLongRunningQueries: adminProcedure.query(async () => {
- return await ConnectionMonitor.getLongRunningQueries()
- }),
-
- /**
- * Get database configuration validation
- */
- validateDatabaseConfig: adminProcedure.query(() => {
- return ConnectionMonitor.validateConfig()
- }),
-
- /**
- * Get connection pool configuration
- */
- getPoolConfig: adminProcedure.query(() => {
- return ConnectionMonitor.getPoolConfig()
- }),
-
- /**
- * Manually trigger connection stats logging
- */
- logConnectionStats: adminProcedure.mutation(async () => {
- await ConnectionMonitor.logConnectionStats()
- return { success: true }
- }),
-})
diff --git a/src/server/api/routers/users.ts b/src/server/api/routers/users.ts
index 920852e91..d781f1872 100644
--- a/src/server/api/routers/users.ts
+++ b/src/server/api/routers/users.ts
@@ -37,7 +37,6 @@ import {
} from '@/server/services/user-profile.service'
import { buildOrderBy, paginate } from '@/server/utils/pagination'
import { buildSearchFilter } from '@/server/utils/query-builders'
-import { createCountQuery } from '@/server/utils/query-performance'
import { updateUserRole } from '@/server/utils/roleSync'
import {
hasPermissionInContext,
@@ -713,7 +712,7 @@ export const usersRouter = createTRPCRouter({
skip,
take: limit,
}),
- createCountQuery(ctx.prisma.user, where),
+ ctx.prisma.user.count({ where }),
])
return {
@@ -752,17 +751,19 @@ export const usersRouter = createTRPCRouter({
superAdminCount,
bannedUsersCount,
] = await Promise.all([
- createCountQuery(ctx.prisma.user, { role: Role.USER }),
- createCountQuery(ctx.prisma.user, { role: Role.AUTHOR }),
- createCountQuery(ctx.prisma.user, { role: Role.DEVELOPER }),
- createCountQuery(ctx.prisma.user, { role: Role.MODERATOR }),
- createCountQuery(ctx.prisma.user, { role: Role.ADMIN }),
- createCountQuery(ctx.prisma.user, { role: Role.SUPER_ADMIN }),
- createCountQuery(ctx.prisma.user, {
- userBans: {
- some: {
- isActive: true,
- OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
+ ctx.prisma.user.count({ where: { role: Role.USER } }),
+ ctx.prisma.user.count({ where: { role: Role.AUTHOR } }),
+ ctx.prisma.user.count({ where: { role: Role.DEVELOPER } }),
+ ctx.prisma.user.count({ where: { role: Role.MODERATOR } }),
+ ctx.prisma.user.count({ where: { role: Role.ADMIN } }),
+ ctx.prisma.user.count({ where: { role: Role.SUPER_ADMIN } }),
+ ctx.prisma.user.count({
+ where: {
+ userBans: {
+ some: {
+ isActive: true,
+ OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
+ },
},
},
}),
diff --git a/src/server/init.ts b/src/server/init.ts
index 6ea749302..a055a33af 100644
--- a/src/server/init.ts
+++ b/src/server/init.ts
@@ -9,7 +9,6 @@
import { logger } from '@/lib/logger'
import { ms } from '@/utils/time'
import { initializeNotificationService } from './notifications/init'
-import { ConnectionMonitor } from './utils/connection-monitor'
import { initializeSwitchGameService } from './utils/switchGameInit'
import { initializeThreeDsGameService } from './utils/threeDsGameInit'
@@ -177,29 +176,6 @@ export async function initializeServer(): Promise
{
} else {
logger.warn('⚠️ Some services failed to initialize. They will be retried on next access.')
}
-
- // Initialize connection monitoring (production only)
- if (process.env.NODE_ENV === 'production') {
- // Validate database configuration
- const configValidation = ConnectionMonitor.validateConfig()
- if (!configValidation.isValid) {
- logger.warn('⚠️ Database configuration issues detected', {
- warnings: configValidation.warnings,
- })
- }
-
- // Start monitoring every 2 minutes in production
- ConnectionMonitor.startMonitoring(2 * 60 * 1000)
- logger.info('✅ Connection monitoring started')
- } else {
- // In development, just validate config once
- const configValidation = ConnectionMonitor.validateConfig()
- if (!configValidation.isValid) {
- logger.warn('⚠️ Database configuration issues detected', {
- warnings: configValidation.warnings,
- })
- }
- }
}
/**
diff --git a/src/server/prisma-client.ts b/src/server/prisma-client.ts
index edc82f8be..90eeae8ee 100644
--- a/src/server/prisma-client.ts
+++ b/src/server/prisma-client.ts
@@ -3,6 +3,7 @@ import { PrismaClient } from '@orm/client'
type PrismaClientOptions = NonNullable[0]>
type PrismaClientConfig = Omit
+const LOCAL_DATABASE_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', '[::1]'])
function getDatabaseUrl() {
const connectionString = process.env.DATABASE_URL
@@ -14,8 +15,31 @@ function getDatabaseUrl() {
return connectionString
}
+function getPoolMax(connectionString: string): number | undefined {
+ try {
+ const url = new URL(connectionString)
+ const raw = url.searchParams.get('connection_limit')
+ if (raw) {
+ const parsed = Number(raw)
+ if (Number.isInteger(parsed) && parsed > 0) return parsed
+ }
+
+ return LOCAL_DATABASE_HOSTS.has(url.hostname.toLowerCase()) ? undefined : 1
+ } catch {
+ return 1
+ }
+}
+
export function createPrismaClient(options?: PrismaClientConfig) {
- const adapter = new PrismaPg({ connectionString: getDatabaseUrl() })
+ const connectionString = getDatabaseUrl()
+ const poolMax = getPoolMax(connectionString)
+
+ const adapter = new PrismaPg({
+ connectionString,
+ ...(poolMax ? { max: poolMax } : {}),
+ connectionTimeoutMillis: 5_000,
+ idleTimeoutMillis: 10_000,
+ })
return new PrismaClient({
...options,
diff --git a/src/server/services/report-stats.service.ts b/src/server/services/report-stats.service.ts
index 6206a366f..ff694e6f2 100644
--- a/src/server/services/report-stats.service.ts
+++ b/src/server/services/report-stats.service.ts
@@ -1,4 +1,3 @@
-import { batchQueries } from '@/server/utils/query-performance'
import type { PrismaClient } from '@orm/client'
interface AuthorReportCounts {
@@ -16,7 +15,7 @@ export async function getAuthorReportCounts(
userId: string,
): Promise {
const [handheldReports, pcReports, handheldListingsReported, pcListingsReported] =
- await batchQueries([
+ await Promise.all([
prisma.listingReport.count({ where: { listing: { authorId: userId } } }),
prisma.pcListingReport.count({ where: { pcListing: { authorId: userId } } }),
prisma.listing.count({ where: { authorId: userId, reports: { some: {} } } }),
diff --git a/src/server/utils/connection-monitor.ts b/src/server/utils/connection-monitor.ts
deleted file mode 100644
index cec386f79..000000000
--- a/src/server/utils/connection-monitor.ts
+++ /dev/null
@@ -1,401 +0,0 @@
-import { logger } from '@/lib/logger'
-import { prisma } from '@/server/db'
-
-interface ConnectionStats {
- total: number
- active: number
- idle: number
- idleInTransaction: number
- waiting: number
-}
-
-interface DetailedConnection {
- pid: number
- username: string
- applicationName: string
- clientAddr: string | null
- state: string
- queryStart: Date | null
- stateChange: Date | null
- waitEventType: string | null
- waitEvent: string | null
- query: string | null
-}
-
-/**
- * Connection monitoring utility for tracking PostgreSQL connection health
- *
- * Use this to:
- * - Monitor active connections
- * - Identify connection leaks
- * - Track query performance
- * - Alert on connection exhaustion
- */
-export class ConnectionMonitor {
- private static isMonitoring = false
- private static monitoringInterval: NodeJS.Timeout | null = null
-
- /**
- * Get current connection statistics
- */
- static async getConnectionStats(): Promise {
- try {
- const result = await prisma.$queryRaw<
- {
- state: string
- count: bigint
- }[]
- >`
- SELECT
- state,
- COUNT(*) as count
- FROM pg_stat_activity
- WHERE datname = current_database()
- GROUP BY state
- `
-
- const stats: ConnectionStats = {
- total: 0,
- active: 0,
- idle: 0,
- idleInTransaction: 0,
- waiting: 0,
- }
-
- for (const row of result) {
- const count = Number(row.count)
- stats.total += count
-
- switch (row.state) {
- case 'active':
- stats.active = count
- break
- case 'idle':
- stats.idle = count
- break
- case 'idle in transaction':
- stats.idleInTransaction = count
- break
- case 'idle in transaction (aborted)':
- stats.idleInTransaction += count
- break
- default:
- stats.waiting += count
- }
- }
-
- return stats
- } catch (error) {
- logger.error('Failed to get connection stats', { error })
- throw error
- }
- }
-
- /**
- * Get detailed information about active connections
- */
- static async getActiveConnections(): Promise {
- try {
- const result = await prisma.$queryRaw<
- {
- pid: number
- usename: string
- application_name: string
- client_addr: string | null
- state: string
- query_start: Date | null
- state_change: Date | null
- wait_event_type: string | null
- wait_event: string | null
- query: string | null
- }[]
- >`
- SELECT
- pid,
- usename,
- application_name,
- client_addr::text,
- state,
- query_start,
- state_change,
- wait_event_type,
- wait_event,
- query
- FROM pg_stat_activity
- WHERE datname = current_database()
- AND state != 'idle'
- AND pid != pg_backend_pid()
- ORDER BY query_start DESC
- LIMIT 50
- `
-
- return result.map((row) => ({
- pid: row.pid,
- username: row.usename,
- applicationName: row.application_name,
- clientAddr: row.client_addr,
- state: row.state,
- queryStart: row.query_start,
- stateChange: row.state_change,
- waitEventType: row.wait_event_type,
- waitEvent: row.wait_event,
- query: row.query,
- }))
- } catch (error) {
- logger.error('Failed to get active connections', { error })
- throw error
- }
- }
-
- /**
- * Get long-running queries (> 5 seconds)
- */
- static async getLongRunningQueries(): Promise {
- try {
- const result = await prisma.$queryRaw<
- {
- pid: number
- usename: string
- application_name: string
- client_addr: string | null
- state: string
- query_start: Date | null
- state_change: Date | null
- wait_event_type: string | null
- wait_event: string | null
- query: string | null
- duration_seconds: number
- }[]
- >`
- SELECT
- pid,
- usename,
- application_name,
- client_addr::text,
- state,
- query_start,
- state_change,
- wait_event_type,
- wait_event,
- query,
- EXTRACT(EPOCH FROM (NOW() - query_start))::integer as duration_seconds
- FROM pg_stat_activity
- WHERE datname = current_database()
- AND state = 'active'
- AND query_start < NOW() - INTERVAL '5 seconds'
- AND pid != pg_backend_pid()
- ORDER BY query_start ASC
- LIMIT 20
- `
-
- const connections = result.map((row) => ({
- pid: row.pid,
- username: row.usename,
- applicationName: row.application_name,
- clientAddr: row.client_addr,
- state: row.state,
- queryStart: row.query_start,
- stateChange: row.state_change,
- waitEventType: row.wait_event_type,
- waitEvent: row.wait_event,
- query: row.query,
- }))
-
- if (connections.length > 0) {
- logger.warn('Long-running queries detected', {
- count: connections.length,
- queries: result.map((r) => ({
- pid: r.pid,
- duration: r.duration_seconds,
- query: r.query?.substring(0, 100),
- })),
- })
- }
-
- return connections
- } catch (error) {
- logger.error('Failed to get long-running queries', { error })
- throw error
- }
- }
-
- /**
- * Log current connection statistics
- */
- static async logConnectionStats(): Promise {
- try {
- const stats = await this.getConnectionStats()
-
- logger.info('Database connection stats', {
- total: stats.total,
- active: stats.active,
- idle: stats.idle,
- idleInTransaction: stats.idleInTransaction,
- waiting: stats.waiting,
- })
-
- // Alert if too many active connections
- if (stats.active > 50) {
- logger.warn('High active connection count', {
- active: stats.active,
- threshold: 50,
- })
- }
-
- // Alert if connections stuck in transaction
- if (stats.idleInTransaction > 10) {
- logger.warn('High idle-in-transaction count', {
- idleInTransaction: stats.idleInTransaction,
- threshold: 10,
- })
- }
-
- // Check for long-running queries
- await this.getLongRunningQueries()
- } catch (error) {
- logger.error('Failed to log connection stats', { error })
- }
- }
-
- /**
- * Start monitoring connections at regular intervals
- *
- * @param intervalMs - Monitoring interval in milliseconds (default: 60000 = 1 minute)
- */
- static startMonitoring(intervalMs = 60000): void {
- if (this.isMonitoring) {
- logger.warn('Connection monitoring already started')
- return
- }
-
- logger.info('Starting connection monitoring', { intervalMs })
-
- // Log immediately
- void this.logConnectionStats()
-
- // Then log at intervals
- this.monitoringInterval = setInterval(() => {
- void this.logConnectionStats()
- }, intervalMs)
-
- this.isMonitoring = true
- }
-
- /**
- * Stop monitoring connections
- */
- static stopMonitoring(): void {
- if (!this.isMonitoring) {
- logger.warn('Connection monitoring not running')
- return
- }
-
- if (this.monitoringInterval) {
- clearInterval(this.monitoringInterval)
- this.monitoringInterval = null
- }
-
- this.isMonitoring = false
- logger.info('Connection monitoring stopped')
- }
-
- /**
- * Check if database is a local development database
- */
- static isLocalDatabase(): boolean {
- const url = process.env.DATABASE_URL
- if (!url) return false
-
- try {
- const urlObj = new URL(url)
- const hostname = urlObj.hostname.toLowerCase()
-
- return (
- hostname === 'localhost' ||
- hostname === '127.0.0.1' ||
- hostname === '0.0.0.0' ||
- hostname === '::1'
- )
- } catch {
- return false
- }
- }
-
- /**
- * Get connection pool configuration from DATABASE_URL
- */
- static getPoolConfig(): {
- hasConnectionLimit: boolean
- hasPgBouncer: boolean
- connectionLimit?: number
- isLocal: boolean
- } {
- const url = process.env.DATABASE_URL
- if (!url) {
- return { hasConnectionLimit: false, hasPgBouncer: false, isLocal: false }
- }
-
- try {
- const urlObj = new URL(url)
- const connectionLimit = urlObj.searchParams.get('connection_limit')
- const pgbouncer = urlObj.searchParams.get('pgbouncer')
- const isLocal = this.isLocalDatabase()
-
- return {
- hasConnectionLimit: connectionLimit !== null,
- hasPgBouncer: pgbouncer === 'true',
- connectionLimit: connectionLimit ? parseInt(connectionLimit, 10) : undefined,
- isLocal,
- }
- } catch {
- return { hasConnectionLimit: false, hasPgBouncer: false, isLocal: false }
- }
- }
-
- /**
- * Validate connection configuration
- */
- static validateConfig(): {
- isValid: boolean
- warnings: string[]
- } {
- const warnings: string[] = []
- const config = this.getPoolConfig()
-
- // Skip validation for local databases
- if (config.isLocal) {
- return { isValid: true, warnings: [] }
- }
-
- // Production/staging validation (Supabase, remote databases)
- if (!config.hasPgBouncer) {
- warnings.push(
- 'DATABASE_URL missing pgbouncer=true parameter. Add it to prevent prepared statement issues.',
- )
- }
-
- // NOTE: We don't warn about missing connection_limit anymore since it's added automatically
- // by getOptimizedDatabaseUrl() in src/server/db.ts
-
- // Only warn if connection_limit is set but too high
- if (config.connectionLimit && config.connectionLimit > 5) {
- warnings.push(
- `connection_limit=${config.connectionLimit} is too high for serverless. Recommended: 1-2`,
- )
- }
-
- if (!process.env.DATABASE_DIRECT_URL) {
- warnings.push(
- 'DATABASE_DIRECT_URL not set. Required for complex transactions and migrations.',
- )
- }
-
- const isValid = warnings.length === 0
-
- if (!isValid) {
- logger.warn('Database configuration issues detected', { warnings })
- }
-
- return { isValid, warnings }
- }
-}
diff --git a/src/server/utils/query-performance.ts b/src/server/utils/query-performance.ts
deleted file mode 100644
index 33861ff92..000000000
--- a/src/server/utils/query-performance.ts
+++ /dev/null
@@ -1,391 +0,0 @@
-import { logger } from '@/lib/logger'
-
-/**
- * Performance monitoring for database queries
- */
-export class QueryPerformanceMonitor {
- private static queries: Map = new Map()
- private static enabled = process.env.NODE_ENV === 'development'
-
- static startQuery(name: string): () => void {
- if (!this.enabled) return () => {}
-
- const startTime = performance.now()
-
- return () => {
- const duration = performance.now() - startTime
- this.recordQuery(name, duration)
- }
- }
-
- static recordQuery(name: string, duration: number): void {
- const existing = this.queries.get(name) || {
- count: 0,
- totalTime: 0,
- minTime: Infinity,
- maxTime: 0,
- avgTime: 0,
- }
-
- existing.count++
- existing.totalTime += duration
- existing.minTime = Math.min(existing.minTime, duration)
- existing.maxTime = Math.max(existing.maxTime, duration)
- existing.avgTime = existing.totalTime / existing.count
-
- this.queries.set(name, existing)
-
- // Log slow queries
- if (duration > 1000) {
- logger.warn(`Slow query detected: ${name} took ${duration.toFixed(2)}ms`)
- }
- }
-
- static getMetrics(): Record {
- return Object.fromEntries(this.queries)
- }
-
- static reset(): void {
- this.queries.clear()
- }
-}
-
-interface QueryMetrics {
- count: number
- totalTime: number
- minTime: number
- maxTime: number
- avgTime: number
-}
-
-/**
- * Optimize select fields to reduce data transfer
- */
-export function optimizeSelect(
- requiredFields: (keyof T)[],
- conditionalFields?: Partial>,
-): Record {
- const select: Record = {}
-
- // Always include required fields
- requiredFields.forEach((field) => {
- select[String(field)] = true
- })
-
- // Add conditional fields
- if (conditionalFields) {
- Object.entries(conditionalFields).forEach(([field, include]) => {
- if (include) {
- select[field] = true
- }
- })
- }
-
- return select
-}
-
-/**
- * Create efficient count queries that skip unnecessary joins
- */
-export function createCountQuery(
- model: { count: (args: { where: T }) => Promise; _name?: string },
- where: T,
-): Promise {
- const end = QueryPerformanceMonitor.startQuery(`count:${model._name || 'unknown'}`)
-
- const query = model.count({ where })
-
- return query.finally(end)
-}
-
-/**
- * Batch multiple queries for better performance
- */
-export async function batchQueries(
- queries: [...{ [K in keyof T]: Promise }],
-): Promise {
- const end = QueryPerformanceMonitor.startQuery('batch:queries')
-
- try {
- return Promise.all(queries) as Promise
- } finally {
- end()
- }
-}
-
-/**
- * Create indexes suggestion based on query patterns
- */
-export function suggestIndexes(
- model: string,
- whereConditions: Record,
- orderBy?: Record,
-): string[] {
- const suggestions: string[] = []
- const fields = Object.keys(whereConditions)
-
- // Single field indexes for equality checks
- fields.forEach((field) => {
- const condition = whereConditions[field]
- if (typeof condition !== 'object' || condition === null || (condition && 'in' in condition)) {
- suggestions.push(`CREATE INDEX idx_${model}_${field} ON ${model}(${field});`)
- }
- })
-
- // Composite indexes for multiple conditions
- if (fields.length > 1) {
- suggestions.push(
- `CREATE INDEX idx_${model}_${fields.join('_')} ON ${model}(${fields.join(', ')});`,
- )
- }
-
- // Index for sorting
- if (orderBy) {
- const sortFields = Object.keys(orderBy)
- sortFields.forEach((field) => {
- if (!fields.includes(field)) {
- suggestions.push(`CREATE INDEX idx_${model}_${field} ON ${model}(${field});`)
- }
- })
- }
-
- return [...new Set(suggestions)]
-}
-
-/**
- * Optimize includes to prevent N+1 queries
- */
-export function optimizeIncludes(
- baseInclude: Record,
- maxDepth = 3,
- currentDepth = 0,
-): Record {
- if (currentDepth >= maxDepth) {
- // Convert deep includes to select with minimal fields
- const optimized: Record = {}
-
- Object.entries(baseInclude).forEach(([key, value]) => {
- if (value === true) {
- optimized[key] = { select: { id: true, name: true } }
- } else if (
- typeof value === 'object' &&
- value !== null &&
- 'include' in value &&
- value.include
- ) {
- optimized[key] = {
- select: {
- id: true,
- ...Object.keys(value.include as Record).reduce(
- (acc, k) => {
- acc[k] = { select: { id: true } }
- return acc
- },
- {} as Record,
- ),
- },
- }
- } else {
- optimized[key] = value
- }
- })
-
- return optimized
- }
-
- // Recursively optimize nested includes
- const optimized: Record = {}
-
- Object.entries(baseInclude).forEach(([key, value]) => {
- if (typeof value === 'object' && value !== null && 'include' in value && value.include) {
- optimized[key] = {
- ...value,
- include: optimizeIncludes(
- value.include as Record,
- maxDepth,
- currentDepth + 1,
- ),
- }
- } else {
- optimized[key] = value
- }
- })
-
- return optimized
-}
-
-/**
- * Create a query cache key based on parameters
- */
-export function createQueryCacheKey(
- model: string,
- operation: string,
- params: Record,
-): string {
- const sortedParams = Object.keys(params)
- .sort()
- .reduce(
- (acc, key) => {
- acc[key] = params[key]
- return acc
- },
- {} as Record,
- )
-
- return `${model}:${operation}:${JSON.stringify(sortedParams)}`
-}
-
-/**
- * Analyze query complexity and warn about potential issues
- */
-export function analyzeQueryComplexity(
- include?: Record,
- where?: Record,
-): QueryComplexityResult {
- let score = 0
- const warnings: string[] = []
-
- // Analyze includes depth
- if (include) {
- const depth = getIncludeDepth(include)
- score += depth * 10
-
- if (depth > 3) {
- warnings.push(`Deep nesting detected (${depth} levels). Consider splitting queries.`)
- }
-
- // Count total relations
- const relationCount = countRelations(include)
- score += relationCount * 5
-
- if (relationCount > 10) {
- warnings.push(
- `Many relations included (${relationCount}). This may cause performance issues.`,
- )
- }
- }
-
- // Analyze where conditions
- if (where) {
- const orConditions = countOrConditions(where)
- score += orConditions * 15
-
- if (orConditions > 5) {
- warnings.push(`Many OR conditions (${orConditions}). Consider using indexed fields.`)
- }
- }
-
- return {
- score,
- complexity: score < 50 ? 'low' : score < 100 ? 'medium' : 'high',
- warnings,
- }
-}
-
-function getIncludeDepth(include: Record, currentDepth = 1): number {
- let maxDepth = currentDepth
-
- Object.values(include).forEach((value) => {
- if (typeof value === 'object' && value !== null && 'include' in value && value.include) {
- const depth = getIncludeDepth(value.include as Record, currentDepth + 1)
- maxDepth = Math.max(maxDepth, depth)
- }
- })
-
- return maxDepth
-}
-
-function countRelations(include: Record): number {
- let count = Object.keys(include).length
-
- Object.values(include).forEach((value) => {
- if (typeof value === 'object' && value !== null && 'include' in value && value.include) {
- count += countRelations(value.include as Record)
- }
- })
-
- return count
-}
-
-function countOrConditions(where: Record): number {
- let count = 0
-
- if (where.OR && Array.isArray(where.OR)) count += where.OR.length
-
- Object.values(where).forEach((value) => {
- if (typeof value === 'object' && value !== null) {
- count += countOrConditions(value as Record)
- }
- })
-
- return count
-}
-
-interface QueryComplexityResult {
- score: number
- complexity: 'low' | 'medium' | 'high'
- warnings: string[]
-}
-
-/**
- * Connection pool optimization settings
- */
-export function getOptimizedPoolSettings() {
- return {
- // Connection limit for serverless (1 connection per instance)
- connection_limit: 1,
-
- // Connection timeout in seconds (how long to wait for initial connection)
- connect_timeout: 10,
- }
-}
-
-/**
- * Generate optimized DATABASE_URL with connection pool parameters
- *
- * Automatically adds connection_limit=1 for remote databases to prevent
- * connection exhaustion in serverless environments.
- *
- * IMPORTANT: Does NOT add pgbouncer=true - that must be in the environment variable
- * to avoid breaking interactive transactions.
- */
-export function getOptimizedDatabaseUrl(baseUrl?: string): string {
- const url = baseUrl || process.env.DATABASE_URL
- if (!url) {
- throw new Error('DATABASE_URL is required')
- }
-
- try {
- const urlObj = new URL(url)
- const hostname = urlObj.hostname.toLowerCase()
-
- // Skip optimization for local databases
- const isLocal =
- hostname === 'localhost' ||
- hostname === '127.0.0.1' ||
- hostname === '0.0.0.0' ||
- hostname === '::1'
-
- if (isLocal) return url
-
- // Get optimized settings
- const settings = getOptimizedPoolSettings()
-
- // Add connection_limit if not already set
- if (!urlObj.searchParams.has('connection_limit')) {
- urlObj.searchParams.set('connection_limit', settings.connection_limit.toString())
- logger.info('Added connection_limit=1 to DATABASE_URL for optimal serverless performance')
- }
-
- // Add connect_timeout if not already set
- if (!urlObj.searchParams.has('connect_timeout')) {
- urlObj.searchParams.set('connect_timeout', settings.connect_timeout.toString())
- }
-
- return urlObj.toString()
- } catch (error) {
- // If URL parsing fails, return as-is
- logger.error('Failed to parse DATABASE_URL', { error })
- return url
- }
-}