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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace RecurringThings.MongoDB.Configuration;

using System;
using global::MongoDB.Bson.Serialization.Conventions;
using global::MongoDB.Driver;
using Microsoft.Extensions.DependencyInjection;
using RecurringThings.Configuration;
Expand Down Expand Up @@ -43,6 +44,13 @@ public static RecurringThingsBuilder UseMongoDb(
configure(options);
options.Validate();

// Register camelCase naming convention for RecurringThings documents
var conventionPack = new ConventionPack { new CamelCaseElementNameConvention() };
ConventionRegistry.Register(
"RecurringThingsCamelCase",
conventionPack,
t => t.FullName?.StartsWith("RecurringThings", StringComparison.Ordinal) == true);

// Mark MongoDB as configured
builder.MongoDbConfigured = true;

Expand Down
16 changes: 0 additions & 16 deletions src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,16 @@ public sealed class RecurringThingDocument
/// <remarks>
/// Valid values: "recurrence", "occurrence", "exception", "override"
/// </remarks>
[BsonElement("documentType")]
public required string DocumentType { get; set; }

/// <summary>
/// Gets or sets the tenant identifier for multi-tenant isolation.
/// </summary>
[BsonElement("organization")]
public required string Organization { get; set; }

/// <summary>
/// Gets or sets the hierarchical resource scope.
/// </summary>
[BsonElement("resourcePath")]
public required string ResourcePath { get; set; }

/// <summary>
Expand All @@ -58,7 +55,6 @@ public sealed class RecurringThingDocument
/// <remarks>
/// Present on recurrences and occurrences. Null for exceptions and overrides.
/// </remarks>
[BsonElement("type")]
[BsonIgnoreIfNull]
public string? Type { get; set; }

Expand All @@ -68,7 +64,6 @@ public sealed class RecurringThingDocument
/// <remarks>
/// Present on recurrences, occurrences, and overrides. Null for exceptions.
/// </remarks>
[BsonElement("startTime")]
[BsonIgnoreIfNull]
public DateTime? StartTime { get; set; }

Expand All @@ -79,7 +74,6 @@ public sealed class RecurringThingDocument
/// Present on occurrences and overrides. Null for recurrences and exceptions.
/// Computed as StartTime + Duration.
/// </remarks>
[BsonElement("endTime")]
[BsonIgnoreIfNull]
public DateTime? EndTime { get; set; }

Expand All @@ -90,7 +84,6 @@ public sealed class RecurringThingDocument
/// Stored as milliseconds for efficient MongoDB operations.
/// Present on recurrences, occurrences, and overrides. Null for exceptions.
/// </remarks>
[BsonElement("durationMs")]
[BsonIgnoreIfNull]
public long? DurationMs { get; set; }

Expand All @@ -100,7 +93,6 @@ public sealed class RecurringThingDocument
/// <remarks>
/// Present on recurrences and occurrences. Null for exceptions and overrides.
/// </remarks>
[BsonElement("timeZone")]
[BsonIgnoreIfNull]
public string? TimeZone { get; set; }

Expand All @@ -110,7 +102,6 @@ public sealed class RecurringThingDocument
/// <remarks>
/// Present only on recurrences.
/// </remarks>
[BsonElement("recurrenceEndTime")]
[BsonIgnoreIfNull]
public DateTime? RecurrenceEndTime { get; set; }

Expand All @@ -120,7 +111,6 @@ public sealed class RecurringThingDocument
/// <remarks>
/// Present only on recurrences.
/// </remarks>
[BsonElement("rrule")]
[BsonIgnoreIfNull]
public string? RRule { get; set; }

Expand All @@ -136,7 +126,6 @@ public sealed class RecurringThingDocument
/// monthly patterns with day &lt;= 28, or patterns where no months are affected).
/// </para>
/// </remarks>
[BsonElement("monthDayBehavior")]
[BsonIgnoreIfNull]
public string? MonthDayBehavior { get; set; }

Expand All @@ -146,7 +135,6 @@ public sealed class RecurringThingDocument
/// <remarks>
/// Present on exceptions and overrides.
/// </remarks>
[BsonElement("recurrenceId")]
[BsonRepresentation(BsonType.String)]
[BsonIgnoreIfNull]
public Guid? RecurrenceId { get; set; }
Expand All @@ -157,7 +145,6 @@ public sealed class RecurringThingDocument
/// <remarks>
/// Present on exceptions and overrides.
/// </remarks>
[BsonElement("originalTimeUtc")]
[BsonIgnoreIfNull]
public DateTime? OriginalTimeUtc { get; set; }

Expand All @@ -168,7 +155,6 @@ public sealed class RecurringThingDocument
/// Denormalized from the parent recurrence at creation time.
/// Present only on overrides.
/// </remarks>
[BsonElement("originalDurationMs")]
[BsonIgnoreIfNull]
public long? OriginalDurationMs { get; set; }

Expand All @@ -179,14 +165,12 @@ public sealed class RecurringThingDocument
/// Denormalized from the parent recurrence at creation time.
/// Present only on overrides.
/// </remarks>
[BsonElement("originalExtensions")]
[BsonIgnoreIfNull]
public Dictionary<string, string>? OriginalExtensions { get; set; }

/// <summary>
/// Gets or sets the user-defined key-value metadata.
/// </summary>
[BsonElement("extensions")]
[BsonIgnoreIfNull]
public Dictionary<string, string>? Extensions { get; set; }
}
Expand Down
4 changes: 2 additions & 2 deletions src/RecurringThings/RecurringThings.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
<RepositoryUrl>https://github.com/ChuckNovice/RecurringThings</RepositoryUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>logo_gray.png</PackageIcon>
<PackageIcon>logo_red.png</PackageIcon>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\assets\logo_gray.png" Pack="true" PackagePath="\" />
<None Include="..\..\assets\logo_red.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
Expand Down
105 changes: 52 additions & 53 deletions tests/RecurringThings.MongoDB.Tests/MongoDbIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace RecurringThings.MongoDB.Tests;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using global::MongoDB.Driver;
using RecurringThings.Domain;
using RecurringThings.MongoDB.Documents;
Expand Down Expand Up @@ -86,13 +85,13 @@ public async Task RecurrenceRepository_CreateAndGetById_ReturnsCreatedRecurrence
var result = await repo.GetByIdAsync(recurrence.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(recurrence.Id);
result.Organization.Should().Be(TestOrganization);
result.ResourcePath.Should().Be(TestResourcePath);
result.Type.Should().Be(TestType);
result.Duration.Should().Be(recurrence.Duration);
result.Extensions.Should().BeEquivalentTo(recurrence.Extensions);
Assert.NotNull(result);
Assert.Equal(recurrence.Id, result!.Id);
Assert.Equal(TestOrganization, result.Organization);
Assert.Equal(TestResourcePath, result.ResourcePath);
Assert.Equal(TestType, result.Type);
Assert.Equal(recurrence.Duration, result.Duration);
Assert.Equivalent(recurrence.Extensions, result.Extensions);
}

[SkippableFact]
Expand All @@ -113,9 +112,9 @@ public async Task RecurrenceRepository_Update_UpdatesDurationAndExtensions()
var result = await repo.GetByIdAsync(recurrence.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().NotBeNull();
result!.Duration.Should().Be(TimeSpan.FromHours(2));
result.Extensions.Should().ContainKey("updated");
Assert.NotNull(result);
Assert.Equal(TimeSpan.FromHours(2), result!.Duration);
Assert.True(result.Extensions?.ContainsKey("updated") == true);
}

[SkippableFact]
Expand All @@ -133,7 +132,7 @@ public async Task RecurrenceRepository_Delete_RemovesRecurrence()
var result = await repo.GetByIdAsync(recurrence.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().BeNull();
Assert.Null(result);
}

[SkippableFact]
Expand All @@ -154,8 +153,8 @@ public async Task RecurrenceRepository_GetInRange_ReturnsRecurrencesInRange()
TestOrganization, TestResourcePath, queryStart, queryEnd, null).ToListAsync();

// Assert
results.Should().HaveCount(1);
results[0].Id.Should().Be(recurrence.Id);
Assert.Single(results);
Assert.Equal(recurrence.Id, results[0].Id);
}

[SkippableFact]
Expand All @@ -177,7 +176,7 @@ public async Task RecurrenceRepository_GetInRange_FiltersOutOfRange()
TestOrganization, TestResourcePath, queryStart, queryEnd, null).ToListAsync();

// Assert
results.Should().BeEmpty();
Assert.Empty(results);
}

[SkippableFact]
Expand Down Expand Up @@ -216,8 +215,8 @@ public async Task RecurrenceRepository_GetInRange_FiltersByType()
TestOrganization, TestResourcePath, queryStart, queryEnd, [TestType]).ToListAsync();

// Assert
results.Should().HaveCount(1);
results[0].Type.Should().Be(TestType);
Assert.Single(results);
Assert.Equal(TestType, results[0].Type);
}

#endregion
Expand All @@ -238,11 +237,11 @@ public async Task OccurrenceRepository_CreateAndGetById_ReturnsCreatedOccurrence
var result = await repo.GetByIdAsync(occurrence.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(occurrence.Id);
result.StartTime.Should().Be(occurrence.StartTime);
result.EndTime.Should().Be(occurrence.EndTime);
result.Duration.Should().Be(occurrence.Duration);
Assert.NotNull(result);
Assert.Equal(occurrence.Id, result!.Id);
Assert.Equal(occurrence.StartTime, result.StartTime);
Assert.Equal(occurrence.EndTime, result.EndTime);
Assert.Equal(occurrence.Duration, result.Duration);
}

[SkippableFact]
Expand All @@ -265,9 +264,9 @@ public async Task OccurrenceRepository_Update_UpdatesFields()
var result = await repo.GetByIdAsync(occurrence.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().NotBeNull();
result!.Duration.Should().Be(TimeSpan.FromMinutes(45));
result.Extensions.Should().ContainKey("updated");
Assert.NotNull(result);
Assert.Equal(TimeSpan.FromMinutes(45), result!.Duration);
Assert.True(result.Extensions?.ContainsKey("updated") == true);
}

#endregion
Expand All @@ -294,9 +293,9 @@ public async Task ExceptionRepository_CreateAndGetById_ReturnsCreatedException()
exception.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().NotBeNull();
result!.RecurrenceId.Should().Be(recurrence.Id);
result.OriginalTimeUtc.Should().Be(exception.OriginalTimeUtc);
Assert.NotNull(result);
Assert.Equal(recurrence.Id, result!.RecurrenceId);
Assert.Equal(exception.OriginalTimeUtc, result.OriginalTimeUtc);
}

[SkippableFact]
Expand All @@ -319,8 +318,8 @@ public async Task ExceptionRepository_GetByRecurrenceIds_ReturnsExceptionsForRec
TestOrganization, TestResourcePath, [recurrence.Id]).ToListAsync();

// Assert
results.Should().HaveCount(1);
results[0].RecurrenceId.Should().Be(recurrence.Id);
Assert.Single(results);
Assert.Equal(recurrence.Id, results[0].RecurrenceId);
}

#endregion
Expand All @@ -346,11 +345,11 @@ public async Task OverrideRepository_CreateAndGetById_ReturnsCreatedOverride()
var result = await overrideRepo.GetByIdAsync(@override.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().NotBeNull();
result!.RecurrenceId.Should().Be(recurrence.Id);
result.OriginalTimeUtc.Should().Be(@override.OriginalTimeUtc);
result.StartTime.Should().Be(@override.StartTime);
result.OriginalDuration.Should().Be(@override.OriginalDuration);
Assert.NotNull(result);
Assert.Equal(recurrence.Id, result!.RecurrenceId);
Assert.Equal(@override.OriginalTimeUtc, result.OriginalTimeUtc);
Assert.Equal(@override.StartTime, result.StartTime);
Assert.Equal(@override.OriginalDuration, result.OriginalDuration);
}

[SkippableFact]
Expand All @@ -376,8 +375,8 @@ public async Task OverrideRepository_GetInRange_ReturnsOverridesWithOriginalTime
TestOrganization, TestResourcePath, [recurrence.Id], queryStart, queryEnd).ToListAsync();

// Assert
results.Should().HaveCount(1);
results[0].RecurrenceId.Should().Be(recurrence.Id);
Assert.Single(results);
Assert.Equal(recurrence.Id, results[0].RecurrenceId);
}

#endregion
Expand Down Expand Up @@ -409,15 +408,15 @@ public async Task RecurrenceRepository_Delete_CascadesDeleteToExceptionsAndOverr
// Assert - All related documents should be deleted
var recurrenceResult = await recurrenceRepo.GetByIdAsync(
recurrence.Id, TestOrganization, TestResourcePath);
recurrenceResult.Should().BeNull();
Assert.Null(recurrenceResult);

var exceptionResult = await exceptionRepo.GetByIdAsync(
exception.Id, TestOrganization, TestResourcePath);
exceptionResult.Should().BeNull();
Assert.Null(exceptionResult);

var overrideResult = await overrideRepo.GetByIdAsync(
@override.Id, TestOrganization, TestResourcePath);
overrideResult.Should().BeNull();
Assert.Null(overrideResult);
}

#endregion
Expand All @@ -437,13 +436,13 @@ public async Task IndexManager_EnsureIndexes_CreatesExpectedIndexes()
var indexList = await indexes.ToListAsync();

// Assert - should have at least 5 indexes (_id + 4 custom)
indexList.Should().HaveCountGreaterThanOrEqualTo(5);
Assert.True(indexList.Count >= 5);

var indexNames = indexList.Select(i => i["name"].AsString).ToList();
indexNames.Should().Contain("idx_recurring_query");
indexNames.Should().Contain("idx_original_time");
indexNames.Should().Contain("idx_override_time_range");
indexNames.Should().Contain("idx_cascade_delete");
Assert.Contains("idx_recurring_query", indexNames);
Assert.Contains("idx_original_time", indexNames);
Assert.Contains("idx_override_time_range", indexNames);
Assert.Contains("idx_cascade_delete", indexNames);
}

#endregion
Expand All @@ -470,12 +469,12 @@ public async Task RecurrenceRepository_Extensions_StoredAndRetrievedCorrectly()
var result = await repo.GetByIdAsync(recurrence.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().NotBeNull();
result!.Extensions.Should().NotBeNull();
result.Extensions.Should().HaveCount(3);
result.Extensions!["title"].Should().Be("Test Meeting");
result.Extensions["location"].Should().Be("Conference Room A");
result.Extensions["unicode"].Should().Be("Hello \u4e16\u754c");
Assert.NotNull(result);
Assert.NotNull(result!.Extensions);
Assert.Equal(3, result.Extensions!.Count);
Assert.Equal("Test Meeting", result.Extensions["title"]);
Assert.Equal("Conference Room A", result.Extensions["location"]);
Assert.Equal("Hello \u4e16\u754c", result.Extensions["unicode"]);
}

#endregion
Expand All @@ -497,8 +496,8 @@ public async Task Duration_StoredAsMillisecondsAndRetrievedCorrectly()
var result = await repo.GetByIdAsync(recurrence.Id, TestOrganization, TestResourcePath);

// Assert
result.Should().NotBeNull();
result!.Duration.Should().Be(TimeSpan.FromHours(2) + TimeSpan.FromMinutes(30) + TimeSpan.FromSeconds(15));
Assert.NotNull(result);
Assert.Equal(TimeSpan.FromHours(2) + TimeSpan.FromMinutes(30) + TimeSpan.FromSeconds(15), result!.Duration);
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
Expand Down
Loading