Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Mimir.MongoDB/Models/DailyActiveUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Mimir.MongoDB.Models;

public record DailyActiveUser(
string Date,
int Count
);

67 changes: 67 additions & 0 deletions Mimir.MongoDB/Repositories/TransactionRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Mimir.MongoDB.Bson;
using Mimir.MongoDB.Exceptions;
using Mimir.MongoDB.Services;
using MongoDB.Bson;
using MongoDB.Driver;

namespace Mimir.MongoDB.Repositories;
Expand All @@ -19,6 +20,7 @@ public interface ITransactionRepository
IExecutable<TransactionDocument> GetBySignerAsync(string signer);
IExecutable<TransactionDocument> GetByAvatarAddressAsync(string avatarAddress);
IExecutable<TransactionDocument> GetByActionTypeIdAsync(string actionTypeId);
Task<List<DailyActiveUser>> GetDailyActiveUsersAsync(DateTime? startDate = null, DateTime? endDate = null);
}

public class TransactionRepository(IMongoDbService dbService) : ITransactionRepository
Expand Down Expand Up @@ -128,4 +130,69 @@ public IExecutable<TransactionDocument> GetByActionTypeIdAsync(string actionType
var sortDefinition = Builders<TransactionDocument>.Sort.Descending("BlockIndex");
return find.Sort(sortDefinition).AsExecutable();
}

public async Task<List<DailyActiveUser>> GetDailyActiveUsersAsync(DateTime? startDate = null, DateTime? endDate = null)
{
var pipeline = new List<BsonDocument>();

var matchStage = new BsonDocument("$match", new BsonDocument
{
{ "BlockTimestamp", new BsonDocument
{
{ "$ne", BsonNull.Value }
}
}
});

if (startDate.HasValue || endDate.HasValue)
{
var timestampFilter = new BsonDocument();
if (startDate.HasValue)
{
timestampFilter.Add("$gte", startDate.Value.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
}
if (endDate.HasValue)
{
timestampFilter.Add("$lte", endDate.Value.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
}
matchStage["$match"]["BlockTimestamp"] = timestampFilter;
}

pipeline.Add(matchStage);

var projectStage = new BsonDocument("$project", new BsonDocument
{
{ "date", new BsonDocument("$substr", new BsonArray { "$BlockTimestamp", 0, 10 }) },
{ "signer", "$Object.Signer" }
});
pipeline.Add(projectStage);

var groupStage = new BsonDocument("$group", new BsonDocument
{
{ "_id", new BsonDocument
{
{ "date", "$date" },
{ "signer", "$signer" }
}
}
});
pipeline.Add(groupStage);

var groupByDateStage = new BsonDocument("$group", new BsonDocument
{
{ "_id", "$_id.date" },
{ "count", new BsonDocument("$sum", 1) }
});
pipeline.Add(groupByDateStage);

var sortStage = new BsonDocument("$sort", new BsonDocument("_id", 1));
pipeline.Add(sortStage);

var results = await _collection.Aggregate<BsonDocument>(pipeline).ToListAsync();

return results.Select(doc => new DailyActiveUser(
doc["_id"].AsString,
doc["count"].AsInt32
)).ToList();
}
}
52 changes: 52 additions & 0 deletions Mimir.Tests/Options/RateLimitOptionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Mimir.Options;
using Xunit;

namespace Mimir.Tests.Options;

public class RateLimitOptionTest
{
[Fact]
public void DefaultValues_ShouldBeCorrect()
{
var option = new RateLimitOption();

Assert.True(option.IsEnabled);
Assert.Equal(50, option.PermitLimit);
Assert.Equal(10, option.Window);
Assert.Equal(10, option.ReplenishmentPeriod);
Assert.Equal(2, option.QueueLimit);
Assert.Equal(8, option.SegmentsPerWindow);
Assert.Equal(50, option.TokenLimit);
Assert.Equal(50, option.TokensPerPeriod);
Assert.True(option.AutoReplenishment);
}

[Fact]
public void IsEnabled_WhenSetToFalse_ShouldBeFalse()
{
var option = new RateLimitOption
{
IsEnabled = false
};

Assert.False(option.IsEnabled);
}

[Fact]
public void IsEnabled_WhenSetToTrue_ShouldBeTrue()
{
var option = new RateLimitOption
{
IsEnabled = true
};

Assert.True(option.IsEnabled);
}

[Fact]
public void SectionName_ShouldBeCorrect()
{
Assert.Equal("RateLimit", RateLimitOption.SectionName);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"data": {
"dailyActiveUsers": [
{
"date": "2024-01-01",
"count": 100
},
{
"date": "2024-01-02",
"count": 150
},
{
"date": "2024-01-03",
"count": 120
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": {
"dailyActiveUsers": [
{
"date": "2024-01-01",
"count": 100
},
{
"date": "2024-01-02",
"count": 150
}
]
}
}
70 changes: 70 additions & 0 deletions Mimir.Tests/QueryTests/DailyActiveUsersTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Mimir.MongoDB.Models;
using Mimir.MongoDB.Repositories;
using Moq;

namespace Mimir.Tests.QueryTests;

public class DailyActiveUsersTest
{
[Fact]
public async Task GraphQL_Query_DailyActiveUsers_Returns_CorrectValue()
{
var mockRepo = new Mock<ITransactionRepository>();
mockRepo
.Setup(repo => repo.GetDailyActiveUsersAsync(It.IsAny<DateTime?>(), It.IsAny<DateTime?>()))
.ReturnsAsync(new List<DailyActiveUser>
{
new DailyActiveUser("2024-01-01", 100),
new DailyActiveUser("2024-01-02", 150),
new DailyActiveUser("2024-01-03", 120)
});

var serviceProvider = TestServices.Builder
.With(mockRepo.Object)
.Build();

var query = """
query {
dailyActiveUsers(startDate: "2024-01-01T00:00:00Z", endDate: "2024-01-03T23:59:59Z") {
date
count
}
}
""";

var result = await TestServices.ExecuteRequestAsync(serviceProvider, b => b.SetDocument(query));

await Verify(result);
}

[Fact]
public async Task GraphQL_Query_DailyActiveUsers_WithoutDateRange_Returns_CorrectValue()
{
var mockRepo = new Mock<ITransactionRepository>();
mockRepo
.Setup(repo => repo.GetDailyActiveUsersAsync(null, null))
.ReturnsAsync(new List<DailyActiveUser>
{
new DailyActiveUser("2024-01-01", 100),
new DailyActiveUser("2024-01-02", 150)
});

var serviceProvider = TestServices.Builder
.With(mockRepo.Object)
.Build();

var query = """
query {
dailyActiveUsers {
date
count
}
}
""";

var result = await TestServices.ExecuteRequestAsync(serviceProvider, b => b.SetDocument(query));

await Verify(result);
}
}

13 changes: 13 additions & 0 deletions Mimir/GraphQL/Queries/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Mimir.GraphQL.Types;
using Mimir.MongoDB;
using Mimir.MongoDB.Bson;
using Mimir.MongoDB.Models;
using Mimir.MongoDB.Repositories;
using Mimir.Services;
using Nekoyume;
Expand Down Expand Up @@ -162,6 +163,18 @@ public async Task<Dictionary<int, CombinationSlotState>> GetCombinationSlotsAsyn
[Service] IAllCombinationSlotStateRepository repo
) => (await repo.GetByAddressAsync(avatarAddress)).Object.CombinationSlots;

/// <summary>
/// Get daily active users count grouped by date.
/// </summary>
/// <param name="startDate">Optional start date filter</param>
/// <param name="endDate">Optional end date filter</param>
/// <returns>List of daily active users with date and count</returns>
public async Task<List<DailyActiveUser>> GetDailyActiveUsersAsync(
DateTime? startDate,
DateTime? endDate,
[Service] ITransactionRepository repo
) => await repo.GetDailyActiveUsersAsync(startDate, endDate);

/// <summary>
/// Get the daily reward received block index by address.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions Mimir/GraphQL/Types/DailyActiveUserType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Mimir.MongoDB.Models;

namespace Mimir.GraphQL.Types;

public class DailyActiveUserType : ObjectType<DailyActiveUser>
{
protected override void Configure(IObjectTypeDescriptor<DailyActiveUser> descriptor)
{
descriptor.Field(d => d.Date).Description("The date in YYYY-MM-DD format");
descriptor.Field(d => d.Count).Description("The number of unique active users on this date");
}
}

13 changes: 13 additions & 0 deletions Mimir/GraphQL/Types/QueryType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,18 @@ protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
Rank = rank
};
});

descriptor
.Field("dailyActiveUsers")
.Description("Get daily active users count grouped by date.")
.Argument("startDate", a => a.Type<DateTimeType>())
.Argument("endDate", a => a.Type<DateTimeType>())
.Type<ListType<DailyActiveUserType>>()
.Resolve(async context =>
{
var startDate = context.ArgumentValue<DateTime?>("startDate");
var endDate = context.ArgumentValue<DateTime?>("endDate");
return await context.Service<ITransactionRepository>().GetDailyActiveUsersAsync(startDate, endDate);
});
}
}
1 change: 1 addition & 0 deletions Mimir/Options/RateLimitOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Mimir.Options;
public class RateLimitOption
{
public const string SectionName = "RateLimit";
public bool IsEnabled { get; set; } = true;
public int PermitLimit { get; set; } = 50;
public int Window { get; set; } = 10;
public int ReplenishmentPeriod { get; set; } = 10;
Expand Down
Loading
Loading