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
Expand Up @@ -203,7 +203,7 @@ public static class DocumentTypes
/// <summary>
/// Provides mapping methods between domain entities and MongoDB documents.
/// </summary>
public static class DocumentMapper
internal static class DocumentMapper
{
/// <summary>
/// Converts a <see cref="RecurringThingDocument"/> to a <see cref="Recurrence"/>.
Expand Down
6 changes: 5 additions & 1 deletion src/RecurringThings.MongoDB/RecurringThings.MongoDB.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="RecurringThings.MongoDB.Tests" />
</ItemGroup>

<PropertyGroup>
<PackageId>RecurringThings.MongoDB</PackageId>
<Authors>RecurringThings Contributors</Authors>
<Authors>Miguel Tremblay</Authors>
<Description>MongoDB persistence provider for RecurringThings library.</Description>
<RepositoryUrl>https://github.com/ChuckNovice/RecurringThings</RepositoryUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace RecurringThings.MongoDB.Repositories;
/// <summary>
/// MongoDB implementation of <see cref="IOccurrenceExceptionRepository"/>.
/// </summary>
public sealed class MongoOccurrenceExceptionRepository : IOccurrenceExceptionRepository
internal sealed class MongoOccurrenceExceptionRepository : IOccurrenceExceptionRepository
{
private readonly IMongoCollection<RecurringThingDocument> _collection;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace RecurringThings.MongoDB.Repositories;
/// <summary>
/// MongoDB implementation of <see cref="IOccurrenceOverrideRepository"/>.
/// </summary>
public sealed class MongoOccurrenceOverrideRepository : IOccurrenceOverrideRepository
internal sealed class MongoOccurrenceOverrideRepository : IOccurrenceOverrideRepository
{
private readonly IMongoCollection<RecurringThingDocument> _collection;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace RecurringThings.MongoDB.Repositories;
/// <summary>
/// MongoDB implementation of <see cref="IOccurrenceRepository"/>.
/// </summary>
public sealed class MongoOccurrenceRepository : IOccurrenceRepository
internal sealed class MongoOccurrenceRepository : IOccurrenceRepository
{
private readonly IMongoCollection<RecurringThingDocument> _collection;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace RecurringThings.MongoDB.Repositories;
/// <summary>
/// MongoDB implementation of <see cref="IRecurrenceRepository"/>.
/// </summary>
public sealed class MongoRecurrenceRepository : IRecurrenceRepository
internal sealed class MongoRecurrenceRepository : IRecurrenceRepository
{
private readonly IMongoCollection<RecurringThingDocument> _collection;

Expand Down
13 changes: 7 additions & 6 deletions src/RecurringThings/Domain/Occurrence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ namespace RecurringThings.Domain;
/// are stored directly in the database.
/// </para>
/// <para>
/// After creation, <see cref="StartTime"/>, <see cref="Duration"/>, and <see cref="Extensions"/> can be modified.
/// After creation, <see cref="StartTime"/>, <see cref="Duration"/>, <see cref="Extensions"/>,
/// <see cref="Type"/>, and <see cref="ResourcePath"/> can be modified.
/// When StartTime or Duration changes, <see cref="EndTime"/> is automatically recomputed.
/// </para>
/// </remarks>
public sealed class Occurrence
internal sealed class Occurrence
{
private DateTime _startTime;
private TimeSpan _duration;
Expand All @@ -35,22 +36,22 @@ public sealed class Occurrence
public required string Organization { get; init; }

/// <summary>
/// Gets the hierarchical resource scope.
/// Gets or sets the hierarchical resource scope.
/// </summary>
/// <remarks>
/// Used for organizing resources hierarchically (e.g., "user123/calendar", "store456").
/// Must be between 0 and 100 characters. Empty string is allowed.
/// </remarks>
public required string ResourcePath { get; init; }
public required string ResourcePath { get; set; }

/// <summary>
/// Gets the user-defined type of this occurrence.
/// Gets or sets the user-defined type of this occurrence.
/// </summary>
/// <remarks>
/// Used to differentiate between different kinds of occurrences (e.g., "appointment", "meeting").
/// Must be between 1 and 100 characters. Empty string is NOT allowed.
/// </remarks>
public required string Type { get; init; }
public required string Type { get; set; }

/// <summary>
/// Gets or sets the UTC timestamp when this occurrence starts.
Expand Down
2 changes: 1 addition & 1 deletion src/RecurringThings/Domain/OccurrenceException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace RecurringThings.Domain;
/// Once an exception is created, the occurrence cannot be restored through the API.
/// </para>
/// </remarks>
public sealed class OccurrenceException
internal sealed class OccurrenceException
{
/// <summary>
/// Gets the unique identifier for this exception.
Expand Down
2 changes: 1 addition & 1 deletion src/RecurringThings/Domain/OccurrenceOverride.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace RecurringThings.Domain;
/// When StartTime or Duration changes, <see cref="EndTime"/> is automatically recomputed.
/// </para>
/// </remarks>
public sealed class OccurrenceOverride
internal sealed class OccurrenceOverride
{
private DateTime _startTime;
private TimeSpan _duration;
Expand Down
2 changes: 1 addition & 1 deletion src/RecurringThings/Domain/Recurrence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace RecurringThings.Domain;
/// To change other fields, delete and recreate the recurrence.
/// </para>
/// </remarks>
public sealed class Recurrence
internal sealed class Recurrence
{
/// <summary>
/// Gets the unique identifier for this recurrence.
Expand Down
13 changes: 6 additions & 7 deletions src/RecurringThings/Engine/IRecurrenceEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace RecurringThings.Engine;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using RecurringThings.Domain;
using RecurringThings.Models;
using Transactional.Abstractions;

Expand Down Expand Up @@ -118,7 +117,7 @@ IAsyncEnumerable<CalendarEntry> GetRecurrencesAsync(
/// <param name="extensions">Optional user-defined key-value metadata.</param>
/// <param name="transactionContext">Optional transaction context.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The created <see cref="Recurrence"/>.</returns>
/// <returns>A <see cref="CalendarEntry"/> representing the created recurrence with <see cref="CalendarEntryType.Recurrence"/>.</returns>
/// <exception cref="ArgumentException">
/// Thrown when validation fails (invalid RRule, missing UNTIL, COUNT used, field length violations, etc.).
/// </exception>
Expand All @@ -131,7 +130,7 @@ IAsyncEnumerable<CalendarEntry> GetRecurrencesAsync(
/// };
/// pattern.ByDay.Add(new WeekDay(DayOfWeek.Monday));
///
/// var recurrence = await engine.CreateRecurrenceAsync(
/// var entry = await engine.CreateRecurrenceAsync(
/// organization: "tenant1",
/// resourcePath: "user123/calendar",
/// type: "appointment",
Expand All @@ -141,7 +140,7 @@ IAsyncEnumerable<CalendarEntry> GetRecurrencesAsync(
/// timeZone: "America/New_York");
/// </code>
/// </example>
Task<Recurrence> CreateRecurrenceAsync(
Task<CalendarEntry> CreateRecurrenceAsync(
string organization,
string resourcePath,
string type,
Expand All @@ -165,7 +164,7 @@ Task<Recurrence> CreateRecurrenceAsync(
/// <param name="extensions">Optional user-defined key-value metadata.</param>
/// <param name="transactionContext">Optional transaction context.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The created <see cref="Occurrence"/>.</returns>
/// <returns>A <see cref="CalendarEntry"/> representing the created occurrence with <see cref="CalendarEntryType.Standalone"/>.</returns>
/// <remarks>
/// EndTime is automatically computed as StartTime + Duration.
/// </remarks>
Expand All @@ -174,7 +173,7 @@ Task<Recurrence> CreateRecurrenceAsync(
/// </exception>
/// <example>
/// <code>
/// var occurrence = await engine.CreateOccurrenceAsync(
/// var entry = await engine.CreateOccurrenceAsync(
/// organization: "tenant1",
/// resourcePath: "user123/calendar",
/// type: "meeting",
Expand All @@ -183,7 +182,7 @@ Task<Recurrence> CreateRecurrenceAsync(
/// timeZone: "America/New_York");
/// </code>
/// </example>
Task<Occurrence> CreateOccurrenceAsync(
Task<CalendarEntry> CreateOccurrenceAsync(
string organization,
string resourcePath,
string type,
Expand Down
49 changes: 27 additions & 22 deletions src/RecurringThings/Engine/RecurrenceEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ namespace RecurringThings.Engine;
/// <item>Streams results as <see cref="CalendarEntry"/> objects</item>
/// </list>
/// </remarks>
public sealed class RecurrenceEngine : IRecurrenceEngine
internal sealed class RecurrenceEngine : IRecurrenceEngine
{
private readonly IRecurrenceRepository _recurrenceRepository;
private readonly IOccurrenceRepository _occurrenceRepository;
Expand Down Expand Up @@ -211,7 +211,7 @@ public async IAsyncEnumerable<CalendarEntry> GetOccurrencesAsync(
}

/// <inheritdoc/>
public async Task<Recurrence> CreateRecurrenceAsync(
public async Task<CalendarEntry> CreateRecurrenceAsync(
string organization,
string resourcePath,
string type,
Expand Down Expand Up @@ -248,14 +248,17 @@ public async Task<Recurrence> CreateRecurrenceAsync(
};

// Persist via repository
return await _recurrenceRepository.CreateAsync(
var created = await _recurrenceRepository.CreateAsync(
recurrence,
transactionContext,
cancellationToken).ConfigureAwait(false);

// Convert to CalendarEntry
return CreateRecurrenceEntry(created);
}

/// <inheritdoc/>
public async Task<Domain.Occurrence> CreateOccurrenceAsync(
public async Task<CalendarEntry> CreateOccurrenceAsync(
string organization,
string resourcePath,
string type,
Expand Down Expand Up @@ -287,10 +290,13 @@ public async Task<Recurrence> CreateRecurrenceAsync(
occurrence.Initialize(startTimeUtc, duration);

// Persist via repository
return await _occurrenceRepository.CreateAsync(
var created = await _occurrenceRepository.CreateAsync(
occurrence,
transactionContext,
cancellationToken).ConfigureAwait(false);

// Convert to CalendarEntry
return CreateStandaloneEntry(created);
}

/// <summary>
Expand Down Expand Up @@ -539,17 +545,20 @@ public async Task<CalendarEntry> UpdateOccurrenceAsync(
}

/// <summary>
/// Updates a standalone occurrence. StartTime, Duration, and Extensions are mutable.
/// Updates a standalone occurrence. StartTime, Duration, Extensions, Type, and ResourcePath are mutable.
/// </summary>
private async Task<CalendarEntry> UpdateStandaloneOccurrenceAsync(
CalendarEntry entry,
ITransactionContext? transactionContext,
CancellationToken cancellationToken)
{
// Use OriginalResourcePath if ResourcePath was modified
var lookupResourcePath = entry.OriginalResourcePath ?? entry.ResourcePath;

var occurrence = await _occurrenceRepository.GetByIdAsync(
entry.OccurrenceId!.Value,
entry.Organization,
entry.ResourcePath,
lookupResourcePath,
transactionContext,
cancellationToken).ConfigureAwait(false) ?? throw new KeyNotFoundException(
$"Occurrence with ID '{entry.OccurrenceId}' not found.");
Expand All @@ -561,6 +570,8 @@ private async Task<CalendarEntry> UpdateStandaloneOccurrenceAsync(
occurrence.StartTime = entry.StartTime;
occurrence.Duration = entry.Duration;
occurrence.Extensions = entry.Extensions;
occurrence.Type = entry.Type;
occurrence.ResourcePath = entry.ResourcePath;

var updated = await _occurrenceRepository.UpdateAsync(
occurrence,
Expand All @@ -581,18 +592,6 @@ private static void ValidateImmutableOccurrenceFields(CalendarEntry entry, Domai
"Cannot modify Organization. This field is immutable after creation.");
}

if (entry.ResourcePath != existing.ResourcePath)
{
throw new InvalidOperationException(
"Cannot modify ResourcePath. This field is immutable after creation.");
}

if (entry.Type != existing.Type)
{
throw new InvalidOperationException(
"Cannot modify Type. This field is immutable after creation.");
}

if (entry.TimeZone != existing.TimeZone)
{
throw new InvalidOperationException(
Expand All @@ -611,10 +610,13 @@ private async Task<CalendarEntry> CreateOverrideForVirtualizedOccurrenceAsync(
var recurrenceId = entry.RecurrenceId
?? throw new InvalidOperationException("Cannot create override: RecurrenceId is missing.");

// Use OriginalResourcePath if ResourcePath was modified (though this should fail validation)
var lookupResourcePath = entry.OriginalResourcePath ?? entry.ResourcePath;

var recurrence = await _recurrenceRepository.GetByIdAsync(
recurrenceId,
entry.Organization,
entry.ResourcePath,
lookupResourcePath,
transactionContext,
cancellationToken).ConfigureAwait(false) ?? throw new KeyNotFoundException(
$"Parent recurrence with ID '{recurrenceId}' not found.");
Expand Down Expand Up @@ -662,10 +664,13 @@ private async Task<CalendarEntry> UpdateVirtualizedOccurrenceWithOverrideAsync(
ITransactionContext? transactionContext,
CancellationToken cancellationToken)
{
// Use OriginalResourcePath if ResourcePath was modified (though this should fail validation)
var lookupResourcePath = entry.OriginalResourcePath ?? entry.ResourcePath;

var @override = await _overrideRepository.GetByIdAsync(
entry.OverrideId!.Value,
entry.Organization,
entry.ResourcePath,
lookupResourcePath,
transactionContext,
cancellationToken).ConfigureAwait(false) ?? throw new KeyNotFoundException(
$"Override with ID '{entry.OverrideId}' not found.");
Expand All @@ -674,7 +679,7 @@ private async Task<CalendarEntry> UpdateVirtualizedOccurrenceWithOverrideAsync(
var recurrence = await _recurrenceRepository.GetByIdAsync(
@override.RecurrenceId,
entry.Organization,
entry.ResourcePath,
lookupResourcePath,
transactionContext,
cancellationToken).ConfigureAwait(false) ?? throw new KeyNotFoundException(
$"Parent recurrence with ID '{@override.RecurrenceId}' not found.");
Expand Down
Loading