using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories; public class CentralUiRepository : ICentralUiRepository { private readonly ScadaBridgeDbContext _context; /// /// Initializes a new instance of the class. /// /// The EF Core database context. public CentralUiRepository(ScadaBridgeDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } /// public async Task> GetAllSitesAsync(CancellationToken cancellationToken = default) { return await _context.Sites .AsNoTracking() .OrderBy(s => s.Name) .ToListAsync(cancellationToken); } /// public async Task> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default) { return await _context.DataConnections .AsNoTracking() .Where(d => d.SiteId == siteId) .OrderBy(d => d.Name) .ToListAsync(cancellationToken); } /// public async Task> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default) { return await _context.DataConnections .AsNoTracking() .OrderBy(d => d.Name) .ToListAsync(cancellationToken); } /// public async Task> GetInstancesFilteredAsync( int? siteId = null, int? templateId = null, string? searchTerm = null, CancellationToken cancellationToken = default) { var query = _context.Instances.AsNoTracking().AsQueryable(); if (siteId.HasValue) query = query.Where(i => i.SiteId == siteId.Value); if (templateId.HasValue) query = query.Where(i => i.TemplateId == templateId.Value); if (!string.IsNullOrWhiteSpace(searchTerm)) query = query.Where(i => i.UniqueName.Contains(searchTerm)); return await query .OrderBy(i => i.UniqueName) .ToListAsync(cancellationToken); } /// public async Task<(IReadOnlyList Rows, int Total)> GetInstancesPagedAsync( IReadOnlyCollection? siteIds, int? templateId, string? search, int skip, int take, CancellationToken cancellationToken = default) { var query = _context.Instances.AsNoTracking().AsQueryable(); if (siteIds is not null) { var ids = siteIds.ToList(); query = query.Where(i => ids.Contains(i.SiteId)); } if (templateId.HasValue) query = query.Where(i => i.TemplateId == templateId.Value); if (!string.IsNullOrWhiteSpace(search)) query = query.Where(i => i.UniqueName.StartsWith(search)); var total = await query.CountAsync(cancellationToken); var rows = await query .OrderBy(i => i.UniqueName) .Skip(Math.Max(0, skip)) .Take(Math.Max(0, take)) .ToListAsync(cancellationToken); return (rows, total); } /// public async Task> GetAreaTreeBySiteIdAsync(int siteId, CancellationToken cancellationToken = default) { return await _context.Areas .AsNoTracking() .Where(a => a.SiteId == siteId) .Include(a => a.Children) .OrderBy(a => a.Name) .ToListAsync(cancellationToken); } /// public async Task<(IReadOnlyList Entries, int TotalCount)> GetAuditLogEntriesAsync( string? user = null, string? entityType = null, string? action = null, DateTimeOffset? from = null, DateTimeOffset? to = null, string? entityId = null, string? entityName = null, Guid? bundleImportId = null, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default, bool entityNameContains = false) { var query = BuildAuditLogQuery( user, entityType, action, from, to, entityId, entityName, bundleImportId, entityNameContains, DateTimeOffset.UtcNow); var totalCount = await query.CountAsync(cancellationToken); var entries = await query // Id tie-break: a bundle import writes hundreds of rows within the same few // milliseconds, so Timestamp alone makes offset pages repeat/drop rows. .OrderByDescending(a => a.Timestamp) .ThenByDescending(a => a.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken); return (entries, totalCount); } /// Default window for an opt-in entity-name substring search with no explicit from. internal static readonly TimeSpan EntityNameContainsDefaultWindow = TimeSpan.FromDays(30); internal IQueryable BuildAuditLogQuery( string? user, string? entityType, string? action, DateTimeOffset? from, DateTimeOffset? to, string? entityId, string? entityName, Guid? bundleImportId, bool entityNameContains, DateTimeOffset nowUtc) { var query = _context.AuditLogEntries.AsNoTracking().AsQueryable(); if (!string.IsNullOrWhiteSpace(user)) query = query.Where(a => a.User == user); if (!string.IsNullOrWhiteSpace(entityType)) query = query.Where(a => a.EntityType == entityType); if (!string.IsNullOrWhiteSpace(action)) query = query.Where(a => a.Action == action); // A substring name search scans, so it is never unbounded: without an explicit // `from` it is limited to the last EntityNameContainsDefaultWindow (ScadaBridge#50). var contains = entityNameContains && !string.IsNullOrWhiteSpace(entityName); var effectiveFrom = from ?? (contains ? nowUtc - EntityNameContainsDefaultWindow : (DateTimeOffset?)null); if (effectiveFrom.HasValue) { var lowerBound = effectiveFrom.Value; query = query.Where(a => a.Timestamp >= lowerBound); } if (to.HasValue) query = query.Where(a => a.Timestamp <= to.Value); if (!string.IsNullOrWhiteSpace(entityId)) query = query.Where(a => a.EntityId == entityId); if (!string.IsNullOrWhiteSpace(entityName)) query = contains ? query.Where(a => a.EntityName.Contains(entityName)) : query.Where(a => a.EntityName.StartsWith(entityName)); if (bundleImportId is Guid bundleId) query = query.Where(a => a.BundleImportId == bundleId); return query; } /// public async Task SaveChangesAsync(CancellationToken cancellationToken = default) { return await _context.SaveChangesAsync(cancellationToken); } }