Browse Source

AI Tools V2 (#1103)

* More AI tools.

* More tools.
pull/1104/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
96efe94fba
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  2. 2
      backend/src/Migrations/Migrations.csproj
  3. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs
  4. 33
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  5. 27
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
  6. 8
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs
  8. 59
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Updates.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  10. 80
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/UpdateValues.cs
  11. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  12. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  13. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  14. 9
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs
  15. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs
  16. 14
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs
  17. 40
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs
  18. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  19. 15
      backend/src/Squidex.Domain.Apps.Entities/AppChatContext.cs
  20. 132
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppChatTools.cs
  21. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  22. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs
  23. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculatePreviewText.cs
  24. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs
  25. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs
  26. 71
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasChatTool.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  28. 2
      backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  29. 2
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  30. 4
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  31. 8
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  32. 2
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  33. 18
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  34. 2
      backend/src/Squidex.Shared/Squidex.Shared.csproj
  35. 2
      backend/src/Squidex.Web/Resources.cs
  36. 2
      backend/src/Squidex.Web/Squidex.Web.csproj
  37. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs
  38. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs
  39. 7
      backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  40. 4
      backend/src/Squidex/Config/Domain/AppsServices.cs
  41. 6
      backend/src/Squidex/Config/Domain/SchemasServices.cs
  42. 40
      backend/src/Squidex/Squidex.csproj
  43. 16
      backend/src/Squidex/appsettings.json
  44. 16
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs
  45. 86
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs
  46. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValuesTests.cs
  47. 79
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateValuesConverterTests.cs
  48. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  49. 88
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs
  50. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  51. 146
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs
  52. 69
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ExtensionsTests.cs
  53. 16
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs
  54. 62
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs
  55. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  56. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs
  57. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt
  58. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt
  59. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt
  60. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt
  61. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt
  62. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt
  63. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt
  64. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt
  65. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt
  66. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt
  67. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt
  68. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt
  69. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt
  70. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt
  71. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteApp_should_create_events_and_update_deleted_flag.verified.txt
  72. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt
  73. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt
  74. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt
  75. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt
  76. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt
  77. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt
  78. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt
  79. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt
  80. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt
  81. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt
  82. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt
  83. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt
  84. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt
  85. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt
  86. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt
  87. 2
      backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  88. 2
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  89. 2
      backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
  90. 4
      frontend/src/app/framework/angular/forms/editors/date-time-editor.component.scss
  91. 9
      frontend/src/app/shell/pages/internal/chat-menu.component.html
  92. 7
      frontend/src/app/shell/pages/internal/chat-menu.component.scss
  93. 33
      frontend/src/app/shell/pages/internal/chat-menu.component.ts
  94. 1
      frontend/src/app/shell/pages/internal/internal-area.component.html
  95. 2
      frontend/src/app/shell/pages/internal/internal-area.component.ts
  96. 57
      tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

6
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -18,8 +18,8 @@
<PackageReference Include="Elasticsearch.Net" Version="7.17.5" />
<PackageReference Include="Google.Cloud.Diagnostics.Common" Version="5.2.0" />
<PackageReference Include="Google.Cloud.Logging.V2" Version="4.4.0" />
<PackageReference Include="Google.Cloud.Monitoring.V3" Version="3.9.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Google.Cloud.Monitoring.V3" Version="3.10.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -27,7 +27,7 @@
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.25.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.21.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.21.3" />
<PackageReference Include="NodaTime" Version="3.1.11" />
<PackageReference Include="OpenSearch.Net" Version="1.7.1" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.8.1" />

2
backend/src/Migrations/Migrations.csproj

@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs

@ -17,7 +17,7 @@ public sealed class LanguagesConfigSurrogate : ISurrogate<LanguagesConfig>
public void FromSource(LanguagesConfig source)
{
Languages = source.Languages.ToDictionary(x => x.Key, source =>
Languages = source.Values.ToDictionary(x => x.Key, source =>
{
var surrogate = new LanguageConfigSurrogate();

33
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs

@ -20,7 +20,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
},
Language.EN);
private readonly Dictionary<string, LanguageConfig> languages;
private readonly Dictionary<string, LanguageConfig> values;
private readonly string master;
public string Master
@ -30,23 +30,22 @@ public sealed class LanguagesConfig : IFieldPartitioning
public IEnumerable<string> AllKeys
{
get => languages.Keys;
get => values.Keys;
}
public IReadOnlyDictionary<string, LanguageConfig> Languages
public IReadOnlyDictionary<string, LanguageConfig> Values
{
get => languages;
get => values;
}
public LanguagesConfig(Dictionary<string, LanguageConfig> languages, string master)
public LanguagesConfig(Dictionary<string, LanguageConfig> values, string master)
{
Guard.NotNull(languages);
Guard.NotNull(values);
Guard.NotNullOrEmpty(master);
Cleanup(languages, ref master);
this.languages = languages;
Cleanup(values, ref master);
this.values = values;
this.master = master;
}
@ -55,7 +54,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
{
Guard.NotNull(language);
return Build(languages, language);
return Build(values, language);
}
[Pure]
@ -63,7 +62,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
{
Guard.NotNull(language);
var newLanguages = new Dictionary<string, LanguageConfig>(languages)
var newLanguages = new Dictionary<string, LanguageConfig>(values)
{
[language] = new LanguageConfig(isOptional, ReadonlyList.Create(fallbacks))
};
@ -76,7 +75,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
{
Guard.NotNull(language);
var newLanguages = new Dictionary<string, LanguageConfig>(languages);
var newLanguages = new Dictionary<string, LanguageConfig>(values);
newLanguages.Remove(language);
@ -102,7 +101,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
private bool EqualLanguages(Dictionary<string, LanguageConfig> newLanguages)
{
return newLanguages.EqualsDictionary(languages);
return newLanguages.EqualsDictionary(values);
}
private void Cleanup(Dictionary<string, LanguageConfig> newLanguages, ref string newMaster)
@ -152,7 +151,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
public string? GetName(string key)
{
if (key != null && languages.ContainsKey(key))
if (key != null && values.ContainsKey(key))
{
return Language.GetLanguage(key).EnglishName;
}
@ -162,7 +161,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
public bool IsOptional(string key)
{
if (key != null && languages.TryGetValue(key, out var value))
if (key != null && values.TryGetValue(key, out var value))
{
return value.IsOptional;
}
@ -178,7 +177,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
{
yield return key;
}
else if (languages.TryGetValue(key, out var config))
else if (values.TryGetValue(key, out var config))
{
yield return key;
@ -197,7 +196,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
public bool Contains(string key)
{
return key != null && languages.ContainsKey(key);
return key != null && values.ContainsKey(key);
}
public override string ToString()

27
backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs

@ -16,13 +16,13 @@ public sealed class ContentData : Dictionary<string, ContentFieldData?>, IEquata
{
}
public ContentData(ContentData source)
: base(source, StringComparer.Ordinal)
public ContentData(int capacity)
: base(capacity, StringComparer.Ordinal)
{
}
public ContentData(int capacity)
: base(capacity, StringComparer.Ordinal)
public ContentData(IDictionary<string, ContentFieldData?> source)
: base(source, StringComparer.Ordinal)
{
}
@ -96,16 +96,29 @@ public sealed class ContentData : Dictionary<string, ContentFieldData?>, IEquata
continue;
}
var targetFieldData = target.GetOrAdd(fieldName, _ => new ContentFieldData());
if (Updates.IsUnset(sourceFieldData))
{
target.Remove(fieldName);
continue;
}
var targetFieldData = target.GetOrAdd(fieldName, _ => []);
if (targetFieldData == null)
{
continue;
}
foreach (var (partition, value) in sourceFieldData)
foreach (var (partition, sourceValue) in sourceFieldData)
{
targetFieldData[partition] = value;
if (Updates.IsUnset(sourceValue))
{
targetFieldData.Remove(partition);
}
else
{
targetFieldData[partition] = sourceValue;
}
}
}
}

8
backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs

@ -23,13 +23,9 @@ public sealed class ContentFieldData : Dictionary<string, JsonValue>, IEquatable
{
}
public ContentFieldData(ContentFieldData source)
: base(source.Count, StringComparer.OrdinalIgnoreCase)
public ContentFieldData(IDictionary<string, JsonValue> source)
: base(source, StringComparer.Ordinal)
{
foreach (var (key, value) in source)
{
this[key] = value;
}
}
public bool TryGetNonNull(string key, [MaybeNullWhen(false)] out JsonValue result)

2
backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs

@ -29,7 +29,7 @@ public class TranslationStatus : Dictionary<string, int>
Guard.NotNull(schema);
Guard.NotNull(languages);
var result = new TranslationStatus(languages.Languages.Count);
var result = new TranslationStatus(languages.Values.Count);
var localizedFields = schema.Fields.Where(x => x.Partitioning == Partitioning.Language).ToList();

59
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Updates.cs

@ -0,0 +1,59 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Contents;
public static class Updates
{
public static bool IsUnset(object? value)
{
if (value is JsonValue json)
{
return IsUnset(json.Value);
}
return value is IReadOnlyDictionary<string, JsonValue> obj && IsUnset(obj);
}
public static bool IsUnset(IReadOnlyDictionary<string, JsonValue>? obj)
{
return
obj is { Count: 1 } &&
obj.TryGetValue("$unset", out var item) &&
Equals(item.Value, true);
}
public static bool IsUpdate(object? value, out string expression)
{
expression = null!;
if (value is JsonValue json)
{
return IsUpdate(json.Value, out expression);
}
return value is IReadOnlyDictionary<string, JsonValue> obj && IsUpdate(obj, out expression);
}
public static bool IsUpdate(IReadOnlyDictionary<string, JsonValue>? obj, out string expression)
{
expression = null!;
if (obj is { Count: > 0 } &&
obj.TryGetValue("$update", out var item) &&
item.Value is string e &&
!string.IsNullOrWhiteSpace(e))
{
expression = e;
return true;
}
return false;
}
}

2
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -12,7 +12,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

80
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/UpdateValues.cs

@ -12,44 +12,110 @@ using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ConvertContent;
public sealed class UpdateValues : IContentValueConverter
public sealed class UpdateValues : IContentValueConverter, IContentDataConverter
{
private readonly ContentData existingData;
private readonly IScriptEngine scriptEngine;
private readonly bool canUnset;
private ScriptVars? vars;
public UpdateValues(ContentData existingData, IScriptEngine scriptEngine)
public UpdateValues(ContentData existingData, IScriptEngine scriptEngine, bool canUnset)
{
this.existingData = existingData;
this.scriptEngine = scriptEngine;
this.canUnset = canUnset;
}
public void ConvertDataBefore(Schema schema, ContentData source)
{
// Avoid unnecessary allocations if nothing has been changed, which is the default.
List<string>? toRemove = null;
List<(string, ContentFieldData)>? toReplace = null;
foreach (var (key, value) in source)
{
if (canUnset && Updates.IsUnset(value))
{
toRemove ??= [];
toRemove.Add(key);
}
if (Updates.IsUpdate(value, out var expression))
{
var options = new ScriptOptions { Readonly = true };
// Reuse the vars to save allocations.
vars ??= new ScriptVars
{
["$data"] = existingData
};
// Give access to the current update statement to carry extra values from the request.
vars["$self"] = value;
// Put the expression in brackets to return an object directly.
var result = scriptEngine.Execute(vars, $"({expression})", options);
if (result.Value is JsonObject obj)
{
var replacement = new ContentFieldData(obj);
if (!replacement.Equals(value))
{
toReplace ??= [];
toReplace.Add((key, replacement));
}
}
}
}
if (toRemove != null)
{
foreach (var key in toRemove)
{
source.Remove(key);
}
}
if (toReplace != null)
{
foreach (var (key, value) in toReplace)
{
source[key] = value;
}
}
}
public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent)
{
if (source.Value is not JsonObject jsonObject)
if (source.Value is not JsonObject obj)
{
return (false, source);
}
if (jsonObject.TryGetValue("$unset", out var value1) && !Equals(value1.Value, false))
if (canUnset && Updates.IsUnset(obj))
{
return (true, source);
}
if (!jsonObject.TryGetValue("$update", out var value2) || value2.Value is not string update)
if (!Updates.IsUpdate(obj, out var expression))
{
return (false, source);
}
var options = new ScriptOptions { Readonly = true };
// Reuse the vars to save allocations.
vars ??= new ScriptVars
{
["$data"] = existingData,
["$self"] = jsonObject
};
var result = scriptEngine.Execute(vars, update, options);
// Give access to the current update statement to carry extra values from the request.
vars["$self"] = obj;
// Put the expression in brackets to return an object directly.
var result = scriptEngine.Execute(vars, $"({expression})", options);
return (false, result);
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -21,15 +21,15 @@
<PackageReference Include="Fluid.Core" Version="2.10.0" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.1.2" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="NJsonSchema" Version="11.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="6.8.8" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.8.8" />
<PackageReference Include="Squidex.AI" Version="6.11.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.11.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

6
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -110,13 +110,13 @@ public sealed class ContentValidator
var valueValidator = CreateValueValidator(field);
var partitioning = partitionResolver(field.Partitioning);
var partitioningValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
var partitions = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
foreach (var partitionKey in partitioning.AllKeys)
{
var optional = partitioning.IsOptional(partitionKey);
partitioningValidators[partitionKey] = (optional, valueValidator);
partitions[partitionKey] = (optional, valueValidator);
}
var typeName = partitioning.ToString()!;
@ -124,7 +124,7 @@ public sealed class ContentValidator
return new AggregateValidator(
CreateFieldValidators(field)
.Union(Enumerable.Repeat(
new ObjectValidator<JsonValue>(partitioningValidators, isPartial, typeName), 1)));
new ObjectValidator<JsonValue>(partitions, isPartial, typeName), 1)));
}
private IValidator CreateValueValidator(IField field)

4
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs

@ -36,7 +36,9 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
if (IsRequired(properties, args.Context, out var r) || properties.MinItems != null || properties.MaxItems != null)
if (IsRequired(properties, args.Context, out var r) ||
properties.MinItems != null ||
properties.MaxItems != null)
{
yield return new CollectionValidator(r, properties.MinItems, properties.MaxItems);
}

9
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs

@ -11,13 +11,20 @@ public static class Undefined
{
public static readonly object Value = new object();
public static readonly object Unset = new object();
public static bool IsUndefined(this object? other)
{
return ReferenceEquals(other, Value);
}
public static bool IsUnset(this object? other)
{
return ReferenceEquals(other, Unset);
}
public static bool IsNullOrUndefined(this object? other)
{
return other == null || other.IsUndefined();
return other == null || other.IsUndefined() || other.IsUnset();
}
}

7
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -34,7 +35,7 @@ public sealed class FieldValidator : IValidator
{
if (value is JsonValue jsonValue)
{
if (jsonValue == default)
if (jsonValue == default || Updates.IsUnset(jsonValue))
{
typedValue = null;
}
@ -42,7 +43,7 @@ public sealed class FieldValidator : IValidator
{
typedValue = jsonValue.Value;
var (json, error) = JsonValueConverter.ConvertValue(field, jsonValue,
var (typed, error) = JsonValueConverter.ConvertValue(field, jsonValue,
context.Root.Serializer,
context.Root.Components);
@ -52,7 +53,7 @@ public sealed class FieldValidator : IValidator
}
else
{
typedValue = json;
typedValue = typed;
}
}
}

14
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Translations;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators;
@ -25,6 +26,8 @@ public sealed class ObjectValidator<TValue> : IValidator
public void Validate(object? value, ValidationContext context)
{
var originalValue = value;
if (value.IsNullOrUndefined())
{
value = DefaultValue;
@ -48,9 +51,10 @@ public sealed class ObjectValidator<TValue> : IValidator
if (!values.TryGetValue(name, out var nestedValue))
{
if (isPartial)
// If the original value was unset, we have to validate the children for required values.
if (isPartial && !originalValue.IsUnset())
{
return;
continue;
}
}
else
@ -58,6 +62,12 @@ public sealed class ObjectValidator<TValue> : IValidator
fieldValue = nestedValue!;
}
// Use a special null values for unsets so we can treat them as null for required validators.
if (Updates.IsUnset(fieldValue))
{
fieldValue = Undefined.Unset;
}
var fieldContext = context.Nested(name, field.IsOptional);
field.Validator.Validate(fieldValue, fieldContext);

40
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs

@ -29,11 +29,11 @@ public static class Extensions
.Add("$expr", new BsonDocument()
.Add("$eq", new BsonArray { "$_id", "$$id" }));
private static Dictionary<string, string> propertyMap;
private static Dictionary<string, string> metaFields;
public static IReadOnlyDictionary<string, string> PropertyMap
private static IReadOnlyDictionary<string, string> MetaFields
{
get => propertyMap ??=
get => metaFields ??=
BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps
.Where(x =>
x.MemberName != nameof(MongoContentEntity.NewData) &&
@ -173,30 +173,44 @@ public static class Extensions
return find.Project<T>(BuildProjection2<T>(fields));
}
private static ProjectionDefinition<T, T> BuildProjection2<T>(IEnumerable<string>? fields)
public static ProjectionDefinition<T, T> BuildProjection2<T>(IEnumerable<string>? fields)
{
var projector = Builders<T>.Projection;
var projections = new List<ProjectionDefinition<T>>();
if (fields?.Any() == true)
{
var dataPrefix = Field.Of<MongoContentEntity>(x => nameof(x.Data));
foreach (var field in fields)
static IEnumerable<string> GetDataFields(IEnumerable<string> fields)
{
var dataField = field;
var dataPrefix = Field.Of<MongoContentEntity>(x => nameof(x.Data));
if (FieldNames.IsDataField(field, out var fieldName))
foreach (var field in fields)
{
dataField = fieldName;
}
var dataField =
FieldNames.IsDataField(field, out var fieldName) ?
fieldName :
field;
projections.Add(projector.Include($"{dataPrefix}.{dataField}"));
yield return $"{dataPrefix}.{dataField}";
}
}
foreach (var field in PropertyMap.Values)
var addedFields = new List<string>();
// Sort the fields to start with prefixes first.
var allFields = GetDataFields(fields).Union(MetaFields.Values).OrderBy(x => x);
foreach (var field in allFields)
{
// If there is at least one field that is a prefix of the current field, we cannot add that.
if (addedFields.Exists(x => field.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
projections.Add(projector.Include(field));
addedFields.Add(field);
}
}
else

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

15
backend/src/Squidex.Domain.Apps.Entities/AppChatContext.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.AI;
namespace Squidex.Domain.Apps.Entities;
public sealed class AppChatContext : ChatContext
{
required public Context BaseContext { get; init; }
}

132
backend/src/Squidex.Domain.Apps.Entities/Apps/AppChatTools.cs

@ -0,0 +1,132 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Runtime.CompilerServices;
using Squidex.AI;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.Json;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Apps;
public sealed class AppChatTools : IChatToolProvider
{
private readonly IJsonSerializer serializer;
private readonly IUrlGenerator urlGenerator;
public AppChatTools(IJsonSerializer serializer, IUrlGenerator urlGenerator)
{
this.serializer = serializer;
this.urlGenerator = urlGenerator;
}
public async IAsyncEnumerable<IChatTool> GetToolsAsync(ChatContext chatContext,
[EnumeratorCancellation] CancellationToken ct)
{
if (chatContext is not AppChatContext appContext)
{
yield break;
}
var context = appContext.BaseContext;
await Task.Yield();
if (context.Allows(PermissionIds.AppClientsRead))
{
yield return new DelegateChatTool(
new ToolSpec("clients", "Clients", "Provides the clients for the Squidex App."),
(_, ct) =>
{
var result = new
{
Clients = context.App.Clients.Select(x =>
new
{
Id = x.Key,
ClientId = $"{context.App.Name}:{x.Key}",
ClientSecret = "obfuscated",
x.Value.Role
}),
Url = urlGenerator.ClientsUI(context.App.NamedId())
};
var json = serializer.Serialize(result, true);
return Task.FromResult(json);
});
}
if (context.Allows(PermissionIds.AppLanguagesRead))
{
yield return new DelegateChatTool(
new ToolSpec("languages", "Languages", "Provides the languages for the Squidex App."),
(_, ct) =>
{
var result = new
{
Languages = context.App.Languages.Values.Select(x =>
new
{
Iso2Code = x.Key,
IsMaster = context.App.Languages.Master.Equals(x.Key),
x.Value.IsOptional
}),
Url = urlGenerator.LanguagesUI(context.App.NamedId())
};
var json = serializer.Serialize(result, true);
return Task.FromResult(json);
});
}
if (context.Allows(PermissionIds.AppRolesRead))
{
yield return new DelegateChatTool(
new ToolSpec("roles", "Roles", "Provides the roles for the Squidex App."),
(_, ct) =>
{
var result = new
{
Roles = context.App.Roles.Custom.Select(x =>
new
{
x.Name
}),
Url = urlGenerator.RolesUI(context.App.NamedId())
};
var json = serializer.Serialize(result, true);
return Task.FromResult(json);
});
}
if (context.Allows(PermissionIds.AppPlansRead))
{
yield return new DelegateChatTool(
new ToolSpec("plan", "Plan", "Provides the plan for the Squidex App."),
(_, ct) =>
{
var result = new
{
Plan = new
{
Name = context.App.Plan?.PlanId,
},
Url = urlGenerator.PlansUI(context.App.NamedId())
};
var json = serializer.Serialize(result, true);
return Task.FromResult(json);
});
}
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs

@ -322,7 +322,7 @@ public partial class ContentDomainObject : DomainObject<WriteContent>
operation.MustHavePermission(PermissionIds.AppContentsUpdate);
operation.MustHaveData(c.Data);
var newData = operation.InvokeUpdates(c.Data, Snapshot.EditingData);
var newData = operation.InvokeUpdates(c.Data, Snapshot.EditingData, true);
if (!c.DoNotValidate)
{
@ -363,7 +363,7 @@ public partial class ContentDomainObject : DomainObject<WriteContent>
operation.MustHavePermission(PermissionIds.AppContentsUpdate);
operation.MustHaveData(c.Data);
c.Data = operation.InvokeUpdates(c.Data, Snapshot.EditingData);
c.Data = operation.InvokeUpdates(c.Data, Snapshot.EditingData, false);
if (!c.DoNotValidate)
{

4
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs

@ -98,13 +98,13 @@ public static class ValidationExtensions
return converter.Convert(data);
}
public static ContentData InvokeUpdates(this ContentOperation operation, ContentData data, ContentData currentData)
public static ContentData InvokeUpdates(this ContentOperation operation, ContentData data, ContentData currentData, bool canUnset)
{
var converter =
new ContentConverter(
operation.Components,
operation.Schema);
converter.Add(new UpdateValues(currentData, operation.Resolve<IScriptEngine>()));
converter.Add(new UpdateValues(currentData, operation.Resolve<IScriptEngine>(), canUnset));
return converter.Convert(data);
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculatePreviewText.cs

@ -41,7 +41,7 @@ public sealed class CalculatePreviewText : IContentEnricherStep
content.ReferenceData ??= [];
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!;
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => [])!;
foreach (var (partitionKey, partitionValue) in fieldData)
{

3
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs

@ -7,7 +7,6 @@
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets;
@ -72,7 +71,7 @@ public sealed class ResolveAssets : IContentEnricherStep
{
content.ReferenceData ??= [];
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!;
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => [])!;
if (content.Data.TryGetValue(field.Name, out var fieldData) && fieldData != null)
{

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs

@ -74,7 +74,7 @@ public sealed class ResolveReferences : IContentEnricherStep
{
content.ReferenceData ??= [];
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!;
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => [])!;
try
{

71
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasChatTool.cs

@ -0,0 +1,71 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Runtime.CompilerServices;
using Squidex.AI;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Schemas;
public sealed class SchemasChatTool : IChatToolProvider
{
private readonly IAppProvider appProvider;
private readonly IJsonSerializer serializer;
private readonly IUrlGenerator urlGenerator;
public SchemasChatTool(IAppProvider appProvider, IJsonSerializer serializer, IUrlGenerator urlGenerator)
{
this.appProvider = appProvider;
this.serializer = serializer;
this.urlGenerator = urlGenerator;
}
public async IAsyncEnumerable<IChatTool> GetToolsAsync(ChatContext chatContext,
[EnumeratorCancellation] CancellationToken ct)
{
if (chatContext is not AppChatContext appContext)
{
yield break;
}
var context = appContext.BaseContext;
await Task.Yield();
if (context.Allows(PermissionIds.AppSchemasRead))
{
yield return new DelegateChatTool(
new ToolSpec("schemas", "Schemas", "Provides the schemas for the Squidex App."),
async (_, ct) =>
{
var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct);
var result = new
{
Schemas = schemas.Select(x =>
new
{
x.Name,
x.IsPublished,
x.Type,
FieldCount = x.Fields.Count,
Url = urlGenerator.SchemaUI(context.App.NamedId(), x.NamedId())
}),
Url = urlGenerator.SchemasUI(context.App.NamedId())
};
var json = serializer.Serialize(result, true);
return json;
});
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -27,7 +27,7 @@
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.DataLoader" Version="7.8.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -19,7 +19,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

4
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -18,11 +18,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityModel" Version="7.0.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.6" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="5.6.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

8
backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj

@ -11,11 +11,11 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="23.3.1" />
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="23.3.1" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.3.1" />
<PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="23.3.2" />
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="23.3.2" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.3.2" />
<PackageReference Include="Grpc.Net.Client" Version="2.63.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

18
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -13,23 +13,23 @@
<ItemGroup>
<PackageReference Include="MailKit" Version="4.6.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="8.0.6" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.21.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.21.3" />
<PackageReference Include="NodaTime" Version="3.1.11" />
<PackageReference Include="OpenTelemetry.Api" Version="1.8.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="6.8.8" />
<PackageReference Include="Squidex.Caching" Version="6.8.8" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.8.8" />
<PackageReference Include="Squidex.Log" Version="6.8.8" />
<PackageReference Include="Squidex.Messaging" Version="6.8.8" />
<PackageReference Include="Squidex.Text" Version="6.8.8" />
<PackageReference Include="Squidex.Assets" Version="6.11.0" />
<PackageReference Include="Squidex.Caching" Version="6.11.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.11.0" />
<PackageReference Include="Squidex.Log" Version="6.11.0" />
<PackageReference Include="Squidex.Messaging" Version="6.11.0" />
<PackageReference Include="Squidex.Text" Version="6.11.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

2
backend/src/Squidex.Shared/Squidex.Shared.csproj

@ -10,7 +10,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Web/Resources.cs

@ -16,7 +16,7 @@ namespace Squidex.Web;
public sealed class Resources
{
private readonly Dictionary<(string Id, string Schema), bool> permissions = new Dictionary<(string, string), bool>();
private readonly Dictionary<(string Id, string Schema), bool> permissions = [];
// Contents
public bool CanReadContent(string schema) => Can(PermissionIds.AppContentsReadOwn, schema);

2
backend/src/Squidex.Web/Squidex.Web.csproj

@ -16,7 +16,7 @@
<PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.7.1" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs

@ -64,7 +64,7 @@ public sealed class AppLanguageDto : Resource
resources.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values));
}
if (resources.CanDeleteLanguage && app.Languages.Languages.Count > 1)
if (resources.CanDeleteLanguage && app.Languages.Values.Count > 1)
{
AddDeleteLink("delete",
resources.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values));

2
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs

@ -23,7 +23,7 @@ public sealed class AppLanguagesDto : Resource
var result = new AppLanguagesDto
{
Items = config.Languages
Items = config.Values
.Select(x => AppLanguageDto.FromDomain(x.Key, x.Value, config))
.Select(x => x.CreateLinks(resources, app))
.OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code)

7
backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs

@ -12,6 +12,7 @@ using NSwag.Annotations;
using Squidex.AI;
using Squidex.Areas.Api.Controllers.Translations.Models;
using Squidex.Assets;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Text.Translations;
@ -92,9 +93,11 @@ public sealed class TranslationsController : ApiController
Prompt = request.Prompt
};
var context = new ChatContext
var context = new AppChatContext
{
User = User
User = User,
// Use a special context to provide access to the app.
BaseContext = Context,
};
return new FileCallbackResult("text/event-stream", async (body, range, ct) =>

4
backend/src/Squidex/Config/Domain/AppsServices.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Microsoft.Extensions.Options;
using Squidex.AI;
using Squidex.Areas.Api.Controllers.UI;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities;
@ -51,6 +52,9 @@ public static class AppsServices
services.AddSingletonAs<AppUISettings>()
.As<IAppUISettings>().As<IDeleter>();
services.AddSingletonAs<AppChatTools>()
.As<IChatToolProvider>();
services.AddSingletonAs<AppSettingsSearchSource>()
.As<ISearchSource>();

6
backend/src/Squidex/Config/Domain/SchemasServices.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.AI;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Search;
@ -15,9 +16,12 @@ public static class SchemasServices
{
public static void AddSquidexSchemas(this IServiceCollection services)
{
services.AddTransientAs<SchemasSearchSource>()
services.AddSingletonAs<SchemasSearchSource>()
.As<ISearchSource>();
services.AddSingletonAs<SchemasChatTool>()
.As<IChatToolProvider>();
services.AddSingletonAs<SchemaHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
}

40
backend/src/Squidex/Squidex.csproj

@ -38,22 +38,22 @@
<PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="7.8.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.6" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.RulesetToEditorconfigConverter" Version="3.3.3" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="7.5.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.21.2" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="7.6.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.21.3" />
<PackageReference Include="MongoDB.Driver" Version="2.25.0" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.OpenTelemetry" Version="1.0.0" />
<PackageReference Include="NetTopologySuite.IO.GeoJSON4STJ" Version="4.0.0" />
@ -64,19 +64,19 @@
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.3.4" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="6.8.8" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="6.8.8" />
<PackageReference Include="Squidex.Assets.FTP" Version="6.8.8" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="6.8.8" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="6.8.8" />
<PackageReference Include="Squidex.Assets.Mongo" Version="6.8.8" />
<PackageReference Include="Squidex.Assets.S3" Version="6.8.8" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="6.8.8" />
<PackageReference Include="ReportGenerator" Version="5.3.6" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="6.11.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="6.11.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="6.11.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="6.11.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="6.11.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="6.11.0" />
<PackageReference Include="Squidex.Assets.S3" Version="6.11.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="6.11.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="19.2.0" />
<PackageReference Include="Squidex.Hosting" Version="6.8.8" />
<PackageReference Include="Squidex.Messaging.All" Version="6.8.8" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.8.8" />
<PackageReference Include="Squidex.Hosting" Version="6.11.0" />
<PackageReference Include="Squidex.Messaging.All" Version="6.11.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.11.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="YDotNet" Version="0.4.0" />

16
backend/src/Squidex/appsettings.json

@ -690,10 +690,9 @@
"defaults": {
"systemMessages": [
"You are a bot to generate text content.",
"You are a bot to help with all support requests related to Squidex.",
"Say hello to the user and explain him about your capabilities in a single, short sentence."
],
"tools": []
]
},
"configurations": {
@ -701,7 +700,16 @@
"systemMessages": [
"You are a bot to generate images.",
"Say hello to the user and explain him the user about your capabilities in a single, short sentence."
]
],
"tools": [ "dall-e" ]
},
"text": {
"systemMessages": [
"You are a bot to generate text content.",
"Say hello to the user and explain him about your capabilities in a single, short sentence."
],
"tools": []
}
}
},

16
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs

@ -87,7 +87,7 @@ public class LanguagesConfigTests
[Fact]
public void Should_create_initial_config()
{
config_0.Languages.Should().BeEquivalentTo(
config_0.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig()
@ -106,7 +106,7 @@ public class LanguagesConfigTests
.Set(Language.IT, true, Language.ES)
.MakeMaster(Language.DE);
config.Languages.Should().BeEquivalentTo(
config.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
@ -141,7 +141,7 @@ public class LanguagesConfigTests
.Set(Language.IT, true, Language.ES)
.MakeMaster(Language.IT);
config.Languages.Should().BeEquivalentTo(
config.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
@ -182,7 +182,7 @@ public class LanguagesConfigTests
var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Remove(Language.DE);
config_3.Languages.Should().BeEquivalentTo(
config_3.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
@ -199,7 +199,7 @@ public class LanguagesConfigTests
var config_2 = config_1.Set(Language.IT, true, Language.UK);
var config_3 = config_2.Remove(Language.DE);
config_3.Languages.Should().BeEquivalentTo(
config_3.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
@ -224,7 +224,7 @@ public class LanguagesConfigTests
var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Remove(Language.EN);
config_3.Languages.Should().BeEquivalentTo(
config_3.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.DE] = new LanguageConfig(),
@ -248,7 +248,7 @@ public class LanguagesConfigTests
var config_1 = config_0.Set(Language.IT);
var config_2 = config_1.Set(Language.IT, true, Language.EN);
config_2.Languages.Should().BeEquivalentTo(
config_2.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
@ -265,7 +265,7 @@ public class LanguagesConfigTests
var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Set(Language.IT, true, Language.EN, Language.IT, Language.DE);
config_3.Languages.Should().BeEquivalentTo(
config_3.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),

86
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs

@ -6,13 +6,14 @@
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Model.Contents;
public class ContentDataTests
{
[Fact]
public void Should_return_same_content_if_merging_same_references()
public void Should_return_same_data_if_merging_same_references()
{
var source =
new ContentData()
@ -29,7 +30,7 @@ public class ContentDataTests
}
[Fact]
public void Should_merge_two_name_models()
public void Should_merge_data()
{
var lhs =
new ContentData()
@ -73,9 +74,86 @@ public class ContentDataTests
}
[Fact]
public void Should_be_equal_if_data_have_same_structure()
public void Should_unset_field_value_when_merging()
{
var rhs =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddInvariant(1))
.AddField("field2",
new ContentFieldData()
.AddLocalized("de", 2)
.AddLocalized("it", 2));
var lhs =
new ContentData()
.AddField("field2",
new ContentFieldData()
.AddLocalized("it",
JsonValue.Object()
.Add("$unset", true))
.AddLocalized("en", 3));
var expected =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddInvariant(1))
.AddField("field2",
new ContentFieldData()
.AddLocalized("de", 2)
.AddLocalized("en", 3));
var actual = lhs.MergeInto(rhs);
Assert.Equal(expected, actual);
Assert.NotSame(expected, rhs);
Assert.NotSame(expected, lhs);
}
[Fact]
public void Should_unset_field_when_merging()
{
var rhs =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddInvariant(1))
.AddField("field2",
new ContentFieldData()
.AddLocalized("de", 2)
.AddLocalized("it", 2));
var lhs =
new ContentData()
.AddField("field2",
new ContentFieldData()
.AddLocalized("$unset", true))
.AddField("field3",
new ContentFieldData()
.AddInvariant(4));
var expected =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddInvariant(1))
.AddField("field3",
new ContentFieldData()
.AddInvariant(4));
var actual = lhs.MergeInto(rhs);
Assert.Equal(expected, actual);
Assert.NotSame(expected, rhs);
Assert.NotSame(expected, lhs);
}
[Fact]
public void Should_be_equal_if_data_have_same_structure()
{
var rhs =
new ContentData()
.AddField("field1",
new ContentFieldData()
@ -84,7 +162,7 @@ public class ContentDataTests
new ContentFieldData()
.AddInvariant(2));
var rhs =
var lhs =
new ContentData()
.AddField("field1",
new ContentFieldData()

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValuesTests.cs

@ -74,7 +74,7 @@ public class DefaultValuesTests
.AddInvariant(now.ToString()))
.AddField("myBoolean",
new ContentFieldData()
.AddInvariant(JsonValue.True))
.AddInvariant(true))
.AddField("myArray",
new ContentFieldData()
.AddInvariant(
@ -154,7 +154,7 @@ public class DefaultValuesTests
.AddInvariant(now.ToString()))
.AddField("myBoolean",
new ContentFieldData()
.AddInvariant(JsonValue.True))
.AddInvariant(true))
.AddField("myArray",
new ContentFieldData()
.AddInvariant(
@ -196,7 +196,7 @@ public class DefaultValuesTests
.AddInvariant(now.ToString()))
.AddField("myBoolean",
new ContentFieldData()
.AddInvariant(JsonValue.True))
.AddInvariant(true))
.AddField("myArray",
new ContentFieldData()
.AddInvariant(

79
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateConverterTests.cs → backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateValuesConverterTests.cs

@ -15,11 +15,11 @@ using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Operations.ConvertContent;
public class UpdateConverterTests
public class UpdateValuesConverterTests
{
private readonly IScriptEngine scriptEngine;
public UpdateConverterTests()
public UpdateValuesConverterTests()
{
scriptEngine = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())),
Options.Create(new JintScriptOptions
@ -30,7 +30,7 @@ public class UpdateConverterTests
}
[Fact]
public void Should_update()
public void Should_update_value()
{
var field1 = Fields.Number(1, "number1", Partitioning.Invariant);
@ -54,7 +54,42 @@ public class UpdateConverterTests
var actual =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine))
.Add(new UpdateValues(existing, scriptEngine, true))
.Convert(source);
var expected =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en", 43));
Assert.Equal(expected, actual);
}
[Fact]
public void Should_update_field()
{
var field1 = Fields.Number(1, "number1", Partitioning.Invariant);
var schema =
new Schema { Name = "my-schema" }
.AddField(field1);
var source =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("$update", "{ \"en\": $data.number1.en + 1 }"));
var existing =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en", 42));
var actual =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine, true))
.Convert(source);
var expected =
@ -92,7 +127,7 @@ public class UpdateConverterTests
var actual =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine))
.Add(new UpdateValues(existing, scriptEngine, true))
.Convert(source);
var expected =
@ -129,13 +164,43 @@ public class UpdateConverterTests
var actual =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine))
.Add(new UpdateValues(existing, scriptEngine, true))
.Convert(source);
var expected =
new ContentData()
.AddField(field1.Name, []);
Assert.Equal(expected, actual);
}
[Fact]
public void Should_unset_field()
{
var field1 = Fields.Number(1, "number1", Partitioning.Invariant);
var schema =
new Schema { Name = "my-schema" }
.AddField(field1);
var source =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("$unset", true));
var existing =
new ContentData()
.AddField(field1.Name,
new ContentFieldData());
new ContentFieldData()
.AddLocalized("en", 42));
var actual =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine, true))
.Convert(source);
var expected = new ContentData();
Assert.Equal(expected, actual);
}

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

@ -623,7 +623,7 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
A<ChatRequest>.That.Matches(x => x.Prompt == "prompt"),
A<ChatContext>._,
A<CancellationToken>._))
.Returns(new ChatResult { Content = "Generated", Metadata = new ChatMetadata() });
.Returns(new ChatResult { Content = "Generated", Metadata = new ChatMetadata(), Tools = [] });
var vars = new ScriptVars
{
@ -639,7 +639,7 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
Assert.Equal("Generated", actual.ToString());
A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<CancellationToken>._))
A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<ChatContext>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
@ -666,7 +666,7 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
A.CallTo(() => chatAgent.PromptAsync(A<ChatRequest>._, A<ChatContext>._, A<CancellationToken>._))
.MustNotHaveHappened();
A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<CancellationToken>._))
A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<ChatContext>._, A<CancellationToken>._))
.MustNotHaveHappened();
}

88
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs

@ -453,4 +453,92 @@ public class ContentValidationTests : IClassFixture<TranslationsFixture>
Assert.Empty(errors);
}
[Fact]
public async Task Should_validate_partial_unset_field_as_null()
{
schema = schema.AddString(1, "myField", Partitioning.Invariant,
new StringFieldProperties { IsRequired = true });
var data =
new ContentData()
.AddField("myField",
new ContentFieldData()
.AddLocalized("$unset", true));
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Field is required.", "myField.iv")
});
}
[Fact]
public async Task Should_handle_partial_unset_field_value_as_null()
{
schema = schema.AddString(1, "myField", Partitioning.Invariant,
new StringFieldProperties { IsRequired = true });
var data =
new ContentData()
.AddField("myField",
new ContentFieldData()
.AddLocalized("iv",
JsonValue.Object()
.Add("$unset", true)));
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Field is required.", "myField.iv")
});
}
[Fact]
public async Task Should_validate_unset_field_as_null()
{
schema = schema.AddString(1, "myField", Partitioning.Invariant,
new StringFieldProperties { IsRequired = true });
var data =
new ContentData()
.AddField("myField",
new ContentFieldData()
.AddLocalized("$unset", true));
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Field is required.", "myField.iv")
});
}
[Fact]
public async Task Should_handle_unset_field_value_as_null()
{
schema = schema.AddString(1, "myField", Partitioning.Invariant,
new StringFieldProperties { IsRequired = true });
var data =
new ContentData()
.AddField("myField",
new ContentFieldData()
.AddLocalized("iv",
JsonValue.Object()
.Add("$unset", true)));
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Field is required.", "myField.iv")
});
}
}

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

146
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs

@ -0,0 +1,146 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Apps;
public class AppChatToolsTests : GivenContext
{
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly AppChatTools sut;
public AppChatToolsTests()
{
sut = new AppChatTools(TestUtils.DefaultSerializer, urlGenerator);
}
[Fact]
public async Task Should_return_clients_if_user_has_permission()
{
App = App with
{
Clients = App.Clients.Add("default", "secret")
};
var chatContext = new AppChatContext
{
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppClientsRead, App.Name).Id)
};
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync();
Assert.NotNull(tool);
Assert.Equal("clients", tool.Spec.Name);
Assert.Equal("Clients", tool.Spec.DisplayName);
var result = await tool.ExecuteAsync(null!, default);
Assert.Contains($"{App.Name}:default", result);
A.CallTo(() => urlGenerator.ClientsUI(AppId))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_languages_if_user_has_permission()
{
App = App with
{
Languages = App.Languages.Set(Language.DE)
};
var chatContext = new AppChatContext
{
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppLanguages, App.Name).Id)
};
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync();
Assert.NotNull(tool);
Assert.Equal("languages", tool.Spec.Name);
Assert.Equal("Languages", tool.Spec.DisplayName);
var result = await tool.ExecuteAsync(null!, default);
Assert.Contains($"\"de\"", result);
A.CallTo(() => urlGenerator.LanguagesUI(AppId))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_roles_if_user_has_permission()
{
App = App with
{
Roles = App.Roles.Add("viewers")
};
var chatContext = new AppChatContext
{
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppRoles, App.Name).Id)
};
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync();
Assert.NotNull(tool);
Assert.Equal("roles", tool.Spec.Name);
Assert.Equal("Roles", tool.Spec.DisplayName);
var result = await tool.ExecuteAsync(null!, default);
Assert.Contains($"viewers", result);
A.CallTo(() => urlGenerator.RolesUI(AppId))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_plan_if_user_has_permission()
{
App = App with
{
Plan = new AssignedPlan(User, "Business")
};
var chatContext = new AppChatContext
{
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppPlans, App.Name).Id)
};
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync();
Assert.NotNull(tool);
Assert.Equal("plan", tool.Spec.Name);
Assert.Equal("Plan", tool.Spec.DisplayName);
var result = await tool.ExecuteAsync(null!, default);
Assert.Contains($"Business", result);
A.CallTo(() => urlGenerator.PlansUI(AppId))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_return_tools_if_user_no_permission()
{
var chatContext = new AppChatContext
{
BaseContext = FrontendContext
};
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync();
Assert.Null(tool);
}
}

69
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ExtensionsTests.cs

@ -0,0 +1,69 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using ExtensionSut = Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations.Extensions;
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb;
public class ExtensionsTests
{
public ExtensionsTests()
{
MongoContentEntity.RegisterClassMap();
}
[Fact]
public void Should_build_projection_without_fields()
{
var projection = ExtensionSut.BuildProjection2<MongoContentEntity>(null);
AssertProjection(projection, "{ 'dd' : 0 }");
}
[Fact]
public void Should_build_projection_with_data_prefix()
{
var projection = ExtensionSut.BuildProjection2<MongoContentEntity>(["data.myField"]);
AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }");
}
[Fact]
public void Should_build_projection_without_data_prefix()
{
var projection = ExtensionSut.BuildProjection2<MongoContentEntity>(["myField"]);
AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }");
}
[Fact]
public void Should_build_projection_without_included_field()
{
var projection = ExtensionSut.BuildProjection2<MongoContentEntity>(["myField.special", "myField"]);
AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }");
}
private static void AssertProjection(ProjectionDefinition<MongoContentEntity, MongoContentEntity> projection, string expected)
{
var rendered =
projection.Render(
BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(),
BsonSerializer.SerializerRegistry)
.Document.ToString();
Assert.Equal(Cleanup(expected), rendered);
}
private static string Cleanup(string filter)
{
return filter.Replace('\'', '"');
}
}

16
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs

@ -46,7 +46,7 @@ public class ConvertDataTests : GivenContext
{
var content = CreateContent();
await sut.EnrichAsync(FrontendContext, new[] { content }, SchemaProvider(), CancellationToken);
await sut.EnrichAsync(FrontendContext, [content], SchemaProvider(), CancellationToken);
Assert.NotNull(content.Data);
}
@ -82,7 +82,7 @@ public class ConvertDataTests : GivenContext
JsonValue.Object()
.Add("nested", JsonValue.Array("default3")))));
await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken);
await sut.EnrichAsync(ApiContext, [content], SchemaProvider(), CancellationToken);
Assert.Equal(expected, content.Data);
}
@ -116,12 +116,12 @@ public class ConvertDataTests : GivenContext
.Add("nested", JsonValue.Array(id2)))));
A.CallTo(() => assetRepository.QueryIdsAsync(AppId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), CancellationToken))
.Returns(new List<DomainId> { id2 });
.Returns([id2]);
A.CallTo(() => contentRepository.QueryIdsAsync(App, A<HashSet<DomainId>>.That.Is(id1, id2), SearchScope.All, CancellationToken))
.Returns(new List<ContentIdStatus> { new ContentIdStatus(id2, id2, Status.Published) });
.Returns([new ContentIdStatus(id2, id2, Status.Published)]);
await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken);
await sut.EnrichAsync(ApiContext, [content], SchemaProvider(), CancellationToken);
Assert.Equal(expected, content.Data);
}
@ -155,12 +155,12 @@ public class ConvertDataTests : GivenContext
.Add("nested", JsonValue.Array()))));
A.CallTo(() => assetRepository.QueryIdsAsync(AppId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), CancellationToken))
.Returns(new List<DomainId>());
.Returns([]);
A.CallTo(() => contentRepository.QueryIdsAsync(App, A<HashSet<DomainId>>.That.Is(id1, id2), SearchScope.All, CancellationToken))
.Returns(new List<ContentIdStatus>());
.Returns([]);
await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken);
await sut.EnrichAsync(ApiContext, [content], SchemaProvider(), CancellationToken);
Assert.Equal(expected, content.Data);
}

62
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs

@ -0,0 +1,62 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Schemas;
public class SchemasChatToolTests : GivenContext
{
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly SchemasChatTool sut;
public SchemasChatToolTests()
{
sut = new SchemasChatTool(AppProvider, TestUtils.DefaultSerializer, urlGenerator);
}
[Fact]
public async Task Should_return_schemas_if_user_has_permission()
{
var chatContext = new AppChatContext
{
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppSchemasRead, App.Name).Id)
};
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync();
Assert.NotNull(tool);
Assert.Equal("schemas", tool.Spec.Name);
Assert.Equal("Schemas", tool.Spec.DisplayName);
var result = await tool.ExecuteAsync(null!, default);
Assert.Contains(Schema.Name, result);
A.CallTo(() => urlGenerator.SchemasUI(AppId))
.MustHaveHappened();
A.CallTo(() => urlGenerator.SchemaUI(AppId, SchemaId))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_return_tools_if_user_no_permission()
{
var chatContext = new AppChatContext
{
BaseContext = FrontendContext
};
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync();
Assert.Null(tool);
}
}

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -27,7 +27,7 @@
<PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageReference Include="Lorem.Universal.Net" Version="4.0.80" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -39,7 +39,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Reactive.Linq" Version="6.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="Verify.Xunit" Version="24.2.0" />
<PackageReference Include="Verify.Xunit" Version="25.0.1" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs

@ -160,6 +160,9 @@ public abstract class GivenContext
A.CallTo(() => result.GetSchemaAsync(AppId.Id, SchemaId.Name, A<bool>._, A<CancellationToken>._))
.ReturnsLazily(() => Schema);
A.CallTo(() => result.GetSchemasAsync(AppId.Id, A<CancellationToken>._))
.ReturnsLazily(() => [Schema]);
A.CallTo(() => result.GetAppWithSchemaAsync(AppId.Id, SchemaId.Id, A<bool>._, A<CancellationToken>._))
.ReturnsLazily(() => (App, Schema));

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt

@ -90,7 +90,7 @@
en,
de
],
Languages: {
Values: {
de: {
IsOptional: false
},

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt

@ -100,7 +100,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt

@ -90,7 +90,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt

@ -90,7 +90,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt

@ -96,7 +96,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt

@ -93,7 +93,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt

@ -93,7 +93,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt

@ -93,7 +93,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt

@ -86,7 +86,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteApp_should_create_events_and_update_deleted_flag.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt

@ -90,7 +90,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt

@ -96,7 +96,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt

@ -90,7 +90,7 @@
en,
de
],
Languages: {
Values: {
de: {
IsOptional: false,
Fallbacks: [

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt

@ -110,7 +110,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt

@ -89,7 +89,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt

@ -91,7 +91,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt

@ -93,7 +93,7 @@
AllKeys: [
en
],
Languages: {
Values: {
en: {
IsOptional: false
}

2
backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

4
frontend/src/app/framework/angular/forms/editors/date-time-editor.component.scss

@ -36,12 +36,12 @@
.btn {
&-clear {
@include absolute(auto, 4px, 3px, auto);
z-index: 100;
z-index: 1;
}
&-time-mode {
@include absolute(1px, auto, -1px, auto);
z-index: 100;
z-index: 1;
}
&:focus {

9
frontend/src/app/shell/pages/internal/chat-menu.component.html

@ -0,0 +1,9 @@
@if ((appsState.selectedApp | async) && hasChatBot) {
<ul class="nav navbar-nav align-items-center">
<li class="nav-item nav-icon" (click)="chatDialog.show()">
<span class="nav-link">AI</span>
</li>
</ul>
}
<sqx-chat-dialog (contentSelect)="chatDialog.hide()" *sqxModal="chatDialog"></sqx-chat-dialog>

7
frontend/src/app/shell/pages/internal/chat-menu.component.scss

@ -0,0 +1,7 @@
@import 'mixins';
@import 'vars';
.nav-link {
font-weight: 450;
font-size: 1.3rem;
}

33
frontend/src/app/shell/pages/internal/chat-menu.component.ts

@ -0,0 +1,33 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { AppsState, ChatDialogComponent, DialogModel, ModalDirective, UIOptions } from '@app/shared';
@Component({
standalone: true,
selector: 'sqx-chat-menu',
styleUrls: ['./chat-menu.component.scss'],
templateUrl: './chat-menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
AsyncPipe,
ChatDialogComponent,
ModalDirective
],
})
export class ChatMenuComponent {
public readonly chatDialog = new DialogModel();
public readonly hasChatBot = inject(UIOptions).value.canUseChatBot;
constructor(
public readonly appsState: AppsState
) {
}
}

1
frontend/src/app/shell/pages/internal/internal-area.component.html

@ -8,6 +8,7 @@
</div>
<div class="navbar-nav align-items-center ms-auto">
<sqx-search-menu></sqx-search-menu>
<sqx-chat-menu></sqx-chat-menu>
<sqx-asset-uploader></sqx-asset-uploader>
<sqx-feedback-menu></sqx-feedback-menu>
<sqx-notifications-menu></sqx-notifications-menu>

2
frontend/src/app/shell/pages/internal/internal-area.component.ts

@ -16,6 +16,7 @@ import { LogoComponent } from './logo.component';
import { NotificationsMenuComponent } from './notifications-menu.component';
import { ProfileMenuComponent } from './profile-menu.component';
import { SearchMenuComponent } from './search-menu.component';
import { ChatMenuComponent } from './chat-menu.component';
@Component({
standalone: true,
@ -26,6 +27,7 @@ import { SearchMenuComponent } from './search-menu.component';
AppsMenuComponent,
AssetUploaderComponent,
AsyncPipe,
ChatMenuComponent,
FeedbackMenuComponent,
LogoComponent,
NotificationsMenuComponent,

57
tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

@ -413,6 +413,63 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
Assert.Null(updated.Data.String);
}
[Fact]
public async Task Should_unset_field_with_patch()
{
// STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData
{
String = "Hello",
// Not relevant for the test, but required in the schema.
Number = 100
}, ContentCreateOptions.AsPublish);
// STEP 2: Update content with script.
await _.Client.DynamicContents(_.SchemaName).PatchAsync(content.Id,
new DynamicData
{
[TestEntityData.StringField] = new JObject
{
["$unset"] = true
}
});
var updated = await _.Contents.GetAsync(content.Id);
Assert.Null(updated.Data.String);
}
[Fact]
public async Task Should_unset_field_value_with_patch()
{
// STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData
{
String = "Hello",
// Not relevant for the test, but required in the schema.
Number = 100
}, ContentCreateOptions.AsPublish);
// STEP 2: Update content with script.
await _.Client.DynamicContents(_.SchemaName).PatchAsync(content.Id,
new DynamicData
{
[TestEntityData.StringField] = new JObject
{
["iv"] = new JObject
{
["$unset"] = true
}
}
});
var updated = await _.Contents.GetAsync(content.Id);
Assert.Null(updated.Data.String);
}
[Theory]
[InlineData(ContentStrategies.Update.Normal)]
[InlineData(ContentStrategies.Update.Upsert)]

Loading…
Cancel
Save