Ctrl K
ComunidadGuías…

prisma/client.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { PrismaClient } from '@prisma/client'
import { createHash } from 'crypto'

// Global Prisma client instance with custom configuration
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

// Tenant-aware Prisma client factory
class TenantAwarePrismaClient {
  private static instances: Map<string, PrismaClient> = new Map()
  private static defaultClient: PrismaClient | undefined

  static getDefaultClient(): PrismaClient {
    if (!this.defaultClient) {
      this.defaultClient = new PrismaClient({
        log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['error'],
        errorFormat: 'pretty',
        datasources: {
          db: {
            url: process.env.DATABASE_URL,
          },
        },
      })

      // Add middleware for audit logging
      this.defaultClient.$use(async (params, next) => {
        const before = Date.now()
        const result = await next(params)
        const after = Date.now()

        // Log slow queries in development
        if (process.env.NODE_ENV === 'development' && (after - before) > 1000) {
          console.log(`Slow query detected: ${params.model}.${params.action} took ${after - before}ms`)
        }

        return result
      })

      // Add middleware for tenant isolation (Row-Level Security)
      this.defaultClient.$use(async (params, next) => {
        // Get current tenant from context (this would be set by your auth middleware)
        const currentTenant = getCurrentTenantId()

        if (currentTenant && this.isTenantAwareModel(params.model)) {
          // Ensure tenant_id is included in queries for tenant-aware models
          if (params.action === 'findMany' || params.action === 'findFirst') {
            params.args.where = {
              ...params.args.where,
              tenant_id: currentTenant,
            }
          } else if (params.action === 'create') {
            params.args.data = {
              ...params.args.data,
              tenant_id: currentTenant,
            }
          } else if (params.action === 'update' || params.action === 'updateMany') {
            params.args.where = {
              ...params.args.where,
              tenant_id: currentTenant,
            }
          } else if (params.action === 'delete' || params.action === 'deleteMany') {
            params.args.where = {
              ...params.args.where,
              tenant_id: currentTenant,
            }
          }
        }

        return next(params)
      })
    }

    return this.defaultClient
  }

  static getTenantClient(tenantConfig: {
    id: string
    database_host?: string
    database_port?: number
    database_name?: string
    database_schema?: string
  }): PrismaClient {
    const cacheKey = this.getTenantCacheKey(tenantConfig)

    if (this.instances.has(cacheKey)) {
      return this.instances.get(cacheKey)!
    }

    // For fully-isolated tenants, create dedicated client
    if (tenantConfig.database_host && tenantConfig.database_name) {
      const databaseUrl = this.buildTenantDatabaseUrl(tenantConfig)

      const client = new PrismaClient({
        datasources: {
          db: {
            url: databaseUrl,
          },
        },
        log: process.env.NODE_ENV === 'development' ? ['error'] : ['error'],
      })

      this.instances.set(cacheKey, client)
      return client
    }

    // For shared/semi-isolated tenants, use default client with schema
    if (tenantConfig.database_schema) {
      const client = new PrismaClient({
        datasources: {
          db: {
            url: `${process.env.DATABASE_URL}?schema=${tenantConfig.database_schema}`,
          },
        },
        log: process.env.NODE_ENV === 'development' ? ['error'] : ['error'],
      })

      this.instances.set(cacheKey, client)
      return client
    }

    // Default to shared client
    return this.getDefaultClient()
  }

  private static buildTenantDatabaseUrl(tenantConfig: {
    database_host?: string
    database_port?: number
    database_name?: string
  }): string {
    const host = tenantConfig.database_host || process.env.TENANT_DB_HOST
    const port = tenantConfig.database_port || process.env.TENANT_DB_PORT
    const database = tenantConfig.database_name
    const user = process.env.TENANT_DB_USER
    const password = process.env.TENANT_DB_PASSWORD

    return `postgresql://${user}:${password}@${host}:${port}/${database}`
  }

  private static getTenantCacheKey(tenantConfig: {
    id: string
    database_host?: string
    database_port?: number
    database_name?: string
    database_schema?: string
  }): string {
    const configStr = JSON.stringify({
      id: tenantConfig.id,
      host: tenantConfig.database_host,
      port: tenantConfig.database_port,
      name: tenantConfig.database_name,
      schema: tenantConfig.database_schema,
    })

    return createHash('md5').update(configStr).digest('hex')
  }

  private static isTenantAwareModel(model: string | undefined): boolean {
    if (!model) return false

    // Models that are tenant-aware (have tenant_id field)
    const tenantAwareModels = [
      'User',
      'Team',
      'TeamMember',
      'TeamInvitation',
      'TeamProject',
      'TeamResource',
      'Role',
      'RoleAssignment',
      'Policy',
      'Session',
      'AuditLog',
      'BillingSubscription',
      'BillingInvoice',
      'BillingAlert',
      'UsageRecord',
      'ApiKey',
      'WebhookEndpoint',
    ]

    return tenantAwareModels.includes(model)
  }

  // Cleanup method for graceful shutdown
  static async disconnect(): Promise<void> {
    await Promise.all([
      this.defaultClient?.$disconnect(),
      ...Array.from(this.instances.values()).map(client => client.$disconnect())
    ])
  }
}

// Helper function to get current tenant ID from context
// This should be implemented based on your authentication/context system
function getCurrentTenantId(): string | null {
  // This is a placeholder - implement based on your context system
  // For example, you might use AsyncLocalStorage or a request context
  return null
}

// Create and export the default Prisma client
export const prisma = globalForPrisma.prisma ?? TenantAwarePrismaClient.getDefaultClient()

// Export the tenant-aware client factory
export { TenantAwarePrismaClient }

// Export utility functions
export const createTenantClient = TenantAwarePrismaClient.getTenantClient
export const disconnectPrisma = TenantAwarePrismaClient.disconnect

// In development, reuse the same instance to prevent hot-reload issues
if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

// Graceful shutdown handling
process.on('beforeExit', async () => {
  await TenantAwarePrismaClient.disconnect()
})

process.on('SIGINT', async () => {
  await TenantAwarePrismaClient.disconnect()
  process.exit(0)
})

process.on('SIGTERM', async () => {
  await TenantAwarePrismaClient.disconnect()
  process.exit(0)
})

// Type exports for TypeScript
export type {
  PrismaClient,
  Tenant,
  User,
  Team,
  TeamMember,
  Role,
  RoleAssignment,
  Session,
  AuditLog,
  BillingPlan,
  BillingSubscription,
  PaymentCustomer,
  PaymentIntent,
  MfaMethod,
  Passkey
} from '@prisma/client'