mirror of https://github.com/Squidex/squidex.git
7 changed files with 290 additions and 15 deletions
@ -0,0 +1,86 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentVersionLoader.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Contents |
||||
|
{ |
||||
|
public sealed class ContentVersionLoader : IContentVersionLoader |
||||
|
{ |
||||
|
private readonly IStreamNameResolver nameResolver; |
||||
|
private readonly IEventStore eventStore; |
||||
|
private readonly EventDataFormatter formatter; |
||||
|
|
||||
|
public ContentVersionLoader(IEventStore eventStore, IStreamNameResolver nameResolver, EventDataFormatter formatter) |
||||
|
{ |
||||
|
Guard.NotNull(formatter, nameof(formatter)); |
||||
|
Guard.NotNull(eventStore, nameof(eventStore)); |
||||
|
Guard.NotNull(nameResolver, nameof(nameResolver)); |
||||
|
|
||||
|
this.formatter = formatter; |
||||
|
this.eventStore = eventStore; |
||||
|
this.nameResolver = nameResolver; |
||||
|
} |
||||
|
|
||||
|
public async Task<NamedContentData> LoadAsync(Guid appId, Guid id, long version) |
||||
|
{ |
||||
|
var streamName = nameResolver.GetStreamName(typeof(ContentDomainObject), id); |
||||
|
|
||||
|
var events = await eventStore.GetEventsAsync(streamName); |
||||
|
|
||||
|
if (events.Count == 0 || events.Count < version - 1) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject)); |
||||
|
} |
||||
|
|
||||
|
NamedContentData contentData = null; |
||||
|
|
||||
|
foreach (var storedEvent in events.Where(x => x.EventStreamNumber <= version)) |
||||
|
{ |
||||
|
var envelope = ParseKnownEvent(storedEvent); |
||||
|
|
||||
|
if (envelope != null) |
||||
|
{ |
||||
|
if (envelope.Payload is ContentCreated contentCreated) |
||||
|
{ |
||||
|
if (contentCreated.AppId.Id != appId) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject)); |
||||
|
} |
||||
|
|
||||
|
contentData = contentCreated.Data; |
||||
|
} |
||||
|
else if (envelope.Payload is ContentUpdated contentUpdated) |
||||
|
{ |
||||
|
contentData = contentUpdated.Data; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return contentData; |
||||
|
} |
||||
|
|
||||
|
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return formatter.Parse(storedEvent.Data); |
||||
|
} |
||||
|
catch (TypeNameNotFoundException) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IContentVersionLoader.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Contents |
||||
|
{ |
||||
|
public interface IContentVersionLoader |
||||
|
{ |
||||
|
Task<NamedContentData> LoadAsync(Guid appId, Guid id, long version); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,146 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentVersionLoaderTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Contents |
||||
|
{ |
||||
|
public class ContentVersionLoaderTests |
||||
|
{ |
||||
|
private readonly IEventStore eventStore = A.Fake<IEventStore>(); |
||||
|
private readonly IStreamNameResolver nameResolver = A.Fake<IStreamNameResolver>(); |
||||
|
private readonly EventDataFormatter formatter = A.Fake<EventDataFormatter>(); |
||||
|
private readonly Guid id = Guid.NewGuid(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
private readonly string streamName = Guid.NewGuid().ToString(); |
||||
|
private readonly ContentVersionLoader sut; |
||||
|
|
||||
|
public ContentVersionLoaderTests() |
||||
|
{ |
||||
|
A.CallTo(() => nameResolver.GetStreamName(typeof(ContentDomainObject), id)) |
||||
|
.Returns(streamName); |
||||
|
|
||||
|
sut = new ContentVersionLoader(eventStore, nameResolver, formatter); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_when_event_store_returns_no_events() |
||||
|
{ |
||||
|
A.CallTo(() => eventStore.GetEventsAsync(streamName)) |
||||
|
.Returns(new List<StoredEvent>()); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(appId, id, -1)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_when_version_not_found() |
||||
|
{ |
||||
|
var events = new List<StoredEvent> |
||||
|
{ |
||||
|
new StoredEvent("0", 0, new EventData()), |
||||
|
new StoredEvent("1", 1, new EventData()), |
||||
|
new StoredEvent("2", 2, new EventData()) |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => eventStore.GetEventsAsync(streamName)) |
||||
|
.Returns(new List<StoredEvent>()); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(appId, id, 3)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_when_content_is_from_another_event() |
||||
|
{ |
||||
|
var eventData1 = new EventData(); |
||||
|
|
||||
|
var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId<Guid>(Guid.NewGuid(), "my-app") }; |
||||
|
|
||||
|
var events = new List<StoredEvent> |
||||
|
{ |
||||
|
new StoredEvent("0", 0, eventData1) |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => eventStore.GetEventsAsync(streamName)) |
||||
|
.Returns(events); |
||||
|
|
||||
|
A.CallTo(() => formatter.Parse(eventData1)) |
||||
|
.Returns(new Envelope<IEvent>(event1)); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(appId, id, 0)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_load_content_from_created_event() |
||||
|
{ |
||||
|
var eventData1 = new EventData(); |
||||
|
var eventData2 = new EventData(); |
||||
|
|
||||
|
var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId<Guid>(appId, "my-app") }; |
||||
|
var event2 = new ContentPublished(); |
||||
|
|
||||
|
var events = new List<StoredEvent> |
||||
|
{ |
||||
|
new StoredEvent("0", 0, eventData1), |
||||
|
new StoredEvent("1", 1, eventData2) |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => eventStore.GetEventsAsync(streamName)) |
||||
|
.Returns(events); |
||||
|
|
||||
|
A.CallTo(() => formatter.Parse(eventData1)) |
||||
|
.Returns(new Envelope<IEvent>(event1)); |
||||
|
A.CallTo(() => formatter.Parse(eventData2)) |
||||
|
.Returns(new Envelope<IEvent>(event2)); |
||||
|
|
||||
|
var data = await sut.LoadAsync(appId, id, 3); |
||||
|
|
||||
|
Assert.Same(event1.Data, data); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_load_content_from_correct_version() |
||||
|
{ |
||||
|
var eventData1 = new EventData(); |
||||
|
var eventData2 = new EventData(); |
||||
|
var eventData3 = new EventData(); |
||||
|
|
||||
|
var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId<Guid>(appId, "my-app") }; |
||||
|
var event2 = new ContentUpdated { Data = new NamedContentData() }; |
||||
|
var event3 = new ContentUpdated { Data = new NamedContentData() }; |
||||
|
|
||||
|
var events = new List<StoredEvent> |
||||
|
{ |
||||
|
new StoredEvent("0", 0, eventData1), |
||||
|
new StoredEvent("1", 1, eventData2), |
||||
|
new StoredEvent("2", 2, eventData3) |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => eventStore.GetEventsAsync(streamName)) |
||||
|
.Returns(events); |
||||
|
|
||||
|
A.CallTo(() => formatter.Parse(eventData1)) |
||||
|
.Returns(new Envelope<IEvent>(event1)); |
||||
|
A.CallTo(() => formatter.Parse(eventData2)) |
||||
|
.Returns(new Envelope<IEvent>(event2)); |
||||
|
A.CallTo(() => formatter.Parse(eventData3)) |
||||
|
.Returns(new Envelope<IEvent>(event3)); |
||||
|
|
||||
|
var data = await sut.LoadAsync(appId, id, 1); |
||||
|
|
||||
|
Assert.Equal(event2.Data, data); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue