/** * @ai-summary Vehicle catalog management service * @ai-context Handles CRUD operations on platform vehicle catalog data with transaction support */ import { Pool } from 'pg'; import { logger } from '../../../core/logging/logger'; import { PlatformCacheService } from '../../platform/domain/platform-cache.service'; export interface CatalogMake { id: number; name: string; } export interface CatalogModel { id: number; makeId: number; name: string; } export interface CatalogYear { id: number; modelId: number; year: number; } export interface CatalogTrim { id: number; yearId: number; name: string; } export interface CatalogEngine { id: number; trimId: number; name: string; description?: string; } export interface PlatformChangeLog { id: string; changeType: 'CREATE' | 'UPDATE' | 'DELETE'; resourceType: 'makes' | 'models' | 'years' | 'trims' | 'engines'; resourceId: string; oldValue: any; newValue: any; changedBy: string; createdAt: Date; } export class VehicleCatalogService { constructor( private pool: Pool, private cacheService: PlatformCacheService ) {} // MAKES OPERATIONS async getAllMakes(): Promise { const query = ` SELECT cache_key, data FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:makes:%' ORDER BY (data->>'name') `; try { const result = await this.pool.query(query); return result.rows.map(row => ({ id: parseInt(row.cache_key.split(':')[2]), name: row.data.name })); } catch (error) { logger.error('Error getting all makes', { error }); throw error; } } async createMake(name: string, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Get next ID const idResult = await client.query(` SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:makes:%' `); const makeId = idResult.rows[0].next_id; // Insert make const make: CatalogMake = { id: makeId, name }; await client.query(` INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at) VALUES ($1, $2, NOW() + INTERVAL '10 years') `, [`catalog:makes:${makeId}`, JSON.stringify(make)]); // Log change await this.logChange(client, 'CREATE', 'makes', makeId.toString(), null, make, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Make created', { makeId, name, changedBy }); return make; } catch (error) { await client.query('ROLLBACK'); logger.error('Error creating make', { error, name }); throw error; } finally { client.release(); } } async updateMake(makeId: number, name: string, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:makes:${makeId}`]); if (oldResult.rows.length === 0) { throw new Error(`Make ${makeId} not found`); } const oldValue = oldResult.rows[0].data; const newValue: CatalogMake = { id: makeId, name }; // Update make await client.query(` UPDATE vehicle_dropdown_cache SET data = $1, updated_at = NOW() WHERE cache_key = $2 `, [JSON.stringify(newValue), `catalog:makes:${makeId}`]); // Log change await this.logChange(client, 'UPDATE', 'makes', makeId.toString(), oldValue, newValue, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Make updated', { makeId, name, changedBy }); return newValue; } catch (error) { await client.query('ROLLBACK'); logger.error('Error updating make', { error, makeId, name }); throw error; } finally { client.release(); } } async deleteMake(makeId: number, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Check for dependent models const modelsCheck = await client.query(` SELECT COUNT(*) as count FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:models:%' AND (data->>'makeId')::int = $1 `, [makeId]); if (parseInt(modelsCheck.rows[0].count) > 0) { throw new Error('Cannot delete make with existing models'); } // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:makes:${makeId}`]); if (oldResult.rows.length === 0) { throw new Error(`Make ${makeId} not found`); } const oldValue = oldResult.rows[0].data; // Delete make await client.query(` DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:makes:${makeId}`]); // Log change await this.logChange(client, 'DELETE', 'makes', makeId.toString(), oldValue, null, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Make deleted', { makeId, changedBy }); } catch (error) { await client.query('ROLLBACK'); logger.error('Error deleting make', { error, makeId }); throw error; } finally { client.release(); } } // MODELS OPERATIONS async getModelsByMake(makeId: number): Promise { const query = ` SELECT cache_key, data FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:models:%' AND (data->>'makeId')::int = $1 ORDER BY (data->>'name') `; try { const result = await this.pool.query(query, [makeId]); return result.rows.map(row => ({ id: parseInt(row.cache_key.split(':')[2]), makeId: row.data.makeId, name: row.data.name })); } catch (error) { logger.error('Error getting models by make', { error, makeId }); throw error; } } async createModel(makeId: number, name: string, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Verify make exists const makeCheck = await client.query(` SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:makes:${makeId}`]); if (makeCheck.rows.length === 0) { throw new Error(`Make ${makeId} not found`); } // Get next ID const idResult = await client.query(` SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:models:%' `); const modelId = idResult.rows[0].next_id; // Insert model const model: CatalogModel = { id: modelId, makeId, name }; await client.query(` INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at) VALUES ($1, $2, NOW() + INTERVAL '10 years') `, [`catalog:models:${modelId}`, JSON.stringify(model)]); // Log change await this.logChange(client, 'CREATE', 'models', modelId.toString(), null, model, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Model created', { modelId, makeId, name, changedBy }); return model; } catch (error) { await client.query('ROLLBACK'); logger.error('Error creating model', { error, makeId, name }); throw error; } finally { client.release(); } } async updateModel(modelId: number, makeId: number, name: string, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Verify make exists const makeCheck = await client.query(` SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:makes:${makeId}`]); if (makeCheck.rows.length === 0) { throw new Error(`Make ${makeId} not found`); } // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:models:${modelId}`]); if (oldResult.rows.length === 0) { throw new Error(`Model ${modelId} not found`); } const oldValue = oldResult.rows[0].data; const newValue: CatalogModel = { id: modelId, makeId, name }; // Update model await client.query(` UPDATE vehicle_dropdown_cache SET data = $1, updated_at = NOW() WHERE cache_key = $2 `, [JSON.stringify(newValue), `catalog:models:${modelId}`]); // Log change await this.logChange(client, 'UPDATE', 'models', modelId.toString(), oldValue, newValue, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Model updated', { modelId, makeId, name, changedBy }); return newValue; } catch (error) { await client.query('ROLLBACK'); logger.error('Error updating model', { error, modelId, name }); throw error; } finally { client.release(); } } async deleteModel(modelId: number, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Check for dependent years const yearsCheck = await client.query(` SELECT COUNT(*) as count FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:years:%' AND (data->>'modelId')::int = $1 `, [modelId]); if (parseInt(yearsCheck.rows[0].count) > 0) { throw new Error('Cannot delete model with existing years'); } // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:models:${modelId}`]); if (oldResult.rows.length === 0) { throw new Error(`Model ${modelId} not found`); } const oldValue = oldResult.rows[0].data; // Delete model await client.query(` DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:models:${modelId}`]); // Log change await this.logChange(client, 'DELETE', 'models', modelId.toString(), oldValue, null, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Model deleted', { modelId, changedBy }); } catch (error) { await client.query('ROLLBACK'); logger.error('Error deleting model', { error, modelId }); throw error; } finally { client.release(); } } // YEARS OPERATIONS async getYearsByModel(modelId: number): Promise { const query = ` SELECT cache_key, data FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:years:%' AND (data->>'modelId')::int = $1 ORDER BY (data->>'year')::int DESC `; try { const result = await this.pool.query(query, [modelId]); return result.rows.map(row => ({ id: parseInt(row.cache_key.split(':')[2]), modelId: row.data.modelId, year: row.data.year })); } catch (error) { logger.error('Error getting years by model', { error, modelId }); throw error; } } async createYear(modelId: number, year: number, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Verify model exists const modelCheck = await client.query(` SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:models:${modelId}`]); if (modelCheck.rows.length === 0) { throw new Error(`Model ${modelId} not found`); } // Get next ID const idResult = await client.query(` SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:years:%' `); const yearId = idResult.rows[0].next_id; // Insert year const yearData: CatalogYear = { id: yearId, modelId, year }; await client.query(` INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at) VALUES ($1, $2, NOW() + INTERVAL '10 years') `, [`catalog:years:${yearId}`, JSON.stringify(yearData)]); // Log change await this.logChange(client, 'CREATE', 'years', yearId.toString(), null, yearData, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Year created', { yearId, modelId, year, changedBy }); return yearData; } catch (error) { await client.query('ROLLBACK'); logger.error('Error creating year', { error, modelId, year }); throw error; } finally { client.release(); } } async updateYear(yearId: number, modelId: number, year: number, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Verify model exists const modelCheck = await client.query(` SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:models:${modelId}`]); if (modelCheck.rows.length === 0) { throw new Error(`Model ${modelId} not found`); } // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:years:${yearId}`]); if (oldResult.rows.length === 0) { throw new Error(`Year ${yearId} not found`); } const oldValue = oldResult.rows[0].data; const newValue: CatalogYear = { id: yearId, modelId, year }; // Update year await client.query(` UPDATE vehicle_dropdown_cache SET data = $1, updated_at = NOW() WHERE cache_key = $2 `, [JSON.stringify(newValue), `catalog:years:${yearId}`]); // Log change await this.logChange(client, 'UPDATE', 'years', yearId.toString(), oldValue, newValue, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Year updated', { yearId, modelId, year, changedBy }); return newValue; } catch (error) { await client.query('ROLLBACK'); logger.error('Error updating year', { error, yearId, year }); throw error; } finally { client.release(); } } async deleteYear(yearId: number, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Check for dependent trims const trimsCheck = await client.query(` SELECT COUNT(*) as count FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:trims:%' AND (data->>'yearId')::int = $1 `, [yearId]); if (parseInt(trimsCheck.rows[0].count) > 0) { throw new Error('Cannot delete year with existing trims'); } // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:years:${yearId}`]); if (oldResult.rows.length === 0) { throw new Error(`Year ${yearId} not found`); } const oldValue = oldResult.rows[0].data; // Delete year await client.query(` DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:years:${yearId}`]); // Log change await this.logChange(client, 'DELETE', 'years', yearId.toString(), oldValue, null, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Year deleted', { yearId, changedBy }); } catch (error) { await client.query('ROLLBACK'); logger.error('Error deleting year', { error, yearId }); throw error; } finally { client.release(); } } // TRIMS OPERATIONS async getTrimsByYear(yearId: number): Promise { const query = ` SELECT cache_key, data FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:trims:%' AND (data->>'yearId')::int = $1 ORDER BY (data->>'name') `; try { const result = await this.pool.query(query, [yearId]); return result.rows.map(row => ({ id: parseInt(row.cache_key.split(':')[2]), yearId: row.data.yearId, name: row.data.name })); } catch (error) { logger.error('Error getting trims by year', { error, yearId }); throw error; } } async createTrim(yearId: number, name: string, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Verify year exists const yearCheck = await client.query(` SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:years:${yearId}`]); if (yearCheck.rows.length === 0) { throw new Error(`Year ${yearId} not found`); } // Get next ID const idResult = await client.query(` SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:trims:%' `); const trimId = idResult.rows[0].next_id; // Insert trim const trim: CatalogTrim = { id: trimId, yearId, name }; await client.query(` INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at) VALUES ($1, $2, NOW() + INTERVAL '10 years') `, [`catalog:trims:${trimId}`, JSON.stringify(trim)]); // Log change await this.logChange(client, 'CREATE', 'trims', trimId.toString(), null, trim, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Trim created', { trimId, yearId, name, changedBy }); return trim; } catch (error) { await client.query('ROLLBACK'); logger.error('Error creating trim', { error, yearId, name }); throw error; } finally { client.release(); } } async updateTrim(trimId: number, yearId: number, name: string, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Verify year exists const yearCheck = await client.query(` SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:years:${yearId}`]); if (yearCheck.rows.length === 0) { throw new Error(`Year ${yearId} not found`); } // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:trims:${trimId}`]); if (oldResult.rows.length === 0) { throw new Error(`Trim ${trimId} not found`); } const oldValue = oldResult.rows[0].data; const newValue: CatalogTrim = { id: trimId, yearId, name }; // Update trim await client.query(` UPDATE vehicle_dropdown_cache SET data = $1, updated_at = NOW() WHERE cache_key = $2 `, [JSON.stringify(newValue), `catalog:trims:${trimId}`]); // Log change await this.logChange(client, 'UPDATE', 'trims', trimId.toString(), oldValue, newValue, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Trim updated', { trimId, yearId, name, changedBy }); return newValue; } catch (error) { await client.query('ROLLBACK'); logger.error('Error updating trim', { error, trimId, name }); throw error; } finally { client.release(); } } async deleteTrim(trimId: number, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Check for dependent engines const enginesCheck = await client.query(` SELECT COUNT(*) as count FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:engines:%' AND (data->>'trimId')::int = $1 `, [trimId]); if (parseInt(enginesCheck.rows[0].count) > 0) { throw new Error('Cannot delete trim with existing engines'); } // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:trims:${trimId}`]); if (oldResult.rows.length === 0) { throw new Error(`Trim ${trimId} not found`); } const oldValue = oldResult.rows[0].data; // Delete trim await client.query(` DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:trims:${trimId}`]); // Log change await this.logChange(client, 'DELETE', 'trims', trimId.toString(), oldValue, null, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Trim deleted', { trimId, changedBy }); } catch (error) { await client.query('ROLLBACK'); logger.error('Error deleting trim', { error, trimId }); throw error; } finally { client.release(); } } // ENGINES OPERATIONS async getEnginesByTrim(trimId: number): Promise { const query = ` SELECT cache_key, data FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:engines:%' AND (data->>'trimId')::int = $1 ORDER BY (data->>'name') `; try { const result = await this.pool.query(query, [trimId]); return result.rows.map(row => ({ id: parseInt(row.cache_key.split(':')[2]), trimId: row.data.trimId, name: row.data.name, description: row.data.description })); } catch (error) { logger.error('Error getting engines by trim', { error, trimId }); throw error; } } async createEngine(trimId: number, name: string, description: string | undefined, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Verify trim exists const trimCheck = await client.query(` SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:trims:${trimId}`]); if (trimCheck.rows.length === 0) { throw new Error(`Trim ${trimId} not found`); } // Get next ID const idResult = await client.query(` SELECT COALESCE(MAX(CAST(SPLIT_PART(cache_key, ':', 3) AS INTEGER)), 0) + 1 as next_id FROM vehicle_dropdown_cache WHERE cache_key LIKE 'catalog:engines:%' `); const engineId = idResult.rows[0].next_id; // Insert engine const engine: CatalogEngine = { id: engineId, trimId, name, description }; await client.query(` INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at) VALUES ($1, $2, NOW() + INTERVAL '10 years') `, [`catalog:engines:${engineId}`, JSON.stringify(engine)]); // Log change await this.logChange(client, 'CREATE', 'engines', engineId.toString(), null, engine, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Engine created', { engineId, trimId, name, changedBy }); return engine; } catch (error) { await client.query('ROLLBACK'); logger.error('Error creating engine', { error, trimId, name }); throw error; } finally { client.release(); } } async updateEngine(engineId: number, trimId: number, name: string, description: string | undefined, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Verify trim exists const trimCheck = await client.query(` SELECT 1 FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:trims:${trimId}`]); if (trimCheck.rows.length === 0) { throw new Error(`Trim ${trimId} not found`); } // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:engines:${engineId}`]); if (oldResult.rows.length === 0) { throw new Error(`Engine ${engineId} not found`); } const oldValue = oldResult.rows[0].data; const newValue: CatalogEngine = { id: engineId, trimId, name, description }; // Update engine await client.query(` UPDATE vehicle_dropdown_cache SET data = $1, updated_at = NOW() WHERE cache_key = $2 `, [JSON.stringify(newValue), `catalog:engines:${engineId}`]); // Log change await this.logChange(client, 'UPDATE', 'engines', engineId.toString(), oldValue, newValue, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Engine updated', { engineId, trimId, name, changedBy }); return newValue; } catch (error) { await client.query('ROLLBACK'); logger.error('Error updating engine', { error, engineId, name }); throw error; } finally { client.release(); } } async deleteEngine(engineId: number, changedBy: string): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); // Get old value const oldResult = await client.query(` SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:engines:${engineId}`]); if (oldResult.rows.length === 0) { throw new Error(`Engine ${engineId} not found`); } const oldValue = oldResult.rows[0].data; // Delete engine await client.query(` DELETE FROM vehicle_dropdown_cache WHERE cache_key = $1 `, [`catalog:engines:${engineId}`]); // Log change await this.logChange(client, 'DELETE', 'engines', engineId.toString(), oldValue, null, changedBy); await client.query('COMMIT'); // Invalidate cache await this.cacheService.invalidateVehicleData(); logger.info('Engine deleted', { engineId, changedBy }); } catch (error) { await client.query('ROLLBACK'); logger.error('Error deleting engine', { error, engineId }); throw error; } finally { client.release(); } } // HELPER METHODS private async logChange( client: any, changeType: 'CREATE' | 'UPDATE' | 'DELETE', resourceType: 'makes' | 'models' | 'years' | 'trims' | 'engines', resourceId: string, oldValue: any, newValue: any, changedBy: string ): Promise { const query = ` INSERT INTO platform_change_log (change_type, resource_type, resource_id, old_value, new_value, changed_by) VALUES ($1, $2, $3, $4, $5, $6) `; await client.query(query, [ changeType, resourceType, resourceId, oldValue ? JSON.stringify(oldValue) : null, newValue ? JSON.stringify(newValue) : null, changedBy ]); } async getChangeLogs(limit: number = 100, offset: number = 0): Promise<{ logs: PlatformChangeLog[]; total: number }> { const countQuery = 'SELECT COUNT(*) as total FROM platform_change_log'; const query = ` SELECT id, change_type, resource_type, resource_id, old_value, new_value, changed_by, created_at FROM platform_change_log ORDER BY created_at DESC LIMIT $1 OFFSET $2 `; try { const [countResult, dataResult] = await Promise.all([ this.pool.query(countQuery), this.pool.query(query, [limit, offset]) ]); const total = parseInt(countResult.rows[0].total, 10); const logs = dataResult.rows.map(row => ({ id: row.id, changeType: row.change_type, resourceType: row.resource_type, resourceId: row.resource_id, oldValue: row.old_value, newValue: row.new_value, changedBy: row.changed_by, createdAt: new Date(row.created_at) })); return { logs, total }; } catch (error) { logger.error('Error fetching change logs', { error }); throw error; } } }