383 lines
20 KiB
C#
383 lines
20 KiB
C#
using adas_core.Application.Repositories.Interfaces;
|
|
using adas_core.Domain.Models.AppSettings;
|
|
using adas_core.Domain.Models.Pumps;
|
|
using Microsoft.Extensions.Options;
|
|
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
|
|
namespace adas_core.Infrastructure.Repositories
|
|
{
|
|
/// <summary>
|
|
/// A repository for managing <see cref="PumpObservation"/> entities in a MongoDB data store.
|
|
/// This class extends the generic <see cref="MongoRepository{T}"/> base class and implements
|
|
/// the <see cref="IPumpObservationRepository"/> contract to provide persistence operations for pump observation data.
|
|
/// </summary>
|
|
/// <typeparam name="PumpObservation">The type of the entity managed by this repository.</typeparam>
|
|
/// <remarks>
|
|
/// As a specialized repository inheriting from <see cref="MongoRepository{PumpObservation}"/>, this class
|
|
/// reuses the base MongoDB storage capabilities while exposing the pump observation-specific repository contract.
|
|
/// </remarks>
|
|
public class PumpObservationRepository : MongoRepository<PumpObservation>, IPumpObservationRepository
|
|
{
|
|
private readonly ApiSettings _apiSettings;
|
|
|
|
|
|
|
|
public PumpObservationRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database)
|
|
{
|
|
_apiSettings = apiSettings.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the collection name used for pump observations, returning the configured value from API settings if available, or falling back to the default "pump_observations" name when no custom configuration is provided.
|
|
/// </summary>
|
|
/// <returns>The configured pump observations collection name from API settings, or the default "pump_observations" string if the setting is null.</returns>
|
|
public override string GetCollectionName()
|
|
{
|
|
return _apiSettings.PumpObservations ?? "pump_observations";
|
|
}
|
|
|
|
public override async Task CreateIndexes()
|
|
{
|
|
var indexModels = new List<CreateIndexModel<PumpObservation>>
|
|
{
|
|
// Timeline por bomba (consulta más frecuente)
|
|
new CreateIndexModel<PumpObservation>(
|
|
Builders<PumpObservation>.IndexKeys
|
|
.Ascending(x => x.DeviceId)
|
|
.Descending(x => x.Time),
|
|
new CreateIndexOptions { Name = "ix_deviceId_time" }),
|
|
|
|
// consultas por paciente
|
|
new CreateIndexModel<PumpObservation>(
|
|
Builders<PumpObservation>.IndexKeys.Ascending(x => x.PatientId),
|
|
new CreateIndexOptions { Name = "ix_patientId" }),
|
|
|
|
|
|
// TTL por antigüedad (si aplica retención directa en Mongo)
|
|
// new CreateIndexModel<PumpObservation>(
|
|
// Builders<PumpObservation>.IndexKeys.Ascending(x => x.Time),
|
|
// new CreateIndexOptions { Name = "ttl_time", ExpireAfter = TimeSpan.FromDays(180) })
|
|
};
|
|
|
|
await Collection.Indexes.CreateManyAsync(indexModels);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously inserts a pump observation into the underlying collection.
|
|
/// </summary>
|
|
/// <param name="obs">The pump observation document to be persisted.</param>
|
|
public async Task InsertAsync(PumpObservation obs)
|
|
{
|
|
await Collection.InsertOneAsync(obs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a batch of pump observations into the underlying collection in a single operation. Returns immediately when the input is null or contains no elements, performing no insertion in those cases.
|
|
/// </summary>
|
|
/// <param name="observations">The pump observations to insert. A null or empty collection results in a no-op.</param>
|
|
public async Task InsertManyAsync(IEnumerable<PumpObservation>? observations)
|
|
{
|
|
if (observations == null) return;
|
|
|
|
var list = observations as IList<PumpObservation> ?? observations.ToList();
|
|
if (list.Count == 0) return;
|
|
|
|
await Collection.InsertManyAsync(list);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves pump observations for a specific device, optionally filtered by a time range and limited to a maximum number of results, sorted by time in descending order.
|
|
/// </summary>
|
|
/// <param name="deviceId">The identifier of the device whose observations should be retrieved.</param>
|
|
/// <param name="from">Optional start timestamp; when provided, only observations with a time greater than or equal to this value are returned.</param>
|
|
/// <param name="to">Optional end timestamp; when provided, only observations with a time less than or equal to this value are returned.</param>
|
|
/// <param name="limit">Optional maximum number of observations to return; when not provided, all matching observations are returned.</param>
|
|
/// <returns>A task that represents the asynchronous operation, containing the collection of matching <see cref="PumpObservation"/> records.</returns>
|
|
public async Task<IEnumerable<PumpObservation>> FindByDeviceIdAsync(
|
|
string deviceId,
|
|
DateTime? from = null,
|
|
DateTime? to = null,
|
|
int? limit = null)
|
|
{
|
|
var filter = Builders<PumpObservation>.Filter.Eq(x => x.DeviceId, deviceId);
|
|
|
|
if (from.HasValue)
|
|
filter &= Builders<PumpObservation>.Filter.Gte(x => x.Time, from.Value);
|
|
|
|
if (to.HasValue)
|
|
filter &= Builders<PumpObservation>.Filter.Lte(x => x.Time, to.Value);
|
|
|
|
var query = Collection.Find(filter)
|
|
.SortByDescending(x => x.Time);
|
|
|
|
if (limit.HasValue)
|
|
query = query.Limit(limit.Value) as IOrderedFindFluent<PumpObservation, PumpObservation>;
|
|
|
|
return await query.ToListAsync();
|
|
}
|
|
|
|
// histórico por paciente
|
|
/// <summary>
|
|
/// Retrieves pump observations for a specific patient, optionally filtered by a time range and optionally capped to a maximum number of results. Results are ordered from most recent to oldest by observation time.
|
|
/// </summary>
|
|
/// <param name="patientId">The identifier of the patient whose pump observations should be retrieved.</param>
|
|
/// <param name="from">Optional inclusive lower bound for the observation time. When null, no lower time bound is applied.</param>
|
|
/// <param name="to">Optional inclusive upper bound for the observation time. When null, no upper time bound is applied.</param>
|
|
/// <param name="limit">Optional maximum number of observations to return. When null, all matching observations are returned.</param>
|
|
/// <returns>A task that represents the asynchronous operation. The task result contains the matching pump observations sorted by time in descending order.</returns>
|
|
public async Task<IEnumerable<PumpObservation>> FindByPatientAsync(
|
|
ObjectId patientId,
|
|
DateTime? from = null,
|
|
DateTime? to = null,
|
|
int? limit = null)
|
|
{
|
|
var filter = Builders<PumpObservation>.Filter.Eq(x => x.PatientId, patientId);
|
|
|
|
if (from.HasValue)
|
|
filter &= Builders<PumpObservation>.Filter.Gte(x => x.Time, from.Value);
|
|
|
|
if (to.HasValue)
|
|
filter &= Builders<PumpObservation>.Filter.Lte(x => x.Time, to.Value);
|
|
|
|
var query = Collection.Find(filter)
|
|
.SortByDescending(x => x.Time);
|
|
|
|
if (limit.HasValue)
|
|
query = query.Limit(limit.Value) as IOrderedFindFluent<PumpObservation, PumpObservation>;
|
|
|
|
return await query.ToListAsync();
|
|
}
|
|
|
|
public async Task<Dictionary<ObjectId, DateTime>> FindAllLastPatientObservationTimeAsync()
|
|
{
|
|
// Pipeline:
|
|
// 1) Filtrar observaciones con PatientId no nulo
|
|
// 2) Agrupar por PatientId
|
|
// 3) Obtener el máximo Time
|
|
// 4) Devolver diccionario
|
|
|
|
var pipeline = new[]
|
|
{
|
|
new BsonDocument("$match", new BsonDocument
|
|
{
|
|
{ "patientid", new BsonDocument("$ne", BsonNull.Value) }
|
|
}),
|
|
new BsonDocument("$group", new BsonDocument
|
|
{
|
|
{ "_id", "$patientid" },
|
|
{ "LastTime", new BsonDocument("$max", "$time") }
|
|
})
|
|
};
|
|
|
|
var docs = await Collection.Aggregate<BsonDocument>(pipeline).ToListAsync();
|
|
|
|
var result = new Dictionary<ObjectId, DateTime>();
|
|
|
|
foreach (var doc in docs.Where(doc =>
|
|
doc["_id"].IsObjectId && doc["LastTime"].IsValidDateTime
|
|
))
|
|
{
|
|
result[doc["_id"].AsObjectId] = doc["LastTime"].ToUniversalTime();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the most recent pump observation associated with the specified device identifier by querying the collection, filtering by device, and returning the observation with the latest timestamp. Returns <c>null</c> when no matching observation exists for the device.
|
|
/// </summary>
|
|
/// <param name="deviceId">The unique identifier of the device whose latest pump observation should be retrieved.</param>
|
|
/// <returns>A task that resolves to the most recent <see cref="PumpObservation"/> for the device, or <c>null</c> if no observation is found.</returns>
|
|
public async Task<PumpObservation?> FindLastByDeviceIdAsync(string deviceId)
|
|
{
|
|
return await Collection
|
|
.Find(x => x.DeviceId == deviceId)
|
|
.SortByDescending(x => x.Time)
|
|
.FirstOrDefaultAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all pump observations associated with the specified patient identifier.
|
|
/// </summary>
|
|
/// <param name="patientId">The optional patient identifier used to filter the pump observations.</param>
|
|
/// <returns>A task that represents the asynchronous operation, containing a collection of pump observations matching the given patient identifier.</returns>
|
|
public async Task<IEnumerable<PumpObservation>> FindByPatientId(ObjectId? patientId)
|
|
{
|
|
return await Collection.Find(x => x.PatientId == patientId).ToListAsync();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Retrieves the most recent pump observations for a specified patient, deduplicated by code and name, and sorted by time in descending order.
|
|
/// </summary>
|
|
/// <param name="patientId">The identifier of the patient whose observations are being retrieved.</param>
|
|
/// <param name="num">The maximum number of observations to consider before deduplication. Defaults to 100.</param>
|
|
/// <returns>A task that represents the asynchronous operation. The task result contains a list of distinct <see cref="PumpObservation"/> entries for the patient, ordered from most recent to oldest.</returns>
|
|
public async Task<List<PumpObservation>> AggregatedPatientLastObservations(ObjectId patientId, int num = 100)
|
|
{
|
|
var filterBuilder = Builders<PumpObservation>.Filter;
|
|
var sortBuilder = Builders<PumpObservation>.Sort;
|
|
|
|
var filter = filterBuilder.Eq(o => o.PatientId, patientId);
|
|
var sort = sortBuilder.Descending("time");
|
|
var options = new FindOptions<PumpObservation> { Sort = sort, Limit = num };
|
|
|
|
var result = await Collection.FindAsync(filter, options);
|
|
var observations = await result.ToListAsync();
|
|
|
|
var distinctObservations = observations.DistinctBy(m => new { m.Code, m.Name }).ToList();
|
|
var sortedObservations = distinctObservations.OrderByDescending(x => x.Time).ToList();
|
|
|
|
return sortedObservations;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all records whose <c>PatientId</c> matches the specified patient identifier.
|
|
/// </summary>
|
|
/// <param name="patientId">The patient identifier whose associated records should be removed; may be <c>null</c>.</param>
|
|
public async Task DeleteByPatientId(ObjectId? patientId)
|
|
{
|
|
await Collection.DeleteManyAsync(x => x.PatientId == patientId);
|
|
}
|
|
/// <summary>
|
|
/// Deletes <see cref="PumpObservation"/> documents whose timestamp is older than the specified number of days, optionally filtered by name.
|
|
/// When <paramref name="name"/> is provided, only observations matching that name are removed; otherwise, all observations older than the cutoff are deleted.
|
|
/// The cutoff date is computed using UTC time.
|
|
/// </summary>
|
|
/// <param name="days">The age threshold in days. Observations with a <c>Time</c> older than <c>DateTime.UtcNow - days</c> are eligible for deletion.</param>
|
|
/// <param name="name">Optional name used to further restrict the deletion to observations with a matching <c>Name</c> value. If null or empty, the name filter is not applied.</param>
|
|
/// <returns>The number of <see cref="PumpObservation"/> documents that were deleted.</returns>
|
|
public async Task<long> DeleteOlderThanDaysAsync(int days, string? name = null)
|
|
{
|
|
var limitDate = DateTime.UtcNow.AddDays(-days);
|
|
|
|
var filter = Builders<PumpObservation>.Filter.Lt(x => x.Time, limitDate);
|
|
if(!string.IsNullOrEmpty(name))
|
|
filter &= Builders<PumpObservation>.Filter.Eq(x=> x.Name, name);
|
|
|
|
return (await Collection.DeleteManyAsync(filter)).DeletedCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes older pump observations while retaining only the most recent <paramref name="maxCount"/> records for each device.
|
|
/// Devices whose observation count is less than or equal to the threshold are left untouched.
|
|
/// </summary>
|
|
/// <param name="maxCount">The maximum number of most recent records to keep per device.</param>
|
|
/// <returns>The total number of observations deleted across all devices.</returns>
|
|
public async Task<long> DeleteKeepLastNAsync(int maxCount)
|
|
{
|
|
// Para cada DeviceId:
|
|
var deviceIds = await Collection
|
|
.Distinct<string>("DeviceId", FilterDefinition<PumpObservation>.Empty)
|
|
.ToListAsync();
|
|
|
|
long totalDeleted = 0;
|
|
|
|
foreach (var filter in deviceIds.Select(deviceId =>
|
|
Builders<PumpObservation>.Filter.Eq(x => x.DeviceId, deviceId)
|
|
))
|
|
{
|
|
var all = await Collection.Find(filter)
|
|
.SortByDescending(x => x.Time)
|
|
.ToListAsync();
|
|
|
|
if (all.Count <= maxCount)
|
|
continue;
|
|
|
|
var toDelete = all.Skip(maxCount).Select(x => x.Id).ToList();
|
|
|
|
var deleteFilter = Builders<PumpObservation>.Filter.In(x => x.Id, toDelete);
|
|
var result = await Collection.DeleteManyAsync(deleteFilter);
|
|
|
|
totalDeleted += result.DeletedCount;
|
|
}
|
|
|
|
return totalDeleted;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the specified field in multiple <see cref="PumpObservation"/> documents, setting it to a new <see cref="ObjectId"/> where it currently matches the optional old <see cref="ObjectId"/>.
|
|
/// </summary>
|
|
/// <param name="fieldName">The name of the field to update. Must not be null or empty.</param>
|
|
/// <param name="newId">The new <see cref="ObjectId"/> value to assign to the field.</param>
|
|
/// <param name="oldId">The current <see cref="ObjectId"/> value used to match documents; if <c>null</c>, the filter matches documents where the field is null.</param>
|
|
/// <returns>The number of documents that were modified by the update operation.</returns>
|
|
/// <exception cref="ArgumentException">Thrown when <paramref name="fieldName"/> is null, empty, or whitespace.</exception>
|
|
public async Task<long> UpdateManyObjectIdByFieldAsync(string fieldName, ObjectId newId, ObjectId? oldId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(fieldName))
|
|
throw new ArgumentException("fieldName can't be null or empty.", nameof(fieldName));
|
|
|
|
// Normalize casing (Mongo is case-sensitive)
|
|
// if (fieldName.Equals("patientid", StringComparison.OrdinalIgnoreCase))
|
|
// fieldName = nameof(PumpObservation.PatientId);
|
|
|
|
var filter = Builders<PumpObservation>.Filter.Eq(fieldName, oldId);
|
|
var update = Builders<PumpObservation>.Update.Set(fieldName, newId);
|
|
|
|
var result = await Collection.UpdateManyAsync(filter, update);
|
|
return result.ModifiedCount;
|
|
}
|
|
|
|
|
|
public async Task<long> DeleteOlderNumberAsync(string name, int maxCount)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return 0;
|
|
|
|
// 1. Filtrar todas las observaciones con ese Name
|
|
var filter = Builders<PumpObservation>.Filter.Eq(x => x.Name, name);
|
|
|
|
// 2. Obtenerlas ordenadas por Time DESC (las más recientes primero)
|
|
var all = await Collection
|
|
.Find(filter)
|
|
.SortByDescending(x => x.Time)
|
|
.ToListAsync();
|
|
|
|
// 3. Si hay menos o igual al número permitido → no borrar nada
|
|
if (all.Count <= maxCount)
|
|
return 0;
|
|
|
|
// 4. Seleccionar TODAS excepto las maxCount más recientes
|
|
var toDeleteIds = all
|
|
.Skip(maxCount)
|
|
.Select(x => x.Id)
|
|
.ToList();
|
|
|
|
if (toDeleteIds.Count == 0)
|
|
return 0;
|
|
|
|
// 5. Borrar las seleccionadas
|
|
var deleteFilter = Builders<PumpObservation>.Filter.In(x => x.Id, toDeleteIds);
|
|
var result = await Collection.DeleteManyAsync(deleteFilter);
|
|
|
|
return result.DeletedCount;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Retrieves up to the two most recent pump observations for the specified patient and observation name, ordered by time descending. Returns an empty list if the name is null, empty, or whitespace.
|
|
/// </summary>
|
|
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
|
|
/// <param name="name">The name of the pump observation to filter by. If null, empty, or whitespace, an empty list is returned.</param>
|
|
/// <returns>A task representing the asynchronous operation, containing a list of the matching pump observations (at most two) sorted from newest to oldest.</returns>
|
|
public async Task<List<PumpObservation>> FindLastObservations(ObjectId patientId, string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return [];
|
|
|
|
var filter = Builders<PumpObservation>.Filter.And(
|
|
Builders<PumpObservation>.Filter.Eq(x => x.PatientId, patientId),
|
|
Builders<PumpObservation>.Filter.Eq(x => x.Name, name)
|
|
);
|
|
|
|
return await Collection
|
|
.Find(filter)
|
|
.SortByDescending(x => x.Time)
|
|
.Limit(2)
|
|
.ToListAsync();
|
|
}
|
|
|
|
}
|
|
} |