diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index 57f857877..d9c2c24e9 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Core.Apps var newMaster = state.Master.Language != language ? state.Master : - state.Languages.Values.FirstOrDefault(); + newLanguages.Values.FirstOrDefault(); state = new State(newLanguages, newMaster); } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs similarity index 53% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs rename to src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs index d6a76ea84..09718ad8f 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs @@ -1,5 +1,5 @@ // ========================================================================== -// CreateWebhook.cs +// WebhookAction.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,15 +9,18 @@ using System; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Core.Rules.Actions { - public sealed class CreateWebhook : WebhookEditCommand + [TypeName(nameof(WebhookAction))] + public sealed class WebhookAction : RuleAction { - public string SharedSecret { get; } = RandomHash.New(); + public Uri Url { get; set; } - public CreateWebhook() + public string SharedSecret { get; set; } + + public override T Accept(IRuleActionVisitor visitor) { - WebhookId = Guid.NewGuid(); + return visitor.Visit(this); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs new file mode 100644 index 000000000..8c999b7ae --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IActionVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules.Actions; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public interface IRuleActionVisitor + { + T Visit(WebhookAction action); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs new file mode 100644 index 000000000..b91c7e604 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IRuleTriggerVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules.Triggers; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public interface IRuleTriggerVisitor + { + T Visit(ContentChangedTrigger trigger); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs new file mode 100644 index 000000000..163a66661 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// JsonRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Rules.Json +{ + public sealed class JsonRule + { + [JsonProperty] + public RuleTrigger Trigger { get; set; } + + [JsonProperty] + public RuleAction Action { get; set; } + + [JsonProperty] + public bool IsEnabled { get; set; } + + public JsonRule() + { + } + + public JsonRule(Rule rule) + { + SimpleMapper.Map(rule, this); + } + + public Rule ToRule() + { + var rule = new Rule(Trigger, Action); + + if (!IsEnabled) + { + rule.Disable(); + } + + return rule; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs new file mode 100644 index 000000000..cd3e9707f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// RuleConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.Rules.Json +{ + public sealed class RuleConverter : JsonClassConverter + { + protected override Rule ReadValue(JsonReader reader, JsonSerializer serializer) + { + return serializer.Deserialize(reader).ToRule(); + } + + protected override void WriteValue(JsonWriter writer, Rule value, JsonSerializer serializer) + { + serializer.Serialize(writer, new JsonRule(value)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs new file mode 100644 index 000000000..8595d84a8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Rule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public sealed class Rule + { + private RuleTrigger trigger; + private RuleAction action; + private bool isEnabled = true; + + public RuleTrigger Trigger + { + get { return trigger; } + } + + public RuleAction Action + { + get { return action; } + } + + public bool IsEnabled + { + get { return isEnabled; } + } + + public Rule(RuleTrigger trigger, RuleAction action) + { + Guard.NotNull(trigger, nameof(trigger)); + Guard.NotNull(action, nameof(action)); + + this.trigger = trigger; + this.action = action; + } + + public void Enable() + { + this.isEnabled = true; + } + + public void Disable() + { + this.isEnabled = false; + } + + public void Update(RuleTrigger newTrigger) + { + Guard.NotNull(newTrigger, nameof(newTrigger)); + + if (newTrigger.GetType() != trigger.GetType()) + { + throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); + } + + trigger = newTrigger; + } + + public void Update(RuleAction newAction) + { + Guard.NotNull(newAction, nameof(newAction)); + + if (newAction.GetType() != action.GetType()) + { + throw new ArgumentException("New action has another type.", nameof(newAction)); + } + + action = newAction; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs new file mode 100644 index 000000000..483abdff9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// RuleAction.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Rules +{ + public abstract class RuleAction + { + public abstract T Accept(IRuleActionVisitor visitor); + } +} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs similarity index 60% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs rename to src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs index 8951c3da6..3117fd8e2 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookJob.cs +// RuleJob.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,24 +9,24 @@ using System; using NodaTime; -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Core.Rules { - public sealed class WebhookJob + public sealed class RuleJob { - public Guid Id { get; set; } + public Guid RuleId { get; set; } public Guid AppId { get; set; } - public Guid WebhookId { get; set; } - - public Uri RequestUrl { get; set; } + public string EventName { get; set; } - public string RequestBody { get; set; } + public string ActionName { get; set; } - public string RequestSignature { get; set; } + public string Description { get; set; } - public string EventName { get; set; } + public Instant Created { get; set; } public Instant Expires { get; set; } + + public RuleJobData ActionData { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs new file mode 100644 index 000000000..ad9cef6c1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// RuleJobData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public sealed class RuleJobData : Dictionary + { + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs new file mode 100644 index 000000000..15284960c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// RuleTrigger.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Rules +{ + public abstract class RuleTrigger + { + public abstract T Accept(IRuleTriggerVisitor visitor); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs new file mode 100644 index 000000000..cc11d969c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// ContentChangedTrigger.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers +{ + [TypeName(nameof(ContentChangedTrigger))] + public sealed class ContentChangedTrigger : RuleTrigger + { + public List Schemas { get; set; } + + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs similarity index 80% rename from src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs rename to src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs index 169e85b7d..600fcaf6e 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookSchema.cs +// ContentChangedTriggerSchema.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,9 +8,9 @@ using System; -namespace Squidex.Domain.Apps.Core.Webhooks +namespace Squidex.Domain.Apps.Core.Rules.Triggers { - public sealed class WebhookSchema + public sealed class ContentChangedTriggerSchema { public Guid SchemaId { get; set; } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs index bf94a3a13..710581ef1 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs @@ -84,7 +84,6 @@ namespace Squidex.Domain.Apps.Core.Schemas new TagsField(id, name, partitioning, (TagsFieldProperties)properties)); typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime"); - typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References"); } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs similarity index 71% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs rename to src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs index 22c33345f..5ed5bdea6 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs +++ b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs @@ -1,14 +1,14 @@ // ========================================================================== -// UpdateWebhook.cs +// SquidexCoreModel.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Core { - public sealed class UpdateWebhook : WebhookEditCommand + public static class SquidexCoreModel { } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs new file mode 100644 index 000000000..aae33d8ca --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs @@ -0,0 +1,115 @@ +// ========================================================================== +// WebhookActionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Http; + +namespace Squidex.Domain.Apps.Core.HandleRules.ActionHandlers +{ + public sealed class WebhookActionHandler : RuleActionHandler + { + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); + + private readonly JsonSerializer serializer; + + public WebhookActionHandler(JsonSerializer serializer) + { + Guard.NotNull(serializer, nameof(serializer)); + + this.serializer = serializer; + } + + protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, WebhookAction action) + { + var body = CreatePayload(@event, eventName); + + var signature = $"{body.ToString(Formatting.Indented)}{action.SharedSecret}".Sha256Base64(); + + var ruleDescription = $"Send event to webhook {action.Url}"; + var ruleData = new RuleJobData + { + ["RequestUrl"] = action.Url, + ["RequestBody"] = body, + ["RequestSignature"] = signature + }; + + return (ruleDescription, ruleData); + } + + private JObject CreatePayload(Envelope @event, string eventName) + { + return new JObject( + new JProperty("type", eventName), + new JProperty("payload", JObject.FromObject(@event.Payload, serializer)), + new JProperty("timestamp", @event.Headers.Timestamp().ToString())); + } + + public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + { + var requestBody = job["RequestBody"].ToString(Formatting.Indented); + var request = BuildRequest(job, requestBody); + + HttpResponseMessage response = null; + + try + { + using (var client = new HttpClient { Timeout = Timeout }) + { + response = await client.SendAsync(request); + + var responseString = await response.Content.ReadAsStringAsync(); + var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString, TimeSpan.Zero, false); + + return (requestDump, null); + } + } + catch (Exception ex) + { + if (request != null) + { + var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString(), TimeSpan.Zero, false); + + return (requestDump, ex); + } + else + { + var requestDump = ex.ToString(); + + return (requestDump, ex); + } + } + } + + private static HttpRequestMessage BuildRequest(Dictionary job, string requestBody) + { + var requestUrl = job["RequestUrl"].ToString(); + var requestSignature = job["RequestSignature"].ToString(); + + var request = new HttpRequestMessage(HttpMethod.Post, requestUrl) + { + Content = new StringContent(requestBody, Encoding.UTF8, "application/json") + }; + + request.Headers.Add("X-Signature", requestSignature); + request.Headers.Add("User-Agent", "Squidex Webhook"); + + return request; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs new file mode 100644 index 000000000..e2078e988 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// IRuleActionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public interface IRuleActionHandler + { + Type ActionType { get; } + + (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, RuleAction action); + + Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData data); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs new file mode 100644 index 000000000..a1507bc5a --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// IRuleTriggerHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public interface IRuleTriggerHandler + { + Type TriggerType { get; } + + bool Triggers(Envelope @event, RuleTrigger trigger); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs new file mode 100644 index 000000000..a97957bdc --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// RuleActionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public abstract class RuleActionHandler : IRuleActionHandler where T : RuleAction + { + Type IRuleActionHandler.ActionType + { + get { return typeof(T); } + } + + (string Description, RuleJobData Data) IRuleActionHandler.CreateJob(Envelope @event, string eventName, RuleAction action) + { + return CreateJob(@event, eventName, (T)action); + } + + protected abstract (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, T action); + + public abstract Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job); + } +} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs similarity index 80% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs rename to src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs index bc8584b5b..76316497a 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs @@ -1,14 +1,14 @@ // ========================================================================== -// WebhookResult.cs +// RuleResult.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Core.HandleRules { - public enum WebhookResult + public enum RuleResult { Pending, Success, diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs new file mode 100644 index 000000000..35fecb5ac --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// RuleService.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public class RuleService + { + private const string ContentPrefix = "Content"; + private static readonly Duration ExpirationTime = Duration.FromDays(2); + private readonly Dictionary ruleActionHandlers; + private readonly Dictionary ruleTriggerHandlers; + private readonly TypeNameRegistry typeNameRegistry; + private readonly IClock clock; + + public RuleService( + IEnumerable ruleTriggerHandlers, + IEnumerable ruleActionHandlers, + IClock clock, + TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(ruleTriggerHandlers, nameof(ruleTriggerHandlers)); + Guard.NotNull(ruleActionHandlers, nameof(ruleActionHandlers)); + Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); + Guard.NotNull(clock, nameof(clock)); + + this.typeNameRegistry = typeNameRegistry; + + this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); + this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); + + this.clock = clock; + } + + public virtual RuleJob CreateJob(Rule rule, Envelope @event) + { + Guard.NotNull(rule, nameof(rule)); + Guard.NotNull(@event, nameof(@event)); + + if (!(@event.Payload is AppEvent appEvent)) + { + return null; + } + + var actionType = rule.Action.GetType(); + + if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) + { + return null; + } + + if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) + { + return null; + } + + var appEventEnvelope = @event.To(); + + if (!triggerHandler.Triggers(appEventEnvelope, rule.Trigger)) + { + return null; + } + + var eventName = CreateEventName(appEvent); + + var now = clock.GetCurrentInstant(); + + var actionName = typeNameRegistry.GetName(actionType); + var actionData = actionHandler.CreateJob(appEventEnvelope, eventName, rule.Action); + + var job = new RuleJob + { + RuleId = Guid.NewGuid(), + ActionName = actionName, + ActionData = actionData.Data, + AppId = appEvent.AppId.Id, + Created = now, + EventName = eventName, + Expires = now.Plus(ExpirationTime), + Description = actionData.Description + }; + + return job; + } + + public virtual async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, RuleJobData job) + { + try + { + var actionType = typeNameRegistry.GetType(actionName); + var actionWatch = Stopwatch.StartNew(); + + var result = await ruleActionHandlers[actionType].ExecuteJobAsync(job); + + actionWatch.Stop(); + + var dumpBuilder = new StringBuilder(result.Dump); + + dumpBuilder.AppendLine(); + dumpBuilder.AppendFormat("Elapsed {0}.", actionWatch.Elapsed); + dumpBuilder.AppendLine(); + + if (result.Exception is TimeoutException || result.Exception is OperationCanceledException) + { + dumpBuilder.AppendLine(); + dumpBuilder.AppendLine("Action timed out."); + + return (dumpBuilder.ToString(), RuleResult.Timeout, actionWatch.Elapsed); + } + else if (result.Exception != null) + { + return (dumpBuilder.ToString(), RuleResult.Failed, actionWatch.Elapsed); + } + else + { + return (dumpBuilder.ToString(), RuleResult.Success, actionWatch.Elapsed); + } + } + catch (Exception ex) + { + return (ex.ToString(), RuleResult.Failed, TimeSpan.Zero); + } + } + + private string CreateEventName(AppEvent appEvent) + { + var eventName = typeNameRegistry.GetName(appEvent.GetType()); + + if (appEvent is SchemaEvent schemaEvent) + { + if (eventName.StartsWith(ContentPrefix, StringComparison.Ordinal)) + { + eventName = eventName.Substring(ContentPrefix.Length); + } + + return $"{schemaEvent.SchemaId.Name.ToPascalCase()}{eventName}"; + } + + return eventName; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs new file mode 100644 index 000000000..131bf9ac1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// RuleTriggerHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public abstract class RuleTriggerHandler : IRuleTriggerHandler where T : RuleTrigger + { + public Type TriggerType + { + get { return typeof(T); } + } + + bool IRuleTriggerHandler.Triggers(Envelope @event, RuleTrigger trigger) + { + return Triggers(@event, (T)trigger); + } + + protected abstract bool Triggers(Envelope @event, T trigger); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs new file mode 100644 index 000000000..e0b8ef424 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// ContentChangedTriggerHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules.Triggers +{ + public sealed class ContentChangedTriggerHandler : RuleTriggerHandler + { + protected override bool Triggers(Envelope @event, ContentChangedTrigger trigger) + { + if (trigger.Schemas != null && @event.Payload is SchemaEvent schemaEvent) + { + foreach (var schema in trigger.Schemas) + { + if (MatchsSchema(schema, schemaEvent) && MatchsType(schema, schemaEvent)) + { + return true; + } + } + } + + return false; + } + + private static bool MatchsSchema(ContentChangedTriggerSchema schema, SchemaEvent @event) + { + return @event.SchemaId.Id == schema.SchemaId; + } + + private static bool MatchsType(ContentChangedTriggerSchema schema, SchemaEvent @event) + { + return + (schema.SendCreate && @event is ContentCreated) || + (schema.SendUpdate && @event is ContentUpdated) || + (schema.SendDelete && @event is ContentDeleted) || + (schema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index ec9a21f4c..e5d74893a 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs similarity index 57% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs rename to src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs index 1cab7fdce..9128687ff 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs @@ -1,18 +1,21 @@ // ========================================================================== -// WebhookCreated.cs +// RuleCreated.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== +using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.CQRS.Events; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Events.Rules { - [EventType(nameof(WebhookCreated))] - public sealed class WebhookCreated : WebhookEditEvent + [EventType(nameof(RuleCreated))] + public sealed class RuleCreated : RuleEvent { - public string SharedSecret { get; set; } + public RuleTrigger Trigger { get; set; } + + public RuleAction Action { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs similarity index 69% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs rename to src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs index a04e86ab6..696acb36b 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookDeleted.cs +// RuleDeleted.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,10 +8,10 @@ using Squidex.Infrastructure.CQRS.Events; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Events.Rules { - [EventType(nameof(WebhookDeleted), 2)] - public sealed class WebhookDeleted : WebhookEvent + [EventType(nameof(RuleDeleted))] + public sealed class RuleDeleted : RuleEvent { } } diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs similarity index 69% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs rename to src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs index c40793935..265d716bc 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookUpdated.cs +// RuleDisabled.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,10 +8,10 @@ using Squidex.Infrastructure.CQRS.Events; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Events.Rules { - [EventType(nameof(WebhookUpdated))] - public sealed class WebhookUpdated : WebhookEditEvent + [EventType(nameof(RuleDisabled))] + public sealed class RuleDisabled : RuleEvent { } } diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs new file mode 100644 index 000000000..f94d1f58f --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// RuleEnabled.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Rules +{ + [EventType(nameof(RuleEnabled))] + public sealed class RuleEnabled : RuleEvent + { + } +} diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs similarity index 68% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs rename to src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs index 99fc08697..d0ee5604c 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookEvent.cs +// RuleEvent.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,10 +8,10 @@ using System; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Events.Rules { - public abstract class WebhookEvent : AppEvent + public abstract class RuleEvent : AppEvent { - public Guid WebhookId { get; set; } + public Guid RuleId { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs new file mode 100644 index 000000000..e4d828138 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// RuleUpdated.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Rules +{ + [EventType(nameof(RuleUpdated))] + public sealed class RuleUpdated : RuleEvent + { + public RuleTrigger Trigger { get; set; } + + public RuleAction Action { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs new file mode 100644 index 000000000..8de79c181 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// RuleEventDispatcher.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Events.Rules.Utils +{ + public static class RuleEventDispatcher + { + public static Rule Create(RuleCreated @event) + { + return new Rule(@event.Trigger, @event.Action); + } + + public static void Apply(this Rule rule, RuleUpdated @event) + { + if (@event.Trigger != null) + { + rule.Update(@event.Trigger); + } + + if (@event.Action != null) + { + rule.Update(@event.Action); + } + } + + public static void Apply(this Rule rule, RuleEnabled @event) + { + rule.Enable(); + } + + public static void Apply(this Rule rule, RuleDisabled @event) + { + rule.Disable(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs similarity index 70% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs rename to src/Squidex.Domain.Apps.Events/SquidexEvents.cs index 462532dfe..ab5d1ec98 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs +++ b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs @@ -1,14 +1,14 @@ // ========================================================================== -// DeleteWebhook.cs +// SquidexEvents.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Events { - public sealed class DeleteWebhook : WebhookAggregateCommand + public static class SquidexEvents { } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index 0fdab29f8..51c627fec 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps.Utils; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs index 1915a40bd..2685c24fc 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index b76240365..077fa6cd1 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs similarity index 98% rename from src/Squidex.Domain.Apps.Read.MongoDb/Utils/EntityMapper.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs index d4b999cf3..0557c28f4 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs @@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Utils +namespace Squidex.Domain.Apps.Read.MongoDb { public static class EntityMapper { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs index 15610b5c8..53368fd51 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs @@ -14,7 +14,6 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.History; using Squidex.Domain.Apps.Read.History.Repositories; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Utils/MongoCollectionExtensions.cs b/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs similarity index 98% rename from src/Squidex.Domain.Apps.Read.MongoDb/Utils/MongoCollectionExtensions.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs index 78cd17175..c9cc5cd8c 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Utils/MongoCollectionExtensions.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Utils +namespace Squidex.Domain.Apps.Read.MongoDb { public static class MongoCollectionExtensions { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs new file mode 100644 index 000000000..be9369804 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// MongoRuleEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Read.MongoDb.Rules +{ + public class MongoRuleEntity : MongoEntity, IRuleEntity + { + [BsonRequired] + [BsonElement] + public Guid AppId { get; set; } + + [BsonRequired] + [BsonElement] + public RefToken CreatedBy { get; set; } + + [BsonRequired] + [BsonElement] + public RefToken LastModifiedBy { get; set; } + + [BsonRequired] + [BsonElement] + public long Version { get; set; } + + [BsonRequired] + [BsonElement] + [BsonJson] + public Rule Rule { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs similarity index 54% rename from src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventEntity.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs index 18f212967..f060b9294 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoWebhookEventEntity.cs +// MongoRuleEventEntity.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,43 +9,26 @@ using System; using MongoDB.Bson.Serialization.Attributes; using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Rules; using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks +namespace Squidex.Domain.Apps.Read.MongoDb.Rules { - public sealed class MongoWebhookEventEntity : MongoEntity, IWebhookEventEntity + public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity { - private WebhookJob job; - [BsonRequired] [BsonElement] public Guid AppId { get; set; } [BsonRequired] [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] - public Uri RequestUrl { get; set; } - - [BsonRequired] - [BsonElement] - public string RequestBody { get; set; } - - [BsonRequired] - [BsonElement] - public string RequestSignature { get; set; } - - [BsonRequired] - [BsonElement] - public string EventName { get; set; } + public string LastDump { get; set; } [BsonRequired] [BsonElement] - public string LastDump { get; set; } + public int NumCalls { get; set; } [BsonRequired] [BsonElement] @@ -57,23 +40,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks [BsonRequired] [BsonElement] - public int NumCalls { get; set; } + public bool IsSending { get; set; } [BsonRequired] [BsonElement] - public bool IsSending { get; set; } + public RuleResult Result { get; set; } [BsonRequired] [BsonElement] - public WebhookResult Result { get; set; } + public RuleJobResult JobResult { get; set; } [BsonRequired] [BsonElement] - public WebhookJobResult JobResult { get; set; } - - public WebhookJob Job - { - get { return job ?? (job = SimpleMapper.Map(this, new WebhookJob())); } - } + [BsonJson] + public RuleJob Job { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs similarity index 56% rename from src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventRepository.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs index d6a4325fc..db39b6675 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoWebhookEventRepository.cs +// MongoRuleEventRepository.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -12,32 +12,28 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks +namespace Squidex.Domain.Apps.Read.MongoDb.Rules { - public sealed class MongoWebhookEventRepository : MongoRepositoryBase, IWebhookEventRepository + public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository { - private readonly IClock clock; - - public MongoWebhookEventRepository(IMongoDatabase database, IClock clock) + public MongoRuleEventRepository(IMongoDatabase database) : base(database) { - Guard.NotNull(clock, nameof(clock)); - - this.clock = clock; } protected override string CollectionName() { - return "WebhookEvents"; + return "RuleEvents"; } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override Task SetupCollectionAsync(IMongoCollection collection) { return Task.WhenAll( collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt).Descending(x => x.IsSending)), @@ -45,29 +41,27 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero })); } - public Task QueryPendingAsync(Func callback, CancellationToken cancellationToken = default(CancellationToken)) + public Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)) { - var now = clock.GetCurrentInstant(); - return Collection.Find(x => x.NextAttempt < now && !x.IsSending).ForEachAsync(callback, cancellationToken); } - public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) + public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) { - var webhookEventEntities = + var ruleEventEntities = await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created) .ToListAsync(); - return webhookEventEntities; + return ruleEventEntities; } - public async Task FindAsync(Guid id) + public async Task FindAsync(Guid id) { - var webhookEventEntity = + var ruleEvent = await Collection.Find(x => x.Id == id) .FirstOrDefaultAsync(); - return webhookEventEntity; + return ruleEvent; } public async Task CountByAppAsync(Guid appId) @@ -80,35 +74,20 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt)); } - public Task TraceSendingAsync(Guid jobId) + public Task EnqueueAsync(RuleJob job, Instant nextAttempt) { - return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true)); - } - - public Task EnqueueAsync(WebhookJob job, Instant nextAttempt) - { - var entity = SimpleMapper.Map(job, new MongoWebhookEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt }); + var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = nextAttempt, NextAttempt = nextAttempt }); return Collection.InsertOneIfNotExistsAsync(entity); } - public Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextAttempt) + public Task MarkSendingAsync(Guid jobId) { - WebhookJobResult jobResult; - - if (result != WebhookResult.Success && nextAttempt == null) - { - jobResult = WebhookJobResult.Failed; - } - else if (result != WebhookResult.Success && nextAttempt.HasValue) - { - jobResult = WebhookJobResult.Retry; - } - else - { - jobResult = WebhookJobResult.Success; - } + return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true)); + } + public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt) + { return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.Result, result) .Set(x => x.LastDump, dump) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs new file mode 100644 index 000000000..ab9ff0204 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// MongoRuleRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Read.MongoDb.Rules +{ + public partial class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, IEventConsumer + { + private static readonly List EmptyRules = new List(); + private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); + private Dictionary> inMemoryRules; + + public MongoRuleRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Projections_Rules"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return Task.WhenAll(collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId))); + } + + public async Task> QueryByAppAsync(Guid appId) + { + var entities = + await Collection.Find(x => x.AppId == appId) + .ToListAsync(); + + return entities.OfType().ToList(); + } + + public async Task> QueryCachedByAppAsync(Guid appId) + { + await EnsureRulesLoadedAsync(); + + return inMemoryRules.GetOrDefault(appId)?.ToList() ?? EmptyRules; + } + + private async Task EnsureRulesLoadedAsync() + { + if (inMemoryRules == null) + { + try + { + await lockObject.WaitAsync(); + + if (inMemoryRules == null) + { + inMemoryRules = new Dictionary>(); + + var webhooks = + await Collection.Find(new BsonDocument()) + .ToListAsync(); + + foreach (var webhook in webhooks) + { + inMemoryRules.GetOrAddNew(webhook.AppId).Add(webhook); + } + } + } + finally + { + lockObject.Release(); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs new file mode 100644 index 000000000..26be2002a --- /dev/null +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// MongoRuleRepository_EventHandling.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Domain.Apps.Events.Rules.Utils; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Read.MongoDb.Rules +{ + public partial class MongoRuleRepository + { + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^rule-"; } + } + + public Task On(Envelope @event) + { + return this.DispatchActionAsync(@event.Payload, @event.Headers); + } + + protected async Task On(RuleCreated @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + await Collection.CreateAsync(@event, headers, w => + { + w.Rule = RuleEventDispatcher.Create(@event); + + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleUpdated @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + await Collection.UpdateAsync(@event, headers, w => + { + w.Rule.Apply(@event); + + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleEnabled @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + await Collection.UpdateAsync(@event, headers, w => + { + w.Rule.Apply(@event); + + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleDisabled @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + await Collection.UpdateAsync(@event, headers, w => + { + w.Rule.Apply(@event); + + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleDeleted @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + inMemoryRules.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.RuleId); + + await Collection.DeleteManyAsync(x => x.Id == @event.RuleId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs index b4a662186..772620b38 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs @@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas.Old; using Squidex.Domain.Apps.Events.Schemas.Utils; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs deleted file mode 100644 index 239ea346a..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// MongoWebhookEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Webhooks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks -{ - public class MongoWebhookEntity : MongoEntity, IWebhookEntity - { - [BsonRequired] - [BsonElement] - public Uri Url { get; set; } - - [BsonRequired] - [BsonElement] - public Guid AppId { get; set; } - - [BsonRequired] - [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken CreatedBy { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken LastModifiedBy { get; set; } - - [BsonRequired] - [BsonElement] - public string SharedSecret { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalSucceeded { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalFailed { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalTimedout { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalRequestTime { get; set; } - - [BsonRequired] - [BsonElement] - public List Schemas { get; set; } - - [BsonRequired] - [BsonElement] - public List SchemaIds { get; set; } - - IEnumerable IWebhookEntity.Schemas - { - get { return Schemas; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs deleted file mode 100644 index d4dc24ac9..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs +++ /dev/null @@ -1,119 +0,0 @@ -// ========================================================================== -// MongoWebhookRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Read.Webhooks; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks -{ - public partial class MongoWebhookRepository : MongoRepositoryBase, IWebhookRepository, IEventConsumer - { - private static readonly List EmptyWebhooks = new List(); - private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); - private Dictionary> inMemoryWebhooks; - - public MongoWebhookRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Projections_SchemaWebhooks"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection) - { - return Task.WhenAll( - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaIds))); - } - - public async Task> QueryByAppAsync(Guid appId) - { - var entities = - await Collection.Find(x => x.AppId == appId) - .ToListAsync(); - - return entities.OfType().ToList(); - } - - public async Task> QueryCachedByAppAsync(Guid appId) - { - await EnsureWebooksLoadedAsync(); - - return inMemoryWebhooks.GetOrDefault(appId) ?? EmptyWebhooks; - } - - public async Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed) - { - var webhookEntity = - await Collection.Find(x => x.Id == webhookId) - .FirstOrDefaultAsync(); - - if (webhookEntity != null) - { - switch (result) - { - case WebhookResult.Success: - webhookEntity.TotalSucceeded++; - break; - case WebhookResult.Failed: - webhookEntity.TotalFailed++; - break; - case WebhookResult.Timeout: - webhookEntity.TotalTimedout++; - break; - } - - webhookEntity.TotalRequestTime += (long)elapsed.TotalMilliseconds; - - await Collection.ReplaceOneAsync(x => x.Id == webhookId, webhookEntity); - } - } - - private async Task EnsureWebooksLoadedAsync() - { - if (inMemoryWebhooks == null) - { - try - { - await lockObject.WaitAsync(); - - if (inMemoryWebhooks == null) - { - inMemoryWebhooks = new Dictionary>(); - - var webhooks = - await Collection.Find(new BsonDocument()) - .ToListAsync(); - - foreach (var webhook in webhooks) - { - inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); - } - } - } - finally - { - lockObject.Release(); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs deleted file mode 100644 index 9646e8d62..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// MongoSchemaWebhookRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Domain.Apps.Events.Webhooks; -using Squidex.Domain.Apps.Read.MongoDb.Utils; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks -{ - public partial class MongoWebhookRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "(^webhook-)|(^schema-)"; } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected async Task On(WebhookCreated @event, EnvelopeHeaders headers) - { - await EnsureWebooksLoadedAsync(); - - await Collection.CreateAsync(@event, headers, w => - { - SimpleMapper.Map(@event, w); - - w.SchemaIds = w.Schemas.Select(x => x.SchemaId).ToList(); - - inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); - }); - } - - protected async Task On(WebhookUpdated @event, EnvelopeHeaders headers) - { - await EnsureWebooksLoadedAsync(); - - await Collection.UpdateAsync(@event, headers, w => - { - SimpleMapper.Map(@event, w); - - w.SchemaIds = w.Schemas.Select(x => x.SchemaId).ToList(); - - inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); - }); - } - - protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers) - { - await EnsureWebooksLoadedAsync(); - - var webhooks = - await Collection.Find(t => t.SchemaIds.Contains(@event.SchemaId.Id)) - .ToListAsync(); - - foreach (var webhook in webhooks) - { - webhook.Schemas.RemoveAll(s => s.SchemaId == @event.SchemaId.Id); - - webhook.SchemaIds = webhook.Schemas.Select(x => x.SchemaId).ToList(); - - inMemoryWebhooks.GetOrAddNew(webhook.AppId).RemoveAll(x => x.Id == webhook.Id); - inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); - - await Collection.ReplaceOneAsync(x => x.Id == webhook.Id, webhook); - } - } - - protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers) - { - await EnsureWebooksLoadedAsync(); - - inMemoryWebhooks.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.WebhookId); - - await Collection.DeleteManyAsync(x => x.Id == @event.WebhookId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlansProvider.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlansProvider.cs index 884dfb3a1..5128fdfb2 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlansProvider.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlansProvider.cs @@ -16,7 +16,11 @@ namespace Squidex.Domain.Apps.Read.Apps.Services bool IsConfiguredPlan(string planId); - IAppLimitsPlan GetPlanForApp(IAppEntity entity); + IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app); + + IAppLimitsPlan GetPlanUpgrade(string planId); + + IAppLimitsPlan GetPlanForApp(IAppEntity app); IAppLimitsPlan GetPlan(string planId); } diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs index 8e82a4a31..becf14af4 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.Apps.Repositories; -using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.CQRS.Events; diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs index ee44aac44..ed2e6181a 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs @@ -24,18 +24,25 @@ namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations MaxContributors = -1 }; - private readonly Dictionary config; + private readonly Dictionary plansById; + private readonly List plansList; public ConfigAppPlansProvider(IEnumerable config) { Guard.NotNull(config, nameof(config)); - this.config = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); + plansList = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToList(); + plansById = plansList.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); } public IEnumerable GetAvailablePlans() { - return config.Values; + return plansList; + } + + public bool IsConfiguredPlan(string planId) + { + return planId != null && plansById.ContainsKey(planId); } public IAppLimitsPlan GetPlanForApp(IAppEntity app) @@ -47,12 +54,33 @@ namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations public IAppLimitsPlan GetPlan(string planId) { - return config.GetOrDefault(planId ?? string.Empty) ?? config.Values.FirstOrDefault() ?? Infinite; + return GetPlanCore(planId); } - public bool IsConfiguredPlan(string planId) + public IAppLimitsPlan GetPlanUpgradeForApp(IAppEntity app) + { + Guard.NotNull(app, nameof(app)); + + return GetPlanUpgrade(app.PlanId); + } + + public IAppLimitsPlan GetPlanUpgrade(string planId) + { + var plan = GetPlanCore(planId); + + var nextPlanIndex = plansList.IndexOf(plan); + + if (nextPlanIndex >= 0 && nextPlanIndex < plansList.Count - 1) + { + return plansList[nextPlanIndex + 1]; + } + + return null; + } + + private ConfigAppLimitsPlan GetPlanCore(string planId) { - return planId != null && config.ContainsKey(planId); + return plansById.GetOrDefault(planId ?? string.Empty) ?? plansById.Values.FirstOrDefault() ?? Infinite; } } } diff --git a/src/Squidex.Domain.Apps.Read/Utils/CachingProviderBase.cs b/src/Squidex.Domain.Apps.Read/CachingProviderBase.cs similarity index 95% rename from src/Squidex.Domain.Apps.Read/Utils/CachingProviderBase.cs rename to src/Squidex.Domain.Apps.Read/CachingProviderBase.cs index 6e5330c04..5883b29ab 100644 --- a/src/Squidex.Domain.Apps.Read/Utils/CachingProviderBase.cs +++ b/src/Squidex.Domain.Apps.Read/CachingProviderBase.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Caching.Memory; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Read.Utils +namespace Squidex.Domain.Apps.Read { public abstract class CachingProviderBase { diff --git a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs index 14fa93c78..7f3fd63ce 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs @@ -14,7 +14,6 @@ using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Read.Contents.Edm diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs index cfabcb279..f20d7cf4c 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs @@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Assets.Repositories; using Squidex.Domain.Apps.Read.Schemas.Repositories; -using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Tasks; diff --git a/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs new file mode 100644 index 000000000..6de2db9dd --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IRuleEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Read.Rules +{ + public interface IRuleEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion + { + Rule Rule { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs similarity index 60% rename from src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs rename to src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs index e35b23747..631ad2322 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs @@ -1,5 +1,5 @@ // ========================================================================== -// IWebhookEventEntity.cs +// IRuleEventEntity.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,18 +7,20 @@ // ========================================================================== using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Read.Rules { - public interface IWebhookEventEntity : IEntity + public interface IRuleEventEntity : IEntity { - WebhookJob Job { get; } + RuleJob Job { get; } Instant? NextAttempt { get; } - WebhookResult Result { get; } + RuleJobResult JobResult { get; } - WebhookJobResult JobResult { get; } + RuleResult Result { get; } int NumCalls { get; } diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs new file mode 100644 index 000000000..256aa9b71 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// IRuleEventRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Read.Rules.Repositories +{ + public interface IRuleEventRepository + { + Task EnqueueAsync(RuleJob job, Instant nextAttempt); + + Task EnqueueAsync(Guid id, Instant nextAttempt); + + Task MarkSendingAsync(Guid jobId); + + Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall); + + Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)); + + Task CountByAppAsync(Guid appId); + + Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); + + Task FindAsync(Guid id); + } +} diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs similarity index 57% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs rename to src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs index 02dde09b3..5f24b1a4e 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookEditEvent.cs +// IRuleRepository.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,14 +8,14 @@ using System; using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Webhooks; +using System.Threading.Tasks; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Read.Rules.Repositories { - public abstract class WebhookEditEvent : WebhookEvent + public interface IRuleRepository { - public Uri Url { get; set; } + Task> QueryByAppAsync(Guid appId); - public List Schemas { get; set; } + Task> QueryCachedByAppAsync(Guid appId); } } diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs similarity index 55% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs rename to src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs index 1d585018d..3f6f6e7c6 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookDequeuer.cs +// RuleDequeuer.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -11,50 +11,44 @@ using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Timers; -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Read.Rules { - public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem + public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem { - private readonly ActionBlock requestBlock; - private readonly TransformBlock blockBlock; - private readonly IWebhookEventRepository webhookEventRepository; - private readonly IWebhookRepository webhookRepository; - private readonly WebhookSender webhookSender; + private readonly ActionBlock requestBlock; + private readonly TransformBlock blockBlock; + private readonly IRuleEventRepository ruleEventRepository; + private readonly RuleService ruleService; private readonly CompletionTimer timer; - private readonly ISemanticLog log; private readonly IClock clock; + private readonly ISemanticLog log; - public WebhookDequeuer(WebhookSender webhookSender, - IWebhookEventRepository webhookEventRepository, - IWebhookRepository webhookRepository, - IClock clock, - ISemanticLog log) + public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) { - Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository)); - Guard.NotNull(webhookRepository, nameof(webhookRepository)); - Guard.NotNull(webhookSender, nameof(webhookSender)); + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + Guard.NotNull(ruleService, nameof(ruleService)); Guard.NotNull(clock, nameof(clock)); Guard.NotNull(log, nameof(log)); - this.webhookEventRepository = webhookEventRepository; - this.webhookRepository = webhookRepository; - this.webhookSender = webhookSender; + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; this.clock = clock; this.log = log; requestBlock = - new ActionBlock(MakeRequestAsync, + new ActionBlock(MakeRequestAsync, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); blockBlock = - new TransformBlock(x => BlockAsync(x), + new TransformBlock(x => BlockAsync(x), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 }); blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true }); @@ -86,7 +80,9 @@ namespace Squidex.Domain.Apps.Read.Webhooks { try { - await webhookEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken); + var now = clock.GetCurrentInstant(); + + await ruleEventRepository.QueryPendingAsync(now, blockBlock.SendAsync, cancellationToken); } catch (Exception ex) { @@ -96,11 +92,11 @@ namespace Squidex.Domain.Apps.Read.Webhooks } } - private async Task BlockAsync(IWebhookEventEntity @event) + private async Task BlockAsync(IRuleEventEntity @event) { try { - await webhookEventRepository.TraceSendingAsync(@event.Id); + await ruleEventRepository.MarkSendingAsync(@event.Id); return @event; } @@ -114,38 +110,51 @@ namespace Squidex.Domain.Apps.Read.Webhooks } } - private async Task MakeRequestAsync(IWebhookEventEntity @event) + private async Task MakeRequestAsync(IRuleEventEntity @event) { try { - var response = await webhookSender.SendAsync(@event.Job); + var job = @event.Job; + + var response = await ruleService.InvokeAsync(job.ActionName, job.ActionData); Instant? nextCall = null; - if (response.Result != WebhookResult.Success) + if (response.Result != RuleResult.Success) { - var now = clock.GetCurrentInstant(); - switch (@event.NumCalls) { case 0: - nextCall = now.Plus(Duration.FromMinutes(5)); + nextCall = job.Created.Plus(Duration.FromMinutes(5)); break; case 1: - nextCall = now.Plus(Duration.FromHours(1)); + nextCall = job.Created.Plus(Duration.FromHours(1)); break; case 2: - nextCall = now.Plus(Duration.FromHours(5)); + nextCall = job.Created.Plus(Duration.FromHours(6)); break; case 3: - nextCall = now.Plus(Duration.FromHours(6)); + nextCall = job.Created.Plus(Duration.FromHours(12)); break; } } - await Task.WhenAll( - webhookRepository.TraceSentAsync(@event.Job.WebhookId, response.Result, response.Elapsed), - webhookEventRepository.TraceSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall)); + RuleJobResult jobResult; + + if (response.Result != RuleResult.Success && !nextCall.HasValue) + { + jobResult = RuleJobResult.Failed; + } + else if (response.Result != RuleResult.Success && nextCall.HasValue) + { + jobResult = RuleJobResult.Retry; + } + else + { + jobResult = RuleJobResult.Success; + } + + await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, jobResult, response.Elapsed, nextCall); } catch (Exception ex) { diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs new file mode 100644 index 000000000..01870edb7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// RuleEnqueuer.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Read.Rules +{ + public sealed class RuleEnqueuer : IEventConsumer + { + private readonly IRuleEventRepository ruleEventRepository; + private readonly IRuleRepository ruleRepository; + private readonly RuleService ruleService; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return ".*"; } + } + + public RuleEnqueuer( + IRuleEventRepository ruleEventRepository, + IRuleRepository ruleRepository, + RuleService ruleService) + { + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + Guard.NotNull(ruleRepository, nameof(ruleRepository)); + Guard.NotNull(ruleService, nameof(ruleService)); + + this.ruleEventRepository = ruleEventRepository; + this.ruleRepository = ruleRepository; + this.ruleService = ruleService; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is AppEvent appEvent) + { + var rules = await ruleRepository.QueryCachedByAppAsync(appEvent.AppId.Id); + + foreach (var ruleEntity in rules) + { + var job = ruleService.CreateJob(ruleEntity.Rule, @event); + + if (job != null) + { + await ruleEventRepository.EnqueueAsync(job, job.Created); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs similarity index 79% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs rename to src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs index 57d68fc19..d8fb997e7 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs @@ -1,14 +1,14 @@ // ========================================================================== -// WebhookJobResult.cs +// RuleJobResult.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Read.Rules { - public enum WebhookJobResult + public enum RuleJobResult { Pending, Success, diff --git a/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs index e7d0ef9e1..ab0a609bd 100644 --- a/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ b/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.Schemas.Repositories; -using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.CQRS.Events; diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs b/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs deleted file mode 100644 index 62ba20035..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// IWebhookEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Webhooks; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public interface IWebhookEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion - { - Uri Url { get; } - - long TotalSucceeded { get; } - - long TotalFailed { get; } - - long TotalTimedout { get; } - - long TotalRequestTime { get; } - - string SharedSecret { get; } - - IEnumerable Schemas { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs b/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs deleted file mode 100644 index 26d6d0ffa..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// IWebhookEventRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using NodaTime; - -namespace Squidex.Domain.Apps.Read.Webhooks.Repositories -{ - public interface IWebhookEventRepository - { - Task EnqueueAsync(WebhookJob job, Instant nextAttempt); - - Task EnqueueAsync(Guid id, Instant nextAttempt); - - Task TraceSendingAsync(Guid jobId); - - Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextCall); - - Task QueryPendingAsync(Func callback, CancellationToken cancellationToken = default(CancellationToken)); - - Task CountByAppAsync(Guid appId); - - Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); - - Task FindAsync(Guid id); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs b/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs deleted file mode 100644 index b0a4dde1f..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// ISchemaWebhookRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.Webhooks.Repositories -{ - public interface IWebhookRepository - { - Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed); - - Task> QueryByAppAsync(Guid appId); - - Task> QueryCachedByAppAsync(Guid appId); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs deleted file mode 100644 index 0e3be5546..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// WebhookEnqueuer.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public sealed class WebhookEnqueuer : IEventConsumer - { - private const string ContentPrefix = "Content"; - private static readonly Duration ExpirationTime = Duration.FromDays(2); - private readonly IWebhookEventRepository webhookEventRepository; - private readonly IWebhookRepository webhookRepository; - private readonly IClock clock; - private readonly TypeNameRegistry typeNameRegistry; - private readonly JsonSerializer webhookSerializer; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^content-"; } - } - - public WebhookEnqueuer(TypeNameRegistry typeNameRegistry, - IWebhookEventRepository webhookEventRepository, - IWebhookRepository webhookRepository, - IClock clock, - JsonSerializer webhookSerializer) - { - Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository)); - Guard.NotNull(webhookSerializer, nameof(webhookSerializer)); - Guard.NotNull(webhookRepository, nameof(webhookRepository)); - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - Guard.NotNull(clock, nameof(clock)); - - this.webhookEventRepository = webhookEventRepository; - this.webhookSerializer = webhookSerializer; - this.webhookRepository = webhookRepository; - - this.clock = clock; - - this.typeNameRegistry = typeNameRegistry; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task On(Envelope @event) - { - if (@event.Payload is ContentEvent contentEvent) - { - var eventType = typeNameRegistry.GetName(@event.Payload.GetType()); - - var webhooks = await webhookRepository.QueryCachedByAppAsync(contentEvent.AppId.Id); - - var matchingWebhooks = webhooks.Where(w => w.Schemas.Any(s => Matchs(s, contentEvent))).ToList(); - - if (matchingWebhooks.Count > 0) - { - var now = clock.GetCurrentInstant(); - - var eventPayload = CreatePayload(@event, eventType); - var eventName = $"{contentEvent.SchemaId.Name.ToPascalCase()}{CreateContentEventName(eventType)}"; - - foreach (var webhook in matchingWebhooks) - { - await EnqueueJobAsync(eventPayload, webhook, contentEvent, eventName, now); - } - } - } - } - - private async Task EnqueueJobAsync(string payload, IWebhookEntity webhook, AppEvent contentEvent, string eventName, Instant now) - { - var signature = $"{payload}{webhook.SharedSecret}".Sha256Base64(); - - var job = new WebhookJob - { - Id = Guid.NewGuid(), - AppId = contentEvent.AppId.Id, - RequestUrl = webhook.Url, - RequestBody = payload, - RequestSignature = signature, - EventName = eventName, - Expires = now.Plus(ExpirationTime), - WebhookId = webhook.Id - }; - - await webhookEventRepository.EnqueueAsync(job, now); - } - - private static bool Matchs(WebhookSchema webhookSchema, SchemaEvent @event) - { - return - (@event.SchemaId.Id == webhookSchema.SchemaId) && - (webhookSchema.SendCreate && @event is ContentCreated || - webhookSchema.SendUpdate && @event is ContentUpdated || - webhookSchema.SendDelete && @event is ContentDeleted || - webhookSchema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); - } - - private string CreatePayload(Envelope @event, string eventType) - { - return new JObject( - new JProperty("type", eventType), - new JProperty("payload", JObject.FromObject(@event.Payload, webhookSerializer)), - new JProperty("timestamp", @event.Headers.Timestamp().ToString())) - .ToString(Formatting.Indented); - } - - private static string CreateContentEventName(string eventType) - { - return eventType.StartsWith(ContentPrefix, StringComparison.Ordinal) ? eventType.Substring(ContentPrefix.Length) : eventType; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs b/src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs deleted file mode 100644 index 6a1b07993..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// WebhookSender.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Diagnostics; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Squidex.Infrastructure.Http; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public class WebhookSender - { - private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); - - public virtual async Task<(string Dump, WebhookResult Result, TimeSpan Elapsed)> SendAsync(WebhookJob job) - { - try - { - var request = BuildRequest(job); - HttpResponseMessage response = null; - - var responseString = string.Empty; - - var isTimeout = false; - - var watch = Stopwatch.StartNew(); - - try - { - using (var client = new HttpClient { Timeout = Timeout }) - { - response = await client.SendAsync(request); - } - } - catch (TimeoutException) - { - isTimeout = true; - } - catch (OperationCanceledException) - { - isTimeout = true; - } - catch (Exception ex) - { - responseString = ex.Message; - } - finally - { - watch.Stop(); - } - - if (response != null) - { - responseString = await response.Content.ReadAsStringAsync(); - } - - var dump = DumpFormatter.BuildDump(request, response, job.RequestBody, responseString, watch.Elapsed, isTimeout); - - var result = WebhookResult.Failed; - - if (isTimeout) - { - result = WebhookResult.Timeout; - } - else if (response?.IsSuccessStatusCode == true) - { - result = WebhookResult.Success; - } - - return (dump, result, watch.Elapsed); - } - catch (Exception ex) - { - return (ex.Message, WebhookResult.Failed, TimeSpan.Zero); - } - } - - private static HttpRequestMessage BuildRequest(WebhookJob job) - { - var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) - { - Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") - }; - - request.Headers.Add("X-Signature", job.RequestSignature); - request.Headers.Add("User-Agent", "Squidex Webhook"); - - return request; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs new file mode 100644 index 000000000..1c6bafd7e --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// CreateRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class CreateRule : RuleEditCommand + { + public CreateRule() + { + RuleId = Guid.NewGuid(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs new file mode 100644 index 000000000..c97f2c6b1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DeleteRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class DeleteRule : RuleAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs new file mode 100644 index 000000000..ccfa2a9be --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DisableRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class DisableRule : RuleAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs new file mode 100644 index 000000000..ac3bf7f4c --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// EnableRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class EnableRule : RuleAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookAggregateCommand.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs similarity index 64% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookAggregateCommand.cs rename to src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs index ec13cb8ef..4af936af5 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookAggregateCommand.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookAggregateCommand.cs +// RuleAggregateCommand.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,15 +9,15 @@ using System; using Squidex.Infrastructure.CQRS.Commands; -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Write.Rules.Commands { - public abstract class WebhookAggregateCommand : AppCommand, IAggregateCommand + public abstract class RuleAggregateCommand : AppCommand, IAggregateCommand { - public Guid WebhookId { get; set; } + public Guid RuleId { get; set; } Guid IAggregateCommand.AggregateId { - get { return WebhookId; } + get { return RuleId; } } } } diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs new file mode 100644 index 000000000..b63be728a --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// RuleEditCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public abstract class RuleEditCommand : RuleAggregateCommand + { + public RuleTrigger Trigger { get; set; } + + public RuleAction Action { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs new file mode 100644 index 000000000..6fe43df4d --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// UpdateRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class UpdateRule : RuleEditCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs new file mode 100644 index 000000000..47a6cc129 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// GuardRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Rules.Guards +{ + public static class GuardRule + { + public static Task CanCreate(CreateRule command, ISchemaProvider schemas) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot create rule.", async error => + { + if (command.Trigger == null) + { + error(new ValidationError("Trigger must be defined.", nameof(command.Trigger))); + } + else + { + var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas); + + errors.Foreach(error); + } + + if (command.Action == null) + { + error(new ValidationError("Trigger must be defined.", nameof(command.Action))); + } + else + { + var errors = await RuleActionValidator.ValidateAsync(command.Action); + + errors.Foreach(error); + } + }); + } + + public static Task CanUpdate(UpdateRule command, ISchemaProvider schemas) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot update rule.", async error => + { + if (command.Trigger == null && command.Action == null) + { + error(new ValidationError("Either trigger or action must be defined.", nameof(command.Trigger), nameof(command.Action))); + } + + if (command.Trigger != null) + { + var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas); + + errors.Foreach(error); + } + + if (command.Action != null) + { + var errors = await RuleActionValidator.ValidateAsync(command.Action); + + errors.Foreach(error); + } + }); + } + + public static void CanEnable(EnableRule command, Rule rule) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot enable rule.", error => + { + if (rule.IsEnabled) + { + error(new ValidationError("Rule is already enabled.")); + } + }); + } + + public static void CanDisable(DisableRule command, Rule rule) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot disable rule.", error => + { + if (!rule.IsEnabled) + { + error(new ValidationError("Rule is already disabled.")); + } + }); + } + + public static void CanDelete(DeleteRule command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs new file mode 100644 index 000000000..4ead12956 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// RuleActionValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Rules.Guards +{ + public sealed class RuleActionValidator : IRuleActionVisitor>> + { + public static Task> ValidateAsync(RuleAction action) + { + Guard.NotNull(action, nameof(action)); + + var visitor = new RuleActionValidator(); + + return action.Accept(visitor); + } + + public Task> Visit(WebhookAction action) + { + var errors = new List(); + + if (action.Url == null || !action.Url.IsAbsoluteUri) + { + errors.Add(new ValidationError("Url must be specified and absolute.", nameof(action.Url))); + } + + return Task.FromResult>(errors); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs new file mode 100644 index 000000000..f9594c26f --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// RuleTriggerValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Rules.Guards +{ + public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> + { + public ISchemaProvider Schemas { get; } + + public RuleTriggerValidator(ISchemaProvider schemas) + { + Schemas = schemas; + } + + public static Task> ValidateAsync(RuleTrigger action, ISchemaProvider schemas) + { + Guard.NotNull(action, nameof(action)); + Guard.NotNull(schemas, nameof(schemas)); + + var visitor = new RuleTriggerValidator(schemas); + + return action.Accept(visitor); + } + + public async Task> Visit(ContentChangedTrigger trigger) + { + if (trigger.Schemas != null) + { + var schemaErrors = await Task.WhenAll( + trigger.Schemas.Select(async s => + await Schemas.FindSchemaByIdAsync(s.SchemaId) == null + ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(trigger.Schemas)) + : null)); + + return schemaErrors.Where(x => x != null).ToList(); + } + + return new List(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs new file mode 100644 index 000000000..b0b2ed082 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// RuleCommandMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Write.Rules.Guards; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Write.Rules +{ + public class RuleCommandMiddleware : ICommandMiddleware + { + private readonly IAggregateHandler handler; + private readonly ISchemaProvider schemas; + + public RuleCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(schemas, nameof(schemas)); + + this.handler = handler; + this.schemas = schemas; + } + + protected Task On(CreateRule command, CommandContext context) + { + return handler.CreateAsync(context, async w => + { + await GuardRule.CanCreate(command, schemas); + + w.Create(command); + }); + } + + protected Task On(UpdateRule command, CommandContext context) + { + return handler.UpdateAsync(context, async c => + { + await GuardRule.CanUpdate(command, schemas); + + c.Update(command); + }); + } + + protected Task On(EnableRule command, CommandContext context) + { + return handler.UpdateAsync(context, r => + { + GuardRule.CanEnable(command, r.Rule); + + r.Enable(command); + }); + } + + protected Task On(DisableRule command, CommandContext context) + { + return handler.UpdateAsync(context, r => + { + GuardRule.CanDisable(command, r.Rule); + + r.Disable(command); + }); + } + + protected Task On(DeleteRule command, CommandContext context) + { + return handler.UpdateAsync(context, c => + { + GuardRule.CanDelete(command); + + c.Delete(command); + }); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (!await this.DispatchActionAsync(context.Command, context)) + { + await next(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs new file mode 100644 index 000000000..ccf2e4a57 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// RuleDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Domain.Apps.Events.Rules.Utils; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Write.Rules +{ + public class RuleDomainObject : DomainObjectBase + { + private Rule rule; + private bool isDeleted; + + public Rule Rule + { + get { return rule; } + } + + public RuleDomainObject(Guid id, int version) + : base(id, version) + { + } + + protected void On(RuleCreated @event) + { + rule = RuleEventDispatcher.Create(@event); + } + + protected void On(RuleUpdated @event) + { + rule.Apply(@event); + } + + protected void On(RuleEnabled @event) + { + rule.Apply(@event); + } + + protected void On(RuleDisabled @event) + { + rule.Apply(@event); + } + + protected void On(RuleDeleted @event) + { + isDeleted = true; + } + + public void Create(CreateRule command) + { + VerifyNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); + } + + public void Update(UpdateRule command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); + } + + public void Enable(EnableRule command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); + } + + public void Disable(DisableRule command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); + } + + public void Delete(DeleteRule command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); + } + + private void VerifyNotCreated() + { + if (rule != null) + { + throw new DomainException("Webhook has already been created."); + } + } + + private void VerifyCreatedAndNotDeleted() + { + if (isDeleted || rule == null) + { + throw new DomainException("Webhook has already been deleted or not created yet."); + } + } + + protected override void DispatchEvent(Envelope @event) + { + this.DispatchAction(@event.Payload); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs b/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs deleted file mode 100644 index 2f8cc5d3f..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// WebhookEditCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Webhooks; - -namespace Squidex.Domain.Apps.Write.Webhooks.Commands -{ - public abstract class WebhookEditCommand : WebhookAggregateCommand - { - public Uri Url { get; set; } - - public List Schemas { get; set; } = new List(); - } -} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs b/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs deleted file mode 100644 index 13359a531..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// GuardWebhook.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Webhooks.Guards -{ - public static class GuardWebhook - { - public static Task CanCreate(CreateWebhook command, ISchemaProvider schemas) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot create webhook.", error => ValidateCommandAsync(command, error, schemas)); - } - - public static Task CanUpdate(UpdateWebhook command, ISchemaProvider schemas) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot update webhook.", error => ValidateCommandAsync(command, error, schemas)); - } - - public static void CanDelete(DeleteWebhook command) - { - Guard.NotNull(command, nameof(command)); - } - - private static async Task ValidateCommandAsync(WebhookEditCommand command, Action error, ISchemaProvider schemas) - { - if (command.Url == null || !command.Url.IsAbsoluteUri) - { - error(new ValidationError("Url must be specified and absolute.", nameof(command.Url))); - } - - if (command.Schemas != null) - { - var schemaErrors = await Task.WhenAll( - command.Schemas.Select(async s => - await schemas.FindSchemaByIdAsync(s.SchemaId) == null - ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(command.Schemas)) - : null)); - - foreach (var schemaError in schemaErrors.Where(x => x != null)) - { - error(schemaError); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs deleted file mode 100644 index 13cee7474..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// WebhookCommandMiddleware.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Domain.Apps.Write.Webhooks.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly ISchemaProvider schemas; - - public WebhookCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(schemas, nameof(schemas)); - - this.handler = handler; - this.schemas = schemas; - } - - protected async Task On(CreateWebhook command, CommandContext context) - { - await handler.CreateAsync(context, async w => - { - await GuardWebhook.CanCreate(command, schemas); - - w.Create(command); - }); - } - - protected async Task On(UpdateWebhook command, CommandContext context) - { - await handler.UpdateAsync(context, async c => - { - await GuardWebhook.CanUpdate(command, schemas); - - c.Update(command); - }); - } - - protected Task On(DeleteWebhook command, CommandContext context) - { - return handler.UpdateAsync(context, c => - { - GuardWebhook.CanDelete(command); - - c.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs b/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs deleted file mode 100644 index fee952cf7..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ========================================================================== -// WebhookDomainObject.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events.Webhooks; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookDomainObject : DomainObjectBase - { - private bool isDeleted; - private bool isCreated; - - public WebhookDomainObject(Guid id, int version) - : base(id, version) - { - } - - protected void On(WebhookCreated @event) - { - isCreated = true; - } - - protected void On(WebhookDeleted @event) - { - isDeleted = true; - } - - public void Create(CreateWebhook command) - { - VerifyNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new WebhookCreated())); - } - - public void Update(UpdateWebhook command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new WebhookUpdated())); - } - - public void Delete(DeleteWebhook command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new WebhookDeleted())); - } - - private void VerifyNotCreated() - { - if (isCreated) - { - throw new DomainException("Webhook has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (isDeleted || !isCreated) - { - throw new DomainException("Webhook has already been deleted or not created yet."); - } - } - - protected override void DispatchEvent(Envelope @event) - { - this.DispatchAction(@event.Payload); - } - } -} diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs index 1fbb026ae..b89fb0957 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs @@ -99,7 +99,12 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors return DoAndUpdateStateAsync(async () => { - await DispatchConsumerAsync(ParseEvent(storedEvent)); + var @event = ParseKnownEvent(storedEvent); + + if (@event != null) + { + await DispatchConsumerAsync(@event); + } statusError = null; statusPosition = storedEvent.EventPosition; @@ -296,14 +301,23 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors } } - private Envelope ParseEvent(StoredEvent message) + private Envelope ParseKnownEvent(StoredEvent message) { - var @event = formatter.Parse(message.Data); + try + { + var @event = formatter.Parse(message.Data); - @event.SetEventPosition(message.EventPosition); - @event.SetEventStreamNumber(message.EventStreamNumber); + @event.SetEventPosition(message.EventPosition); + @event.SetEventStreamNumber(message.EventStreamNumber); - return @event; + return @event; + } + catch (TypeNameNotFoundException) + { + log.LogDebug(w => w.WriteProperty("oldEventFound", message.Data.Type)); + + return null; + } } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs b/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs index c0dabb460..9f7ef7d67 100644 --- a/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs +++ b/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs @@ -10,9 +10,11 @@ using System; using System.Linq; using System.Reflection; +#pragma warning disable IDE0033 // Use explicitly provided tuple name + namespace Squidex.Infrastructure.Dispatching { - public sealed class ActionContextDispatcher + public static class ActionContextDispatcher { public delegate void ActionContextDelegate(TTarget target, T input, TContext context) where T : TIn; diff --git a/src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs b/src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs index 5663a5e52..4902f0f44 100644 --- a/src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs +++ b/src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs @@ -10,9 +10,11 @@ using System; using System.Linq; using System.Reflection; +#pragma warning disable IDE0033 // Use explicitly provided tuple name + namespace Squidex.Infrastructure.Dispatching { - public sealed class ActionDispatcher + public static class ActionDispatcher { public delegate void ActionDelegate(TTarget target, T input) where T : TIn; diff --git a/src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs b/src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs index 294488276..00042fe62 100644 --- a/src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs +++ b/src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs @@ -9,9 +9,11 @@ using System.Linq; using System.Reflection; +#pragma warning disable IDE0033 // Use explicitly provided tuple name + namespace Squidex.Infrastructure.Dispatching { - public sealed class FuncContextDispatcher + public static class FuncContextDispatcher { public delegate TOut FuncContextDelegate(TTarget target, T input, TContext context) where T : TIn; diff --git a/src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs b/src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs index d600e5b33..08e1fd48e 100644 --- a/src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs +++ b/src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs @@ -9,9 +9,11 @@ using System.Linq; using System.Reflection; +#pragma warning disable IDE0033 // Use explicitly provided tuple name + namespace Squidex.Infrastructure.Dispatching { - public sealed class FuncDispatcher + public static class FuncDispatcher { public delegate TOut FuncDelegate(TTarget target, T input) where T : TIn; diff --git a/src/Squidex.Infrastructure/SquidexInfrastructure.cs b/src/Squidex.Infrastructure/SquidexInfrastructure.cs new file mode 100644 index 000000000..c0a3692f3 --- /dev/null +++ b/src/Squidex.Infrastructure/SquidexInfrastructure.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// SquidexInfrastructure.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure +{ + public static class SquidexInfrastructure + { + } +} diff --git a/src/Squidex.Infrastructure/TypeNameRegistry.cs b/src/Squidex.Infrastructure/TypeNameRegistry.cs index f1aef2a1f..61135259c 100644 --- a/src/Squidex.Infrastructure/TypeNameRegistry.cs +++ b/src/Squidex.Infrastructure/TypeNameRegistry.cs @@ -17,20 +17,6 @@ namespace Squidex.Infrastructure private readonly Dictionary namesByType = new Dictionary(); private readonly Dictionary typesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); - public TypeNameRegistry Map(Type type) - { - Guard.NotNull(type, nameof(type)); - - var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute(); - - if (typeNameAttribute != null) - { - Map(type, typeNameAttribute.TypeName); - } - - return this; - } - public TypeNameRegistry MapObsolete(Type type, string name) { Guard.NotNull(type, nameof(type)); @@ -56,6 +42,20 @@ namespace Squidex.Infrastructure return this; } + public TypeNameRegistry Map(Type type) + { + Guard.NotNull(type, nameof(type)); + + var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute(); + + if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) + { + Map(type, typeNameAttribute.TypeName); + } + + return this; + } + public TypeNameRegistry Map(Type type, string name) { Guard.NotNull(type, nameof(type)); @@ -95,15 +95,13 @@ namespace Squidex.Infrastructure return this; } - public TypeNameRegistry Map(Assembly assembly) + public TypeNameRegistry MapUnmapped(Assembly assembly) { foreach (var type in assembly.GetTypes()) { - var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute(); - - if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) + if (!namesByType.ContainsKey(type)) { - Map(type, typeNameAttribute.TypeName); + Map(type); } } diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index c9cd02801..fca1204f7 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -11,6 +11,9 @@ using System.Linq; using Autofac; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.ActionHandlers; +using Squidex.Domain.Apps.Core.HandleRules.Triggers; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Apps.Services.Implementations; @@ -18,10 +21,10 @@ using Squidex.Domain.Apps.Read.Contents; using Squidex.Domain.Apps.Read.Contents.Edm; using Squidex.Domain.Apps.Read.Contents.GraphQL; using Squidex.Domain.Apps.Read.History; +using Squidex.Domain.Apps.Read.Rules; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Read.Schemas.Services.Implementations; -using Squidex.Domain.Apps.Read.Webhooks; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; @@ -104,17 +107,27 @@ namespace Squidex.Config.Domain .AsSelf() .InstancePerDependency(); - builder.RegisterType() + builder.RegisterType() .As() .AsSelf() .InstancePerDependency(); - builder.RegisterType() + builder.RegisterType() .As() .AsSelf() .InstancePerDependency(); - builder.RegisterType() + builder.RegisterType() + .As() + .AsSelf() + .SingleInstance(); + + builder.RegisterType() + .As() + .AsSelf() + .SingleInstance(); + + builder.RegisterType() .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 400333d51..97c3d3139 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -6,18 +6,18 @@ // All rights reserved. // ========================================================================== -using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NodaTime; using NodaTime.Serialization.JsonNet; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.MongoDb; @@ -45,6 +45,7 @@ namespace Squidex.Config.Domain new NamedStringIdConverter(), new PropertiesBagConverter(), new RefTokenConverter(), + new RuleConverter(), new SchemaConverter(FieldRegistry), new StringEnumConverter()); @@ -62,8 +63,9 @@ namespace Squidex.Config.Domain static Serializers() { - TypeNameRegistry.Map(typeof(SquidexEvent).GetTypeInfo().Assembly); - TypeNameRegistry.Map(typeof(NoopEvent).GetTypeInfo().Assembly); + TypeNameRegistry.MapUnmapped(typeof(SquidexCoreModel).Assembly); + TypeNameRegistry.MapUnmapped(typeof(SquidexEvents).Assembly); + TypeNameRegistry.MapUnmapped(typeof(SquidexInfrastructure).Assembly); ConfigureJson(SerializerSettings, TypeNameHandling.Auto); diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs index f232207f3..faa0c355f 100644 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ b/src/Squidex/Config/Domain/StoreMongoDbModule.cs @@ -22,11 +22,11 @@ using Squidex.Domain.Apps.Read.MongoDb.Apps; using Squidex.Domain.Apps.Read.MongoDb.Assets; using Squidex.Domain.Apps.Read.MongoDb.Contents; using Squidex.Domain.Apps.Read.MongoDb.History; +using Squidex.Domain.Apps.Read.MongoDb.Rules; using Squidex.Domain.Apps.Read.MongoDb.Schemas; -using Squidex.Domain.Apps.Read.MongoDb.Webhooks; +using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Domain.Apps.Read.Schemas.Repositories; using Squidex.Domain.Apps.Read.Schemas.Services.Implementations; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; using Squidex.Domain.Users; using Squidex.Domain.Users.MongoDb; using Squidex.Domain.Users.MongoDb.Infrastructure; @@ -136,9 +136,9 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() + .As() .As() .AsSelf() .SingleInstance(); @@ -171,9 +171,9 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() + .As() .As() .As() .AsSelf() diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index c93e5ea55..e6ce99535 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Write.Apps; using Squidex.Domain.Apps.Write.Assets; using Squidex.Domain.Apps.Write.Contents; +using Squidex.Domain.Apps.Write.Rules; using Squidex.Domain.Apps.Write.Schemas; -using Squidex.Domain.Apps.Write.Webhooks; using Squidex.Domain.Users; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline.CommandMiddlewares; @@ -80,7 +80,7 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); @@ -96,7 +96,7 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.Register>(c => (id => new WebhookDomainObject(id, -1))) + builder.Register>(c => (id => new RuleDomainObject(id, -1))) .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Controllers/Api/Apps/AppsController.cs b/src/Squidex/Controllers/Api/Apps/AppsController.cs index dbb119dbd..31ddaa735 100644 --- a/src/Squidex/Controllers/Api/Apps/AppsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppsController.cs @@ -12,7 +12,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Controllers.Api.Apps.Models; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Read.Apps.Repositories; +using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -30,11 +32,15 @@ namespace Squidex.Controllers.Api.Apps public sealed class AppsController : ControllerBase { private readonly IAppRepository appRepository; + private readonly IAppPlansProvider appPlansProvider; - public AppsController(ICommandBus commandBus, IAppRepository appRepository) + public AppsController(ICommandBus commandBus, + IAppRepository appRepository, + IAppPlansProvider appPlansProvider) : base(commandBus) { this.appRepository = appRepository; + this.appPlansProvider = appPlansProvider; } /// @@ -57,11 +63,14 @@ namespace Squidex.Controllers.Api.Apps var apps = await appRepository.QueryAllAsync(subject); - var response = apps.Select(s => + var response = apps.Select(a => { - var dto = SimpleMapper.Map(s, new AppDto()); + var dto = SimpleMapper.Map(a, new AppDto()); - dto.Permission = s.Contributors[subject]; + dto.Permission = a.Contributors[subject]; + + dto.PlanName = appPlansProvider.GetPlanForApp(a)?.Name; + dto.PlanUpgrade = appPlansProvider.GetPlanUpgradeForApp(a)?.Name; return dto; }).ToList(); @@ -84,7 +93,7 @@ namespace Squidex.Controllers.Api.Apps /// [HttpPost] [Route("apps/")] - [ProducesResponseType(typeof(EntityCreatedDto), 201)] + [ProducesResponseType(typeof(AppCreatedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 409)] [ApiCosts(1)] @@ -95,7 +104,12 @@ namespace Squidex.Controllers.Api.Apps var context = await CommandBus.PublishAsync(command); var result = context.Result>(); - var response = new EntityCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version }; + var response = new AppCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version }; + + response.Permission = AppContributorPermission.Owner; + + response.PlanName = appPlansProvider.GetPlan(null)?.Name; + response.PlanUpgrade = appPlansProvider.GetPlanUpgrade(null)?.Name; return CreatedAtAction(nameof(GetApps), response); } diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppCreatedDto.cs b/src/Squidex/Controllers/Api/Apps/Models/AppCreatedDto.cs new file mode 100644 index 000000000..90c191755 --- /dev/null +++ b/src/Squidex/Controllers/Api/Apps/Models/AppCreatedDto.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// AppCreatedDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Domain.Apps.Core.Apps; + +namespace Squidex.Controllers.Api.Apps.Models +{ + public sealed class AppCreatedDto + { + /// + /// Id of the created entity. + /// + [Required] + public string Id { get; set; } + + /// + /// The new version of the entity. + /// + public long Version { get; set; } + + /// + /// The permission level of the user. + /// + [JsonConverter(typeof(StringEnumConverter))] + public AppContributorPermission Permission { get; set; } + + /// + /// Gets the current plan name. + /// + public string PlanName { get; set; } + + /// + /// Gets the next plan name. + /// + public string PlanUpgrade { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs b/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs index f0eddfd0e..f48917dae 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs @@ -49,5 +49,15 @@ namespace Squidex.Controllers.Api.Apps.Models /// [JsonConverter(typeof(StringEnumConverter))] public AppContributorPermission Permission { get; set; } + + /// + /// Gets the current plan name. + /// + public string PlanName { get; set; } + + /// + /// Gets the next plan name. + /// + public string PlanUpgrade { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs b/src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs similarity index 54% rename from src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs index f00234562..d7f1c909e 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookCreatedDto.cs +// WebhookActionDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,25 +8,30 @@ using System; 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.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models.Actions { - public sealed class WebhookCreatedDto + [JsonSchema("Webhook")] + public sealed class WebhookActionDto : RuleActionDto { /// - /// The id of the webhook. + /// The url of the rule. /// - public Guid Id { get; set; } + [Required] + public Uri Url { get; set; } /// /// The shared secret that is used to calculate the signature. /// - [Required] public string SharedSecret { get; set; } - /// - /// The version of the schema. - /// - public long Version { get; set; } + public override RuleAction ToAction() + { + return SimpleMapper.Map(this, new WebhookAction()); + } } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs new file mode 100644 index 000000000..fe28cd931 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// RuleActionDtoFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Controllers.Api.Rules.Models.Actions; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Rules.Models.Converters +{ + public sealed class RuleActionDtoFactory : IRuleActionVisitor + { + private static readonly RuleActionDtoFactory Instance = new RuleActionDtoFactory(); + + private RuleActionDtoFactory() + { + } + + public static RuleActionDto Create(RuleAction properties) + { + return properties.Accept(Instance); + } + + public RuleActionDto Visit(WebhookAction action) + { + return SimpleMapper.Map(action, new WebhookActionDto()); + } + } +} diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs new file mode 100644 index 000000000..339fabf2c --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// RuleConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Rules.Models.Converters +{ + public static class RuleConverter + { + public static RuleDto ToModel(this IRuleEntity entity) + { + var dto = new RuleDto(); + + SimpleMapper.Map(entity, dto); + SimpleMapper.Map(entity.Rule, dto); + + if (entity.Rule.Trigger != null) + { + dto.Trigger = RuleTriggerDtoFactory.Create(entity.Rule.Trigger); + } + + if (entity.Rule.Action != null) + { + dto.Action = RuleActionDtoFactory.Create(entity.Rule.Action); + } + + return dto; + } + + public static UpdateRule ToCommand(this UpdateRuleDto dto, Guid id) + { + var command = new UpdateRule { RuleId = id }; + + if (dto.Action != null) + { + command.Action = dto.Action.ToAction(); + } + + if (dto.Trigger != null) + { + command.Trigger = dto.Trigger.ToTrigger(); + } + + return command; + } + + public static CreateRule ToCommand(this CreateRuleDto dto) + { + var command = new CreateRule(); + + if (dto.Action != null) + { + command.Action = dto.Action.ToAction(); + } + + if (dto.Trigger != null) + { + command.Trigger = dto.Trigger.ToTrigger(); + } + + return command; + } + } +} diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs new file mode 100644 index 000000000..a9d38a6bf --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// RuleTriggerDtoFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using Squidex.Controllers.Api.Rules.Models.Triggers; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Rules.Models.Converters +{ + public sealed class RuleTriggerDtoFactory : IRuleTriggerVisitor + { + private static readonly RuleTriggerDtoFactory Instance = new RuleTriggerDtoFactory(); + + private RuleTriggerDtoFactory() + { + } + + public static RuleTriggerDto Create(RuleTrigger properties) + { + return properties.Accept(Instance); + } + + public RuleTriggerDto Visit(ContentChangedTrigger trigger) + { + return new ContentChangedTriggerDto + { + Schemas = trigger.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchemaDto())).ToList() + }; + } + } +} diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs b/src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs similarity index 60% rename from src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs index d7dcd7f46..060ff6461 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs @@ -1,29 +1,27 @@ // ========================================================================== -// CreateWebhookDto.cs +// CreateRuleDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models { - public sealed class CreateWebhookDto + public sealed class CreateRuleDto { /// - /// The url of the webhook. + /// The trigger properties. /// [Required] - public Uri Url { get; set; } + public RuleTriggerDto Trigger { get; set; } /// - /// The schema settings. + /// The action properties. /// [Required] - public List Schemas { get; set; } + public RuleActionDto Action { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs new file mode 100644 index 000000000..2b95ed5b6 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// RuleActionDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Squidex.Controllers.Api.Rules.Models.Actions; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Controllers.Api.Rules.Models +{ + [JsonConverter(typeof(JsonInheritanceConverter), "actionType")] + [KnownType(typeof(WebhookActionDto))] + public abstract class RuleActionDto + { + public abstract RuleAction ToAction(); + } +} diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs new file mode 100644 index 000000000..d6c3ade48 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// RuleDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Controllers.Api.Rules.Models +{ + public sealed class RuleDto + { + /// + /// The id of the rule. + /// + public Guid Id { get; set; } + + /// + /// The user that has created the rule. + /// + [Required] + public RefToken CreatedBy { get; set; } + + /// + /// The user that has updated the rule. + /// + [Required] + public RefToken LastModifiedBy { get; set; } + + /// + /// The date and time when the rule has been created. + /// + public Instant Created { get; set; } + + /// + /// The date and time when the rule has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The version of the rule. + /// + public int Version { get; set; } + + /// + /// The trigger properties. + /// + [Required] + public RuleTriggerDto Trigger { get; set; } + + /// + /// The action properties. + /// + [Required] + public RuleActionDto Action { get; set; } + + /// + /// Determines if the rule is enabled. + /// + public bool IsEnabled { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs similarity index 79% rename from src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs index 4473fe749..5513d5a25 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookEventDto.cs +// RuleEventDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,11 +9,12 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Read.Rules; -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models { - public sealed class WebhookEventDto + public sealed class RuleEventDto { /// /// The id of the event. @@ -26,10 +27,10 @@ namespace Squidex.Controllers.Api.Webhooks.Models public Instant Created { get; set; } /// - /// The request url. + /// The description /// [Required] - public Uri RequestUrl { get; set; } + public string Description { get; set; } /// /// The name of the event. @@ -55,11 +56,11 @@ namespace Squidex.Controllers.Api.Webhooks.Models /// /// The result of the event. /// - public WebhookResult Result { get; set; } + public RuleResult Result { get; set; } /// /// The result of the job. /// - public WebhookJobResult JobResult { get; set; } + public RuleJobResult JobResult { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs similarity index 65% rename from src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs index 977f44c04..f0cda8a38 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs @@ -1,23 +1,23 @@ // ========================================================================== -// WebhookEventsDto.cs +// RuleEventsDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models { - public sealed class WebhookEventsDto + public sealed class RuleEventsDto { /// - /// The total number of webhook events. + /// The total number of rule events. /// public long Total { get; set; } /// - /// The webhook events. + /// The rule events. /// - public WebhookEventDto[] Items { get; set; } + public RuleEventDto[] Items { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs new file mode 100644 index 000000000..f65ea3f46 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// RuleTriggerDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Squidex.Controllers.Api.Rules.Models.Triggers; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Controllers.Api.Rules.Models +{ + [JsonConverter(typeof(JsonInheritanceConverter), "triggerType")] + [KnownType(typeof(ContentChangedTriggerDto))] + public abstract class RuleTriggerDto + { + public abstract RuleTrigger ToTrigger(); + } +} diff --git a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs new file mode 100644 index 000000000..05af3cf26 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// ContentChangedTriggerDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using NJsonSchema.Annotations; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Rules.Models.Triggers +{ + [JsonSchema("ContentChanged")] + public sealed class ContentChangedTriggerDto : RuleTriggerDto + { + /// + /// The schema settings. + /// + [Required] + public List Schemas { get; set; } + + public override RuleTrigger ToTrigger() + { + return new ContentChangedTrigger + { + Schemas = Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchema())).ToList() + }; + } + } +} diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs similarity index 87% rename from src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs index 251afb1e6..29e0c71fd 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookSchemaDto.cs +// ContentChangedTriggerSchemaDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,9 +8,9 @@ using System; -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models.Triggers { - public sealed class WebhookSchemaDto + public sealed class ContentChangedTriggerSchemaDto { /// /// The id of the schema. diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/UpdateWebhookDto.cs b/src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs similarity index 50% rename from src/Squidex/Controllers/Api/Webhooks/Models/UpdateWebhookDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs index d5b7da231..bad710cc8 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/UpdateWebhookDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs @@ -1,29 +1,23 @@ // ========================================================================== -// UpdateWebhookDto.cs +// UpdateRuleDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models { - public sealed class UpdateWebhookDto + public sealed class UpdateRuleDto { /// - /// The url of the webhook. + /// The trigger properties. /// - [Required] - public Uri Url { get; set; } + public RuleTriggerDto Trigger { get; set; } /// - /// The schema settings. + /// The action properties. /// - [Required] - public List Schemas { get; set; } + public RuleActionDto Action { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Rules/RulesController.cs b/src/Squidex/Controllers/Api/Rules/RulesController.cs new file mode 100644 index 000000000..100bb3633 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/RulesController.cs @@ -0,0 +1,245 @@ +// ========================================================================== +// RulesController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using NodaTime; +using NSwag.Annotations; +using Squidex.Controllers.Api.Rules.Models; +using Squidex.Controllers.Api.Rules.Models.Converters; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Pipeline; + +namespace Squidex.Controllers.Api.Rules +{ + /// + /// Manages and retrieves information about schemas. + /// + [ApiAuthorize] + [ApiExceptionFilter] + [AppApi] + [SwaggerTag(nameof(Rules))] + [MustBeAppDeveloper] + public sealed class RulesController : ControllerBase + { + private readonly IRuleRepository rulesRepository; + private readonly IRuleEventRepository ruleEventsRepository; + + public RulesController(ICommandBus commandBus, + IRuleRepository rulesRepository, + IRuleEventRepository ruleEventsRepository) + : base(commandBus) + { + this.rulesRepository = rulesRepository; + this.ruleEventsRepository = ruleEventsRepository; + } + + /// + /// Get rules. + /// + /// The name of the app. + /// + /// 200 => Rules returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/rules/")] + [ProducesResponseType(typeof(RuleDto[]), 200)] + [ApiCosts(1)] + public async Task GetRules(string app) + { + var rules = await rulesRepository.QueryByAppAsync(App.Id); + + var response = rules.Select(r => r.ToModel()); + + return Ok(response); + } + + /// + /// Create a new rule. + /// + /// The name of the app. + /// The rule object that needs to be added to the app. + /// + /// 201 => Rule created. + /// 400 => Rule is not valid. + /// 404 => App not found. + /// + [HttpPost] + [Route("apps/{app}/rules/")] + [ProducesResponseType(typeof(EntityCreatedDto), 201)] + [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] + public async Task PostRule(string app, [FromBody] CreateRuleDto request) + { + var command = request.ToCommand(); + + var context = await CommandBus.PublishAsync(command); + + var result = context.Result>(); + var response = new EntityCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version }; + + return CreatedAtAction(nameof(GetRules), new { app }, response); + } + + /// + /// Update a rule. + /// + /// The name of the app. + /// The id of the rule to update. + /// The rule object that needs to be added to the app. + /// + /// 204 => Rule updated. + /// 400 => Rule is not valid. + /// 404 => Rule or app not found. + /// + /// + /// All events for the specified schemas will be sent to the url. The timeout is 2 seconds. + /// + [HttpPut] + [Route("apps/{app}/rules/{id}/")] + [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] + public async Task PutRule(string app, Guid id, [FromBody] UpdateRuleDto request) + { + var command = request.ToCommand(id); + + await CommandBus.PublishAsync(command); + + return NoContent(); + } + + /// + /// Enable a rule. + /// + /// The name of the app. + /// The id of the rule to enable. + /// + /// 204 => Rule enabled. + /// 400 => Rule already enabled. + /// 404 => Rule or app not found. + /// + [HttpPut] + [Route("apps/{app}/rules/{id}/enable/")] + [ApiCosts(1)] + public async Task EnableRule(string app, Guid id) + { + await CommandBus.PublishAsync(new EnableRule { RuleId = id }); + + return NoContent(); + } + + /// + /// Disable a rule. + /// + /// The name of the app. + /// The id of the rule to disable. + /// + /// 204 => Rule disabled. + /// 400 => Rule already disabled. + /// 404 => Rule or app not found. + /// + [HttpPut] + [Route("apps/{app}/rules/{id}/disable/")] + [ApiCosts(1)] + public async Task DisableRule(string app, Guid id) + { + await CommandBus.PublishAsync(new DisableRule { RuleId = id }); + + return NoContent(); + } + + /// + /// Delete a rule. + /// + /// The name of the app. + /// The id of the rule to delete. + /// + /// 204 => Rule has been deleted. + /// 404 => Rule or app not found. + /// + [HttpDelete] + [Route("apps/{app}/rules/{id}/")] + [ApiCosts(1)] + public async Task DeleteRule(string app, Guid id) + { + await CommandBus.PublishAsync(new DeleteRule { RuleId = id }); + + return NoContent(); + } + + /// + /// Get rule events. + /// + /// The name of the app. + /// The number of events to skip. + /// The number of events to take. + /// + /// 200 => Rule events returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/rules/events/")] + [ProducesResponseType(typeof(RuleEventsDto), 200)] + [ApiCosts(0)] + public async Task GetEvents(string app, [FromQuery] int skip = 0, [FromQuery] int take = 20) + { + var taskForItems = ruleEventsRepository.QueryByAppAsync(App.Id, skip, take); + var taskForCount = ruleEventsRepository.CountByAppAsync(App.Id); + + await Task.WhenAll(taskForItems, taskForCount); + + var response = new RuleEventsDto + { + Total = taskForCount.Result, + Items = taskForItems.Result.Select(x => + { + var itemModel = new RuleEventDto(); + + SimpleMapper.Map(x, itemModel); + SimpleMapper.Map(x.Job, itemModel); + + return itemModel; + }).ToArray() + }; + + return Ok(response); + } + + /// + /// Enqueue the event to be send. + /// + /// The name of the app. + /// The event to enqueue. + /// + /// 200 => Rule enqueued. + /// 404 => App or rule event not found. + /// + [HttpPut] + [Route("apps/{app}/rules/events/{id}/")] + [ApiCosts(0)] + public async Task PutEvent(string app, Guid id) + { + var entity = await ruleEventsRepository.FindAsync(id); + + if (entity == null) + { + return NotFound(); + } + + await ruleEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant()); + + return NoContent(); + } + } +} diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs index 12db361af..5b803f707 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs @@ -7,6 +7,7 @@ // ========================================================================== using System.Linq; +using Squidex.Controllers.Api.Schemas.Models.Fields; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs index 4271dd057..31e33fbad 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs @@ -9,7 +9,7 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Newtonsoft.Json; -using Squidex.Controllers.Api.Schemas.Models.Converters; +using Squidex.Controllers.Api.Schemas.Models.Fields; using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Controllers.Api.Schemas.Models diff --git a/src/Squidex/Controllers/Api/Schemas/Models/AssetsFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/AssetsFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs index 1becd319c..b2e7a3d50 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/AssetsFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Assets")] public sealed class AssetsFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/BooleanFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/BooleanFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs index 82c5c081d..8516d4b92 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/BooleanFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Boolean")] public sealed class BooleanFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs index 2c2c68b13..4cdc78c49 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs @@ -13,7 +13,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("DateTime")] public sealed class DateTimeFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs index 4ad558c5c..76e1a1ce0 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Geolocation")] public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/JsonFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Schemas/Models/JsonFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs index 2957c8cce..68f9fe290 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/JsonFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Json")] public sealed class JsonFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs index 3bd914770..ff5b8fc14 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Number")] public sealed class NumberFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/ReferencesFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/ReferencesFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index 38fbcb992..e35541086 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/ReferencesFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -11,7 +11,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("References")] public sealed class ReferencesFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs index 32a3e00da..ebf41e4e7 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("String")] public sealed class StringFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/TagsFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/TagsFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs index 45e91f5f0..0769967ad 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/TagsFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Tags")] public sealed class TagsFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs b/src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs deleted file mode 100644 index 29bb7670e..000000000 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ========================================================================== -// WebhookDto.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Controllers.Api.Webhooks.Models -{ - public sealed class WebhookDto - { - /// - /// The id of the webhook. - /// - public Guid Id { get; set; } - - /// - /// The user that has created the webhook. - /// - [Required] - public RefToken CreatedBy { get; set; } - - /// - /// The user that has updated the webhook. - /// - [Required] - public RefToken LastModifiedBy { get; set; } - - /// - /// The date and time when the webhook has been created. - /// - public Instant Created { get; set; } - - /// - /// The date and time when the webhook has been modified last. - /// - public Instant LastModified { get; set; } - - /// - /// The version of the webhook. - /// - public int Version { get; set; } - - /// - /// The number of succceeded calls. - /// - public long TotalSucceeded { get; set; } - - /// - /// The number of failed calls. - /// - public long TotalFailed { get; set; } - - /// - /// The number of timedout calls. - /// - public long TotalTimedout { get; set; } - - /// - /// The average response time in milliseconds. - /// - public long AverageRequestTimeMs { get; set; } - - /// - /// The url of the webhook. - /// - [Required] - public Uri Url { get; set; } - - /// - /// The shared secret that is used to calculate the signature. - /// - [Required] - public string SharedSecret { get; set; } - - /// - /// The schema settings. - /// - [Required] - public List Schemas { get; set; } - } -} diff --git a/src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs b/src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs deleted file mode 100644 index e672333b5..000000000 --- a/src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs +++ /dev/null @@ -1,220 +0,0 @@ -// ========================================================================== -// WebhooksController.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using NodaTime; -using NSwag.Annotations; -using Squidex.Controllers.Api.Webhooks.Models; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.Reflection; -using Squidex.Pipeline; - -namespace Squidex.Controllers.Api.Webhooks -{ - /// - /// Manages and retrieves information about schemas. - /// - [ApiAuthorize] - [ApiExceptionFilter] - [AppApi] - [SwaggerTag(nameof(Webhooks))] - [MustBeAppDeveloper] - public sealed class WebhooksController : ControllerBase - { - private readonly IWebhookRepository webhooksRepository; - private readonly IWebhookEventRepository webhookEventsRepository; - - public WebhooksController(ICommandBus commandBus, - IWebhookRepository webhooksRepository, - IWebhookEventRepository webhookEventsRepository) - : base(commandBus) - { - this.webhooksRepository = webhooksRepository; - this.webhookEventsRepository = webhookEventsRepository; - } - - /// - /// Get webhooks. - /// - /// The name of the app. - /// - /// 200 => Webhooks returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/webhooks/")] - [ProducesResponseType(typeof(WebhookDto[]), 200)] - [ApiCosts(1)] - public async Task GetWebhooks(string app) - { - var webhooks = await webhooksRepository.QueryByAppAsync(App.Id); - - var response = webhooks.Select(w => - { - var totalCount = w.TotalTimedout + w.TotalSucceeded + w.TotalFailed; - var totalAverage = totalCount == 0 ? 0 : w.TotalRequestTime / totalCount; - - var schemas = w.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchemaDto())).ToList(); - - return SimpleMapper.Map(w, new WebhookDto { AverageRequestTimeMs = totalAverage, Schemas = schemas }); - }); - - return Ok(response); - } - - /// - /// Create a new webhook. - /// - /// The name of the app. - /// The webhook object that needs to be added to the app. - /// - /// 201 => Webhook created. - /// 400 => Webhook is not valid. - /// 404 => App not found. - /// - /// - /// All events for the specified schemas will be sent to the url. The timeout is 2 seconds. - /// - [HttpPost] - [Route("apps/{app}/webhooks/")] - [ProducesResponseType(typeof(EntityCreatedDto), 201)] - [ProducesResponseType(typeof(ErrorDto), 400)] - [ApiCosts(1)] - public async Task PostWebhook(string app, [FromBody] CreateWebhookDto request) - { - var schemas = request.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchema())).ToList(); - - var command = new CreateWebhook { Url = request.Url, Schemas = schemas }; - - var context = await CommandBus.PublishAsync(command); - - var result = context.Result>(); - var response = new WebhookCreatedDto { Id = result.IdOrValue, SharedSecret = command.SharedSecret, Version = result.Version }; - - return CreatedAtAction(nameof(GetWebhooks), new { app }, response); - } - - /// - /// Update a webhook. - /// - /// The name of the app. - /// The id of the webhook to update. - /// The webhook object that needs to be added to the app. - /// - /// 203 => Webhook updated. - /// 400 => Webhook is not valid. - /// 404 => Webhook or app not found. - /// - /// - /// All events for the specified schemas will be sent to the url. The timeout is 2 seconds. - /// - [HttpPut] - [Route("apps/{app}/webhooks/{id}/")] - [ProducesResponseType(typeof(ErrorDto), 400)] - [ApiCosts(1)] - public async Task PutWebhook(string app, Guid id, [FromBody] CreateWebhookDto request) - { - var schemas = request.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchema())).ToList(); - - var command = new UpdateWebhook { WebhookId = id, Url = request.Url, Schemas = schemas }; - - await CommandBus.PublishAsync(command); - - return NoContent(); - } - - /// - /// Delete a webhook. - /// - /// The name of the app. - /// The id of the webhook to delete. - /// - /// 204 => Webhook has been deleted. - /// 404 => Webhook or app not found. - /// - [HttpDelete] - [Route("apps/{app}/webhooks/{id}/")] - [ApiCosts(1)] - public async Task DeleteWebhook(string app, Guid id) - { - await CommandBus.PublishAsync(new DeleteWebhook { WebhookId = id }); - - return NoContent(); - } - - /// - /// Get webhook events. - /// - /// The name of the app. - /// The number of events to skip. - /// The number of events to take. - /// - /// 200 => Webhook events returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/webhooks/events/")] - [ProducesResponseType(typeof(WebhookEventsDto), 200)] - [ApiCosts(0)] - public async Task GetEvents(string app, [FromQuery] int skip = 0, [FromQuery] int take = 20) - { - var taskForItems = webhookEventsRepository.QueryByAppAsync(App.Id, skip, take); - var taskForCount = webhookEventsRepository.CountByAppAsync(App.Id); - - await Task.WhenAll(taskForItems, taskForCount); - - var response = new WebhookEventsDto - { - Total = taskForCount.Result, - Items = taskForItems.Result.Select(x => - { - var itemModel = new WebhookEventDto(); - - SimpleMapper.Map(x, itemModel); - SimpleMapper.Map(x.Job, itemModel); - - return itemModel; - }).ToArray() - }; - - return Ok(response); - } - - /// - /// Enqueue the event to be send. - /// - /// The name of the app. - /// The event to enqueue. - /// - /// 200 => Webhook enqueued. - /// 404 => App or webhook event not found. - /// - [HttpPut] - [Route("apps/{app}/webhooks/events/{id}/")] - [ApiCosts(0)] - public async Task PutEvent(string app, Guid id) - { - var entity = await webhookEventsRepository.FindAsync(id); - - if (entity == null) - { - return NotFound(); - } - - await webhookEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant()); - - return Ok(); - } - } -} diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/JsonInheritanceConverter.cs b/src/Squidex/Controllers/JsonInheritanceConverter.cs similarity index 98% rename from src/Squidex/Controllers/Api/Schemas/Models/Converters/JsonInheritanceConverter.cs rename to src/Squidex/Controllers/JsonInheritanceConverter.cs index e55080547..5076a3791 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/JsonInheritanceConverter.cs +++ b/src/Squidex/Controllers/JsonInheritanceConverter.cs @@ -16,7 +16,7 @@ using NJsonSchema.Annotations; #pragma warning disable SA1306 // Field names must begin with lower-case letter -namespace Squidex.Controllers.Api.Schemas.Models.Converters +namespace Squidex.Controllers { public sealed class JsonInheritanceConverter : JsonConverter { diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 0f90c7e1c..87169d20d 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -66,11 +66,11 @@ - + - + diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 3b4e8fea7..62346ffb2 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -20,7 +20,8 @@ import { import { AppMustExistGuard, MustBeAuthenticatedGuard, - MustBeNotAuthenticatedGuard + MustBeNotAuthenticatedGuard, + UnsetAppGuard } from './shared'; export const routes: Routes = [ @@ -36,11 +37,13 @@ export const routes: Routes = [ children: [ { path: '', - loadChildren: './features/apps/module#SqxFeatureAppsModule' + loadChildren: './features/apps/module#SqxFeatureAppsModule', + canActivate: [UnsetAppGuard] }, { path: 'administration', - loadChildren: './features/administration/module#SqxFeatureAdministrationModule' + loadChildren: './features/administration/module#SqxFeatureAdministrationModule', + canActivate: [UnsetAppGuard] }, { path: ':appName', @@ -64,8 +67,8 @@ export const routes: Routes = [ loadChildren: './features/assets/module#SqxFeatureAssetsModule' }, { - path: 'webhooks', - loadChildren: './features/webhooks/module#SqxFeatureWebhooksModule' + path: 'rules', + loadChildren: './features/rules/module#SqxFeatureRulesModule' }, { path: 'settings', diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html index 8fa8f7792..fd3a2f58e 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html @@ -1,6 +1,6 @@ - +
diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index a87dc7006..a2485aab4 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -9,8 +9,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; import { - ComponentBase, - DialogService, + AppContext, EventConsumerDto, EventConsumersService, fadeAnimation, @@ -22,21 +21,23 @@ import { selector: 'sqx-event-consumers-page', styleUrls: ['./event-consumers-page.component.scss'], templateUrl: './event-consumers-page.component.html', + providers: [ + AppContext + ], animations: [ fadeAnimation ] }) -export class EventConsumersPageComponent extends ComponentBase implements OnDestroy, OnInit { +export class EventConsumersPageComponent implements OnDestroy, OnInit { private subscription: Subscription; public eventConsumerErrorDialog = new ModalView(); public eventConsumerError = ''; public eventConsumers = ImmutableArray.empty(); - constructor(dialogs: DialogService, + constructor(public readonly ctx: AppContext, private readonly eventConsumersService: EventConsumersService ) { - super(dialogs); } public ngOnDestroy() { @@ -58,11 +59,11 @@ export class EventConsumersPageComponent extends ComponentBase implements OnDest this.eventConsumers = ImmutableArray.of(dtos); if (showInfo) { - this.notifyInfo('Event Consumers reloaded.'); + this.ctx.notifyInfo('Event Consumers reloaded.'); } }, error => { if (showError) { - this.notifyError(error); + this.ctx.notifyError(error); } }); } @@ -72,7 +73,7 @@ export class EventConsumersPageComponent extends ComponentBase implements OnDest .subscribe(() => { this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.start()); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -81,7 +82,7 @@ export class EventConsumersPageComponent extends ComponentBase implements OnDest .subscribe(() => { this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.stop()); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -90,7 +91,7 @@ export class EventConsumersPageComponent extends ComponentBase implements OnDest .subscribe(() => { this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.reset()); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.html b/src/Squidex/app/features/administration/pages/users/user-page.component.html index 3dbd6bc57..39a96fe6d 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.html @@ -3,7 +3,7 @@
- +
@@ -52,7 +52,7 @@
-
+
diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/src/Squidex/app/features/administration/pages/users/user-page.component.ts index eb8824a30..a14838100 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.ts @@ -7,13 +7,10 @@ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { - AuthService, - ComponentBase, - DialogService, - MessageBus, + AppContext, UserDto, UserManagementService, ValidatorsEx @@ -24,12 +21,14 @@ import { UserCreated, UserUpdated } from './../messages'; @Component({ selector: 'sqx-user-page', styleUrls: ['./user-page.component.scss'], - templateUrl: './user-page.component.html' + templateUrl: './user-page.component.html', + providers: [ + AppContext + ] }) -export class UserPageComponent extends ComponentBase implements OnInit { - private user: UserDto; +export class UserPageComponent implements OnInit { + public user: UserDto; - public currentUserId: string; public userFormSubmitted = false; public userForm: FormGroup; public userFormError? = ''; @@ -37,21 +36,15 @@ export class UserPageComponent extends ComponentBase implements OnInit { public isCurrentUser = false; public isNewMode = false; - constructor(dialogs: DialogService, - private readonly authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly formBuilder: FormBuilder, - private readonly messageBus: MessageBus, - private readonly route: ActivatedRoute, private readonly router: Router, private readonly userManagementService: UserManagementService ) { - super(dialogs); } public ngOnInit() { - this.currentUserId = this.authService.user!.id; - - this.route.data.map(p => p['user']) + this.ctx.route.data.map(d => d.user) .subscribe((user: UserDto) => { this.user = user; @@ -78,8 +71,9 @@ export class UserPageComponent extends ComponentBase implements OnInit { created.pictureUrl!, false); + this.ctx.notifyInfo('User created successfully.'); + this.emitUserCreated(this.user); - this.notifyInfo('User created successfully.'); this.back(); }, error => { this.resetUserForm(error.displayMessage); @@ -92,8 +86,9 @@ export class UserPageComponent extends ComponentBase implements OnInit { requestDto.email, requestDto.displayMessage); + this.ctx.notifyInfo('User saved successfully.'); + this.emitUserUpdated(this.user); - this.notifyInfo('User saved successfully.'); this.resetUserForm(); }, error => { this.resetUserForm(error.displayMessage); @@ -103,15 +98,15 @@ export class UserPageComponent extends ComponentBase implements OnInit { } private back() { - this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true }); + this.router.navigate(['../'], { relativeTo: this.ctx.route, replaceUrl: true }); } private emitUserCreated(user: UserDto) { - this.messageBus.emit(new UserCreated(user)); + this.ctx.bus.emit(new UserCreated(user)); } private emitUserUpdated(user: UserDto) { - this.messageBus.emit(new UserUpdated(user)); + this.ctx.bus.emit(new UserUpdated(user)); } private setupAndPopulateForm() { @@ -141,7 +136,7 @@ export class UserPageComponent extends ComponentBase implements OnInit { ]] }); - this.isCurrentUser = this.user && this.user.id === this.currentUserId; + this.isCurrentUser = this.user && this.user.id === this.ctx.userId; this.resetUserForm(); } diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html index c4370b21f..d22a8362c 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -1,6 +1,6 @@ - +
@@ -69,7 +69,7 @@ {{user.email}} - + diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.ts b/src/Squidex/app/features/administration/pages/users/users-page.component.ts index 918619e22..dc3c508e4 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.ts @@ -10,11 +10,8 @@ import { FormControl } from '@angular/forms'; import { Subscription } from 'rxjs'; import { - AuthService, - ComponentBase, - DialogService, + AppContext, ImmutableArray, - MessageBus, Pager, UserDto, UserManagementService @@ -25,25 +22,23 @@ import { UserCreated, UserUpdated } from './../messages'; @Component({ selector: 'sqx-users-page', styleUrls: ['./users-page.component.scss'], - templateUrl: './users-page.component.html' + templateUrl: './users-page.component.html', + providers: [ + AppContext + ] }) -export class UsersPageComponent extends ComponentBase implements OnDestroy, OnInit { +export class UsersPageComponent implements OnDestroy, OnInit { private userCreatedSubscription: Subscription; private userUpdatedSubscription: Subscription; - public currentUserId: string; - public usersItems = ImmutableArray.empty(); public usersPager = new Pager(0); public usersFilter = new FormControl(); public usersQuery = ''; - constructor(dialogs: DialogService, - private readonly userManagementService: UserManagementService, - private readonly authService: AuthService, - private readonly messageBus: MessageBus + constructor(public readonly ctx: AppContext, + private readonly userManagementService: UserManagementService ) { - super(dialogs); } public ngOnDestroy() { @@ -53,20 +48,18 @@ export class UsersPageComponent extends ComponentBase implements OnDestroy, OnIn public ngOnInit() { this.userCreatedSubscription = - this.messageBus.of(UserCreated) + this.ctx.bus.of(UserCreated) .subscribe(message => { this.usersItems = this.usersItems.pushFront(message.user); this.usersPager = this.usersPager.incrementCount(); }); this.userUpdatedSubscription = - this.messageBus.of(UserUpdated) + this.ctx.bus.of(UserUpdated) .subscribe(message => { this.usersItems = this.usersItems.replaceBy('id', message.user); }); - this.currentUserId = this.authService.user!.id; - this.load(); } @@ -84,10 +77,10 @@ export class UsersPageComponent extends ComponentBase implements OnDestroy, OnIn this.usersPager = this.usersPager.setCount(dtos.total); if (showInfo) { - this.notifyInfo('Users reloaded.'); + this.ctx.notifyInfo('Users reloaded.'); } }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -96,7 +89,7 @@ export class UsersPageComponent extends ComponentBase implements OnDestroy, OnIn .subscribe(() => { this.usersItems = this.usersItems.replaceBy('id', user.lock()); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -105,7 +98,7 @@ export class UsersPageComponent extends ComponentBase implements OnDestroy, OnIn .subscribe(() => { this.usersItems = this.usersItems.replaceBy('id', user.unlock()); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } diff --git a/src/Squidex/app/features/api/api-area.component.html b/src/Squidex/app/features/api/api-area.component.html index c94f15286..59c48ccf6 100644 --- a/src/Squidex/app/features/api/api-area.component.html +++ b/src/Squidex/app/features/api/api-area.component.html @@ -1,6 +1,6 @@ - + - +

API

@@ -21,7 +21,7 @@ diff --git a/src/Squidex/app/features/api/api-area.component.ts b/src/Squidex/app/features/api/api-area.component.ts index cf4864388..8278d5b36 100644 --- a/src/Squidex/app/features/api/api-area.component.ts +++ b/src/Squidex/app/features/api/api-area.component.ts @@ -7,21 +7,19 @@ import { Component } from '@angular/core'; -import { - AppComponentBase, - AppsStoreService, - AuthService, - DialogService -} from 'shared'; +import { AppContext } from 'shared'; @Component({ selector: 'sqx-api-area', styleUrls: ['./api-area.component.scss'], - templateUrl: './api-area.component.html' + templateUrl: './api-area.component.html', + providers: [ + AppContext + ] }) -export class ApiAreaComponent extends AppComponentBase { - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService +export class ApiAreaComponent { + constructor( + public readonly ctx: AppContext ) { - super(dialogs, apps, authService); } } \ No newline at end of file diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html index 7dedbeb1b..ff2fc5573 100644 --- a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html +++ b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html @@ -1,5 +1,5 @@ - + - +
\ No newline at end of file diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts index c045b75df..323a1b033 100644 --- a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts +++ b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts @@ -16,10 +16,7 @@ const GraphiQL = require('graphiql'); /* tslint:disable:use-view-encapsulation */ import { - AppComponentBase, - AppsStoreService, - AuthService, - DialogService, + AppContext, GraphQlService, LocalStoreService } from 'shared'; @@ -28,17 +25,19 @@ import { selector: 'sqx-graphql-page', styleUrls: ['./graphql-page.component.scss'], templateUrl: './graphql-page.component.html', + providers: [ + AppContext + ], encapsulation: ViewEncapsulation.None }) -export class GraphQLPageComponent extends AppComponentBase implements OnInit { +export class GraphQLPageComponent implements OnInit { @ViewChild('graphiQLContainer') public graphiQLContainer: ElementRef; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly graphQlService: GraphQlService, private readonly localStoreService: LocalStoreService ) { - super(dialogs, apps, authService); } public ngOnInit() { @@ -57,9 +56,7 @@ export class GraphQLPageComponent extends AppComponentBase implements OnInit { } private request(params: any) { - return this.appNameOnce() - .switchMap(app => this.graphQlService.query(app, params).catch(response => Observable.of(response.error))) - .toPromise(); + return this.graphQlService.query(this.ctx.appName, params).catch(response => Observable.of(response.error)).toPromise(); } } diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index e485a137e..aa3f2a039 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -1,13 +1,13 @@ 
-
+

You are not collaborating to any app yet

-
+

{{app.name}}

@@ -31,7 +31,8 @@
diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/src/Squidex/app/features/apps/pages/apps-page.component.ts index 741dc448f..5b33ebf9b 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.ts +++ b/src/Squidex/app/features/apps/pages/apps-page.component.ts @@ -9,6 +9,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; import { + AppDto, AppsStoreService, fadeAnimation, ModalView, @@ -24,10 +25,10 @@ import { ] }) export class AppsPageComponent implements OnDestroy, OnInit { - private onboardingAppsSubscription: Subscription; + private appsSubscription: Subscription; public addAppDialog = new ModalView(); - public apps = this.appsStore.apps; + public apps: AppDto[]; public onboardingModal = new ModalView(); @@ -38,19 +39,19 @@ export class AppsPageComponent implements OnDestroy, OnInit { } public ngOnDestroy() { - this.onboardingAppsSubscription.unsubscribe(); + this.appsSubscription.unsubscribe(); } public ngOnInit() { - this.appsStore.selectApp(null); - - this.onboardingAppsSubscription = + this.appsSubscription = this.appsStore.apps .subscribe(apps => { if (apps.length === 0 && this.onboardingService.shouldShow('dialog')) { this.onboardingService.disable('dialog'); this.onboardingModal.show(); } + + this.apps = apps; }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html index 6dfaa3fcc..53286f311 100644 --- a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html +++ b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html @@ -28,15 +28,14 @@

- An App is the repository for your project, e.g. (blog, webshop or mobile app). You can assign contributors to your app to - work together. + An App is the repository for your project, e.g. (blog, webshop or mobile app). You can assign contributors to your app to work together.

You can create an unlimited number of Apps in Squidex to manage multiple projects at the same time.

-
+
@@ -57,12 +56,11 @@ Schemas define the structure of your content, the fields and the data types of a content item.

- Beforew you can add content to your schema, make sure to hit the 'Publish' button at the top to make the schema availabel - to your content editors. + Before you can add content to your schema, make sure to hit the 'Publish' button at the top to make the schema available to your content editors.

-
+
@@ -87,7 +85,7 @@

-
+
@@ -109,12 +107,11 @@ The assets contains all files that can also be linked to your content. For example images, videos or documents.

- You can upload the assets here and use them later or also upload them directly when you create a new content item with an - asset field. + You can upload the assets here and use them later or also upload them directly when you create a new content item with an asset field.

-
+
@@ -131,8 +128,7 @@

Awesome, now you know the basics!

- But that's not all of the support we can provide.
You can go to https://docs.squidex.io/ to read more. + But that's not all of the support we can provide.
You can go to https://docs.squidex.io/ to read more.

Do you want to join our community? diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss index 7be768784..88131e189 100644 --- a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss +++ b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss @@ -14,6 +14,11 @@ p { line-height: 1.8rem; } +.col-image { + min-width: 489px; + max-width: 489px; +} + .modal { &-content, &-dialog { diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.html b/src/Squidex/app/features/assets/pages/assets-page.component.html index a127b3ab1..640ed5fcd 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.html +++ b/src/Squidex/app/features/assets/pages/assets-page.component.html @@ -1,6 +1,6 @@ - + - +

diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.ts b/src/Squidex/app/features/assets/pages/assets-page.component.ts index 4e879650f..a20a27a93 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.ts +++ b/src/Squidex/app/features/assets/pages/assets-page.component.ts @@ -12,24 +12,23 @@ import { FormControl } from '@angular/forms'; import { Subscription } from 'rxjs'; import { - AppComponentBase, - AppsStoreService, + AppContext, AssetDto, AssetsService, AssetUpdated, - AuthService, - DialogService, ImmutableArray, - MessageBus, Pager } from 'shared'; @Component({ selector: 'sqx-assets-page', styleUrls: ['./assets-page.component.scss'], - templateUrl: './assets-page.component.html' + templateUrl: './assets-page.component.html', + providers: [ + AppContext + ] }) -export class AssetsPageComponent extends AppComponentBase implements OnDestroy, OnInit { +export class AssetsPageComponent implements OnDestroy, OnInit { private assetUpdatedSubscription: Subscription; public newFiles = ImmutableArray.empty(); @@ -39,11 +38,9 @@ export class AssetsPageComponent extends AppComponentBase implements OnDestroy, public assetsFilter = new FormControl(); public assertQuery = ''; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, - private readonly assetsService: AssetsService, - private readonly messageBus: MessageBus + constructor(public readonly ctx: AppContext, + private readonly assetsService: AssetsService ) { - super(dialogs, apps, authService); } public ngOnDestroy() { @@ -52,7 +49,7 @@ export class AssetsPageComponent extends AppComponentBase implements OnDestroy, public ngOnInit() { this.assetUpdatedSubscription = - this.messageBus.of(AssetUpdated) + this.ctx.bus.of(AssetUpdated) .subscribe(event => { if (event.sender !== this) { this.assetsItems = this.assetsItems.replaceBy('id', event.assetDto); @@ -70,28 +67,26 @@ export class AssetsPageComponent extends AppComponentBase implements OnDestroy, } public load(showInfo = false) { - this.appNameOnce() - .switchMap(app => this.assetsService.getAssets(app, this.assetsPager.pageSize, this.assetsPager.skip, this.assertQuery)) + this.assetsService.getAssets(this.ctx.appName, this.assetsPager.pageSize, this.assetsPager.skip, this.assertQuery) .subscribe(dtos => { this.assetsItems = ImmutableArray.of(dtos.items); this.assetsPager = this.assetsPager.setCount(dtos.total); if (showInfo) { - this.notifyInfo('Assets reloaded.'); + this.ctx.notifyInfo('Assets reloaded.'); } }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public onAssetDeleting(asset: AssetDto) { - this.appNameOnce() - .switchMap(app => this.assetsService.deleteAsset(app, asset.id, asset.version)) + this.assetsService.deleteAsset(this.ctx.appName, asset.id, asset.version) .subscribe(dto => { this.assetsItems = this.assetsItems.filter(x => x.id !== asset.id); this.assetsPager = this.assetsPager.decrementCount(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -103,7 +98,7 @@ export class AssetsPageComponent extends AppComponentBase implements OnDestroy, } public onAssetUpdated(asset: AssetDto) { - this.messageBus.emit(new AssetUpdated(asset, this)); + this.ctx.bus.emit(new AssetUpdated(asset, this)); } public onAssetFailed(file: File) { diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index 7b91bb002..a621e5a0c 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -61,7 +61,7 @@ const routes: Routes = [ isReadOnly: true }, resolve: { - schemaOverride: ResolvePublishedSchemaGuard + schema: ResolvePublishedSchemaGuard } } ] diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.html b/src/Squidex/app/features/content/pages/content/content-history.component.html index cd7db5ce1..f04d85e62 100644 --- a/src/Squidex/app/features/content/pages/content/content-history.component.html +++ b/src/Squidex/app/features/content/pages/content/content-history.component.html @@ -1,4 +1,4 @@ - +

Activity

diff --git a/src/Squidex/app/features/content/pages/content/content-history.component.ts b/src/Squidex/app/features/content/pages/content/content-history.component.ts index cc4ed02ed..29d7fed8e 100644 --- a/src/Squidex/app/features/content/pages/content/content-history.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-history.component.ts @@ -6,19 +6,14 @@ */ import { Component } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { allParams, - AppComponentBase, - AppsStoreService, - AuthService, - DialogService, + AppContext, HistoryChannelUpdated, HistoryEventDto, HistoryService, - MessageBus, UsersProviderService } from 'shared'; @@ -29,14 +24,17 @@ const REPLACEMENT_TEMP = '$TEMP$'; @Component({ selector: 'sqx-history', styleUrls: ['./content-history.component.scss'], - templateUrl: './content-history.component.html' + templateUrl: './content-history.component.html', + providers: [ + AppContext + ] }) -export class ContentHistoryComponent extends AppComponentBase { +export class ContentHistoryComponent { public get channel(): string { - let channelPath = this.route.snapshot.data['channel']; + let channelPath = this.ctx.route.snapshot.data['channel']; if (channelPath) { - const params = allParams(this.route); + const params = allParams(this.ctx.route); for (let key in params) { if (params.hasOwnProperty(key)) { @@ -51,18 +49,13 @@ export class ContentHistoryComponent extends AppComponentBase { } public events: Observable = - Observable.timer(0, 10000) - .merge(this.messageBus.of(HistoryChannelUpdated).delay(1000)) - .switchMap(() => this.appNameOnce()) - .switchMap(app => this.historyService.getHistory(app, this.channel).retry(2)); + Observable.timer(0, 10000).merge(this.ctx.bus.of(HistoryChannelUpdated).delay(1000)) + .switchMap(app => this.historyService.getHistory(this.ctx.appName, this.channel)); - constructor(appsStore: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly users: UsersProviderService, - private readonly historyService: HistoryService, - private readonly messageBus: MessageBus, - private readonly route: ActivatedRoute + private readonly historyService: HistoryService ) { - super(dialogs, appsStore, authService); } private userName(userId: string): Observable { @@ -80,7 +73,7 @@ export class ContentHistoryComponent extends AppComponentBase { } public loadVersion(version: number) { - this.messageBus.emit(new ContentVersionSelected(version)); + this.ctx.bus.emit(new ContentVersionSelected(version)); } public format(message: string): Observable { diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index eaf689467..bb578347a 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -1,7 +1,7 @@ - + - +
diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index aa85c4da8..bd7f81dc7 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -7,7 +7,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { Observable, Subscription } from 'rxjs'; import { @@ -20,16 +20,12 @@ import { } from './../messages'; import { - AppComponentBase, + AppContext, AppLanguageDto, - AppsStoreService, allData, - AuthService, CanComponentDeactivate, ContentDto, ContentsService, - DialogService, - MessageBus, SchemaDetailsDto, Version } from 'shared'; @@ -37,9 +33,12 @@ import { @Component({ selector: 'sqx-content-page', styleUrls: ['./content-page.component.scss'], - templateUrl: './content-page.component.html' + templateUrl: './content-page.component.html', + providers: [ + AppContext + ] }) -export class ContentPageComponent extends AppComponentBase implements CanComponentDeactivate, OnDestroy, OnInit { +export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit { private contentPublishedSubscription: Subscription; private contentUnpublishedSubscription: Subscription; private contentDeletedSubscription: Subscription; @@ -55,13 +54,10 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone public languages: AppLanguageDto[] = []; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly contentsService: ContentsService, - private readonly route: ActivatedRoute, - private readonly router: Router, - private readonly messageBus: MessageBus + private readonly router: Router ) { - super(dialogs, apps, authService); } public ngOnDestroy() { @@ -72,18 +68,15 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone } public ngOnInit() { - const routeData = allData(this.route); - - this.languages = routeData['appLanguages']; this.contentVersionSelectedSubscription = - this.messageBus.of(ContentVersionSelected) + this.ctx.bus.of(ContentVersionSelected) .subscribe(message => { this.loadVersion(message.version); }); this.contentPublishedSubscription = - this.messageBus.of(ContentPublished) + this.ctx.bus.of(ContentPublished) .subscribe(message => { if (this.content && message.content.id === this.content.id) { this.content = this.content.publish(message.content.lastModifiedBy, message.content.version, message.content.lastModified); @@ -91,7 +84,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone }); this.contentUnpublishedSubscription = - this.messageBus.of(ContentUnpublished) + this.ctx.bus.of(ContentUnpublished) .subscribe(message => { if (this.content && message.content.id === this.content.id) { this.content = this.content.unpublish(message.content.lastModifiedBy, message.content.version, message.content.lastModified); @@ -99,16 +92,20 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone }); this.contentDeletedSubscription = - this.messageBus.of(ContentRemoved) + this.ctx.bus.of(ContentRemoved) .subscribe(message => { if (this.content && message.content.id === this.content.id) { - this.router.navigate(['../'], { relativeTo: this.route }); + this.router.navigate(['../'], { relativeTo: this.ctx.route }); } }); - this.setupContentForm(routeData['schema']); + const routeData = allData(this.ctx.route); + + this.languages = routeData.appLanguages; + + this.setupContentForm(routeData.schema); - this.route.data.map(p => p['content']) + this.ctx.route.data.map(d => d.content) .subscribe((content: ContentDto) => { this.content = content; @@ -120,7 +117,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone if (!this.contentForm.dirty || this.isNewMode) { return Observable.of(true); } else { - return this.dialogs.confirmUnsavedChanges(); + return this.ctx.confirmUnsavedChanges(); } } @@ -141,64 +138,66 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone const requestDto = this.contentForm.value; if (this.isNewMode) { - this.appNameOnce() - .switchMap(app => this.contentsService.postContent(app, this.schema.name, requestDto, publish)) + this.contentsService.postContent(this.ctx.appName, this.schema.name, requestDto, publish) .subscribe(dto => { this.content = dto; + this.ctx.notifyInfo('Content created successfully.'); + this.emitContentCreated(this.content); - this.notifyInfo('Content created successfully.'); this.back(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); + this.enableContentForm(); }); } else { - this.appNameOnce() - .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.content.id, requestDto, this.content.version)) + this.contentsService.putContent(this.ctx.appName, this.schema.name, this.content.id, requestDto, this.content.version) .subscribe(dto => { - this.content = this.content.update(dto.payload, this.userToken, dto.version); + this.content = this.content.update(dto.payload, this.ctx.userToken, dto.version); + + this.ctx.notifyInfo('Content saved successfully.'); this.emitContentUpdated(this.content); - this.notifyInfo('Content saved successfully.'); this.enableContentForm(); this.populateContentForm(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); + this.enableContentForm(); }); } } else { - this.notifyError('Content element not valid, please check the field with the red bar on the left in all languages (if localizable).'); + this.ctx.notifyError('Content element not valid, please check the field with the red bar on the left in all languages (if localizable).'); } } private loadVersion(version: number) { if (!this.isNewMode && this.content) { - this.appNameOnce() - .switchMap(app => this.contentsService.getVersionData(app, this.schema.name, this.content.id, new Version(version.toString()))) + this.contentsService.getVersionData(this.ctx.appName, this.schema.name, this.content.id, new Version(version.toString())) .subscribe(dto => { this.content = this.content.setData(dto); + this.ctx.notifyInfo('Content version loaded successfully.'); + this.emitContentUpdated(this.content); - this.notifyInfo('Content version loaded successfully.'); this.populateContentForm(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } } private back() { - this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true }); + this.router.navigate(['../'], { relativeTo: this.ctx.route, replaceUrl: true }); } private emitContentCreated(content: ContentDto) { - this.messageBus.emit(new ContentCreated(content)); + this.ctx.bus.emit(new ContentCreated(content)); } private emitContentUpdated(content: ContentDto) { - this.messageBus.emit(new ContentUpdated(content)); + this.ctx.bus.emit(new ContentUpdated(content)); } private disableContentForm() { @@ -208,8 +207,12 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone private enableContentForm() { this.contentForm.markAsPristine(); - for (const field of this.schema.fields.filter(f => !f.isDisabled)) { - this.contentForm.controls[field.name].enable(); + if (this.schema.fields.length === 0) { + this.contentForm.enable(); + } else { + for (const field of this.schema.fields.filter(f => !f.isDisabled)) { + this.contentForm.controls[field.name].enable(); + } } } diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index d5d39e2c4..40e630622 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -1,6 +1,6 @@ - + - +
diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index 76e94b060..dc74f9a34 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -7,7 +7,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; import { Subscription } from 'rxjs'; import { @@ -20,16 +19,12 @@ import { import { allData, - AppComponentBase, + AppContext, AppLanguageDto, - AppsStoreService, - AuthService, ContentDto, ContentsService, - DialogService, FieldDto, ImmutableArray, - MessageBus, ModalView, Pager, SchemaDetailsDto @@ -38,9 +33,12 @@ import { @Component({ selector: 'sqx-contents-page', styleUrls: ['./contents-page.component.scss'], - templateUrl: './contents-page.component.html' + templateUrl: './contents-page.component.html', + providers: [ + AppContext + ] }) -export class ContentsPageComponent extends AppComponentBase implements OnDestroy, OnInit { +export class ContentsPageComponent implements OnDestroy, OnInit { private contentCreatedSubscription: Subscription; private contentUpdatedSubscription: Subscription; @@ -63,12 +61,9 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public columnWidth: number; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, - private readonly contentsService: ContentsService, - private readonly route: ActivatedRoute, - private readonly messageBus: MessageBus + constructor(public readonly ctx: AppContext, + private readonly contentsService: ContentsService ) { - super(dialogs, apps, authService); } public ngOnDestroy() { @@ -77,37 +72,40 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy } public ngOnInit() { - const routeData = allData(this.route); - - this.languages = routeData['appLanguages']; - this.contentCreatedSubscription = - this.messageBus.of(ContentCreated) + this.ctx.bus.of(ContentCreated) .subscribe(message => { this.contentItems = this.contentItems.pushFront(message.content); this.contentsPager = this.contentsPager.incrementCount(); }); this.contentUpdatedSubscription = - this.messageBus.of(ContentUpdated) + this.ctx.bus.of(ContentUpdated) .subscribe(message => { this.contentItems = this.contentItems.replaceBy('id', message.content, (o, n) => o.update(n.data, n.lastModifiedBy, n.version, n.lastModified)); }); - this.route.params.map(p => p['language']) + const routeData = allData(this.ctx.route); + + this.languages = routeData.appLanguages; + + this.ctx.route.data.map(p => p.isReadOnly) + .subscribe(isReadOnly => { + this.isReadOnly = isReadOnly; + }); + + this.ctx.route.params.map(p => p.language) .subscribe(language => { this.languageSelected = this.languages.find(l => l.iso2Code === language) || this.languages.find(l => l.isMaster) || this.languages[0]; }); - this.route.data.map(p => p['schemaOverride'] || p['schema']) + this.ctx.route.data.map(d => d.schema) .subscribe(schema => { this.schema = schema; this.resetContents(); this.load(); }); - - this.isReadOnly = routeData['isReadOnly']; } public dropData(content: ContentDto) { @@ -115,79 +113,73 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy } public publishContent(content: ContentDto) { - this.appNameOnce() - .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version)) + this.contentsService.publishContent(this.ctx.appName, this.schema.name, content.id, content.version) .subscribe(dto => { - content = content.publish(this.userToken, dto.version); + content = content.publish(this.ctx.userToken, dto.version); this.contentItems = this.contentItems.replaceBy('id', content); this.emitContentPublished(content); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public unpublishContent(content: ContentDto) { - this.appNameOnce() - .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version)) + this.contentsService.unpublishContent(this.ctx.appName, this.schema.name, content.id, content.version) .subscribe(dto => { - content = content.unpublish(this.userToken, dto.version); + content = content.unpublish(this.ctx.userToken, dto.version); this.contentItems = this.contentItems.replaceBy('id', content); this.emitContentUnpublished(content); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public archiveContent(content: ContentDto) { - this.appNameOnce() - .switchMap(app => this.contentsService.archiveContent(app, this.schema.name, content.id, content.version)) + this.contentsService.archiveContent(this.ctx.appName, this.schema.name, content.id, content.version) .subscribe(dto => { - content = content.archive(this.userToken, dto.version); + content = content.archive(this.ctx.userToken, dto.version); this.removeContent(content); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public restoreContent(content: ContentDto) { - this.appNameOnce() - .switchMap(app => this.contentsService.restoreContent(app, this.schema.name, content.id, content.version)) + this.contentsService.restoreContent(this.ctx.appName, this.schema.name, content.id, content.version) .subscribe(dto => { - content = content.restore(this.userToken, dto.version); + content = content.restore(this.ctx.userToken, dto.version); this.removeContent(content); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public deleteContent(content: ContentDto) { - this.appNameOnce() - .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version)) + this.contentsService.deleteContent(this.ctx.appName, this.schema.name, content.id, content.version) .subscribe(() => { this.removeContent(content); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public load(showInfo = false) { - this.appNameOnce() - .switchMap(app => this.contentsService.getContents(app, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, null, this.isArchive)) + this.contentsService.getContents(this.ctx.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, null, this.isArchive) .subscribe(dtos => { this.contentItems = ImmutableArray.of(dtos.items); this.contentsPager = this.contentsPager.setCount(dtos.total); if (showInfo) { - this.notifyInfo('Contents reloaded.'); + this.ctx.notifyInfo('Contents reloaded.'); } }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -226,15 +218,15 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy } private emitContentPublished(content: ContentDto) { - this.messageBus.emit(new ContentPublished(content)); + this.ctx.bus.emit(new ContentPublished(content)); } private emitContentUnpublished(content: ContentDto) { - this.messageBus.emit(new ContentUnpublished(content)); + this.ctx.bus.emit(new ContentUnpublished(content)); } private emitContentRemoved(content: ContentDto) { - this.messageBus.emit(new ContentRemoved(content)); + this.ctx.bus.emit(new ContentRemoved(content)); } private resetContents() { diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html index f2c1d6c6b..e49d84873 100644 --- a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html @@ -1,6 +1,6 @@ - + - +

Schemas

diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts index 0664d01ff..c4e18eeb2 100644 --- a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts @@ -10,10 +10,7 @@ import { FormControl } from '@angular/forms'; import { Observable } from 'rxjs'; import { - AppComponentBase, - AppsStoreService, - AuthService, - DialogService, + AppContext, SchemaDto, SchemasService } from 'shared'; @@ -21,9 +18,12 @@ import { @Component({ selector: 'sqx-schemas-page', styleUrls: ['./schemas-page.component.scss'], - templateUrl: './schemas-page.component.html' + templateUrl: './schemas-page.component.html', + providers: [ + AppContext + ] }) -export class SchemasPageComponent extends AppComponentBase { +export class SchemasPageComponent { public schemasFilter = new FormControl(); public schemasFiltered = this.schemasFilter.valueChanges @@ -53,17 +53,15 @@ export class SchemasPageComponent extends AppComponentBase { }); }); - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly schemasService: SchemasService ) { - super(dialogs, apps, authService); } private loadSchemas(): Observable { - return this.appNameOnce() - .switchMap(app => this.schemasService.getSchemas(app).retry(2)) + return this.schemasService.getSchemas(this.ctx.appName) .catch(error => { - this.notifyError(error); + this.ctx.notifyError(error); return []; }); } diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index f00531292..3a8505fb9 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/src/Squidex/app/features/content/shared/assets-editor.component.ts @@ -12,15 +12,11 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subscription } from 'rxjs'; import { - AppComponentBase, - AppsStoreService, + AppContext, AssetDto, AssetsService, AssetUpdated, - AuthService, - DialogService, ImmutableArray, - MessageBus, Types } from 'shared'; @@ -32,9 +28,12 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-assets-editor', styleUrls: ['./assets-editor.component.scss'], templateUrl: './assets-editor.component.html', - providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [ + AppContext, + SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR + ] }) -export class AssetsEditorComponent extends AppComponentBase implements ControlValueAccessor, OnDestroy, OnInit { +export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, OnInit { private assetUpdatedSubscription: Subscription; private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; @@ -44,11 +43,9 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa public isDisabled = false; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, - private readonly assetsService: AssetsService, - private readonly messageBus: MessageBus + constructor(public readonly ctx: AppContext, + private readonly assetsService: AssetsService ) { - super(dialogs, apps, authService); } public ngOnDestroy() { @@ -57,7 +54,7 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa public ngOnInit() { this.assetUpdatedSubscription = - this.messageBus.of(AssetUpdated) + this.ctx.bus.of(AssetUpdated) .subscribe(event => { if (event.sender !== this) { this.oldAssets = this.oldAssets.replaceBy('id', event.assetDto); @@ -71,8 +68,7 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa if (Types.isArrayOfString(value) && value.length > 0) { const assetIds: string[] = value; - this.appNameOnce() - .switchMap(app => this.assetsService.getAssets(app, 10000, 0, undefined, undefined, value)) + this.assetsService.getAssets(this.ctx.appName, 10000, 0, undefined, undefined, value) .subscribe(dtos => { this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id))); }); @@ -129,7 +125,7 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa } public onAssetUpdated(asset: AssetDto) { - this.messageBus.emit(new AssetUpdated(asset, this)); + this.ctx.bus.emit(new AssetUpdated(asset, this)); } public onAssetFailed(file: File) { diff --git a/src/Squidex/app/features/content/shared/content-item.component.ts b/src/Squidex/app/features/content/shared/content-item.component.ts index 9f251d814..32f411b98 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.ts +++ b/src/Squidex/app/features/content/shared/content-item.component.ts @@ -8,11 +8,8 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { - AppComponentBase, - AppsStoreService, - AuthService, + AppContext, ContentDto, - DialogService, fadeAnimation, FieldDto, ModalView, @@ -25,11 +22,14 @@ import { selector: '[sqxContent]', styleUrls: ['./content-item.component.scss'], templateUrl: './content-item.component.html', + providers: [ + AppContext + ], animations: [ fadeAnimation ] }) -export class ContentItemComponent extends AppComponentBase implements OnInit, OnChanges { +export class ContentItemComponent implements OnInit, OnChanges { @Output() public publishing = new EventEmitter(); @@ -67,8 +67,8 @@ export class ContentItemComponent extends AppComponentBase implements OnInit, On public values: any[] = []; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService) { - super(dialogs, apps, authService); + constructor(public readonly ctx: AppContext + ) { } public ngOnChanges() { diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/src/Squidex/app/features/content/shared/references-editor.component.ts index 91b6a159f..5af111897 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.ts +++ b/src/Squidex/app/features/content/shared/references-editor.component.ts @@ -11,14 +11,12 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { - AppComponentBase, - AppsStoreService, - AuthService, + AppContext, ContentDto, ContentsService, - DialogService, FieldDto, ImmutableArray, + MathHelper, SchemaDetailsDto, SchemasService, Types @@ -32,9 +30,12 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-references-editor', styleUrls: ['./references-editor.component.scss'], templateUrl: './references-editor.component.html', - providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR] + providers: [ + AppContext, + SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR + ] }) -export class ReferencesEditorComponent extends AppComponentBase implements ControlValueAccessor, OnInit { +export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; @@ -54,16 +55,18 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr public isDisabled = false; public isInvalidSchema = false; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly contentsService: ContentsService, private readonly schemasService: SchemasService ) { - super(dialogs, apps, authService); } public ngOnInit() { - this.appNameOnce() - .switchMap(app => this.schemasService.getSchema(app, this.schemaId)) + if (this.schemaId === MathHelper.EMPTY_GUID) { + return; + } + + this.schemasService.getSchema(this.ctx.appName, this.schemaId) .subscribe(dto => { this.schema = dto; @@ -79,8 +82,7 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr if (Types.isArrayOfString(value) && value.length > 0) { const contentIds: string[] = value; - this.appNameOnce() - .switchMap(app => this.contentsService.getContents(app, this.schemaId, 10000, 0, undefined, contentIds)) + this.contentsService.getContents(this.ctx.appName, this.schemaId, 10000, 0, undefined, contentIds) .subscribe(dtos => { this.contentItems = ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)).filter(c => !!c)); }); diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html index 3630a224a..a311a9be7 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html @@ -1,13 +1,13 @@ - +
-

Hi {{profileDisplayName}}

+

Hi {{ctx.user.displayName}}

- Welcome to {{appName() | async}} dashboard. + Welcome to {{ctx.appName}} dashboard.
diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts index 15ded7f97..25e67d56e 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts @@ -8,11 +8,8 @@ import { Component, OnInit } from '@angular/core'; import { - AppComponentBase, - AppsStoreService, - AuthService, + AppContext, DateTime, - DialogService, fadeAnimation, UsagesService } from 'shared'; @@ -23,11 +20,14 @@ declare var _urq: any; selector: 'sqx-dashboard-page', styleUrls: ['./dashboard-page.component.scss'], templateUrl: './dashboard-page.component.html', + providers: [ + AppContext + ], animations: [ fadeAnimation ] }) -export class DashboardPageComponent extends AppComponentBase implements OnInit { +export class DashboardPageComponent implements OnInit { public profileDisplayName = ''; public chartStorageCount: any; @@ -60,29 +60,28 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit { public callsCurrent = 0; public callsMax = 0; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly usagesService: UsagesService ) { - super(dialogs, apps, authService); } public ngOnInit() { - this.appName() - .switchMap(app => this.usagesService.getTodayStorage(app)) + this.ctx.appChanges + .switchMap(app => this.usagesService.getTodayStorage(app.name)) .subscribe(dto => { this.assetsCurrent = dto.size; this.assetsMax = dto.maxAllowed; }); - this.appName() - .switchMap(app => this.usagesService.getMonthCalls(app)) + this.ctx.appChanges + .switchMap(app => this.usagesService.getMonthCalls(app.name)) .subscribe(dto => { this.callsCurrent = dto.count; this.callsMax = dto.maxAllowed; }); - this.appName() - .switchMap(app => this.usagesService.getStorageUsages(app, DateTime.today().addDays(-20), DateTime.today())) + this.ctx.appChanges + .switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today())) .subscribe(dtos => { this.chartStorageCount = { labels: createLabels(dtos), @@ -115,8 +114,8 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit { }; }); - this.appName() - .switchMap(app => this.usagesService.getCallsUsages(app, DateTime.today().addDays(-20), DateTime.today())) + this.ctx.appChanges + .switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today())) .subscribe(dtos => { this.chartCallsCount = { labels: createLabels(dtos), @@ -144,8 +143,6 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit { ] }; }); - - this.profileDisplayName = this.authService.user!.displayName; } public showForum() { diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts new file mode 100644 index 000000000..c2f70d3ef --- /dev/null +++ b/src/Squidex/app/features/rules/declarations.ts @@ -0,0 +1,13 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export * from './pages/rules/actions/webhook-action.component'; +export * from './pages/rules/triggers/content-changed-trigger.component'; +export * from './pages/rules/rule-wizard.component'; +export * from './pages/rules/rules-page.component'; + +export * from './pages/events/rule-events-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/index.ts b/src/Squidex/app/features/rules/index.ts similarity index 100% rename from src/Squidex/app/features/webhooks/index.ts rename to src/Squidex/app/features/rules/index.ts diff --git a/src/Squidex/app/features/webhooks/module.ts b/src/Squidex/app/features/rules/module.ts similarity index 53% rename from src/Squidex/app/features/webhooks/module.ts rename to src/Squidex/app/features/rules/module.ts index 12a58e960..23a869ede 100644 --- a/src/Squidex/app/features/webhooks/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -9,32 +9,26 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { - HelpComponent, SqxFrameworkModule, SqxSharedModule } from 'shared'; import { - WebhookComponent, - WebhookEventsPageComponent, - WebhooksPageComponent + ContentChangedTriggerComponent, + RuleEventsPageComponent, + RulesPageComponent, + RuleWizardComponent, + WebhookActionComponent } from './declarations'; const routes: Routes = [ { path: '', - component: WebhooksPageComponent, + component: RulesPageComponent, children: [ { path: 'events', - component: WebhookEventsPageComponent - }, - { - path: 'help', - component: HelpComponent, - data: { - helpPage: '05-integrated/webhooks' - } + component: RuleEventsPageComponent } ] } @@ -47,9 +41,11 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ - WebhookComponent, - WebhookEventsPageComponent, - WebhooksPageComponent + ContentChangedTriggerComponent, + RuleEventsPageComponent, + RulesPageComponent, + RuleWizardComponent, + WebhookActionComponent ] }) -export class SqxFeatureWebhooksModule { } \ No newline at end of file +export class SqxFeatureRulesModule { } \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html similarity index 93% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.html index b68b5b51c..d6c8f5f3a 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html @@ -1,6 +1,6 @@ - + - +
@@ -39,7 +39,7 @@ Event - Url + Description Created @@ -57,7 +57,7 @@ {{event.eventName}} - {{event.requestUrl}} + {{event.description}} {{event.created | sqxFromNow}} @@ -71,7 +71,7 @@
-

Last Request

+

Last Invocation

diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss similarity index 87% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss index e131e5532..bc650f4f1 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss @@ -5,6 +5,10 @@ h3 { margin-bottom: 1rem; } +.truncate { + @include truncate; +} + .event { &-stats { font-size: .8rem; @@ -31,7 +35,7 @@ h3 { &::before { @include caret-top($color-border); - @include absolute(-1.1rem, 1.9rem, auto, auto); + @include absolute(-1.1rem, 2.3rem, auto, auto); } h3 { diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts similarity index 55% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts index 53bed278e..eed28b81e 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts @@ -8,31 +8,30 @@ import { Component, OnInit } from '@angular/core'; import { - AppComponentBase, - AppsStoreService, - AuthService, - DialogService, + AppContext, ImmutableArray, Pager, - WebhookEventDto, - WebhooksService + RuleEventDto, + RulesService } from 'shared'; @Component({ - selector: 'sqx-webhook-events-page', - styleUrls: ['./webhook-events-page.component.scss'], - templateUrl: './webhook-events-page.component.html' + selector: 'sqx-rule-events-page', + styleUrls: ['./rule-events-page.component.scss'], + templateUrl: './rule-events-page.component.html', + providers: [ + AppContext + ] }) -export class WebhookEventsPageComponent extends AppComponentBase implements OnInit { - public eventsItems = ImmutableArray.empty(); +export class RuleEventsPageComponent implements OnInit { + public eventsItems = ImmutableArray.empty(); public eventsPager = new Pager(0); public selectedEventId: string | null = null; - constructor(dialogs: DialogService, appsStore: AppsStoreService, authService: AuthService, - private readonly webhooksService: WebhooksService + constructor(public readonly ctx: AppContext, + private readonly rulesService: RulesService ) { - super(dialogs, appsStore, authService); } public ngOnInit() { @@ -40,27 +39,25 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn } public load(showInfo = false) { - this.appNameOnce() - .switchMap(app => this.webhooksService.getEvents(app, this.eventsPager.pageSize, this.eventsPager.skip)) + this.rulesService.getEvents(this.ctx.appName, this.eventsPager.pageSize, this.eventsPager.skip) .subscribe(dtos => { this.eventsItems = ImmutableArray.of(dtos.items); this.eventsPager = this.eventsPager.setCount(dtos.total); if (showInfo) { - this.notifyInfo('Events reloaded.'); + this.ctx.notifyInfo('Events reloaded.'); } }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } - public enqueueEvent(event: WebhookEventDto) { - this.appNameOnce() - .switchMap(app => this.webhooksService.enqueueEvent(app, event.id)) + public enqueueEvent(event: RuleEventDto) { + this.rulesService.enqueueEvent(this.ctx.appName, event.id) .subscribe(() => { - this.notifyInfo('Events enqueued. Will be send in a few seconds.'); + this.ctx.notifyInfo('Events enqueued. Will be resend in a few seconds.'); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html new file mode 100644 index 000000000..62f2231d5 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html @@ -0,0 +1,29 @@ + +
+ + +
+ + + + + + The url where the events will be sent to. + +
+
+ +
+ + +
+ + + + + + The shared secret will be used to add a header X-Signature=Sha256(RequestBody + Secret) + +
+
+ \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts new file mode 100644 index 000000000..d11b3e8c8 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts @@ -0,0 +1,55 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; + +@Component({ + selector: 'sqx-webhook-action', + styleUrls: ['./webhook-action.component.scss'], + templateUrl: './webhook-action.component.html' +}) +export class WebhookActionComponent implements OnInit { + @Input() + public action: any; + + @Output() + public actionChanged = new EventEmitter(); + + public actionFormSubmitted = false; + public actionForm = + this.formBuilder.group({ + url: ['', + [ + Validators.required + ]], + sharedSecret: [''] + }); + + constructor( + private readonly formBuilder: FormBuilder + ) { + } + + public ngOnInit() { + this.action = Object.assign({}, { url: '', sharedSecret: '' }, this.action || {}); + + this.actionFormSubmitted = false; + this.actionForm.reset(); + this.actionForm.setValue(this.action); + } + + public save() { + this.actionFormSubmitted = true; + + if (this.actionForm.valid) { + const action = this.actionForm.value; + + this.actionChanged.emit(action); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html new file mode 100644 index 000000000..88bc0b3a7 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -0,0 +1,91 @@ + \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss new file mode 100644 index 000000000..d17243924 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss @@ -0,0 +1,38 @@ +@import '_vars'; +@import '_mixins'; + +.modal { + &-dialog { + height: 70%; + } + + &-content { + min-height: 100%; + max-height: 100%; + } + + &-body { + overflow-y: auto; + } + + &-header { + @include flex-shrink(0); + } + + &-footer { + @include flex-shrink(0); + } + + &-form { + padding-top: 2em; + padding-bottom: 0; + } +} + +.clearfix { + width: 100%; +} + +.rule-element { + margin-right: .5rem; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts new file mode 100644 index 000000000..f9da79804 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts @@ -0,0 +1,164 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; + +import { + AppContext, + CreateRuleDto, + DateTime, + fadeAnimation, + ruleActions, + ruleTriggers, + RuleDto, + RulesService, + SchemaDto, + UpdateRuleDto +} from 'shared'; + +export const MODE_WIZARD = 'Wizard'; +export const MODE_EDIT_TRIGGER = 'EditTrigger'; +export const MODE_EDIT_ACTION = 'EditAction'; + +@Component({ + selector: 'sqx-rule-wizard', + styleUrls: ['./rule-wizard.component.scss'], + templateUrl: './rule-wizard.component.html', + providers: [ + AppContext + ], + animations: [ + fadeAnimation + ] +}) +export class RuleWizardComponent implements OnInit { + public ruleActions = ruleActions; + public ruleTriggers = ruleTriggers; + + public triggerType: string; + public trigger: any = {}; + public actionType: string; + public action: any = {}; + public step = 1; + + @ViewChild('triggerControl') + public triggerControl: any; + + @ViewChild('actionControl') + public actionControl: any; + + @Output() + public cancelled = new EventEmitter(); + + @Output() + public created = new EventEmitter(); + + @Output() + public updated = new EventEmitter(); + + @Input() + public schemas: SchemaDto[]; + + @Input() + public rule: RuleDto; + + @Input() + public mode = MODE_WIZARD; + + constructor(public readonly ctx: AppContext, + private readonly rulesService: RulesService + ) { + } + + public ngOnInit() { + if (this.mode === MODE_EDIT_ACTION) { + this.step = 4; + + this.action = Object.assign({}, this.rule.action); + this.actionType = this.rule.actionType; + + delete this.action.actionType; + } else if (this.mode === MODE_EDIT_TRIGGER) { + this.step = 2; + + this.trigger = Object.assign({}, this.rule.trigger); + this.triggerType = this.rule.triggerType; + + delete this.trigger.triggerType; + } + } + + public selectTriggerType(type: string) { + this.triggerType = type; + this.step++; + } + + public selectActionType(type: string) { + this.actionType = type; + this.step++; + } + + public selectTrigger(value: any) { + this.trigger = Object.assign({}, value, { triggerType: this.triggerType }); + + if (this.mode === MODE_WIZARD) { + this.step++; + } else { + this.updateTrigger(); + } + } + + public selectAction(value: any) { + this.action = Object.assign({}, value, { actionType: this.actionType }); + + if (this.mode === MODE_WIZARD) { + this.createRule(); + } else { + this.updateAction(); + } + } + + private createRule() { + const requestDto = new CreateRuleDto(this.trigger, this.action); + + this.rulesService.postRule(this.ctx.appName, requestDto, this.ctx.userToken, DateTime.now()) + .subscribe(dto => { + this.created.emit(dto); + }, error => { + this.ctx.notifyError(error); + }); + } + + private updateTrigger() { + const requestDto = new UpdateRuleDto(this.trigger, null); + + this.rulesService.putRule(this.ctx.appName, this.rule.id, requestDto, this.rule.version) + .subscribe(dto => { + const rule = this.rule.updateTrigger(this.trigger, this.ctx.userToken, dto.version, DateTime.now()); + this.updated.emit(rule); + }, error => { + this.ctx.notifyError(error); + }); + } + + private updateAction() { + const requestDto = new UpdateRuleDto(null, this.action); + + this.rulesService.putRule(this.ctx.appName, this.rule.id, requestDto, this.rule.version) + .subscribe(dto => { + const rule = this.rule.updateAction(this.action, this.ctx.userToken, dto.version, DateTime.now()); + + this.updated.emit(rule); + }, error => { + this.ctx.notifyError(error); + }); + } + + public cancel() { + this.cancelled.emit(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html new file mode 100644 index 000000000..c57303e10 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -0,0 +1,108 @@ + + + +
+
+
+ + + + + + +
+ +

Rules

+
+ + + + +
+ +
+
+
+ No Rule created yet. +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+

If

+
+ + + + + + {{ruleTriggers[rule.triggerType]}} + + + +

then

+
+ + + + + + {{ruleActions[rule.actionType]}} + + + + + + +
+
+ +
+ + + +
+
+
+ + + + \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss b/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss new file mode 100644 index 000000000..c7674827d --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss @@ -0,0 +1,26 @@ +@import '_vars'; +@import '_mixins'; + +sqx-toggle { + display: inline-block; +} + +.table-items { + tbody { + td { + padding-bottom: .5rem; + } + } +} + +.step-if { + padding-left: 1.25rem; + padding-right: 0; + text-align: left; +} + +.step-then { + padding-left: 0; + padding-right: 0; + text-align: center; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts new file mode 100644 index 000000000..62c491b82 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts @@ -0,0 +1,131 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; + +import { + AppContext, + DateTime, + fadeAnimation, + ImmutableArray, + ModalView, + ruleActions, + ruleTriggers, + RuleDto, + RulesService, + SchemaDto, + SchemasService +} from 'shared'; + +@Component({ + selector: 'sqx-rules-page', + styleUrls: ['./rules-page.component.scss'], + templateUrl: './rules-page.component.html', + providers: [ + AppContext + ], + animations: [ + fadeAnimation + ] +}) +export class RulesPageComponent implements OnInit { + public ruleActions = ruleActions; + public ruleTriggers = ruleTriggers; + + public addRuleDialog = new ModalView(); + + public rules: ImmutableArray; + public schemas: SchemaDto[]; + + public wizardMode = 'Wizard'; + public wizardRule: RuleDto; + + constructor(public readonly ctx: AppContext, + private readonly schemasService: SchemasService, + private readonly rulesService: RulesService + ) { + } + + public ngOnInit() { + this.load(); + } + + public load(showInfo = false) { + this.schemasService.getSchemas(this.ctx.appName) + .combineLatest(this.rulesService.getRules(this.ctx.appName), (s, w) => { return { rules: w, schemas: s }; }) + .subscribe(dtos => { + this.schemas = dtos.schemas; + this.rules = ImmutableArray.of(dtos.rules); + + if (showInfo) { + this.ctx.notifyInfo('Rules reloaded.'); + } + }, error => { + this.ctx.notifyError(error); + }); + } + + public createNew() { + this.wizardMode = 'Wizard'; + this.wizardRule = null; + + this.addRuleDialog.show(); + } + + public editTrigger(rule: RuleDto) { + this.wizardMode = 'EditTrigger'; + this.wizardRule = rule; + + this.addRuleDialog.show(); + } + + public editAction(rule: RuleDto) { + this.wizardMode = 'EditAction'; + this.wizardRule = rule; + + this.addRuleDialog.show(); + } + + public onRuleUpdated(rule: RuleDto) { + this.rules = this.rules.replaceBy('id', rule); + + this.addRuleDialog.hide(); + } + + public onRuleCreated(rule: RuleDto) { + this.rules = this.rules.push(rule); + + this.addRuleDialog.hide(); + } + + public toggleRule(rule: RuleDto) { + if (rule.isEnabled) { + this.rulesService.disableRule(this.ctx.appName, rule.id, rule.version) + .subscribe(dto => { + this.rules = this.rules.replace(rule, rule.disable(this.ctx.userToken, dto.version, DateTime.now())); + }, error => { + this.ctx.notifyError(error); + }); + } else { + this.rulesService.enableRule(this.ctx.appName, rule.id, rule.version) + .subscribe(dto => { + this.rules = this.rules.replace(rule, rule.enable(this.ctx.userToken, dto.version, DateTime.now())); + }, error => { + this.ctx.notifyError(error); + }); + } + } + + public deleteRule(rule: RuleDto) { + this.rulesService.deleteRule(this.ctx.appName, rule.id, rule.version) + .subscribe(dto => { + this.rules = this.rules.remove(rule); + }, error => { + this.ctx.notifyError(error); + }); + } +} diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html new file mode 100644 index 000000000..dca702cbe --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Schema + + All + + C + + U + + D + + P +
+ {{schema.schema.name}} + + + + + + + + + + + + +
+ +
+
+
+ +
+ + +
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss new file mode 100644 index 000000000..4869322fb --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss @@ -0,0 +1,8 @@ +@import '_vars'; +@import '_mixins'; + +.section { + border-top: 1px solid $color-border; + padding-top: 1rem; + padding-bottom: 0; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts new file mode 100644 index 000000000..dabe56214 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts @@ -0,0 +1,147 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { + ImmutableArray, + SchemaDto +} from 'shared'; + +export interface TriggerSchemaForm { + schema: SchemaDto; + sendAll: boolean; + sendCreate: boolean; + sendUpdate: boolean; + sendDelete: boolean; + sendPublish: boolean; +} + +@Component({ + selector: 'sqx-content-changed-trigger', + styleUrls: ['./content-changed-trigger.component.scss'], + templateUrl: './content-changed-trigger.component.html' +}) +export class ContentChangedTriggerComponent implements OnInit { + @Input() + public schemas: SchemaDto[]; + + @Input() + public trigger: any; + + @Output() + public triggerChanged = new EventEmitter(); + + public triggerSchemas: ImmutableArray; + + public schemaToAdd: SchemaDto; + public schemasToAdd: ImmutableArray; + + public get hasSchema() { + return !!this.schemaToAdd; + } + + public ngOnInit() { + const triggerSchemas: any[] = (this.trigger.schemas = this.trigger.schemas || []); + + this.triggerSchemas = + ImmutableArray.of( + triggerSchemas.map(triggerSchema => { + const schema = this.schemas.find(s => s.id === triggerSchema.schemaId); + + if (schema) { + return this.updateSendAll({ + schema: schema, + sendAll: false, + sendCreate: triggerSchema.sendCreate, + sendUpdate: triggerSchema.sendUpdate, + sendDelete: triggerSchema.sendDelete, + sendPublish: triggerSchema.sendPublish + }); + } else { + return null; + } + }).filter(s => s !== null).map(s => s!)).sortByStringAsc(s => s.schema.name); + + this.schemasToAdd = + ImmutableArray.of( + this.schemas.filter(schema => + !triggerSchemas.find(s => s.schemaId === schema.id))) + .sortByStringAsc(x => x.name); + + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public save() { + const schemas = + this.triggerSchemas.values.map(s => { + return { + schemaId: s.schema.id, + sendCreate: s.sendCreate, + sendUpdate: s.sendUpdate, + sendDelete: s.sendDelete, + sendPublish: s.sendPublish + }; + }); + + this.triggerChanged.emit({ schemas }); + } + + public removeSchema(schemaForm: TriggerSchemaForm) { + this.triggerSchemas = this.triggerSchemas.remove(schemaForm); + + this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name); + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public addSchema() { + this.triggerSchemas = + this.triggerSchemas.push( + this.updateSendAll({ + schema: this.schemaToAdd, + sendAll: false, + sendCreate: false, + sendUpdate: false, + sendDelete: false, + sendPublish: false + })).sortByStringAsc(x => x.schema.name); + + this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public toggle(schemaForm: TriggerSchemaForm, property: string) { + const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); + + this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema); + } + + public toggleAll(schemaForm: TriggerSchemaForm) { + const newSchema = this.updateAll({ schema: schemaForm.schema }, !schemaForm.sendAll); + + this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema); + } + + private updateAll(schemaForm: TriggerSchemaForm, value: boolean): TriggerSchemaForm { + schemaForm.sendAll = value; + schemaForm.sendCreate = value; + schemaForm.sendUpdate = value; + schemaForm.sendDelete = value; + schemaForm.sendPublish = value; + return schemaForm; + } + + private updateSendAll(schemaForm: TriggerSchemaForm): TriggerSchemaForm { + schemaForm.sendAll = + schemaForm.sendCreate && + schemaForm.sendUpdate && + schemaForm.sendDelete && + schemaForm.sendPublish; + + return schemaForm; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts index fd9f28897..7cf81e3b9 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts @@ -9,8 +9,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; import { - ComponentBase, - DialogService, + AppContext, SchemaPropertiesDto, SchemasService, Version @@ -19,9 +18,12 @@ import { @Component({ selector: 'sqx-schema-edit-form', styleUrls: ['./schema-edit-form.component.scss'], - templateUrl: './schema-edit-form.component.html' + templateUrl: './schema-edit-form.component.html', + providers: [ + AppContext + ] }) -export class SchemaEditFormComponent extends ComponentBase implements OnInit { +export class SchemaEditFormComponent implements OnInit { @Output() public saved = new EventEmitter(); @@ -37,9 +39,6 @@ export class SchemaEditFormComponent extends ComponentBase implements OnInit { @Input() public version: Version; - @Input() - public appName: string; - public editFormSubmitted = false; public editForm = this.formBuilder.group({ @@ -53,11 +52,10 @@ export class SchemaEditFormComponent extends ComponentBase implements OnInit { ]] }); - constructor(dialogs: DialogService, + constructor(public readonly ctx: AppContext, private readonly schemas: SchemasService, private readonly formBuilder: FormBuilder ) { - super(dialogs); } public ngOnInit() { @@ -77,12 +75,12 @@ export class SchemaEditFormComponent extends ComponentBase implements OnInit { const requestDto = this.editForm.value; - this.schemas.putSchema(this.appName, this.name, requestDto, this.version) + this.schemas.putSchema(this.ctx.appName, this.name, requestDto, this.version) .subscribe(dto => { this.emitSaved(requestDto); this.resetEditForm(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); this.enableEditForm(); }); } diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 056a1f665..3569894ae 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -1,6 +1,6 @@ - + - +
@@ -138,7 +138,7 @@
@@ -167,7 +167,7 @@ diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index 5cbbca5d7..69d6c8955 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -7,22 +7,18 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; import { AddFieldDto, - AppComponentBase, - AppsStoreService, - AuthService, + AppContext, createProperties, - DialogService, fadeAnimation, FieldDto, fieldTypes, HistoryChannelUpdated, ImmutableArray, - MessageBus, ModalView, SchemaDetailsDto, SchemaDto, @@ -44,11 +40,14 @@ import { selector: 'sqx-schema-page', styleUrls: ['./schema-page.component.scss'], templateUrl: './schema-page.component.html', + providers: [ + AppContext + ], animations: [ fadeAnimation ] }) -export class SchemaPageComponent extends AppComponentBase implements OnDestroy, OnInit { +export class SchemaPageComponent implements OnDestroy, OnInit { private schemaCreatedSubscription: Subscription; public fieldTypes = fieldTypes; @@ -83,14 +82,11 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, return this.addFieldForm.controls['name'].value && this.addFieldForm.controls['name'].value.length > 0; } - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly formBuilder: FormBuilder, - private readonly messageBus: MessageBus, - private readonly route: ActivatedRoute, private readonly router: Router, private readonly schemasService: SchemasService ) { - super(dialogs, apps, authService); } public ngOnDestroy() { @@ -99,14 +95,14 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, public ngOnInit() { this.schemaCreatedSubscription = - this.messageBus.of(SchemaCreated) + this.ctx.bus.of(SchemaCreated) .subscribe(message => { if (this.schemas) { this.schemas = this.schemas.push(message.schema); } }); - this.route.data.map(p => p['schema']) + this.ctx.route.data.map(d => d.schema) .subscribe((schema: SchemaDetailsDto) => { this.schema = schema; @@ -117,125 +113,113 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, } private load() { - this.appNameOnce() - .switchMap(app => this.schemasService.getSchemas(app)) + this.schemasService.getSchemas(this.ctx.appName) .subscribe(dtos => { this.schemas = ImmutableArray.of(dtos); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public publish() { - this.appNameOnce() - .switchMap(app => this.schemasService.publishSchema(app, this.schema.name, this.schema.version)).retry(2) + this.schemasService.publishSchema(this.ctx.appName, this.schema.name, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.publish(this.userToken, dto.version)); + this.updateSchema(this.schema.publish(this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public unpublish() { - this.appNameOnce() - .switchMap(app => this.schemasService.unpublishSchema(app, this.schema.name, this.schema.version)).retry(2) + this.schemasService.unpublishSchema(this.ctx.appName, this.schema.name, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.unpublish(this.userToken, dto.version)); + this.updateSchema(this.schema.unpublish(this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public enableField(field: FieldDto) { - this.appNameOnce() - .switchMap(app => this.schemasService.enableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) + this.schemasService.enableField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.updateField(field.enable(), this.userToken, dto.version)); + this.updateSchema(this.schema.updateField(field.enable(), this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public disableField(field: FieldDto) { - this.appNameOnce() - .switchMap(app => this.schemasService.disableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) + this.schemasService.disableField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.updateField(field.disable(), this.userToken, dto.version)); + this.updateSchema(this.schema.updateField(field.disable(), this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public lockField(field: FieldDto) { - this.appNameOnce() - .switchMap(app => this.schemasService.lockField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) + this.schemasService.lockField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.updateField(field.lock(), this.userToken, dto.version)); + this.updateSchema(this.schema.updateField(field.lock(), this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public showField(field: FieldDto) { - this.appNameOnce() - .switchMap(app => this.schemasService.showField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) + this.schemasService.showField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.updateField(field.show(), this.userToken, dto.version)); + this.updateSchema(this.schema.updateField(field.show(), this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public hideField(field: FieldDto) { - this.appNameOnce() - .switchMap(app => this.schemasService.hideField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) + this.schemasService.hideField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.updateField(field.hide(), this.userToken, dto.version)); + this.updateSchema(this.schema.updateField(field.hide(), this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public deleteField(field: FieldDto) { - this.appNameOnce() - .switchMap(app => this.schemasService.deleteField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) + this.schemasService.deleteField(this.ctx.appName, this.schema.name, field.fieldId, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.removeField(field, this.userToken, dto.version)); + this.updateSchema(this.schema.removeField(field, this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public sortFields(fields: FieldDto[]) { - this.appNameOnce() - .switchMap(app => this.schemasService.putFieldOrdering(app, this.schema.name, fields.map(t => t.fieldId), this.schema.version)).retry(2) + this.schemasService.putFieldOrdering(this.ctx.appName, this.schema.name, fields.map(t => t.fieldId), this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.replaceFields(fields, this.userToken, dto.version)); + this.updateSchema(this.schema.replaceFields(fields, this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public saveField(field: FieldDto) { const requestDto = new UpdateFieldDto(field.properties); - this.appNameOnce() - .switchMap(app => this.schemasService.putField(app, this.schema.name, field.fieldId, requestDto, this.schema.version)).retry(2) + this.schemasService.putField(this.ctx.appName, this.schema.name, field.fieldId, requestDto, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.updateField(field, this.userToken, dto.version)); + this.updateSchema(this.schema.updateField(field, this.ctx.userToken, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public deleteSchema() { - this.appNameOnce() - .switchMap(app => this.schemasService.deleteSchema(app, this.schema.name, this.schema.version)).retry(2) + this.schemasService.deleteSchema(this.ctx.appName, this.schema.name, this.schema.version) .subscribe(() => { this.onSchemaRemoved(this.schema); this.back(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -250,13 +234,12 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, const requestDto = new AddFieldDto(this.addFieldForm.controls['name'].value, partitioning, properties); - this.appNameOnce() - .switchMap(app => this.schemasService.postField(app, this.schema.name, requestDto, this.schema.version)) + this.schemasService.postField(this.ctx.appName, this.schema.name, requestDto, this.schema.version) .subscribe(dto => { - this.updateSchema(this.schema.addField(dto.payload, this.userToken, dto.version)); + this.updateSchema(this.schema.addField(dto.payload, this.ctx.userToken, dto.version)); this.resetFieldForm(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); this.resetFieldForm(); }); } @@ -267,13 +250,13 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, } public onSchemaSaved(properties: SchemaPropertiesDto, version: Version) { - this.updateSchema(this.schema.update(properties, this.userToken, version)); + this.updateSchema(this.schema.update(properties, this.ctx.userToken, version)); this.editSchemaDialog.hide(); } public onSchemaScriptsSaved(scripts: UpdateSchemaScriptsDto, version: Version) { - this.updateSchema(this.schema.configureScripts(scripts, this.userToken, version)); + this.updateSchema(this.schema.configureScripts(scripts, this.ctx.userToken, version)); this.configureScriptsDialog.hide(); } @@ -295,7 +278,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, this.schemas = this.schemas.replaceBy('id', schema); this.emitSchemaUpdated(schema); - this.notify(); + this.emitHistoryUpdate(); this.export(); } @@ -331,19 +314,19 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy, } private back() { - this.router.navigate(['../'], { relativeTo: this.route }); + this.router.navigate(['../'], { relativeTo: this.ctx.route }); } private emitSchemaDeleted(schema: SchemaDto) { - this.messageBus.emit(new SchemaDeleted(schema)); + this.ctx.bus.emit(new SchemaDeleted(schema)); } private emitSchemaUpdated(schema: SchemaDto) { - this.messageBus.emit(new SchemaUpdated(schema)); + this.ctx.bus.emit(new SchemaUpdated(schema)); } - private notify() { - this.messageBus.emit(new HistoryChannelUpdated()); + private emitHistoryUpdate() { + this.ctx.bus.emit(new HistoryChannelUpdated()); } } diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts index 5d5c602f8..d9315eba1 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts @@ -9,8 +9,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { - ComponentBase, - DialogService, + AppContext, SchemaDetailsDto, SchemasService, UpdateSchemaScriptsDto @@ -19,9 +18,12 @@ import { @Component({ selector: 'sqx-schema-scripts-form', styleUrls: ['./schema-scripts-form.component.scss'], - templateUrl: './schema-scripts-form.component.html' + templateUrl: './schema-scripts-form.component.html', + providers: [ + AppContext + ] }) -export class SchemaScriptsFormComponent extends ComponentBase implements OnInit { +export class SchemaScriptsFormComponent implements OnInit { @Output() public saved = new EventEmitter(); @@ -31,9 +33,6 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit @Input() public schema: SchemaDetailsDto; - @Input() - public appName: string; - public selectedField = 'scriptQuery'; public scripts = [ @@ -54,11 +53,10 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit scriptChange: '' }); - constructor(dialogs: DialogService, + constructor(public readonly ctx: AppContext, private readonly schemas: SchemasService, private readonly formBuilder: FormBuilder ) { - super(dialogs); } public ngOnInit() { @@ -84,12 +82,12 @@ export class SchemaScriptsFormComponent extends ComponentBase implements OnInit this.editForm.controls['scriptDelete'].value, this.editForm.controls['scriptChange'].value); - this.schemas.putSchemaScripts(this.appName, this.schema.name, requestDto, this.schema.version) + this.schemas.putSchemaScripts(this.ctx.appName, this.schema.name, requestDto, this.schema.version) .subscribe(dto => { this.emitSaved(requestDto); this.resetEditForm(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); this.enableEditForm(); }); } diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html index 734c84f47..2dfe312bc 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html @@ -13,7 +13,7 @@ - The schema name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName}}/{{schemaName | async}}/. + The schema name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{ctx.appName}}/{{schemaName | async}}/.
It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later. diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts index 5442c6b70..e59892ed1 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts @@ -5,12 +5,12 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Output } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; import { ApiUrlConfig, - AuthService, + AppContext, DateTime, fadeAnimation, SchemaDetailsDto, @@ -24,6 +24,9 @@ const FALLBACK_NAME = 'my-schema'; selector: 'sqx-schema-form', styleUrls: ['./schema-form.component.scss'], templateUrl: './schema-form.component.html', + providers: [ + AppContext + ], animations: [ fadeAnimation ] @@ -35,9 +38,6 @@ export class SchemaFormComponent { @Output() public cancelled = new EventEmitter(); - @Input() - public appName: string; - public showImport = false; public createFormError = ''; @@ -59,9 +59,9 @@ export class SchemaFormComponent { constructor( public readonly apiUrl: ApiUrlConfig, + public readonly ctx: AppContext, private readonly schemas: SchemasService, - private readonly formBuilder: FormBuilder, - private readonly authService: AuthService + private readonly formBuilder: FormBuilder ) { } @@ -85,9 +85,7 @@ export class SchemaFormComponent { const schemaName = this.createForm.controls['name'].value; const schemaDto = Object.assign(this.createForm.controls['import'].value || {}, { name: schemaName }); - const me = this.authService.user!.token; - - this.schemas.postSchema(this.appName, schemaDto, me, DateTime.now()) + this.schemas.postSchema(this.ctx.appName, schemaDto, this.ctx.userToken, DateTime.now()) .subscribe(dto => { this.emitCreated(dto); this.resetCreateForm(); diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html index 29831ee93..39ce9a103 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html @@ -1,6 +1,6 @@ - + - +

Schemas

@@ -66,7 +66,10 @@
diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index d087820aa..c050a653d 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -6,18 +6,13 @@ */ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { FormControl } from '@angular/forms'; import { Subscription } from 'rxjs'; import { - AppComponentBase, - AppsStoreService, - AuthService, - DialogService, + AppContext, fadeAnimation, ImmutableArray, - MessageBus, ModalView, SchemaDto, SchemasService @@ -33,11 +28,14 @@ import { selector: 'sqx-schemas-page', styleUrls: ['./schemas-page.component.scss'], templateUrl: './schemas-page.component.html', + providers: [ + AppContext + ], animations: [ fadeAnimation ] }) -export class SchemasPageComponent extends AppComponentBase implements OnDestroy, OnInit { +export class SchemasPageComponent implements OnDestroy, OnInit { private schemaUpdatedSubscription: Subscription; private schemaDeletedSubscription: Subscription; @@ -48,12 +46,9 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, public schemasFilter = new FormControl(); public schemasFiltered = ImmutableArray.empty(); - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, - private readonly schemasService: SchemasService, - private readonly messageBus: MessageBus, - private readonly route: ActivatedRoute + constructor(public readonly ctx: AppContext, + private readonly schemasService: SchemasService ) { - super(dialogs, apps, authService); } public ngOnDestroy() { @@ -69,7 +64,7 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, this.updateSchemas(this.schemas, this.schemaQuery = q); }); - this.route.params.map(q => q['showDialog']) + this.ctx.route.params.map(q => q['showDialog']) .subscribe(showDialog => { if (showDialog) { this.addSchemaDialog.show(); @@ -77,13 +72,13 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, }); this.schemaUpdatedSubscription = - this.messageBus.of(SchemaUpdated) + this.ctx.bus.of(SchemaUpdated) .subscribe(m => { this.updateSchemas(this.schemas.replaceBy('id', m.schema)); }); this.schemaDeletedSubscription = - this.messageBus.of(SchemaDeleted) + this.ctx.bus.of(SchemaDeleted) .subscribe(m => { this.updateSchemas(this.schemas.filter(s => s.id !== m.schema.id)); }); @@ -92,12 +87,11 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, } private load() { - this.appNameOnce() - .switchMap(app => this.schemasService.getSchemas(app).retry(2)) + this.schemasService.getSchemas(this.ctx.appName) .subscribe(dtos => { this.updateSchemas(ImmutableArray.of(dtos)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -109,7 +103,7 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, } private emitSchemaCreated(schema: SchemaDto) { - this.messageBus.emit(new SchemaCreated(schema)); + this.ctx.bus.emit(new SchemaCreated(schema)); } private updateSchemas(schemas: ImmutableArray, query?: string) { diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html index c6bb7a5b7..f7d867a31 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.html +++ b/src/Squidex/app/features/settings/pages/clients/client.component.html @@ -50,7 +50,7 @@ Client Id: - + diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts index 7963fe414..c343aadb3 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts @@ -10,16 +10,12 @@ import { FormBuilder, Validators } from '@angular/forms'; import { Observable } from 'rxjs'; import { - AppComponentBase, + AppContext, AppContributorDto, AppContributorsDto, AppContributorsService, - AppsStoreService, - AuthService, AutocompleteSource, - DialogService, HistoryChannelUpdated, - MessageBus, UsersService } from 'shared'; @@ -48,13 +44,14 @@ export class UsersDataSource implements AutocompleteSource { @Component({ selector: 'sqx-contributors-page', styleUrls: ['./contributors-page.component.scss'], - templateUrl: './contributors-page.component.html' + templateUrl: './contributors-page.component.html', + providers: [ + AppContext + ] }) -export class ContributorsPageComponent extends AppComponentBase implements OnInit { +export class ContributorsPageComponent implements OnInit { public appContributors: AppContributorsDto; - public currentUserId: string; - public maxContributors = -1; public usersDataSource: UsersDataSource; @@ -72,64 +69,56 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni ]] }); - constructor(apps: AppsStoreService, dialogs: DialogService, usersService: UsersService, authService: AuthService, + constructor(public readonly ctx: AppContext, usersService: UsersService, private readonly appContributorsService: AppContributorsService, - private readonly messageBus: MessageBus, private readonly formBuilder: FormBuilder ) { - super(dialogs, apps, authService); - this.usersDataSource = new UsersDataSource(usersService, this); } public ngOnInit() { - this.currentUserId = this.authService.user!.id; - this.load(); } public load() { - this.appNameOnce() - .switchMap(app => this.appContributorsService.getContributors(app).retry(2)) + this.appContributorsService.getContributors(this.ctx.appName) .subscribe(dto => { this.updateContributorsFromDto(dto); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public removeContributor(contributor: AppContributorDto) { - this.appNameOnce() - .switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId, this.appContributors.version)) + this.appContributorsService.deleteContributor(this.ctx.appName, contributor.contributorId, this.appContributors.version) .subscribe(dto => { this.updateContributors(this.appContributors.removeContributor(contributor, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public changePermission(contributor: AppContributorDto, permission: string) { const requestDto = contributor.changePermission(permission); - this.appNameOnce() - .switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.appContributors.version)) + this.appContributorsService.postContributor(this.ctx.appName, requestDto, this.appContributors.version) .subscribe(dto => { this.updateContributors(this.appContributors.updateContributor(contributor, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public assignContributor() { const requestDto = new AppContributorDto(this.addContributorForm.controls['user'].value.id, 'Editor'); - this.appNameOnce() - .switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.appContributors.version)) + this.appContributorsService.postContributor(this.ctx.appName, requestDto, this.appContributors.version) .subscribe(dto => { this.updateContributors(this.appContributors.addContributor(requestDto, dto.version)); this.resetContributorForm(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); + this.resetContributorForm(); }); } @@ -147,6 +136,6 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni private updateContributors(appContributors: AppContributorsDto) { this.appContributors = appContributors; - this.messageBus.emit(new HistoryChannelUpdated()); + this.ctx.bus.emit(new HistoryChannelUpdated()); } } diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html index 6b507f902..9ebf675d8 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html @@ -1,6 +1,6 @@ - + - +

Languages

diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts index ee105294b..40b82b236 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts @@ -10,16 +10,12 @@ import { FormBuilder, Validators } from '@angular/forms'; import { AddAppLanguageDto, - AppComponentBase, + AppContext, AppLanguageDto, AppLanguagesDto, AppLanguagesService, - AppsStoreService, - AuthService, - DialogService, HistoryChannelUpdated, ImmutableArray, - MessageBus, LanguageDto, LanguagesService } from 'shared'; @@ -27,9 +23,12 @@ import { @Component({ selector: 'sqx-languages-page', styleUrls: ['./languages-page.component.scss'], - templateUrl: './languages-page.component.html' + templateUrl: './languages-page.component.html', + providers: [ + AppContext + ] }) -export class LanguagesPageComponent extends AppComponentBase implements OnInit { +export class LanguagesPageComponent implements OnInit { public allLanguages: LanguageDto[] = []; public newLanguages: LanguageDto[] = []; public appLanguages: AppLanguagesDto; @@ -41,13 +40,11 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { ] }); - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly appLanguagesService: AppLanguagesService, private readonly languagesService: LanguagesService, - private readonly messageBus: MessageBus, private readonly formBuilder: FormBuilder ) { - super(dialogs, apps, authService); } public ngOnInit() { @@ -56,55 +53,51 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { } public load() { - this.appNameOnce() - .switchMap(app => this.appLanguagesService.getLanguages(app).retry(2)) + this.appLanguagesService.getLanguages(this.ctx.appName) .subscribe(dto => { this.updateLanguages(dto); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public removeLanguage(language: AppLanguageDto) { - this.appNameOnce() - .switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code, this.appLanguages.version)) + this.appLanguagesService.deleteLanguage(this.ctx.appName, language.iso2Code, this.appLanguages.version) .subscribe(dto => { this.updateLanguages(this.appLanguages.removeLanguage(language, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public addLanguage() { const requestDto = new AddAppLanguageDto(this.addLanguageForm.controls['language'].value.iso2Code); - this.appNameOnce() - .switchMap(app => this.appLanguagesService.postLanguages(app, requestDto, this.appLanguages.version)) + this.appLanguagesService.postLanguages(this.ctx.appName, requestDto, this.appLanguages.version) .subscribe(dto => { this.updateLanguages(this.appLanguages.addLanguage(dto.payload, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public updateLanguage(language: AppLanguageDto) { - this.appNameOnce() - .switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, language, this.appLanguages.version)) + this.appLanguagesService.updateLanguage(this.ctx.appName, language.iso2Code, language, this.appLanguages.version) .subscribe(dto => { this.updateLanguages(this.appLanguages.updateLanguage(language, dto.version)); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } private loadAllLanguages() { - this.languagesService.getLanguages().retry(2) + this.languagesService.getLanguages() .subscribe(languages => { this.allLanguages = ImmutableArray.of(languages).sortByStringAsc(l => l.englishName).values; this.updateNewLanguages(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } @@ -132,7 +125,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { this.updateNewLanguages(); - this.messageBus.emit(new HistoryChannelUpdated()); + this.ctx.bus.emit(new HistoryChannelUpdated()); } private updateNewLanguages() { diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html index 8eefaeca8..d1a9a8a0f 100644 --- a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html +++ b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html @@ -1,6 +1,6 @@ - + - +
diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts b/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts index 47cfc4c3a..7dffbb45a 100644 --- a/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts +++ b/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts @@ -6,26 +6,25 @@ */ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { Subscription } from 'rxjs'; import { ApiUrlConfig, - AppComponentBase, + AppContext, AppPlansDto, - AppsStoreService, - AuthService, ChangePlanDto, - DialogService, PlansService } from 'shared'; @Component({ selector: 'sqx-plans-page', styleUrls: ['./plans-page.component.scss'], - templateUrl: './plans-page.component.html' + templateUrl: './plans-page.component.html', + providers: [ + AppContext + ] }) -export class PlansPageComponent extends AppComponentBase implements OnDestroy, OnInit { +export class PlansPageComponent implements OnDestroy, OnInit { private queryParamsSubscription: Subscription; private overridePlanId: string; @@ -36,12 +35,10 @@ export class PlansPageComponent extends AppComponentBase implements OnDestroy, O public isDisabled = false; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly plansService: PlansService, - private readonly route: ActivatedRoute, private readonly apiUrl: ApiUrlConfig ) { - super(dialogs, apps, authService); } public ngOnDestroy() { @@ -50,7 +47,7 @@ export class PlansPageComponent extends AppComponentBase implements OnDestroy, O public ngOnInit() { this.queryParamsSubscription = - this.route.queryParams.subscribe(params => { + this.ctx.route.queryParams.subscribe(params => { this.overridePlanId = params['planId']; }); @@ -58,8 +55,7 @@ export class PlansPageComponent extends AppComponentBase implements OnDestroy, O } public load(showInfo = false) { - this.appNameOnce() - .switchMap(app => this.plansService.getPlans(app).retry(2)) + this.plansService.getPlans(this.ctx.appName) .subscribe(dto => { if (this.overridePlanId) { this.plans = dto.changePlanId(this.overridePlanId); @@ -67,21 +63,20 @@ export class PlansPageComponent extends AppComponentBase implements OnDestroy, O this.plans = dto; } - this.planOwned = !dto.planOwner || (dto.planOwner === this.authService.user!.id); + this.planOwned = !dto.planOwner || (dto.planOwner === this.ctx.userId); if (showInfo) { - this.notifyInfo('Plans reloaded.'); + this.ctx.notifyInfo('Plans reloaded.'); } }, error => { - this.notifyError(error); + this.ctx.notifyError(error); }); } public changePlan(planId: string) { this.isDisabled = true; - this.appNameOnce() - .switchMap(app => this.plansService.putPlan(app, new ChangePlanDto(planId), this.plans.version)) + this.plansService.putPlan(this.ctx.appName, new ChangePlanDto(planId), this.plans.version) .subscribe(dto => { if (dto.payload.redirectUri && dto.payload.redirectUri.length > 0) { window.location.href = dto.payload.redirectUri; @@ -90,7 +85,7 @@ export class PlansPageComponent extends AppComponentBase implements OnDestroy, O this.isDisabled = false; } }, error => { - this.notifyError(error); + this.ctx.notifyError(error); this.isDisabled = false; }); } diff --git a/src/Squidex/app/features/settings/settings-area.component.html b/src/Squidex/app/features/settings/settings-area.component.html index 20320c74f..36e78e5a5 100644 --- a/src/Squidex/app/features/settings/settings-area.component.html +++ b/src/Squidex/app/features/settings/settings-area.component.html @@ -1,6 +1,6 @@ - + - +

Settings

diff --git a/src/Squidex/app/features/settings/settings-area.component.ts b/src/Squidex/app/features/settings/settings-area.component.ts index f7221f151..3e9094721 100644 --- a/src/Squidex/app/features/settings/settings-area.component.ts +++ b/src/Squidex/app/features/settings/settings-area.component.ts @@ -7,21 +7,18 @@ import { Component } from '@angular/core'; -import { - AppComponentBase, - AppsStoreService, - AuthService, - DialogService -} from 'shared'; +import { AppContext } from 'shared'; @Component({ selector: 'sqx-settings-area', styleUrls: ['./settings-area.component.scss'], - templateUrl: './settings-area.component.html' + templateUrl: './settings-area.component.html', + providers: [ + AppContext + ] }) -export class SettingsAreaComponent extends AppComponentBase { - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService +export class SettingsAreaComponent { + constructor(public readonly ctx: AppContext ) { - super(dialogs, apps, authService); } } \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/declarations.ts b/src/Squidex/app/features/webhooks/declarations.ts deleted file mode 100644 index 84b17769c..000000000 --- a/src/Squidex/app/features/webhooks/declarations.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -export * from './pages/webhook-events-page.component'; -export * from './pages/webhook.component'; -export * from './pages/webhooks-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.html b/src/Squidex/app/features/webhooks/pages/webhook.component.html deleted file mode 100644 index 2f015bbb6..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.html +++ /dev/null @@ -1,149 +0,0 @@ -
-
- - - - - - - - - - - - - - - - - - - - - -
- - - -
Url: - - - -
Secret: - - - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Schema - - All - - C - - U - - D - - P -
- {{schema.schema.name}} - - - - - - - - - - - - -
-
- -
-
-
- -
- - -
-
- -
-
-
- - {{webhook.totalSucceeded}} - -
-
- - {{webhook.totalFailed}} - -
-
- - {{webhook.totalTimedout}} - -
-
- - {{webhook.averageRequestTimeMs}} ms - -
-
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.scss b/src/Squidex/app/features/webhooks/pages/webhook.component.scss deleted file mode 100644 index 61873b1a2..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -.schemas-control { - width: 18rem; -} - -.webhook-section { - border-top: 1px solid $color-border; - margin-left: -1.25rem; - margin-right: -1.25rem; - padding: 1rem 1.25rem 0; -} \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.ts b/src/Squidex/app/features/webhooks/pages/webhook.component.ts deleted file mode 100644 index 7ac843b2d..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; - -import { - ImmutableArray, - SchemaDto, - UpdateWebhookDto, - WebhookDto, - WebhookSchemaDto -} from 'shared'; - -export interface WebhookSchemaForm { - schema: SchemaDto; - sendAll: boolean; - sendCreate: boolean; - sendUpdate: boolean; - sendDelete: boolean; - sendPublish: boolean; -} - -@Component({ - selector: 'sqx-webhook', - styleUrls: ['./webhook.component.scss'], - templateUrl: './webhook.component.html' -}) -export class WebhookComponent implements OnInit { - @Output() - public deleting = new EventEmitter(); - - @Output() - public updating = new EventEmitter(); - - @Input() - public allSchemas: SchemaDto[]; - - @Input() - public webhook: WebhookDto; - - public schemas: ImmutableArray; - - public schemaToAdd: SchemaDto; - public schemasToAdd: ImmutableArray; - - public webhookForm = - this.formBuilder.group({ - url: ['', - [ - Validators.required - ]] - }); - - public get hasUrl() { - return this.webhookForm.controls['url'].value && this.webhookForm.controls['url'].value.length > 0; - } - - public get hasSchema() { - return !!this.schemaToAdd; - } - - constructor( - private readonly formBuilder: FormBuilder - ) { - } - - public ngOnInit() { - this.webhookForm.controls['url'].setValue(this.webhook.url); - - this.schemas = - ImmutableArray.of( - this.webhook.schemas.map(webhookSchema => { - const schema = this.allSchemas.find(s => s.id === webhookSchema.schemaId); - - if (schema) { - return this.updateSendAll({ - schema: schema, - sendAll: false, - sendCreate: webhookSchema.sendCreate, - sendUpdate: webhookSchema.sendUpdate, - sendDelete: webhookSchema.sendDelete, - sendPublish: webhookSchema.sendPublish - }); - } else { - return null; - } - }).filter(w => w !== null).map(w => w!)).sortByStringAsc(x => x.schema.name); - - this.schemasToAdd = - ImmutableArray.of( - this.allSchemas.filter(schema => - !this.webhook.schemas.find(w => w.schemaId === schema.id))) - .sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public removeSchema(schemaForm: WebhookSchemaForm) { - this.schemas = this.schemas.remove(schemaForm); - - this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public addSchema() { - this.schemas = - this.schemas.push( - this.updateSendAll({ - schema: this.schemaToAdd, - sendAll: false, - sendCreate: false, - sendUpdate: false, - sendDelete: false, - sendPublish: false - })).sortByStringAsc(x => x.schema.name); - - this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public save() { - const requestDto = - new UpdateWebhookDto( - this.webhookForm.controls['url'].value, - this.schemas.values.map(schema => - new WebhookSchemaDto( - schema.schema.id, - schema.sendCreate, - schema.sendUpdate, - schema.sendDelete, - schema.sendPublish))); - - this.emitUpdating(requestDto); - } - - public toggle(schemaForm: WebhookSchemaForm, property: string) { - const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); - - this.schemas = this.schemas.replace(schemaForm, newSchema); - } - - public toggleAll(schemaForm: WebhookSchemaForm) { - const newSchema = this.updateAll({ schema: schemaForm.schema }, !schemaForm.sendAll); - - this.schemas = this.schemas.replace(schemaForm, newSchema); - } - - private emitUpdating(dto: UpdateWebhookDto) { - this.updating.emit(dto); - } - - private updateAll(schemaForm: WebhookSchemaForm, value: boolean): WebhookSchemaForm { - schemaForm.sendAll = value; - schemaForm.sendCreate = value; - schemaForm.sendUpdate = value; - schemaForm.sendDelete = value; - schemaForm.sendPublish = value; - return schemaForm; - } - - private updateSendAll(schemaForm: WebhookSchemaForm): WebhookSchemaForm { - schemaForm.sendAll = - schemaForm.sendCreate && - schemaForm.sendUpdate && - schemaForm.sendDelete && - schemaForm.sendPublish; - - return schemaForm; - } -} diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html deleted file mode 100644 index e49cc91eb..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html +++ /dev/null @@ -1,72 +0,0 @@ - - - -
-
-
- - - -
- -

Webhooks

-
- - - - -
- -
-
-
- No Webhook created yet. -
- -
- - - -
-
- -
- - - - - - - - - The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. - - - - Click the help icon to show a context specific help page. Go to https://docs.squidex.io for the full documentation. - -
-
-
- - \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss deleted file mode 100644 index f78f04a07..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -.failed { - color: $color-theme-error; -} - -.success { - color: $color-theme-green; -} \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts deleted file mode 100644 index 6661a4dd3..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Component, OnInit } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; - -import { - AppComponentBase, - AppsStoreService, - AuthService, - CreateWebhookDto, - DateTime, - DialogService, - ImmutableArray, - SchemaDto, - SchemasService, - WebhookDto, - WebhooksService, - UpdateWebhookDto -} from 'shared'; - -@Component({ - selector: 'sqx-webhooks-page', - styleUrls: ['./webhooks-page.component.scss'], - templateUrl: './webhooks-page.component.html' -}) -export class WebhooksPageComponent extends AppComponentBase implements OnInit { - public webhooks: ImmutableArray; - public schemas: SchemaDto[]; - - public addWebhookFormSubmitted = false; - public addWebhookForm = - this.formBuilder.group({ - url: ['', - [ - Validators.required - ]] - }); - - public get hasUrl() { - return this.addWebhookForm.controls['url'].value && this.addWebhookForm.controls['url'].value.length > 0; - } - - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, - private readonly schemasService: SchemasService, - private readonly webhooksService: WebhooksService, - private readonly formBuilder: FormBuilder - ) { - super(dialogs, apps, authService); - } - - public ngOnInit() { - this.load(); - } - - public load(showInfo = false) { - this.appNameOnce() - .switchMap(app => - this.schemasService.getSchemas(app) - .combineLatest(this.webhooksService.getWebhooks(app), - (s, w) => { return { webhooks: w, schemas: s }; })) - .subscribe(dtos => { - this.schemas = dtos.schemas; - this.webhooks = ImmutableArray.of(dtos.webhooks); - - if (showInfo) { - this.notifyInfo('Webhooks reloaded.'); - } - }, error => { - this.notifyError(error); - }); - } - - public deleteWebhook(webhook: WebhookDto) { - this.appNameOnce() - .switchMap(app => this.webhooksService.deleteWebhook(app, webhook.id, webhook.version)) - .subscribe(dto => { - this.webhooks = this.webhooks.remove(webhook); - }, error => { - this.notifyError(error); - }); - } - - public updateWebhook(webhook: WebhookDto, requestDto: UpdateWebhookDto) { - this.appNameOnce() - .switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version)) - .subscribe(dto => { - this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.userToken, dto.version)); - - this.notifyInfo('Webhook saved.'); - }, error => { - this.notifyError(error); - }); - } - - public addWebhook() { - this.addWebhookFormSubmitted = true; - - if (this.addWebhookForm.valid) { - this.addWebhookForm.disable(); - - const requestDto = new CreateWebhookDto(this.addWebhookForm.controls['url'].value, []); - - const me = this.userToken; - - this.appNameOnce() - .switchMap(app => this.webhooksService.postWebhook(app, requestDto, me, DateTime.now())) - .subscribe(dto => { - this.webhooks = this.webhooks.push(dto); - - this.resetWebhookForm(); - }, error => { - this.notifyError(error); - this.enableWebhookForm(); - }); - } - } - - public cancelAddWebhook() { - this.resetWebhookForm(); - } - - private enableWebhookForm() { - this.addWebhookForm.enable(); - } - - private resetWebhookForm() { - this.addWebhookFormSubmitted = false; - this.addWebhookForm.enable(); - this.addWebhookForm.reset(); - } -} diff --git a/src/Squidex/app/framework/angular/keys.pipe.spec.ts b/src/Squidex/app/framework/angular/keys.pipe.spec.ts new file mode 100644 index 000000000..f3a3728b9 --- /dev/null +++ b/src/Squidex/app/framework/angular/keys.pipe.spec.ts @@ -0,0 +1,26 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { + KeysPipe +} from './keys.pipe'; + +describe('KeysPipe', () => { + it('should return keys', () => { + const value = { + key1: 1, + key2: 2 + }; + + const pipe = new KeysPipe(); + + const actual = pipe.transform(value); + const expected = ['key1', 'key2']; + + expect(actual).toEqual(expected); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/keys.pipe.ts b/src/Squidex/app/framework/angular/keys.pipe.ts new file mode 100644 index 000000000..8b3b91334 --- /dev/null +++ b/src/Squidex/app/framework/angular/keys.pipe.ts @@ -0,0 +1,18 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'sqxKeys', + pure: true +}) +export class KeysPipe implements PipeTransform { + public transform(value: any, args: any[] = null): any { + return Object.keys(value); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/panel-container.directive.ts b/src/Squidex/app/framework/angular/panel-container.directive.ts index d5c1e30bd..6414af63d 100644 --- a/src/Squidex/app/framework/angular/panel-container.directive.ts +++ b/src/Squidex/app/framework/angular/panel-container.directive.ts @@ -67,12 +67,12 @@ export class PanelContainerDirective implements AfterViewInit, OnDestroy { for (let panel of this.panels) { const panelRoot = panel.panel.nativeElement; - let width = panel.clientWidth; + let width = panel.desiredWidth; - if (panel.expand && panel === last) { - width = this.containerWidth - currentPosition; + if (panel.desiredWidth === '*' && panel === last) { + panel.actualWidth = this.containerWidth - currentPosition; - panel.panelWidth = width + 'px'; + panel.desiredWidth = width + 'px'; } this.renderer.setElementStyle(panelRoot, 'top', '0px'); @@ -81,7 +81,7 @@ export class PanelContainerDirective implements AfterViewInit, OnDestroy { this.renderer.setElementStyle(panelRoot, 'position', 'absolute'); this.renderer.setElementStyle(panelRoot, 'z-index', currentLayer.toString()); - currentPosition += width; + currentPosition += panel.actualWidth; currentLayer -= 10; } diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index 578d3e92a..1a2fc8790 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/src/Squidex/app/framework/angular/panel.component.ts @@ -14,7 +14,7 @@ import { PanelContainerDirective } from './panel-container.directive'; @Component({ selector: 'sqx-panel', template: ` -
+
@@ -24,13 +24,13 @@ import { PanelContainerDirective } from './panel-container.directive'; ] }) export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { - private clientWidthValue = 0; + public actualWidth = 0; @Input() public theme = 'light'; @Input() - public panelWidth = '10rem'; + public desiredWidth = '10rem'; @Input() public expand = false; @@ -38,10 +38,6 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { @ViewChild('panel') public panel: ElementRef; - public get clientWidth() { - return this.clientWidthValue; - } - constructor( private readonly container: PanelContainerDirective ) { @@ -56,7 +52,7 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { } public ngAfterViewInit() { - this.clientWidthValue = this.panel.nativeElement.getBoundingClientRect().width; + this.actualWidth = this.panel.nativeElement.getBoundingClientRect().width; this.container.invalidate(); } diff --git a/src/Squidex/app/framework/angular/toggle.component.scss b/src/Squidex/app/framework/angular/toggle.component.scss index b0a2f4323..d3c6432b6 100644 --- a/src/Squidex/app/framework/angular/toggle.component.scss +++ b/src/Squidex/app/framework/angular/toggle.component.scss @@ -1,8 +1,8 @@ @import '_mixins'; @import '_vars'; -$toggle-width: 3.2rem; -$toggle-height: 2rem; +$toggle-width: 2.2rem; +$toggle-height: 1.4rem; $toggle-button-size: $toggle-height - .3rem; .toggle { diff --git a/src/Squidex/app/framework/angular/toggle.component.ts b/src/Squidex/app/framework/angular/toggle.component.ts index bac7739ba..5fa5aaa23 100644 --- a/src/Squidex/app/framework/angular/toggle.component.ts +++ b/src/Squidex/app/framework/angular/toggle.component.ts @@ -28,7 +28,7 @@ export class ToggleComponent implements ControlValueAccessor { public isDisabled = false; public writeValue(value: boolean | null | undefined) { - this.isChecked = Types.isBoolean(value) ? value || null : null; + this.isChecked = Types.isBoolean(value) ? value : null; } public setDisabledState(isDisabled: boolean): void { diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 7ba82733d..51fcb48ac 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -23,6 +23,7 @@ export * from './angular/image-source.directive'; export * from './angular/indeterminate-value.directive'; export * from './angular/jscript-editor.component'; export * from './angular/json-editor.component'; +export * from './angular/keys.pipe'; export * from './angular/lowercase-input.directive'; export * from './angular/markdown-editor.component'; export * from './angular/modal-target.directive'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 7a99b4dbc..f61b8acfd 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -36,6 +36,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LocalCacheService, LocalStoreService, @@ -101,6 +102,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LowerCaseInputDirective, MarkdownEditorComponent, @@ -149,6 +151,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LowerCaseInputDirective, MarkdownEditorComponent, diff --git a/src/Squidex/app/framework/utils/math-helper.ts b/src/Squidex/app/framework/utils/math-helper.ts index 9b3ce8514..d9f0864c5 100644 --- a/src/Squidex/app/framework/utils/math-helper.ts +++ b/src/Squidex/app/framework/utils/math-helper.ts @@ -8,6 +8,8 @@ /* tslint:disable: no-bitwise */ export module MathHelper { + export const EMPTY_GUID = '00000000-0000-0000-0000-000000000000'; + const CRC32_TABLE: number[] = []; function ensureCrc32Table() { diff --git a/src/Squidex/app/shared/components/app-context.ts b/src/Squidex/app/shared/components/app-context.ts new file mode 100644 index 000000000..f9418223c --- /dev/null +++ b/src/Squidex/app/shared/components/app-context.ts @@ -0,0 +1,85 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable, OnDestroy } from '@angular/core'; +import { Observable, Subscription } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; + +import { MessageBus } from 'framework'; + +import { + AppDto, + AppsStoreService, + AuthService, + DialogService, + ErrorDto, + Notification, + Profile +} from './../declarations-base'; + +@Injectable() +export class AppContext implements OnDestroy { + private readonly appSubscription: Subscription; + private appField: AppDto; + + public get app(): AppDto { + return this.appField; + } + + public get appChanges(): Observable { + return this.appsStore.selectedApp; + } + + public get appName(): string { + return this.appField ? this.appField.name : ''; + } + + public get userToken(): string { + return this.authService.user!.token; + } + + public get userId(): string { + return this.authService.user!.id; + } + + public get user(): Profile { + return this.authService.user!; + } + + constructor( + public readonly dialogs: DialogService, + public readonly authService: AuthService, + public readonly appsStore: AppsStoreService, + public readonly route: ActivatedRoute, + public readonly bus: MessageBus + ) { + this.appSubscription = + this.appsStore.selectedApp.subscribe(app => { + this.appField = app; + }); + } + + public ngOnDestroy() { + this.appSubscription.unsubscribe(); + } + + public confirmUnsavedChanges(): Observable { + return this.dialogs.confirmUnsavedChanges(); + } + + public notifyInfo(error: string) { + this.dialogs.notify(Notification.info(error)); + } + + public notifyError(error: string | ErrorDto) { + if (error instanceof ErrorDto) { + this.dialogs.notify(Notification.error(error.displayMessage)); + } else { + this.dialogs.notify(Notification.error(error)); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/components/app.component-base.ts b/src/Squidex/app/shared/components/app.component-base.ts deleted file mode 100644 index 9cbac29b5..000000000 --- a/src/Squidex/app/shared/components/app.component-base.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Observable } from 'rxjs'; - -import { - AppsStoreService, - AuthService, - DialogService -} from './../declarations-base'; - -import { ComponentBase } from './component-base'; - -export abstract class AppComponentBase extends ComponentBase { - private appName$: Observable; - - public get userToken(): string { - return this.authService.user!.token; - } - - constructor(dialogs: DialogService, - protected readonly appsStore: AppsStoreService, - protected readonly authService: AuthService - ) { - super(dialogs); - - this.appName$ = this.appsStore.selectedApp.filter(a => !!a).map(a => a!.name); - } - - public appName(): Observable { - return this.appName$; - } - - public appNameOnce(): Observable { - return this.appName$.first(); - } -} - diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index 5e5715602..8aeaccf55 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -8,16 +8,13 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; -import { AppComponentBase } from './app.component-base'; +import { AppContext } from './app-context'; import { - AppsStoreService, AssetDto, AssetsService, AssetDragged, - AuthService, DateTime, - DialogService, fadeAnimation, ModalView, UpdateAssetDto, @@ -30,11 +27,14 @@ import { selector: 'sqx-asset', styleUrls: ['./asset.component.scss'], templateUrl: './asset.component.html', + providers: [ + AppContext + ], animations: [ fadeAnimation ] }) -export class AssetComponent extends AppComponentBase implements OnInit { +export class AssetComponent implements OnInit { private assetVersion: Version; @Input() @@ -79,20 +79,18 @@ export class AssetComponent extends AppComponentBase implements OnInit { public progress = 0; - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly formBuilder: FormBuilder, private readonly assetsService: AssetsService, private readonly messageBus: MessageBus ) { - super(dialogs, apps, authService); } public ngOnInit() { const initFile = this.initFile; if (initFile) { - this.appNameOnce() - .switchMap(app => this.assetsService.uploadFile(app, initFile, this.userToken, DateTime.now())) + this.assetsService.uploadFile(this.ctx.appName, initFile, this.ctx.userToken, DateTime.now()) .subscribe(dto => { if (dto instanceof AssetDto) { this.emitLoaded(dto); @@ -100,7 +98,8 @@ export class AssetComponent extends AppComponentBase implements OnInit { this.progress = dto; } }, error => { - this.notifyError(error); + this.ctx.notifyError(error); + this.emitFailed(error); }); } else { @@ -110,16 +109,16 @@ export class AssetComponent extends AppComponentBase implements OnInit { public updateFile(files: FileList) { if (files.length === 1) { - this.appNameOnce() - .switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.assetVersion)) + this.assetsService.replaceFile(this.ctx.appName, this.asset.id, files[0], this.assetVersion) .subscribe(dto => { if (dto instanceof Versioned) { - this.updateAsset(this.asset.update(dto.payload, this.userToken, dto.version), true); + this.updateAsset(this.asset.update(dto.payload, this.ctx.userToken, dto.version), true); } else { this.setProgress(dto); } }, error => { - this.notifyError(error); + this.ctx.notifyError(error); + this.setProgress(); }); } @@ -133,13 +132,13 @@ export class AssetComponent extends AppComponentBase implements OnInit { const requestDto = new UpdateAssetDto(this.renameForm.controls['name'].value); - this.appNameOnce() - .switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.assetVersion)) + this.assetsService.putAsset(this.ctx.appName, this.asset.id, requestDto, this.assetVersion) .subscribe(dto => { - this.updateAsset(this.asset.rename(requestDto.fileName, this.userToken, dto.version), true); + this.updateAsset(this.asset.rename(requestDto.fileName, this.ctx.userToken, dto.version), true); this.resetRenameForm(); }, error => { - this.notifyError(error); + this.ctx.notifyError(error); + this.enableRenameForm(); }); } diff --git a/src/Squidex/app/shared/components/component-base.ts b/src/Squidex/app/shared/components/component-base.ts deleted file mode 100644 index 8471f8488..000000000 --- a/src/Squidex/app/shared/components/component-base.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { - DialogService, - ErrorDto, - Notification -} from './../declarations-base'; - -export abstract class ComponentBase { - constructor( - public readonly dialogs: DialogService - ) { - } - - protected notifyError(error: string | ErrorDto) { - if (error instanceof ErrorDto) { - this.dialogs.notify(Notification.error(error.displayMessage)); - } else { - this.dialogs.notify(Notification.error(error)); - } - } - - protected notifyInfo(error: string) { - this.dialogs.notify(Notification.info(error)); - } -} \ No newline at end of file diff --git a/src/Squidex/app/shared/components/help.component.html b/src/Squidex/app/shared/components/help.component.html index 7ff0156d1..80eb827f2 100644 --- a/src/Squidex/app/shared/components/help.component.html +++ b/src/Squidex/app/shared/components/help.component.html @@ -1,4 +1,4 @@ - +

Help

diff --git a/src/Squidex/app/shared/components/history.component.html b/src/Squidex/app/shared/components/history.component.html index 458f33183..8a57f5269 100644 --- a/src/Squidex/app/shared/components/history.component.html +++ b/src/Squidex/app/shared/components/history.component.html @@ -1,4 +1,4 @@ - +

Activity

diff --git a/src/Squidex/app/shared/components/history.component.ts b/src/Squidex/app/shared/components/history.component.ts index 526a7de7e..817e938b7 100644 --- a/src/Squidex/app/shared/components/history.component.ts +++ b/src/Squidex/app/shared/components/history.component.ts @@ -6,20 +6,15 @@ */ import { Component } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { AppComponentBase } from './app.component-base'; +import { AppContext } from './app-context'; import { allParams, - AppsStoreService, - AuthService, - DialogService, HistoryChannelUpdated, HistoryEventDto, HistoryService, - MessageBus, UsersProviderService } from './../declarations-base'; @@ -28,14 +23,17 @@ const REPLACEMENT_TEMP = '$TEMP$'; @Component({ selector: 'sqx-history', styleUrls: ['./history.component.scss'], - templateUrl: './history.component.html' + templateUrl: './history.component.html', + providers: [ + AppContext + ] }) -export class HistoryComponent extends AppComponentBase { +export class HistoryComponent { public get channel(): string { - let channelPath = this.route.snapshot.data['channel']; + let channelPath = this.ctx.route.snapshot.data['channel']; if (channelPath) { - const params = allParams(this.route); + const params = allParams(this.ctx.route); for (let key in params) { if (params.hasOwnProperty(key)) { @@ -50,18 +48,13 @@ export class HistoryComponent extends AppComponentBase { } public events: Observable = - Observable.timer(0, 10000) - .merge(this.messageBus.of(HistoryChannelUpdated).delay(1000)) - .switchMap(() => this.appNameOnce()) - .switchMap(app => this.historyService.getHistory(app, this.channel).retry(2)); + Observable.timer(0, 10000).merge(this.ctx.bus.of(HistoryChannelUpdated).delay(1000)) + .switchMap(app => this.historyService.getHistory(this.ctx.appName, this.channel)); - constructor(appsStore: AppsStoreService, dialogs: DialogService, authService: AuthService, + constructor(public readonly ctx: AppContext, private readonly users: UsersProviderService, - private readonly historyService: HistoryService, - private readonly messageBus: MessageBus, - private readonly route: ActivatedRoute + private readonly historyService: HistoryService ) { - super(dialogs, appsStore, authService); } private userName(userId: string): Observable { diff --git a/src/Squidex/app/shared/declarations-base.ts b/src/Squidex/app/shared/declarations-base.ts index 01d4a0bdc..fecc4272e 100644 --- a/src/Squidex/app/shared/declarations-base.ts +++ b/src/Squidex/app/shared/declarations-base.ts @@ -13,6 +13,7 @@ export * from './guards/resolve-content.guard'; export * from './guards/resolve-published-schema.guard'; export * from './guards/resolve-schema.guard'; export * from './guards/resolve-user.guard'; +export * from './guards/unset-app.guard'; export * from './interceptors/auth.interceptor'; @@ -30,12 +31,12 @@ export * from './services/help.service'; export * from './services/history.service'; export * from './services/languages.service'; export * from './services/plans.service'; +export * from './services/rules.service'; export * from './services/schemas.service'; export * from './services/ui.service'; export * from './services/usages.service'; export * from './services/users-provider.service'; export * from './services/users.service'; -export * from './services/webhooks.service'; export * from './utils/messages'; diff --git a/src/Squidex/app/shared/declarations.ts b/src/Squidex/app/shared/declarations.ts index 5fc29bd25..0d7371c00 100644 --- a/src/Squidex/app/shared/declarations.ts +++ b/src/Squidex/app/shared/declarations.ts @@ -5,10 +5,9 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -export * from './components/app.component-base'; +export * from './components/app-context'; export * from './components/app-form.component'; export * from './components/asset.component'; -export * from './components/component-base'; export * from './components/help.component'; export * from './components/history.component'; export * from './components/language-selector.component'; diff --git a/src/Squidex/app/shared/guards/unset-app.guard.spec.ts b/src/Squidex/app/shared/guards/unset-app.guard.spec.ts new file mode 100644 index 000000000..e98222b70 --- /dev/null +++ b/src/Squidex/app/shared/guards/unset-app.guard.spec.ts @@ -0,0 +1,39 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { IMock, Mock, Times } from 'typemoq'; +import { Observable } from 'rxjs'; + +import { AppsStoreService } from 'shared'; + +import { UnsetAppGuard } from './unset-app.guard'; + +describe('UnsetAppGuard', () => { + let appStoreService: IMock; + + beforeEach(() => { + appStoreService = Mock.ofType(AppsStoreService); + }); + + it('should unselect app', () => { + appStoreService.setup(x => x.selectApp(null)) + .returns(() => Observable.of(false)); + + const guard = new UnsetAppGuard(appStoreService.object); + + let result: boolean = undefined; + + guard.canActivate(null, null) + .subscribe(value => { + result = value; + }); + + expect(result).toBeTruthy(); + + appStoreService.verify(x => x.selectApp(null), Times.once()); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/unset-app.guard.ts b/src/Squidex/app/shared/guards/unset-app.guard.ts new file mode 100644 index 000000000..81d7b6e54 --- /dev/null +++ b/src/Squidex/app/shared/guards/unset-app.guard.ts @@ -0,0 +1,24 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { AppsStoreService } from './../services/apps-store.service'; + +@Injectable() +export class UnsetAppGuard implements CanActivate { + constructor( + private readonly appsStore: AppsStoreService + ) { + } + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.appsStore.selectApp(null).map(a => !a); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 6f229df77..3cc68672f 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -16,9 +16,9 @@ import { AppClientsService, AppContributorsService, AppLanguagesService, + AppMustExistGuard, AppsStoreService, AppsService, - AppMustExistGuard, AssetComponent, AssetPreviewUrlPipe, AssetsService, @@ -44,7 +44,9 @@ import { ResolveSchemaGuard, SchemasService, ResolveUserGuard, + RulesService, UIService, + UnsetAppGuard, UsagesService, UserDtoPicture, UserEmailPipe, @@ -57,7 +59,6 @@ import { UserManagementService, UsersProviderService, UsersService, - WebhooksService, RichEditorComponent } from './declarations'; @@ -113,9 +114,9 @@ export class SqxSharedModule { AppClientsService, AppContributorsService, AppLanguagesService, - AppsStoreService, - AppsService, AppMustExistGuard, + AppsService, + AppsStoreService, AssetsService, AuthService, ContentsService, @@ -132,13 +133,14 @@ export class SqxSharedModule { ResolvePublishedSchemaGuard, ResolveSchemaGuard, ResolveUserGuard, + RulesService, SchemasService, UIService, + UnsetAppGuard, UsagesService, UserManagementService, UsersProviderService, UsersService, - WebhooksService, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, diff --git a/src/Squidex/app/shared/services/apps-store.service.spec.ts b/src/Squidex/app/shared/services/apps-store.service.spec.ts index 5e9bd36e7..96f86dded 100644 --- a/src/Squidex/app/shared/services/apps-store.service.spec.ts +++ b/src/Squidex/app/shared/services/apps-store.service.spec.ts @@ -19,8 +19,11 @@ import { describe('AppsStoreService', () => { const now = DateTime.now(); - const oldApps = [new AppDto('id', 'old-name', 'Owner', now, now)]; - const newApp = new AppDto('id', 'new-name', 'Owner', now, now); + const oldApps = [ + new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan'), + new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan') + ]; + const newApp = new AppDto('id', 'new-name', 'Owner', now, now, 'Free', 'Plan'); let appsService: IMock; diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/src/Squidex/app/shared/services/apps.service.spec.ts index 4f29d6075..d255b7f23 100644 --- a/src/Squidex/app/shared/services/apps.service.spec.ts +++ b/src/Squidex/app/shared/services/apps.service.spec.ts @@ -57,20 +57,24 @@ describe('AppsService', () => { name: 'name1', permission: 'Owner', created: '2016-01-01', - lastModified: '2016-02-02' + lastModified: '2016-02-02', + planName: 'Free', + planUpgrade: 'Basic' }, { id: '456', name: 'name2', permission: 'Owner', created: '2017-01-01', - lastModified: '2017-02-02' + lastModified: '2017-02-02', + planName: 'Basic', + planUpgrade: 'Enterprise' } ]); expect(apps).toEqual([ - new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02')), - new AppDto('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02')) + new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'), + new AppDto('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise') ]); })); @@ -90,8 +94,13 @@ describe('AppsService', () => { expect(req.request.method).toEqual('POST'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush({ id: '123' }); + req.flush({ + id: '123', + permission: 'Reader', + planName: 'Basic', + planUpgrade: 'Enterprise' + }); - expect(app).toEqual(new AppDto('123', dto.name, 'Owner', now, now)); + expect(app).toEqual(new AppDto('123', dto.name, 'Reader', now, now, 'Basic', 'Enterprise')); })); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps.service.ts b/src/Squidex/app/shared/services/apps.service.ts index 8267a17ca..d1f99b348 100644 --- a/src/Squidex/app/shared/services/apps.service.ts +++ b/src/Squidex/app/shared/services/apps.service.ts @@ -24,7 +24,9 @@ export class AppDto { public readonly name: string, public readonly permission: string, public readonly created: DateTime, - public readonly lastModified: DateTime + public readonly lastModified: DateTime, + public readonly planName: string, + public readonly planUpgrade: string ) { } } @@ -60,7 +62,9 @@ export class AppsService { item.name, item.permission, DateTime.parseISO(item.created), - DateTime.parseISO(item.lastModified)); + DateTime.parseISO(item.lastModified), + item.planName, + item.planUpgrade); }); }) .pretifyError('Failed to load apps. Please reload.'); @@ -75,7 +79,7 @@ export class AppsService { now = now || DateTime.now(); - return new AppDto(body.id, dto.name, 'Owner', now, now); + return new AppDto(body.id, dto.name, body.permission, now, now, body.planName, body.planUpgrade); }) .do(() => { this.analytics.trackEvent('App', 'Created', dto.name); diff --git a/src/Squidex/app/shared/services/rules.service.spec.ts b/src/Squidex/app/shared/services/rules.service.spec.ts new file mode 100644 index 000000000..daa592c37 --- /dev/null +++ b/src/Squidex/app/shared/services/rules.service.spec.ts @@ -0,0 +1,323 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + + +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; + +import { + AnalyticsService, + ApiUrlConfig, + CreateRuleDto, + DateTime, + UpdateRuleDto, + Version, + RuleDto, + RuleEventDto, + RuleEventsDto, + RulesService +} from './../'; + +describe('RuleDto', () => { + const creation = DateTime.today(); + const creator = 'not-me'; + const modified = DateTime.now(); + const modifier = 'me'; + const version = new Version('1'); + const newVersion = new Version('2'); + + it('should update trigger', () => { + const trigger = { param2: 2, triggerType: 'NewType' }; + + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.updateTrigger(trigger, modifier, newVersion, modified); + + expect(rule_2.trigger).toEqual(trigger); + expect(rule_2.triggerType).toEqual(trigger.triggerType); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); + + it('should update action', () => { + const action = { param2: 2, actionType: 'NewType' }; + + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.updateAction(action, modifier, newVersion, modified); + + expect(rule_2.action).toEqual(action); + expect(rule_2.actionType).toEqual(action.actionType); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); + + it('should enable', () => { + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.enable(modifier, newVersion, modified); + + expect(rule_2.isEnabled).toBeTruthy(); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); + + it('should disable', () => { + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.disable(modifier, newVersion, modified); + + expect(rule_2.isEnabled).toBeFalsy(); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); +}); + +describe('RulesService', () => { + const now = DateTime.now(); + const user = 'me'; + const version = new Version('1'); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + RulesService, + { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, + { provide: AnalyticsService, useValue: new AnalyticsService() } + ] + }); + }); + + afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { + httpMock.verify(); + })); + + it('should make get request to get app rules', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + let rules: RuleDto[] | null = null; + + rulesService.getRules('my-app').subscribe(result => { + rules = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush([ + { + id: 'id1', + created: '2016-12-12T10:10', + createdBy: 'CreatedBy1', + lastModified: '2017-12-12T10:10', + lastModifiedBy: 'LastModifiedBy1', + url: 'http://squidex.io/hook', + version: '1', + trigger: { + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, + action: { + param3: 3, + param4: 4, + actionType: 'Webhook' + }, + isEnabled: true + } + ]); + + expect(rules).toEqual([ + new RuleDto('id1', 'CreatedBy1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + version, + true, + { + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, + 'ContentChanged', + { + param3: 3, + param4: 4, + actionType: 'Webhook' + }, + 'Webhook') + ]); + })); + + it('should make post request to create rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + const dto = new CreateRuleDto({ + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, { + param3: 3, + param4: 4, + actionType: 'Webhook' + }); + + let rule: RuleDto | null = null; + + rulesService.postRule('my-app', dto, user, now).subscribe(result => { + rule = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules'); + + expect(req.request.method).toEqual('POST'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, { + headers: { + etag: '1' + } + }); + + expect(rule).toEqual( + new RuleDto('id1', user, user, now, now, + version, + true, + { + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, + 'ContentChanged', + { + param3: 3, + param4: 4, + actionType: 'Webhook' + }, + 'Webhook')); + })); + + it('should make put request to update rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + const dto = new UpdateRuleDto({ param1: 1 }, { param2: 2 }); + + rulesService.putRule('my-app', '123', dto, version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make put request to enable rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.enableRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/enable'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make put request to disable rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.disableRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/disable'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make delete request to delete rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.deleteRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); + + expect(req.request.method).toEqual('DELETE'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + })); + + it('should make get request to get app rule events', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + let rules: RuleEventsDto | null = null; + + rulesService.getEvents('my-app', 10, 20).subscribe(result => { + rules = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events?take=10&skip=20'); + + expect(req.request.method).toEqual('GET'); + + req.flush({ + total: 20, + items: [ + { + id: 'id1', + created: '2017-12-12T10:10', + eventName: 'event1', + nextAttempt: '2017-12-12T12:10', + jobResult: 'Failed', + lastDump: 'dump1', + numCalls: 1, + description: 'url1', + result: 'Failed' + }, + { + id: 'id2', + created: '2017-12-13T10:10', + eventName: 'event2', + nextAttempt: '2017-12-13T12:10', + jobResult: 'Failed', + lastDump: 'dump2', + numCalls: 2, + description: 'url2', + result: 'Failed' + } + ] + }); + + expect(rules).toEqual( + new RuleEventsDto(20, [ + new RuleEventDto('id1', + DateTime.parseISO_UTC('2017-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T12:10'), + 'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), + new RuleEventDto('id2', + DateTime.parseISO_UTC('2017-12-13T10:10'), + DateTime.parseISO_UTC('2017-12-13T12:10'), + 'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) + ])); + })); + + it('should make put request to enqueue rule event', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.enqueueEvent('my-app', '123').subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events/123'); + + expect(req.request.method).toEqual('PUT'); + })); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/rules.service.ts b/src/Squidex/app/shared/services/rules.service.ts new file mode 100644 index 000000000..2a5ae47a0 --- /dev/null +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -0,0 +1,273 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import 'framework/angular/http-extensions'; + +import { + AnalyticsService, + ApiUrlConfig, + DateTime, + HTTP, + Version, + Versioned +} from 'framework'; + +export const ruleTriggers: any = { + 'ContentChanged': 'Content changed' +}; + +export const ruleActions: any = { + 'Webhook': 'Send Webhook' +}; + +export class RuleDto { + constructor( + public readonly id: string, + public readonly createdBy: string, + public readonly lastModifiedBy: string, + public readonly created: DateTime, + public readonly lastModified: DateTime, + public readonly version: Version, + public readonly isEnabled: boolean, + public readonly trigger: any, + public readonly triggerType: string, + public readonly action: any, + public readonly actionType: string + ) { + } + + public updateTrigger(trigger: any, user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + this.isEnabled, + trigger, + trigger.triggerType, + this.action, + this.action.actionType); + } + + public updateAction(action: any, user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + this.isEnabled, + this.trigger, + this.trigger.triggerType, + action, + action.actionType); + } + + public enable(user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + true, + this.trigger, + this.triggerType, + this.action, + this.actionType); + } + + public disable(user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + false, + this.trigger, + this.triggerType, + this.action, + this.actionType); + } +} + +export class RuleEventDto { + constructor( + public readonly id: string, + public readonly created: DateTime, + public readonly nextAttempt: DateTime | null, + public readonly eventName: string, + public readonly description: string, + public readonly lastDump: string, + public readonly result: string, + public readonly jobResult: string, + public readonly numCalls: number + ) { + } +} + +export class RuleEventsDto { + constructor( + public readonly total: number, + public readonly items: RuleEventDto[] + ) { + } +} + +export class CreateRuleDto { + constructor( + public readonly trigger: any, + public readonly action: any + ) { + } +} + +export class UpdateRuleDto { + constructor( + public readonly trigger: any, + public readonly action: any + ) { + } +} + +@Injectable() +export class RulesService { + constructor( + private readonly http: HttpClient, + private readonly apiUrl: ApiUrlConfig, + private readonly analytics: AnalyticsService + ) { + } + + public getRules(appName: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); + + return HTTP.getVersioned(this.http, url) + .map(response => { + const items: any[] = response.payload.body; + + return items.map(item => { + return new RuleDto( + item.id, + item.createdBy, + item.lastModifiedBy, + DateTime.parseISO_UTC(item.created), + DateTime.parseISO_UTC(item.lastModified), + new Version(item.version.toString()), + item.isEnabled, + item.trigger, + item.trigger.triggerType, + item.action, + item.action.actionType); + }); + }) + .pretifyError('Failed to load Rules. Please reload.'); + } + + public postRule(appName: string, dto: CreateRuleDto, user: string, now: DateTime): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); + + return HTTP.postVersioned(this.http, url, dto) + .map(response => { + const body = response.payload.body; + + return new RuleDto( + body.id, + user, + user, + now, + now, + response.version, + true, + dto.trigger, + dto.trigger.triggerType, + dto.action, + dto.action.actionType); + }) + .do(() => { + this.analytics.trackEvent('Rule', 'Created', appName); + }) + .pretifyError('Failed to create rule. Please reload.'); + } + + public putRule(appName: string, id: string, dto: UpdateRuleDto, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); + + return HTTP.putVersioned(this.http, url, dto, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to update rule. Please reload.'); + } + + public enableRule(appName: string, id: string, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/enable`); + + return HTTP.putVersioned(this.http, url, {}, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to enable rule. Please reload.'); + } + + public disableRule(appName: string, id: string, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/disable`); + + return HTTP.putVersioned(this.http, url, {}, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to disable rule. Please reload.'); + } + + public deleteRule(appName: string, id: string, version: Version): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); + + return HTTP.deleteVersioned(this.http, url, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Deleted', appName); + }) + .pretifyError('Failed to delete rule. Please reload.'); + } + + public getEvents(appName: string, take: number, skip: number): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`); + + return HTTP.getVersioned(this.http, url) + .map(response => { + const body = response.payload.body; + + const items: any[] = body.items; + + return new RuleEventsDto(body.total, items.map(item => { + return new RuleEventDto( + item.id, + DateTime.parseISO_UTC(item.created), + item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, + item.eventName, + item.description, + item.lastDump, + item.result, + item.jobResult, + item.numCalls); + })); + }) + .pretifyError('Failed to load events. Please reload.'); + } + + public enqueueEvent(appName: string, id: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`); + + return HTTP.putVersioned(this.http, url, {}) + .do(() => { + this.analytics.trackEvent('Rule', 'EventEnqueued', appName); + }) + .pretifyError('Failed to enqueue rule event. Please reload.'); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/users-provider.service.ts b/src/Squidex/app/shared/services/users-provider.service.ts index 166221e27..942fb0ec2 100644 --- a/src/Squidex/app/shared/services/users-provider.service.ts +++ b/src/Squidex/app/shared/services/users-provider.service.ts @@ -27,7 +27,7 @@ export class UsersProviderService { if (!result) { const request = - this.usersService.getUser(id).retry(2) + this.usersService.getUser(id) .catch(error => { return Observable.of(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', null, false)); }) diff --git a/src/Squidex/app/shared/services/webhooks.service.spec.ts b/src/Squidex/app/shared/services/webhooks.service.spec.ts deleted file mode 100644 index d25b39c94..000000000 --- a/src/Squidex/app/shared/services/webhooks.service.spec.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - - -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { inject, TestBed } from '@angular/core/testing'; - -import { - AnalyticsService, - ApiUrlConfig, - CreateWebhookDto, - DateTime, - UpdateWebhookDto, - Version, - WebhookDto, - WebhookEventDto, - WebhookEventsDto, - WebhookSchemaDto, - WebhooksService -} from './../'; - -describe('WebhookDto', () => { - const creation = DateTime.today(); - const creator = 'not-me'; - const modified = DateTime.now(); - const modifier = 'me'; - const version = new Version('1'); - const newVersion = new Version('2'); - - it('should update url and schemas', () => { - const webhook_1 = new WebhookDto('id1', 'token1', creator, creator, creation, creation, version, [], 'http://squidex.io/hook', 1, 2, 3, 4); - const webhook_2 = - webhook_1.update(new UpdateWebhookDto('http://squidex.io/hook2', - [ - new WebhookSchemaDto('1', true, true, true, true), - new WebhookSchemaDto('2', true, true, true, true) - ]), modifier, newVersion, modified); - - expect(webhook_2.url).toEqual('http://squidex.io/hook2'); - expect(webhook_2.schemas.length).toEqual(2); - expect(webhook_2.lastModified).toEqual(modified); - expect(webhook_2.lastModifiedBy).toEqual(modifier); - expect(webhook_2.version).toEqual(newVersion); - }); -}); - -describe('WebhooksService', () => { - const now = DateTime.now(); - const user = 'me'; - const version = new Version('1'); - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - providers: [ - WebhooksService, - { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() } - ] - }); - }); - - afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { - httpMock.verify(); - })); - - it('should make get request to get app webhooks', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - let webhooks: WebhookDto[] | null = null; - - webhooksService.getWebhooks('my-app').subscribe(result => { - webhooks = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); - - expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush([ - { - id: 'id1', - sharedSecret: 'token1', - created: '2016-12-12T10:10', - createdBy: 'CreatedBy1', - lastModified: '2017-12-12T10:10', - lastModifiedBy: 'LastModifiedBy1', - url: 'http://squidex.io/hook', - version: '1', - totalSucceeded: 1, - totalFailed: 2, - totalTimedout: 3, - averageRequestTimeMs: 4, - schemas: [{ - schemaId: '1', - sendCreate: true, - sendUpdate: true, - sendDelete: true, - sendPublish: true - }, { - schemaId: '2', - sendCreate: true, - sendUpdate: true, - sendDelete: true, - sendPublish: true - }] - } - ]); - - expect(webhooks).toEqual([ - new WebhookDto('id1', 'token1', 'CreatedBy1', 'LastModifiedBy1', - DateTime.parseISO_UTC('2016-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T10:10'), - version, - [ - new WebhookSchemaDto('1', true, true, true, true), - new WebhookSchemaDto('2', true, true, true, true) - ], - 'http://squidex.io/hook', 1, 2, 3, 4) - ]); - })); - - it('should make post request to create webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - const dto = new CreateWebhookDto('http://squidex.io/hook', []); - - let webhook: WebhookDto | null = null; - - webhooksService.postWebhook('my-app', dto, user, now).subscribe(result => { - webhook = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); - - expect(req.request.method).toEqual('POST'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, { - headers: { - etag: '1' - } - }); - - expect(webhook).toEqual( - new WebhookDto('id1', 'token1', user, user, now, now, version, [], 'http://squidex.io/hook', 0, 0, 0, 0)); - })); - - it('should make put request to update webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - const dto = new UpdateWebhookDto('http://squidex.io/hook', []); - - webhooksService.putWebhook('my-app', '123', dto, version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/123'); - - expect(req.request.method).toEqual('PUT'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - - req.flush({}); - })); - - it('should make delete request to delete webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - webhooksService.deleteWebhook('my-app', '123', version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/123'); - - expect(req.request.method).toEqual('DELETE'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - })); - - it('should make get request to get app webhook events', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - let webhooks: WebhookEventsDto | null = null; - - webhooksService.getEvents('my-app', 10, 20).subscribe(result => { - webhooks = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/events?take=10&skip=20'); - - expect(req.request.method).toEqual('GET'); - - req.flush({ - total: 20, - items: [ - { - id: 'id1', - created: '2017-12-12T10:10', - eventName: 'event1', - nextAttempt: '2017-12-12T12:10', - jobResult: 'Failed', - lastDump: 'dump1', - numCalls: 1, - requestUrl: 'url1', - result: 'Failed' - }, - { - id: 'id2', - created: '2017-12-13T10:10', - eventName: 'event2', - nextAttempt: '2017-12-13T12:10', - jobResult: 'Failed', - lastDump: 'dump2', - numCalls: 2, - requestUrl: 'url2', - result: 'Failed' - } - ] - }); - - expect(webhooks).toEqual( - new WebhookEventsDto(20, [ - new WebhookEventDto('id1', - DateTime.parseISO_UTC('2017-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T12:10'), - 'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), - new WebhookEventDto('id2', - DateTime.parseISO_UTC('2017-12-13T10:10'), - DateTime.parseISO_UTC('2017-12-13T12:10'), - 'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) - ])); - })); - - it('should make put request to enqueue webhook event', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - webhooksService.enqueueEvent('my-app', '123').subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/events/123'); - - expect(req.request.method).toEqual('PUT'); - })); -}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/webhooks.service.ts b/src/Squidex/app/shared/services/webhooks.service.ts deleted file mode 100644 index 596409cf7..000000000 --- a/src/Squidex/app/shared/services/webhooks.service.ts +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; - -import 'framework/angular/http-extensions'; - -import { - AnalyticsService, - ApiUrlConfig, - DateTime, - HTTP, - Version, - Versioned -} from 'framework'; - -export class WebhookDto { - constructor( - public readonly id: string, - public readonly sharedSecret: string, - public readonly createdBy: string, - public readonly lastModifiedBy: string, - public readonly created: DateTime, - public readonly lastModified: DateTime, - public readonly version: Version, - public readonly schemas: WebhookSchemaDto[], - public readonly url: string, - public readonly totalSucceeded: number, - public readonly totalFailed: number, - public readonly totalTimedout: number, - public readonly averageRequestTimeMs: number - ) { - } - - public update(update: UpdateWebhookDto, user: string, version: Version, now?: DateTime): WebhookDto { - return new WebhookDto( - this.id, - this.sharedSecret, - this.createdBy, user, - this.created, now || DateTime.now(), - version, - update.schemas, - update.url, - this.totalSucceeded, - this.totalFailed, - this.totalTimedout, - this.averageRequestTimeMs); - } -} - -export class WebhookSchemaDto { - constructor( - public readonly schemaId: string, - public readonly sendCreate: boolean, - public readonly sendUpdate: boolean, - public readonly sendDelete: boolean, - public readonly sendPublish: boolean - ) { - } -} - -export class WebhookEventDto { - constructor( - public readonly id: string, - public readonly created: DateTime, - public readonly nextAttempt: DateTime | null, - public readonly eventName: string, - public readonly requestUrl: string, - public readonly lastDump: string, - public readonly result: string, - public readonly jobResult: string, - public readonly numCalls: number - ) { - } -} - -export class WebhookEventsDto { - constructor( - public readonly total: number, - public readonly items: WebhookEventDto[] - ) { - } -} - -export class CreateWebhookDto { - constructor( - public readonly url: string, - public readonly schemas: WebhookSchemaDto[] - ) { - } -} - -export class UpdateWebhookDto { - constructor( - public readonly url: string, - public readonly schemas: WebhookSchemaDto[] - ) { - } -} - -@Injectable() -export class WebhooksService { - constructor( - private readonly http: HttpClient, - private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService - ) { - } - - public getWebhooks(appName: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); - - return HTTP.getVersioned(this.http, url) - .map(response => { - const items: any[] = response.payload.body; - - return items.map(item => { - const schemas = item.schemas.map((schema: any) => - new WebhookSchemaDto( - schema.schemaId, - schema.sendCreate, - schema.sendUpdate, - schema.sendDelete, - schema.sendPublish)); - - return new WebhookDto( - item.id, - item.sharedSecret, - item.createdBy, - item.lastModifiedBy, - DateTime.parseISO_UTC(item.created), - DateTime.parseISO_UTC(item.lastModified), - new Version(item.version.toString()), - schemas, - item.url, - item.totalSucceeded, - item.totalFailed, - item.totalTimedout, - item.averageRequestTimeMs); - }); - }) - .pretifyError('Failed to load webhooks. Please reload.'); - } - - public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now: DateTime): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); - - return HTTP.postVersioned(this.http, url, dto) - .map(response => { - const body = response.payload.body; - - return new WebhookDto( - body.id, - body.sharedSecret, - user, - user, - now, - now, - response.version, - dto.schemas, - dto.url, - 0, 0, 0, 0); - }) - .do(() => { - this.analytics.trackEvent('Webhook', 'Created', appName); - }) - .pretifyError('Failed to create webhook. Please reload.'); - } - - public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); - - return HTTP.putVersioned(this.http, url, dto, version) - .do(() => { - this.analytics.trackEvent('Webhook', 'Updated', appName); - }) - .pretifyError('Failed to update webhook. Please reload.'); - } - - public deleteWebhook(appName: string, id: string, version: Version): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); - - return HTTP.deleteVersioned(this.http, url, version) - .do(() => { - this.analytics.trackEvent('Webhook', 'Deleted', appName); - }) - .pretifyError('Failed to delete webhook. Please reload.'); - } - - public getEvents(appName: string, take: number, skip: number): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events?take=${take}&skip=${skip}`); - - return HTTP.getVersioned(this.http, url) - .map(response => { - const body = response.payload.body; - - const items: any[] = body.items; - - return new WebhookEventsDto(body.total, items.map(item => { - return new WebhookEventDto( - item.id, - DateTime.parseISO_UTC(item.created), - item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, - item.eventName, - item.requestUrl, - item.lastDump, - item.result, - item.jobResult, - item.numCalls); - })); - }) - .pretifyError('Failed to load events. Please reload.'); - } - - public enqueueEvent(appName: string, id: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events/${id}`); - - return HTTP.putVersioned(this.http, url, {}) - .do(() => { - this.analytics.trackEvent('Webhook', 'EventEnqueued', appName); - }) - .pretifyError('Failed to enqueue webhook event. Please reload.'); - } -} \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.html b/src/Squidex/app/shell/pages/app/left-menu.component.html index f420ad2a4..c882bb56d 100644 --- a/src/Squidex/app/shell/pages/app/left-menu.component.html +++ b/src/Squidex/app/shell/pages/app/left-menu.component.html @@ -1,31 +1,31 @@ + +
diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.scss b/src/Squidex/app/shell/pages/internal/apps-menu.component.scss index f5ce63a43..1e3bff318 100644 --- a/src/Squidex/app/shell/pages/internal/apps-menu.component.scss +++ b/src/Squidex/app/shell/pages/internal/apps-menu.component.scss @@ -4,6 +4,10 @@ $color-apps-border: #65a6ff; .nav { + & { + @include flex-direction(row); + } + .nav-item { .nav-link { & { @@ -16,12 +20,10 @@ $color-apps-border: #65a6ff; color: $color-dark-foreground; border: 1px solid $color-theme-blue-lighter; background: $color-theme-blue; - padding-top: .1rem; + line-height: 1.25rem; padding-left: 1rem; padding-right: 2rem; - padding-bottom: .1rem; min-width: 150px; - margin-top: -.1rem; } &:hover { @@ -57,6 +59,19 @@ $color-apps-border: #65a6ff; } &-pill { - @include absolute(.8rem, .625rem, auto, auto); + @include absolute(.5rem, .625rem, auto, auto); + } +} + +.app-upgrade { + & { + margin-left: 2rem; + } + + .btn-primary { + @include box-shadow-inner; + background: $color-theme-blue-dark; + border: 0; + pointer-events: none; } } \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.ts b/src/Squidex/app/shell/pages/internal/apps-menu.component.ts index 32982458a..340739279 100644 --- a/src/Squidex/app/shell/pages/internal/apps-menu.component.ts +++ b/src/Squidex/app/shell/pages/internal/apps-menu.component.ts @@ -9,41 +9,39 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; import { + AppContext, AppDto, AppsStoreService, fadeAnimation, ModalView } from 'shared'; -const FALLBACK_NAME = 'Apps Overview'; - @Component({ selector: 'sqx-apps-menu', styleUrls: ['./apps-menu.component.scss'], templateUrl: './apps-menu.component.html', + providers: [ + AppContext + ], animations: [ fadeAnimation ] }) export class AppsMenuComponent implements OnDestroy, OnInit { private appsSubscription: Subscription; - private appSubscription: Subscription; public modalMenu = new ModalView(false, true); public modalDialog = new ModalView(); public apps: AppDto[] = []; - public appName = FALLBACK_NAME; - - constructor( + constructor(public readonly ctx: AppContext, private readonly appsStore: AppsStoreService ) { } public ngOnDestroy() { this.appsSubscription.unsubscribe(); - this.appSubscription.unsubscribe(); } public ngOnInit() { @@ -51,9 +49,6 @@ export class AppsMenuComponent implements OnDestroy, OnInit { this.appsStore.apps.subscribe(apps => { this.apps = apps; }); - - this.appSubscription = - this.appsStore.selectedApp.subscribe(selectedApp => this.appName = selectedApp ? selectedApp.name : FALLBACK_NAME); } public createApp() { diff --git a/src/Squidex/app/theme/_mixins.scss b/src/Squidex/app/theme/_mixins.scss index 0d963d90f..c6b03ced2 100644 --- a/src/Squidex/app/theme/_mixins.scss +++ b/src/Squidex/app/theme/_mixins.scss @@ -80,6 +80,13 @@ display: inline-flex; } +@mixin flex-direction($direction) { + -webkit-flex-direction: $direction; + -moz-flex-direction: $direction; + -ms-flex-direction: $direction; + flex-direction: $direction; +} + @mixin flex-flow($values: (row nowrap)) { -webkit-flex-flow: $values; -moz-flex-flow: $values; @@ -95,6 +102,12 @@ flex-grow: $int; } +@mixin flex-shrink($int: 0) { + -webkit-flex-shrink: $int; + -moz-flex-shrink: $int; + flex-shrink: $int; +} + @mixin inner-border($color: #999) { -webkit-box-shadow: inset 1px 1px 1px $color; -moz-box-shadow: inset 1px 1px 1px $color; diff --git a/src/Squidex/app/theme/_rules.scss b/src/Squidex/app/theme/_rules.scss new file mode 100644 index 000000000..b8684d272 --- /dev/null +++ b/src/Squidex/app/theme/_rules.scss @@ -0,0 +1,56 @@ +@import '_mixins'; +@import '_vars'; + +$trigger-content-changed-text: #3389ff; +$trigger-content-changed-icon: #297ff6; + +$action-webhook-text: #4bb958; +$action-webhook-icon: #47b353; + +@mixin build-element($text-color, $icon-color) { + & { + background: $text-color; + } + + &:hover { + background: $icon-color; + } + + .rule-element-icon { + background: $icon-color; + } +} + +.rule-element { + & { + @include truncate; + @include transition(background-color .4s ease); + color: $color-dark-foreground; + cursor: pointer; + display: inline-block; + line-height: 2.8rem; + font-size: 1rem; + font-weight: normal; + padding-right: .8rem; + width: 15rem; + } + + &-icon { + height: 3rem; + display: inline-block; + line-height: 3rem; + font-size: 1.2rem; + font-weight: normal; + margin: 0; + margin-right: .5rem; + padding: 0 .8rem; + } +} + +.rule-element-ContentChanged { + @include build-element($trigger-content-changed-text, $trigger-content-changed-icon) +} + +.rule-element-Webhook { + @include build-element($action-webhook-text, $action-webhook-icon) +} \ No newline at end of file diff --git a/src/Squidex/app/theme/_vars.scss b/src/Squidex/app/theme/_vars.scss index f2e31dfba..0d95b4757 100644 --- a/src/Squidex/app/theme/_vars.scss +++ b/src/Squidex/app/theme/_vars.scss @@ -53,7 +53,7 @@ $color-dark2-control: #2e3842; $color-dark2-separator: #2e3842; $color-dark2-placeholder: #757e8d; -$color-dark-onboarding: #2d333c; +$color-dark-onboarding: #273039; $color-panel-icon: #a2b0b6; diff --git a/src/Squidex/app/theme/icomoon/demo.html b/src/Squidex/app/theme/icomoon/demo.html index 6a8dccfce..65d64e8ea 100644 --- a/src/Squidex/app/theme/icomoon/demo.html +++ b/src/Squidex/app/theme/icomoon/demo.html @@ -1008,23 +1008,714 @@
+
+
+ + + + icon-trigger-ContentChanged +
+
+ + +
+
+ liga: + +
+
- icon-control-Date + icon-control-Date +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-DateTime +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Markdown +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-grid +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-list +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-user-o +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-rules +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-action-Webhook +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-location +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Map +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-Geolocation +
+
+ + +
+
+ liga: + +
+
+
+
+ + icon-logo +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-media +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-Assets +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-more +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-dots +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-pencil +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-reference +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-schemas +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-search +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-settings +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-Boolean +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-DateTime +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-Json +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-json +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-Number +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-String +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-user +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-document-lock +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-document-unpublish +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-angle-down +
+
+ + +
+
+ liga: + +
+
+
+
+

Grid Size: 16

+
+
+ + + + icon-bin2 +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-earth +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-elapsed
- - + +
liga:
-
+
+
+ + + + icon-google +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-lock +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-microsoft +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-pause +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-play +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-reset +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-settings2 +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-timeout +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-unlocked +
+
+ + +
+
+ liga: + +
+
+
@@ -1040,7 +1731,7 @@
-
+
@@ -1056,7 +1747,7 @@
-
+
@@ -1072,7 +1763,7 @@
-
+
@@ -1088,7 +1779,7 @@
-
+
@@ -1104,12 +1795,28 @@
-
+
- + - icon-webhooks + icon-rules +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-action-Webhook
@@ -1120,9 +1827,6 @@
-
-
-

Grid Size: 16

@@ -1171,55 +1875,58 @@
-
+
+
+

Grid Size: 32

+
- + - icon-google + icon-browser
- - + +
liga:
-
+
- + - icon-lock + icon-checkmark
- - + +
liga:
-
+
- + - icon-microsoft + icon-control-Stars
- - + +
liga:
-
+
@@ -1235,7 +1942,7 @@
-
+
@@ -1251,7 +1958,7 @@
-
+
@@ -1267,7 +1974,7 @@
-
+
@@ -1283,7 +1990,7 @@
-
+
@@ -1299,7 +2006,7 @@
-
+
@@ -1315,9 +2022,6 @@
-
-
-

Grid Size: 32

@@ -1401,6 +2105,22 @@
+
+
+ + + + icon-info +
+
+ + +
+
+ liga: + +
+

Grid Size: 20

diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot index 34ac483eb..fd387093e 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot and b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot differ diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg index e26db2b44..eda549c2d 100644 --- a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg +++ b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg @@ -77,8 +77,8 @@ - - + + diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf index a454fef5c..4202d1013 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf and b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf differ diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.woff b/src/Squidex/app/theme/icomoon/fonts/icomoon.woff index b899d7c8a..86edad2cc 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.woff and b/src/Squidex/app/theme/icomoon/fonts/icomoon.woff differ diff --git a/src/Squidex/app/theme/icomoon/selection.json b/src/Squidex/app/theme/icomoon/selection.json index ea64b0c2c..f4ddaef19 100644 --- a/src/Squidex/app/theme/icomoon/selection.json +++ b/src/Squidex/app/theme/icomoon/selection.json @@ -51,7 +51,7 @@ "prevSize": 32, "code": 59722 }, - "setIdx": 0, + "setIdx": 1, "setId": 2, "iconIdx": 0 }, @@ -80,7 +80,7 @@ "prevSize": 32, "code": 59652 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 21 }, @@ -109,7 +109,7 @@ "prevSize": 32, "code": 59653 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 22 }, @@ -138,7 +138,7 @@ "prevSize": 32, "code": 59654 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 23 }, @@ -167,7 +167,7 @@ "prevSize": 32, "code": 59655 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 24 }, @@ -196,7 +196,7 @@ "prevSize": 32, "code": 59656 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 25 }, @@ -225,7 +225,7 @@ "prevSize": 32, "code": 59657 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 26 }, @@ -254,7 +254,7 @@ "prevSize": 32, "code": 59658 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 27 }, @@ -283,7 +283,7 @@ "prevSize": 32, "code": 59659 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 28 }, @@ -312,7 +312,7 @@ "prevSize": 32, "code": 59660 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 29 }, @@ -341,7 +341,7 @@ "prevSize": 32, "code": 59661 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 30 }, @@ -370,7 +370,7 @@ "prevSize": 32, "code": 59662 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 31 }, @@ -399,7 +399,7 @@ "prevSize": 32, "code": 59663 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 32 }, @@ -428,7 +428,7 @@ "prevSize": 32, "code": 59664 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 33 }, @@ -463,7 +463,7 @@ "prevSize": 32, "code": 59665 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 34 }, @@ -492,7 +492,7 @@ "prevSize": 32, "code": 59666 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 35 }, @@ -521,7 +521,7 @@ "prevSize": 32, "code": 59667 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 36 }, @@ -550,7 +550,7 @@ "prevSize": 32, "code": 59668 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 37 }, @@ -579,7 +579,7 @@ "prevSize": 32, "code": 59669 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 38 }, @@ -608,7 +608,7 @@ "prevSize": 32, "code": 59670 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 39 }, @@ -637,7 +637,7 @@ "prevSize": 32, "code": 59671 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 40 }, @@ -666,7 +666,7 @@ "prevSize": 32, "code": 59672 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 41 }, @@ -696,7 +696,7 @@ "prevSize": 32, "code": 59713 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 42 }, @@ -725,7 +725,7 @@ "prevSize": 32, "code": 59673 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 43 }, @@ -754,7 +754,7 @@ "prevSize": 32, "code": 59675 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 44 }, @@ -783,7 +783,7 @@ "prevSize": 32, "code": 59676 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 45 }, @@ -812,7 +812,7 @@ "prevSize": 32, "code": 59677 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 46 }, @@ -841,7 +841,7 @@ "prevSize": 32, "code": 59678 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 47 }, @@ -870,7 +870,7 @@ "prevSize": 32, "code": 59679 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 48 }, @@ -899,7 +899,7 @@ "prevSize": 32, "code": 59680 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 49 }, @@ -937,7 +937,7 @@ "prevSize": 32, "code": 59681 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 50 }, @@ -966,7 +966,7 @@ "prevSize": 32, "code": 59682 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 51 }, @@ -995,7 +995,7 @@ "prevSize": 32, "code": 59683 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 52 }, @@ -1024,7 +1024,7 @@ "prevSize": 32, "code": 59684 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 53 }, @@ -1053,7 +1053,7 @@ "prevSize": 32, "code": 59685 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 54 }, @@ -1082,7 +1082,7 @@ "prevSize": 32, "code": 59674 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 55 }, @@ -1111,7 +1111,7 @@ "prevSize": 32, "code": 59686 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 56 }, @@ -1140,7 +1140,7 @@ "prevSize": 32, "code": 59687 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 57 }, @@ -1169,7 +1169,7 @@ "prevSize": 32, "code": 59688 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 58 }, @@ -1201,7 +1201,7 @@ "prevSize": 32, "code": 59721 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 0 }, @@ -1233,7 +1233,7 @@ "prevSize": 32, "code": 59711 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 1 }, @@ -1263,7 +1263,7 @@ "code": 59648, "name": "angle-down" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 2 }, @@ -1293,7 +1293,7 @@ "code": 59649, "name": "angle-left" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 3 }, @@ -1323,7 +1323,7 @@ "code": 59697, "name": "angle-right" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 4 }, @@ -1353,7 +1353,7 @@ "code": 59651, "name": "angle-up" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 5 }, @@ -1394,7 +1394,7 @@ "prevSize": 32, "code": 59717 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 6 }, @@ -1429,7 +1429,7 @@ "prevSize": 32, "code": 59720 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 7 }, @@ -1459,7 +1459,7 @@ "code": 59709, "name": "bug" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 8 }, @@ -1489,7 +1489,7 @@ "code": 59692, "name": "caret-down" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 9 }, @@ -1519,7 +1519,7 @@ "code": 59690, "name": "caret-left" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 10 }, @@ -1549,7 +1549,7 @@ "code": 59689, "name": "caret-right" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 11 }, @@ -1579,7 +1579,7 @@ "code": 59691, "name": "caret-up" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 12 }, @@ -1613,11 +1613,11 @@ "properties": { "order": 93, "id": 204, - "name": "contents", + "name": "contents, trigger-ContentChanged", "prevSize": 32, "code": 59718 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 13 }, @@ -1646,7 +1646,7 @@ "prevSize": 32, "code": 59702 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 14 }, @@ -1675,7 +1675,7 @@ "prevSize": 32, "code": 59703 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 15 }, @@ -1704,7 +1704,7 @@ "prevSize": 32, "code": 59704 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 16 }, @@ -1731,7 +1731,7 @@ "prevSize": 32, "code": 61450 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 17 }, @@ -1760,7 +1760,7 @@ "prevSize": 32, "code": 61641 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 18 }, @@ -1789,7 +1789,7 @@ "prevSize": 32, "code": 59698 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 19 }, @@ -1835,11 +1835,11 @@ "properties": { "order": 92, "id": 203, - "name": "webhooks", + "name": "rules, action-Webhook", "prevSize": 32, "code": 59719 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 20 }, @@ -1876,7 +1876,7 @@ "prevSize": 32, "code": 59650 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 59 }, @@ -1909,7 +1909,7 @@ "prevSize": 32, "code": 59850 }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 60 }, @@ -1942,7 +1942,7 @@ "code": 59715, "name": "elapsed" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 61 }, @@ -1972,7 +1972,7 @@ "code": 59707, "name": "google" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 62 }, @@ -2004,7 +2004,7 @@ "code": 59700, "name": "lock" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 63 }, @@ -2035,7 +2035,7 @@ "code": 59712, "name": "microsoft" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 64 }, @@ -2065,7 +2065,7 @@ "code": 59695, "name": "pause" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 65 }, @@ -2095,7 +2095,7 @@ "code": 59696, "name": "play" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 66 }, @@ -2134,7 +2134,7 @@ "code": 59694, "name": "reset" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 67 }, @@ -2169,7 +2169,7 @@ "code": 59693, "name": "settings2" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 68 }, @@ -2203,7 +2203,7 @@ "code": 59716, "name": "timeout" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 69 }, @@ -2233,7 +2233,7 @@ "code": 59699, "name": "unlocked" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 70 }, @@ -2266,7 +2266,7 @@ "code": 59701, "name": "browser" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 71 }, @@ -2298,7 +2298,7 @@ "code": 59714, "name": "checkmark" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 72 }, @@ -2328,7 +2328,7 @@ "code": 59706, "name": "control-Stars" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 73 }, @@ -2357,7 +2357,7 @@ "code": 59705, "name": "control-RichText" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 74 }, @@ -2386,7 +2386,7 @@ "code": 59710, "name": "download" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 75 }, @@ -2415,7 +2415,7 @@ "code": 59708, "name": "info" }, - "setIdx": 1, + "setIdx": 2, "setId": 1, "iconIdx": 76 } diff --git a/src/Squidex/app/theme/icomoon/style.css b/src/Squidex/app/theme/icomoon/style.css index 76d816b7e..a99599455 100644 --- a/src/Squidex/app/theme/icomoon/style.css +++ b/src/Squidex/app/theme/icomoon/style.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('fonts/icomoon.eot?2889ep'); - src: url('fonts/icomoon.eot?2889ep#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?2889ep') format('truetype'), - url('fonts/icomoon.woff?2889ep') format('woff'), - url('fonts/icomoon.svg?2889ep#icomoon') format('svg'); + src: url('fonts/icomoon.eot?yuqff8'); + src: url('fonts/icomoon.eot?yuqff8#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?yuqff8') format('truetype'), + url('fonts/icomoon.woff?yuqff8') format('woff'), + url('fonts/icomoon.svg?yuqff8#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -211,6 +211,9 @@ .icon-contents:before { content: "\e946"; } +.icon-trigger-ContentChanged:before { + content: "\e946"; +} .icon-control-Date:before { content: "\e936"; } @@ -229,9 +232,81 @@ .icon-user-o:before { content: "\e932"; } -.icon-webhooks:before { +.icon-rules:before { content: "\e947"; } +.icon-action-Webhook:before { + content: "\e947"; +} +.icon-location:before { + content: "\e91b"; +} +.icon-control-Map:before { + content: "\e91b"; +} +.icon-type-Geolocation:before { + content: "\e91b"; +} +.icon-logo:before { + content: "\e91c"; +} +.icon-media:before { + content: "\e91d"; +} +.icon-type-Assets:before { + content: "\e91d"; +} +.icon-more:before { + content: "\e91e"; +} +.icon-dots:before { + content: "\e91e"; +} +.icon-pencil:before { + content: "\e91f"; +} +.icon-reference:before { + content: "\e920"; +} +.icon-schemas:before { + content: "\e921"; +} +.icon-search:before { + content: "\e922"; +} +.icon-settings:before { + content: "\e923"; +} +.icon-type-Boolean:before { + content: "\e924"; +} +.icon-type-DateTime:before { + content: "\e925"; +} +.icon-type-Json:before { + content: "\e91a"; +} +.icon-json:before { + content: "\e91a"; +} +.icon-type-Number:before { + content: "\e926"; +} +.icon-type-String:before { + content: "\e927"; +} +.icon-user:before { + content: "\e928"; +} +.icon-document-lock:before { + content: "\e949"; +} +.icon-document-unpublish:before { + content: "\e93f"; +} +.icon-angle-down:before { + content: "\e900"; +} .icon-bin2:before { content: "\e902"; } @@ -268,6 +343,63 @@ .icon-unlocked:before { content: "\e933"; } +.icon-control-DateTime:before { + content: "\e937"; +} +.icon-control-Markdown:before { + content: "\e938"; +} +.icon-grid:before { + content: "\f00a"; +} +.icon-list:before { + content: "\f0c9"; +} +.icon-user-o:before { + content: "\e932"; +} +.icon-rules:before { + content: "\e947"; +} +.icon-action-Webhook:before { + content: "\e947"; +} +.icon-bin2:before { + content: "\e902"; +} +.icon-earth:before { + content: "\e9ca"; +} +.icon-elapsed:before { + content: "\e943"; +} +.icon-browser:before { + content: "\e935"; +} +.icon-checkmark:before { + content: "\e942"; +} +.icon-control-Stars:before { + content: "\e93a"; +} +.icon-pause:before { + content: "\e92f"; +} +.icon-play:before { + content: "\e930"; +} +.icon-reset:before { + content: "\e92e"; +} +.icon-settings2:before { + content: "\e92d"; +} +.icon-timeout:before { + content: "\e944"; +} +.icon-unlocked:before { + content: "\e933"; +} .icon-browser:before { content: "\e935"; } @@ -286,3 +418,6 @@ .icon-info:before { content: "\e93c"; } +.icon-info:before { + content: "\e93c"; +} diff --git a/src/Squidex/app/theme/theme.scss b/src/Squidex/app/theme/theme.scss index 72aa22ddc..c91f6a74d 100644 --- a/src/Squidex/app/theme/theme.scss +++ b/src/Squidex/app/theme/theme.scss @@ -19,4 +19,5 @@ @import '_panels'; @import '_forms'; @import '_lists'; +@import '_rules'; @import '_static'; \ No newline at end of file diff --git a/src/Squidex/package.json b/src/Squidex/package.json index 377e68b06..6d6007411 100644 --- a/src/Squidex/package.json +++ b/src/Squidex/package.json @@ -15,36 +15,36 @@ "build:clean": "rimraf wwwroot/build" }, "dependencies": { - "@angular/animations": "4.4.4", - "@angular/common": "4.4.4", - "@angular/compiler": "4.4.4", - "@angular/core": "4.4.4", - "@angular/forms": "4.4.4", - "@angular/http": "4.4.4", - "@angular/platform-browser": "4.4.4", - "@angular/platform-browser-dynamic": "4.4.4", - "@angular/platform-server": "4.4.4", - "@angular/router": "4.4.4", + "@angular/animations": "4.4.6", + "@angular/common": "4.4.6", + "@angular/compiler": "4.4.6", + "@angular/core": "4.4.6", + "@angular/forms": "4.4.6", + "@angular/http": "4.4.6", + "@angular/platform-browser": "4.4.6", + "@angular/platform-browser-dynamic": "4.4.6", + "@angular/platform-server": "4.4.6", + "@angular/router": "4.4.6", "angular2-chartjs": "0.3.0", "babel-polyfill": "6.26.0", "bootstrap": "4.0.0-alpha.6", "core-js": "2.5.1", - "graphiql": "0.11.5", - "moment": "2.18.1", + "graphiql": "0.11.10", + "moment": "2.19.1", "mousetrap": "1.6.1", "ng2-dnd": "4.2.0", "oidc-client": "1.4.1", "pikaday": "1.6.1", "progressbar.js": "1.0.1", - "react": "15.6.2", - "react-dom": "15.6.2", + "react": "16.0.0", + "react-dom": "16.0.0", "redoc": "1.19.1", - "rxjs": "5.4.3", + "rxjs": "5.5.2", "zone.js": "0.8.18" }, "devDependencies": { - "@angular/compiler-cli": "4.4.4", - "@angular/tsc-wrapped": "4.4.4", + "@angular/compiler-cli": "4.4.6", + "@angular/tsc-wrapped": "4.4.6", "@ngtools/webpack": "1.7.2", "@types/core-js": "0.9.35", "@types/jasmine": "2.5.45", @@ -54,12 +54,12 @@ "@types/react-dom": "15.5.6", "angular2-router-loader": "0.3.5", "angular2-template-loader": "0.6.2", - "awesome-typescript-loader": "3.2.3", - "codelyzer": "^3.1.2", + "awesome-typescript-loader": "3.3.0", + "codelyzer": "4.0.1", "cpx": "1.5.0", "css-loader": "0.28.7", "exports-loader": "0.6.4", - "extract-text-webpack-plugin": "3.0.1", + "extract-text-webpack-plugin": "3.0.2", "file-loader": "1.1.5", "html-loader": "0.5.1", "html-webpack-plugin": "2.30.1", @@ -73,26 +73,26 @@ "karma-htmlfile-reporter": "0.3.5", "karma-jasmine": "1.1.0", "karma-jasmine-html-reporter": "^0.2.2", - "karma-mocha-reporter": "2.2.4", + "karma-mocha-reporter": "2.2.5", "karma-phantomjs-launcher": "1.0.4", "karma-sourcemap-loader": "0.3.7", "karma-webpack": "2.0.5", "node-sass": "4.5.3", "noop-loader": "^1.0.0", "null-loader": "0.1.1", - "phantomjs-prebuilt": "2.1.15", + "phantomjs-prebuilt": "2.1.16", "raw-loader": "0.5.1", "rimraf": "2.6.2", - "sass-lint": "1.12.0", + "sass-lint": "1.12.1", "sass-loader": "6.0.6", "style-loader": "0.19.0", - "tslint": "5.7.0", + "tslint": "5.8.0", "tslint-loader": "3.5.3", - "typemoq": "2.0.1", + "typemoq": "2.1.0", "typescript": "2.5.3", "underscore": "1.8.3", - "webpack": "3.6.0", - "webpack-dev-server": "2.9.1", - "webpack-merge": "4.1.0" + "webpack": "3.8.1", + "webpack-dev-server": "2.9.4", + "webpack-merge": "4.1.1" } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs index a3fb1aafa..cfb46ba91 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs @@ -172,6 +172,14 @@ namespace Squidex.Domain.Apps.Core.Model.Apps config.Remove(Language.EN); } + [Fact] + public void Should_hrow_exception_if_language_to_remove_is_master() + { + var config = LanguagesConfig.Build(Language.DE); + + Assert.Throws(() => config.Remove(Language.DE)); + } + [Fact] public void Should_update_language() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs index 10ed1df6f..49498115b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs @@ -12,6 +12,46 @@ namespace Squidex.Domain.Apps.Core.Model { public sealed class PartitioningTests { + [Fact] + public void Should_consider_null_as_valid_partitioning() + { + string partitioning = null; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_consider_invariant_as_valid_partitioning() + { + var partitioning = "invariant"; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_consider_language_as_valid_partitioning() + { + var partitioning = "language"; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_not_consider_empty_as_valid_partitioning() + { + var partitioning = string.Empty; + + Assert.False(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_not_consider_other_string_as_valid_partitioning() + { + var partitioning = "invalid"; + + Assert.False(partitioning.IsValidPartitioning()); + } + [Fact] public void Should_provide_invariant_instance() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs new file mode 100644 index 000000000..1d83454fc --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// RuleTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Rules +{ + public class RuleTests + { + private readonly JsonSerializer serializer = TestData.DefaultSerializer(); + private readonly Rule sut = new Rule(new ContentChangedTrigger(), new WebhookAction()); + + public sealed class OtherTrigger : RuleTrigger + { + public override T Accept(IRuleTriggerVisitor visitor) + { + throw new NotSupportedException(); + } + } + + public sealed class OtherAction : RuleAction + { + public override T Accept(IRuleActionVisitor visitor) + { + throw new NotSupportedException(); + } + } + + [Fact] + public void Should_create_with_trigger_and_action() + { + var ruleTrigger = new ContentChangedTrigger(); + var ruleAction = new WebhookAction(); + + var newRule = new Rule(ruleTrigger, ruleAction); + + Assert.Equal(ruleTrigger, newRule.Trigger); + Assert.Equal(ruleAction, newRule.Action); + Assert.True(newRule.IsEnabled); + } + + [Fact] + public void Should_set_enabled_to_true_when_enabling() + { + sut.Enable(); + + Assert.True(sut.IsEnabled); + } + + [Fact] + public void Should_set_enabled_to_false_when_disabling() + { + sut.Enable(); + sut.Disable(); + + Assert.False(sut.IsEnabled); + } + + [Fact] + public void Should_replace_trigger_when_updating() + { + var newTrigger = new ContentChangedTrigger(); + + sut.Update(newTrigger); + + Assert.Same(newTrigger, sut.Trigger); + } + + [Fact] + public void Should_throw_exception_when_new_trigger_has_other_type() + { + Assert.Throws(() => sut.Update(new OtherTrigger())); + } + + [Fact] + public void Should_replace_action_when_updating() + { + var newAction = new WebhookAction(); + + sut.Update(newAction); + + Assert.Same(newAction, sut.Action); + } + + [Fact] + public void Should_throw_exception_when_new_action_has_other_type() + { + Assert.Throws(() => sut.Update(new OtherAction())); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + sut.Disable(); + + var appClients = JToken.FromObject(sut, serializer).ToObject(serializer); + + appClients.ShouldBeEquivalentTo(sut); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs new file mode 100644 index 000000000..207fdf0fc --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -0,0 +1,229 @@ +// ========================================================================== +// RuleServiceTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Xunit; + +#pragma warning disable xUnit2009 // Do not use boolean check to check for string equality + +namespace Squidex.Domain.Apps.Core.Operations.HandleRules +{ + public class RuleServiceTests + { + private readonly IRuleTriggerHandler ruleTriggerHandler = A.Fake(); + private readonly IRuleActionHandler ruleActionHandler = A.Fake(); + private readonly IClock clock = A.Fake(); + private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + private readonly RuleService sut; + + public sealed class InvalidEvent : IEvent + { + } + + public sealed class InvalidAction : RuleAction + { + public override T Accept(IRuleActionVisitor visitor) + { + return default(T); + } + } + + public sealed class InvalidTrigger : RuleTrigger + { + public override T Accept(IRuleTriggerVisitor visitor) + { + return default(T); + } + } + + public RuleServiceTests() + { + typeNameRegistry.Map(typeof(ContentCreated)); + typeNameRegistry.Map(typeof(WebhookAction)); + + A.CallTo(() => ruleActionHandler.ActionType) + .Returns(typeof(WebhookAction)); + + A.CallTo(() => ruleTriggerHandler.TriggerType) + .Returns(typeof(ContentChangedTrigger)); + + sut = new RuleService(new[] { ruleTriggerHandler }, new[] { ruleActionHandler }, clock, typeNameRegistry); + } + + [Fact] + public void Should_not_create_job_for_invalid_event() + { + var ruleConfig = new Rule(new ContentChangedTrigger(), new WebhookAction()); + var ruleEnvelope = Envelope.Create(new InvalidEvent()); + + var job = sut.CreateJob(ruleConfig, ruleEnvelope); + + Assert.Null(job); + } + + [Fact] + public void Should_not_create_trigger_if_no_trigger_handler_registered() + { + var ruleConfig = new Rule(new InvalidTrigger(), new WebhookAction()); + var ruleEnvelope = Envelope.Create(new ContentCreated()); + + var job = sut.CreateJob(ruleConfig, ruleEnvelope); + + Assert.Null(job); + } + + [Fact] + public void Should_not_create_trigger_if_no_action_handler_registered() + { + var ruleConfig = new Rule(new ContentChangedTrigger(), new InvalidAction()); + var ruleEnvelope = Envelope.Create(new ContentCreated()); + + var job = sut.CreateJob(ruleConfig, ruleEnvelope); + + Assert.Null(job); + } + + [Fact] + public void Should_not_create_if_not_triggered() + { + var ruleConfig = new Rule(new ContentChangedTrigger(), new InvalidAction()); + var ruleEnvelope = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Triggers(A>.Ignored, ruleConfig.Trigger)) + .Returns(false); + + var job = sut.CreateJob(ruleConfig, ruleEnvelope); + + Assert.Null(job); + } + + [Fact] + public void Should_create_job_if_triggeres() + { + var e = new ContentCreated { SchemaId = new NamedId(Guid.NewGuid(), "my-schema"), AppId = new NamedId(Guid.NewGuid(), "my-event") }; + + var now = SystemClock.Instance.GetCurrentInstant(); + + var ruleConfig = new Rule(new ContentChangedTrigger(), new WebhookAction()); + var ruleEnvelope = Envelope.Create(e); + + var actionName = "WebhookAction"; + var actionData = new RuleJobData(); + var actionDescription = "MyDescription"; + + var eventName = "MySchemaCreatedEvent"; + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); + + A.CallTo(() => ruleTriggerHandler.Triggers(A>.Ignored, ruleConfig.Trigger)) + .Returns(true); + + A.CallTo(() => ruleActionHandler.CreateJob(A>.Ignored, eventName, ruleConfig.Action)) + .Returns((actionDescription, actionData)); + + var job = sut.CreateJob(ruleConfig, ruleEnvelope); + + Assert.Equal(eventName, job.EventName); + + Assert.Equal(actionData, job.ActionData); + Assert.Equal(actionName, job.ActionName); + Assert.Equal(actionDescription, job.Description); + + Assert.Equal(now, job.Created); + Assert.Equal(now.Plus(Duration.FromDays(2)), job.Expires); + + Assert.Equal(e.AppId.Id, job.AppId); + + Assert.NotEqual(Guid.Empty, job.RuleId); + } + + [Fact] + public async Task Should_return_succeeded_job_with_full_dump_when_handler_returns_no_exception() + { + var ruleJob = new RuleJobData(); + var ruleEx = new InvalidOperationException(); + + var actionDump = "MyDump"; + + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(ruleJob)) + .Returns((actionDump, null)); + + var result = await sut.InvokeAsync("WebhookAction", ruleJob); + + Assert.Equal(RuleResult.Success, result.Result); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.StartsWith(actionDump, result.Dump, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task Should_return_failed_job_with_full_dump_when_handler_returns_exception() + { + var ruleJob = new RuleJobData(); + var ruleEx = new InvalidOperationException(); + + var actionDump = "MyDump"; + + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(ruleJob)) + .Returns((actionDump, new InvalidOperationException())); + + var result = await sut.InvokeAsync("WebhookAction", ruleJob); + + Assert.Equal(RuleResult.Failed, result.Result); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.True(result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task Should_return_timedout_job_with_full_dump_when_exception_from_handler_indicates_timeout() + { + var ruleJob = new RuleJobData(); + var ruleEx = new InvalidOperationException(); + + var actionDump = "MyDump"; + + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(ruleJob)) + .Returns((actionDump, new TimeoutException())); + + var result = await sut.InvokeAsync("WebhookAction", ruleJob); + + Assert.Equal(RuleResult.Timeout, result.Result); + + Assert.True(result.Elapsed >= TimeSpan.Zero); + Assert.True(result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + Assert.True(result.Dump.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); + } + + [Fact] + public async Task Should_create_exception_details_when_job_to_execute_failed() + { + var ruleJob = new RuleJobData(); + var ruleEx = new InvalidOperationException(); + + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(ruleJob)) + .Throws(ruleEx); + + var result = await sut.InvokeAsync("WebhookAction", ruleJob); + + Assert.Equal((ruleEx.ToString(), RuleResult.Failed, TimeSpan.Zero), result); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs new file mode 100644 index 000000000..db51764cb --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// ContentChangedTriggerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.Triggers; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers +{ + public sealed class ContentChangedTriggerTests + { + private readonly IRuleTriggerHandler sut = new ContentChangedTriggerHandler(); + private static readonly NamedId SchemaMatch = new NamedId(Guid.NewGuid(), "my-schema1"); + private static readonly NamedId SchemaNonMatch = new NamedId(Guid.NewGuid(), "my-schema2"); + + public static IEnumerable TestData + { + get + { + return new[] + { + new object[] { 0, 1, 1, 1, 1, new RuleCreated() }, + new object[] { 0, 1, 1, 1, 1, new ContentCreated { SchemaId = SchemaNonMatch } }, + new object[] { 1, 1, 0, 0, 0, new ContentCreated { SchemaId = SchemaMatch } }, + new object[] { 0, 0, 0, 0, 0, new ContentCreated { SchemaId = SchemaMatch } }, + new object[] { 1, 0, 1, 0, 0, new ContentUpdated { SchemaId = SchemaMatch } }, + new object[] { 0, 0, 0, 0, 0, new ContentUpdated { SchemaId = SchemaMatch } }, + new object[] { 1, 0, 0, 1, 0, new ContentDeleted { SchemaId = SchemaMatch } }, + new object[] { 0, 0, 0, 0, 0, new ContentDeleted { SchemaId = SchemaMatch } }, + new object[] { 1, 0, 0, 0, 1, new ContentStatusChanged { SchemaId = SchemaMatch, Status = Status.Published } }, + new object[] { 0, 0, 0, 0, 0, new ContentStatusChanged { SchemaId = SchemaMatch, Status = Status.Published } }, + new object[] { 0, 1, 1, 1, 1, new ContentStatusChanged { SchemaId = SchemaMatch, Status = Status.Archived } }, + new object[] { 0, 1, 1, 1, 1, new ContentStatusChanged { SchemaId = SchemaMatch, Status = Status.Draft } }, + new object[] { 0, 1, 1, 1, 1, new SchemaCreated { SchemaId = SchemaNonMatch } } + }; + } + } + + [Fact] + public void Should_return_false_when_trigger_contains_no_schemas() + { + var trigger = new ContentChangedTrigger(); + + var result = sut.Triggers(new Envelope(new ContentCreated()), trigger); + + Assert.False(result); + } + + [Theory] + [MemberData(nameof(TestData))] + public void Should_return_result_depending_on_event(int expected, int sendCreate, int sendUpdate, int sendDelete, int sendPublish, AppEvent @event) + { + var trigger = new ContentChangedTrigger + { + Schemas = new List + { + new ContentChangedTriggerSchema + { + SendCreate = sendCreate == 1, + SendUpdate = sendUpdate == 1, + SendDelete = sendDelete == 1, + SendPublish = sendPublish == 1, + SchemaId = SchemaMatch.Id + } + } + }; + + var result = sut.Triggers(new Envelope(@event), trigger); + + Assert.Equal(expected == 1, result); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 6314f5295..d71558483 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -10,6 +10,7 @@ + @@ -19,10 +20,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs index 15b4449a0..ad21aafa0 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Infrastructure; @@ -36,6 +37,7 @@ namespace Squidex.Domain.Apps.Core new NamedLongIdConverter(), new NamedStringIdConverter(), new RefTokenConverter(), + new RuleConverter(), new SchemaConverter(new FieldRegistry(typeNameRegistry)), new StringEnumConverter()), diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs index 33a72e3fe..f81da552b 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs @@ -16,26 +16,35 @@ namespace Squidex.Domain.Apps.Read.Apps { public class ConfigAppLimitsProviderTests { - private static readonly ConfigAppLimitsPlan[] Plans = + private static readonly ConfigAppLimitsPlan InfinitePlan = new ConfigAppLimitsPlan { - new ConfigAppLimitsPlan - { - Id = "basic", - Name = "Basic", - MaxApiCalls = 150000, - MaxAssetSize = 1024 * 1024 * 2, - MaxContributors = 5 - }, - new ConfigAppLimitsPlan - { - Id = "free", - Name = "Free", - MaxApiCalls = 50000, - MaxAssetSize = 1024 * 1024 * 10, - MaxContributors = 2 - } + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1 }; + private static readonly ConfigAppLimitsPlan FreePlan = new ConfigAppLimitsPlan + { + Id = "free", + Name = "Free", + MaxApiCalls = 50000, + MaxAssetSize = 1024 * 1024 * 10, + MaxContributors = 2 + }; + + private static readonly ConfigAppLimitsPlan BasicPlan = new ConfigAppLimitsPlan + { + Id = "basic", + Name = "Basic", + MaxApiCalls = 150000, + MaxAssetSize = 1024 * 1024 * 2, + MaxContributors = 5 + }; + + private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan }; + [Fact] public void Should_return_plans() { @@ -53,61 +62,80 @@ namespace Squidex.Domain.Apps.Read.Apps var plan = sut.GetPlanForApp(CreateApp(planId)); - plan.ShouldBeEquivalentTo(new ConfigAppLimitsPlan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1 - }); + plan.ShouldBeEquivalentTo(InfinitePlan); } [Fact] - public void Should_check_plan_exists() + public void Should_return_fitting_app_plan() { var sut = new ConfigAppPlansProvider(Plans); - Assert.True(sut.IsConfiguredPlan("basic")); - Assert.True(sut.IsConfiguredPlan("free")); + var plan = sut.GetPlanForApp(CreateApp("basic")); - Assert.False(sut.IsConfiguredPlan("infinite")); - Assert.False(sut.IsConfiguredPlan("invalid")); - Assert.False(sut.IsConfiguredPlan(null)); + plan.ShouldBeEquivalentTo(BasicPlan); } [Fact] - public void Should_return_fitting_app_plan() + public void Should_smallest_plan_if_none_fits() { var sut = new ConfigAppPlansProvider(Plans); - var plan = sut.GetPlanForApp(CreateApp("basic")); + var plan = sut.GetPlanForApp(CreateApp("enterprise")); - plan.ShouldBeEquivalentTo(new ConfigAppLimitsPlan - { - Id = "basic", - Name = "Basic", - MaxApiCalls = 150000, - MaxAssetSize = 1024 * 1024 * 2, - MaxContributors = 5 - }); + plan.ShouldBeEquivalentTo(FreePlan); } [Fact] - public void Should_smallest_plan_if_none_fits() + public void Should_return_second_plan_for_upgrade_if_plan_is_null() { var sut = new ConfigAppPlansProvider(Plans); - var plan = sut.GetPlanForApp(CreateApp("Enterprise")); + var upgradePlan = sut.GetPlanUpgrade(null); + + upgradePlan.ShouldBeEquivalentTo(BasicPlan); + } - plan.ShouldBeEquivalentTo(new ConfigAppLimitsPlan - { - Id = "free", - Name = "Free", - MaxApiCalls = 50000, - MaxAssetSize = 1024 * 1024 * 10, - MaxContributors = 2 - }); + [Fact] + public void Should_return_second_plan_for_upgrade_if_plan_not_found() + { + var sut = new ConfigAppPlansProvider(Plans); + + var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("enterprise")); + + upgradePlan.ShouldBeEquivalentTo(BasicPlan); + } + + [Fact] + public void Should_not_return_plan_for_upgrade_if_plan_is_highest_plan() + { + var sut = new ConfigAppPlansProvider(Plans); + + var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("basic")); + + Assert.Null(upgradePlan); + } + + [Fact] + public void Should_return_next_plan_if_plan_is_upgradeable() + { + var sut = new ConfigAppPlansProvider(Plans); + + var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("free")); + + upgradePlan.ShouldBeEquivalentTo(BasicPlan); + } + + [Fact] + public void Should_check_plan_exists() + { + var sut = new ConfigAppPlansProvider(Plans); + + Assert.True(sut.IsConfiguredPlan("basic")); + Assert.True(sut.IsConfiguredPlan("free")); + + Assert.False(sut.IsConfiguredPlan("infinite")); + Assert.False(sut.IsConfiguredPlan("invalid")); + Assert.False(sut.IsConfiguredPlan(null)); } private static IAppEntity CreateApp(string plan) diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs new file mode 100644 index 000000000..8914346d7 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// RuleDequeuerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure.Log; +using Xunit; + +#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void + +namespace Squidex.Domain.Apps.Read.Rules +{ + public class RuleDequeuerTests + { + private readonly IClock clock = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly IRuleRepository ruleRepository = A.Fake(); + private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly RuleService ruleService = A.Fake(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + + public RuleDequeuerTests() + { + A.CallTo(() => clock.GetCurrentInstant()).Returns(now); + } + + [Theory] + [InlineData(0, 0, RuleResult.Success, RuleJobResult.Success)] + [InlineData(0, 5, RuleResult.Timeout, RuleJobResult.Retry)] + [InlineData(1, 60, RuleResult.Timeout, RuleJobResult.Retry)] + [InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] + [InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] + [InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] + public void Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult) + { + var actionData = new RuleJobData(); + var actionName = "MyAction"; + + var @event = CreateEvent(calls, actionName, actionData); + + var requestElapsed = TimeSpan.FromMinutes(1); + var requestDump = "Dump"; + + SetupSender(@event, requestDump, result, requestElapsed); + SetupPendingEvents(@event); + + var sut = new RuleDequeuer( + ruleService, + ruleEventRepository, + log, + clock); + + sut.Next(); + sut.Dispose(); + + Instant? nextCall = null; + + if (minutes > 0) + { + nextCall = now.Plus(Duration.FromMinutes(minutes)); + } + + VerifyRepositories(@event, requestDump, result, jobResult, requestElapsed, nextCall); + } + + private void SetupSender(IRuleEventEntity @event, string requestDump, RuleResult requestResult, TimeSpan requestTime) + { + A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData)) + .Returns((requestDump, requestResult, requestTime)); + } + + private void SetupPendingEvents(IRuleEventEntity @event) + { + A.CallTo(() => ruleEventRepository.QueryPendingAsync( + now, + A>.Ignored, + A.Ignored)) + .Invokes(async (Instant n, Func callback, CancellationToken ct) => + { + await callback(@event); + }); + } + + private void VerifyRepositories(IRuleEventEntity @event, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall) + { + A.CallTo(() => ruleEventRepository.MarkSendingAsync(@event.Id)) + .MustHaveHappened(); + + A.CallTo(() => ruleEventRepository.MarkSentAsync(@event.Id, dump, result, jobResult, elapsed, nextCall)) + .MustHaveHappened(); + } + + private IRuleEventEntity CreateEvent(int numCalls, string actionName, RuleJobData actionData) + { + var @event = A.Fake(); + + var job = new RuleJob + { + RuleId = Guid.NewGuid(), + ActionData = actionData, + ActionName = actionName, + Created = now + }; + + A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); + A.CallTo(() => @event.Job).Returns(job); + A.CallTo(() => @event.Created).Returns(now); + A.CallTo(() => @event.NumCalls).Returns(numCalls); + + return @event; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs new file mode 100644 index 000000000..500bc2a44 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// RuleEnqueuerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Xunit; + +namespace Squidex.Domain.Apps.Read.Rules +{ + public class RuleEnqueuerTests + { + private readonly IRuleRepository ruleRepository = A.Fake(); + private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly RuleService ruleService = A.Fake(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + private readonly RuleEnqueuer sut; + + public RuleEnqueuerTests() + { + sut = new RuleEnqueuer( + ruleEventRepository, + ruleRepository, + ruleService); + } + + [Fact] + public void Should_return_contents_filter_for_events_filter() + { + Assert.Equal(".*", sut.EventsFilter); + } + + [Fact] + public void Should_return_type_name_for_name() + { + Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); + } + + [Fact] + public Task Should_do_nothing_on_clear() + { + return sut.ClearAsync(); + } + + [Fact] + public async Task Should_update_repositories_on_with_jobs_from_sender() + { + var @event = Envelope.Create(new ContentCreated { AppId = appId }); + + var rule1 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + var rule2 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + var rule3 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + + var job1 = new RuleJob { Created = now }; + var job2 = new RuleJob { Created = now }; + + var ruleEntity1 = A.Fake(); + var ruleEntity2 = A.Fake(); + var ruleEntity3 = A.Fake(); + + A.CallTo(() => ruleEntity1.Rule).Returns(rule1); + A.CallTo(() => ruleEntity2.Rule).Returns(rule2); + A.CallTo(() => ruleEntity3.Rule).Returns(rule3); + + A.CallTo(() => ruleRepository.QueryCachedByAppAsync(appId.Id)) + .Returns(new List { ruleEntity1, ruleEntity2, ruleEntity3 }); + + A.CallTo(() => ruleService.CreateJob(rule1, @event)) + .Returns(job1); + + A.CallTo(() => ruleService.CreateJob(rule2, @event)) + .Returns(job2); + + A.CallTo(() => ruleService.CreateJob(rule3, @event)) + .Returns(null); + + await sut.On(@event); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) + .MustHaveHappened(); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job2, now)) + .MustHaveHappened(); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj b/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj index 48bba6f42..06b97a592 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj @@ -29,10 +29,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookDequeuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookDequeuerTests.cs deleted file mode 100644 index d860bf55a..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookDequeuerTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -// ========================================================================== -// WebhookDequeuerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure.Log; -using Xunit; - -#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public class WebhookDequeuerTests - { - private readonly IClock clock = A.Fake(); - private readonly IWebhookRepository webhookRepository = A.Fake(); - private readonly IWebhookEventRepository webhookEventRepository = A.Fake(); - private readonly WebhookSender webhookSender = A.Fake(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - - public WebhookDequeuerTests() - { - A.CallTo(() => clock.GetCurrentInstant()).Returns(now); - } - - [Fact] - public void Should_update_repositories_on_successful_requests() - { - var @event = CreateEvent(0); - - var requestResult = WebhookResult.Success; - var requestTime = TimeSpan.FromMinutes(1); - var requestDump = "Dump"; - - SetupSender(@event, requestDump, requestResult, requestTime); - SetupPendingEvents(@event); - - var sut = new WebhookDequeuer( - webhookSender, - webhookEventRepository, - webhookRepository, - clock, A.Fake()); - - sut.Next(); - sut.Dispose(); - - VerifyRepositories(@event, requestDump, requestResult, requestTime, null); - } - - [Theory] - [InlineData(0, 5)] - [InlineData(1, 60)] - [InlineData(2, 300)] - [InlineData(3, 360)] - public void Should_set_next_attempt_based_on_num_calls(int calls, int minutes) - { - var @event = CreateEvent(calls); - - var requestResult = WebhookResult.Failed; - var requestTime = TimeSpan.FromMinutes(1); - var requestDump = "Dump"; - - SetupSender(@event, requestDump, requestResult, requestTime); - SetupPendingEvents(@event); - - var sut = new WebhookDequeuer( - webhookSender, - webhookEventRepository, - webhookRepository, - clock, A.Fake()); - - sut.Next(); - sut.Dispose(); - - VerifyRepositories(@event, requestDump, requestResult, requestTime, now.Plus(Duration.FromMinutes(minutes))); - } - - private void SetupSender(IWebhookEventEntity @event, string requestDump, WebhookResult requestResult, TimeSpan requestTime) - { - A.CallTo(() => webhookSender.SendAsync(@event.Job)) - .Returns((requestDump, requestResult, requestTime)); - } - - private void SetupPendingEvents(IWebhookEventEntity @event) - { - A.CallTo(() => webhookEventRepository.QueryPendingAsync(A>.Ignored, A.Ignored)) - .Invokes(async (Func callback, CancellationToken ct) => - { - await callback(@event); - }); - } - - private void VerifyRepositories(IWebhookEventEntity @event, string requestDump, WebhookResult requestResult, TimeSpan requestTime, Instant? nextAttempt) - { - A.CallTo(() => webhookEventRepository.TraceSendingAsync(@event.Id)) - .MustHaveHappened(); - - A.CallTo(() => webhookEventRepository.TraceSendingAsync(@event.Id)) - .MustHaveHappened(); - - A.CallTo(() => webhookEventRepository.TraceSentAsync(@event.Id, requestDump, requestResult, requestTime, nextAttempt)) - .MustHaveHappened(); - - A.CallTo(() => webhookRepository.TraceSentAsync(@event.Job.WebhookId, requestResult, requestTime)) - .MustHaveHappened(); - } - - private static IWebhookEventEntity CreateEvent(int numCalls) - { - var @event = A.Fake(); - - A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); - A.CallTo(() => @event.Job).Returns(new WebhookJob { WebhookId = Guid.NewGuid() }); - A.CallTo(() => @event.NumCalls).Returns(numCalls); - - return @event; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs deleted file mode 100644 index 6caec39ce..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// WebhookEnqueuerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Newtonsoft.Json; -using NodaTime; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Xunit; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public class WebhookEnqueuerTests - { - private readonly IClock clock = A.Fake(); - private readonly IWebhookRepository webhookRepository = A.Fake(); - private readonly IWebhookEventRepository webhookEventRepository = A.Fake(); - private readonly TypeNameRegistry typeNameRegisty = new TypeNameRegistry(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); - private readonly WebhookEnqueuer sut; - - public WebhookEnqueuerTests() - { - A.CallTo(() => clock.GetCurrentInstant()).Returns(now); - - typeNameRegisty.Map(typeof(ContentCreated)); - - sut = new WebhookEnqueuer( - typeNameRegisty, - webhookEventRepository, - webhookRepository, - clock, new JsonSerializer()); - } - - [Fact] - public void Should_return_contents_filter_for_events_filter() - { - Assert.Equal("^content-", sut.EventsFilter); - } - - [Fact] - public void Should_return_type_name_for_name() - { - Assert.Equal(typeof(WebhookEnqueuer).Name, sut.Name); - } - - [Fact] - public Task Should_do_nothing_on_clear() - { - return sut.ClearAsync(); - } - - [Fact] - public async Task Should_update_repositories_on_successful_requests() - { - var @event = Envelope.Create(new ContentCreated { AppId = appId, SchemaId = schemaId }); - - var webhook1 = CreateWebhook(1); - var webhook2 = CreateWebhook(2); - - A.CallTo(() => webhookRepository.QueryCachedByAppAsync(appId.Id)) - .Returns(new List { webhook1, webhook2 }); - - await sut.On(@event); - - A.CallTo(() => webhookEventRepository.EnqueueAsync( - A.That.Matches(webhookJob => - !string.IsNullOrWhiteSpace(webhookJob.RequestSignature) - && !string.IsNullOrWhiteSpace(webhookJob.RequestBody) - && webhookJob.Id != Guid.Empty - && webhookJob.Expires == now.Plus(Duration.FromDays(2)) - && webhookJob.AppId == appId.Id - && webhookJob.EventName == "MySchemaCreatedEvent" - && webhookJob.RequestUrl == webhook1.Url - && webhookJob.WebhookId == webhook1.Id), now)).MustHaveHappened(); - - A.CallTo(() => webhookEventRepository.EnqueueAsync( - A.That.Matches(webhookJob => - !string.IsNullOrWhiteSpace(webhookJob.RequestSignature) - && !string.IsNullOrWhiteSpace(webhookJob.RequestBody) - && webhookJob.Id != Guid.Empty - && webhookJob.Expires == now.Plus(Duration.FromDays(2)) - && webhookJob.AppId == appId.Id - && webhookJob.EventName == "MySchemaCreatedEvent" - && webhookJob.RequestUrl == webhook2.Url - && webhookJob.WebhookId == webhook2.Id), now)).MustHaveHappened(); - } - - private IWebhookEntity CreateWebhook(int offset) - { - var webhook = A.Dummy(); - - var schema = new WebhookSchema - { - SchemaId = schemaId.Id, - SendCreate = true, - SendUpdate = true - }; - - A.CallTo(() => webhook.Id).Returns(Guid.NewGuid()); - A.CallTo(() => webhook.Url).Returns(new Uri($"http://domain{offset}.com")); - A.CallTo(() => webhook.Schemas).Returns(new[] { schema }); - A.CallTo(() => webhook.SharedSecret).Returns($"secret{offset}"); - - return webhook; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs new file mode 100644 index 000000000..fe3487cce --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// WebhookActionTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules.Guards.Actions +{ + public sealed class WebhookActionTests + { + [Fact] + public async Task Should_add_error_if_url_is_null() + { + var action = new WebhookAction { Url = null }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_add_error_if_url_is_relative() + { + var action = new WebhookAction { Url = new Uri("/invalid", UriKind.Relative) }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_url_is_absolute() + { + var action = new WebhookAction { Url = new Uri("https://squidex.io", UriKind.Absolute) }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs new file mode 100644 index 000000000..1699e64d8 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// GuardRuleTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules.Guards +{ + public class GuardRuleTests + { + private readonly Uri validUrl = new Uri("https://squidex.io"); + private readonly Rule rule = new Rule(new ContentChangedTrigger(), new WebhookAction()); + private readonly ISchemaProvider schemas = A.Fake(); + + public GuardRuleTests() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(A.Fake()); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_trigger_null() + { + var command = new CreateRule + { + Trigger = null, + Action = new WebhookAction + { + Url = validUrl + } + }; + + await Assert.ThrowsAsync(() => GuardRule.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_action_null() + { + var command = new CreateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = new List() + }, + Action = null + }; + + await Assert.ThrowsAsync(() => GuardRule.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = new CreateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = new List() + }, + Action = new WebhookAction + { + Url = validUrl + } + }; + + await GuardRule.CanCreate(command, schemas); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() + { + var command = new UpdateRule(); + + await Assert.ThrowsAsync(() => GuardRule.CanUpdate(command, schemas)); + } + + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = new UpdateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = new List() + }, + Action = new WebhookAction + { + Url = validUrl + } + }; + + await GuardRule.CanUpdate(command, schemas); + } + + [Fact] + public void CanEnable_should_throw_exception_if_rule_enabled() + { + var command = new EnableRule(); + + rule.Enable(); + + Assert.Throws(() => GuardRule.CanEnable(command, rule)); + } + + [Fact] + public void CanEnable_should_not_throw_exception_if_rule_disabled() + { + var command = new EnableRule(); + + rule.Disable(); + + GuardRule.CanEnable(command, rule); + } + + [Fact] + public void CanDisable_should_throw_exception_if_rule_disabled() + { + var command = new DisableRule(); + + rule.Disable(); + + Assert.Throws(() => GuardRule.CanDisable(command, rule)); + } + + [Fact] + public void CanDisable_should_not_throw_exception_if_rule_enabled() + { + var command = new DisableRule(); + + rule.Enable(); + + GuardRule.CanDisable(command, rule); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteRule(); + + GuardRule.CanDelete(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs new file mode 100644 index 000000000..87cfdec3d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// ContentChangedTriggerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules.Guards.Triggers +{ + public class ContentChangedTriggerTests + { + private readonly ISchemaProvider schemas = A.Fake(); + + [Fact] + public async Task Should_add_error_if_schemas_ids_are_not_valid() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(Task.FromResult(null)); + + var trigger = new ContentChangedTrigger + { + Schemas = new List + { + new ContentChangedTriggerSchema() + } + }; + + var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_null() + { + var trigger = new ContentChangedTrigger(); + + var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_empty() + { + var trigger = new ContentChangedTrigger + { + Schemas = new List() + }; + + var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_ids_are_valid() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(A.Fake()); + + var trigger = new ContentChangedTrigger + { + Schemas = new List + { + new ContentChangedTriggerSchema() + } + }; + + var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs new file mode 100644 index 000000000..87d38c924 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// RuleCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure.CQRS.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules +{ + public class RuleCommandMiddlewareTests : HandlerTestBase + { + private readonly ISchemaProvider schemas = A.Fake(); + private readonly RuleCommandMiddleware sut; + private readonly RuleDomainObject rule; + private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); + private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; + private readonly Guid ruleId = Guid.NewGuid(); + + public RuleCommandMiddlewareTests() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(A.Fake()); + + rule = new RuleDomainObject(ruleId, -1); + + sut = new RuleCommandMiddleware(Handler, schemas); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + + await TestCreate(rule, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + var context = CreateContextForCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }); + + CreateRule(); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Enable_should_update_domain_object() + { + CreateRule(); + DisableRule(); + + var command = CreateContextForCommand(new EnableRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + [Fact] + public async Task Disable_should_update_domain_object() + { + CreateRule(); + + var command = CreateContextForCommand(new DisableRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateRule(); + + var command = CreateContextForCommand(new DeleteRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + private void DisableRule() + { + rule.Disable(new DisableRule()); + } + + private void CreateRule() + { + rule.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs new file mode 100644 index 000000000..2bc45e1b3 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs @@ -0,0 +1,240 @@ +// ========================================================================== +// RuleDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules +{ + public class RuleDomainObjectTests : HandlerTestBase + { + private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); + private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; + private readonly RuleDomainObject sut; + + public Guid RuleId { get; } = Guid.NewGuid(); + + public RuleDomainObjectTests() + { + sut = new RuleDomainObject(RuleId, 0); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + + Assert.Throws(() => + { + sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Create_should_create_events() + { + var command = new CreateRule { Trigger = ruleTrigger, Action = ruleAction }; + + sut.Create(CreateRuleCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleCreated { Trigger = ruleTrigger, Action = ruleAction }) + ); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Update_should_create_events() + { + var newTrigger = new ContentChangedTrigger + { + Schemas = new List() + }; + + var newAction = new WebhookAction + { + Url = new Uri("https://squidex.io/v2") + }; + + CreateRule(); + + var command = new UpdateRule { Trigger = newTrigger, Action = newAction }; + + sut.Update(CreateRuleCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleUpdated { Trigger = newTrigger, Action = newAction }) + ); + } + + [Fact] + public void Enable_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Enable(CreateRuleCommand(new EnableRule())); + }); + } + + [Fact] + public void Enable_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Enable(CreateRuleCommand(new EnableRule())); + }); + } + + [Fact] + public void Enable_should_create_events() + { + CreateRule(); + + var command = new EnableRule(); + + sut.Enable(CreateRuleCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleEnabled()) + ); + } + + [Fact] + public void Disable_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Disable(CreateRuleCommand(new DisableRule())); + }); + } + + [Fact] + public void Disable_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Disable(CreateRuleCommand(new DisableRule())); + }); + } + + [Fact] + public void Disable_should_create_events() + { + CreateRule(); + + var command = new DisableRule(); + + sut.Disable(CreateRuleCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDisabled()) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + }); + } + + [Fact] + public void Delete_should_update_create_events() + { + CreateRule(); + + sut.Delete(CreateRuleCommand(new DeleteRule())); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDeleted()) + ); + } + + private void CreateRule() + { + sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void DeleteRule() + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + protected T CreateRuleEvent(T @event) where T : RuleEvent + { + @event.RuleId = RuleId; + + return CreateEvent(@event); + } + + protected T CreateRuleCommand(T command) where T : RuleAggregateCommand + { + command.RuleId = RuleId; + + return CreateCommand(command); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs index cf61d7f51..63bb7bdda 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs @@ -18,6 +18,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public class GuardSchemaFieldTests { private readonly Schema schema = new Schema("my-schema"); + private readonly StringFieldProperties validProperties = new StringFieldProperties(); + private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; public GuardSchemaFieldTests() { @@ -210,7 +212,7 @@ namespace Squidex.Domain.Apps.Write.Schemas [Fact] public void CanAdd_should_throw_exception_if_name_not_valid() { - var command = new AddField { Name = "INVALID_NAME", Properties = new StringFieldProperties() }; + var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; Assert.Throws(() => GuardSchemaField.CanAdd(schema, command)); } @@ -218,7 +220,15 @@ namespace Squidex.Domain.Apps.Write.Schemas [Fact] public void CanAdd_should_throw_exception_if_properties_not_valid() { - var command = new AddField { Name = "field3", Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; + var command = new AddField { Name = "field3", Properties = invalidProperties }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_partitioning_not_valid() + { + var command = new AddField { Name = "field3", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; Assert.Throws(() => GuardSchemaField.CanAdd(schema, command)); } diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj b/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj index b049b5e7c..ddbddff8e 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj @@ -21,10 +21,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs deleted file mode 100644 index 567e4a241..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -// ========================================================================== -// GuardWebhookTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Webhooks.Guards -{ - public class GuardWebhookTests - { - private readonly ISchemaProvider schemas = A.Fake(); - - public GuardWebhookTests() - { - A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) - .Returns(A.Fake()); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_url_defined() - { - var command = new CreateWebhook(); - - await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_url_not_valid() - { - var command = new CreateWebhook { Url = new Uri("/invalid", UriKind.Relative) }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_schema_id_not_found() - { - A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) - .Returns(Task.FromResult(null)); - - var command = new CreateWebhook - { - Schemas = new List - { - new WebhookSchema() - }, - Url = new Uri("/invalid", UriKind.Relative) - }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_if_schema_id_found() - { - var command = new CreateWebhook - { - Schemas = new List - { - new WebhookSchema() - }, - Url = new Uri("/invalid", UriKind.Relative) - }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_url_not_defined() - { - var command = new UpdateWebhook(); - - await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_url_not_valid() - { - var command = new UpdateWebhook { Url = new Uri("/invalid", UriKind.Relative) }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_schema_id_not_found() - { - A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) - .Returns(Task.FromResult(null)); - - var command = new UpdateWebhook - { - Schemas = new List - { - new WebhookSchema() - }, - Url = new Uri("/invalid", UriKind.Relative) - }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); - } - - [Fact] - public async Task CanUpdate_should_not_throw_exception_if_schema_id_found() - { - var command = new UpdateWebhook - { - Schemas = new List - { - new WebhookSchema() - }, - Url = new Uri("/invalid", UriKind.Relative) - }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteWebhook(); - - GuardWebhook.CanDelete(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs deleted file mode 100644 index 37e786b7d..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -// ========================================================================== -// WebhookCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookCommandMiddlewareTests : HandlerTestBase - { - private readonly ISchemaProvider schemas = A.Fake(); - private readonly WebhookCommandMiddleware sut; - private readonly WebhookDomainObject webhook; - private readonly Uri url = new Uri("http://squidex.io"); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Guid webhookId = Guid.NewGuid(); - private readonly List webhookSchemas; - - public WebhookCommandMiddlewareTests() - { - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)) - .Returns(A.Fake()); - - webhook = new WebhookDomainObject(webhookId, -1); - - webhookSchemas = new List - { - new WebhookSchema { SchemaId = schemaId } - }; - - sut = new WebhookCommandMiddleware(Handler, schemas); - } - - [Fact] - public async Task Create_should_create_domain_object() - { - var context = CreateContextForCommand(new CreateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - await TestCreate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns(A.Fake()); - - CreateWebhook(); - - await TestUpdate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); - } - - [Fact] - public async Task Update_should_throw_exception_when_schema_is_not_found() - { - var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns((ISchemaEntity)null); - - CreateWebhook(); - - await Assert.ThrowsAsync(async () => - { - await TestCreate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - }); - } - - [Fact] - public async Task Delete_should_update_domain_object() - { - CreateWebhook(); - - var command = CreateContextForCommand(new DeleteWebhook { WebhookId = webhookId }); - - await TestUpdate(webhook, async _ => - { - await sut.HandleAsync(command); - }); - } - - private void CreateWebhook() - { - webhook.Create(new CreateWebhook { Url = url }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs deleted file mode 100644 index 182f00d84..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// ========================================================================== -// WebhookDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events.Webhooks; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookDomainObjectTests : HandlerTestBase - { - private readonly Uri url = new Uri("http://squidex.io"); - private readonly WebhookDomainObject sut; - - public Guid WebhookId { get; } = Guid.NewGuid(); - - public WebhookDomainObjectTests() - { - sut = new WebhookDomainObject(WebhookId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateWebhook { Url = url }); - - Assert.Throws(() => - { - sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); - }); - } - - [Fact] - public void Create_should_create_events() - { - var command = new CreateWebhook { Url = url }; - - sut.Create(CreateWebhookCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookCreated - { - Url = url, - Schemas = command.Schemas, - SharedSecret = command.SharedSecret, - WebhookId = command.WebhookId - }) - ); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_webhook_is_deleted() - { - CreateWebhook(); - DeleteWebhook(); - - Assert.Throws(() => - { - sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); - }); - } - - [Fact] - public void Update_should_create_events() - { - CreateWebhook(); - - var command = new UpdateWebhook { Url = url }; - - sut.Update(CreateWebhookCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookUpdated { Url = url, Schemas = command.Schemas }) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateWebhook(); - DeleteWebhook(); - - Assert.Throws(() => - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - }); - } - - [Fact] - public void Delete_should_update_properties_create_events() - { - CreateWebhook(); - - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookDeleted()) - ); - } - - private void CreateWebhook() - { - sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteWebhook() - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - protected T CreateWebhookEvent(T @event) where T : WebhookEvent - { - @event.WebhookId = WebhookId; - - return CreateEvent(@event); - } - - protected T CreateWebhookCommand(T command) where T : WebhookAggregateCommand - { - command.WebhookId = WebhookId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index f54f04636..41bc3752e 100644 --- a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -21,10 +21,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs index 43a719e3c..1dde168d1 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs @@ -185,6 +185,26 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors .MustHaveHappened(Repeated.Exactly.Once); } + [Fact] + public async Task Should_ignore_old_events() + { + A.CallTo(() => formatter.Parse(eventData, true)) + .Throws(new TypeNameNotFoundException()); + + var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); + + await OnSubscribeAsync(); + await OnEventAsync(eventSubscription, @event); + + sut.Dispose(); + + A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, @event.EventPosition, false, null)) + .MustHaveHappened(Repeated.Exactly.Once); + + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } + [Fact] public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() { diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index feee70e93..1ae104ea4 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -24,10 +24,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs b/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs index f34a342f9..0f65a5191 100644 --- a/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs @@ -54,7 +54,7 @@ namespace Squidex.Infrastructure [Fact] public void Should_register_from_assembly() { - sut.Map(typeof(TypeNameRegistryTests).GetTypeInfo().Assembly); + sut.MapUnmapped(typeof(TypeNameRegistryTests).GetTypeInfo().Assembly); Assert.Equal("my", sut.GetName()); Assert.Equal("my", sut.GetName(typeof(MyType))); @@ -66,7 +66,7 @@ namespace Squidex.Infrastructure [Fact] public void Should_register_event_type_from_assembly() { - sut.Map(typeof(TypeNameRegistryTests).GetTypeInfo().Assembly); + sut.MapUnmapped(typeof(TypeNameRegistryTests).GetTypeInfo().Assembly); Assert.Equal("MyAddedEventV2", sut.GetName()); Assert.Equal("MyAddedEventV2", sut.GetName(typeof(MyAdded)));