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

2
backend/src/Migrations/Migrations.csproj

@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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) 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(); 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); Language.EN);
private readonly Dictionary<string, LanguageConfig> languages; private readonly Dictionary<string, LanguageConfig> values;
private readonly string master; private readonly string master;
public string Master public string Master
@ -30,23 +30,22 @@ public sealed class LanguagesConfig : IFieldPartitioning
public IEnumerable<string> AllKeys 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); Guard.NotNullOrEmpty(master);
Cleanup(languages, ref master); Cleanup(values, ref master);
this.languages = languages;
this.values = values;
this.master = master; this.master = master;
} }
@ -55,7 +54,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
{ {
Guard.NotNull(language); Guard.NotNull(language);
return Build(languages, language); return Build(values, language);
} }
[Pure] [Pure]
@ -63,7 +62,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
{ {
Guard.NotNull(language); Guard.NotNull(language);
var newLanguages = new Dictionary<string, LanguageConfig>(languages) var newLanguages = new Dictionary<string, LanguageConfig>(values)
{ {
[language] = new LanguageConfig(isOptional, ReadonlyList.Create(fallbacks)) [language] = new LanguageConfig(isOptional, ReadonlyList.Create(fallbacks))
}; };
@ -76,7 +75,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
{ {
Guard.NotNull(language); Guard.NotNull(language);
var newLanguages = new Dictionary<string, LanguageConfig>(languages); var newLanguages = new Dictionary<string, LanguageConfig>(values);
newLanguages.Remove(language); newLanguages.Remove(language);
@ -102,7 +101,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
private bool EqualLanguages(Dictionary<string, LanguageConfig> newLanguages) 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) private void Cleanup(Dictionary<string, LanguageConfig> newLanguages, ref string newMaster)
@ -152,7 +151,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
public string? GetName(string key) public string? GetName(string key)
{ {
if (key != null && languages.ContainsKey(key)) if (key != null && values.ContainsKey(key))
{ {
return Language.GetLanguage(key).EnglishName; return Language.GetLanguage(key).EnglishName;
} }
@ -162,7 +161,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
public bool IsOptional(string key) 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; return value.IsOptional;
} }
@ -178,7 +177,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
{ {
yield return key; yield return key;
} }
else if (languages.TryGetValue(key, out var config)) else if (values.TryGetValue(key, out var config))
{ {
yield return key; yield return key;
@ -197,7 +196,7 @@ public sealed class LanguagesConfig : IFieldPartitioning
public bool Contains(string key) public bool Contains(string key)
{ {
return key != null && languages.ContainsKey(key); return key != null && values.ContainsKey(key);
} }
public override string ToString() 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) public ContentData(int capacity)
: base(source, StringComparer.Ordinal) : base(capacity, StringComparer.Ordinal)
{ {
} }
public ContentData(int capacity) public ContentData(IDictionary<string, ContentFieldData?> source)
: base(capacity, StringComparer.Ordinal) : base(source, StringComparer.Ordinal)
{ {
} }
@ -96,16 +96,29 @@ public sealed class ContentData : Dictionary<string, ContentFieldData?>, IEquata
continue; continue;
} }
var targetFieldData = target.GetOrAdd(fieldName, _ => new ContentFieldData()); if (Updates.IsUnset(sourceFieldData))
{
target.Remove(fieldName);
continue;
}
var targetFieldData = target.GetOrAdd(fieldName, _ => []);
if (targetFieldData == null) if (targetFieldData == null)
{ {
continue; 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) public ContentFieldData(IDictionary<string, JsonValue> source)
: base(source.Count, StringComparer.OrdinalIgnoreCase) : base(source, StringComparer.Ordinal)
{ {
foreach (var (key, value) in source)
{
this[key] = value;
}
} }
public bool TryGetNonNull(string key, [MaybeNullWhen(false)] out JsonValue result) 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(schema);
Guard.NotNull(languages); 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(); 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> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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; namespace Squidex.Domain.Apps.Core.ConvertContent;
public sealed class UpdateValues : IContentValueConverter public sealed class UpdateValues : IContentValueConverter, IContentDataConverter
{ {
private readonly ContentData existingData; private readonly ContentData existingData;
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly bool canUnset;
private ScriptVars? vars; private ScriptVars? vars;
public UpdateValues(ContentData existingData, IScriptEngine scriptEngine) public UpdateValues(ContentData existingData, IScriptEngine scriptEngine, bool canUnset)
{ {
this.existingData = existingData; this.existingData = existingData;
this.scriptEngine = scriptEngine; 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) 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); return (false, source);
} }
if (jsonObject.TryGetValue("$unset", out var value1) && !Equals(value1.Value, false)) if (canUnset && Updates.IsUnset(obj))
{ {
return (true, source); 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); return (false, source);
} }
var options = new ScriptOptions { Readonly = true }; var options = new ScriptOptions { Readonly = true };
// Reuse the vars to save allocations.
vars ??= new ScriptVars vars ??= new ScriptVars
{ {
["$data"] = existingData, ["$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); 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="Fluid.Core" Version="2.10.0" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" /> <PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.1.2" /> <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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="NJsonSchema" Version="11.0.0" /> <PackageReference Include="NJsonSchema" Version="11.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="6.8.8" /> <PackageReference Include="Squidex.AI" Version="6.11.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.8.8" /> <PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.11.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <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 valueValidator = CreateValueValidator(field);
var partitioning = partitionResolver(field.Partitioning); 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) foreach (var partitionKey in partitioning.AllKeys)
{ {
var optional = partitioning.IsOptional(partitionKey); var optional = partitioning.IsOptional(partitionKey);
partitioningValidators[partitionKey] = (optional, valueValidator); partitions[partitionKey] = (optional, valueValidator);
} }
var typeName = partitioning.ToString()!; var typeName = partitioning.ToString()!;
@ -124,7 +124,7 @@ public sealed class ContentValidator
return new AggregateValidator( return new AggregateValidator(
CreateFieldValidators(field) CreateFieldValidators(field)
.Union(Enumerable.Repeat( .Union(Enumerable.Repeat(
new ObjectValidator<JsonValue>(partitioningValidators, isPartial, typeName), 1))); new ObjectValidator<JsonValue>(partitions, isPartial, typeName), 1)));
} }
private IValidator CreateValueValidator(IField field) 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; 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); 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 Value = new object();
public static readonly object Unset = new object();
public static bool IsUndefined(this object? other) public static bool IsUndefined(this object? other)
{ {
return ReferenceEquals(other, Value); return ReferenceEquals(other, Value);
} }
public static bool IsUnset(this object? other)
{
return ReferenceEquals(other, Unset);
}
public static bool IsNullOrUndefined(this object? other) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -34,7 +35,7 @@ public sealed class FieldValidator : IValidator
{ {
if (value is JsonValue jsonValue) if (value is JsonValue jsonValue)
{ {
if (jsonValue == default) if (jsonValue == default || Updates.IsUnset(jsonValue))
{ {
typedValue = null; typedValue = null;
} }
@ -42,7 +43,7 @@ public sealed class FieldValidator : IValidator
{ {
typedValue = jsonValue.Value; typedValue = jsonValue.Value;
var (json, error) = JsonValueConverter.ConvertValue(field, jsonValue, var (typed, error) = JsonValueConverter.ConvertValue(field, jsonValue,
context.Root.Serializer, context.Root.Serializer,
context.Root.Components); context.Root.Components);
@ -52,7 +53,7 @@ public sealed class FieldValidator : IValidator
} }
else 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators;
@ -25,6 +26,8 @@ public sealed class ObjectValidator<TValue> : IValidator
public void Validate(object? value, ValidationContext context) public void Validate(object? value, ValidationContext context)
{ {
var originalValue = value;
if (value.IsNullOrUndefined()) if (value.IsNullOrUndefined())
{ {
value = DefaultValue; value = DefaultValue;
@ -48,9 +51,10 @@ public sealed class ObjectValidator<TValue> : IValidator
if (!values.TryGetValue(name, out var nestedValue)) 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 else
@ -58,6 +62,12 @@ public sealed class ObjectValidator<TValue> : IValidator
fieldValue = nestedValue!; 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); var fieldContext = context.Nested(name, field.IsOptional);
field.Validator.Validate(fieldValue, fieldContext); 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("$expr", new BsonDocument()
.Add("$eq", new BsonArray { "$_id", "$$id" })); .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 BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps
.Where(x => .Where(x =>
x.MemberName != nameof(MongoContentEntity.NewData) && x.MemberName != nameof(MongoContentEntity.NewData) &&
@ -173,30 +173,44 @@ public static class Extensions
return find.Project<T>(BuildProjection2<T>(fields)); 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 projector = Builders<T>.Projection;
var projections = new List<ProjectionDefinition<T>>(); var projections = new List<ProjectionDefinition<T>>();
if (fields?.Any() == true) if (fields?.Any() == true)
{ {
var dataPrefix = Field.Of<MongoContentEntity>(x => nameof(x.Data)); static IEnumerable<string> GetDataFields(IEnumerable<string> fields)
foreach (var field in 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)); projections.Add(projector.Include(field));
addedFields.Add(field);
} }
} }
else else

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

@ -19,7 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" /> <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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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.MustHavePermission(PermissionIds.AppContentsUpdate);
operation.MustHaveData(c.Data); operation.MustHaveData(c.Data);
var newData = operation.InvokeUpdates(c.Data, Snapshot.EditingData); var newData = operation.InvokeUpdates(c.Data, Snapshot.EditingData, true);
if (!c.DoNotValidate) if (!c.DoNotValidate)
{ {
@ -363,7 +363,7 @@ public partial class ContentDomainObject : DomainObject<WriteContent>
operation.MustHavePermission(PermissionIds.AppContentsUpdate); operation.MustHavePermission(PermissionIds.AppContentsUpdate);
operation.MustHaveData(c.Data); operation.MustHaveData(c.Data);
c.Data = operation.InvokeUpdates(c.Data, Snapshot.EditingData); c.Data = operation.InvokeUpdates(c.Data, Snapshot.EditingData, false);
if (!c.DoNotValidate) 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); 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 = var converter =
new ContentConverter( new ContentConverter(
operation.Components, operation.Components,
operation.Schema); operation.Schema);
converter.Add(new UpdateValues(currentData, operation.Resolve<IScriptEngine>())); converter.Add(new UpdateValues(currentData, operation.Resolve<IScriptEngine>(), canUnset));
return converter.Convert(data); 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 ??= []; content.ReferenceData ??= [];
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => [])!;
foreach (var (partitionKey, partitionValue) in fieldData) 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;
using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
@ -72,7 +71,7 @@ public sealed class ResolveAssets : IContentEnricherStep
{ {
content.ReferenceData ??= []; 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) 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 ??= []; content.ReferenceData ??= [];
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => [])!;
try 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="CsvHelper" Version="32.0.3" />
<PackageReference Include="GraphQL" Version="7.8.0" /> <PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.DataLoader" 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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

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

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

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

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

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

@ -18,11 +18,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="IdentityModel" Version="7.0.0" /> <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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="5.6.0" /> <PackageReference Include="OpenIddict.AspNetCore" Version="5.6.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <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> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="23.3.1" /> <PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="23.3.2" />
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="23.3.1" /> <PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="23.3.2" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.3.1" /> <PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.3.2" />
<PackageReference Include="Grpc.Net.Client" Version="2.63.0" /> <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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

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

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

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

@ -13,23 +13,23 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MailKit" Version="4.6.0" /> <PackageReference Include="MailKit" Version="4.6.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <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.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="NodaTime" Version="3.1.11" />
<PackageReference Include="OpenTelemetry.Api" Version="1.8.1" /> <PackageReference Include="OpenTelemetry.Api" Version="1.8.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="6.8.8" /> <PackageReference Include="Squidex.Assets" Version="6.11.0" />
<PackageReference Include="Squidex.Caching" Version="6.8.8" /> <PackageReference Include="Squidex.Caching" Version="6.11.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.8.8" /> <PackageReference Include="Squidex.Hosting.Abstractions" Version="6.11.0" />
<PackageReference Include="Squidex.Log" Version="6.8.8" /> <PackageReference Include="Squidex.Log" Version="6.11.0" />
<PackageReference Include="Squidex.Messaging" Version="6.8.8" /> <PackageReference Include="Squidex.Messaging" Version="6.11.0" />
<PackageReference Include="Squidex.Text" Version="6.8.8" /> <PackageReference Include="Squidex.Text" Version="6.11.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.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> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.153"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.155">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

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

@ -16,7 +16,7 @@ namespace Squidex.Web;
public sealed class Resources 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 // Contents
public bool CanReadContent(string schema) => Can(PermissionIds.AppContentsReadOwn, schema); 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" Version="7.8.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" /> <PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.7.1" /> <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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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)); 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", AddDeleteLink("delete",
resources.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values)); 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 var result = new AppLanguagesDto
{ {
Items = config.Languages Items = config.Values
.Select(x => AppLanguageDto.FromDomain(x.Key, x.Value, config)) .Select(x => AppLanguageDto.FromDomain(x.Key, x.Value, config))
.Select(x => x.CreateLinks(resources, app)) .Select(x => x.CreateLinks(resources, app))
.OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code) .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.AI;
using Squidex.Areas.Api.Controllers.Translations.Models; using Squidex.Areas.Api.Controllers.Translations.Models;
using Squidex.Assets; using Squidex.Assets;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Text.Translations; using Squidex.Text.Translations;
@ -92,9 +93,11 @@ public sealed class TranslationsController : ApiController
Prompt = request.Prompt 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) => 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 Microsoft.Extensions.Options;
using Squidex.AI;
using Squidex.Areas.Api.Controllers.UI; using Squidex.Areas.Api.Controllers.UI;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
@ -51,6 +52,9 @@ public static class AppsServices
services.AddSingletonAs<AppUISettings>() services.AddSingletonAs<AppUISettings>()
.As<IAppUISettings>().As<IDeleter>(); .As<IAppUISettings>().As<IDeleter>();
services.AddSingletonAs<AppChatTools>()
.As<IChatToolProvider>();
services.AddSingletonAs<AppSettingsSearchSource>() services.AddSingletonAs<AppSettingsSearchSource>()
.As<ISearchSource>(); .As<ISearchSource>();

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

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

40
backend/src/Squidex/Squidex.csproj

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

16
backend/src/Squidex/appsettings.json

@ -690,10 +690,9 @@
"defaults": { "defaults": {
"systemMessages": [ "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." "Say hello to the user and explain him about your capabilities in a single, short sentence."
], ]
"tools": []
}, },
"configurations": { "configurations": {
@ -701,7 +700,16 @@
"systemMessages": [ "systemMessages": [
"You are a bot to generate images.", "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." "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] [Fact]
public void Should_create_initial_config() public void Should_create_initial_config()
{ {
config_0.Languages.Should().BeEquivalentTo( config_0.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig> new Dictionary<string, LanguageConfig>
{ {
[Language.EN] = new LanguageConfig() [Language.EN] = new LanguageConfig()
@ -106,7 +106,7 @@ public class LanguagesConfigTests
.Set(Language.IT, true, Language.ES) .Set(Language.IT, true, Language.ES)
.MakeMaster(Language.DE); .MakeMaster(Language.DE);
config.Languages.Should().BeEquivalentTo( config.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig> new Dictionary<string, LanguageConfig>
{ {
[Language.EN] = new LanguageConfig(), [Language.EN] = new LanguageConfig(),
@ -141,7 +141,7 @@ public class LanguagesConfigTests
.Set(Language.IT, true, Language.ES) .Set(Language.IT, true, Language.ES)
.MakeMaster(Language.IT); .MakeMaster(Language.IT);
config.Languages.Should().BeEquivalentTo( config.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig> new Dictionary<string, LanguageConfig>
{ {
[Language.EN] = new LanguageConfig(), [Language.EN] = new LanguageConfig(),
@ -182,7 +182,7 @@ public class LanguagesConfigTests
var config_2 = config_1.Set(Language.IT); var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Remove(Language.DE); var config_3 = config_2.Remove(Language.DE);
config_3.Languages.Should().BeEquivalentTo( config_3.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig> new Dictionary<string, LanguageConfig>
{ {
[Language.EN] = new 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_2 = config_1.Set(Language.IT, true, Language.UK);
var config_3 = config_2.Remove(Language.DE); var config_3 = config_2.Remove(Language.DE);
config_3.Languages.Should().BeEquivalentTo( config_3.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig> new Dictionary<string, LanguageConfig>
{ {
[Language.EN] = new LanguageConfig(), [Language.EN] = new LanguageConfig(),
@ -224,7 +224,7 @@ public class LanguagesConfigTests
var config_2 = config_1.Set(Language.IT); var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Remove(Language.EN); var config_3 = config_2.Remove(Language.EN);
config_3.Languages.Should().BeEquivalentTo( config_3.Values.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig> new Dictionary<string, LanguageConfig>
{ {
[Language.DE] = new LanguageConfig(), [Language.DE] = new LanguageConfig(),
@ -248,7 +248,7 @@ public class LanguagesConfigTests
var config_1 = config_0.Set(Language.IT); var config_1 = config_0.Set(Language.IT);
var config_2 = config_1.Set(Language.IT, true, Language.EN); 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> new Dictionary<string, LanguageConfig>
{ {
[Language.EN] = new LanguageConfig(), [Language.EN] = new LanguageConfig(),
@ -265,7 +265,7 @@ public class LanguagesConfigTests
var config_2 = config_1.Set(Language.IT); var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Set(Language.IT, true, Language.EN, Language.IT, Language.DE); 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> new Dictionary<string, LanguageConfig>
{ {
[Language.EN] = new 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.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Model.Contents; namespace Squidex.Domain.Apps.Core.Model.Contents;
public class ContentDataTests public class ContentDataTests
{ {
[Fact] [Fact]
public void Should_return_same_content_if_merging_same_references() public void Should_return_same_data_if_merging_same_references()
{ {
var source = var source =
new ContentData() new ContentData()
@ -29,7 +30,7 @@ public class ContentDataTests
} }
[Fact] [Fact]
public void Should_merge_two_name_models() public void Should_merge_data()
{ {
var lhs = var lhs =
new ContentData() new ContentData()
@ -73,9 +74,86 @@ public class ContentDataTests
} }
[Fact] [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 = 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() new ContentData()
.AddField("field1", .AddField("field1",
new ContentFieldData() new ContentFieldData()
@ -84,7 +162,7 @@ public class ContentDataTests
new ContentFieldData() new ContentFieldData()
.AddInvariant(2)); .AddInvariant(2));
var rhs = var lhs =
new ContentData() new ContentData()
.AddField("field1", .AddField("field1",
new ContentFieldData() new ContentFieldData()

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

@ -74,7 +74,7 @@ public class DefaultValuesTests
.AddInvariant(now.ToString())) .AddInvariant(now.ToString()))
.AddField("myBoolean", .AddField("myBoolean",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.True)) .AddInvariant(true))
.AddField("myArray", .AddField("myArray",
new ContentFieldData() new ContentFieldData()
.AddInvariant( .AddInvariant(
@ -154,7 +154,7 @@ public class DefaultValuesTests
.AddInvariant(now.ToString())) .AddInvariant(now.ToString()))
.AddField("myBoolean", .AddField("myBoolean",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.True)) .AddInvariant(true))
.AddField("myArray", .AddField("myArray",
new ContentFieldData() new ContentFieldData()
.AddInvariant( .AddInvariant(
@ -196,7 +196,7 @@ public class DefaultValuesTests
.AddInvariant(now.ToString())) .AddInvariant(now.ToString()))
.AddField("myBoolean", .AddField("myBoolean",
new ContentFieldData() new ContentFieldData()
.AddInvariant(JsonValue.True)) .AddInvariant(true))
.AddField("myArray", .AddField("myArray",
new ContentFieldData() new ContentFieldData()
.AddInvariant( .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; namespace Squidex.Domain.Apps.Core.Operations.ConvertContent;
public class UpdateConverterTests public class UpdateValuesConverterTests
{ {
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
public UpdateConverterTests() public UpdateValuesConverterTests()
{ {
scriptEngine = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), scriptEngine = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())),
Options.Create(new JintScriptOptions Options.Create(new JintScriptOptions
@ -30,7 +30,7 @@ public class UpdateConverterTests
} }
[Fact] [Fact]
public void Should_update() public void Should_update_value()
{ {
var field1 = Fields.Number(1, "number1", Partitioning.Invariant); var field1 = Fields.Number(1, "number1", Partitioning.Invariant);
@ -54,7 +54,42 @@ public class UpdateConverterTests
var actual = var actual =
new ContentConverter(ResolvedComponents.Empty, schema) 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); .Convert(source);
var expected = var expected =
@ -92,7 +127,7 @@ public class UpdateConverterTests
var actual = var actual =
new ContentConverter(ResolvedComponents.Empty, schema) new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine)) .Add(new UpdateValues(existing, scriptEngine, true))
.Convert(source); .Convert(source);
var expected = var expected =
@ -129,13 +164,43 @@ public class UpdateConverterTests
var actual = var actual =
new ContentConverter(ResolvedComponents.Empty, schema) new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine)) .Add(new UpdateValues(existing, scriptEngine, true))
.Convert(source); .Convert(source);
var expected = 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() new ContentData()
.AddField(field1.Name, .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); 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<ChatRequest>.That.Matches(x => x.Prompt == "prompt"),
A<ChatContext>._, A<ChatContext>._,
A<CancellationToken>._)) A<CancellationToken>._))
.Returns(new ChatResult { Content = "Generated", Metadata = new ChatMetadata() }); .Returns(new ChatResult { Content = "Generated", Metadata = new ChatMetadata(), Tools = [] });
var vars = new ScriptVars var vars = new ScriptVars
{ {
@ -639,7 +639,7 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
Assert.Equal("Generated", actual.ToString()); Assert.Equal("Generated", actual.ToString());
A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<CancellationToken>._)) A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<ChatContext>._, A<CancellationToken>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -666,7 +666,7 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
A.CallTo(() => chatAgent.PromptAsync(A<ChatRequest>._, A<ChatContext>._, A<CancellationToken>._)) A.CallTo(() => chatAgent.PromptAsync(A<ChatRequest>._, A<ChatContext>._, A<CancellationToken>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<CancellationToken>._)) A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<ChatContext>._, A<CancellationToken>._))
.MustNotHaveHappened(); .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); 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> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" /> <PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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(); var content = CreateContent();
await sut.EnrichAsync(FrontendContext, new[] { content }, SchemaProvider(), CancellationToken); await sut.EnrichAsync(FrontendContext, [content], SchemaProvider(), CancellationToken);
Assert.NotNull(content.Data); Assert.NotNull(content.Data);
} }
@ -82,7 +82,7 @@ public class ConvertDataTests : GivenContext
JsonValue.Object() JsonValue.Object()
.Add("nested", JsonValue.Array("default3"))))); .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); Assert.Equal(expected, content.Data);
} }
@ -116,12 +116,12 @@ public class ConvertDataTests : GivenContext
.Add("nested", JsonValue.Array(id2))))); .Add("nested", JsonValue.Array(id2)))));
A.CallTo(() => assetRepository.QueryIdsAsync(AppId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), CancellationToken)) 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)) 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); Assert.Equal(expected, content.Data);
} }
@ -155,12 +155,12 @@ public class ConvertDataTests : GivenContext
.Add("nested", JsonValue.Array())))); .Add("nested", JsonValue.Array()))));
A.CallTo(() => assetRepository.QueryIdsAsync(AppId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), CancellationToken)) 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)) 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); 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" Version="7.8.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" /> <PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageReference Include="Lorem.Universal.Net" Version="4.0.80" /> <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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -39,7 +39,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Reactive.Linq" Version="6.0.1" /> <PackageReference Include="System.Reactive.Linq" Version="6.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <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" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1"> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets> <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>._)) A.CallTo(() => result.GetSchemaAsync(AppId.Id, SchemaId.Name, A<bool>._, A<CancellationToken>._))
.ReturnsLazily(() => Schema); .ReturnsLazily(() => Schema);
A.CallTo(() => result.GetSchemasAsync(AppId.Id, A<CancellationToken>._))
.ReturnsLazily(() => [Schema]);
A.CallTo(() => result.GetAppWithSchemaAsync(AppId.Id, SchemaId.Id, A<bool>._, A<CancellationToken>._)) A.CallTo(() => result.GetAppWithSchemaAsync(AppId.Id, SchemaId.Id, A<bool>._, A<CancellationToken>._))
.ReturnsLazily(() => (App, Schema)); .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, en,
de de
], ],
Languages: { Values: {
de: { de: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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, en,
de de
], ],
Languages: { Values: {
de: { de: {
IsOptional: false, IsOptional: false,
Fallbacks: [ 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false 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: [ AllKeys: [
en en
], ],
Languages: { Values: {
en: { en: {
IsOptional: false IsOptional: false
} }

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

@ -16,7 +16,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" /> <PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

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

@ -16,7 +16,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" /> <PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

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

@ -16,7 +16,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" /> <PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

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

@ -36,12 +36,12 @@
.btn { .btn {
&-clear { &-clear {
@include absolute(auto, 4px, 3px, auto); @include absolute(auto, 4px, 3px, auto);
z-index: 100; z-index: 1;
} }
&-time-mode { &-time-mode {
@include absolute(1px, auto, -1px, auto); @include absolute(1px, auto, -1px, auto);
z-index: 100; z-index: 1;
} }
&:focus { &: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>
<div class="navbar-nav align-items-center ms-auto"> <div class="navbar-nav align-items-center ms-auto">
<sqx-search-menu></sqx-search-menu> <sqx-search-menu></sqx-search-menu>
<sqx-chat-menu></sqx-chat-menu>
<sqx-asset-uploader></sqx-asset-uploader> <sqx-asset-uploader></sqx-asset-uploader>
<sqx-feedback-menu></sqx-feedback-menu> <sqx-feedback-menu></sqx-feedback-menu>
<sqx-notifications-menu></sqx-notifications-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 { NotificationsMenuComponent } from './notifications-menu.component';
import { ProfileMenuComponent } from './profile-menu.component'; import { ProfileMenuComponent } from './profile-menu.component';
import { SearchMenuComponent } from './search-menu.component'; import { SearchMenuComponent } from './search-menu.component';
import { ChatMenuComponent } from './chat-menu.component';
@Component({ @Component({
standalone: true, standalone: true,
@ -26,6 +27,7 @@ import { SearchMenuComponent } from './search-menu.component';
AppsMenuComponent, AppsMenuComponent,
AssetUploaderComponent, AssetUploaderComponent,
AsyncPipe, AsyncPipe,
ChatMenuComponent,
FeedbackMenuComponent, FeedbackMenuComponent,
LogoComponent, LogoComponent,
NotificationsMenuComponent, NotificationsMenuComponent,

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

@ -413,6 +413,63 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
Assert.Null(updated.Data.String); 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] [Theory]
[InlineData(ContentStrategies.Update.Normal)] [InlineData(ContentStrategies.Update.Normal)]
[InlineData(ContentStrategies.Update.Upsert)] [InlineData(ContentStrategies.Update.Upsert)]

Loading…
Cancel
Save