mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
93 changed files with 2671 additions and 915 deletions
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Contents |
||||
|
{ |
||||
|
public enum StatusChange |
||||
|
{ |
||||
|
Archived, |
||||
|
Published, |
||||
|
Restored, |
||||
|
Unpublished |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Rules.Actions |
||||
|
{ |
||||
|
[TypeName(nameof(MediumAction))] |
||||
|
public sealed class MediumAction : RuleAction |
||||
|
{ |
||||
|
public string AccessToken { get; set; } |
||||
|
|
||||
|
public string Tags { get; set; } |
||||
|
|
||||
|
public string Title { get; set; } |
||||
|
|
||||
|
public string CanonicalUrl { get; set; } |
||||
|
|
||||
|
public string Content { get; set; } |
||||
|
|
||||
|
public bool IsHtml { get; set; } |
||||
|
|
||||
|
public override T Accept<T>(IRuleActionVisitor<T> visitor) |
||||
|
{ |
||||
|
return visitor.Visit(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,145 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
#pragma warning disable SA1649 // File name must match first type name
|
||||
|
|
||||
|
using System; |
||||
|
using System.Net.Http; |
||||
|
using System.Text; |
||||
|
using System.Threading.Tasks; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules.Actions.Utils; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Actions; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Http; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.Actions |
||||
|
{ |
||||
|
public sealed class MediumJob |
||||
|
{ |
||||
|
public string RequestBody { get; set; } |
||||
|
|
||||
|
public string AccessToken { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class MediumActionHandler : RuleActionHandler<MediumAction, MediumJob> |
||||
|
{ |
||||
|
private const string Description = "Post to medium"; |
||||
|
|
||||
|
private readonly RuleEventFormatter formatter; |
||||
|
private readonly ClientPool<string, HttpClient> clients; |
||||
|
|
||||
|
public MediumActionHandler(RuleEventFormatter formatter) |
||||
|
{ |
||||
|
Guard.NotNull(formatter, nameof(formatter)); |
||||
|
|
||||
|
this.formatter = formatter; |
||||
|
|
||||
|
clients = new ClientPool<string, HttpClient>(key => |
||||
|
{ |
||||
|
var client = new HttpClient |
||||
|
{ |
||||
|
Timeout = TimeSpan.FromSeconds(4) |
||||
|
}; |
||||
|
|
||||
|
client.DefaultRequestHeaders.Add("Accept", "application/json"); |
||||
|
client.DefaultRequestHeaders.Add("Accept-Charset", "utf-8"); |
||||
|
client.DefaultRequestHeaders.Add("User-Agent", "Squidex Headless CMS"); |
||||
|
|
||||
|
return client; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected override (string Description, MediumJob Data) CreateJob(EnrichedEvent @event, MediumAction action) |
||||
|
{ |
||||
|
var requestBody = |
||||
|
new JObject( |
||||
|
new JProperty("title", formatter.Format(action.Title, @event)), |
||||
|
new JProperty("contentFormat", action.IsHtml ? "html" : "markdown"), |
||||
|
new JProperty("content", formatter.Format(action.Content, @event)), |
||||
|
new JProperty("canonicalUrl", formatter.Format(action.CanonicalUrl, @event)), |
||||
|
new JProperty("tags", ParseTags(@event, action))); |
||||
|
|
||||
|
var ruleJob = new MediumJob { AccessToken = action.AccessToken, RequestBody = requestBody.ToString(Formatting.Indented) }; |
||||
|
|
||||
|
return (Description, ruleJob); |
||||
|
} |
||||
|
|
||||
|
private JArray ParseTags(EnrichedEvent @event, MediumAction action) |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(action.Tags)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
string[] tags; |
||||
|
try |
||||
|
{ |
||||
|
var jsonTags = formatter.Format(action.Tags, @event); |
||||
|
|
||||
|
tags = JsonConvert.DeserializeObject<string[]>(jsonTags); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
tags = action.Tags.Split(','); |
||||
|
} |
||||
|
|
||||
|
return new JArray(tags); |
||||
|
} |
||||
|
|
||||
|
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(MediumJob job) |
||||
|
{ |
||||
|
var httpClient = clients.GetClient(string.Empty); |
||||
|
|
||||
|
string id; |
||||
|
|
||||
|
HttpResponseMessage response = null; |
||||
|
|
||||
|
var meRequest = BuildMeRequest(job); |
||||
|
try |
||||
|
{ |
||||
|
response = await httpClient.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); |
||||
|
} |
||||
|
|
||||
|
return await httpClient.OneWayRequestAsync(BuildPostRequest(job, id), job.RequestBody); |
||||
|
} |
||||
|
|
||||
|
private static HttpRequestMessage BuildPostRequest(MediumJob job, string id) |
||||
|
{ |
||||
|
var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.medium.com/v1/users/{id}/posts") |
||||
|
{ |
||||
|
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") |
||||
|
}; |
||||
|
|
||||
|
request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); |
||||
|
|
||||
|
return request; |
||||
|
} |
||||
|
|
||||
|
private static HttpRequestMessage BuildMeRequest(MediumJob job) |
||||
|
{ |
||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.medium.com/v1/me"); |
||||
|
|
||||
|
request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); |
||||
|
|
||||
|
return request; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Net.Http; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Http; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.Actions.Utils |
||||
|
{ |
||||
|
public static class HttpHelper |
||||
|
{ |
||||
|
public static async Task<(string Dump, Exception Exception)> OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null) |
||||
|
{ |
||||
|
HttpResponseMessage response = null; |
||||
|
try |
||||
|
{ |
||||
|
response = await client.SendAsync(request); |
||||
|
|
||||
|
var responseString = await response.Content.ReadAsStringAsync(); |
||||
|
|
||||
|
var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString); |
||||
|
|
||||
|
Exception ex = null; |
||||
|
|
||||
|
if (!response.IsSuccessStatusCode) |
||||
|
{ |
||||
|
ex = new HttpRequestException($"Response code does not indicate success: {(int)response.StatusCode} ({response.StatusCode})."); |
||||
|
} |
||||
|
|
||||
|
return (requestDump, ex); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString()); |
||||
|
|
||||
|
return (requestDump, ex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using NodaTime; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents |
||||
|
{ |
||||
|
public sealed class EnrichedAssetEvent : EnrichedEvent |
||||
|
{ |
||||
|
public EnrichedAssetEventType Type { get; set; } |
||||
|
|
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public Instant Created { get; set; } |
||||
|
|
||||
|
public Instant LastModified { get; set; } |
||||
|
|
||||
|
public RefToken CreatedBy { get; set; } |
||||
|
|
||||
|
public RefToken LastModifiedBy { get; set; } |
||||
|
|
||||
|
public string MimeType { get; set; } |
||||
|
|
||||
|
public string FileName { get; set; } |
||||
|
|
||||
|
public long FileVersion { get; set; } |
||||
|
|
||||
|
public long FileSize { get; set; } |
||||
|
|
||||
|
public bool IsImage { get; set; } |
||||
|
|
||||
|
public int? PixelWidth { get; set; } |
||||
|
|
||||
|
public int? PixelHeight { get; set; } |
||||
|
|
||||
|
public override Guid AggregateId |
||||
|
{ |
||||
|
get { return Id; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents |
||||
|
{ |
||||
|
public enum EnrichedAssetEventType |
||||
|
{ |
||||
|
Created, |
||||
|
Deleted, |
||||
|
Renamed, |
||||
|
Updated |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents |
||||
|
{ |
||||
|
public sealed class EnrichedContentEvent : EnrichedSchemaEvent |
||||
|
{ |
||||
|
public EnrichedContentEventType Type { get; set; } |
||||
|
|
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public Instant Created { get; set; } |
||||
|
|
||||
|
public Instant LastModified { get; set; } |
||||
|
|
||||
|
public RefToken CreatedBy { get; set; } |
||||
|
|
||||
|
public RefToken LastModifiedBy { get; set; } |
||||
|
|
||||
|
public NamedContentData Data { get; set; } |
||||
|
|
||||
|
public Status Status { get; set; } |
||||
|
|
||||
|
public override Guid AggregateId |
||||
|
{ |
||||
|
get { return Id; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents |
||||
|
{ |
||||
|
public enum EnrichedContentEventType |
||||
|
{ |
||||
|
Archived, |
||||
|
Created, |
||||
|
Deleted, |
||||
|
Published, |
||||
|
Restored, |
||||
|
Unpublished, |
||||
|
Updated |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Newtonsoft.Json; |
||||
|
using NodaTime; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Shared.Users; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents |
||||
|
{ |
||||
|
public abstract class EnrichedEvent |
||||
|
{ |
||||
|
public NamedId<Guid> AppId { get; set; } |
||||
|
|
||||
|
public RefToken Actor { get; set; } |
||||
|
|
||||
|
public Instant Timestamp { get; set; } |
||||
|
|
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
[JsonIgnore] |
||||
|
public abstract Guid AggregateId { get; } |
||||
|
|
||||
|
[JsonIgnore] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[JsonIgnore] |
||||
|
public IUser User { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents |
||||
|
{ |
||||
|
public abstract class EnrichedSchemaEvent : EnrichedEvent |
||||
|
{ |
||||
|
public NamedId<Guid> SchemaId { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules |
||||
|
{ |
||||
|
public interface IEventEnricher |
||||
|
{ |
||||
|
Task<EnrichedEvent> EnrichAsync(Envelope<AppEvent> @event); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,184 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Orleans; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Entities.Assets; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Assets; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Shared.Users; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules |
||||
|
{ |
||||
|
public sealed class EventEnricher : IEventEnricher |
||||
|
{ |
||||
|
private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); |
||||
|
private readonly IGrainFactory grainFactory; |
||||
|
private readonly IMemoryCache userCache; |
||||
|
private readonly IUserResolver userResolver; |
||||
|
|
||||
|
public EventEnricher(IGrainFactory grainFactory, IMemoryCache userCache, IUserResolver userResolver) |
||||
|
{ |
||||
|
Guard.NotNull(grainFactory, nameof(grainFactory)); |
||||
|
Guard.NotNull(userCache, nameof(userCache)); |
||||
|
Guard.NotNull(userResolver, nameof(userResolver)); |
||||
|
|
||||
|
this.grainFactory = grainFactory; |
||||
|
this.userCache = userCache; |
||||
|
this.userResolver = userResolver; |
||||
|
} |
||||
|
|
||||
|
public async Task<EnrichedEvent> EnrichAsync(Envelope<AppEvent> @event) |
||||
|
{ |
||||
|
Guard.NotNull(@event, nameof(@event)); |
||||
|
|
||||
|
if (@event.Payload is ContentEvent contentEvent) |
||||
|
{ |
||||
|
var result = new EnrichedContentEvent(); |
||||
|
|
||||
|
await Task.WhenAll( |
||||
|
EnrichContentAsync(result, contentEvent, @event), |
||||
|
EnrichDefaultAsync(result, @event)); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
if (@event.Payload is AssetEvent assetEvent) |
||||
|
{ |
||||
|
var result = new EnrichedAssetEvent(); |
||||
|
|
||||
|
await Task.WhenAll( |
||||
|
EnrichAssetAsync(result, assetEvent, @event), |
||||
|
EnrichDefaultAsync(result, @event)); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private async Task EnrichAssetAsync(EnrichedAssetEvent result, AssetEvent assetEvent, Envelope<AppEvent> @event) |
||||
|
{ |
||||
|
var asset = |
||||
|
(await grainFactory |
||||
|
.GetGrain<IAssetGrain>(assetEvent.AssetId) |
||||
|
.GetStateAsync(@event.Headers.EventStreamNumber())).Value; |
||||
|
|
||||
|
SimpleMapper.Map(asset, result); |
||||
|
|
||||
|
switch (assetEvent) |
||||
|
{ |
||||
|
case AssetCreated _: |
||||
|
result.Type = EnrichedAssetEventType.Created; |
||||
|
break; |
||||
|
case AssetRenamed _: |
||||
|
result.Type = EnrichedAssetEventType.Renamed; |
||||
|
break; |
||||
|
case AssetUpdated _: |
||||
|
result.Type = EnrichedAssetEventType.Updated; |
||||
|
break; |
||||
|
case AssetDeleted _: |
||||
|
result.Type = EnrichedAssetEventType.Deleted; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
result.Name = $"Asset{result.Type}"; |
||||
|
} |
||||
|
|
||||
|
private async Task EnrichContentAsync(EnrichedContentEvent result, ContentEvent contentEvent, Envelope<AppEvent> @event) |
||||
|
{ |
||||
|
var content = |
||||
|
(await grainFactory |
||||
|
.GetGrain<IContentGrain>(contentEvent.ContentId) |
||||
|
.GetStateAsync(@event.Headers.EventStreamNumber())).Value; |
||||
|
|
||||
|
SimpleMapper.Map(content, result); |
||||
|
|
||||
|
result.Data = content.Data ?? content.DataDraft; |
||||
|
|
||||
|
switch (contentEvent) |
||||
|
{ |
||||
|
case ContentCreated _: |
||||
|
result.Type = EnrichedContentEventType.Created; |
||||
|
break; |
||||
|
case ContentDeleted _: |
||||
|
result.Type = EnrichedContentEventType.Deleted; |
||||
|
break; |
||||
|
case ContentChangesPublished _: |
||||
|
case ContentUpdated _: |
||||
|
result.Type = EnrichedContentEventType.Updated; |
||||
|
break; |
||||
|
case ContentStatusChanged contentStatusChanged: |
||||
|
switch (contentStatusChanged.Change) |
||||
|
{ |
||||
|
case StatusChange.Published: |
||||
|
result.Type = EnrichedContentEventType.Published; |
||||
|
break; |
||||
|
case StatusChange.Unpublished: |
||||
|
result.Type = EnrichedContentEventType.Unpublished; |
||||
|
break; |
||||
|
case StatusChange.Archived: |
||||
|
result.Type = EnrichedContentEventType.Archived; |
||||
|
break; |
||||
|
case StatusChange.Restored: |
||||
|
result.Type = EnrichedContentEventType.Restored; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}"; |
||||
|
} |
||||
|
|
||||
|
private async Task EnrichDefaultAsync(EnrichedEvent result, Envelope<AppEvent> @event) |
||||
|
{ |
||||
|
result.Timestamp = @event.Headers.Timestamp(); |
||||
|
|
||||
|
if (@event.Payload is SquidexEvent squidexEvent) |
||||
|
{ |
||||
|
result.Actor = squidexEvent.Actor; |
||||
|
} |
||||
|
|
||||
|
if (@event.Payload is AppEvent appEvent) |
||||
|
{ |
||||
|
result.AppId = appEvent.AppId; |
||||
|
} |
||||
|
|
||||
|
result.User = await FindUserAsync(result.Actor); |
||||
|
} |
||||
|
|
||||
|
private Task<IUser> FindUserAsync(RefToken actor) |
||||
|
{ |
||||
|
var key = $"EventEnrichers_Users_${actor.Identifier}"; |
||||
|
|
||||
|
return userCache.GetOrCreateAsync(key, async x => |
||||
|
{ |
||||
|
x.AbsoluteExpirationRelativeToNow = UserCacheDuration; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
return await userResolver.FindByIdOrEmailAsync(actor.Identifier); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,202 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Commands |
||||
|
{ |
||||
|
public abstract class DomainObjectGrainBase<T> : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() |
||||
|
{ |
||||
|
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>(); |
||||
|
private readonly ISemanticLog log; |
||||
|
private Guid id; |
||||
|
|
||||
|
public Guid Id |
||||
|
{ |
||||
|
get { return id; } |
||||
|
} |
||||
|
|
||||
|
public long Version |
||||
|
{ |
||||
|
get { return Snapshot.Version; } |
||||
|
} |
||||
|
|
||||
|
public abstract T Snapshot { get; } |
||||
|
|
||||
|
protected DomainObjectGrainBase(ISemanticLog log) |
||||
|
{ |
||||
|
Guard.NotNull(log, nameof(log)); |
||||
|
|
||||
|
this.log = log; |
||||
|
} |
||||
|
|
||||
|
public sealed override async Task OnActivateAsync(Guid key) |
||||
|
{ |
||||
|
using (log.MeasureInformation(w => w |
||||
|
.WriteProperty("action", "ActivateDomainObject") |
||||
|
.WriteProperty("domainObjectType", GetType().Name) |
||||
|
.WriteProperty("domainObjectKey", key.ToString()))) |
||||
|
{ |
||||
|
id = key; |
||||
|
|
||||
|
await ReadAsync(GetType(), id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void RaiseEvent(IEvent @event) |
||||
|
{ |
||||
|
RaiseEvent(Envelope.Create(@event)); |
||||
|
} |
||||
|
|
||||
|
public virtual void RaiseEvent(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
Guard.NotNull(@event, nameof(@event)); |
||||
|
|
||||
|
@event.SetAggregateId(id); |
||||
|
|
||||
|
ApplyEvent(@event); |
||||
|
|
||||
|
uncomittedEvents.Add(@event); |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents() |
||||
|
{ |
||||
|
return uncomittedEvents; |
||||
|
} |
||||
|
|
||||
|
public void ClearUncommittedEvents() |
||||
|
{ |
||||
|
uncomittedEvents.Clear(); |
||||
|
} |
||||
|
|
||||
|
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
return InvokeAsync(command, handler, false); |
||||
|
} |
||||
|
|
||||
|
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
return InvokeAsync(command, handler?.ToAsync(), false); |
||||
|
} |
||||
|
|
||||
|
protected Task<object> CreateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
return InvokeAsync(command, handler.ToDefault<TCommand, object>(), false); |
||||
|
} |
||||
|
|
||||
|
protected Task<object> CreateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), false); |
||||
|
} |
||||
|
|
||||
|
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
return InvokeAsync(command, handler, true); |
||||
|
} |
||||
|
|
||||
|
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
return InvokeAsync(command, handler?.ToAsync(), true); |
||||
|
} |
||||
|
|
||||
|
protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), true); |
||||
|
} |
||||
|
|
||||
|
protected Task<object> UpdateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), true); |
||||
|
} |
||||
|
|
||||
|
private async Task<object> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler, bool isUpdate) where TCommand : class, IAggregateCommand |
||||
|
{ |
||||
|
Guard.NotNull(command, nameof(command)); |
||||
|
|
||||
|
if (command.ExpectedVersion != EtagVersion.Any && command.ExpectedVersion != Version) |
||||
|
{ |
||||
|
throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); |
||||
|
} |
||||
|
|
||||
|
if (isUpdate && Version < 0) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
DeactivateOnIdle(); |
||||
|
} |
||||
|
catch (InvalidOperationException) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
throw new DomainObjectNotFoundException(id.ToString(), GetType()); |
||||
|
} |
||||
|
|
||||
|
if (!isUpdate && Version >= 0) |
||||
|
{ |
||||
|
throw new DomainException("Object has already been created."); |
||||
|
} |
||||
|
|
||||
|
var previousSnapshot = Snapshot; |
||||
|
var previousVersion = Version; |
||||
|
try |
||||
|
{ |
||||
|
var result = await handler(command); |
||||
|
|
||||
|
var events = uncomittedEvents.ToArray(); |
||||
|
|
||||
|
await WriteAsync(events, previousVersion); |
||||
|
|
||||
|
if (result == null) |
||||
|
{ |
||||
|
if (isUpdate) |
||||
|
{ |
||||
|
result = new EntitySavedResult(Version); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
result = EntityCreatedResult.Create(id, Version); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
RestorePreviousSnapshot(previousSnapshot, previousVersion); |
||||
|
|
||||
|
throw; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
uncomittedEvents.Clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion); |
||||
|
|
||||
|
protected abstract void ApplyEvent(Envelope<IEvent> @event); |
||||
|
|
||||
|
protected abstract Task ReadAsync(Type type, Guid id); |
||||
|
|
||||
|
protected abstract Task WriteAsync(Envelope<IEvent>[] events, long previousVersion); |
||||
|
|
||||
|
public async Task<J<object>> ExecuteAsync(J<IAggregateCommand> command) |
||||
|
{ |
||||
|
var result = await ExecuteAsync(command.Value); |
||||
|
|
||||
|
return result.AsJ(); |
||||
|
} |
||||
|
|
||||
|
protected abstract Task<object> ExecuteAsync(IAggregateCommand command); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Commands |
||||
|
{ |
||||
|
public abstract class LogSnapshotDomainObjectGrain<T> : DomainObjectGrainBase<T> where T : IDomainState, new() |
||||
|
{ |
||||
|
private readonly IStore<Guid> store; |
||||
|
private readonly List<T> snapshots = new List<T> { new T { Version = EtagVersion.Empty } }; |
||||
|
private IPersistence persistence; |
||||
|
|
||||
|
public override T Snapshot |
||||
|
{ |
||||
|
get { return snapshots.Last(); } |
||||
|
} |
||||
|
|
||||
|
protected LogSnapshotDomainObjectGrain(IStore<Guid> store, ISemanticLog log) |
||||
|
: base(log) |
||||
|
{ |
||||
|
Guard.NotNull(log, nameof(log)); |
||||
|
|
||||
|
this.store = store; |
||||
|
} |
||||
|
|
||||
|
public T GetSnapshot(long version) |
||||
|
{ |
||||
|
if (version == EtagVersion.Any) |
||||
|
{ |
||||
|
return Snapshot; |
||||
|
} |
||||
|
|
||||
|
if (version == EtagVersion.Empty) |
||||
|
{ |
||||
|
return snapshots[0]; |
||||
|
} |
||||
|
|
||||
|
if (version >= 0 && version < snapshots.Count - 1) |
||||
|
{ |
||||
|
return snapshots[(int)version + 1]; |
||||
|
} |
||||
|
|
||||
|
return default(T); |
||||
|
} |
||||
|
|
||||
|
protected sealed override void ApplyEvent(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
var snapshot = OnEvent(@event); |
||||
|
|
||||
|
snapshot.Version = Version + 1; |
||||
|
snapshots.Add(snapshot); |
||||
|
} |
||||
|
|
||||
|
protected sealed override Task ReadAsync(Type type, Guid id) |
||||
|
{ |
||||
|
persistence = store.WithEventSourcing<Guid>(type, id, ApplyEvent); |
||||
|
|
||||
|
return persistence.ReadAsync(); |
||||
|
} |
||||
|
|
||||
|
protected sealed override async Task WriteAsync(Envelope<IEvent>[] events, long previousVersion) |
||||
|
{ |
||||
|
if (events.Length > 0) |
||||
|
{ |
||||
|
var persistedSnapshots = store.GetSnapshotStore<T>(); |
||||
|
|
||||
|
await persistence.WriteEventsAsync(events); |
||||
|
await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, previousVersion + events.Length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) |
||||
|
{ |
||||
|
while (snapshots.Count > previousVersion + 2) |
||||
|
{ |
||||
|
snapshots.RemoveAt(snapshots.Count - 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract T OnEvent(Envelope<IEvent> @event); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public interface IPersistence<TState> |
||||
|
{ |
||||
|
long Version { get; } |
||||
|
|
||||
|
Task WriteEventsAsync(IEnumerable<Envelope<IEvent>> @events); |
||||
|
|
||||
|
Task WriteSnapshotAsync(TState state); |
||||
|
|
||||
|
Task ReadAsync(long expectedVersion = EtagVersion.Any); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using NJsonSchema.Annotations; |
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Actions; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Rules.Models.Actions |
||||
|
{ |
||||
|
[JsonSchema("Medium")] |
||||
|
public class MediumActionDto : RuleActionDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The self issued access token.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string AccessToken { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The optional comma separated list of tags.
|
||||
|
/// </summary>
|
||||
|
public string Tags { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The title, used for the url.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string Title { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The content, either html or markdown.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string Content { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The original home of this content, if it was originally published elsewhere.
|
||||
|
/// </summary>
|
||||
|
public string CanonicalUrl { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates whether the content is markdown or html.
|
||||
|
/// </summary>
|
||||
|
public bool IsHtml { get; set; } |
||||
|
|
||||
|
public override RuleAction ToAction() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new MediumAction()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
<h3 class="wizard-title">Post to Medium</h3> |
||||
|
|
||||
|
<div [formGroup]="actionForm" class="form-horizontal"> |
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="accessToken">Access Token</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="accessToken" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="accessToken" formControlName="accessToken" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The self issued access token. Can be created under <a target="_blank" href="https://medium.com/me/settings">https://medium.com/me/settings</a>. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="title">Title</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="title" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="title" formControlName="title" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The title of the post. Note that this title is used for SEO and when rendering the post as a listing. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="content">Content</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="content" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<textarea class="form-control" id="content" formControlName="content"></textarea> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The body of the post, in a valid, semantic, HTML fragment, or Markdown. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<div class="col col-9 offset-3"> |
||||
|
<div class="form-check"> |
||||
|
<input class="form-check-input" type="checkbox" id="isHtml" formControlName="isHtml" /> |
||||
|
<label class="form-check-label" for="isHtml"> |
||||
|
Is Html content |
||||
|
</label> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="canonicalUrl">Canonical URL</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="canonicalUrl" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="canonicalUrl" formControlName="canonicalUrl" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The original home of this content, if it was originally published elsewhere. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="tags">Tags</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="tags" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="tags" formControlName="tags" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
Comma-separated list of tags. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,6 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
textarea { |
||||
|
height: 150px; |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, Input, OnInit } from '@angular/core'; |
||||
|
import { FormControl, FormGroup, Validators } from '@angular/forms'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-medium-action', |
||||
|
styleUrls: ['./medium-action.component.scss'], |
||||
|
templateUrl: './medium-action.component.html' |
||||
|
}) |
||||
|
export class MediumActionComponent implements OnInit { |
||||
|
@Input() |
||||
|
public action: any; |
||||
|
|
||||
|
@Input() |
||||
|
public actionForm: FormGroup; |
||||
|
|
||||
|
@Input() |
||||
|
public actionFormSubmitted = false; |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.actionForm.setControl('accessToken', |
||||
|
new FormControl(this.action.accessToken || '', [ |
||||
|
Validators.required |
||||
|
])); |
||||
|
|
||||
|
this.actionForm.setControl('title', |
||||
|
new FormControl(this.action.title || '', [ |
||||
|
Validators.required |
||||
|
])); |
||||
|
|
||||
|
this.actionForm.setControl('content', |
||||
|
new FormControl(this.action.content || '', [ |
||||
|
Validators.required |
||||
|
])); |
||||
|
|
||||
|
this.actionForm.setControl('canonicalUrl', |
||||
|
new FormControl(this.action.canonicalUrl || '')); |
||||
|
|
||||
|
this.actionForm.setControl('tags', |
||||
|
new FormControl(this.action.tags || '')); |
||||
|
|
||||
|
this.actionForm.setControl('isHtml', |
||||
|
new FormControl(this.action.isHtml || false)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Actions; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions |
||||
|
{ |
||||
|
public class MediumActionTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_access_token_is_null() |
||||
|
{ |
||||
|
var action = new MediumAction { AccessToken = null, Title = "title", Content = "content" }; |
||||
|
|
||||
|
var errors = await RuleActionValidator.ValidateAsync(action); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
new List<ValidationError> |
||||
|
{ |
||||
|
new ValidationError("Access token is required.", "AccessToken") |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_title_null() |
||||
|
{ |
||||
|
var action = new MediumAction { AccessToken = "token", Title = null, Content = "content" }; |
||||
|
|
||||
|
var errors = await RuleActionValidator.ValidateAsync(action); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
new List<ValidationError> |
||||
|
{ |
||||
|
new ValidationError("Title is required.", "Title") |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_content_is_null() |
||||
|
{ |
||||
|
var action = new MediumAction { AccessToken = "token", Title = "title", Content = null }; |
||||
|
|
||||
|
var errors = await RuleActionValidator.ValidateAsync(action); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
new List<ValidationError> |
||||
|
{ |
||||
|
new ValidationError("Content is required.", "Content") |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_add_error_if_values_are_valid() |
||||
|
{ |
||||
|
var action = new MediumAction { AccessToken = "token", Title = "title", Content = "content" }; |
||||
|
|
||||
|
var errors = await RuleActionValidator.ValidateAsync(action); |
||||
|
|
||||
|
Assert.Empty(errors); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Reflection; |
||||
|
using FakeItEasy; |
||||
|
using Orleans; |
||||
|
using Squidex.Infrastructure.TestHelpers; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Commands |
||||
|
{ |
||||
|
public class DomainObjectGrainFormatterTests |
||||
|
{ |
||||
|
private readonly IGrainCallContext context = A.Fake<IGrainCallContext>(); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_fallback_if_no_method_is_defined() |
||||
|
{ |
||||
|
A.CallTo(() => context.InterfaceMethod) |
||||
|
.Returns(null); |
||||
|
|
||||
|
var result = DomainObjectGrainFormatter.Format(context); |
||||
|
|
||||
|
Assert.Equal("Unknown", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_method_name_if_not_domain_object_method() |
||||
|
{ |
||||
|
var methodInfo = A.Fake<MethodInfo>(); |
||||
|
|
||||
|
A.CallTo(() => methodInfo.Name) |
||||
|
.Returns("Calculate"); |
||||
|
|
||||
|
A.CallTo(() => context.InterfaceMethod) |
||||
|
.Returns(methodInfo); |
||||
|
|
||||
|
var result = DomainObjectGrainFormatter.Format(context); |
||||
|
|
||||
|
Assert.Equal("Calculate", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_nice_method_name_if_domain_object_execute() |
||||
|
{ |
||||
|
var methodInfo = A.Fake<MethodInfo>(); |
||||
|
|
||||
|
A.CallTo(() => methodInfo.Name) |
||||
|
.Returns(nameof(IDomainObjectGrain.ExecuteAsync)); |
||||
|
|
||||
|
A.CallTo(() => context.Arguments) |
||||
|
.Returns(new object[] { new MyCommand() }); |
||||
|
|
||||
|
A.CallTo(() => context.InterfaceMethod) |
||||
|
.Returns(methodInfo); |
||||
|
|
||||
|
var result = DomainObjectGrainFormatter.Format(context); |
||||
|
|
||||
|
Assert.Equal("ExecuteAsync(MyCommand)", result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,300 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
using Squidex.Infrastructure.TestHelpers; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Commands |
||||
|
{ |
||||
|
public class LogSnapshotDomainObjectGrainTests |
||||
|
{ |
||||
|
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>(); |
||||
|
private readonly ISnapshotStore<MyDomainState, Guid> snapshotStore = A.Fake<ISnapshotStore<MyDomainState, Guid>>(); |
||||
|
private readonly IPersistence persistence = A.Fake<IPersistence>(); |
||||
|
private readonly Guid id = Guid.NewGuid(); |
||||
|
private readonly MyDomainObject sut; |
||||
|
|
||||
|
public sealed class ValueChanged : IEvent |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class CreateAuto : MyCommand |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class CreateCustom : MyCommand |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class UpdateAuto : MyCommand |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class UpdateCustom : MyCommand |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class MyDomainObject : LogSnapshotDomainObjectGrain<MyDomainState> |
||||
|
{ |
||||
|
public MyDomainObject(IStore<Guid> store) |
||||
|
: base(store, A.Dummy<ISemanticLog>()) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override Task<object> ExecuteAsync(IAggregateCommand command) |
||||
|
{ |
||||
|
switch (command) |
||||
|
{ |
||||
|
case CreateAuto createAuto: |
||||
|
return CreateAsync(createAuto, c => |
||||
|
{ |
||||
|
RaiseEvent(new ValueChanged { Value = c.Value }); |
||||
|
}); |
||||
|
|
||||
|
case CreateCustom createCustom: |
||||
|
return CreateReturnAsync(createCustom, c => |
||||
|
{ |
||||
|
RaiseEvent(new ValueChanged { Value = c.Value }); |
||||
|
|
||||
|
return "CREATED"; |
||||
|
}); |
||||
|
|
||||
|
case UpdateAuto updateAuto: |
||||
|
return UpdateAsync(updateAuto, c => |
||||
|
{ |
||||
|
RaiseEvent(new ValueChanged { Value = c.Value }); |
||||
|
}); |
||||
|
|
||||
|
case UpdateCustom updateCustom: |
||||
|
return UpdateReturnAsync(updateCustom, c => |
||||
|
{ |
||||
|
RaiseEvent(new ValueChanged { Value = c.Value }); |
||||
|
|
||||
|
return "UPDATED"; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult<object>(null); |
||||
|
} |
||||
|
|
||||
|
protected override MyDomainState OnEvent(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
return new MyDomainState { Value = ((ValueChanged)@event.Payload).Value }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public LogSnapshotDomainObjectGrainTests() |
||||
|
{ |
||||
|
A.CallTo(() => store.WithEventSourcing(typeof(MyDomainObject), id, A<Func<Envelope<IEvent>, Task>>.Ignored)) |
||||
|
.Returns(persistence); |
||||
|
|
||||
|
A.CallTo(() => store.GetSnapshotStore<MyDomainState>()) |
||||
|
.Returns(snapshotStore); |
||||
|
|
||||
|
sut = new MyDomainObject(store); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_latestet_version_when_requesting_state_with_any() |
||||
|
{ |
||||
|
await SetupUpdatedAsync(); |
||||
|
|
||||
|
var result = sut.GetSnapshot(EtagVersion.Any); |
||||
|
|
||||
|
result.Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_empty_version_when_requesting_state_with_empty_version() |
||||
|
{ |
||||
|
await SetupUpdatedAsync(); |
||||
|
|
||||
|
var result = sut.GetSnapshot(EtagVersion.Empty); |
||||
|
|
||||
|
result.Should().BeEquivalentTo(new MyDomainState { Value = 0, Version = -1 }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_specific_version_when_requesting_state_with_specific_version() |
||||
|
{ |
||||
|
await SetupUpdatedAsync(); |
||||
|
|
||||
|
sut.GetSnapshot(0).Should().BeEquivalentTo(new MyDomainState { Value = 4, Version = 0 }); |
||||
|
sut.GetSnapshot(1).Should().BeEquivalentTo(new MyDomainState { Value = 8, Version = 1 }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_get_null_state_when_requesting_state_with_invalid_version() |
||||
|
{ |
||||
|
await SetupUpdatedAsync(); |
||||
|
|
||||
|
Assert.Null(sut.GetSnapshot(-3)); |
||||
|
Assert.Null(sut.GetSnapshot(2)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_instantiate() |
||||
|
{ |
||||
|
Assert.Equal(EtagVersion.Empty, sut.Version); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_write_state_and_events_when_created() |
||||
|
{ |
||||
|
await SetupEmptyAsync(); |
||||
|
|
||||
|
var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); |
||||
|
|
||||
|
A.CallTo(() => snapshotStore.WriteAsync(id, A<MyDomainState>.That.Matches(x => x.Value == 4), -1, 0)) |
||||
|
.MustHaveHappened(); |
||||
|
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1))) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
Assert.True(result.Value is EntityCreatedResult<Guid>); |
||||
|
|
||||
|
Assert.Empty(sut.GetUncomittedEvents()); |
||||
|
|
||||
|
Assert.Equal(4, sut.Snapshot.Value); |
||||
|
Assert.Equal(0, sut.Snapshot.Version); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_write_state_and_events_when_updated() |
||||
|
{ |
||||
|
await SetupCreatedAsync(); |
||||
|
|
||||
|
var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); |
||||
|
|
||||
|
A.CallTo(() => snapshotStore.WriteAsync(id, A<MyDomainState>.That.Matches(x => x.Value == 8), 0, 1)) |
||||
|
.MustHaveHappened(); |
||||
|
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1))) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
Assert.True(result.Value is EntitySavedResult); |
||||
|
|
||||
|
Assert.Empty(sut.GetUncomittedEvents()); |
||||
|
|
||||
|
Assert.Equal(8, sut.Snapshot.Value); |
||||
|
Assert.Equal(1, sut.Snapshot.Version); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_when_already_created() |
||||
|
{ |
||||
|
await SetupCreatedAsync(); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(C(new CreateAuto()))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_when_not_created() |
||||
|
{ |
||||
|
await SetupEmptyAsync(); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(C(new UpdateAuto()))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_custom_result_on_create() |
||||
|
{ |
||||
|
await SetupEmptyAsync(); |
||||
|
|
||||
|
var result = await sut.ExecuteAsync(C(new CreateCustom())); |
||||
|
|
||||
|
Assert.Equal("CREATED", result.Value); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_custom_result_on_update() |
||||
|
{ |
||||
|
await SetupCreatedAsync(); |
||||
|
|
||||
|
var result = await sut.ExecuteAsync(C(new UpdateCustom())); |
||||
|
|
||||
|
Assert.Equal("UPDATED", result.Value); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_when_other_verison_expected() |
||||
|
{ |
||||
|
await SetupCreatedAsync(); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_reset_state_when_writing_snapshot_for_create_failed() |
||||
|
{ |
||||
|
await SetupEmptyAsync(); |
||||
|
|
||||
|
A.CallTo(() => snapshotStore.WriteAsync(A<Guid>.Ignored, A<MyDomainState>.Ignored, -1, 0)) |
||||
|
.Throws(new InvalidOperationException()); |
||||
|
|
||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(C(new CreateAuto()))); |
||||
|
|
||||
|
Assert.Empty(sut.GetUncomittedEvents()); |
||||
|
|
||||
|
Assert.Equal(0, sut.Snapshot.Value); |
||||
|
Assert.Equal(-1, sut.Snapshot.Version); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_reset_state_when_writing_snapshot_for_update_failed() |
||||
|
{ |
||||
|
await SetupCreatedAsync(); |
||||
|
|
||||
|
A.CallTo(() => snapshotStore.WriteAsync(A<Guid>.Ignored, A<MyDomainState>.Ignored, 0, 1)) |
||||
|
.Throws(new InvalidOperationException()); |
||||
|
|
||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(C(new UpdateAuto()))); |
||||
|
|
||||
|
Assert.Empty(sut.GetUncomittedEvents()); |
||||
|
|
||||
|
Assert.Equal(4, sut.Snapshot.Value); |
||||
|
Assert.Equal(0, sut.Snapshot.Version); |
||||
|
} |
||||
|
|
||||
|
private async Task SetupCreatedAsync() |
||||
|
{ |
||||
|
await sut.OnActivateAsync(id); |
||||
|
|
||||
|
await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); |
||||
|
} |
||||
|
|
||||
|
private async Task SetupUpdatedAsync() |
||||
|
{ |
||||
|
await SetupCreatedAsync(); |
||||
|
|
||||
|
await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); |
||||
|
} |
||||
|
|
||||
|
private async Task SetupEmptyAsync() |
||||
|
{ |
||||
|
await sut.OnActivateAsync(id); |
||||
|
} |
||||
|
|
||||
|
private static J<IAggregateCommand> C(IAggregateCommand command) |
||||
|
{ |
||||
|
return command.AsJ(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.TestHelpers |
||||
|
{ |
||||
|
public sealed class MyDomainObject : DomainObjectGrain<MyDomainState> |
||||
|
{ |
||||
|
public sealed class ValueChanged : IEvent |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class CreateAuto : MyCommand |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class CreateCustom : MyCommand |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class UpdateAuto : MyCommand |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public sealed class UpdateCustom : MyCommand |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
} |
||||
|
|
||||
|
public MyDomainObject(IStore<Guid> store) |
||||
|
: base(store, A.Dummy<ISemanticLog>()) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override Task<object> ExecuteAsync(IAggregateCommand command) |
||||
|
{ |
||||
|
switch (command) |
||||
|
{ |
||||
|
case CreateAuto createAuto: |
||||
|
return CreateAsync(createAuto, c => |
||||
|
{ |
||||
|
RaiseEvent(new ValueChanged { Value = c.Value }); |
||||
|
}); |
||||
|
|
||||
|
case CreateCustom createCustom: |
||||
|
return CreateReturnAsync(createCustom, c => |
||||
|
{ |
||||
|
RaiseEvent(new ValueChanged { Value = c.Value }); |
||||
|
|
||||
|
return "CREATED"; |
||||
|
}); |
||||
|
|
||||
|
case UpdateAuto updateAuto: |
||||
|
return UpdateAsync(updateAuto, c => |
||||
|
{ |
||||
|
RaiseEvent(new ValueChanged { Value = c.Value }); |
||||
|
}); |
||||
|
|
||||
|
case UpdateCustom updateCustom: |
||||
|
return UpdateReturnAsync(updateCustom, c => |
||||
|
{ |
||||
|
RaiseEvent(new ValueChanged { Value = c.Value }); |
||||
|
|
||||
|
return "UPDATED"; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult<object>(null); |
||||
|
} |
||||
|
|
||||
|
protected override MyDomainState OnEvent(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
return new MyDomainState { Value = ((ValueChanged)@event.Payload).Value }; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue