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