mirror of https://github.com/Squidex/squidex.git
40 changed files with 2019 additions and 1502 deletions
@ -0,0 +1,31 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Rules.Actions |
||||
|
{ |
||||
|
[TypeName(nameof(ElasticSearchAction))] |
||||
|
public sealed class ElasticSearchAction : RuleAction |
||||
|
{ |
||||
|
public Uri Host { get; set; } |
||||
|
|
||||
|
public string Username { get; set; } |
||||
|
|
||||
|
public string Password { get; set; } |
||||
|
|
||||
|
public string IndexName { get; set; } |
||||
|
|
||||
|
public string IndexType { get; set; } |
||||
|
|
||||
|
public override T Accept<T>(IRuleActionVisitor<T> visitor) |
||||
|
{ |
||||
|
return visitor.Visit(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,181 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Elasticsearch.Net; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
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 ElasticSearchActionHandler : RuleActionHandler<ElasticSearchAction> |
||||
|
{ |
||||
|
private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients; |
||||
|
private readonly RuleEventFormatter formatter; |
||||
|
|
||||
|
public ElasticSearchActionHandler(RuleEventFormatter formatter) |
||||
|
{ |
||||
|
Guard.NotNull(formatter, nameof(formatter)); |
||||
|
|
||||
|
this.formatter = formatter; |
||||
|
|
||||
|
clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(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 ElasticLowLevelClient(config); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, ElasticSearchAction action) |
||||
|
{ |
||||
|
var ruleDescription = string.Empty; |
||||
|
var ruleData = new RuleJobData |
||||
|
{ |
||||
|
["Host"] = action.Host, |
||||
|
["Username"] = action.Username, |
||||
|
["Password"] = action.Password |
||||
|
}; |
||||
|
|
||||
|
if (@event.Payload is ContentEvent contentEvent) |
||||
|
{ |
||||
|
ruleData["ContentId"] = contentEvent.ContentId.ToString(); |
||||
|
ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event); |
||||
|
ruleData["IndexType"] = formatter.FormatString(action.IndexType, @event); |
||||
|
|
||||
|
var timestamp = @event.Headers.Timestamp().ToString(); |
||||
|
|
||||
|
switch (@event.Payload) |
||||
|
{ |
||||
|
case ContentCreated created: |
||||
|
{ |
||||
|
ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; |
||||
|
|
||||
|
ruleData["Operation"] = "Create"; |
||||
|
ruleData["Content"] = new JObject( |
||||
|
new JProperty("id", contentEvent.ContentId), |
||||
|
new JProperty("created", timestamp), |
||||
|
new JProperty("createdBy", created.Actor.ToString()), |
||||
|
new JProperty("lastModified", timestamp), |
||||
|
new JProperty("lastModifiedBy", created.Actor.ToString()), |
||||
|
new JProperty("status", Status.Draft.ToString()), |
||||
|
new JProperty("data", formatter.ToRouteData(created.Data))); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case ContentUpdated updated: |
||||
|
{ |
||||
|
ruleDescription = $"Update entry in Algolia index: {action.IndexName}"; |
||||
|
|
||||
|
ruleData["Operation"] = "Update"; |
||||
|
ruleData["Content"] = new JObject( |
||||
|
new JProperty("lastModified", timestamp), |
||||
|
new JProperty("lastModifiedBy", updated.Actor.ToString()), |
||||
|
new JProperty("data", formatter.ToRouteData(updated.Data))); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case ContentStatusChanged statusChanged: |
||||
|
{ |
||||
|
ruleDescription = $"Update entry in Algolia index: {action.IndexName}"; |
||||
|
|
||||
|
ruleData["Operation"] = "Update"; |
||||
|
ruleData["Content"] = new JObject( |
||||
|
new JProperty("lastModified", timestamp), |
||||
|
new JProperty("lastModifiedBy", statusChanged.Actor.ToString()), |
||||
|
new JProperty("status", statusChanged.Status.ToString())); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case ContentDeleted deleted: |
||||
|
{ |
||||
|
ruleDescription = $"Delete entry from Index: {action.IndexName}"; |
||||
|
|
||||
|
ruleData["Operation"] = "Delete"; |
||||
|
ruleData["Content"] = new JObject(); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return (ruleDescription, ruleData); |
||||
|
} |
||||
|
|
||||
|
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) |
||||
|
{ |
||||
|
if (!job.TryGetValue("Operation", out var operationToken)) |
||||
|
{ |
||||
|
return (null, new InvalidOperationException("The action cannot handle this event.")); |
||||
|
} |
||||
|
|
||||
|
var host = new Uri(job["Host"].Value<string>(), UriKind.Absolute); |
||||
|
|
||||
|
var username = job["Username"].Value<string>(); |
||||
|
var password = job["Password"].Value<string>(); |
||||
|
|
||||
|
var client = clients.GetClient((host, username, password)); |
||||
|
|
||||
|
var indexName = job["IndexName"].Value<string>(); |
||||
|
var indexType = job["IndexType"].Value<string>(); |
||||
|
|
||||
|
var operation = operationToken.Value<string>(); |
||||
|
var content = job["Content"].Value<JObject>(); |
||||
|
var contentId = job["ContentId"].Value<string>(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
switch (operation) |
||||
|
{ |
||||
|
case "Create": |
||||
|
{ |
||||
|
var doc = content.ToString(); |
||||
|
|
||||
|
var response = await client.IndexAsync<StringResponse>(indexName, indexType, contentId, doc); |
||||
|
|
||||
|
return (response.Body, response.OriginalException); |
||||
|
} |
||||
|
|
||||
|
case "Update": |
||||
|
{ |
||||
|
var doc = new JObject(new JProperty("doc", content)).ToString(); |
||||
|
|
||||
|
var response = await client.UpdateAsync<StringResponse>(indexName, indexType, contentId, doc); |
||||
|
|
||||
|
return (response.Body, response.OriginalException); |
||||
|
} |
||||
|
|
||||
|
case "Delete": |
||||
|
{ |
||||
|
var response = await client.DeleteAsync<StringResponse>(indexName, indexType, contentId); |
||||
|
|
||||
|
return (response.Body, response.OriginalException); |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
return (null, null); |
||||
|
} |
||||
|
} |
||||
|
catch (ElasticsearchClientException ex) |
||||
|
{ |
||||
|
return (ex.Message, ex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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("ElasticSearch")] |
||||
|
public sealed class ElasticSearchActionDto : RuleActionDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The host to the elastic search instance.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public Uri Host { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The name of the index.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string IndexName { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The name of the index type.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string IndexType { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The optional username for authentication.
|
||||
|
/// </summary>
|
||||
|
public string Username { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The optional password for authentication.
|
||||
|
/// </summary>
|
||||
|
public string Password { get; set; } |
||||
|
|
||||
|
public override RuleAction ToAction() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new ElasticSearchAction()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
<h3 class="rule-title">Populate index in ElasticSearch with content</h3> |
||||
|
|
||||
|
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()"> |
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="host">Host</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="host" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="host" formControlName="host" placeholder="http://localhost:9200" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The url to your elastic search instance. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="username">Username</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="username" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="username" formControlName="username" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The username for authentication. Highly recommended. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="username">Password</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="password" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="password" formControlName="password" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The password for authentication. Highly recommended. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="sharedSecret">Index Name</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="indexName" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="indexName" formControlName="indexName" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The name of the index. You can use advanced formatting (read help section). |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-3 col-form-label" for="sharedSecret">Type Name</label> |
||||
|
|
||||
|
<div class="col col-9"> |
||||
|
<sqx-control-errors for="indexType" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="indexType" formControlName="indexType" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The name of the type. You can use advanced formatting (read help section). |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
@ -0,0 +1,70 @@ |
|||||
|
/* |
||||
|
* 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-elastic-search-action', |
||||
|
styleUrls: ['./elastic-search-action.component.scss'], |
||||
|
templateUrl: './elastic-search-action.component.html' |
||||
|
}) |
||||
|
export class ElasticSearchActionComponent implements OnInit { |
||||
|
@Input() |
||||
|
public action: any; |
||||
|
|
||||
|
@Output() |
||||
|
public actionChanged = new EventEmitter<object>(); |
||||
|
|
||||
|
public actionFormSubmitted = false; |
||||
|
public actionForm = |
||||
|
this.formBuilder.group({ |
||||
|
host: ['', |
||||
|
[ |
||||
|
Validators.required |
||||
|
]], |
||||
|
indexName: ['$APP_NAME', |
||||
|
[ |
||||
|
Validators.required |
||||
|
]], |
||||
|
indexType: ['$SCHEMA_NAME', |
||||
|
[ |
||||
|
// Validators.required
|
||||
|
]], |
||||
|
username: '', |
||||
|
password: '' |
||||
|
}); |
||||
|
|
||||
|
constructor( |
||||
|
private readonly formBuilder: FormBuilder |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.action = Object.assign({}, { |
||||
|
host: '', |
||||
|
indexName: '$APP_NAME', |
||||
|
indexType: '$SCHEMA_NAME', |
||||
|
username: '', |
||||
|
password: '' |
||||
|
}, 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Loading…
Reference in new issue