Browse Source

API for loading content.

pull/107/head
Sebastian Stehle 9 years ago
parent
commit
40bb95ca36
  1. 86
      src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs
  2. 19
      src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs
  3. 4
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  4. 4
      src/Squidex/Config/Domain/WriteModule.cs
  5. 22
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  6. 146
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs
  7. 24
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs

86
src/Squidex.Domain.Apps.Write/Contents/ContentVersionLoader.cs

@ -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;
}
}
}
}

19
src/Squidex.Domain.Apps.Write/Contents/IContentVersionLoader.cs

@ -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);
}
}

4
src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs

@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
foreach (var storedEvent in events)
{
var envelope = ParseKnownCommand(storedEvent);
var envelope = ParseKnownEvent(storedEvent);
if (envelope != null)
{
@ -79,7 +79,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
}
}
private Envelope<IEvent> ParseKnownCommand(StoredEvent storedEvent)
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)
{
try
{

4
src/Squidex/Config/Domain/WriteModule.cs

@ -55,6 +55,10 @@ namespace Squidex.Config.Domain
.As<IScriptEngine>()
.SingleInstance();
builder.RegisterType<ContentVersionLoader>()
.As<IContentVersionLoader>()
.SingleInstance();
builder.RegisterType<FieldRegistry>()
.AsSelf()
.SingleInstance();

22
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -31,12 +31,17 @@ namespace Squidex.Controllers.ContentApi
public sealed class ContentsController : ControllerBase
{
private readonly IContentQueryService contentQuery;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IGraphQLService graphQl;
public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IGraphQLService graphQl)
public ContentsController(ICommandBus commandBus,
IContentQueryService contentQuery,
IContentVersionLoader contentVersionLoader,
IGraphQLService graphQl)
: base(commandBus)
{
this.contentQuery = contentQuery;
this.contentVersionLoader = contentVersionLoader;
this.graphQl = graphQl;
}
@ -124,6 +129,21 @@ namespace Squidex.Controllers.ContentApi
return Ok(response);
}
[MustBeAppReader]
[HttpGet]
[Route("content/{app}/{name}/{id}/{version}")]
[ApiCosts(1)]
public async Task<IActionResult> GetContentVersion(string name, Guid id, int version)
{
var contentData = await contentVersionLoader.LoadAsync(App.Id, id, version);
var response = contentData;
Response.Headers["ETag"] = new StringValues(version.ToString());
return Ok(response);
}
[MustBeAppEditor]
[HttpPost]
[Route("content/{app}/{name}/")]

146
tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs

@ -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);
}
}
}

24
tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs

@ -20,8 +20,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
private readonly IDomainObjectFactory factory = A.Fake<IDomainObjectFactory>();
private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly EventDataFormatter eventDataFormatter = A.Fake<EventDataFormatter>();
private readonly IStreamNameResolver nameResolver = A.Fake<IStreamNameResolver>();
private readonly EventDataFormatter formatter = A.Fake<EventDataFormatter>();
private readonly string streamName = Guid.NewGuid().ToString();
private readonly Guid aggregateId = Guid.NewGuid();
private readonly MyDomainObject domainObject;
@ -31,13 +31,13 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
domainObject = new MyDomainObject(aggregateId, 123);
A.CallTo(() => streamNameResolver.GetStreamName(A<Type>.Ignored, aggregateId))
A.CallTo(() => nameResolver.GetStreamName(A<Type>.Ignored, aggregateId))
.Returns(streamName);
A.CallTo(() => factory.CreateNew<MyDomainObject>(aggregateId))
.Returns(domainObject);
sut = new DefaultDomainObjectRepository(eventStore, streamNameResolver, eventDataFormatter);
sut = new DefaultDomainObjectRepository(eventStore, nameResolver, formatter);
}
public sealed class MyEvent : IEvent
@ -96,9 +96,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
A.CallTo(() => eventStore.GetEventsAsync(streamName))
.Returns(events);
A.CallTo(() => eventDataFormatter.Parse(eventData1))
A.CallTo(() => formatter.Parse(eventData1))
.Returns(new Envelope<IEvent>(event1));
A.CallTo(() => eventDataFormatter.Parse(eventData2))
A.CallTo(() => formatter.Parse(eventData2))
.Returns(new Envelope<IEvent>(event2));
await sut.LoadAsync(domainObject);
@ -124,9 +124,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
A.CallTo(() => eventStore.GetEventsAsync(streamName))
.Returns(events);
A.CallTo(() => eventDataFormatter.Parse(eventData1))
A.CallTo(() => formatter.Parse(eventData1))
.Returns(new Envelope<IEvent>(event1));
A.CallTo(() => eventDataFormatter.Parse(eventData2))
A.CallTo(() => formatter.Parse(eventData2))
.Returns(new Envelope<IEvent>(event2));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.LoadAsync(domainObject, 200));
@ -143,9 +143,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
var eventData1 = new EventData();
var eventData2 = new EventData();
A.CallTo(() => eventDataFormatter.ToEventData(A<Envelope<IEvent>>.That.Matches(e => e.Payload == event1), commitId))
A.CallTo(() => formatter.ToEventData(A<Envelope<IEvent>>.That.Matches(e => e.Payload == event1), commitId))
.Returns(eventData1);
A.CallTo(() => eventDataFormatter.ToEventData(A<Envelope<IEvent>>.That.Matches(e => e.Payload == event2), commitId))
A.CallTo(() => formatter.ToEventData(A<Envelope<IEvent>>.That.Matches(e => e.Payload == event2), commitId))
.Returns(eventData2);
A.CallTo(() => eventStore.AppendEventsAsync(commitId, streamName, 123, A<ICollection<EventData>>.That.Matches(e => e.Count == 2)))
@ -170,9 +170,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
var eventData1 = new EventData();
var eventData2 = new EventData();
A.CallTo(() => eventDataFormatter.ToEventData(A<Envelope<IEvent>>.That.Matches(e => e.Payload == event1), commitId))
A.CallTo(() => formatter.ToEventData(A<Envelope<IEvent>>.That.Matches(e => e.Payload == event1), commitId))
.Returns(eventData1);
A.CallTo(() => eventDataFormatter.ToEventData(A<Envelope<IEvent>>.That.Matches(e => e.Payload == event2), commitId))
A.CallTo(() => formatter.ToEventData(A<Envelope<IEvent>>.That.Matches(e => e.Payload == event2), commitId))
.Returns(eventData2);
A.CallTo(() => eventStore.AppendEventsAsync(commitId, streamName, 123, A<ICollection<EventData>>.That.Matches(e => e.Count == 2)))

Loading…
Cancel
Save