Browse Source

Medium task improved.

pull/306/head
Sebastian 8 years ago
parent
commit
3d5c5b6135
  1. 4
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/MediumAction.cs
  2. 11
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs
  3. 58
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/MediumActionHandler.cs
  4. 8
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs
  5. 8
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
  6. 6
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  7. 18
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  8. 3
      src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs
  9. 14
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  10. 35
      src/Squidex.Domain.Apps.Entities/Contents/ContentVersionLoader.cs
  11. 3
      src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs
  12. 7
      src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs
  13. 5
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs
  14. 6
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  15. 34
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs
  16. 6
      src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
  17. 5
      src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  18. 6
      src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs
  19. 12
      src/Squidex.Infrastructure/Http/DumpFormatter.cs
  20. 10
      src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/MediumActionDto.cs
  21. 28
      src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.html
  22. 6
      src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts
  23. 75
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentVersionLoaderTests.cs
  24. 22
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/MediumActionTests.cs
  25. 16
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs
  26. 6
      tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs

4
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/MediumAction.cs

@ -14,10 +14,6 @@ namespace Squidex.Domain.Apps.Core.Rules.Actions
{ {
public string AccessToken { get; set; } public string AccessToken { get; set; }
public string Author { get; set; }
public string Publication { get; set; }
public string Tags { get; set; } public string Tags { get; set; }
public string Title { get; set; } public string Title { get; set; }

11
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs

@ -10,6 +10,7 @@ using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Http; using Squidex.Infrastructure.Http;
#pragma warning disable SA1649 // File name must match first type name #pragma warning disable SA1649 // File name must match first type name
@ -47,22 +48,24 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
return (null, new InvalidOperationException("The action cannot handle this event.")); return (null, new InvalidOperationException("The action cannot handle this event."));
} }
var requestMsg = BuildRequest(job); var request = BuildRequest(job);
HttpResponseMessage response = null; HttpResponseMessage response = null;
try try
{ {
response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg); var valueWatch = ValueStopwatch.StartNew();
response = await HttpClientPool.GetHttpClient().SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync(); var responseString = await response.Content.ReadAsStringAsync();
var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, responseString, TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(request, response, null, responseString, TimeSpan.Zero, false);
return (requestDump, null); return (requestDump, null);
} }
catch (Exception ex) catch (Exception ex)
{ {
var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, ex.ToString(), TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(request, response, null, ex.ToString(), TimeSpan.Zero, false);
return (requestDump, ex); return (requestDump, ex);
} }

58
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/MediumActionHandler.cs

@ -22,8 +22,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
public sealed class MediumJob public sealed class MediumJob
{ {
public string RequestUrl { get; set; }
public string RequestBody { get; set; } public string RequestBody { get; set; }
public string AccessToken { get; set; } public string AccessToken { get; set; }
@ -44,11 +42,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
protected override (string Description, MediumJob Data) CreateJob(EnrichedEvent @event, MediumAction action) protected override (string Description, MediumJob Data) CreateJob(EnrichedEvent @event, MediumAction action)
{ {
var requestUrl =
!string.IsNullOrWhiteSpace(action.Author) ?
$"https://api.medium.com/v1/users/{action.Author}/posts" :
$"https://api.medium.com/v1/publication/{action.Publication}/posts";
var requestBody = var requestBody =
new JObject( new JObject(
new JProperty("title", formatter.Format(action.Title, @event)), new JProperty("title", formatter.Format(action.Title, @event)),
@ -57,12 +50,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
new JProperty("canonicalUrl", formatter.Format(action.CanonicalUrl, @event)), new JProperty("canonicalUrl", formatter.Format(action.CanonicalUrl, @event)),
new JProperty("tags", ParseTags(@event, action))); new JProperty("tags", ParseTags(@event, action)));
var ruleJob = new MediumJob var ruleJob = new MediumJob { AccessToken = action.AccessToken, RequestBody = requestBody.ToString(Formatting.Indented) };
{
AccessToken = action.AccessToken,
RequestUrl = requestUrl,
RequestBody = requestBody.ToString(Formatting.Indented)
};
return (Description, ruleJob); return (Description, ruleJob);
} }
@ -91,17 +79,36 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(MediumJob job) protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(MediumJob job)
{ {
var requestBody = job.RequestBody; string id;
var requestMessage = BuildRequest(job, requestBody);
HttpResponseMessage response = null; HttpResponseMessage response = null;
var meRequest = BuildMeRequest(job);
try
{
response = await HttpClientPool.GetHttpClient().SendAsync(meRequest);
var responseString = await response.Content.ReadAsStringAsync();
var responseJson = JToken.Parse(responseString);
id = responseJson["data"]["id"].ToString();
}
catch (Exception ex)
{
var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString());
return (requestDump, ex);
}
var postRequestBody = job.RequestBody;
var postRequest = BuildPostRequest(job, postRequestBody, id);
try try
{ {
response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage); response = await HttpClientPool.GetHttpClient().SendAsync(postRequest);
var responseString = await response.Content.ReadAsStringAsync(); var responseString = await response.Content.ReadAsStringAsync();
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, responseString, TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(postRequest, response, postRequestBody, responseString);
Exception ex = null; Exception ex = null;
@ -114,15 +121,15 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
} }
catch (Exception ex) catch (Exception ex)
{ {
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(postRequest, response, postRequestBody, ex.ToString());
return (requestDump, ex); return (requestDump, ex);
} }
} }
private static HttpRequestMessage BuildRequest(MediumJob job, string requestBody) private static HttpRequestMessage BuildPostRequest(MediumJob job, string requestBody, string id)
{ {
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.medium.com/v1/users/{id}/posts")
{ {
Content = new StringContent(requestBody, Encoding.UTF8, "application/json") Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
}; };
@ -133,5 +140,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
return request; return request;
} }
private static HttpRequestMessage BuildMeRequest(MediumJob job)
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.medium.com/v1/me");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Accept-Charset", "utf-8");
request.Headers.Add("Authorization", $"Bearer {job.AccessToken}");
return request;
}
} }
} }

8
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs

@ -67,22 +67,22 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(SlackJob job) protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(SlackJob job)
{ {
var requestBody = job.Body; var requestBody = job.Body;
var requestMessage = BuildRequest(job, requestBody); var request = BuildRequest(job, requestBody);
HttpResponseMessage response = null; HttpResponseMessage response = null;
try try
{ {
response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage); response = await HttpClientPool.GetHttpClient().SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync(); var responseString = await response.Content.ReadAsStringAsync();
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, responseString, TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString);
return (requestDump, null); return (requestDump, null);
} }
catch (Exception ex) catch (Exception ex)
{ {
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString());
return (requestDump, ex); return (requestDump, ex);
} }

8
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs

@ -69,16 +69,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(WebhookJob job) protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(WebhookJob job)
{ {
var requestBody = job.Body; var requestBody = job.Body;
var requestMessage = BuildRequest(job, requestBody); var request = BuildRequest(job, requestBody);
HttpResponseMessage response = null; HttpResponseMessage response = null;
try try
{ {
response = await HttpClientPool.GetHttpClient().SendAsync(requestMessage); response = await HttpClientPool.GetHttpClient().SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync(); var responseString = await response.Content.ReadAsStringAsync();
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, responseString, TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString);
Exception ex = null; Exception ex = null;
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
} }
catch (Exception ex) catch (Exception ex)
{ {
var requestDump = DumpFormatter.BuildDump(requestMessage, response, requestBody, ex.ToString(), TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString());
return (requestDump, ex); return (requestDump, ex);
} }

6
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -72,14 +72,14 @@ namespace Squidex.Domain.Apps.Entities.Apps
}); });
case AssignContributor assigneContributor: case AssignContributor assigneContributor:
return UpdateReturnAsync(assigneContributor, async c => return UpdateReturnAsync(assigneContributor, (Func<AssignContributor, Task<object>>)(async c =>
{ {
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId)); await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId));
AssignContributor(c); AssignContributor(c);
return EntityCreatedResult.Create(c.ContributorId, NewVersion); return EntityCreatedResult.Create(c.ContributorId, (long)base.Version);
}); }));
case RemoveContributor removeContributor: case RemoveContributor removeContributor:
return UpdateAsync(removeContributor, c => return UpdateAsync(removeContributor, c =>

18
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -22,7 +22,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetGrain : SquidexDomainObjectGrain<AssetState>, IAssetGrain public sealed class AssetGrain : SquidexDomainObjectGrainLogSnapshots<AssetState>, IAssetGrain
{ {
public AssetGrain(IStore<Guid> store, ISemanticLog log) public AssetGrain(IStore<Guid> store, ISemanticLog log)
: base(store, log) : base(store, log)
@ -34,23 +34,23 @@ namespace Squidex.Domain.Apps.Entities.Assets
switch (command) switch (command)
{ {
case CreateAsset createRule: case CreateAsset createRule:
return CreateReturnAsync(createRule, c => return CreateReturnAsync(createRule, (Func<CreateAsset, object>)(c =>
{ {
GuardAsset.CanCreate(c); GuardAsset.CanCreate(c);
Create(c); Create(c);
return new AssetSavedResult(NewVersion, Snapshot.FileVersion); return new AssetSavedResult((long)base.Version, Snapshot.FileVersion);
}); }));
case UpdateAsset updateRule: case UpdateAsset updateRule:
return UpdateReturnAsync(updateRule, c => return UpdateReturnAsync(updateRule, (Func<UpdateAsset, object>)(c =>
{ {
GuardAsset.CanUpdate(c); GuardAsset.CanUpdate(c);
Update(c); Update(c);
return new AssetSavedResult(NewVersion, Snapshot.FileVersion); return new AssetSavedResult((long)base.Version, Snapshot.FileVersion);
}); }));
case RenameAsset renameAsset: case RenameAsset renameAsset:
return UpdateAsync(renameAsset, c => return UpdateAsync(renameAsset, c =>
{ {
@ -140,9 +140,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
return Snapshot.Apply(@event); return Snapshot.Apply(@event);
} }
public Task<J<IAssetEntity>> GetStateAsync() public Task<J<IAssetEntity>> GetStateAsync(long version = EtagVersion.Any)
{ {
return J.AsTask<IAssetEntity>(Snapshot); return J.AsTask<IAssetEntity>(GetSnapshot(version));
} }
} }
} }

3
src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
@ -13,6 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public interface IAssetGrain : IDomainObjectGrain public interface IAssetGrain : IDomainObjectGrain
{ {
Task<J<IAssetEntity>> GetStateAsync(); Task<J<IAssetEntity>> GetStateAsync(long version = EtagVersion.Any);
} }
} }

14
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -26,7 +26,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentGrain : SquidexDomainObjectGrain<ContentState>, IContentGrain public sealed class ContentGrain : SquidexDomainObjectGrainLogSnapshots<ContentState>, IContentGrain
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
switch (command) switch (command)
{ {
case CreateContent createContent: case CreateContent createContent:
return CreateReturnAsync(createContent, async c => return CreateReturnAsync(createContent, (Func<CreateContent, Task<object>>)(async c =>
{ {
var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, () => "Failed to create content."); var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, () => "Failed to create content.");
@ -77,8 +77,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
Create(c); Create(c);
return EntityCreatedResult.Create(c.Data, NewVersion); return EntityCreatedResult.Create(c.Data, (long)base.Version);
}); }));
case UpdateContent updateContent: case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, c => return UpdateReturnAsync(updateContent, c =>
@ -216,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
return new ContentDataChangedResult(newData, NewVersion); return new ContentDataChangedResult(newData, Version);
} }
public void Create(CreateContent command) public void Create(CreateContent command)
@ -307,9 +307,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
return operationContext; return operationContext;
} }
public Task<J<IContentEntity>> GetStateAsync() public Task<J<IContentEntity>> GetStateAsync(long version = EtagVersion.Any)
{ {
return J.AsTask<IContentEntity>(Snapshot); return J.AsTask<IContentEntity>(GetSnapshot(version));
} }
} }
} }

35
src/Squidex.Domain.Apps.Entities/Contents/ContentVersionLoader.cs

@ -7,53 +7,38 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Schemas; using Orleans;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentVersionLoader : IContentVersionLoader public sealed class ContentVersionLoader : IContentVersionLoader
{ {
private readonly IStore<Guid> store; private readonly IGrainFactory grainFactory;
private readonly FieldRegistry registry;
public ContentVersionLoader(IStore<Guid> store, FieldRegistry registry) public ContentVersionLoader(IGrainFactory grainFactory)
{ {
Guard.NotNull(store, nameof(store)); Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(registry, nameof(registry));
this.store = store; this.grainFactory = grainFactory;
this.registry = registry;
} }
public async Task<IContentEntity> LoadAsync(Guid id, long version) public async Task<IContentEntity> LoadAsync(Guid id, long version)
{ {
using (Profiler.TraceMethod<ContentVersionLoader>()) using (Profiler.TraceMethod<ContentVersionLoader>())
{ {
var content = new ContentState(); var grain = grainFactory.GetGrain<IContentGrain>(id);
var persistence = store.WithEventSourcing<ContentGrain, Guid>(id, e =>
{
if (content.Version < version)
{
content = content.Apply(e);
content.Version++;
}
});
await persistence.ReadAsync(); var content = await grain.GetStateAsync(version);
if (content.Version != version) if (content.Value == null || content.Value.Version != version)
{ {
throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity));
} }
return content; return content.Value;
} }
} }
} }
} }

3
src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
@ -13,6 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentGrain : IDomainObjectGrain public interface IContentGrain : IDomainObjectGrain
{ {
Task<J<IContentEntity>> GetStateAsync(); Task<J<IContentEntity>> GetStateAsync(long version = EtagVersion.Any);
} }
} }

7
src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs

@ -76,10 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
var asset = var asset =
(await grainFactory (await grainFactory
.GetGrain<IAssetGrain>(assetEvent.AssetId) .GetGrain<IAssetGrain>(assetEvent.AssetId)
.GetStateAsync()).Value; .GetStateAsync(@event.Headers.EventStreamNumber())).Value;
SimpleMapper.Map(asset, result); SimpleMapper.Map(asset, result);
SimpleMapper.Map(assetEvent, result);
switch (assetEvent) switch (assetEvent)
{ {
@ -105,14 +104,12 @@ namespace Squidex.Domain.Apps.Entities.Rules
var content = var content =
(await grainFactory (await grainFactory
.GetGrain<IContentGrain>(contentEvent.ContentId) .GetGrain<IContentGrain>(contentEvent.ContentId)
.GetStateAsync()).Value; .GetStateAsync(@event.Headers.EventStreamNumber())).Value;
SimpleMapper.Map(content, result); SimpleMapper.Map(content, result);
result.Data = content.Data ?? content.DataDraft; result.Data = content.Data ?? content.DataDraft;
SimpleMapper.Map(contentEvent, result);
switch (contentEvent) switch (contentEvent)
{ {
case ContentCreated _: case ContentCreated _:

5
src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs

@ -116,11 +116,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
errors.Add(new ValidationError("Access token is required.", nameof(action.AccessToken))); errors.Add(new ValidationError("Access token is required.", nameof(action.AccessToken)));
} }
if (string.IsNullOrWhiteSpace(action.Author) && string.IsNullOrWhiteSpace(action.Publication))
{
errors.Add(new ValidationError("Author or publication is required.", nameof(action.Author), nameof(action.Publication)));
}
if (string.IsNullOrWhiteSpace(action.Content)) if (string.IsNullOrWhiteSpace(action.Content))
{ {
errors.Add(new ValidationError("Content is required.", nameof(action.Content))); errors.Add(new ValidationError("Content is required.", nameof(action.Content)));

6
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
switch (command) switch (command)
{ {
case AddField addField: case AddField addField:
return UpdateReturnAsync(addField, c => return UpdateReturnAsync(addField, (Func<AddField, object>)(c =>
{ {
GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); GuardSchemaField.CanAdd(Snapshot.SchemaDef, c);
@ -64,8 +64,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas
id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id; id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id;
} }
return EntityCreatedResult.Create(id, NewVersion); return EntityCreatedResult.Create(id, (long)base.Version);
}); }));
case CreateSchema createSchema: case CreateSchema createSchema:
return CreateAsync(createSchema, async c => return CreateAsync(createSchema, async c =>

34
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs

@ -0,0 +1,34 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities
{
public abstract class SquidexDomainObjectGrainLogSnapshots<T> : LogSnapshotDomainObjectGrain<T> where T : IDomainState, new()
{
protected SquidexDomainObjectGrainLogSnapshots(IStore<Guid> store, ISemanticLog log)
: base(store, log)
{
}
public override void RaiseEvent(Envelope<IEvent> @event)
{
if (@event.Payload is AppEvent appEvent)
{
@event.SetAppId(appEvent.AppId.Id);
}
base.RaiseEvent(@event);
}
}
}

6
src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs

@ -34,8 +34,12 @@ namespace Squidex.Infrastructure.Commands
protected sealed override void ApplyEvent(Envelope<IEvent> @event) protected sealed override void ApplyEvent(Envelope<IEvent> @event)
{ {
var newVersion = Version + 1;
var snapshotNew = OnEvent(@event);
snapshot = OnEvent(@event); snapshot = OnEvent(@event);
snapshot.Version = NewVersion + 1; snapshot.Version = newVersion;
} }
protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion)

5
src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs

@ -31,11 +31,6 @@ namespace Squidex.Infrastructure.Commands
get { return Snapshot.Version; } get { return Snapshot.Version; }
} }
public long NewVersion
{
get { return Snapshot.Version + uncomittedEvents.Count; }
}
public abstract T Snapshot { get; } public abstract T Snapshot { get; }
protected DomainObjectGrainBase(ISemanticLog log) protected DomainObjectGrainBase(ISemanticLog log)

6
src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs

@ -15,7 +15,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands namespace Squidex.Infrastructure.Commands
{ {
public abstract class MultiSnapshotDomainObjectGrain<T> : DomainObjectGrainBase<T> where T : IDomainState, new() public abstract class LogSnapshotDomainObjectGrain<T> : DomainObjectGrainBase<T> where T : IDomainState, new()
{ {
private readonly IStore<Guid> store; private readonly IStore<Guid> store;
private readonly List<T> snapshots = new List<T> { new T { Version = EtagVersion.Empty } }; private readonly List<T> snapshots = new List<T> { new T { Version = EtagVersion.Empty } };
@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.Commands
get { return snapshots.Last(); } get { return snapshots.Last(); }
} }
protected MultiSnapshotDomainObjectGrain(IStore<Guid> store, ISemanticLog log) protected LogSnapshotDomainObjectGrain(IStore<Guid> store, ISemanticLog log)
: base(log) : base(log)
{ {
Guard.NotNull(log, nameof(log)); Guard.NotNull(log, nameof(log));
@ -58,7 +58,7 @@ namespace Squidex.Infrastructure.Commands
{ {
var snapshot = OnEvent(@event); var snapshot = OnEvent(@event);
snapshot.Version = NewVersion + 1; snapshot.Version = Version + 1;
snapshots.Add(snapshot); snapshots.Add(snapshot);
} }

12
src/Squidex.Infrastructure/Http/DumpFormatter.cs

@ -15,7 +15,17 @@ namespace Squidex.Infrastructure.Http
{ {
public static class DumpFormatter public static class DumpFormatter
{ {
public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string requestBody, string responseBody, TimeSpan elapsed, bool isTimeout) public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string responseBody)
{
return BuildDump(request, response, null, responseBody, TimeSpan.Zero, false);
}
public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string requestBody, string responseBody)
{
return BuildDump(request, response, requestBody, responseBody, TimeSpan.Zero, false);
}
public static string BuildDump(HttpRequestMessage request, HttpResponseMessage response, string requestBody, string responseBody, TimeSpan elapsed, bool isTimeout = false)
{ {
var writer = new StringBuilder(); var writer = new StringBuilder();

10
src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/MediumactionDto.cs → src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/MediumActionDto.cs

@ -22,16 +22,6 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Actions
[Required] [Required]
public string AccessToken { get; set; } public string AccessToken { get; set; }
/// <summary>
/// The author name.
/// </summary>
public string Author { get; set; }
/// <summary>
/// The author name.
/// </summary>
public string Publication { get; set; }
/// <summary> /// <summary>
/// The optional comma separated list of tags. /// The optional comma separated list of tags.
/// </summary> /// </summary>

28
src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.html

@ -15,34 +15,6 @@
</div> </div>
</div> </div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="author">Author</label>
<div class="col col-9">
<sqx-control-errors for="author" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="author" formControlName="author" />
<small class="form-text text-muted">
The name of the author. You can also define the publication.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="publication">Publication</label>
<div class="col col-9">
<sqx-control-errors for="publication" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="publication" formControlName="publication" />
<small class="form-text text-muted">
The name of the publication. You can also define the author.
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col col-3 col-form-label" for="title">Title</label> <label class="col col-3 col-form-label" for="title">Title</label>

6
src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts

@ -39,12 +39,6 @@ export class MediumActionComponent implements OnInit {
Validators.required Validators.required
])); ]));
this.actionForm.setControl('author',
new FormControl(this.action.author || ''));
this.actionForm.setControl('publication',
new FormControl(this.action.publication || ''));
this.actionForm.setControl('canonicalUrl', this.actionForm.setControl('canonicalUrl',
new FormControl(this.action.canonicalUrl || '')); new FormControl(this.action.canonicalUrl || ''));

75
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentVersionLoaderTests.cs

@ -0,0 +1,75 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Orleans;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentVersionLoaderTests
{
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IContentGrain grain = A.Fake<IContentGrain>();
private readonly FieldRegistry fieldRegistry = new FieldRegistry(new TypeNameRegistry());
private readonly Guid id = Guid.NewGuid();
private readonly ContentVersionLoader sut;
public ContentVersionLoaderTests()
{
A.CallTo(() => grainFactory.GetGrain<IContentGrain>(id, null))
.Returns(grain);
sut = new ContentVersionLoader(grainFactory);
}
[Fact]
public async Task Should_throw_exception_if_no_state_returned()
{
A.CallTo(() => grain.GetStateAsync(10))
.Returns(new J<IContentEntity>(null));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(id, 10));
}
[Fact]
public async Task Should_throw_exception_if_state_has_other_version()
{
var entity = A.Fake<IContentEntity>();
A.CallTo(() => entity.Version)
.Returns(5);
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of(entity));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(id, 10));
}
[Fact]
public async Task Should_return_content_from_state()
{
var entity = A.Fake<IContentEntity>();
A.CallTo(() => entity.Version)
.Returns(10);
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of(entity));
var result = await sut.LoadAsync(id, 10);
Assert.Same(entity, result);
}
}
}

22
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/MediumActionTests.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions
[Fact] [Fact]
public async Task Should_add_error_if_access_token_is_null() public async Task Should_add_error_if_access_token_is_null()
{ {
var action = new MediumAction { AccessToken = null, Author = "author", Title = "title", Content = "content" }; var action = new MediumAction { AccessToken = null, Title = "title", Content = "content" };
var errors = await RuleActionValidator.ValidateAsync(action); var errors = await RuleActionValidator.ValidateAsync(action);
@ -30,24 +30,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions
}); });
} }
[Fact]
public async Task Should_add_error_if_author_is_null()
{
var action = new MediumAction { AccessToken = "token", Author = null, Title = "title", Content = "content" };
var errors = await RuleActionValidator.ValidateAsync(action);
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Author or publication is required.", "Author", "Publication")
});
}
[Fact] [Fact]
public async Task Should_add_error_if_title_null() public async Task Should_add_error_if_title_null()
{ {
var action = new MediumAction { AccessToken = "token", Author = "author", Title = null, Content = "content" }; var action = new MediumAction { AccessToken = "token", Title = null, Content = "content" };
var errors = await RuleActionValidator.ValidateAsync(action); var errors = await RuleActionValidator.ValidateAsync(action);
@ -61,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions
[Fact] [Fact]
public async Task Should_add_error_if_content_is_null() public async Task Should_add_error_if_content_is_null()
{ {
var action = new MediumAction { AccessToken = "token", Author = "author", Title = "title", Content = null }; var action = new MediumAction { AccessToken = "token", Title = "title", Content = null };
var errors = await RuleActionValidator.ValidateAsync(action); var errors = await RuleActionValidator.ValidateAsync(action);
@ -75,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions
[Fact] [Fact]
public async Task Should_not_add_error_if_values_are_valid() public async Task Should_not_add_error_if_values_are_valid()
{ {
var action = new MediumAction { AccessToken = "token", Author = "author", Title = "title", Content = "content" }; var action = new MediumAction { AccessToken = "token", Title = "title", Content = "content" };
var errors = await RuleActionValidator.ValidateAsync(action); var errors = await RuleActionValidator.ValidateAsync(action);

16
tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs

@ -24,7 +24,8 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
public abstract class HandlerTestBase<T, TState> where T : IDomainObjectGrain public abstract class HandlerTestBase<T, TState> where T : IDomainObjectGrain
{ {
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>(); private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly IPersistence<TState> persistence = A.Fake<IPersistence<TState>>(); private readonly IPersistence<TState> persistence1 = A.Fake<IPersistence<TState>>();
private readonly IPersistence persistence2 = A.Fake<IPersistence>();
protected RefToken User { get; } = new RefToken("subject", Guid.NewGuid().ToString()); protected RefToken User { get; } = new RefToken("subject", Guid.NewGuid().ToString());
@ -58,9 +59,18 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
protected HandlerTestBase() protected HandlerTestBase()
{ {
A.CallTo(() => store.WithSnapshotsAndEventSourcing(A<Type>.Ignored, Id, A<Func<TState, Task>>.Ignored, A<Func<Envelope<IEvent>, Task>>.Ignored)) A.CallTo(() => store.WithSnapshotsAndEventSourcing(A<Type>.Ignored, Id, A<Func<TState, Task>>.Ignored, A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence); .Returns(persistence1);
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored)) A.CallTo(() => store.WithEventSourcing(A<Type>.Ignored, Id, A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence2);
A.CallTo(() => persistence1.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.Invokes(new Action<IEnumerable<Envelope<IEvent>>>(events =>
{
LastEvents = events;
}));
A.CallTo(() => persistence2.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.Invokes(new Action<IEnumerable<Envelope<IEvent>>>(events => .Invokes(new Action<IEnumerable<Envelope<IEvent>>>(events =>
{ {
LastEvents = events; LastEvents = events;

6
tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs

@ -20,7 +20,7 @@ using Xunit;
namespace Squidex.Infrastructure.Commands namespace Squidex.Infrastructure.Commands
{ {
public class MultiSnapshotDomainObjectGrainTests public class LogSnapshotDomainObjectGrainTests
{ {
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>(); private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly ISnapshotStore<MyDomainState, Guid> snapshotStore = A.Fake<ISnapshotStore<MyDomainState, Guid>>(); private readonly ISnapshotStore<MyDomainState, Guid> snapshotStore = A.Fake<ISnapshotStore<MyDomainState, Guid>>();
@ -53,7 +53,7 @@ namespace Squidex.Infrastructure.Commands
public int Value { get; set; } public int Value { get; set; }
} }
public sealed class MyDomainObject : MultiSnapshotDomainObjectGrain<MyDomainState> public sealed class MyDomainObject : LogSnapshotDomainObjectGrain<MyDomainState>
{ {
public MyDomainObject(IStore<Guid> store) public MyDomainObject(IStore<Guid> store)
: base(store, A.Dummy<ISemanticLog>()) : base(store, A.Dummy<ISemanticLog>())
@ -102,7 +102,7 @@ namespace Squidex.Infrastructure.Commands
} }
} }
public MultiSnapshotDomainObjectGrainTests() public LogSnapshotDomainObjectGrainTests()
{ {
A.CallTo(() => store.WithEventSourcing(typeof(MyDomainObject), id, A<Func<Envelope<IEvent>, Task>>.Ignored)) A.CallTo(() => store.WithEventSourcing(typeof(MyDomainObject), id, A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence); .Returns(persistence);

Loading…
Cancel
Save