119 lines
5.7 KiB
C#
119 lines
5.7 KiB
C#
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
using QuestionsApi.Models;
|
|
|
|
namespace QuestionsApi.Services;
|
|
|
|
public sealed class UserProfileStore
|
|
{
|
|
private readonly IMongoDatabase _database;
|
|
private readonly IMongoCollection<UserStudyProfile> _profiles;
|
|
private readonly IMongoCollection<DeletedUserTombstone> _deletedUsers;
|
|
|
|
private static readonly string[] OwnedCollections =
|
|
[
|
|
"QuestionAttempts", "UserRatings", "QuestionBookmarks", "UserNotepads",
|
|
"ReviewQueueItems", "QuestionReviewAttempts", "ReviewQueueBackfillStates",
|
|
"DynamicQuestionAttempts", "DynamicObjectiveMastery", "DynamicReviewItems",
|
|
"CcsAttempts", "CcsUserRatings", "CcsRatedCaseResults", "ExamSimulations",
|
|
"Step3Outcomes", "QuestionReports", "CcsCaseReports", "AppFeedback", "UserStudyProfiles",
|
|
"ExternalIdentities", "ExternalAuthTransactions",
|
|
"FlashcardDecks", "FlashcardNotes", "FlashcardSchedules", "FlashcardReviewLogs",
|
|
"FlashcardMediaAssets", "FlashcardSettings", "FlashcardImportPreviews",
|
|
"UserCreditAccounts"
|
|
];
|
|
|
|
public UserProfileStore(MongoDatabaseProvider mongo)
|
|
{
|
|
_database = mongo.Database;
|
|
_profiles = _database.GetCollection<UserStudyProfile>("UserStudyProfiles");
|
|
_deletedUsers = _database.GetCollection<DeletedUserTombstone>("DeletedUserTombstones");
|
|
_profiles.Indexes.CreateOne(new CreateIndexModel<UserStudyProfile>(
|
|
Builders<UserStudyProfile>.IndexKeys.Ascending(value => value.UserId),
|
|
new CreateIndexOptions { Unique = true }));
|
|
}
|
|
|
|
public async Task<UserStudyProfile?> GetAsync(string userId) =>
|
|
await _profiles.Find(value => value.UserId == userId).FirstOrDefaultAsync();
|
|
|
|
public async Task<UserStudyProfile> UpsertAsync(string userId, UpdateUserStudyProfileRequest request)
|
|
{
|
|
var update = Builders<UserStudyProfile>.Update
|
|
.Set(value => value.TargetExamDate, request.TargetExamDate)
|
|
.Set(value => value.PreparationStage, request.PreparationStage)
|
|
.Set(value => value.WeeklyGoalHours, request.WeeklyGoalHours)
|
|
.Set(value => value.PreferredStudyMode, request.PreferredStudyMode)
|
|
.Set(value => value.UpdatedUtc, DateTime.UtcNow)
|
|
.Inc(value => value.Revision, 1)
|
|
.SetOnInsert(value => value.UserId, userId);
|
|
return await _profiles.FindOneAndUpdateAsync(
|
|
value => value.UserId == userId,
|
|
update,
|
|
new FindOneAndUpdateOptions<UserStudyProfile> { IsUpsert = true, ReturnDocument = ReturnDocument.After });
|
|
}
|
|
|
|
public async Task<UserStudyProfile> UpdateTextScaleAsync(string userId, UpdateTextScaleRequest request)
|
|
{
|
|
var update = Builders<UserStudyProfile>.Update.Set(value => value.UpdatedUtc, DateTime.UtcNow);
|
|
if (request.QuestionTextScale is { } questionScale)
|
|
update = update.Set(value => value.QuestionTextScale, questionScale);
|
|
if (request.CcsTextScale is { } ccsScale)
|
|
update = update.Set(value => value.CcsTextScale, ccsScale);
|
|
update = update.SetOnInsert(value => value.UserId, userId);
|
|
return await _profiles.FindOneAndUpdateAsync(
|
|
value => value.UserId == userId,
|
|
update,
|
|
new FindOneAndUpdateOptions<UserStudyProfile> { IsUpsert = true, ReturnDocument = ReturnDocument.After });
|
|
}
|
|
|
|
public Task<bool> IsDeletedAsync(string userId) =>
|
|
_deletedUsers.Find(value => value.UserId == userId).AnyAsync();
|
|
|
|
public async Task<Dictionary<string, List<object?>>> ExportAsync(string userId)
|
|
{
|
|
var result = new Dictionary<string, List<object?>>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var collectionName in OwnedCollections)
|
|
{
|
|
var documents = await _database.GetCollection<BsonDocument>(collectionName)
|
|
.Find(Builders<BsonDocument>.Filter.Eq("UserId", userId)).ToListAsync();
|
|
result[collectionName] = documents.Select(document => Map(document)).ToList();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public async Task PurgeAsync(string userId)
|
|
{
|
|
await _deletedUsers.ReplaceOneAsync(
|
|
value => value.UserId == userId,
|
|
new DeletedUserTombstone { UserId = userId, DeletedUtc = DateTime.UtcNow },
|
|
new ReplaceOptions { IsUpsert = true });
|
|
|
|
foreach (var collectionName in OwnedCollections)
|
|
{
|
|
await _database.GetCollection<BsonDocument>(collectionName)
|
|
.DeleteManyAsync(Builders<BsonDocument>.Filter.Eq("UserId", userId));
|
|
}
|
|
|
|
foreach (var collectionName in new[] { "QuestionReports", "CcsCaseReports", "AppFeedback" })
|
|
{
|
|
await _database.GetCollection<BsonDocument>(collectionName).UpdateManyAsync(
|
|
Builders<BsonDocument>.Filter.Eq("ReviewedByUserId", userId),
|
|
Builders<BsonDocument>.Update.Unset("ReviewedByUserId"));
|
|
}
|
|
}
|
|
|
|
private static object? Map(BsonValue value)
|
|
{
|
|
if (value.IsBsonNull) return null;
|
|
if (value.IsBsonDocument)
|
|
return value.AsBsonDocument.Elements.ToDictionary(element => element.Name, element => Map(element.Value));
|
|
if (value.IsBsonArray) return value.AsBsonArray.Select(Map).ToList();
|
|
if (value.IsObjectId) return value.AsObjectId.ToString();
|
|
if (value.IsValidDateTime) return value.ToUniversalTime();
|
|
if (value.IsDecimal128) return Decimal128.ToDecimal(value.AsDecimal128);
|
|
if (value.IsBsonBinaryData) return Convert.ToBase64String(value.AsBsonBinaryData.Bytes);
|
|
if (value.IsBsonRegularExpression) return value.AsBsonRegularExpression.ToString();
|
|
return BsonTypeMapper.MapToDotNetValue(value);
|
|
}
|
|
}
|