mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
32 changed files with 1147 additions and 176 deletions
@ -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<T>(IRuleActionVisitor<T> visitor) |
||||
|
{ |
||||
|
return visitor.Visit(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,156 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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<AlgoliaAction> |
||||
|
{ |
||||
|
private const string SchemaNamePlaceholder = "$SCHEMA_NAME"; |
||||
|
private readonly ConcurrentDictionary<(string AppId, string ApiKey, string IndexName), Index> clients = new ConcurrentDictionary<(string AppId, string ApiKey, string IndexName), Index>(); |
||||
|
private readonly RuleEventFormatter formatter; |
||||
|
|
||||
|
public AlgoliaActionHandler(RuleEventFormatter formatter) |
||||
|
{ |
||||
|
Guard.NotNull(formatter, nameof(formatter)); |
||||
|
|
||||
|
this.formatter = formatter; |
||||
|
} |
||||
|
|
||||
|
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, AlgoliaAction action) |
||||
|
{ |
||||
|
var ruleDescription = string.Empty; |
||||
|
var ruleData = new RuleJobData |
||||
|
{ |
||||
|
["AppId"] = action.AppId, |
||||
|
["ApiKey"] = action.ApiKey |
||||
|
}; |
||||
|
|
||||
|
if (@event.Payload is ContentEvent contentEvent) |
||||
|
{ |
||||
|
ruleData["ContentId"] = contentEvent.ContentId.ToString(); |
||||
|
ruleData["Operation"] = "Upsert"; |
||||
|
ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event); |
||||
|
|
||||
|
var timestamp = @event.Headers.Timestamp().ToString(); |
||||
|
|
||||
|
switch (@event.Payload) |
||||
|
{ |
||||
|
case ContentCreated created: |
||||
|
{ |
||||
|
/* |
||||
|
* Do not add the status property here. Sometimes the published event is faster and therefore |
||||
|
* a content item would never become published. We have to find a way to improve the scheduling |
||||
|
* of rules first before we can fix it. |
||||
|
*/ |
||||
|
ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; |
||||
|
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("data", formatter.ToRouteData(created.Data))); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case ContentUpdated updated: |
||||
|
{ |
||||
|
ruleDescription = $"Update entry in Algolia index: {action.IndexName}"; |
||||
|
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["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 = job["AppId"].Value<string>(); |
||||
|
var apiKey = job["ApiKey"].Value<string>(); |
||||
|
var indexName = job["IndexName"].Value<string>(); |
||||
|
|
||||
|
var index = clients.GetOrAdd((appId, apiKey, indexName), s => |
||||
|
{ |
||||
|
var client = new AlgoliaClient(appId, apiKey); |
||||
|
|
||||
|
return client.InitIndex(indexName); |
||||
|
}); |
||||
|
|
||||
|
var operation = job["Operation"].Value<string>(); |
||||
|
var content = job["Content"].Value<JObject>(); |
||||
|
var contentId = job["ContentId"].Value<string>(); |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,137 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// =========================================-=================================
|
||||
|
|
||||
|
using System.Globalization; |
||||
|
using System.Text; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules |
||||
|
{ |
||||
|
public class RuleEventFormatter |
||||
|
{ |
||||
|
private const string Undefined = "UNDEFINED"; |
||||
|
private const string AppIdPlaceholder = "$APP_ID"; |
||||
|
private const string AppNamePlaceholder = "$APP_NAME"; |
||||
|
private const string SchemaIdPlaceholder = "$SCHEMA_ID"; |
||||
|
private const string SchemaNamePlaceholder = "$SCHEMA_NAME"; |
||||
|
private const string TimestampDatePlaceholder = "$TIMESTAMP_DATE"; |
||||
|
private const string TimestampDateTimePlaceholder = "$TIMESTAMP_DATETIME"; |
||||
|
private static readonly Regex ContentDataPlaceholder = new Regex(@"\$CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); |
||||
|
private readonly JsonSerializer serializer; |
||||
|
|
||||
|
public RuleEventFormatter(JsonSerializer serializer) |
||||
|
{ |
||||
|
Guard.NotNull(serializer, nameof(serializer)); |
||||
|
|
||||
|
this.serializer = serializer; |
||||
|
} |
||||
|
|
||||
|
public virtual JToken ToRouteData(object value) |
||||
|
{ |
||||
|
return JToken.FromObject(value, serializer); |
||||
|
} |
||||
|
|
||||
|
public virtual string FormatString(string text, Envelope<AppEvent> @event) |
||||
|
{ |
||||
|
var sb = new StringBuilder(text); |
||||
|
|
||||
|
if (@event.Headers.Contains(CommonHeaders.Timestamp)) |
||||
|
{ |
||||
|
var timestamp = @event.Headers.Timestamp().ToDateTimeUtc(); |
||||
|
|
||||
|
sb.Replace(TimestampDateTimePlaceholder, timestamp.ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture)); |
||||
|
sb.Replace(TimestampDatePlaceholder, timestamp.ToString("yyy-MM-dd", CultureInfo.InvariantCulture)); |
||||
|
} |
||||
|
|
||||
|
if (@event.Payload.AppId != null) |
||||
|
{ |
||||
|
sb.Replace(AppIdPlaceholder, @event.Payload.AppId.Id.ToString()); |
||||
|
sb.Replace(AppNamePlaceholder, @event.Payload.AppId.Name); |
||||
|
} |
||||
|
|
||||
|
if (@event.Payload is SchemaEvent schemaEvent && schemaEvent.SchemaId != null) |
||||
|
{ |
||||
|
sb.Replace(SchemaIdPlaceholder, schemaEvent.SchemaId.Id.ToString()); |
||||
|
sb.Replace(SchemaNamePlaceholder, schemaEvent.SchemaId.Name); |
||||
|
} |
||||
|
|
||||
|
var result = sb.ToString(); |
||||
|
|
||||
|
if (@event.Payload is ContentCreated contentCreated && contentCreated.Data != null) |
||||
|
{ |
||||
|
result = ReplaceData(contentCreated.Data, result); |
||||
|
} |
||||
|
|
||||
|
if (@event.Payload is ContentUpdated contentUpdated && contentUpdated.Data != null) |
||||
|
{ |
||||
|
result = ReplaceData(contentUpdated.Data, result); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private static string ReplaceData(NamedContentData data, string text) |
||||
|
{ |
||||
|
return ContentDataPlaceholder.Replace(text, match => |
||||
|
{ |
||||
|
var captures = match.Groups[2].Captures; |
||||
|
|
||||
|
var path = new string[captures.Count]; |
||||
|
|
||||
|
for (var i = 0; i < path.Length; i++) |
||||
|
{ |
||||
|
path[i] = captures[i].Value; |
||||
|
} |
||||
|
|
||||
|
if (!data.TryGetValue(path[0], out var field)) |
||||
|
{ |
||||
|
return Undefined; |
||||
|
} |
||||
|
|
||||
|
if (!field.TryGetValue(path[1], out var value)) |
||||
|
{ |
||||
|
return Undefined; |
||||
|
} |
||||
|
|
||||
|
for (var j = 2; j < path.Length; j++) |
||||
|
{ |
||||
|
if (value is JObject obj && obj.TryGetValue(path[j], out value)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
if (value is JArray arr && int.TryParse(path[j], out var idx) && idx >= 0 && idx < arr.Count) |
||||
|
{ |
||||
|
value = arr[idx]; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return Undefined; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (value == null || value.Type == JTokenType.Null || value.Type == JTokenType.Undefined) |
||||
|
{ |
||||
|
return Undefined; |
||||
|
} |
||||
|
|
||||
|
if (value is JValue jValue && jValue != null) |
||||
|
{ |
||||
|
return jValue.Value.ToString(); |
||||
|
} |
||||
|
|
||||
|
return value?.ToString(Formatting.Indented) ?? Undefined; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The application ID.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string AppId { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The API key to grant access to Squidex.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string ApiKey { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The name of the index.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string IndexName { get; set; } |
||||
|
|
||||
|
public override RuleAction ToAction() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new AlgoliaAction()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()"> |
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-2 col-form-label" for="appId">App ID</label> |
||||
|
|
||||
|
<div class="col col-10"> |
||||
|
<sqx-control-errors for="text" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="appId" formControlName="appId" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The ID to you algolia application. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-2 col-form-label" for="apiKey">Api Key</label> |
||||
|
|
||||
|
<div class="col col-10"> |
||||
|
<sqx-control-errors for="apiKey" [submitted]="actionFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="text" class="form-control" id="apiKey" formControlName="apiKey" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The API Key to access you algolia app. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group row"> |
||||
|
<label class="col col-2 col-form-label" for="sharedSecret">Index Name</label> |
||||
|
|
||||
|
<div class="col col-10"> |
||||
|
<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. Use $SCHEMA_NAME as a placeholder. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
@ -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<object>(); |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,246 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Events; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.HandleRules |
||||
|
{ |
||||
|
public class RuleEventFormatterTests |
||||
|
{ |
||||
|
private readonly JsonSerializer serializer = JsonSerializer.CreateDefault(); |
||||
|
private readonly RuleEventFormatter sut; |
||||
|
|
||||
|
public RuleEventFormatterTests() |
||||
|
{ |
||||
|
sut = new RuleEventFormatter(serializer); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_serialize_object_to_json() |
||||
|
{ |
||||
|
var result = sut.ToRouteData(new { Value = 1 }); |
||||
|
|
||||
|
Assert.True(result is JObject); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_replace_app_information_from_event() |
||||
|
{ |
||||
|
var appId = Guid.NewGuid(); |
||||
|
|
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
AppId = new NamedId<Guid>(appId, "my-app") |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("Name $APP_NAME has id $APP_ID", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal($"Name my-app has id {appId}", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_replace_schema_information_from_event() |
||||
|
{ |
||||
|
var schemaId = Guid.NewGuid(); |
||||
|
|
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
SchemaId = new NamedId<Guid>(schemaId, "my-schema") |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("Name $SCHEMA_NAME has id $SCHEMA_ID", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal($"Name my-schema has id {schemaId}", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_replace_timestamp_information_from_event() |
||||
|
{ |
||||
|
var now = DateTime.UtcNow; |
||||
|
|
||||
|
var envelope = Envelope.Create(new ContentCreated()).To<AppEvent>().SetTimestamp(Instant.FromDateTimeUtc(now)); |
||||
|
|
||||
|
var result = sut.FormatString("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope); |
||||
|
|
||||
|
Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_undefined_when_field_not_found() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", "Berlin")) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.country.iv", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("UNDEFINED", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_undefined_when_partition_not_found() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", "Berlin")) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.de", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("UNDEFINED", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_undefined_when_array_item_not_found() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", new JArray())) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.de.10", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("UNDEFINED", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_undefined_when_property_not_found() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", new JObject( |
||||
|
new JProperty("name", "Berlin")))) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.de.Name", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("UNDEFINED", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_plain_value_when_found() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", "Berlin")) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.iv", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("Berlin", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_undefined_when_null() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", JValue.CreateNull())) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.iv", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("UNDEFINED", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_undefined_when_undefined() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", JValue.CreateUndefined())) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.iv", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("UNDEFINED", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_string_when_object() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", new JObject( |
||||
|
new JProperty("name", "Berlin")))) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.iv", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal(JObject.FromObject(new { name = "Berlin" }).ToString(Formatting.Indented), result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_plain_value_from_array_when_found() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", new JArray( |
||||
|
"Berlin"))) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.iv.0", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("Berlin", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_plain_value_from_object_when_found() |
||||
|
{ |
||||
|
var @event = new ContentCreated |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", new ContentFieldData() |
||||
|
.AddValue("iv", new JObject( |
||||
|
new JProperty("name", "Berlin")))) |
||||
|
}; |
||||
|
|
||||
|
var result = sut.FormatString("$CONTENT_DATA.city.iv.name", Envelope.Create(@event).To<AppEvent>()); |
||||
|
|
||||
|
Assert.Equal("Berlin", result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 AlgoliaActionTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_app_id_not_defined() |
||||
|
{ |
||||
|
var action = new AlgoliaAction { AppId = null, ApiKey = "KEY", IndexName = "IDX" }; |
||||
|
|
||||
|
var errors = await RuleActionValidator.ValidateAsync(action); |
||||
|
|
||||
|
Assert.NotEmpty(errors); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_api_key_not_defined() |
||||
|
{ |
||||
|
var action = new AlgoliaAction { AppId = "APP", ApiKey = null, IndexName = "IDX" }; |
||||
|
|
||||
|
var errors = await RuleActionValidator.ValidateAsync(action); |
||||
|
|
||||
|
Assert.NotEmpty(errors); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_index_name_not_defined() |
||||
|
{ |
||||
|
var action = new AlgoliaAction { AppId = "APP", ApiKey = "KEY", IndexName = null }; |
||||
|
|
||||
|
var errors = await RuleActionValidator.ValidateAsync(action); |
||||
|
|
||||
|
Assert.NotEmpty(errors); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_add_error_everything_defined() |
||||
|
{ |
||||
|
var action = new AlgoliaAction { AppId = "APP", ApiKey = "KEY", IndexName = "IDX" }; |
||||
|
|
||||
|
var errors = await RuleActionValidator.ValidateAsync(action); |
||||
|
|
||||
|
Assert.Empty(errors); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue