All files / src/modules/prisma prisma.service.ts

64.51% Statements 60/93
56.48% Branches 61/108
83.33% Functions 5/6
62.79% Lines 54/86

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 24165x 65x 65x     65x             13x 13x 13x 13x 13x     13x                   13x         13x         13x   13x 13x 13x       3589x 3589x     3585x   3319x   3319x 162x 162x 162x                               3157x   39x 39x 39x 39x 39x         39x 39x                                 39x 8x 8x 1x 1x                               3319x     4x 4x                         4x                 3577x       12x     11x                     11x 11x 11x   11x 11x 11x 11x 11x   11x                                                                                                                       12x      
import { Injectable, OnModuleInit, OnModuleDestroy, Optional } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { LoggerService } from '@app/common/services/logger.service';
 
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  private _extendedClient: any;
  private logger?: LoggerService;
 
  constructor(@Optional() logger?: LoggerService) {
    // Ensure Prisma uses the local engine when a standard database URL is provided.
    // This avoids accidental Data Proxy/Accelerate mode which requires prisma:// URLs.
    try {
      const url = process.env.DATABASE_URL || '';
      const engineType = (process.env.PRISMA_CLIENT_ENGINE_TYPE || '').toLowerCase();
      const isPrismaUrl = url.startsWith('prisma://') || url.startsWith('prisma+');
      Iif (!isPrismaUrl && (engineType === 'dataproxy' || engineType === 'edge')) {
        process.env.PRISMA_CLIENT_ENGINE_TYPE = 'library';
      }
      Iif (!process.env.PRISMA_CLIENT_ENGINE_TYPE && !isPrismaUrl) {
        process.env.PRISMA_CLIENT_ENGINE_TYPE = 'library';
      }
    } catch {
      // noop – best-effort safeguard
    }
 
    // In unit tests/CI where DATABASE_URL may be absent, provide a safe fallback URL
    // to avoid Prisma attempting to read env("DATABASE_URL") during client init.
    // We also skip connecting in test/when SKIP_DB is set.
    const effectiveUrl = process.env.DATABASE_URL ||
      (process.env.NODE_ENV === 'test' || process.env.SKIP_DB === '1'
        ? 'postgresql://invalid:invalid@localhost:5432/invalid?schema=public'
        : undefined);
 
    super({
      datasources: effectiveUrl ? { db: { url: effectiveUrl } } : undefined,
    } as any);
    
    // Store logger for error handling in extended client operations
    this.logger = logger;
    
    const root: any = this;
    const loggerRef = this.logger; // Capture logger reference for use in closure
    this._extendedClient = this.$extends({
        query: {
          $allModels: {
            async $allOperations({ model, operation, args, query }: any) {
              try {
                const result = await query(args);
 
                // Skip auditing for AuditLog and volatile/session-like models
                if (!model || model === 'AuditLog' || model === 'RefreshToken') return result;
 
                const userId: number | undefined = (global as any).currentUser?.id;
 
                if (operation === 'create') {
                  if (result && typeof result === 'object' && 'id' in result) {
                    try {
                      await root.auditLog.create({
                        data: {
                          entityType: model,
                          entityId: (result as any).id,
                          action: 'CREATE',
                          newValue: result,
                          userId: userId ?? null,
                        },
                      });
                    } catch (err) {
                      Iif (process.env.NODE_ENV !== 'production') {
                       
                        console.warn('[prisma.audit] create audit failed:', err);
                      }
                    }
                  }
                } else if (operation === 'update' || operation === 'delete') {
                  // Map Prisma model name (e.g., ExternalAccount) to client property (externalAccount)
                  const modelProp = typeof model === 'string' && model.length > 0 ? model.charAt(0).toLowerCase() + model.slice(1) : model;
                  const modelClient = (root as any)[modelProp];
                  let oldRecord: any = null;
                  if (modelClient && typeof modelClient.findUnique === 'function' && (args as any)?.where) {
                    oldRecord = await modelClient.findUnique({
                      where: (args as any).where,
                    });
                  }
 
                  try {
                    await root.auditLog.create({
                      data: {
                        entityType: model,
                        entityId: (args as any)?.where?.id ?? (result && typeof result === 'object' && 'id' in result ? (result as any).id : null),
                        action: operation === 'update' ? 'UPDATE' : 'DELETE',
                        oldValue: oldRecord,
                        newValue: operation === 'update' ? result : null,
                        userId: userId ?? null,
                      },
                    });
                  } catch (err) {
                    Iif (process.env.NODE_ENV !== 'production') {
                     
                      console.warn('[prisma.audit] update/delete audit failed:', err);
                    }
                  }
 
                  if (model === 'Listing' && operation === 'update' && oldRecord) {
                    const newPrice = (args as any).data?.price ?? (result && typeof result === 'object' && 'price' in result ? (result as any).price : undefined);
                    if (newPrice != null && oldRecord.price !== newPrice) {
                      try {
                        await root.listingPriceHistory.create({
                          data: {
                            listingId: oldRecord.id,
                            price: newPrice,
                          },
                        });
                      } catch (err) {
                        Iif (process.env.NODE_ENV !== 'production') {
                         
                          console.warn('[prisma.audit] price history write failed:', err);
                        }
                      }
                    }
                  }
                }
 
                return result;
              } catch (err: any) {
                // Log Prisma errors using logger if available
                if (loggerRef) {
                  loggerRef.error('Prisma error occurred', {
                    error: {
                      code: err?.code,
                      message: err?.message,
                      meta: err?.meta,
                      model,
                      operation,
                    },
                  });
                } else IEif (process.env.NODE_ENV !== 'production') {
                  console.error('[PrismaService] Error:', err);
                }
                // Re-throw the error so callers can handle it
                throw err;
              }
            },
          },
        },
      });
  }
 
  get client() {
    return this._extendedClient;
  }
 
  async onModuleInit() {
    if (process.env.SKIP_DB === '1') return;
    
    // Verify DATABASE_URL is set before attempting connection
    Iif (!process.env.DATABASE_URL) {
      const errorMsg = 'DATABASE_URL environment variable is required but not set';
      if (this.logger) {
        this.logger.error(errorMsg);
      } else {
        console.error(`[PrismaService] ${errorMsg}`);
      }
      throw new Error(errorMsg);
    }
 
    // Attempt connection with retry logic for transient failures
    const maxRetries = 5;
    const baseDelay = 1000; // 1 second
    let lastError: any = null;
 
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        await this.$connect();
        if (this.logger) {
          this.logger.info('Prisma client connected successfully');
        }
        return; // Success
      } catch (error: any) {
        lastError = error;
        
        // Check if error is transient (connection-related)
        const isTransient = 
          error?.code === 'P1001' || // Can't reach database server
          error?.code === 'P1008' || // Operations timed out
          error?.code === 'P1017' || // Server closed connection
          error?.name === 'PrismaClientInitializationError' ||
          error?.message?.includes("Can't reach database server") ||
          error?.message?.includes('Connection') ||
          error?.message?.includes('timeout') ||
          error?.message?.includes('ETIMEDOUT') ||
          error?.message?.includes('EHOSTUNREACH') ||
          error?.message?.includes('ECONNREFUSED');
 
        Iif (!isTransient || attempt === maxRetries) {
          // Non-retryable error or max retries reached
          const errorMsg = `Failed to connect to database after ${attempt} attempt(s)`;
          if (this.logger) {
            this.logger.error(errorMsg, {
              error: {
                code: error?.code,
                name: error?.name,
                message: error?.message,
                attempts: attempt,
              },
            });
          } else {
            console.error(`[PrismaService] ${errorMsg}:`, error?.message);
          }
          throw error; // Re-throw to fail fast
        }
 
        // Exponential backoff with jitter
        const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), 10000);
        const jitter = Math.floor(Math.random() * 500);
        const totalDelay = delay + jitter;
 
        if (this.logger) {
          this.logger.warn(`Database connection attempt ${attempt}/${maxRetries} failed, retrying in ${totalDelay}ms...`, {
            error: {
              code: error?.code,
              message: error?.message,
            },
          });
        } else {
          console.warn(`[PrismaService] Connection attempt ${attempt}/${maxRetries} failed, retrying in ${totalDelay}ms...`);
        }
 
        await new Promise(resolve => setTimeout(resolve, totalDelay));
      }
    }
 
    // Should never reach here, but handle it anyway
    throw lastError || new Error('Failed to connect to database');
  }
 
  async onModuleDestroy() {
    if (process.env.SKIP_DB === '1' || process.env.NODE_ENV === 'test') return;
    await this.$disconnect();
  }
}