/** * Database Helper Functions * Additional utility functions for database operations */ import postgresClient from '../services/postgres-client'; import { EntityType } from '../types/sync'; import { getTableName } from './sync-helpers'; /** * Upsert a single record * @param entity Entity type * @param data Record data * @returns Upserted record */ export async function upsertRecord( entity: EntityType, data: Record ): Promise { const tableName = getTableName(entity); return await postgresClient.upsert(tableName, data); } /** * Bulk upsert records with batching * @param entity Entity type * @param records Array of records * @param batchSize Number of records per batch (default: 100) * @returns Total number of records upserted */ export async function bulkUpsertRecords( entity: EntityType, records: Record[], batchSize: number = 100 ): Promise { if (records.length === 0) return 0; const tableName = getTableName(entity); let totalUpserted = 0; // Process in batches for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); const count = await postgresClient.bulkUpsert(tableName, batch); totalUpserted += count; } return totalUpserted; } /** * Soft delete records not in the provided ID list * @param entity Entity type * @param activeIds Array of IDs that should remain active * @returns Number of records soft-deleted */ export async function softDeleteMissingRecords( entity: EntityType, activeIds: (number | string)[] ): Promise { if (activeIds.length === 0) return 0; const tableName = getTableName(entity); const query = ` UPDATE ${tableName} SET is_deleted = true, deleted_at = CURRENT_TIMESTAMP WHERE id NOT IN (${activeIds.map((_, i) => `$${i + 1}`).join(', ')}) AND is_deleted = false `; const result = await postgresClient.query(query, activeIds); return result.rowCount || 0; } /** * Get last sync time for an entity * @param entity Entity type * @returns Last sync timestamp or null */ export async function getLastSyncTime( entity: EntityType ): Promise { const query = ` SELECT completed_at FROM sync_history WHERE entity_type = $1 AND status = 'completed' ORDER BY completed_at DESC LIMIT 1 `; const result = await postgresClient.query<{ completed_at: Date }>( query, [entity] ); return result.rows[0]?.completed_at || null; } /** * Get record count for an entity * @param entity Entity type * @param includeDeleted Include soft-deleted records * @returns Record count */ export async function getRecordCount( entity: EntityType, includeDeleted: boolean = false ): Promise { const tableName = getTableName(entity); return await postgresClient.count(tableName, {}, includeDeleted); } /** * Get records modified since a specific date * @param entity Entity type * @param since Date to filter from * @param limit Maximum number of records * @returns Array of records */ export async function getRecordsModifiedSince( entity: EntityType, since: Date, limit?: number ): Promise { const tableName = getTableName(entity); const query = ` SELECT * FROM ${tableName} WHERE synced_at >= $1 AND is_deleted = false ORDER BY synced_at DESC ${limit ? `LIMIT ${limit}` : ''} `; const result = await postgresClient.query(query, [since]); return result.rows; } /** * Get all active IDs for an entity * @param entity Entity type * @returns Array of active record IDs */ export async function getActiveIds(entity: EntityType): Promise { const tableName = getTableName(entity); const query = ` SELECT id FROM ${tableName} WHERE is_deleted = false `; const result = await postgresClient.query<{ id: number }>(query); return result.rows.map((row: { id: number }) => row.id); } /** * Restore soft-deleted record * @param entity Entity type * @param id Record ID */ export async function restoreRecord( entity: EntityType, id: number | string ): Promise { const tableName = getTableName(entity); const query = ` UPDATE ${tableName} SET is_deleted = false, deleted_at = NULL WHERE id = $1 `; await postgresClient.query(query, [id]); } /** * Hard delete soft-deleted records older than specified days * @param entity Entity type * @param daysOld Number of days * @returns Number of records deleted */ export async function purgeOldDeletedRecords( entity: EntityType, daysOld: number = 90 ): Promise { const tableName = getTableName(entity); const query = ` DELETE FROM ${tableName} WHERE is_deleted = true AND deleted_at < NOW() - INTERVAL '${daysOld} days' `; const result = await postgresClient.query(query); return result.rowCount || 0; } /** * Get sync statistics for an entity * @param entity Entity type * @returns Sync statistics */ export async function getSyncStatistics(entity: EntityType): Promise<{ totalRecords: number; activeRecords: number; deletedRecords: number; lastSyncTime: Date | null; lastSyncStatus: string | null; }> { const tableName = getTableName(entity); // Get record counts const countQuery = ` SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE is_deleted = false) as active, COUNT(*) FILTER (WHERE is_deleted = true) as deleted FROM ${tableName} `; const countResult = await postgresClient.query<{ total: string; active: string; deleted: string; }>(countQuery); // Get last sync info const syncQuery = ` SELECT completed_at, status FROM sync_history WHERE entity_type = $1 ORDER BY started_at DESC LIMIT 1 `; const syncResult = await postgresClient.query<{ completed_at: Date; status: string; }>(syncQuery, [entity]); return { totalRecords: parseInt(countResult.rows[0]?.total || '0'), activeRecords: parseInt(countResult.rows[0]?.active || '0'), deletedRecords: parseInt(countResult.rows[0]?.deleted || '0'), lastSyncTime: syncResult.rows[0]?.completed_at || null, lastSyncStatus: syncResult.rows[0]?.status || null, }; } /** * Vacuum analyze table to optimize performance * @param entity Entity type */ export async function optimizeTable(entity: EntityType): Promise { const tableName = getTableName(entity); await postgresClient.query(`VACUUM ANALYZE ${tableName}`); } /** * Check if record exists * @param entity Entity type * @param id Record ID * @returns True if record exists */ export async function recordExists( entity: EntityType, id: number | string ): Promise { const tableName = getTableName(entity); const query = ` SELECT EXISTS(SELECT 1 FROM ${tableName} WHERE id = $1) as exists `; const result = await postgresClient.query<{ exists: boolean }>(query, [id]); return result.rows[0]?.exists || false; } /** * Get records by IDs * @param entity Entity type * @param ids Array of record IDs * @returns Array of records */ export async function getRecordsByIds( entity: EntityType, ids: (number | string)[] ): Promise { if (ids.length === 0) return []; const tableName = getTableName(entity); const query = ` SELECT * FROM ${tableName} WHERE id = ANY($1::bigint[]) AND is_deleted = false `; const result = await postgresClient.query(query, [ids]); return result.rows; } /** * Update sync timestamp for records * @param entity Entity type * @param ids Array of record IDs */ export async function updateSyncTimestamp( entity: EntityType, ids: (number | string)[] ): Promise { if (ids.length === 0) return; const tableName = getTableName(entity); const query = ` UPDATE ${tableName} SET synced_at = CURRENT_TIMESTAMP WHERE id = ANY($1::bigint[]) `; await postgresClient.query(query, [ids]); }