diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings index 235489b1a..3698200b1 100644 --- a/Squidex.sln.DotSettings +++ b/Squidex.sln.DotSettings @@ -36,12 +36,12 @@ <?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile> + False ========================================================================== - $FILENAME$ Squidex Headless CMS ========================================================================== - Copyright (c) Squidex Group - All rights reserved. + Copyright (c) Squidex UG (haftungsbeschraenkt) + All rights reserved. Licensed under the MIT license. ========================================================================== \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs new file mode 100644 index 000000000..e8040e57d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// 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(AlgoliaAction))] + public sealed class AlgoliaAction : RuleAction + { + private string appId; + private string apiKey; + private string indexName; + + public string AppId + { + get + { + return appId; + } + set + { + ThrowIfFrozen(); + + appId = value; + } + } + + public string ApiKey + { + get + { + return apiKey; + } + set + { + ThrowIfFrozen(); + + apiKey = value; + } + } + + public string IndexName + { + get + { + return indexName; + } + set + { + ThrowIfFrozen(); + + indexName = value; + } + } + + 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 41f68b54e..44651d1fe 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs @@ -11,6 +11,8 @@ namespace Squidex.Domain.Apps.Core.Rules { public interface IRuleActionVisitor { + T Visit(AlgoliaAction 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 new file mode 100644 index 000000000..2d19a7278 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs @@ -0,0 +1,141 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Algolia.Search; +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.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Core.HandleRules.Actions +{ + public sealed class AlgoliaActionHandler : RuleActionHandler + { + private readonly ConcurrentDictionary<(string AppId, string ApiKey, string IndexName), Index> clients = new ConcurrentDictionary<(string AppId, string ApiKey, string IndexName), Index>(); + private readonly JsonSerializer serializer; + + public AlgoliaActionHandler(JsonSerializer serializer) + { + Guard.NotNull(serializer, nameof(serializer)); + + this.serializer = serializer; + } + + protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, AlgoliaAction action) + { + var ruleDescription = string.Empty; + var ruleData = new RuleJobData + { + ["AppId"] = action.AppId, + ["ApiKey"] = action.ApiKey, + ["IndexName"] = action.IndexName + }; + + if (@event.Payload is ContentEvent contentEvent) + { + ruleData["ContentId"] = contentEvent.ContentId.ToString(); + ruleData["Operation"] = "Upsert"; + + switch (@event.Payload) + { + case ContentCreated created: + { + ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; + ruleData["Content"] = new JObject( + new JProperty("data", JObject.FromObject(created.Data, serializer))); + break; + } + + case ContentUpdated updated: + { + ruleDescription = $"Update entry in Algolia index: {action.IndexName}"; + ruleData["Content"] = new JObject( + new JProperty("data", JObject.FromObject(updated.Data, serializer))); + break; + } + + case ContentStatusChanged statusChanged: + { + ruleDescription = $"Update entry in Algolia index: {action.IndexName}"; + ruleData["Content"] = new JObject( + new JProperty("status", statusChanged.Status.ToString())); + break; + } + + case ContentDeleted deleted: + { + ruleDescription = $"Delete entry from Index: {action.IndexName}"; + ruleData["Content"] = new JObject(); + break; + } + } + } + + return (ruleDescription, ruleData); + } + + public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + { + var appId = (string)job["AppId"]; + var apiKey = (string)job["ApiKey"]; + var indexName = (string)job["IndexName"]; + + var index = clients.GetOrAdd((appId, apiKey, indexName), s => + { + var client = new AlgoliaClient(appId, apiKey); + + return client.InitIndex(indexName); + }); + + var operation = (string)job["Operation"]; + var content = (JObject)job["Content"]; + var contentId = (string)job["ContentId"]; + + try + { + switch (operation) + { + case "Upsert": + { + content["objectID"] = contentId; + + var resonse = await index.PartialUpdateObjectAsync(content); + + return (resonse.ToString(Formatting.Indented), null); + } + + case "Delete": + { + var resonse = await index.DeleteObjectAsync(contentId); + + return (resonse.ToString(Formatting.Indented), null); + } + + default: + { + return ("Nothing to do!", null); + } + } + } + catch (AlgoliaException ex) + { + return (ex.Message, ex); + } + catch (Exception ex) + { + return (null, ex); + } + } + } +} 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 1932d0681..631f0b094 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 @@ -13,6 +13,7 @@ + diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs index a871182b2..2e786f76c 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs @@ -24,6 +24,28 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards return action.Accept(visitor); } + public Task> Visit(AlgoliaAction 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.AppId)) + { + errors.Add(new ValidationError("Application ID key must be defined.", nameof(action.AppId))); + } + + if (string.IsNullOrWhiteSpace(action.IndexName)) + { + errors.Add(new ValidationError("Index name key must be defined.", nameof(action.ApiKey))); + } + + return Task.FromResult>(errors); + } + public Task> Visit(WebhookAction action) { var errors = new List(); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/AlgoliaActionDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/AlgoliaActionDto.cs new file mode 100644 index 000000000..38c9b1d67 --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/AlgoliaActionDto.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +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.Areas.Api.Controllers.Rules.Models.Actions +{ + [JsonSchema("Algolia")] + public sealed class AlgoliaActionDto : RuleActionDto + { + /// + /// The application ID. + /// + [Required] + public string AppId { get; set; } + + /// + /// The API key to grant access to Squidex. + /// + [Required] + public string ApiKey { get; set; } + + /// + /// The name of the index. + /// + [Required] + public string IndexName { get; set; } + + public override RuleAction ToAction() + { + return SimpleMapper.Map(this, new AlgoliaAction()); + } + } +} 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 264298fee..22b85de66 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs @@ -25,6 +25,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters return properties.Accept(Instance); } + public RuleActionDto Visit(AlgoliaAction action) + { + return SimpleMapper.Map(action, new AlgoliaActionDto()); + } + public RuleActionDto Visit(WebhookAction action) { return SimpleMapper.Map(action, new WebhookActionDto()); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs index 629781317..6e8591440 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs @@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Areas.Api.Controllers.Rules.Models { [JsonConverter(typeof(JsonInheritanceConverter), "actionType")] + [KnownType(typeof(AlgoliaActionDto))] [KnownType(typeof(WebhookActionDto))] public abstract class RuleActionDto { diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index e1a4da864..ac5cd1df0 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -92,6 +92,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 422b68ef3..367d22302 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -48,6 +48,7 @@ + diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts index f955e0400..938940695 100644 --- a/src/Squidex/app/features/rules/declarations.ts +++ b/src/Squidex/app/features/rules/declarations.ts @@ -5,6 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +export * from './pages/rules/actions/algolia-action.component'; export * from './pages/rules/actions/webhook-action.component'; export * from './pages/rules/triggers/content-changed-trigger.component'; export * from './pages/rules/rule-wizard.component'; diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index 856ee69ed..7fe03a653 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -14,6 +14,7 @@ import { } from 'shared'; import { + AlgoliaActionComponent, ContentChangedTriggerComponent, RuleEventsPageComponent, RulesPageComponent, @@ -41,6 +42,7 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ + AlgoliaActionComponent, ContentChangedTriggerComponent, RuleEventsPageComponent, RulesPageComponent, 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 new file mode 100644 index 000000000..204eeadf5 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html @@ -0,0 +1,43 @@ +
+
+ + +
+ + + + + + The ID to you algolia application. + +
+
+ +
+ + +
+ + + + + + The API Key to access you algolia app. + +
+
+ +
+ + +
+ + + + + + The name of the index. Use $SCHEMA_NAME as a placeholder + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/algolia-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/algolia-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts new file mode 100644 index 000000000..40992ae89 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts @@ -0,0 +1,62 @@ +/* + * 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-algolia-action', + styleUrls: ['./algolia-action.component.scss'], + templateUrl: './algolia-action.component.html' +}) +export class AlgoliaActionComponent implements OnInit { + @Input() + public action: any; + + @Output() + public actionChanged = new EventEmitter(); + + public actionFormSubmitted = false; + public actionForm = + this.formBuilder.group({ + appId: ['', + [ + Validators.required + ]], + apiKey: ['', + [ + Validators.required + ]], + indexName: ['$SCHEMA_NAME', + [ + Validators.required + ]] + }); + + constructor( + private readonly formBuilder: FormBuilder + ) { + } + + public ngOnInit() { + this.action = Object.assign({}, { appId: '', apiKey: '', indexName: '$SCHEMA_NAME' }, 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 index 88bc0b3a7..0472353d0 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 @@ -62,6 +62,12 @@