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
1,439 changes: 0 additions & 1,439 deletions PRD.md

This file was deleted.

41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,38 @@ public class CalendarService(IRecurrenceEngine engine)
## Quick Example

```csharp
using Ical.Net.DataTypes;
using RecurringThings;

// Build a recurrence pattern using Ical.Net
var pattern = new RecurrencePattern
{
Frequency = FrequencyType.Weekly,
Until = new CalDateTime(new DateTime(2025, 12, 31, 23, 59, 59, DateTimeKind.Local).ToUniversalTime())
};
pattern.ByDay.Add(new WeekDay(DayOfWeek.Monday));

// Create a weekly recurring meeting
// Note: RecurrenceEndTime is automatically extracted from the RRule UNTIL clause
await engine.CreateRecurrenceAsync(new RecurrenceCreate
await engine.CreateRecurrenceAsync(
organization: "tenant1",
resourcePath: "user123/calendar",
type: "meeting",
startTime: DateTime.Now,
duration: TimeSpan.FromHours(1),
rrule: pattern.ToString(),
timeZone: "America/New_York");

// Query occurrences in a date range
await foreach (var entry in engine.GetOccurrencesAsync("tenant1", "user123/calendar", start, end, null))
{
Organization = "tenant1",
ResourcePath = "user123/calendar",
Type = "meeting",
StartTime = new DateTime(2025, 1, 6, 14, 0, 0, DateTimeKind.Utc), // Or DateTime.Now for local time
Duration = TimeSpan.FromHours(1),
RRule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=20251231T235959Z",
TimeZone = "America/New_York"
});

// Query entries in a date range
await foreach (var entry in engine.GetAsync("tenant1", "user123/calendar", start, end, null))
Console.WriteLine($"{entry.Type}: {entry.StartTime} ({entry.EntryType})");
}

// Query recurrence patterns in a date range
await foreach (var entry in engine.GetRecurrencesAsync("tenant1", "user123/calendar", start, end, null))
{
Console.WriteLine($"{entry.Type}: {entry.StartTime}");
Console.WriteLine($"Recurrence: {entry.RecurrenceDetails?.RRule}");
}
```

Expand Down
Binary file added assets/logo_blue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo_gray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo_green.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo_orange.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo_pink.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo_purple.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo_red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo_yellow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 0 additions & 133 deletions src/RecurringThings.MongoDB/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,132 +26,6 @@ services.AddRecurringThings(builder =>
}));
```

## Indexes

The provider automatically creates indexes for efficient querying:

- `organization + resourcePath + documentType`
- `organization + resourcePath + startTime + recurrenceEndTime` (for recurrences)
- `organization + resourcePath + startTime + endTime` (for occurrences)
- `recurrenceId` (for exceptions and overrides)

## Transactions

Use `IMongoTransactionManager` from the [Transactional](https://github.com/ChuckNovice/transactional) library:

```csharp
using Transactional.MongoDB;

public class CalendarService(IRecurrenceEngine engine, IMongoTransactionManager transactionManager)
{
public async Task CreateMultipleEntriesAsync()
{
await using var context = await transactionManager.BeginTransactionAsync();

try
{
await engine.CreateRecurrenceAsync(request1, context);
await engine.CreateOccurrenceAsync(request2, context);
await context.CommitAsync();
}
catch
{
await context.RollbackAsync();
throw;
}
}
}
```

**Note**: MongoDB transactions require a replica set. Standalone MongoDB instances do not support transactions.

## Usage Examples

### Basic Setup

```csharp
public class CalendarService(IRecurrenceEngine engine)
{
public async Task CreateWeeklyMeetingAsync()
{
// RecurrenceEndTime is automatically extracted from the RRule UNTIL clause
var recurrence = await engine.CreateRecurrenceAsync(new RecurrenceCreate
{
Organization = "tenant1",
ResourcePath = "user123/calendar",
Type = "meeting",
StartTime = new DateTime(2025, 1, 6, 14, 0, 0, DateTimeKind.Utc), // Or DateTime.Now for local time
Duration = TimeSpan.FromHours(1),
RRule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=20251231T235959Z",
TimeZone = "America/New_York",
Extensions = new Dictionary<string, string>
{
["title"] = "Weekly Team Standup",
["location"] = "Conference Room A"
}
});
}

public async Task GetJanuaryEntriesAsync()
{
var start = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var end = new DateTime(2025, 1, 31, 23, 59, 59, DateTimeKind.Utc);

await foreach (var entry in engine.GetAsync("tenant1", "user123/calendar", start, end, null))
{
Console.WriteLine($"{entry.Type}: {entry.StartTime} - {entry.EndTime}");
}
}
}
```

### Querying with Type Filter

```csharp
// Get only appointments and meetings
await foreach (var entry in engine.GetAsync(
"tenant1", "user123/calendar", start, end,
types: ["appointment", "meeting"]))
{
// Process filtered entries
}
```

### Updating Entries

```csharp
// Update a standalone occurrence
var entries = await engine.GetAsync(org, path, start, end, null).ToListAsync();
var entry = entries.First(e => e.OccurrenceId.HasValue);

entry.StartTime = entry.StartTime.AddHours(1);
entry.Duration = TimeSpan.FromMinutes(45);
var updated = await engine.UpdateAsync(entry);
// EndTime is automatically recomputed

// Update a virtualized occurrence (creates an override)
var virtualizedEntry = entries.First(e => e.RecurrenceOccurrenceDetails != null);
virtualizedEntry.Duration = TimeSpan.FromMinutes(45);
var overridden = await engine.UpdateAsync(virtualizedEntry);
// Original values preserved in RecurrenceOccurrenceDetails.Original
```

### Deleting Entries

```csharp
// Delete entire recurrence series (cascade deletes exceptions/overrides)
await engine.DeleteAsync(recurrenceEntry);

// Delete a virtualized occurrence (creates an exception)
await engine.DeleteAsync(virtualizedEntry);

// Restore an overridden occurrence to original state
if (entry.OverrideId.HasValue)
{
await engine.RestoreAsync(entry);
}
```

## Integration Tests

Set the environment variable before running integration tests:
Expand All @@ -160,10 +34,3 @@ Set the environment variable before running integration tests:
export MONGODB_CONNECTION_STRING="mongodb://localhost:27017"
dotnet test --filter 'Category=Integration'
```

## Limitations

- MongoDB transactions require replica set (not available on standalone instances)
- DateTime values can be UTC or Local (`DateTimeKind.Unspecified` is not allowed)
- RRule must use UNTIL (COUNT is not supported)
- UNTIL must have UTC suffix (Z)
2 changes: 2 additions & 0 deletions src/RecurringThings.MongoDB/RecurringThings.MongoDB.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
<RepositoryUrl>https://github.com/ChuckNovice/RecurringThings</RepositoryUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>logo_green.png</PackageIcon>
</PropertyGroup>

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

<ItemGroup>
Expand Down
130 changes: 0 additions & 130 deletions src/RecurringThings.PostgreSQL/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,129 +39,6 @@ builder.UsePostgreSql(options =>
});
```

## Indexes

The provider creates indexes for efficient querying:

- `(organization, resource_path, start_time, recurrence_end_time)` on recurrences
- `(organization, resource_path, start_time, end_time)` on occurrences
- `(recurrence_id)` on exceptions and overrides

## Transactions

Use `IPostgresTransactionManager` from the [Transactional](https://github.com/ChuckNovice/transactional) library:

```csharp
using Transactional.PostgreSQL;

public class CalendarService(IRecurrenceEngine engine, IPostgresTransactionManager transactionManager)
{
public async Task CreateMultipleEntriesAsync()
{
await using var context = await transactionManager.BeginTransactionAsync();

try
{
await engine.CreateRecurrenceAsync(request1, context);
await engine.CreateOccurrenceAsync(request2, context);
await context.CommitAsync();
}
catch
{
await context.RollbackAsync();
throw;
}
}
}
```

## Usage Examples

### Basic Setup

```csharp
public class CalendarService(IRecurrenceEngine engine)
{
public async Task CreateWeeklyMeetingAsync()
{
// RecurrenceEndTime is automatically extracted from the RRule UNTIL clause
var recurrence = await engine.CreateRecurrenceAsync(new RecurrenceCreate
{
Organization = "tenant1",
ResourcePath = "user123/calendar",
Type = "meeting",
StartTime = new DateTime(2025, 1, 6, 14, 0, 0, DateTimeKind.Utc), // Or DateTime.Now for local time
Duration = TimeSpan.FromHours(1),
RRule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=20251231T235959Z",
TimeZone = "America/New_York",
Extensions = new Dictionary<string, string>
{
["title"] = "Weekly Team Standup",
["location"] = "Conference Room A"
}
});
}

public async Task GetJanuaryEntriesAsync()
{
var start = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var end = new DateTime(2025, 1, 31, 23, 59, 59, DateTimeKind.Utc);

await foreach (var entry in engine.GetAsync("tenant1", "user123/calendar", start, end, null))
{
Console.WriteLine($"{entry.Type}: {entry.StartTime} - {entry.EndTime}");
}
}
}
```

### Querying with Type Filter

```csharp
// Get only appointments and meetings
await foreach (var entry in engine.GetAsync(
"tenant1", "user123/calendar", start, end,
types: ["appointment", "meeting"]))
{
// Process filtered entries
}
```

### Updating Entries

```csharp
// Update a standalone occurrence
var entries = await engine.GetAsync(org, path, start, end, null).ToListAsync();
var entry = entries.First(e => e.OccurrenceId.HasValue);

entry.StartTime = entry.StartTime.AddHours(1);
entry.Duration = TimeSpan.FromMinutes(45);
var updated = await engine.UpdateAsync(entry);
// EndTime is automatically recomputed

// Update a virtualized occurrence (creates an override)
var virtualizedEntry = entries.First(e => e.RecurrenceOccurrenceDetails != null);
virtualizedEntry.Duration = TimeSpan.FromMinutes(45);
var overridden = await engine.UpdateAsync(virtualizedEntry);
// Original values preserved in RecurrenceOccurrenceDetails.Original
```

### Deleting Entries

```csharp
// Delete entire recurrence series (cascade deletes exceptions/overrides)
await engine.DeleteAsync(recurrenceEntry);

// Delete a virtualized occurrence (creates an exception)
await engine.DeleteAsync(virtualizedEntry);

// Restore an overridden occurrence to original state
if (entry.OverrideId.HasValue)
{
await engine.RestoreAsync(entry);
}
```

## Integration Tests

Set the environment variable before running integration tests:
Expand All @@ -170,10 +47,3 @@ Set the environment variable before running integration tests:
export POSTGRES_CONNECTION_STRING="Host=localhost;Database=test;Username=user;Password=pass"
dotnet test --filter 'Category=Integration'
```

## Limitations

- Database must exist before running the application (schema is auto-created)
- DateTime values can be UTC or Local (`DateTimeKind.Unspecified` is not allowed)
- RRule must use UNTIL (COUNT is not supported)
- UNTIL must have UTC suffix (Z)
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
<RepositoryUrl>https://github.com/ChuckNovice/RecurringThings</RepositoryUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>logo_blue.png</PackageIcon>
</PropertyGroup>

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

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public static IServiceCollection AddRecurringThings(
builder.Validate();

// Register FluentValidation validators from the assembly (including internal validators)
services.AddValidatorsFromAssemblyContaining<RecurrenceCreateValidator>(
services.AddValidatorsFromAssemblyContaining<OccurrenceExceptionValidator>(
ServiceLifetime.Scoped,
includeInternalTypes: true);

Expand Down
Loading