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 241 | 65x 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();
}
} |