Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 23 additions & 5 deletions src/SIL.Harmony.Tests/DataModelSimpleChanges.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,28 +190,46 @@ public async Task CanDeleteAnEntry()
}

[Fact]
public async Task CanModifyAnEntryAfterDelete()
public async Task ApplyChange_ModifiesADeletedEntry()
{
await WriteNextChange(SetWord(_entity1Id, "test-value"));
var deleteCommit = await WriteNextChange(new DeleteChange<Word>(_entity1Id));
await WriteNextChange(SetWord(_entity1Id, "after-delete"));
var newNoteChange = new SetWordNoteChange(_entity1Id, "note-after-delete");
newNoteChange.SupportsApplyChange().Should().BeTrue();
newNoteChange.SupportsNewEntity().Should().BeFalse(); // otherwise it will override the delete
await WriteNextChange(newNoteChange);
var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
var word = snapshot.Entity.Is<Word>();
word.Text.Should().Be("after-delete");
word.Text.Should().Be("test-value");
word.Note.Should().Be("note-after-delete");
word.DeletedAt.Should().Be(deleteCommit.DateTime);
}

[Fact]
public async Task NewEntity_UndeletesAnEntry()
{
await WriteNextChange(SetWord(_entity1Id, "test-value"));
await WriteNextChange(new DeleteChange<Word>(_entity1Id));
var recreateChange = SetWord(_entity1Id, "after-undo-delete");
recreateChange.SupportsNewEntity().Should().BeTrue();
await WriteNextChange(recreateChange);
var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync();
var word = snapshot.Entity.Is<Word>();
word.Text.Should().Be("after-undo-delete");
word.DeletedAt.Should().Be(null);
}

[Fact]
public async Task ChangesToSnapshotsAreNotSaved()
{
await WriteNextChange(SetWord(_entity1Id, "test-value"));
var word = await DataModel.GetLatest<Word>(_entity1Id);
word!.Text.Should().Be("test-value");
word.Note.Should().BeNull();

//change made outside the model, should not be saved when writing the next change
word.Note = "a note";

var commit = await WriteNextChange(SetWord(_entity1Id, "after-change"));
var objectSnapshot = commit.Snapshots.Should().ContainSingle().Subject;
objectSnapshot.Entity.Is<Word>().Text.Should().Be("after-change");
Expand Down
8 changes: 5 additions & 3 deletions src/SIL.Harmony.Tests/SnapshotTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using SIL.Harmony.Sample.Models;
using SIL.Harmony.Sample.Changes;
using SIL.Harmony.Changes;
using Microsoft.EntityFrameworkCore;

namespace SIL.Harmony.Tests;
Expand All @@ -19,7 +21,7 @@ public async Task MultipleChangesPreservesRootSnapshot()
{
var entityId = Guid.NewGuid();
var commits = new List<Commit>();
for (int i = 0; i < 4; i++)
for (var i = 0; i < 4; i++)
{
commits.Add(await WriteChange(_localClientId,
new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero).AddHours(i),
Expand All @@ -39,7 +41,7 @@ public async Task MultipleChangesPreservesSomeIntermediateSnapshots()
{
var entityId = Guid.NewGuid();
var commits = new List<Commit>();
for (int i = 0; i < 6; i++)
for (var i = 0; i < 6; i++)
{
commits.Add(await WriteChange(_localClientId,
new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero).AddHours(i),
Expand Down Expand Up @@ -135,7 +137,7 @@ await WriteNextChange(
var tagCreation = await WriteNextChange(TagWord(wordId, tagId));
await WriteChangeBefore(tagCreation, TagWord(wordId, tagId));

var word = await DataModel.QueryLatest<Word>(q=> q.Include(w => w.Tags)
var word = await DataModel.QueryLatest<Word>(q => q.Include(w => w.Tags)
.Where(w => w.Id == wordId)).FirstOrDefaultAsync();
word.Should().NotBeNull();
word.Tags.Should().BeEquivalentTo([new Tag { Id = tagId, Text = "tag-1" }]);
Expand Down
14 changes: 13 additions & 1 deletion src/SIL.Harmony/Changes/Change.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public interface IChange

ValueTask ApplyChange(IObjectBase entity, IChangeContext context);
ValueTask<IObjectBase> NewEntity(Commit commit, IChangeContext context);
bool SupportsApplyChange();
bool SupportsNewEntity();
}

/// <summary>
Expand Down Expand Up @@ -44,7 +46,7 @@ async ValueTask<IObjectBase> IChange.NewEntity(Commit commit, IChangeContext con

public async ValueTask ApplyChange(IObjectBase entity, IChangeContext context)
{
if (this is CreateChange<T>)
if (!SupportsApplyChange())
return; // skip attempting to apply changes on CreateChange as it does not support apply changes
if (entity.DbObject is T entityT)
{
Expand All @@ -56,6 +58,16 @@ public async ValueTask ApplyChange(IObjectBase entity, IChangeContext context)
}
}

public virtual bool SupportsApplyChange()
{
return this is not CreateChange<T>;
}

public virtual bool SupportsNewEntity()
{
return this is not EditChange<T>;
}

[JsonIgnore]
public Type EntityType => typeof(T);
}
37 changes: 24 additions & 13 deletions src/SIL.Harmony/SnapshotWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,26 +81,37 @@ private async ValueTask ApplyCommitChanges(IEnumerable<Commit> commits, bool upd
IObjectBase entity;
var prevSnapshot = await GetSnapshot(commitChange.EntityId);
var changeContext = new ChangeContext(commit, commitIndex, intermediateSnapshots, this, _crdtConfig);
bool wasDeleted;
if (prevSnapshot is not null)

if (prevSnapshot is null)
{
entity = prevSnapshot.Entity.Copy();
wasDeleted = entity.DeletedAt.HasValue;
// create brand new entity
entity = await commitChange.Change.NewEntity(commit, changeContext);
}
else
else if (prevSnapshot.EntityIsDeleted && commitChange.Change.SupportsNewEntity())
{
// revive deleted entity
entity = await commitChange.Change.NewEntity(commit, changeContext);
wasDeleted = false;
}

await commitChange.Change.ApplyChange(entity, changeContext);

var deletedByChange = !wasDeleted && entity.DeletedAt.HasValue;
if (deletedByChange)
else if (commitChange.Change.SupportsApplyChange())
{
await MarkDeleted(entity.Id, changeContext);
// update existing entity
entity = prevSnapshot.Entity.Copy();
var wasDeleted = prevSnapshot.EntityIsDeleted;
await commitChange.Change.ApplyChange(entity, changeContext);
var deletedByChange = !wasDeleted && entity.DeletedAt.HasValue;
if (deletedByChange)
{
await MarkDeleted(entity.Id, changeContext);
}
}

else
{
// entity already exists (and is not deleted)
// and change does not support updating existing entities
// so do nothing
continue;
}

await GenerateSnapshotForEntity(entity, prevSnapshot, changeContext);
}
_newIntermediateSnapshots.AddRange(intermediateSnapshots.Values);
Expand Down
Loading