From 96d1dad46eb474b00cefb25ebb21299fa9fcc9e5 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 2 Feb 2018 21:29:46 +0100 Subject: [PATCH] Fastly action. --- .../Rules/Actions/FastlyAction.cs | 24 +++++ .../Rules/IRuleActionVisitor.cs | 2 + .../Actions/AlgoliaActionHandler.cs | 4 +- .../Actions/FastlyActionHandler.cs | 92 +++++++++++++++++++ .../Rules/Guards/RuleActionValidator.cs | 19 +++- .../Rules/Models/Actions/FastlyActionDto.cs | 36 ++++++++ .../Models/Converters/RuleActionDtoFactory.cs | 5 + .../Controllers/Rules/Models/RuleActionDto.cs | 1 + src/Squidex/Config/Domain/ReadServices.cs | 3 + .../app/features/rules/declarations.ts | 1 + src/Squidex/app/features/rules/module.ts | 2 + .../actions/algolia-action.component.html | 2 +- .../actions/azure-queue-action.component.html | 4 +- .../actions/fastly-action.component.html | 31 +++++++ .../actions/fastly-action.component.scss | 2 + .../rules/actions/fastly-action.component.ts | 58 ++++++++++++ .../rules/actions/slack-action.component.html | 2 +- .../actions/webhook-action.component.html | 2 +- .../pages/rules/rule-wizard.component.html | 6 ++ .../pages/rules/rule-wizard.component.scss | 2 +- .../asset-changed-trigger.component.html | 2 +- .../content-changed-trigger.component.html | 2 +- .../app/shared/services/rules.service.ts | 1 + src/Squidex/app/theme/_rules.scss | 9 ++ .../Rules/Guards/Actions/FastlyActionTests.cs | 46 ++++++++++ 25 files changed, 347 insertions(+), 11 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs create mode 100644 src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/FastlyActionDto.cs create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.scss create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/FastlyActionTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs new file mode 100644 index 000000000..2d459d500 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules.Actions +{ + [TypeName(nameof(FastlyAction))] + public sealed class FastlyAction : RuleAction + { + public string ApiKey { get; set; } + + public string ServiceId { get; set; } + + public override T Accept(IRuleActionVisitor visitor) + { + 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 index b90f8b872..00fa80744 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs @@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Rules T Visit(AzureQueueAction action); + T Visit(FastlyAction action); + T Visit(SlackAction action); T Visit(WebhookAction action); diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs index 3a0114be4..fe9c8f7bd 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs @@ -108,7 +108,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) { - if (job["Operation"] == null) + if (!job.TryGetValue("Operation", out var operationToken)) { return (null, new InvalidOperationException("The action cannot handle this event.")); } @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions var index = clients.GetClient((appId, apiKey, indexName)); - var operation = job["Operation"].Value(); + var operation = operationToken.Value(); var content = job["Content"].Value(); var contentId = job["ContentId"].Value(); diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs new file mode 100644 index 000000000..bc3e716b3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.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.EventSourcing; +using Squidex.Infrastructure.Http; + +namespace Squidex.Domain.Apps.Core.HandleRules.Actions +{ + public sealed class FastlyActionHandler : RuleActionHandler + { + protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, FastlyAction action) + { + var ruleDescription = "Purge key in fastly"; + var ruleData = new RuleJobData + { + ["FastlyApiKey"] = action.ApiKey, + ["FastlyServiceID"] = action.ServiceId + }; + + if (@event.Headers.Contains(CommonHeaders.AggregateId)) + { + ruleData["Key"] = @event.Headers.AggregateId().ToString(); + } + + return (ruleDescription, ruleData); + } + + public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + { + if (!job.TryGetValue("Key", out var keyToken)) + { + return (null, new InvalidOperationException("The action cannot handle this event.")); + } + + var requestMsg = BuildRequest(job, keyToken.Value()); + + HttpResponseMessage response = null; + + try + { + response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg); + + var responseString = await response.Content.ReadAsStringAsync(); + var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, responseString, TimeSpan.Zero, false); + + return (requestDump, null); + } + catch (Exception ex) + { + if (requestMsg != null) + { + var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, ex.ToString(), TimeSpan.Zero, false); + + return (requestDump, ex); + } + else + { + var requestDump = ex.ToString(); + + return (requestDump, ex); + } + } + } + + private static HttpRequestMessage BuildRequest(Dictionary job, string key) + { + var serviceId = job["FastlyServiceID"].Value(); + + var requestUrl = $"https://api.fastly.com/service/{serviceId}/purge/{key}"; + var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); + + request.Headers.Add("Fastly-Key", job["FastlyApiKey"].Value()); + + return request; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs index 7cb300d54..2d36a13c5 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (string.IsNullOrWhiteSpace(action.IndexName)) { - errors.Add(new ValidationError("Index name key must be defined.", nameof(action.ApiKey))); + errors.Add(new ValidationError("Index name must be defined.", nameof(action.ApiKey))); } return Task.FromResult>(errors); @@ -68,6 +68,23 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards return Task.FromResult>(errors); } + public Task> Visit(FastlyAction action) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(action.ApiKey)) + { + errors.Add(new ValidationError("Api key must be defined.", nameof(action.ApiKey))); + } + + if (string.IsNullOrWhiteSpace(action.ServiceId)) + { + errors.Add(new ValidationError("Service name must be defined.", nameof(action.ServiceId))); + } + + return Task.FromResult>(errors); + } + public Task> Visit(SlackAction action) { var errors = new List(); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/FastlyActionDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/FastlyActionDto.cs new file mode 100644 index 000000000..c9431f02e --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/FastlyActionDto.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using NJsonSchema.Annotations; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Rules.Models.Actions +{ + [JsonSchema("Fastly")] + public sealed class FastlyActionDto : RuleActionDto + { + /// + /// The ID of the fastly service. + /// + [Required] + public string ServiceId { get; set; } + + /// + /// The API key to grant access to Squidex. + /// + [Required] + public string ApiKey { get; set; } + + public override RuleAction ToAction() + { + return SimpleMapper.Map(this, new FastlyAction()); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs index 6fee353e9..705bf8935 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs @@ -35,6 +35,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters return SimpleMapper.Map(action, new AzureQueueActionDto()); } + public RuleActionDto Visit(FastlyAction action) + { + return SimpleMapper.Map(action, new FastlyActionDto()); + } + public RuleActionDto Visit(SlackAction action) { return SimpleMapper.Map(action, new SlackActionDto()); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs index 7a6643d18..c75fb39dd 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs @@ -15,6 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models [JsonConverter(typeof(JsonInheritanceConverter), "actionType")] [KnownType(typeof(AlgoliaActionDto))] [KnownType(typeof(AzureQueueActionDto))] + [KnownType(typeof(FastlyActionDto))] [KnownType(typeof(SlackActionDto))] [KnownType(typeof(WebhookActionDto))] public abstract class RuleActionDto diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index e51951702..cd3961628 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -104,6 +104,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts index 56bb26dbf..9aac2dbdb 100644 --- a/src/Squidex/app/features/rules/declarations.ts +++ b/src/Squidex/app/features/rules/declarations.ts @@ -7,6 +7,7 @@ export * from './pages/rules/actions/algolia-action.component'; export * from './pages/rules/actions/azure-queue-action.component'; +export * from './pages/rules/actions/fastly-action.component'; export * from './pages/rules/actions/slack-action.component'; export * from './pages/rules/actions/webhook-action.component'; export * from './pages/rules/triggers/asset-changed-trigger.component'; diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index a386b55c2..c112a8bd4 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -19,6 +19,7 @@ import { AssetChangedTriggerComponent, AzureQueueActionComponent, ContentChangedTriggerComponent, + FastlyActionComponent, RuleEventsPageComponent, RulesPageComponent, RuleWizardComponent, @@ -57,6 +58,7 @@ const routes: Routes = [ AssetChangedTriggerComponent, AzureQueueActionComponent, ContentChangedTriggerComponent, + FastlyActionComponent, RuleEventsPageComponent, RulesPageComponent, RuleWizardComponent, diff --git a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html index 1920dfe1e..2bb1b6d6c 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html +++ b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html @@ -1,4 +1,4 @@ -

Populate index in algolia with content

+

Populate index in algolia with content

diff --git a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html index 3d32c9e46..c8f7a621c 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html +++ b/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html @@ -1,8 +1,8 @@ -

Send event payload to Azure Storage Queue

+

Send event payload to Azure Storage Queue

- +
diff --git a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html new file mode 100644 index 000000000..4f72f62c9 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html @@ -0,0 +1,31 @@ +

Purge cache entries in Fastly

+ + +
+ + +
+ + + + + + The service ID of the fastly account. + +
+
+ +
+ + +
+ + + + + + The API key for the fastly account. + +
+
+ \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/fastly-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/fastly-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts new file mode 100644 index 000000000..e0502a61b --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts @@ -0,0 +1,58 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; + +@Component({ + selector: 'sqx-fastly-action', + styleUrls: ['./fastly-action.component.scss'], + templateUrl: './fastly-action.component.html' +}) +export class FastlyActionComponent implements OnInit { + @Input() + public action: any; + + @Output() + public actionChanged = new EventEmitter(); + + public actionFormSubmitted = false; + public actionForm = + this.formBuilder.group({ + serviceId: ['', + [ + Validators.required + ]], + apiKey: ['', + [ + Validators.required + ]] + }); + + constructor( + private readonly formBuilder: FormBuilder + ) { + } + + public ngOnInit() { + this.action = Object.assign({}, { serviceId: '', apiKey: '' }, 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/actions/slack-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html index 015b00ba5..3ab3729ca 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html +++ b/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html @@ -1,4 +1,4 @@ -

Send custom text to an incoming webhook in Slack

+

Send custom text to an incoming webhook in Slack

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 index f09e81529..ef9503dea 100644 --- 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 @@ -1,4 +1,4 @@ -

Send event payload to webhook

+

Send event payload to webhook

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 index 1ea0805f2..b09792026 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -80,6 +80,12 @@ (actionChanged)="selectAction($event)">
+
+ + +
Trigger rule when asset has been... +

Trigger rule when asset has been...

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 index 534b780da..8080fde1d 100644 --- 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 @@ -1,4 +1,4 @@ -

Trigger rule when the events for the following schemas happen

+

Trigger rule when an events for a schemas happens

diff --git a/src/Squidex/app/shared/services/rules.service.ts b/src/Squidex/app/shared/services/rules.service.ts index 4f7a41a4e..150b4ac66 100644 --- a/src/Squidex/app/shared/services/rules.service.ts +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -28,6 +28,7 @@ export const ruleTriggers: any = { export const ruleActions: any = { 'Algolia': 'Populate Algolia Index', 'AzureQueue': 'Send to Azure Queue', + 'Fastly': 'Purge fastly Cache', 'Slack': 'Send to Slack', 'Webhook': 'Send Webhook' }; diff --git a/src/Squidex/app/theme/_rules.scss b/src/Squidex/app/theme/_rules.scss index 5a0e5a6c8..c9cf20d62 100644 --- a/src/Squidex/app/theme/_rules.scss +++ b/src/Squidex/app/theme/_rules.scss @@ -8,6 +8,7 @@ $action-webhook: #4bb958; $action-algolia: #0d9bf9; $action-slack: #5c3a58; $action-azure: #55b3ff; +$action-fastly: #e23335; @mixin build-element($color) { & { @@ -23,6 +24,10 @@ $action-azure: #55b3ff; } } +.rule-title { + margin-bottom: 1rem; +} + .rule-element { & { @include truncate; @@ -65,6 +70,10 @@ $action-azure: #55b3ff; @include build-element($action-algolia); } +.rule-element-Fastly { + @include build-element($action-fastly); +} + .rule-element-Slack { @include build-element($action-slack); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/FastlyActionTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/FastlyActionTests.cs new file mode 100644 index 000000000..1e614edac --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/FastlyActionTests.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions +{ + public sealed class FastlyActionTests + { + [Fact] + public async Task Should_add_error_if_service_id_not_defined() + { + var action = new FastlyAction { ServiceId = null, ApiKey = "KEY" }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_add_error_if_api_key_not_defined() + { + var action = new FastlyAction { ServiceId = "APP", ApiKey = null }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_not_add_error_everything_defined() + { + var action = new FastlyAction { ServiceId = "APP", ApiKey = "KEY" }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.Empty(errors); + } + } +}