// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using OpenSearch.Net; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Flows; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; namespace Squidex.Extensions.Actions.OpenSearch; [FlowStep( Title = "OpenSearch", IconImage = "", IconColor = "#005EB8", Display = "Populate OpenSearch index", Description = "Populate a full text search index in OpenSearch.", ReadMore = "https://opensearch.org/")] #pragma warning disable CS0618 // Type or member is obsolete public sealed record OpenSearchFlowStep : FlowStep, IConvertibleToAction #pragma warning restore CS0618 // Type or member is obsolete { [AbsoluteUrl] [LocalizedRequired] [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] [Editor(FlowStepEditor.Url)] public Uri Host { get; set; } [LocalizedRequired] [Display(Name = "Index Name", Description = "The name of the index.")] [Editor(FlowStepEditor.Text)] [Expression] public string IndexName { get; set; } [Display(Name = "Username", Description = "The optional username.")] [Editor(FlowStepEditor.Text)] public string? Username { get; set; } [Display(Name = "Password", Description = "The optional password.")] [Editor(FlowStepEditor.Text)] public string? Password { get; set; } [Display(Name = "Document", Description = "The optional custom document.")] [Editor(FlowStepEditor.TextArea)] [Expression(ExpressionFallback.Event)] public string? Document { get; set; } [Display(Name = "Deletion", Description = "The condition when to delete the document.")] [Editor(FlowStepEditor.Text)] public string? Delete { get; set; } private static readonly ClientPool<(Uri Host, string? Username, string? Password), OpenSearchLowLevelClient> Clients = new (key => { var config = new ConnectionConfiguration(key.Host); if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) { config = config.BasicAuthentication(key.Username, key.Password); } return new OpenSearchLowLevelClient(config); }); public override ValueTask PrepareAsync(FlowExecutionContext executionContext, CancellationToken ct) { var @event = ((FlowEventContext)executionContext.Context).Event; if (@event.ShouldDelete(executionContext, Delete)) { Document = null; return default; } OpenSearchContent content; try { content = executionContext.DeserializeJson(Document!); } catch (Exception ex) { content = new OpenSearchContent { More = new Dictionary { ["error"] = $"Invalid JSON: {ex.Message}", }, }; } Document = executionContext.SerializeJson(content); return default; } public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, CancellationToken ct) { var @event = ((FlowEventContext)executionContext.Context).Event; var (id, isGenerated) = @event.GetOrCreateId(); if (isGenerated && Document == null) { executionContext.LogSkipped("Can only delete content for static identities."); return Next(); } if (executionContext.IsSimulation) { executionContext.LogSkipSimulation(); return Next(); } try { void HandleResult(StringResponse response, string message) { if (response.OriginalException != null) { executionContext.Log("Failed with error", response.OriginalException.Message); throw response.OriginalException; } executionContext.Log(message, response.Body); } var client = await Clients.GetClientAsync((Host, Username, Password)); if (Document != null) { var response = await client.IndexAsync(IndexName, id, Document, ctx: ct); HandleResult(response, $"Document with ID '{id}' upserted"); } else { var response = await client.DeleteAsync(IndexName, id, ctx: ct); HandleResult(response, $"Document with ID '{id}' deleted"); } return Next(); } catch (OpenSearchClientException ex) { executionContext.Log("Failed with error", ex.Message); throw; } } #pragma warning disable CS0618 // Type or member is obsolete public RuleAction ToAction() { return SimpleMapper.Map(this, new OpenSearchAction()); } #pragma warning restore CS0618 // Type or member is obsolete private sealed class OpenSearchContent { public string ContentId { get; set; } [JsonExtensionData] public Dictionary More { get; set; } = []; } }